From: Devika Babrekar Date: Fri, 6 Mar 2026 07:58:45 +0000 (+0530) Subject: mgr/dashboard: Fixing message when prometheus is disabled in performance charts X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=refs%2Fpull%2F67690%2Fhead;p=ceph.git mgr/dashboard: Fixing message when prometheus is disabled in performance charts Fixes: https://tracker.ceph.com/issues/75322 Signed-off-by: Devika Babrekar --- diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.spec.ts index ac36e65d5ef..e852e8cc9c3 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.spec.ts @@ -32,7 +32,8 @@ describe('OverviewComponent', () => { }; const mockMgrModuleService = { - getConfig: jest.fn(() => of({ hw_monitoring: false })) + getConfig: jest.fn(() => of({ hw_monitoring: false })), + list: jest.fn(() => of([])) }; const mockHardwareService = { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/performance-card/performance-card.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/performance-card/performance-card.component.html index 0f61a6dc3a6..2f207ea7510 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/performance-card/performance-card.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/performance-card/performance-card.component.html @@ -4,7 +4,7 @@ i18n-headerTitle [applyShadow]="false" > - @if(chartDataLengthSignal() > 0) { + @if(emptyStateKey().length === 0) {

Performance

@@ -71,7 +71,7 @@ } - @if(chartDataLengthSignal() === 0) { + @if(emptyStateKey().length > 0) {
- You must have storage configured to access this capability. + {{ emptyStateText[emptyStateKey()] }}
} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/performance-card/performance-card.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/performance-card/performance-card.component.spec.ts index a26fbae9824..1247e154c0c 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/performance-card/performance-card.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/performance-card/performance-card.component.spec.ts @@ -1,22 +1,195 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed, fakeAsync, tick, flush } from '@angular/core/testing'; import { PerformanceCardComponent } from './performance-card.component'; import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { of } from 'rxjs'; +import { PrometheusService } from '../../api/prometheus.service'; +import { PerformanceCardService } from '../../api/performance-card.service'; +import { MgrModuleService } from '../../api/mgr-module.service'; +import { StorageType, PerformanceData } from '../../models/performance-data'; +import { DatePipe } from '@angular/common'; +import { NumberFormatterService } from '../../services/number-formatter.service'; describe('PerformanceCardComponent', () => { let component: PerformanceCardComponent; let fixture: ComponentFixture; + let prometheusService: PrometheusService; + let performanceCardService: PerformanceCardService; + let mgrModuleService: MgrModuleService; + + const mockChartData: PerformanceData = { + iops: [{ timestamp: new Date(), values: { 'Read IOPS': 100, 'Write IOPS': 50 } }], + latency: [{ timestamp: new Date(), values: { 'Read Latency': 1.5, 'Write Latency': 2.5 } }], + throughput: [ + { timestamp: new Date(), values: { 'Read Throughput': 1000, 'Write Throughput': 500 } } + ] + }; + + const mockMgrModules = [ + { name: 'prometheus', enabled: true }, + { name: 'other', enabled: false } + ]; beforeEach(async () => { + const prometheusServiceMock = { + lastHourDateObject: { start: 1000, end: 2000, step: 14 }, + ifPrometheusConfigured: jest.fn((fn) => fn()) + }; + + const performanceCardServiceMock = { + getChartData: jest.fn().mockReturnValue(of(mockChartData)) + }; + + const mgrModuleServiceMock = { + list: jest.fn().mockReturnValue(of(mockMgrModules)) + }; + + const numberFormatterMock = { + formatFromTo: jest.fn().mockReturnValue('1.00'), + bytesPerSecondLabels: [ + 'B/s', + 'KiB/s', + 'MiB/s', + 'GiB/s', + 'TiB/s', + 'PiB/s', + 'EiB/s', + 'ZiB/s', + 'YiB/s' + ], + bytesLabels: ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'YiB'], + unitlessLabels: ['', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'] + }; + + const datePipeMock = { + transform: jest.fn().mockReturnValue('01 Jan, 00:00:00') + }; + await TestBed.configureTestingModule({ - imports: [HttpClientTestingModule, PerformanceCardComponent] + imports: [HttpClientTestingModule, PerformanceCardComponent], + providers: [ + { provide: PrometheusService, useValue: prometheusServiceMock }, + { provide: PerformanceCardService, useValue: performanceCardServiceMock }, + { provide: MgrModuleService, useValue: mgrModuleServiceMock }, + { provide: NumberFormatterService, useValue: numberFormatterMock }, + { provide: DatePipe, useValue: datePipeMock } + ] }).compileComponents(); fixture = TestBed.createComponent(PerformanceCardComponent); component = fixture.componentInstance; - fixture.detectChanges(); + prometheusService = TestBed.inject(PrometheusService); + performanceCardService = TestBed.inject(PerformanceCardService); + mgrModuleService = TestBed.inject(MgrModuleService); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should initialize list signal from mgrModuleService', fakeAsync(() => { + tick(); + expect(mgrModuleService.list).toHaveBeenCalled(); + expect(component.list()).toEqual(mockMgrModules); + flush(); + })); + + it('should call loadCharts on ngOnInit', () => { + const loadChartsSpy = jest.spyOn(component, 'loadCharts'); + component.ngOnInit(); + expect(loadChartsSpy).toHaveBeenCalledWith(component.time); + }); + + it('should load charts and update chartDataSignal', fakeAsync(() => { + const time = { start: 1000, end: 2000, step: 14 }; + component.loadCharts(time); + + expect(component.time).toEqual(time); + expect(performanceCardService.getChartData).toHaveBeenCalledWith( + time, + component.selectedStorageType + ); + + tick(); + expect(component.chartDataSignal()).toEqual(mockChartData); + })); + + it('should set emptyStateKey when prometheus is enabled', fakeAsync(() => { + const time = { start: 1000, end: 2000, step: 14 }; + component.loadCharts(time); + + tick(); + expect(mgrModuleService.list).toHaveBeenCalled(); + expect(component.emptyStateKey()).toBe(''); + })); + + it('should set emptyStateKey to prometheusDisabled when prometheus module is disabled', fakeAsync(async () => { + const mockMgrModulesDisabled = [ + { name: 'prometheus', enabled: false }, + { name: 'other', enabled: true } + ]; + (mgrModuleService.list as jest.Mock).mockReturnValue(of(mockMgrModulesDisabled)); + + // Recreate component with new mock value + fixture = TestBed.createComponent(PerformanceCardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + tick(); + + const time = { start: 1000, end: 2000, step: 14 }; + component.loadCharts(time); + + tick(); + expect(mgrModuleService.list).toHaveBeenCalled(); + expect(component.emptyStateKey()).toBe('prometheusDisabled'); + })); + + it('should handle empty mgr modules list', fakeAsync(() => { + const mockMgrModulesEmpty: any[] = []; + (mgrModuleService.list as jest.Mock).mockReturnValue(of(mockMgrModulesEmpty)); + + // Recreate component with new mock value + fixture = TestBed.createComponent(PerformanceCardComponent); + component = fixture.componentInstance; + // Don't call detectChanges() as it triggers ngOnInit which calls loadCharts + // and loadCharts will crash with empty array + tick(); + + expect(mgrModuleService.list).toHaveBeenCalled(); + expect(component.list()).toEqual([]); + flush(); + })); + + it('should set emptyStateKey when prometheus is not configured', fakeAsync(() => { + (prometheusService.ifPrometheusConfigured as jest.Mock).mockImplementation((_fn, elseFn) => { + if (elseFn) { + elseFn(); + } + }); + + const time = { start: 1000, end: 2000, step: 14 }; + component.loadCharts(time); + + tick(); + expect(component.emptyStateKey()).toBe('prometheusNotAvailable'); + })); + + it('should update selectedStorageType and reload charts on storage type selection', () => { + const loadChartsSpy = jest.spyOn(component, 'loadCharts'); + const event = { item: { value: StorageType.Filesystem } }; + + component.onStorageTypeSelection(event); + + expect(component.selectedStorageType).toBe(StorageType.Filesystem); + expect(loadChartsSpy).toHaveBeenCalledWith(component.time); + }); + + it('should cleanup subscriptions on ngOnDestroy', () => { + const destroyNextSpy = jest.spyOn(component['destroy$'], 'next'); + const destroyCompleteSpy = jest.spyOn(component['destroy$'], 'complete'); + + component.ngOnDestroy(); + + expect(destroyNextSpy).toHaveBeenCalled(); + expect(destroyCompleteSpy).toHaveBeenCalled(); + }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/performance-card/performance-card.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/performance-card/performance-card.component.ts index 73416288096..e2c08a3aba7 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/performance-card/performance-card.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/performance-card/performance-card.component.ts @@ -23,6 +23,8 @@ import { ProductiveCardComponent } from '../productive-card/productive-card.comp import { CommonModule } from '@angular/common'; import { TimePickerComponent } from '../time-picker/time-picker.component'; import { AreaChartComponent } from '../area-chart/area-chart.component'; +import { MgrModuleService } from '../../api/mgr-module.service'; +import { toSignal } from '@angular/core/rxjs-interop'; @Component({ selector: 'cd-performance-card', @@ -50,6 +52,14 @@ export class PerformanceCardComponent implements OnInit, OnDestroy { metricUnitMap = METRIC_UNIT_MAP; icons = Icons; iconSize = IconSize; + emptyStateText = { + prometheusNotAvailable: $localize`You must have prometheus configured to access this capability.`, + storageNotAvailable: $localize`You must have storage configured to access this capability.`, + prometheusDisabled: $localize`You must enable prometheus to access this capability.` + }; + emptyStateKey = signal< + 'prometheusNotAvailable' | 'storageNotAvailable' | 'prometheusDisabled' | '' + >('prometheusNotAvailable'); private destroy$ = new Subject(); @@ -76,10 +86,13 @@ export class PerformanceCardComponent implements OnInit, OnDestroy { private prometheusService = inject(PrometheusService); private performanceCardService = inject(PerformanceCardService); + private mgrModuleService = inject(MgrModuleService); time = { ...this.prometheusService.lastHourDateObject }; private chartSub?: Subscription; + readonly list = toSignal(this.mgrModuleService.list(), { initialValue: [] }); + ngOnInit() { this.loadCharts(this.time); } @@ -94,6 +107,22 @@ export class PerformanceCardComponent implements OnInit, OnDestroy { .pipe(takeUntil(this.destroy$)) .subscribe((data) => { this.chartDataSignal.set(data); + this.prometheusService.ifPrometheusConfigured( + () => { + let enabled$ = this.list().filter((a) => a.name === 'prometheus')[0].enabled; + if (enabled$) { + this.chartDataSignal.set(data); + this.emptyStateKey.set(''); + } else if (!enabled$) { + this.emptyStateKey.set('prometheusDisabled'); + } else { + this.emptyStateKey.set('storageNotAvailable'); + } + }, + () => { + this.emptyStateKey.set('prometheusNotAvailable'); + } + ); }); }