]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Add health check panel 67386/head
authorAfreen Misbah <afreen@ibm.com>
Mon, 16 Feb 2026 13:57:24 +0000 (19:27 +0530)
committerAfreen Misbah <afreen@ibm.com>
Sun, 22 Feb 2026 09:40:43 +0000 (15:10 +0530)
Fixes https://tracker.ceph.com/issues/74958

- adds helath check panel in overview dashboard
- updates tests
- refactors component as per modern Angular convention
- using onPush CDS in Overview component
- using view model pattern to aggregate data for rendering

Signed-off-by: Afreen Misbah <afreen@ibm.com>
13 files changed:
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/shared/components/components.module.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/copy2clipboard-button/copy2clipboard-button.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/components/icon/icon.component.scss
src/pybind/mgr/dashboard/frontend/src/app/shared/components/side-panel/side-panel.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/overview.ts [new file with mode: 0644]

index 1371bfae110d006b49a507945835b3e40c5d2548..ac27f336ddafe19236dd5f56de93ad9984309528 100644 (file)
@@ -1,4 +1,5 @@
-@let data = (data$ | async);
+@let data=(data$ | async);
+@let colorClass="overview-health-card-status--" + data?.currentHealth?.icon;
 <cd-productive-card>
   <!-- HEALTH CARD Title -->
   @if(fsid) {
@@ -33,7 +34,7 @@
   <!-- HEALTH CARD BODY -->
   @if(data?.currentHealth){
   <p class="cds--type-heading-05 cds-mb-0"
-     [ngClass]="'overview-health-card-status--' + data?.currentHealth?.icon">
+     [ngClass]="colorClass">
     {{data?.currentHealth?.title}}
     <cd-icon [type]="data?.currentHealth?.icon"></cd-icon>
   </p>
@@ -47,7 +48,6 @@
   <p class="cds--type-label-02">
     <span i18n>Ceph version:&nbsp;</span>
     <span class="cds--type-heading-compact-01">{{ data?.summary?.version | cephVersion  }}</span>&nbsp;
-    <!-- UPGRADE AVAILABLE -->
     @if (data?.upgrade?.versions?.length) {
     <a [routerLink]="['/upgrade']"
        cdsLink
     [lines]="1"
     [maxLineWidth]="250"></cds-skeleton-text>
   }
+  <!-- ------------------------------------------- -->
+  <!-- HEALTH CHECKS -->
+   @if(incidents > 0) {
+  <cd-icon
+    type="incidentReporter"
+    [ngClass]="colorClass"></cd-icon>
+  <cds-tooltip-definition
+    [highContrast]="true"
+    [openOnHover]="true"
+    [dropShadow]="true"
+    [caret]="true"
+    (click)="onViewIncidentsClick()"
+    description="Click to view health incidents"
+    i18n-description>
+    <span
+      class="cds--type-heading-compact-01"
+      [ngClass]="colorClass"
+      i18n>
+      {{incidents}} Health incidents
+    </span>
+  </cds-tooltip-definition>
+  <cds-tooltip
+    class="cds-ml-3"
+    [caret]="true"
+    description="Health incidents represent Ceph health check warnings that indicate abnormal conditions requiring intervention and persist until the condition is resolved."
+    i8n-description
+  >
+  <cd-icon type="help"></cd-icon>
+</cds-tooltip>
+}
 </cd-productive-card>
index 33a495f3ed084a8f42d4a19149401d7f966ad620..7b99a71631cae75a5209738b01f3fe95bd1aa713 100644 (file)
@@ -3,7 +3,6 @@
     display: flex;
     align-items: end;
   }
-
   // CSS for status text, modifier names match icons name
   &-status--success {
     color: var(--cds-support-success);
   &-status--error {
     color: var(--cds-text-error);
   }
-}
-
-// Overrides
-.clipboard-btn {
-  padding: var(--cds-spacing-02);
-}
+  // Overrides
+  .clipboard-btn {
+    padding: var(--cds-spacing-02);
+  }
 
-.cds--btn--icon-only {
-  padding: var(--cds-spacing-01);
-}
+  .cds--btn--icon-only {
+    padding: var(--cds-spacing-01);
+  }
 
-.cds--link.cds--link--inline {
-  text-decoration: none;
-}
+  .cds--link.cds--link--inline {
+    text-decoration: none;
+  }
 
-.cds--skeleton__placeholder {
-  margin-bottom: var(--cds-spacing-03);
+  .cds--skeleton__placeholder {
+    margin-bottom: var(--cds-spacing-03);
+  }
 }
index a644300d6904831349651e06e39c129d3d3f55a3..412d750b7ae385b50fb64b89f1ce024ed9c56048 100644 (file)
@@ -1,11 +1,13 @@
 import {
   ChangeDetectionStrategy,
   Component,
+  EventEmitter,
   inject,
   Input,
+  Output,
   ViewEncapsulation
 } from '@angular/core';
-import { SkeletonModule, ButtonModule, LinkModule } from 'carbon-components-angular';
+import { SkeletonModule, ButtonModule, LinkModule, TooltipModule } from 'carbon-components-angular';
 import { ProductiveCardComponent } from '~/app/shared/components/productive-card/productive-card.component';
 import { RouterModule } from '@angular/router';
 import { ComponentsModule } from '~/app/shared/components/components.module';
@@ -17,6 +19,7 @@ import { PipesModule } from '~/app/shared/pipes/pipes.module';
 import { UpgradeInfoInterface } from '~/app/shared/models/upgrade.interface';
 import { UpgradeService } from '~/app/shared/api/upgrade.service';
 import { catchError, filter, map, startWith } from 'rxjs/operators';
+import { HealthIconMap, HealthStatus } from '~/app/shared/models/overview';
 
 type OverviewHealthData = {
   summary: Summary;
@@ -30,23 +33,22 @@ type Health = {
   icon: string;
 };
 
-type HealthStatus = 'HEALTH_OK' | 'HEALTH_WARN' | 'HEALTH_ERR';
 const WarnAndErrMessage = $localize`There are active alerts and unresolved health warnings.`;
 
 const HealthMap: Record<HealthStatus, Health> = {
   HEALTH_OK: {
     message: $localize`All core services are running normally`,
-    icon: 'success',
+    icon: HealthIconMap['HEALTH_OK'],
     title: $localize`Healthy`
   },
   HEALTH_WARN: {
     message: WarnAndErrMessage,
-    icon: 'warningAltFilled',
+    icon: HealthIconMap['HEALTH_WARN'],
     title: $localize`Warning`
   },
   HEALTH_ERR: {
     message: WarnAndErrMessage,
-    icon: 'error',
+    icon: HealthIconMap['HEALTH_ERR'],
     title: $localize`Critical`
   }
 };
@@ -61,7 +63,8 @@ const HealthMap: Record<HealthStatus, Health> = {
     RouterModule,
     ComponentsModule,
     LinkModule,
-    PipesModule
+    PipesModule,
+    TooltipModule
   ],
   standalone: true,
   templateUrl: './overview-health-card.component.html',
@@ -70,19 +73,21 @@ const HealthMap: Record<HealthStatus, Health> = {
   changeDetection: ChangeDetectionStrategy.OnPush
 })
 export class OverviewHealthCardComponent {
+  private readonly summaryService = inject(SummaryService);
+  private readonly upgradeService = inject(UpgradeService);
+
   @Input() fsid!: string;
   @Input()
-  set health(value: HealthStatus) {
+  set status(value: HealthStatus) {
     this.health$.next(value);
   }
-  private health$ = new ReplaySubject<HealthStatus>(1);
+  @Input() incidents!: number;
+  @Output() viewIncidents = new EventEmitter<void>();
 
-  private readonly summaryService = inject(SummaryService);
-  private readonly upgradeService = inject(UpgradeService);
+  private health$ = new ReplaySubject<HealthStatus>(1);
 
   readonly data$: Observable<OverviewHealthData> = combineLatest([
     this.summaryService.summaryData$.pipe(filter((summary): summary is Summary => !!summary)),
-
     this.upgradeService.listCached().pipe(
       startWith(null as UpgradeInfoInterface),
       catchError(() => of(null))
@@ -91,4 +96,8 @@ export class OverviewHealthCardComponent {
   ]).pipe(
     map(([summary, upgrade, health]) => ({ summary, upgrade, currentHealth: HealthMap?.[health] }))
   );
+
+  onViewIncidentsClick() {
+    this.viewIncidents.emit();
+  }
 }
index b713bd33ab929c47542797fc20dc118d4ee62b23..b23a6c4a11ac4d64b0e4c7dab4af315630aa31b8 100644 (file)
@@ -1,17 +1,16 @@
-@let healthData = healthData$ | async;
+@let vm = vm$ | async;
 <div cdsGrid
-     [narrow]="true"
-     [condensed]="false"
      [fullWidth]="true"
      class="cds-mt-5 cds-mb-5">
-  <div cdsRow
-       [narrow]="true">
+  <div cdsRow>
     <div cdsCol
          class="cds-mb-5"
          [columnNumbers]="{lg: 11}">
       <cd-overview-health-card
-        [fsid]="healthData?.fsid"
-        [health]="healthData?.health?.status">
+        [fsid]="vm?.healthData?.fsid"
+        [status]="vm?.healthData?.health?.status"
+        [incidents]="vm?.incidentCount"
+        (viewIncidents)="togglePanel()">
       </cd-overview-health-card>
     </div>
     <div cdsCol
       <cds-tile>Alerts card</cds-tile>
     </div>
   </div>
-  <div cdsRow
-       [narrow]="true">
+  <div cdsRow>
     <div cdsCol
          class="cds-mb-5"
          [columnNumbers]="{lg: 16}">
       <cd-overview-storage-card
-        [total]="healthData?.pgmap.bytes_total"
-        [used]="healthData?.pgmap.bytes_used">
+        [total]="vm?.healthData?.pgmap.bytes_total"
+        [used]="vm?.healthData?.pgmap.bytes_used">
       </cd-overview-storage-card>
     </div>
   </div>
     </div>
   </div>
 </div>
+@if (isHealthPanelOpen && vm?.incidentCount > 0) {
+  <cd-side-panel
+    [headerText]="'Health incidents ('+ vm?.incidentCount +')'"
+    [expanded]="isHealthPanelOpen"
+    size="md"
+    (closed)="togglePanel()">
+    <div panel-header-description
+         class="cds--type-body-01">
+      <span>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 vm?.checks; track key) {
+      <div>
+        <div class="overview-check-header">
+          <cd-icon [type]="check?.icon"></cd-icon>
+          <span class="cds--type-body-compact-01 overview-check-name">
+            {{ check?.name }}
+          </span>
+        </div>
+        <p class="cds--type-body-compact-01 overview-check-description">{{ check?.description }}</p>
+      </div>
+      }
+    </div>
+  </cd-side-panel>
+}
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..094b0957927c60e127a1f6ccad528d377870bc5d 100644 (file)
@@ -0,0 +1,18 @@
+.overview {
+  &-check-header {
+    display: flex;
+    align-items: center;
+    gap: var(--cds-spacing-02);
+    margin-bottom: var(--cds-spacing-02);
+  }
+
+  &-check-name {
+    color: var(--cds-text-primary);
+    margin-bottom: var(--cds-spacing-01);
+    margin-top: var(--cds-spacing-02);
+  }
+
+  &-check-description {
+    color: var(--cds-text-secondary);
+  }
+}
index 3f893f5254e0383077542bd6af304731f03a3c1d..f3512c99e150be10cf0b50b6d5a6f21158ba82c5 100644 (file)
@@ -10,27 +10,19 @@ import { CommonModule } from '@angular/common';
 import { GridModule, TilesModule } from 'carbon-components-angular';
 import { OverviewHealthCardComponent } from './health-card/overview-health-card.component';
 import { OverviewStorageCardComponent } from './storage-card/overview-storage-card.component';
+import { provideHttpClientTesting } from '@angular/common/http/testing';
+import { provideRouter } from '@angular/router';
 
 describe('OverviewComponent', () => {
   let component: OverviewComponent;
   let fixture: ComponentFixture<OverviewComponent>;
 
-  let mockHealthService: {
-    getHealthSnapshot: jest.Mock;
-  };
-
-  let mockRefreshIntervalService: {
-    intervalData$: Subject<void>;
-  };
+  let mockHealthService: { getHealthSnapshot: jest.Mock };
+  let mockRefreshIntervalService: { intervalData$: Subject<void> };
 
   beforeEach(async () => {
-    mockHealthService = {
-      getHealthSnapshot: jest.fn()
-    };
-
-    mockRefreshIntervalService = {
-      intervalData$: new Subject<void>()
-    };
+    mockHealthService = { getHealthSnapshot: jest.fn() };
+    mockRefreshIntervalService = { intervalData$: new Subject<void>() };
 
     await TestBed.configureTestingModule({
       imports: [
@@ -43,8 +35,10 @@ describe('OverviewComponent', () => {
       ],
       providers: [
         provideHttpClient(),
+        provideHttpClientTesting(),
         { provide: HealthService, useValue: mockHealthService },
-        { provide: RefreshIntervalService, useValue: mockRefreshIntervalService }
+        { provide: RefreshIntervalService, useValue: mockRefreshIntervalService },
+        provideRouter([])
       ]
     }).compileComponents();
 
@@ -53,49 +47,41 @@ describe('OverviewComponent', () => {
     fixture.detectChanges();
   });
 
-  afterEach(() => {
-    jest.clearAllMocks();
-  });
-
-  // --------------------------------------------------
-  // CREATION
-  // --------------------------------------------------
+  afterEach(() => jest.clearAllMocks());
 
+  // -----------------------------
+  // Component creation
+  // -----------------------------
   it('should create', () => {
     expect(component).toBeTruthy();
   });
 
-  // --------------------------------------------------
-  // refreshIntervalObs - success case
-  // --------------------------------------------------
+  // -----------------------------
+  // Vie model stream success
+  // -----------------------------
+  it('vm$ should emit transformed HealthSnapshotMap', (done) => {
+    const mockData: HealthSnapshotMap = { health: { checks: { a: {} } } } as any;
+    mockHealthService.getHealthSnapshot.mockReturnValue(of(mockData));
 
-  it('should call healthService when interval emits', (done) => {
-    const mockResponse: HealthSnapshotMap = { status: 'OK' } as any;
-
-    mockHealthService.getHealthSnapshot.mockReturnValue(of(mockResponse));
-
-    component.healthData$.subscribe((data) => {
-      expect(data).toEqual(mockResponse);
-      expect(mockHealthService.getHealthSnapshot).toHaveBeenCalled();
+    component.vm$.subscribe((vm) => {
+      expect(vm.healthData).toEqual(mockData);
+      expect(vm.incidentCount).toBe(1);
       done();
     });
 
     mockRefreshIntervalService.intervalData$.next();
   });
 
-  // --------------------------------------------------
-  // refreshIntervalObs - error case (catchError → EMPTY)
-  // --------------------------------------------------
-
-  it('should return EMPTY when healthService throws error', (done) => {
+  // -----------------------------
+  // View model stream error → EMPTY
+  // -----------------------------
+  it('vm$ should not emit if healthService throws', (done) => {
     mockHealthService.getHealthSnapshot.mockReturnValue(throwError(() => new Error('API Error')));
 
     let emitted = false;
 
-    component.healthData$.subscribe({
-      next: () => {
-        emitted = true;
-      },
+    component.vm$.subscribe({
+      next: () => (emitted = true),
       complete: () => {
         expect(emitted).toBe(false);
         done();
@@ -106,73 +92,28 @@ describe('OverviewComponent', () => {
     mockRefreshIntervalService.intervalData$.complete();
   });
 
-  // --------------------------------------------------
-  // refreshIntervalObs - exhaustMap behavior
-  // --------------------------------------------------
-
-  it('should ignore new interval emissions until previous completes', () => {
-    const interval$ = new Subject<void>();
-    const inner$ = new Subject<any>();
-
-    const mockRefreshService = {
-      intervalData$: interval$
-    };
-
-    const testComponent = new OverviewComponent(
-      mockHealthService as any,
-      mockRefreshService as any
-    );
-
-    mockHealthService.getHealthSnapshot.mockReturnValue(inner$);
-
-    testComponent.healthData$.subscribe();
-
-    // First emission
-    interval$.next();
-
-    // Second emission (should be ignored)
-    interval$.next();
-
-    expect(mockHealthService.getHealthSnapshot).toHaveBeenCalledTimes(1);
-
-    // Complete first inner observable
-    inner$.complete();
-
-    // Now it should allow another call
-    interval$.next();
-
-    expect(mockHealthService.getHealthSnapshot).toHaveBeenCalledTimes(2);
+  // -----------------------------
+  // toggle health panel
+  // -----------------------------
+  it('should toggle panel open/close', () => {
+    expect(component.isHealthPanelOpen).toBe(false);
+    component.togglePanel();
+    expect(component.isHealthPanelOpen).toBe(true);
+    component.togglePanel();
+    expect(component.isHealthPanelOpen).toBe(false);
   });
 
-  // --------------------------------------------------
+  // -----------------------------
   // ngOnDestroy
-  // --------------------------------------------------
-
-  it('should complete destroy$ on destroy', () => {
-    const nextSpy = jest.spyOn((component as any).destroy$, 'next');
-    const completeSpy = jest.spyOn((component as any).destroy$, 'complete');
+  // -----------------------------
+  it('should complete destroy$', () => {
+    const destroy$ = (component as any).destroy$;
+    const nextSpy = jest.spyOn(destroy$, 'next');
+    const completeSpy = jest.spyOn(destroy$, 'complete');
 
     component.ngOnDestroy();
 
     expect(nextSpy).toHaveBeenCalled();
     expect(completeSpy).toHaveBeenCalled();
   });
-
-  // --------------------------------------------------
-  // refreshIntervalObs manual test
-  // --------------------------------------------------
-
-  it('refreshIntervalObs should pipe intervalData$', (done) => {
-    const testFn = jest.fn().mockReturnValue(of('TEST'));
-
-    const obs$ = component.refreshIntervalObs(testFn);
-
-    obs$.subscribe((value) => {
-      expect(value).toBe('TEST');
-      expect(testFn).toHaveBeenCalled();
-      done();
-    });
-
-    mockRefreshIntervalService.intervalData$.next();
-  });
 });
index d89b68fec5d819605d555b0f244d80a2c5787caa..6825d45d8170e915e6d02b33ae916fb37531312f 100644 (file)
@@ -1,13 +1,21 @@
-import { Component, OnDestroy } from '@angular/core';
+import { ChangeDetectionStrategy, Component, inject, OnDestroy } from '@angular/core';
 import { GridModule, TilesModule } from 'carbon-components-angular';
 import { OverviewStorageCardComponent } from './storage-card/overview-storage-card.component';
 import { HealthService } from '~/app/shared/api/health.service';
-import { HealthSnapshotMap } from '~/app/shared/models/health.interface';
+import { HealthCheck, HealthSnapshotMap } from '~/app/shared/models/health.interface';
 import { RefreshIntervalService } from '~/app/shared/services/refresh-interval.service';
-import { catchError, exhaustMap, takeUntil } from 'rxjs/operators';
+import { catchError, exhaustMap, map, takeUntil } from 'rxjs/operators';
 import { EMPTY, Observable, Subject } from 'rxjs';
 import { CommonModule } from '@angular/common';
 import { OverviewHealthCardComponent } from './health-card/overview-health-card.component';
+import { ComponentsModule } from '~/app/shared/components/components.module';
+import { HealthIconMap } from '~/app/shared/models/overview';
+
+interface OverviewVM {
+  healthData: HealthSnapshotMap | null;
+  incidentCount: number;
+  checks: { name: string; description: string; icon: string }[];
+}
 
 @Component({
   selector: 'cd-overview',
@@ -16,32 +24,52 @@ import { OverviewHealthCardComponent } from './health-card/overview-health-card.
     GridModule,
     TilesModule,
     OverviewStorageCardComponent,
-    OverviewHealthCardComponent
+    OverviewHealthCardComponent,
+    ComponentsModule
   ],
   standalone: true,
   templateUrl: './overview.component.html',
-  styleUrl: './overview.component.scss'
+  styleUrl: './overview.component.scss',
+  changeDetection: ChangeDetectionStrategy.OnPush
 })
 export class OverviewComponent implements OnDestroy {
+  isHealthPanelOpen: boolean = false;
+
+  private readonly healthService = inject(HealthService);
+  private readonly refreshIntervalService = inject(RefreshIntervalService);
+
   private destroy$ = new Subject<void>();
-  public healthData$: Observable<HealthSnapshotMap>;
-
-  constructor(
-    private healthService: HealthService,
-    private refreshIntervalService: RefreshIntervalService
-  ) {
-    this.healthData$ = this.refreshIntervalObs<HealthSnapshotMap>(() =>
-      this.healthService.getHealthSnapshot()
-    );
-  }
 
-  refreshIntervalObs<T>(fn: () => Observable<T>): Observable<T> {
+  private healthData$: Observable<HealthSnapshotMap> = this.refreshIntervalObs(() =>
+    this.healthService.getHealthSnapshot()
+  );
+
+  public vm$: Observable<OverviewVM> = this.healthData$.pipe(
+    map((data: HealthSnapshotMap) => {
+      const checks = data?.health?.checks ?? {};
+      return {
+        healthData: data,
+        incidentCount: Object.keys(checks)?.length,
+        checks: Object.entries(checks)?.map((check: [string, HealthCheck]) => ({
+          name: check?.[0],
+          description: check?.[1]?.summary?.message,
+          icon: HealthIconMap[check?.[1]?.severity]
+        }))
+      };
+    })
+  );
+
+  private refreshIntervalObs<T>(fn: () => Observable<T>): Observable<T> {
     return this.refreshIntervalService.intervalData$.pipe(
       exhaustMap(() => fn().pipe(catchError(() => EMPTY))),
       takeUntil(this.destroy$)
     );
   }
 
+  togglePanel() {
+    this.isHealthPanelOpen = !this.isHealthPanelOpen;
+  }
+
   ngOnDestroy() {
     this.destroy$.next();
     this.destroy$.complete();
index bbded48855bae288423a38ab20210b1fd0edce74..27d7ce1fc2fa4f5ccc4fc3d3e2f8ba32401f0f70 100644 (file)
@@ -111,6 +111,8 @@ import DataCenter16 from '@carbon/icons/es/data--center/16';
 import Upgrade16 from '@carbon/icons/es/upgrade/16';
 import Close16 from '@carbon/icons/es/close/16';
 import WarningAltFilled16 from '@carbon/icons/es/warning--alt--filled/16';
+import Help16 from '@carbon/icons/es/help/16';
+import IncidentReporter16 from '@carbon/icons/es/incident-reporter/16';
 
 import { TearsheetStepComponent } from './tearsheet-step/tearsheet-step.component';
 import { PageHeaderComponent } from './page-header/page-header.component';
@@ -280,7 +282,9 @@ export class ComponentsModule {
       DataViewAlt16,
       DataCenter16,
       Upgrade16,
-      WarningAltFilled16
+      WarningAltFilled16,
+      Help16,
+      IncidentReporter16
     ]);
   }
 }
index 426b15074805a210ea50d5b6f43f04efce71c511..4046a4e2a21535097975f949b142787f1a71c3da 100644 (file)
@@ -9,7 +9,7 @@
   @if(text) {
   <span data-toggle="tooltip"
         [title]="text"
-        ngClass="cds--type-mono">{{text}}</span>
+        class="cds--type-mono">{{text}}</span>
   }
   <cd-icon type="copy"></cd-icon>
 </button>
index 28d9c1cfe18fbade801f7abe892038947d83a82d..854b18549a80d3edba24ce12eb5d00bdd09714e3 100644 (file)
@@ -31,8 +31,12 @@ Using `color` in css and seyting svg will fill="currentColor does not work.
   fill: theme.$support-caution-major !important;
 }
 
+.warningAltFilled-icon {
+  fill: theme.$support-caution-major !important;
+}
+
 .error-icon {
-  fill: theme.$support-error !important;
+  fill: var(--cds-text-error) !important;
 }
 
 .info-icon {
index 74bc95dd5cc0a14c9f13bb7551615a9443702fd8..0af5a25a4024a2078d4996344748b5c894ef08cf 100644 (file)
     <cds-icon-button kind="ghost"
                      class="float-end"
                      title="Close"
+                     i18n-title
                      (click)="close()">
       <svg cdsIcon="close"></svg>
     </cds-icon-button>
-    <div class="panel-header cds--type-heading-03"
+    <div class="panel-header"
          *ngIf="headerText">
-    {{ headerText }}
+      <p class="cds--type-heading-03">{{ headerText }}</p>
+      <ng-content select="[panel-header-description]"></ng-content>
     </div>
   </div>
   <ng-content select=".panel-content"></ng-content>
index 318bd3c57b1a4b173b48c418f22ba3886746d4d9..6d97392b1ddc10ab4a082f9e6527d00c7185a952 100644 (file)
@@ -109,7 +109,9 @@ export enum Icons {
   dataViewAlt = 'data--view--alt',
   dataCenter = 'data--center',
   upgrade = 'upgrade',
-  warningAltFilled = 'warning--alt--filled'
+  warningAltFilled = 'warning--alt--filled',
+  help = 'help',
+  incidentReporter = 'incident-reporter'
 }
 
 export enum IconSize {
@@ -137,5 +139,7 @@ export const ICON_TYPE = {
   dataViewAlt: 'data--view--alt',
   dataCenter: 'data--center',
   upgrade: 'upgrade',
-  warningAltFilled: 'warning--alt--filled'
+  warningAltFilled: 'warning--alt--filled',
+  help: 'help',
+  incidentReporter: 'incident-reporter'
 } as const;
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
new file mode 100644 (file)
index 0000000..e43fa28
--- /dev/null
@@ -0,0 +1,6 @@
+export type HealthStatus = 'HEALTH_OK' | 'HEALTH_WARN' | 'HEALTH_ERR';
+export const HealthIconMap = {
+  HEALTH_OK: 'success',
+  HEALTH_WARN: 'warningAltFilled',
+  HEALTH_ERR: 'error'
+};