From: Afreen Misbah Date: Thu, 26 Feb 2026 01:38:44 +0000 (+0530) Subject: mgr/dashboard: Add data resileincy panel X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=361f2fd59a042c0eee9bfac352e91d5efe7057a0;p=ceph.git mgr/dashboard: Add data resileincy panel - adds table to show PG states and counts - adds recovery io,read/write IO Signed-off-by: Afreen Misbah (cherry picked from commit 03818d2e6a20b9018457c3e3f8f56dc186ae341c) Conflicts: src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_spacings.scss --- diff --git a/src/pybind/mgr/dashboard/controllers/health.py b/src/pybind/mgr/dashboard/controllers/health.py index abc6dfee823c..5a84ee921fe4 100644 --- a/src/pybind/mgr/dashboard/controllers/health.py +++ b/src/pybind/mgr/dashboard/controllers/health.py @@ -403,7 +403,8 @@ class Health(BaseController): 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'), } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.spec.ts index 1f168561cd4c..10bf9d558343 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.spec.ts @@ -80,7 +80,9 @@ describe('Dashbord Component', () => { 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, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/alerts-card/overview-alerts-card.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/alerts-card/overview-alerts-card.component.html index 7018f7fdaf82..434b4469bd12 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/alerts-card/overview-alerts-card.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/alerts-card/overview-alerts-card.component.html @@ -16,7 +16,7 @@ - @if (vm?.total) { + @if (vm?.total || vm?.total === 0) {
{{ vm.total }} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.html index 1673b3f7fb34..fca3e2800d9d 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.html @@ -2,7 +2,7 @@ @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; @@ -41,13 +41,13 @@ [minLineWidth]="400"> } - @if(vm?.health){ + @if(vm?.clusterHealth){

- {{vm?.health?.title}} - + {{vm?.clusterHealth?.title}} +

-

{{vm?.health?.message}}

+

{{vm?.clusterHealth?.message}}

} @else { } @@ -163,7 +163,7 @@
+ [type]="vm?.resiliencyHealth?.icon">
- + - Status unavailable for some data + {{vm?.resiliencyHealth?.title}} -

Ceph cannot reliably determine the current state of some data. Availability may be affected.

+

+ {{vm?.resiliencyHealth?.description}}

+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.ts index e044b5efd850..af9c9871703f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.ts @@ -54,34 +54,6 @@ type HwRowVM = { 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: [ @@ -113,10 +85,10 @@ export class OverviewHealthCardComponent { @Input({ required: true }) vm!: HealthCardVM; @Output() viewIncidents = new EventEmitter(); + @Output() viewPGStates = new EventEmitter(); @Output() activeSectionChange = new EventEmitter(); - activeSection: HealthCardTabSection | null = null; - data = DATA_RESILIENCY; + activeSection: HealthCardTabSection | null = 'resiliency'; healthItems: HealthItemConfig[] = [ { key: 'mon', label: $localize`Monitor` }, @@ -130,6 +102,14 @@ export class OverviewHealthCardComponent { this.activeSectionChange.emit(this.activeSection); } + onViewIncidentsClick() { + this.viewIncidents.emit(); + } + + onViewPGStatesClick() { + this.viewPGStates.emit(); + } + readonly data$: Observable = combineLatest([ this.summaryService.summaryData$.pipe(filter((summary): summary is Summary => !!summary)), this.upgradeService.listCached().pipe( @@ -138,10 +118,6 @@ export class OverviewHealthCardComponent { ) ]).pipe(map(([summary, upgrade]) => ({ summary, upgrade }))); - onViewIncidentsClick() { - this.viewIncidents.emit(); - } - private readonly permissions = this.authStorageService.getPermissions(); readonly enabled$: Observable = this.permissions?.configOpt?.read 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 bf243f8d0143..066a632518a4 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 @@ -3,14 +3,15 @@
+ class="cds-mt-5 cds-mb-5 overview">
@@ -43,13 +44,13 @@ [headerText]="'Health incidents ('+ health?.incidents +')'" [expanded]="isHealthPanelOpen" size="md" - (closed)="togglePanel()"> + (closed)="toggleHealthPanel()">
Health incidents are Ceph health checks warnings indicating conditions that require attention and remain until resolved.
- @for (check of health?.checks; track key) { + @for (check of health?.checks; track check.name) {
@@ -63,3 +64,37 @@
} +@if (isPGStatePanelOpen) { + +
+ + Placement groups are how Ceph groups and distributes data across storage devices to manage replication, recovery, and performance. + +
+
+
+ @for (data of health?.pgs?.io; track $index ; let isLast = $last) { +
+

{{data.label}}

+

{{data.value}}

+
+ } +
+ + +
+
+} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.scss index 094b0957927c..425800f9f11c 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.scss +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.scss @@ -15,4 +15,32 @@ &-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); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.spec.ts index ac36e65d5ef2..82116c2d4ec2 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.spec.ts @@ -114,7 +114,7 @@ describe('OverviewComponent', () => { ); 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({ @@ -198,9 +198,9 @@ describe('OverviewComponent', () => { // ----------------------------- 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); }); 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 3a327ce4e261..2a514774bf5d 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,6 +1,12 @@ 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'; @@ -8,13 +14,15 @@ import { HealthService } from '~/app/shared/api/health.service'; 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'; @@ -25,22 +33,7 @@ import { ComponentsModule } from '~/app/shared/components/components.module'; 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 @@ -48,34 +41,22 @@ function safeDifference(a: number, b: number): number | null { */ export function buildHealthCardVM(d: HealthSnapshotMap): HealthCardVM { const checksObj: Record = 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; @@ -83,16 +64,18 @@ export function buildHealthCardVM(d: HealthSnapshotMap): HealthCardVM { 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], @@ -100,7 +83,18 @@ export function buildHealthCardVM(d: HealthSnapshotMap): HealthCardVM { 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: { @@ -125,16 +119,24 @@ export function buildHealthCardVM(d: HealthSnapshotMap): HealthCardVM { 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); @@ -164,7 +166,11 @@ export class OverviewComponent { ); } - togglePanel(): void { + toggleHealthPanel(): void { this.isHealthPanelOpen = !this.isHealthPanelOpen; } + + togglePGStatesPanel(): void { + this.isPGStatePanelOpen = !this.isPGStatePanelOpen; + } } 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 07672ca99b4e..9c459f995068 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 @@ -117,6 +117,8 @@ import Plug16 from '@carbon/icons/es/plug/16'; 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'; @NgModule({ imports: [ @@ -280,7 +282,9 @@ export class ComponentsModule { Plug16, VmdkDisk16, WarningAlt16, - CheckMarkOutline16 + CheckMarkOutline16, + ArrowUpRight16, + InProgress16 ]); } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts index 55d2f19cd2df..60eb5d259b66 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts @@ -118,7 +118,9 @@ export enum Icons { plug = 'plug', vmdkDisk = 'vmdk-disk', checkMarkOutline = 'checkmark--outline', - warningAlt = 'warning--alt' + warningAlt = 'warning--alt', + arrowUpRight = 'arrow--up-right', + inProgress = 'in-progress' } export enum IconSize { @@ -155,5 +157,7 @@ export const ICON_TYPE = { plug: 'plug', vmdkDisk: 'vmdk-disk', warningAlt: 'warning--alt', - checkMarkOutline: 'checkmark--outline' + checkMarkOutline: 'checkmark--outline', + arrowUpRight: ' arrow--up-right', + inProgress: 'in-progress' } as const; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/health.interface.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/health.interface.ts index c784b06685e1..adfba20ffc5e 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/health.interface.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/health.interface.ts @@ -38,6 +38,8 @@ export interface PgMap { bytes_used: number; bytes_total: number; num_pgs: number; + write_bytes_sec: number; + read_bytes_sec: number; } export interface HealthMapCommon { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/overview.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/overview.ts index 3effe7d82c8b..23af3a90c56b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/overview.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/overview.ts @@ -1,3 +1,5 @@ +import { HealthCheck, PgStateCount } from './health.interface'; + export type HealthStatus = 'HEALTH_OK' | 'HEALTH_WARN' | 'HEALTH_ERR'; export const HealthIconMap = { @@ -9,11 +11,12 @@ 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; @@ -58,6 +61,12 @@ export interface HealthCardSubStateVM { severity: string; } +type ResileincyHealthType = { + title: string; + description: string; + icon: string; +}; + export interface HealthCardVM { fsid: string; overallSystemSev: string; @@ -65,7 +74,15 @@ export interface HealthCardVM { 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; @@ -74,3 +91,95 @@ export interface HealthCardVM { } 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) { + 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; +}