]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
mgr/dashboard: Removed Raw capacity toggle
authorAfreen Misbah <afreen@ibm.com>
Wed, 18 Feb 2026 02:08:08 +0000 (07:38 +0530)
committerAfreen Misbah <afreen@ibm.com>
Thu, 19 Feb 2026 03:38:38 +0000 (09:08 +0530)
- removed raw capacity toggle
- updated tests
- added polling for promethues queries
- added tests for formatter service functions

Signed-off-by: Afreen Misbah <afreen@ibm.com>
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/storage-card/overview-storage-card.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/storage-card/overview-storage-card.component.scss
src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/storage-card/overview-storage-card.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/storage-card/overview-storage-card.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/productive-card/productive-card.component.scss
src/pybind/mgr/dashboard/frontend/src/app/shared/services/formatter.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/formatter.service.ts

index 2e74622998de3cd3e64b465c18d24ffa28501f01..e7504450d9e9e0249f8a893908b98faa9dfbaef5 100644 (file)
@@ -2,8 +2,8 @@
   <div class="row d-flex flex-row ps-3">
     <!-- First Grid to hold Details and Inventory Card-->
     <div class="col-sm-3 d-flex flex-column ps-2 details-card">
-      <cd-productive-card title="Details"
-                          i18n-title>
+      <cd-productive-card headerTitle="Details"
+                          i18n-headerTitle>
         <dl>
           <dt>Cluster ID</dt>
           <dd>{{ detailsCardData.fsid }}</dd>
index 8c234111c98a451a0b1fb930c96ec8eee769a323..246edfff161cc73c5a4505cfd81b18d87d03e2fc 100644 (file)
@@ -23,8 +23,8 @@
          [columnNumbers]="{lg: 16}">
       @if (healthData$ | async; as healthData) {
       <cd-overview-storage-card
-        [total]="healthData?.pgmap.bytes_total"
-        [used]="healthData?.pgmap.bytes_used">
+        [total]="healthData.pgmap.bytes_total"
+        [used]="healthData.pgmap.bytes_used">
       </cd-overview-storage-card>
       }
     </div>
index c76dceb4e93764dfbc5469243015a4ca30c64497..16f5afd02a460b25902e7bb7744aa00c8ad39fbb 100644 (file)
@@ -16,8 +16,6 @@ import { CommonModule } from '@angular/common';
   styleUrl: './overview.component.scss'
 })
 export class OverviewComponent implements OnDestroy {
-  totalCapacity: number;
-  usedCapacity: number;
   private destroy$ = new Subject<void>();
   public healthData$: Observable<HealthSnapshotMap>;
 
index eb528c88055144ffd8e833618998e8158d762a0e..6400faf4dd56e1d7828e5df94bb1af75445b6ecf 100644 (file)
       <cds-dropdown-list [items]="dropdownItems"></cds-dropdown-list>
     </cds-dropdown>
   </ng-template>
-  <!-- CAPACITY USAGE TEXT AND RAW CAPACITY TOGGLE-->
+  <!-- CAPACITY USAGE TEXT -->
   <div class="overview-storage-card-usage-text">
-    @if(usedRaw && totalRaw && usedRawUnit && totalRawUnit) {
+    @if(displayUsedRaw && totalRaw && usedRawUnit && totalRawUnit) {
     <h5>
       <span
         class="cds--type-heading-05"
-        i18n>{{usedRaw}}&ngsp;</span>
+        i18n>{{displayUsedRaw}}&ngsp;</span>
       <span
         class="cds--type-body-02"
         i18n>{{usedRawUnit}} of {{totalRaw}} {{totalRawUnit}} used</span>
       [maxLineWidth]="400">
     </cds-skeleton-text>
     }
-    <cds-checkbox
-      [checked]="isRawCapacity"
-      [disabled]="!(displayData && usedRaw && totalRaw && usedRawUnit && totalRawUnit)"
-      (checkedChange)="toggleRawCapacity($event)">
-      <cds-tooltip-definition
-        [autoAlign]="true"
-        [highContrast]="true"
-        [openOnHover]="false"
-        [dropShadow]="true"
-        title=""
-        [caret]="true"
-        description="Raw capacity includes all physical storage before replication or overhead."
-        i18n-description
-        i18n>
-        Raw capacity
-      </cds-tooltip-definition>
-    </cds-checkbox>
   </div>
   <!-- CAPACITY CHART -->
   @if(displayData) {
index bd0e7a1cbed1832a71450bbbce94c0027a3c47f5..1d8cdca85581b6419c789e22f05367e2f9851ac4 100644 (file)
   // Hiding the native chart title
   &-chart {
     .meter-title {
-      height: 0 !important;
       display: none !important;
     }
 
     .spacer {
-      height: 0 !important;
       display: none !important;
     }
   }
index 5a8e63011f6258fed66146ab6d823dc3150057ee..83ac658b15151287e2b72d79e44feb8ef545b2b3 100644 (file)
@@ -22,15 +22,15 @@ describe('OverviewStorageCardComponent (Jest)', () => {
     result: [
       {
         metric: { application: 'Block' },
-        value: [0, 1024]
+        value: [0, '1024']
       },
       {
         metric: { application: 'Filesystem' },
-        value: [0, 2048]
+        value: [0, '2048']
       },
       {
         metric: { application: 'Object' },
-        value: [0, 0] // should be filtered
+        value: [0, '0'] // should be filtered
       }
     ]
   };
@@ -103,31 +103,6 @@ describe('OverviewStorageCardComponent (Jest)', () => {
     expect(component.usedRaw).toBe(10);
     expect(component.usedRawUnit).toBe('GiB');
   });
-
-  // --------------------------------------------------
-  // TOGGLE
-  // --------------------------------------------------
-
-  it('should switch to RAW when toggled true', () => {
-    component.toggleRawCapacity(true);
-
-    expect(component.isRawCapacity).toBe(true);
-    expect(component.selectedCapacityType).toBe('raw');
-  });
-
-  it('should switch to USED when toggled false', () => {
-    component.toggleRawCapacity(false);
-
-    expect(component.isRawCapacity).toBe(false);
-    expect(component.selectedCapacityType).toBe('used');
-  });
-
-  it('should call Prometheus again when toggled', () => {
-    component.toggleRawCapacity(false);
-
-    expect(mockPrometheusService.getPrometheusQueryData).toHaveBeenCalledTimes(2);
-  });
-
   // --------------------------------------------------
   // ngOnInit data load
   // --------------------------------------------------
@@ -147,11 +122,9 @@ describe('OverviewStorageCardComponent (Jest)', () => {
       { group: 'Filesystem', value: 20 }
     ];
 
-    component.selectedStorageType = 'Block';
-    (component as any).setChartData();
+    component.onStorageTypeSelect({ item: { content: 'Block', selected: true } } as any);
 
-    expect(component.displayData.length).toBe(1);
-    expect(component.displayData[0].group).toBe('Block');
+    expect(component.displayData).toEqual([{ group: 'Block', value: 10 }]);
   });
 
   it('should show all data when ALL selected', () => {
@@ -160,8 +133,7 @@ describe('OverviewStorageCardComponent (Jest)', () => {
       { group: 'Filesystem', value: 20 }
     ];
 
-    component.selectedStorageType = 'All';
-    (component as any).setChartData();
+    component.onStorageTypeSelect({ item: { content: 'All', selected: true } } as any);
 
     expect(component.displayData.length).toBe(2);
   });
@@ -181,23 +153,10 @@ describe('OverviewStorageCardComponent (Jest)', () => {
   it('should auto-select single item if only one exists', () => {
     component.allData = [{ group: 'Block', value: 10 }];
 
-    (component as any).setDropdownItemsAndStorageType();
-
-    expect(component.selectedStorageType).toBe('Block');
-    expect(component.dropdownItems.length).toBe(1);
-  });
-
-  it('should reset to ALL if previous selection missing', () => {
-    component.selectedStorageType = 'Block';
-
-    component.allData = [
-      { group: 'Filesystem', value: 20 },
-      { group: 'Object', value: 30 }
-    ];
-
-    (component as any).setDropdownItemsAndStorageType();
+    (component as any)._setDropdownItemsAndStorageType();
 
     expect(component.selectedStorageType).toBe('All');
+    expect(component.dropdownItems.length).toBe(2);
   });
 
   // --------------------------------------------------
index 4c0183ae90b5fc89778e8f623e914b19d0644f86..56117d8442a02e376d0d725c18316df0c2e4f0ac 100644 (file)
@@ -2,6 +2,7 @@ import {
   ChangeDetectionStrategy,
   ChangeDetectorRef,
   Component,
+  inject,
   Input,
   OnDestroy,
   OnInit,
@@ -24,32 +25,27 @@ import {
   PromqlGuageMetric
 } from '~/app/shared/api/prometheus.service';
 import { FormatterService } from '~/app/shared/services/formatter.service';
-import { BehaviorSubject, Subject } from 'rxjs';
-import { switchMap, takeUntil } from 'rxjs/operators';
+import { interval, Subject } from 'rxjs';
+import { startWith, switchMap, takeUntil } from 'rxjs/operators';
 
 const CHART_HEIGHT = '45px';
 
+const REFRESH_INTERVAL_MS = 15_000;
+
 const StorageType = {
   ALL: $localize`All`,
   BLOCK: $localize`Block`,
-  FILE: $localize`Filesystem`,
+  FILE: $localize`File system`,
   OBJECT: $localize`Object`
 };
 
-const CapacityType = {
-  RAW: 'raw',
-  USED: 'used'
-};
-
 type ChartData = {
   group: string;
   value: number;
 };
 
-const Query = {
-  [CapacityType.RAW]: `sum by (application) (ceph_pool_bytes_used * on(pool_id) group_left(instance, name, application) ceph_pool_metadata{application=~"(.*Block.*)|(.*Filesystem.*)|(.*Object.*)|(..*)"})`,
-  [CapacityType.USED]: `sum by (application) (ceph_pool_stored * on(pool_id) group_left(instance, name, application) ceph_pool_metadata{application=~"(.*Block.*)|(.*Filesystem.*)|(.*Object.*)|(..*)"})`
-};
+const RawUsedByStorageType =
+  'sum by (application) (ceph_pool_bytes_used * on(pool_id) group_left(instance, name, application) ceph_pool_metadata{application=~"(.*Block.*)|(.*Filesystem.*)|(.*Object.*)|(..*)"})';
 
 const chartGroupLabels = [StorageType.BLOCK, StorageType.FILE, StorageType.OBJECT];
 
@@ -73,13 +69,18 @@ const chartGroupLabels = [StorageType.BLOCK, StorageType.FILE, StorageType.OBJEC
   changeDetection: ChangeDetectionStrategy.OnPush
 })
 export class OverviewStorageCardComponent implements OnInit, OnDestroy {
+  private readonly prometheusService = inject(PrometheusService);
+  private readonly formatterService = inject(FormatterService);
+  private readonly cdr = inject(ChangeDetectorRef);
+  private destroy$ = new Subject<void>();
+
   @Input()
   set total(value: number) {
     const [totalValue, totalUnit] = this.formatterService.formatToBinary(value, true);
     if (Number.isNaN(totalValue)) return;
     this.totalRaw = totalValue;
     this.totalRawUnit = totalUnit;
-    this.setTotalAndUsed();
+    this._setTotalAndUsed();
   }
   @Input()
   set used(value: number) {
@@ -87,15 +88,12 @@ export class OverviewStorageCardComponent implements OnInit, OnDestroy {
     if (Number.isNaN(usedValue)) return;
     this.usedRaw = usedValue;
     this.usedRawUnit = usedUnit;
-    this.setTotalAndUsed();
+    this._setTotalAndUsed();
   }
   totalRaw: number;
   usedRaw: number;
   totalRawUnit: string;
   usedRawUnit: string;
-  isRawCapacity: boolean = true;
-  selectedStorageType: string = StorageType.ALL;
-  selectedCapacityType: string = CapacityType.RAW;
   options: MeterChartOptions = {
     height: CHART_HEIGHT,
     meter: {
@@ -117,6 +115,8 @@ export class OverviewStorageCardComponent implements OnInit, OnDestroy {
   };
   allData: ChartData[] = null;
   displayData: ChartData[] = null;
+  displayUsedRaw: number;
+  selectedStorageType: string = StorageType.ALL;
   dropdownItems = [
     { content: StorageType.ALL },
     { content: StorageType.BLOCK },
@@ -124,16 +124,7 @@ export class OverviewStorageCardComponent implements OnInit, OnDestroy {
     { content: StorageType.OBJECT }
   ];
 
-  constructor(
-    private prometheusService: PrometheusService,
-    private formatterService: FormatterService,
-    private cdr: ChangeDetectorRef
-  ) {}
-
-  private destroy$ = new Subject<void>();
-  private capacityType$ = new BehaviorSubject<string>(CapacityType.RAW);
-
-  private setTotalAndUsed() {
+  private _setTotalAndUsed() {
     // Chart reacts to 'options' and 'data' object changes only, hence mandatory to replace whole object.
     this.options = {
       ...this.options,
@@ -149,79 +140,68 @@ export class OverviewStorageCardComponent implements OnInit, OnDestroy {
         valueFormatter: (value) => `${value.toLocaleString()} ${this.usedRawUnit}`
       }
     };
-    this.updateCard();
+    this._updateCard();
   }
 
-  private getAllData(data: PromqlGuageMetric) {
+  private _getAllData(data: PromqlGuageMetric) {
     const result = data?.result ?? [];
     const chartData = result
       .map((r: PromethuesGaugeMetricResult) => {
         const group = r?.metric?.application;
-        const value = this.formatterService.convertToUnit(r?.value?.[1], 'B', this.usedRawUnit, 10);
-        return { group, value };
+        const value = this.formatterService.convertToUnit(r?.value?.[1], 'B', this.usedRawUnit, 1);
+        return { group: group === 'Filesystem' ? StorageType.FILE : group, value };
       })
-      // Removing 0 values and legends other than Block, Filesystem, and Object.
-      .filter((r) => chartGroupLabels.includes(r.group) && r.value > 0);
+      // Removing 0 values and legends other than Block, File system, and Object.
+      .filter((r) => chartGroupLabels.includes(r?.group) && r?.value > 0);
     return chartData;
   }
 
-  private setChartData() {
+  private _setChartData() {
     if (this.selectedStorageType === StorageType.ALL) {
       this.displayData = this.allData;
+      this.displayUsedRaw = this.usedRaw;
     } else {
-      this.displayData = this.allData.filter(
+      this.displayData = this.allData?.filter(
         (d: ChartData) => d.group === this.selectedStorageType
       );
+      this.displayUsedRaw = this.displayData?.[0]?.value;
     }
   }
 
-  private setDropdownItemsAndStorageType() {
-    const dynamicItems = this.allData.map((data) => ({ content: data.group }));
-    const hasExistingItem = dynamicItems.some((item) => item.content === this.selectedStorageType);
-
-    if (dynamicItems.length === 1) {
-      this.dropdownItems = dynamicItems;
-      this.selectedStorageType = dynamicItems[0]?.content;
+  private _setDropdownItemsAndStorageType() {
+    const newData = this.allData?.map((data) => ({ content: data.group }));
+    if (newData.length) {
+      this.dropdownItems = [{ content: StorageType.ALL }, ...newData];
     } else {
-      this.dropdownItems = [{ content: StorageType.ALL }, ...dynamicItems];
-    }
-    // Change the current dropdown selection to 'ALL' if prev selection is absent in current data, and current data has more than one item.
-    if (!hasExistingItem && dynamicItems.length > 1) {
-      this.selectedStorageType = StorageType.ALL;
+      this.dropdownItems = [{ content: StorageType.ALL }];
     }
   }
 
-  private updateCard() {
+  private _updateCard() {
     this.cdr.markForCheck();
   }
 
-  public toggleRawCapacity(isChecked: boolean) {
-    this.isRawCapacity = isChecked;
-    this.selectedCapacityType = isChecked ? CapacityType.RAW : CapacityType.USED;
-    // Reloads Prometheus Query
-    this.capacityType$.next(this.selectedCapacityType);
-  }
-
   public onStorageTypeSelect(selected: { item: { content: string; selected: true } }) {
     this.selectedStorageType = selected?.item?.content;
-    this.setChartData();
+    this._setChartData();
   }
 
   ngOnInit() {
-    this.capacityType$
+    interval(REFRESH_INTERVAL_MS)
       .pipe(
-        switchMap((capacityType) =>
+        startWith(0),
+        switchMap(() =>
           this.prometheusService.getPrometheusQueryData({
-            params: Query[capacityType]
+            params: RawUsedByStorageType
           })
         ),
         takeUntil(this.destroy$)
       )
       .subscribe((data: PromqlGuageMetric) => {
-        this.allData = this.getAllData(data);
-        this.setDropdownItemsAndStorageType();
-        this.setChartData();
-        this.updateCard();
+        this.allData = this._getAllData(data);
+        this._setDropdownItemsAndStorageType();
+        this._setChartData();
+        this._updateCard();
       });
   }
 
index 4a9f1ce5297c6dc9f726923312a63e7c977197a5..dd7751b289c9abf64abd33afa0ca923f473a1386 100644 (file)
@@ -208,8 +208,7 @@ import { SidebarLayoutComponent } from './sidebar-layout/sidebar-layout.componen
     TearsheetComponent,
     TearsheetStepComponent,
     PageHeaderComponent,
-    SidebarLayoutComponent,
-    PageHeaderComponent
+    SidebarLayoutComponent
   ],
   providers: [provideCharts(withDefaultRegisterables())],
   exports: [
@@ -256,8 +255,7 @@ import { SidebarLayoutComponent } from './sidebar-layout/sidebar-layout.componen
     TearsheetComponent,
     TearsheetStepComponent,
     PageHeaderComponent,
-    SidebarLayoutComponent,
-    PageHeaderComponent
+    SidebarLayoutComponent
   ]
 })
 export class ComponentsModule {
index c5f13d9eb6fcc917456e1d85c082ebd3b05e0874..4309d1b17aebb948788f1e2946717ce84f88b47e 100644 (file)
@@ -109,4 +109,70 @@ describe('FormatterService', () => {
       convertToBytesAndBack('123.5 EiB');
     });
   });
+
+  describe('formatToBinary', () => {
+    it('should return formatted string when split=false (default decimals=1)', () => {
+      expect(service.formatToBinary('0', false)).toBe('0 B');
+      expect(service.formatToBinary('0.1', false)).toBe('0.1 B');
+      expect(service.formatToBinary('1', false)).toBe('1 B');
+      expect(service.formatToBinary('1024', false)).toBe('1 KiB');
+      expect(service.formatToBinary(23.45678 * Math.pow(1024, 3), false)).toBe('23.5 GiB');
+    });
+
+    it('should respect decimals param when split=false', () => {
+      expect(service.formatToBinary(23.45678 * Math.pow(1024, 3), false, 2)).toBe('23.46 GiB');
+      expect(service.formatToBinary('1024', false, 3)).toBe('1 KiB');
+    });
+
+    it('should return tuple [number, unit] when split=true', () => {
+      expect(service.formatToBinary('0', true)).toEqual([0, 'B']);
+      expect(service.formatToBinary('1024', true)).toEqual([1, 'KiB']);
+      expect(service.formatToBinary(23.45678 * Math.pow(1024, 3), true)).toEqual([23.5, 'GiB']);
+    });
+
+    it('should return "-" for unsupported values when split=false', () => {
+      expect(service.formatToBinary(undefined as any, false)).toBe('-');
+      expect(service.formatToBinary(null as any, false)).toBe('-');
+      expect(service.formatToBinary(service as any, false)).toBe('-');
+    });
+
+    it('should return a safe tuple when split=true and input is unsupported', () => {
+      expect(service.formatToBinary(undefined as any, true)).toEqual([0, 'B']);
+      expect(service.formatToBinary(null as any, true)).toEqual([0, 'B']);
+      expect(service.formatToBinary(service as any, true)).toEqual([0, 'B']);
+    });
+  });
+
+  describe('convertToUnit', () => {
+    it('should return 0 for empty-ish values', () => {
+      expect(service.convertToUnit('', 'B', 'KiB')).toBe(0);
+      expect(service.convertToUnit(undefined as any, 'B', 'KiB')).toBe(0);
+      expect(service.convertToUnit(null as any, 'B', 'KiB')).toBe(0);
+    });
+
+    it('should convert between binary units (default decimals=1)', () => {
+      expect(service.convertToUnit('1024', 'B', 'KiB')).toBe(1);
+      expect(service.convertToUnit('1', 'GiB', 'MiB')).toBe(1024);
+      expect(service.convertToUnit('1', 'MiB', 'KiB')).toBe(1024);
+    });
+
+    it('should respect decimals rounding', () => {
+      // 1000 MiB -> 0.9765625 GiB -> with 3 decimals => 0.977
+      expect(service.convertToUnit('1000', 'mib', 'gib', 3)).toBe(0.977);
+
+      // with 1 decimal => 1.0 (rounding)
+      expect(service.convertToUnit('1000', 'mib', 'gib', 1)).toBe(1);
+    });
+
+    it('should handle very small conversions that round to 0', () => {
+      expect(service.convertToUnit('0.1', 'B', 'TiB')).toBe(0);
+    });
+  });
+
+  describe('convertToNumber', () => {
+    it('should remove commas and trim', () => {
+      expect(service.convertToNumber('1,024')).toBe(1024);
+      expect(service.convertToNumber('  23.5  ')).toBe(23.5);
+    });
+  });
 });
index f112b76632221fe98eb3cc39de6d6647c99e933e..91ec7a17e9f63a85a2a2e3d1a0083774975c1f85 100644 (file)
@@ -3,7 +3,8 @@ import { AbstractControl, ValidationErrors } from '@angular/forms';
 import _ from 'lodash';
 import { isEmptyInputValue } from '../forms/cd-validators';
 
-const binaryUnits = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
+const BINARY_UNITS = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
+const BINARY_FACTOR = 1024;
 
 @Injectable({
   providedIn: 'root'
@@ -189,24 +190,34 @@ export class FormatterService {
     split: boolean = false,
     decimals: number = 1
   ): string | [number, string] {
-    const conversionFactor = 1024;
-    const convertedString = this.format_number(num, conversionFactor, binaryUnits, decimals);
-    if (split) {
-      const [value, unit] = convertedString.split(/\s+/);
-      return [this.convertToNumber(value), unit];
+    const convertedString = this.format_number(num, BINARY_FACTOR, BINARY_UNITS, decimals);
+    const FALLBACK: [number, string] = [0, BINARY_UNITS[0]]; // when convertedString is 'N/A', '-', or 'NaN', return [0, 'B']
+    if (!split) return convertedString;
+
+    const parts = convertedString.trim().split(/\s+/);
+
+    if (parts.length < 2) {
+      return FALLBACK;
+    }
+
+    const value = this.convertToNumber(parts[0]);
+    const unit = parts[1];
+
+    if (!Number.isFinite(value) || !unit) {
+      return FALLBACK;
     }
-    return convertedString;
+
+    return [value, unit];
   }
 
   convertToUnit(value: string, fromUnit: string, toUnit: string, decimals: number = 1): number {
     if (!value) return 0;
-    const conversionFactor = 1024;
     const convertedString = this.formatNumberFromTo(
       value,
       fromUnit,
       toUnit,
-      conversionFactor,
-      binaryUnits,
+      BINARY_FACTOR,
+      BINARY_UNITS,
       decimals
     );
     return this.convertToNumber(convertedString.split(/\s+/)[0]);