mirror of
https://github.com/ceph/ceph
synced 2025-02-23 11:07:35 +00:00
Merge pull request #20757 from tspmelo/wip-chart-tooltip
mgr/dashboard_v2: Improve charts tooltips Reviewed-by: Stephan Müller <smueller@suse.com> Reviewed-by: Ricardo Marques <rimarques@suse.com>
This commit is contained in:
commit
be113e3e92
@ -0,0 +1,12 @@
|
||||
<div class="chart-container">
|
||||
<canvas baseChart
|
||||
#chartCanvas
|
||||
[datasets]="chart?.datasets"
|
||||
[options]="chart?.options"
|
||||
[chartType]="chart?.chartType">
|
||||
</canvas>
|
||||
<div class="chartjs-tooltip"
|
||||
#chartTooltip>
|
||||
<table></table>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,6 @@
|
||||
@import '../../../../styles/chart-tooltip.scss';
|
||||
|
||||
.chart-container {
|
||||
height: 500px;
|
||||
width: 100%;
|
||||
}
|
@ -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<CephfsChartComponent>;
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
@ -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;
|
||||
}
|
||||
}
|
@ -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 {}
|
||||
|
@ -48,15 +48,9 @@
|
||||
</div>
|
||||
|
||||
<div class="row"
|
||||
*ngFor="let mdsCounter of objectValues(mdsCounters)">
|
||||
*ngFor="let mdsCounter of objectValues(mdsCounters); trackBy: trackByFn">
|
||||
<div class="cold-md-12">
|
||||
<div class="chart-container">
|
||||
<canvas baseChart
|
||||
[datasets]="mdsCounter.datasets"
|
||||
[options]="mdsCounter.options"
|
||||
[chartType]="mdsCounter.chartType">
|
||||
</canvas>
|
||||
</div>
|
||||
<cd-cephfs-chart [mdsCounter]="mdsCounter"></cd-cephfs-chart>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -1,10 +1,3 @@
|
||||
.chart-container {
|
||||
position: relative;
|
||||
margin: auto;
|
||||
height: 500px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.progress {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
@ -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 }
|
||||
]
|
||||
|
@ -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<any>;
|
||||
@ViewChild('activityTmpl') activityTmpl: TemplateRef<any>;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -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]
|
||||
})
|
||||
|
@ -0,0 +1,15 @@
|
||||
<div class="chart-container">
|
||||
<canvas baseChart
|
||||
#chartCanvas
|
||||
[datasets]="chart.dataset"
|
||||
[chartType]="chart.chartType"
|
||||
[options]="chart.options"
|
||||
[labels]="chart.labels"
|
||||
[colors]="chart.colors"
|
||||
width="120"
|
||||
height="120"></canvas>
|
||||
<div class="chartjs-tooltip"
|
||||
#chartTooltip>
|
||||
<table></table>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1 @@
|
||||
@import '../../../../styles/chart-tooltip.scss';
|
@ -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<HealthPieComponent>;
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
@ -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]);
|
||||
}
|
||||
}
|
@ -27,7 +27,9 @@
|
||||
</div>
|
||||
<div class="media-body">
|
||||
<span class="media-heading"
|
||||
i18n="ceph monitors"><a routerLink="/monitor/">Monitors</a></span>
|
||||
i18n="ceph monitors">
|
||||
<a routerLink="/monitor/">Monitors</a>
|
||||
</span>
|
||||
<span class="media-text">{{ contentData.mon_status | monSummary }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -41,7 +43,9 @@
|
||||
</div>
|
||||
<div class="media-body">
|
||||
<span class="media-heading"
|
||||
i18n="ceph OSDs"><a routerLink="/osd/">OSDs</a></span>
|
||||
i18n="ceph OSDs">
|
||||
<a routerLink="/osd/">OSDs</a>
|
||||
</span>
|
||||
<span class="media-text">{{ contentData.osd_map | osdSummary }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -94,31 +98,15 @@
|
||||
<span style="font-size: 45px;">{{ contentData.df.stats.total_objects | dimless }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="center-block pie"
|
||||
*ngIf="rawUsage.dataset">
|
||||
<canvas baseChart
|
||||
id="raw_usage_chart"
|
||||
[datasets]="rawUsage.dataset"
|
||||
[chartType]="rawUsage.chartType"
|
||||
[options]="rawUsage.options"
|
||||
[labels]="rawUsage.labels"
|
||||
[colors]="rawUsage.colors"
|
||||
width="120"
|
||||
height="120"></canvas>
|
||||
<div class="center-block pie">
|
||||
<cd-health-pie [data]="contentData"
|
||||
(prepareFn)="prepareRawUsage($event[0], $event[1])"></cd-health-pie>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="center-block pie"
|
||||
*ngIf="poolUsage.dataset">
|
||||
<canvas baseChart
|
||||
id="pool_usage_chart"
|
||||
[datasets]="poolUsage.dataset"
|
||||
[chartType]="poolUsage.chartType"
|
||||
[options]="poolUsage.options"
|
||||
[labels]="poolUsage.labels"
|
||||
[colors]="poolUsage.colors"
|
||||
width="120"
|
||||
height="120"></canvas>
|
||||
<div class="center-block pie">
|
||||
<cd-health-pie [data]="contentData"
|
||||
(prepareFn)="preparePoolUsage($event[0], $event[1])"></cd-health-pie>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -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<HealthComponent>;
|
||||
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();
|
||||
})
|
||||
);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,13 @@
|
||||
<div class="chart-container"
|
||||
[ngStyle]="style">
|
||||
<canvas baseChart
|
||||
<canvas baseChart #sparkCanvas
|
||||
[labels]="labels"
|
||||
[datasets]="datasets"
|
||||
[options]="options"
|
||||
[colors]="colors"
|
||||
[chartType]="'line'">
|
||||
</canvas>
|
||||
<div class="chartjs-tooltip" #sparkTooltip>
|
||||
<table></table>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,4 +1,5 @@
|
||||
@import '../../../../styles/chart-tooltip.scss';
|
||||
|
||||
.chart-container {
|
||||
position: relative;
|
||||
margin: auto;
|
||||
position: static !important;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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 = '<thead>';
|
||||
|
||||
titleLines.forEach(title => {
|
||||
innerHtml += '<tr><th>' + this.getTitle(title) + '</th></tr>';
|
||||
});
|
||||
innerHtml += '</thead><tbody>';
|
||||
|
||||
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 = '<span class="chartjs-tooltip-key" style="' + style + '"></span>';
|
||||
innerHtml += '<tr><td nowrap>' + span + this.getBody(body) + '</td></tr>';
|
||||
});
|
||||
innerHtml += '</tbody>';
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user