]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: dashboard-v3: status card
authorbryanmontalvan <68972382+bryanmontalvan@users.noreply.github.com>
Wed, 3 Aug 2022 01:39:05 +0000 (21:39 -0400)
committerPedro Gonzalez Gomez <pegonzal@redhat.com>
Wed, 15 Feb 2023 12:20:40 +0000 (13:20 +0100)
This commit is the bare-bones work of the status card. The only logic
written in this commit is the Cluster health status icon.

tracker: https://tracker.ceph.com/issues/58728
Signed-off-by: bryanmontalvan <bmontalv@redhat.com>
mgr/dashboard: introduce active alerts to status cards

Signed-off-by: Nizamudeen A <nia@redhat.com>
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.scss
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/shared/enum/health-icon.enum.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/models/prometheus-alerts.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-icon.pipe.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-icon.pipe.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/pipes.module.ts

index 34d41ddb31adbf1cb23e5db3daf13173f82eabf0..ac60eec648169c476aa6f6c50d08d311a103284e 100644 (file)
@@ -5,6 +5,7 @@ import { RouterModule } from '@angular/router';
 
 import { NgbNavModule, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap';
 import { ChartsModule } from 'ng2-charts';
+import { SimplebarAngularModule } from 'simplebar-angular';
 
 import { SharedModule } from '~/app/shared/shared.module';
 import { CephSharedModule } from '../shared/ceph-shared.module';
@@ -22,7 +23,8 @@ import { DashboardComponent } from './dashboard/dashboard.component';
     RouterModule,
     NgbPopoverModule,
     FormsModule,
-    ReactiveFormsModule
+    ReactiveFormsModule,
+    SimplebarAngularModule
   ],
 
   declarations: [DashboardComponent, CardComponent, DashboardPieComponent]
index 50789c877319e9283cde76a1076ecfee61ef7a1e..140f5f78fa4ad842ce31feef75cf27e1d5d658a4 100644 (file)
@@ -1,3 +1,11 @@
+.alerts {
+  height: 17rem;
+
+  div {
+    padding-top: 0;
+  }
+}
+
 div {
   padding-top: 20px;
 }
index 113ac8cfe956fbbd55921ad8d183611f58a1f7e0..b3d5c3990f492719baad47f96a819cae3decf289 100644 (file)
@@ -1,14 +1,20 @@
 import { HttpClientTestingModule } from '@angular/common/http/testing';
 import { NO_ERRORS_SCHEMA } from '@angular/core';
 import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
 import { RouterTestingModule } from '@angular/router/testing';
 
+import _ from 'lodash';
+import { ToastrModule } from 'ngx-toastr';
 import { BehaviorSubject, of } from 'rxjs';
 
 import { ConfigurationService } from '~/app/shared/api/configuration.service';
+import { HealthService } from '~/app/shared/api/health.service';
 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 { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+import { AlertmanagerAlert } from '~/app/shared/models/prometheus-alerts';
+import { PipesModule } from '~/app/shared/pipes/pipes.module';
 import { SummaryService } from '~/app/shared/services/summary.service';
 import { configureTestBed } from '~/testing/unit-test-helper';
 import { CardComponent } from '../card/card.component';
@@ -28,11 +34,98 @@ export class SummaryServiceMock {
   }
 }
 
-describe('CardComponent', () => {
+describe('Dashbord Component', () => {
   let component: DashboardComponent;
   let fixture: ComponentFixture<DashboardComponent>;
   let configurationService: ConfigurationService;
   let orchestratorService: MgrModuleService;
+  let getHealthSpy: jasmine.Spy;
+  let getAlertsSpy: jasmine.Spy;
+
+  const healthPayload: Record<string, any> = {
+    health: { status: 'HEALTH_OK' },
+    mon_status: { monmap: { mons: [] }, quorum: [] },
+    osd_map: { osds: [] },
+    mgr_map: { standbys: [] },
+    hosts: 0,
+    rgw: 0,
+    fs_map: { filesystems: [], standbys: [] },
+    iscsi_daemons: 0,
+    client_perf: {},
+    scrub_status: 'Inactive',
+    pools: [],
+    df: { stats: {} },
+    pg_info: { object_stats: { num_objects: 0 } }
+  };
+
+  const alertsPayload: AlertmanagerAlert[] = [
+    {
+      labels: {
+        alertname: 'CephMgrPrometheusModuleInactive',
+        instance: 'ceph2:9283',
+        job: 'ceph',
+        severity: 'critical'
+      },
+      annotations: {
+        description: 'The mgr/prometheus module at ceph2:9283 is unreachable.',
+        summary: 'The mgr/prometheus module is not available'
+      },
+      startsAt: '2022-09-28T08:23:41.152Z',
+      endsAt: '2022-09-28T15:28:01.152Z',
+      generatorURL: 'http://prometheus:9090/testUrl',
+      status: {
+        state: 'active',
+        silencedBy: null,
+        inhibitedBy: null
+      },
+      receivers: ['ceph2'],
+      fingerprint: 'fingerprint'
+    },
+    {
+      labels: {
+        alertname: 'CephOSDDownHigh',
+        instance: 'ceph:9283',
+        job: 'ceph',
+        severity: 'critical'
+      },
+      annotations: {
+        description: '66.67% or 2 of 3 OSDs are down (>= 10%).',
+        summary: 'More than 10% of OSDs are down'
+      },
+      startsAt: '2022-09-28T14:17:22.665Z',
+      endsAt: '2022-09-28T15:28:32.665Z',
+      generatorURL: 'http://prometheus:9090/testUrl',
+      status: {
+        state: 'active',
+        silencedBy: null,
+        inhibitedBy: null
+      },
+      receivers: ['default'],
+      fingerprint: 'fingerprint'
+    },
+    {
+      labels: {
+        alertname: 'CephHealthWarning',
+        instance: 'ceph:9283',
+        job: 'ceph',
+        severity: 'warning'
+      },
+      annotations: {
+        description: 'The cluster state has been HEALTH_WARN for more than 15 minutes.',
+        summary: 'Ceph is in the WARNING state'
+      },
+      startsAt: '2022-09-28T08:41:38.454Z',
+      endsAt: '2022-09-28T15:28:38.454Z',
+      generatorURL: 'http://prometheus:9090/testUrl',
+      status: {
+        state: 'active',
+        silencedBy: null,
+        inhibitedBy: null
+      },
+      receivers: ['ceph'],
+      fingerprint: 'fingerprint'
+    }
+  ];
 
   const configValueData: any = {
     value: [
@@ -52,14 +145,10 @@ describe('CardComponent', () => {
   };
 
   configureTestBed({
-    imports: [RouterTestingModule, HttpClientTestingModule],
+    imports: [RouterTestingModule, HttpClientTestingModule, ToastrModule.forRoot(), PipesModule],
     declarations: [DashboardComponent, CardComponent, DashboardPieComponent],
     schemas: [NO_ERRORS_SCHEMA],
-    providers: [
-      CssHelper,
-      DimlessBinaryPipe,
-      { provide: SummaryService, useClass: SummaryServiceMock }
-    ]
+    providers: [{ provide: SummaryService, useClass: SummaryServiceMock }, CssHelper]
   });
 
   beforeEach(() => {
@@ -67,6 +156,11 @@ describe('CardComponent', () => {
     component = fixture.componentInstance;
     configurationService = TestBed.inject(ConfigurationService);
     orchestratorService = TestBed.inject(MgrModuleService);
+    getHealthSpy = spyOn(TestBed.inject(HealthService), 'getMinimalHealth');
+    getHealthSpy.and.returnValue(of(healthPayload));
+    spyOn(TestBed.inject(PrometheusService), 'ifAlertmanagerConfigured').and.callFake((fn) => fn());
+    getAlertsSpy = spyOn(TestBed.inject(PrometheusService), 'getAlerts');
+    getAlertsSpy.and.returnValue(of(alertsPayload));
   });
 
   it('should create', () => {
@@ -86,4 +180,84 @@ describe('CardComponent', () => {
     expect(component.detailsCardData.orchestrator).toBe('Cephadm');
     expect(component.detailsCardData.cephVersion).toBe('17.0.0-12222-gcd0cd7cb quincy (dev)');
   });
+
+  it('should check if the respective icon is shown for each status', () => {
+    const payload = _.cloneDeep(healthPayload);
+
+    // HEALTH_WARN
+    payload.health['status'] = 'HEALTH_WARN';
+    payload.health['checks'] = [
+      { severity: 'HEALTH_WARN', type: 'WRN', summary: { message: 'fake warning' } }
+    ];
+
+    getHealthSpy.and.returnValue(of(payload));
+    fixture.detectChanges();
+    const clusterStatusCard = fixture.debugElement.query(By.css('cd-card[title="Status"] i'));
+    expect(clusterStatusCard.nativeElement.title).toEqual(`${payload.health.status}`);
+
+    // HEALTH_ERR
+    payload.health['status'] = 'HEALTH_ERR';
+    payload.health['checks'] = [
+      { severity: 'HEALTH_ERR', type: 'ERR', summary: { message: 'fake error' } }
+    ];
+
+    getHealthSpy.and.returnValue(of(payload));
+    fixture.detectChanges();
+    expect(clusterStatusCard.nativeElement.title).toEqual(`${payload.health.status}`);
+
+    // HEALTH_OK
+    payload.health['status'] = 'HEALTH_OK';
+    payload.health['checks'] = [
+      { severity: 'HEALTH_OK', type: 'OK', summary: { message: 'fake success' } }
+    ];
+
+    getHealthSpy.and.returnValue(of(payload));
+    fixture.detectChanges();
+    expect(clusterStatusCard.nativeElement.title).toEqual(`${payload.health.status}`);
+  });
+
+  it('should show the actual alert count on each alerts pill', () => {
+    fixture.detectChanges();
+
+    const successNotification = fixture.debugElement.query(By.css('button[id=warningAlerts] span'));
+
+    const dangerNotification = fixture.debugElement.query(By.css('button[id=dangerAlerts] span'));
+
+    expect(successNotification.nativeElement.textContent).toBe('1');
+    expect(dangerNotification.nativeElement.textContent).toBe('2');
+  });
+
+  it('should show the critical alerts window and its content', () => {
+    const payload = _.cloneDeep(alertsPayload[0]);
+    component.toggleAlertsWindow('danger');
+    fixture.detectChanges();
+
+    const cardTitle = fixture.debugElement.query(By.css('.tc_alerts h6.card-title'));
+
+    expect(cardTitle.nativeElement.textContent).toBe(payload.labels.alertname);
+    expect(component.alertType).not.toBe('warning');
+  });
+
+  it('should show the warning alerts window and its content', () => {
+    const payload = _.cloneDeep(alertsPayload[2]);
+    component.toggleAlertsWindow('warning');
+    fixture.detectChanges();
+
+    const cardTitle = fixture.debugElement.query(By.css('.tc_alerts h6.card-title'));
+
+    expect(cardTitle.nativeElement.textContent).toBe(payload.labels.alertname);
+    expect(component.alertType).not.toBe('critical');
+  });
+
+  it('should only show the pills when the alerts are not empty', () => {
+    getAlertsSpy.and.returnValue(of({}));
+    fixture.detectChanges();
+
+    const successNotification = fixture.debugElement.query(By.css('button[id=warningAlerts]'));
+
+    const dangerNotification = fixture.debugElement.query(By.css('button[id=dangerAlerts]'));
+
+    expect(successNotification).toBe(null);
+    expect(dangerNotification).toBe(null);
+  });
 });
index 1864c0e729a24335b46aef6fbcba0dbfde5b148f..009b6717721c9973fe8eedd8e9f9c21bd79735cf 100644 (file)
@@ -1,4 +1,4 @@
-import { Component, OnDestroy, OnInit } from '@angular/core';
+import { Component, NgZone, OnDestroy, OnInit } from '@angular/core';
 
 import _ from 'lodash';
 import { Observable, Subscription } from 'rxjs';
@@ -6,10 +6,14 @@ import { take } from 'rxjs/operators';
 
 import { ClusterService } from '~/app/shared/api/cluster.service';
 import { ConfigurationService } from '~/app/shared/api/configuration.service';
+import { HealthService } from '~/app/shared/api/health.service';
 import { MgrModuleService } from '~/app/shared/api/mgr-module.service';
 import { OsdService } from '~/app/shared/api/osd.service';
+import { PrometheusService } from '~/app/shared/api/prometheus.service';
+import { Icons } from '~/app/shared/enum/icons.enum';
 import { DashboardDetails } from '~/app/shared/models/cd-details';
 import { Permissions } from '~/app/shared/models/permissions';
+import { AlertmanagerAlert } from '~/app/shared/models/prometheus-alerts';
 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
 import {
   FeatureTogglesMap$,
@@ -96,13 +100,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
   }
 
   ngOnDestroy() {
-    this.interval.unsubscribe();
-  }
-
-  getHealth() {
-    this.healthService.getMinimalHealth().subscribe((data: any) => {
-      this.healthData = data;
-    });
+    window.clearInterval(this.interval);
   }
 
   toggleAlertsWindow(type: string, isToggleButton: boolean = false) {
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/health-icon.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/health-icon.enum.ts
new file mode 100644 (file)
index 0000000..7330a25
--- /dev/null
@@ -0,0 +1,5 @@
+export enum HealthIcon {
+  HEALTH_ERR = 'fa fa-exclamation-circle',
+  HEALTH_WARN = 'fa fa-exclamation-triangle',
+  HEALTH_OK = 'fa fa-check-circle'
+}
index a08bfcecc3603f31e6f05b6245fe4fdb2f78e65f..8d7f9a8f8ab7d93ede639079ca10b6d059f703c7 100644 (file)
@@ -34,6 +34,8 @@ export enum Icons {
   info = 'fa fa-info', // Notification information
   infoCircle = 'fa fa-info-circle', // Info on landing page
   questionCircle = 'fa fa-question-circle-o',
+  danger = 'fa fa-exclamation-circle',
+  success = 'fa fa-check-circle',
   check = 'fa fa-check', // Notification check
   show = 'fa fa-eye', // Show
   paragraph = 'fa fa-paragraph', // Silence Matcher - Attribute name
index 1239dcccdfe247e28bbd289df03cab4aae1daf3d..9deaa537895310e7a6bfcb9cc53436007091a36a 100644 (file)
@@ -7,6 +7,7 @@ export class PrometheusAlertLabels {
 
 class Annotations {
   description: string;
+  summary: string;
 }
 
 class CommonAlertmanagerAlert {
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-icon.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-icon.pipe.spec.ts
new file mode 100644 (file)
index 0000000..e4450d9
--- /dev/null
@@ -0,0 +1,20 @@
+import { HealthIconPipe } from './health-icon.pipe';
+
+describe('HealthIconPipe', () => {
+  const pipe = new HealthIconPipe();
+  it('create an instance', () => {
+    expect(pipe).toBeTruthy();
+  });
+
+  it('transforms "HEALTH_OK"', () => {
+    expect(pipe.transform('HEALTH_OK')).toEqual('fa fa-check-circle');
+  });
+
+  it('transforms "HEALTH_WARN"', () => {
+    expect(pipe.transform('HEALTH_WARN')).toEqual('fa fa-exclamation-triangle');
+  });
+
+  it('transforms "HEALTH_ERR"', () => {
+    expect(pipe.transform('HEALTH_ERR')).toEqual('fa fa-exclamation-circle');
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-icon.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-icon.pipe.ts
new file mode 100644 (file)
index 0000000..1cb58e0
--- /dev/null
@@ -0,0 +1,12 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+import { HealthIcon } from '../enum/health-icon.enum';
+
+@Pipe({
+  name: 'healthIcon'
+})
+export class HealthIconPipe implements PipeTransform {
+  transform(value: string): string {
+    return Object.keys(HealthIcon).includes(value as HealthIcon) ? HealthIcon[value] : '';
+  }
+}
index 5094a9913355e6f61596dd23ba6dcb76dd24f9f4..226972ce0cae084665951a72d44f0f85cedc5f85 100755 (executable)
@@ -15,6 +15,7 @@ import { EmptyPipe } from './empty.pipe';
 import { EncodeUriPipe } from './encode-uri.pipe';
 import { FilterPipe } from './filter.pipe';
 import { HealthColorPipe } from './health-color.pipe';
+import { HealthIconPipe } from './health-icon.pipe';
 import { HealthLabelPipe } from './health-label.pipe';
 import { IopsPipe } from './iops.pipe';
 import { IscsiBackstorePipe } from './iscsi-backstore.pipe';
@@ -64,7 +65,8 @@ import { UpperFirstPipe } from './upper-first.pipe';
     MapPipe,
     TruncatePipe,
     SanitizeHtmlPipe,
-    SearchHighlightPipe
+    SearchHighlightPipe,
+    HealthIconPipe
   ],
   exports: [
     ArrayPipe,
@@ -96,7 +98,8 @@ import { UpperFirstPipe } from './upper-first.pipe';
     MapPipe,
     TruncatePipe,
     SanitizeHtmlPipe,
-    SearchHighlightPipe
+    SearchHighlightPipe,
+    HealthIconPipe
   ],
   providers: [
     ArrayPipe,
@@ -123,7 +126,8 @@ import { UpperFirstPipe } from './upper-first.pipe';
     DurationPipe,
     MapPipe,
     TruncatePipe,
-    SanitizeHtmlPipe
+    SanitizeHtmlPipe,
+    HealthIconPipe
   ]
 })
 export class PipesModule {}