From 79439213b510238a0d9d17ede423e05951a911a4 Mon Sep 17 00:00:00 2001 From: bryanmontalvan <68972382+bryanmontalvan@users.noreply.github.com> Date: Tue, 2 Aug 2022 21:39:05 -0400 Subject: [PATCH] mgr/dashboard: dashboard-v3: status card 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 mgr/dashboard: introduce active alerts to status cards Signed-off-by: Nizamudeen A --- .../ceph/new-dashboard/dashboard.module.ts | 4 +- .../dashboard/dashboard.component.scss | 8 + .../dashboard/dashboard.component.spec.ts | 190 +++++++++++++++++- .../dashboard/dashboard.component.ts | 14 +- .../src/app/shared/enum/health-icon.enum.ts | 5 + .../src/app/shared/enum/icons.enum.ts | 2 + .../app/shared/models/prometheus-alerts.ts | 1 + .../app/shared/pipes/health-icon.pipe.spec.ts | 20 ++ .../src/app/shared/pipes/health-icon.pipe.ts | 12 ++ .../src/app/shared/pipes/pipes.module.ts | 10 +- 10 files changed, 246 insertions(+), 20 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/enum/health-icon.enum.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-icon.pipe.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-icon.pipe.ts diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard.module.ts index 34d41ddb31a..ac60eec6481 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard.module.ts @@ -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] diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.scss index 50789c87731..140f5f78fa4 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.scss +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.scss @@ -1,3 +1,11 @@ +.alerts { + height: 17rem; + + div { + padding-top: 0; + } +} + div { padding-top: 20px; } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.spec.ts index 113ac8cfe95..b3d5c3990f4 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.spec.ts @@ -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; let configurationService: ConfigurationService; let orchestratorService: MgrModuleService; + let getHealthSpy: jasmine.Spy; + let getAlertsSpy: jasmine.Spy; + + const healthPayload: Record = { + 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); + }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.ts index 1864c0e729a..009b6717721 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.ts @@ -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 index 00000000000..7330a250bde --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/health-icon.enum.ts @@ -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' +} 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 a08bfcecc36..8d7f9a8f8ab 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 @@ -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 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/prometheus-alerts.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/prometheus-alerts.ts index 1239dcccdfe..9deaa537895 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/prometheus-alerts.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/prometheus-alerts.ts @@ -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 index 00000000000..e4450d9e1c1 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-icon.pipe.spec.ts @@ -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 index 00000000000..1cb58e0419e --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-icon.pipe.ts @@ -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] : ''; + } +} 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 5094a991335..226972ce0ca 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 @@ -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 {} -- 2.39.5