From 3dcd809aaf94e7398f7b193e18358f767773167e Mon Sep 17 00:00:00 2001 From: grafakus Date: Tue, 18 Nov 2025 18:35:49 +0100 Subject: [PATCH] Translate CustomVariableEditor + improve JSON validation --- .../transformSaveModelSchemaV2ToScene.ts | 5 +- .../components/CustomVariableForm.tsx | 72 +++---------------- .../CustomVariableEditor.tsx | 62 +++++++++++++++- public/locales/en-US/grafana.json | 2 + 4 files changed, 71 insertions(+), 70 deletions(-) diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts index d0ec5616de5..e508a5f77ea 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts @@ -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({ diff --git a/public/app/features/dashboard-scene/settings/variables/components/CustomVariableForm.tsx b/public/app/features/dashboard-scene/settings/variables/components/CustomVariableForm.tsx index fb06a020186..84f89df1c6a 100644 --- a/public/app/features/dashboard-scene/settings/variables/components/CustomVariableForm.tsx +++ b/public/app/features/dashboard-scene/settings/variables/components/CustomVariableForm.tsx @@ -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) => void; onMultiChange: (event: FormEvent) => void; onIncludeAllChange: (event: FormEvent) => 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(); - - const onChangeFormat = (newFormat: CustomVariableModel['valuesFormat']) => { - onValuesFormatChange?.(newFormat); - setValidationError(undefined); - }; - - const onQueryBlur = (e: FormEvent) => { - if (valuesFormat === 'json') { - setValidationError(validateJsonQuery(e.currentTarget.value)); - } - onQueryChange(e); - }; - return ( <> @@ -64,7 +51,7 @@ export function CustomVariableForm({ - {validationError && {validationError.message}} + {queryValidationError && {queryValidationError.message}} Selection options @@ -124,8 +110,7 @@ export function CustomVariableForm({ function TooltipJsonFormat() { return ( - // TODO: add translation - + Provide a JSON representing an array of objects, where each object can have any number of properties.
Check{' '} @@ -136,42 +121,3 @@ function TooltipJsonFormat() {
); } - -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; - } -}; diff --git a/public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/CustomVariableEditor.tsx b/public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/CustomVariableEditor.tsx index da97d2a1fe7..6c3774407f1 100644 --- a/public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/CustomVariableEditor.tsx +++ b/public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/CustomVariableEditor.tsx @@ -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(); 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) => { + 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; + } +}; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 2707b681ae4..8f32a853de3 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -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.
Check <4>our docs for more information.", + "name-json-values": "Object values in a JSON array", "name-values-separated-comma": "Values separated by comma", "selection-options": "Selection options" },