diff --git a/.betterer.results b/.betterer.results
index ba53b876c21..1f976380ac4 100644
--- a/.betterer.results
+++ b/.betterer.results
@@ -4330,9 +4330,6 @@ exports[`better eslint`] = {
"public/app/features/variables/datasource/actions.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
- "public/app/features/variables/editor/LegacyVariableQueryEditor.tsx:5381": [
- [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
- ],
"public/app/features/variables/editor/VariableEditorContainer.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"]
diff --git a/packages/grafana-e2e-selectors/src/selectors/pages.ts b/packages/grafana-e2e-selectors/src/selectors/pages.ts
index 285b0447aa2..20bb47d9029 100644
--- a/packages/grafana-e2e-selectors/src/selectors/pages.ts
+++ b/packages/grafana-e2e-selectors/src/selectors/pages.ts
@@ -145,14 +145,14 @@ export const Pages = {
applyButton: 'data-testid Variable editor Apply button',
},
QueryVariable: {
- queryOptionsDataSourceSelect: Components.DataSourcePicker.container,
+ queryOptionsDataSourceSelect: Components.DataSourcePicker.inputV2,
queryOptionsRefreshSelect: 'Variable editor Form Query Refresh select',
queryOptionsRefreshSelectV2: 'data-testid Variable editor Form Query Refresh select',
queryOptionsRegExInput: 'Variable editor Form Query RegEx field',
queryOptionsRegExInputV2: 'data-testid Variable editor Form Query RegEx field',
queryOptionsSortSelect: 'Variable editor Form Query Sort select',
queryOptionsSortSelectV2: 'data-testid Variable editor Form Query Sort select',
- queryOptionsQueryInput: 'Variable editor Form Default Variable Query Editor textarea',
+ queryOptionsQueryInput: 'data-testid Variable editor Form Default Variable Query Editor textarea',
valueGroupsTagsEnabledSwitch: 'Variable editor Form Query UseTags switch',
valueGroupsTagsTagsQueryInput: 'Variable editor Form Query TagsQuery field',
valueGroupsTagsTagsValuesQueryInput: 'Variable editor Form Query TagsValuesQuery field',
diff --git a/public/app/features/dashboard-scene/settings/variables/components/QueryEditor.tsx b/public/app/features/dashboard-scene/settings/variables/components/QueryEditor.tsx
new file mode 100644
index 00000000000..18436c85c2b
--- /dev/null
+++ b/public/app/features/dashboard-scene/settings/variables/components/QueryEditor.tsx
@@ -0,0 +1,78 @@
+import React from 'react';
+
+import { DataSourceApi, LoadingState, TimeRange } from '@grafana/data';
+import { getTemplateSrv } from '@grafana/runtime';
+import { QueryVariable } from '@grafana/scenes';
+import { Text, Box } from '@grafana/ui';
+import { isLegacyQueryEditor, isQueryEditor } from 'app/features/variables/guard';
+import { VariableQueryEditorType } from 'app/features/variables/types';
+
+type VariableQueryType = QueryVariable['state']['query'];
+
+interface QueryEditorProps {
+ query: VariableQueryType;
+ datasource: DataSourceApi;
+ VariableQueryEditor: VariableQueryEditorType;
+ timeRange: TimeRange;
+ onLegacyQueryChange: (query: VariableQueryType, definition: string) => void;
+ onQueryChange: (query: VariableQueryType) => void;
+}
+
+export function QueryEditor({
+ query,
+ datasource,
+ VariableQueryEditor,
+ onLegacyQueryChange,
+ onQueryChange,
+ timeRange,
+}: QueryEditorProps) {
+ let queryWithDefaults;
+ if (typeof query === 'string') {
+ queryWithDefaults = query || (datasource.variables?.getDefaultQuery?.() ?? '');
+ } else {
+ queryWithDefaults = {
+ ...datasource.variables?.getDefaultQuery?.(),
+ ...query,
+ };
+ }
+
+ if (VariableQueryEditor && isLegacyQueryEditor(VariableQueryEditor, datasource)) {
+ return (
+
+ Query
+
+
+
+
+ );
+ }
+
+ if (VariableQueryEditor && isQueryEditor(VariableQueryEditor, datasource)) {
+ return (
+
+ Query
+
+ {}}
+ data={{ series: [], state: LoadingState.Done, timeRange }}
+ range={timeRange}
+ onBlur={() => {}}
+ history={[]}
+ />
+
+
+ );
+ }
+
+ return null;
+}
diff --git a/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.test.tsx b/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.test.tsx
new file mode 100644
index 00000000000..8e37364ccc7
--- /dev/null
+++ b/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.test.tsx
@@ -0,0 +1,269 @@
+import { render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import React, { FormEvent } from 'react';
+import { of } from 'rxjs';
+import { MockDataSourceApi } from 'test/mocks/datasource_srv';
+
+import {
+ LoadingState,
+ PanelData,
+ getDefaultTimeRange,
+ toDataFrame,
+ FieldType,
+ VariableSupportType,
+} from '@grafana/data';
+import { selectors } from '@grafana/e2e-selectors';
+import { setRunRequest } from '@grafana/runtime';
+import { VariableRefresh, VariableSort } from '@grafana/schema';
+import { mockDataSource } from 'app/features/alerting/unified/mocks';
+import { LegacyVariableQueryEditor } from 'app/features/variables/editor/LegacyVariableQueryEditor';
+
+import { QueryVariableEditorForm } from './QueryVariableForm';
+
+const defaultDatasource = mockDataSource({
+ name: 'Default Test Data Source',
+ type: 'test',
+});
+
+const promDatasource = mockDataSource({
+ name: 'Prometheus',
+ type: 'prometheus',
+});
+
+jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => ({
+ ...jest.requireActual('@grafana/runtime/src/services/dataSourceSrv'),
+ getDataSourceSrv: () => ({
+ get: async () => ({
+ ...defaultDatasource,
+ variables: {
+ getType: () => VariableSupportType.Custom,
+ query: jest.fn(),
+ editor: jest.fn().mockImplementation(LegacyVariableQueryEditor),
+ },
+ }),
+ getList: () => [defaultDatasource, promDatasource],
+ getInstanceSettings: () => ({ ...defaultDatasource }),
+ }),
+}));
+
+const runRequestMock = jest.fn().mockReturnValue(
+ of({
+ state: LoadingState.Done,
+ series: [
+ toDataFrame({
+ fields: [{ name: 'text', type: FieldType.string, values: ['val1', 'val2', 'val11'] }],
+ }),
+ ],
+ timeRange: getDefaultTimeRange(),
+ })
+);
+
+setRunRequest(runRequestMock);
+
+describe('QueryVariableEditorForm', () => {
+ const mockOnDataSourceChange = jest.fn();
+ const mockOnQueryChange = jest.fn();
+ const mockOnLegacyQueryChange = jest.fn();
+ const mockOnRegExChange = jest.fn();
+ const mockOnSortChange = jest.fn();
+ const mockOnRefreshChange = jest.fn();
+ const mockOnMultiChange = jest.fn();
+ const mockOnIncludeAllChange = jest.fn();
+ const mockOnAllValueChange = jest.fn();
+
+ const defaultProps = {
+ datasource: new MockDataSourceApi(promDatasource.name, undefined, promDatasource.meta),
+ onDataSourceChange: mockOnDataSourceChange,
+ query: 'my-query',
+ onQueryChange: mockOnQueryChange,
+ onLegacyQueryChange: mockOnLegacyQueryChange,
+ timeRange: getDefaultTimeRange(),
+ VariableQueryEditor: LegacyVariableQueryEditor,
+ regex: '.*',
+ onRegExChange: mockOnRegExChange,
+ sort: VariableSort.alphabeticalAsc,
+ onSortChange: mockOnSortChange,
+ refresh: VariableRefresh.onDashboardLoad,
+ onRefreshChange: mockOnRefreshChange,
+ isMulti: true,
+ onMultiChange: mockOnMultiChange,
+ includeAll: true,
+ onIncludeAllChange: mockOnIncludeAllChange,
+ allValue: 'custom all value',
+ onAllValueChange: mockOnAllValueChange,
+ };
+
+ function setup(props?: React.ComponentProps) {
+ return {
+ renderer: render(),
+ user: userEvent.setup(),
+ };
+ }
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should render the component with initializing the components correctly', () => {
+ const {
+ renderer: { getByTestId, getByRole },
+ } = setup();
+ const dataSourcePicker = getByTestId(selectors.components.DataSourcePicker.container);
+ //const queryEditor = getByTestId('query-editor');
+ const regexInput = getByTestId(
+ selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2
+ );
+ const sortSelect = getByTestId(
+ selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsSortSelectV2
+ );
+ const refreshSelect = getByTestId(
+ selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRefreshSelectV2
+ );
+
+ const multiSwitch = getByTestId(
+ selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsMultiSwitch
+ );
+ const includeAllSwitch = getByTestId(
+ selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsIncludeAllSwitch
+ );
+ const allValueInput = getByTestId(
+ selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput
+ );
+
+ expect(dataSourcePicker).toBeInTheDocument();
+ expect(dataSourcePicker).toHaveTextContent('Default Test Data Source');
+ expect(regexInput).toBeInTheDocument();
+ expect(regexInput).toHaveValue('.*');
+ expect(sortSelect).toBeInTheDocument();
+ expect(sortSelect).toHaveTextContent('Alphabetical (asc)');
+ expect(refreshSelect).toBeInTheDocument();
+ expect(getByRole('radio', { name: 'On dashboard load' })).toBeChecked();
+ expect(multiSwitch).toBeInTheDocument();
+ expect(multiSwitch).toBeChecked();
+ expect(includeAllSwitch).toBeInTheDocument();
+ expect(includeAllSwitch).toBeChecked();
+ expect(allValueInput).toBeInTheDocument();
+ expect(allValueInput).toHaveValue('custom all value');
+ });
+
+ it('should call onDataSourceChange when changing the datasource', async () => {
+ const {
+ renderer: { getByTestId },
+ } = setup();
+ const dataSourcePicker = getByTestId(selectors.components.DataSourcePicker.inputV2);
+ await waitFor(async () => {
+ await userEvent.click(dataSourcePicker); // open the select
+ await userEvent.tab();
+ });
+ expect(mockOnDataSourceChange).toHaveBeenCalledTimes(1);
+ expect(mockOnDataSourceChange).toHaveBeenCalledWith(defaultDatasource);
+ });
+
+ it('should call onQueryChange when changing the query', async () => {
+ const {
+ renderer: { getByTestId },
+ } = setup();
+ const queryEditor = getByTestId(
+ selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput
+ );
+
+ await waitFor(async () => {
+ await userEvent.type(queryEditor, '-new');
+ await userEvent.tab();
+ });
+
+ expect(mockOnLegacyQueryChange).toHaveBeenCalledTimes(1);
+ expect(mockOnLegacyQueryChange).toHaveBeenCalledWith('my-query-new', expect.anything());
+ });
+
+ it('should call onRegExChange when changing the regex', async () => {
+ const {
+ renderer: { getByTestId },
+ } = setup();
+ const regexInput = getByTestId(
+ selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2
+ );
+ await userEvent.type(regexInput, '{backspace}?');
+ await userEvent.tab();
+ expect(mockOnRegExChange).toHaveBeenCalledTimes(1);
+ expect(
+ ((mockOnRegExChange.mock.calls[0][0] as FormEvent).target as HTMLTextAreaElement).value
+ ).toBe('.?');
+ });
+
+ it('should call onSortChange when changing the sort', async () => {
+ const {
+ renderer: { getByTestId },
+ } = setup();
+ const sortSelect = getByTestId(
+ selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsSortSelectV2
+ );
+ await userEvent.click(sortSelect); // open the select
+ const anotherOption = await screen.getByText('Alphabetical (desc)');
+ await userEvent.click(anotherOption);
+
+ expect(mockOnSortChange).toHaveBeenCalledTimes(1);
+ expect(mockOnSortChange).toHaveBeenCalledWith(
+ expect.objectContaining({ value: VariableSort.alphabeticalDesc }),
+ expect.anything()
+ );
+ });
+
+ it('should call onRefreshChange when changing the refresh', async () => {
+ const {
+ renderer: { getByTestId },
+ } = setup();
+ const refreshSelect = getByTestId(
+ selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRefreshSelectV2
+ );
+ await userEvent.click(refreshSelect); // open the select
+ const anotherOption = await screen.getByText('On time range change');
+ await userEvent.click(anotherOption);
+
+ expect(mockOnRefreshChange).toHaveBeenCalledTimes(1);
+ expect(mockOnRefreshChange).toHaveBeenCalledWith(VariableRefresh.onTimeRangeChanged);
+ });
+
+ it('should call onMultiChange when changing the multi switch', async () => {
+ const {
+ renderer: { getByTestId },
+ } = setup();
+ const multiSwitch = getByTestId(
+ selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsMultiSwitch
+ );
+ await userEvent.click(multiSwitch);
+ expect(mockOnMultiChange).toHaveBeenCalledTimes(1);
+ expect(
+ (mockOnMultiChange.mock.calls[0][0] as FormEvent).target as HTMLInputElement
+ ).toBeChecked();
+ });
+
+ it('should call onIncludeAllChange when changing the include all switch', async () => {
+ const {
+ renderer: { getByTestId },
+ } = setup();
+ const includeAllSwitch = getByTestId(
+ selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsIncludeAllSwitch
+ );
+ await userEvent.click(includeAllSwitch);
+ expect(mockOnIncludeAllChange).toHaveBeenCalledTimes(1);
+ expect(
+ (mockOnIncludeAllChange.mock.calls[0][0] as FormEvent).target as HTMLInputElement
+ ).toBeChecked();
+ });
+
+ it('should call onAllValueChange when changing the all value', async () => {
+ const {
+ renderer: { getByTestId },
+ } = setup();
+ const allValueInput = getByTestId(
+ selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput
+ );
+ await userEvent.type(allValueInput, ' and another value');
+ await userEvent.tab();
+ expect(mockOnAllValueChange).toHaveBeenCalledTimes(1);
+ expect(
+ ((mockOnAllValueChange.mock.calls[0][0] as FormEvent).target as HTMLInputElement).value
+ ).toBe('custom all value and another value');
+ });
+});
diff --git a/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.tsx b/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.tsx
new file mode 100644
index 00000000000..0e7d86740cc
--- /dev/null
+++ b/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.tsx
@@ -0,0 +1,128 @@
+import React, { FormEvent } from 'react';
+
+import { DataSourceApi, DataSourceInstanceSettings, SelectableValue, TimeRange } from '@grafana/data';
+import { selectors } from '@grafana/e2e-selectors';
+import { QueryVariable } from '@grafana/scenes';
+import { VariableRefresh, VariableSort } from '@grafana/schema';
+import { Field } 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';
+import { QueryVariableRefreshSelect } from 'app/features/variables/query/QueryVariableRefreshSelect';
+import { QueryVariableSortSelect } from 'app/features/variables/query/QueryVariableSortSelect';
+import { VariableQueryEditorType } from 'app/features/variables/types';
+
+import { VariableLegend } from './VariableLegend';
+import { VariableTextAreaField } from './VariableTextAreaField';
+
+type VariableQueryType = QueryVariable['state']['query'];
+
+interface QueryVariableEditorFormProps {
+ datasource: DataSourceApi | undefined;
+ onDataSourceChange: (dsSettings: DataSourceInstanceSettings) => void;
+ query: VariableQueryType;
+ onQueryChange: (query: VariableQueryType) => void;
+ onLegacyQueryChange: (query: VariableQueryType, definition: string) => void;
+ VariableQueryEditor: VariableQueryEditorType | undefined;
+ timeRange: TimeRange;
+ regex: string | null;
+ onRegExChange: (event: FormEvent) => void;
+ sort: VariableSort;
+ onSortChange: (option: SelectableValue) => void;
+ refresh: VariableRefresh;
+ onRefreshChange: (option: VariableRefresh) => void;
+ isMulti: boolean;
+ onMultiChange: (event: FormEvent) => void;
+ includeAll: boolean;
+ onIncludeAllChange: (event: FormEvent) => void;
+ allValue: string;
+ onAllValueChange: (event: FormEvent) => void;
+}
+
+export function QueryVariableEditorForm({
+ datasource,
+ onDataSourceChange,
+ query,
+ onQueryChange,
+ onLegacyQueryChange,
+ VariableQueryEditor,
+ timeRange,
+ regex,
+ onRegExChange,
+ sort,
+ onSortChange,
+ refresh,
+ onRefreshChange,
+ isMulti,
+ onMultiChange,
+ includeAll,
+ onIncludeAllChange,
+ allValue,
+ onAllValueChange,
+}: QueryVariableEditorFormProps) {
+ return (
+ <>
+ Query options
+
+
+
+
+ {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
+
+ ).
+
+ }
+ placeholder="/.*-(?.*)-(?.*)-.*/"
+ onBlur={onRegExChange}
+ testId={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2}
+ width={52}
+ />
+
+
+
+
+
+ Selection options
+
+ >
+ );
+}
diff --git a/public/app/features/dashboard-scene/settings/variables/components/VariableSelectField.tsx b/public/app/features/dashboard-scene/settings/variables/components/VariableSelectField.tsx
index 17f273dab46..9f0024a7ab1 100644
--- a/public/app/features/dashboard-scene/settings/variables/components/VariableSelectField.tsx
+++ b/public/app/features/dashboard-scene/settings/variables/components/VariableSelectField.tsx
@@ -1,5 +1,5 @@
import { css } from '@emotion/css';
-import React, { PropsWithChildren, ReactElement, useId } from 'react';
+import React, { PropsWithChildren, useId } from 'react';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { Field, Select, useStyles2 } from '@grafana/ui';
@@ -22,7 +22,7 @@ export function VariableSelectField({
onChange,
testId,
width,
-}: PropsWithChildren>): ReactElement {
+}: PropsWithChildren>) {
const styles = useStyles2(getStyles);
const uniqueId = useId();
const inputId = `variable-select-input-${name}-${uniqueId}`;
diff --git a/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.test.tsx b/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.test.tsx
new file mode 100644
index 00000000000..867ecb3e836
--- /dev/null
+++ b/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.test.tsx
@@ -0,0 +1,288 @@
+import { getByRole, render, screen, act, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import React from 'react';
+import { lastValueFrom, of } from 'rxjs';
+
+import {
+ VariableSupportType,
+ PanelData,
+ LoadingState,
+ toDataFrame,
+ getDefaultTimeRange,
+ FieldType,
+} from '@grafana/data';
+import { selectors } from '@grafana/e2e-selectors';
+import { setRunRequest } from '@grafana/runtime';
+import { QueryVariable } from '@grafana/scenes';
+import { VariableRefresh, VariableSort } from '@grafana/schema';
+import { mockDataSource } from 'app/features/alerting/unified/mocks';
+import { LegacyVariableQueryEditor } from 'app/features/variables/editor/LegacyVariableQueryEditor';
+
+import { QueryVariableEditor } from './QueryVariableEditor';
+
+const defaultDatasource = mockDataSource({
+ name: 'Default Test Data Source',
+ type: 'test',
+});
+
+const promDatasource = mockDataSource({
+ name: 'Prometheus',
+ type: 'prometheus',
+});
+
+jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => ({
+ ...jest.requireActual('@grafana/runtime/src/services/dataSourceSrv'),
+ getDataSourceSrv: () => ({
+ get: async () => ({
+ ...defaultDatasource,
+ variables: {
+ getType: () => VariableSupportType.Custom,
+ query: jest.fn(),
+ editor: jest.fn().mockImplementation(LegacyVariableQueryEditor),
+ },
+ }),
+ getList: () => [defaultDatasource, promDatasource],
+ getInstanceSettings: () => ({ ...defaultDatasource }),
+ }),
+}));
+
+const runRequestMock = jest.fn().mockReturnValue(
+ of({
+ state: LoadingState.Done,
+ series: [
+ toDataFrame({
+ fields: [{ name: 'text', type: FieldType.string, values: ['val1', 'val2', 'val11'] }],
+ }),
+ ],
+ timeRange: getDefaultTimeRange(),
+ })
+);
+
+setRunRequest(runRequestMock);
+
+describe('QueryVariableEditor', () => {
+ const onRunQueryMock = jest.fn();
+
+ async function setup(props?: React.ComponentProps) {
+ const variable = new QueryVariable({
+ datasource: {
+ uid: defaultDatasource.uid,
+ type: defaultDatasource.type,
+ },
+ query: 'my-query',
+ regex: '.*',
+ sort: VariableSort.alphabeticalAsc,
+ refresh: VariableRefresh.onDashboardLoad,
+ isMulti: true,
+ includeAll: true,
+ allValue: 'custom all value',
+ });
+
+ return {
+ renderer: await act(() => {
+ return render();
+ }),
+ variable,
+ user: userEvent.setup(),
+ };
+ }
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should render the component with initializing the components correctly', async () => {
+ const { renderer } = await setup();
+ const dataSourcePicker = renderer.getByTestId(selectors.components.DataSourcePicker.container);
+ const queryEditor = renderer.getByTestId(
+ selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput
+ );
+ const regexInput = renderer.getByTestId(
+ selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2
+ );
+ const sortSelect = renderer.getByTestId(
+ selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsSortSelectV2
+ );
+ const refreshSelect = renderer.getByTestId(
+ selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRefreshSelectV2
+ );
+
+ const multiSwitch = renderer.getByTestId(
+ selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsMultiSwitch
+ );
+ const includeAllSwitch = renderer.getByTestId(
+ selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsIncludeAllSwitch
+ );
+ const allValueInput = renderer.getByTestId(
+ selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput
+ );
+
+ expect(dataSourcePicker).toBeInTheDocument();
+ expect(dataSourcePicker).toHaveTextContent('Default Test Data Source');
+ expect(queryEditor).toBeInTheDocument();
+ expect(queryEditor).toHaveValue('my-query');
+ expect(regexInput).toBeInTheDocument();
+ expect(regexInput).toHaveValue('.*');
+ expect(sortSelect).toBeInTheDocument();
+ expect(sortSelect).toHaveTextContent('Alphabetical (asc)');
+ expect(refreshSelect).toBeInTheDocument();
+ expect(getByRole(refreshSelect, 'radio', { name: 'On dashboard load' })).toBeChecked();
+ expect(multiSwitch).toBeInTheDocument();
+ expect(multiSwitch).toBeChecked();
+ expect(includeAllSwitch).toBeInTheDocument();
+ expect(includeAllSwitch).toBeChecked();
+ expect(allValueInput).toBeInTheDocument();
+ expect(allValueInput).toHaveValue('custom all value');
+ });
+
+ it('should update variable state when changing the datasource', async () => {
+ const {
+ variable,
+ renderer: { getByTestId },
+ user,
+ } = await setup();
+ const dataSourcePicker = getByTestId(selectors.components.DataSourcePicker.container).getElementsByTagName('input');
+
+ await waitFor(async () => {
+ await user.type(dataSourcePicker[0], 'm');
+ await user.tab();
+ await lastValueFrom(variable.validateAndUpdate());
+ });
+
+ expect(variable.state.datasource).toEqual({ uid: 'mock-ds-2', type: 'test' });
+ });
+
+ it('should update the variable state when changing the query', async () => {
+ const {
+ variable,
+ renderer: { getByTestId },
+ user,
+ } = await setup();
+ const queryEditor = getByTestId(
+ selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput
+ );
+
+ await waitFor(async () => {
+ await user.type(queryEditor, '-new');
+ await user.tab();
+ await lastValueFrom(variable.validateAndUpdate());
+ });
+
+ expect(variable.state.query).toEqual('my-query-new');
+ expect(onRunQueryMock).toHaveBeenCalledTimes(1);
+ });
+
+ it('should update the variable state when changing the regex', async () => {
+ const {
+ variable,
+ renderer: { getByTestId },
+ user,
+ } = await setup();
+ const regexInput = getByTestId(
+ selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2
+ );
+
+ await waitFor(async () => {
+ await user.type(regexInput, '{backspace}?');
+ await user.tab();
+ await lastValueFrom(variable.validateAndUpdate());
+ });
+
+ expect(variable.state.regex).toBe('.?');
+ });
+
+ it('should update the variable state when changing the sort', async () => {
+ const {
+ variable,
+ renderer: { getByTestId },
+ user,
+ } = await setup();
+ const sortSelect = getByTestId(
+ selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsSortSelectV2
+ );
+
+ await waitFor(async () => {
+ await user.click(sortSelect);
+ const anotherOption = await screen.getByText('Alphabetical (desc)');
+ await user.click(anotherOption);
+ await lastValueFrom(variable.validateAndUpdate());
+ });
+
+ expect(variable.state.sort).toBe(VariableSort.alphabeticalDesc);
+ });
+
+ it('should update the variable state when changing the refresh', async () => {
+ const {
+ variable,
+ renderer: { getByTestId },
+ user,
+ } = await setup();
+ const refreshSelect = getByTestId(
+ selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRefreshSelectV2
+ );
+
+ await waitFor(async () => {
+ await user.click(refreshSelect);
+ const anotherOption = await screen.getByText('On time range change');
+ await user.click(anotherOption);
+ await lastValueFrom(variable.validateAndUpdate());
+ });
+
+ expect(variable.state.refresh).toBe(VariableRefresh.onTimeRangeChanged);
+ });
+
+ it('should update the variable state when changing the multi switch', async () => {
+ const {
+ variable,
+ renderer: { getByTestId },
+ user,
+ } = await setup();
+ const multiSwitch = getByTestId(
+ selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsMultiSwitch
+ );
+
+ await waitFor(async () => {
+ await user.click(multiSwitch);
+ await lastValueFrom(variable.validateAndUpdate());
+ });
+
+ expect(variable.state.isMulti).toBe(false);
+ });
+
+ it('should update the variable state when changing the include all switch', async () => {
+ const {
+ variable,
+ renderer: { getByTestId },
+ user,
+ } = await setup();
+ const includeAllSwitch = getByTestId(
+ selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsIncludeAllSwitch
+ );
+
+ await waitFor(async () => {
+ await user.click(includeAllSwitch);
+ await lastValueFrom(variable.validateAndUpdate());
+ });
+
+ expect(variable.state.includeAll).toBe(false);
+ });
+
+ it('should update the variable state when changing the all value', async () => {
+ const {
+ variable,
+ renderer: { getByTestId },
+ user,
+ } = await setup();
+ const allValueInput = getByTestId(
+ selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput
+ );
+
+ await waitFor(async () => {
+ await user.type(allValueInput, ' and another value');
+ await user.tab();
+ await lastValueFrom(variable.validateAndUpdate());
+ });
+
+ expect(variable.state.allValue).toBe('custom all value and another value');
+ });
+});
diff --git a/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.tsx b/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.tsx
index 57cf0b39ef4..0c577d74b6b 100644
--- a/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.tsx
+++ b/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.tsx
@@ -1,12 +1,80 @@
-import React from 'react';
+import React, { FormEvent } from 'react';
+import { useAsync } from 'react-use';
-import { QueryVariable } from '@grafana/scenes';
+import { SelectableValue, DataSourceInstanceSettings } from '@grafana/data';
+import { getDataSourceSrv } from '@grafana/runtime';
+import { QueryVariable, sceneGraph } from '@grafana/scenes';
+import { DataSourceRef, VariableRefresh, VariableSort } from '@grafana/schema';
+import { getVariableQueryEditor } from 'app/features/variables/editor/getVariableQueryEditor';
+
+import { QueryVariableEditorForm } from '../components/QueryVariableForm';
interface QueryVariableEditorProps {
variable: QueryVariable;
- onChange: (variable: QueryVariable) => void;
+ onRunQuery: () => void;
}
+type VariableQueryType = QueryVariable['state']['query'];
+
+export function QueryVariableEditor({ variable, onRunQuery }: QueryVariableEditorProps) {
+ const { datasource: datasourceRef, regex, sort, refresh, isMulti, includeAll, allValue, query } = variable.useState();
+ const { value: timeRange } = sceneGraph.getTimeRange(variable).useState();
+
+ const { value: dsConfig } = useAsync(async () => {
+ const datasource = await getDataSourceSrv().get(datasourceRef ?? '');
+ const VariableQueryEditor = await getVariableQueryEditor(datasource);
+
+ return { datasource, VariableQueryEditor };
+ }, [datasourceRef]);
+ const { datasource, VariableQueryEditor } = dsConfig ?? {};
+
+ const onRegExChange = (event: React.FormEvent) => {
+ variable.setState({ regex: event.currentTarget.value });
+ };
+ const onSortChange = (sort: SelectableValue) => {
+ variable.setState({ sort: sort.value });
+ };
+ const onRefreshChange = (refresh: VariableRefresh) => {
+ variable.setState({ refresh: refresh });
+ };
+ const onMultiChange = (event: FormEvent) => {
+ variable.setState({ isMulti: event.currentTarget.checked });
+ };
+ const onIncludeAllChange = (event: FormEvent) => {
+ variable.setState({ includeAll: event.currentTarget.checked });
+ };
+ const onAllValueChange = (event: FormEvent) => {
+ variable.setState({ allValue: event.currentTarget.value });
+ };
+ const onDataSourceChange = (dsInstanceSettings: DataSourceInstanceSettings) => {
+ const datasource: DataSourceRef = { uid: dsInstanceSettings.uid, type: dsInstanceSettings.type };
+ variable.setState({ datasource });
+ };
+ const onQueryChange = (query: VariableQueryType) => {
+ variable.setState({ query });
+ onRunQuery();
+ };
-export function QueryVariableEditor(props: QueryVariableEditorProps) {
- return QueryVariableEditor
;
+ return (
+
+ );
}
diff --git a/public/app/features/dashboard-scene/settings/variables/utils.test.ts b/public/app/features/dashboard-scene/settings/variables/utils.test.ts
index 6d43b34891a..9cdafc13411 100644
--- a/public/app/features/dashboard-scene/settings/variables/utils.test.ts
+++ b/public/app/features/dashboard-scene/settings/variables/utils.test.ts
@@ -115,7 +115,7 @@ describe('getVariableEditor', () => {
});
it.each(Object.keys(EDITABLE_VARIABLES) as EditableVariableType[])(
- 'should define an editor for every variable type',
+ 'should define an editor for variable type "%s"',
(type) => {
const editor = getVariableEditor(type);
expect(editor).toBeDefined();
@@ -130,7 +130,7 @@ describe('getVariableEditor', () => {
['datasource', DataSourceVariableEditor],
['adhoc', AdHocFiltersVariableEditor],
['textbox', TextBoxVariableEditor],
- ])('should return the correct editor for each variable type', (type, ExpectedVariableEditor) => {
+ ])('should return the correct editor for variable type "%s"', (type, ExpectedVariableEditor) => {
expect(getVariableEditor(type as EditableVariableType)).toBe(ExpectedVariableEditor);
});
});
diff --git a/public/app/features/variables/editor/LegacyVariableQueryEditor.tsx b/public/app/features/variables/editor/LegacyVariableQueryEditor.tsx
index 069aed31fb0..2b5f3e24317 100644
--- a/public/app/features/variables/editor/LegacyVariableQueryEditor.tsx
+++ b/public/app/features/variables/editor/LegacyVariableQueryEditor.tsx
@@ -34,7 +34,7 @@ export const LegacyVariableQueryEditor = ({ onChange, query }: VariableQueryEdit
onBlur={onBlur}
placeholder="Metric name or tags query"
required
- aria-label={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput}
+ data-testid={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput}
cols={52}
className={styles.textarea}
/>
diff --git a/public/app/features/variables/query/QueryVariableEditor.test.tsx b/public/app/features/variables/query/QueryVariableEditor.test.tsx
index 03a1884428e..264d53fd6a8 100644
--- a/public/app/features/variables/query/QueryVariableEditor.test.tsx
+++ b/public/app/features/variables/query/QueryVariableEditor.test.tsx
@@ -3,6 +3,7 @@ import userEvent from '@testing-library/user-event';
import React from 'react';
import { DataSourceApi, VariableSupportType } from '@grafana/data';
+import { selectors } from '@grafana/e2e-selectors';
import { mockDataSource } from 'app/features/alerting/unified/mocks';
import { DataSourceType } from 'app/features/alerting/unified/utils/datasource';
@@ -144,7 +145,7 @@ describe('QueryVariableEditor', () => {
});
const getQueryField = () =>
- screen.getByRole('textbox', { name: /variable editor form default variable query editor textarea/i });
+ screen.getByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput);
const getRegExField = () => screen.getByLabelText(/Regex/);
diff --git a/public/app/features/variables/query/QueryVariableEditor.tsx b/public/app/features/variables/query/QueryVariableEditor.tsx
index 8cdea8c630d..1f4e0016815 100644
--- a/public/app/features/variables/query/QueryVariableEditor.tsx
+++ b/public/app/features/variables/query/QueryVariableEditor.tsx
@@ -1,28 +1,19 @@
import React, { FormEvent, PureComponent } from 'react';
import { connect, ConnectedProps } from 'react-redux';
-import { DataSourceInstanceSettings, getDataSourceRef, LoadingState, SelectableValue } from '@grafana/data';
-import { selectors } from '@grafana/e2e-selectors';
-import { getTemplateSrv } from '@grafana/runtime';
-import { Field, Text, Box } from '@grafana/ui';
-import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
+import { DataSourceInstanceSettings, getDataSourceRef, SelectableValue } from '@grafana/data';
+import { QueryVariableEditorForm } from 'app/features/dashboard-scene/settings/variables/components/QueryVariableForm';
import { StoreState } from '../../../types';
import { getTimeSrv } from '../../dashboard/services/TimeSrv';
-import { VariableLegend } from '../../dashboard-scene/settings/variables/components/VariableLegend';
-import { VariableTextAreaField } from '../../dashboard-scene/settings/variables/components/VariableTextAreaField';
-import { SelectionOptionsEditor } from '../editor/SelectionOptionsEditor';
import { initialVariableEditorState } from '../editor/reducer';
import { getQueryVariableEditorState } from '../editor/selectors';
-import { OnPropChangeArguments, VariableEditorProps } from '../editor/types';
-import { isLegacyQueryEditor, isQueryEditor } from '../guard';
+import { VariableEditorProps } from '../editor/types';
import { changeVariableMultiValue } from '../state/actions';
import { getVariablesState } from '../state/selectors';
-import { QueryVariableModel, VariableRefresh, VariableSort, VariableWithMultiSupport } from '../types';
+import { QueryVariableModel, VariableRefresh, VariableSort } from '../types';
import { toKeyedVariableIdentifier } from '../utils';
-import { QueryVariableRefreshSelect } from './QueryVariableRefreshSelect';
-import { QueryVariableSortSelect } from './QueryVariableSortSelect';
import { changeQueryVariableDataSource, changeQueryVariableQuery, initQueryVariableEditor } from './actions';
const mapStateToProps = (state: StoreState, ownProps: OwnProps) => {
@@ -105,10 +96,6 @@ export class QueryVariableEditorUnConnected extends PureComponent
}
};
- onRegExChange = (event: FormEvent) => {
- this.setState({ regex: event.currentTarget.value });
- };
-
onRegExBlur = async (event: FormEvent) => {
const regex = event.currentTarget.value;
if (this.props.variable.regex !== regex) {
@@ -124,11 +111,19 @@ export class QueryVariableEditorUnConnected extends PureComponent
this.props.onPropChange({ propName: 'sort', propValue: option.value, updateOptions: true });
};
- onSelectionOptionsChange = async ({ propValue, propName }: OnPropChangeArguments) => {
- this.props.onPropChange({ propName, propValue, updateOptions: true });
+ onMultiChange = (event: FormEvent) => {
+ this.props.onPropChange({ propName: 'multi', propValue: event.currentTarget.checked });
+ };
+
+ onIncludeAllChange = (event: FormEvent) => {
+ this.props.onPropChange({ propName: 'includeAll', propValue: event.currentTarget.checked });
+ };
+
+ onAllValueChange = (event: FormEvent) => {
+ this.props.onPropChange({ propName: 'allValue', propValue: event.currentTarget.value });
};
- renderQueryEditor = () => {
+ render() {
const { extended, variable } = this.props;
if (!extended || !extended.dataSource || !extended.VariableQueryEditor) {
@@ -137,112 +132,30 @@ export class QueryVariableEditorUnConnected extends PureComponent
const datasource = extended.dataSource;
const VariableQueryEditor = extended.VariableQueryEditor;
+ const timeRange = getTimeSrv().timeRange();
- let query = variable.query;
-
- if (typeof query === 'string') {
- query = query || (datasource.variables?.getDefaultQuery?.() ?? '');
- } else {
- query = {
- ...datasource.variables?.getDefaultQuery?.(),
- ...variable.query,
- };
- }
-
- if (isLegacyQueryEditor(VariableQueryEditor, datasource)) {
- return (
-
- Query
-
-
-
-
- );
- }
-
- const range = getTimeSrv().timeRange();
-
- if (isQueryEditor(VariableQueryEditor, datasource)) {
- return (
-
- Query
-
- {}}
- data={{ series: [], state: LoadingState.Done, timeRange: range }}
- range={range}
- onBlur={() => {}}
- history={[]}
- />
-
-
- );
- }
-
- return null;
- };
-
- render() {
return (
- <>
- Query options
-
-
-
-
- {this.renderQueryEditor()}
-
-
- 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
-
- ).
-
- }
- placeholder="/.*-(?.*)-(?.*)-.*/"
- onChange={this.onRegExChange}
- onBlur={this.onRegExBlur}
- testId={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2}
- width={52}
- />
-
-
-
-
-
- Selection options
-
- >
+
);
}
}
diff --git a/public/app/features/variables/query/QueryVariableRefreshSelect.tsx b/public/app/features/variables/query/QueryVariableRefreshSelect.tsx
index 36c5500959d..88475bcc75e 100644
--- a/public/app/features/variables/query/QueryVariableRefreshSelect.tsx
+++ b/public/app/features/variables/query/QueryVariableRefreshSelect.tsx
@@ -7,6 +7,7 @@ import { useMediaQueryChange } from 'app/core/hooks/useMediaQueryChange';
interface Props {
onChange: (option: VariableRefresh) => void;
refresh: VariableRefresh;
+ testId?: string;
}
const REFRESH_OPTIONS = [
@@ -14,7 +15,7 @@ const REFRESH_OPTIONS = [
{ label: 'On time range change', value: VariableRefresh.onTimeRangeChanged },
];
-export function QueryVariableRefreshSelect({ onChange, refresh }: PropsWithChildren) {
+export function QueryVariableRefreshSelect({ onChange, refresh, testId }: PropsWithChildren) {
const theme = useTheme2();
const [isSmallScreen, setIsSmallScreen] = useState(false);
@@ -31,7 +32,7 @@ export function QueryVariableRefreshSelect({ onChange, refresh }: PropsWithChild
);
return (
-
+
) => void;
sort: VariableSort;
+ testId?: string;
}
const SORT_OPTIONS = [
@@ -23,7 +23,7 @@ const SORT_OPTIONS = [
{ label: 'Natural (desc)', value: VariableSort.naturalDesc },
];
-export function QueryVariableSortSelect({ onChange, sort }: PropsWithChildren) {
+export function QueryVariableSortSelect({ onChange, sort, testId }: PropsWithChildren) {
const value = useMemo(() => SORT_OPTIONS.find((o) => o.value === sort) ?? SORT_OPTIONS[0], [sort]);
return (
@@ -33,7 +33,7 @@ export function QueryVariableSortSelect({ onChange, sort }: PropsWithChildren
);