diff --git a/apps/dashboard/kinds/v2beta1/dashboard_spec.cue b/apps/dashboard/kinds/v2beta1/dashboard_spec.cue
index b81175db77d..975a1c0af29 100644
--- a/apps/dashboard/kinds/v2beta1/dashboard_spec.cue
+++ b/apps/dashboard/kinds/v2beta1/dashboard_spec.cue
@@ -911,6 +911,7 @@ CustomVariableSpec: {
skipUrlSync: bool | *false
description?: string
allowCustomValue: bool | *true
+ valuesFormat?: "csv" | "json"
}
// Custom variable kind
diff --git a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue
index b2340d5c22e..1bcd550e339 100644
--- a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue
+++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue
@@ -915,6 +915,7 @@ CustomVariableSpec: {
skipUrlSync: bool | *false
description?: string
allowCustomValue: bool | *true
+ valuesFormat?: "csv" | "json"
}
// Custom variable kind
diff --git a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go
index ffdad0045c6..89ddd5982d5 100644
--- a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go
+++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go
@@ -1675,18 +1675,19 @@ func NewDashboardCustomVariableKind() *DashboardCustomVariableKind {
// Custom variable specification
// +k8s:openapi-gen=true
type DashboardCustomVariableSpec struct {
- Name string `json:"name"`
- Query string `json:"query"`
- Current DashboardVariableOption `json:"current"`
- Options []DashboardVariableOption `json:"options"`
- Multi bool `json:"multi"`
- IncludeAll bool `json:"includeAll"`
- AllValue *string `json:"allValue,omitempty"`
- Label *string `json:"label,omitempty"`
- Hide DashboardVariableHide `json:"hide"`
- SkipUrlSync bool `json:"skipUrlSync"`
- Description *string `json:"description,omitempty"`
- AllowCustomValue bool `json:"allowCustomValue"`
+ Name string `json:"name"`
+ Query string `json:"query"`
+ Current DashboardVariableOption `json:"current"`
+ Options []DashboardVariableOption `json:"options"`
+ Multi bool `json:"multi"`
+ IncludeAll bool `json:"includeAll"`
+ AllValue *string `json:"allValue,omitempty"`
+ Label *string `json:"label,omitempty"`
+ Hide DashboardVariableHide `json:"hide"`
+ SkipUrlSync bool `json:"skipUrlSync"`
+ Description *string `json:"description,omitempty"`
+ AllowCustomValue bool `json:"allowCustomValue"`
+ ValuesFormat *DashboardCustomVariableSpecValuesFormat `json:"valuesFormat,omitempty"`
}
// NewDashboardCustomVariableSpec creates a new DashboardCustomVariableSpec object.
@@ -2101,6 +2102,14 @@ const (
DashboardQueryVariableSpecStaticOptionsOrderSorted DashboardQueryVariableSpecStaticOptionsOrder = "sorted"
)
+// +k8s:openapi-gen=true
+type DashboardCustomVariableSpecValuesFormat string
+
+const (
+ DashboardCustomVariableSpecValuesFormatCsv DashboardCustomVariableSpecValuesFormat = "csv"
+ DashboardCustomVariableSpecValuesFormatJson DashboardCustomVariableSpecValuesFormat = "json"
+)
+
// +k8s:openapi-gen=true
type DashboardPanelKindOrLibraryPanelKind struct {
PanelKind *DashboardPanelKind `json:"PanelKind,omitempty"`
diff --git a/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi.go
index f833f1ddee0..9b0eb14b36b 100644
--- a/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi.go
+++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi.go
@@ -1510,6 +1510,12 @@ func schema_pkg_apis_dashboard_v2beta1_DashboardCustomVariableSpec(ref common.Re
Format: "",
},
},
+ "valuesFormat": {
+ SchemaProps: spec.SchemaProps{
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
},
Required: []string{"name", "query", "current", "options", "multi", "includeAll", "hide", "skipUrlSync", "allowCustomValue"},
},
diff --git a/package.json b/package.json
index 2e6fcba94f2..0059c42439a 100644
--- a/package.json
+++ b/package.json
@@ -296,8 +296,8 @@
"@grafana/plugin-ui": "^0.11.1",
"@grafana/prometheus": "workspace:*",
"@grafana/runtime": "workspace:*",
- "@grafana/scenes": "6.47.1",
- "@grafana/scenes-react": "6.47.1",
+ "@grafana/scenes": "^6.48.0",
+ "@grafana/scenes-react": "^6.48.0",
"@grafana/schema": "workspace:*",
"@grafana/sql": "workspace:*",
"@grafana/ui": "workspace:*",
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/packages/grafana-schema/src/schema/dashboard/v2_examples.ts b/packages/grafana-schema/src/schema/dashboard/v2_examples.ts
index 7c542121a7b..ab730913bc2 100644
--- a/packages/grafana-schema/src/schema/dashboard/v2_examples.ts
+++ b/packages/grafana-schema/src/schema/dashboard/v2_examples.ts
@@ -316,6 +316,7 @@ export const handyTestingSchema: Spec = {
query: 'option1, option2',
skipUrlSync: false,
allowCustomValue: true,
+ valuesFormat: 'csv',
},
},
{
diff --git a/packages/grafana-schema/src/schema/dashboard/v2beta1/types.spec.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2beta1/types.spec.gen.ts
index 95c0dac4230..cdbe87ef687 100644
--- a/packages/grafana-schema/src/schema/dashboard/v2beta1/types.spec.gen.ts
+++ b/packages/grafana-schema/src/schema/dashboard/v2beta1/types.spec.gen.ts
@@ -1335,6 +1335,7 @@ export interface CustomVariableSpec {
skipUrlSync: boolean;
description?: string;
allowCustomValue: boolean;
+ valuesFormat?: "csv" | "json";
}
export const defaultCustomVariableSpec = (): CustomVariableSpec => ({
diff --git a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts
index a5afac1ee04..3095087b558 100644
--- a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts
+++ b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts
@@ -849,6 +849,7 @@ describe('DashboardSceneSerializer', () => {
query: 'app1',
skipUrlSync: false,
allowCustomValue: true,
+ valuesFormat: 'csv',
},
},
]);
diff --git a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap
index f119b3c611c..fb6d2b8d259 100644
--- a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap
+++ b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap
@@ -294,6 +294,7 @@ exports[`Given a scene with custom quick ranges should save quick ranges to save
"options": [],
"query": "a, b, c",
"type": "custom",
+ "valuesFormat": "csv",
},
{
"current": {
@@ -679,6 +680,7 @@ exports[`transformSceneToSaveModel Given a scene with rows Should transform back
"options": [],
"query": "A,B,C,D,E,F,E,G,H,I,J,K,L",
"type": "custom",
+ "valuesFormat": "csv",
},
{
"current": {
@@ -697,6 +699,7 @@ exports[`transformSceneToSaveModel Given a scene with rows Should transform back
"options": [],
"query": "Bob : 1, Rob : 2,Sod : 3, Hod : 4, Cod : 5",
"type": "custom",
+ "valuesFormat": "csv",
},
],
},
@@ -1019,6 +1022,7 @@ exports[`transformSceneToSaveModel Given a simple scene with custom settings Sho
"options": [],
"query": "a, b, c",
"type": "custom",
+ "valuesFormat": "csv",
},
{
"current": {
@@ -1378,6 +1382,7 @@ exports[`transformSceneToSaveModel Given a simple scene with variables Should tr
"options": [],
"query": "a, b, c",
"type": "custom",
+ "valuesFormat": "csv",
},
{
"current": {
diff --git a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap
index add2123cdad..499cabcff77 100644
--- a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap
+++ b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap
@@ -197,6 +197,7 @@ exports[`transformSceneToSaveModelSchemaV2 should transform scene to save model
"options": [],
"query": "option1, option2",
"skipUrlSync": false,
+ "valuesFormat": "csv",
},
},
{
diff --git a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts
index cc49cfadc77..e2b1420e19b 100644
--- a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts
+++ b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts
@@ -374,6 +374,7 @@ describe('sceneVariablesSetToVariables', () => {
"options": [],
"query": "test,test1,test2",
"type": "custom",
+ "valuesFormat": "csv",
}
`);
});
@@ -1148,6 +1149,7 @@ describe('sceneVariablesSetToVariables', () => {
"options": [],
"query": "test,test1,test2",
"skipUrlSync": false,
+ "valuesFormat": "csv",
},
}
`);
diff --git a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts
index fa7c2cc3855..30ca7fdafb8 100644
--- a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts
+++ b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts
@@ -110,6 +110,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({
@@ -392,6 +393,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 683f22c16fb..83cb357acc5 100644
--- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts
+++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts
@@ -335,12 +335,12 @@ function createSceneVariableFromVariableModel(variable: TypedVariableModelV2): S
),
});
}
+
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,
@@ -348,6 +348,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',
});
} 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..53d5ee64915 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';
@@ -68,6 +68,9 @@ export function VariableEditorForm({ variable, onTypeChange, onGoBack, onDelete
const onHideChange = (hide: VariableHide) => variable.setState({ hide });
const isHasVariableOptions = hasVariableOptions(variable);
+ const optionsForSelect = isHasVariableOptions ? variable.getOptionsForSelect(false) : [];
+ const hasJsonValuesFormat = 'valuesFormat' in variable.state && variable.state.valuesFormat === 'json';
+ const hasMultiProps = hasJsonValuesFormat || optionsForSelect.every((o) => Boolean(o.properties));
const onDeleteVariable = (hideModal: () => void) => () => {
reportInteraction('Delete variable');
@@ -123,7 +126,7 @@ export function VariableEditorForm({ variable, onTypeChange, onGoBack, onDelete
{EditorToRender && }
- {isHasVariableOptions && }
+ {isHasVariableOptions && }
diff --git a/public/app/features/dashboard-scene/settings/variables/components/CustomVariableForm.test.tsx b/public/app/features/dashboard-scene/settings/variables/components/CustomVariableForm.test.tsx
index 924f9fa5702..34c06aea3ed 100644
--- a/public/app/features/dashboard-scene/settings/variables/components/CustomVariableForm.test.tsx
+++ b/public/app/features/dashboard-scene/settings/variables/components/CustomVariableForm.test.tsx
@@ -1,4 +1,5 @@
-import { render, fireEvent } from '@testing-library/react';
+import { fireEvent, render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
import { selectors } from '@grafana/e2e-selectors';
@@ -130,4 +131,71 @@ describe('CustomVariableForm', () => {
expect(onMultiChange).not.toHaveBeenCalled();
expect(onIncludeAllChange).not.toHaveBeenCalled();
});
+
+ describe('JSON values format', () => {
+ test('should render the form fields correctly', async () => {
+ const { getByTestId, queryByTestId } = render(
+
+ );
+
+ await userEvent.click(screen.getByText('Object values in a JSON array'));
+
+ const multiCheckbox = getByTestId(
+ selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsMultiSwitch
+ );
+ const allowCustomValueCheckbox = queryByTestId(
+ selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsAllowCustomValueSwitch
+ );
+ const includeAllCheckbox = getByTestId(
+ selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsIncludeAllSwitch
+ );
+ const allValueInput = queryByTestId(
+ selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput
+ );
+
+ expect(multiCheckbox).toBeInTheDocument();
+ expect(multiCheckbox).toBeChecked();
+ expect(includeAllCheckbox).toBeInTheDocument();
+ expect(includeAllCheckbox).toBeChecked();
+
+ expect(allowCustomValueCheckbox).not.toBeInTheDocument();
+ expect(allValueInput).not.toBeInTheDocument();
+ });
+
+ test('should display validation error', async () => {
+ const validationError = new Error('Ooops! Validation error.');
+
+ const { findByText } = render(
+
+ );
+
+ await userEvent.click(screen.getByText('Object values in a JSON array'));
+
+ const errorEl = await findByText(validationError.message);
+ expect(errorEl).toBeInTheDocument();
+ });
+ });
});
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..b4ea09d190a 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,9 @@
import { FormEvent } from 'react';
+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';
import { SelectionOptionsForm } from './SelectionOptionsForm';
import { VariableLegend } from './VariableLegend';
@@ -9,10 +11,12 @@ import { VariableTextAreaField } from './VariableTextAreaField';
interface CustomVariableFormProps {
query: string;
+ valuesFormat?: CustomVariableModel['valuesFormat'];
multi: boolean;
allValue?: string | null;
includeAll: boolean;
allowCustomValue?: boolean;
+ queryValidationError?: Error;
onQueryChange: (event: FormEvent) => void;
onMultiChange: (event: FormEvent) => void;
onIncludeAllChange: (event: FormEvent) => void;
@@ -20,19 +24,23 @@ 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,
allowCustomValue,
+ queryValidationError,
onQueryChange,
onMultiChange,
onIncludeAllChange,
onAllValueChange,
onAllowCustomValueChange,
+ onValuesFormatChange,
}: CustomVariableFormProps) {
return (
<>
@@ -40,16 +48,27 @@ export function CustomVariableForm({
Custom options
+
+
+ {queryValidationError && {queryValidationError.message}}
+
Selection options
@@ -58,6 +77,8 @@ export function CustomVariableForm({
includeAll={includeAll}
allValue={allValue}
allowCustomValue={allowCustomValue}
+ disableAllowCustomValue={valuesFormat === 'json'}
+ disableCustomAllValue={valuesFormat === 'json'}
onMultiChange={onMultiChange}
onIncludeAllChange={onIncludeAllChange}
onAllValueChange={onAllValueChange}
@@ -66,3 +87,48 @@ export function CustomVariableForm({
>
);
}
+
+interface ValuesFormatSelectorProps {
+ valuesFormat?: CustomVariableModel['valuesFormat'];
+ onValuesFormatChange?: (format: CustomVariableModel['valuesFormat']) => void;
+}
+
+export function ValuesFormatSelector({ valuesFormat, onValuesFormatChange }: ValuesFormatSelectorProps) {
+ return (
+
+
+ {valuesFormat === 'json' && (
+
+ Provide a JSON representing an array of objects, where each object can have any number of properties.
+
+ Check{' '}
+
+ our docs
+ {' '}
+ for more information.
+
+ }
+ placement="top"
+ interactive
+ >
+
+
+ )}
+
+ );
+}
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 030de016a5f..0fe1f0e1f46 100644
--- a/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.tsx
+++ b/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.tsx
@@ -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, 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';
@@ -34,6 +34,7 @@ interface QueryVariableEditorFormProps {
timeRange: TimeRange;
regex: string | null;
onRegExChange: (event: FormEvent) => void;
+ disableRegexEdition?: boolean;
sort: VariableSort;
onSortChange: (option: SelectableValue) => void;
refresh: VariableRefresh;
@@ -42,14 +43,17 @@ interface QueryVariableEditorFormProps {
onMultiChange: (event: FormEvent) => void;
allowCustomValue?: boolean;
onAllowCustomValueChange?: (event: FormEvent) => void;
+ disableAllowCustomValue?: boolean;
includeAll: boolean;
onIncludeAllChange: (event: FormEvent) => void;
allValue: string;
onAllValueChange: (event: FormEvent) => void;
+ disableCustomAllValue?: boolean;
staticOptions?: StaticOptionsType;
staticOptionsOrder?: StaticOptionsOrderType;
onStaticOptionsChange?: (staticOptions: StaticOptionsType) => void;
onStaticOptionsOrderChange?: (staticOptionsOrder: StaticOptionsOrderType) => void;
+ disableStaticOptions?: boolean;
}
export function QueryVariableEditorForm({
@@ -61,6 +65,7 @@ export function QueryVariableEditorForm({
timeRange,
regex,
onRegExChange,
+ disableRegexEdition,
sort,
onSortChange,
refresh,
@@ -69,14 +74,17 @@ export function QueryVariableEditorForm({
onMultiChange,
allowCustomValue,
onAllowCustomValueChange,
+ disableAllowCustomValue,
includeAll,
onIncludeAllChange,
allValue,
onAllValueChange,
+ disableCustomAllValue,
staticOptions,
staticOptionsOrder,
onStaticOptionsChange,
onStaticOptionsOrderChange,
+ disableStaticOptions,
}: QueryVariableEditorFormProps) {
const { value: dsConfig } = useAsync(async () => {
const datasource = await getDataSourceSrv().get(datasourceRef ?? '');
@@ -116,48 +124,53 @@ export function QueryVariableEditorForm({
{datasource && VariableQueryEditor && (
-
+
+
+
)}
-
-
- 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}
- />
+ {!disableRegexEdition && (
+
+
+ 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 && (
+ {!disableStaticOptions && 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 = ({ options, hasMultiProps }: Props) => {
+ if (!options.length) {
+ return null;
+ }
+
+ if (hasMultiProps) {
+ 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 }));
+ // the first item in data may be the "All" option, which does not have any extra properties, so we try the 2nd item to determine the column names
+ const columns = Object.keys(data[1] || data[0]).map((id) => ({ id, header: id, sortType: 'alphanumeric' as const }));
+
+ return (
+
+
+ Preview of values
+
+ String(r.value)}
+ pageSize={8}
+ />
+
+ );
+}
+
+function VariableValuesWithoutPropsPreview({ options }: { options: VariableValueOption[] }) {
+ const styles = useStyles2(getStyles);
const [previewLimit, setPreviewLimit] = useState(20);
const [previewOptions, setPreviewOptions] = useState([]);
const showMoreOptions = useCallback(
@@ -21,15 +58,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 +83,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),
@@ -71,5 +103,10 @@ function getStyles(theme: GrafanaTheme2) {
textOverflow: 'ellipsis',
maxWidth: '50vw',
}),
+ table: css({
+ td: css({
+ padding: theme.spacing(0.5, 1),
+ }),
+ }),
};
}
diff --git a/public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/CustomVariableEditor.test.tsx b/public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/CustomVariableEditor.test.tsx
index a7315d37a76..e182ce1e2af 100644
--- a/public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/CustomVariableEditor.test.tsx
+++ b/public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/CustomVariableEditor.test.tsx
@@ -5,117 +5,225 @@ import { CustomVariable } from '@grafana/scenes';
import { CustomVariableEditor } from './CustomVariableEditor';
+function setup(options: Partial
[0]> = {}) {
+ return {
+ variable: new CustomVariable({
+ name: 'customVar',
+ ...options,
+ }),
+ onRunQuery: jest.fn(),
+ };
+}
+
+function renderEditor(ui: React.ReactNode) {
+ const renderResult = render(ui);
+
+ const elements = {
+ formatButton: (label: string) => renderResult.queryByLabelText(label) as HTMLElement,
+ queryInput: () =>
+ renderResult.queryByTestId(
+ selectors.pages.Dashboard.Settings.Variables.Edit.CustomVariable.customValueInput
+ ) as HTMLTextAreaElement,
+ multiValueCheckbox: () =>
+ renderResult.queryByTestId(
+ selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsMultiSwitch
+ ) as HTMLInputElement,
+ allowCustomValueCheckbox: () =>
+ renderResult.queryByTestId(
+ selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsAllowCustomValueSwitch
+ ) as HTMLInputElement,
+ includeAllCheckbox: () =>
+ renderResult.queryByTestId(
+ selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsIncludeAllSwitch
+ ) as HTMLInputElement,
+ customAllValueInput: () =>
+ renderResult.queryByTestId(
+ selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput
+ ) as HTMLInputElement,
+ };
+
+ return {
+ ...renderResult,
+ elements,
+ actions: {
+ updateValuesInput(newQuery: string) {
+ fireEvent.change(elements.queryInput(), { target: { value: newQuery } });
+ fireEvent.blur(elements.queryInput());
+ },
+ changeValuesFormat(newFormat: 'csv' | 'json') {
+ const targetLabel = newFormat === 'json' ? 'Object values in a JSON array' : 'Values separated by comma';
+
+ const formatButton = elements.formatButton(targetLabel);
+ if (formatButton === null) {
+ throw new Error(`Unable to fire a "click" event - button with label "${targetLabel}" not found in DOM`);
+ }
+
+ fireEvent.click(formatButton);
+ },
+ },
+ };
+}
+
describe('CustomVariableEditor', () => {
- it('should render the CustomVariableForm with correct initial values', () => {
- const variable = new CustomVariable({
- name: 'customVar',
- query: 'test, test2',
- value: 'test',
- isMulti: true,
- includeAll: true,
- allValue: 'test',
+ describe('CSV values format', () => {
+ it('should render CustomVariableForm with the correct initial values', () => {
+ const { variable, onRunQuery } = setup({
+ query: 'test, test2',
+ value: 'test',
+ isMulti: true,
+ includeAll: true,
+ allowCustomValue: true,
+ allValue: 'all',
+ });
+
+ const { elements } = renderEditor();
+
+ expect(elements.queryInput().value).toBe('test, test2');
+ expect(elements.multiValueCheckbox().checked).toBe(true);
+ expect(elements.allowCustomValueCheckbox().checked).toBe(true);
+ expect(elements.includeAllCheckbox().checked).toBe(true);
+ expect(elements.customAllValueInput().value).toBe('all');
});
- const onRunQuery = jest.fn();
- const { getByTestId } = render();
+ it('should update the variable state when some input values change ("Multi-value", "Allow custom values" & "Include All option")', () => {
+ const { variable, onRunQuery } = setup({
+ query: 'test, test2',
+ value: 'test',
+ isMulti: false,
+ allowCustomValue: false,
+ includeAll: false,
+ });
- const queryInput = getByTestId(
- selectors.pages.Dashboard.Settings.Variables.Edit.CustomVariable.customValueInput
- ) as HTMLInputElement;
- const allValueInput = getByTestId(
- selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput
- ) as HTMLInputElement;
- const multiCheckbox = getByTestId(
- selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsMultiSwitch
- ) as HTMLInputElement;
- const includeAllCheckbox = getByTestId(
- selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsIncludeAllSwitch
- ) as HTMLInputElement;
+ const { elements } = renderEditor();
- expect(queryInput.value).toBe('test, test2');
- expect(allValueInput.value).toBe('test');
- expect(multiCheckbox.checked).toBe(true);
- expect(includeAllCheckbox.checked).toBe(true);
+ expect(elements.multiValueCheckbox().checked).toBe(false);
+ expect(elements.allowCustomValueCheckbox().checked).toBe(false);
+ expect(elements.includeAllCheckbox().checked).toBe(false);
+ // include-all-custom input appears after include-all checkbox is checked only
+ expect(elements.customAllValueInput()).not.toBeInTheDocument();
+
+ fireEvent.click(elements.multiValueCheckbox());
+ fireEvent.click(elements.allowCustomValueCheckbox());
+ fireEvent.click(elements.includeAllCheckbox());
+
+ expect(variable.state.isMulti).toBe(true);
+ expect(variable.state.allowCustomValue).toBe(true);
+ expect(variable.state.includeAll).toBe(true);
+ expect(elements.customAllValueInput()).toBeInTheDocument();
+ });
+
+ describe('when the values textarea loses focus after its value has changed', () => {
+ it('should update the query in the variable state and call the onRunQuery callback', async () => {
+ const { variable, onRunQuery } = setup({ query: 'test, test2', value: 'test' });
+
+ const { actions } = renderEditor();
+
+ actions.updateValuesInput('test3, test4');
+
+ expect(variable.state.query).toBe('test3, test4');
+ expect(onRunQuery).toHaveBeenCalled();
+ });
+ });
+
+ describe('when the "Custom all value" input loses focus after its value has changed', () => {
+ it('should update the variable state', () => {
+ const { variable, onRunQuery } = setup({
+ query: 'test, test2',
+ value: 'test',
+ isMulti: true,
+ includeAll: true,
+ });
+
+ const { elements } = renderEditor();
+
+ fireEvent.change(elements.customAllValueInput(), { target: { value: 'new custom all' } });
+ fireEvent.blur(elements.customAllValueInput());
+
+ expect(variable.state.allValue).toBe('new custom all');
+ });
+ });
});
- it('should update the variable state when input values change', () => {
- const variable = new CustomVariable({
- name: 'customVar',
- query: 'test, test2',
- value: 'test',
+ describe('JSON values format', () => {
+ const initialJsonQuery = `[
+ {"value":1,"text":"Development","aws":"dev","azure":"development"},
+ {"value":2,"text":"Production","aws":"prod","azure":"production"}
+ ]`;
+
+ it('should render CustomVariableForm with the correct initial values', () => {
+ const { variable, onRunQuery } = setup({
+ valuesFormat: 'json',
+ query: initialJsonQuery,
+ isMulti: true,
+ includeAll: true,
+ });
+
+ const { elements } = renderEditor();
+
+ expect(elements.queryInput().value).toBe(initialJsonQuery);
+ expect(elements.multiValueCheckbox().checked).toBe(true);
+ expect(elements.allowCustomValueCheckbox()).not.toBeInTheDocument();
+ expect(elements.includeAllCheckbox().checked).toBe(true);
+ expect(elements.customAllValueInput()).not.toBeInTheDocument();
});
- const onRunQuery = jest.fn();
- const { getByTestId } = render();
+ describe('when the values textarea loses focus after its value has changed', () => {
+ describe('if the value is valid JSON', () => {
+ it('should update the query in the variable state and call the onRunQuery callback', async () => {
+ const { variable, onRunQuery } = setup({ valuesFormat: 'json', query: initialJsonQuery });
- const multiCheckbox = getByTestId(
- selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsMultiSwitch
- );
- const includeAllCheckbox = getByTestId(
- selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsIncludeAllSwitch
- );
+ const { actions } = renderEditor();
- const allowCustomValueCheckbox = getByTestId(
- selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsAllowCustomValueSwitch
- );
+ actions.updateValuesInput('[]');
- // It include-all-custom input appears after include-all checkbox is checked only
- expect(() =>
- getByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput)
- ).toThrow('Unable to find an element');
+ expect(variable.state.query).toBe('[]');
+ expect(onRunQuery).toHaveBeenCalled();
+ });
+ });
- fireEvent.click(allowCustomValueCheckbox);
+ describe('if the value is NOT valid JSON', () => {
+ it('should display a validation error message and neither update the query in the variable state nor call the onRunQuery callback', async () => {
+ const { variable, onRunQuery } = setup({ valuesFormat: 'json', query: initialJsonQuery });
- fireEvent.click(multiCheckbox);
+ const { actions, getByRole } = renderEditor(
+
+ );
- fireEvent.click(includeAllCheckbox);
- const allValueInput = getByTestId(
- selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput
- );
+ actions.updateValuesInput('[x]');
- expect(variable.state.isMulti).toBe(true);
- expect(variable.state.includeAll).toBe(true);
- expect(variable.state.allowCustomValue).toBe(false);
- expect(allValueInput).toBeInTheDocument();
+ expect(getByRole('alert')).toHaveTextContent(`Unexpected token 'x', "[x]" is not valid JSON`);
+ expect(variable.state.query).toBe(initialJsonQuery);
+ expect(onRunQuery).not.toHaveBeenCalled();
+ });
+ });
+ });
});
- it('should call update query and re-run query when input loses focus', async () => {
- const variable = new CustomVariable({
- name: 'customVar',
- query: 'test, test2',
- value: 'test',
+ describe('when switching values format', () => {
+ it('should switch the visibility of the proper form inputs ("Allow custom values" and "Custom all value")', () => {
+ const { variable, onRunQuery } = setup({
+ valuesFormat: 'csv',
+ query: '',
+ isMulti: true,
+ includeAll: true,
+ allowCustomValue: true,
+ allValue: '',
+ });
+
+ const { elements, actions } = renderEditor();
+
+ expect(elements.allowCustomValueCheckbox()).toBeInTheDocument();
+ expect(elements.customAllValueInput()).toBeInTheDocument();
+
+ actions.changeValuesFormat('json');
+
+ expect(elements.allowCustomValueCheckbox()).not.toBeInTheDocument();
+ expect(elements.customAllValueInput()).not.toBeInTheDocument();
+
+ actions.changeValuesFormat('csv');
+
+ expect(elements.allowCustomValueCheckbox()).toBeInTheDocument();
+ expect(elements.customAllValueInput()).toBeInTheDocument();
});
- const onRunQuery = jest.fn();
-
- const { getByTestId } = render();
-
- const queryInput = getByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.CustomVariable.customValueInput);
- fireEvent.change(queryInput, { target: { value: 'test3, test4' } });
- fireEvent.blur(queryInput);
-
- expect(onRunQuery).toHaveBeenCalled();
- expect(variable.state.query).toBe('test3, test4');
- });
-
- it('should update the variable state when all-custom-value input loses focus', () => {
- const variable = new CustomVariable({
- name: 'customVar',
- query: 'test, test2',
- value: 'test',
- isMulti: true,
- includeAll: true,
- });
- const onRunQuery = jest.fn();
-
- const { getByTestId } = render();
-
- const allValueInput = getByTestId(
- selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput
- ) as HTMLInputElement;
-
- fireEvent.change(allValueInput, { target: { value: 'new custom all' } });
- fireEvent.blur(allValueInput);
-
- expect(variable.state.allValue).toBe('new custom all');
});
});
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..5c1f3158443 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,7 @@
-import { FormEvent, useCallback } from 'react';
+import { isObject } from 'lodash';
+import { FormEvent, useCallback, useState } from 'react';
+import { CustomVariableModel, shallowCompare } from '@grafana/data';
import { t } from '@grafana/i18n';
import { CustomVariable, SceneVariable } from '@grafana/scenes';
@@ -14,7 +16,26 @@ 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 [queryValidationError, setQueryValidationError] = useState();
+
+ const [prevQuery, setPrevQuery] = useState('');
+ const onValuesFormatChange = useCallback(
+ (format: CustomVariableModel['valuesFormat']) => {
+ variable.setState({ query: prevQuery });
+ variable.setState({ value: isMulti ? [] : undefined });
+ variable.setState({ valuesFormat: format });
+ variable.setState({ allowCustomValue: false });
+ variable.setState({ allValue: undefined });
+ onRunQuery();
+
+ setQueryValidationError(undefined);
+ if (query !== prevQuery) {
+ setPrevQuery(query);
+ }
+ },
+ [isMulti, onRunQuery, prevQuery, query, variable]
+ );
const onMultiChange = useCallback(
(event: FormEvent) => {
@@ -32,10 +53,20 @@ export function CustomVariableEditor({ variable, onRunQuery }: CustomVariableEdi
const onQueryChange = useCallback(
(event: FormEvent) => {
+ setPrevQuery('');
+
+ 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(
@@ -55,15 +86,18 @@ export function CustomVariableEditor({ variable, onRunQuery }: CustomVariableEdi
return (
);
}
@@ -81,3 +115,47 @@ export function getCustomVariableOptions(variable: SceneVariable): OptionsPaneIt
}),
];
}
+
+export 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/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/ModalEditor.tsx b/public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/ModalEditor.tsx
index 3e8a8aa57b1..fa6dd46680a 100644
--- a/public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/ModalEditor.tsx
+++ b/public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor/ModalEditor.tsx
@@ -1,15 +1,16 @@
-import { useCallback, useRef } from 'react';
+import { FormEvent, useCallback, useState } from 'react';
+import { lastValueFrom } from 'rxjs';
+import { CustomVariableModel } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { t, Trans } from '@grafana/i18n';
import { CustomVariable } from '@grafana/scenes';
-import { Button, Modal, Stack } from '@grafana/ui';
+import { Button, FieldValidationMessage, Modal, Stack, TextArea } from '@grafana/ui';
-import { VariableStaticOptionsFormRef } from '../../components/VariableStaticOptionsForm';
-import { VariableStaticOptionsFormAddButton } from '../../components/VariableStaticOptionsFormAddButton';
+import { ValuesFormatSelector } from '../../components/CustomVariableForm';
+import { VariableValuesPreview } from '../../components/VariableValuesPreview';
-import { ValuesBuilder } from './ValuesBuilder';
-import { ValuesPreview } from './ValuesPreview';
+import { validateJsonQuery } from './CustomVariableEditor';
interface ModalEditorProps {
variable: CustomVariable;
@@ -18,9 +19,49 @@ interface ModalEditorProps {
}
export function ModalEditor({ variable, isOpen, onClose }: ModalEditorProps) {
- const formRef = useRef(null);
+ const { query, valuesFormat, isMulti } = variable.useState();
+ const [prevQuery, setPrevQuery] = useState('');
+ const [queryValidationError, setQueryValidationError] = useState();
- const handleOnAdd = useCallback(() => formRef.current?.addItem(), []);
+ const onValuesFormatChange = useCallback(
+ async (format: CustomVariableModel['valuesFormat']) => {
+ variable.setState({ query: prevQuery });
+ variable.setState({ value: isMulti ? [] : undefined });
+ variable.setState({ valuesFormat: format });
+ variable.setState({ allowCustomValue: false });
+ variable.setState({ allValue: undefined });
+
+ await lastValueFrom(variable.validateAndUpdate());
+
+ setQueryValidationError(undefined);
+ if (query !== prevQuery) {
+ setPrevQuery(query);
+ }
+ },
+ [isMulti, prevQuery, query, variable]
+ );
+
+ const onQueryChange = useCallback(
+ async (event: FormEvent) => {
+ setPrevQuery('');
+
+ if (valuesFormat === 'json') {
+ const validationError = validateJsonQuery(event.currentTarget.value);
+ setQueryValidationError(validationError);
+ if (validationError) {
+ return;
+ }
+ }
+
+ variable.setState({ query: event.currentTarget.value });
+ await lastValueFrom(variable.validateAndUpdate());
+ },
+ [valuesFormat, variable]
+ );
+
+ const optionsForSelect = variable.getOptionsForSelect(false);
+ const hasJsonValuesFormat = variable.state.valuesFormat === 'json';
+ const hasMultiProps = hasJsonValuesFormat || optionsForSelect.every((o) => Boolean(o.properties));
return (
-
-
+
+
+
+ {queryValidationError && {queryValidationError.message}}
+
+
+
+
- }>
+
+ }
+ // 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 && (
)}
- {isHasVariableOptions && }
+ {isHasVariableOptions && }
);
}
diff --git a/public/app/features/dashboard-scene/settings/variables/useVariableSelectionOptionsCategory.tsx b/public/app/features/dashboard-scene/settings/variables/useVariableSelectionOptionsCategory.tsx
index 429c9221998..5f8260ea1b9 100644
--- a/public/app/features/dashboard-scene/settings/variables/useVariableSelectionOptionsCategory.tsx
+++ b/public/app/features/dashboard-scene/settings/variables/useVariableSelectionOptionsCategory.tsx
@@ -45,7 +45,11 @@ export function useVariableSelectionOptionsCategory(variable: MultiValueVariable
'A wildcard regex or other value to represent All'
),
useShowIf: () => {
- return variable.useState().includeAll ?? false;
+ const state = variable.useState();
+ const hasMultiProps =
+ ('valuesFormat' in state && state.valuesFormat === 'json') ||
+ state.options.every((o) => Boolean(o.properties));
+ return hasMultiProps ? false : (state.includeAll ?? false);
},
render: (descriptor) => ,
})
@@ -58,6 +62,13 @@ export function useVariableSelectionOptionsCategory(variable: MultiValueVariable
'dashboard.edit-pane.variable.selection-options.allow-custom-values-description',
'Enables users to enter values'
),
+ useShowIf: () => {
+ const state = variable.useState();
+ const hasMultiProps =
+ ('valuesFormat' in state && state.valuesFormat === 'json') ||
+ state.options.every((o) => Boolean(o.properties));
+ return !hasMultiProps;
+ },
render: (descriptor) => ,
})
);
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,
}),
diff --git a/public/app/features/dashboard-scene/utils/variables.test.ts b/public/app/features/dashboard-scene/utils/variables.test.ts
index e627bd01866..e8126023fc9 100644
--- a/public/app/features/dashboard-scene/utils/variables.test.ts
+++ b/public/app/features/dashboard-scene/utils/variables.test.ts
@@ -103,6 +103,7 @@ describe('when creating variables objects', () => {
text: 'a',
type: 'custom',
value: 'a',
+ valuesFormat: 'csv',
hide: 0,
});
});
diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json
index d9efaadc4d6..46920e9d8fc 100644
--- a/public/locales/en-US/grafana.json
+++ b/public/locales/en-US/grafana.json
@@ -5880,6 +5880,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 docs4> for more information.",
+ "name-json-values": "Object values in a JSON array",
"name-values-separated-comma": "Values separated by comma",
"selection-options": "Selection options"
},
diff --git a/yarn.lock b/yarn.lock
index 356d0eaacf4..6d346c7ff5c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3603,11 +3603,11 @@ __metadata:
languageName: unknown
linkType: soft
-"@grafana/scenes-react@npm:6.47.1":
- version: 6.47.1
- resolution: "@grafana/scenes-react@npm:6.47.1"
+"@grafana/scenes-react@npm:^6.48.0":
+ version: 6.48.0
+ resolution: "@grafana/scenes-react@npm:6.48.0"
dependencies:
- "@grafana/scenes": "npm:6.47.1"
+ "@grafana/scenes": "npm:6.48.0"
lru-cache: "npm:^10.2.2"
react-use: "npm:^17.4.0"
peerDependencies:
@@ -3619,7 +3619,7 @@ __metadata:
react: ^18.0.0
react-dom: ^18.0.0
react-router-dom: ^6.28.0
- checksum: 10/dc20f9ee80eaf648665f7449e3ccb3b640a931f8f4a1be89599dce17eb0f52e763e3a603a4d491d9886b3e6cdf2ad3634124252c223315917206200f7cd6da16
+ checksum: 10/5afb2aa79271dd824cc35f0a59ec193ddcbd4e1f14e756551228ce218a19faad54923d3a83f8bbb10d38c1d23c49846df29e7fce8feba8ec9aec2d32d9c1cf8d
languageName: node
linkType: hard
@@ -3649,9 +3649,9 @@ __metadata:
languageName: node
linkType: hard
-"@grafana/scenes@npm:6.47.1":
- version: 6.47.1
- resolution: "@grafana/scenes@npm:6.47.1"
+"@grafana/scenes@npm:6.48.0, @grafana/scenes@npm:^6.48.0":
+ version: 6.48.0
+ resolution: "@grafana/scenes@npm:6.48.0"
dependencies:
"@floating-ui/react": "npm:^0.26.16"
"@leeoniya/ufuzzy": "npm:^1.0.16"
@@ -3671,7 +3671,7 @@ __metadata:
react: ^18.0.0
react-dom: ^18.0.0
react-router-dom: ^6.28.0
- checksum: 10/bc0c76258955058e7493b04e7cdd5d59dcc4159adf06da0837e992716ea15700b54f8403614df04326350363dc3344fb2602a2e8f7807724571659b4bd95aded
+ checksum: 10/28cd64ea3c4faf87173ea71ffc136a7a525c33ec2e263ab2a98df718e3968ed7b7a12ecf0f309400af78e3c3269ae6048da8113412645a361a3e4925f9a2a810
languageName: node
linkType: hard
@@ -19234,8 +19234,8 @@ __metadata:
"@grafana/plugin-ui": "npm:^0.11.1"
"@grafana/prometheus": "workspace:*"
"@grafana/runtime": "workspace:*"
- "@grafana/scenes": "npm:6.47.1"
- "@grafana/scenes-react": "npm:6.47.1"
+ "@grafana/scenes": "npm:^6.48.0"
+ "@grafana/scenes-react": "npm:^6.48.0"
"@grafana/schema": "workspace:*"
"@grafana/sql": "workspace:*"
"@grafana/test-utils": "workspace:*"