]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Fixing message when prometheus is disabled in performance charts 67690/head
authorDevika Babrekar <devika.babrekar@ibm.com>
Fri, 6 Mar 2026 07:58:45 +0000 (13:28 +0530)
committerDevika Babrekar <devika.babrekar@ibm.com>
Mon, 16 Mar 2026 05:59:20 +0000 (11:29 +0530)
Fixes: https://tracker.ceph.com/issues/75322
Signed-off-by: Devika Babrekar <devika.babrekar@ibm.com>
src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/performance-card/performance-card.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/components/performance-card/performance-card.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/performance-card/performance-card.component.ts

index ac36e65d5ef23d338a52aafb71160887f5def81c..e852e8cc9c3df691bcd505b0c18a0fe71dc7fe56 100644 (file)
@@ -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 = {
index 0f61a6dc3a60f4f7e8cf1b0c17aceb071cd9ed46..2f207ea75106b1773e3ed86858c769b0f1a62177 100644 (file)
@@ -4,7 +4,7 @@
     i18n-headerTitle
     [applyShadow]="false"
   >
-    @if(chartDataLengthSignal() > 0) {
+    @if(emptyStateKey().length === 0) {
     <ng-template #header>
       <h2 class="cds--type-heading-compact-02"
           i18n>Performance</h2>
@@ -71,7 +71,7 @@
     </div>
     }
 
-    @if(chartDataLengthSignal() === 0) {
+    @if(emptyStateKey().length > 0) {
     <div class="performance-card-empty-msg">
       <div class="performance-card-empty-msg-icon">
         <img src="assets/locked.png"
@@ -79,7 +79,7 @@
       </div>
       <span class="cds--type-label-01"
             i18n>
-        You must have storage configured to access this capability.
+        {{ emptyStateText[emptyStateKey()] }}
       </span>
     </div>
     }
index a26fbae9824f0d5ceab32c40d34cd6d12796c138..1247e154c0c2bb69e7d0165504f95aa4c5245b14 100644 (file)
-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<PerformanceCardComponent>;
+  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();
+  });
 });
index 734162880966cec14e37525535072e5e551b1160..e2c08a3aba7f0417aff0e0bd6734ef9a7ed70300 100644 (file)
@@ -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<void>();
 
@@ -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');
+          }
+        );
       });
   }