]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Fix snapshot Api firing twice 67541/head
authorAfreen Misbah <afreen@ibm.com>
Tue, 3 Mar 2026 16:45:48 +0000 (22:15 +0530)
committerAfreen Misbah <afreen@ibm.com>
Fri, 6 Mar 2026 06:03:38 +0000 (11:33 +0530)
- two subs being created

Signed-off-by: Afreen Misbah <afreen@ibm.com>
16 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/health-card/overview-health-card.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.scss
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/ceph/shared/pg-category.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/icon/icon.component.scss
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/openapi.yaml

index 5a84ee921fe4fe7a5aec36120ee11fd854027b55..b9509511123ec4cd4c2eb353585163a950e71351 100644 (file)
@@ -144,6 +144,7 @@ HEALTH_SNAPSHOT_SCHEMA = ({
         'num_pgs': (int, 'Total PG count'),
         'bytes_used': (int, 'Used capacity in bytes'),
         'bytes_total': (int, 'Total capacity in bytes'),
+        'recovering_bytes_per_sec': (int, 'Total recovery in bytes'),
     }, 'Placement group map details'),
     'mgrmap': ({
         'num_active': (int, 'Number of active managers'),
@@ -407,6 +408,8 @@ class Health(BaseController):
                 '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_pgs': data.get('pgmap', {}).get('num_pgs'),
+                'recovering_bytes_per_sec': data.get('pgmap', {}).get('recovering_bytes_per_sec'),
             }
 
         if self._has_permissions(Permission.READ, Scope.MANAGER):
@@ -457,7 +460,7 @@ class Health(BaseController):
             summary['num_hosts'] = len(hosts)
             available_hosts = [
                 h for h in hosts
-                if h.get("status") == "Available"
+                if h.get("status") == ""
             ]
             summary['num_hosts_available'] = len(available_hosts)
 
index f8e1f5f44b73d7d41197852bfe547b0bde795f87..9a9ede1362bad42a110c7df143b8c2541d84d5ad 100644 (file)
@@ -83,7 +83,8 @@ describe('Dashbord Component', () => {
       bytes_total: 325343772672,
       num_pgs: 497,
       write_bytes_sec: 0,
-      read_bytes_sec: 0
+      read_bytes_sec: 0,
+      recovering_bytes_per_sec: 0
     },
     mgrmap: {
       num_active: 1,
index fca3e2800d9d28419d91df154313dcd241df3b4c..3b02bf92443696afe1962b6e51b8c951d0980b05 100644 (file)
@@ -45,7 +45,7 @@
   <p class="cds--type-heading-05 cds-mb-0"
      [ngClass]="colorClass">
     {{vm?.clusterHealth?.title}}
-    <cd-icon [type]="colorClass"></cd-icon>
+    <cd-icon [type]="vm?.clusterHealth?.icon"></cd-icon>
   </p>
   <p class="cds--type-label-01 overview-health-card-secondary-text">{{vm?.clusterHealth?.message}}</p>
   } @else {
     </ng-container>
     <!-- RESILIENCY TAB CONTENT -->
     <ng-container *ngSwitchCase="'resiliency'">
-      <div class="overview-health-card-tab-content">
-        <span class="overview-health-card-icon-and-text">
-          <cd-icon [type]="vm?.resiliencyHealth?.icon"></cd-icon>
-          <span class="cds--type-body-compact-01">
-            {{vm?.resiliencyHealth?.title}}
+      <div class="overview-health-card-tab-content overview-health-card-tab-content-item-row">
+        <div>
+          <span class="overview-health-card-icon-and-text">
+            <cd-icon [type]="vm?.resiliencyHealth?.icon"></cd-icon>
+            <span class="cds--type-body-compact-01">
+              {{vm?.resiliencyHealth?.title}}
+            </span>
           </span>
-        </span>
-        <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>
+          <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>
+        @if (vm?.pgs?.activeCleanChartData && vm?.pgs?.activeCleanChartOptions) {
+        <div class="overview-health-card-tab-content-item-row">
+          <ibm-gauge-chart
+            class="overview-health-card-resiliency-chart"
+            [options]="vm?.pgs?.activeCleanChartOptions"
+            [data]="vm?.pgs?.activeCleanChartData"></ibm-gauge-chart>
+          <div class="overview-health-card-resiliency-chart-text">
+            <p
+              i18n
+              class="cds--type-helper-text-01 overview-health-card-secondary-text"><em>Data resiliency</em> reflects data availability and replication (% of placement groups that are active and clean).
+            </p>
+            @if (vm?.pgs?.activeCleanChartReason?.length) {
+            <p
+              class="cds--type-helper-text-01 overview-health-card-secondary-text overview-health-card-bold cds-mb-2"
+              i18n>
+              {{vm?.pgs?.activeCleanChartSeverity === 'progress' ? 'Data cleanup in progress' : 'What is affecting resiliency?'}}
+            </p>
+            @for (item of vm?.pgs?.activeCleanChartReason; track item.state; let isLast =$last) {
+            @if(item.count) {
+            <p
+              [class.cds-mb-0]="isLast"
+              class="cds-mb-2">
+              <span class="cds--type-label-01 cds-mr-1">{{item?.state}}:</span>
+              <span class="cds--type-label-01 overview-health-card-bold">{{item.count}} %</span>
+              @if(!item?.state?.includes('Scrub') || item?.state !== 'remapped') {
+              <cd-icon type="arrowDown"></cd-icon>
+              }
+            </p>
+            }
+            }
+            }
+          </div>
+        </div>
+        } @else {
+        <cds-skeleton-placeholder></cds-skeleton-placeholder>
+        }
       </div>
     </ng-container>
     <ng-container *ngSwitchDefault></ng-container>
index b8b2a653534954bd1844f803091311775034704c..2a436894fcb8714cda1b226eec793275f4606fc5 100644 (file)
     color: var(--cds-text-secondary);
   }
 
+  &-bold {
+    font-weight: 700 !important;
+  }
+
   &-tab {
     display: flex;
   }
     justify-content: space-between;
   }
 
+  &-resiliency-tab-content {
+    display: flex;
+  }
+
+  &-resiliency-chart {
+    flex-grow: 1;
+    flex-shrink: 0;
+  }
+
+  &-resiliency-chart-text {
+    width: 13rem;
+    margin-left: var(--cds-spacing-04);
+  }
+
   &-icon-and-text {
     display: inline-flex;
     align-items: center;
index af9c9871703f19abcdc0b433e80ec989bc3e4d9c..7f32ab09227486237a41467d656d0c4122223196 100644 (file)
@@ -32,6 +32,7 @@ import { MgrModuleService } from '~/app/shared/api/mgr-module.service';
 import { RefreshIntervalService } from '~/app/shared/services/refresh-interval.service';
 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
 import { HardwareNameMapping } from '~/app/shared/enum/hardware.enum';
+import { GaugeChartComponent } from '@carbon/charts-angular';
 
 type OverviewHealthData = {
   summary: Summary;
@@ -67,7 +68,8 @@ type HwRowVM = {
     PipesModule,
     TooltipModule,
     TabsModule,
-    LayoutModule
+    LayoutModule,
+    GaugeChartComponent
   ],
   standalone: true,
   templateUrl: './overview-health-card.component.html',
@@ -88,7 +90,7 @@ export class OverviewHealthCardComponent {
   @Output() viewPGStates = new EventEmitter<void>();
   @Output() activeSectionChange = new EventEmitter<HealthCardTabSection | null>();
 
-  activeSection: HealthCardTabSection | null = 'resiliency';
+  activeSection: HealthCardTabSection | null;
 
   healthItems: HealthItemConfig[] = [
     { key: 'mon', label: $localize`Monitor` },
index 066a632518a494efb02c9a8ca1bbfd60a03fa547..eee4c1e37305aaddccbf69479d5567f3ec1aa8ba 100644 (file)
@@ -86,7 +86,7 @@
           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>
+          <p class="cds--type-heading-03 cds-mb-0">{{data.value | dimlessBinary }} / s</p>
         </div>
         }
       </div>
index 425800f9f11cb4f5bf6da97b61ff86444a6b8a52..7ed0a90bfb63f757ab055644e3a25a871b437482 100644 (file)
     background-color: var(--cds-background);
   }
 
-  cds-panel .panel-content {
-    padding: 0 !important;
-  }
-
   cds-panel .panel-header {
     margin-bottom: var(--cds-spacing-03);
   }
index 82116c2d4ec267e952f455200ba89ac58066f4ee..574930db6f06773c997cc58a1e422ae69a1f1687 100644 (file)
@@ -91,6 +91,22 @@ describe('OverviewComponent', () => {
           b: { severity: 'HEALTH_ERR', summary: { message: 'B issue' } }
         }
       },
+      // data resileincy
+      pgmap: {
+        pgs_by_state: [
+          {
+            state_name: 'active+clean',
+            count: 497
+          }
+        ],
+        num_pools: 14,
+        bytes_used: 3236978688,
+        bytes_total: 325343772672,
+        num_pgs: 497,
+        write_bytes_sec: 0,
+        read_bytes_sec: 0,
+        recovering_bytes_per_sec: 0
+      },
       // subsystem inputs used by mapper
       monmap: { num_mons: 3, quorum: [0, 1, 2] } as any,
       mgrmap: { num_active: 1, num_standbys: 1 } as any,
@@ -157,6 +173,21 @@ describe('OverviewComponent', () => {
       monmap: { num_mons: 3, quorum: [0, 1, 2] } as any, // ok
       mgrmap: { num_active: 0, num_standbys: 0 } as any, // err (active < 1)
       osdmap: { num_osds: 2, up: 2, in: 2 } as any, // ok
+      pgmap: {
+        pgs_by_state: [
+          {
+            state_name: 'active+clean',
+            count: 497
+          }
+        ],
+        num_pools: 14,
+        bytes_used: 3236978688,
+        bytes_total: 325343772672,
+        num_pgs: 497,
+        write_bytes_sec: 0,
+        read_bytes_sec: 0,
+        recovering_bytes_per_sec: 0
+      },
       num_hosts: 1,
       num_hosts_down: 0 // ok
     } as any;
index 2a514774bf5d4efc808058960df53e9f98219ffd..e9f8cecf40fa961066f90f5ac3736dbe7ac743f9 100644 (file)
@@ -14,6 +14,8 @@ 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 {
+  ACTIVE_CLEAN_CHART_OPTIONS,
+  calcActiveCleanSeverityAndReasons,
   getClusterHealth,
   getHealthChecksAndIncidents,
   getResiliencyDisplay,
@@ -24,6 +26,7 @@ import {
   safeDifference,
   SEVERITY,
   Severity,
+  SEVERITY_TO_COLOR,
   SeverityIconMap
 } from '~/app/shared/models/overview';
 
@@ -34,16 +37,25 @@ 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';
 import { DataTableModule } from '~/app/shared/datatable/datatable.module';
+import { PipesModule } from '~/app/shared/pipes/pipes.module';
 
 /**
  * Mapper: HealthSnapshotMap -> HealthCardVM
  * Runs only when healthData$ emits.
  */
-export function buildHealthCardVM(d: HealthSnapshotMap): HealthCardVM {
+function buildHealthCardVM(d: HealthSnapshotMap): HealthCardVM {
   const checksObj: Record<string, HealthCheck> = d.health?.checks ?? {};
   const clusterHealth = getClusterHealth(d.health.status as HealthStatus);
+  const pgStates = d?.pgmap?.pgs_by_state ?? [];
+  const totalPg = d?.pgmap?.num_pgs ?? 0;
+
   const { incidents, checks } = getHealthChecksAndIncidents(checksObj);
-  const resiliencyHealth = getResiliencyDisplay(checks);
+  const resiliencyHealth = getResiliencyDisplay(checks, pgStates);
+  const {
+    activeCleanPercent,
+    severity: activeCleanChartSeverity,
+    reasons: activeCleanChartReason
+  } = calcActiveCleanSeverityAndReasons(pgStates, totalPg);
 
   // --- System sub-states ---
 
@@ -74,8 +86,6 @@ export function buildHealthCardVM(d: HealthSnapshotMap): HealthCardVM {
   // Overall = worst of the subsystem severities.
   const overallSystemSev = maxSeverity(monSev, mgrSev, osdSev, hostsSev);
 
-  // Resiliency
-
   return {
     fsid: d.fsid,
     overallSystemSev: SeverityIconMap[overallSystemSev],
@@ -84,13 +94,19 @@ export function buildHealthCardVM(d: HealthSnapshotMap): HealthCardVM {
     checks,
 
     pgs: {
-      total: d?.pgmap?.num_pgs,
-      states: d?.pgmap?.pgs_by_state,
+      total: totalPg,
+      states: pgStates,
       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 }
-      ]
+        { label: $localize`Client write`, value: d?.pgmap?.write_bytes_sec ?? 0 },
+        { label: $localize`Client read`, value: d?.pgmap?.read_bytes_sec ?? 0 },
+        { label: $localize`Recovery I/O`, value: d?.pgmap?.recovering_bytes_per_sec ?? 0 }
+      ],
+      activeCleanChartData: [{ group: 'value', value: activeCleanPercent }],
+      activeCleanChartOptions: {
+        ...ACTIVE_CLEAN_CHART_OPTIONS,
+        color: { scale: { value: SEVERITY_TO_COLOR[activeCleanChartSeverity] } }
+      },
+      activeCleanChartReason
     },
 
     clusterHealth,
@@ -101,7 +117,7 @@ export function buildHealthCardVM(d: HealthSnapshotMap): HealthCardVM {
       value: $localize`${mgrActive} active, ${mgrStandby} standby`,
       severity: SeverityIconMap[mgrSev]
     },
-    osd: { value: $localize`${osdUp}/${osdTotal} in/up`, severity: SeverityIconMap[osdSev] },
+    osd: { value: $localize`${osdIn}/${osdUp} in/up`, severity: SeverityIconMap[osdSev] },
     hosts: {
       value: $localize`${hostsAvailable} / ${hostsTotal} available`,
       severity: SeverityIconMap[hostsSev]
@@ -121,7 +137,8 @@ export function buildHealthCardVM(d: HealthSnapshotMap): HealthCardVM {
     OverviewAlertsCardComponent,
     PerformanceCardComponent,
     LayoutModule,
-    DataTableModule
+    DataTableModule,
+    PipesModule
   ],
   standalone: true,
   templateUrl: './overview.component.html',
@@ -144,7 +161,7 @@ export class OverviewComponent {
 
   private readonly healthData$: Observable<HealthSnapshotMap> = this.refreshIntervalObs(() =>
     this.healthService.getHealthSnapshot()
-  );
+  ).pipe(shareReplay({ bufferSize: 1, refCount: true }));
 
   readonly healthCardVm$: Observable<HealthCardVM> = this.healthData$.pipe(
     map(buildHealthCardVM),
index ae178ded2769d31692f3c6ef537f55223905287f..31108019630199bfcf5e1e224ef18ab7511bf557 100644 (file)
@@ -52,7 +52,7 @@ export class PgCategoryService {
     );
   }
 
-  private getPgStatesFromText(pgStatesText: string) {
+  getPgStatesFromText(pgStatesText: string) {
     const pgStates = pgStatesText
       .replace(/[^a-z_]+/g, ' ')
       .trim()
index b5dd02c44438d3717ae2b5ba7d1517031a76b006..913f0f89880c932bd44632cdcdcdc4cee3287cd2 100644 (file)
@@ -123,6 +123,7 @@ 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 ArrowDown16 from '@carbon/icons/es/arrow--down/16';
 
 import { TearsheetStepComponent } from './tearsheet-step/tearsheet-step.component';
 import { PageHeaderComponent } from './page-header/page-header.component';
@@ -304,7 +305,8 @@ export class ComponentsModule {
       WarningAlt16,
       CheckMarkOutline16,
       ArrowUpRight16,
-      InProgress16
+      InProgress16,
+      ArrowDown16
     ]);
   }
 }
index f5ad4e445e6ab5939ba9cdbd39cd6e76668e67c4..f38d638a62b197a219eea64f2712305001b85923 100644 (file)
@@ -62,3 +62,11 @@ Using `color` in css and seyting svg will fill="currentColor does not work.
 .checkMarkOutline-icon {
   fill: theme.$support-success !important;
 }
+
+.inProgress-icon {
+  fill: theme.$support-info !important;
+}
+
+.arrowDown-icon {
+  fill: theme.$support-error !important;
+}
index eec9d2fc96e4408955ed3605c50f3fb7bfc4d56a..da9701254781d1494e647b5e68acae57d2caffd8 100644 (file)
@@ -124,7 +124,8 @@ export enum Icons {
   checkMarkOutline = 'checkmark--outline',
   warningAlt = 'warning--alt',
   arrowUpRight = 'arrow--up-right',
-  inProgress = 'in-progress'
+  inProgress = 'in-progress',
+  arrowDown = 'arrow--down'
 }
 
 export enum IconSize {
@@ -164,5 +165,6 @@ export const ICON_TYPE = {
   warningAlt: 'warning--alt',
   checkMarkOutline: 'checkmark--outline',
   arrowUpRight: ' arrow--up-right',
-  inProgress: 'in-progress'
+  inProgress: 'in-progress',
+  arrowDown: 'arrow--down'
 } as const;
index adfba20ffc5e23acb5ee2778d4a0046ef8f24304..d1234047cf1dd9bffbabcb78d6c77f16d4ea7a7d 100644 (file)
@@ -40,6 +40,7 @@ export interface PgMap {
   num_pgs: number;
   write_bytes_sec: number;
   read_bytes_sec: number;
+  recovering_bytes_per_sec: number;
 }
 
 export interface HealthMapCommon {
index 23af3a90c56b6c77e2871dee20b1e6eabe20ab4a..cb8b40595d81dc0c89b556d8a98d0410569be4dc 100644 (file)
@@ -1,6 +1,18 @@
+import { ChartTabularData, GaugeChartOptions } from '@carbon/charts-angular';
 import { HealthCheck, PgStateCount } from './health.interface';
+import _ from 'lodash';
 
-export type HealthStatus = 'HEALTH_OK' | 'HEALTH_WARN' | 'HEALTH_ERR';
+// Types
+type ResileincyHealthType = {
+  title: string;
+  description: string;
+  icon: string;
+  severity: ResiliencyState;
+};
+
+type ResiliencyState = typeof DATA_RESILIENCY_STATE[keyof typeof DATA_RESILIENCY_STATE];
+
+type PG_STATES = typeof PG_STATES[number];
 
 export const HealthIconMap = {
   HEALTH_OK: 'success',
@@ -15,8 +27,12 @@ export const SeverityIconMap = {
   3: 'inProgress'
 };
 
-/** 0 ok, 1 warn, 2 err , 3 sync*/
-export type Severity = 0 | 1 | 2 | 3;
+export type HealthStatus = 'HEALTH_OK' | 'HEALTH_WARN' | 'HEALTH_ERR';
+
+export type HealthCardTabSection = 'system' | 'hardware' | 'resiliency';
+
+/** 0 ok, 1 warn, 2 err */
+export type Severity = 0 | 1 | 2;
 
 export type Health = {
   message: string;
@@ -24,25 +40,7 @@ export type Health = {
   icon: string;
 };
 
-const WarnAndErrMessage = $localize`There are active alerts and unresolved health warnings.`;
-
-export const HealthMap: Record<HealthStatus, Health> = {
-  HEALTH_OK: {
-    message: $localize`All core services are running normally`,
-    icon: HealthIconMap['HEALTH_OK'],
-    title: $localize`Healthy`
-  },
-  HEALTH_WARN: {
-    message: WarnAndErrMessage,
-    icon: HealthIconMap['HEALTH_WARN'],
-    title: $localize`Warning`
-  },
-  HEALTH_ERR: {
-    message: WarnAndErrMessage,
-    icon: HealthIconMap['HEALTH_ERR'],
-    title: $localize`Critical`
-  }
-};
+// Interfaces
 
 export interface HealthDisplayVM {
   title: string;
@@ -61,12 +59,6 @@ export interface HealthCardSubStateVM {
   severity: string;
 }
 
-type ResileincyHealthType = {
-  title: string;
-  description: string;
-  icon: string;
-};
-
 export interface HealthCardVM {
   fsid: string;
   overallSystemSev: string;
@@ -82,6 +74,9 @@ export interface HealthCardVM {
     total: number;
     states: PgStateCount[];
     io: Array<{ label: string; value: number }>;
+    activeCleanChartData: ChartTabularData;
+    activeCleanChartOptions: GaugeChartOptions;
+    activeCleanChartReason: Array<{ state: string; count: number }>;
   };
 
   mon: HealthCardSubStateVM;
@@ -90,19 +85,9 @@ export interface HealthCardVM {
   hosts: 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;
+// Constants
 
-export const RESILIENCY_CHECK = {
-  error: ['PG_DAMAGED', 'PG_RECOVERY_FULL'],
-  warn: ['PG_DEGRADED', 'PG_AVAILABILITY', 'PG_BACKFILL_FULL']
-};
+const WarnAndErrMessage = $localize`There are active alerts and unresolved health warnings.`;
 
 const DATA_RESILIENCY_STATE = {
   ok: 'ok',
@@ -110,61 +95,133 @@ const DATA_RESILIENCY_STATE = {
   warn: 'warn',
   warnDataLoss: 'warnDataLoss',
   progress: 'progress'
+} as const;
+
+const CHECK_TO_STATE: Record<string, ResiliencyState> = {
+  PG_DAMAGED: DATA_RESILIENCY_STATE.error,
+  PG_RECOVERY_FULL: DATA_RESILIENCY_STATE.error,
+
+  PG_DEGRADED: DATA_RESILIENCY_STATE.warn,
+  PG_AVAILABILITY: DATA_RESILIENCY_STATE.warnDataLoss,
+  PG_BACKFILL_FULL: DATA_RESILIENCY_STATE.warn
+} as const;
+
+const RESILIENCY_PRIORITY: Record<ResiliencyState, number> = {
+  ok: 0,
+  progress: 1,
+  warn: 2,
+  warnDataLoss: 3,
+  error: 4
 };
 
-export const DATA_RESILIENCY = {
+// Priority: DO NOT CHANGE ORDER HERE
+const PG_STATES = [
+  // ERROR OR WARN
+  'offline',
+  'inconsistent',
+  'down',
+  'stale',
+  'degraded',
+  'undersized',
+  'recovering',
+  'recovery_wait',
+  'backfilling',
+  'backfill_wait',
+  'remapped',
+  // PROGRESS
+  'deep',
+  'scrubbing'
+] as const;
+
+const LABELS: Record<string, string> = {
+  scrubbing: 'Scrub',
+  deep: 'Deep-Scrub'
+};
+
+export const HealthMap: Record<HealthStatus, Health> = {
+  HEALTH_OK: {
+    message: $localize`All core services are running normally`,
+    icon: HealthIconMap['HEALTH_OK'],
+    title: $localize`Healthy`
+  },
+  HEALTH_WARN: {
+    message: WarnAndErrMessage,
+    icon: HealthIconMap['HEALTH_WARN'],
+    title: $localize`Warning`
+  },
+  HEALTH_ERR: {
+    message: WarnAndErrMessage,
+    icon: HealthIconMap['HEALTH_ERR'],
+    title: $localize`Critical`
+  }
+};
+
+export const SEVERITY = {
+  ok: 0 as Severity,
+  warn: 1 as Severity,
+  err: 2 as Severity,
+  sync: 3 as Severity
+} as const;
+
+export const ACTIVE_CLEAN_CHART_OPTIONS: GaugeChartOptions = {
+  resizable: true,
+  height: '100px',
+  width: '100px',
+  gauge: { type: 'full' },
+  toolbar: {
+    enabled: false
+  }
+};
+
+export const DATA_RESILIENCY: Record<ResiliencyState, ResileincyHealthType> = {
   [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.`
+    description: $localize`All replicas are in place and I/O is operating normally. No action is required.`,
+    severity: DATA_RESILIENCY_STATE.ok
   },
   [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.`
+    description: $localize`Ceph is running routine consistency checks on stored data and metadata to ensure data integrity. Data remains safe and accessible.`,
+    severity: DATA_RESILIENCY_STATE.progress
   },
   [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.`
+    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.`,
+    severity: DATA_RESILIENCY_STATE.warn
   },
   [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.`
+    description: $localize`Ceph cannot reliably determine the current state of some data. Availability may be affected.`,
+    severity: DATA_RESILIENCY_STATE.warnDataLoss
   },
   [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.`
+    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.`,
+    severity: DATA_RESILIENCY_STATE.error
   }
+} as const;
+
+export const SEVERITY_TO_COLOR: Record<ResiliencyState, string> = {
+  ok: '#24A148',
+  progress: '#24A148',
+  warn: '#F1C21B',
+  warnDataLoss: '#F1C21B',
+  error: '#DA1E28'
 };
 
+// Utilities
+
 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;
@@ -183,3 +240,125 @@ export function getHealthChecksAndIncidents(checksObj: Record<string, HealthChec
 export function safeDifference(a: number, b: number): number | null {
   return a != null && b != null ? a - b : null;
 }
+
+export function getResiliencyDisplay(
+  checks: HealthCardCheckVM[] = [],
+  pgStates: PgStateCount[] = []
+): ResileincyHealthType {
+  let state: ResiliencyState = DATA_RESILIENCY_STATE.ok;
+
+  for (const check of checks) {
+    const next = CHECK_TO_STATE[check?.name];
+    if (next && RESILIENCY_PRIORITY[next] > RESILIENCY_PRIORITY[state]) state = next;
+    if (state === DATA_RESILIENCY_STATE.error) break;
+  }
+
+  if (state === DATA_RESILIENCY_STATE.ok) {
+    const hasScrubbing = pgStates.some((s) => {
+      const n = s?.state_name ?? '';
+      return n.includes('scrubbing') || n.includes('deep');
+    });
+    if (hasScrubbing) state = DATA_RESILIENCY_STATE.progress;
+  }
+
+  return DATA_RESILIENCY[state];
+}
+
+export function getActiveCleanChartSeverity(
+  pgStates: PgStateCount[] = [],
+  activeCleanRatio: number
+): ResiliencyState {
+  if (activeCleanRatio >= 1) return DATA_RESILIENCY_STATE.ok;
+
+  const hasActive = pgStates.some((s) => (s?.state_name ?? '').includes('active'));
+  return hasActive ? DATA_RESILIENCY_STATE.warn : DATA_RESILIENCY_STATE.error;
+}
+
+function labelOf(key: string) {
+  return LABELS[key] ?? key.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
+}
+
+function isActiveCleanRow(pgRow: string) {
+  // E.g active+clean+remapped
+  return pgRow.includes('active') && pgRow.includes('clean');
+}
+
+function isScrubbing(pgRow: string) {
+  return pgRow.includes('scrubbing') || pgRow.includes('deep');
+}
+
+/**
+ * If any PG state is active and not clean => Warn
+ * If any PG state is not active -> Error
+ *
+ * In case above is true, the states contributing to that as per
+ * PG_STATES priotity List will be added.
+ *
+ * If all OKAY. then scrubbing shown (if active)
+ */
+export function calcActiveCleanSeverityAndReasons(
+  pgStates: PgStateCount[] = [],
+  totalPg: number
+): {
+  activeCleanPercent: number;
+  severity: ResiliencyState;
+  reasons: Array<{ state: string; count: number }>;
+} {
+  if (totalPg <= 0) {
+    return { activeCleanPercent: 0, severity: DATA_RESILIENCY_STATE.ok, reasons: [] };
+  }
+
+  const reasonCounts = new Map<PG_STATES, number>();
+  let severity: ResiliencyState = DATA_RESILIENCY_STATE.ok;
+  let activeCleanTotal = 0;
+  let hasProgress = false;
+  let hasNotActiveNotClean = false;
+  let hasActiveNotClean = false;
+
+  for (const state of pgStates) {
+    const stateName = (state?.state_name ?? '').trim();
+    const stateCount = state?.count ?? 0;
+    const isActive = stateName.includes('active');
+    const isClean = stateName.includes('clean');
+
+    if (!isActive && !isClean) hasNotActiveNotClean = true;
+    if (isActive && !isClean) hasActiveNotClean = true;
+
+    // If all okay then only scrubbing state is shown
+    if (!hasProgress && isScrubbing(stateName)) {
+      hasProgress = true;
+    }
+
+    // active+clean*: no reasons required hence continuing
+    if (isActiveCleanRow(stateName)) {
+      activeCleanTotal += stateCount;
+      continue;
+    }
+
+    // Non active, non-clean or non-active+clean: reasons needed
+    for (const state of PG_STATES) {
+      if (stateName.includes(state)) {
+        reasonCounts.set(state, (reasonCounts.get(state) ?? 0) + stateCount);
+        break;
+      }
+    }
+  }
+
+  if (hasNotActiveNotClean) severity = DATA_RESILIENCY_STATE.error;
+  else if (hasActiveNotClean) severity = DATA_RESILIENCY_STATE.warn;
+  else if (hasProgress) severity = DATA_RESILIENCY_STATE.progress;
+
+  const reasons =
+    reasonCounts.size === 0
+      ? []
+      : [...reasonCounts.entries()]
+          .sort((a, b) => b[1] - a[1])
+          .map(([state, count]) => ({
+            state: labelOf(state),
+            count: Number(((count / totalPg) * 100).toFixed(2))
+          }));
+
+  const activeCleanPercent = Number(((activeCleanTotal / totalPg) * 100).toFixed(2));
+
+  return { activeCleanPercent, severity, reasons };
+}
index 8403cb99fea46013f8a5c6781e7749bd8993ebae..bce3619b3d207304afd1c0aa298898778c21e300 100755 (executable)
@@ -8271,12 +8271,16 @@ paths:
                           - count
                           type: object
                         type: array
+                      recovering_bytes_per_sec:
+                        description: Total recovery in bytes
+                        type: integer
                     required: &id058
                     - pgs_by_state
                     - num_pools
                     - num_pgs
                     - bytes_used
                     - bytes_total
+                    - recovering_bytes_per_sec
                     type: object
                 required: &id059
                 - fsid
@@ -8434,6 +8438,9 @@ paths:
                           required: *id057
                           type: object
                         type: array
+                      recovering_bytes_per_sec:
+                        description: Total recovery in bytes
+                        type: integer
                     required: *id058
                     type: object
                 required: *id059