Translate CustomVariableEditor + improve JSON validation

This commit is contained in:
grafakus
2025-11-18 18:35:49 +01:00
parent 6b7fac65b1
commit 3dcd809aaf
4 changed files with 71 additions and 70 deletions
@@ -335,10 +335,7 @@ function createSceneVariableFromVariableModel(variable: TypedVariableModelV2): S
defaultToAll: Boolean(variable.spec.includeAll),
skipUrlSync: variable.spec.skipUrlSync,
hide: transformVariableHideToEnumV1(variable.spec.hide),
valuesFormat:
variable.spec.valuesFormat === 'csv' || variable.spec.valuesFormat === 'json'
? variable.spec.valuesFormat
: undefined,
valuesFormat: variable.spec.valuesFormat || 'csv',
});
} else if (variable.kind === defaultQueryVariableKind().kind) {
return new QueryVariable({
@@ -1,7 +1,6 @@
import { isObject } from 'lodash';
import { FormEvent, useState } from 'react';
import { FormEvent } from 'react';
import { CustomVariableModel, shallowCompare } from '@grafana/data';
import { CustomVariableModel } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { Trans, t } from '@grafana/i18n';
import { FieldValidationMessage, Icon, RadioButtonGroup, Stack, TextLink, Tooltip } from '@grafana/ui';
@@ -17,6 +16,7 @@ interface CustomVariableFormProps {
allValue?: string | null;
includeAll: boolean;
allowCustomValue?: boolean;
queryValidationError?: Error;
onQueryChange: (event: FormEvent<HTMLTextAreaElement>) => void;
onMultiChange: (event: FormEvent<HTMLInputElement>) => void;
onIncludeAllChange: (event: FormEvent<HTMLInputElement>) => void;
@@ -34,6 +34,7 @@ export function CustomVariableForm({
allValue,
includeAll,
allowCustomValue,
queryValidationError,
onQueryChange,
onMultiChange,
onIncludeAllChange,
@@ -41,20 +42,6 @@ export function CustomVariableForm({
onAllowCustomValueChange,
onValuesFormatChange,
}: CustomVariableFormProps) {
const [validationError, setValidationError] = useState<Error>();
const onChangeFormat = (newFormat: CustomVariableModel['valuesFormat']) => {
onValuesFormatChange?.(newFormat);
setValidationError(undefined);
};
const onQueryBlur = (e: FormEvent<HTMLTextAreaElement>) => {
if (valuesFormat === 'json') {
setValidationError(validateJsonQuery(e.currentTarget.value));
}
onQueryChange(e);
};
return (
<>
<VariableLegend>
@@ -64,7 +51,7 @@ export function CustomVariableForm({
<Stack direction="row" gap={1}>
<RadioButtonGroup
value={valuesFormat}
onChange={onChangeFormat}
onChange={onValuesFormatChange}
options={[
{
value: 'csv',
@@ -72,7 +59,6 @@ export function CustomVariableForm({
},
{
value: 'json',
// TODO: add translation
label: t('dashboard-scene.custom-variable-form.name-json-values', 'Object values in a JSON array'),
},
]}
@@ -91,17 +77,17 @@ export function CustomVariableForm({
placeholder={
valuesFormat === 'json'
? // eslint-disable-next-line @grafana/i18n/no-untranslated-strings
'[{ "text": "text1", "propA": "a1", "propB": "b1" },\n{ "text": "text2", "propA": "a2", "propB": "b2" }]'
'[{ "text":"text1", "value":"val1", "propA":"a1", "propB":"b1" },\n{ "text":"text2", "value":"val2", "propA":"a2", "propB":"b2" }]'
: // eslint-disable-next-line @grafana/i18n/no-untranslated-strings
'1, 10, mykey : myvalue, myvalue, escaped\,value'
}
defaultValue={query}
onBlur={onQueryBlur}
onBlur={onQueryChange}
required
width={52}
testId={selectors.pages.Dashboard.Settings.Variables.Edit.CustomVariable.customValueInput}
/>
{validationError && <FieldValidationMessage>{validationError.message}</FieldValidationMessage>}
{queryValidationError && <FieldValidationMessage>{queryValidationError.message}</FieldValidationMessage>}
<VariableLegend>
<Trans i18nKey="dashboard-scene.custom-variable-form.selection-options">Selection options</Trans>
@@ -124,8 +110,7 @@ export function CustomVariableForm({
function TooltipJsonFormat() {
return (
// TODO: add translation
<Trans i18nKey="">
<Trans i18nKey="dashboard-scene.custom-variable-form.json-values-tooltip">
Provide a JSON representing an array of objects, where each object can have any number of properties.
<br />
Check{' '}
@@ -136,42 +121,3 @@ function TooltipJsonFormat() {
</Trans>
);
}
const validateJsonQuery = (rawQuery: string): Error | undefined => {
const query = rawQuery.trim();
if (!query) {
return;
}
try {
const options = JSON.parse(query);
if (!Array.isArray(options)) {
throw new Error('Invalid JSON array!');
}
if (!options.length) {
return;
}
const keys = Object.keys(options[0]);
if (!keys.length || !keys.some((k) => k === 'value')) {
throw new Error('The objects in the array must have at least a "value" key!');
}
for (let i = 0; i < options.length; i += 1) {
if (!isObject(options[i])) {
throw new Error(`All items in the array must be objects. Item at index=${i} is incorrect!`);
}
if (!shallowCompare(keys, Object.keys(options[i]))) {
throw new Error(`All objects in the array must have the same keys. Object at index=${i} is incorrect!`);
}
}
return;
} catch (error) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return error as Error;
}
};
@@ -1,6 +1,7 @@
import { isObject } from 'lodash';
import { FormEvent, useCallback, useState } from 'react';
import { CustomVariableModel } from '@grafana/data';
import { CustomVariableModel, shallowCompare } from '@grafana/data';
import { t } from '@grafana/i18n';
import { CustomVariable, SceneVariable } from '@grafana/scenes';
@@ -16,6 +17,7 @@ interface CustomVariableEditorProps {
export function CustomVariableEditor({ variable, onRunQuery }: CustomVariableEditorProps) {
const { query, valuesFormat, isMulti, allValue, includeAll, allowCustomValue } = variable.useState();
const [queryValidationError, setQueryValidationError] = useState<Error>();
const [prevQuery, setPrevQuery] = useState('');
const onValuesFormatChange = useCallback(
@@ -26,6 +28,7 @@ export function CustomVariableEditor({ variable, onRunQuery }: CustomVariableEdi
variable.setState({ allValue: undefined });
onRunQuery();
setQueryValidationError(undefined);
if (query !== prevQuery) {
setPrevQuery(query);
}
@@ -49,10 +52,18 @@ export function CustomVariableEditor({ variable, onRunQuery }: CustomVariableEdi
const onQueryChange = useCallback(
(event: FormEvent<HTMLTextAreaElement>) => {
if (valuesFormat === 'json') {
const validationError = validateJsonQuery(event.currentTarget.value);
setQueryValidationError(validationError);
if (validationError) {
return;
}
}
variable.setState({ query: event.currentTarget.value });
onRunQuery();
},
[variable, onRunQuery]
[valuesFormat, variable, onRunQuery]
);
const onAllValueChange = useCallback(
@@ -77,9 +88,10 @@ export function CustomVariableEditor({ variable, onRunQuery }: CustomVariableEdi
allValue={allValue ?? ''}
includeAll={!!includeAll}
allowCustomValue={allowCustomValue}
queryValidationError={queryValidationError}
onQueryChange={onQueryChange}
onMultiChange={onMultiChange}
onIncludeAllChange={onIncludeAllChange}
onQueryChange={onQueryChange}
onAllValueChange={onAllValueChange}
onAllowCustomValueChange={onAllowCustomValueChange}
onValuesFormatChange={onValuesFormatChange}
@@ -100,3 +112,47 @@ export function getCustomVariableOptions(variable: SceneVariable): OptionsPaneIt
}),
];
}
const validateJsonQuery = (rawQuery: string): Error | undefined => {
const query = rawQuery.trim();
if (!query) {
return;
}
try {
const options = JSON.parse(query);
if (!Array.isArray(options)) {
throw new Error('Enter a valid JSON array of objects');
}
if (!options.length) {
return;
}
let errorIndex = options.findIndex((item) => !isObject(item));
if (errorIndex !== -1) {
throw new Error(`All items must be objects. The item at index ${errorIndex} is not an object.`);
}
const keys = Object.keys(options[0]);
if (!keys.includes('value')) {
throw new Error('Each object in the array must include at least a "value" property');
}
if (keys.includes('')) {
throw new Error('Object property names cannot be empty strings');
}
errorIndex = options.findIndex((o) => !shallowCompare(keys, Object.keys(o)));
if (errorIndex !== -1) {
throw new Error(
`All objects must have the same set of properties. The object at index ${errorIndex} does not match the expected properties`
);
}
return;
} catch (error) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return error as Error;
}
};
+2
View File
@@ -5813,6 +5813,8 @@
},
"custom-variable-form": {
"custom-options": "Custom options",
"json-values-tooltip": "Provide a JSON representing an array of objects, where each object can have any number of properties.<br/>Check <4>our docs</4> for more information.",
"name-json-values": "Object values in a JSON array",
"name-values-separated-comma": "Values separated by comma",
"selection-options": "Selection options"
},