]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Add data resileincy panel
authorAfreen Misbah <afreen@ibm.com>
Thu, 26 Feb 2026 01:38:44 +0000 (07:08 +0530)
committerAfreen Misbah <afreen@ibm.com>
Fri, 6 Mar 2026 05:58:16 +0000 (11:28 +0530)
- adds table to show PG states and counts
- adds recovery io,read/write IO

Signed-off-by: Afreen Misbah <afreen@ibm.com>
14 files changed:
src/pybind/mgr/dashboard/controllers/health.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/alerts-card/overview-alerts-card.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.scss
src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/health.interface.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/overview.ts
src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_spacings.scss

index abc6dfee823c8b8ad4b97c4edb82112c4853e8d3..5a84ee921fe4fe7a5aec36120ee11fd854027b55 100644 (file)
@@ -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'),
             }
index 14300de21db071fcc587344ef566139e78eb233b..f8e1f5f44b73d7d41197852bfe547b0bde795f87 100644 (file)
@@ -81,7 +81,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,
index 7018f7fdaf82756e9537a5126ae6b0b12366d6a9..434b4469bd12f68fc7c580fcbc326ee131c56d0f 100644 (file)
@@ -16,7 +16,7 @@
     </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>
index 1673b3f7fb3495465fccc3569acb8d676e995a88..fca3e2800d9d28419d91df154313dcd241df3b4c 100644 (file)
@@ -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;
 
 
 <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>
index e044b5efd85077295fbabde623fff0e565d0aab2..af9c9871703f19abcdc0b433e80ec989bc3e4d9c 100644 (file)
@@ -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<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` },
@@ -130,6 +102,14 @@ export class OverviewHealthCardComponent {
     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(
@@ -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<boolean> = this.permissions?.configOpt?.read
index bf243f8d0143e3c9c69771d722df08bbf0d950af..066a632518a494efb02c9a8ca1bbfd60a03fa547 100644 (file)
@@ -3,14 +3,15 @@
 <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>
+}
index 094b0957927c60e127a1f6ccad528d377870bc5d..425800f9f11cb4f5bf6da97b61ff86444a6b8a52 100644 (file)
   &-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);
+  }
 }
index ac36e65d5ef23d338a52aafb71160887f5def81c..82116c2d4ec267e952f455200ba89ac58066f4ee 100644 (file)
@@ -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);
   });
 
index 3a327ce4e2616fc780d4ad3f69113de92cf7ecc9..2a514774bf5d4efc808058960df53e9f98219ffd 100644 (file)
@@ -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<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;
@@ -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;
+  }
 }
index 45fe3a93e4aeef5d41d3365cbb3092e4e3b8df4d..b5dd02c44438d3717ae2b5ba7d1517031a76b006 100644 (file)
@@ -121,6 +121,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';
 
 import { TearsheetStepComponent } from './tearsheet-step/tearsheet-step.component';
 import { PageHeaderComponent } from './page-header/page-header.component';
@@ -300,7 +302,9 @@ export class ComponentsModule {
       Plug16,
       VmdkDisk16,
       WarningAlt16,
-      CheckMarkOutline16
+      CheckMarkOutline16,
+      ArrowUpRight16,
+      InProgress16
     ]);
   }
 }
index eec24923f43baf40f2e12862d343f013ce96578c..eec9d2fc96e4408955ed3605c50f3fb7bfc4d56a 100644 (file)
@@ -122,7 +122,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 {
@@ -160,5 +162,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;
index c784b06685e1675e0ec4816b0cfe8118d060a34c..adfba20ffc5e23acb5ee2778d4a0046ef8f24304 100644 (file)
@@ -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 {
index 3effe7d82c8bfd587e000e984c74c6d64a74561c..23af3a90c56b6c77e2871dee20b1e6eabe20ab4a 100644 (file)
@@ -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<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;
+}
index 7d33231d04ff748ec7377d92aa4a73271eabe6d8..10d29cb10016e53dd8f394fc89810080da624277 100644 (file)
@@ -5,6 +5,10 @@
   padding: 0;
 }
 
+.cds-pl-6 {
+  padding-left: layout.$spacing-06;
+}
+
 .cds-pt-2px {
   padding-top: 2px;
 }