From 004994d743e284be6466293d8f9d00d89f80024a Mon Sep 17 00:00:00 2001 From: Afreen Misbah Date: Tue, 10 Feb 2026 01:28:52 +0530 Subject: [PATCH] Added qurey data Signed-off-by: Afreen Misbah --- .../app/ceph/overview/overview.component.html | 7 +- .../app/ceph/overview/overview.component.ts | 25 +- .../overview-storage-card.component.html | 36 ++- .../overview-storage-card.component.scss | 36 +-- .../overview-storage-card.component.ts | 234 ++++++++++++------ .../shared/components/components.module.ts | 9 +- .../productive-card.component.html | 2 +- .../productive-card.component.ts | 2 +- .../app/shared/services/formatter.service.ts | 36 +++ 9 files changed, 264 insertions(+), 123 deletions(-) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.html index a765474a6f0..8c234111c98 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.html @@ -21,9 +21,12 @@
+ @if (healthData$ | async; as healthData) { + [total]="healthData?.pgmap.bytes_total" + [used]="healthData?.pgmap.bytes_used"> + + }
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.ts index 399a0f8c6d3..c76dceb4e93 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.ts @@ -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(); + public healthData$: Observable; 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(() => + this.healthService.getHealthSnapshot() + ); } - refreshIntervalObs(fn: Function) { + refreshIntervalObs(fn: () => Observable): Observable { return this.refreshIntervalService.intervalData$.pipe( exhaustMap(() => fn().pipe(catchError(() => EMPTY))), takeUntil(this.destroy$) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/storage-card/overview-storage-card.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/storage-card/overview-storage-card.component.html index d810ffc6ac1..eb528c88055 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/storage-card/overview-storage-card.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/storage-card/overview-storage-card.component.html @@ -1,28 +1,41 @@ + headerTitle="Storage overview" + i18n-headerTitle> + -
+ +
+ @if(usedRaw && totalRaw && usedRawUnit && totalRawUnit) {
{{usedRaw}}{{' '}} + i18n>{{usedRaw}}&ngsp; {{usedRawUnit}} of {{totalRaw}} {{totalRawUnit}} used
+ } + @else { + + + }
+ + @if(displayData) { + } + @else { + + + + + } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/storage-card/overview-storage-card.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/storage-card/overview-storage-card.component.scss index dffdb9415b7..bd0e7a1cbed 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/storage-card/overview-storage-card.component.scss +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/storage-card/overview-storage-card.component.scss @@ -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; @@ -30,4 +12,22 @@ 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; + } + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/storage-card/overview-storage-card.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/storage-card/overview-storage-card.component.ts index 48baeb1a5ee..4c0183ae90b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/storage-card/overview-storage-card.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/storage-card/overview-storage-card.component.ts @@ -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(); + private capacityType$ = new BehaviorSubject(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(); } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts index c7d2258e880..4a9f1ce5297 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts @@ -108,7 +108,6 @@ import WarningFilledIcon from '@carbon/icons/es/warning--filled/16'; import NotificationFilledIcon from '@carbon/icons/es/notification--filled/16'; import { Close16 } from '@carbon/icons'; import { TearsheetStepComponent } from './tearsheet-step/tearsheet-step.component'; -import { ProductiveCardComponent } from './productive-card/productive-card.component'; import { PageHeaderComponent } from './page-header/page-header.component'; import { SidebarLayoutComponent } from './sidebar-layout/sidebar-layout.component'; @@ -208,9 +207,9 @@ import { SidebarLayoutComponent } from './sidebar-layout/sidebar-layout.componen ToastComponent, TearsheetComponent, TearsheetStepComponent, - ProductiveCardComponent, PageHeaderComponent, - SidebarLayoutComponent + SidebarLayoutComponent, + PageHeaderComponent ], providers: [provideCharts(withDefaultRegisterables())], exports: [ @@ -256,9 +255,9 @@ import { SidebarLayoutComponent } from './sidebar-layout/sidebar-layout.componen ToastComponent, TearsheetComponent, TearsheetStepComponent, - ProductiveCardComponent, PageHeaderComponent, - SidebarLayoutComponent + SidebarLayoutComponent, + PageHeaderComponent ] }) export class ComponentsModule { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/productive-card/productive-card.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/productive-card/productive-card.component.html index aa328a0ab44..97c8b98600a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/productive-card/productive-card.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/productive-card/productive-card.component.html @@ -8,7 +8,7 @@ class="productive-card-header-row">
-

{{title}}

+

{{headerTitle}}

@if(!!headerActionTemplate) {
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()); + } } -- 2.47.3