]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Carbonised Notification Header 64375/head
authorAnikait Sehwag <anikaitsehwag.amg@gmail.com>
Mon, 7 Jul 2025 18:13:04 +0000 (23:43 +0530)
committerAnikait Sehwag <128905481+SundownRises@users.noreply.github.com>
Tue, 2 Sep 2025 22:19:13 +0000 (03:49 +0530)
Fixes: https://tracker.ceph.com/issues/71732
Signed-off-by: Anikait Sehwag <anikaitsehwag.amg@gmail.com>
16 files changed:
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation.module.ts
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.ts
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/header/notification-header.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/header/notification-header.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/header/notification-header.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/header/notification-header.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-panel.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-panel.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-panel.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-panel.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notifications/notifications.component.html
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notifications/notifications.component.scss
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notifications/notifications.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/notifications-sidebar/notifications-sidebar.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/notifications-sidebar/notifications-sidebar.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/notification.service.ts

index 0b6eaa8d531b025468e56c6034c0182a31a0e656..a1d1cc419219a0be70c050050a873d0d620b8524 100644 (file)
@@ -1,5 +1,5 @@
 import { CommonModule } from '@angular/common';
-import { NgModule } from '@angular/core';
+import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
 import { RouterModule } from '@angular/router';
 
 import { NgbCollapseModule, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
@@ -12,7 +12,9 @@ import {
   DialogModule,
   GridModule,
   BreadcrumbModule,
-  ModalModule
+  ModalModule,
+  ToggleModule,
+  ButtonModule
 } from 'carbon-components-angular';
 
 import { AppRoutingModule } from '~/app/app-routing.module';
@@ -26,6 +28,8 @@ import { DashboardHelpComponent } from './dashboard-help/dashboard-help.componen
 import { IdentityComponent } from './identity/identity.component';
 import { NavigationComponent } from './navigation/navigation.component';
 import { NotificationsComponent } from './notifications/notifications.component';
+import { NotificationPanelComponent } from './notification-panel/notification-panel.component';
+import { NotificationHeaderComponent } from './notification-panel/header/notification-header.component';
 
 // Icons
 import UserFilledIcon from '@carbon/icons/es/user--filled/20';
@@ -61,7 +65,9 @@ import { ModalCdsService } from '~/app/shared/services/modal-cds.service';
     DialogModule,
     GridModule,
     BreadcrumbModule,
-    ModalModule
+    ModalModule,
+    ToggleModule,
+    ButtonModule
   ],
   declarations: [
     AboutComponent,
@@ -69,12 +75,15 @@ import { ModalCdsService } from '~/app/shared/services/modal-cds.service';
     BreadcrumbsComponent,
     NavigationComponent,
     NotificationsComponent,
+    NotificationPanelComponent,
+    NotificationHeaderComponent,
     DashboardHelpComponent,
     AdministrationComponent,
     IdentityComponent
   ],
-  providers: [ModalCdsService],
-  exports: [NavigationComponent, BreadcrumbsComponent]
+  exports: [NavigationComponent, BreadcrumbsComponent],
+  schemas: [CUSTOM_ELEMENTS_SCHEMA],
+  providers: [ModalCdsService]
 })
 export class NavigationModule {
   constructor(private iconService: IconService) {
index cb637e51f784c4c1be5be79c661611020fc85517..b276da84ff6af3a90c6bf73f14a3c191d97261c2 100644 (file)
@@ -209,7 +209,7 @@ export class NavigationComponent implements OnInit, OnDestroy {
     );
   }
   toggleSidebar() {
-    this.notificationService.toggleSidebar();
+    this.notificationService.toggleSidebar(true, true);
   }
   trackByFn(item: any) {
     return item;
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/header/notification-header.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/header/notification-header.component.html
new file mode 100644 (file)
index 0000000..0038d3c
--- /dev/null
@@ -0,0 +1,27 @@
+<div class="notification-header">
+  <div class="notification-header__top">
+    <div class="notification-header__title">
+      <cds-text i18n>Tasks and Notifications</cds-text>
+    </div>
+
+    <button
+      i18n
+      cdsButton="ghost"
+      size="sm"
+      (click)="onDismissAll()"
+      class="notification-header__dismiss-btn">
+      Dismiss all
+    </button>
+  </div>
+
+  <div class="notification-header__toggle">
+    <cds-toggle
+      [checked]="isMuted"
+      size="sm"
+      (checkedChange)="onToggleMute()"
+      label="Mute notifications"
+      i18n-label
+      hideLabel="true"> <!--hides the toggle state values (like "On/Off" in the toggle button)-->
+    </cds-toggle>
+  </div>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/header/notification-header.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/header/notification-header.component.scss
new file mode 100644 (file)
index 0000000..3540b68
--- /dev/null
@@ -0,0 +1,56 @@
+@use '@carbon/styles/scss/type';
+@use '@carbon/styles/scss/spacing';
+@use '@carbon/styles/scss/theme';
+
+.notification-header {
+  display: flex;
+  flex-direction: column;
+  padding: spacing.$spacing-04;
+  border-bottom: 1px solid theme.$border-subtle-01;
+  background-color: theme.$layer-01;
+
+  &__top {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    width: 100%;
+    margin-bottom: spacing.$spacing-03;
+  }
+
+  &__title {
+    h4 {
+      @include type.type-style('heading-compact-01');
+
+      color: theme.$text-primary;
+      margin: 0;
+    }
+  }
+
+  &__dismiss-btn {
+    color: theme.$text-primary;
+
+    &:hover {
+      color: theme.$link-primary;
+    }
+  }
+
+  &__toggle {
+    cds-toggle {
+      margin: 0;
+
+      ::ng-deep {
+        .cds--toggle__label-text {
+          color: theme.$text-primary;
+        }
+
+        .cds--toggle__label {
+          color: theme.$text-primary;
+        }
+
+        .cds--toggle__text {
+          color: theme.$text-primary;
+        }
+      }
+    }
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/header/notification-header.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/header/notification-header.component.spec.ts
new file mode 100644 (file)
index 0000000..444a6d1
--- /dev/null
@@ -0,0 +1,72 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { NotificationHeaderComponent } from './notification-header.component';
+import { NotificationService } from '../../../../shared/services/notification.service';
+import { BehaviorSubject } from 'rxjs';
+
+describe('NotificationHeaderComponent', () => {
+  let component: NotificationHeaderComponent;
+  let fixture: ComponentFixture<NotificationHeaderComponent>;
+  let notificationService: NotificationService;
+  let muteStateSubject: BehaviorSubject<boolean>;
+
+  beforeEach(async () => {
+    muteStateSubject = new BehaviorSubject<boolean>(false);
+    await TestBed.configureTestingModule({
+      declarations: [NotificationHeaderComponent],
+      providers: [
+        {
+          provide: NotificationService,
+          useValue: {
+            muteState$: muteStateSubject.asObservable(),
+            removeAll: jasmine.createSpy('removeAll'),
+            suspendToasties: jasmine.createSpy('suspendToasties')
+          }
+        }
+      ]
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(NotificationHeaderComponent);
+    component = fixture.componentInstance;
+    notificationService = TestBed.inject(NotificationService);
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+
+  it('should initialize with default mute state', () => {
+    expect(component.isMuted).toBe(false);
+  });
+
+  it('should update mute state when subscription emits', () => {
+    muteStateSubject.next(true);
+    fixture.detectChanges();
+    expect(component.isMuted).toBe(true);
+  });
+
+  it('should emit dismissAll event and call removeAll on dismiss', () => {
+    spyOn(component.dismissAll, 'emit');
+
+    component.onDismissAll();
+
+    expect(component.dismissAll.emit).toHaveBeenCalled();
+    expect(notificationService.removeAll).toHaveBeenCalled();
+  });
+
+  it('should toggle mute state', () => {
+    component.isMuted = false;
+    component.onToggleMute();
+    expect(notificationService.suspendToasties).toHaveBeenCalledWith(true);
+
+    component.isMuted = true;
+    component.onToggleMute();
+    expect(notificationService.suspendToasties).toHaveBeenCalledWith(false);
+  });
+
+  it('should unsubscribe on destroy', () => {
+    spyOn(component['subs'], 'unsubscribe');
+    component.ngOnDestroy();
+    expect(component['subs'].unsubscribe).toHaveBeenCalled();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/header/notification-header.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/header/notification-header.component.ts
new file mode 100644 (file)
index 0000000..df7698f
--- /dev/null
@@ -0,0 +1,38 @@
+import { Component, Output, EventEmitter, OnInit, OnDestroy } from '@angular/core';
+import { NotificationService } from '../../../../shared/services/notification.service';
+import { Subscription } from 'rxjs';
+
+@Component({
+  selector: 'cd-notification-header',
+  templateUrl: './notification-header.component.html',
+  styleUrls: ['./notification-header.component.scss']
+})
+export class NotificationHeaderComponent implements OnInit, OnDestroy {
+  @Output() dismissAll = new EventEmitter<void>();
+
+  isMuted = false;
+  private subs = new Subscription();
+
+  constructor(private notificationService: NotificationService) {}
+
+  ngOnInit(): void {
+    this.subs.add(
+      this.notificationService.muteState$.subscribe((isMuted) => {
+        this.isMuted = isMuted;
+      })
+    );
+  }
+
+  ngOnDestroy(): void {
+    this.subs.unsubscribe();
+  }
+
+  onDismissAll(): void {
+    this.dismissAll.emit();
+    this.notificationService.removeAll();
+  }
+
+  onToggleMute(): void {
+    this.notificationService.suspendToasties(!this.isMuted);
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-panel.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-panel.component.html
new file mode 100644 (file)
index 0000000..65577e9
--- /dev/null
@@ -0,0 +1,4 @@
+<div class="notification-panel">
+  <cd-notification-header></cd-notification-header>
+  <!-- Body and footer components will be added here later -->
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-panel.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-panel.component.scss
new file mode 100644 (file)
index 0000000..b90127b
--- /dev/null
@@ -0,0 +1,18 @@
+@use '@carbon/styles/scss/theme';
+@use '@carbon/styles/scss/spacing';
+@use '@carbon/styles/scss/themes';
+@use '@carbon/styles/scss/theme' as *;
+
+.notification-panel {
+  @include theme.theme(themes.$g10);
+
+  position: absolute;
+  top: spacing.$spacing-09;
+  right: 0;
+  width: 400px; // Keep original width as it doesn't map to a spacing token
+  background-color: $layer-01;
+  box-shadow: $shadow;
+  border: 1px solid $border-subtle-01;
+  z-index: 6000;
+  color: $text-primary;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-panel.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-panel.component.spec.ts
new file mode 100644 (file)
index 0000000..e2df0fa
--- /dev/null
@@ -0,0 +1,60 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { NotificationPanelComponent } from './notification-panel.component';
+import { NotificationService } from '../../../shared/services/notification.service';
+
+describe('NotificationPanelComponent', () => {
+  let component: NotificationPanelComponent;
+  let fixture: ComponentFixture<NotificationPanelComponent>;
+  let notificationService: NotificationService;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [NotificationPanelComponent],
+      providers: [
+        {
+          provide: NotificationService,
+          useValue: {
+            toggleSidebar: jasmine.createSpy('toggleSidebar')
+          }
+        }
+      ]
+    }).compileComponents();
+  });
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(NotificationPanelComponent);
+    component = fixture.componentInstance;
+    notificationService = TestBed.inject(NotificationService);
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+
+  describe('handleClickOutside', () => {
+    it('should close sidebar when clicking outside', () => {
+      // Create a click event outside the component
+      const outsideClickEvent = new MouseEvent('click', {
+        bubbles: true,
+        cancelable: true
+      });
+      document.dispatchEvent(outsideClickEvent);
+
+      expect(notificationService.toggleSidebar).toHaveBeenCalledWith(false, true);
+    });
+
+    it('should not close sidebar when clicking inside', () => {
+      // Create a click event inside the component
+      const insideClickEvent = new MouseEvent('click', {
+        bubbles: true,
+        cancelable: true
+      });
+
+      const componentElement = fixture.nativeElement;
+      componentElement.dispatchEvent(insideClickEvent);
+
+      expect(notificationService.toggleSidebar).not.toHaveBeenCalled();
+    });
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-panel.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-panel.component.ts
new file mode 100644 (file)
index 0000000..b5cdbad
--- /dev/null
@@ -0,0 +1,19 @@
+import { Component, ElementRef, HostListener } from '@angular/core';
+import { NotificationService } from '../../../shared/services/notification.service';
+
+@Component({
+  selector: 'cd-notification-panel',
+  templateUrl: './notification-panel.component.html',
+  styleUrls: ['./notification-panel.component.scss']
+})
+export class NotificationPanelComponent {
+  constructor(public notificationService: NotificationService, private elementRef: ElementRef) {}
+
+  @HostListener('document:click', ['$event'])
+  handleClickOutside(event: Event) {
+    const clickedInside = this.elementRef.nativeElement.contains(event.target);
+    if (!clickedInside) {
+      this.notificationService.toggleSidebar(false, true);
+    }
+  }
+}
index f106e6647e358443f1d37a71c8946815267e61b0..ed6aecdcec051d5647106c7ebc8f99931ab36a1f 100644 (file)
@@ -1,13 +1,19 @@
 <a i18n-title
    title="Tasks and Notifications"
    [ngClass]="{ 'running': hasRunningTasks }"
-   >
-  <svg cdsIcon="notification"
-       size="20"
-       title="notification"></svg>
-  <span class="dot"
-        *ngIf="hasNotifications">
-  </span>
+   (click)="togglePanel($event)">
+  <div class="notification-icon-wrapper">
+    <svg cdsIcon="notification"
+         size="20"
+         title="notification"></svg>
+    <span class="notification-count"
+          *ngIf="notificationCount > 0">
+      {{ notificationCount }}
+    </span>
+  </div>
   <span class="d-md-none"
         i18n>Tasks and Notifications</span>
 </a>
+
+<cd-notification-panel *ngIf="isPanelOpen && useNewPanel"></cd-notification-panel>
+<cd-notifications-sidebar *ngIf="isPanelOpen && !useNewPanel"></cd-notifications-sidebar>
index 8bee3d8ff4e40aac63a5e84d2f2ee525867af1e3..999ea22523b2225631a45c7aaccb537e56029432 100644 (file)
@@ -1,4 +1,7 @@
 @use './src/styles/vendor/variables' as vv;
+@use '@carbon/styles/scss/spacing';
+@use '@carbon/styles/scss/theme' as *;
+@use '@carbon/styles/scss/type';
 
 .running i {
   color: vv.$primary;
@@ -25,3 +28,28 @@ a {
     border-color: vv.$primary-500;
   }
 }
+
+.notification-icon-wrapper {
+  position: relative;
+  display: inline-flex;
+  padding: spacing.$spacing-04;
+  cursor: pointer;
+  border-radius: spacing.$spacing-01;
+  transition: background-color 0.2s ease;
+}
+
+.notification-count {
+  position: absolute;
+  top: spacing.$spacing-01;
+  right: spacing.$spacing-01;
+  min-width: spacing.$spacing-04;
+  height: spacing.$spacing-04;
+  padding: 0 spacing.$spacing-01;
+  border-radius: spacing.$spacing-02;
+  background-color: $support-error;
+  color: $text-on-color;
+  font-size: spacing.$spacing-04;
+  line-height: spacing.$spacing-04;
+  text-align: center;
+  font-weight: 500;
+}
index b6e5fd41ab42a4c0d8b1df8d1e1693a81cd985ea..ca3b58167c7ff23e4925ebcc1e257635835f2d83 100644 (file)
@@ -16,6 +16,9 @@ export class NotificationsComponent implements OnInit, OnDestroy {
   icons = Icons;
   hasRunningTasks = false;
   hasNotifications = false;
+  isPanelOpen = false;
+  useNewPanel = true;
+  notificationCount = 0;
   private subs = new Subscription();
 
   constructor(
@@ -33,8 +36,22 @@ export class NotificationsComponent implements OnInit, OnDestroy {
     this.subs.add(
       this.notificationService.data$.subscribe((notifications: CdNotification[]) => {
         this.hasNotifications = notifications.length > 0;
+        this.notificationCount = notifications.length;
       })
     );
+
+    this.subs.add(
+      this.notificationService.panelState$.subscribe((state) => {
+        this.isPanelOpen = state.isOpen;
+        this.useNewPanel = state.useNewPanel;
+      })
+    );
+  }
+
+  togglePanel(event: Event) {
+    event.preventDefault();
+    event.stopPropagation();
+    this.notificationService.toggleSidebar(!this.isPanelOpen, this.useNewPanel);
   }
 
   ngOnDestroy(): void {
index f3fe9cea3cff41040f772b761ea409f14c7a35e7..627fefb4fe02ad91345f849fb0283344ef6bd00b 100644 (file)
@@ -180,14 +180,14 @@ describe('NotificationsSidebarComponent', () => {
     it('should always close if sidebarSubject value is true', fakeAsync(() => {
       // Closed before next value
       expect(component.isSidebarOpened).toBeFalsy();
-      notificationService.sidebarSubject.next(true);
+      notificationService.toggleSidebar(true, true);
       tick();
       expect(component.isSidebarOpened).toBeFalsy();
 
       // Opened before next value
       component.isSidebarOpened = true;
       expect(component.isSidebarOpened).toBeTruthy();
-      notificationService.sidebarSubject.next(true);
+      notificationService.toggleSidebar(true, true);
       tick();
       expect(component.isSidebarOpened).toBeFalsy();
     }));
@@ -195,13 +195,13 @@ describe('NotificationsSidebarComponent', () => {
     it('should toggle sidebar visibility if sidebarSubject value is false', () => {
       // Closed before next value
       expect(component.isSidebarOpened).toBeFalsy();
-      notificationService.sidebarSubject.next(false);
+      notificationService.toggleSidebar(true, false);
       expect(component.isSidebarOpened).toBeTruthy();
 
       // Opened before next value
       component.isSidebarOpened = true;
       expect(component.isSidebarOpened).toBeTruthy();
-      notificationService.sidebarSubject.next(false);
+      notificationService.toggleSidebar(false, false);
       expect(component.isSidebarOpened).toBeFalsy();
     });
   });
index a662a898b163bca68dc41aca67b607e64e67e249..8393eb889498008b52cff3c9a211db28421f6d68 100644 (file)
@@ -102,17 +102,9 @@ export class NotificationsSidebarComponent implements OnInit, OnDestroy {
     );
 
     this.subs.add(
-      this.notificationService.sidebarSubject.subscribe((forceClose) => {
-        if (forceClose) {
-          this.isSidebarOpened = false;
-        } else {
-          this.isSidebarOpened = !this.isSidebarOpened;
-        }
-
-        window.clearTimeout(this.timeout);
-        this.timeout = window.setTimeout(() => {
-          this.cdRef.detectChanges();
-        }, 0);
+      this.notificationService.panelState$.subscribe((state) => {
+        this.isSidebarOpened = state.isOpen && !state.useNewPanel;
+        this.cdRef.detectChanges();
       })
     );
 
@@ -167,7 +159,7 @@ export class NotificationsSidebarComponent implements OnInit, OnDestroy {
   }
 
   closeSidebar() {
-    this.isSidebarOpened = false;
+    this.notificationService.toggleSidebar(false, false);
   }
 
   trackByFn(index: number) {
index 52a06e30598d4d226f978571081ee4417e8e804a..2c326a146666b0605b0b30fe65ed6b5b45ea6df7 100644 (file)
@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
 
 import _ from 'lodash';
 import { IndividualConfig, ToastrService } from 'ngx-toastr';
-import { BehaviorSubject, Subject } from 'rxjs';
+import { BehaviorSubject } from 'rxjs';
 
 import { NotificationType } from '../enum/notification-type.enum';
 import { CdNotification, CdNotificationConfig } from '../models/cd-notification';
@@ -18,14 +18,20 @@ export class NotificationService {
 
   // Data observable
   private dataSource = new BehaviorSubject<CdNotification[]>([]);
-  data$ = this.dataSource.asObservable();
+  private panelStateSource = new BehaviorSubject<{ isOpen: boolean; useNewPanel: boolean }>({
+    isOpen: false,
+    useNewPanel: true
+  });
+  private muteStateSource = new BehaviorSubject<boolean>(false);
 
-  // Sidebar observable
-  sidebarSubject = new Subject();
+  data$ = this.dataSource.asObservable();
+  panelState$ = this.panelStateSource.asObservable();
+  muteState$ = this.muteStateSource.asObservable();
 
   private queued: CdNotificationConfig[] = [];
   private queuedTimeoutId: number;
   KEY = 'cdNotifications';
+  MUTE_KEY = 'cdNotificationsMuted';
 
   constructor(
     public toastr: ToastrService,
@@ -45,6 +51,11 @@ export class NotificationService {
     }
 
     this.dataSource.next(notifications);
+
+    // Load mute state from localStorage
+    const isMuted = localStorage.getItem(this.MUTE_KEY) === 'true';
+    this.hideToasties = isMuted;
+    this.muteStateSource.next(isMuted);
   }
 
   /**
@@ -171,7 +182,14 @@ export class NotificationService {
     if (this.hideToasties) {
       return;
     }
-    this.toastr[['error', 'info', 'success'][notification.type]](
+    const toastrFn =
+      notification.type === NotificationType.error
+        ? this.toastr.error.bind(this.toastr)
+        : notification.type === NotificationType.info
+        ? this.toastr.info.bind(this.toastr)
+        : this.toastr.success.bind(this.toastr);
+
+    toastrFn(
       (notification.message ? notification.message + '<br>' : '') +
         this.renderTimeAndApplicationHtml(notification),
       notification.title,
@@ -229,9 +247,19 @@ export class NotificationService {
    */
   suspendToasties(suspend: boolean) {
     this.hideToasties = suspend;
+    this.muteStateSource.next(suspend);
+    localStorage.setItem(this.MUTE_KEY, suspend.toString());
   }
 
-  toggleSidebar(forceClose = false) {
-    this.sidebarSubject.next(forceClose);
+  /**
+   * Toggle the sidebar/panel visibility
+   * @param isOpen whether to open or close the panel
+   * @param useNewPanel which panel type to use
+   */
+  toggleSidebar(isOpen: boolean, useNewPanel: boolean = true) {
+    this.panelStateSource.next({
+      isOpen: isOpen,
+      useNewPanel: useNewPanel
+    });
   }
 }