mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
QueryVariable: Be able to edit the variable using scenes (#80847)
This commit is contained in:
parent
0851c18b55
commit
a9f17a3f24
@ -4330,9 +4330,6 @@ exports[`better eslint`] = {
|
|||||||
"public/app/features/variables/datasource/actions.ts:5381": [
|
"public/app/features/variables/datasource/actions.ts:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
[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": [
|
"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.", "0"],
|
||||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
[0, 0, 0, "Do not use any type assertions.", "1"]
|
||||||
|
@ -145,14 +145,14 @@ export const Pages = {
|
|||||||
applyButton: 'data-testid Variable editor Apply button',
|
applyButton: 'data-testid Variable editor Apply button',
|
||||||
},
|
},
|
||||||
QueryVariable: {
|
QueryVariable: {
|
||||||
queryOptionsDataSourceSelect: Components.DataSourcePicker.container,
|
queryOptionsDataSourceSelect: Components.DataSourcePicker.inputV2,
|
||||||
queryOptionsRefreshSelect: 'Variable editor Form Query Refresh select',
|
queryOptionsRefreshSelect: 'Variable editor Form Query Refresh select',
|
||||||
queryOptionsRefreshSelectV2: 'data-testid Variable editor Form Query Refresh select',
|
queryOptionsRefreshSelectV2: 'data-testid Variable editor Form Query Refresh select',
|
||||||
queryOptionsRegExInput: 'Variable editor Form Query RegEx field',
|
queryOptionsRegExInput: 'Variable editor Form Query RegEx field',
|
||||||
queryOptionsRegExInputV2: 'data-testid Variable editor Form Query RegEx field',
|
queryOptionsRegExInputV2: 'data-testid Variable editor Form Query RegEx field',
|
||||||
queryOptionsSortSelect: 'Variable editor Form Query Sort select',
|
queryOptionsSortSelect: 'Variable editor Form Query Sort select',
|
||||||
queryOptionsSortSelectV2: 'data-testid 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',
|
valueGroupsTagsEnabledSwitch: 'Variable editor Form Query UseTags switch',
|
||||||
valueGroupsTagsTagsQueryInput: 'Variable editor Form Query TagsQuery field',
|
valueGroupsTagsTagsQueryInput: 'Variable editor Form Query TagsQuery field',
|
||||||
valueGroupsTagsTagsValuesQueryInput: 'Variable editor Form Query TagsValuesQuery field',
|
valueGroupsTagsTagsValuesQueryInput: 'Variable editor Form Query TagsValuesQuery field',
|
||||||
|
@ -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 (
|
||||||
|
<Box marginBottom={2}>
|
||||||
|
<Text element={'h4'}>Query</Text>
|
||||||
|
<Box marginTop={1}>
|
||||||
|
<VariableQueryEditor
|
||||||
|
key={datasource.uid}
|
||||||
|
datasource={datasource}
|
||||||
|
query={queryWithDefaults}
|
||||||
|
templateSrv={getTemplateSrv()}
|
||||||
|
onChange={onLegacyQueryChange}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (VariableQueryEditor && isQueryEditor(VariableQueryEditor, datasource)) {
|
||||||
|
return (
|
||||||
|
<Box marginBottom={2}>
|
||||||
|
<Text element={'h4'}>Query</Text>
|
||||||
|
<Box marginTop={1}>
|
||||||
|
<VariableQueryEditor
|
||||||
|
key={datasource.uid}
|
||||||
|
datasource={datasource}
|
||||||
|
query={queryWithDefaults}
|
||||||
|
onChange={onQueryChange}
|
||||||
|
onRunQuery={() => {}}
|
||||||
|
data={{ series: [], state: LoadingState.Done, timeRange }}
|
||||||
|
range={timeRange}
|
||||||
|
onBlur={() => {}}
|
||||||
|
history={[]}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
@ -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<PanelData>({
|
||||||
|
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<typeof QueryVariableEditorForm>) {
|
||||||
|
return {
|
||||||
|
renderer: render(<QueryVariableEditorForm {...defaultProps} {...props} />),
|
||||||
|
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<HTMLTextAreaElement>).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<HTMLInputElement>).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<HTMLInputElement>).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<HTMLInputElement>).target as HTMLInputElement).value
|
||||||
|
).toBe('custom all value and another value');
|
||||||
|
});
|
||||||
|
});
|
@ -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<HTMLTextAreaElement>) => void;
|
||||||
|
sort: VariableSort;
|
||||||
|
onSortChange: (option: SelectableValue<VariableSort>) => void;
|
||||||
|
refresh: VariableRefresh;
|
||||||
|
onRefreshChange: (option: VariableRefresh) => void;
|
||||||
|
isMulti: boolean;
|
||||||
|
onMultiChange: (event: FormEvent<HTMLInputElement>) => void;
|
||||||
|
includeAll: boolean;
|
||||||
|
onIncludeAllChange: (event: FormEvent<HTMLInputElement>) => void;
|
||||||
|
allValue: string;
|
||||||
|
onAllValueChange: (event: FormEvent<HTMLInputElement>) => 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 (
|
||||||
|
<>
|
||||||
|
<VariableLegend>Query options</VariableLegend>
|
||||||
|
<Field label="Data source" htmlFor="data-source-picker">
|
||||||
|
<DataSourcePicker current={datasource} onChange={onDataSourceChange} variables={true} width={30} />
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
{datasource && VariableQueryEditor && (
|
||||||
|
<QueryEditor
|
||||||
|
onQueryChange={onQueryChange}
|
||||||
|
onLegacyQueryChange={onLegacyQueryChange}
|
||||||
|
datasource={datasource}
|
||||||
|
query={query}
|
||||||
|
VariableQueryEditor={VariableQueryEditor}
|
||||||
|
timeRange={timeRange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<VariableTextAreaField
|
||||||
|
defaultValue={regex ?? ''}
|
||||||
|
name="Regex"
|
||||||
|
description={
|
||||||
|
<div>
|
||||||
|
Optional, if you want to extract part of a series name or metric node segment.
|
||||||
|
<br />
|
||||||
|
Named capture groups can be used to separate the display text and value (
|
||||||
|
<a
|
||||||
|
className="external-link"
|
||||||
|
href="https://grafana.com/docs/grafana/latest/variables/filter-variables-with-regex#filter-and-modify-using-named-text-and-value-capture-groups"
|
||||||
|
target="__blank"
|
||||||
|
>
|
||||||
|
see examples
|
||||||
|
</a>
|
||||||
|
).
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
placeholder="/.*-(?<text>.*)-(?<value>.*)-.*/"
|
||||||
|
onBlur={onRegExChange}
|
||||||
|
testId={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2}
|
||||||
|
width={52}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<QueryVariableSortSelect
|
||||||
|
testId={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsSortSelectV2}
|
||||||
|
onChange={onSortChange}
|
||||||
|
sort={sort}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<QueryVariableRefreshSelect
|
||||||
|
testId={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRefreshSelectV2}
|
||||||
|
onChange={onRefreshChange}
|
||||||
|
refresh={refresh}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<VariableLegend>Selection options</VariableLegend>
|
||||||
|
<SelectionOptionsForm
|
||||||
|
multi={!!isMulti}
|
||||||
|
includeAll={!!includeAll}
|
||||||
|
allValue={allValue}
|
||||||
|
onMultiChange={onMultiChange}
|
||||||
|
onIncludeAllChange={onIncludeAllChange}
|
||||||
|
onAllValueChange={onAllValueChange}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import { css } from '@emotion/css';
|
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 { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||||
import { Field, Select, useStyles2 } from '@grafana/ui';
|
import { Field, Select, useStyles2 } from '@grafana/ui';
|
||||||
@ -22,7 +22,7 @@ export function VariableSelectField({
|
|||||||
onChange,
|
onChange,
|
||||||
testId,
|
testId,
|
||||||
width,
|
width,
|
||||||
}: PropsWithChildren<VariableSelectFieldProps<any>>): ReactElement {
|
}: PropsWithChildren<VariableSelectFieldProps<any>>) {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const uniqueId = useId();
|
const uniqueId = useId();
|
||||||
const inputId = `variable-select-input-${name}-${uniqueId}`;
|
const inputId = `variable-select-input-${name}-${uniqueId}`;
|
||||||
|
@ -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<PanelData>({
|
||||||
|
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<typeof QueryVariableEditor>) {
|
||||||
|
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(<QueryVariableEditor variable={variable} onRunQuery={onRunQueryMock} />);
|
||||||
|
}),
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
});
|
@ -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 {
|
interface QueryVariableEditorProps {
|
||||||
variable: QueryVariable;
|
variable: QueryVariable;
|
||||||
onChange: (variable: QueryVariable) => void;
|
onRunQuery: () => void;
|
||||||
}
|
}
|
||||||
|
type VariableQueryType = QueryVariable['state']['query'];
|
||||||
|
|
||||||
export function QueryVariableEditor(props: QueryVariableEditorProps) {
|
export function QueryVariableEditor({ variable, onRunQuery }: QueryVariableEditorProps) {
|
||||||
return <div>QueryVariableEditor</div>;
|
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<HTMLTextAreaElement>) => {
|
||||||
|
variable.setState({ regex: event.currentTarget.value });
|
||||||
|
};
|
||||||
|
const onSortChange = (sort: SelectableValue<VariableSort>) => {
|
||||||
|
variable.setState({ sort: sort.value });
|
||||||
|
};
|
||||||
|
const onRefreshChange = (refresh: VariableRefresh) => {
|
||||||
|
variable.setState({ refresh: refresh });
|
||||||
|
};
|
||||||
|
const onMultiChange = (event: FormEvent<HTMLInputElement>) => {
|
||||||
|
variable.setState({ isMulti: event.currentTarget.checked });
|
||||||
|
};
|
||||||
|
const onIncludeAllChange = (event: FormEvent<HTMLInputElement>) => {
|
||||||
|
variable.setState({ includeAll: event.currentTarget.checked });
|
||||||
|
};
|
||||||
|
const onAllValueChange = (event: FormEvent<HTMLInputElement>) => {
|
||||||
|
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();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QueryVariableEditorForm
|
||||||
|
datasource={datasource}
|
||||||
|
onDataSourceChange={onDataSourceChange}
|
||||||
|
query={query}
|
||||||
|
onQueryChange={onQueryChange}
|
||||||
|
onLegacyQueryChange={onQueryChange}
|
||||||
|
VariableQueryEditor={VariableQueryEditor}
|
||||||
|
timeRange={timeRange}
|
||||||
|
regex={regex}
|
||||||
|
onRegExChange={onRegExChange}
|
||||||
|
sort={sort}
|
||||||
|
onSortChange={onSortChange}
|
||||||
|
refresh={refresh}
|
||||||
|
onRefreshChange={onRefreshChange}
|
||||||
|
isMulti={!!isMulti}
|
||||||
|
onMultiChange={onMultiChange}
|
||||||
|
includeAll={!!includeAll}
|
||||||
|
onIncludeAllChange={onIncludeAllChange}
|
||||||
|
allValue={allValue ?? ''}
|
||||||
|
onAllValueChange={onAllValueChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -115,7 +115,7 @@ describe('getVariableEditor', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it.each(Object.keys(EDITABLE_VARIABLES) as EditableVariableType[])(
|
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) => {
|
(type) => {
|
||||||
const editor = getVariableEditor(type);
|
const editor = getVariableEditor(type);
|
||||||
expect(editor).toBeDefined();
|
expect(editor).toBeDefined();
|
||||||
@ -130,7 +130,7 @@ describe('getVariableEditor', () => {
|
|||||||
['datasource', DataSourceVariableEditor],
|
['datasource', DataSourceVariableEditor],
|
||||||
['adhoc', AdHocFiltersVariableEditor],
|
['adhoc', AdHocFiltersVariableEditor],
|
||||||
['textbox', TextBoxVariableEditor],
|
['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);
|
expect(getVariableEditor(type as EditableVariableType)).toBe(ExpectedVariableEditor);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -34,7 +34,7 @@ export const LegacyVariableQueryEditor = ({ onChange, query }: VariableQueryEdit
|
|||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
placeholder="Metric name or tags query"
|
placeholder="Metric name or tags query"
|
||||||
required
|
required
|
||||||
aria-label={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput}
|
data-testid={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput}
|
||||||
cols={52}
|
cols={52}
|
||||||
className={styles.textarea}
|
className={styles.textarea}
|
||||||
/>
|
/>
|
||||||
|
@ -3,6 +3,7 @@ import userEvent from '@testing-library/user-event';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { DataSourceApi, VariableSupportType } from '@grafana/data';
|
import { DataSourceApi, VariableSupportType } from '@grafana/data';
|
||||||
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { mockDataSource } from 'app/features/alerting/unified/mocks';
|
import { mockDataSource } from 'app/features/alerting/unified/mocks';
|
||||||
import { DataSourceType } from 'app/features/alerting/unified/utils/datasource';
|
import { DataSourceType } from 'app/features/alerting/unified/utils/datasource';
|
||||||
|
|
||||||
@ -144,7 +145,7 @@ describe('QueryVariableEditor', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const getQueryField = () =>
|
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/);
|
const getRegExField = () => screen.getByLabelText(/Regex/);
|
||||||
|
|
||||||
|
@ -1,28 +1,19 @@
|
|||||||
import React, { FormEvent, PureComponent } from 'react';
|
import React, { FormEvent, PureComponent } from 'react';
|
||||||
import { connect, ConnectedProps } from 'react-redux';
|
import { connect, ConnectedProps } from 'react-redux';
|
||||||
|
|
||||||
import { DataSourceInstanceSettings, getDataSourceRef, LoadingState, SelectableValue } from '@grafana/data';
|
import { DataSourceInstanceSettings, getDataSourceRef, SelectableValue } from '@grafana/data';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { QueryVariableEditorForm } from 'app/features/dashboard-scene/settings/variables/components/QueryVariableForm';
|
||||||
import { getTemplateSrv } from '@grafana/runtime';
|
|
||||||
import { Field, Text, Box } from '@grafana/ui';
|
|
||||||
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
|
|
||||||
|
|
||||||
import { StoreState } from '../../../types';
|
import { StoreState } from '../../../types';
|
||||||
import { getTimeSrv } from '../../dashboard/services/TimeSrv';
|
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 { initialVariableEditorState } from '../editor/reducer';
|
||||||
import { getQueryVariableEditorState } from '../editor/selectors';
|
import { getQueryVariableEditorState } from '../editor/selectors';
|
||||||
import { OnPropChangeArguments, VariableEditorProps } from '../editor/types';
|
import { VariableEditorProps } from '../editor/types';
|
||||||
import { isLegacyQueryEditor, isQueryEditor } from '../guard';
|
|
||||||
import { changeVariableMultiValue } from '../state/actions';
|
import { changeVariableMultiValue } from '../state/actions';
|
||||||
import { getVariablesState } from '../state/selectors';
|
import { getVariablesState } from '../state/selectors';
|
||||||
import { QueryVariableModel, VariableRefresh, VariableSort, VariableWithMultiSupport } from '../types';
|
import { QueryVariableModel, VariableRefresh, VariableSort } from '../types';
|
||||||
import { toKeyedVariableIdentifier } from '../utils';
|
import { toKeyedVariableIdentifier } from '../utils';
|
||||||
|
|
||||||
import { QueryVariableRefreshSelect } from './QueryVariableRefreshSelect';
|
|
||||||
import { QueryVariableSortSelect } from './QueryVariableSortSelect';
|
|
||||||
import { changeQueryVariableDataSource, changeQueryVariableQuery, initQueryVariableEditor } from './actions';
|
import { changeQueryVariableDataSource, changeQueryVariableQuery, initQueryVariableEditor } from './actions';
|
||||||
|
|
||||||
const mapStateToProps = (state: StoreState, ownProps: OwnProps) => {
|
const mapStateToProps = (state: StoreState, ownProps: OwnProps) => {
|
||||||
@ -105,10 +96,6 @@ export class QueryVariableEditorUnConnected extends PureComponent<Props, State>
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onRegExChange = (event: FormEvent<HTMLTextAreaElement>) => {
|
|
||||||
this.setState({ regex: event.currentTarget.value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onRegExBlur = async (event: FormEvent<HTMLTextAreaElement>) => {
|
onRegExBlur = async (event: FormEvent<HTMLTextAreaElement>) => {
|
||||||
const regex = event.currentTarget.value;
|
const regex = event.currentTarget.value;
|
||||||
if (this.props.variable.regex !== regex) {
|
if (this.props.variable.regex !== regex) {
|
||||||
@ -124,11 +111,19 @@ export class QueryVariableEditorUnConnected extends PureComponent<Props, State>
|
|||||||
this.props.onPropChange({ propName: 'sort', propValue: option.value, updateOptions: true });
|
this.props.onPropChange({ propName: 'sort', propValue: option.value, updateOptions: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
onSelectionOptionsChange = async ({ propValue, propName }: OnPropChangeArguments<VariableWithMultiSupport>) => {
|
onMultiChange = (event: FormEvent<HTMLInputElement>) => {
|
||||||
this.props.onPropChange({ propName, propValue, updateOptions: true });
|
this.props.onPropChange({ propName: 'multi', propValue: event.currentTarget.checked });
|
||||||
};
|
};
|
||||||
|
|
||||||
renderQueryEditor = () => {
|
onIncludeAllChange = (event: FormEvent<HTMLInputElement>) => {
|
||||||
|
this.props.onPropChange({ propName: 'includeAll', propValue: event.currentTarget.checked });
|
||||||
|
};
|
||||||
|
|
||||||
|
onAllValueChange = (event: FormEvent<HTMLInputElement>) => {
|
||||||
|
this.props.onPropChange({ propName: 'allValue', propValue: event.currentTarget.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
const { extended, variable } = this.props;
|
const { extended, variable } = this.props;
|
||||||
|
|
||||||
if (!extended || !extended.dataSource || !extended.VariableQueryEditor) {
|
if (!extended || !extended.dataSource || !extended.VariableQueryEditor) {
|
||||||
@ -137,112 +132,30 @@ export class QueryVariableEditorUnConnected extends PureComponent<Props, State>
|
|||||||
|
|
||||||
const datasource = extended.dataSource;
|
const datasource = extended.dataSource;
|
||||||
const VariableQueryEditor = extended.VariableQueryEditor;
|
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 (
|
|
||||||
<Box marginBottom={2}>
|
|
||||||
<Text element={'h4'}>Query</Text>
|
|
||||||
<Box marginTop={1}>
|
|
||||||
<VariableQueryEditor
|
|
||||||
key={datasource.uid}
|
|
||||||
datasource={datasource}
|
|
||||||
query={query}
|
|
||||||
templateSrv={getTemplateSrv()}
|
|
||||||
onChange={this.onLegacyQueryChange}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const range = getTimeSrv().timeRange();
|
|
||||||
|
|
||||||
if (isQueryEditor(VariableQueryEditor, datasource)) {
|
|
||||||
return (
|
|
||||||
<Box marginBottom={2}>
|
|
||||||
<Text element={'h4'}>Query</Text>
|
|
||||||
<Box marginTop={1}>
|
|
||||||
<VariableQueryEditor
|
|
||||||
key={datasource.uid}
|
|
||||||
datasource={datasource}
|
|
||||||
query={query}
|
|
||||||
onChange={this.onQueryChange}
|
|
||||||
onRunQuery={() => {}}
|
|
||||||
data={{ series: [], state: LoadingState.Done, timeRange: range }}
|
|
||||||
range={range}
|
|
||||||
onBlur={() => {}}
|
|
||||||
history={[]}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<QueryVariableEditorForm
|
||||||
<VariableLegend>Query options</VariableLegend>
|
datasource={datasource}
|
||||||
<Field label="Data source" htmlFor="data-source-picker">
|
onDataSourceChange={this.onDataSourceChange}
|
||||||
<DataSourcePicker
|
query={variable.query}
|
||||||
current={this.props.variable.datasource}
|
onQueryChange={this.onQueryChange}
|
||||||
onChange={this.onDataSourceChange}
|
onLegacyQueryChange={this.onLegacyQueryChange}
|
||||||
variables={true}
|
VariableQueryEditor={VariableQueryEditor}
|
||||||
width={30}
|
timeRange={timeRange}
|
||||||
/>
|
regex={variable.regex}
|
||||||
</Field>
|
onRegExChange={this.onRegExBlur}
|
||||||
|
sort={variable.sort}
|
||||||
{this.renderQueryEditor()}
|
onSortChange={this.onSortChange}
|
||||||
|
refresh={variable.refresh}
|
||||||
<VariableTextAreaField
|
onRefreshChange={this.onRefreshChange}
|
||||||
value={this.state.regex ?? this.props.variable.regex}
|
isMulti={variable.multi}
|
||||||
name="Regex"
|
includeAll={variable.includeAll}
|
||||||
description={
|
allValue={variable.allValue ?? ''}
|
||||||
<div>
|
onMultiChange={this.onMultiChange}
|
||||||
Optional, if you want to extract part of a series name or metric node segment.
|
onIncludeAllChange={this.onIncludeAllChange}
|
||||||
<br />
|
onAllValueChange={this.onAllValueChange}
|
||||||
Named capture groups can be used to separate the display text and value (
|
/>
|
||||||
<a
|
|
||||||
className="external-link"
|
|
||||||
href="https://grafana.com/docs/grafana/latest/variables/filter-variables-with-regex#filter-and-modify-using-named-text-and-value-capture-groups"
|
|
||||||
target="__blank"
|
|
||||||
>
|
|
||||||
see examples
|
|
||||||
</a>
|
|
||||||
).
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
placeholder="/.*-(?<text>.*)-(?<value>.*)-.*/"
|
|
||||||
onChange={this.onRegExChange}
|
|
||||||
onBlur={this.onRegExBlur}
|
|
||||||
testId={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2}
|
|
||||||
width={52}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<QueryVariableSortSelect onChange={this.onSortChange} sort={this.props.variable.sort} />
|
|
||||||
|
|
||||||
<QueryVariableRefreshSelect onChange={this.onRefreshChange} refresh={this.props.variable.refresh} />
|
|
||||||
|
|
||||||
<VariableLegend>Selection options</VariableLegend>
|
|
||||||
<SelectionOptionsEditor
|
|
||||||
variable={this.props.variable}
|
|
||||||
onPropChange={this.onSelectionOptionsChange}
|
|
||||||
onMultiChanged={this.props.changeVariableMultiValue}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import { useMediaQueryChange } from 'app/core/hooks/useMediaQueryChange';
|
|||||||
interface Props {
|
interface Props {
|
||||||
onChange: (option: VariableRefresh) => void;
|
onChange: (option: VariableRefresh) => void;
|
||||||
refresh: VariableRefresh;
|
refresh: VariableRefresh;
|
||||||
|
testId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const REFRESH_OPTIONS = [
|
const REFRESH_OPTIONS = [
|
||||||
@ -14,7 +15,7 @@ const REFRESH_OPTIONS = [
|
|||||||
{ label: 'On time range change', value: VariableRefresh.onTimeRangeChanged },
|
{ label: 'On time range change', value: VariableRefresh.onTimeRangeChanged },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function QueryVariableRefreshSelect({ onChange, refresh }: PropsWithChildren<Props>) {
|
export function QueryVariableRefreshSelect({ onChange, refresh, testId }: PropsWithChildren<Props>) {
|
||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
|
|
||||||
const [isSmallScreen, setIsSmallScreen] = useState(false);
|
const [isSmallScreen, setIsSmallScreen] = useState(false);
|
||||||
@ -31,7 +32,7 @@ export function QueryVariableRefreshSelect({ onChange, refresh }: PropsWithChild
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Field label="Refresh" description="When to update the values of this variable">
|
<Field label="Refresh" description="When to update the values of this variable" data-testid={testId}>
|
||||||
<RadioButtonGroup
|
<RadioButtonGroup
|
||||||
options={REFRESH_OPTIONS}
|
options={REFRESH_OPTIONS}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import React, { PropsWithChildren, useMemo } from 'react';
|
import React, { PropsWithChildren, useMemo } from 'react';
|
||||||
|
|
||||||
import { SelectableValue } from '@grafana/data';
|
import { SelectableValue } from '@grafana/data';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
|
||||||
|
|
||||||
import { VariableSelectField } from '../../dashboard-scene/settings/variables/components/VariableSelectField';
|
import { VariableSelectField } from '../../dashboard-scene/settings/variables/components/VariableSelectField';
|
||||||
import { VariableSort } from '../types';
|
import { VariableSort } from '../types';
|
||||||
@ -9,6 +8,7 @@ import { VariableSort } from '../types';
|
|||||||
interface Props {
|
interface Props {
|
||||||
onChange: (option: SelectableValue<VariableSort>) => void;
|
onChange: (option: SelectableValue<VariableSort>) => void;
|
||||||
sort: VariableSort;
|
sort: VariableSort;
|
||||||
|
testId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SORT_OPTIONS = [
|
const SORT_OPTIONS = [
|
||||||
@ -23,7 +23,7 @@ const SORT_OPTIONS = [
|
|||||||
{ label: 'Natural (desc)', value: VariableSort.naturalDesc },
|
{ label: 'Natural (desc)', value: VariableSort.naturalDesc },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function QueryVariableSortSelect({ onChange, sort }: PropsWithChildren<Props>) {
|
export function QueryVariableSortSelect({ onChange, sort, testId }: PropsWithChildren<Props>) {
|
||||||
const value = useMemo(() => SORT_OPTIONS.find((o) => o.value === sort) ?? SORT_OPTIONS[0], [sort]);
|
const value = useMemo(() => SORT_OPTIONS.find((o) => o.value === sort) ?? SORT_OPTIONS[0], [sort]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -33,7 +33,7 @@ export function QueryVariableSortSelect({ onChange, sort }: PropsWithChildren<Pr
|
|||||||
value={value}
|
value={value}
|
||||||
options={SORT_OPTIONS}
|
options={SORT_OPTIONS}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
testId={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsSortSelectV2}
|
testId={testId}
|
||||||
width={25}
|
width={25}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user