]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
Added qurey data
authorAfreen Misbah <afreen@ibm.com>
Mon, 9 Feb 2026 19:58:52 +0000 (01:28 +0530)
committerAfreen Misbah <afreen@ibm.com>
Mon, 16 Mar 2026 07:20:14 +0000 (12:50 +0530)
Signed-off-by: Afreen Misbah <afreen@ibm.com>
(cherry picked from commit 004994d743e284be6466293d8f9d00d89f80024a)

 Conflicts:
src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts

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.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/productive-card/productive-card.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/components/productive-card/productive-card.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/formatter.service.ts

index a765474a6f014e671a9f469e62c6fd19546818fd..8c234111c98a451a0b1fb930c96ec8eee769a323 100644 (file)
     <div cdsCol
          class="cds-mb-5"
          [columnNumbers]="{lg: 16}">
+      @if (healthData$ | async; as healthData) {
       <cd-overview-storage-card
-        [total]="totalCapacity"
-        [used]="usedCapacity"></cd-overview-storage-card>
+        [total]="healthData?.pgmap.bytes_total"
+        [used]="healthData?.pgmap.bytes_used">
+      </cd-overview-storage-card>
+      }
     </div>
   </div>
   <div cdsRow>
index 399a0f8c6d377262a3985b05db7fbe70e966931c..c76dceb4e93764dfbc5469243015a4ca30c64497 100644 (file)
@@ -1,39 +1,36 @@
-import { Component, OnInit, OnDestroy } from '@angular/core';
+import { Component, OnDestroy } from '@angular/core';
 import { GridModule, TilesModule } from 'carbon-components-angular';
 import { OverviewStorageCardComponent } from './storage-card/overview-storage-card.component';
 import { HealthService } from '~/app/shared/api/health.service';
 import { HealthSnapshotMap } from '~/app/shared/models/health.interface';
 import { RefreshIntervalService } from '~/app/shared/services/refresh-interval.service';
 import { catchError, exhaustMap, takeUntil } from 'rxjs/operators';
-import { EMPTY, Subject } from 'rxjs';
+import { EMPTY, Observable, Subject } from 'rxjs';
+import { CommonModule } from '@angular/common';
 
 @Component({
   selector: 'cd-overview',
-  imports: [GridModule, TilesModule, OverviewStorageCardComponent],
+  imports: [GridModule, TilesModule, OverviewStorageCardComponent, CommonModule],
   standalone: true,
   templateUrl: './overview.component.html',
   styleUrl: './overview.component.scss'
 })
-export class OverviewComponent implements OnInit, OnDestroy {
+export class OverviewComponent implements OnDestroy {
   totalCapacity: number;
   usedCapacity: number;
   private destroy$ = new Subject<void>();
+  public healthData$: Observable<HealthSnapshotMap>;
 
   constructor(
     private healthService: HealthService,
     private refreshIntervalService: RefreshIntervalService
-  ) {}
-
-  ngOnInit(): void {
-    this.refreshIntervalObs(() => this.healthService.getHealthSnapshot()).subscribe({
-      next: (healthData: HealthSnapshotMap) => {
-        this.totalCapacity = healthData?.pgmap?.bytes_total;
-        this.usedCapacity = healthData?.pgmap?.bytes_used;
-      }
-    });
+  ) {
+    this.healthData$ = this.refreshIntervalObs<HealthSnapshotMap>(() =>
+      this.healthService.getHealthSnapshot()
+    );
   }
 
-  refreshIntervalObs(fn: Function) {
+  refreshIntervalObs<T>(fn: () => Observable<T>): Observable<T> {
     return this.refreshIntervalService.intervalData$.pipe(
       exhaustMap(() => fn().pipe(catchError(() => EMPTY))),
       takeUntil(this.destroy$)
index d810ffc6ac1eae147baf0b334daa70b3697f4d00..eb528c88055144ffd8e833618998e8158d762a0e 100644 (file)
@@ -1,28 +1,41 @@
 <cd-productive-card
-  title="Storage overview"
-  i18n-title>
+  headerTitle="Storage overview"
+  i18n-headerTitle>
+  <!-- STORAGE CARD HEADER DROPDOWN -->
   <ng-template #headerAction>
     <cds-dropdown
       label="Storage type"
       class="overview-storage-card-dropdown"
       i18n-label
       size="sm"
+      [disabled]="!(displayData && usedRaw && totalRaw && usedRawUnit && totalRawUnit)"
       [placeholder]="selectedStorageType"
       (selected)="onStorageTypeSelect($event)">
       <cds-dropdown-list [items]="dropdownItems"></cds-dropdown-list>
     </cds-dropdown>
   </ng-template>
-  <div class="overview-storage-card-chart-header">
+  <!-- CAPACITY USAGE TEXT AND RAW CAPACITY TOGGLE-->
+  <div class="overview-storage-card-usage-text">
+    @if(usedRaw && totalRaw && usedRawUnit && totalRawUnit) {
     <h5>
       <span
         class="cds--type-heading-05"
-        i18n>{{usedRaw}}{{' '}}</span>
+        i18n>{{usedRaw}}&ngsp;</span>
       <span
         class="cds--type-body-02"
         i18n>{{usedRawUnit}} of {{totalRaw}} {{totalRawUnit}} used</span>
     </h5>
+    }
+    @else {
+    <cds-skeleton-text
+      [lines]="1"
+      [minLineWidth]="400"
+      [maxLineWidth]="400">
+    </cds-skeleton-text>
+    }
     <cds-checkbox
       [checked]="isRawCapacity"
+      [disabled]="!(displayData && usedRaw && totalRaw && usedRawUnit && totalRawUnit)"
       (checkedChange)="toggleRawCapacity($event)">
       <cds-tooltip-definition
         [autoAlign]="true"
       </cds-tooltip-definition>
     </cds-checkbox>
   </div>
+  <!-- CAPACITY CHART -->
+  @if(displayData) {
   <ibm-meter-chart
     [options]="options"
     [data]="displayData"
     class="overview-storage-card-chart"></ibm-meter-chart>
+  }
+  @else {
+  <cds-skeleton-text
+    [lines]="1"
+    [minLineWidth]="1025"
+    [maxLineWidth]="1025">
+  </cds-skeleton-text>
+  <cds-skeleton-text
+    [lines]="1"
+    [minLineWidth]="200"
+    [maxLineWidth]="200">
+  </cds-skeleton-text>
+  }
 </cd-productive-card>
index dffdb9415b774d433a1d9c324611ab266e208ab2..bd0e7a1cbed1832a71450bbbce94c0027a3c47f5 100644 (file)
@@ -1,22 +1,4 @@
 .overview-storage-card {
-  // Hiding the native chart title
-  &-chart {
-    .meter-title {
-      height: 0 !important;
-      display: none !important;
-    }
-
-    .spacer {
-      height: 0 !important;
-      display: none !important;
-    }
-  }
-
-  &-chart-header {
-    display: flex;
-    justify-content: space-between;
-  }
-
   &-dropdown {
     display: flex;
     justify-content: flex-end;
       flex: 0 0 40%;
     }
   }
+
+  &-usage-text {
+    display: flex;
+    justify-content: space-between;
+  }
+
+  // Hiding the native chart title
+  &-chart {
+    .meter-title {
+      height: 0 !important;
+      display: none !important;
+    }
+
+    .spacer {
+      height: 0 !important;
+      display: none !important;
+    }
+  }
 }
index 48baeb1a5ee89dbbdde178a7fd417d940aae5448..4c0183ae90b5fc89778e8f623e914b19d0644f86 100644 (file)
@@ -1,42 +1,57 @@
-import { Component, Input, OnChanges, ViewEncapsulation } from '@angular/core';
+import {
+  ChangeDetectionStrategy,
+  ChangeDetectorRef,
+  Component,
+  Input,
+  OnDestroy,
+  OnInit,
+  ViewEncapsulation
+} from '@angular/core';
 import {
   CheckboxModule,
   DropdownModule,
   GridModule,
   TilesModule,
-  TooltipModule
+  TooltipModule,
+  SkeletonModule,
+  LayoutModule
 } from 'carbon-components-angular';
 import { ProductiveCardComponent } from '~/app/shared/components/productive-card/productive-card.component';
 import { MeterChartComponent, MeterChartOptions } from '@carbon/charts-angular';
-import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+import {
+  PrometheusService,
+  PromethuesGaugeMetricResult,
+  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';
+
+const CHART_HEIGHT = '45px';
 
 const StorageType = {
   ALL: $localize`All`,
   BLOCK: $localize`Block`,
-  FILE: $localize`File`,
+  FILE: $localize`Filesystem`,
   OBJECT: $localize`Object`
 };
 
-// const Query = {
-//   RAW: {
-//     [StorageType.ALL]: `sum by (application) (ceph_pool_bytes_used * on(pool_id) group_left(instance, name, application) ceph_pool_metadata{application=~"(.*Block.*)|(.*Filesystem.*)|(.*Object.*)|(..*)"})`,
-//     [StorageType.BLOCK]: "",
-//     [StorageType.FILE]: "",
-//     [StorageType.OBJECT]: ""
-//   },
-//   USED: {
-//     [StorageType.ALL]: `sum by (application) (ceph_pool_stored * on(pool_id) group_left(instance, name, application) ceph_pool_metadata{application=~"(.*Block.*)|(.*Filesystem.*)|(.*Object.*)|(..*)"})`,
-//     [StorageType.BLOCK]: "",
-//     [StorageType.FILE]: "",
-//     [StorageType.OBJECT]: ""
-//   }
-// }
-
-/**
- * 4. Set data for block, file , object, all -> raw, sep queries
- * 5. Set data for block, file object + replicated -> usable
- * 6. Dont show what is 0
- */
+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 chartGroupLabels = [StorageType.BLOCK, StorageType.FILE, StorageType.OBJECT];
 
 @Component({
   selector: 'cd-overview-storage-card',
@@ -47,22 +62,42 @@ const StorageType = {
     MeterChartComponent,
     CheckboxModule,
     DropdownModule,
-    TooltipModule
+    TooltipModule,
+    SkeletonModule,
+    LayoutModule
   ],
   standalone: true,
   templateUrl: './overview-storage-card.component.html',
   styleUrl: './overview-storage-card.component.scss',
-  encapsulation: ViewEncapsulation.None
+  encapsulation: ViewEncapsulation.None,
+  changeDetection: ChangeDetectionStrategy.OnPush
 })
-export class OverviewStorageCardComponent implements OnChanges {
-  @Input() total!: number;
-  @Input() used!: number;
-  totalRaw: string;
-  usedRaw: string;
+export class OverviewStorageCardComponent implements OnInit, OnDestroy {
+  @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();
+  }
+  @Input()
+  set used(value: number) {
+    const [usedValue, usedUnit] = this.formatterService.formatToBinary(value, true);
+    if (Number.isNaN(usedValue)) return;
+    this.usedRaw = usedValue;
+    this.usedRawUnit = usedUnit;
+    this.setTotalAndUsed();
+  }
+  totalRaw: number;
+  usedRaw: number;
   totalRawUnit: string;
   usedRawUnit: string;
+  isRawCapacity: boolean = true;
+  selectedStorageType: string = StorageType.ALL;
+  selectedCapacityType: string = CapacityType.RAW;
   options: MeterChartOptions = {
-    height: '45px',
+    height: CHART_HEIGHT,
     meter: {
       proportional: {
         total: null,
@@ -78,77 +113,120 @@ export class OverviewStorageCardComponent implements OnChanges {
       pairing: {
         option: 2
       }
-    },
-    canvasZoom: {
-      enabled: false
     }
   };
-  allData = [
-    {
-      group: StorageType.BLOCK,
-      value: 100
-    },
-    {
-      group: StorageType.FILE,
-      value: 105
-    },
-    {
-      group: StorageType.OBJECT,
-      value: 60
-    }
-  ];
+  allData: ChartData[] = null;
+  displayData: ChartData[] = null;
   dropdownItems = [
     { content: StorageType.ALL },
     { content: StorageType.BLOCK },
     { content: StorageType.FILE },
     { content: StorageType.OBJECT }
   ];
-  isRawCapacity: boolean = true;
-  selectedStorageType: string = StorageType.ALL;
-  displayData = this.allData;
-
-  constructor(private dimlessBinaryPipe: DimlessBinaryPipe) {}
-
-  ngOnChanges(): void {
-    if (this.total == null || this.used == null) return;
 
-    const totalRaw = this.dimlessBinaryPipe.transform(this.total);
-    const usedRaw = this.dimlessBinaryPipe.transform(this.used);
+  constructor(
+    private prometheusService: PrometheusService,
+    private formatterService: FormatterService,
+    private cdr: ChangeDetectorRef
+  ) {}
 
-    const [totalValue, totalUnit] = totalRaw.split(/\s+/);
-    const [usedValue, usedUnit] = usedRaw.split(/\s+/);
+  private destroy$ = new Subject<void>();
+  private capacityType$ = new BehaviorSubject<string>(CapacityType.RAW);
 
-    const cleanedTotal = Number(totalValue.replace(/,/g, '').trim());
-
-    if (Number.isNaN(cleanedTotal)) return;
-
-    this.totalRaw = totalValue;
-    this.totalRawUnit = totalUnit;
-    this.usedRaw = usedValue;
-    this.usedRawUnit = usedUnit;
-
-    // chart reacts to 'options' and 'data' object changes only, hence mandatory to replace whole object
+  private setTotalAndUsed() {
+    // Chart reacts to 'options' and 'data' object changes only, hence mandatory to replace whole object.
     this.options = {
       ...this.options,
       meter: {
         ...this.options.meter,
         proportional: {
           ...this.options.meter.proportional,
-          total: cleanedTotal,
-          unit: totalUnit
+          total: this.totalRaw,
+          unit: this.totalRawUnit
         }
+      },
+      tooltip: {
+        valueFormatter: (value) => `${value.toLocaleString()} ${this.usedRawUnit}`
       }
     };
+    this.updateCard();
   }
 
-  toggleRawCapacity(isChecked: boolean) {
-    this.isRawCapacity = isChecked;
+  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 };
+      })
+      // Removing 0 values and legends other than Block, Filesystem, and Object.
+      .filter((r) => chartGroupLabels.includes(r.group) && r.value > 0);
+    return chartData;
   }
 
-  onStorageTypeSelect(selected: { item: { content: string; selected: true } }) {
-    this.selectedStorageType = selected?.item?.content;
+  private setChartData() {
     if (this.selectedStorageType === StorageType.ALL) {
       this.displayData = this.allData;
-    } else this.displayData = this.allData.filter((d) => d.group === this.selectedStorageType);
+    } else {
+      this.displayData = this.allData.filter(
+        (d: ChartData) => d.group === this.selectedStorageType
+      );
+    }
+  }
+
+  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;
+    } 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;
+    }
+  }
+
+  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();
+  }
+
+  ngOnInit() {
+    this.capacityType$
+      .pipe(
+        switchMap((capacityType) =>
+          this.prometheusService.getPrometheusQueryData({
+            params: Query[capacityType]
+          })
+        ),
+        takeUntil(this.destroy$)
+      )
+      .subscribe((data: PromqlGuageMetric) => {
+        this.allData = this.getAllData(data);
+        this.setDropdownItemsAndStorageType();
+        this.setChartData();
+        this.updateCard();
+      });
+  }
+
+  ngOnDestroy(): void {
+    this.destroy$.next();
+    this.destroy$.complete();
   }
 }
index aa328a0ab44e11ea00e2e8d9c0a3d5cb036677da..97c8b98600a4d4358b0e3009e8f1510d57f051ae 100644 (file)
@@ -8,7 +8,7 @@
          class="productive-card-header-row">
       <div cdsCol
            [columnNumbers]="{sm: headerActionTemplate ? 12 : 16, md: headerActionTemplate ? 12 : 16, lg: headerActionTemplate ? 12 : 16}">
-        <h2 class="cds--type-heading-compact-02">{{title}}</h2>
+        <h2 class="cds--type-heading-compact-02">{{headerTitle}}</h2>
       </div>
       @if(!!headerActionTemplate) {
       <div cdsCol
index aba50e37af731cbee06c73f228dd38330adcbbea..461c40edc48ecf95d117436b20c9526c9c923cbe 100644 (file)
@@ -22,7 +22,7 @@ import { GridModule, LayerModule, TilesModule } from 'carbon-components-angular'
 })
 export class ProductiveCardComponent {
   /* Card Title */
-  @Input() title!: string;
+  @Input() headerTitle!: string;
 
   /* Optional: Applies a tinted-colored background to card */
   @Input() applyShadow: boolean = false;
index 011fefd734214a753d47d48e7f61fc4d58ae29bf..f112b76632221fe98eb3cc39de6d6647c99e933e 100644 (file)
@@ -3,6 +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'];
+
 @Injectable({
   providedIn: 'root'
 })
@@ -179,4 +181,38 @@ export class FormatterService {
     }
     return control.value.toString()?.length > 18 ? { rateOpsMaxSize: true } : null;
   }
+
+  formatToBinary(num: any, split: false, decimals?: number): string;
+  formatToBinary(num: any, split: true, decimals?: number): [number, string];
+  formatToBinary(
+    num: any,
+    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];
+    }
+    return convertedString;
+  }
+
+  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,
+      decimals
+    );
+    return this.convertToNumber(convertedString.split(/\s+/)[0]);
+  }
+
+  convertToNumber(num: string) {
+    return Number(num.replace(/,/g, '').trim());
+  }
 }