Extracted the charts into a new component and it is now using a new tooltip.
Signed-off-by: Tiago Melo <tmelo@suse.com>
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';
MgrSummaryPipe,
PgStatusPipe,
MdsSummaryPipe,
- PgStatusStylePipe
+ PgStatusStylePipe,
+ HealthPieComponent
],
providers: [DashboardService]
})
--- /dev/null
+<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>
--- /dev/null
+@import '../../../../styles/chart-tooltip.scss';
--- /dev/null
+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();
+ });
+});
--- /dev/null
+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]);
+ }
+}
</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>
</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>
<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>
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();
})
);
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({
})
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);
}
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',
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;
}
}