]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard : Carbonises Notification body
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>
Fri, 26 Sep 2025 08:17:34 +0000 (13:47 +0530)
Fixes: https://tracker.ceph.com/issue/71734
Signed-off-by: Anikait Sehwag <anikaitsehwag.amg@gmail.com>
src/pybind/mgr/dashboard/frontend/src/app/core/core.module.ts
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation.module.ts
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/header/notification-header.component.scss
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-area/notification-area.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-area/notification-area.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-area/notification-area.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-area/notification-area.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-panel.component.html
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-panel.component.scss
src/pybind/mgr/dashboard/frontend/src/app/shared/components/icon/icon.component.scss
src/pybind/mgr/dashboard/frontend/src/app/shared/services/notification.service.ts

index c0b0807b2d558384ef00282d85bc57bcf8b0bb41..dc4e1c189ab38d8c9281c1b66afbbcda535f5e80 100644 (file)
@@ -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: [
index a1d1cc419219a0be70c050050a873d0d620b8524..91b51d8682b2f7a3885944003c86cab91bc859be 100644 (file)
@@ -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) {
index 3540b6848f3baad44a954817fed2cab77cdd2fe1..c8ab9aa30cbbb384cf86254e1b67504dd87fffd3 100644 (file)
@@ -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 (file)
index 0000000..03135a1
--- /dev/null
@@ -0,0 +1,50 @@
+<!-- Reusable notification item template -->
+<ng-template #notificationItemTemplate
+             let-notification="notification"
+             let-last="last">
+  <div class="notification-wrapper">
+    <div class="notification-item">
+      <div class="notification-icon">
+        <cd-icon [type]="notificationIconMap[notification.type] || 'info'">
+        </cd-icon>
+      </div>
+      <div class="notification-content">
+        <div class="notification-timestamp">{{ notification.timestamp | relativeDate }}</div>
+        <div class="notification-title">{{ notification.title }}</div>
+        <div class="notification-message"
+             [innerHTML]="notification.message | sanitizeHtml"></div>
+      </div>
+      <button cdsButton="ghost"
+              size="sm"
+              class="notification-close"
+              (click)="removeNotification(notification, $event)">
+        <cd-icon type="destroy"></cd-icon>
+      </button>
+    </div>
+  @if (!last) {
+    <div class="notification-divider"></div>
+  }
+  </div>
+</ng-template>
+
+@if (todayNotifications.length > 0) {
+  <div class="notification-section-heading"
+       i18n>Today</div>
+  @for (notification of todayNotifications; track notification.timestamp; let last = $last) {
+    <ng-container *ngTemplateOutlet="notificationItemTemplate; context: { notification: notification, last: last }"></ng-container>
+  }
+}
+
+@if (previousNotifications.length > 0) {
+  <div class="notification-section-heading"
+       i18n>Previous</div>
+  @for (notification of previousNotifications; track notification.timestamp; let last = $last) {
+    <ng-container *ngTemplateOutlet="notificationItemTemplate; context: { notification: notification, last: last }"></ng-container>
+  }
+}
+
+@if (todayNotifications.length === 0 && previousNotifications.length === 0) {
+  <div class="notification-empty">
+    <div i18n>No notifications</div>
+  </div>
+}
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 (file)
index 0000000..14bf38f
--- /dev/null
@@ -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 (file)
index 0000000..7fb06f5
--- /dev/null
@@ -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<NotificationAreaComponent>;
+  let notificationService: any;
+  let mockDataSource: BehaviorSubject<CdNotification[]>;
+
+  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<CdNotification[]>(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 (file)
index 0000000..8bba8a4
--- /dev/null
@@ -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);
+    }
+  }
+}
index 65577e9e07d0a2b95fcbadbbf249e4a286eaa02c..43e881d37bda0ef3cfc0a0ed86de2f34643f2313 100644 (file)
@@ -1,4 +1,4 @@
 <div class="notification-panel">
   <cd-notification-header></cd-notification-header>
-  <!-- Body and footer components will be added here later -->
+  <cd-notification-area></cd-notification-area>
 </div>
index b90127b4d297fd85753b1f391a02bd60b355d214..8370b7c10e317ba33d3badd417291be46da02e47 100644 (file)
@@ -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
+  }
 }
index 96e563783678d246cf45fa308a521d190d4bb079..a10d058b8a7c1f676ca135a3af21ae04eaa11743 100644 (file)
 .warning-icon {
   color: theme.$support-caution-major;
 }
+
+.error-icon {
+  color: theme.$support-error;
+}
+
+.info-icon {
+  color: theme.$support-info;
+}
index 6b5117616931a13d793fe9a80b787cba7444f882..1dba3684c58f72b79bd27103274a09159ee70eb8 100644 (file)
@@ -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[] {