]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard:Notification Footer+ Notification Page
authorAnikait Sehwag <anikaitsehwag.amg@gmail.com>
Mon, 7 Jul 2025 18:13:04 +0000 (23:43 +0530)
committerAfreen Misbah <afreen@ibm.com>
Mon, 26 Jan 2026 06:45:47 +0000 (12:15 +0530)
Fixes: https://tracker.ceph.com/issues/71738
A comprehensive notifications management page that allows users to view, search, and manage system notifications with a modern Carbon Design System interface.

Signed-off-by: Anikait Sehwag <anikaitsehwag.amg@gmail.com>
Co-authored-by: Afreen Misbah <afreen@ibm.com>
34 files changed:
src/pybind/mgr/dashboard/frontend/src/app/app-routing.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.html [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/header/notification-header.component.scss [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/header/notification-header.component.spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/header/notification-header.component.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-area/notification-area.component.scss
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-footer/notification-footer.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-footer/notification-footer.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-footer/notification-footer.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-footer/notification-footer.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-header/notification-header.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-header/notification-header.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-header/notification-header.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-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 [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-panel.component.scss [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-panel.component.spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-panel.component.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-panel/notification-panel.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-panel/notification-panel.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/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/notification-panel.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notifications-page/notifications-page.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notifications-page/notifications-page.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notifications-page/notifications-page.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notifications-page/notifications-page.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/components.module.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-notification.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/prometheus-alerts.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-alert-formatter.ts

index a40c58631af3bd47506d766672ddd12ead6ae026..fcab76f098884720509bfad9688b02739f2ab47a 100644 (file)
@@ -60,6 +60,7 @@ import { SmbUsersgroupsListComponent } from './ceph/smb/smb-usersgroups-list/smb
 import { SmbOverviewComponent } from './ceph/smb/smb-overview/smb-overview.component';
 import { MultiClusterFormComponent } from './ceph/cluster/multi-cluster/multi-cluster-form/multi-cluster-form.component';
 import { CephfsMirroringListComponent } from './ceph/cephfs/cephfs-mirroring-list/cephfs-mirroring-list.component';
+import { NotificationsPageComponent } from './core/navigation/notification-panel/notifications-page/notifications-page.component';
 
 @Injectable()
 export class PerformanceCounterBreadcrumbsResolver extends BreadcrumbsResolver {
@@ -106,6 +107,14 @@ const routes: Routes = [
       { path: 'overview', component: DashboardComponent },
       { path: 'error', component: ErrorComponent },
 
+      // Notifications
+      {
+        path: 'notifications',
+        data: {
+          breadcrumbs: 'Cluster/Notifications'
+        },
+        component: NotificationsPageComponent
+      },
       // Cluster
       {
         path: 'expand-cluster',
index a9791bf2179fe2fdf1b3fde87775df9903a4aff1..089f47a16f2cb77a517fd7a60fd57034f36c5f65 100644 (file)
@@ -12,16 +12,19 @@ import {
   DialogModule,
   GridModule,
   BreadcrumbModule,
-  ModalModule,
   ToggleModule,
   ButtonModule,
   PlaceholderModule,
   TagModule,
-  ProgressBarModule
+  ProgressBarModule,
+  StructuredListModule,
+  SearchModule
 } from 'carbon-components-angular';
 
 import { AppRoutingModule } from '~/app/app-routing.module';
 import { SharedModule } from '~/app/shared/shared.module';
+import { ModalCdsService } from '~/app/shared/services/modal-cds.service';
+
 import { AuthModule } from '../auth/auth.module';
 import { AboutComponent } from './about/about.component';
 import { AdministrationComponent } from './administration/administration.component';
@@ -31,15 +34,14 @@ 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';
-import { NotificationAreaComponent } from './notification-panel/notification-area/notification-area.component';
+import { NotificationsPageComponent } from './notification-panel/notifications-page/notifications-page.component';
 
 // Icons
 import UserFilledIcon from '@carbon/icons/es/user--filled/20';
 import SettingsIcon from '@carbon/icons/es/settings/20';
 import HelpIcon from '@carbon/icons/es/help/20';
 import NotificationIcon from '@carbon/icons/es/notification/20';
+import NotificationOffIcon from '@carbon/icons/es/notification--off/20';
 import LaunchIcon from '@carbon/icons/es/launch/16';
 import DashboardIcon from '@carbon/icons/es/template/20';
 import ClusterIcon from '@carbon/icons/es/web-services--cluster/20';
@@ -51,7 +53,16 @@ import ObservabilityIcon from '@carbon/icons/es/observed--hail/20';
 import AdminIcon from '@carbon/icons/es/network--admin-control/20';
 import LockedIcon from '@carbon/icons/es/locked/16';
 import LogoutIcon from '@carbon/icons/es/logout/16';
-import { ModalCdsService } from '~/app/shared/services/modal-cds.service';
+import CheckmarkFilledIcon from '@carbon/icons/es/checkmark--filled/16';
+import ErrorFilledIcon from '@carbon/icons/es/error--filled/16';
+import InformationFilledIcon from '@carbon/icons/es/information--filled/16';
+import WarningFilledIcon from '@carbon/icons/es/warning--filled/16';
+import NotificationFilledIcon from '@carbon/icons/es/notification--filled/16';
+import CloseIcon from '@carbon/icons/es/close/16';
+import { NotificationPanelComponent } from './notification-panel/notification-panel/notification-panel.component';
+import { NotificationHeaderComponent } from './notification-panel/notification-header/notification-header.component';
+import { NotificationAreaComponent } from './notification-panel/notification-area/notification-area.component';
+import { NotificationFooterComponent } from './notification-panel/notification-footer/notification-footer.component';
 
 @NgModule({
   imports: [
@@ -69,12 +80,13 @@ import { ModalCdsService } from '~/app/shared/services/modal-cds.service';
     DialogModule,
     GridModule,
     BreadcrumbModule,
-    ModalModule,
     ToggleModule,
     ButtonModule,
     PlaceholderModule,
     TagModule,
-    ProgressBarModule
+    ProgressBarModule,
+    StructuredListModule,
+    SearchModule
   ],
   declarations: [
     AboutComponent,
@@ -85,12 +97,22 @@ import { ModalCdsService } from '~/app/shared/services/modal-cds.service';
     NotificationPanelComponent,
     NotificationHeaderComponent,
     NotificationAreaComponent,
+    NotificationFooterComponent,
+    NotificationsPageComponent,
     DashboardHelpComponent,
     AdministrationComponent,
     IdentityComponent
   ],
   providers: [ModalCdsService],
-  exports: [NavigationComponent, BreadcrumbsComponent],
+  exports: [
+    NavigationComponent,
+    NotificationsPageComponent,
+    NotificationPanelComponent,
+    NotificationHeaderComponent,
+    NotificationAreaComponent,
+    NotificationFooterComponent,
+    BreadcrumbsComponent
+  ],
   schemas: [CUSTOM_ELEMENTS_SCHEMA]
 })
 export class NavigationModule {
@@ -100,6 +122,7 @@ export class NavigationModule {
       SettingsIcon,
       HelpIcon,
       NotificationIcon,
+      NotificationOffIcon,
       LaunchIcon,
       DashboardIcon,
       ClusterIcon,
@@ -110,7 +133,13 @@ export class NavigationModule {
       ObservabilityIcon,
       AdminIcon,
       LockedIcon,
-      LogoutIcon
+      LogoutIcon,
+      CheckmarkFilledIcon,
+      ErrorFilledIcon,
+      InformationFilledIcon,
+      WarningFilledIcon,
+      NotificationFilledIcon,
+      CloseIcon
     ]);
   }
 }
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
deleted file mode 100644 (file)
index 0038d3c..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-<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
deleted file mode 100644 (file)
index c8ab9aa..0000000
+++ /dev/null
@@ -1,57 +0,0 @@
-@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;
-  flex-shrink: 0;
-
-  &__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
deleted file mode 100644 (file)
index 444a6d1..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-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
deleted file mode 100644 (file)
index e3f9113..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-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'],
-  standalone: false
-})
-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-footer/notification-footer.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-footer/notification-footer.component.html
new file mode 100644 (file)
index 0000000..4d6475a
--- /dev/null
@@ -0,0 +1,8 @@
+<div class="notification-footer">
+  <cds-button
+    kind="ghost"
+    size="sm"
+    [routerLink]="['/notifications']"
+    (click)="closePanel($event)"
+    i18n>View all</cds-button>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-footer/notification-footer.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-footer/notification-footer.component.scss
new file mode 100644 (file)
index 0000000..0a513f7
--- /dev/null
@@ -0,0 +1,17 @@
+@use '@carbon/styles/scss/spacing';
+@use '@carbon/styles/scss/theme';
+
+.notification-footer {
+  display: flex;
+  justify-content: flex-start;
+  padding: spacing.$spacing-03;
+  border-top: 1px solid theme.$border-subtle-01;
+
+  cds-button {
+    color: theme.$text-primary;
+
+    &:hover {
+      color: theme.$link-primary;
+    }
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-footer/notification-footer.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-footer/notification-footer.component.spec.ts
new file mode 100644 (file)
index 0000000..65d51d6
--- /dev/null
@@ -0,0 +1,29 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { NotificationFooterComponent } from './notification-footer.component';
+
+describe('NotificationFooterComponent', () => {
+  let component: NotificationFooterComponent;
+  let fixture: ComponentFixture<NotificationFooterComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [NotificationFooterComponent]
+    }).compileComponents();
+  });
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(NotificationFooterComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+
+  it('should render view all button', () => {
+    const compiled = fixture.nativeElement as HTMLElement;
+    const button = compiled.querySelector('cds-button');
+    expect(button?.textContent).toContain('View all');
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-footer/notification-footer.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-footer/notification-footer.component.ts
new file mode 100644 (file)
index 0000000..5dcf0d3
--- /dev/null
@@ -0,0 +1,17 @@
+import { Component } from '@angular/core';
+import { NotificationService } from '~/app/shared/services/notification.service';
+
+@Component({
+  selector: 'cd-notification-footer',
+  templateUrl: './notification-footer.component.html',
+  styleUrls: ['./notification-footer.component.scss']
+})
+export class NotificationFooterComponent {
+  constructor(public notificationService: NotificationService) {}
+
+  closePanel(event: Event) {
+    event.preventDefault();
+    event.stopPropagation();
+    this.notificationService.toggleSidebar(false, true);
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-header/notification-header.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-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/notification-header/notification-header.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-header/notification-header.component.scss
new file mode 100644 (file)
index 0000000..c8ab9aa
--- /dev/null
@@ -0,0 +1,57 @@
+@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;
+  flex-shrink: 0;
+
+  &__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/notification-header/notification-header.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-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/notification-header/notification-header.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-header/notification-header.component.ts
new file mode 100644 (file)
index 0000000..06e0a42
--- /dev/null
@@ -0,0 +1,39 @@
+import { Component, Output, EventEmitter, OnInit, OnDestroy } from '@angular/core';
+import { Subscription } from 'rxjs';
+import { NotificationService } from '~/app/shared/services/notification.service';
+
+@Component({
+  selector: 'cd-notification-header',
+  templateUrl: './notification-header.component.html',
+  styleUrls: ['./notification-header.component.scss'],
+  standalone: false
+})
+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
deleted file mode 100644 (file)
index 43e881d..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-<div class="notification-panel">
-  <cd-notification-header></cd-notification-header>
-  <cd-notification-area></cd-notification-area>
-</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
deleted file mode 100644 (file)
index 8370b7c..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-@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;
-  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/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
deleted file mode 100644 (file)
index e2df0fa..0000000
+++ /dev/null
@@ -1,60 +0,0 @@
-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
deleted file mode 100644 (file)
index 58066db..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-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'],
-  standalone: false
-})
-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);
-    }
-  }
-}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-panel/notification-panel.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-panel/notification-panel.component.html
new file mode 100644 (file)
index 0000000..83b6e33
--- /dev/null
@@ -0,0 +1,5 @@
+<div class="notification-panel">
+  <cd-notification-header></cd-notification-header>
+  <cd-notification-area></cd-notification-area>
+  <cd-notification-footer></cd-notification-footer>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-panel/notification-panel.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-panel/notification-panel.component.scss
new file mode 100644 (file)
index 0000000..862e2c3
--- /dev/null
@@ -0,0 +1,32 @@
+@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: fixed;
+  top: spacing.$spacing-09;
+  right: 0;
+  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-y: auto;
+
+  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/core/navigation/notification-panel/notification-panel/notification-panel.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/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/notification-panel.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-panel/notification-panel.component.ts
new file mode 100644 (file)
index 0000000..1dcd166
--- /dev/null
@@ -0,0 +1,20 @@
+import { Component, ElementRef, HostListener } from '@angular/core';
+import { NotificationService } from '~/app/shared/services/notification.service';
+
+@Component({
+  selector: 'cd-notification-panel',
+  templateUrl: './notification-panel.component.html',
+  styleUrls: ['./notification-panel.component.scss'],
+  standalone: false
+})
+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);
+    }
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notifications-page/notifications-page.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notifications-page/notifications-page.component.html
new file mode 100644 (file)
index 0000000..9074523
--- /dev/null
@@ -0,0 +1,214 @@
+<div class="notifications-page-container">
+  <div class="notification-section-heading"
+       i18n>Notifications</div>
+  <div class="row">
+    <!-- Left Panel - Notifications List -->
+    <div class="col-md-4">
+      <!-- Search Bar -->
+      <div class="search-container mb-4">
+        <cds-search
+          [size]="'md'"
+          [placeholder]="'Search notifications...'"
+          [(ngModel)]="searchText"
+          (valueChange)="onSearch($event)">
+        </cds-search>
+      </div>
+      <!-- Notifications List using Carbon Structured List -->
+      <cds-structured-list
+        class="notifications-list"
+        *ngIf="filteredNotifications.length > 0">
+        <cds-list-row
+          *ngFor="let notification of filteredNotifications"
+          [class.active]="selectedNotification === notification"
+          (click)="onNotificationSelect(notification)"
+          class="notification-row">
+          <cds-list-column>
+            <div class="notification-item-content">
+              <h6 class="notification-title">
+                <ng-container *ngIf="notification.prometheusAlert; else regularTitle">
+                  {{ notification.prometheusAlert.alertName }}
+                  <cds-tag
+                    [type]="notification.prometheusAlert.status === 'active' || notification.prometheusAlert.status === 'firing'?'red':
+                            notification.prometheusAlert.status === 'resolved' ? 'green' : 'blue'"
+                    [size]="'sm'"
+                    class="ml-2">
+                    {{ notification.prometheusAlert.status }}
+                  </cds-tag>
+                </ng-container>
+                <ng-template #regularTitle>
+                  {{ notification.title }}
+                </ng-template>
+              </h6>
+              <small class="notification-meta">
+                {{ notification.application }}
+                <ng-container *ngIf="notification.prometheusAlert">
+                  â€¢ {{ notification.prometheusAlert.severity }}
+                  <ng-container *ngIf="notification.prometheusAlert.instance">
+                    â€¢ {{ notification.prometheusAlert.instance }}
+                  </ng-container>
+                </ng-container>
+              </small>
+            </div>
+          </cds-list-column>
+          <cds-list-column nowrap="true">
+            <small class="notification-date">{{ formatDate(notification.timestamp) }}</small>
+          </cds-list-column>
+        </cds-list-row>
+      </cds-structured-list>
+      <!-- Empty state -->
+      <div *ngIf="filteredNotifications.length === 0"
+           class="empty-state">
+        <svg
+          cdsIcon="notification"
+          size="48"
+          class="empty-icon"></svg>
+        <p *ngIf="searchText">No notifications match your search</p>
+        <p *ngIf="!searchText">No notifications available</p>
+      </div>
+    </div>
+    <!-- Right Panel - Notification Details -->
+    <div class="col-md-8">
+      <div class="notification-section-heading">Notification Details</div>
+      <div *ngIf="selectedNotification"
+           class="notification-details">
+        <div class="notification-section-heading">{{ selectedNotification.title }}</div>
+        <!-- Details using Carbon Structured List -->
+        <cds-structured-list class="details-list mt-4">
+          <cds-list-row>
+            <cds-list-column
+              nowrap="true"
+              class="detail-label">Application:</cds-list-column>
+            <cds-list-column>{{ selectedNotification.application }}</cds-list-column>
+          </cds-list-row>
+          <cds-list-row>
+            <cds-list-column
+              nowrap="true"
+              class="detail-label">Type:</cds-list-column>
+            <cds-list-column>
+              <cds-tag
+                [type]="selectedNotification.type === 0 ? 'red' :
+                        selectedNotification.type === 1 ? 'blue' :
+                        selectedNotification.type === 2 ? 'green' : 'yellow'"
+                [size]="'sm'">
+                {{ selectedNotification.type === 0 ? 'Error' :
+                   selectedNotification.type === 1 ? 'Info' :
+                   selectedNotification.type === 2 ? 'Success' : 'Warning' }}
+              </cds-tag>
+            </cds-list-column>
+          </cds-list-row>
+          <cds-list-row>
+            <cds-list-column
+              nowrap="true"
+              class="detail-label">Date:</cds-list-column>
+            <cds-list-column>{{ formatDate(selectedNotification.timestamp) }}</cds-list-column>
+          </cds-list-row>
+          <cds-list-row>
+            <cds-list-column
+              nowrap="true"
+              class="detail-label">Time:</cds-list-column>
+            <cds-list-column>{{ formatTime(selectedNotification.timestamp) }}</cds-list-column>
+          </cds-list-row>
+          <!-- Prometheus-specific metadata fields -->
+          <ng-container *ngIf="selectedNotification.prometheusAlert">
+            <cds-list-row>
+              <cds-list-column
+                nowrap="true"
+                class="detail-label">Alert Name:</cds-list-column>
+              <cds-list-column>{{ selectedNotification.prometheusAlert.alertName }}</cds-list-column>
+            </cds-list-row>
+            <cds-list-row>
+              <cds-list-column
+                nowrap="true"
+                class="detail-label">Status:</cds-list-column>
+              <cds-list-column>
+                <cds-tag
+                  [type]="selectedNotification.prometheusAlert.status === 'active' || selectedNotification.prometheusAlert.status === 'firing' ? 'red' :
+                          selectedNotification.prometheusAlert.status === 'resolved' ? 'green' :
+                          selectedNotification.prometheusAlert.status === 'suppressed' ? 'gray' : 'blue'"
+                  [size]="'sm'">
+                  {{ selectedNotification.prometheusAlert.status }}
+                </cds-tag>
+              </cds-list-column>
+            </cds-list-row>
+            <cds-list-row>
+              <cds-list-column
+                nowrap="true"
+                class="detail-label">Severity:</cds-list-column>
+              <cds-list-column>
+                <cds-tag
+                  [type]="selectedNotification.prometheusAlert.severity === 'critical' ? 'red' :
+                          selectedNotification.prometheusAlert.severity === 'warning' ? 'yellow' :
+                          selectedNotification.prometheusAlert.severity === 'resolved' ? 'green' : 'blue'"
+                  [size]="'sm'">
+                  {{ selectedNotification.prometheusAlert.severity }}
+                </cds-tag>
+              </cds-list-column>
+            </cds-list-row>
+            <cds-list-row *ngIf="selectedNotification.prometheusAlert.instance">
+              <cds-list-column
+                nowrap="true"
+                class="detail-label">Instance:</cds-list-column>
+              <cds-list-column>{{ selectedNotification.prometheusAlert.instance }}</cds-list-column>
+            </cds-list-row>
+            <cds-list-row *ngIf="selectedNotification.prometheusAlert.job">
+              <cds-list-column
+                nowrap="true"
+                class="detail-label">Job:</cds-list-column>
+              <cds-list-column>{{ selectedNotification.prometheusAlert.job }}</cds-list-column>
+            </cds-list-row>
+            <cds-list-row>
+              <cds-list-column
+                nowrap="true"
+                class="detail-label">Description:</cds-list-column>
+              <cds-list-column>
+                <p class="message-content">{{ selectedNotification.prometheusAlert.description }}</p>
+              </cds-list-column>
+            </cds-list-row>
+            <cds-list-row *ngIf="selectedNotification.prometheusAlert.sourceUrl">
+              <cds-list-column
+                nowrap="true"
+                class="detail-label">Source:</cds-list-column>
+              <cds-list-column>
+                <a
+                  [href]="selectedNotification.prometheusAlert.sourceUrl"
+                  target="_blank"
+                  class="source-link">
+                  <svg
+                    cdsIcon="launch"
+                    size="16"></svg>
+                  View in Prometheus
+                </a>
+              </cds-list-column>
+            </cds-list-row>
+            <cds-list-row *ngIf="selectedNotification.prometheusAlert.fingerprint">
+              <cds-list-column
+                nowrap="true"
+                class="detail-label">Fingerprint:</cds-list-column>
+              <cds-list-column>
+                <code class="fingerprint">{{ selectedNotification.prometheusAlert.fingerprint }}</code>
+              </cds-list-column>
+            </cds-list-row>
+          </ng-container>
+          <!-- Regular message for non-Prometheus notifications -->
+          <cds-list-row
+            *ngIf="selectedNotification.message && !selectedNotification.prometheusAlert">
+            <cds-list-column
+              nowrap="true"
+              class="detail-label">Message:</cds-list-column>
+            <cds-list-column>
+              <p class="message-content">{{ selectedNotification.message }}</p>
+            </cds-list-column>
+          </cds-list-row>
+        </cds-structured-list>
+      </div>
+      <div *ngIf="!selectedNotification"
+           class="no-selection-state">
+        <svg
+          cdsIcon="notification"
+          size="64"
+          class="no-selection-icon"></svg>
+        <p>Select a notification to view details</p>
+      </div>
+    </div>
+  </div>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notifications-page/notifications-page.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notifications-page/notifications-page.component.scss
new file mode 100644 (file)
index 0000000..1f04392
--- /dev/null
@@ -0,0 +1,220 @@
+// Main container
+.notifications-page-container {
+  padding: var(--cds-spacing-05);
+  background-color: var(--cds-layer-01);
+  min-height: 100vh;
+}
+
+// Section headings
+.notification-section-heading {
+  font-size: var(--cds-productive-heading-03-font-size);
+  line-height: var(--cds-productive-heading-03-line-height);
+  font-weight: var(--cds-productive-heading-03-font-weight);
+  color: var(--cds-text-primary);
+  margin-bottom: var(--cds-spacing-05);
+}
+
+// Search container
+.search-container {
+  cds-search {
+    width: 100%;
+  }
+}
+
+// Notifications list
+.notifications-list {
+  cds-list-row {
+    cursor: pointer;
+    transition: background-color 0.2s ease;
+
+    &:hover {
+      background-color: var(--cds-layer-hover);
+    }
+
+    &.active {
+      background-color: var(--cds-layer-selected);
+
+      .notification-title {
+        color: var(--cds-text-primary);
+        font-weight: var(--cds-font-weight-semibold);
+      }
+    }
+  }
+}
+
+// Notification item content
+.notification-item-content {
+  .notification-title {
+    margin: 0 0 var(--cds-spacing-02) 0;
+    font-size: var(--cds-productive-heading-compact-01-font-size);
+    line-height: var(--cds-productive-heading-compact-01-line-height);
+    font-weight: var(--cds-productive-heading-compact-01-font-weight);
+    color: var(--cds-text-primary);
+  }
+
+  .notification-meta {
+    color: var(--cds-text-secondary);
+    font-size: var(--cds-label-01-font-size);
+    line-height: var(--cds-label-01-line-height);
+  }
+}
+
+.notification-date {
+  color: var(--cds-text-secondary);
+  font-size: var(--cds-label-01-font-size);
+  white-space: nowrap;
+}
+
+// Empty state
+.empty-state {
+  text-align: center;
+  color: var(--cds-text-secondary);
+  margin-top: var(--cds-spacing-05);
+
+  p {
+    margin: var(--cds-spacing-03) 0 0 0;
+    font-size: var(--cds-body-01-font-size);
+    line-height: var(--cds-body-01-line-height);
+  }
+}
+
+// No selection state
+.no-selection-state {
+  text-align: center;
+  color: var(--cds-text-secondary);
+  margin-top: var(--cds-spacing-06);
+
+  p {
+    margin: var(--cds-spacing-03) 0 0 0;
+    font-size: var(--cds-body-01-font-size);
+    line-height: var(--cds-body-01-line-height);
+  }
+}
+
+// Notification details
+.notification-details {
+  padding: var(--cds-spacing-05);
+  border: 1px solid var(--cds-border-subtle);
+  border-radius: var(--cds-border-radius);
+  background-color: var(--cds-layer-01);
+
+  h4 {
+    margin: 0 0 var(--cds-spacing-05) 0;
+    font-size: var(--cds-productive-heading-03-font-size);
+    line-height: var(--cds-productive-heading-03-line-height);
+    font-weight: var(--cds-productive-heading-03-font-weight);
+    color: var(--cds-text-primary);
+  }
+}
+
+// Details list
+.details-list {
+  cds-list-row {
+    border-bottom: 1px solid var(--cds-border-subtle);
+
+    &:last-child {
+      border-bottom: none;
+    }
+
+    &:hover {
+      background-color: transparent;
+    }
+  }
+
+  .detail-label {
+    color: var(--cds-text-secondary);
+    font-weight: var(--cds-font-weight-semibold);
+    font-size: var(--cds-body-compact-01-font-size);
+    min-width: 120px;
+  }
+
+  cds-list-column:last-child {
+    color: var(--cds-text-primary);
+    font-size: var(--cds-body-compact-01-font-size);
+    line-height: var(--cds-body-compact-01-line-height);
+  }
+}
+
+// Message content
+.message-content {
+  margin: 0;
+  padding: var(--cds-spacing-04);
+  background-color: var(--cds-layer-02);
+  border-radius: var(--cds-border-radius);
+  border-left: 3px solid var(--cds-support-info);
+  font-size: var(--cds-body-compact-01-font-size);
+  line-height: var(--cds-body-compact-01-line-height);
+  color: var(--cds-text-primary);
+}
+
+// Timestamp
+.timestamp {
+  color: var(--cds-text-secondary);
+  font-family: var(--cds-font-mono);
+  font-size: var(--cds-code-01-font-size);
+}
+
+// Icons
+.empty-icon,
+.no-selection-icon {
+  fill: var(--cds-icon-secondary);
+  margin-bottom: var(--cds-spacing-05);
+}
+
+// Text utilities
+.text-muted {
+  color: var(--cds-text-secondary);
+}
+
+// Typography
+h1 {
+  font-size: var(--cds-productive-heading-05-font-size);
+  line-height: var(--cds-productive-heading-05-line-height);
+  font-weight: var(--cds-productive-heading-05-font-weight);
+  color: var(--cds-text-primary);
+  margin-bottom: var(--cds-spacing-03);
+}
+
+h3 {
+  font-size: var(--cds-productive-heading-03-font-size);
+  line-height: var(--cds-productive-heading-03-line-height);
+  font-weight: var(--cds-productive-heading-03-font-weight);
+  color: var(--cds-text-primary);
+  margin-bottom: var(--cds-spacing-05);
+}
+
+p {
+  font-size: var(--cds-body-01-font-size);
+  line-height: var(--cds-body-01-line-height);
+  color: var(--cds-text-primary);
+}
+
+// Spacing utilities
+.mb-4 {
+  margin-bottom: var(--cds-spacing-05);
+}
+
+.mt-4 {
+  margin-top: var(--cds-spacing-05);
+}
+
+.mt-5 {
+  margin-top: var(--cds-spacing-06);
+}
+
+// Responsive design
+@media (width <= 768px) {
+  .notifications-page-container {
+    padding: var(--cds-spacing-04);
+  }
+
+  .notification-details {
+    padding: var(--cds-spacing-04);
+  }
+
+  .details-list {
+    .detail-label {
+      min-width: auto;
+    }
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notifications-page/notifications-page.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notifications-page/notifications-page.component.spec.ts
new file mode 100644 (file)
index 0000000..64e7f27
--- /dev/null
@@ -0,0 +1,239 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormsModule } from '@angular/forms';
+import { BehaviorSubject } from 'rxjs';
+import {
+  IconModule,
+  SearchModule,
+  StructuredListModule,
+  TagModule
+} from 'carbon-components-angular';
+
+import { NotificationsPageComponent } from './notifications-page.component';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { CdNotification } from '~/app/shared/models/cd-notification';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { SharedModule } from '~/app/shared/shared.module';
+
+describe('NotificationsPageComponent', () => {
+  let component: NotificationsPageComponent;
+  let fixture: ComponentFixture<NotificationsPageComponent>;
+  let notificationService: NotificationService;
+  let mockNotifications: CdNotification[];
+  let dataSourceSubject: BehaviorSubject<CdNotification[]>;
+
+  // Mock notification service
+  const createMockNotificationService = () => {
+    dataSourceSubject = new BehaviorSubject<CdNotification[]>([]);
+    return {
+      data$: dataSourceSubject.asObservable(),
+      dataSource: dataSourceSubject,
+      remove: jasmine.createSpy('remove')
+    };
+  };
+
+  beforeEach(async () => {
+    mockNotifications = [
+      {
+        title: 'Success Notification',
+        message: 'Operation completed successfully',
+        timestamp: new Date().toISOString(),
+        type: NotificationType.success,
+        application: 'TestApp'
+      },
+      {
+        title: 'Error Notification',
+        message: 'An error occurred',
+        timestamp: new Date(Date.now() - 86400000).toISOString(), // Yesterday
+        type: NotificationType.error,
+        application: 'TestApp'
+      },
+      {
+        title: 'Info Notification',
+        message: 'System update available',
+        timestamp: new Date(Date.now() - 172800000).toISOString(), // 2 days ago
+        type: NotificationType.info,
+        application: 'Updates'
+      }
+    ];
+
+    await TestBed.configureTestingModule({
+      imports: [
+        FormsModule,
+        SharedModule,
+        IconModule,
+        SearchModule,
+        StructuredListModule,
+        TagModule
+      ],
+      declarations: [NotificationsPageComponent],
+      providers: [{ provide: NotificationService, useFactory: createMockNotificationService }]
+    }).compileComponents();
+
+    notificationService = TestBed.inject(NotificationService);
+  });
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(NotificationsPageComponent);
+    component = fixture.componentInstance;
+    dataSourceSubject.next(mockNotifications);
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+
+  it('should load notifications on init', () => {
+    expect(component.notifications).toEqual(mockNotifications);
+    expect(component.filteredNotifications).toEqual(mockNotifications);
+  });
+
+  it('should select notification when clicked', () => {
+    const notification = mockNotifications[0];
+    component.onNotificationSelect(notification);
+    expect(component.selectedNotification).toBe(notification);
+  });
+
+  describe('search functionality', () => {
+    it('should filter notifications by title', () => {
+      component.onSearch('Success');
+      expect(component.filteredNotifications.length).toBe(1);
+      expect(component.filteredNotifications[0].title).toBe('Success Notification');
+    });
+
+    it('should filter notifications by message', () => {
+      component.onSearch('error');
+      expect(component.filteredNotifications.length).toBe(1);
+      expect(component.filteredNotifications[0].title).toBe('Error Notification');
+    });
+
+    it('should filter notifications by application', () => {
+      component.onSearch('Updates');
+      expect(component.filteredNotifications.length).toBe(1);
+      expect(component.filteredNotifications[0].application).toBe('Updates');
+    });
+
+    it('should show all notifications when search is cleared', () => {
+      component.onSearch('');
+      expect(component.filteredNotifications).toEqual(mockNotifications);
+    });
+
+    it('should be case insensitive', () => {
+      component.onSearch('SUCCESS');
+      expect(component.filteredNotifications.length).toBe(1);
+      expect(component.filteredNotifications[0].title).toBe('Success Notification');
+    });
+  });
+
+  describe('notification removal', () => {
+    it('should remove notification', () => {
+      const notification = mockNotifications[0];
+      const mockEvent = {
+        stopPropagation: jasmine.createSpy('stopPropagation'),
+        preventDefault: jasmine.createSpy('preventDefault')
+      };
+
+      // Set up the dataSource with notifications
+      dataSourceSubject.next(mockNotifications);
+      fixture.detectChanges();
+
+      component.removeNotification(notification, mockEvent as any);
+
+      expect(mockEvent.stopPropagation).toHaveBeenCalled();
+      expect(mockEvent.preventDefault).toHaveBeenCalled();
+      expect(notificationService.remove).toHaveBeenCalledWith(0); // Should be called with index 0
+    });
+
+    it('should clear selection if removed notification was selected', () => {
+      const notification = mockNotifications[0];
+      component.selectedNotification = notification;
+      const mockEvent = {
+        stopPropagation: jasmine.createSpy('stopPropagation'),
+        preventDefault: jasmine.createSpy('preventDefault')
+      };
+
+      // Set up the dataSource with notifications
+      dataSourceSubject.next(mockNotifications);
+      fixture.detectChanges();
+
+      component.removeNotification(notification, mockEvent as any);
+
+      expect(component.selectedNotification).toBeNull();
+    });
+  });
+
+  describe('icon handling', () => {
+    it('should return correct Carbon icon for success', () => {
+      expect(component.getCarbonIcon(NotificationType.success)).toBe('checkmark--filled');
+    });
+
+    it('should return correct Carbon icon for error', () => {
+      expect(component.getCarbonIcon(NotificationType.error)).toBe('error--filled');
+    });
+
+    it('should return correct Carbon icon for info', () => {
+      expect(component.getCarbonIcon(NotificationType.info)).toBe('information--filled');
+    });
+
+    it('should return correct Carbon icon for warning', () => {
+      expect(component.getCarbonIcon(NotificationType.warning)).toBe('warning--filled');
+    });
+
+    it('should return default icon for unknown type', () => {
+      expect(component.getCarbonIcon(-1)).toBe('notification--filled');
+    });
+  });
+
+  describe('icon color classes', () => {
+    it('should return correct class for success', () => {
+      expect(component.getIconColorClass(NotificationType.success)).toBe('icon-success');
+    });
+
+    it('should return correct class for error', () => {
+      expect(component.getIconColorClass(NotificationType.error)).toBe('icon-error');
+    });
+
+    it('should return correct class for info', () => {
+      expect(component.getIconColorClass(NotificationType.info)).toBe('icon-info');
+    });
+
+    it('should return correct class for warning', () => {
+      expect(component.getIconColorClass(NotificationType.warning)).toBe('icon-warning');
+    });
+
+    it('should return empty string for unknown type', () => {
+      expect(component.getIconColorClass(-1)).toBe('');
+    });
+  });
+
+  describe('date formatting', () => {
+    it('should format today\'s date as "Today"', () => {
+      const today = new Date().toISOString();
+      expect(component.formatDate(today)).toBe('Today');
+    });
+
+    it('should format yesterday\'s date as "Yesterday"', () => {
+      const yesterday = new Date(Date.now() - 86400000).toISOString();
+      expect(component.formatDate(yesterday)).toBe('Yesterday');
+    });
+
+    it('should format older dates in short format', () => {
+      const oldDate = new Date('2023-01-15').toISOString();
+      expect(component.formatDate(oldDate)).toMatch(/[A-Z][a-z]{2} \d{1,2}/);
+    });
+  });
+
+  describe('time formatting', () => {
+    it('should format time in 12-hour format', () => {
+      const date = new Date('2023-01-15T15:30:00').toISOString();
+      const formattedTime = component.formatTime(date);
+      expect(formattedTime).toMatch(/\d{1,2}:\d{2} [AP]M/);
+    });
+  });
+
+  it('should unsubscribe on destroy', () => {
+    const unsubscribeSpy = spyOn(component['sub'], 'unsubscribe');
+    component.ngOnDestroy();
+    expect(unsubscribeSpy).toHaveBeenCalled();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notifications-page/notifications-page.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notifications-page/notifications-page.component.ts
new file mode 100644 (file)
index 0000000..dd5ef35
--- /dev/null
@@ -0,0 +1,163 @@
+import { Component, OnInit, OnDestroy, AfterViewInit } from '@angular/core';
+import { Subscription } from 'rxjs';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { CdNotification } from '~/app/shared/models/cd-notification';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { PrometheusAlertService } from '~/app/shared/services/prometheus-alert.service';
+import { PrometheusNotificationService } from '~/app/shared/services/prometheus-notification.service';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+
+@Component({
+  selector: 'cd-notifications-page',
+  templateUrl: './notifications-page.component.html',
+  styleUrls: ['./notifications-page.component.scss']
+})
+export class NotificationsPageComponent implements OnInit, OnDestroy, AfterViewInit {
+  notifications: CdNotification[] = [];
+  selectedNotification: CdNotification | null = null;
+  searchText: string = '';
+  filteredNotifications: CdNotification[] = [];
+  private sub: Subscription;
+  private interval: number;
+
+  constructor(
+    private notificationService: NotificationService,
+    private prometheusAlertService: PrometheusAlertService,
+    private prometheusNotificationService: PrometheusNotificationService,
+    private authStorageService: AuthStorageService
+  ) {}
+
+  ngOnInit(): void {
+    // Check permissions and trigger Prometheus alerts refresh
+    const permissions = this.authStorageService.getPermissions();
+    if (permissions.prometheus.read && permissions.configOpt.read) {
+      this.triggerPrometheusAlerts();
+      // Set up periodic refresh similar to sidebar component
+      this.interval = window.setInterval(() => {
+        this.triggerPrometheusAlerts();
+      }, 5000);
+    }
+
+    // Subscribe to notifications from the service
+    this.sub = this.notificationService.data$.subscribe((notifications) => {
+      this.notifications = notifications;
+      this.filteredNotifications = notifications;
+    });
+  }
+
+  ngOnDestroy(): void {
+    if (this.sub) {
+      this.sub.unsubscribe();
+    }
+    if (this.interval) {
+      window.clearInterval(this.interval);
+    }
+  }
+
+  onNotificationSelect(notification: CdNotification): void {
+    this.selectedNotification = notification;
+  }
+
+  onSearch(value: string): void {
+    this.searchText = value;
+    if (!value || value.trim() === '') {
+      this.filteredNotifications = this.notifications;
+    } else {
+      const searchLower = value.toLowerCase();
+      this.filteredNotifications = this.notifications.filter(
+        (notification) =>
+          notification.title?.toLowerCase().includes(searchLower) ||
+          notification.message?.toLowerCase().includes(searchLower) ||
+          notification.application?.toLowerCase().includes(searchLower)
+      );
+    }
+  }
+
+  removeNotification(notification: CdNotification, event: MouseEvent): void {
+    // 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);
+
+      // Clear selection if the removed notification was selected
+      if (this.selectedNotification === notification) {
+        this.selectedNotification = null;
+      }
+    }
+  }
+
+  getCarbonIcon(type: NotificationType): string {
+    switch (type) {
+      case NotificationType.success:
+        return 'checkmark--filled';
+      case NotificationType.error:
+        return 'error--filled';
+      case NotificationType.info:
+        return 'information--filled';
+      case NotificationType.warning:
+        return 'warning--filled';
+      default:
+        return 'notification--filled';
+    }
+  }
+
+  getIconColorClass(type: NotificationType): string {
+    switch (type) {
+      case NotificationType.success:
+        return 'icon-success';
+      case NotificationType.error:
+        return 'icon-error';
+      case NotificationType.info:
+        return 'icon-info';
+      case NotificationType.warning:
+        return 'icon-warning';
+      default:
+        return '';
+    }
+  }
+
+  formatDate(timestamp: string): string {
+    const date = new Date(timestamp);
+    const today = new Date();
+    const yesterday = new Date(today);
+    yesterday.setDate(yesterday.getDate() - 1);
+
+    if (date.toDateString() === today.toDateString()) {
+      return 'Today';
+    } else if (date.toDateString() === yesterday.toDateString()) {
+      return 'Yesterday';
+    } else {
+      return date.toLocaleDateString('en-US', {
+        month: 'short',
+        day: 'numeric'
+      });
+    }
+  }
+
+  formatTime(timestamp: string): string {
+    const date = new Date(timestamp);
+    return date.toLocaleTimeString('en-US', {
+      hour: 'numeric',
+      minute: '2-digit',
+      hour12: true
+    });
+  }
+
+  private triggerPrometheusAlerts(): void {
+    this.prometheusAlertService.refresh();
+    this.prometheusNotificationService.refresh();
+  }
+
+  ngAfterViewInit(): void {
+    this.sub.add(this.notificationService.data$.subscribe(() => {}));
+  }
+}
index 8daf9176b309a9ae6a512a0ccb27d528b15f25cc..77eaddcb63279346f2c8c2aa6e92eaa18eb48869 100644 (file)
@@ -4,8 +4,13 @@
    (click)="togglePanel($event)">
   <div class="notification-icon-wrapper">
     <cd-icon type="notification"
+             *ngIf="!isMuted"
              size="20">
     </cd-icon>
+    <svg *ngIf="isMuted"
+         cdsIcon="notification--off"
+         size="20"
+         title="Notifications muted"></svg>
     <span class="notification-count"
           *ngIf="notificationCount > 0">
       {{ notificationCount }}
@@ -14,6 +19,6 @@
   <span class="d-md-none"
         i18n>Tasks and Notifications</span>
 </a>
-
+<s-v><main></main></s-v>
 <cd-notification-panel *ngIf="isPanelOpen && useNewPanel"></cd-notification-panel>
 <cd-notifications-sidebar *ngIf="isPanelOpen && !useNewPanel"></cd-notifications-sidebar>
index 999ea22523b2225631a45c7aaccb537e56029432..2ef1e08d03243c3a47e713196abfe544ad65b34d 100644 (file)
@@ -40,8 +40,8 @@ a {
 
 .notification-count {
   position: absolute;
-  top: spacing.$spacing-01;
-  right: spacing.$spacing-01;
+  top: spacing.$spacing-02;
+  right: spacing.$spacing-02;
   min-width: spacing.$spacing-04;
   height: spacing.$spacing-04;
   padding: 0 spacing.$spacing-01;
index 174531dff23fae750c636453db036b724827f6a5..bbeff3ed75497013d8581e0b32b4f2fbbe90f22c 100644 (file)
@@ -20,6 +20,7 @@ export class NotificationsComponent implements OnInit, OnDestroy {
   isPanelOpen = false;
   useNewPanel = true;
   notificationCount = 0;
+  isMuted = false;
   private subs = new Subscription();
 
   constructor(
@@ -47,6 +48,11 @@ export class NotificationsComponent implements OnInit, OnDestroy {
         this.useNewPanel = state.useNewPanel;
       })
     );
+    this.subs.add(
+      this.notificationService.muteState$.subscribe((isMuted) => {
+        this.isMuted = isMuted;
+      })
+    );
   }
 
   togglePanel(event: Event) {
index 947c80aece04432011a3c82f275a029bee6d3798..9148cb5d0c4b1bf619b09c5b4c02faee431e24d1 100644 (file)
@@ -100,7 +100,11 @@ import { TearsheetComponent } from './tearsheet/tearsheet.component';
 import InfoIcon from '@carbon/icons/es/information/16';
 import CopyIcon from '@carbon/icons/es/copy/32';
 import downloadIcon from '@carbon/icons/es/download/16';
-import IdeaIcon from '@carbon/icons/es/idea/20';
+import CheckmarkFilledIcon from '@carbon/icons/es/checkmark--filled/16';
+import ErrorFilledIcon from '@carbon/icons/es/error--filled/16';
+import InformationFilledIcon from '@carbon/icons/es/information--filled/16';
+import WarningFilledIcon from '@carbon/icons/es/warning--filled/16';
+import NotificationFilledIcon from '@carbon/icons/es/notification--filled/16';
 import CloseIcon from '@carbon/icons/es/close/16';
 import { TearsheetStepComponent } from './tearsheet-step/tearsheet-step.component';
 import { ProductiveCardComponent } from './productive-card/productive-card.component';
@@ -257,7 +261,11 @@ export class ComponentsModule {
       EditIcon,
       CodeIcon,
       downloadIcon,
-      IdeaIcon,
+      CheckmarkFilledIcon,
+      ErrorFilledIcon,
+      InformationFilledIcon,
+      WarningFilledIcon,
+      NotificationFilledIcon,
       CloseIcon
     ]);
   }
index b555494b1926753ccfd600a288d8be4cb453b8f3..a5f6ad36353626bc7f82f7ae706cdc38b8e053d2 100644 (file)
@@ -13,7 +13,19 @@ export class CdNotificationConfig {
     caption: ''
   };
 
-  private classes = {
+  // Prometheus-specific metadata
+  prometheusAlert?: {
+    alertName: string;
+    status: string;
+    severity: string;
+    instance?: string;
+    job?: string;
+    description: string;
+    sourceUrl?: string;
+    fingerprint?: string;
+  };
+
+  private classes: { [key: string]: string } = {
     Ceph: 'ceph-icon',
     Prometheus: 'prometheus-icon'
   };
@@ -48,6 +60,12 @@ export class CdNotification extends CdNotificationConfig {
 
   constructor(private config: CdNotificationConfig = new CdNotificationConfig()) {
     super(config.type, config.title, config.message, config.options, config.application);
+
+    // Copy Prometheus metadata if present
+    if (config.prometheusAlert) {
+      this.prometheusAlert = config.prometheusAlert;
+    }
+
     delete this.config;
     /* string representation of the Date object so it can be directly compared
     with the timestamps parsed from localStorage */
index a27f4c741781a481c80dd1c95c7ec74da91d7d1f..6f811f16361f479897b3ce7008d4ef3b96e0cb7b 100644 (file)
@@ -91,7 +91,8 @@ export class PrometheusCustomAlert {
   url: string;
   description: string;
   fingerprint?: string | boolean;
-  severity?: string;
+  labels?: PrometheusAlertLabels;
+  annotations?: Annotations;
 }
 
 export const AlertState = {
index 658127aa024c6413921e7fdfe82d042134e10f33..9724536246071511b706a12a541ca5ccdae7c618 100644 (file)
@@ -35,7 +35,9 @@ export class PrometheusAlertFormatter {
           url: alert.generatorURL,
           description: alert.annotations.description,
           fingerprint: _.isObject(alert.status) && (alert as AlertmanagerAlert).fingerprint,
-          severity: alert.labels.severity
+          // Store additional metadata for later use
+          labels: alert.labels,
+          annotations: alert.annotations
         };
       }),
       _.isEqual
@@ -51,29 +53,55 @@ export class PrometheusAlertFormatter {
   }
 
   convertAlertToNotification(alert: PrometheusCustomAlert): CdNotificationConfig {
-    return new CdNotificationConfig(
-      this.formatType(alert.status, alert.severity),
+    const config = new CdNotificationConfig(
+      this.formatType(alert.status),
       `${alert.name} (${alert.status})`,
       this.appendSourceLink(alert, alert.description),
       undefined,
       'Prometheus'
     );
-  }
 
-  private formatType(status: string, severity?: string): NotificationType {
-    if (status === 'active' && severity === 'warning') {
-      return NotificationType.warning;
-    }
+    // Add Prometheus-specific metadata
+    config['prometheusAlert'] = {
+      alertName: alert.name,
+      status: alert.status,
+      severity: alert.labels?.severity || this.mapStatusToSeverity(alert.status),
+      instance: alert.labels?.instance,
+      job: alert.labels?.job,
+      description: alert.description,
+      sourceUrl: alert.url,
+      fingerprint: alert.fingerprint ? String(alert.fingerprint) : undefined
+    };
+
+    return config;
+  }
 
+  private formatType(status: string): any {
     const types = {
       error: ['firing', 'active'],
       info: ['suppressed', 'unprocessed'],
       success: ['resolved']
     };
-    return NotificationType[_.findKey(types, (type) => type.includes(status))];
+    return NotificationType[_.findKey(types, (type: any) => type.includes(status))];
   }
 
   private appendSourceLink(alert: PrometheusCustomAlert, message: string): string {
     return `${message} <a href="${alert.url}" target="_blank"><svg cdsIcon="${Icons.lineChart}" size="${Icons.size16}" ></svg></a>`;
   }
+
+  private mapStatusToSeverity(status: string): string {
+    switch (status) {
+      case 'active':
+      case 'firing':
+        return 'critical';
+      case 'resolved':
+        return 'resolved';
+      case 'suppressed':
+        return 'suppressed';
+      case 'unprocessed':
+        return 'warning';
+      default:
+        return 'unknown';
+    }
+  }
 }