From 93973cbc5f91e6c8deeaaa490078f7c0c7a34d2f Mon Sep 17 00:00:00 2001 From: rozetko Date: Fri, 29 Mar 2024 18:13:14 +0300 Subject: [PATCH] gauge: display additional info --- src/components/Panel.tsx | 87 +++++++++++++++++++----------- src/models/options/gaugeOptions.ts | 34 ++++-------- src/module.ts | 69 ++++++++++++++++++++++-- src/types.ts | 9 ++++ src/utils.ts | 32 +++++++++++ 5 files changed, 170 insertions(+), 61 deletions(-) diff --git a/src/components/Panel.tsx b/src/components/Panel.tsx index db5cc29..9a1eb2d 100644 --- a/src/components/Panel.tsx +++ b/src/components/Panel.tsx @@ -4,13 +4,12 @@ import { Series } from '../models/series'; import { BarSeries } from '../models/barSeries'; import { PanelOptions, Pod } from '../types'; - -import { DataProcessor } from '../grafana/data_processor'; +import { formatValue, getGrafanaSeriesList, getLastMetricValue } from '../utils'; import { ChartwerkGaugePod } from '@chartwerk/gauge-pod'; import { ChartwerkBarPod } from '@chartwerk/bar-pod'; -import { PanelData, TimeRange, PanelProps } from '@grafana/data'; +import { PanelProps } from '@grafana/data'; import { VizLegend } from '@grafana/ui'; import { LegendDisplayMode } from '@grafana/schema'; @@ -25,7 +24,6 @@ export function Panel({ options, data, width, height, timeRange, onChangeTimeRan const podContainerRef = useRef(null); const podContainer = useMemo(() => { - const chartHeight = height - 20; const chartClickHandler = (event: React.MouseEvent) => { event.preventDefault(); @@ -37,19 +35,29 @@ export function Panel({ options, data, width, height, timeRange, onChangeTimeRan }; const isLinkActive = options.gauge.link !== undefined && options.gauge.link !== ''; + let containerHeight = height; + if (options.visualizationType === Pod.BAR) { + containerHeight = height - 20; + } + if ( + options.visualizationType === Pod.GAUGE && + options.gauge.additionalInfo.display + ) { + containerHeight = height - options.gauge.additionalInfo.size - 8; + } return (
); - }, [width, height, options.gauge.link, replaceVariables]); + }, [width, height, options, replaceVariables]); useEffect(() => { let pod; @@ -68,26 +76,56 @@ export function Panel({ options, data, width, height, timeRange, onChangeTimeRan pod = new ChartwerkBarPod(podContainerRef.current, barSeries, chartwerkBarOptions); break; default: - throw new Error(`Unknown visualization type: ${options.visualizationType}`); + console.warn(`Unknown visualization type: ${options.visualizationType}`); + return; } pod.render(); }, [podContainer, grafanaSeriesList, onChangeTimeRange, options]); - const legendItems = useMemo( - () => - _.map(grafanaSeriesList, (serie) => { + switch (options.visualizationType) { + case Pod.GAUGE: + let additionalInfo; + const additionalInfoConfig = options.gauge.additionalInfo; + if (additionalInfoConfig.display) { + let value: number | undefined = undefined; + if (!additionalInfoConfig.value?.useMetric) { + value = additionalInfoConfig.value.value; + } else { + if (!_.isEmpty(additionalInfoConfig.value.metricName)) { + const aggregatedValue = getLastMetricValue(grafanaSeriesList, additionalInfoConfig.value.metricName, 'Max'); + value = aggregatedValue !== null ? aggregatedValue : undefined; + } + } + + additionalInfo =
+ {additionalInfoConfig.prefix} {value !== undefined ? formatValue(value, additionalInfoConfig) : '-'} +
+ } + return ( +
+ {podContainer} + {additionalInfo && additionalInfo} +
+ ); + case Pod.BAR: + const legendItems = _.map(grafanaSeriesList, (serie) => { return { label: serie.alias, color: serie.color, yAxis: 1, }; - }), - [grafanaSeriesList] - ); + }); - switch (options.visualizationType) { - case Pod.LINE: - case Pod.BAR: return (
{podContainer} @@ -95,21 +133,8 @@ export function Panel({ options, data, width, height, timeRange, onChangeTimeRan
); - case Pod.GAUGE: - return ( -
- {podContainer} -
- ); default: - throw new Error(`Unknown visualization type: ${options.visualizationType}`); + console.warn(`Unknown visualization type: ${options.visualizationType}`); + return
This visualization is not supported
; } } - -function getGrafanaSeriesList(grafanaData: PanelData, timeRange: TimeRange): any[] { - const processor = new DataProcessor({}); - return processor.getSeriesList({ - dataList: grafanaData.series, - range: timeRange, - }); -} diff --git a/src/models/options/gaugeOptions.ts b/src/models/options/gaugeOptions.ts index 5bc4698..9b7ccd4 100644 --- a/src/models/options/gaugeOptions.ts +++ b/src/models/options/gaugeOptions.ts @@ -1,8 +1,6 @@ -import { PanelOptions, Aggregation, Threshold, Icon, IconPosition, Condition } from 'types'; +import { PanelOptions, Threshold, Icon, IconPosition, Condition } from 'types'; -import { filterMetricListByAlias, getAggregatedValueFromSerie } from '../../utils'; - -import { getValueFormat } from '@grafana/data'; +import { formatValue, getLastMetricValue } from '../../utils'; import _ from 'lodash'; @@ -28,7 +26,7 @@ export class GaugeOptions { this.minValue = this.grafanaOptions.gauge.min.value; return; } - const aggregatedValue = this.getLastValueFromMetrics(this.grafanaOptions.gauge.min.metricName, 'Min'); + const aggregatedValue = getLastMetricValue(this.grafanaSeriesList, this.grafanaOptions.gauge.min.metricName, 'Min'); this.minValue = aggregatedValue ? aggregatedValue : undefined; } @@ -40,7 +38,7 @@ export class GaugeOptions { this.maxValue = this.grafanaOptions.gauge.max.value; return; } - const aggregatedValue = this.getLastValueFromMetrics(this.grafanaOptions.gauge.max.metricName, 'Max'); + const aggregatedValue = getLastMetricValue(this.grafanaSeriesList, this.grafanaOptions.gauge.max.metricName, 'Max'); this.maxValue = aggregatedValue ? aggregatedValue : undefined; } @@ -55,7 +53,7 @@ export class GaugeOptions { private _setThreshold(threshold: Threshold, idx: number): void { const value = threshold.useMetric - ? this.getLastValueFromMetrics(threshold.metricName, `Threshold ${idx + 1}`) + ? getLastMetricValue(this.grafanaSeriesList, threshold.metricName, `Threshold ${idx + 1}`) : threshold.value; if (value === null || value === undefined) { // TODO: may be throw an error @@ -67,12 +65,6 @@ export class GaugeOptions { }); } - private _valueFormatter(value: number): string { - const suffix = getValueFormat(this.grafanaOptions.gauge.unit)(0)?.suffix || ''; - const decimals = _.isNumber(this.grafanaOptions.gauge.decimals) ? this.grafanaOptions.gauge.decimals : 2; - return `${value.toFixed(decimals)} ${suffix}`; - } - private _setIcons(): void { if (_.isEmpty(this.grafanaOptions.gauge.icons)) { return; @@ -100,12 +92,12 @@ export class GaugeOptions { // check each condition and return false if something goes wrong for (let [conditionIdx, metric] of icon.metrics.entries()) { - const value = this.getLastValueFromMetrics(metric, `Icon ${iconIdx + 1}, Condition ${conditionIdx + 1}`); + const value = getLastMetricValue(this.grafanaSeriesList, metric, `Icon ${iconIdx + 1}, Condition ${conditionIdx + 1}`); if (value === null || value === undefined) { // TODO: may be throw an error return false; } - if (!this.checkIconCondition(value, icon.values[conditionIdx], icon.conditions[conditionIdx])) { + if (!this._checkIconCondition(value, icon.values[conditionIdx], icon.conditions[conditionIdx])) { return false; } } @@ -113,7 +105,7 @@ export class GaugeOptions { return true; } - private checkIconCondition(metricValue: number, inputValue: number, condition: Condition): boolean { + private _checkIconCondition(metricValue: number, inputValue: number, condition: Condition): boolean { if (inputValue === undefined || inputValue === null) { return true; } @@ -151,7 +143,7 @@ export class GaugeOptions { return { maxValue: this.maxValue, minValue: this.minValue, - valueFormatter: (val: number) => this._valueFormatter(val), + valueFormatter: (val: number) => formatValue(val, this.grafanaOptions.gauge), defaultColor: this.grafanaOptions.gauge.thresholds.defaultColor, valueArcBackgroundColor: this.grafanaOptions.gauge.thresholds.arcBackground, reversed: this.grafanaOptions.gauge.reversed, @@ -160,12 +152,4 @@ export class GaugeOptions { icons: this.icons, }; } - - getLastValueFromMetrics(metricName: string | undefined, optionName: string): number | null { - // optionName -> helper in Error, mb use option path instead - const filteredSeries = filterMetricListByAlias(this.grafanaSeriesList, metricName, optionName); - const serie = filteredSeries[0]; - // Last value for now - return getAggregatedValueFromSerie(serie, Aggregation.LAST); - } } diff --git a/src/module.ts b/src/module.ts index de92132..d04cb8f 100644 --- a/src/module.ts +++ b/src/module.ts @@ -23,14 +23,14 @@ export const plugin = new PanelPlugin(Panel).setPanelOptions((buil label: 'Gauge', value: Pod.GAUGE, }, - { - label: 'Line', - value: Pod.LINE, - }, { label: 'Bar', value: Pod.BAR, }, + { + label: 'Line', + value: Pod.LINE, + }, ], }, }) @@ -42,13 +42,72 @@ export const plugin = new PanelPlugin(Panel).setPanelOptions((buil showIf: (config) => config.visualizationType !== Pod.GAUGE, editor: NotSupportedText as any, }) - .addFieldNamePicker({ name: 'Metric', path: 'gauge.value.metricName', category: ['Value'], showIf: (config) => config.visualizationType === Pod.GAUGE, }) + + .addBooleanSwitch({ + path: 'gauge.additionalInfo.display', + name: 'Display', + defaultValue: false, + category: ['Additional Info'], + showIf: (config) => config.visualizationType === Pod.GAUGE, + }) + .addCustomEditor({ + id: 'additionalInfo', + name: 'Value', + path: 'gauge.additionalInfo.value', + category: ['Additional Info'], + defaultValue: { useMetric: false, value: 0 }, + showIf: (config) => config.visualizationType === Pod.GAUGE && config.gauge.additionalInfo?.display, + editor: UseMetricEditor as any, + }) + .addSliderInput({ + path: 'gauge.additionalInfo.size', + defaultValue: 20, + name: 'Size (px)', + settings: { + min: 1, + max: 50, + }, + category: ['Additional Info'], + showIf: (config) => config.visualizationType === Pod.GAUGE && config.gauge.additionalInfo?.display, + }) + .addTextInput({ + path: 'gauge.additionalInfo.prefix', + defaultValue: '', + name: 'Prefix', + category: ['Additional Info'], + showIf: (config) => config.visualizationType === Pod.GAUGE && config.gauge.additionalInfo?.display, + }) + .addColorPicker({ + path: 'gauge.additionalInfo.color', + defaultValue: 'white', + name: 'Text Color', + category: ['Additional Info'], + showIf: (config) => config.visualizationType === Pod.GAUGE && config.gauge.additionalInfo?.display, + }) + .addUnitPicker({ + path: 'gauge.additionalInfo.unit', + name: 'Unit', + category: ['Additional Info'], + showIf: (config) => config.visualizationType === Pod.GAUGE && config.gauge.additionalInfo?.display, + }) + .addNumberInput({ + path: 'gauge.additionalInfo.decimals', + name: 'Decimals', + settings: { + placeholder: 'auto', + min: 0, + max: 5, + }, + category: ['Additional Info'], + showIf: (config) => config.visualizationType === Pod.GAUGE && config.gauge.additionalInfo?.display, + }) + .addCustomEditor({ id: 'min', name: 'Min', diff --git a/src/types.ts b/src/types.ts index cf3f960..bdd344b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -15,6 +15,15 @@ export interface PanelOptions { decimals?: number; unit?: string; link?: string; + additionalInfo: { + display: boolean; + value: ExtremumOptions; + size: number; + color: number; + prefix: string; + unit: string; + decimals: number; + } }; } diff --git a/src/utils.ts b/src/utils.ts index 007f8d2..f4d63ef 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,8 +1,40 @@ import { Aggregation } from './types'; +import { DataProcessor } from './grafana/data_processor'; + +import { PanelData, TimeRange, getValueFormat } from '@grafana/data'; +import TimeSeries from 'grafana/app/core/time_series2'; + import * as _ from 'lodash'; +export function formatValue(value: number, options: { unit?: string, decimals?: number }): string { + const suffix = getValueFormat(options.unit)(0)?.suffix || ''; + const decimals = _.isNumber(options.decimals) ? options.decimals : 2; + return `${value.toFixed(decimals)} ${suffix}`; +} + +export function getGrafanaSeriesList(grafanaData: PanelData, timeRange: TimeRange): TimeSeries[] { + const processor = new DataProcessor({}); + return processor.getSeriesList({ + dataList: grafanaData.series, + range: timeRange, + }); +} + +export function getLastMetricValue( + grafanaSeriesList: TimeSeries[], + metricName: string | undefined, + optionName: string, +): number | null { + // optionName -> helper in Error, mb use option path instead + const filteredSeries = filterMetricListByAlias(grafanaSeriesList, metricName, optionName); + const serie = filteredSeries[0]; + // Last value for now + return getAggregatedValueFromSerie(serie, Aggregation.LAST); +} + export function filterMetricListByAlias(list: any[], alias: string | undefined, option: string): any[] { + console.log(list) const filteredSeries = _.filter(list, (serie) => serie.alias === alias); if (filteredSeries.length === 0) { throw new Error(`${option}: Can't find metric for ${alias} name.`);