Gauge: Additional Metric #3

Merged
rozetko merged 6 commits from gauge-additional-metric into main 8 months ago
  1. 133
      src/components/Panel.tsx
  2. 38
      src/models/options/gaugeOptions.ts
  3. 69
      src/module.ts
  4. 9
      src/types.ts
  5. 31
      src/utils.ts

133
src/components/Panel.tsx

@ -4,55 +4,123 @@ import { Series } from '../models/series';
import { BarSeries } from '../models/barSeries'; import { BarSeries } from '../models/barSeries';
import { PanelOptions, Pod } from '../types'; import { PanelOptions, Pod } from '../types';
import { formatValue, getGrafanaSeriesList, getLastMetricValue } from '../utils';
import { DataProcessor } from '../grafana/data_processor';
import { ChartwerkGaugePod } from '@chartwerk/gauge-pod'; import { ChartwerkGaugePod } from '@chartwerk/gauge-pod';
import { ChartwerkBarPod } from '@chartwerk/bar-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 { VizLegend } from '@grafana/ui';
import { LegendDisplayMode } from '@grafana/schema'; import { LegendDisplayMode } from '@grafana/schema';
import React, { useCallback, useRef } from 'react'; import React, { useRef, useEffect, useMemo } from 'react';
import { css } from 'emotion'; import { css } from 'emotion';
import * as _ from 'lodash'; import * as _ from 'lodash';
interface Props extends PanelProps<PanelOptions> {} interface Props extends PanelProps<PanelOptions> {}
export function Panel({ options, data, width, height, timeRange, onChangeTimeRange, replaceVariables }: Props) { export function Panel({ options, data, width, height, timeRange, onChangeTimeRange, replaceVariables }: Props) {
const grafanaSeriesList = getGrafanaSeriesList(data, timeRange); const grafanaSeriesList = useMemo(() => getGrafanaSeriesList(data, timeRange), [data, timeRange]);
let chartContainer = useRef(null); const podContainerRef = useRef<HTMLDivElement>(null);
const podContainer = useMemo(() => {
const chartClickHandler = (event: React.MouseEvent<HTMLDivElement>) => {
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;
}
return (
<div
ref={podContainerRef}
className={css`
width: ${width}px;
height: ${containerHeight}px;
cursor: ${isLinkActive ? 'pointer' : 'default'};
`}
onClick={chartClickHandler}
></div>
);
}, [width, height, options, replaceVariables]);
// we request animation frame here because we need an existing DOM-element at the moment we render the pod useEffect(() => {
window.requestAnimationFrame(() => {
let pod; let pod;
if (podContainerRef.current === null) {
return;
}
switch (options.visualizationType) { switch (options.visualizationType) {
case Pod.GAUGE: case Pod.GAUGE:
const series = new Series(grafanaSeriesList, options.gauge.value).getChartwerkSeries(); const series = new Series(grafanaSeriesList, options.gauge.value).getChartwerkSeries();
const chartwerkGaugeOptions = new GaugeOptions(grafanaSeriesList, options).getChartwerkOptions(); const chartwerkGaugeOptions = new GaugeOptions(grafanaSeriesList, options).getChartwerkOptions();
pod = new ChartwerkGaugePod((chartContainer as any).current, series, chartwerkGaugeOptions); pod = new ChartwerkGaugePod(podContainerRef.current, series, chartwerkGaugeOptions);
break; break;
case Pod.BAR: case Pod.BAR:
const barSeries = new BarSeries(grafanaSeriesList).getChartwerkSeries(); const barSeries = new BarSeries(grafanaSeriesList).getChartwerkSeries();
const chartwerkBarOptions = new BarOptions(grafanaSeriesList, onChangeTimeRange).getChartwerkOptions(); const chartwerkBarOptions = new BarOptions(grafanaSeriesList, onChangeTimeRange).getChartwerkOptions();
pod = new ChartwerkBarPod((chartContainer as any).current, barSeries, chartwerkBarOptions); pod = new ChartwerkBarPod(podContainerRef.current, barSeries, chartwerkBarOptions);
break; break;
default: default:
throw new Error(`Unknown visualization type: ${options.visualizationType}`); console.warn(`Unknown visualization type: ${options.visualizationType}`);
return;
} }
pod.render(); pod.render();
}); }, [podContainer, grafanaSeriesList, onChangeTimeRange, options]);
const isLinkActive = !_.isEmpty(options.gauge.link); switch (options.visualizationType) {
const chartClickHandler = useCallback((event: React.MouseEvent<HTMLDivElement>) => { case Pod.GAUGE:
event.preventDefault(); let additionalInfo;
if (!isLinkActive) { const additionalInfoConfig = options.gauge.additionalInfo;
return; 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;
} }
const link = replaceVariables(options.gauge.link as string); }
window.open(link, '_self');
}, [options.gauge.link]); additionalInfo = (
<div
className={css`
width: ${width}px;
height: ${additionalInfoConfig.size + 8}px;
font-size: ${additionalInfoConfig.size}px;
color: ${additionalInfoConfig.color};
display: flex;
justify-content: center;
align-items: center;
`}
>
{additionalInfoConfig.prefix} {value !== undefined ? formatValue(value, additionalInfoConfig) : '-'}
</div>
);
}
return (
<div>
{podContainer}
{additionalInfo && additionalInfo}
</div>
);
case Pod.BAR:
const legendItems = _.map(grafanaSeriesList, (serie) => { const legendItems = _.map(grafanaSeriesList, (serie) => {
return { return {
label: serie.alias, label: serie.alias,
@ -60,29 +128,16 @@ export function Panel({ options, data, width, height, timeRange, onChangeTimeRan
yAxis: 1, yAxis: 1,
}; };
}); });
const chartHeight = options.visualizationType !== Pod.GAUGE ? height - 20 : height;
return ( return (
<div> <div>
<div {podContainer}
ref={chartContainer}
className={css`
width: ${width}px;
height: ${chartHeight}px;
cursor: ${isLinkActive ? 'pointer' : 'default'};
`}
onClick={chartClickHandler}
></div>
{options.visualizationType !== Pod.GAUGE && (
<VizLegend placement={'bottom'} items={legendItems} displayMode={LegendDisplayMode.List} /> <VizLegend placement={'bottom'} items={legendItems} displayMode={LegendDisplayMode.List} />
)}
</div> </div>
); );
default:
console.warn(`Unknown visualization type: ${options.visualizationType}`);
return <div>This visualization is not supported</div>;
} }
function getGrafanaSeriesList(grafanaData: PanelData, timeRange: TimeRange): any[] {
const processor = new DataProcessor({});
return processor.getSeriesList({
dataList: grafanaData.series,
range: timeRange,
});
} }

38
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 { formatValue, getLastMetricValue } from '../../utils';
import { getValueFormat } from '@grafana/data';
import _ from 'lodash'; import _ from 'lodash';
@ -28,7 +26,7 @@ export class GaugeOptions {
this.minValue = this.grafanaOptions.gauge.min.value; this.minValue = this.grafanaOptions.gauge.min.value;
return; 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; this.minValue = aggregatedValue ? aggregatedValue : undefined;
} }
@ -40,7 +38,7 @@ export class GaugeOptions {
this.maxValue = this.grafanaOptions.gauge.max.value; this.maxValue = this.grafanaOptions.gauge.max.value;
return; 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; this.maxValue = aggregatedValue ? aggregatedValue : undefined;
} }
@ -55,7 +53,7 @@ export class GaugeOptions {
private _setThreshold(threshold: Threshold, idx: number): void { private _setThreshold(threshold: Threshold, idx: number): void {
const value = threshold.useMetric const value = threshold.useMetric
? this.getLastValueFromMetrics(threshold.metricName, `Threshold ${idx + 1}`) ? getLastMetricValue(this.grafanaSeriesList, threshold.metricName, `Threshold ${idx + 1}`)
: threshold.value; : threshold.value;
if (value === null || value === undefined) { if (value === null || value === undefined) {
// TODO: may be throw an error // 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 { private _setIcons(): void {
if (_.isEmpty(this.grafanaOptions.gauge.icons)) { if (_.isEmpty(this.grafanaOptions.gauge.icons)) {
return; return;
@ -100,12 +92,16 @@ export class GaugeOptions {
// check each condition and return false if something goes wrong // check each condition and return false if something goes wrong
for (let [conditionIdx, metric] of icon.metrics.entries()) { 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) { if (value === null || value === undefined) {
// TODO: may be throw an error // TODO: may be throw an error
return false; return false;
} }
if (!this.checkIconCondition(value, icon.values[conditionIdx], icon.conditions[conditionIdx])) { if (!this._checkIconCondition(value, icon.values[conditionIdx], icon.conditions[conditionIdx])) {
return false; return false;
} }
} }
@ -113,7 +109,7 @@ export class GaugeOptions {
return true; 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) { if (inputValue === undefined || inputValue === null) {
return true; return true;
} }
@ -151,7 +147,7 @@ export class GaugeOptions {
return { return {
maxValue: this.maxValue, maxValue: this.maxValue,
minValue: this.minValue, minValue: this.minValue,
valueFormatter: (val: number) => this._valueFormatter(val), valueFormatter: (val: number) => formatValue(val, this.grafanaOptions.gauge),
defaultColor: this.grafanaOptions.gauge.thresholds.defaultColor, defaultColor: this.grafanaOptions.gauge.thresholds.defaultColor,
valueArcBackgroundColor: this.grafanaOptions.gauge.thresholds.arcBackground, valueArcBackgroundColor: this.grafanaOptions.gauge.thresholds.arcBackground,
reversed: this.grafanaOptions.gauge.reversed, reversed: this.grafanaOptions.gauge.reversed,
@ -160,12 +156,4 @@ export class GaugeOptions {
icons: this.icons, 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);
}
} }

69
src/module.ts

@ -23,14 +23,14 @@ export const plugin = new PanelPlugin<PanelOptions>(Panel).setPanelOptions((buil
label: 'Gauge', label: 'Gauge',
value: Pod.GAUGE, value: Pod.GAUGE,
}, },
{
label: 'Line',
value: Pod.LINE,
},
{ {
label: 'Bar', label: 'Bar',
value: Pod.BAR, value: Pod.BAR,
}, },
{
label: 'Line',
value: Pod.LINE,
},
], ],
}, },
}) })
@ -42,13 +42,72 @@ export const plugin = new PanelPlugin<PanelOptions>(Panel).setPanelOptions((buil
showIf: (config) => config.visualizationType !== Pod.GAUGE, showIf: (config) => config.visualizationType !== Pod.GAUGE,
editor: NotSupportedText as any, editor: NotSupportedText as any,
}) })
.addFieldNamePicker({ .addFieldNamePicker({
name: 'Metric', name: 'Metric',
path: 'gauge.value.metricName', path: 'gauge.value.metricName',
category: ['Value'], category: ['Value'],
showIf: (config) => config.visualizationType === Pod.GAUGE, 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({ .addCustomEditor({
id: 'min', id: 'min',
name: 'Min', name: 'Min',

9
src/types.ts

@ -15,6 +15,15 @@ export interface PanelOptions {
decimals?: number; decimals?: number;
unit?: string; unit?: string;
link?: string; link?: string;
additionalInfo: {
display: boolean;
value: ExtremumOptions;
size: number;
color: number;
prefix: string;
unit: string;
decimals: number;
};
}; };
} }

31
src/utils.ts

@ -1,7 +1,38 @@
import { Aggregation } from './types'; 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'; 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[] { export function filterMetricListByAlias(list: any[], alias: string | undefined, option: string): any[] {
const filteredSeries = _.filter(list, (serie) => serie.alias === alias); const filteredSeries = _.filter(list, (serie) => serie.alias === alias);
if (filteredSeries.length === 0) { if (filteredSeries.length === 0) {

Loading…
Cancel
Save