From 6fbc9ae01a892f5ce34e7bbf409361fe45641f47 Mon Sep 17 00:00:00 2001 From: bryanmontalvan <68972382+bryanmontalvan@users.noreply.github.com> Date: Thu, 7 Jul 2022 12:36:15 -0400 Subject: [PATCH] mgr/dashboard: dashboard-v3: landing-page revamp This commit add the following - A new dashboard component, which will exist in parallel with in the current landing-page - Created a route for this dashboard `/dashboard_3` - Created a bare-bones bootstrap grid with mock-up card components Signed-off-by: bryanmontalvan Signed-off-by: Pedro Gonzalez Gomez mgr/dashboard: changes to first layout CHANGES: - Renamed dashboardcomponents - Removed unnecesary styling - Added unit tests Signed-off-by: Pedro Gonzalez Gomez Moved router.url logic inside html template This commit removes the `this.router.url` logic which was located in the `workbench-layout.component.ts` file and moved it into the HTML template section. Signed-off-by: bryanmontalvan mgr/dashboard: syntax changes from bootstrap 4 to 5 Signed-off-by: Pedro Gonzalez Gomez mgr/dashboard: small fixes and improvements over all cards and layout - all cards placed evenly with the same height - increased font size on details card and adjusted margin - changed capacity card legend to: "Used" - adjusted cluster utilization card margins and increased graphs height - improved status card toggle - changed orchestror to orchestrator - switched IPS/OPS graph colors Signed-off-by: Pedro Gonzalez Gomez mgr/dashboard: added recovery throghput graph, improve promqls, fix inventory's card lines Signed-off-by: Pedro Gonzalez Gomez --- .../frontend/src/app/app-routing.module.ts | 6 +- .../frontend/src/app/ceph/ceph.module.ts | 2 + .../app/ceph/dashboard/dashboard.module.ts | 4 +- .../dashboard/dashboard.component.spec.ts | 10 +- .../dashboard/dashboard.component.ts | 2 +- .../new-dashboard/card/card.component.html | 8 + .../new-dashboard/card/card.component.scss | 5 + .../new-dashboard/card/card.component.spec.ts | 33 +++ .../ceph/new-dashboard/card/card.component.ts | 11 + .../dashboard-area-chart.component.html | 23 ++ .../dashboard-area-chart.component.scss | 9 + .../dashboard-area-chart.component.ts | 265 ++++++++++++++++++ .../dashboard-pie/dashboard-pie.component.ts | 189 +++++++++++++ .../dashboard-time-selector.component.ts | 77 +++++ .../ceph/new-dashboard/dashboard.module.ts | 29 ++ .../dashboard/dashboard.component.html | 258 +++++++++++++++++ .../dashboard/dashboard.component.scss | 59 ++++ .../dashboard/dashboard.component.spec.ts | 30 ++ .../dashboard/dashboard.component.ts | 202 +++++++++++++ .../workbench-layout.component.html | 4 +- .../workbench-layout.component.ts | 6 +- .../app/shared/enum/dashboard-promqls.enum.ts | 10 + 22 files changed, 1225 insertions(+), 17 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card/card.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card/card.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card/card.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card/card.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-area-chart/dashboard-area-chart.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-area-chart/dashboard-area-chart.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-area-chart/dashboard-area-chart.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-pie/dashboard-pie.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-time-selector/dashboard-time-selector.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard.module.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/enum/dashboard-promqls.enum.ts diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts index 6880a1561c1..ec30ccc7208 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts @@ -24,7 +24,8 @@ import { SilenceListComponent } from './ceph/cluster/prometheus/silence-list/sil import { ServiceFormComponent } from './ceph/cluster/services/service-form/service-form.component'; import { ServicesComponent } from './ceph/cluster/services/services.component'; import { TelemetryComponent } from './ceph/cluster/telemetry/telemetry.component'; -import { DashboardComponent } from './ceph/dashboard/dashboard/dashboard.component'; +import { DeprecatedDashboardComponent } from './ceph/dashboard/dashboard/dashboard.component'; +import { DashboardComponent } from './ceph/new-dashboard/dashboard/dashboard.component'; import { NfsFormComponent } from './ceph/nfs/nfs-form/nfs-form.component'; import { NfsListComponent } from './ceph/nfs/nfs-list/nfs-list.component'; import { PerformanceCounterComponent } from './ceph/performance-counter/performance-counter/performance-counter.component'; @@ -88,7 +89,8 @@ const routes: Routes = [ canActivate: [AuthGuardService, ChangePasswordGuardService], canActivateChild: [AuthGuardService, ChangePasswordGuardService], children: [ - { path: 'dashboard', component: DashboardComponent }, + { path: 'dashboard', component: DeprecatedDashboardComponent }, + { path: 'dashboard_3', component: DashboardComponent }, { path: 'error', component: ErrorComponent }, // Cluster diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/ceph.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/ceph.module.ts index 47772304b50..17d62469761 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/ceph.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/ceph.module.ts @@ -5,6 +5,7 @@ import { SharedModule } from '../shared/shared.module'; import { CephfsModule } from './cephfs/cephfs.module'; import { ClusterModule } from './cluster/cluster.module'; import { DashboardModule } from './dashboard/dashboard.module'; +import { NewDashboardModule } from './new-dashboard/dashboard.module'; import { NfsModule } from './nfs/nfs.module'; import { PerformanceCounterModule } from './performance-counter/performance-counter.module'; @@ -13,6 +14,7 @@ import { PerformanceCounterModule } from './performance-counter/performance-coun CommonModule, ClusterModule, DashboardModule, + NewDashboardModule, PerformanceCounterModule, CephfsModule, NfsModule, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard.module.ts index 4bdfd50a51b..98f29e571a2 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard.module.ts @@ -9,7 +9,7 @@ import { ChartsModule } from 'ng2-charts'; import { SharedModule } from '~/app/shared/shared.module'; import { CephSharedModule } from '../shared/ceph-shared.module'; import { FeedbackComponent } from '../shared/feedback/feedback.component'; -import { DashboardComponent } from './dashboard/dashboard.component'; +import { DeprecatedDashboardComponent } from './dashboard/dashboard.component'; import { HealthPieComponent } from './health-pie/health-pie.component'; import { HealthComponent } from './health/health.component'; import { InfoCardComponent } from './info-card/info-card.component'; @@ -34,7 +34,7 @@ import { OsdSummaryPipe } from './osd-summary.pipe'; declarations: [ HealthComponent, - DashboardComponent, + DeprecatedDashboardComponent, MonSummaryPipe, OsdSummaryPipe, MgrSummaryPipe, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.spec.ts index 7bc4980bb82..86a99271291 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.spec.ts @@ -4,20 +4,20 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap'; import { configureTestBed } from '~/testing/unit-test-helper'; -import { DashboardComponent } from './dashboard.component'; +import { DeprecatedDashboardComponent } from './dashboard.component'; describe('DashboardComponent', () => { - let component: DashboardComponent; - let fixture: ComponentFixture; + let component: DeprecatedDashboardComponent; + let fixture: ComponentFixture; configureTestBed({ imports: [NgbNavModule], - declarations: [DashboardComponent], + declarations: [DeprecatedDashboardComponent], schemas: [NO_ERRORS_SCHEMA] }); beforeEach(() => { - fixture = TestBed.createComponent(DashboardComponent); + fixture = TestBed.createComponent(DeprecatedDashboardComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.ts index 354e3890359..b5d62a62c47 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.ts @@ -5,6 +5,6 @@ import { Component } from '@angular/core'; templateUrl: './dashboard.component.html', styleUrls: ['./dashboard.component.scss'] }) -export class DashboardComponent { +export class DeprecatedDashboardComponent { hasGrafana = false; // TODO: Temporary var, remove when grafana is implemented } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card/card.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card/card.component.html new file mode 100644 index 00000000000..35dffd46c04 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card/card.component.html @@ -0,0 +1,8 @@ +
+

+ {{ title }} +

+
+ +
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card/card.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card/card.component.scss new file mode 100644 index 00000000000..fdf19a00ec6 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card/card.component.scss @@ -0,0 +1,5 @@ +.card-body { + display: flex; + flex-direction: column; + justify-content: space-evenly; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card/card.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card/card.component.spec.ts new file mode 100644 index 00000000000..fdc34fdf7b2 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card/card.component.spec.ts @@ -0,0 +1,33 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { configureTestBed } from '~/testing/unit-test-helper'; +import { CardComponent } from './card.component'; + +describe('CardComponent', () => { + let component: CardComponent; + let fixture: ComponentFixture; + + configureTestBed({ + imports: [RouterTestingModule], + declarations: [CardComponent] + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CardComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('Setting cards title makes title visible', () => { + const title = 'Card Title'; + component.title = title; + fixture.detectChanges(); + const titleDiv = fixture.debugElement.nativeElement.querySelector('.card-title'); + + expect(titleDiv.textContent).toContain(title); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card/card.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card/card.component.ts new file mode 100644 index 00000000000..b6bb99c6690 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card/card.component.ts @@ -0,0 +1,11 @@ +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'cd-card', + templateUrl: './card.component.html', + styleUrls: ['./card.component.scss'] +}) +export class CardComponent { + @Input() + title: string; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-area-chart/dashboard-area-chart.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-area-chart/dashboard-area-chart.component.html new file mode 100644 index 00000000000..6ac991fd58c --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-area-chart/dashboard-area-chart.component.html @@ -0,0 +1,23 @@ +
+
+
+ {{ chartTitle }} +
+ {{currentData}} {{ currentDataUnits }} +
+ {{currentData2}} {{ currentDataUnits2 }} +
+
+
+ + +
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-area-chart/dashboard-area-chart.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-area-chart/dashboard-area-chart.component.scss new file mode 100644 index 00000000000..12e9b9c1c6a --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-area-chart/dashboard-area-chart.component.scss @@ -0,0 +1,9 @@ +.center-text { + margin-top: 1.2vw; + position: relative; +} + +.chart { + height: 8vh; + margin-top: 15px; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-area-chart/dashboard-area-chart.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-area-chart/dashboard-area-chart.component.ts new file mode 100644 index 00000000000..3da4334210d --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-area-chart/dashboard-area-chart.component.ts @@ -0,0 +1,265 @@ +import { AfterViewInit, Component, Input, OnChanges, OnInit, ViewChild } from '@angular/core'; + +import { CssHelper } from '~/app/shared/classes/css-helper'; +import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe'; +import { DimlessBinaryPerSecondPipe } from '~/app/shared/pipes/dimless-binary-per-second.pipe'; +import { FormatterService } from '~/app/shared/services/formatter.service'; +import { BaseChartDirective, PluginServiceGlobalRegistrationAndOptions } from 'ng2-charts'; +import { DimlessPipe } from '~/app/shared/pipes/dimless.pipe'; + +@Component({ + selector: 'cd-dashboard-area-chart', + templateUrl: './dashboard-area-chart.component.html', + styleUrls: ['./dashboard-area-chart.component.scss'] +}) +export class DashboardAreaChartComponent implements OnInit, OnChanges, AfterViewInit { + @ViewChild(BaseChartDirective) chart: BaseChartDirective; + + @Input() + chartTitle: string; + @Input() + maxValue?: any; + @Input() + dataUnits: string; + @Input() + data: any; + @Input() + data2?: any; + @Input() + label: any; + @Input() + label2?: any; + + currentDataUnits: string; + currentData: number; + currentDataUnits2?: string; + currentData2?: number; + + chartData: any = { + dataset: [ + { + label: '', + data: [{ x: 0, y: 0 }], + tension: 0, + pointBackgroundColor: this.cssHelper.propertyValue('chart-color-strong-blue'), + backgroundColor: this.cssHelper.propertyValue('chart-color-translucent-blue'), + borderColor: this.cssHelper.propertyValue('chart-color-strong-blue') + }, + { + label: '', + data: [], + tension: 0, + pointBackgroundColor: this.cssHelper.propertyValue('chart-color-orange'), + backgroundColor: this.cssHelper.propertyValue('chart-color-yellow'), + borderColor: this.cssHelper.propertyValue('chart-color-orange') + } + ] + }; + + options: any = { + responsive: true, + maintainAspectRatio: false, + elements: { + point: { + radius: 0 + } + }, + legend: { + display: false + }, + tooltips: { + intersect: false, + displayColors: true, + backgroundColor: this.cssHelper.propertyValue('chart-color-tooltip-background'), + callbacks: { + title: function (tooltipItem: any): any { + return tooltipItem[0].xLabel; + } + } + }, + hover: { + intersect: false + }, + scales: { + xAxes: [ + { + display: false, + type: 'time', + gridLines: { + display: false + }, + time: { + tooltipFormat: 'YYYY/MM/DD hh:mm:ss' + } + } + ], + yAxes: [ + { + gridLines: { + display: false + }, + ticks: { + beginAtZero: true, + maxTicksLimit: 3, + callback: (value: any) => { + if (value === 0) { + return null; + } + return this.fillString(this.convertUnits(value)); + } + } + } + ] + }, + plugins: { + borderArea: true, + chartAreaBorder: { + borderColor: this.cssHelper.propertyValue('chart-color-slight-dark-gray'), + borderWidth: 2 + } + } + }; + + public chartAreaBorderPlugin: PluginServiceGlobalRegistrationAndOptions[] = [ + { + beforeDraw(chart: Chart) { + if (!chart.options.plugins.borderArea) { + return; + } + const { + ctx, + chartArea: { left, top, right, bottom } + } = chart; + ctx.save(); + ctx.strokeStyle = chart.options.plugins.chartAreaBorder.borderColor; + ctx.lineWidth = chart.options.plugins.chartAreaBorder.borderWidth; + ctx.setLineDash(chart.options.plugins.chartAreaBorder.borderDash || []); + ctx.lineDashOffset = chart.options.plugins.chartAreaBorder.borderDashOffset; + ctx.strokeRect(left, top, right - left - 1, bottom); + ctx.restore(); + } + } + ]; + + constructor( + private cssHelper: CssHelper, + private dimlessBinary: DimlessBinaryPipe, + private dimlessBinaryPerSecond: DimlessBinaryPerSecondPipe, + private dimlessPipe: DimlessPipe, + private formatter: FormatterService + ) {} + + ngOnInit(): void { + this.currentData = Number( + this.chartData.dataset[0].data[this.chartData.dataset[0].data.length - 1].y + ); + if (this.data2) { + this.currentData2 = Number( + this.chartData.dataset[1].data[this.chartData.dataset[1].data.length - 1].y + ); + } + } + + ngOnChanges(): void { + if (this.data) { + this.setChartTicks(); + this.chartData.dataset[0].data = this.formatData(this.data); + this.chartData.dataset[0].label = this.label; + [this.currentData, this.currentDataUnits] = this.convertUnits( + this.data[this.data.length - 1][1] + ).split(' '); + } + if (this.data2) { + this.chartData.dataset[1].data = this.formatData(this.data2); + this.chartData.dataset[1].label = this.label2; + [this.currentData2, this.currentDataUnits2] = this.convertUnits( + this.data2[this.data2.length - 1][1] + ).split(' '); + } + } + + ngAfterViewInit(): void { + if (this.data) { + this.setChartTicks(); + } + } + + private formatData(array: Array): any { + let formattedData = {}; + formattedData = array.map((data: any) => ({ + x: data[0] * 1000, + y: Number(this.convertUnits(data[1]).replace(/[^\d,.]+/g, '')) + })); + return formattedData; + } + + private convertUnits(data: any): any { + let dataWithUnits: string; + if (this.dataUnits === 'bytes') { + dataWithUnits = this.dimlessBinary.transform(data); + } else if (this.dataUnits === 'bytesPerSecond') { + dataWithUnits = this.dimlessBinaryPerSecond.transform(data); + } else if (this.dataUnits === 'ms') { + dataWithUnits = this.formatter.format_number(data, 1000, ['ms', 's']); + } else { + dataWithUnits = this.dimlessPipe.transform(data); + } + return dataWithUnits; + } + + private fillString(str: string): string { + let maxNumberOfChar: number = 8; + let numberOfChars: number = str.length; + if (str.length < 4) { + maxNumberOfChar = 11; + } + for (; numberOfChars < maxNumberOfChar; numberOfChars++) { + str = '\u00A0' + str; + } + return str + '\u00A0\u00A0'; + } + + private setChartTicks() { + if (this.chart && this.maxValue) { + let [maxValue, maxValueDataUnits] = this.convertUnits(this.maxValue).split(' '); + this.chart.chart.options.scales.yAxes[0].ticks.suggestedMax = maxValue; + this.chart.chart.options.scales.yAxes[0].ticks.suggestedMin = 0; + this.chart.chart.options.scales.yAxes[0].ticks.stepSize = Number((maxValue / 2).toFixed(0)); + this.chart.chart.options.scales.yAxes[0].ticks.callback = (value: any) => { + if (value === 0) { + return null; + } + return this.fillString(`${value} ${maxValueDataUnits}`); + }; + this.chart.chart.update(); + } else if (this.chart && this.data) { + let maxValue = 0, + maxValueDataUnits = ''; + let maxValueData = Math.max(...this.data.map((values: any) => values[1])); + if (this.data2) { + var maxValueData2 = Math.max(...this.data2.map((values: any) => values[1])); + [maxValue, maxValueDataUnits] = this.convertUnits( + Math.max(maxValueData, maxValueData2) + ).split(' '); + } else { + [maxValue, maxValueDataUnits] = this.convertUnits(Math.max(maxValueData)).split(' '); + } + + this.chart.chart.options.scales.yAxes[0].ticks.suggestedMax = maxValue * 1.2; + this.chart.chart.options.scales.yAxes[0].ticks.suggestedMin = 0; + this.chart.chart.options.scales.yAxes[0].ticks.stepSize = Number( + ((maxValue * 1.2) / 2).toFixed(0) + ); + this.chart.chart.options.scales.yAxes[0].ticks.callback = (value: any) => { + if (value === 0) { + return null; + } + if (!maxValueDataUnits) { + return this.fillString(`${value}`); + } + return this.fillString(`${value} ${maxValueDataUnits}`); + }; + this.chart.chart.update(); + } + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-pie/dashboard-pie.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-pie/dashboard-pie.component.ts new file mode 100644 index 00000000000..4aaabb6eb4a --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-pie/dashboard-pie.component.ts @@ -0,0 +1,189 @@ +import { Component, Input, OnChanges, OnInit } from '@angular/core'; + +import * as Chart from 'chart.js'; +import _ from 'lodash'; +import { PluginServiceGlobalRegistrationAndOptions } from 'ng2-charts'; + +import { CssHelper } from '~/app/shared/classes/css-helper'; +import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe'; + +@Component({ + selector: 'cd-dashboard-pie', + templateUrl: './dashboard-pie.component.html', + styleUrls: ['./dashboard-pie.component.scss'] +}) +export class DashboardPieComponent implements OnChanges, OnInit { + @Input() + data: any; + @Input() + highThreshold: number; + @Input() + lowThreshold: number; + + color: string; + + chartConfig: any = { + chartType: 'doughnut', + labels: ['', '', ''], + dataset: [ + { + label: null, + backgroundColor: [ + this.cssHelper.propertyValue('chart-color-light-gray'), + this.cssHelper.propertyValue('chart-color-slight-dark-gray'), + this.cssHelper.propertyValue('chart-color-dark-gray') + ] + }, + { + label: null, + borderWidth: 0, + backgroundColor: [ + this.cssHelper.propertyValue('chart-color-blue'), + this.cssHelper.propertyValue('chart-color-white') + ] + } + ], + options: { + cutoutPercentage: 70, + events: ['click', 'mouseout', 'touchstart'], + legend: { + display: true, + position: 'right', + labels: { + boxWidth: 10, + usePointStyle: false, + generateLabels: (chart: any) => { + const labels = { 0: {}, 1: {}, 2: {} }; + labels[0] = { + text: $localize`Used: ${chart.data.datasets[1].data[2]}`, + fillStyle: chart.data.datasets[1].backgroundColor[0], + strokeStyle: chart.data.datasets[1].backgroundColor[0] + }; + labels[1] = { + text: $localize`Warning: ${chart.data.datasets[0].data[0]}%`, + fillStyle: chart.data.datasets[0].backgroundColor[1], + strokeStyle: chart.data.datasets[0].backgroundColor[1] + }; + labels[2] = { + text: $localize`Danger: ${ + chart.data.datasets[0].data[0] + chart.data.datasets[0].data[1] + }%`, + fillStyle: chart.data.datasets[0].backgroundColor[2], + strokeStyle: chart.data.datasets[0].backgroundColor[2] + }; + + return labels; + } + } + }, + plugins: { + center_text: true + }, + tooltips: { + enabled: true, + displayColors: false, + backgroundColor: this.cssHelper.propertyValue('chart-color-tooltip-background'), + cornerRadius: 0, + bodyFontSize: 14, + bodyFontStyle: '600', + position: 'nearest', + xPadding: 12, + yPadding: 12, + filter: (tooltipItem: any) => { + return tooltipItem.datasetIndex === 1; + }, + callbacks: { + label: (item: Record, data: Record) => { + let text = data.labels[item.index]; + if (!text.includes('%')) { + text = `${text} (${data.datasets[item.datasetIndex].data[item.index]}%)`; + } + return text; + } + } + }, + title: { + display: false + } + } + }; + + public doughnutChartPlugins: PluginServiceGlobalRegistrationAndOptions[] = [ + { + id: 'center_text', + beforeDraw(chart: Chart) { + const cssHelper = new CssHelper(); + const defaultFontFamily = 'Helvetica Neue, Helvetica, Arial, sans-serif'; + Chart.defaults.global.defaultFontFamily = defaultFontFamily; + const ctx = chart.ctx; + if (!chart.options.plugins.center_text || !chart.data.datasets[0].label) { + return; + } + + ctx.save(); + const label = chart.data.datasets[0].label[0].split('\n'); + + const centerX = (chart.chartArea.left + chart.chartArea.right) / 2; + const centerY = (chart.chartArea.top + chart.chartArea.bottom) / 2; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + + ctx.font = `24px ${defaultFontFamily}`; + ctx.fillText(label[0], centerX, centerY - 10); + + if (label.length > 1) { + ctx.font = `14px ${defaultFontFamily}`; + ctx.fillStyle = cssHelper.propertyValue('chart-color-center-text-description'); + ctx.fillText(label[1], centerX, centerY + 10); + } + ctx.restore(); + } + } + ]; + + constructor(private cssHelper: CssHelper, private dimlessBinary: DimlessBinaryPipe) {} + + ngOnInit() { + this.prepareRawUsage(this.chartConfig, this.data); + } + + ngOnChanges() { + this.prepareRawUsage(this.chartConfig, this.data); + } + + private prepareRawUsage(chart: Record, data: Record) { + const nearFullRatioPercent = this.lowThreshold * 100; + const fullRatioPercent = this.highThreshold * 100; + const percentAvailable = this.calcPercentage(data.max - data.current, data.max); + const percentUsed = this.calcPercentage(data.current, data.max); + if (percentUsed >= fullRatioPercent) { + this.color = 'chart-color-red'; + } else if (percentUsed >= nearFullRatioPercent) { + this.color = 'chart-color-yellow'; + } else { + this.color = 'chart-color-blue'; + } + + chart.dataset[0].data = [ + Math.round(nearFullRatioPercent), + Math.round(Math.abs(nearFullRatioPercent - fullRatioPercent)), + Math.round(100 - fullRatioPercent) + ]; + + chart.dataset[1].data = [ + percentUsed, + percentAvailable, + this.dimlessBinary.transform(data.current) + ]; + chart.dataset[1].backgroundColor[0] = this.cssHelper.propertyValue(this.color); + + chart.dataset[0].label = [`${percentUsed}%\nof ${this.dimlessBinary.transform(data.max)}`]; + } + + private calcPercentage(dividend: number, divisor: number) { + if (!_.isNumber(dividend) || !_.isNumber(divisor) || divisor === 0) { + return 0; + } + return Math.ceil((dividend / divisor) * 100 * 100) / 100; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-time-selector/dashboard-time-selector.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-time-selector/dashboard-time-selector.component.ts new file mode 100644 index 00000000000..3b0915232b6 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-time-selector/dashboard-time-selector.component.ts @@ -0,0 +1,77 @@ +import { Component, EventEmitter, Output } from '@angular/core'; + +import moment from 'moment'; + +@Component({ + selector: 'cd-dashboard-time-selector', + templateUrl: './dashboard-time-selector.component.html', + styleUrls: ['./dashboard-time-selector.component.scss'] +}) +export class DashboardTimeSelectorComponent { + @Output() + selectedTime = new EventEmitter(); + + times: any; + time: any; + + constructor() { + this.times = [ + { + name: $localize`Last 5 minutes`, + value: this.timeToDate(5 * 60, 1) + }, + { + name: $localize`Last 15 minutes`, + value: this.timeToDate(15 * 60, 3) + }, + { + name: $localize`Last 30 minutes`, + value: this.timeToDate(30 * 60, 6) + }, + { + name: $localize`Last 1 hour`, + value: this.timeToDate(3600, 12) + }, + { + name: $localize`Last 3 hours`, + value: this.timeToDate(3 * 3600, 36) + }, + { + name: $localize`Last 6 hours`, + value: this.timeToDate(6 * 3600, 72) + }, + { + name: $localize`Last 12 hours`, + value: this.timeToDate(12 * 3600, 144) + }, + { + name: $localize`Last 24 hours`, + value: this.timeToDate(24 * 3600, 288) + }, + { + name: $localize`Last 2 days`, + value: this.timeToDate(48 * 3600, 576) + }, + { + name: $localize`Last 7 days`, + value: this.timeToDate(168 * 3600, 2016) + } + ]; + this.time = this.times[3].value; + } + + emitTime() { + this.selectedTime.emit(this.timeToDate(this.time.end - this.time.start, this.time.step)); + } + + private timeToDate(secondsAgo: number, step: number): any { + const date: number = moment().unix() - secondsAgo; + const dateNow: number = moment().unix(); + const formattedDate: any = { + start: date, + end: dateNow, + step: step + }; + return formattedDate; + } +} 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 new file mode 100644 index 00000000000..27ed0f2d760 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard.module.ts @@ -0,0 +1,29 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { RouterModule } from '@angular/router'; + +import { NgbNavModule, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'; +import { ChartsModule } from 'ng2-charts'; + +import { SharedModule } from '~/app/shared/shared.module'; +import { CephSharedModule } from '../shared/ceph-shared.module'; +import { CardComponent } from './card/card.component'; +import { DashboardComponent } from './dashboard/dashboard.component'; + +@NgModule({ + imports: [ + CephSharedModule, + CommonModule, + NgbNavModule, + SharedModule, + ChartsModule, + RouterModule, + NgbPopoverModule, + FormsModule, + ReactiveFormsModule + ], + + declarations: [DashboardComponent, CardComponent] +}) +export class NewDashboardModule {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.html new file mode 100644 index 00000000000..8065bb860c6 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.html @@ -0,0 +1,258 @@ +
+
+ +
+
FSID
+
{{ detailsCardData.fsid }}
+
Orchestrator
+
{{ detailsCardData.orchestrator || 'Orchestrator is not available' }}
+
Ceph version
+
{{ detailsCardData.cephVersion }}
+
+
+ + +
+ + Cluster +
+
+
+ Alerts + + + + + +
+ + +
+
+
+
+ +
+ +
+
+
+
+
+ + + + + + + + + +
+ +
+ + +
+ +
  • + +
  • +
    + +
  • + +
  • +
    + +
  • + +
  • +
    + +
  • + +
  • +
    + +
  • + +
  • +
    + +
  • + +
  • +
    + +
  • + +
  • +
    + +
  • + +
  • +
    + +
  • + +
  • +
    + + +
    + + + + + + + + + + + + + + +
    +
    +
    +
    + + + +
    +
    +
    +
    + + + + +
    +
    +
    +
    {{ alert.labels.alertname }}
    +

    +

    + Active since: {{ alert.startsAt | relativeDate }} +

    +
    +
    +
    +
    +
    +
    +
    +
    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 new file mode 100644 index 00000000000..50789c87731 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.scss @@ -0,0 +1,59 @@ +div { + padding-top: 20px; +} + +ngx-simplebar { + height: 18rem; +} + +hr { + margin-bottom: 2px; + margin-top: 2px; +} + +.position-right { + margin-left: auto; + order: 2; +} + +.center-content { + align-items: center; + margin-top: 30px; + position: relative; +} + +button.dropdown-toggle { + position: relative; + + &::after { + border: 0; + content: '\f054'; + font-family: 'ForkAwesome'; + font-size: 1rem; + position: absolute; + right: 20px; + transition: transform 0.3s ease-in-out; + } + + &[aria-expanded='true']::after { + transform: rotate(90deg); + } + + &:focus { + box-shadow: none; + } +} + +.list-group-item { + border: 0; +} + +dt { + font-size: larger; + margin-bottom: 0.3rem; +} + +dd { + font-size: larger; + margin-bottom: 0.8rem; +} 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 new file mode 100644 index 00000000000..cf3f518f1cf --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.spec.ts @@ -0,0 +1,30 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { configureTestBed } from '~/testing/unit-test-helper'; +import { CardComponent } from '../card/card.component'; +import { DashboardComponent } from './dashboard.component'; + +describe('CardComponent', () => { + let component: DashboardComponent; + let fixture: ComponentFixture; + + configureTestBed({ + imports: [RouterTestingModule], + declarations: [DashboardComponent, CardComponent] + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DashboardComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should render all cards', () => { + const dashboardCards = fixture.debugElement.nativeElement.querySelectorAll('cd-card'); + expect(dashboardCards.length).toBe(5); + }); +}); 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 new file mode 100644 index 00000000000..843bc863728 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard/dashboard.component.ts @@ -0,0 +1,202 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'cd-dashboard', + templateUrl: './dashboard.component.html', + styleUrls: ['./dashboard.component.scss'] +}) +export class DashboardComponent implements OnInit, OnDestroy { + detailsCardData: DashboardDetails = {}; + osdSettingsService: any; + osdSettings: any; + interval = new Subscription(); + permissions: Permissions; + enabledFeature$: FeatureTogglesMap$; + color: string; + capacityService: any; + capacity: any; + healthData$: Observable; + prometheusAlerts$: Observable; + + isAlertmanagerConfigured = false; + icons = Icons; + showAlerts = false; + flexHeight = true; + simplebar = { + autoHide: false + }; + textClass: string; + borderClass: string; + alertType: string; + alerts: AlertmanagerAlert[]; + crticialActiveAlerts: number; + warningActiveAlerts: number; + healthData: any; + categoryPgAmount: Record = {}; + totalPgs = 0; + queriesResults: any = { + USEDCAPACITY: '', + IPS: '', + OPS: '', + READLATENCY: '', + WRITELATENCY: '', + READCLIENTTHROUGHPUT: '', + WRITECLIENTTHROUGHPUT: '', + RECOVERYBYTES: '' + }; + timerGetPrometheusDataSub: Subscription; + timerTime = 30000; + readonly lastHourDateObject = { + start: moment().unix() - 3600, + end: moment().unix(), + step: 12 + }; + + constructor( + private summaryService: SummaryService, + private configService: ConfigurationService, + private mgrModuleService: MgrModuleService, + private clusterService: ClusterService, + private osdService: OsdService, + private authStorageService: AuthStorageService, + private featureToggles: FeatureTogglesService, + private healthService: HealthService, + public prometheusService: PrometheusService, + private refreshIntervalService: RefreshIntervalService + ) { + this.permissions = this.authStorageService.getPermissions(); + this.enabledFeature$ = this.featureToggles.get(); + } + + ngOnInit() { + this.interval = this.refreshIntervalService.intervalData$.subscribe(() => { + this.getHealth(); + this.triggerPrometheusAlerts(); + this.getCapacityCardData(); + }); + this.getPrometheusData(this.lastHourDateObject); + this.getDetailsCardData(); + } + + ngOnDestroy() { + this.interval.unsubscribe(); + } + + getHealth() { + this.healthService.getMinimalHealth().subscribe((data: any) => { + this.healthData = data; + }); + } + + toggleAlertsWindow(type: string, isToggleButton: boolean = false) { + if (isToggleButton) { + this.showAlerts = !this.showAlerts; + this.flexHeight = !this.flexHeight; + } else if ( + !this.showAlerts || + (this.alertType === type && type !== 'danger') || + (this.alertType !== 'warning' && type === 'danger') + ) { + this.showAlerts = !this.showAlerts; + this.flexHeight = !this.flexHeight; + } + + type === 'danger' ? (this.alertType = 'critical') : (this.alertType = type); + this.textClass = `text-${type}`; + this.borderClass = `border-${type}`; + } + + getDetailsCardData() { + this.configService.get('fsid').subscribe((data) => { + this.detailsCardData.fsid = data['value'][0]['value']; + }); + this.mgrModuleService.getConfig('orchestrator').subscribe((data) => { + const orchStr = data['orchestrator']; + this.detailsCardData.orchestrator = orchStr.charAt(0).toUpperCase() + orchStr.slice(1); + }); + this.summaryService.subscribe((summary) => { + const version = summary.version.replace('ceph version ', '').split(' '); + this.detailsCardData.cephVersion = + version[0] + ' ' + version.slice(2, version.length).join(' '); + }); + } + + getCapacityCardData() { + this.osdSettingsService = this.osdService + .getOsdSettings() + .pipe(take(1)) + .subscribe((data: any) => { + this.osdSettings = data; + }); + this.capacityService = this.clusterService.getCapacity().subscribe((data: any) => { + this.capacity = data; + }); + } + + triggerPrometheusAlerts() { + this.prometheusService.ifAlertmanagerConfigured(() => { + this.isAlertmanagerConfigured = true; + + this.prometheusService.getAlerts().subscribe((alerts) => { + this.alerts = alerts; + this.crticialActiveAlerts = alerts.filter( + (alert: AlertmanagerAlert) => + alert.status.state === 'active' && alert.labels.severity === 'critical' + ).length; + this.warningActiveAlerts = alerts.filter( + (alert: AlertmanagerAlert) => + alert.status.state === 'active' && alert.labels.severity === 'warning' + ).length; + }); + }); + } + + getPrometheusData(selectedTime: any) { + if (this.timerGetPrometheusDataSub) { + this.timerGetPrometheusDataSub.unsubscribe(); + } + this.timerGetPrometheusDataSub = timer(0, this.timerTime).subscribe(() => { + selectedTime = this.updateTimeStamp(selectedTime); + + for (const queryName in queries) { + if (queries.hasOwnProperty(queryName)) { + const query = queries[queryName]; + let interval = selectedTime.step; + + if (query.includes('rate') && selectedTime.step < 20) { + interval = 20; + } else if (query.includes('rate')) { + interval = selectedTime.step * 2; + } + + const intervalAdjustedQuery = query.replace(/\[(.*?)\]/g, `[${interval}s]`); + + this.prometheusService + .getPrometheusData({ + params: intervalAdjustedQuery, + start: selectedTime['start'], + end: selectedTime['end'], + step: selectedTime['step'] + }) + .subscribe((data: any) => { + if (data.result.length) { + this.queriesResults[queryName] = data.result[0].values; + } + }); + } + } + }); + } + + private updateTimeStamp(selectedTime: any): any { + let formattedDate = {}; + const date: number = selectedTime['start'] + this.timerTime / 1000; + const dateNow: number = selectedTime['end'] + this.timerTime / 1000; + formattedDate = { + start: date, + end: dateNow, + step: selectedTime['step'] + }; + return formattedDate; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.html index 3979ad7a4a9..d8c1891fc21 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.html @@ -1,8 +1,8 @@
    - + [ngClass]="{'dashboard': (router.url == '/dashboard' || router.url == '/dashboard_3')}"> +
    diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.ts index f2070be5fe0..afc7a83bb27 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.ts @@ -17,7 +17,7 @@ export class WorkbenchLayoutComponent implements OnInit, OnDestroy { private subs = new Subscription(); constructor( - private router: Router, + public router: Router, private summaryService: SummaryService, private taskManagerService: TaskManagerService, private faviconService: FaviconService @@ -32,8 +32,4 @@ export class WorkbenchLayoutComponent implements OnInit, OnDestroy { ngOnDestroy() { this.subs.unsubscribe(); } - - isDashboardPage() { - return this.router.url === '/dashboard'; - } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/dashboard-promqls.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/dashboard-promqls.enum.ts new file mode 100644 index 00000000000..7afd069978d --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/dashboard-promqls.enum.ts @@ -0,0 +1,10 @@ +export enum Promqls { + USEDCAPACITY = 'ceph_cluster_total_used_bytes', + IPS = 'sum(rate(ceph_osd_op_w_in_bytes[$interval]))', + OPS = 'sum(rate(ceph_osd_op_r_out_bytes[$interval]))', + READLATENCY = 'avg_over_time(ceph_osd_apply_latency_ms[$interval])', + WRITELATENCY = 'avg_over_time(ceph_osd_commit_latency_ms[$interval])', + READCLIENTTHROUGHPUT = 'sum(rate(ceph_pool_rd_bytes[$interval]))', + WRITECLIENTTHROUGHPUT = 'sum(rate(ceph_pool_wr_bytes[$interval]))', + RECOVERYBYTES = 'sum(rate(ceph_osd_recovery_bytes[$interval]))' +} -- 2.39.5