-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$)
-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',
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,
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();
}
}