]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: dashboard-v3: inventory card
authorbryanmontalvan <bmontalv@redhat.com>
Wed, 10 Aug 2022 18:22:02 +0000 (14:22 -0400)
committerNizamudeen A <nia@redhat.com>
Tue, 21 Mar 2023 16:08:53 +0000 (21:38 +0530)
This commit starts the inventory-card which is one of the cards which
will be located in the landing-page revamp

tracker: https://tracker.ceph.com/issues/58065
Signed-off-by: bryanmontalvan <bmontalv@redhat.com>
Signed-off-by: Nizamudeen A <nia@redhat.com>
Signed-off-by: Pedro Gonzalez Gomez <pegonzal@redhat.com>
(cherry picked from commit c63523119dee6cc82ffae5162f2270ac7d06d02f)

18 files changed:
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card-row/card-row.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card-row/card-row.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card-row/card-row.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card-row/card-row.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/pg-summary.pipe.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/pg-summary.pipe.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/mds-summary.pipe.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/mds-summary.pipe.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/mgr-summary.pipe.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/mgr-summary.pipe.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/osd-summary.pipe.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/osd-summary.pipe.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/pipes.module.ts

diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card-row/card-row.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card-row/card-row.component.html
new file mode 100644 (file)
index 0000000..9b7bf03
--- /dev/null
@@ -0,0 +1,167 @@
+<div class="d-flex pl-1 pb-2 pt-2">
+  <div class="ms-2 me-auto">
+    <a [routerLink]="link"
+       *ngIf="link && total > 0; else noLinkTitle"
+       [ngPlural]="total"
+       i18n>
+        {{ total }}
+      <ng-template ngPluralCase="=0">{{ title }}</ng-template>
+      <ng-template ngPluralCase="=1">{{ title }}</ng-template>
+      <ng-template ngPluralCase="other">{{ title }}s</ng-template>
+    </a>
+  </div>
+
+  <ng-container [ngSwitch]="summaryType">
+    <ng-container *ngSwitchCase="'iscsi'">
+      <ng-container *ngTemplateOutlet="iscsiSummary"></ng-container>
+    </ng-container>
+    <ng-container *ngSwitchCase="'osd'">
+      <ng-container *ngTemplateOutlet="osdSummary"></ng-container>
+    </ng-container>
+    <ng-container *ngSwitchCase="'simplified'">
+      <ng-container *ngTemplateOutlet="simplifiedSummary"></ng-container>
+    </ng-container>
+    <ng-container *ngSwitchDefault>
+      <ng-container *ngTemplateOutlet="defaultSummary"></ng-container>
+    </ng-container>
+  </ng-container>
+</div>
+
+<ng-template #defaultSummary>
+  <span *ngIf="data.success || data.categoryPgAmount?.clean || (data.success === 0 && data.total === 0)">
+    <span *ngIf="data.success || (data.success === 0 && data.total === 0)">
+      {{ data.success }}
+    </span>
+    <span *ngIf="data.categoryPgAmount?.clean">
+      {{ data.categoryPgAmount?.clean }}
+    </span>
+    <i class="text-success"
+       [ngClass]="[icons.success]">
+    </i>
+  </span>
+  <span *ngIf="data.info"
+        class="ms-2">
+    <span *ngIf="data.info">
+      {{ data.info }}
+    </span>
+    <i class="text-info"
+       [ngClass]="[icons.danger]">
+    </i>
+  </span>
+  <span *ngIf="data.warn || data.categoryPgAmount?.warning"
+        class="ms-2">
+    <span *ngIf="data.warn">
+      {{ data.warn }}
+    </span>
+    <span *ngIf="data.categoryPgAmount?.warning">
+      {{ data.categoryPgAmount?.warning }}
+    </span>
+    <i class="text-warning"
+       [ngClass]="[icons.warning]">
+    </i>
+  </span>
+  <span *ngIf="data.error || data.categoryPgAmount?.unknown"
+        class="ms-2">
+    <span *ngIf="data.error">
+      {{ data.error }}
+    </span>
+    <span *ngIf="data.categoryPgAmount?.unknown">
+      {{ data.categoryPgAmount?.unknown }}
+    </span>
+    <i class="text-danger"
+       [ngClass]="[icons.danger]">
+    </i>
+  </span>
+  <span *ngIf="data.categoryPgAmount?.working"
+        class="ms-2">
+    <span *ngIf="data.categoryPgAmount?.working">
+      {{ data.categoryPgAmount?.working }}
+    </span>
+    <i class="text-warning"
+       [ngClass]="[icons.spinner, icons.spin]">
+    </i>
+  </span>
+</ng-template>
+
+<ng-template #osdSummary>
+  <span *ngIf="data.up === data.in">
+    {{ data.up }}
+    <i class="text-success"
+       [ngClass]="[icons.success]">
+    </i>
+  </span>
+  <span *ngIf="data.up !== data.in">
+    {{ data.up }}
+    <span class="fw-bold text-success">
+        up
+    </span>
+  </span>
+  <span *ngIf="data.in !== data.up"
+        class="ms-2">
+    {{ data.in }}
+    <span class="fw-bold text-success">
+        in
+    </span>
+  </span>
+  <span *ngIf="data.down"
+        class="ms-2">
+    {{ data.down }}
+    <span class="fw-bold text-danger me-2">
+        down
+    </span>
+  </span>
+  <span *ngIf="data.out"
+        class="ms-2">
+    {{ data.out }}
+    <span class="fw-bold text-danger me-2">
+        out
+    </span>
+  </span>
+  <span *ngIf="data.nearfull"
+        class="ms-2">
+        {{ data.nearfull }}
+    <span class="fw-bold text-warning me-2">
+      nearfull</span></span>
+  <span *ngIf="data.full"
+        class="ms-2">
+        {{ data.full }}
+    <span class="fw-bold text-danger">
+      full
+    </span>
+  </span>
+</ng-template>
+
+<ng-template #iscsiSummary>
+  <span>
+    {{ data.up }}
+    <i class="text-success"
+       *ngIf="data.up || data.up === 0"
+       [ngClass]="[icons.success]">
+    </i>
+  </span>
+  <span *ngIf="data.down"
+        class="ms-2">
+        {{ data.down }}
+    <i class="text-danger"
+       [ngClass]="[icons.danger]">
+    </i>
+  </span>
+</ng-template>
+
+<ng-template #simplifiedSummary>
+  <span>
+    {{ data }}
+    <i class="text-success"
+       [ngClass]="[icons.success]"></i>
+  </span>
+</ng-template>
+
+<ng-template #noLinkTitle>
+  <span *ngIf="total || total === 0"
+        [ngPlural]="total">
+    {{ total }}
+    <ng-template ngPluralCase="=0">{{ title }}</ng-template>
+    <ng-template ngPluralCase="=1">{{ title }}</ng-template>
+    <ng-template ngPluralCase="other">{{ title }}s</ng-template>
+  </span>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card-row/card-row.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card-row/card-row.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card-row/card-row.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card-row/card-row.component.spec.ts
new file mode 100644 (file)
index 0000000..8932e67
--- /dev/null
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { CardRowComponent } from './card-row.component';
+
+describe('CardRowComponent', () => {
+  let component: CardRowComponent;
+  let fixture: ComponentFixture<CardRowComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [CardRowComponent]
+    }).compileComponents();
+  });
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(CardRowComponent);
+    component = fixture.componentInstance;
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card-row/card-row.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card-row/card-row.component.ts
new file mode 100644 (file)
index 0000000..90c9391
--- /dev/null
@@ -0,0 +1,34 @@
+import { Component, Input, OnChanges } from '@angular/core';
+import { Icons } from '~/app/shared/enum/icons.enum';
+
+@Component({
+  selector: 'cd-card-row',
+  templateUrl: './card-row.component.html',
+  styleUrls: ['./card-row.component.scss']
+})
+export class CardRowComponent implements OnChanges {
+  @Input()
+  title: string;
+
+  @Input()
+  link: string;
+
+  @Input()
+  data: any;
+
+  @Input()
+  summaryType = 'default';
+
+  icons = Icons;
+  total: number;
+
+  ngOnChanges(): void {
+    if (this.data.total || this.data.total === 0) {
+      this.total = this.data.total;
+    } else if (this.summaryType === 'iscsi') {
+      this.total = this.data.up + this.data.down || 0;
+    } else {
+      this.total = this.data;
+    }
+  }
+}
index ac60eec648169c476aa6f6c50d08d311a103284e..c32c2a630a19d9dcfb308aa90fb95b594aa79a0d 100644 (file)
@@ -12,6 +12,8 @@ import { CephSharedModule } from '../shared/ceph-shared.module';
 import { CardComponent } from './card/card.component';
 import { DashboardPieComponent } from './dashboard-pie/dashboard-pie.component';
 import { DashboardComponent } from './dashboard/dashboard.component';
+import { CardRowComponent } from './card-row/card-row.component';
+import { PgSummaryPipe } from './pg-summary.pipe';
 
 @NgModule({
   imports: [
@@ -27,6 +29,13 @@ import { DashboardComponent } from './dashboard/dashboard.component';
     SimplebarAngularModule
   ],
 
-  declarations: [DashboardComponent, CardComponent, DashboardPieComponent]
+  declarations: [
+    DashboardComponent,
+    CardComponent,
+    DashboardPieComponent,
+    DashboardPieComponent,
+    CardRowComponent,
+    PgSummaryPipe
+  ]
 })
 export class NewDashboardModule {}
index b910f2f856b79086f8f42230fd2b495c78d5ba38..ec8f75a905d570834d1d9f313a85e37e380538a5 100644 (file)
@@ -1,4 +1,5 @@
-<div class="container-fluid">
+<div class="container-fluid"
+     *ngIf="healthData && enabledFeature$ | async as enabledFeature">
   <div class="row mx-0">
     <cd-card title="Details"
              i18n-title
index b3d5c3990f492719baad47f96a819cae3decf289..68ee9b5486c197deb525de2135efde396468bd38 100644 (file)
@@ -14,11 +14,15 @@ import { MgrModuleService } from '~/app/shared/api/mgr-module.service';
 import { PrometheusService } from '~/app/shared/api/prometheus.service';
 import { CssHelper } from '~/app/shared/classes/css-helper';
 import { AlertmanagerAlert } from '~/app/shared/models/prometheus-alerts';
-import { PipesModule } from '~/app/shared/pipes/pipes.module';
+import { FeatureTogglesService } from '~/app/shared/services/feature-toggles.service';
 import { SummaryService } from '~/app/shared/services/summary.service';
+import { SharedModule } from '~/app/shared/shared.module';
 import { configureTestBed } from '~/testing/unit-test-helper';
+import { PgCategoryService } from '../../shared/pg-category.service';
+import { CardRowComponent } from '../card-row/card-row.component';
 import { CardComponent } from '../card/card.component';
 import { DashboardPieComponent } from '../dashboard-pie/dashboard-pie.component';
+import { PgSummaryPipe } from '../pg-summary.pipe';
 import { DashboardComponent } from './dashboard.component';
 
 export class SummaryServiceMock {
@@ -41,6 +45,7 @@ describe('Dashbord Component', () => {
   let orchestratorService: MgrModuleService;
   let getHealthSpy: jasmine.Spy;
   let getAlertsSpy: jasmine.Spy;
+  let fakeFeatureTogglesService: jasmine.Spy;
 
   const healthPayload: Record<string, any> = {
     health: { status: 'HEALTH_OK' },
@@ -50,12 +55,12 @@ describe('Dashbord Component', () => {
     hosts: 0,
     rgw: 0,
     fs_map: { filesystems: [], standbys: [] },
-    iscsi_daemons: 0,
+    iscsi_daemons: 1,
     client_perf: {},
     scrub_status: 'Inactive',
     pools: [],
     df: { stats: {} },
-    pg_info: { object_stats: { num_objects: 0 } }
+    pg_info: { object_stats: { num_objects: 1 } }
   };
 
   const alertsPayload: AlertmanagerAlert[] = [
@@ -145,13 +150,32 @@ describe('Dashbord Component', () => {
   };
 
   configureTestBed({
-    imports: [RouterTestingModule, HttpClientTestingModule, ToastrModule.forRoot(), PipesModule],
-    declarations: [DashboardComponent, CardComponent, DashboardPieComponent],
+    imports: [RouterTestingModule, HttpClientTestingModule, ToastrModule.forRoot(), SharedModule],
+    declarations: [
+      DashboardComponent,
+      CardComponent,
+      DashboardPieComponent,
+      CardRowComponent,
+      PgSummaryPipe
+    ],
     schemas: [NO_ERRORS_SCHEMA],
-    providers: [{ provide: SummaryService, useClass: SummaryServiceMock }, CssHelper]
+    providers: [
+      { provide: SummaryService, useClass: SummaryServiceMock },
+      CssHelper,
+      PgCategoryService
+    ]
   });
 
   beforeEach(() => {
+    fakeFeatureTogglesService = spyOn(TestBed.inject(FeatureTogglesService), 'get').and.returnValue(
+      of({
+        rbd: true,
+        mirroring: true,
+        iscsi: true,
+        cephfs: true,
+        rgw: true
+      })
+    );
     fixture = TestBed.createComponent(DashboardComponent);
     component = fixture.componentInstance;
     configurationService = TestBed.inject(ConfigurationService);
@@ -168,6 +192,7 @@ describe('Dashbord Component', () => {
   });
 
   it('should render all cards', () => {
+    fixture.detectChanges();
     const dashboardCards = fixture.debugElement.nativeElement.querySelectorAll('cd-card');
     expect(dashboardCards.length).toBe(5);
   });
@@ -260,4 +285,32 @@ describe('Dashbord Component', () => {
     expect(successNotification).toBe(null);
     expect(dangerNotification).toBe(null);
   });
+
+  describe('features disabled', () => {
+    beforeEach(() => {
+      fakeFeatureTogglesService.and.returnValue(
+        of({
+          rbd: false,
+          mirroring: false,
+          iscsi: false,
+          cephfs: false,
+          rgw: false
+        })
+      );
+      fixture = TestBed.createComponent(DashboardComponent);
+      component = fixture.componentInstance;
+    });
+
+    it('should not render items related to disabled features', () => {
+      fixture.detectChanges();
+
+      const iscsiCard = fixture.debugElement.query(By.css('li[id=iscsi-item]'));
+      const rgwCard = fixture.debugElement.query(By.css('li[id=rgw-item]'));
+      const mds = fixture.debugElement.query(By.css('li[id=mds-item]'));
+
+      expect(iscsiCard).toBeFalsy();
+      expect(rgwCard).toBeFalsy();
+      expect(mds).toBeFalsy();
+    });
+  });
 });
index 009b6717721c9973fe8eedd8e9f9c21bd79735cf..ba6eccedff429a9d8b5f3dc2c220fe3619abb868 100644 (file)
@@ -1,4 +1,4 @@
-import { Component, NgZone, OnDestroy, OnInit } from '@angular/core';
+import { Component, OnDestroy, OnInit } from '@angular/core';
 
 import _ from 'lodash';
 import { Observable, Subscription } from 'rxjs';
@@ -19,6 +19,7 @@ import {
   FeatureTogglesMap$,
   FeatureTogglesService
 } from '~/app/shared/services/feature-toggles.service';
+import { RefreshIntervalService } from '~/app/shared/services/refresh-interval.service';
 import { SummaryService } from '~/app/shared/services/summary.service';
 
 @Component({
@@ -100,7 +101,13 @@ export class DashboardComponent implements OnInit, OnDestroy {
   }
 
   ngOnDestroy() {
-    window.clearInterval(this.interval);
+    this.interval.unsubscribe();
+  }
+
+  getHealth() {
+    this.healthService.getMinimalHealth().subscribe((data: any) => {
+      this.healthData = data;
+    });
   }
 
   toggleAlertsWindow(type: string, isToggleButton: boolean = false) {
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/pg-summary.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/pg-summary.pipe.spec.ts
new file mode 100644 (file)
index 0000000..b467167
--- /dev/null
@@ -0,0 +1,36 @@
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { PgCategoryService } from '../shared/pg-category.service';
+import { PgSummaryPipe } from './pg-summary.pipe';
+
+describe('OsdSummaryPipe', () => {
+  let pipe: PgSummaryPipe;
+
+  configureTestBed({
+    providers: [PgSummaryPipe, PgCategoryService]
+  });
+
+  beforeEach(() => {
+    pipe = TestBed.inject(PgSummaryPipe);
+  });
+
+  it('create an instance', () => {
+    expect(pipe).toBeTruthy();
+  });
+
+  it('tranforms value', () => {
+    const value = {
+      statuses: {
+        'active+clean': 241
+      },
+      pgs_per_osd: 241
+    };
+    expect(pipe.transform(value)).toEqual({
+      categoryPgAmount: {
+        clean: 241
+      },
+      total: 241
+    });
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/pg-summary.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/pg-summary.pipe.ts
new file mode 100644 (file)
index 0000000..a26097e
--- /dev/null
@@ -0,0 +1,27 @@
+import { Pipe, PipeTransform } from '@angular/core';
+import _ from 'lodash';
+import { PgCategoryService } from '~/app/ceph/shared/pg-category.service';
+
+@Pipe({
+  name: 'pgSummary'
+})
+export class PgSummaryPipe implements PipeTransform {
+  constructor(private pgCategoryService: PgCategoryService) {}
+
+  transform(value: any): any {
+    const categoryPgAmount: Record<string, number> = {};
+    let total = 0;
+    _.forEach(value.statuses, (pgAmount, pgStatesText) => {
+      const categoryType = this.pgCategoryService.getTypeByStates(pgStatesText);
+      if (_.isUndefined(categoryPgAmount[categoryType])) {
+        categoryPgAmount[categoryType] = 0;
+      }
+      categoryPgAmount[categoryType] += pgAmount;
+      total += pgAmount;
+    });
+    return {
+      categoryPgAmount,
+      total
+    };
+  }
+}
index 8d7f9a8f8ab7d93ede639079ca10b6d059f703c7..dfeecc52c088abcb6d333fb108b4a64d93045ff5 100644 (file)
@@ -21,6 +21,7 @@ export enum Icons {
   analyse = 'fa fa-stethoscope', // Scrub
   deepCheck = 'fa fa-cog', // Deep Scrub, Setting, Configuration
   reweight = 'fa fa-balance-scale', // Reweight
+  up = 'fa fa-arrow-up', // Up
   left = 'fa fa-arrow-left', // Mark out
   right = 'fa fa-arrow-right', // Mark in
   down = 'fa fa-arrow-down', // Mark Down
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/mds-summary.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/mds-summary.pipe.spec.ts
new file mode 100644 (file)
index 0000000..846cfb0
--- /dev/null
@@ -0,0 +1,76 @@
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { MdsSummaryPipe } from './mds-summary.pipe';
+
+describe('MdsSummaryPipe', () => {
+  let pipe: MdsSummaryPipe;
+
+  configureTestBed({
+    providers: [MdsSummaryPipe]
+  });
+
+  beforeEach(() => {
+    pipe = TestBed.inject(MdsSummaryPipe);
+  });
+
+  it('create an instance', () => {
+    expect(pipe).toBeTruthy();
+  });
+
+  it('transforms with 0 active and 2 standy', () => {
+    const payload = {
+      standbys: [{ name: 'a' }],
+      filesystems: [{ mdsmap: { info: [{ state: 'up:standby-replay' }] } }]
+    };
+
+    expect(pipe.transform(payload)).toEqual({
+      success: 0,
+      info: 2,
+      total: 2
+    });
+  });
+
+  it('transforms with 1 active and 1 standy', () => {
+    const payload = {
+      standbys: [{ name: 'b' }],
+      filesystems: [{ mdsmap: { info: [{ state: 'up:active', name: 'a' }] } }]
+    };
+    expect(pipe.transform(payload)).toEqual({
+      success: 1,
+      info: 1,
+      total: 2
+    });
+  });
+
+  it('transforms with 0 filesystems', () => {
+    const payload: Record<string, any> = {
+      standbys: [0],
+      filesystems: []
+    };
+
+    expect(pipe.transform(payload)).toEqual({
+      success: 0,
+      info: 0,
+      total: 0
+    });
+  });
+
+  it('transforms without filesystem', () => {
+    const payload = { standbys: [{ name: 'a' }] };
+
+    expect(pipe.transform(payload)).toEqual({
+      success: 0,
+      info: 1,
+      total: 1
+    });
+  });
+
+  it('transforms without value', () => {
+    expect(pipe.transform(undefined)).toEqual({
+      success: 0,
+      info: 0,
+      total: 0
+    });
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/mds-summary.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/mds-summary.pipe.ts
new file mode 100644 (file)
index 0000000..77758b7
--- /dev/null
@@ -0,0 +1,55 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+import _ from 'lodash';
+
+@Pipe({
+  name: 'mdsSummary'
+})
+export class MdsSummaryPipe implements PipeTransform {
+  transform(value: any): any {
+    if (!value) {
+      return {
+        success: 0,
+        info: 0,
+        total: 0
+      };
+    }
+
+    let activeCount = 0;
+    let standbyCount = 0;
+    let standbys = 0;
+    let active = 0;
+    let standbyReplay = 0;
+    _.each(value.standbys, () => {
+      standbys += 1;
+    });
+
+    if (value.standbys && !value.filesystems) {
+      standbyCount = standbys;
+      activeCount = 0;
+    } else if (value.filesystems.length === 0) {
+      activeCount = 0;
+    } else {
+      _.each(value.filesystems, (fs) => {
+        _.each(fs.mdsmap.info, (mds) => {
+          if (mds.state === 'up:standby-replay') {
+            standbyReplay += 1;
+          } else {
+            active += 1;
+          }
+        });
+      });
+
+      activeCount = active;
+      standbyCount = standbys + standbyReplay;
+    }
+    const totalCount = activeCount + standbyCount;
+    const mdsSummary = {
+      success: activeCount,
+      info: standbyCount,
+      total: totalCount
+    };
+
+    return mdsSummary;
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/mgr-summary.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/mgr-summary.pipe.spec.ts
new file mode 100644 (file)
index 0000000..ac7dcc6
--- /dev/null
@@ -0,0 +1,38 @@
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { MgrSummaryPipe } from './mgr-summary.pipe';
+
+describe('MgrSummaryPipe', () => {
+  let pipe: MgrSummaryPipe;
+
+  configureTestBed({
+    providers: [MgrSummaryPipe]
+  });
+
+  beforeEach(() => {
+    pipe = TestBed.inject(MgrSummaryPipe);
+  });
+
+  it('create an instance', () => {
+    expect(pipe).toBeTruthy();
+  });
+
+  it('transforms without value', () => {
+    expect(pipe.transform(undefined)).toEqual({
+      success: 0,
+      info: 0,
+      total: 0
+    });
+  });
+
+  it('transforms with 1 active and 2 standbys', () => {
+    const payload = {
+      active_name: 'x',
+      standbys: [{ name: 'y' }, { name: 'z' }]
+    };
+    const expected = { success: 1, info: 2, total: 3 };
+
+    expect(pipe.transform(payload)).toEqual(expected);
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/mgr-summary.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/mgr-summary.pipe.ts
new file mode 100644 (file)
index 0000000..14b3809
--- /dev/null
@@ -0,0 +1,37 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+import _ from 'lodash';
+
+@Pipe({
+  name: 'mgrSummary'
+})
+export class MgrSummaryPipe implements PipeTransform {
+  transform(value: any): any {
+    if (!value) {
+      return {
+        success: 0,
+        info: 0,
+        total: 0
+      };
+    }
+
+    let activeCount: number;
+    const activeTitleText = _.isUndefined(value.active_name)
+      ? ''
+      : `${$localize`active daemon`}: ${value.active_name}`;
+    // There is always one standbyreplay to replace active daemon, if active one is down
+    if (activeTitleText.length > 0) {
+      activeCount = 1;
+    }
+    const standbyCount = value.standbys.length;
+    const totalCount = activeCount + standbyCount;
+
+    const mgrSummary = {
+      success: activeCount,
+      info: standbyCount,
+      total: totalCount
+    };
+
+    return mgrSummary;
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/osd-summary.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/osd-summary.pipe.spec.ts
new file mode 100644 (file)
index 0000000..2c60fa5
--- /dev/null
@@ -0,0 +1,43 @@
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { OsdSummaryPipe } from './osd-summary.pipe';
+
+describe('OsdSummaryPipe', () => {
+  let pipe: OsdSummaryPipe;
+
+  configureTestBed({
+    providers: [OsdSummaryPipe]
+  });
+
+  beforeEach(() => {
+    pipe = TestBed.inject(OsdSummaryPipe);
+  });
+
+  it('create an instance', () => {
+    expect(pipe).toBeTruthy();
+  });
+
+  it('transforms without value', () => {
+    expect(pipe.transform(undefined)).toBe('');
+  });
+
+  it('transforms having 3 osd with 3 up, 3 in, 0 down, 0 out', () => {
+    const value = {
+      osds: [
+        { up: 1, in: 1, state: ['up', 'exists'] },
+        { up: 1, in: 1, state: ['up', 'exists'] },
+        { up: 1, in: 1, state: ['up', 'exists'] }
+      ]
+    };
+    expect(pipe.transform(value)).toEqual({
+      total: 3,
+      down: 0,
+      out: 0,
+      up: 3,
+      in: 3,
+      nearfull: 0,
+      full: 0
+    });
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/osd-summary.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/osd-summary.pipe.ts
new file mode 100644 (file)
index 0000000..66e8697
--- /dev/null
@@ -0,0 +1,46 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+import _ from 'lodash';
+
+@Pipe({
+  name: 'osdSummary'
+})
+export class OsdSummaryPipe implements PipeTransform {
+  transform(value: any): any {
+    if (!value) {
+      return '';
+    }
+
+    let inCount = 0;
+    let upCount = 0;
+    let nearFullCount = 0;
+    let fullCount = 0;
+    _.each(value.osds, (osd) => {
+      if (osd.in) {
+        inCount++;
+      }
+      if (osd.up) {
+        upCount++;
+      }
+      if (osd.state.includes('nearfull')) {
+        nearFullCount++;
+      }
+      if (osd.state.includes('full')) {
+        fullCount++;
+      }
+    });
+
+    const downCount = value.osds.length - upCount;
+    const outCount = value.osds.length - inCount;
+    const osdSummary = {
+      total: value.osds.length,
+      down: downCount,
+      out: outCount,
+      up: upCount,
+      in: inCount,
+      nearfull: nearFullCount,
+      full: fullCount
+    };
+    return osdSummary;
+  }
+}
index 226972ce0cae084665951a72d44f0f85cedc5f85..4abc029533ac39f993d63c397f49c29bcc4c4477 100755 (executable)
@@ -22,9 +22,12 @@ import { IscsiBackstorePipe } from './iscsi-backstore.pipe';
 import { JoinPipe } from './join.pipe';
 import { LogPriorityPipe } from './log-priority.pipe';
 import { MapPipe } from './map.pipe';
+import { MdsSummaryPipe } from './mds-summary.pipe';
+import { MgrSummaryPipe } from './mgr-summary.pipe';
 import { MillisecondsPipe } from './milliseconds.pipe';
 import { NotAvailablePipe } from './not-available.pipe';
 import { OrdinalPipe } from './ordinal.pipe';
+import { OsdSummaryPipe } from './osd-summary.pipe';
 import { RbdConfigurationSourcePipe } from './rbd-configuration-source.pipe';
 import { RelativeDatePipe } from './relative-date.pipe';
 import { RoundPipe } from './round.pipe';
@@ -66,7 +69,10 @@ import { UpperFirstPipe } from './upper-first.pipe';
     TruncatePipe,
     SanitizeHtmlPipe,
     SearchHighlightPipe,
-    HealthIconPipe
+    HealthIconPipe,
+    MgrSummaryPipe,
+    MdsSummaryPipe,
+    OsdSummaryPipe
   ],
   exports: [
     ArrayPipe,
@@ -99,7 +105,10 @@ import { UpperFirstPipe } from './upper-first.pipe';
     TruncatePipe,
     SanitizeHtmlPipe,
     SearchHighlightPipe,
-    HealthIconPipe
+    HealthIconPipe,
+    MgrSummaryPipe,
+    MdsSummaryPipe,
+    OsdSummaryPipe
   ],
   providers: [
     ArrayPipe,
@@ -127,7 +136,10 @@ import { UpperFirstPipe } from './upper-first.pipe';
     MapPipe,
     TruncatePipe,
     SanitizeHtmlPipe,
-    HealthIconPipe
+    HealthIconPipe,
+    MgrSummaryPipe,
+    MdsSummaryPipe,
+    OsdSummaryPipe
   ]
 })
 export class PipesModule {}