summary['pgmap'] = {
'pgs_by_state': data.get('pgmap', {}).get('pgs_by_state', []),
'num_pools': data.get('pgmap', {}).get('num_pools'),
- 'num_pgs': data.get('pgmap', {}).get('num_pgs'),
+ 'write_bytes_sec': data.get('pgmap', {}).get('write_bytes_sec'),
+ 'read_bytes_sec': data.get('pgmap', {}).get('read_bytes_sec'),
'bytes_used': data.get('pgmap', {}).get('bytes_used'),
'bytes_total': data.get('pgmap', {}).get('bytes_total'),
}
num_pools: 14,
bytes_used: 3236978688,
bytes_total: 325343772672,
- num_pgs: 497
+ num_pgs: 497,
+ write_bytes_sec: 0,
+ read_bytes_sec: 0
},
mgrmap: {
num_active: 1,
</button>
</ng-template>
<!-- TOTAL COUNT -->
- @if (vm?.total) {
+ @if (vm?.total || vm?.total === 0) {
<div>
<span class="cds--type-heading-07">{{ vm.total }}</span>
<cd-icon [type]="vm.icon"></cd-icon>
@let hwEnabled = (enabled$ | async);
@let hwSections = (sections$ | async);
-@let colorClass="overview-health-card-status--" + vm?.health?.icon;
+@let colorClass="overview-health-card-status--" + vm?.clusterHealth?.icon;
<cd-productive-card class="overview-health-card">
[minLineWidth]="400"></cds-skeleton-text>
}
<!------------------------- HEALTH CARD CLUSTER STATUS ------------------>
- @if(vm?.health){
+ @if(vm?.clusterHealth){
<p class="cds--type-heading-05 cds-mb-0"
[ngClass]="colorClass">
- {{vm?.health?.title}}
- <cd-icon [type]="vm?.health?.icon"></cd-icon>
+ {{vm?.clusterHealth?.title}}
+ <cd-icon [type]="colorClass"></cd-icon>
</p>
- <p class="cds--type-label-01 overview-health-card-secondary-text">{{vm?.health?.message}}</p>
+ <p class="cds--type-label-01 overview-health-card-secondary-text">{{vm?.clusterHealth?.message}}</p>
} @else {
<cds-skeleton-placeholder></cds-skeleton-placeholder>
}
<div class="overview-health-card-tab"
[class.overview-health-card-tab-selected]="activeSection === 'resiliency'">
<div class="cds-mb-1"><cd-icon
- [type]="vm?.overallSystemSev"></cd-icon></div>
+ [type]="vm?.resiliencyHealth?.icon"></cd-icon></div>
<cds-tooltip-definition
[highContrast]="true"
[openOnHover]="true"
<ng-container *ngSwitchCase="'resiliency'">
<div class="overview-health-card-tab-content">
<span class="overview-health-card-icon-and-text">
- <cd-icon type="warningAltFilled"></cd-icon>
+ <cd-icon [type]="vm?.resiliencyHealth?.icon"></cd-icon>
<span class="cds--type-body-compact-01">
- Status unavailable for some data
+ {{vm?.resiliencyHealth?.title}}
</span>
</span>
- <p
- class="overview-health-card-secondary-text cds--type-label-01"
- i18n>Ceph cannot reliably determine the current state of some data. Availability may be affected.</p>
+ <p class="overview-health-card-secondary-text cds--type-label-01">
+ {{vm?.resiliencyHealth?.description}}</p>
+ <button
+ cdsButton="tertiary"
+ size="sm"
+ (click)="onViewPGStatesClick()">
+ <span
+ i18n
+ class="cds-ml-3">See all PGs states</span>
+ <cd-icon type="arrowUpRight"></cd-icon>
+ </button>
</div>
</ng-container>
<ng-container *ngSwitchDefault></ng-container>
error: number;
};
-const DATA_RESILIENCY = {
- ok: {
- icon: 'success',
- title: $localize`Data is fully replicated and available.`,
- description: $localize`All replicas are in place and I/O is operating normally. No action is required.`
- },
- progress: {
- icon: 'sync',
- title: $localize`Data integrity checks in progress`,
- description: $localize`Ceph is running routine consistency checks on stored data and metadata to ensure data integrity. Data remains safe and accessible.`
- },
- warn: {
- icon: 'warning',
- title: $localize`Restoring data redundancy`,
- description: $localize`Some data replicas are missing or not yet in their final location. Ceph is actively rebalancing data to return to a healthy state.`
- },
- warnDataLoss: {
- icon: 'warning',
- title: $localize`Status unavailable for some data`,
- description: $localize`Ceph cannot reliably determine the current state of some data. Availability may be affected.`
- },
- error: {
- icon: 'error',
- title: $localize`Data unavailable or inconsistent, manual intervention required`,
- description: $localize`Some data is currently unavailable or inconsistent. Ceph could not automatically restore these resources, and manual intervention is required to restore data availability and consistency.`
- }
-};
-
@Component({
selector: 'cd-overview-health-card',
imports: [
@Input({ required: true }) vm!: HealthCardVM;
@Output() viewIncidents = new EventEmitter<void>();
+ @Output() viewPGStates = new EventEmitter<void>();
@Output() activeSectionChange = new EventEmitter<HealthCardTabSection | null>();
- activeSection: HealthCardTabSection | null = null;
- data = DATA_RESILIENCY;
+ activeSection: HealthCardTabSection | null = 'resiliency';
healthItems: HealthItemConfig[] = [
{ key: 'mon', label: $localize`Monitor` },
this.activeSectionChange.emit(this.activeSection);
}
+ onViewIncidentsClick() {
+ this.viewIncidents.emit();
+ }
+
+ onViewPGStatesClick() {
+ this.viewPGStates.emit();
+ }
+
readonly data$: Observable<OverviewHealthData> = combineLatest([
this.summaryService.summaryData$.pipe(filter((summary): summary is Summary => !!summary)),
this.upgradeService.listCached().pipe(
)
]).pipe(map(([summary, upgrade]) => ({ summary, upgrade })));
- onViewIncidentsClick() {
- this.viewIncidents.emit();
- }
-
private readonly permissions = this.authStorageService.getPermissions();
readonly enabled$: Observable<boolean> = this.permissions?.configOpt?.read
<div cdsGrid
[fullWidth]="true"
[narrow]="true"
- class="cds-mt-5 cds-mb-5">
+ class="cds-mt-5 cds-mb-5 overview">
<div cdsRow>
<div cdsCol
class="cds-mb-5"
[columnNumbers]="{lg: 11}">
<cd-overview-health-card
[vm]="health"
- (viewIncidents)="togglePanel()"
+ (viewIncidents)="toggleHealthPanel()"
+ (viewPGStates)="togglePGStatesPanel()"
(activeSectionChange)="activeHealthTab = $event"
></cd-overview-health-card>
</div>
[headerText]="'Health incidents ('+ health?.incidents +')'"
[expanded]="isHealthPanelOpen"
size="md"
- (closed)="togglePanel()">
+ (closed)="toggleHealthPanel()">
<div panel-header-description
class="cds--type-body-01">
<span i18n>Health incidents are Ceph health checks warnings indicating conditions that require attention and remain until resolved.</span>
</div>
<div class="panel-content">
- @for (check of health?.checks; track key) {
+ @for (check of health?.checks; track check.name) {
<div>
<div class="overview-check-header">
<cd-icon [type]="check?.icon"></cd-icon>
</div>
</cd-side-panel>
}
+@if (isPGStatePanelOpen) {
+ <cd-side-panel
+ [headerText]="'Placement groups ('+ health?.pgs?.total +')'"
+ [expanded]="isPGStatePanelOpen"
+ size="md"
+ (closed)="togglePGStatesPanel()">
+ <div panel-header-description
+ class="cds--type-body-01">
+ <span i18n>
+ Placement groups are how Ceph groups and distributes data across storage devices to manage replication, recovery, and performance.
+ </span>
+ </div>
+ <div class="panel-content">
+ <div
+ class="overview-pg-side-panel-rw cds-mb-4"
+ cdsStack="horizontal"
+ gap="4">
+ @for (data of health?.pgs?.io; track $index ; let isLast = $last) {
+ <div
+ class="overview-pg-side-panel-rw-item"
+ [class.overview-pg-side-panel-rw-item--border]="!isLast">
+ <p class="cds--type-label-01 cds-mb-2">{{data.label}}</p>
+ <p class="cds--type-heading-03 cds-mb-0">{{data.value}}</p>
+ </div>
+ }
+ </div>
+ <cd-table
+ [data]="health?.pgs?.states"
+ [columns]="tableColumns"
+ size="xs">
+ </cd-table>
+ </div>
+ </cd-side-panel>
+}
&-check-description {
color: var(--cds-text-secondary);
}
+
+ &-pg-side-panel-rw {
+ padding: var(--cds-spacing-04);
+ background-color: var(--cds-layer-01);
+ width: 100%;
+ }
+
+ &-pg-side-panel-rw-item {
+ max-block-size: fit-content;
+ padding-left: var(--cds-spacing-04);
+ }
+
+ &-pg-side-panel-rw-item--border {
+ border-right: 1px solid var(--cds-border-subtle-01);
+ }
+
+ // Overrides
+ cds-panel .cds--header-panel {
+ background-color: var(--cds-background);
+ }
+
+ cds-panel .panel-content {
+ padding: 0 !important;
+ }
+
+ cds-panel .panel-header {
+ margin-bottom: var(--cds-spacing-03);
+ }
}
);
expect(vm.checks[0].icon).toEqual(expect.any(String));
- expect(vm.health).toEqual(HealthMap['HEALTH_OK']);
+ expect(vm.clusterHealth).toEqual(HealthMap['HEALTH_OK']);
expect(vm.mon).toEqual(
expect.objectContaining({
// -----------------------------
it('should toggle panel open/close', () => {
expect(component.isHealthPanelOpen).toBe(false);
- component.togglePanel();
+ component.toggleHealthPanel();
expect(component.isHealthPanelOpen).toBe(true);
- component.togglePanel();
+ component.toggleHealthPanel();
expect(component.isHealthPanelOpen).toBe(false);
});
import { CommonModule } from '@angular/common';
-import { ChangeDetectionStrategy, Component, DestroyRef, inject } from '@angular/core';
-import { GridModule, TilesModule } from 'carbon-components-angular';
+import {
+ ChangeDetectionStrategy,
+ Component,
+ DestroyRef,
+ inject,
+ ViewEncapsulation
+} from '@angular/core';
+import { GridModule, LayoutModule, TilesModule } from 'carbon-components-angular';
import { EMPTY, Observable } from 'rxjs';
import { catchError, exhaustMap, map, shareReplay } from 'rxjs/operators';
import { RefreshIntervalService } from '~/app/shared/services/refresh-interval.service';
import { HealthCheck, HealthSnapshotMap } from '~/app/shared/models/health.interface';
import {
- HealthCardCheckVM,
+ getClusterHealth,
+ getHealthChecksAndIncidents,
+ getResiliencyDisplay,
HealthCardTabSection,
HealthCardVM,
- HealthDisplayVM,
- HealthIconMap,
- HealthMap,
HealthStatus,
+ maxSeverity,
+ safeDifference,
+ SEVERITY,
Severity,
SeverityIconMap
} from '~/app/shared/models/overview';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { OverviewAlertsCardComponent } from './alerts-card/overview-alerts-card.component';
import { PerformanceCardComponent } from '~/app/shared/components/performance-card/performance-card.component';
-
-const sev = {
- ok: 0 as Severity,
- warn: 1 as Severity,
- err: 2 as Severity
-} as const;
-
-const maxSeverity = (...values: Severity[]): Severity => Math.max(...values) as Severity;
-
-function buildHealthDisplay(status: HealthStatus): HealthDisplayVM {
- return HealthMap[status] ?? HealthMap['HEALTH_OK'];
-}
-
-function safeDifference(a: number, b: number): number | null {
- return a != null && b != null ? a - b : null;
-}
+import { DataTableModule } from '~/app/shared/datatable/datatable.module';
/**
* Mapper: HealthSnapshotMap -> HealthCardVM
*/
export function buildHealthCardVM(d: HealthSnapshotMap): HealthCardVM {
const checksObj: Record<string, HealthCheck> = d.health?.checks ?? {};
- const healthDisplay = buildHealthDisplay(d.health.status as HealthStatus);
-
- // --- Health panel ---
-
- // Count incidents
- let incidents = 0;
- const checks: HealthCardCheckVM[] = [];
-
- for (const [name, check] of Object.entries(checksObj)) {
- incidents++;
- checks.push({
- name,
- description: check?.summary?.message ?? '',
- icon: HealthIconMap[check?.severity] ?? ''
- });
- }
+ const clusterHealth = getClusterHealth(d.health.status as HealthStatus);
+ const { incidents, checks } = getHealthChecksAndIncidents(checksObj);
+ const resiliencyHealth = getResiliencyDisplay(checks);
// --- System sub-states ---
// MON
const monTotal = d.monmap?.num_mons ?? 0;
const monQuorum = (d.monmap as any)?.quorum?.length ?? 0;
- const monSev: Severity = monQuorum < monTotal ? sev.warn : sev.ok;
+ const monSev: Severity = monQuorum < monTotal ? SEVERITY.warn : SEVERITY.ok;
// MGR
const mgrActive = d.mgrmap?.num_active ?? 0;
const mgrStandby = d.mgrmap?.num_standbys ?? 0;
- const mgrSev: Severity = mgrActive < 1 ? sev.err : mgrStandby < 1 ? sev.warn : sev.ok;
+ const mgrSev: Severity =
+ mgrActive < 1 ? SEVERITY.err : mgrStandby < 1 ? SEVERITY.warn : SEVERITY.ok;
// OSD
const osdUp = (d.osdmap as any)?.up ?? 0;
const osdTotal = (d.osdmap as any)?.num_osds ?? 0;
const osdDown = safeDifference(osdTotal, osdUp);
const osdOut = safeDifference(osdTotal, osdIn);
- const osdSev: Severity = osdDown > 0 || osdOut > 0 ? sev.err : sev.ok;
+ const osdSev: Severity = osdDown > 0 || osdOut > 0 ? SEVERITY.err : SEVERITY.ok;
// HOSTS
const hostsTotal = d.num_hosts ?? 0;
const hostsAvailable = (d as any)?.num_hosts_available ?? 0;
- const hostsSev: Severity = hostsAvailable < hostsTotal ? sev.warn : sev.ok;
+ const hostsSev: Severity = hostsAvailable < hostsTotal ? SEVERITY.warn : SEVERITY.ok;
// Overall = worst of the subsystem severities.
const overallSystemSev = maxSeverity(monSev, mgrSev, osdSev, hostsSev);
+ // Resiliency
+
return {
fsid: d.fsid,
overallSystemSev: SeverityIconMap[overallSystemSev],
incidents,
checks,
- health: healthDisplay,
+ pgs: {
+ total: d?.pgmap?.num_pgs,
+ states: d?.pgmap?.pgs_by_state,
+ io: [
+ { label: $localize`Client write`, value: d?.pgmap?.write_bytes_sec },
+ { label: $localize`Client read`, value: d?.pgmap?.read_bytes_sec },
+ { label: $localize`Recovery I/O`, value: 0 }
+ ]
+ },
+
+ clusterHealth,
+ resiliencyHealth,
mon: { value: $localize`Quorum: ${monQuorum}/${monTotal}`, severity: SeverityIconMap[monSev] },
mgr: {
OverviewHealthCardComponent,
ComponentsModule,
OverviewAlertsCardComponent,
- PerformanceCardComponent
+ PerformanceCardComponent,
+ LayoutModule,
+ DataTableModule
],
standalone: true,
templateUrl: './overview.component.html',
styleUrl: './overview.component.scss',
- changeDetection: ChangeDetectionStrategy.OnPush
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ encapsulation: ViewEncapsulation.None
})
export class OverviewComponent {
isHealthPanelOpen = false;
+ isPGStatePanelOpen = false;
activeHealthTab: HealthCardTabSection | null = null;
+ tableColumns = [
+ { prop: 'count', name: $localize`PGs count` },
+ { prop: 'state_name', name: $localize`Status` }
+ ];
private readonly healthService = inject(HealthService);
private readonly refreshIntervalService = inject(RefreshIntervalService);
);
}
- togglePanel(): void {
+ toggleHealthPanel(): void {
this.isHealthPanelOpen = !this.isHealthPanelOpen;
}
+
+ togglePGStatesPanel(): void {
+ this.isPGStatePanelOpen = !this.isPGStatePanelOpen;
+ }
}
import VmdkDisk16 from '@carbon/icons/es/vmdk-disk/16';
import WarningAlt16 from '@carbon/icons/es/warning--alt/16';
import CheckMarkOutline16 from '@carbon/icons/es/checkmark--outline/16';
+import ArrowUpRight16 from '@carbon/icons/es/arrow--up-right/16';
+import InProgress16 from '@carbon/icons/es/in-progress/16';
import { TearsheetStepComponent } from './tearsheet-step/tearsheet-step.component';
import { PageHeaderComponent } from './page-header/page-header.component';
Plug16,
VmdkDisk16,
WarningAlt16,
- CheckMarkOutline16
+ CheckMarkOutline16,
+ ArrowUpRight16,
+ InProgress16
]);
}
}
plug = 'plug',
vmdkDisk = 'vmdk-disk',
checkMarkOutline = 'checkmark--outline',
- warningAlt = 'warning--alt'
+ warningAlt = 'warning--alt',
+ arrowUpRight = 'arrow--up-right',
+ inProgress = 'in-progress'
}
export enum IconSize {
plug: 'plug',
vmdkDisk: 'vmdk-disk',
warningAlt: 'warning--alt',
- checkMarkOutline: 'checkmark--outline'
+ checkMarkOutline: 'checkmark--outline',
+ arrowUpRight: ' arrow--up-right',
+ inProgress: 'in-progress'
} as const;
bytes_used: number;
bytes_total: number;
num_pgs: number;
+ write_bytes_sec: number;
+ read_bytes_sec: number;
}
export interface HealthMapCommon {
+import { HealthCheck, PgStateCount } from './health.interface';
+
export type HealthStatus = 'HEALTH_OK' | 'HEALTH_WARN' | 'HEALTH_ERR';
export const HealthIconMap = {
export const SeverityIconMap = {
0: 'success',
1: 'warningAltFilled',
- 2: 'error'
+ 2: 'error',
+ 3: 'inProgress'
};
-/** 0 ok, 1 warn, 2 err */
-export type Severity = 0 | 1 | 2;
+/** 0 ok, 1 warn, 2 err , 3 sync*/
+export type Severity = 0 | 1 | 2 | 3;
export type Health = {
message: string;
severity: string;
}
+type ResileincyHealthType = {
+ title: string;
+ description: string;
+ icon: string;
+};
+
export interface HealthCardVM {
fsid: string;
overallSystemSev: string;
incidents: number;
checks: HealthCardCheckVM[];
- health: HealthDisplayVM;
+ clusterHealth: HealthDisplayVM;
+
+ resiliencyHealth: ResileincyHealthType;
+
+ pgs: {
+ total: number;
+ states: PgStateCount[];
+ io: Array<{ label: string; value: number }>;
+ };
mon: HealthCardSubStateVM;
mgr: HealthCardSubStateVM;
}
export type HealthCardTabSection = 'system' | 'hardware' | 'resiliency';
+
+export const SEVERITY = {
+ ok: 0 as Severity,
+ warn: 1 as Severity,
+ err: 2 as Severity,
+ sync: 3 as Severity
+} as const;
+
+export const RESILIENCY_CHECK = {
+ error: ['PG_DAMAGED', 'PG_RECOVERY_FULL'],
+ warn: ['PG_DEGRADED', 'PG_AVAILABILITY', 'PG_BACKFILL_FULL']
+};
+
+const DATA_RESILIENCY_STATE = {
+ ok: 'ok',
+ error: 'error',
+ warn: 'warn',
+ warnDataLoss: 'warnDataLoss',
+ progress: 'progress'
+};
+
+export const DATA_RESILIENCY = {
+ [DATA_RESILIENCY_STATE.ok]: {
+ icon: 'success',
+ title: $localize`Data is fully replicated and available.`,
+ description: $localize`All replicas are in place and I/O is operating normally. No action is required.`
+ },
+ [DATA_RESILIENCY_STATE.progress]: {
+ icon: 'inProgress',
+ title: $localize`Data integrity checks in progress`,
+ description: $localize`Ceph is running routine consistency checks on stored data and metadata to ensure data integrity. Data remains safe and accessible.`
+ },
+ [DATA_RESILIENCY_STATE.warn]: {
+ icon: 'warning',
+ title: $localize`Restoring data redundancy`,
+ description: $localize`Some data replicas are missing or not yet in their final location. Ceph is actively rebalancing data to return to a healthy state.`
+ },
+ [DATA_RESILIENCY_STATE.warnDataLoss]: {
+ icon: 'warning',
+ title: $localize`Status unavailable for some data`,
+ description: $localize`Ceph cannot reliably determine the current state of some data. Availability may be affected.`
+ },
+ [DATA_RESILIENCY_STATE.error]: {
+ icon: 'error',
+ title: $localize`Data unavailable or inconsistent, manual intervention required`,
+ description: $localize`Some data is currently unavailable or inconsistent. Ceph could not automatically restore these resources, and manual intervention is required to restore data availability and consistency.`
+ }
+};
+
+export const maxSeverity = (...values: Severity[]): Severity => Math.max(...values) as Severity;
+
+export function getClusterHealth(status: HealthStatus): HealthDisplayVM {
+ return HealthMap[status] ?? HealthMap['HEALTH_OK'];
+}
+
+export function getResiliencyDisplay(checks: HealthCardCheckVM[] = []): ResileincyHealthType {
+ let resileincyState: string = DATA_RESILIENCY_STATE.ok;
+ checks.forEach((check) => {
+ switch (check?.name) {
+ case RESILIENCY_CHECK.error[0]:
+ case RESILIENCY_CHECK.error[1]:
+ resileincyState = DATA_RESILIENCY_STATE.error;
+ break;
+ case RESILIENCY_CHECK.warn[0]:
+ resileincyState = DATA_RESILIENCY_STATE.warn;
+ break;
+ case RESILIENCY_CHECK.warn[1]:
+ resileincyState = DATA_RESILIENCY_STATE.warnDataLoss;
+ break;
+ }
+ });
+ return DATA_RESILIENCY[resileincyState];
+}
+
+export function getHealthChecksAndIncidents(checksObj: Record<string, HealthCheck>) {
+ const checks: HealthCardCheckVM[] = [];
+ let incidents = 0;
+ for (const [name, check] of Object.entries(checksObj)) {
+ incidents++;
+ checks.push({
+ name,
+ description: check?.summary?.message ?? '',
+ icon: HealthIconMap[check?.severity] ?? ''
+ });
+ }
+
+ return { incidents, checks };
+}
+
+export function safeDifference(a: number, b: number): number | null {
+ return a != null && b != null ? a - b : null;
+}
padding: 0;
}
+.cds-pl-6 {
+ padding-left: layout.$spacing-06;
+}
+
.cds-pt-2px {
padding-top: 2px;
}