From 2a64e0d674edd80ebed6ee2d30b649b7a461eccf Mon Sep 17 00:00:00 2001 From: Anikait Sehwag Date: Mon, 7 Jul 2025 23:43:04 +0530 Subject: [PATCH] mgr/dashboard : Carbonises Notification body Fixes: https://tracker.ceph.com/issue/71734 Signed-off-by: Anikait Sehwag --- .../frontend/src/app/core/core.module.ts | 12 +- .../app/core/navigation/navigation.module.ts | 12 +- .../header/notification-header.component.scss | 1 + .../notification-area.component.html | 50 +++ .../notification-area.component.scss | 128 ++++++++ .../notification-area.component.spec.ts | 292 ++++++++++++++++++ .../notification-area.component.ts | 68 ++++ .../notification-panel.component.html | 2 +- .../notification-panel.component.scss | 16 +- .../components/icon/icon.component.scss | 8 + .../shared/services/notification.service.ts | 33 +- 11 files changed, 602 insertions(+), 20 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-area/notification-area.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-area/notification-area.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-area/notification-area.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-area/notification-area.component.ts diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/core.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/core.module.ts index c0b0807b2d5..dc4e1c189ab 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/core.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/core.module.ts @@ -4,6 +4,12 @@ import { RouterModule } from '@angular/router'; import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; import { BlockUIModule } from 'ng-block-ui'; +import { + PlaceholderModule, + IconModule, + ThemeModule, + ButtonModule +} from 'carbon-components-angular'; import { ContextComponent } from '~/app/core/context/context.component'; import { SharedModule } from '~/app/shared/shared.module'; @@ -12,7 +18,6 @@ import { BlankLayoutComponent } from './layouts/blank-layout/blank-layout.compon import { LoginLayoutComponent } from './layouts/login-layout/login-layout.component'; import { WorkbenchLayoutComponent } from './layouts/workbench-layout/workbench-layout.component'; import { NavigationModule } from './navigation/navigation.module'; -import { PlaceholderModule } from 'carbon-components-angular'; @NgModule({ imports: [ @@ -22,7 +27,10 @@ import { PlaceholderModule } from 'carbon-components-angular'; NgbDropdownModule, RouterModule, SharedModule, - PlaceholderModule + PlaceholderModule, + IconModule, + ThemeModule, + ButtonModule ], exports: [NavigationModule], declarations: [ diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation.module.ts index a1d1cc41921..91b51d8682b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation.module.ts @@ -14,7 +14,8 @@ import { BreadcrumbModule, ModalModule, ToggleModule, - ButtonModule + ButtonModule, + PlaceholderModule } from 'carbon-components-angular'; import { AppRoutingModule } from '~/app/app-routing.module'; @@ -30,6 +31,7 @@ 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'; +import { NotificationAreaComponent } from './notification-panel/notification-area/notification-area.component'; // Icons import UserFilledIcon from '@carbon/icons/es/user--filled/20'; @@ -67,7 +69,8 @@ import { ModalCdsService } from '~/app/shared/services/modal-cds.service'; BreadcrumbModule, ModalModule, ToggleModule, - ButtonModule + ButtonModule, + PlaceholderModule ], declarations: [ AboutComponent, @@ -77,13 +80,14 @@ import { ModalCdsService } from '~/app/shared/services/modal-cds.service'; NotificationsComponent, NotificationPanelComponent, NotificationHeaderComponent, + NotificationAreaComponent, DashboardHelpComponent, AdministrationComponent, IdentityComponent ], + providers: [ModalCdsService], exports: [NavigationComponent, BreadcrumbsComponent], - schemas: [CUSTOM_ELEMENTS_SCHEMA], - providers: [ModalCdsService] + schemas: [CUSTOM_ELEMENTS_SCHEMA] }) export class NavigationModule { constructor(private iconService: IconService) { 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 index 3540b6848f3..c8ab9aa30cb 100644 --- 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 @@ -8,6 +8,7 @@ padding: spacing.$spacing-04; border-bottom: 1px solid theme.$border-subtle-01; background-color: theme.$layer-01; + flex-shrink: 0; &__top { display: flex; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-area/notification-area.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-area/notification-area.component.html new file mode 100644 index 00000000000..03135a10b5e --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-area/notification-area.component.html @@ -0,0 +1,50 @@ + + +
+
+
+ + +
+
+
{{ notification.timestamp | relativeDate }}
+
{{ notification.title }}
+
+
+ +
+ @if (!last) { +
+ } +
+
+ +@if (todayNotifications.length > 0) { +
Today
+ @for (notification of todayNotifications; track notification.timestamp; let last = $last) { + + } +} + +@if (previousNotifications.length > 0) { +
Previous
+ @for (notification of previousNotifications; track notification.timestamp; let last = $last) { + + } +} + +@if (todayNotifications.length === 0 && previousNotifications.length === 0) { +
+
No notifications
+
+} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-area/notification-area.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-area/notification-area.component.scss new file mode 100644 index 00000000000..14bf38ff9db --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-area/notification-area.component.scss @@ -0,0 +1,128 @@ +@use '@carbon/styles/scss/theme' as *; +@use '@carbon/styles/scss/spacing' as *; +@use '@carbon/type'; + +.notification-section-heading { + @include type.type-style('heading-compact-01'); + + margin: 0; + color: $text-primary; + padding: $spacing-05 $spacing-05 $spacing-03; + background-color: $layer-01; + position: sticky; + top: 0; + z-index: 2; + display: block; +} + +.notification-timestamp { + @include type.type-style('label-01'); + + color: $text-secondary; + line-height: 1; + margin-top: 0; + display: block; +} + +.notification-title { + @include type.type-style('body-short-01'); + + margin: 0; + color: $text-primary; + line-height: 1.25; + display: block; +} + +.notification-message { + @include type.type-style('body-short-01'); + + color: $text-helper; + margin: 0; + line-height: 1.4; + margin-top: -$spacing-01; + display: block; +} + +.notification-icon { + flex-shrink: 0; + margin-top: 0; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + + svg { + width: 16px; + height: 16px; + } +} + +.notification-empty { + margin: 0; + padding: $spacing-05; + color: $text-secondary; + text-align: center; + + @include type.type-style('body-short-01'); +} + +.notification-wrapper { + padding: 0 $spacing-05; + position: relative; + z-index: 1; + background-color: $layer-01; + + &:last-child { + padding-bottom: $spacing-05; + } +} + +.notification-item { + display: flex; + gap: $spacing-05; + padding: $spacing-03 0; + align-items: flex-start; + position: relative; + + .notification-close { + position: absolute; + right: 0; + top: $spacing-03; + padding: $spacing-02; + min-height: 0; + color: $text-helper; + opacity: 0; + transition: opacity 0.2s ease-in-out; + + &:hover { + background-color: $layer-hover; + } + } + + &:hover { + .notification-close { + opacity: 1; + } + } +} + +.notification-content { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: $spacing-02; + padding-right: $spacing-06; +} + +.notification-divider { + border-bottom: 1px solid $border-subtle-01; +} + +:host { + display: block; + height: 100%; + overflow-y: auto; + background-color: $layer-01; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-area/notification-area.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-area/notification-area.component.spec.ts new file mode 100644 index 00000000000..7fb06f5ce7c --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-area/notification-area.component.spec.ts @@ -0,0 +1,292 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { BehaviorSubject } from 'rxjs'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; + +import { NotificationAreaComponent } from './notification-area.component'; +import { NotificationService } from '../../../../shared/services/notification.service'; +import { CdNotification, CdNotificationConfig } from '../../../../shared/models/cd-notification'; +import { NotificationType } from '../../../../shared/enum/notification-type.enum'; +import { SharedModule } from '../../../../shared/shared.module'; +import { configureTestBed } from '~/testing/unit-test-helper'; + +describe('NotificationAreaComponent', () => { + let component: NotificationAreaComponent; + let fixture: ComponentFixture; + let notificationService: any; + let mockDataSource: BehaviorSubject; + + const today = new Date(); + const yesterday = new Date(today); + yesterday.setDate(yesterday.getDate() - 1); + + const createNotification = ( + type: NotificationType, + title: string, + timestamp: string + ): CdNotification => { + const config = new CdNotificationConfig(type, title, 'message'); + const notification = new CdNotification(config); + notification.timestamp = timestamp; + return notification; + }; + + const mockNotifications: CdNotification[] = [ + createNotification(NotificationType.success, 'Success Today', today.toISOString()), + createNotification(NotificationType.error, 'Error Yesterday', yesterday.toISOString()) + ]; + + configureTestBed({ + imports: [SharedModule, NoopAnimationsModule], + declarations: [NotificationAreaComponent] + }); + + beforeEach(() => { + mockDataSource = new BehaviorSubject(mockNotifications); + const spy = { + remove: jasmine.createSpy('remove'), + dataSource: mockDataSource, + data$: mockDataSource.asObservable() + }; + + TestBed.overrideProvider(NotificationService, { useValue: spy }); + fixture = TestBed.createComponent(NotificationAreaComponent); + component = fixture.componentInstance; + notificationService = TestBed.inject(NotificationService); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should separate notifications into today and previous', () => { + expect(component.todayNotifications.length).toBe(1); + expect(component.previousNotifications.length).toBe(1); + expect(component.todayNotifications[0].title).toBe('Success Today'); + expect(component.previousNotifications[0].title).toBe('Error Yesterday'); + }); + + it('should display empty state when no notifications exist', () => { + mockDataSource.next([]); + fixture.detectChanges(); + + const emptyElement = fixture.debugElement.query(By.css('.notification-empty')); + expect(emptyElement).toBeTruthy(); + expect(emptyElement.nativeElement.textContent).toContain('No notifications'); + }); + + it('should remove notification when close button is clicked', () => { + const notification = mockNotifications[0]; + const event = new MouseEvent('click'); + spyOn(event, 'stopPropagation'); + spyOn(event, 'preventDefault'); + + component.removeNotification(notification, event); + + expect(event.stopPropagation).toHaveBeenCalled(); + expect(event.preventDefault).toHaveBeenCalled(); + expect(notificationService.remove).toHaveBeenCalledWith(0); + }); + + it('should unsubscribe from notification service on destroy', () => { + const subSpy = spyOn(component['sub'], 'unsubscribe'); + component.ngOnDestroy(); + expect(subSpy).toHaveBeenCalled(); + }); + + it('should render notifications with correct structure', () => { + const notificationElements = fixture.debugElement.queryAll(By.css('.notification-item')); + expect(notificationElements.length).toBe(2); + + const firstNotification = notificationElements[0]; + expect( + firstNotification.query(By.css('.notification-title')).nativeElement.textContent + ).toContain('Success Today'); + expect( + firstNotification.query(By.css('.notification-message')).nativeElement.textContent + ).toContain('message'); + + const iconElement = firstNotification.query(By.css('.notification-icon cd-icon')); + expect(iconElement).toBeTruthy(); + }); + + it('should display notification timestamps with relative date pipe', () => { + const timestampElements = fixture.debugElement.queryAll(By.css('.notification-timestamp')); + expect(timestampElements.length).toBe(2); + expect(timestampElements[0].nativeElement.textContent).toBeTruthy(); + expect(timestampElements[1].nativeElement.textContent).toBeTruthy(); + }); + + it('should render notification icons with correct types', () => { + const iconElements = fixture.debugElement.queryAll(By.css('.notification-icon cd-icon')); + expect(iconElements.length).toBe(2); + + // Check that icons have the correct type attribute + expect(iconElements[0].attributes['ng-reflect-type']).toBe('success'); + expect(iconElements[1].attributes['ng-reflect-type']).toBe('danger'); + }); + + it('should render notification dividers between items', () => { + const dividerElements = fixture.debugElement.queryAll(By.css('.notification-divider')); + expect(dividerElements.length).toBe(0); + }); + + it('should render close buttons for each notification', () => { + const closeButtons = fixture.debugElement.queryAll(By.css('.notification-close')); + expect(closeButtons.length).toBe(2); + + const closeIcons = fixture.debugElement.queryAll(By.css('.notification-close cd-icon')); + expect(closeIcons.length).toBe(2); + expect(closeIcons[0].attributes['ng-reflect-type']).toBe('destroy'); + }); + + it('should render notification content with proper structure', () => { + const contentElements = fixture.debugElement.queryAll(By.css('.notification-content')); + expect(contentElements.length).toBe(2); + + contentElements.forEach((content) => { + expect(content.query(By.css('.notification-timestamp'))).toBeTruthy(); + expect(content.query(By.css('.notification-title'))).toBeTruthy(); + expect(content.query(By.css('.notification-message'))).toBeTruthy(); + }); + }); + + it('should render notification wrappers with proper structure', () => { + const wrapperElements = fixture.debugElement.queryAll(By.css('.notification-wrapper')); + expect(wrapperElements.length).toBe(2); + + wrapperElements.forEach((wrapper) => { + expect(wrapper.query(By.css('.notification-item'))).toBeTruthy(); + }); + }); + + it('should show section headings correctly', () => { + const headings = fixture.debugElement.queryAll(By.css('.notification-section-heading')); + expect(headings.length).toBe(2); + expect(headings[0].nativeElement.textContent).toContain('Today'); + expect(headings[1].nativeElement.textContent).toContain('Previous'); + }); + + it('should handle notification icon mapping correctly', () => { + expect(component.notificationIconMap[NotificationType.success]).toBe('success'); + expect(component.notificationIconMap[NotificationType.error]).toBe('danger'); + expect(component.notificationIconMap[NotificationType.info]).toBe('info'); + expect(component.notificationIconMap[NotificationType.warning]).toBe('warning'); + }); + + it('should handle notifications with different types', () => { + const infoNotification = createNotification( + NotificationType.info, + 'Info Today', + new Date(today.getTime() + 1000).toISOString() + ); + const warningNotification = createNotification( + NotificationType.warning, + 'Warning Today', + new Date(today.getTime() + 2000).toISOString() + ); + + mockDataSource.next([infoNotification, warningNotification]); + fixture.detectChanges(); + + expect(component.todayNotifications.length).toBe(2); + expect(component.todayNotifications[0].type).toBe(NotificationType.info); + expect(component.todayNotifications[1].type).toBe(NotificationType.warning); + }); + + it('should handle empty notifications array', () => { + mockDataSource.next([]); + fixture.detectChanges(); + + expect(component.todayNotifications.length).toBe(0); + expect(component.previousNotifications.length).toBe(0); + + const emptyElement = fixture.debugElement.query(By.css('.notification-empty')); + expect(emptyElement).toBeTruthy(); + }); + + it('should handle notifications with only today items', () => { + const todayOnly = [ + createNotification( + NotificationType.success, + 'Success 1', + new Date(today.getTime() + 1000).toISOString() + ), + createNotification( + NotificationType.info, + 'Info 1', + new Date(today.getTime() + 2000).toISOString() + ) + ]; + + mockDataSource.next(todayOnly); + fixture.detectChanges(); + + expect(component.todayNotifications.length).toBe(2); + expect(component.previousNotifications.length).toBe(0); + + const headings = fixture.debugElement.queryAll(By.css('.notification-section-heading')); + expect(headings.length).toBe(1); + expect(headings[0].nativeElement.textContent).toContain('Today'); + }); + + it('should handle notifications with only previous items', () => { + const previousOnly = [ + createNotification( + NotificationType.error, + 'Error 1', + new Date(yesterday.getTime() + 1000).toISOString() + ), + createNotification( + NotificationType.warning, + 'Warning 1', + new Date(yesterday.getTime() + 2000).toISOString() + ) + ]; + + mockDataSource.next(previousOnly); + fixture.detectChanges(); + + expect(component.todayNotifications.length).toBe(0); + expect(component.previousNotifications.length).toBe(2); + + const headings = fixture.debugElement.queryAll(By.css('.notification-section-heading')); + expect(headings.length).toBe(1); + expect(headings[0].nativeElement.textContent).toContain('Previous'); + }); + + it('should find correct notification index when removing', () => { + const notification = mockNotifications[0]; + const event = new MouseEvent('click'); + spyOn(event, 'stopPropagation'); + spyOn(event, 'preventDefault'); + + spyOn(notificationService['dataSource'], 'getValue').and.returnValue(mockNotifications); + + component.removeNotification(notification, event); + + expect(event.stopPropagation).toHaveBeenCalled(); + expect(event.preventDefault).toHaveBeenCalled(); + expect(notificationService.remove).toHaveBeenCalledWith(0); + }); + + it('should handle remove notification when index not found', () => { + const notification = createNotification( + NotificationType.info, + 'Not Found', + today.toISOString() + ); + const event = new MouseEvent('click'); + spyOn(event, 'stopPropagation'); + spyOn(event, 'preventDefault'); + + spyOn(notificationService['dataSource'], 'getValue').and.returnValue([]); + + component.removeNotification(notification, event); + + expect(event.stopPropagation).toHaveBeenCalled(); + expect(event.preventDefault).toHaveBeenCalled(); + expect(notificationService.remove).not.toHaveBeenCalled(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-area/notification-area.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-area/notification-area.component.ts new file mode 100644 index 00000000000..8bba8a443df --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-area/notification-area.component.ts @@ -0,0 +1,68 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { NotificationService } from '../../../../shared/services/notification.service'; +import { CdNotification } from '../../../../shared/models/cd-notification'; +import { NotificationType } from '../../../../shared/enum/notification-type.enum'; + +@Component({ + selector: 'cd-notification-area', + templateUrl: './notification-area.component.html', + styleUrls: ['./notification-area.component.scss'] +}) +export class NotificationAreaComponent implements OnInit, OnDestroy { + todayNotifications: CdNotification[] = []; + previousNotifications: CdNotification[] = []; + private sub: Subscription; + + readonly notificationIconMap = { + [NotificationType.success]: 'success', + [NotificationType.error]: 'danger', + [NotificationType.info]: 'info', + [NotificationType.warning]: 'warning' + } as const; + + constructor(private notificationService: NotificationService) {} + + ngOnInit(): void { + this.sub = this.notificationService.data$.subscribe((notifications: CdNotification[]) => { + const today: Date = new Date(); + this.todayNotifications = []; + this.previousNotifications = []; + notifications.forEach((n: CdNotification) => { + const notifDate = new Date(n.timestamp); + if ( + notifDate.getDate() === today.getDate() && + notifDate.getMonth() === today.getMonth() && + notifDate.getFullYear() === today.getFullYear() + ) { + this.todayNotifications.push(n); + } else { + this.previousNotifications.push(n); + } + }); + }); + } + + ngOnDestroy(): void { + if (this.sub) { + this.sub.unsubscribe(); + } + } + + removeNotification(notification: CdNotification, event: MouseEvent) { + // Stop event propagation to prevent panel closing + event.stopPropagation(); + event.preventDefault(); + + // Get the notification index from the service's data + const notifications = this.notificationService['dataSource'].getValue(); + const index = notifications.findIndex( + (n) => n.timestamp === notification.timestamp && n.title === notification.title + ); + + if (index > -1) { + // Remove the notification through the service + this.notificationService.remove(index); + } + } +} 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 index 65577e9e07d..43e881d37bd 100644 --- 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 @@ -1,4 +1,4 @@
- +
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 index b90127b4d29..8370b7c10e3 100644 --- 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 @@ -9,10 +9,24 @@ position: absolute; top: spacing.$spacing-09; right: 0; - width: 400px; // Keep original width as it doesn't map to a spacing token + width: 400px; + height: 700px; background-color: $layer-01; box-shadow: $shadow; border: 1px solid $border-subtle-01; z-index: 6000; color: $text-primary; + display: flex; + flex-direction: column; + overflow: hidden; + + cd-notification-header { + flex: 0 0 auto; + } + + cd-notification-area { + flex: 1 1 auto; + overflow-y: auto; + min-height: 0; // Failing in firefox without this + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/icon/icon.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/icon/icon.component.scss index 96e56378367..a10d058b8a7 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/icon/icon.component.scss +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/icon/icon.component.scss @@ -17,3 +17,11 @@ .warning-icon { color: theme.$support-caution-major; } + +.error-icon { + color: theme.$support-error; +} + +.info-icon { + color: theme.$support-info; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/notification.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/notification.service.ts index 6b511761693..1dba3684c58 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/notification.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/notification.service.ts @@ -45,6 +45,7 @@ export class NotificationService { private activeToasts: ToastContent[] = []; KEY = 'cdNotifications'; MUTE_KEY = 'cdNotificationsMuted'; + private readonly MAX_NOTIFICATIONS = 10; constructor( private taskMessageService: TaskMessageService, @@ -80,27 +81,34 @@ export class NotificationService { } /** - * Removes a single saved notifications + * Removes a single saved notification */ remove(index: number) { - const recent = this.dataSource.getValue(); - recent.splice(index, 1); - this.dataSource.next(recent); - localStorage.setItem(this.KEY, JSON.stringify(recent)); + const notifications = this.dataSource.getValue(); + notifications.splice(index, 1); + this.dataSource.next(notifications); + this.persistNotifications(notifications); } /** * Method used for saving a shown notification (check show() method). */ save(notification: CdNotification) { - const recent = this.dataSource.getValue(); - recent.push(notification); - recent.sort((a, b) => (a.timestamp > b.timestamp ? -1 : 1)); - while (recent.length > 10) { - recent.pop(); + const notifications = this.dataSource.getValue(); + notifications.push(notification); + notifications.sort((a, b) => (a.timestamp > b.timestamp ? -1 : 1)); + while (notifications.length > this.MAX_NOTIFICATIONS) { + notifications.pop(); } - this.dataSource.next(recent); - localStorage.setItem(this.KEY, JSON.stringify(recent)); + this.dataSource.next(notifications); + this.persistNotifications(notifications); + } + + /** + * Persists notifications to localStorage + */ + private persistNotifications(notifications: CdNotification[]) { + localStorage.setItem(this.KEY, JSON.stringify(notifications)); } /** @@ -166,6 +174,7 @@ export class NotificationService { } this.showToasty(notification); }); + this.queued = []; } private getUnifiedTitleQueue(): CdNotificationConfig[] { -- 2.39.5