diff --git a/src/components/Panel.tsx b/src/components/Panel.tsx index 6cc1e1d..b2b2603 100644 --- a/src/components/Panel.tsx +++ b/src/components/Panel.tsx @@ -4,85 +4,140 @@ 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'; -import React, { useCallback, useRef } from 'react'; +import React, { useRef, useEffect, useMemo } from 'react'; import { css } from 'emotion'; import * as _ from 'lodash'; interface Props extends PanelProps {} export function Panel({ options, data, width, height, timeRange, onChangeTimeRange, replaceVariables }: Props) { - const grafanaSeriesList = getGrafanaSeriesList(data, timeRange); - let chartContainer = useRef(null); + const grafanaSeriesList = useMemo(() => getGrafanaSeriesList(data, timeRange), [data, timeRange]); + const podContainerRef = useRef(null); + + const podContainer = useMemo(() => { + const chartClickHandler = (event: React.MouseEvent) => { + event.preventDefault(); + if (options.gauge.link === undefined || options.gauge.link === '') { + return; + } + const link = replaceVariables(options.gauge.link); + window.open(link, '_self'); + }; + + 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; + } - // we request animation frame here because we need an existing DOM-element at the moment we render the pod - window.requestAnimationFrame(() => { + return ( +
+ ); + }, [width, height, options, replaceVariables]); + + useEffect(() => { let pod; + if (podContainerRef.current === null) { + return; + } switch (options.visualizationType) { case Pod.GAUGE: const series = new Series(grafanaSeriesList, options.gauge.value).getChartwerkSeries(); const chartwerkGaugeOptions = new GaugeOptions(grafanaSeriesList, options).getChartwerkOptions(); - pod = new ChartwerkGaugePod((chartContainer as any).current, series, chartwerkGaugeOptions); + pod = new ChartwerkGaugePod(podContainerRef.current, series, chartwerkGaugeOptions); break; case Pod.BAR: const barSeries = new BarSeries(grafanaSeriesList).getChartwerkSeries(); const chartwerkBarOptions = new BarOptions(grafanaSeriesList, onChangeTimeRange).getChartwerkOptions(); - pod = new ChartwerkBarPod((chartContainer as any).current, barSeries, chartwerkBarOptions); + 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 isLinkActive = !_.isEmpty(options.gauge.link); - const chartClickHandler = useCallback((event: React.MouseEvent) => { - event.preventDefault(); - if (!isLinkActive) { - return; - } - const link = replaceVariables(options.gauge.link as string); - window.open(link, '_self'); - }, [options.gauge.link]); - const legendItems = _.map(grafanaSeriesList, (serie) => { - return { - label: serie.alias, - color: serie.color, - yAxis: 1, - }; - }); - const chartHeight = options.visualizationType !== Pod.GAUGE ? height - 20 : height; - return ( -
-
- {options.visualizationType !== Pod.GAUGE && ( - - )} -
- ); -} + switch (options.visualizationType) { + case Pod.GAUGE: + let additionalInfo; + const additionalInfoConfig = options.gauge.additionalInfo; + if (additionalInfoConfig.display) { + // TODO: move `useMetric` handling to utils, as it's duplicated in gaugeOptions.ts + 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, + 'Additional Info' + ); + 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, + }; + }); + + return ( +
+ {podContainer} -function getGrafanaSeriesList(grafanaData: PanelData, timeRange: TimeRange): any[] { - const processor = new DataProcessor({}); - return processor.getSeriesList({ - dataList: grafanaData.series, - range: timeRange, - }); + +
+ ); + default: + console.warn(`Unknown visualization type: ${options.visualizationType}`); + return
This visualization is not supported
; + } } diff --git a/src/models/options/gaugeOptions.ts b/src/models/options/gaugeOptions.ts index 5bc4698..0049b38 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,16 @@ 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 +109,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 +147,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 +156,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..4ca1d01 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..1a29ba3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,7 +1,38 @@ 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[] { const filteredSeries = _.filter(list, (serie) => serie.alias === alias); if (filteredSeries.length === 0) {