mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Heatmap: Fix ability to define bucket size as an interval string, like 30s (#95923)
* validate with durations * update docs * Add default values to calculation, show error if too many bins * move default generation to separate function * Update docs/sources/panels-visualizations/visualizations/heatmap/index.md Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> * dont try to parse ‘’ as a duration, move max to variable * Add new function to support duration and ms, only calculate if valid * Add radix * Remove validation and precalc to determine bucket quantity * simplify * simplify more * less * cleanup transformationsVariableSupport. reset value to auto on mode changes * maybe... * by hook or by crook * Change function name back --------- Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
This commit is contained in:
parent
88621d6fa0
commit
53052def52
@ -80,7 +80,7 @@ This setting determines if the data is already a calculated heatmap (from the da
|
||||
|
||||
### X Bucket
|
||||
|
||||
This setting determines how the X-axis is split into buckets. You can specify a time interval in the **Size** input. For example, a time range of `1h` makes the cells 1-hour wide on the X-axis.
|
||||
This setting determines how the X-axis is split into buckets. You can specify a time interval in the **Size** input. For example, a time range of `1h` makes the cells 1-hour wide on the X-axis. If the value is a number only, the duration is in milliseconds.
|
||||
|
||||
### Y Bucket
|
||||
|
||||
|
@ -1,12 +1,13 @@
|
||||
import { useState } from 'react';
|
||||
import { useRef, useState } from 'react';
|
||||
|
||||
import { SelectableValue, StandardEditorProps, VariableOrigin } from '@grafana/data';
|
||||
import { getTemplateSrv, config as cfg } from '@grafana/runtime';
|
||||
import { getTemplateSrv } from '@grafana/runtime';
|
||||
import { HeatmapCalculationBucketConfig, HeatmapCalculationMode } from '@grafana/schema';
|
||||
import { HorizontalGroup, Input, RadioButtonGroup, ScaleDistribution } from '@grafana/ui';
|
||||
import { HorizontalGroup, RadioButtonGroup, ScaleDistribution } from '@grafana/ui';
|
||||
|
||||
import { SuggestionsInput } from '../../suggestionsInput/SuggestionsInput';
|
||||
import { numberOrVariableValidator } from '../../utils';
|
||||
import { convertDurationToMilliseconds } from '../utils';
|
||||
|
||||
const modeOptions: Array<SelectableValue<HeatmapCalculationMode>> = [
|
||||
{
|
||||
@ -32,12 +33,26 @@ const logModeOptions: Array<SelectableValue<HeatmapCalculationMode>> = [
|
||||
export const AxisEditor = ({ value, onChange, item }: StandardEditorProps<HeatmapCalculationBucketConfig>) => {
|
||||
const [isInvalid, setInvalid] = useState<boolean>(false);
|
||||
|
||||
const onValueChange = (bucketValue: string) => {
|
||||
setInvalid(!numberOrVariableValidator(bucketValue));
|
||||
onChange({
|
||||
...value,
|
||||
value: bucketValue,
|
||||
});
|
||||
const modeSwitchCounter = useRef(0);
|
||||
|
||||
const allowInterval = item.settings?.allowInterval ?? false;
|
||||
|
||||
const onValueChange = ({ mode, scale, value = '' }: HeatmapCalculationBucketConfig) => {
|
||||
let isValid = true;
|
||||
|
||||
if (mode !== HeatmapCalculationMode.Count) {
|
||||
if (!allowInterval) {
|
||||
isValid = numberOrVariableValidator(value);
|
||||
} else if (value !== '') {
|
||||
let durationMS = convertDurationToMilliseconds(value);
|
||||
if (durationMS === undefined) {
|
||||
isValid = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setInvalid(!isValid);
|
||||
onChange({ mode, scale, value });
|
||||
};
|
||||
|
||||
const templateSrv = getTemplateSrv();
|
||||
@ -51,33 +66,28 @@ export const AxisEditor = ({ value, onChange, item }: StandardEditorProps<Heatma
|
||||
value={value?.mode || HeatmapCalculationMode.Size}
|
||||
options={value?.scale?.type === ScaleDistribution.Log ? logModeOptions : modeOptions}
|
||||
onChange={(mode) => {
|
||||
onChange({
|
||||
modeSwitchCounter.current++;
|
||||
|
||||
onValueChange({
|
||||
...value,
|
||||
value: '',
|
||||
mode,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{cfg.featureToggles.transformationsVariableSupport ? (
|
||||
<SuggestionsInput
|
||||
invalid={isInvalid}
|
||||
error={'Value needs to be an integer or a variable'}
|
||||
value={value?.value ?? ''}
|
||||
placeholder="Auto"
|
||||
onChange={onValueChange}
|
||||
suggestions={variables}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
value={value?.value ?? ''}
|
||||
placeholder="Auto"
|
||||
onChange={(v) => {
|
||||
onChange({
|
||||
...value,
|
||||
value: v.currentTarget.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<SuggestionsInput
|
||||
// we need this cause the value prop is not changeable after init
|
||||
// so we have to re-create the component during mode switches to reset the value to auto
|
||||
key={modeSwitchCounter.current}
|
||||
invalid={isInvalid}
|
||||
error={'Value needs to be an integer or a variable'}
|
||||
value={value?.value ?? ''}
|
||||
placeholder="Auto"
|
||||
onChange={(text) => {
|
||||
onValueChange({ ...value, value: text });
|
||||
}}
|
||||
suggestions={variables}
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
);
|
||||
};
|
||||
|
@ -19,6 +19,9 @@ export function addHeatmapCalculationOptions(
|
||||
defaultValue: {
|
||||
mode: HeatmapCalculationMode.Size,
|
||||
},
|
||||
settings: {
|
||||
allowInterval: true,
|
||||
},
|
||||
});
|
||||
|
||||
builder.addCustomEditor({
|
||||
|
@ -12,8 +12,6 @@ import {
|
||||
Field,
|
||||
getValueFormat,
|
||||
formattedValueToString,
|
||||
durationToMilliseconds,
|
||||
parseDuration,
|
||||
TransformationApplicabilityLevels,
|
||||
TimeRange,
|
||||
} from '@grafana/data';
|
||||
@ -26,7 +24,7 @@ import {
|
||||
HeatmapCalculationOptions,
|
||||
} from '@grafana/schema';
|
||||
|
||||
import { niceLinearIncrs, niceTimeIncrs } from './utils';
|
||||
import { convertDurationToMilliseconds, niceLinearIncrs, niceTimeIncrs } from './utils';
|
||||
|
||||
export interface HeatmapTransformerOptions extends HeatmapCalculationOptions {
|
||||
/** the raw values will still exist in results after transformation */
|
||||
@ -329,7 +327,7 @@ export function calculateHeatmapFromData(
|
||||
xMode: xBucketsCfg.mode,
|
||||
xSize:
|
||||
xBucketsCfg.mode === HeatmapCalculationMode.Size
|
||||
? durationToMilliseconds(parseDuration(xBucketsCfg.value ?? ''))
|
||||
? convertDurationToMilliseconds(xBucketsCfg.value ?? '')
|
||||
: xBucketsCfg.value
|
||||
? +xBucketsCfg.value
|
||||
: undefined,
|
||||
|
@ -0,0 +1,21 @@
|
||||
import { convertDurationToMilliseconds } from './utils';
|
||||
|
||||
describe('Heatmap utils', () => {
|
||||
const cases: Array<[string, number | undefined]> = [
|
||||
['1', 1],
|
||||
['6', 6],
|
||||
['2.3', 2],
|
||||
['1ms', 1],
|
||||
['5MS', 5],
|
||||
['1s', 1000],
|
||||
['1.5s', undefined],
|
||||
['1.2345s', undefined],
|
||||
['one', undefined],
|
||||
['20sec', undefined],
|
||||
['', undefined],
|
||||
];
|
||||
|
||||
test.each(cases)('convertToMilliseconds can correctly convert "%s"', (input, output) => {
|
||||
expect(convertDurationToMilliseconds(input)).toEqual(output);
|
||||
});
|
||||
});
|
@ -1,4 +1,6 @@
|
||||
import { guessDecimals, roundDecimals } from '@grafana/data';
|
||||
import { durationToMilliseconds, guessDecimals, isValidDuration, parseDuration, roundDecimals } from '@grafana/data';
|
||||
|
||||
import { numberOrVariableValidator } from '../utils';
|
||||
|
||||
const { abs, pow } = Math;
|
||||
|
||||
@ -117,3 +119,26 @@ export const niceTimeIncrs = [
|
||||
9 * year,
|
||||
10 * year,
|
||||
];
|
||||
|
||||
// convert a string to the number of milliseconds. valid inputs are a number, variable, or duration. duration in ms is supported.
|
||||
// value out will always be an integer, as ms is the lowest granularity for heatmaps
|
||||
export const convertDurationToMilliseconds = (duration: string): number | undefined => {
|
||||
const isValidNumberOrVariable = numberOrVariableValidator(duration); // check if number only. if so, equals number of ms
|
||||
if (isValidNumberOrVariable) {
|
||||
const durationMs = Number.parseInt(duration, 10);
|
||||
return Number.isNaN(durationMs) ? undefined : durationMs;
|
||||
} else {
|
||||
const validDuration = isValidDuration(duration); // check if non-ms duration. If so, convert value to number of ms
|
||||
if (validDuration) {
|
||||
return durationToMilliseconds(parseDuration(duration));
|
||||
} else {
|
||||
const match = duration.match(/(\d+)ms$/i);
|
||||
if (match) {
|
||||
const durationMs = Number.parseInt(match[1], 10);
|
||||
return Number.isNaN(durationMs) ? undefined : durationMs;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -14,8 +14,12 @@ import {
|
||||
ValueFormatter,
|
||||
} from '@grafana/data';
|
||||
import { parseSampleValue, sortSeriesByLabel } from '@grafana/prometheus';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { HeatmapCellLayout } from '@grafana/schema';
|
||||
import {
|
||||
HeatmapCalculationMode,
|
||||
HeatmapCalculationOptions,
|
||||
HeatmapCellLayout,
|
||||
ScaleDistribution,
|
||||
} from '@grafana/schema';
|
||||
import {
|
||||
calculateHeatmapFromData,
|
||||
isHeatmapCellsDense,
|
||||
@ -98,36 +102,16 @@ export function prepareHeatmapData({
|
||||
});
|
||||
|
||||
if (options.calculate) {
|
||||
if (config.featureToggles.transformationsVariableSupport) {
|
||||
const optionsCopy = {
|
||||
...options,
|
||||
calculation: {
|
||||
xBuckets: { ...options.calculation?.xBuckets } ?? undefined,
|
||||
yBuckets: { ...options.calculation?.yBuckets } ?? undefined,
|
||||
},
|
||||
};
|
||||
// if calculate is true, we need to have the default values for the calculation if they don't exist
|
||||
let calculation = getCalculationObjectWithDefaults(options.calculation);
|
||||
|
||||
if (optionsCopy.calculation?.xBuckets?.value && replaceVariables !== undefined) {
|
||||
optionsCopy.calculation.xBuckets.value = replaceVariables(optionsCopy.calculation.xBuckets.value);
|
||||
}
|
||||
|
||||
if (optionsCopy.calculation?.yBuckets?.value && replaceVariables !== undefined) {
|
||||
optionsCopy.calculation.yBuckets.value = replaceVariables(optionsCopy.calculation.yBuckets.value);
|
||||
}
|
||||
|
||||
return getDenseHeatmapData(
|
||||
calculateHeatmapFromData(frames, { ...options.calculation, timeRange }),
|
||||
exemplars,
|
||||
optionsCopy,
|
||||
palette,
|
||||
theme
|
||||
);
|
||||
}
|
||||
calculation.xBuckets.value = replaceVariables(calculation.xBuckets.value ?? '');
|
||||
calculation.yBuckets.value = replaceVariables(calculation.yBuckets.value ?? '');
|
||||
|
||||
return getDenseHeatmapData(
|
||||
calculateHeatmapFromData(frames, { ...options.calculation, timeRange }),
|
||||
calculateHeatmapFromData(frames, { ...calculation, timeRange }),
|
||||
exemplars,
|
||||
options,
|
||||
{ ...options, calculation },
|
||||
palette,
|
||||
theme
|
||||
);
|
||||
@ -207,6 +191,23 @@ export function prepareHeatmapData({
|
||||
};
|
||||
}
|
||||
|
||||
const getCalculationObjectWithDefaults = (calculation?: HeatmapCalculationOptions) => {
|
||||
return {
|
||||
xBuckets: {
|
||||
...calculation?.xBuckets,
|
||||
mode: calculation?.xBuckets?.mode ?? HeatmapCalculationMode.Size,
|
||||
},
|
||||
yBuckets: {
|
||||
...calculation?.yBuckets,
|
||||
mode: calculation?.yBuckets?.mode ?? HeatmapCalculationMode.Size,
|
||||
scale: {
|
||||
...calculation?.yBuckets?.scale,
|
||||
type: calculation?.yBuckets?.scale?.type ?? ScaleDistribution.Linear,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const getSparseHeatmapData = (
|
||||
frame: DataFrame,
|
||||
exemplars: DataFrame | undefined,
|
||||
|
Loading…
Reference in New Issue
Block a user