]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: add view alerts button in data resiliency tab
authorAfreen Misbah <afreen@ibm.com>
Tue, 21 Jul 2026 17:31:31 +0000 (23:01 +0530)
committerAfreen Misbah <afreen@ibm.com>
Tue, 21 Jul 2026 19:34:57 +0000 (01:04 +0530)
Signed-off-by: Afreen Misbah <afreen@ibm.com>
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/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/overview.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/models/overview.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-alert.service.ts

index d171c4a6d9558e83bc3fd5f7ac3dd104cd8304d5..80be6845654b86349cf56266fb41ebf1a8264517 100644 (file)
@@ -2,6 +2,7 @@
 @let hwEnabled = enabled$ | async;
 @let hwData = hardwareData$ | async;
 @let telemetryEnabled = telemetryEnabled$ | async;
+@let pgAlertCount = pgAlertCount$ | async;
 
 @let colorClass = 'overview-health-card-status--' + vm?.clusterHealth?.icon;
 
           <p class="overview-health-card-secondary-text cds--type-label-01">
             {{ vm?.resiliencyHealth?.description }}
           </p>
-          <button
-            cdsButton="tertiary"
-            size="sm"
-            type="button"
-            (click)="onViewPGStatesClick()"
+          <div
+            cdsStack="horizontal"
+            [gap]="4"
+            class="cds-mt-4"
           >
-            <span
-              i18n
-              class="cds-ml-3"
-              >See all PGs states</span
+            @if (pgAlertCount > 0) {
+              <a
+                cdsButton="primary"
+                size="sm"
+                class="overview-health-card-resiliency-btn"
+                [routerLink]="['/monitoring/active-alerts']"
+              >
+                <span i18n>View alerts ({{ pgAlertCount }})</span>
+                <cd-icon type="arrowRight"></cd-icon>
+              </a>
+            }
+            <button
+              cdsButton="tertiary"
+              size="sm"
+              class="overview-health-card-resiliency-btn"
+              type="button"
+              (click)="onViewPGStatesClick()"
             >
-            <cd-icon type="arrowUpRight"></cd-icon>
-          </button>
+              <span i18n>See all PGs states</span>
+              <cd-icon type="arrowUpRight"></cd-icon>
+            </button>
+          </div>
         </div>
         @if (vm?.pgs?.activeCleanChartData && vm?.pgs?.activeCleanChartOptions) {
           <div class="overview-health-card-tab-content-item-row">
             <div class="overview-health-card-resiliency-chart-text">
               <p
                 i18n
-                class="cds--type-helper-text-01 overview-health-card-secondary-text"
+                class="cds--type-helper-text-01 overview-health-card-secondary-text cds-mt-2"
               >
                 <em>Data resiliency</em> reflects data availability and replication (% of placement
                 groups that are active and clean).
                 ) {
                   @if (item.count) {
                     <p
+                      [class.cds-mb-2]="!isLast"
                       [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"
index 0c16f9d3da29ef4352dc6eb303ac553942724e75..58a04a5ff55cec2badee06a0881215685e224868 100644 (file)
     justify-content: space-between;
   }
 
+  &-resiliency-btn {
+    gap: var(--cds-spacing-05);
+    padding-inline: var(--cds-spacing-05);
+  }
+
   &-resiliency-tab-content {
     display: flex;
   }
@@ -60,7 +65,7 @@
   }
 
   &-resiliency-chart-text {
-    width: 13rem;
+    width: 15rem;
     margin-left: var(--cds-spacing-04);
   }
 
index bba7e29aa44d2275b94a4d83eff6075f4911e7f9..1eac035f9b18c9d05d996cba0436446798a3676e 100644 (file)
@@ -38,6 +38,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 { GaugeChartComponent } from '@carbon/charts-angular';
+import { PrometheusAlertService } from '~/app/shared/services/prometheus-alert.service';
 
 type OverviewHealthData = {
   summary: Summary;
@@ -81,6 +82,7 @@ export class OverviewHealthCardComponent {
   private readonly mgrModuleService = inject(MgrModuleService);
   private readonly refreshIntervalService = inject(RefreshIntervalService);
   private readonly authStorageService = inject(AuthStorageService);
+  private readonly prometheusAlertService = inject(PrometheusAlertService);
 
   @Input({ required: true }) vm!: HealthCardVM;
   @Output() viewIncidents = new EventEmitter<void>();
@@ -146,6 +148,8 @@ export class OverviewHealthCardComponent {
     shareReplay({ bufferSize: 1, refCount: true })
   );
 
+  readonly pgAlertCount$ = this.prometheusAlertService.pgAlerts$.pipe(startWith(0));
+
   readonly telemetryEnabled$: Observable<boolean> = this.healthService.getTelemetryStatus().pipe(
     map((enabled: any) => !!enabled),
     catchError(() => of(false)),
index 24429e46f0220ce7407346e43a1b1b21a2a426df..273d5d8724911344600d9a88f6491a92f663f6f6 100644 (file)
@@ -120,6 +120,7 @@ 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 ArrowRight16 from '@carbon/icons/es/arrow--right/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';
@@ -318,6 +319,7 @@ export class ComponentsModule {
       VmdkDisk16,
       WarningAlt16,
       CheckMarkOutline16,
+      ArrowRight16,
       ArrowUpRight16,
       InProgress16,
       ArrowDown16,
index fbccf2e940ee8a60b04215a8eb295f088d32a19f..191f7e00055b4d37aed82623bb105a8ae7d01075 100644 (file)
@@ -120,6 +120,7 @@ export enum Icons {
   vmdkDisk = 'vmdk-disk',
   checkMarkOutline = 'checkmark--outline',
   warningAlt = 'warning--alt',
+  arrowRight = 'arrow--right',
   arrowUpRight = 'arrow--up-right',
   inProgress = 'in-progress',
   arrowDown = 'arrow--down',
@@ -164,6 +165,7 @@ export const ICON_TYPE = {
   vmdkDisk: 'vmdk-disk',
   warningAlt: 'warning--alt',
   checkMarkOutline: 'checkmark--outline',
+  arrowRight: 'arrow--right',
   arrowUpRight: ' arrow--up-right',
   inProgress: 'in-progress',
   arrowDown: 'arrow--down',
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/overview.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/overview.spec.ts
new file mode 100644 (file)
index 0000000..7fbb8d6
--- /dev/null
@@ -0,0 +1,51 @@
+import { buildHealthCardVM } from './overview';
+import { HealthSnapshotMap } from './health.interface';
+
+function makeSnapshot(checks: Record<string, any> = {}): HealthSnapshotMap {
+  return {
+    fsid: 'test-fsid',
+    health: { status: 'HEALTH_OK', checks, mutes: [] },
+    monmap: { num_mons: 3 },
+    osdmap: { in: 3, up: 3, num_osds: 3 },
+    pgmap: {
+      pgs_by_state: [{ state_name: 'active+clean', count: 100 }],
+      num_pools: 1,
+      bytes_used: 0,
+      bytes_total: 1000,
+      num_pgs: 100,
+      write_bytes_sec: 0,
+      read_bytes_sec: 0,
+      recovering_bytes_per_sec: 0
+    },
+    mgrmap: { num_active: 1, num_standbys: 1 },
+    fsmap: { num_active: 0, num_standbys: 0 },
+    num_rgw_gateways: 0,
+    num_iscsi_gateways: { up: 0, down: 0 },
+    num_hosts: 3
+  } as any;
+}
+
+describe('buildHealthCardVM', () => {
+  it('should not include pgAlertCount in the VM', () => {
+    const vm = buildHealthCardVM(makeSnapshot());
+    expect('pgAlertCount' in vm).toBe(false);
+  });
+
+  it('should build resiliencyHealth from health checks', () => {
+    const vm = buildHealthCardVM(
+      makeSnapshot({
+        PG_DEGRADED: {
+          severity: 'HEALTH_WARN',
+          summary: { message: 'degraded', count: 5 },
+          muted: false
+        }
+      })
+    );
+    expect(vm.resiliencyHealth.severity).toBe('warn');
+  });
+
+  it('should report ok resiliency when no PG checks', () => {
+    const vm = buildHealthCardVM(makeSnapshot());
+    expect(vm.resiliencyHealth.severity).toBe('ok');
+  });
+});
index 6bc21f692be3dd2deae556804192cd1419aecb3e..a039533cbace760f17d79c000e363375ae5bd8df 100644 (file)
@@ -210,8 +210,8 @@ export const SEVERITY = {
 
 export const ACTIVE_CLEAN_CHART_OPTIONS: GaugeChartOptions = {
   resizable: true,
-  height: '100px',
-  width: '100px',
+  height: '130px',
+  width: '130px',
   gauge: { type: 'full' },
   toolbar: {
     enabled: false
index 4273f8b4f95f090c8f3568ac2c3d55e2a4ef6964..bfed174febe34fa4e82c7d6cfd336fef441957ba 100644 (file)
@@ -34,6 +34,9 @@ export class PrometheusAlertService {
   private warningSubject = new BehaviorSubject<number>(0);
   readonly warningAlerts$ = this.warningSubject.asObservable();
 
+  private pgAlertsSubject = new BehaviorSubject<number>(0);
+  readonly pgAlerts$ = this.pgAlertsSubject.asObservable();
+
   constructor(
     private alertFormatter: PrometheusAlertFormatter,
     private prometheusService: PrometheusService
@@ -117,9 +120,16 @@ export class PrometheusAlertService {
       0
     );
 
+    const activePgAlerts = alerts.filter(
+      (alert) =>
+        alert.status.state === AlertState.ACTIVE &&
+        alert.labels.alertname?.startsWith('CephPG')
+    ).length;
+
     this.totalSubject.next(this.activeAlerts);
     this.criticalSubject.next(this.activeCriticalAlerts);
     this.warningSubject.next(this.activeWarningAlerts);
+    this.pgAlertsSubject.next(activePgAlerts);
 
     this.alerts = alerts
       .reverse()