Timeseries: More nuanced editing of linear threshold to avoid crashes (#112301)

* Timeseries: More nuanced editing of linear threshold to avoid crashes

* remove useEffect import

* remove unused import

* update error and invalid field handling to call out error case
This commit is contained in:
Paul Marbach
2025-11-20 18:49:46 +00:00
committed by GitHub
parent 0b020c5e30
commit 6f9c867660
21 changed files with 168 additions and 45 deletions
@@ -0,0 +1,33 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ScaleDistribution } from '@grafana/schema';
import { ScaleDistributionEditor } from './axis';
describe('ScaleDistributionEditor', () => {
describe('Symlog', () => {
it('linear threshold should not dispatch a change for 0', async () => {
const onChange = jest.fn();
const origValue = { type: ScaleDistribution.Symlog, log: 10 };
render(<ScaleDistributionEditor value={{ type: ScaleDistribution.Symlog, log: 10 }} onChange={onChange} />);
// so annoying that this doesn't work.
// const el = await screen.findByLabelText('Linear threshold');
const el = screen.getByTestId('input-wrapper').querySelector('input')!;
await userEvent.type(el, '0');
expect(onChange).not.toHaveBeenCalled();
await userEvent.type(el, '.');
expect(onChange).not.toHaveBeenCalled();
await userEvent.type(el, '5');
expect(onChange).toHaveBeenCalledWith({ linearThreshold: 0.5, ...origValue });
await userEvent.clear(el);
expect(onChange).toHaveBeenCalledWith(origValue);
});
});
});
@@ -1,3 +1,5 @@
import { useState } from 'react';
import {
FieldConfigEditorBuilder,
FieldType,
@@ -126,12 +128,31 @@ const LOG_DISTRIBUTION_OPTIONS: Array<SelectableValue<number>> = [
},
];
const isValidLinearThreshold = (value: number): string | undefined => {
if (Number.isNaN(value)) {
return t('grafana-ui.axis-builder.linear-threshold.warning.nan', 'Linear threshold must be a number');
}
if (value === 0) {
return t('grafana-ui.axis-builder.linear-threshold.warning.zero', 'Linear threshold cannot be zero');
}
return;
};
/**
* @internal
*/
export const ScaleDistributionEditor = ({ value, onChange }: StandardEditorProps<ScaleDistributionConfig>) => {
export const ScaleDistributionEditor = ({
value,
onChange,
}: Pick<StandardEditorProps<ScaleDistributionConfig>, 'value' | 'onChange'>) => {
const type = value?.type ?? ScaleDistribution.Linear;
const log = value?.log ?? 2;
const [localLinearThreshold, setLocalLinearThreshold] = useState<string>(
value?.linearThreshold != null ? String(value.linearThreshold) : ''
);
const [linearThresholdWarning, setLinearThresholdWarning] = useState<string | undefined>();
const DISTRIBUTION_OPTIONS: Array<SelectableValue<ScaleDistribution>> = [
{
label: t('grafana-ui.builder.axis.scale-distribution-editor.distribution-options.label-linear', 'Linear'),
@@ -161,7 +182,7 @@ export const ScaleDistributionEditor = ({ value, onChange }: StandardEditorProps
}}
/>
{(type === ScaleDistribution.Log || type === ScaleDistribution.Symlog) && (
<Field label={t('grafana-ui.axis-builder.log-base', 'Log base')}>
<Field label={t('grafana-ui.axis-builder.log-base', 'Log base')} noMargin>
<Select
options={LOG_DISTRIBUTION_OPTIONS}
value={log}
@@ -175,16 +196,43 @@ export const ScaleDistributionEditor = ({ value, onChange }: StandardEditorProps
</Field>
)}
{type === ScaleDistribution.Symlog && (
<Field label={t('grafana-ui.axis-builder.linear-threshold', 'Linear threshold')} style={{ marginBottom: 0 }}>
// ADD error and invalid to field when needed
<Field
label={t('grafana-ui.axis-builder.linear-threshold.label', 'Linear threshold')}
invalid={!!linearThresholdWarning}
error={linearThresholdWarning}
style={{ marginBottom: 0 }}
noMargin
>
<Input
// eslint-disable-next-line @grafana/i18n/no-untranslated-strings
placeholder="1"
value={value?.linearThreshold}
value={localLinearThreshold}
invalid={!!linearThresholdWarning}
type="number"
onBlur={(ev) => {
if (ev.currentTarget.value) {
setLinearThresholdWarning(isValidLinearThreshold(Number(ev.currentTarget.value)));
}
}}
onChange={(v) => {
onChange({
...value,
linearThreshold: Number(v.currentTarget.value),
});
setLocalLinearThreshold(v.currentTarget.value);
if (v.currentTarget.value === '') {
const newValue = { ...value };
delete newValue.linearThreshold;
onChange(newValue);
setLinearThresholdWarning(undefined);
return;
}
const asNumber = Number(v.currentTarget.value);
if (isValidLinearThreshold(asNumber) == null) {
setLinearThresholdWarning(undefined);
onChange({
...value,
linearThreshold: asNumber,
});
}
}}
/>
</Field>
+4 -2
View File
@@ -8661,7 +8661,9 @@
"saving": "Ukládání <1></1>"
},
"axis-builder": {
"linear-threshold": "Lineární práh",
"linear-threshold": {
"label": "Lineární práh"
},
"log-base": "Základ protokolu"
},
"builder": {
@@ -14694,4 +14696,4 @@
"label-points": "Body"
}
}
}
}
+4 -2
View File
@@ -8589,7 +8589,9 @@
"saving": "<1></1> wird gespeichert"
},
"axis-builder": {
"linear-threshold": "Linearer Schwellenwert",
"linear-threshold": {
"label": "Linearer Schwellenwert"
},
"log-base": "Logarithmische Basis"
},
"builder": {
@@ -14580,4 +14582,4 @@
"label-points": "Punkte"
}
}
}
}
+7 -1
View File
@@ -8589,7 +8589,13 @@
"saving": "Saving <1></1>"
},
"axis-builder": {
"linear-threshold": "Linear threshold",
"linear-threshold": {
"label": "Linear threshold",
"warning": {
"nan": "Linear threshold must be a number",
"zero": "Linear threshold cannot be zero"
}
},
"log-base": "Log base"
},
"builder": {
+4 -2
View File
@@ -8589,7 +8589,9 @@
"saving": "Guardando <1></1>"
},
"axis-builder": {
"linear-threshold": "Umbral lineal",
"linear-threshold": {
"label": "Umbral lineal"
},
"log-base": "Base de registro"
},
"builder": {
@@ -14580,4 +14582,4 @@
"label-points": "Puntos"
}
}
}
}
+4 -2
View File
@@ -8589,7 +8589,9 @@
"saving": "Enregistrement en cours <1></1>"
},
"axis-builder": {
"linear-threshold": "Seuil linéaire",
"linear-threshold": {
"label": "Seuil linéaire"
},
"log-base": "Base du journal"
},
"builder": {
@@ -14580,4 +14582,4 @@
"label-points": "Points"
}
}
}
}
+4 -2
View File
@@ -8589,7 +8589,9 @@
"saving": "Mentés: <1></1>"
},
"axis-builder": {
"linear-threshold": "Lineáris küszöbérték",
"linear-threshold": {
"label": "Lineáris küszöbérték"
},
"log-base": "Naplóbázis"
},
"builder": {
@@ -14580,4 +14582,4 @@
"label-points": "Pontok"
}
}
}
}
+4 -2
View File
@@ -8553,7 +8553,9 @@
"saving": "Menyimpan <1></1>"
},
"axis-builder": {
"linear-threshold": "Ambang linear",
"linear-threshold": {
"label": "Ambang linear"
},
"log-base": "Basis log"
},
"builder": {
@@ -14523,4 +14525,4 @@
"label-points": "Poin"
}
}
}
}
+4 -2
View File
@@ -8589,7 +8589,9 @@
"saving": "Salvataggio in corso di <1></1>"
},
"axis-builder": {
"linear-threshold": "Soglia lineare",
"linear-threshold": {
"label": "Soglia lineare"
},
"log-base": "Base logaritmica"
},
"builder": {
@@ -14580,4 +14582,4 @@
"label-points": "Punti"
}
}
}
}
+4 -2
View File
@@ -8553,7 +8553,9 @@
"saving": "<1></1>を保存しています"
},
"axis-builder": {
"linear-threshold": "線形のしきい値",
"linear-threshold": {
"label": "線形のしきい値"
},
"log-base": "ログベース"
},
"builder": {
@@ -14523,4 +14525,4 @@
"label-points": "ポイント"
}
}
}
}
+4 -2
View File
@@ -8553,7 +8553,9 @@
"saving": "<1></1> 저장 중"
},
"axis-builder": {
"linear-threshold": "선형 임계값",
"linear-threshold": {
"label": "선형 임계값"
},
"log-base": "로그 베이스"
},
"builder": {
@@ -14523,4 +14525,4 @@
"label-points": "포인트"
}
}
}
}
+4 -2
View File
@@ -8589,7 +8589,9 @@
"saving": "Opslaan <1></1>"
},
"axis-builder": {
"linear-threshold": "Lineaire drempel",
"linear-threshold": {
"label": "Lineaire drempel"
},
"log-base": "Logboekbase"
},
"builder": {
@@ -14580,4 +14582,4 @@
"label-points": "Punten"
}
}
}
}
+4 -2
View File
@@ -8661,7 +8661,9 @@
"saving": "Zapisywanie <1></1>"
},
"axis-builder": {
"linear-threshold": "Próg liniowy",
"linear-threshold": {
"label": "Próg liniowy"
},
"log-base": "Baza logów"
},
"builder": {
@@ -14694,4 +14696,4 @@
"label-points": "Punkty"
}
}
}
}
+4 -2
View File
@@ -8589,7 +8589,9 @@
"saving": "Salvando <1></1>"
},
"axis-builder": {
"linear-threshold": "Limite linear",
"linear-threshold": {
"label": "Limite linear"
},
"log-base": "Base de log"
},
"builder": {
@@ -14580,4 +14582,4 @@
"label-points": "Pontos"
}
}
}
}
+4 -2
View File
@@ -8589,7 +8589,9 @@
"saving": "A guardar <1></1>"
},
"axis-builder": {
"linear-threshold": "Limite linear",
"linear-threshold": {
"label": "Limite linear"
},
"log-base": "Base de registo"
},
"builder": {
@@ -14580,4 +14582,4 @@
"label-points": "Pontos"
}
}
}
}
+4 -2
View File
@@ -8661,7 +8661,9 @@
"saving": "Сохранение <1></1>"
},
"axis-builder": {
"linear-threshold": "Линейный порог",
"linear-threshold": {
"label": "Линейный порог"
},
"log-base": "База журналов"
},
"builder": {
@@ -14694,4 +14696,4 @@
"label-points": "Точки"
}
}
}
}
+4 -2
View File
@@ -8589,7 +8589,9 @@
"saving": "Sparar <1></1>"
},
"axis-builder": {
"linear-threshold": "Lineär tröskel",
"linear-threshold": {
"label": "Lineär tröskel"
},
"log-base": "Loggbas"
},
"builder": {
@@ -14580,4 +14582,4 @@
"label-points": "Poäng"
}
}
}
}
+4 -2
View File
@@ -8589,7 +8589,9 @@
"saving": "<1></1> kaydediliyor"
},
"axis-builder": {
"linear-threshold": "Doğrusal eşik",
"linear-threshold": {
"label": "Doğrusal eşik"
},
"log-base": "Günlük tabanı"
},
"builder": {
@@ -14580,4 +14582,4 @@
"label-points": "Noktalar"
}
}
}
}
+4 -2
View File
@@ -8553,7 +8553,9 @@
"saving": "正在保存<1></1>"
},
"axis-builder": {
"linear-threshold": "线性阈值",
"linear-threshold": {
"label": "线性阈值"
},
"log-base": "记录基础"
},
"builder": {
@@ -14523,4 +14525,4 @@
"label-points": "点"
}
}
}
}
+4 -2
View File
@@ -8553,7 +8553,9 @@
"saving": "正在儲存<1></1>"
},
"axis-builder": {
"linear-threshold": "線性臨界值",
"linear-threshold": {
"label": "線性臨界值"
},
"log-base": "日誌基礎"
},
"builder": {
@@ -14523,4 +14525,4 @@
"label-points": "點"
}
}
}
}