From: Afreen Misbah Date: Fri, 13 Feb 2026 23:14:46 +0000 (+0530) Subject: mgr/dashboard: Add health card X-Git-Tag: v21.0.0~261^2~1 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=a2fcb3c9772870b33780642c29451b9b9d42decd;p=ceph.git mgr/dashboard: Add health card Fixes https://tracker.ceph.com/issues/74958 Signed-off-by: Afreen Misbah --- diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.html new file mode 100644 index 000000000000..1371bfae110d --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.html @@ -0,0 +1,66 @@ +@let data = (data$ | async); + + + @if(fsid) { + +
+
+

+ {{fsid}} +

+ +
+ + + +
+ } @else { + + } + + @if(data?.currentHealth){ +

+ {{data?.currentHealth?.title}} + +

+

{{data?.currentHealth?.message}}

+ } @else { + + } + + @if(data?.summary?.version) { + +

+ Ceph version:  + {{ data?.summary?.version | cephVersion }}  + + @if (data?.upgrade?.versions?.length) { + + Upgrade available + + + } +

+ } @else { + + } +
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.scss new file mode 100644 index 000000000000..33a495f3ed08 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.scss @@ -0,0 +1,36 @@ +.overview-health-card { + &-header { + display: flex; + align-items: end; + } + + // CSS for status text, modifier names match icons name + &-status--success { + color: var(--cds-support-success); + } + + &-status--warningAltFilled { + color: var(--cds-support-caution-major); + } + + &-status--error { + color: var(--cds-text-error); + } +} + +// Overrides +.clipboard-btn { + padding: var(--cds-spacing-02); +} + +.cds--btn--icon-only { + padding: var(--cds-spacing-01); +} + +.cds--link.cds--link--inline { + text-decoration: none; +} + +.cds--skeleton__placeholder { + margin-bottom: var(--cds-spacing-03); +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.spec.ts new file mode 100644 index 000000000000..308957a6b8b1 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.spec.ts @@ -0,0 +1,57 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { of } from 'rxjs'; + +import { OverviewHealthCardComponent } from './overview-health-card.component'; +import { SummaryService } from '~/app/shared/services/summary.service'; +import { UpgradeService } from '~/app/shared/api/upgrade.service'; +import { provideRouter, RouterModule } from '@angular/router'; +import { CommonModule } from '@angular/common'; +import { SkeletonModule, ButtonModule, LinkModule } from 'carbon-components-angular'; +import { ComponentsModule } from '~/app/shared/components/components.module'; +import { ProductiveCardComponent } from '~/app/shared/components/productive-card/productive-card.component'; +import { PipesModule } from '~/app/shared/pipes/pipes.module'; + +describe('OverviewStorageCardComponent (Jest)', () => { + let component: OverviewHealthCardComponent; + let fixture: ComponentFixture; + + const summaryServiceMock = { + summaryData$: of({ + version: + 'ceph version 13.1.0-419-g251e2515b5 (251e2515b563856349498c6caf34e7a282f62937) nautilus (dev)' + }) + }; + + const upgradeServiceMock = { + listCached: jest.fn(() => of({ versions: [] })) + }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + OverviewHealthCardComponent, + CommonModule, + ProductiveCardComponent, + SkeletonModule, + ButtonModule, + RouterModule, + ComponentsModule, + LinkModule, + PipesModule + ], + providers: [ + { provide: SummaryService, useValue: summaryServiceMock }, + { provide: UpgradeService, useValue: upgradeServiceMock }, + provideRouter([]) + ] + }).compileComponents(); + + fixture = TestBed.createComponent(OverviewHealthCardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.ts new file mode 100644 index 000000000000..a644300d6904 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.ts @@ -0,0 +1,94 @@ +import { + ChangeDetectionStrategy, + Component, + inject, + Input, + ViewEncapsulation +} from '@angular/core'; +import { SkeletonModule, ButtonModule, LinkModule } 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'; +import { SummaryService } from '~/app/shared/services/summary.service'; +import { Summary } from '~/app/shared/models/summary.model'; +import { combineLatest, Observable, of, ReplaySubject } from 'rxjs'; +import { CommonModule } from '@angular/common'; +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'; + +type OverviewHealthData = { + summary: Summary; + upgrade: UpgradeInfoInterface; + currentHealth: Health; +}; + +type Health = { + message: string; + title: string; + icon: string; +}; + +type HealthStatus = 'HEALTH_OK' | 'HEALTH_WARN' | 'HEALTH_ERR'; +const WarnAndErrMessage = $localize`There are active alerts and unresolved health warnings.`; + +const HealthMap: Record = { + HEALTH_OK: { + message: $localize`All core services are running normally`, + icon: 'success', + title: $localize`Healthy` + }, + HEALTH_WARN: { + message: WarnAndErrMessage, + icon: 'warningAltFilled', + title: $localize`Warning` + }, + HEALTH_ERR: { + message: WarnAndErrMessage, + icon: 'error', + title: $localize`Critical` + } +}; + +@Component({ + selector: 'cd-overview-health-card', + imports: [ + CommonModule, + ProductiveCardComponent, + SkeletonModule, + ButtonModule, + RouterModule, + ComponentsModule, + LinkModule, + PipesModule + ], + standalone: true, + templateUrl: './overview-health-card.component.html', + styleUrl: './overview-health-card.component.scss', + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class OverviewHealthCardComponent { + @Input() fsid!: string; + @Input() + set health(value: HealthStatus) { + this.health$.next(value); + } + private health$ = new ReplaySubject(1); + + private readonly summaryService = inject(SummaryService); + private readonly upgradeService = inject(UpgradeService); + + readonly data$: Observable = combineLatest([ + this.summaryService.summaryData$.pipe(filter((summary): summary is Summary => !!summary)), + + this.upgradeService.listCached().pipe( + startWith(null as UpgradeInfoInterface), + catchError(() => of(null)) + ), + this.health$ + ]).pipe( + map(([summary, upgrade, health]) => ({ summary, upgrade, currentHealth: HealthMap?.[health] })) + ); +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.html index 246edfff161c..b713bd33ab92 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.html @@ -1,3 +1,4 @@ +@let healthData = healthData$ | async;
- Health card + +
- @if (healthData$ | async; as healthData) { + [total]="healthData?.pgmap.bytes_total" + [used]="healthData?.pgmap.bytes_used"> - }
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.spec.ts index 831eb7458b1f..3f893f5254e0 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.spec.ts @@ -5,8 +5,13 @@ import { OverviewComponent } from './overview.component'; import { HealthService } from '~/app/shared/api/health.service'; import { RefreshIntervalService } from '~/app/shared/services/refresh-interval.service'; import { HealthSnapshotMap } from '~/app/shared/models/health.interface'; +import { provideHttpClient } from '@angular/common/http'; +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'; -describe('OverviewComponent (Jest)', () => { +describe('OverviewComponent', () => { let component: OverviewComponent; let fixture: ComponentFixture; @@ -28,8 +33,16 @@ describe('OverviewComponent (Jest)', () => { }; await TestBed.configureTestingModule({ - imports: [OverviewComponent], + imports: [ + OverviewComponent, + CommonModule, + GridModule, + TilesModule, + OverviewStorageCardComponent, + OverviewHealthCardComponent + ], providers: [ + provideHttpClient(), { provide: HealthService, useValue: mockHealthService }, { provide: RefreshIntervalService, useValue: mockRefreshIntervalService } ] diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.ts index 16f5afd02a46..d89b68fec5d8 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.ts @@ -7,10 +7,17 @@ import { RefreshIntervalService } from '~/app/shared/services/refresh-interval.s import { catchError, exhaustMap, takeUntil } from 'rxjs/operators'; import { EMPTY, Observable, Subject } from 'rxjs'; import { CommonModule } from '@angular/common'; +import { OverviewHealthCardComponent } from './health-card/overview-health-card.component'; @Component({ selector: 'cd-overview', - imports: [GridModule, TilesModule, OverviewStorageCardComponent, CommonModule], + imports: [ + CommonModule, + GridModule, + TilesModule, + OverviewStorageCardComponent, + OverviewHealthCardComponent + ], standalone: true, templateUrl: './overview.component.html', styleUrl: './overview.component.scss' diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/storage-card/overview-storage-card.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/storage-card/overview-storage-card.component.html index 6400faf4dd56..691462af09c4 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/storage-card/overview-storage-card.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/storage-card/overview-storage-card.component.html @@ -1,8 +1,7 @@ - - - + + + +

Storage Overview

{{text}} + ngClass="cds--type-mono">{{text}} } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/productive-card/productive-card.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/productive-card/productive-card.component.html index 97c8b98600a4..d0e3187f2546 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/productive-card/productive-card.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/productive-card/productive-card.component.html @@ -1,23 +1,8 @@ -
-
-
-

{{headerTitle}}

-
- @if(!!headerActionTemplate) { -
- -
- } -
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/productive-card/productive-card.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/productive-card/productive-card.component.scss index a8b5edad58b4..49596dbb1e85 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/productive-card/productive-card.component.scss +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/productive-card/productive-card.component.scss @@ -5,16 +5,11 @@ padding: 0; &-header { - padding-inline: var(--cds-spacing-05); margin: 0; - } - - &-header-row { padding: var(--cds-spacing-05); - } - - &-header-actions { - padding-right: 0; + display: flex; + justify-content: space-between; + align-items: end; } &-section { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/productive-card/productive-card.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/productive-card/productive-card.component.ts index 461c40edc48e..e7be726237fc 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/productive-card/productive-card.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/productive-card/productive-card.component.ts @@ -21,17 +21,14 @@ import { GridModule, LayerModule, TilesModule } from 'carbon-components-angular' styleUrl: './productive-card.component.scss' }) export class ProductiveCardComponent { - /* Card Title */ - @Input() headerTitle!: string; - /* Optional: Applies a tinted-colored background to card */ @Input() applyShadow: boolean = false; /* Optional: Header action template, appears alongwith title in top-right corner */ - @ContentChild('headerAction', { + @ContentChild('header', { read: TemplateRef }) - headerActionTemplate?: TemplateRef; + headerTemplate?: TemplateRef; /* Optional: Footer template , otherwise no footer will be used for card.*/ @ContentChild('footer', { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts index 1e6b926ed88c..318bd3c57b1a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts @@ -105,7 +105,11 @@ export enum Icons { error = 'error--filled', notificationOff = 'notification--off', notificationNew = 'notification--new', - emptySearch = 'search' + emptySearch = 'search', + dataViewAlt = 'data--view--alt', + dataCenter = 'data--center', + upgrade = 'upgrade', + warningAltFilled = 'warning--alt--filled' } export enum IconSize { @@ -129,5 +133,9 @@ export const ICON_TYPE = { success: 'success', warning: 'warning', add: 'add', - emptySearch: 'emptySearch' + emptySearch: 'emptySearch', + dataViewAlt: 'data--view--alt', + dataCenter: 'data--center', + upgrade: 'upgrade', + warningAltFilled: 'warning--alt--filled' } as const; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ceph-version.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ceph-version.pipe.spec.ts new file mode 100644 index 000000000000..322ffe9fda66 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ceph-version.pipe.spec.ts @@ -0,0 +1,15 @@ +import { VERSION_PREFIX } from '~/app/shared/constants/app.constants'; +import { CephVersionPipe } from './ceph-version.pipe'; + +describe('CephVersionPipe', () => { + const pipe = new CephVersionPipe(); + + it('create an instance', () => { + expect(pipe).toBeTruthy(); + }); + + it('extracts version correctly', () => { + const value = `${VERSION_PREFIX} 20.3.0-5182-g70be2125 (70be21257b5dac58119850e36211f267cc8b541a) tentacle (dev - RelWithDebInfo)`; + expect(pipe.transform(value)).toBe('20.3.0-5182-g70be2125 tentacle (dev - RelWithDebInfo)'); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ceph-version.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ceph-version.pipe.ts new file mode 100644 index 000000000000..6e2bedaaf860 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ceph-version.pipe.ts @@ -0,0 +1,19 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { VERSION_PREFIX } from '~/app/shared/constants/app.constants'; + +@Pipe({ + name: 'cephVersion', + standalone: false +}) +export class CephVersionPipe implements PipeTransform { + transform(value: string = ''): string { + // Expect "ceph version 13.1.0-419-g251e2515b5 + // (251e2515b563856349498c6caf34e7a282f62937) nautilus (dev)" + if (value) { + const version = value.replace(`${VERSION_PREFIX} `, '').split(' '); + return version[0] + ' ' + version.slice(2, version.length).join(' '); + } + + return value; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/pipes.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/pipes.module.ts index ce21efa609c2..458d0d5a311a 100755 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/pipes.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/pipes.module.ts @@ -41,6 +41,7 @@ import { MbpersecondPipe } from './mbpersecond.pipe'; import { PipeFunctionPipe } from './pipe-function.pipe'; import { DimlessBinaryPerMinutePipe } from './dimless-binary-per-minute.pipe'; import { RedirectLinkResolverPipe } from './redirect-link-resolver.pipe'; +import { CephVersionPipe } from './ceph-version.pipe'; @NgModule({ imports: [CommonModule], @@ -84,7 +85,8 @@ import { RedirectLinkResolverPipe } from './redirect-link-resolver.pipe'; MbpersecondPipe, PipeFunctionPipe, DimlessBinaryPerMinutePipe, - RedirectLinkResolverPipe + RedirectLinkResolverPipe, + CephVersionPipe ], exports: [ ArrayPipe, @@ -126,7 +128,8 @@ import { RedirectLinkResolverPipe } from './redirect-link-resolver.pipe'; MbpersecondPipe, PipeFunctionPipe, DimlessBinaryPerMinutePipe, - RedirectLinkResolverPipe + RedirectLinkResolverPipe, + CephVersionPipe ], providers: [ ArrayPipe, @@ -159,7 +162,8 @@ import { RedirectLinkResolverPipe } from './redirect-link-resolver.pipe'; OctalToHumanReadablePipe, MbpersecondPipe, DimlessBinaryPerMinutePipe, - RedirectLinkResolverPipe + RedirectLinkResolverPipe, + CephVersionPipe ] }) export class PipesModule {} diff --git a/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_spacings.scss b/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_spacings.scss index 5b0a6f598b9a..6f59c43b99d7 100644 --- a/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_spacings.scss +++ b/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_spacings.scss @@ -34,6 +34,10 @@ margin-bottom: layout.$spacing-03; } +.cds-mb-4 { + margin-bottom: layout.$spacing-04; +} + .cds-mb-5 { margin-bottom: layout.$spacing-05; }