diff --git a/apps/dashboard/kinds/v2beta1/dashboard_spec.cue b/apps/dashboard/kinds/v2beta1/dashboard_spec.cue
index bfc23ad87cb..36c98d8f89e 100644
--- a/apps/dashboard/kinds/v2beta1/dashboard_spec.cue
+++ b/apps/dashboard/kinds/v2beta1/dashboard_spec.cue
@@ -113,7 +113,7 @@ DashboardLink: {
placement?: DashboardLinkPlacement
}
-// Dashboard Link placement. Defines where the link should be displayed.
+// Dashboard Link placement. Defines where the link should be displayed.
// - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu
DashboardLinkPlacement: "inControlsMenu"
@@ -376,7 +376,7 @@ FetchOptions: {
url: string
body?: string
// These are 2D arrays of strings, each representing a key-value pair
- // We are defining them this way because we can't generate a go struct that
+ // We are defining them this way because we can't generate a go struct that
// that would have exactly two strings in each sub-array
queryParams?: [...[...string]]
headers?: [...[...string]]
@@ -386,7 +386,7 @@ InfinityOptions: FetchOptions & {
datasourceUid: string
}
-HttpRequestMethod: "GET" | "PUT" | "POST" | "DELETE" | "PATCH"
+HttpRequestMethod: "GET" | "PUT" | "POST" | "DELETE" | "PATCH"
// Action variable type
ActionVariableType: "string"
@@ -903,6 +903,7 @@ CustomVariableSpec: {
skipUrlSync: bool | *false
description?: string
allowCustomValue: bool | *true
+ valuesFormat?: string
}
// Custom variable kind
diff --git a/packages/grafana-data/src/types/templateVars.ts b/packages/grafana-data/src/types/templateVars.ts
index e6feea4dd3f..f4e3f2ccd44 100644
--- a/packages/grafana-data/src/types/templateVars.ts
+++ b/packages/grafana-data/src/types/templateVars.ts
@@ -101,6 +101,7 @@ export interface IntervalVariableModel extends VariableWithOptions {
export interface CustomVariableModel extends VariableWithMultiSupport {
type: 'custom';
+ valuesFormat?: 'csv' | 'json';
}
export interface DataSourceVariableModel extends VariableWithMultiSupport {
diff --git a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts
index 01fe0927619..64fe4c49293 100644
--- a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts
+++ b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts
@@ -111,6 +111,7 @@ export function sceneVariablesSetToVariables(set: SceneVariables, keepQueryOptio
allValue: variable.state.allValue,
includeAll: variable.state.includeAll,
allowCustomValue: variable.state.allowCustomValue,
+ valuesFormat: variable.state.valuesFormat,
});
} else if (sceneUtils.isDataSourceVariable(variable)) {
variables.push({
@@ -393,6 +394,7 @@ export function sceneVariablesSetToSchemaV2Variables(
allValue: variable.state.allValue,
includeAll: variable.state.includeAll ?? false,
allowCustomValue: variable.state.allowCustomValue ?? true,
+ valuesFormat: variable.state.valuesFormat,
},
};
variables.push(customVariable);
diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts
index 2a2f96caa30..d536fdd1875 100644
--- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts
+++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts
@@ -321,12 +321,12 @@ function createSceneVariableFromVariableModel(variable: TypedVariableModelV2): S
supportsMultiValueOperators: Boolean(getDataSourceSrv().getInstanceSettings(ds)?.meta.multiValueFilterOperators),
});
}
+
if (variable.kind === defaultCustomVariableKind().kind) {
return new CustomVariable({
...commonProperties,
value: variable.spec.current?.value ?? '',
text: variable.spec.current?.text ?? '',
-
query: variable.spec.query,
isMulti: variable.spec.multi,
allValue: variable.spec.allValue || undefined,
@@ -334,6 +334,10 @@ 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,
});
} else if (variable.kind === defaultQueryVariableKind().kind) {
return new QueryVariable({
diff --git a/public/app/features/dashboard-scene/settings/variables/VariableEditorForm.tsx b/public/app/features/dashboard-scene/settings/variables/VariableEditorForm.tsx
index a261cd00852..765710f37aa 100644
--- a/public/app/features/dashboard-scene/settings/variables/VariableEditorForm.tsx
+++ b/public/app/features/dashboard-scene/settings/variables/VariableEditorForm.tsx
@@ -9,7 +9,7 @@ import { Trans, t } from '@grafana/i18n';
import { reportInteraction } from '@grafana/runtime';
import { SceneVariable } from '@grafana/scenes';
import { VariableHide, defaultVariableModel } from '@grafana/schema';
-import { Button, LoadingPlaceholder, ConfirmModal, ModalsController, Stack, useStyles2 } from '@grafana/ui';
+import { Button, ConfirmModal, LoadingPlaceholder, ModalsController, Stack, useStyles2 } from '@grafana/ui';
import { VariableHideSelect } from 'app/features/dashboard-scene/settings/variables/components/VariableHideSelect';
import { VariableLegend } from 'app/features/dashboard-scene/settings/variables/components/VariableLegend';
import { VariableTextAreaField } from 'app/features/dashboard-scene/settings/variables/components/VariableTextAreaField';
@@ -123,7 +123,7 @@ export function VariableEditorForm({ variable, onTypeChange, onGoBack, onDelete
{EditorToRender && }
- {isHasVariableOptions && }
+ {isHasVariableOptions && }
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 b3c78330156..8cbf2c847ae 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,10 @@
-import { FormEvent } from 'react';
+import { isObject } from 'lodash';
+import { FormEvent, useState } from 'react';
+import { CustomVariableModel, shallowCompare } 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';
import { SelectionOptionsForm } from './SelectionOptionsForm';
import { VariableLegend } from './VariableLegend';
@@ -9,6 +12,7 @@ import { VariableTextAreaField } from './VariableTextAreaField';
interface CustomVariableFormProps {
query: string;
+ valuesFormat?: CustomVariableModel['valuesFormat'];
multi: boolean;
allValue?: string | null;
includeAll: boolean;
@@ -20,10 +24,12 @@ interface CustomVariableFormProps {
onQueryBlur?: (event: FormEvent) => void;
onAllValueBlur?: (event: FormEvent) => void;
onAllowCustomValueChange?: (event: FormEvent) => void;
+ onValuesFormatChange?: (format: CustomVariableModel['valuesFormat']) => void;
}
export function CustomVariableForm({
query,
+ valuesFormat,
multi,
allValue,
includeAll,
@@ -33,23 +39,70 @@ export function CustomVariableForm({
onIncludeAllChange,
onAllValueChange,
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 (
<>
Custom options
+
+
+ {valuesFormat === 'json' && (
+
+
+
+ )}
+
+
+ {validationError && {validationError.message}}
+
Selection options
@@ -58,6 +111,8 @@ export function CustomVariableForm({
includeAll={includeAll}
allValue={allValue}
allowCustomValue={allowCustomValue}
+ disableAllowCustomValue={valuesFormat === 'json'}
+ disableCustomAllValue={valuesFormat === 'json'}
onMultiChange={onMultiChange}
onIncludeAllChange={onIncludeAllChange}
onAllValueChange={onAllValueChange}
@@ -66,3 +121,57 @@ 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{' '}
+
+ our docs
+ {' '}
+ for more information.
+
+ );
+}
+
+const validateJsonQuery = (rawQuwey: string): Error | undefined => {
+ const query = rawQuwey.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/components/QueryVariableForm.tsx b/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.tsx
index d6d4d467c9a..837a1877330 100644
--- a/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.tsx
+++ b/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.tsx
@@ -1,4 +1,4 @@
-import { FormEvent } from 'react';
+import { FormEvent, useState } from 'react';
import { useAsync } from 'react-use';
import { DataSourceInstanceSettings, SelectableValue, TimeRange } from '@grafana/data';
@@ -7,7 +7,7 @@ import { Trans, t } from '@grafana/i18n';
import { getDataSourceSrv } from '@grafana/runtime';
import { QueryVariable } from '@grafana/scenes';
import { DataSourceRef, VariableRefresh, VariableSort } from '@grafana/schema';
-import { Field, TextLink } from '@grafana/ui';
+import { Box, Field, Switch, TextLink } from '@grafana/ui';
import { QueryEditor } from 'app/features/dashboard-scene/settings/variables/components/QueryEditor';
import { SelectionOptionsForm } from 'app/features/dashboard-scene/settings/variables/components/SelectionOptionsForm';
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
@@ -15,9 +15,9 @@ import { getVariableQueryEditor } from 'app/features/variables/editor/getVariabl
import { QueryVariableRefreshSelect } from 'app/features/variables/query/QueryVariableRefreshSelect';
import { QueryVariableSortSelect } from 'app/features/variables/query/QueryVariableSortSelect';
import {
+ QueryVariableStaticOptions,
StaticOptionsOrderType,
StaticOptionsType,
- QueryVariableStaticOptions,
} from 'app/features/variables/query/QueryVariableStaticOptions';
import { VariableLegend } from './VariableLegend';
@@ -94,6 +94,16 @@ export function QueryVariableEditorForm({
const { datasource, VariableQueryEditor } = dsConfig ?? {};
+ // TODO: remove me after finished testing - each DS can/should implement their own UI
+ const [returnsMultiProps, setReturnsMultiProps] = useState(false);
+ const onChangeReturnsMultipleProps = (e: FormEvent) => {
+ setReturnsMultiProps(e.currentTarget.checked);
+ onAllowCustomValueChange?.({ currentTarget: { checked: false } });
+ onAllValueChange({ currentTarget: { value: '' } });
+ onRegExChange({ currentTarget: { value: '' } });
+ onStaticOptionsChange?.([]);
+ };
+
return (
<>
@@ -102,48 +112,70 @@ export function QueryVariableEditorForm({
{datasource && VariableQueryEditor && (
-
+
+
+
+ Check{' '}
+
+ our docs
+ {' '}
+ for more information.
+
+ }
+ noMargin
+ >
+
+
+
)}
-
-
- Optional, if you want to extract part of a series name or metric node segment.
-
-
-
- Named capture groups can be used to separate the display text and value (
-
- see examples
-
- ).
-
-
- }
- // eslint-disable-next-line @grafana/i18n/no-untranslated-strings
- placeholder="/.*-(?.*)-(?.*)-.*/"
- onBlur={onRegExChange}
- testId={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2}
- width={52}
- />
+ {!returnsMultiProps && (
+
+
+ Optional, if you want to extract part of a series name or metric node segment.
+
+
+
+ Named capture groups can be used to separate the display text and value (
+
+ see examples
+
+ ).
+
+
+ }
+ // eslint-disable-next-line @grafana/i18n/no-untranslated-strings
+ placeholder="/.*-(?.*)-(?.*)-.*/"
+ onBlur={onRegExChange}
+ testId={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2}
+ width={52}
+ />
+ )}
- {onStaticOptionsChange && onStaticOptionsOrderChange && (
+ {!returnsMultiProps && onStaticOptionsChange && onStaticOptionsOrderChange && (
) => void;
onAllowCustomValueChange?: (event: ChangeEvent) => void;
onIncludeAllChange: (event: ChangeEvent) => void;
@@ -20,8 +22,10 @@ interface SelectionOptionsFormProps {
export function SelectionOptionsForm({
multi,
allowCustomValue,
+ disableAllowCustomValue,
includeAll,
allValue,
+ disableCustomAllValue,
onMultiChange,
onAllowCustomValueChange,
onIncludeAllChange,
@@ -39,18 +43,19 @@ export function SelectionOptionsForm({
onChange={onMultiChange}
testId={selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsMultiSwitch}
/>
- {onAllowCustomValueChange && ( // backwards compat with old arch, remove on cleanup
-
- )}
+ {!disableAllowCustomValue &&
+ onAllowCustomValueChange && ( // backwards compat with old arch, remove on cleanup
+
+ )}
- {includeAll && (
+ {!disableCustomAllValue && includeAll && (
{
+export const VariableValuesPreview = ({ variable }: Props) => {
+ const options = variable.getOptionsForSelect(false);
+ if (!options.length) {
+ return null;
+ }
+
+ if ('valuesFormat' in variable.state && variable.state.valuesFormat === 'json') {
+ return ;
+ }
+
+ return ;
+};
+VariableValuesPreview.displayName = 'VariableValuesPreview';
+
+function VariableValuesWithPropsPreview({ options }: { options: VariableValueOption[] }) {
+ const styles = useStyles2(getStyles);
+ const data = options.map((o) => ({ label: String(o.label), value: String(o.value), ...o.properties }));
+ const columns = Object.keys(data[0]).map((id) => ({ id, header: id, sortType: 'alphanumeric' as const }));
+
+ return (
+
+
+ Preview of values
+
+ String(r.value)} pageSize={2} />
+
+ );
+}
+
+function VariableValuesWithoutPropsPreview({ options }: { options: VariableValueOption[] }) {
+ const styles = useStyles2(getStyles);
const [previewLimit, setPreviewLimit] = useState(20);
const [previewOptions, setPreviewOptions] = useState([]);
const showMoreOptions = useCallback(
@@ -21,15 +51,10 @@ export const VariableValuesPreview = ({ options }: VariableValuesPreviewProps) =
},
[previewLimit, setPreviewLimit]
);
- const styles = useStyles2(getStyles);
useEffect(() => setPreviewOptions(options.slice(0, previewLimit)), [previewLimit, options]);
- if (!previewOptions.length) {
- return null;
- }
-
return (
-
+
Preview of values
@@ -51,12 +76,12 @@ export const VariableValuesPreview = ({ options }: VariableValuesPreviewProps) =
)}
);
-};
-VariableValuesPreview.displayName = 'VariableValuesPreview';
+}
+VariableValuesWithoutPropsPreview.displayName = 'VariableValuesWithoutPropsPreview';
function getStyles(theme: GrafanaTheme2) {
return {
- wrapper: css({
+ previewContainer: css({
display: 'flex',
flexDirection: 'column',
marginTop: theme.spacing(2),
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 8dfc7b3eac1..59f1f8ac8c5 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,5 +1,6 @@
-import { FormEvent, useCallback } from 'react';
+import { FormEvent, useCallback, useState } from 'react';
+import { CustomVariableModel } from '@grafana/data';
import { t } from '@grafana/i18n';
import { CustomVariable, SceneVariable } from '@grafana/scenes';
@@ -14,7 +15,23 @@ interface CustomVariableEditorProps {
}
export function CustomVariableEditor({ variable, onRunQuery }: CustomVariableEditorProps) {
- const { query, isMulti, allValue, includeAll, allowCustomValue } = variable.useState();
+ const { query, valuesFormat, isMulti, allValue, includeAll, allowCustomValue } = variable.useState();
+
+ const [prevQuery, setPrevQuery] = useState('');
+ const onValuesFormatChange = useCallback(
+ (format: CustomVariableModel['valuesFormat']) => {
+ variable.setState({ valuesFormat: format });
+ variable.setState({ allowCustomValue: false });
+ variable.setState({ allValue: undefined });
+
+ variable.setState({ query: prevQuery });
+ if (query !== prevQuery) {
+ setPrevQuery(query);
+ }
+ onRunQuery();
+ },
+ [onRunQuery, prevQuery, query, variable]
+ );
const onMultiChange = useCallback(
(event: FormEvent
) => {
@@ -55,6 +72,7 @@ export function CustomVariableEditor({ variable, onRunQuery }: CustomVariableEdi
return (
);
}
diff --git a/public/app/features/dashboard-scene/utils/clone.ts b/public/app/features/dashboard-scene/utils/clone.ts
index 9923357bf5d..ceee21b56a3 100644
--- a/public/app/features/dashboard-scene/utils/clone.ts
+++ b/public/app/features/dashboard-scene/utils/clone.ts
@@ -43,6 +43,7 @@ export function getLocalVariableValueSet(
name: variable.state.name,
value,
text,
+ properties: variable.state.options.find((o) => o.value === value)?.properties,
isMulti: variable.state.isMulti,
includeAll: variable.state.includeAll,
}),