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;
+}