Translate CustomVariableEditor + improve JSON validation
This commit is contained in:
+1
-4
@@ -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({
|
||||
|
||||
+9
-63
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
+59
-3
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user