import { Component, AfterViewInit, Input, ViewChild, OnDestroy, ElementRef, } from '@angular/core'; import { MediaObserver } from '@angular/flex-layout'; import { NgForm } from '@angular/forms'; import { Router } from '@angular/router'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { TranslateService } from '@ngx-translate/core'; import { UUID } from 'angular2-uuid'; import { Chart, ChartData, ChartDataSets, ChartOptions, ChartTooltipItem, InteractionMode, } from 'chart.js'; import * as d3 from 'd3'; import { Subject } from 'rxjs'; import { ThemeUtils } from 'app/core/classes/theme-utils/theme-utils'; import { ViewChartBarComponent } from 'app/core/components/view-chart-bar/view-chart-bar.component'; import { GaugeConfig, ViewChartGaugeComponent } from 'app/core/components/view-chart-gauge/view-chart-gauge.component'; import { CoreEvent } from 'app/interfaces/events'; import { CpuStatsEvent } from 'app/interfaces/events/cpu-stats-event.interface'; import { SysInfoEvent } from 'app/interfaces/events/sys-info-event.interface'; import { AllCpusUpdate } from 'app/interfaces/reporting.interface'; import { WidgetComponent } from 'app/pages/dashboard/components/widget/widget.component'; import { WidgetCpuData } from 'app/pages/dashboard/interfaces/widget-data.interface'; import { Theme } from 'app/services/theme/theme.service'; import { T } from 'app/translate-marker'; @UntilDestroy() @Component({ selector: 'widget-cpu', templateUrl: './widget-cpu.component.html', styleUrls: ['./widget-cpu.component.scss'], }) export class WidgetCpuComponent extends WidgetComponent implements AfterViewInit, OnDestroy { @ViewChild('load', { static: true }) cpuLoad: ViewChartGaugeComponent; @ViewChild('cores', { static: true }) cpuCores: ViewChartBarComponent; @Input() data: Subject<CoreEvent>; @Input() cpuModel: string; chart: any;// Chart.js instance with per core data ctx: CanvasRenderingContext2D; // canvas context for chart.js private _cpuData: WidgetCpuData; get cpuData(): WidgetCpuData { return this._cpuData; } set cpuData(value) { this._cpuData = value; } cpuAvg: GaugeConfig; title: string = T('CPU'); subtitle: string = T('% of all cores'); configurable = false; chartId = UUID.UUID(); coreCount: number; threadCount: number; hyperthread: boolean; legendData: ChartDataSets[]; screenType = 'Desktop'; // Desktop || Mobile // Mobile Stats tempAvailable = false; tempMax: number; tempMaxThreads: number[] = []; tempMin: number; tempMinThreads: number[] = []; usageMax: number; usageMaxThreads: number[] = []; usageMin: number; usageMinThreads: number[] = []; legendColors: string[]; legendIndex: number; labels: string[] = []; protected currentTheme: Theme; private utils: ThemeUtils; constructor( router: Router, public translate: TranslateService, public mediaObserver: MediaObserver, private el: ElementRef<HTMLElement>, ) { super(translate); this.utils = new ThemeUtils(); mediaObserver.media$.pipe(untilDestroyed(this)).subscribe((evt) => { const size = { width: evt.mqAlias == 'xs' ? 320 : 536, height: 140, }; const st = evt.mqAlias == 'xs' ? 'Mobile' : 'Desktop'; if (this.chart && this.screenType !== st) { this.chart.resize(size); } this.screenType = st; }); // Fetch CPU core count from SysInfo cache this.core.register({ observerClass: this, eventName: 'SysInfo', }).pipe(untilDestroyed(this)).subscribe((evt: SysInfoEvent) => { this.threadCount = evt.data.cores; this.coreCount = evt.data.physical_cores; this.hyperthread = this.threadCount !== this.coreCount; }); this.core.emit({ name: 'SysInfoRequest', sender: this, }); } ngOnDestroy(): void { this.core.unregister({ observerClass: this }); } ngAfterViewInit(): void { this.core.register({ observerClass: this, eventName: 'ThemeChanged', }).pipe(untilDestroyed(this)).subscribe(() => { d3.select('#grad1 .begin') .style('stop-color', this.getHighlightColor(0)); d3.select('#grad1 .end') .style('stop-color', this.getHighlightColor(0.15)); }); this.data.pipe(untilDestroyed(this)).subscribe((evt: CoreEvent) => { if (evt.name !== 'CpuStats') { return; } const cpuData = (evt as CpuStatsEvent).data; if (!cpuData.average) { return; } this.setCpuLoadData(['Load', parseInt(cpuData.average.usage.toFixed(1))]); this.setCpuData(cpuData); }); } parseCpuData(cpuData: AllCpusUpdate): (string | number)[][] { this.tempAvailable = Boolean(cpuData.temperature && Object.keys(cpuData.temperature).length > 0); const usageColumn: (string | number)[] = ['Usage']; let temperatureColumn: string[] = ['Temperature']; const temperatureValues = []; // Filter out stats per thread const keys = Object.keys(cpuData); const threads = keys.filter((n) => !Number.isNaN(parseFloat(n))); for (let i = 0; i < this.threadCount; i++) { usageColumn.push(parseInt(cpuData[i].usage.toFixed(1))); const mod = threads.length % 2; const temperatureIndex = this.hyperthread ? Math.floor(i / 2 - mod) : i; if (cpuData.temperature && cpuData.temperature[temperatureIndex] && !cpuData.temperature_celsius) { const temperatureAsCelsius = (cpuData.temperature[temperatureIndex] / 10 - 273.05).toFixed(0); temperatureValues.push(parseInt(temperatureAsCelsius)); } else if (cpuData.temperature_celsius && cpuData.temperature_celsius[temperatureIndex]) { temperatureValues.push(cpuData.temperature_celsius[temperatureIndex].toFixed(0)); } } temperatureColumn = temperatureColumn.concat(temperatureValues); this.setMobileStats(Object.assign([], usageColumn), Object.assign([], temperatureColumn)); return [usageColumn, temperatureColumn]; } setMobileStats(usage: number[], temps: number[]): void { // Usage usage.splice(0, 1); this.usageMin = Number(Math.min(...usage).toFixed(0)); this.usageMax = Number(Math.max(...usage).toFixed(0)); this.usageMinThreads = []; this.usageMaxThreads = []; for (let u = 0; u < usage.length; u++) { if (usage[u] == this.usageMin) { this.usageMinThreads.push(Number(u.toFixed(0))); } if (usage[u] == this.usageMax) { this.usageMaxThreads.push(Number(u.toFixed(0))); } } // Temperature temps.splice(0, 1); this.tempMin = Number(Math.min(...temps).toFixed(0)); this.tempMax = Number(Math.max(...temps).toFixed(0)); this.tempMinThreads = []; this.tempMaxThreads = []; for (let t = 0; t < temps.length; t++) { if (temps[t] == this.tempMin) { this.tempMinThreads.push(Number(t.toFixed(0))); } if (temps[t] == this.tempMax) { this.tempMaxThreads.push(Number(t.toFixed(0))); } } } setCpuData(cpuData: AllCpusUpdate): void { const config = { title: this.translate.instant('Cores'), orientation: 'horizontal', max: 100, data: this.parseCpuData(cpuData), }; this.cpuData = config; this.coresChartInit(); } setCpuLoadData(data: (string | number)[]): void { const config = { data, units: '%', diameter: 136, fontSize: 28, max: 100, subtitle: 'Avg Usage', } as GaugeConfig; this.cpuAvg = config; } setPreferences(form: NgForm): void { const filtered: string[] = []; for (const i in form.value) { if (form.value[i]) { filtered.push(i); } } } // chart.js renderer renderChart(): void { if (!this.ctx) { const el: HTMLCanvasElement = this.el.nativeElement.querySelector('#cpu-cores-chart canvas'); if (!el) { return; } const ds = this.makeDatasets(this.cpuData.data); this.ctx = el.getContext('2d'); const data = { labels: this.labels, datasets: ds, }; const options: ChartOptions = { events: ['mousemove', 'mouseout'], onHover: (e: MouseEvent) => { if (e.type == 'mouseout') { this.legendData = null; this.legendIndex = null; } }, tooltips: { enabled: false, mode: 'nearest' as InteractionMode, intersect: true, callbacks: { label: (tt: ChartTooltipItem, data: ChartData) => { if (this.screenType.toLowerCase() == 'mobile') { this.legendData = null; this.legendIndex = null; return; } this.legendData = data.datasets; this.legendIndex = tt.index; return ''; }, }, custom: () => {}, }, responsive: true, maintainAspectRatio: false, legend: { display: false, }, responsiveAnimationDuration: 0, animation: { duration: 1000, animateRotate: true, animateScale: true, }, hover: { animationDuration: 0, }, scales: { xAxes: [{ maxBarThickness: 16, type: 'category', labels: this.labels, } as any], yAxes: [{ ticks: { max: 100, beginAtZero: true, }, }], }, }; this.chart = new Chart(this.ctx, { type: 'bar', data, options, }); } else { const ds = this.makeDatasets(this.cpuData.data); this.chart.data.datasets[0].data = ds[0].data; this.chart.data.datasets[1].data = ds[1].data; this.chart.update(); } } coresChartInit(): void { this.currentTheme = this.themeService.currentTheme(); this.renderChart(); } coresChartUpdate(): void { this.chart.load({ columns: this.cpuData.data, }); } protected makeDatasets(data: (string | number)[][]): ChartDataSets[] { const datasets: ChartDataSets[] = []; const labels: string[] = []; for (let i = 0; i < this.threadCount; i++) { labels.push((i).toString()); } this.labels = labels; // Create the data... data.forEach((item, index) => { const ds: ChartDataSets = { label: item[0] as any, data: data[index].slice(1) as any, backgroundColor: '', borderColor: '', borderWidth: 1, }; const accent = this.themeService.isDefaultTheme ? 'orange' : 'accent'; let color; if (accent !== 'accent' && ds.label == 'Temperature') { color = accent; } else { const cssVar = ds.label == 'Temperature' ? accent : 'primary'; color = this.stripVar(this.currentTheme[cssVar]); } const bgRGB = this.utils.convertToRGB((this.currentTheme as any)[color]).rgb; ds.backgroundColor = this.rgbToString(bgRGB as any, 0.85); ds.borderColor = this.rgbToString(bgRGB as any); datasets.push(ds); }); return datasets; } private processThemeColors(theme: Theme): string[] { return theme.accentColors.map((color) => (theme as any)[color]); } rgbToString(rgb: string[], alpha?: number): string { const a = alpha ? alpha.toString() : '1'; return 'rgba(' + rgb.join(',') + ',' + a + ')'; } stripVar(str: string): string { return str.replace('var(--', '').replace(')', ''); } getHighlightColor(opacity: number): string { // Get highlight color const currentTheme = this.themeService.currentTheme(); const txtColor = currentTheme.fg2; const valueType = this.utils.getValueType(txtColor); // convert to rgb const rgb = valueType == 'hex' ? this.utils.hexToRGB(txtColor).rgb : this.utils.rgbToArray(txtColor); // return rgba const rgba = 'rgba(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ',' + opacity + ')'; return rgba; } }