]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/blob
4c0183ae90b5fc89778e8f623e914b19d0644f86
[ceph.git] /
1 import {
2   ChangeDetectionStrategy,
3   ChangeDetectorRef,
4   Component,
5   Input,
6   OnDestroy,
7   OnInit,
8   ViewEncapsulation
9 } from '@angular/core';
10 import {
11   CheckboxModule,
12   DropdownModule,
13   GridModule,
14   TilesModule,
15   TooltipModule,
16   SkeletonModule,
17   LayoutModule
18 } from 'carbon-components-angular';
19 import { ProductiveCardComponent } from '~/app/shared/components/productive-card/productive-card.component';
20 import { MeterChartComponent, MeterChartOptions } from '@carbon/charts-angular';
21 import {
22   PrometheusService,
23   PromethuesGaugeMetricResult,
24   PromqlGuageMetric
25 } from '~/app/shared/api/prometheus.service';
26 import { FormatterService } from '~/app/shared/services/formatter.service';
27 import { BehaviorSubject, Subject } from 'rxjs';
28 import { switchMap, takeUntil } from 'rxjs/operators';
29
30 const CHART_HEIGHT = '45px';
31
32 const StorageType = {
33   ALL: $localize`All`,
34   BLOCK: $localize`Block`,
35   FILE: $localize`Filesystem`,
36   OBJECT: $localize`Object`
37 };
38
39 const CapacityType = {
40   RAW: 'raw',
41   USED: 'used'
42 };
43
44 type ChartData = {
45   group: string;
46   value: number;
47 };
48
49 const Query = {
50   [CapacityType.RAW]: `sum by (application) (ceph_pool_bytes_used * on(pool_id) group_left(instance, name, application) ceph_pool_metadata{application=~"(.*Block.*)|(.*Filesystem.*)|(.*Object.*)|(..*)"})`,
51   [CapacityType.USED]: `sum by (application) (ceph_pool_stored * on(pool_id) group_left(instance, name, application) ceph_pool_metadata{application=~"(.*Block.*)|(.*Filesystem.*)|(.*Object.*)|(..*)"})`
52 };
53
54 const chartGroupLabels = [StorageType.BLOCK, StorageType.FILE, StorageType.OBJECT];
55
56 @Component({
57   selector: 'cd-overview-storage-card',
58   imports: [
59     GridModule,
60     TilesModule,
61     ProductiveCardComponent,
62     MeterChartComponent,
63     CheckboxModule,
64     DropdownModule,
65     TooltipModule,
66     SkeletonModule,
67     LayoutModule
68   ],
69   standalone: true,
70   templateUrl: './overview-storage-card.component.html',
71   styleUrl: './overview-storage-card.component.scss',
72   encapsulation: ViewEncapsulation.None,
73   changeDetection: ChangeDetectionStrategy.OnPush
74 })
75 export class OverviewStorageCardComponent implements OnInit, OnDestroy {
76   @Input()
77   set total(value: number) {
78     const [totalValue, totalUnit] = this.formatterService.formatToBinary(value, true);
79     if (Number.isNaN(totalValue)) return;
80     this.totalRaw = totalValue;
81     this.totalRawUnit = totalUnit;
82     this.setTotalAndUsed();
83   }
84   @Input()
85   set used(value: number) {
86     const [usedValue, usedUnit] = this.formatterService.formatToBinary(value, true);
87     if (Number.isNaN(usedValue)) return;
88     this.usedRaw = usedValue;
89     this.usedRawUnit = usedUnit;
90     this.setTotalAndUsed();
91   }
92   totalRaw: number;
93   usedRaw: number;
94   totalRawUnit: string;
95   usedRawUnit: string;
96   isRawCapacity: boolean = true;
97   selectedStorageType: string = StorageType.ALL;
98   selectedCapacityType: string = CapacityType.RAW;
99   options: MeterChartOptions = {
100     height: CHART_HEIGHT,
101     meter: {
102       proportional: {
103         total: null,
104         unit: '',
105         breakdownFormatter: (_e) => null,
106         totalFormatter: (_e) => null
107       }
108     },
109     toolbar: {
110       enabled: false
111     },
112     color: {
113       pairing: {
114         option: 2
115       }
116     }
117   };
118   allData: ChartData[] = null;
119   displayData: ChartData[] = null;
120   dropdownItems = [
121     { content: StorageType.ALL },
122     { content: StorageType.BLOCK },
123     { content: StorageType.FILE },
124     { content: StorageType.OBJECT }
125   ];
126
127   constructor(
128     private prometheusService: PrometheusService,
129     private formatterService: FormatterService,
130     private cdr: ChangeDetectorRef
131   ) {}
132
133   private destroy$ = new Subject<void>();
134   private capacityType$ = new BehaviorSubject<string>(CapacityType.RAW);
135
136   private setTotalAndUsed() {
137     // Chart reacts to 'options' and 'data' object changes only, hence mandatory to replace whole object.
138     this.options = {
139       ...this.options,
140       meter: {
141         ...this.options.meter,
142         proportional: {
143           ...this.options.meter.proportional,
144           total: this.totalRaw,
145           unit: this.totalRawUnit
146         }
147       },
148       tooltip: {
149         valueFormatter: (value) => `${value.toLocaleString()} ${this.usedRawUnit}`
150       }
151     };
152     this.updateCard();
153   }
154
155   private getAllData(data: PromqlGuageMetric) {
156     const result = data?.result ?? [];
157     const chartData = result
158       .map((r: PromethuesGaugeMetricResult) => {
159         const group = r?.metric?.application;
160         const value = this.formatterService.convertToUnit(r?.value?.[1], 'B', this.usedRawUnit, 10);
161         return { group, value };
162       })
163       // Removing 0 values and legends other than Block, Filesystem, and Object.
164       .filter((r) => chartGroupLabels.includes(r.group) && r.value > 0);
165     return chartData;
166   }
167
168   private setChartData() {
169     if (this.selectedStorageType === StorageType.ALL) {
170       this.displayData = this.allData;
171     } else {
172       this.displayData = this.allData.filter(
173         (d: ChartData) => d.group === this.selectedStorageType
174       );
175     }
176   }
177
178   private setDropdownItemsAndStorageType() {
179     const dynamicItems = this.allData.map((data) => ({ content: data.group }));
180     const hasExistingItem = dynamicItems.some((item) => item.content === this.selectedStorageType);
181
182     if (dynamicItems.length === 1) {
183       this.dropdownItems = dynamicItems;
184       this.selectedStorageType = dynamicItems[0]?.content;
185     } else {
186       this.dropdownItems = [{ content: StorageType.ALL }, ...dynamicItems];
187     }
188     // Change the current dropdown selection to 'ALL' if prev selection is absent in current data, and current data has more than one item.
189     if (!hasExistingItem && dynamicItems.length > 1) {
190       this.selectedStorageType = StorageType.ALL;
191     }
192   }
193
194   private updateCard() {
195     this.cdr.markForCheck();
196   }
197
198   public toggleRawCapacity(isChecked: boolean) {
199     this.isRawCapacity = isChecked;
200     this.selectedCapacityType = isChecked ? CapacityType.RAW : CapacityType.USED;
201     // Reloads Prometheus Query
202     this.capacityType$.next(this.selectedCapacityType);
203   }
204
205   public onStorageTypeSelect(selected: { item: { content: string; selected: true } }) {
206     this.selectedStorageType = selected?.item?.content;
207     this.setChartData();
208   }
209
210   ngOnInit() {
211     this.capacityType$
212       .pipe(
213         switchMap((capacityType) =>
214           this.prometheusService.getPrometheusQueryData({
215             params: Query[capacityType]
216           })
217         ),
218         takeUntil(this.destroy$)
219       )
220       .subscribe((data: PromqlGuageMetric) => {
221         this.allData = this.getAllData(data);
222         this.setDropdownItemsAndStorageType();
223         this.setChartData();
224         this.updateCard();
225       });
226   }
227
228   ngOnDestroy(): void {
229     this.destroy$.next();
230     this.destroy$.complete();
231   }
232 }