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 {
{ path: 'overview', component: DashboardComponent },
{ path: 'error', component: ErrorComponent },
+ // Notifications
+ {
+ path: 'notifications',
+ data: {
+ breadcrumbs: 'Cluster/Notifications'
+ },
+ component: NotificationsPageComponent
+ },
// Cluster
{
path: 'expand-cluster',
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';
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';
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: [
DialogModule,
GridModule,
BreadcrumbModule,
- ModalModule,
ToggleModule,
ButtonModule,
PlaceholderModule,
TagModule,
- ProgressBarModule
+ ProgressBarModule,
+ StructuredListModule,
+ SearchModule
],
declarations: [
AboutComponent,
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 {
SettingsIcon,
HelpIcon,
NotificationIcon,
+ NotificationOffIcon,
LaunchIcon,
DashboardIcon,
ClusterIcon,
ObservabilityIcon,
AdminIcon,
LockedIcon,
- LogoutIcon
+ LogoutIcon,
+ CheckmarkFilledIcon,
+ ErrorFilledIcon,
+ InformationFilledIcon,
+ WarningFilledIcon,
+ NotificationFilledIcon,
+ CloseIcon
]);
}
}
+++ /dev/null
-<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>
+++ /dev/null
-@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;
- }
- }
- }
- }
-}
+++ /dev/null
-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();
- });
-});
+++ /dev/null
-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);
- }
-}
}
}
+.notification-icon {
+ flex-shrink: 0;
+ margin-top: 0;
+}
+
.notification-content {
flex: 1;
min-width: 0;
--- /dev/null
+<div class="notification-footer">
+ <cds-button
+ kind="ghost"
+ size="sm"
+ [routerLink]="['/notifications']"
+ (click)="closePanel($event)"
+ i18n>View all</cds-button>
+</div>
--- /dev/null
+@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;
+ }
+ }
+}
--- /dev/null
+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');
+ });
+});
--- /dev/null
+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);
+ }
+}
--- /dev/null
+<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>
--- /dev/null
+@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;
+ }
+ }
+ }
+ }
+}
--- /dev/null
+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();
+ });
+});
--- /dev/null
+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);
+ }
+}
+++ /dev/null
-<div class="notification-panel">
- <cd-notification-header></cd-notification-header>
- <cd-notification-area></cd-notification-area>
-</div>
+++ /dev/null
-@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
- }
-}
+++ /dev/null
-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();
- });
- });
-});
+++ /dev/null
-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);
- }
- }
-}
--- /dev/null
+<div class="notification-panel">
+ <cd-notification-header></cd-notification-header>
+ <cd-notification-area></cd-notification-area>
+ <cd-notification-footer></cd-notification-footer>
+</div>
--- /dev/null
+@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
+ }
+}
--- /dev/null
+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();
+ });
+ });
+});
--- /dev/null
+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);
+ }
+ }
+}
--- /dev/null
+<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>
--- /dev/null
+// 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;
+ }
+ }
+}
--- /dev/null
+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();
+ });
+});
--- /dev/null
+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(() => {}));
+ }
+}
(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 }}
<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>
.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;
isPanelOpen = false;
useNewPanel = true;
notificationCount = 0;
+ isMuted = false;
private subs = new Subscription();
constructor(
this.useNewPanel = state.useNewPanel;
})
);
+ this.subs.add(
+ this.notificationService.muteState$.subscribe((isMuted) => {
+ this.isMuted = isMuted;
+ })
+ );
}
togglePanel(event: Event) {
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';
EditIcon,
CodeIcon,
downloadIcon,
- IdeaIcon,
+ CheckmarkFilledIcon,
+ ErrorFilledIcon,
+ InformationFilledIcon,
+ WarningFilledIcon,
+ NotificationFilledIcon,
CloseIcon
]);
}
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'
};
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 */
url: string;
description: string;
fingerprint?: string | boolean;
- severity?: string;
+ labels?: PrometheusAlertLabels;
+ annotations?: Annotations;
}
export const AlertState = {
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
}
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';
+ }
+ }
}