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 1/6] 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]))' +} From cc93d5308002c21efae155cf97b6bddd73f9db17 Mon Sep 17 00:00:00 2001 From: Pedro Gonzalez Gomez Date: Mon, 25 Jul 2022 21:50:47 +0200 Subject: [PATCH 2/6] mgr/dashboard: dashboard-v3: details card mgr/dashboard: added interface, changed html table to list, using mgrModuleService now tracker: https://tracker.ceph.com/issues/58731 Signed-off-by: Pedro Gonzalez Gomez --- .../dashboard/dashboard.component.spec.ts | 55 ++++++++++++++++++- .../dashboard/dashboard.component.ts | 7 ++- .../src/app/shared/models/cd-details.ts | 5 ++ 3 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-details.ts 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 cf3f518f1cf..8e981a933cc 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,22 +1,62 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { RouterTestingModule } from '@angular/router/testing'; +import { BehaviorSubject, of } from 'rxjs'; + +import { ConfigurationService } from '~/app/shared/api/configuration.service'; +import { MgrModuleService } from '~/app/shared/api/mgr-module.service'; +import { SummaryService } from '~/app/shared/services/summary.service'; import { configureTestBed } from '~/testing/unit-test-helper'; import { CardComponent } from '../card/card.component'; import { DashboardComponent } from './dashboard.component'; +export class SummaryServiceMock { + summaryDataSource = new BehaviorSubject({ + version: + 'ceph version 17.0.0-12222-gcd0cd7cb ' + + '(b8193bb4cda16ccc5b028c3e1df62bc72350a15d) quincy (dev)' + }); + summaryData$ = this.summaryDataSource.asObservable(); + + subscribe(call: any) { + return this.summaryData$.subscribe(call); + } +} + describe('CardComponent', () => { let component: DashboardComponent; let fixture: ComponentFixture; + let configurationService: ConfigurationService; + let orchestratorService: MgrModuleService; + + const configValueData: any = { + value: [ + { + section: 'mgr', + value: 'e90a0d58-658e-4148-8f61-e896c86f0696' + } + ] + }; + + const orchData: any = { + log_level: '', + log_to_cluster: false, + log_to_cluster_level: 'info', + log_to_file: false, + orchestrator: 'cephadm' + }; configureTestBed({ - imports: [RouterTestingModule], - declarations: [DashboardComponent, CardComponent] + imports: [HttpClientTestingModule], + declarations: [DashboardComponent, CardComponent], + providers: [{ provide: SummaryService, useClass: SummaryServiceMock }] }); beforeEach(() => { fixture = TestBed.createComponent(DashboardComponent); component = fixture.componentInstance; + configurationService = TestBed.inject(ConfigurationService); + orchestratorService = TestBed.inject(MgrModuleService); }); it('should create', () => { @@ -27,4 +67,13 @@ describe('CardComponent', () => { const dashboardCards = fixture.debugElement.nativeElement.querySelectorAll('cd-card'); expect(dashboardCards.length).toBe(5); }); + + it('should get corresponding data into detailsCardData', () => { + spyOn(configurationService, 'get').and.returnValue(of(configValueData)); + spyOn(orchestratorService, 'getConfig').and.returnValue(of(orchData)); + component.ngOnInit(); + expect(component.detailsCardData.fsid).toBe('e90a0d58-658e-4148-8f61-e896c86f0696'); + expect(component.detailsCardData.orchestrator).toBe('Cephadm'); + expect(component.detailsCardData.cephVersion).toBe('17.0.0-12222-gcd0cd7cb quincy (dev)'); + }); }); 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 843bc863728..a43b319e00a 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,9 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; + +import { ConfigurationService } from '~/app/shared/api/configuration.service'; +import { MgrModuleService } from '~/app/shared/api/mgr-module.service'; +import { DashboardDetails } from '~/app/shared/models/cd-details'; +import { SummaryService } from '~/app/shared/services/summary.service'; @Component({ selector: 'cd-dashboard', diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-details.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-details.ts new file mode 100644 index 00000000000..d021f19eba7 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-details.ts @@ -0,0 +1,5 @@ +export interface DashboardDetails { + fsid?: string; + orchestrator?: string; + cephVersion?: string; +} From 95e20d27b3d0e861ff572481e9426f09e386c6d8 Mon Sep 17 00:00:00 2001 From: Pedro Gonzalez Gomez Date: Mon, 1 Aug 2022 22:17:29 +0200 Subject: [PATCH 3/6] xmgr/dashboard: dashboard-v3: capacity card tracker: https://tracker.ceph.com/issues/57862 Signed-off-by: Pedro Gonzalez Gomez mgr/dashboard: fix openapi tox error Signed-off-by: Pedro Gonzalez Gomez mgr/dashboard: capacity-card set interval and display bytes used Signed-off-by: Pedro Gonzalez Gomez mgr/dashboard: Landing Page v3 Signed-off-by: Pedro Gonzalez Gomez --- .../mgr/dashboard/controllers/cluster.py | 4 +++ .../dashboard-pie.component.html | 16 +++++++++++ .../dashboard-pie.component.scss | 22 +++++++++++++++ .../dashboard-pie.component.spec.ts | 27 +++++++++++++++++++ .../ceph/new-dashboard/dashboard.module.ts | 3 ++- .../dashboard/dashboard.component.html | 12 ++++----- .../dashboard/dashboard.component.spec.ts | 16 ++++++++--- .../dashboard/dashboard.component.ts | 14 +++++++++- .../src/app/shared/api/cluster.service.ts | 4 +++ .../styles/defaults/_bootstrap-defaults.scss | 6 ++++- src/pybind/mgr/dashboard/openapi.yaml | 22 +++++++++++++++ src/pybind/mgr/dashboard/services/cluster.py | 14 ++++++++++ 12 files changed, 147 insertions(+), 13 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-pie/dashboard-pie.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-pie/dashboard-pie.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-pie/dashboard-pie.component.spec.ts diff --git a/src/pybind/mgr/dashboard/controllers/cluster.py b/src/pybind/mgr/dashboard/controllers/cluster.py index d8170e672e9..5d776e06351 100644 --- a/src/pybind/mgr/dashboard/controllers/cluster.py +++ b/src/pybind/mgr/dashboard/controllers/cluster.py @@ -19,3 +19,7 @@ class Cluster(RESTController): parameters={'status': (str, 'Cluster Status')}) def singleton_set(self, status: str): ClusterModel(status).to_db() + + @RESTController.Collection('GET', 'capacity') + def get_capacity(self): + return ClusterModel.get_capacity() diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-pie/dashboard-pie.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-pie/dashboard-pie.component.html new file mode 100644 index 00000000000..ba8176beab3 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-pie/dashboard-pie.component.html @@ -0,0 +1,16 @@ +
    + + +
    +
    +
    +
    diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-pie/dashboard-pie.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-pie/dashboard-pie.component.scss new file mode 100644 index 00000000000..64e7a9822e2 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-pie/dashboard-pie.component.scss @@ -0,0 +1,22 @@ +@use './src/styles/chart-tooltip'; + +$canvas-width: 100%; +$canvas-height: 100%; + +.chart-container { + height: $canvas-height; + margin-left: auto; + margin-right: auto; + position: unset; + width: $canvas-width; +} + +.chart-canvas { + height: $canvas-height; + margin-left: auto; + margin-right: auto; + max-height: $canvas-height; + max-width: $canvas-width; + position: unset; + width: $canvas-width; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-pie/dashboard-pie.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-pie/dashboard-pie.component.spec.ts new file mode 100644 index 00000000000..892913dab6b --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-pie/dashboard-pie.component.spec.ts @@ -0,0 +1,27 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CssHelper } from '~/app/shared/classes/css-helper'; +import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe'; +import { configureTestBed } from '~/testing/unit-test-helper'; +import { DashboardPieComponent } from './dashboard-pie.component'; + +describe('DashboardPieComponent', () => { + let component: DashboardPieComponent; + let fixture: ComponentFixture; + + configureTestBed({ + schemas: [NO_ERRORS_SCHEMA], + declarations: [DashboardPieComponent], + providers: [CssHelper, DimlessBinaryPipe] + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DashboardPieComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); 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 27ed0f2d760..34d41ddb31a 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 @@ -9,6 +9,7 @@ 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 { DashboardPieComponent } from './dashboard-pie/dashboard-pie.component'; import { DashboardComponent } from './dashboard/dashboard.component'; @NgModule({ @@ -24,6 +25,6 @@ import { DashboardComponent } from './dashboard/dashboard.component'; ReactiveFormsModule ], - declarations: [DashboardComponent, CardComponent] + declarations: [DashboardComponent, CardComponent, DashboardPieComponent] }) 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 index 8065bb860c6..b910f2f856b 100644 --- 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 @@ -79,13 +79,11 @@ class="col-sm-3 px-3" [ngClass]="{'d-flex': flexHeight}"> - - - - + *ngIf="capacity && osdSettings"> + + 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 8e981a933cc..113ac8cfe95 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,13 +1,18 @@ import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; import { BehaviorSubject, of } from 'rxjs'; import { ConfigurationService } from '~/app/shared/api/configuration.service'; import { MgrModuleService } from '~/app/shared/api/mgr-module.service'; +import { CssHelper } from '~/app/shared/classes/css-helper'; +import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe'; import { SummaryService } from '~/app/shared/services/summary.service'; import { configureTestBed } from '~/testing/unit-test-helper'; import { CardComponent } from '../card/card.component'; +import { DashboardPieComponent } from '../dashboard-pie/dashboard-pie.component'; import { DashboardComponent } from './dashboard.component'; export class SummaryServiceMock { @@ -47,9 +52,14 @@ describe('CardComponent', () => { }; configureTestBed({ - imports: [HttpClientTestingModule], - declarations: [DashboardComponent, CardComponent], - providers: [{ provide: SummaryService, useClass: SummaryServiceMock }] + imports: [RouterTestingModule, HttpClientTestingModule], + declarations: [DashboardComponent, CardComponent, DashboardPieComponent], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + CssHelper, + DimlessBinaryPipe, + { provide: SummaryService, useClass: SummaryServiceMock } + ] }); beforeEach(() => { 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 a43b319e00a..1864c0e729a 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,8 +1,20 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit } from '@angular/core'; +import _ from 'lodash'; +import { Observable, Subscription } from 'rxjs'; +import { take } from 'rxjs/operators'; + +import { ClusterService } from '~/app/shared/api/cluster.service'; import { ConfigurationService } from '~/app/shared/api/configuration.service'; import { MgrModuleService } from '~/app/shared/api/mgr-module.service'; +import { OsdService } from '~/app/shared/api/osd.service'; import { DashboardDetails } from '~/app/shared/models/cd-details'; +import { Permissions } from '~/app/shared/models/permissions'; +import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; +import { + FeatureTogglesMap$, + FeatureTogglesService +} from '~/app/shared/services/feature-toggles.service'; import { SummaryService } from '~/app/shared/services/summary.service'; @Component({ diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cluster.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cluster.service.ts index 6b435d6ffed..f5b8e4d7cc1 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cluster.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cluster.service.ts @@ -24,4 +24,8 @@ export class ClusterService { { headers: { Accept: 'application/vnd.ceph.api.v0.1+json' } } ); } + + getCapacity() { + return this.http.get(`${this.baseURL}/capacity`, {}); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/styles/defaults/_bootstrap-defaults.scss b/src/pybind/mgr/dashboard/frontend/src/styles/defaults/_bootstrap-defaults.scss index 941f639a363..f871b4ff80f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/styles/defaults/_bootstrap-defaults.scss +++ b/src/pybind/mgr/dashboard/frontend/src/styles/defaults/_bootstrap-defaults.scss @@ -67,7 +67,6 @@ $body-color-bright: $light !default; $body-bg: $white !default; $body-color: $gray-900 !default; $body-bg-alt: $gray-200 !default; - // Health colors. $health-color-error: $red !default; $health-color-healthy: $green !default; @@ -82,7 +81,12 @@ $chart-color-yellow: #f6d173 !default; $chart-color-green: $green !default; $chart-color-gray: #ededed !default; $chart-color-cyan: $primary-500 !default; +$chart-color-light-gray: #f0f0f0 !default; +$chart-color-slight-dark-gray: #d7d7d7 !default; +$chart-color-dark-gray: #afafaf !default; +$chart-color-cyan: #73c5c5 !default; $chart-color-purple: #3c3d99 !default; +$chart-color-white: #fff !default; $chart-color-center-text: #151515 !default; $chart-color-center-text-description: #72767b !default; $chart-color-tooltip-background: $black !default; diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index d607cfb0668..a85740ef56d 100644 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -2145,6 +2145,28 @@ paths: summary: Update the cluster status tags: - Cluster + /api/cluster/capacity: + get: + parameters: [] + responses: + '200': + content: + application/vnd.ceph.api.v1.0+json: + type: object + description: OK + '400': + description: Operation exception. Please check the response body for details. + '401': + description: Unauthenticated access. Please login first. + '403': + description: Unauthorized access. Please check your permissions. + '500': + description: Unexpected error. Please check the response body for the stack + trace. + security: + - jwt: [] + tags: + - Cluster /api/cluster/user: get: description: "\n Get list of ceph users and its respective data\n \ diff --git a/src/pybind/mgr/dashboard/services/cluster.py b/src/pybind/mgr/dashboard/services/cluster.py index a057f24381f..fbb00bc7370 100644 --- a/src/pybind/mgr/dashboard/services/cluster.py +++ b/src/pybind/mgr/dashboard/services/cluster.py @@ -1,9 +1,16 @@ # -*- coding: utf-8 -*- from enum import Enum +from typing import NamedTuple from .. import mgr +class ClusterCapacity(NamedTuple): + total_avail_bytes: int + total_bytes: int + total_used_raw_bytes: int + + class ClusterModel: class Status(Enum): @@ -33,3 +40,10 @@ class ClusterModel: If the status is not set, assume it is already fully functional. """ return cls(status=mgr.get_store('cluster/status', cls.Status.POST_INSTALLED.name)) + + @classmethod + def get_capacity(cls) -> ClusterCapacity: + df = mgr.get('df') + return ClusterCapacity(total_avail_bytes=df['stats']['total_avail_bytes'], + total_bytes=df['stats']['total_bytes'], + total_used_raw_bytes=df['stats']['total_used_raw_bytes'])._asdict() 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 4/6] 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 {} From c63523119dee6cc82ffae5162f2270ac7d06d02f Mon Sep 17 00:00:00 2001 From: bryanmontalvan Date: Wed, 10 Aug 2022 14:22:02 -0400 Subject: [PATCH 5/6] mgr/dashboard: dashboard-v3: inventory card 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 Signed-off-by: Nizamudeen A Signed-off-by: Pedro Gonzalez Gomez --- .../card-row/card-row.component.html | 167 ++++++++++++++++++ .../card-row/card-row.component.scss | 0 .../card-row/card-row.component.spec.ts | 23 +++ .../card-row/card-row.component.ts | 34 ++++ .../ceph/new-dashboard/dashboard.module.ts | 11 +- .../dashboard/dashboard.component.html | 3 +- .../dashboard/dashboard.component.spec.ts | 65 ++++++- .../dashboard/dashboard.component.ts | 11 +- .../new-dashboard/pg-summary.pipe.spec.ts | 36 ++++ .../app/ceph/new-dashboard/pg-summary.pipe.ts | 27 +++ .../src/app/shared/enum/icons.enum.ts | 1 + .../app/shared/pipes/mds-summary.pipe.spec.ts | 76 ++++++++ .../src/app/shared/pipes/mds-summary.pipe.ts | 55 ++++++ .../app/shared/pipes/mgr-summary.pipe.spec.ts | 38 ++++ .../src/app/shared/pipes/mgr-summary.pipe.ts | 37 ++++ .../app/shared/pipes/osd-summary.pipe.spec.ts | 43 +++++ .../src/app/shared/pipes/osd-summary.pipe.ts | 46 +++++ .../src/app/shared/pipes/pipes.module.ts | 18 +- 18 files changed, 678 insertions(+), 13 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card-row/card-row.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card-row/card-row.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card-row/card-row.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card-row/card-row.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/pg-summary.pipe.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/pg-summary.pipe.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/mds-summary.pipe.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/mds-summary.pipe.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/mgr-summary.pipe.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/mgr-summary.pipe.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/osd-summary.pipe.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/osd-summary.pipe.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 index 00000000000..9b7bf03e775 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card-row/card-row.component.html @@ -0,0 +1,167 @@ + + + + + + {{ data.success }} + + + {{ data.categoryPgAmount?.clean }} + + + + + + + {{ data.info }} + + + + + + + {{ data.warn }} + + + {{ data.categoryPgAmount?.warning }} + + + + + + + {{ data.error }} + + + {{ data.categoryPgAmount?.unknown }} + + + + + + + {{ data.categoryPgAmount?.working }} + + + + + + + + + {{ data.up }} + + + + + {{ data.up }} + + up + + + + {{ data.in }} + + in + + + + {{ data.down }} + + down + + + + {{ data.out }} + + out + + + + {{ data.nearfull }} + + nearfull + + {{ data.full }} + + full + + + + + + + {{ data.up }} + + + + + {{ data.down }} + + + + + + + + {{ data }} + + + + + + + {{ total }} + {{ title }} + {{ title }} + {{ title }}s + + 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 index 00000000000..e69de29bb2d 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 index 00000000000..8932e67b29c --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card-row/card-row.component.spec.ts @@ -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; + + 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 index 00000000000..90c939160eb --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/card-row/card-row.component.ts @@ -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; + } + } +} 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 ac60eec6481..c32c2a630a1 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 @@ -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 {} 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 index b910f2f856b..ec8f75a905d 100644 --- 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 @@ -1,4 +1,5 @@ -
    +
    { let orchestratorService: MgrModuleService; let getHealthSpy: jasmine.Spy; let getAlertsSpy: jasmine.Spy; + let fakeFeatureTogglesService: jasmine.Spy; const healthPayload: Record = { 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(); + }); + }); }); 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 009b6717721..ba6eccedff4 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, 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 index 00000000000..b467167fdce --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/pg-summary.pipe.spec.ts @@ -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 index 00000000000..a26097ee005 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/pg-summary.pipe.ts @@ -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 = {}; + 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 + }; + } +} 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 8d7f9a8f8ab..dfeecc52c08 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 @@ -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 index 00000000000..846cfb0bc5d --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/mds-summary.pipe.spec.ts @@ -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 = { + 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 index 00000000000..77758b71d3d --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/mds-summary.pipe.ts @@ -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 index 00000000000..ac7dcc63fb9 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/mgr-summary.pipe.spec.ts @@ -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 index 00000000000..14b38095210 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/mgr-summary.pipe.ts @@ -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 index 00000000000..2c60fa585c7 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/osd-summary.pipe.spec.ts @@ -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 index 00000000000..66e86970c63 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/osd-summary.pipe.ts @@ -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; + } +} 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 226972ce0ca..4abc029533a 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 @@ -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 {} From 7f7996fa55199ce0062612e89fc441467337e8c9 Mon Sep 17 00:00:00 2001 From: Pedro Gonzalez Gomez Date: Tue, 11 Oct 2022 19:43:34 +0200 Subject: [PATCH 6/6] mgr/dashboard: dashboard-v3: cluster-utilization card mgr/dashboard: some improvements mgr/dashboard: updated promqls and improved tooltips mgr/dashboard: some fixes and improvements mgr/dashboard: added square box plugin and improved/fixed code mgr/dashboard: added maxValue option for graphs and added tooltips to text mgr/dashboard: removed unnecessary variables mgr/dashboard: fix openapi tracker: https://tracker.ceph.com/issues/57863 Signed-off-by: Pedro Gonzalez Gomez --- .../mgr/dashboard/controllers/prometheus.py | 5 +++ .../dashboard-area-chart.component.spec.ts | 36 +++++++++++++++++++ .../dashboard-time-selector.component.html | 11 ++++++ .../dashboard-time-selector.component.scss | 10 ++++++ .../dashboard-time-selector.component.spec.ts | 24 +++++++++++++ .../ceph/new-dashboard/dashboard.module.ts | 10 ++++-- .../dashboard/dashboard.component.html | 2 +- .../dashboard/dashboard.component.ts | 4 ++- .../src/app/shared/api/prometheus.service.ts | 4 +++ .../styles/defaults/_bootstrap-defaults.scss | 2 ++ src/pybind/mgr/dashboard/openapi.yaml | 22 ++++++++++++ 11 files changed, 125 insertions(+), 5 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-area-chart/dashboard-area-chart.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-time-selector/dashboard-time-selector.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-time-selector/dashboard-time-selector.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-time-selector/dashboard-time-selector.component.spec.ts diff --git a/src/pybind/mgr/dashboard/controllers/prometheus.py b/src/pybind/mgr/dashboard/controllers/prometheus.py index ae4abfc1668..1c50cec45b5 100644 --- a/src/pybind/mgr/dashboard/controllers/prometheus.py +++ b/src/pybind/mgr/dashboard/controllers/prometheus.py @@ -86,6 +86,11 @@ class Prometheus(PrometheusRESTController): def rules(self, **params): return self.prometheus_proxy('GET', '/rules', params) + @RESTController.Collection(method='GET', path='/data') + def get_prometeus_data(self, **params): + params['query'] = params.pop('params') + return self.prometheus_proxy('GET', '/query_range', params) + @RESTController.Collection(method='GET', path='/silences') def get_silences(self, **params): return self.alert_proxy('GET', '/silences', params) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-area-chart/dashboard-area-chart.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-area-chart/dashboard-area-chart.component.spec.ts new file mode 100644 index 00000000000..0501ac75dde --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-area-chart/dashboard-area-chart.component.spec.ts @@ -0,0 +1,36 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CssHelper } from '~/app/shared/classes/css-helper'; +import { DimlessBinaryPerSecondPipe } from '~/app/shared/pipes/dimless-binary-per-second.pipe'; +import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe'; +import { DimlessPipe } from '~/app/shared/pipes/dimless.pipe'; +import { FormatterService } from '~/app/shared/services/formatter.service'; +import { configureTestBed } from '~/testing/unit-test-helper'; +import { DashboardAreaChartComponent } from './dashboard-area-chart.component'; + +describe('DashboardAreaChartComponent', () => { + let component: DashboardAreaChartComponent; + let fixture: ComponentFixture; + + configureTestBed({ + schemas: [NO_ERRORS_SCHEMA], + declarations: [DashboardAreaChartComponent], + providers: [ + CssHelper, + DimlessBinaryPipe, + DimlessBinaryPerSecondPipe, + DimlessPipe, + FormatterService + ] + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DashboardAreaChartComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-time-selector/dashboard-time-selector.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-time-selector/dashboard-time-selector.component.html new file mode 100644 index 00000000000..cd960d07bd9 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-time-selector/dashboard-time-selector.component.html @@ -0,0 +1,11 @@ +
    + +
    diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-time-selector/dashboard-time-selector.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-time-selector/dashboard-time-selector.component.scss new file mode 100644 index 00000000000..13572dc2f0b --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-time-selector/dashboard-time-selector.component.scss @@ -0,0 +1,10 @@ +select#timepicker { + border: 0; +} + +.timeSelector { + position: absolute; + right: 18px; + top: 20px; + width: 12rem; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-time-selector/dashboard-time-selector.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-time-selector/dashboard-time-selector.component.spec.ts new file mode 100644 index 00000000000..9aeec4dcb51 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/new-dashboard/dashboard-time-selector/dashboard-time-selector.component.spec.ts @@ -0,0 +1,24 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { configureTestBed } from '~/testing/unit-test-helper'; +import { DashboardTimeSelectorComponent } from './dashboard-time-selector.component'; + +describe('DashboardTimeSelectorComponent', () => { + let component: DashboardTimeSelectorComponent; + let fixture: ComponentFixture; + + configureTestBed({ + schemas: [NO_ERRORS_SCHEMA], + declarations: [DashboardTimeSelectorComponent] + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DashboardTimeSelectorComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); 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 c32c2a630a1..466da562569 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 @@ -3,14 +3,16 @@ 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 { NgbNavModule, NgbPopoverModule, NgbTooltipModule } 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'; import { CardComponent } from './card/card.component'; +import { DashboardAreaChartComponent } from './dashboard-area-chart/dashboard-area-chart.component'; import { DashboardPieComponent } from './dashboard-pie/dashboard-pie.component'; +import { DashboardTimeSelectorComponent } from './dashboard-time-selector/dashboard-time-selector.component'; import { DashboardComponent } from './dashboard/dashboard.component'; import { CardRowComponent } from './card-row/card-row.component'; import { PgSummaryPipe } from './pg-summary.pipe'; @@ -24,6 +26,7 @@ import { PgSummaryPipe } from './pg-summary.pipe'; ChartsModule, RouterModule, NgbPopoverModule, + NgbTooltipModule, FormsModule, ReactiveFormsModule, SimplebarAngularModule @@ -33,9 +36,10 @@ import { PgSummaryPipe } from './pg-summary.pipe'; DashboardComponent, CardComponent, DashboardPieComponent, - DashboardPieComponent, CardRowComponent, - PgSummaryPipe + PgSummaryPipe, + DashboardAreaChartComponent, + DashboardTimeSelectorComponent ] }) 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 index ec8f75a905d..1c1db13bc56 100644 --- 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 @@ -180,7 +180,7 @@
    -
    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 ba6eccedff4..e19cac14635 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,8 +1,9 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import _ from 'lodash'; -import { Observable, Subscription } from 'rxjs'; +import { Observable, Subscription, timer } from 'rxjs'; import { take } from 'rxjs/operators'; +import moment from 'moment'; import { ClusterService } from '~/app/shared/api/cluster.service'; import { ConfigurationService } from '~/app/shared/api/configuration.service'; @@ -10,6 +11,7 @@ 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 { Promqls as queries } from '~/app/shared/enum/dashboard-promqls.enum'; import { Icons } from '~/app/shared/enum/icons.enum'; import { DashboardDetails } from '~/app/shared/models/cd-details'; import { Permissions } from '~/app/shared/models/permissions'; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.ts index 58191721953..30d7d488649 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.ts @@ -24,6 +24,10 @@ export class PrometheusService { constructor(private http: HttpClient, private settingsService: SettingsService) {} + getPrometheusData(params: any): any { + return this.http.get(`${this.baseURL}/data`, { params }); + } + ifAlertmanagerConfigured(fn: (value?: string) => void, elseFn?: () => void): void { this.settingsService.ifSettingConfigured(this.settingsKey.alertmanager, fn, elseFn); } diff --git a/src/pybind/mgr/dashboard/frontend/src/styles/defaults/_bootstrap-defaults.scss b/src/pybind/mgr/dashboard/frontend/src/styles/defaults/_bootstrap-defaults.scss index f871b4ff80f..1272c3c77ce 100644 --- a/src/pybind/mgr/dashboard/frontend/src/styles/defaults/_bootstrap-defaults.scss +++ b/src/pybind/mgr/dashboard/frontend/src/styles/defaults/_bootstrap-defaults.scss @@ -91,6 +91,8 @@ $chart-color-center-text: #151515 !default; $chart-color-center-text-description: #72767b !default; $chart-color-tooltip-background: $black !default; $chart-danger: #c9190b !default; +$chart-color-strong-blue: #0078c8 !default; +$chart-color-translucent-blue: #0096dc80 !default; // Typography diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index a85740ef56d..8b11aa2480d 100644 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -7532,6 +7532,28 @@ paths: - jwt: [] tags: - Prometheus + /api/prometheus/data: + get: + parameters: [] + responses: + '200': + content: + application/vnd.ceph.api.v1.0+json: + type: object + description: OK + '400': + description: Operation exception. Please check the response body for details. + '401': + description: Unauthenticated access. Please login first. + '403': + description: Unauthorized access. Please check your permissions. + '500': + description: Unexpected error. Please check the response body for the stack + trace. + security: + - jwt: [] + tags: + - Prometheus /api/prometheus/notifications: get: parameters: []