Dashboards: Add possibility to lock multi value variables option list (#95949)

* add new option for multi variables to lock value list wip

* WIP - lock option list

* tests

* fix

* fixes + canary scenes

* wip

* wip

* fix snapshot

* bump scenes

* Dashboards: Add possibility to lock adhoc variables options list (#96077)

* Lock list of options flag for ad hoc

* refactor

* fix snapshot
pull/96835/head
Victor Marin 7 months ago committed by GitHub
parent d567be2f10
commit d5f404d082
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      kinds/dashboard/dashboard_kind.cue
  2. 2
      packages/grafana-data/src/types/templateVars.ts
  3. 3
      packages/grafana-e2e-selectors/src/selectors/pages.ts
  4. 5
      packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts
  5. 3
      pkg/kinds/dashboard/dashboard_spec_gen.go
  6. 10
      public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap
  7. 12
      public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts
  8. 4
      public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts
  9. 16
      public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.test.tsx
  10. 17
      public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.tsx
  11. 18
      public/app/features/dashboard-scene/settings/variables/components/CustomVariableForm.test.tsx
  12. 6
      public/app/features/dashboard-scene/settings/variables/components/CustomVariableForm.tsx
  13. 6
      public/app/features/dashboard-scene/settings/variables/components/DataSourceVariableForm.tsx
  14. 22
      public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.test.tsx
  15. 6
      public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.tsx
  16. 13
      public/app/features/dashboard-scene/settings/variables/components/SelectionOptionsForm.tsx
  17. 6
      public/app/features/dashboard-scene/settings/variables/editors/AdHocFiltersVariableEditor.test.tsx
  18. 9
      public/app/features/dashboard-scene/settings/variables/editors/AdHocFiltersVariableEditor.tsx
  19. 7
      public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor.test.tsx
  20. 7
      public/app/features/dashboard-scene/settings/variables/editors/CustomVariableEditor.tsx
  21. 5
      public/app/features/dashboard-scene/settings/variables/editors/DataSourceVariableEditor.test.tsx
  22. 8
      public/app/features/dashboard-scene/settings/variables/editors/DataSourceVariableEditor.tsx
  23. 6
      public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.test.tsx
  24. 8
      public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.tsx
  25. 11
      public/app/features/dashboard-scene/utils/variables.test.ts
  26. 4
      public/app/features/dashboard-scene/utils/variables.ts

@ -200,6 +200,8 @@ lineage: schemas: [{
current?: #VariableOption current?: #VariableOption
// Whether multiple values can be selected or not from variable value list // Whether multiple values can be selected or not from variable value list
multi?: bool | *false multi?: bool | *false
// Allow custom values to be entered in the variable
allowCustomValue?: bool | *true
// Options that can be selected for a variable. // Options that can be selected for a variable.
options?: [...#VariableOption] options?: [...#VariableOption]
// Options to config when to refresh a variable // Options to config when to refresh a variable

@ -70,6 +70,7 @@ export interface AdHocVariableModel extends BaseVariableModel {
* Static keys that override any dynamic keys from the datasource. * Static keys that override any dynamic keys from the datasource.
*/ */
defaultKeys?: MetricFindValue[]; defaultKeys?: MetricFindValue[];
allowCustomValue?: boolean;
} }
export interface GroupByVariableModel extends VariableWithOptions { export interface GroupByVariableModel extends VariableWithOptions {
@ -127,6 +128,7 @@ export interface VariableWithMultiSupport extends VariableWithOptions {
multi: boolean; multi: boolean;
includeAll: boolean; includeAll: boolean;
allValue?: string | null; allValue?: string | null;
allowCustomValue?: boolean;
} }
export interface VariableWithOptions extends BaseVariableModel { export interface VariableWithOptions extends BaseVariableModel {

@ -414,6 +414,9 @@ export const versionedPages = {
generalHideSelectV2: { generalHideSelectV2: {
[MIN_GRAFANA_VERSION]: 'data-testid Variable editor Form Hide select', [MIN_GRAFANA_VERSION]: 'data-testid Variable editor Form Hide select',
}, },
selectionOptionsAllowCustomValueSwitch: {
[MIN_GRAFANA_VERSION]: 'data-testid Variable editor Form Allow Custom Value switch',
},
selectionOptionsMultiSwitch: { selectionOptionsMultiSwitch: {
'10.4.0': 'data-testid Variable editor Form Multi switch', '10.4.0': 'data-testid Variable editor Form Multi switch',
[MIN_GRAFANA_VERSION]: 'Variable editor Form Multi switch', [MIN_GRAFANA_VERSION]: 'Variable editor Form Multi switch',

@ -130,6 +130,10 @@ export interface VariableModel {
* Custom all value * Custom all value
*/ */
allValue?: string; allValue?: string;
/**
* Allow custom values to be entered in the variable
*/
allowCustomValue?: boolean;
/** /**
* Shows current selected variable text/value on the dashboard * Shows current selected variable text/value on the dashboard
*/ */
@ -194,6 +198,7 @@ export interface VariableModel {
} }
export const defaultVariableModel: Partial<VariableModel> = { export const defaultVariableModel: Partial<VariableModel> = {
allowCustomValue: true,
includeAll: false, includeAll: false,
multi: false, multi: false,
options: [], options: [],

@ -924,6 +924,9 @@ type VariableModel struct {
// Custom all value // Custom all value
AllValue *string `json:"allValue,omitempty"` AllValue *string `json:"allValue,omitempty"`
// Allow custom values to be entered in the variable
AllowCustomValue *bool `json:"allowCustomValue,omitempty"`
// Option to be selected in a variable. // Option to be selected in a variable.
Current *VariableOption `json:"current,omitempty"` Current *VariableOption `json:"current,omitempty"`

@ -288,6 +288,7 @@ exports[`transformSceneToSaveModel Given a scene with rows Should transform back
"templating": { "templating": {
"list": [ "list": [
{ {
"allowCustomValue": true,
"current": { "current": {
"text": [ "text": [
"A", "A",
@ -306,6 +307,7 @@ exports[`transformSceneToSaveModel Given a scene with rows Should transform back
"type": "custom", "type": "custom",
}, },
{ {
"allowCustomValue": true,
"current": { "current": {
"text": [ "text": [
"1", "1",
@ -556,6 +558,7 @@ exports[`transformSceneToSaveModel Given a simple scene with custom settings Sho
"templating": { "templating": {
"list": [ "list": [
{ {
"allowCustomValue": true,
"baseFilters": [], "baseFilters": [],
"datasource": { "datasource": {
"type": "prometheus", "type": "prometheus",
@ -631,6 +634,7 @@ exports[`transformSceneToSaveModel Given a simple scene with custom settings Sho
"type": "interval", "type": "interval",
}, },
{ {
"allowCustomValue": true,
"current": { "current": {
"text": [ "text": [
"a", "a",
@ -647,6 +651,7 @@ exports[`transformSceneToSaveModel Given a simple scene with custom settings Sho
"type": "custom", "type": "custom",
}, },
{ {
"allowCustomValue": true,
"current": { "current": {
"text": "gdev-testdata", "text": "gdev-testdata",
"value": "PD8C576611E62080A", "value": "PD8C576611E62080A",
@ -660,6 +665,7 @@ exports[`transformSceneToSaveModel Given a simple scene with custom settings Sho
"type": "datasource", "type": "datasource",
}, },
{ {
"allowCustomValue": true,
"current": { "current": {
"text": "A", "text": "A",
"value": "A", "value": "A",
@ -915,6 +921,7 @@ exports[`transformSceneToSaveModel Given a simple scene with variables Should tr
"templating": { "templating": {
"list": [ "list": [
{ {
"allowCustomValue": true,
"baseFilters": [], "baseFilters": [],
"datasource": { "datasource": {
"type": "prometheus", "type": "prometheus",
@ -990,6 +997,7 @@ exports[`transformSceneToSaveModel Given a simple scene with variables Should tr
"type": "interval", "type": "interval",
}, },
{ {
"allowCustomValue": true,
"current": { "current": {
"text": [ "text": [
"a", "a",
@ -1006,6 +1014,7 @@ exports[`transformSceneToSaveModel Given a simple scene with variables Should tr
"type": "custom", "type": "custom",
}, },
{ {
"allowCustomValue": true,
"current": { "current": {
"text": "gdev-testdata", "text": "gdev-testdata",
"value": "PD8C576611E62080A", "value": "PD8C576611E62080A",
@ -1019,6 +1028,7 @@ exports[`transformSceneToSaveModel Given a simple scene with variables Should tr
"type": "datasource", "type": "datasource",
}, },
{ {
"allowCustomValue": true,
"current": { "current": {
"text": "A", "text": "A",
"value": "A", "value": "A",

@ -98,6 +98,7 @@ describe('sceneVariablesSetToVariables', () => {
datasource: { uid: 'fake-std', type: 'fake-std' }, datasource: { uid: 'fake-std', type: 'fake-std' },
query: 'query', query: 'query',
includeAll: true, includeAll: true,
allowCustomValue: true,
allValue: 'test-all', allValue: 'test-all',
isMulti: true, isMulti: true,
}); });
@ -112,6 +113,7 @@ describe('sceneVariablesSetToVariables', () => {
expect(result[0]).toMatchInlineSnapshot(` expect(result[0]).toMatchInlineSnapshot(`
{ {
"allValue": "test-all", "allValue": "test-all",
"allowCustomValue": true,
"current": { "current": {
"text": [ "text": [
"selected-value-text", "selected-value-text",
@ -151,6 +153,7 @@ describe('sceneVariablesSetToVariables', () => {
definition: 'query', definition: 'query',
includeAll: true, includeAll: true,
allValue: 'test-all', allValue: 'test-all',
allowCustomValue: false,
isMulti: true, isMulti: true,
}); });
const set = new SceneVariableSet({ const set = new SceneVariableSet({
@ -163,6 +166,7 @@ describe('sceneVariablesSetToVariables', () => {
expect(result[0]).toMatchInlineSnapshot(` expect(result[0]).toMatchInlineSnapshot(`
{ {
"allValue": "test-all", "allValue": "test-all",
"allowCustomValue": false,
"current": { "current": {
"text": [ "text": [
"selected-value-text", "selected-value-text",
@ -256,6 +260,7 @@ describe('sceneVariablesSetToVariables', () => {
pluginId: 'fake-std', pluginId: 'fake-std',
includeAll: true, includeAll: true,
allValue: 'test-all', allValue: 'test-all',
allowCustomValue: true,
isMulti: true, isMulti: true,
}); });
const set = new SceneVariableSet({ const set = new SceneVariableSet({
@ -268,6 +273,7 @@ describe('sceneVariablesSetToVariables', () => {
expect(result[0]).toMatchInlineSnapshot(` expect(result[0]).toMatchInlineSnapshot(`
{ {
"allValue": "test-all", "allValue": "test-all",
"allowCustomValue": true,
"current": { "current": {
"text": [ "text": [
"selected-ds-1-text", "selected-ds-1-text",
@ -307,6 +313,7 @@ describe('sceneVariablesSetToVariables', () => {
], ],
includeAll: true, includeAll: true,
allValue: 'test-all', allValue: 'test-all',
allowCustomValue: true,
isMulti: true, isMulti: true,
}); });
const set = new SceneVariableSet({ const set = new SceneVariableSet({
@ -319,6 +326,7 @@ describe('sceneVariablesSetToVariables', () => {
expect(result[0]).toMatchInlineSnapshot(` expect(result[0]).toMatchInlineSnapshot(`
{ {
"allValue": "test-all", "allValue": "test-all",
"allowCustomValue": true,
"current": { "current": {
"text": [ "text": [
"test", "test",
@ -488,6 +496,7 @@ describe('sceneVariablesSetToVariables', () => {
it('should handle AdHocFiltersVariable', () => { it('should handle AdHocFiltersVariable', () => {
const variable = new AdHocFiltersVariable({ const variable = new AdHocFiltersVariable({
name: 'test', name: 'test',
allowCustomValue: true,
label: 'test-label', label: 'test-label',
description: 'test-desc', description: 'test-desc',
datasource: { uid: 'fake-std', type: 'fake-std' }, datasource: { uid: 'fake-std', type: 'fake-std' },
@ -515,6 +524,7 @@ describe('sceneVariablesSetToVariables', () => {
expect(result).toHaveLength(1); expect(result).toHaveLength(1);
expect(result[0]).toMatchInlineSnapshot(` expect(result[0]).toMatchInlineSnapshot(`
{ {
"allowCustomValue": true,
"baseFilters": [ "baseFilters": [
{ {
"key": "baseFilterTest", "key": "baseFilterTest",
@ -545,6 +555,7 @@ describe('sceneVariablesSetToVariables', () => {
it('should handle AdHocFiltersVariable with defaultKeys', () => { it('should handle AdHocFiltersVariable with defaultKeys', () => {
const variable = new AdHocFiltersVariable({ const variable = new AdHocFiltersVariable({
name: 'test', name: 'test',
allowCustomValue: true,
label: 'test-label', label: 'test-label',
description: 'test-desc', description: 'test-desc',
datasource: { uid: 'fake-std', type: 'fake-std' }, datasource: { uid: 'fake-std', type: 'fake-std' },
@ -586,6 +597,7 @@ describe('sceneVariablesSetToVariables', () => {
expect(result).toHaveLength(1); expect(result).toHaveLength(1);
expect(result[0]).toMatchInlineSnapshot(` expect(result[0]).toMatchInlineSnapshot(`
{ {
"allowCustomValue": true,
"baseFilters": [ "baseFilters": [
{ {
"key": "baseFilterTest", "key": "baseFilterTest",

@ -74,6 +74,7 @@ export function sceneVariablesSetToVariables(set: SceneVariables, keepQueryOptio
allValue: variable.state.allValue, allValue: variable.state.allValue,
includeAll: variable.state.includeAll, includeAll: variable.state.includeAll,
multi: variable.state.isMulti, multi: variable.state.isMulti,
allowCustomValue: variable.state.allowCustomValue,
skipUrlSync: variable.state.skipUrlSync, skipUrlSync: variable.state.skipUrlSync,
}); });
} else if (sceneUtils.isCustomVariable(variable)) { } else if (sceneUtils.isCustomVariable(variable)) {
@ -90,6 +91,7 @@ export function sceneVariablesSetToVariables(set: SceneVariables, keepQueryOptio
multi: variable.state.isMulti, multi: variable.state.isMulti,
allValue: variable.state.allValue, allValue: variable.state.allValue,
includeAll: variable.state.includeAll, includeAll: variable.state.includeAll,
allowCustomValue: variable.state.allowCustomValue,
}); });
} else if (sceneUtils.isDataSourceVariable(variable)) { } else if (sceneUtils.isDataSourceVariable(variable)) {
variables.push({ variables.push({
@ -107,6 +109,7 @@ export function sceneVariablesSetToVariables(set: SceneVariables, keepQueryOptio
multi: variable.state.isMulti, multi: variable.state.isMulti,
allValue: variable.state.allValue, allValue: variable.state.allValue,
includeAll: variable.state.includeAll, includeAll: variable.state.includeAll,
allowCustomValue: variable.state.allowCustomValue,
}); });
} else if (sceneUtils.isConstantVariable(variable)) { } else if (sceneUtils.isConstantVariable(variable)) {
variables.push({ variables.push({
@ -175,6 +178,7 @@ export function sceneVariablesSetToVariables(set: SceneVariables, keepQueryOptio
name: variable.state.name, name: variable.state.name,
type: 'adhoc', type: 'adhoc',
datasource: variable.state.datasource, datasource: variable.state.datasource,
allowCustomValue: variable.state.allowCustomValue,
// @ts-expect-error // @ts-expect-error
baseFilters: variable.state.baseFilters, baseFilters: variable.state.baseFilters,
filters: variable.state.filters, filters: variable.state.filters,

@ -63,6 +63,22 @@ describe('AdHocVariableForm', () => {
expect(onDataSourceChange).toHaveBeenCalledWith(promDatasource, undefined); expect(onDataSourceChange).toHaveBeenCalledWith(promDatasource, undefined);
}); });
it('should render the form with allow custom value true', async () => {
const mockOnAllowCustomValueChange = jest.fn();
const { renderer } = await setup({
...defaultProps,
allowCustomValue: true,
onAllowCustomValueChange: mockOnAllowCustomValueChange,
});
const allowCustomValueCheckbox = renderer.getByTestId(
selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsAllowCustomValueSwitch
);
expect(allowCustomValueCheckbox).toBeInTheDocument();
expect(allowCustomValueCheckbox).toBeChecked();
});
it('should not render code editor when no default keys provided', async () => { it('should not render code editor when no default keys provided', async () => {
await setup(defaultProps); await setup(defaultProps);

@ -1,4 +1,4 @@
import { useCallback } from 'react'; import { FormEvent, useCallback } from 'react';
import { DataSourceInstanceSettings, MetricFindValue, readCSV } from '@grafana/data'; import { DataSourceInstanceSettings, MetricFindValue, readCSV } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
@ -6,21 +6,26 @@ import { DataSourceRef } from '@grafana/schema';
import { Alert, CodeEditor, Field, Switch } from '@grafana/ui'; import { Alert, CodeEditor, Field, Switch } from '@grafana/ui';
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
import { VariableCheckboxField } from './VariableCheckboxField';
import { VariableLegend } from './VariableLegend'; import { VariableLegend } from './VariableLegend';
export interface AdHocVariableFormProps { export interface AdHocVariableFormProps {
datasource?: DataSourceRef; datasource?: DataSourceRef;
onDataSourceChange: (dsSettings: DataSourceInstanceSettings) => void; onDataSourceChange: (dsSettings: DataSourceInstanceSettings) => void;
allowCustomValue?: boolean;
infoText?: string; infoText?: string;
defaultKeys?: MetricFindValue[]; defaultKeys?: MetricFindValue[];
onDefaultKeysChange?: (keys?: MetricFindValue[]) => void; onDefaultKeysChange?: (keys?: MetricFindValue[]) => void;
onAllowCustomValueChange?: (event: FormEvent<HTMLInputElement>) => void;
} }
export function AdHocVariableForm({ export function AdHocVariableForm({
datasource, datasource,
infoText, infoText,
allowCustomValue,
onDataSourceChange, onDataSourceChange,
onDefaultKeysChange, onDefaultKeysChange,
onAllowCustomValueChange,
defaultKeys, defaultKeys,
}: AdHocVariableFormProps) { }: AdHocVariableFormProps) {
const updateStaticKeys = useCallback( const updateStaticKeys = useCallback(
@ -80,6 +85,16 @@ export function AdHocVariableForm({
)} )}
</> </>
)} )}
{onAllowCustomValueChange && (
<VariableCheckboxField
value={allowCustomValue ?? true}
name="Allow custom values"
description="Enables users to add custom values to the list"
onChange={onAllowCustomValueChange}
testId={selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsAllowCustomValueSwitch}
/>
)}
</> </>
); );
} }

@ -9,6 +9,7 @@ describe('CustomVariableForm', () => {
const onMultiChange = jest.fn(); const onMultiChange = jest.fn();
const onIncludeAllChange = jest.fn(); const onIncludeAllChange = jest.fn();
const onAllValueChange = jest.fn(); const onAllValueChange = jest.fn();
const onAllowCustomValueChange = jest.fn();
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
@ -21,10 +22,12 @@ describe('CustomVariableForm', () => {
multi={true} multi={true}
allValue="custom value" allValue="custom value"
includeAll={true} includeAll={true}
allowCustomValue={true}
onQueryChange={onQueryChange} onQueryChange={onQueryChange}
onMultiChange={onMultiChange} onMultiChange={onMultiChange}
onIncludeAllChange={onIncludeAllChange} onIncludeAllChange={onIncludeAllChange}
onAllValueChange={onAllValueChange} onAllValueChange={onAllValueChange}
onAllowCustomValueChange={onAllowCustomValueChange}
/> />
); );
@ -39,12 +42,18 @@ describe('CustomVariableForm', () => {
selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput
); );
const allowCustomValueCheckbox = getByTestId(
selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsAllowCustomValueSwitch
);
expect(queryInput).toBeInTheDocument(); expect(queryInput).toBeInTheDocument();
expect(queryInput).toHaveValue('query'); expect(queryInput).toHaveValue('query');
expect(multiCheckbox).toBeInTheDocument(); expect(multiCheckbox).toBeInTheDocument();
expect(multiCheckbox).toBeChecked(); expect(multiCheckbox).toBeChecked();
expect(includeAllCheckbox).toBeInTheDocument(); expect(includeAllCheckbox).toBeInTheDocument();
expect(includeAllCheckbox).toBeChecked(); expect(includeAllCheckbox).toBeChecked();
expect(allowCustomValueCheckbox).toBeInTheDocument();
expect(allowCustomValueCheckbox).toBeChecked();
expect(allValueInput).toBeInTheDocument(); expect(allValueInput).toBeInTheDocument();
expect(allValueInput).toHaveValue('custom value'); expect(allValueInput).toHaveValue('custom value');
}); });
@ -56,10 +65,12 @@ describe('CustomVariableForm', () => {
multi={true} multi={true}
allValue="" allValue=""
includeAll={true} includeAll={true}
allowCustomValue={true}
onQueryChange={onQueryChange} onQueryChange={onQueryChange}
onMultiChange={onMultiChange} onMultiChange={onMultiChange}
onIncludeAllChange={onIncludeAllChange} onIncludeAllChange={onIncludeAllChange}
onAllValueChange={onAllValueChange} onAllValueChange={onAllValueChange}
onAllowCustomValueChange={onAllowCustomValueChange}
/> />
); );
@ -73,13 +84,18 @@ describe('CustomVariableForm', () => {
const allValueInput = getByTestId( const allValueInput = getByTestId(
selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput
); );
const allowCustomValueCheckbox = getByTestId(
selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsAllowCustomValueSwitch
);
fireEvent.click(multiCheckbox); fireEvent.click(multiCheckbox);
fireEvent.click(includeAllCheckbox); fireEvent.click(includeAllCheckbox);
fireEvent.click(allowCustomValueCheckbox);
fireEvent.change(queryInput, { currentTarget: { value: 'test query' } }); fireEvent.change(queryInput, { currentTarget: { value: 'test query' } });
fireEvent.change(allValueInput, { currentTarget: { value: 'test value' } }); fireEvent.change(allValueInput, { currentTarget: { value: 'test value' } });
expect(onMultiChange).toHaveBeenCalledTimes(1); expect(onMultiChange).toHaveBeenCalledTimes(1);
expect(onAllowCustomValueChange).toHaveBeenCalledTimes(1);
expect(onIncludeAllChange).toHaveBeenCalledTimes(1); expect(onIncludeAllChange).toHaveBeenCalledTimes(1);
expect(onQueryChange).not.toHaveBeenCalledTimes(1); expect(onQueryChange).not.toHaveBeenCalledTimes(1);
expect(onAllValueChange).not.toHaveBeenCalledTimes(1); expect(onAllValueChange).not.toHaveBeenCalledTimes(1);
@ -92,10 +108,12 @@ describe('CustomVariableForm', () => {
multi={true} multi={true}
allValue="custom all value" allValue="custom all value"
includeAll={true} includeAll={true}
allowCustomValue={true}
onQueryChange={onQueryChange} onQueryChange={onQueryChange}
onMultiChange={onMultiChange} onMultiChange={onMultiChange}
onIncludeAllChange={onIncludeAllChange} onIncludeAllChange={onIncludeAllChange}
onAllValueChange={onAllValueChange} onAllValueChange={onAllValueChange}
onAllowCustomValueChange={onAllowCustomValueChange}
/> />
); );

@ -12,12 +12,14 @@ interface CustomVariableFormProps {
multi: boolean; multi: boolean;
allValue?: string | null; allValue?: string | null;
includeAll: boolean; includeAll: boolean;
allowCustomValue?: boolean;
onQueryChange: (event: FormEvent<HTMLTextAreaElement>) => void; onQueryChange: (event: FormEvent<HTMLTextAreaElement>) => void;
onMultiChange: (event: FormEvent<HTMLInputElement>) => void; onMultiChange: (event: FormEvent<HTMLInputElement>) => void;
onIncludeAllChange: (event: FormEvent<HTMLInputElement>) => void; onIncludeAllChange: (event: FormEvent<HTMLInputElement>) => void;
onAllValueChange: (event: FormEvent<HTMLInputElement>) => void; onAllValueChange: (event: FormEvent<HTMLInputElement>) => void;
onQueryBlur?: (event: FormEvent<HTMLTextAreaElement>) => void; onQueryBlur?: (event: FormEvent<HTMLTextAreaElement>) => void;
onAllValueBlur?: (event: FormEvent<HTMLInputElement>) => void; onAllValueBlur?: (event: FormEvent<HTMLInputElement>) => void;
onAllowCustomValueChange?: (event: FormEvent<HTMLInputElement>) => void;
} }
export function CustomVariableForm({ export function CustomVariableForm({
@ -25,10 +27,12 @@ export function CustomVariableForm({
multi, multi,
allValue, allValue,
includeAll, includeAll,
allowCustomValue,
onQueryChange, onQueryChange,
onMultiChange, onMultiChange,
onIncludeAllChange, onIncludeAllChange,
onAllValueChange, onAllValueChange,
onAllowCustomValueChange,
}: CustomVariableFormProps) { }: CustomVariableFormProps) {
return ( return (
<> <>
@ -48,9 +52,11 @@ export function CustomVariableForm({
multi={multi} multi={multi}
includeAll={includeAll} includeAll={includeAll}
allValue={allValue} allValue={allValue}
allowCustomValue={allowCustomValue}
onMultiChange={onMultiChange} onMultiChange={onMultiChange}
onIncludeAllChange={onIncludeAllChange} onIncludeAllChange={onIncludeAllChange}
onAllValueChange={onAllValueChange} onAllValueChange={onAllValueChange}
onAllowCustomValueChange={onAllowCustomValueChange}
/> />
</> </>
); );

@ -13,6 +13,7 @@ interface DataSourceVariableFormProps {
regex: string; regex: string;
multi: boolean; multi: boolean;
allValue?: string | null; allValue?: string | null;
allowCustomValue?: boolean;
includeAll: boolean; includeAll: boolean;
onChange: (option: SelectableValue) => void; onChange: (option: SelectableValue) => void;
optionTypes: Array<{ value: string; label: string }>; optionTypes: Array<{ value: string; label: string }>;
@ -20,6 +21,7 @@ interface DataSourceVariableFormProps {
onMultiChange: (event: FormEvent<HTMLInputElement>) => void; onMultiChange: (event: FormEvent<HTMLInputElement>) => void;
onIncludeAllChange: (event: FormEvent<HTMLInputElement>) => void; onIncludeAllChange: (event: FormEvent<HTMLInputElement>) => void;
onAllValueChange: (event: FormEvent<HTMLInputElement>) => void; onAllValueChange: (event: FormEvent<HTMLInputElement>) => void;
onAllowCustomValueChange?: (event: FormEvent<HTMLInputElement>) => void;
onQueryBlur?: (event: FormEvent<HTMLTextAreaElement>) => void; onQueryBlur?: (event: FormEvent<HTMLTextAreaElement>) => void;
onAllValueBlur?: (event: FormEvent<HTMLInputElement>) => void; onAllValueBlur?: (event: FormEvent<HTMLInputElement>) => void;
} }
@ -28,6 +30,7 @@ export function DataSourceVariableForm({
query, query,
regex, regex,
optionTypes, optionTypes,
allowCustomValue,
onChange, onChange,
onRegExBlur, onRegExBlur,
multi, multi,
@ -36,6 +39,7 @@ export function DataSourceVariableForm({
onMultiChange, onMultiChange,
onIncludeAllChange, onIncludeAllChange,
onAllValueChange, onAllValueChange,
onAllowCustomValueChange,
}: DataSourceVariableFormProps) { }: DataSourceVariableFormProps) {
const typeValue = optionTypes.find((o) => o.value === query) ?? optionTypes[0]; const typeValue = optionTypes.find((o) => o.value === query) ?? optionTypes[0];
@ -70,9 +74,11 @@ export function DataSourceVariableForm({
multi={multi} multi={multi}
includeAll={includeAll} includeAll={includeAll}
allValue={allValue} allValue={allValue}
allowCustomValue={allowCustomValue}
onMultiChange={onMultiChange} onMultiChange={onMultiChange}
onIncludeAllChange={onIncludeAllChange} onIncludeAllChange={onIncludeAllChange}
onAllValueChange={onAllValueChange} onAllValueChange={onAllValueChange}
onAllowCustomValueChange={onAllowCustomValueChange}
/> />
</> </>
); );

@ -76,6 +76,7 @@ describe('QueryVariableEditorForm', () => {
const mockOnMultiChange = jest.fn(); const mockOnMultiChange = jest.fn();
const mockOnIncludeAllChange = jest.fn(); const mockOnIncludeAllChange = jest.fn();
const mockOnAllValueChange = jest.fn(); const mockOnAllValueChange = jest.fn();
const mockOnAllowCustomValueChange = jest.fn();
const defaultProps: React.ComponentProps<typeof QueryVariableEditorForm> = { const defaultProps: React.ComponentProps<typeof QueryVariableEditorForm> = {
datasource: { uid: defaultDatasource.uid, type: defaultDatasource.type }, datasource: { uid: defaultDatasource.uid, type: defaultDatasource.type },
@ -90,12 +91,14 @@ describe('QueryVariableEditorForm', () => {
onSortChange: mockOnSortChange, onSortChange: mockOnSortChange,
refresh: VariableRefresh.onDashboardLoad, refresh: VariableRefresh.onDashboardLoad,
onRefreshChange: mockOnRefreshChange, onRefreshChange: mockOnRefreshChange,
allowCustomValue: true,
isMulti: true, isMulti: true,
onMultiChange: mockOnMultiChange, onMultiChange: mockOnMultiChange,
includeAll: true, includeAll: true,
onIncludeAllChange: mockOnIncludeAllChange, onIncludeAllChange: mockOnIncludeAllChange,
allValue: 'custom all value', allValue: 'custom all value',
onAllValueChange: mockOnAllValueChange, onAllValueChange: mockOnAllValueChange,
onAllowCustomValueChange: mockOnAllowCustomValueChange,
}; };
async function setup(props?: React.ComponentProps<typeof QueryVariableEditorForm>) { async function setup(props?: React.ComponentProps<typeof QueryVariableEditorForm>) {
@ -135,6 +138,9 @@ describe('QueryVariableEditorForm', () => {
const allValueInput = getByTestId( const allValueInput = getByTestId(
selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput
); );
const allowCustomValueSwitch = getByTestId(
selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsAllowCustomValueSwitch
);
expect(dataSourcePicker).toBeInTheDocument(); expect(dataSourcePicker).toBeInTheDocument();
expect(dataSourcePicker.getAttribute('placeholder')).toBe('Default Test Data Source'); expect(dataSourcePicker.getAttribute('placeholder')).toBe('Default Test Data Source');
@ -146,6 +152,8 @@ describe('QueryVariableEditorForm', () => {
expect(getByRole('radio', { name: 'On dashboard load' })).toBeChecked(); expect(getByRole('radio', { name: 'On dashboard load' })).toBeChecked();
expect(multiSwitch).toBeInTheDocument(); expect(multiSwitch).toBeInTheDocument();
expect(multiSwitch).toBeChecked(); expect(multiSwitch).toBeChecked();
expect(allowCustomValueSwitch).toBeInTheDocument();
expect(allowCustomValueSwitch).toBeChecked();
expect(includeAllSwitch).toBeInTheDocument(); expect(includeAllSwitch).toBeInTheDocument();
expect(includeAllSwitch).toBeChecked(); expect(includeAllSwitch).toBeChecked();
expect(allValueInput).toBeInTheDocument(); expect(allValueInput).toBeInTheDocument();
@ -257,6 +265,20 @@ describe('QueryVariableEditorForm', () => {
).toBeChecked(); ).toBeChecked();
}); });
it('should call onAllowCustomValue when changing the allow custom value switch', async () => {
const {
renderer: { getByTestId },
} = await setup();
const allowCustomValue = getByTestId(
selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsAllowCustomValueSwitch
);
await userEvent.click(allowCustomValue);
expect(mockOnAllowCustomValueChange).toHaveBeenCalledTimes(1);
expect(
(mockOnAllowCustomValueChange.mock.calls[0][0] as FormEvent<HTMLInputElement>).target as HTMLInputElement
).toBeChecked();
});
it('should call onAllValueChange when changing the all value', async () => { it('should call onAllValueChange when changing the all value', async () => {
const { const {
renderer: { getByTestId }, renderer: { getByTestId },

@ -34,6 +34,8 @@ interface QueryVariableEditorFormProps {
onRefreshChange: (option: VariableRefresh) => void; onRefreshChange: (option: VariableRefresh) => void;
isMulti: boolean; isMulti: boolean;
onMultiChange: (event: FormEvent<HTMLInputElement>) => void; onMultiChange: (event: FormEvent<HTMLInputElement>) => void;
allowCustomValue?: boolean;
onAllowCustomValueChange?: (event: FormEvent<HTMLInputElement>) => void;
includeAll: boolean; includeAll: boolean;
onIncludeAllChange: (event: FormEvent<HTMLInputElement>) => void; onIncludeAllChange: (event: FormEvent<HTMLInputElement>) => void;
allValue: string; allValue: string;
@ -55,6 +57,8 @@ export function QueryVariableEditorForm({
onRefreshChange, onRefreshChange,
isMulti, isMulti,
onMultiChange, onMultiChange,
allowCustomValue,
onAllowCustomValueChange,
includeAll, includeAll,
onIncludeAllChange, onIncludeAllChange,
allValue, allValue,
@ -126,10 +130,12 @@ export function QueryVariableEditorForm({
<SelectionOptionsForm <SelectionOptionsForm
multi={!!isMulti} multi={!!isMulti}
includeAll={!!includeAll} includeAll={!!includeAll}
allowCustomValue={allowCustomValue}
allValue={allValue} allValue={allValue}
onMultiChange={onMultiChange} onMultiChange={onMultiChange}
onIncludeAllChange={onIncludeAllChange} onIncludeAllChange={onIncludeAllChange}
onAllValueChange={onAllValueChange} onAllValueChange={onAllValueChange}
onAllowCustomValueChange={onAllowCustomValueChange}
/> />
</> </>
); );

@ -8,17 +8,21 @@ import { VariableTextField } from 'app/features/dashboard-scene/settings/variabl
interface SelectionOptionsFormProps { interface SelectionOptionsFormProps {
multi: boolean; multi: boolean;
includeAll: boolean; includeAll: boolean;
allowCustomValue?: boolean;
allValue?: string | null; allValue?: string | null;
onMultiChange: (event: ChangeEvent<HTMLInputElement>) => void; onMultiChange: (event: ChangeEvent<HTMLInputElement>) => void;
onAllowCustomValueChange?: (event: ChangeEvent<HTMLInputElement>) => void;
onIncludeAllChange: (event: ChangeEvent<HTMLInputElement>) => void; onIncludeAllChange: (event: ChangeEvent<HTMLInputElement>) => void;
onAllValueChange: (event: FormEvent<HTMLInputElement>) => void; onAllValueChange: (event: FormEvent<HTMLInputElement>) => void;
} }
export function SelectionOptionsForm({ export function SelectionOptionsForm({
multi, multi,
allowCustomValue,
includeAll, includeAll,
allValue, allValue,
onMultiChange, onMultiChange,
onAllowCustomValueChange,
onIncludeAllChange, onIncludeAllChange,
onAllValueChange, onAllValueChange,
}: SelectionOptionsFormProps) { }: SelectionOptionsFormProps) {
@ -31,6 +35,15 @@ export function SelectionOptionsForm({
onChange={onMultiChange} onChange={onMultiChange}
testId={selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsMultiSwitch} testId={selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsMultiSwitch}
/> />
{onAllowCustomValueChange && ( // backwards compat with old arch, remove on cleanup
<VariableCheckboxField
value={allowCustomValue ?? true}
name="Allow custom values"
description="Enables users to add custom values to the list"
onChange={onAllowCustomValueChange}
testId={selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsAllowCustomValueSwitch}
/>
)}
<VariableCheckboxField <VariableCheckboxField
value={includeAll} value={includeAll}
name="Include All option" name="Include All option"

@ -70,7 +70,12 @@ describe('AdHocFiltersVariableEditor', () => {
const infoText = renderer.getByTestId( const infoText = renderer.getByTestId(
selectors.pages.Dashboard.Settings.Variables.Edit.AdHocFiltersVariable.infoText selectors.pages.Dashboard.Settings.Variables.Edit.AdHocFiltersVariable.infoText
); );
const allowCustomValueCheckbox = renderer.getByTestId(
selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsAllowCustomValueSwitch
);
expect(allowCustomValueCheckbox).toBeInTheDocument();
expect(allowCustomValueCheckbox).toBeChecked();
expect(dataSourcePicker).toBeInTheDocument(); expect(dataSourcePicker).toBeInTheDocument();
expect(dataSourcePicker.getAttribute('placeholder')).toBe('Default Test Data Source'); expect(dataSourcePicker.getAttribute('placeholder')).toBe('Default Test Data Source');
expect(infoText).toBeInTheDocument(); expect(infoText).toBeInTheDocument();
@ -132,6 +137,7 @@ async function setup(props?: React.ComponentProps<typeof AdHocFiltersVariableEdi
value: 'baseTestValue', value: 'baseTestValue',
}, },
], ],
allowCustomValue: true,
defaultKeys: withDefaultKeys ? [{ text: 'A', value: 'A' }] : undefined, defaultKeys: withDefaultKeys ? [{ text: 'A', value: 'A' }] : undefined,
}); });
return { return {

@ -1,3 +1,4 @@
import { FormEvent } from 'react';
import { useAsync } from 'react-use'; import { useAsync } from 'react-use';
import { DataSourceInstanceSettings, MetricFindValue, getDataSourceRef } from '@grafana/data'; import { DataSourceInstanceSettings, MetricFindValue, getDataSourceRef } from '@grafana/data';
@ -13,7 +14,7 @@ interface AdHocFiltersVariableEditorProps {
export function AdHocFiltersVariableEditor(props: AdHocFiltersVariableEditorProps) { export function AdHocFiltersVariableEditor(props: AdHocFiltersVariableEditorProps) {
const { variable } = props; const { variable } = props;
const { datasource: datasourceRef, defaultKeys } = variable.useState(); const { datasource: datasourceRef, defaultKeys, allowCustomValue } = variable.useState();
const { value: datasourceSettings } = useAsync(async () => { const { value: datasourceSettings } = useAsync(async () => {
return await getDataSourceSrv().get(datasourceRef); return await getDataSourceSrv().get(datasourceRef);
@ -38,13 +39,19 @@ export function AdHocFiltersVariableEditor(props: AdHocFiltersVariableEditorProp
}); });
}; };
const onAllowCustomValueChange = (event: FormEvent<HTMLInputElement>) => {
variable.setState({ allowCustomValue: event.currentTarget.checked });
};
return ( return (
<AdHocVariableForm <AdHocVariableForm
datasource={datasourceRef ?? undefined} datasource={datasourceRef ?? undefined}
infoText={message} infoText={message}
allowCustomValue={allowCustomValue}
onDataSourceChange={onDataSourceChange} onDataSourceChange={onDataSourceChange}
defaultKeys={defaultKeys} defaultKeys={defaultKeys}
onDefaultKeysChange={onDefaultKeysChange} onDefaultKeysChange={onDefaultKeysChange}
onAllowCustomValueChange={onAllowCustomValueChange}
/> />
); );
} }

@ -55,11 +55,17 @@ describe('CustomVariableEditor', () => {
selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsIncludeAllSwitch selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsIncludeAllSwitch
); );
const allowCustomValueCheckbox = getByTestId(
selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsAllowCustomValueSwitch
);
// It include-all-custom input appears after include-all checkbox is checked only // It include-all-custom input appears after include-all checkbox is checked only
expect(() => expect(() =>
getByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput) getByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput)
).toThrow('Unable to find an element'); ).toThrow('Unable to find an element');
fireEvent.click(allowCustomValueCheckbox);
fireEvent.click(multiCheckbox); fireEvent.click(multiCheckbox);
fireEvent.click(includeAllCheckbox); fireEvent.click(includeAllCheckbox);
@ -69,6 +75,7 @@ describe('CustomVariableEditor', () => {
expect(variable.state.isMulti).toBe(true); expect(variable.state.isMulti).toBe(true);
expect(variable.state.includeAll).toBe(true); expect(variable.state.includeAll).toBe(true);
expect(variable.state.allowCustomValue).toBe(false);
expect(allValueInput).toBeInTheDocument(); expect(allValueInput).toBeInTheDocument();
}); });

@ -10,7 +10,7 @@ interface CustomVariableEditorProps {
} }
export function CustomVariableEditor({ variable, onRunQuery }: CustomVariableEditorProps) { export function CustomVariableEditor({ variable, onRunQuery }: CustomVariableEditorProps) {
const { query, isMulti, allValue, includeAll } = variable.useState(); const { query, isMulti, allValue, includeAll, allowCustomValue } = variable.useState();
const onMultiChange = (event: FormEvent<HTMLInputElement>) => { const onMultiChange = (event: FormEvent<HTMLInputElement>) => {
variable.setState({ isMulti: event.currentTarget.checked }); variable.setState({ isMulti: event.currentTarget.checked });
@ -25,6 +25,9 @@ export function CustomVariableEditor({ variable, onRunQuery }: CustomVariableEdi
const onAllValueChange = (event: FormEvent<HTMLInputElement>) => { const onAllValueChange = (event: FormEvent<HTMLInputElement>) => {
variable.setState({ allValue: event.currentTarget.value }); variable.setState({ allValue: event.currentTarget.value });
}; };
const onAllowCustomValueChange = (event: FormEvent<HTMLInputElement>) => {
variable.setState({ allowCustomValue: event.currentTarget.checked });
};
return ( return (
<CustomVariableForm <CustomVariableForm
@ -32,10 +35,12 @@ export function CustomVariableEditor({ variable, onRunQuery }: CustomVariableEdi
multi={!!isMulti} multi={!!isMulti}
allValue={allValue ?? ''} allValue={allValue ?? ''}
includeAll={!!includeAll} includeAll={!!includeAll}
allowCustomValue={allowCustomValue}
onMultiChange={onMultiChange} onMultiChange={onMultiChange}
onIncludeAllChange={onIncludeAllChange} onIncludeAllChange={onIncludeAllChange}
onQueryChange={onQueryChange} onQueryChange={onQueryChange}
onAllValueChange={onAllValueChange} onAllValueChange={onAllValueChange}
onAllowCustomValueChange={onAllowCustomValueChange}
/> />
); );
} }

@ -61,6 +61,9 @@ describe('DataSourceVariableEditor', () => {
const includeAllCheckbox = getByTestId( const includeAllCheckbox = getByTestId(
selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsIncludeAllSwitch selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsIncludeAllSwitch
); );
const allowCustomValueCheckbox = getByTestId(
selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsAllowCustomValueSwitch
);
const typeSelect = getByTestId( const typeSelect = getByTestId(
selectors.pages.Dashboard.Settings.Variables.Edit.DatasourceVariable.datasourceSelect selectors.pages.Dashboard.Settings.Variables.Edit.DatasourceVariable.datasourceSelect
@ -69,6 +72,8 @@ describe('DataSourceVariableEditor', () => {
expect(typeSelect.textContent).toBe('ds1'); expect(typeSelect.textContent).toBe('ds1');
expect(multiCheckbox).toBeInTheDocument(); expect(multiCheckbox).toBeInTheDocument();
expect(multiCheckbox).not.toBeChecked(); expect(multiCheckbox).not.toBeChecked();
expect(allowCustomValueCheckbox).toBeInTheDocument();
expect(allowCustomValueCheckbox).toBeChecked();
expect(includeAllCheckbox).toBeInTheDocument(); expect(includeAllCheckbox).toBeInTheDocument();
expect(includeAllCheckbox).not.toBeChecked(); expect(includeAllCheckbox).not.toBeChecked();
}); });

@ -12,7 +12,7 @@ interface DataSourceVariableEditorProps {
} }
export function DataSourceVariableEditor({ variable, onRunQuery }: DataSourceVariableEditorProps) { export function DataSourceVariableEditor({ variable, onRunQuery }: DataSourceVariableEditorProps) {
const { pluginId, regex, isMulti, allValue, includeAll } = variable.useState(); const { pluginId, regex, isMulti, allValue, includeAll, allowCustomValue } = variable.useState();
const optionTypes = getOptionDataSourceTypes(); const optionTypes = getOptionDataSourceTypes();
@ -44,6 +44,10 @@ export function DataSourceVariableEditor({ variable, onRunQuery }: DataSourceVar
variable.setState({ allValue: event.currentTarget.value }); variable.setState({ allValue: event.currentTarget.value });
}; };
const onAllowCustomValueChange = (event: FormEvent<HTMLInputElement>) => {
variable.setState({ allowCustomValue: event.currentTarget.checked });
};
return ( return (
<DataSourceVariableForm <DataSourceVariableForm
query={pluginId} query={pluginId}
@ -52,11 +56,13 @@ export function DataSourceVariableEditor({ variable, onRunQuery }: DataSourceVar
allValue={allValue} allValue={allValue}
includeAll={includeAll || false} includeAll={includeAll || false}
optionTypes={optionTypes} optionTypes={optionTypes}
allowCustomValue={allowCustomValue}
onChange={onChangeType} onChange={onChangeType}
onRegExBlur={onRegExChange} onRegExBlur={onRegExChange}
onMultiChange={onMultiChange} onMultiChange={onMultiChange}
onIncludeAllChange={onIncludeAllChange} onIncludeAllChange={onIncludeAllChange}
onAllValueChange={onAllValueChange} onAllValueChange={onAllValueChange}
onAllowCustomValueChange={onAllowCustomValueChange}
/> />
); );
} }

@ -117,6 +117,10 @@ describe('QueryVariableEditor', () => {
selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput
); );
const allowCustomValueCheckbox = renderer.getByTestId(
selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsAllowCustomValueSwitch
);
expect(dataSourcePicker).toBeInTheDocument(); expect(dataSourcePicker).toBeInTheDocument();
expect(dataSourcePicker.getAttribute('placeholder')).toBe('Default Test Data Source'); expect(dataSourcePicker.getAttribute('placeholder')).toBe('Default Test Data Source');
expect(queryEditor).toBeInTheDocument(); expect(queryEditor).toBeInTheDocument();
@ -129,6 +133,8 @@ describe('QueryVariableEditor', () => {
expect(getByRole(refreshSelect, 'radio', { name: 'On dashboard load' })).toBeChecked(); expect(getByRole(refreshSelect, 'radio', { name: 'On dashboard load' })).toBeChecked();
expect(multiSwitch).toBeInTheDocument(); expect(multiSwitch).toBeInTheDocument();
expect(multiSwitch).toBeChecked(); expect(multiSwitch).toBeChecked();
expect(allowCustomValueCheckbox).toBeInTheDocument();
expect(allowCustomValueCheckbox).toBeChecked();
expect(includeAllSwitch).toBeInTheDocument(); expect(includeAllSwitch).toBeInTheDocument();
expect(includeAllSwitch).toBeChecked(); expect(includeAllSwitch).toBeChecked();
expect(allValueInput).toBeInTheDocument(); expect(allValueInput).toBeInTheDocument();

@ -14,7 +14,8 @@ interface QueryVariableEditorProps {
type VariableQueryType = QueryVariable['state']['query']; type VariableQueryType = QueryVariable['state']['query'];
export function QueryVariableEditor({ variable, onRunQuery }: QueryVariableEditorProps) { export function QueryVariableEditor({ variable, onRunQuery }: QueryVariableEditorProps) {
const { datasource, regex, sort, refresh, isMulti, includeAll, allValue, query } = variable.useState(); const { datasource, regex, sort, refresh, isMulti, includeAll, allValue, query, allowCustomValue } =
variable.useState();
const { value: timeRange } = sceneGraph.getTimeRange(variable).useState(); const { value: timeRange } = sceneGraph.getTimeRange(variable).useState();
const onRegExChange = (event: React.FormEvent<HTMLTextAreaElement>) => { const onRegExChange = (event: React.FormEvent<HTMLTextAreaElement>) => {
@ -35,6 +36,9 @@ export function QueryVariableEditor({ variable, onRunQuery }: QueryVariableEdito
const onAllValueChange = (event: FormEvent<HTMLInputElement>) => { const onAllValueChange = (event: FormEvent<HTMLInputElement>) => {
variable.setState({ allValue: event.currentTarget.value }); variable.setState({ allValue: event.currentTarget.value });
}; };
const onAllowCustomValueChange = (event: FormEvent<HTMLInputElement>) => {
variable.setState({ allowCustomValue: event.currentTarget.checked });
};
const onDataSourceChange = (dsInstanceSettings: DataSourceInstanceSettings) => { const onDataSourceChange = (dsInstanceSettings: DataSourceInstanceSettings) => {
const datasource = getDataSourceRef(dsInstanceSettings); const datasource = getDataSourceRef(dsInstanceSettings);
@ -78,6 +82,8 @@ export function QueryVariableEditor({ variable, onRunQuery }: QueryVariableEdito
onIncludeAllChange={onIncludeAllChange} onIncludeAllChange={onIncludeAllChange}
allValue={allValue ?? ''} allValue={allValue ?? ''}
onAllValueChange={onAllValueChange} onAllValueChange={onAllValueChange}
allowCustomValue={allowCustomValue}
onAllowCustomValueChange={onAllowCustomValueChange}
/> />
); );
} }

@ -45,6 +45,7 @@ describe('when creating variables objects', () => {
hide: 0, hide: 0,
includeAll: false, includeAll: false,
multi: false, multi: false,
allowCustomValue: true,
name: 'query0', name: 'query0',
options: [ options: [
{ {
@ -91,6 +92,7 @@ describe('when creating variables objects', () => {
description: null, description: null,
includeAll: false, includeAll: false,
isMulti: false, isMulti: false,
allowCustomValue: true,
label: undefined, label: undefined,
name: 'query0', name: 'query0',
options: [], options: [],
@ -106,6 +108,7 @@ describe('when creating variables objects', () => {
it('should migrate query variable with definition', () => { it('should migrate query variable with definition', () => {
const variable: QueryVariableModel = { const variable: QueryVariableModel = {
allValue: null, allValue: null,
allowCustomValue: false,
current: { current: {
text: 'America', text: 'America',
value: 'America', value: 'America',
@ -164,6 +167,7 @@ describe('when creating variables objects', () => {
expect(migrated).toBeInstanceOf(QueryVariable); expect(migrated).toBeInstanceOf(QueryVariable);
expect(rest).toEqual({ expect(rest).toEqual({
allValue: undefined, allValue: undefined,
allowCustomValue: false,
datasource: { datasource: {
type: 'influxdb', type: 'influxdb',
uid: 'P15396BDD62B2BE29', uid: 'P15396BDD62B2BE29',
@ -191,6 +195,7 @@ describe('when creating variables objects', () => {
it('should migrate datasource variable', () => { it('should migrate datasource variable', () => {
const variable: DataSourceVariableModel = { const variable: DataSourceVariableModel = {
id: 'query1', id: 'query1',
allowCustomValue: true,
rootStateKey: 'N4XLmH5Vz', rootStateKey: 'N4XLmH5Vz',
name: 'query1', name: 'query1',
type: 'datasource', type: 'datasource',
@ -237,6 +242,7 @@ describe('when creating variables objects', () => {
expect(migrated).toBeInstanceOf(DataSourceVariable); expect(migrated).toBeInstanceOf(DataSourceVariable);
expect(rest).toEqual({ expect(rest).toEqual({
allValue: 'Custom all', allValue: 'Custom all',
allowCustomValue: true,
defaultToAll: true, defaultToAll: true,
includeAll: true, includeAll: true,
label: undefined, label: undefined,
@ -385,6 +391,7 @@ describe('when creating variables objects', () => {
it('should migrate adhoc variable', () => { it('should migrate adhoc variable', () => {
const variable: TypedVariableModel = { const variable: TypedVariableModel = {
id: 'adhoc', id: 'adhoc',
allowCustomValue: false,
global: false, global: false,
index: 0, index: 0,
state: LoadingState.Done, state: LoadingState.Done,
@ -423,6 +430,7 @@ describe('when creating variables objects', () => {
expect(filterVarState).toEqual({ expect(filterVarState).toEqual({
key: expect.any(String), key: expect.any(String),
description: 'Adhoc Description', description: 'Adhoc Description',
allowCustomValue: false,
hide: 0, hide: 0,
label: 'Adhoc Label', label: 'Adhoc Label',
name: 'adhoc', name: 'adhoc',
@ -493,6 +501,7 @@ describe('when creating variables objects', () => {
expect(filterVarState).toEqual({ expect(filterVarState).toEqual({
key: expect.any(String), key: expect.any(String),
description: 'Adhoc Description', description: 'Adhoc Description',
allowCustomValue: true,
hide: 0, hide: 0,
label: 'Adhoc Label', label: 'Adhoc Label',
name: 'adhoc', name: 'adhoc',
@ -663,6 +672,7 @@ describe('when creating variables objects', () => {
expect(migrated).toBeInstanceOf(DataSourceVariable); expect(migrated).toBeInstanceOf(DataSourceVariable);
expect(rest).toEqual({ expect(rest).toEqual({
allValue: 'Custom all', allValue: 'Custom all',
allowCustomValue: true,
defaultToAll: true, defaultToAll: true,
includeAll: true, includeAll: true,
label: undefined, label: undefined,
@ -705,6 +715,7 @@ describe('when creating variables objects', () => {
expect(migrated).toBeInstanceOf(DataSourceVariable); expect(migrated).toBeInstanceOf(DataSourceVariable);
expect(rest).toEqual({ expect(rest).toEqual({
allValue: 'Custom all', allValue: 'Custom all',
allowCustomValue: true,
defaultToAll: true, defaultToAll: true,
includeAll: true, includeAll: true,
label: undefined, label: undefined,

@ -138,6 +138,7 @@ export function createSceneVariableFromVariableModel(variable: TypedVariableMode
filters: variable.filters ?? [], filters: variable.filters ?? [],
baseFilters: variable.baseFilters ?? [], baseFilters: variable.baseFilters ?? [],
defaultKeys: variable.defaultKeys, defaultKeys: variable.defaultKeys,
allowCustomValue: variable.allowCustomValue ?? true,
useQueriesAsFilterForOptions: true, useQueriesAsFilterForOptions: true,
layout: config.featureToggles.newFiltersUI ? 'combobox' : undefined, layout: config.featureToggles.newFiltersUI ? 'combobox' : undefined,
supportsMultiValueOperators: Boolean( supportsMultiValueOperators: Boolean(
@ -158,6 +159,7 @@ export function createSceneVariableFromVariableModel(variable: TypedVariableMode
defaultToAll: Boolean(variable.includeAll), defaultToAll: Boolean(variable.includeAll),
skipUrlSync: variable.skipUrlSync, skipUrlSync: variable.skipUrlSync,
hide: variable.hide, hide: variable.hide,
allowCustomValue: variable.allowCustomValue ?? true,
}); });
} else if (variable.type === 'query') { } else if (variable.type === 'query') {
return new QueryVariable({ return new QueryVariable({
@ -177,6 +179,7 @@ export function createSceneVariableFromVariableModel(variable: TypedVariableMode
skipUrlSync: variable.skipUrlSync, skipUrlSync: variable.skipUrlSync,
hide: variable.hide, hide: variable.hide,
definition: variable.definition, definition: variable.definition,
allowCustomValue: variable.allowCustomValue ?? true,
}); });
} else if (variable.type === 'datasource') { } else if (variable.type === 'datasource') {
return new DataSourceVariable({ return new DataSourceVariable({
@ -192,6 +195,7 @@ export function createSceneVariableFromVariableModel(variable: TypedVariableMode
isMulti: variable.multi, isMulti: variable.multi,
hide: variable.hide, hide: variable.hide,
defaultOptionEnabled: variable.current?.value === DEFAULT_DATASOURCE && variable.current?.text === 'default', defaultOptionEnabled: variable.current?.value === DEFAULT_DATASOURCE && variable.current?.text === 'default',
allowCustomValue: variable.allowCustomValue ?? true,
}); });
} else if (variable.type === 'interval') { } else if (variable.type === 'interval') {
const intervals = getIntervalsFromQueryString(variable.query); const intervals = getIntervalsFromQueryString(variable.query);

Loading…
Cancel
Save