]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Add health card
authorAfreen Misbah <afreen@ibm.com>
Fri, 13 Feb 2026 23:14:46 +0000 (04:44 +0530)
committerAfreen Misbah <afreen@ibm.com>
Sun, 22 Feb 2026 09:36:12 +0000 (15:06 +0530)
Fixes https://tracker.ceph.com/issues/74958

Signed-off-by: Afreen Misbah <afreen@ibm.com>
18 files changed:
src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.html
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/overview/storage-card/overview-storage-card.component.html
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/productive-card/productive-card.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/components/productive-card/productive-card.component.scss
src/pybind/mgr/dashboard/frontend/src/app/shared/components/productive-card/productive-card.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ceph-version.pipe.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ceph-version.pipe.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/pipes.module.ts
src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_spacings.scss

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 (file)
index 0000000..1371bfa
--- /dev/null
@@ -0,0 +1,66 @@
+@let data = (data$ | async);
+<cd-productive-card>
+  <!-- HEALTH CARD Title -->
+  @if(fsid) {
+  <ng-template #header>
+    <div class="overview-health-card-header">
+      <div class="cds-mb-4 cds-mr-3"><cd-icon type="dataCenter"></cd-icon></div>
+      <h2
+        class="cds--type-heading-compact-02"
+        id="fsid">
+        <span>{{fsid}}</span>
+      </h2>
+      <cd-copy-2-clipboard-button
+        size="sm"
+        source="fsid"></cd-copy-2-clipboard-button>
+    </div>
+    <cds-icon-button
+      type="button"
+      kind="ghost"
+      size="sm"
+      description="Check logs"
+      i18n-description
+      [routerLink]="['/logs']">
+      <cd-icon type="dataViewAlt"></cd-icon>
+    </cds-icon-button>
+  </ng-template>
+  } @else {
+  <cds-skeleton-text
+    [lines]="1"
+    [maxLineWidth]="400"
+    [minLineWidth]="400"></cds-skeleton-text>
+  }
+  <!-- HEALTH CARD BODY -->
+  @if(data?.currentHealth){
+  <p class="cds--type-heading-05 cds-mb-0"
+     [ngClass]="'overview-health-card-status--' + data?.currentHealth?.icon">
+    {{data?.currentHealth?.title}}
+    <cd-icon [type]="data?.currentHealth?.icon"></cd-icon>
+  </p>
+  <p class="cds--type-label-01">{{data?.currentHealth?.message}}</p>
+  } @else {
+  <cds-skeleton-placeholder></cds-skeleton-placeholder>
+  }
+
+  @if(data?.summary?.version) {
+  <!-- CEPH VERSION -->
+  <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
+       [inline]="true"
+       i18n>
+      Upgrade available
+      <cd-icon type="upgrade"></cd-icon>
+    </a>
+    }
+  </p>
+  } @else {
+  <cds-skeleton-text
+    [lines]="1"
+    [maxLineWidth]="250"></cds-skeleton-text>
+  }
+</cd-productive-card>
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 (file)
index 0000000..33a495f
--- /dev/null
@@ -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 (file)
index 0000000..308957a
--- /dev/null
@@ -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<OverviewHealthCardComponent>;
+
+  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 (file)
index 0000000..a644300
--- /dev/null
@@ -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<HealthStatus, Health> = {
+  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<HealthStatus>(1);
+
+  private readonly summaryService = inject(SummaryService);
+  private readonly upgradeService = inject(UpgradeService);
+
+  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))
+    ),
+    this.health$
+  ]).pipe(
+    map(([summary, upgrade, health]) => ({ summary, upgrade, currentHealth: HealthMap?.[health] }))
+  );
+}
index 246edfff161cc73c5a4505cfd81b18d87d03e2fc..b713bd33ab929c47542797fc20dc118d4ee62b23 100644 (file)
@@ -1,3 +1,4 @@
+@let healthData = healthData$ | async;
 <div cdsGrid
      [narrow]="true"
      [condensed]="false"
@@ -8,7 +9,10 @@
     <div cdsCol
          class="cds-mb-5"
          [columnNumbers]="{lg: 11}">
-      <cds-tile>Health card</cds-tile>
+      <cd-overview-health-card
+        [fsid]="healthData?.fsid"
+        [health]="healthData?.health?.status">
+      </cd-overview-health-card>
     </div>
     <div cdsCol
          class="cds-mb-5"
     <div cdsCol
          class="cds-mb-5"
          [columnNumbers]="{lg: 16}">
-      @if (healthData$ | async; as healthData) {
       <cd-overview-storage-card
-        [total]="healthData.pgmap.bytes_total"
-        [used]="healthData.pgmap.bytes_used">
+        [total]="healthData?.pgmap.bytes_total"
+        [used]="healthData?.pgmap.bytes_used">
       </cd-overview-storage-card>
-      }
     </div>
   </div>
   <div cdsRow>
index 831eb7458b1fdc26038632af86f00bc754fec979..3f893f5254e0383077542bd6af304731f03a3c1d 100644 (file)
@@ -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<OverviewComponent>;
 
@@ -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 }
       ]
index 16f5afd02a460b25902e7bb7744aa00c8ad39fbb..d89b68fec5d819605d555b0f244d80a2c5787caa 100644 (file)
@@ -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'
index 6400faf4dd56e1d7828e5df94bb1af75445b6ecf..691462af09c4de803b04e8157e69ddcce2446ac7 100644 (file)
@@ -1,8 +1,7 @@
-<cd-productive-card
-  headerTitle="Storage overview"
-  i18n-headerTitle>
-  <!-- STORAGE CARD HEADER DROPDOWN -->
-  <ng-template #headerAction>
+<cd-productive-card>
+  <!-- STORAGE CARD HEADER -->
+  <ng-template #header>
+    <h2 class="cds--type-heading-compact-02">Storage Overview</h2>
     <cds-dropdown
       label="Storage type"
       class="overview-storage-card-dropdown"
index dd7751b289c9abf64abd33afa0ca923f473a1386..bbded48855bae288423a38ab20210b1fd0edce74 100644 (file)
@@ -106,7 +106,12 @@ import ErrorFilledIcon from '@carbon/icons/es/error--filled/16';
 import InformationFilledIcon from '@carbon/icons/es/information--filled/16';
 import WarningFilledIcon from '@carbon/icons/es/warning--filled/16';
 import NotificationFilledIcon from '@carbon/icons/es/notification--filled/16';
-import { Close16 } from '@carbon/icons';
+import DataViewAlt16 from '@carbon/icons/es/data--view--alt/16';
+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 { TearsheetStepComponent } from './tearsheet-step/tearsheet-step.component';
 import { PageHeaderComponent } from './page-header/page-header.component';
 import { SidebarLayoutComponent } from './sidebar-layout/sidebar-layout.component';
@@ -271,7 +276,11 @@ export class ComponentsModule {
       InformationFilledIcon,
       WarningFilledIcon,
       NotificationFilledIcon,
-      Close16
+      Close16,
+      DataViewAlt16,
+      DataCenter16,
+      Upgrade16,
+      WarningAltFilled16
     ]);
   }
 }
index 4046a4e2a21535097975f949b142787f1a71c3da..426b15074805a210ea50d5b6f43f04efce71c511 100644 (file)
@@ -9,7 +9,7 @@
   @if(text) {
   <span data-toggle="tooltip"
         [title]="text"
-        class="cds--type-mono">{{text}}</span>
+        ngClass="cds--type-mono">{{text}}</span>
   }
   <cd-icon type="copy"></cd-icon>
 </button>
index 97c8b98600a4d4358b0e3009e8f1510d57f051ae..d0e3187f254686d1a4c54ddda88db0402d853109 100644 (file)
@@ -1,23 +1,8 @@
 <cds-tile class="productive-card"
           [ngClass]="{'productive-card--shadow': applyShadow}"
           [cdsLayer]="0">
-  <header
-    cdsGrid
-    class="productive-card-header">
-    <div cdsRow
-         class="productive-card-header-row">
-      <div cdsCol
-           [columnNumbers]="{sm: headerActionTemplate ? 12 : 16, md: headerActionTemplate ? 12 : 16, lg: headerActionTemplate ? 12 : 16}">
-        <h2 class="cds--type-heading-compact-02">{{headerTitle}}</h2>
-      </div>
-      @if(!!headerActionTemplate) {
-      <div cdsCol
-           [columnNumbers]="{sm: 4, md: 4, lg: 4}"
-           class="productive-card-header-actions">
-        <ng-container *ngTemplateOutlet="headerActionTemplate"></ng-container>
-      </div>
-      }
-    </div>
+  <header class="productive-card-header">
+    <ng-container *ngTemplateOutlet="headerTemplate"></ng-container>
   </header>
   <section class="productive-card-section cds--type-body-compact-01"
            [ngClass]="{'productive-card-section--footer': footerTemplate}">
index a8b5edad58b4a4ac1969c41d35b884573c1c578a..49596dbb1e8587c9cd68b2afc1fe4cc2569d2c0c 100644 (file)
@@ -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 {
index 461c40edc48ecf95d117436b20c9526c9c923cbe..e7be726237fc411e666cfe13d96776e81cf62f7d 100644 (file)
@@ -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<any>;
+  headerTemplate?: TemplateRef<any>;
 
   /* Optional: Footer template , otherwise no footer will be used for card.*/
   @ContentChild('footer', {
index 1e6b926ed88cec17ea244d43f6d4c92119f149ee..318bd3c57b1a4b173b48c418f22ba3886746d4d9 100644 (file)
@@ -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 (file)
index 0000000..322ffe9
--- /dev/null
@@ -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 (file)
index 0000000..6e2beda
--- /dev/null
@@ -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;
+  }
+}
index ce21efa609c2827e8df1c00bf09ada066d2d92f4..458d0d5a311aae82abea7fbbacbfcbe593c7aff9 100755 (executable)
@@ -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 {}
index 5b0a6f598b9a6fa4076d22d1d3fcbe0f47e23391..6f59c43b99d7a87c0e19bbf907bae8f0fcfe1475 100644 (file)
   margin-bottom: layout.$spacing-03;
 }
 
+.cds-mb-4 {
+  margin-bottom: layout.$spacing-04;
+}
+
 .cds-mb-5 {
   margin-bottom: layout.$spacing-05;
 }