diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.html new file mode 100644 index 00000000000..b98d70838a0 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.html @@ -0,0 +1,12 @@ +
+ + +
+
+
+
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.scss new file mode 100644 index 00000000000..62a023b9aaf --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.scss @@ -0,0 +1,6 @@ +@import '../../../../styles/chart-tooltip.scss'; + +.chart-container { + height: 500px; + width: 100%; +} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.spec.ts new file mode 100644 index 00000000000..6d552041f1c --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.spec.ts @@ -0,0 +1,29 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ChartsModule } from 'ng2-charts/ng2-charts'; + +import { CephfsChartComponent } from './cephfs-chart.component'; + +describe('CephfsChartComponent', () => { + let component: CephfsChartComponent; + let fixture: ComponentFixture; + + beforeEach( + async(() => { + TestBed.configureTestingModule({ + imports: [ChartsModule], + declarations: [CephfsChartComponent] + }).compileComponents(); + }) + ); + + beforeEach(() => { + fixture = TestBed.createComponent(CephfsChartComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.ts new file mode 100644 index 00000000000..cca1ae2feb9 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.ts @@ -0,0 +1,164 @@ +import { Component, ElementRef, Input, OnChanges, OnInit, ViewChild } from '@angular/core'; + +import * as _ from 'lodash'; +import * as moment from 'moment'; + +import { ChartTooltip } from '../../../shared/models/chart-tooltip'; + +@Component({ + selector: 'cd-cephfs-chart', + templateUrl: './cephfs-chart.component.html', + styleUrls: ['./cephfs-chart.component.scss'] +}) +export class CephfsChartComponent implements OnChanges, OnInit { + @ViewChild('chartCanvas') chartCanvas: ElementRef; + @ViewChild('chartTooltip') chartTooltip: ElementRef; + + @Input() mdsCounter: any; + + lhsCounter = 'mds.inodes'; + rhsCounter = 'mds_server.handle_client_request'; + + chart: any; + + constructor() {} + + ngOnInit() { + if (_.isUndefined(this.mdsCounter)) { + return; + } + + const getTitle = title => { + return moment(title).format('LTS'); + }; + + const getStyleTop = tooltip => { + return tooltip.caretY - tooltip.height - 15 + 'px'; + }; + + const getStyleLeft = tooltip => { + return tooltip.caretX + 'px'; + }; + + const chartTooltip = new ChartTooltip( + this.chartCanvas, + this.chartTooltip, + getStyleLeft, + getStyleTop + ); + chartTooltip.getTitle = getTitle; + chartTooltip.checkOffset = true; + + const lhsData = this.convert_timeseries(this.mdsCounter[this.lhsCounter]); + const rhsData = this.delta_timeseries(this.mdsCounter[this.rhsCounter]); + + this.chart = { + datasets: [ + { + label: this.lhsCounter, + yAxisID: 'LHS', + data: lhsData, + tension: 0.1 + }, + { + label: this.rhsCounter, + yAxisID: 'RHS', + data: rhsData, + tension: 0.1 + } + ], + options: { + responsive: true, + maintainAspectRatio: false, + legend: { + position: 'top' + }, + scales: { + xAxes: [ + { + position: 'top', + type: 'time', + time: { + displayFormats: { + quarter: 'MMM YYYY' + } + } + } + ], + yAxes: [ + { + id: 'LHS', + type: 'linear', + position: 'left', + min: 0 + }, + { + id: 'RHS', + type: 'linear', + position: 'right', + min: 0 + } + ] + }, + tooltips: { + enabled: false, + mode: 'index', + intersect: false, + position: 'nearest', + custom: tooltip => { + chartTooltip.customTooltips(tooltip); + } + } + }, + chartType: 'line' + }; + } + + ngOnChanges() { + if (!this.chart) { + return; + } + + const lhsData = this.convert_timeseries(this.mdsCounter[this.lhsCounter]); + const rhsData = this.delta_timeseries(this.mdsCounter[this.rhsCounter]); + + this.chart.datasets[0].data = lhsData; + this.chart.datasets[1].data = rhsData; + } + + // Convert ceph-mgr's time series format (list of 2-tuples + // with seconds-since-epoch timestamps) into what chart.js + // can handle (list of objects with millisecs-since-epoch + // timestamps) + convert_timeseries(sourceSeries) { + const data = []; + _.each(sourceSeries, dp => { + data.push({ + x: dp[0] * 1000, + y: dp[1] + }); + }); + + return data; + } + + delta_timeseries(sourceSeries) { + let i; + let prev = sourceSeries[0]; + const result = []; + for (i = 1; i < sourceSeries.length; i++) { + const cur = sourceSeries[i]; + const tdelta = cur[0] - prev[0]; + const vdelta = cur[1] - prev[1]; + const rate = vdelta / tdelta; + + result.push({ + x: cur[0] * 1000, + y: rate + }); + + prev = cur; + } + return result; + } +} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs.module.ts index 2c1432d168e..c47051c18e6 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs.module.ts +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs.module.ts @@ -6,6 +6,7 @@ import { ProgressbarModule } from 'ngx-bootstrap/progressbar'; import { AppRoutingModule } from '../../app-routing.module'; import { SharedModule } from '../../shared/shared.module'; +import { CephfsChartComponent } from './cephfs-chart/cephfs-chart.component'; import { CephfsService } from './cephfs.service'; import { CephfsComponent } from './cephfs/cephfs.component'; import { ClientsComponent } from './clients/clients.component'; @@ -18,7 +19,7 @@ import { ClientsComponent } from './clients/clients.component'; ChartsModule, ProgressbarModule.forRoot() ], - declarations: [CephfsComponent, ClientsComponent], + declarations: [CephfsComponent, ClientsComponent, CephfsChartComponent], providers: [CephfsService] }) export class CephfsModule {} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.html index 2333f770b66..eb970cec270 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.html +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.html @@ -48,15 +48,9 @@
+ *ngFor="let mdsCounter of objectValues(mdsCounters); trackBy: trackByFn">
-
- - -
+
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.scss index 567fbf3aa15..d82829af85c 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.scss +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.scss @@ -1,10 +1,3 @@ -.chart-container { - position: relative; - margin: auto; - height: 500px; - width: 100%; -} - .progress { margin-bottom: 0px; } diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.spec.ts index 03e5b1ba3bc..3df655defa6 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.spec.ts +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.spec.ts @@ -6,6 +6,7 @@ import { BsDropdownModule, ProgressbarModule } from 'ngx-bootstrap'; import { Observable } from 'rxjs/Observable'; import { SharedModule } from '../../../shared/shared.module'; +import { CephfsChartComponent } from '../cephfs-chart/cephfs-chart.component'; import { CephfsService } from '../cephfs.service'; import { CephfsComponent } from './cephfs.component'; @@ -36,7 +37,7 @@ describe('CephfsComponent', () => { BsDropdownModule.forRoot(), ProgressbarModule.forRoot() ], - declarations: [CephfsComponent], + declarations: [CephfsComponent, CephfsChartComponent], providers: [ { provide: CephfsService, useValue: fakeFilesystemService } ] diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.ts index 1cd93f3a9ca..d8fe382fec6 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.ts +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.ts @@ -2,6 +2,7 @@ import { Component, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/c import { ActivatedRoute } from '@angular/router'; import * as _ from 'lodash'; +import { Subscription } from 'rxjs/Subscription'; import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe'; import { DimlessPipe } from '../../../shared/pipes/dimless.pipe'; @@ -16,15 +17,10 @@ export class CephfsComponent implements OnInit, OnDestroy { @ViewChild('poolProgressTmpl') poolProgressTmpl: TemplateRef; @ViewChild('activityTmpl') activityTmpl: TemplateRef; - routeParamsSubscribe: any; + routeParamsSubscribe: Subscription; objectValues = Object.values; - single: any[]; - multi: any[]; - - view: any[] = [700, 400]; - id: number; name: string; ranks: any; @@ -34,11 +30,6 @@ export class CephfsComponent implements OnInit, OnDestroy { mdsCounters = {}; - lhsCounter = 'mds.inodes'; - rhsCounter = 'mds_server.handle_client_request'; - charts = {}; - interval: any; - constructor( private route: ActivatedRoute, private cephfsService: CephfsService, @@ -113,119 +104,23 @@ export class CephfsComponent implements OnInit, OnDestroy { ]; this.name = data.cephfs.name; this.clientCount = data.cephfs.client_count; - this.draw_chart(); }); - } - draw_chart() { this.cephfsService.getMdsCounters(this.id).subscribe(data => { - const topChart = true; - _.each(this.mdsCounters, (value, key) => { if (data[key] === undefined) { delete this.mdsCounters[key]; } }); - _.each(data, (mdsData, mdsName) => { - const lhsData = this.convert_timeseries(mdsData[this.lhsCounter]); - const rhsData = this.delta_timeseries(mdsData[this.rhsCounter]); - - if (this.mdsCounters[mdsName] === undefined) { - this.mdsCounters[mdsName] = { - datasets: [ - { - label: this.lhsCounter, - yAxisID: 'LHS', - data: lhsData, - tension: 0.1 - }, - { - label: this.rhsCounter, - yAxisID: 'RHS', - data: rhsData, - tension: 0.1 - } - ], - options: { - responsive: true, - maintainAspectRatio: false, - legend: { - position: 'top', - display: topChart - }, - scales: { - xAxes: [ - { - position: 'top', - type: 'time', - display: topChart, - time: { - displayFormats: { - quarter: 'MMM YYYY' - } - } - } - ], - yAxes: [ - { - id: 'LHS', - type: 'linear', - position: 'left', - min: 0 - }, - { - id: 'RHS', - type: 'linear', - position: 'right', - min: 0 - } - ] - } - }, - chartType: 'line' - }; - } else { - this.mdsCounters[mdsName].datasets[0].data = lhsData; - this.mdsCounters[mdsName].datasets[1].data = rhsData; - } + _.each(data, (mdsData: any, mdsName) => { + mdsData.name = mdsName; + this.mdsCounters[mdsName] = mdsData; }); }); } - // Convert ceph-mgr's time series format (list of 2-tuples - // with seconds-since-epoch timestamps) into what chart.js - // can handle (list of objects with millisecs-since-epoch - // timestamps) - convert_timeseries(sourceSeries) { - const data = []; - _.each(sourceSeries, dp => { - data.push({ - x: dp[0] * 1000, - y: dp[1] - }); - }); - - return data; - } - - delta_timeseries(sourceSeries) { - let i; - let prev = sourceSeries[0]; - const result = []; - for (i = 1; i < sourceSeries.length; i++) { - const cur = sourceSeries[i]; - const tdelta = cur[0] - prev[0]; - const vdelta = cur[1] - prev[1]; - const rate = vdelta / tdelta; - - result.push({ - x: cur[0] * 1000, - y: rate - }); - - prev = cur; - } - return result; + trackByFn(index, item) { + return item.name; } } diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard.module.ts index cae29e974aa..cf4c025060f 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard.module.ts +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard.module.ts @@ -8,6 +8,7 @@ import { TabsModule } from 'ngx-bootstrap/tabs'; import { SharedModule } from '../../shared/shared.module'; import { DashboardService } from './dashboard.service'; import { DashboardComponent } from './dashboard/dashboard.component'; +import { HealthPieComponent } from './health-pie/health-pie.component'; import { HealthComponent } from './health/health.component'; import { LogColorPipe } from './log-color.pipe'; import { MdsSummaryPipe } from './mds-summary.pipe'; @@ -28,7 +29,8 @@ import { PgStatusPipe } from './pg-status.pipe'; MgrSummaryPipe, PgStatusPipe, MdsSummaryPipe, - PgStatusStylePipe + PgStatusStylePipe, + HealthPieComponent ], providers: [DashboardService] }) diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.html new file mode 100644 index 00000000000..7135f96f67b --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.html @@ -0,0 +1,15 @@ +
+ +
+
+
+
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.scss new file mode 100644 index 00000000000..b3abf8681a2 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.scss @@ -0,0 +1 @@ +@import '../../../../styles/chart-tooltip.scss'; diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.spec.ts new file mode 100644 index 00000000000..dca539f041c --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.spec.ts @@ -0,0 +1,30 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ChartsModule } from 'ng2-charts/ng2-charts'; + +import { SharedModule } from '../../../shared/shared.module'; +import { HealthPieComponent } from './health-pie.component'; + +describe('HealthPieComponent', () => { + let component: HealthPieComponent; + let fixture: ComponentFixture; + + beforeEach( + async(() => { + TestBed.configureTestingModule({ + imports: [ChartsModule, SharedModule], + declarations: [HealthPieComponent] + }).compileComponents(); + }) + ); + + beforeEach(() => { + fixture = TestBed.createComponent(HealthPieComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.ts new file mode 100644 index 00000000000..196d871066a --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.ts @@ -0,0 +1,117 @@ +import { + Component, + ElementRef, + EventEmitter, + Input, + OnChanges, + OnInit, + Output, + ViewChild +} from '@angular/core'; + +import * as Chart from 'chart.js'; +import * as _ from 'lodash'; + +import { ChartTooltip } from '../../../shared/models/chart-tooltip'; +import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe'; + +@Component({ + selector: 'cd-health-pie', + templateUrl: './health-pie.component.html', + styleUrls: ['./health-pie.component.scss'] +}) +export class HealthPieComponent implements OnChanges, OnInit { + @ViewChild('chartCanvas') chartCanvasRef: ElementRef; + @ViewChild('chartTooltip') chartTooltipRef: ElementRef; + + @Input() data: any; + @Input() tooltipFn: any; + @Output() prepareFn = new EventEmitter(); + + chart: any = { + chartType: 'doughnut', + dataset: [ + { + label: null, + borderWidth: 0 + } + ], + options: { + responsive: true, + legend: { display: false }, + animation: { duration: 0 }, + + tooltips: { + enabled: false + } + }, + colors: [ + { + borderColor: 'transparent' + } + ] + }; + + constructor(private dimlessBinary: DimlessBinaryPipe) {} + + ngOnInit() { + // An extension to Chart.js to enable rendering some + // text in the middle of a doughnut + Chart.pluginService.register({ + beforeDraw: function(chart) { + if (!chart.options.center_text) { + return; + } + + const width = chart.chart.width, + height = chart.chart.height, + ctx = chart.chart.ctx; + + ctx.restore(); + const fontSize = (height / 114).toFixed(2); + ctx.font = fontSize + 'em sans-serif'; + ctx.textBaseline = 'middle'; + + const text = chart.options.center_text, + textX = Math.round((width - ctx.measureText(text).width) / 2), + textY = height / 2; + + ctx.fillText(text, textX, textY); + ctx.save(); + } + }); + + const getStyleTop = (tooltip, positionY) => { + return positionY + tooltip.caretY - tooltip.height - 10 + 'px'; + }; + + const getStyleLeft = (tooltip, positionX) => { + return positionX + tooltip.caretX + 'px'; + }; + + const getBody = (body) => { + const bodySplit = body[0].split(': '); + bodySplit[1] = this.dimlessBinary.transform(bodySplit[1]); + return bodySplit.join(': '); + }; + + const chartTooltip = new ChartTooltip( + this.chartCanvasRef, + this.chartTooltipRef, + getStyleLeft, + getStyleTop, + ); + chartTooltip.getBody = getBody; + + const self = this; + this.chart.options.tooltips.custom = (tooltip) => { + chartTooltip.customTooltips(tooltip); + }; + + this.prepareFn.emit([this.chart, this.data]); + } + + ngOnChanges() { + this.prepareFn.emit([this.chart, this.data]); + } +} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.html index 98ebb918953..348324e4dc1 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.html +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.html @@ -27,7 +27,9 @@
Monitors + i18n="ceph monitors"> + Monitors + {{ contentData.mon_status | monSummary }}
@@ -41,7 +43,9 @@
OSDs + i18n="ceph OSDs"> + OSDs + {{ contentData.osd_map | osdSummary }}
@@ -94,31 +98,15 @@ {{ contentData.df.stats.total_objects | dimless }} -
- +
+
-
- +
+
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.spec.ts index cac806a0f77..983b1452e89 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.spec.ts +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.spec.ts @@ -1,36 +1,28 @@ import { HttpClientModule } from '@angular/common/http'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { ChartsModule } from 'ng2-charts'; import { TabsModule } from 'ngx-bootstrap/tabs'; -import { AppModule } from '../../../app.module'; import { SharedModule } from '../../../shared/shared.module'; import { DashboardService } from '../dashboard.service'; -import { LogColorPipe } from '../log-color.pipe'; -import { MdsSummaryPipe } from '../mds-summary.pipe'; -import { MgrSummaryPipe } from '../mgr-summary.pipe'; -import { MonSummaryPipe } from '../mon-summary.pipe'; -import { OsdSummaryPipe } from '../osd-summary.pipe'; -import { PgStatusStylePipe } from '../pg-status-style.pipe'; -import { PgStatusPipe } from '../pg-status.pipe'; import { HealthComponent } from './health.component'; describe('HealthComponent', () => { let component: HealthComponent; let fixture: ComponentFixture; - const dashboardServiceStub = { + + const fakeService = { getHealth() { return {}; } }; + beforeEach( async(() => { TestBed.configureTestingModule({ - providers: [ - { provide: DashboardService, useValue: dashboardServiceStub } - ], - imports: [AppModule] + providers: [{ provide: DashboardService, useValue: fakeService }], + imports: [SharedModule], + declarations: [HealthComponent] }).compileComponents(); }) ); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.ts index 0a065c10bd3..3cdddc970e3 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.ts +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.ts @@ -1,9 +1,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; -import * as Chart from 'chart.js'; import * as _ from 'lodash'; -import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe'; import { DashboardService } from '../dashboard.service'; @Component({ @@ -13,48 +11,13 @@ import { DashboardService } from '../dashboard.service'; }) export class HealthComponent implements OnInit, OnDestroy { contentData: any; - interval: any; - poolUsage: any = { - chartType: 'doughnut' - }; - rawUsage: any = { - chartType: 'doughnut', - center_text: 0 - }; + interval: number; - constructor( - private dimlessBinary: DimlessBinaryPipe, - private dashboardService: DashboardService - ) {} + constructor(private dashboardService: DashboardService) {} ngOnInit() { - // An extension to Chart.js to enable rendering some - // text in the middle of a doughnut - Chart.pluginService.register({ - beforeDraw: function(chart) { - if (!chart.options.center_text) { - return; - } - const width = chart.chart.width, - height = chart.chart.height, - ctx = chart.chart.ctx; - - ctx.restore(); - const fontSize = (height / 114).toFixed(2); - ctx.font = fontSize + 'em sans-serif'; - ctx.textBaseline = 'middle'; - - const text = chart.options.center_text, - textX = Math.round((width - ctx.measureText(text).width) / 2), - textY = height / 2; - - ctx.fillText(text, textX, textY); - ctx.save(); - } - }); - this.getInfo(); - this.interval = setInterval(() => { + this.interval = window.setInterval(() => { this.getInfo(); }, 5000); } @@ -66,80 +29,38 @@ export class HealthComponent implements OnInit, OnDestroy { getInfo() { this.dashboardService.getHealth().subscribe((data: any) => { this.contentData = data; - this.draw_usage_charts(); }); } - draw_usage_charts() { + prepareRawUsage(chart, data) { let rawUsageChartColor; + const rawUsageText = - Math.round( - 100 * - (this.contentData.df.stats.total_used_bytes / - this.contentData.df.stats.total_bytes) - ) + '%'; - if ( - this.contentData.df.stats.total_used_bytes / - this.contentData.df.stats.total_bytes >= - this.contentData.osd_map.full_ratio - ) { + Math.round(100 * (data.df.stats.total_used_bytes / data.df.stats.total_bytes)) + '%'; + + if (data.df.stats.total_used_bytes / data.df.stats.total_bytes >= data.osd_map.full_ratio) { rawUsageChartColor = '#ff0000'; } else if ( - this.contentData.df.stats.total_used_bytes / - this.contentData.df.stats.total_bytes >= - this.contentData.osd_map.backfillfull_ratio + data.df.stats.total_used_bytes / data.df.stats.total_bytes >= + data.osd_map.backfillfull_ratio ) { rawUsageChartColor = '#ff6600'; } else if ( - this.contentData.df.stats.total_used_bytes / - this.contentData.df.stats.total_bytes >= - this.contentData.osd_map.nearfull_ratio + data.df.stats.total_used_bytes / data.df.stats.total_bytes >= + data.osd_map.nearfull_ratio ) { rawUsageChartColor = '#ffc200'; } else { rawUsageChartColor = '#00bb00'; } - this.rawUsage = { - chartType: 'doughnut', - dataset: [ - { - label: null, - borderWidth: 0, - data: [ - this.contentData.df.stats.total_used_bytes, - this.contentData.df.stats.total_avail_bytes - ] - } - ], - options: { - center_text: rawUsageText, - responsive: true, - legend: { display: false }, - animation: { duration: 0 }, - tooltips: { - callbacks: { - label: (tooltipItem, chart) => { - return ( - chart.labels[tooltipItem.index] + - ': ' + - this.dimlessBinary.transform( - chart.datasets[0].data[tooltipItem.index] - ) - ); - } - } - } - }, - colors: [ - { - backgroundColor: [rawUsageChartColor, '#424d52'], - borderColor: 'transparent' - } - ], - labels: ['Raw Used', 'Raw Available'] - }; + chart.dataset[0].data = [data.df.stats.total_used_bytes, data.df.stats.total_avail_bytes]; + chart.options.center_text = rawUsageText; + chart.colors = [{ backgroundColor: [rawUsageChartColor, '#424d52'] }]; + chart.labels = ['Raw Used', 'Raw Available']; + } + preparePoolUsage(chart, data) { const colors = [ '#3366CC', '#109618', @@ -166,45 +87,13 @@ export class HealthComponent implements OnInit, OnDestroy { const poolLabels = []; const poolData = []; - _.each(this.contentData.df.pools, function(pool, i) { + _.each(data.df.pools, (pool, i) => { poolLabels.push(pool['name']); poolData.push(pool['stats']['bytes_used']); }); - this.poolUsage = { - chartType: 'doughnut', - dataset: [ - { - label: null, - borderWidth: 0, - data: poolData - } - ], - options: { - responsive: true, - legend: { display: false }, - animation: { duration: 0 }, - tooltips: { - callbacks: { - label: (tooltipItem, chart) => { - return ( - chart.labels[tooltipItem.index] + - ': ' + - this.dimlessBinary.transform( - chart.datasets[0].data[tooltipItem.index] - ) - ); - } - } - } - }, - colors: [ - { - backgroundColor: colors, - borderColor: 'transparent' - } - ], - labels: poolLabels - }; + chart.dataset[0].data = poolData; + chart.colors = [{ backgroundColor: colors }]; + chart.labels = poolLabels; } } diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/sparkline/sparkline.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/sparkline/sparkline.component.html index 91f0d73e0f2..4b7a1b87223 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/sparkline/sparkline.component.html +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/sparkline/sparkline.component.html @@ -1,10 +1,13 @@
- +
+
+
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/sparkline/sparkline.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/sparkline/sparkline.component.scss index ba8a8ebb7d8..ec7d98291e8 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/sparkline/sparkline.component.scss +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/sparkline/sparkline.component.scss @@ -1,4 +1,5 @@ +@import '../../../../styles/chart-tooltip.scss'; + .chart-container { - position: relative; - margin: auto; + position: static !important; } diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/sparkline/sparkline.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/sparkline/sparkline.component.ts index 35b3d91c8d1..fa20ce30186 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/sparkline/sparkline.component.ts +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/sparkline/sparkline.component.ts @@ -1,12 +1,17 @@ -import { Component, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { Component, ElementRef, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core'; import { Input } from '@angular/core'; +import { ChartTooltip } from '../../../shared/models/chart-tooltip'; + @Component({ selector: 'cd-sparkline', templateUrl: './sparkline.component.html', styleUrls: ['./sparkline.component.scss'] }) export class SparklineComponent implements OnInit, OnChanges { + @ViewChild('sparkCanvas') chartCanvasRef: ElementRef; + @ViewChild('sparkTooltip') chartTooltipRef: ElementRef; + @Input() data: any; @Input() style = { @@ -40,7 +45,10 @@ export class SparklineComponent implements OnInit, OnChanges { } }, tooltips: { - enabled: true + enabled: false, + mode: 'index', + intersect: false, + custom: undefined }, scales: { yAxes: [ @@ -66,7 +74,31 @@ export class SparklineComponent implements OnInit, OnChanges { constructor() {} - ngOnInit() {} + ngOnInit() { + const getStyleTop = (tooltip, positionY) => { + return (tooltip.caretY - tooltip.height - tooltip.yPadding - 5) + 'px'; + }; + + const getStyleLeft = (tooltip, positionX) => { + return positionX + tooltip.caretX + 'px'; + }; + + const chartTooltip = new ChartTooltip( + this.chartCanvasRef, + this.chartTooltipRef, + getStyleLeft, + getStyleTop + ); + + chartTooltip.customColors = { + backgroundColor: this.colors[0].pointBackgroundColor, + borderColor: this.colors[0].pointBorderColor + }; + + this.options.tooltips.custom = tooltip => { + chartTooltip.customTooltips(tooltip); + }; + } ngOnChanges(changes: SimpleChanges) { this.datasets[0].data = changes['data'].currentValue; diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/models/chart-tooltip.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/models/chart-tooltip.ts new file mode 100644 index 00000000000..56962f3da69 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/models/chart-tooltip.ts @@ -0,0 +1,117 @@ +import { ElementRef } from '@angular/core'; + +import * as _ from 'lodash'; + +export class ChartTooltip { + tooltipEl: any; + chartEl: any; + getStyleLeft: Function; + getStyleTop: Function; + customColors = { + backgroundColor: undefined, + borderColor: undefined + }; + checkOffset = false; + + /** + * Creates an instance of ChartTooltip. + * @param {ElementRef} chartCanvas Canvas Element + * @param {ElementRef} chartTooltip Tooltip Element + * @param {Function} getStyleLeft Function that calculates the value of Left + * @param {Function} getStyleTop Function that calculates the value of Top + * @memberof ChartTooltip + */ + constructor( + chartCanvas: ElementRef, + chartTooltip: ElementRef, + getStyleLeft: Function, + getStyleTop: Function + ) { + this.chartEl = chartCanvas.nativeElement; + this.getStyleLeft = getStyleLeft; + this.getStyleTop = getStyleTop; + this.tooltipEl = chartTooltip.nativeElement; + } + + /** + * Implementation of a ChartJS custom tooltip function. + * + * @param {any} tooltip + * @memberof ChartTooltip + */ + customTooltips(tooltip) { + // Hide if no tooltip + if (tooltip.opacity === 0) { + this.tooltipEl.style.opacity = 0; + return; + } + + // Set caret Position + this.tooltipEl.classList.remove('above', 'below', 'no-transform'); + if (tooltip.yAlign) { + this.tooltipEl.classList.add(tooltip.yAlign); + } else { + this.tooltipEl.classList.add('no-transform'); + } + + // Set Text + if (tooltip.body) { + const titleLines = tooltip.title || []; + const bodyLines = tooltip.body.map(bodyItem => { + return bodyItem.lines; + }); + + let innerHtml = ''; + + titleLines.forEach(title => { + innerHtml += '' + this.getTitle(title) + ''; + }); + innerHtml += ''; + + bodyLines.forEach((body, i) => { + const colors = tooltip.labelColors[i]; + let style = 'background:' + (this.customColors.backgroundColor || colors.backgroundColor); + style += '; border-color:' + (this.customColors.borderColor || colors.borderColor); + style += '; border-width: 2px'; + const span = ''; + innerHtml += '' + span + this.getBody(body) + ''; + }); + innerHtml += ''; + + const tableRoot = this.tooltipEl.querySelector('table'); + tableRoot.innerHTML = innerHtml; + } + + const positionY = this.chartEl.offsetTop; + const positionX = this.chartEl.offsetLeft; + + // Display, position, and set styles for font + if (this.checkOffset) { + const halfWidth = tooltip.width / 2; + this.tooltipEl.classList.remove('transform-left'); + this.tooltipEl.classList.remove('transform-right'); + if (tooltip.caretX - halfWidth < 0) { + this.tooltipEl.classList.add('transform-left'); + } else if (tooltip.caretX + halfWidth > this.chartEl.width) { + this.tooltipEl.classList.add('transform-right'); + } + } + + this.tooltipEl.style.left = this.getStyleLeft(tooltip, positionX); + this.tooltipEl.style.top = this.getStyleTop(tooltip, positionY); + + this.tooltipEl.style.opacity = 1; + this.tooltipEl.style.fontFamily = tooltip._fontFamily; + this.tooltipEl.style.fontSize = tooltip.fontSize; + this.tooltipEl.style.fontStyle = tooltip._fontStyle; + this.tooltipEl.style.padding = tooltip.yPadding + 'px ' + tooltip.xPadding + 'px'; + } + + getBody(body) { + return body; + } + + getTitle(title) { + return title; + } +} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/styles/chart-tooltip.scss b/src/pybind/mgr/dashboard_v2/frontend/src/styles/chart-tooltip.scss new file mode 100644 index 00000000000..835bb362db4 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/styles/chart-tooltip.scss @@ -0,0 +1,62 @@ +.chart-container { + position: absolute; + margin: auto; + cursor: pointer; + overflow: visible; +} + +canvas { + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.chartjs-tooltip { + opacity: 0; + position: absolute; + background: rgba(0, 0, 0, 0.7); + color: white; + border-radius: 3px; + -webkit-transition: all 0.1s ease; + transition: all 0.1s ease; + pointer-events: none; + font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif !important; + + -webkit-transform: translate(-50%, 0); + transform: translate(-50%, 0); + + &.transform-left { + transform: translate(-10%, 0); + + &::after { + left: 10%; + } + } + + &.transform-right { + transform: translate(-90%, 0); + + &::after { + left: 90%; + } + } +} + +.chartjs-tooltip::after { + content: ' '; + position: absolute; + top: 100%; /* At the bottom of the tooltip */ + left: 50%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: black transparent transparent transparent; +} + +::ng-deep .chartjs-tooltip-key { + display: inline-block; + width: 10px; + height: 10px; + margin-right: 10px; +}