<h5>
<i class="text-success"
[ngClass]="[icons.success]"
- *ngIf="(healthData.mgr_map | mgrSummary).total > 1; else warningIcon">
+ *ngIf="(healthData.mgr_map | mgrSummary)?.total > 1; else warningIcon">
</i>
- {{ (healthData.mgr_map | mgrSummary).total }}
+ {{ (healthData.mgr_map | mgrSummary)?.total }}
</h5>
</div>
</cd-card>
*ngIf="info$ | async as info; else checkingForUpgradeStatus">
<ng-container *ngIf="info.versions.length > 0; else noUpgradesAvailable">
<div i18n-ngbTooltip
- [ngbTooltip]="(healthData.mgr_map | mgrSummary).total <= 1 ? 'To upgrade, you need minimum 2 mgr daemons.' : ''">
+ [ngbTooltip]="(healthData.mgr_map | mgrSummary)?.total <= 1 ? 'To upgrade, you need minimum 2 mgr daemons.' : ''">
<button class="btn btn-accent mt-2"
id="upgrade"
aria-label="Upgrade now"
(click)="upgradeNow(info.versions[info.versions.length - 1])"
- [disabled]="(healthData.mgr_map | mgrSummary).total <= 1"
+ [disabled]="(healthData.mgr_map | mgrSummary)?.total <= 1"
i18n>Upgrade to {{ info.versions[info.versions.length - 1] }}</button>
</div>
<a class="mt-2 link-primary mb-2"
import { DashboardTimeSelectorComponent } from './dashboard-time-selector/dashboard-time-selector.component';
import { DashboardV3Component } from './dashboard/dashboard-v3.component';
import { PgSummaryPipe } from './pg-summary.pipe';
-import { ToggletipModule } from 'carbon-components-angular';
+import { InlineLoadingModule, ToggletipModule } from 'carbon-components-angular';
@NgModule({
imports: [
ReactiveFormsModule,
SimplebarAngularModule,
BaseChartDirective,
- ToggletipModule
+ ToggletipModule,
+ InlineLoadingModule
],
declarations: [
DashboardV3Component,
-<div class="container-fluid p-4"
- *ngIf="healthData && enabledFeature$ | async as enabledFeature">
-
+<div class="container-fluid p-4">
<div class="row d-flex flex-row ps-3">
<!-- First Grid to hold Details and Inventory Card-->
i18n-title
class="pt-4"
aria-label="Inventory card">
- <!-- Hosts -->
- <cd-card-row [data]="healthData.hosts"
- link="/hosts"
- title="Host"
- summaryType="simplified"
- *ngIf="healthData.hosts != null"
- [dropdownData]="(isHardwareEnabled$ | async) && (hardwareSummary$ | async)">
- </cd-card-row>
- <!-- Monitors -->
- <cd-card-row [data]="healthData.mon_status.monmap.mons.length"
- link="/monitor"
- title="Monitor"
- summaryType="simplified"
- *ngIf="healthData.mon_status"></cd-card-row>
- <!-- Managers -->
- <cd-card-row [data]="healthData.mgr_map | mgrSummary"
- title="Manager"
- *ngIf="healthData.mgr_map"></cd-card-row>
+ <ng-container *ngIf="enabledFeature$ | async as enabledFeature">
+ <!-- Hosts -->
+ <cd-card-row [data]="hostsCount"
+ link="/hosts"
+ title="Host"
+ summaryType="simplified"
+ [dropdownData]="(isHardwareEnabled$ | async) && (hardwareSummary$ | async)">
+ </cd-card-row>
+ <!-- Monitors -->
+ <cd-card-row [data]="monMap?.monmap.mons.length"
+ link="/monitor"
+ title="Monitor"
+ summaryType="simplified"></cd-card-row>
+ <!-- Managers -->
+ <cd-card-row [data]="mgrMap | mgrSummary"
+ title="Manager"></cd-card-row>
- <!-- OSDs -->
- <cd-card-row [data]="healthData.osd_map | osdSummary"
- link="/osd"
- title="OSD"
- summaryType="osd"
- *ngIf="healthData.osd_map"></cd-card-row>
+ <!-- OSDs -->
+ <cd-card-row [data]="osdMap | osdSummary"
+ link="/osd"
+ title="OSD"
+ summaryType="osd"></cd-card-row>
- <!-- Pools -->
- <cd-card-row [data]="healthData.pools.length"
- link="/pool"
- title="Pool"
- summaryType="simplified"
- *ngIf="healthData.pools"></cd-card-row>
+ <!-- Pools -->
+ <cd-card-row [data]="poolStatus?.length"
+ link="/pool"
+ title="Pool"
+ summaryType="simplified"></cd-card-row>
- <!-- PG Info -->
- <cd-card-row [data]="healthData.pg_info | pgSummary"
- title="PG"
- *ngIf="healthData.pg_info"></cd-card-row>
+ <!-- PG Info -->
+ <cd-card-row [data]="pgStatus | pgSummary"
+ title="PG"></cd-card-row>
- <!-- Object gateways -->
- <cd-card-row [data]="healthData.rgw"
- link="/rgw/daemon"
- title="Object Gateway"
- summaryType="simplified"
- id="rgw-item"
- *ngIf="enabledFeature.rgw && healthData.rgw || healthData.rgw === 0 "></cd-card-row>
+ <!-- Object gateways -->
+ <cd-card-row [data]="rgwCount"
+ link="/rgw/daemon"
+ title="Object Gateway"
+ summaryType="simplified"
+ id="rgw-item"
+ *ngIf="enabledFeature.rgw"></cd-card-row>
- <!-- Metadata Servers -->
- <cd-card-row [data]="healthData.fs_map | mdsSummary"
- title="Metadata Server"
- id="mds-item"
- *ngIf="enabledFeature.cephfs && healthData.fs_map"></cd-card-row>
- <!-- iSCSI Gateways -->
- <cd-card-row [data]="healthData.iscsi_daemons"
- link="/iscsi/daemon"
- title="iSCSI Gateway"
- summaryType="iscsi"
- id="iscsi-item"
- *ngIf="enabledFeature.iscsi && healthData.iscsi_daemons"></cd-card-row>
+ <!-- Metadata Servers -->
+ <cd-card-row [data]="mdsMap | mdsSummary"
+ title="Metadata Server"
+ id="mds-item"
+ *ngIf="enabledFeature.cephfs"></cd-card-row>
+ <!-- iSCSI Gateways -->
+ <cd-card-row [data]="iscsiMap"
+ link="/iscsi/daemon"
+ title="iSCSI Gateway"
+ summaryType="iscsi"
+ id="iscsi-item"
+ *ngIf="enabledFeature.iscsi"></cd-card-row>
+ </ng-container>
</cd-card>
</div>
</div>
<div class="d-flex flex-column ms-4 me-4 mt-4 mb-4">
<div class="d-flex flex-row col-md-3 ms-4">
- <i *ngIf="healthData.health?.status"
- [ngClass]="[healthData.health.status | healthIcon, icons.large2x]"
- [ngStyle]="healthData.health.status | healthColor"
- [title]="healthData.health.status">
+ <i *ngIf="healthData?.status else loadingTpl"
+ [ngClass]="[healthData.status | healthIcon, icons.large2x]"
+ [ngStyle]="healthData.status | healthColor"
+ [title]="healthData.status">
</i>
<span class="ms-2 mt-n1 lead"
- *ngIf="!healthData.health?.checks?.length"
+ *ngIf="!healthData?.checks?.length"
i18n>Cluster</span>
<cds-toggletip [dropShadow]="true"
[autoAlign]="true">
<div cdsToggletipButton>
<a class="ms-2 mt-n1 lead text-primary"
popoverClass="info-card-popover-cluster-status"
- *ngIf="healthData.health?.checks?.length"
+ *ngIf="healthData?.checks?.length"
i18n>Cluster
</a>
</div>
<div cdsToggletipContent
#healthCheck>
<div class="cds--popover-scroll-container">
- <cd-health-checks *ngIf="healthData?.health?.checks"
- [healthData]="healthData.health.checks">
+ <cd-health-checks *ngIf="healthData?.checks"
+ [healthData]="healthData.checks">
</cd-health-checks>
</div>
</div>
i18n><i [ngClass]="[icons.infoCircle]"></i> See <a routerLink="/logs">Logs</a> for more details.</p>
</ng-container>
</ng-template>
+
+<ng-template #loadingTpl>
+ <cds-inline-loading></cds-inline-loading>
+</ng-template>
import { Component, OnDestroy, OnInit } from '@angular/core';
import _ from 'lodash';
-import { BehaviorSubject, Observable, Subscription, of } from 'rxjs';
-import { switchMap, take } from 'rxjs/operators';
+import { BehaviorSubject, EMPTY, Observable, Subject, Subscription, of } from 'rxjs';
+import { catchError, exhaustMap, switchMap, take, takeUntil } from 'rxjs/operators';
import { HealthService } from '~/app/shared/api/health.service';
import { OsdService } from '~/app/shared/api/osd.service';
import { HardwareService } from '~/app/shared/api/hardware.service';
import { SettingsService } from '~/app/shared/api/settings.service';
import { OsdSettings } from '~/app/shared/models/osd-settings';
+import {
+ IscsiMap,
+ MdsMap,
+ MgrMap,
+ MonMap,
+ OsdMap,
+ PgStatus
+} from '~/app/shared/models/health.interface';
@Component({
selector: 'cd-dashboard-v3',
detailsCardData: DashboardDetails = {};
osdSettingsService: any;
osdSettings = new OsdSettings();
- interval = new Subscription();
permissions: Permissions;
enabledFeature$: FeatureTogglesMap$;
color: string;
hardwareSubject = new BehaviorSubject<any>([]);
managedByConfig$: Observable<any>;
private subs = new Subscription();
+ private destroy$ = new Subject<void>();
+
+ hostsCount: number = null;
+ monMap: MonMap = null;
+ mgrMap: MgrMap = null;
+ osdMap: OsdMap = null;
+ poolStatus: Record<string, any>[] = null;
+ pgStatus: PgStatus = null;
+ rgwCount: number = null;
+ mdsMap: MdsMap = null;
+ iscsiMap: IscsiMap = null;
constructor(
private summaryService: SummaryService,
ngOnInit() {
super.ngOnInit();
if (this.permissions.configOpt.read) {
+ this.getOsdSettings();
this.isHardwareEnabled$ = this.getHardwareConfig();
this.hardwareSummary$ = this.hardwareSubject.pipe(
switchMap(() =>
);
this.managedByConfig$ = this.settingsService.getValues('MANAGED_BY_CLUSTERS');
}
- this.interval = this.refreshIntervalService.intervalData$.subscribe(() => {
- this.getHealth();
- this.getCapacity();
- if (this.permissions.configOpt.read) this.getOsdSettings();
- if (this.hardwareEnabled) this.hardwareSubject.next([]);
+
+ this.loadInventories();
+
+ // fetch capacity to load the capacity chart
+ this.refreshIntervalObs(() => this.healthService.getClusterCapacity()).subscribe({
+ next: (capacity: any) => {
+ this.capacity = capacity;
+ }
});
+
this.getPrometheusData(this.prometheusService.lastHourDateObject);
this.getDetailsCardData();
this.getTelemetryReport();
Telemetry configration.';
}
ngOnDestroy() {
- this.interval.unsubscribe();
this.prometheusService.unsubscribe();
this.subs?.unsubscribe();
- }
-
- getHealth() {
- this.healthService.getMinimalHealth().subscribe((data: any) => {
- this.healthData = data;
- });
+ this.destroy$.next();
+ this.destroy$.complete();
}
toggleAlertsWindow(type: AlertClass) {
);
}
- private getCapacity() {
- this.capacityService = this.healthService.getClusterCapacity().subscribe((data: any) => {
- this.capacity = data;
- });
- }
-
private getOsdSettings() {
this.osdSettingsService = this.osdService
.getOsdSettings()
})
);
}
+
+ refreshIntervalObs(fn: Function) {
+ return this.refreshIntervalService.intervalData$.pipe(
+ exhaustMap(() => fn().pipe(catchError(() => EMPTY))),
+ takeUntil(this.destroy$)
+ );
+ }
+
+ loadInventories() {
+ this.refreshIntervalObs(() => this.healthService.getMinimalHealth()).subscribe({
+ next: (result: any) => {
+ this.hostsCount = result.hosts;
+ this.monMap = result.mon_status;
+ this.mgrMap = result.mgr_map;
+ this.osdMap = result.osd_map;
+ this.poolStatus = result.pools;
+ this.pgStatus = result.pg_info;
+ this.rgwCount = result.rgw;
+ this.mdsMap = result.fs_map;
+ this.iscsiMap = result.iscsi_daemons;
+ this.healthData = result.health;
+ this.enabledFeature$ = this.featureToggles.get();
+ }
+ });
+ }
}
constructor(private pgCategoryService: PgCategoryService) {}
transform(value: any): any {
+ if (!value) return null;
const categoryPgAmount: Record<string, number> = {};
let total = 0;
_.forEach(value.statuses, (pgAmount, pgStatesText) => {
this.getSyncStatus();
});
this.realmSub = this.rgwRealmService.list().subscribe((data: any) => {
- this.rgwRealmCount = data['realms'].length;
+ this.rgwRealmCount = data['realms'].length || 0;
});
this.ZonegroupSub = this.rgwZonegroupService.list().subscribe((data: any) => {
this.rgwZonegroupCount = data['zonegroups'].length;
<li class="list-group-item">
<div class="d-flex pl-1 pb-2 pt-2 position-relative">
<div class="ms-4 me-auto">
+ @if (link && data !== null || total) {
+ <a [routerLink]="link"
+ *ngIf="link && total > 0; else noLinkTitle"
+ [ngPlural]="total"
+ i18n>
+ {{ total }}
+ <ng-template ngPluralCase="=0">{{ title }}</ng-template>
+ <ng-template ngPluralCase="=1">{{ title }}</ng-template>
+ <ng-template ngPluralCase="other">{{ title }}s</ng-template>
+ </a>
+ } @else {
<a [routerLink]="link"
- *ngIf="link && total > 0; else noLinkTitle"
- [ngPlural]="total"
- i18n>
- {{ total }}
- <ng-template ngPluralCase="=0">{{ title }}</ng-template>
- <ng-template ngPluralCase="=1">{{ title }}</ng-template>
- <ng-template ngPluralCase="other">{{ title }}s</ng-template>
+ *ngIf="link; else noLinkTitle">{{title}}
</a>
+ }
</div>
<span class="me-4">
<ng-container [ngSwitch]="summaryType">
</div>
<ng-template #defaultSummary>
- <span *ngIf="data.success || data.categoryPgAmount?.clean || (data.success === 0 && data.total === 0)">
+ @if (data === null) {
+ <ng-container *ngTemplateOutlet="loadingTpl"></ng-container>
+ } @else {
+ <span *ngIf="data.success || data.categoryPgAmount?.clean || (data.success === 0 && data.total === 0)">
<span *ngIf="data.success || (data.success === 0 && data.total === 0)">
{{ data.success }}
</span>
[ngClass]="[icons.spinner, icons.spin]">
</i>
</span>
+ }
</ng-template>
<ng-template #osdSummary>
+ @if (data === null) {
+ <ng-container *ngTemplateOutlet="loadingTpl"></ng-container>
+ } @else {
<span *ngIf="data.up === data.in">
{{ data.up }}
<cd-icon
full
</span>
</span>
+ }
</ng-template>
<ng-template #iscsiSummary>
+ @if (data === null || total === null) {
+ <ng-container *ngTemplateOutlet="loadingTpl"></ng-container>
+ } @else {
<span>
- {{ data.up }}
+ {{ data?.up }}
<cd-icon
- *ngIf="data.up || data.up === 0"
+ *ngIf="data?.up || data?.up === 0"
type="success">
</cd-icon >
</span>
- <span *ngIf="data.down"
+ <span *ngIf="data?.down"
class="ms-2">
- {{ data.down }}
+ {{ data?.down }}
<cd-icon
type="danger">
</cd-icon >
</span>
+
+ }
</ng-template>
<ng-template #simplifiedSummary>
+ @if (data === 0 || data) {
<span *ngIf="!dropdownTotalError else showErrorNum">
{{ data }}
<cd-icon
- type="success"></cd-icon >
+ type="success"></cd-icon >
</span>
+ } @else {
+ <ng-container *ngTemplateOutlet="loadingTpl"></ng-container>
+ }
<ng-template #showErrorNum>
<span *ngIf="data - dropdownTotalError > 0">
{{ data - dropdownTotalError }}
</ng-template>
</ng-template>
+<ng-template #loadingTpl>
+ <cds-inline-loading></cds-inline-loading>
+</ng-template>
+
<ng-template #noLinkTitle>
- <span *ngIf="total || total === 0"
- [ngPlural]="total">
- {{ total }}
- <ng-template ngPluralCase="=0">{{ title }}</ng-template>
- <ng-template ngPluralCase="=1">{{ title }}</ng-template>
- <ng-template ngPluralCase="other">{{ title }}s</ng-template>
- </span>
+@if (data !== null || total) {
+<span *ngIf="total || total === 0"
+ [ngPlural]="total">
+ {{ total }}
+ <ng-template ngPluralCase="=0">{{ title }}</ng-template>
+ <ng-template ngPluralCase="=1">{{ title }}</ng-template>
+ <ng-template ngPluralCase="other">{{ title }}s</ng-template>
+</span>
+} @else {
+ <span>{{ title }}</span>
+}
</ng-template>
<ng-template #dropdownTemplate>
link: string;
@Input()
- data: any;
+ data: any = null;
@Input()
summaryType = 'default';
hwNames = HardwareNameMapping;
icons = Icons;
- total: number;
+ total: number = null;
dropdownTotalError: number = 0;
dropdownToggled: boolean = false;
ngOnChanges(): void {
- if (this.data.total || this.data.total === 0) {
- this.total = this.data.total;
+ if (this.data?.total || this.data?.total === 0) {
+ this.total = this.data?.total;
} else if (this.summaryType === 'iscsi') {
- this.total = this.data.up + this.data.down || 0;
+ this.total = this.data?.up + this.data?.down;
} else {
this.total = this.data;
}
PanelModule,
LayoutModule,
TilesModule,
- PopoverModule
+ PopoverModule,
+ InlineLoadingModule
} from 'carbon-components-angular';
import EditIcon from '@carbon/icons/es/edit/20';
import CodeIcon from '@carbon/icons/es/code/16';
ChartsModule,
LayoutModule,
TilesModule,
- PopoverModule
+ PopoverModule,
+ InlineLoadingModule
],
declarations: [
SparklineComponent,
--- /dev/null
+export interface MonMap {
+ monmap: {
+ mons: Record<string, any>[];
+ };
+ quorum: number[];
+}
+
+export interface MgrMap {
+ active_name: string;
+ standbys: string[];
+}
+
+export interface OsdMap {
+ osds: Osd[];
+}
+
+export interface PgStatus {
+ object_stats: ObjectStats;
+ statuses: Status;
+ pgs_per_osd: number;
+}
+
+export interface MdsMap {
+ filesystems: any[];
+ standbys: any[];
+}
+
+export interface IscsiMap {
+ up: number;
+ down: number;
+}
+
+interface ObjectStats {
+ num_objects: number;
+ num_object_copies: number;
+ num_objects_degraded: number;
+ num_objects_misplaced: number;
+ num_objects_unfound: number;
+}
+
+interface Status {
+ 'active+clean': number;
+}
+
+interface Osd {
+ in: number;
+ up: number;
+ state: string[];
+}
});
it('transforms without value', () => {
- expect(pipe.transform(undefined)).toEqual({
- success: 0,
- info: 0,
- total: 0
- });
+ expect(pipe.transform(undefined)).toEqual(null);
});
});
export class MdsSummaryPipe implements PipeTransform {
transform(value: any): any {
if (!value) {
- return {
- success: 0,
- info: 0,
- total: 0
- };
+ return null;
}
let activeCount = 0;
});
it('transforms without value', () => {
- expect(pipe.transform(undefined)).toEqual({
- success: 0,
- info: 0,
- total: 0
- });
+ expect(pipe.transform(undefined)).toEqual(null);
});
it('transforms with 1 active and 2 standbys', () => {
export class MgrSummaryPipe implements PipeTransform {
transform(value: any): any {
if (!value) {
- return {
- success: 0,
- info: 0,
- total: 0
- };
+ return null;
}
let activeCount: number;
});
it('transforms without value', () => {
- expect(pipe.transform(undefined)).toBe('');
+ expect(pipe.transform(undefined)).toBe(null);
});
it('transforms having 3 osd with 3 up, 3 in, 0 down, 0 out', () => {
export class OsdSummaryPipe implements PipeTransform {
transform(value: any): any {
if (!value) {
- return '';
+ return null;
}
let inCount = 0;