From: Afreen Misbah Date: Tue, 16 Dec 2025 14:09:32 +0000 (+0530) Subject: mgr/dashboard: Added header action for tasks and notifications X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=68226642963c26b37ccc93055015e105f9d71d60;p=ceph.git mgr/dashboard: Added header action for tasks and notifications - removed the custom css and using carbon component - fixed unit tests of notification page Signed-off-by: Afreen Misbah --- diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation.module.ts index 089f47a16f2c..9b642e8f35fe 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation.module.ts @@ -42,6 +42,7 @@ 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 NotificationNewIcon from '@carbon/icons/es/notification--new/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'; @@ -58,6 +59,7 @@ 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'; @@ -139,6 +141,7 @@ export class NavigationModule { InformationFilledIcon, WarningFilledIcon, NotificationFilledIcon, + NotificationNewIcon, CloseIcon ]); } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html index 3b024375066d..20f4d50b5932 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html @@ -1,8 +1,5 @@
- - - - + @@ -28,15 +25,15 @@ - -
- -
+ + +
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.ts index 612ef52ddc37..2864776c94c6 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.ts @@ -33,7 +33,7 @@ export class NavigationComponent implements OnInit, OnDestroy { clusterTokenStatus: object = {}; summaryData: any; - rightSidebarOpen = false; // rightSidebar only opens when width is less than 768px + isNotifPanelOpen = false; showMenuSidebar = true; simplebar = { @@ -161,8 +161,8 @@ export class NavigationComponent implements OnInit, OnDestroy { this.displayedSubMenu[menu] = !this.displayedSubMenu[menu]; } - toggleRightSidebar() { - this.rightSidebarOpen = !this.rightSidebarOpen; + toggleSidebar() { + this.isNotifPanelOpen = !this.isNotifPanelOpen; } onClusterSelection(value: object) { @@ -210,9 +210,7 @@ export class NavigationComponent implements OnInit, OnDestroy { } ); } - toggleSidebar() { - this.notificationService.toggleSidebar(true, true); - } + trackByFn(item: any) { return item; } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/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 index 65d51d6af8ff..608a4ebff346 100644 --- 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 @@ -1,5 +1,11 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { NotificationFooterComponent } from './notification-footer.component'; +import { NotificationService } from '~/app/shared/services/notification.service'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; + +class MockNotificationService { + toggleSidebar = () => {}; +} describe('NotificationFooterComponent', () => { let component: NotificationFooterComponent; @@ -7,7 +13,9 @@ describe('NotificationFooterComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [NotificationFooterComponent] + declarations: [NotificationFooterComponent], + providers: [{ provide: NotificationService, useClass: MockNotificationService }], + schemas: [CUSTOM_ELEMENTS_SCHEMA] }).compileComponents(); }); 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 index 5dcf0d39c367..f26660ef3aaf 100644 --- 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 @@ -4,7 +4,8 @@ import { NotificationService } from '~/app/shared/services/notification.service' @Component({ selector: 'cd-notification-footer', templateUrl: './notification-footer.component.html', - styleUrls: ['./notification-footer.component.scss'] + styleUrls: ['./notification-footer.component.scss'], + standalone: false }) export class NotificationFooterComponent { constructor(public notificationService: NotificationService) {} 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 index e2df0faf7c40..67bc2924686c 100644 --- 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 @@ -1,6 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { NotificationPanelComponent } from './notification-panel.component'; -import { NotificationService } from '../../../shared/services/notification.service'; +import { NotificationService } from '~/app/shared/services/notification.service'; describe('NotificationPanelComponent', () => { let component: NotificationPanelComponent; 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 index 1e6d3e67d935..bbc832bef4ac 100644 --- 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 @@ -1,4 +1,4 @@ -

No notifications match your search

No notifications available

@@ -70,7 +70,7 @@
-
{ let component: NotificationsPageComponent; let fixture: ComponentFixture; - let notificationService: NotificationService; let mockNotifications: CdNotification[]; let dataSourceSubject: BehaviorSubject; + let notificationService: any; - // Mock notification service + // Create mocks const createMockNotificationService = () => { dataSourceSubject = new BehaviorSubject([]); return { data$: dataSourceSubject.asObservable(), - dataSource: dataSourceSubject, + dataSource: { + getValue: () => dataSourceSubject.getValue(), + next: (value: CdNotification[]) => dataSourceSubject.next(value) + }, remove: jasmine.createSpy('remove') }; }; + const mockPrometheusAlertService = { + refresh: jasmine.createSpy('refresh') + }; + + const mockPrometheusNotificationService = { + refresh: jasmine.createSpy('refresh') + }; + + const mockAuthStorageService = { + getPermissions: jasmine.createSpy('getPermissions').and.returnValue({ + prometheus: { read: false }, + configOpt: { read: false } + }) + }; + + const mockChangeDetectorRef = { + detectChanges: jasmine.createSpy('detectChanges') + }; + + // Create mock notifications + const createMockNotification = (overrides: any): CdNotification => { + return { + title: overrides.title || '', + message: overrides.message || '', + application: overrides.application || '', + timestamp: overrides.timestamp || new Date().toISOString(), + type: overrides.type || NotificationType.info, + priority: 'normal', + textClass: '', + iconClass: '', + duration: 0, + borderClass: '', + timeout: 0, + id: '', + isError: false, + isFinishedTask: false, + progress: 0, + progressText: '', + task: undefined, + error: undefined, + isSilent: false, + silentNotifications: [], + userData: undefined, + alertSilenced: false, + ...overrides + } as CdNotification; + }; + beforeEach(async () => { mockNotifications = [ - { + createMockNotification({ title: 'Success Notification', message: 'Operation completed successfully', - timestamp: new Date().toISOString(), type: NotificationType.success, - application: 'TestApp' - }, - { + application: 'TestApp', + timestamp: new Date().toISOString() + }), + createMockNotification({ title: 'Error Notification', message: 'An error occurred', - timestamp: new Date(Date.now() - 86400000).toISOString(), // Yesterday type: NotificationType.error, - application: 'TestApp' - }, - { + application: 'TestApp', + timestamp: new Date(Date.now() - 86400000).toISOString() + }), + createMockNotification({ title: 'Info Notification', message: 'System update available', - timestamp: new Date(Date.now() - 172800000).toISOString(), // 2 days ago type: NotificationType.info, - application: 'Updates' - } + application: 'Updates', + timestamp: new Date(Date.now() - 172800000).toISOString() + }) ]; + const mockNotificationService = createMockNotificationService(); + notificationService = mockNotificationService; // Store reference + await TestBed.configureTestingModule({ - imports: [ - FormsModule, - SharedModule, - IconModule, - SearchModule, - StructuredListModule, - TagModule - ], + imports: [FormsModule, GridModule, IconModule, SearchModule, StructuredListModule, TagModule], declarations: [NotificationsPageComponent], - providers: [{ provide: NotificationService, useFactory: createMockNotificationService }] + providers: [ + { provide: NotificationService, useValue: mockNotificationService }, + { provide: PrometheusAlertService, useValue: mockPrometheusAlertService }, + { provide: PrometheusNotificationService, useValue: mockPrometheusNotificationService }, + { provide: AuthStorageService, useValue: mockAuthStorageService }, + { provide: ChangeDetectorRef, useValue: mockChangeDetectorRef } + ] }).compileComponents(); - - notificationService = TestBed.inject(NotificationService); }); beforeEach(() => { fixture = TestBed.createComponent(NotificationsPageComponent); component = fixture.componentInstance; + + // Update the data source with mock notifications BEFORE ngOnInit dataSourceSubject.next(mockNotifications); + + // Initialize the component fixture.detectChanges(); }); + afterEach(() => { + if (component['interval']) { + window.clearInterval(component['interval']); + } + }); + it('should create', () => { expect(component).toBeTruthy(); }); @@ -133,15 +198,11 @@ describe('NotificationsPageComponent', () => { 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 + expect(notificationService.remove).toHaveBeenCalledWith(0); // FIXED: use notificationService }); it('should clear selection if removed notification was selected', () => { @@ -152,10 +213,6 @@ describe('NotificationsPageComponent', () => { 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(); @@ -231,9 +288,33 @@ describe('NotificationsPageComponent', () => { }); }); + it('should set up interval for Prometheus alerts when permissions exist', () => { + mockAuthStorageService.getPermissions.and.returnValue({ + prometheus: { read: true }, + configOpt: { read: true } + }); + + // Re-initialize component to trigger ngOnInit with new permissions + fixture = TestBed.createComponent(NotificationsPageComponent); + component = fixture.componentInstance; + dataSourceSubject.next(mockNotifications); + fixture.detectChanges(); + + expect(component['interval']).toBeDefined(); + }); + it('should unsubscribe on destroy', () => { + component['sub'] = new Subscription(); const unsubscribeSpy = spyOn(component['sub'], 'unsubscribe'); + + if (!component['interval']) { + component['interval'] = window.setInterval(() => {}, 5000); + } + const clearIntervalSpy = spyOn(window, 'clearInterval'); + component.ngOnDestroy(); + expect(unsubscribeSpy).toHaveBeenCalled(); + expect(clearIntervalSpy).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 index f507941a2147..d8952ec0fe42 100644 --- 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 @@ -1,4 +1,11 @@ -import { Component, OnInit, OnDestroy, AfterViewInit, ChangeDetectorRef, AfterViewChecked } from '@angular/core'; +import { + Component, + OnInit, + OnDestroy, + AfterViewInit, + ChangeDetectorRef, + AfterViewChecked +} from '@angular/core'; import { Subscription } from 'rxjs'; import { NotificationService } from '~/app/shared/services/notification.service'; import { CdNotification } from '~/app/shared/models/cd-notification'; @@ -10,9 +17,11 @@ import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; @Component({ selector: 'cd-notifications-page', templateUrl: './notifications-page.component.html', - styleUrls: ['./notifications-page.component.scss'] + styleUrls: ['./notifications-page.component.scss'], + standalone: false }) -export class NotificationsPageComponent implements OnInit, OnDestroy, AfterViewInit, AfterViewChecked { +export class NotificationsPageComponent + implements OnInit, OnDestroy, AfterViewInit, AfterViewChecked { notifications: CdNotification[] = []; selectedNotification: CdNotification | null = null; searchText: string = ''; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notifications/notifications.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notifications/notifications.component.html index 77eaddcb6327..da785b10266b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notifications/notifications.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notifications/notifications.component.html @@ -1,24 +1,15 @@ - -
- - - - - {{ notificationCount }} - -
- Tasks and Notifications -
-
- - +@if(isMuted) { + + +} +@else if(!isMuted && hasRunningTasks) { + + +} +@else { + + +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notifications/notifications.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notifications/notifications.component.scss index 2ef1e08d0324..e69de29bb2d1 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notifications/notifications.component.scss +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notifications/notifications.component.scss @@ -1,55 +0,0 @@ -@use './src/styles/vendor/variables' as vv; -@use '@carbon/styles/scss/spacing'; -@use '@carbon/styles/scss/theme' as *; -@use '@carbon/styles/scss/type'; - -.running i { - color: vv.$primary; -} - -.running:hover i { - color: vv.$white; -} - -a { - .dot { - background-color: vv.$primary-500; - border: 2px solid vv.$secondary; - border-radius: 50%; - height: 11px; - position: absolute; - right: 17px; - top: 10px; - width: 10px; - } - - &:hover .dot { - background-color: vv.$white; - border-color: vv.$primary-500; - } -} - -.notification-icon-wrapper { - position: relative; - display: inline-flex; - padding: spacing.$spacing-04; - cursor: pointer; - border-radius: spacing.$spacing-01; - transition: background-color 0.2s ease; -} - -.notification-count { - position: absolute; - top: spacing.$spacing-02; - right: spacing.$spacing-02; - min-width: spacing.$spacing-04; - height: spacing.$spacing-04; - padding: 0 spacing.$spacing-01; - border-radius: spacing.$spacing-02; - background-color: $support-error; - color: $text-on-color; - font-size: spacing.$spacing-04; - line-height: spacing.$spacing-04; - text-align: center; - font-weight: 500; -} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notifications/notifications.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notifications/notifications.component.ts index bbeff3ed7549..ff0a276959d3 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notifications/notifications.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notifications/notifications.component.ts @@ -1,8 +1,8 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Component, Input, OnDestroy, OnInit } from '@angular/core'; import { Subscription } from 'rxjs'; -import { Icons } from '~/app/shared/enum/icons.enum'; +import { ICON_TYPE, IconSize } from '~/app/shared/enum/icons.enum'; import { CdNotification } from '~/app/shared/models/cd-notification'; import { NotificationService } from '~/app/shared/services/notification.service'; import { SummaryService } from '~/app/shared/services/summary.service'; @@ -14,11 +14,11 @@ import { SummaryService } from '~/app/shared/services/summary.service'; standalone: false }) export class NotificationsComponent implements OnInit, OnDestroy { - icons = Icons; + @Input() isPanelOpen: boolean = false; + icons = ICON_TYPE; + iconSize = IconSize.size20; hasRunningTasks = false; hasNotifications = false; - isPanelOpen = false; - useNewPanel = true; notificationCount = 0; isMuted = false; private subs = new Subscription(); @@ -42,12 +42,6 @@ export class NotificationsComponent implements OnInit, OnDestroy { }) ); - this.subs.add( - this.notificationService.panelState$.subscribe((state) => { - this.isPanelOpen = state.isOpen; - this.useNewPanel = state.useNewPanel; - }) - ); this.subs.add( this.notificationService.muteState$.subscribe((isMuted) => { this.isMuted = isMuted; @@ -55,12 +49,6 @@ export class NotificationsComponent implements OnInit, OnDestroy { ); } - togglePanel(event: Event) { - event.preventDefault(); - event.stopPropagation(); - this.notificationService.toggleSidebar(!this.isPanelOpen, this.useNewPanel); - } - ngOnDestroy(): void { this.subs.unsubscribe(); } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/icon/icon.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/icon/icon.component.scss index 2024593ec224..7f88eb3dd0b2 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/icon/icon.component.scss +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/icon/icon.component.scss @@ -42,3 +42,7 @@ Using `color` in css and seyting svg will fill="currentColor does not work. .deploy-icon { fill: theme.$layer-selected-disabled !important; } + +.notificationNew-icon circle { + fill: theme.$support-error !important; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/icon/icon.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/icon/icon.component.ts index 5266aeae3233..5af1ff726544 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/icon/icon.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/icon/icon.component.ts @@ -1,11 +1,12 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core'; import { ICON_TYPE, Icons, IconSize } from '../../enum/icons.enum'; @Component({ selector: 'cd-icon', templateUrl: './icon.component.html', styleUrl: './icon.component.scss', - standalone: false + standalone: false, + encapsulation: ViewEncapsulation.None }) export class IconComponent implements OnInit { @Input() type!: keyof typeof ICON_TYPE; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts index 5f45acf054e6..27b1f333754b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts @@ -102,7 +102,9 @@ export enum Icons { spin = 'fa fa-spin', // To get any icon to rotate inverse = 'fa fa-inverse', // To get an alternative icon color notification = 'notification', - error = 'error--filled' + error = 'error--filled', + notificationOff = 'notification--off', + notificationNew = 'notification--new' } export enum IconSize { @@ -121,6 +123,8 @@ export const ICON_TYPE = { error: 'error--filled', infoCircle: 'info-circle', notification: 'notification', + notificationOff: 'notification--off', + notificationNew: 'notification--new', success: 'success', warning: 'warning' } as const;