mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Variables: migrates data source variable type to React/Redux (#22770)
* Refactor: moves all the newVariables part to features/variables directory * Feature: adds datasource type * Tests: adds reducer tests * Tests: covers data source actions with tests * Chore: reduces strict null errors
This commit is contained in:
309
public/app/features/variables/query/QueryVariableEditor.tsx
Normal file
309
public/app/features/variables/query/QueryVariableEditor.tsx
Normal file
@@ -0,0 +1,309 @@
|
||||
import React, { ChangeEvent, PureComponent } from 'react';
|
||||
import { e2e } from '@grafana/e2e';
|
||||
import { FormLabel, Switch } from '@grafana/ui';
|
||||
|
||||
import templateSrv from '../../templating/template_srv';
|
||||
import { SelectionOptionsEditor } from '../editor/SelectionOptionsEditor';
|
||||
import { QueryVariableModel, VariableRefresh, VariableSort, VariableWithMultiSupport } from '../../templating/variable';
|
||||
import { QueryVariableEditorState } from './reducer';
|
||||
import { changeQueryVariableDataSource, changeQueryVariableQuery, initQueryVariableEditor } from './actions';
|
||||
import { VariableEditorState } from '../editor/reducer';
|
||||
import { OnPropChangeArguments, VariableEditorProps } from '../editor/types';
|
||||
import { MapDispatchToProps, MapStateToProps } from 'react-redux';
|
||||
import { StoreState } from '../../../types';
|
||||
import { connectWithStore } from '../../../core/utils/connectWithReduxStore';
|
||||
import { toVariableIdentifier } from '../state/types';
|
||||
|
||||
export interface OwnProps extends VariableEditorProps<QueryVariableModel> {}
|
||||
|
||||
interface ConnectedProps {
|
||||
editor: VariableEditorState<QueryVariableEditorState>;
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
initQueryVariableEditor: typeof initQueryVariableEditor;
|
||||
changeQueryVariableDataSource: typeof changeQueryVariableDataSource;
|
||||
changeQueryVariableQuery: typeof changeQueryVariableQuery;
|
||||
}
|
||||
|
||||
type Props = OwnProps & ConnectedProps & DispatchProps;
|
||||
|
||||
export interface State {
|
||||
regex: string | null;
|
||||
tagsQuery: string | null;
|
||||
tagValuesQuery: string | null;
|
||||
}
|
||||
|
||||
export class QueryVariableEditorUnConnected extends PureComponent<Props, State> {
|
||||
state: State = {
|
||||
regex: null,
|
||||
tagsQuery: null,
|
||||
tagValuesQuery: null,
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
await this.props.initQueryVariableEditor(toVariableIdentifier(this.props.variable));
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Readonly<Props>): void {
|
||||
if (prevProps.variable.datasource !== this.props.variable.datasource) {
|
||||
this.props.changeQueryVariableDataSource(
|
||||
toVariableIdentifier(this.props.variable),
|
||||
this.props.variable.datasource
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
getSelectedDataSourceValue = (): string => {
|
||||
if (!this.props.editor.extended?.dataSources.length) {
|
||||
return '';
|
||||
}
|
||||
const foundItem = this.props.editor.extended?.dataSources.find(ds => ds.value === this.props.variable.datasource);
|
||||
const value = foundItem ? foundItem.value : this.props.editor.extended?.dataSources[0].value;
|
||||
return value ?? '';
|
||||
};
|
||||
|
||||
onDataSourceChange = (event: ChangeEvent<HTMLSelectElement>) => {
|
||||
this.props.onPropChange({ propName: 'query', propValue: '' });
|
||||
this.props.onPropChange({ propName: 'datasource', propValue: event.target.value });
|
||||
};
|
||||
|
||||
onQueryChange = async (query: any, definition: string) => {
|
||||
this.props.changeQueryVariableQuery(toVariableIdentifier(this.props.variable), query, definition);
|
||||
};
|
||||
|
||||
onRegExChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ regex: event.target.value });
|
||||
};
|
||||
|
||||
onRegExBlur = async (event: ChangeEvent<HTMLInputElement>) => {
|
||||
this.props.onPropChange({ propName: 'regex', propValue: event.target.value, updateOptions: true });
|
||||
};
|
||||
|
||||
onTagsQueryChange = async (event: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ tagsQuery: event.target.value });
|
||||
};
|
||||
|
||||
onTagsQueryBlur = async (event: ChangeEvent<HTMLInputElement>) => {
|
||||
this.props.onPropChange({ propName: 'tagsQuery', propValue: event.target.value, updateOptions: true });
|
||||
};
|
||||
|
||||
onTagValuesQueryChange = async (event: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ tagValuesQuery: event.target.value });
|
||||
};
|
||||
|
||||
onTagValuesQueryBlur = async (event: ChangeEvent<HTMLInputElement>) => {
|
||||
this.props.onPropChange({ propName: 'tagValuesQuery', propValue: event.target.value, updateOptions: true });
|
||||
};
|
||||
|
||||
onRefreshChange = (event: ChangeEvent<HTMLSelectElement>) => {
|
||||
this.props.onPropChange({ propName: 'refresh', propValue: parseInt(event.target.value, 10) });
|
||||
};
|
||||
|
||||
onSortChange = async (event: ChangeEvent<HTMLSelectElement>) => {
|
||||
this.props.onPropChange({ propName: 'sort', propValue: parseInt(event.target.value, 10), updateOptions: true });
|
||||
};
|
||||
|
||||
onSelectionOptionsChange = async ({ propValue, propName }: OnPropChangeArguments<VariableWithMultiSupport>) => {
|
||||
this.props.onPropChange({ propName, propValue, updateOptions: true });
|
||||
};
|
||||
|
||||
onUseTagsChange = async (event: ChangeEvent<HTMLInputElement>) => {
|
||||
this.props.onPropChange({ propName: 'useTags', propValue: event.target.checked, updateOptions: true });
|
||||
};
|
||||
|
||||
render() {
|
||||
const VariableQueryEditor = this.props.editor.extended?.VariableQueryEditor;
|
||||
return (
|
||||
<>
|
||||
<div className="gf-form-group">
|
||||
<h5 className="section-heading">Query Options</h5>
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form max-width-21">
|
||||
<span className="gf-form-label width-10">Data source</span>
|
||||
<div className="gf-form-select-wrapper max-width-14">
|
||||
<select
|
||||
className="gf-form-input"
|
||||
value={this.getSelectedDataSourceValue()}
|
||||
onChange={this.onDataSourceChange}
|
||||
required
|
||||
aria-label={
|
||||
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.selectors.queryOptionsDataSourceSelect
|
||||
}
|
||||
>
|
||||
{this.props.editor.extended?.dataSources.length &&
|
||||
this.props.editor.extended?.dataSources.map(ds => (
|
||||
<option key={ds.value ?? ''} value={ds.value ?? ''} label={ds.name}>
|
||||
{ds.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="gf-form max-width-22">
|
||||
<FormLabel width={10} tooltip={'When to update the values of this variable.'}>
|
||||
Refresh
|
||||
</FormLabel>
|
||||
<div className="gf-form-select-wrapper width-15">
|
||||
<select
|
||||
className="gf-form-input"
|
||||
value={this.props.variable.refresh}
|
||||
onChange={this.onRefreshChange}
|
||||
aria-label={
|
||||
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.selectors.queryOptionsRefreshSelect
|
||||
}
|
||||
>
|
||||
<option label="Never" value={VariableRefresh.never}>
|
||||
Never
|
||||
</option>
|
||||
<option label="On Dashboard Load" value={VariableRefresh.onDashboardLoad}>
|
||||
On Dashboard Load
|
||||
</option>
|
||||
<option label="On Time Range Change" value={VariableRefresh.onTimeRangeChanged}>
|
||||
On Time Range Change
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{VariableQueryEditor && this.props.editor.extended?.dataSource && (
|
||||
<VariableQueryEditor
|
||||
datasource={this.props.editor.extended?.dataSource}
|
||||
query={this.props.variable.query}
|
||||
templateSrv={templateSrv}
|
||||
onChange={this.onQueryChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="gf-form">
|
||||
<FormLabel
|
||||
width={10}
|
||||
tooltip={'Optional, if you want to extract part of a series name or metric node segment.'}
|
||||
>
|
||||
Regex
|
||||
</FormLabel>
|
||||
<input
|
||||
type="text"
|
||||
className="gf-form-input"
|
||||
placeholder="/.*-(.*)-.*/"
|
||||
value={this.state.regex ?? this.props.variable.regex}
|
||||
onChange={this.onRegExChange}
|
||||
onBlur={this.onRegExBlur}
|
||||
aria-label={e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.selectors.queryOptionsRegExInput}
|
||||
/>
|
||||
</div>
|
||||
<div className="gf-form max-width-21">
|
||||
<FormLabel width={10} tooltip={'How to sort the values of this variable.'}>
|
||||
Sort
|
||||
</FormLabel>
|
||||
<div className="gf-form-select-wrapper max-width-14">
|
||||
<select
|
||||
className="gf-form-input"
|
||||
value={this.props.variable.sort}
|
||||
onChange={this.onSortChange}
|
||||
aria-label={e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.selectors.queryOptionsSortSelect}
|
||||
>
|
||||
<option label="Disabled" value={VariableSort.disabled}>
|
||||
Disabled
|
||||
</option>
|
||||
<option label="Alphabetical (asc)" value={VariableSort.alphabeticalAsc}>
|
||||
Alphabetical (asc)
|
||||
</option>
|
||||
<option label="Alphabetical (desc)" value={VariableSort.alphabeticalDesc}>
|
||||
Alphabetical (desc)
|
||||
</option>
|
||||
<option label="Numerical (asc)" value={VariableSort.numericalAsc}>
|
||||
Numerical (asc)
|
||||
</option>
|
||||
<option label="Numerical (desc)" value={VariableSort.numericalDesc}>
|
||||
Numerical (desc)
|
||||
</option>
|
||||
<option
|
||||
label="Alphabetical (case-insensitive, asc)"
|
||||
value={VariableSort.alphabeticalCaseInsensitiveAsc}
|
||||
>
|
||||
Alphabetical (case-insensitive, asc)
|
||||
</option>
|
||||
<option
|
||||
label="Alphabetical (case-insensitive, desc)"
|
||||
value={VariableSort.alphabeticalCaseInsensitiveDesc}
|
||||
>
|
||||
Alphabetical (case-insensitive, desc)
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SelectionOptionsEditor variable={this.props.variable} onPropChange={this.onSelectionOptionsChange} />
|
||||
|
||||
<div className="gf-form-group">
|
||||
<h5>Value groups/tags (Experimental feature)</h5>
|
||||
<div
|
||||
aria-label={
|
||||
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.selectors.valueGroupsTagsEnabledSwitch
|
||||
}
|
||||
>
|
||||
<Switch
|
||||
label="Enabled"
|
||||
label-class="width-10"
|
||||
checked={this.props.variable.useTags}
|
||||
onChange={this.onUseTagsChange}
|
||||
/>
|
||||
</div>
|
||||
{this.props.variable.useTags && (
|
||||
<>
|
||||
<div className="gf-form last">
|
||||
<span className="gf-form-label width-10">Tags query</span>
|
||||
<input
|
||||
type="text"
|
||||
className="gf-form-input"
|
||||
value={this.state.tagsQuery ?? this.props.variable.tagsQuery}
|
||||
placeholder="metric name or tags query"
|
||||
onChange={this.onTagsQueryChange}
|
||||
onBlur={this.onTagsQueryBlur}
|
||||
aria-label={
|
||||
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.selectors.valueGroupsTagsTagsQueryInput
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="gf-form">
|
||||
<li className="gf-form-label width-10">Tag values query</li>
|
||||
<input
|
||||
type="text"
|
||||
className="gf-form-input"
|
||||
value={this.state.tagValuesQuery ?? this.props.variable.tagValuesQuery}
|
||||
placeholder="apps.$tag.*"
|
||||
onChange={this.onTagValuesQueryChange}
|
||||
onBlur={this.onTagValuesQueryBlur}
|
||||
aria-label={
|
||||
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.selectors
|
||||
.valueGroupsTagsTagsValuesQueryInput
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (state, ownProps) => ({
|
||||
editor: state.templating.editor as VariableEditorState<QueryVariableEditorState>,
|
||||
});
|
||||
|
||||
const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = {
|
||||
initQueryVariableEditor,
|
||||
changeQueryVariableDataSource,
|
||||
changeQueryVariableQuery,
|
||||
};
|
||||
|
||||
export const QueryVariableEditor = connectWithStore(
|
||||
QueryVariableEditorUnConnected,
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
);
|
||||
572
public/app/features/variables/query/actions.test.ts
Normal file
572
public/app/features/variables/query/actions.test.ts
Normal file
@@ -0,0 +1,572 @@
|
||||
import { variableAdapters } from '../adapters';
|
||||
import { createQueryVariableAdapter } from './adapter';
|
||||
import { reduxTester } from '../../../../test/core/redux/reduxTester';
|
||||
import { getTemplatingRootReducer } from '../state/helpers';
|
||||
import { QueryVariableModel, VariableHide, VariableRefresh, VariableSort } from '../../templating/variable';
|
||||
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE, toVariablePayload } from '../state/types';
|
||||
import { changeVariableProp, setCurrentVariableValue } from '../state/sharedReducer';
|
||||
import { initDashboardTemplating } from '../state/actions';
|
||||
import { TemplatingState } from '../state/reducers';
|
||||
import {
|
||||
changeQueryVariableDataSource,
|
||||
changeQueryVariableQuery,
|
||||
initQueryVariableEditor,
|
||||
updateQueryVariableOptions,
|
||||
} from './actions';
|
||||
import { updateVariableOptions, updateVariableTags } from './reducer';
|
||||
import {
|
||||
addVariableEditorError,
|
||||
changeVariableEditorExtended,
|
||||
removeVariableEditorError,
|
||||
setIdInEditor,
|
||||
} from '../editor/reducer';
|
||||
import DefaultVariableQueryEditor from '../../templating/DefaultVariableQueryEditor';
|
||||
import { expect } from 'test/lib/common';
|
||||
|
||||
const mocks: Record<string, any> = {
|
||||
datasource: {
|
||||
metricFindQuery: jest.fn().mockResolvedValue([]),
|
||||
},
|
||||
datasourceSrv: {
|
||||
getMetricSources: jest.fn().mockReturnValue([]),
|
||||
},
|
||||
pluginLoader: {
|
||||
importDataSourcePlugin: jest.fn().mockResolvedValue({ components: {} }),
|
||||
},
|
||||
};
|
||||
|
||||
jest.mock('../../plugins/datasource_srv', () => ({
|
||||
getDatasourceSrv: jest.fn(() => ({
|
||||
get: jest.fn((name: string) => mocks[name]),
|
||||
getMetricSources: () => mocks.datasourceSrv.getMetricSources(),
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('../../plugins/plugin_loader', () => ({
|
||||
importDataSourcePlugin: () => mocks.pluginLoader.importDataSourcePlugin(),
|
||||
}));
|
||||
|
||||
describe('query actions', () => {
|
||||
variableAdapters.set('query', createQueryVariableAdapter());
|
||||
|
||||
describe('when updateQueryVariableOptions is dispatched for variable with tags and includeAll', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const variable = createVariable({ includeAll: true });
|
||||
const optionsMetrics = [createMetric('A'), createMetric('B')];
|
||||
const tagsMetrics = [createMetric('tagA'), createMetric('tagB')];
|
||||
|
||||
mockDatasourceMetrics(variable, optionsMetrics, tagsMetrics);
|
||||
|
||||
const tester = await reduxTester<{ templating: TemplatingState }>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(initDashboardTemplating([variable]))
|
||||
.whenAsyncActionIsDispatched(updateQueryVariableOptions(toVariablePayload(variable)), true);
|
||||
|
||||
const option = createOption(ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE);
|
||||
|
||||
tester.thenDispatchedActionPredicateShouldEqual(actions => {
|
||||
const [updateOptions, updateTags, setCurrentAction] = actions;
|
||||
const expectedNumberOfActions = 3;
|
||||
|
||||
expect(updateOptions).toEqual(updateVariableOptions(toVariablePayload(variable, optionsMetrics)));
|
||||
expect(updateTags).toEqual(updateVariableTags(toVariablePayload(variable, tagsMetrics)));
|
||||
expect(setCurrentAction).toEqual(setCurrentVariableValue(toVariablePayload(variable, { option })));
|
||||
return actions.length === expectedNumberOfActions;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when updateQueryVariableOptions is dispatched for variable with tags', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const variable = createVariable({ includeAll: false });
|
||||
const optionsMetrics = [createMetric('A'), createMetric('B')];
|
||||
const tagsMetrics = [createMetric('tagA'), createMetric('tagB')];
|
||||
|
||||
mockDatasourceMetrics(variable, optionsMetrics, tagsMetrics);
|
||||
|
||||
const tester = await reduxTester<{ templating: TemplatingState }>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(initDashboardTemplating([variable]))
|
||||
.whenAsyncActionIsDispatched(updateQueryVariableOptions(toVariablePayload(variable)), true);
|
||||
|
||||
const option = createOption('A');
|
||||
|
||||
tester.thenDispatchedActionPredicateShouldEqual(actions => {
|
||||
const [updateOptions, updateTags, setCurrentAction] = actions;
|
||||
const expectedNumberOfActions = 3;
|
||||
|
||||
expect(updateOptions).toEqual(updateVariableOptions(toVariablePayload(variable, optionsMetrics)));
|
||||
expect(updateTags).toEqual(updateVariableTags(toVariablePayload(variable, tagsMetrics)));
|
||||
expect(setCurrentAction).toEqual(setCurrentVariableValue(toVariablePayload(variable, { option })));
|
||||
return actions.length === expectedNumberOfActions;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when updateQueryVariableOptions is dispatched for variable without both tags and includeAll', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const variable = createVariable({ includeAll: false, useTags: false });
|
||||
const optionsMetrics = [createMetric('A'), createMetric('B')];
|
||||
|
||||
mockDatasourceMetrics(variable, optionsMetrics, []);
|
||||
|
||||
const tester = await reduxTester<{ templating: TemplatingState }>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(initDashboardTemplating([variable]))
|
||||
.whenAsyncActionIsDispatched(updateQueryVariableOptions(toVariablePayload(variable)), true);
|
||||
|
||||
const option = createOption('A');
|
||||
|
||||
tester.thenDispatchedActionPredicateShouldEqual(actions => {
|
||||
const [updateOptions, setCurrentAction] = actions;
|
||||
const expectedNumberOfActions = 2;
|
||||
|
||||
expect(updateOptions).toEqual(updateVariableOptions(toVariablePayload(variable, optionsMetrics)));
|
||||
expect(setCurrentAction).toEqual(setCurrentVariableValue(toVariablePayload(variable, { option })));
|
||||
return actions.length === expectedNumberOfActions;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when updateQueryVariableOptions is dispatched for variable with includeAll but without tags', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const variable = createVariable({ includeAll: true, useTags: false });
|
||||
const optionsMetrics = [createMetric('A'), createMetric('B')];
|
||||
|
||||
mockDatasourceMetrics(variable, optionsMetrics, []);
|
||||
|
||||
const tester = await reduxTester<{ templating: TemplatingState }>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(initDashboardTemplating([variable]))
|
||||
.whenAsyncActionIsDispatched(updateQueryVariableOptions(toVariablePayload(variable)), true);
|
||||
|
||||
const option = createOption(ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE);
|
||||
|
||||
tester.thenDispatchedActionPredicateShouldEqual(actions => {
|
||||
const [updateOptions, setCurrentAction] = actions;
|
||||
const expectedNumberOfActions = 2;
|
||||
|
||||
expect(updateOptions).toEqual(updateVariableOptions(toVariablePayload(variable, optionsMetrics)));
|
||||
expect(setCurrentAction).toEqual(setCurrentVariableValue(toVariablePayload(variable, { option })));
|
||||
return actions.length === expectedNumberOfActions;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when updateQueryVariableOptions is dispatched for variable open in editor', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const variable = createVariable({ includeAll: true, useTags: false });
|
||||
const optionsMetrics = [createMetric('A'), createMetric('B')];
|
||||
|
||||
mockDatasourceMetrics(variable, optionsMetrics, []);
|
||||
|
||||
const tester = await reduxTester<{ templating: TemplatingState }>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(initDashboardTemplating([variable]))
|
||||
.whenActionIsDispatched(setIdInEditor({ id: variable.uuid! }))
|
||||
.whenAsyncActionIsDispatched(updateQueryVariableOptions(toVariablePayload(variable)), true);
|
||||
|
||||
const option = createOption(ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE);
|
||||
|
||||
tester.thenDispatchedActionPredicateShouldEqual(actions => {
|
||||
const [clearErrors, updateOptions, setCurrentAction] = actions;
|
||||
const expectedNumberOfActions = 3;
|
||||
|
||||
expect(clearErrors).toEqual(removeVariableEditorError({ errorProp: 'update' }));
|
||||
expect(updateOptions).toEqual(updateVariableOptions(toVariablePayload(variable, optionsMetrics)));
|
||||
expect(setCurrentAction).toEqual(setCurrentVariableValue(toVariablePayload(variable, { option })));
|
||||
return actions.length === expectedNumberOfActions;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when updateQueryVariableOptions is dispatched and fails for variable open in editor', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const variable = createVariable({ includeAll: true, useTags: false });
|
||||
const error = { message: 'failed to fetch metrics' };
|
||||
|
||||
mocks[variable.datasource!].metricFindQuery = jest.fn(() => Promise.reject(error));
|
||||
|
||||
const tester = await reduxTester<{ templating: TemplatingState }>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(initDashboardTemplating([variable]))
|
||||
.whenActionIsDispatched(setIdInEditor({ id: variable.uuid! }))
|
||||
.whenAsyncActionIsDispatched(updateQueryVariableOptions(toVariablePayload(variable)), true);
|
||||
|
||||
tester.thenDispatchedActionPredicateShouldEqual(actions => {
|
||||
const [clearErrors, errorOccurred] = actions;
|
||||
const expectedNumberOfActions = 2;
|
||||
|
||||
expect(errorOccurred).toEqual(addVariableEditorError({ errorProp: 'update', errorText: error.message }));
|
||||
expect(clearErrors).toEqual(removeVariableEditorError({ errorProp: 'update' }));
|
||||
return actions.length === expectedNumberOfActions;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when initQueryVariableEditor is dispatched', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const variable = createVariable({ includeAll: true, useTags: false });
|
||||
const defaultMetricSource = { name: '', value: '', meta: {}, sort: '' };
|
||||
const testMetricSource = { name: 'test', value: 'test', meta: {}, sort: '' };
|
||||
const editor = {};
|
||||
|
||||
mocks.datasourceSrv.getMetricSources = jest.fn().mockReturnValue([testMetricSource]);
|
||||
mocks.pluginLoader.importDataSourcePlugin = jest.fn().mockResolvedValue({
|
||||
components: { VariableQueryEditor: editor },
|
||||
});
|
||||
|
||||
const tester = await reduxTester<{ templating: TemplatingState }>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(initDashboardTemplating([variable]))
|
||||
.whenAsyncActionIsDispatched(initQueryVariableEditor(toVariablePayload(variable)), true);
|
||||
|
||||
tester.thenDispatchedActionPredicateShouldEqual(actions => {
|
||||
const [updateDatasources, setDatasource, setEditor] = actions;
|
||||
const expectedNumberOfActions = 3;
|
||||
|
||||
expect(updateDatasources).toEqual(
|
||||
changeVariableEditorExtended({ propName: 'dataSources', propValue: [defaultMetricSource, testMetricSource] })
|
||||
);
|
||||
expect(setDatasource).toEqual(
|
||||
changeVariableEditorExtended({ propName: 'dataSource', propValue: mocks['datasource'] })
|
||||
);
|
||||
expect(setEditor).toEqual(changeVariableEditorExtended({ propName: 'VariableQueryEditor', propValue: editor }));
|
||||
return actions.length === expectedNumberOfActions;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when initQueryVariableEditor is dispatched and metricsource without value is available', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const variable = createVariable({ includeAll: true, useTags: false });
|
||||
const defaultMetricSource = { name: '', value: '', meta: {}, sort: '' };
|
||||
const testMetricSource = { name: 'test', value: (null as unknown) as string, meta: {}, sort: '' };
|
||||
const editor = {};
|
||||
|
||||
mocks.datasourceSrv.getMetricSources = jest.fn().mockReturnValue([testMetricSource]);
|
||||
mocks.pluginLoader.importDataSourcePlugin = jest.fn().mockResolvedValue({
|
||||
components: { VariableQueryEditor: editor },
|
||||
});
|
||||
|
||||
const tester = await reduxTester<{ templating: TemplatingState }>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(initDashboardTemplating([variable]))
|
||||
.whenAsyncActionIsDispatched(initQueryVariableEditor(toVariablePayload(variable)), true);
|
||||
|
||||
tester.thenDispatchedActionPredicateShouldEqual(actions => {
|
||||
const [updateDatasources, setDatasource, setEditor] = actions;
|
||||
const expectedNumberOfActions = 3;
|
||||
|
||||
expect(updateDatasources).toEqual(
|
||||
changeVariableEditorExtended({ propName: 'dataSources', propValue: [defaultMetricSource] })
|
||||
);
|
||||
expect(setDatasource).toEqual(
|
||||
changeVariableEditorExtended({ propName: 'dataSource', propValue: mocks['datasource'] })
|
||||
);
|
||||
expect(setEditor).toEqual(changeVariableEditorExtended({ propName: 'VariableQueryEditor', propValue: editor }));
|
||||
return actions.length === expectedNumberOfActions;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when initQueryVariableEditor is dispatched and no metric sources was found', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const variable = createVariable({ includeAll: true, useTags: false });
|
||||
const defaultDatasource = { name: '', value: '', meta: {}, sort: '' };
|
||||
const editor = {};
|
||||
|
||||
mocks.datasourceSrv.getMetricSources = jest.fn().mockReturnValue([]);
|
||||
mocks.pluginLoader.importDataSourcePlugin = jest.fn().mockResolvedValue({
|
||||
components: { VariableQueryEditor: editor },
|
||||
});
|
||||
|
||||
const tester = await reduxTester<{ templating: TemplatingState }>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(initDashboardTemplating([variable]))
|
||||
.whenAsyncActionIsDispatched(initQueryVariableEditor(toVariablePayload(variable)), true);
|
||||
|
||||
tester.thenDispatchedActionPredicateShouldEqual(actions => {
|
||||
const [updateDatasources, setDatasource, setEditor] = actions;
|
||||
const expectedNumberOfActions = 3;
|
||||
|
||||
expect(updateDatasources).toEqual(
|
||||
changeVariableEditorExtended({ propName: 'dataSources', propValue: [defaultDatasource] })
|
||||
);
|
||||
expect(setDatasource).toEqual(
|
||||
changeVariableEditorExtended({ propName: 'dataSource', propValue: mocks['datasource'] })
|
||||
);
|
||||
expect(setEditor).toEqual(changeVariableEditorExtended({ propName: 'VariableQueryEditor', propValue: editor }));
|
||||
return actions.length === expectedNumberOfActions;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when initQueryVariableEditor is dispatched and variable dont have datasource', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const variable = createVariable({ datasource: undefined });
|
||||
const ds = { name: '', value: '', meta: {}, sort: '' };
|
||||
|
||||
const tester = await reduxTester<{ templating: TemplatingState }>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(initDashboardTemplating([variable]))
|
||||
.whenAsyncActionIsDispatched(initQueryVariableEditor(toVariablePayload(variable)), true);
|
||||
|
||||
tester.thenDispatchedActionPredicateShouldEqual(actions => {
|
||||
const [updateDatasources] = actions;
|
||||
const expectedNumberOfActions = 1;
|
||||
|
||||
expect(updateDatasources).toEqual(changeVariableEditorExtended({ propName: 'dataSources', propValue: [ds] }));
|
||||
return actions.length === expectedNumberOfActions;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when changeQueryVariableDataSource is dispatched', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const variable = createVariable({ datasource: 'other' });
|
||||
const editor = {};
|
||||
|
||||
mocks.pluginLoader.importDataSourcePlugin = jest.fn().mockResolvedValue({
|
||||
components: { VariableQueryEditor: editor },
|
||||
});
|
||||
|
||||
const tester = await reduxTester<{ templating: TemplatingState }>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(initDashboardTemplating([variable]))
|
||||
.whenAsyncActionIsDispatched(changeQueryVariableDataSource(toVariablePayload(variable), 'datasource'), true);
|
||||
|
||||
tester.thenDispatchedActionPredicateShouldEqual(actions => {
|
||||
const [updateDatasource, updateEditor] = actions;
|
||||
const expectedNumberOfActions = 2;
|
||||
|
||||
expect(updateDatasource).toEqual(
|
||||
changeVariableEditorExtended({ propName: 'dataSource', propValue: mocks.datasource })
|
||||
);
|
||||
expect(updateEditor).toEqual(
|
||||
changeVariableEditorExtended({ propName: 'VariableQueryEditor', propValue: editor })
|
||||
);
|
||||
|
||||
return actions.length === expectedNumberOfActions;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when changeQueryVariableDataSource is dispatched and editor is not configured', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const variable = createVariable({ datasource: 'other' });
|
||||
const editor = DefaultVariableQueryEditor;
|
||||
|
||||
mocks.pluginLoader.importDataSourcePlugin = jest.fn().mockResolvedValue({
|
||||
components: {},
|
||||
});
|
||||
|
||||
const tester = await reduxTester<{ templating: TemplatingState }>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(initDashboardTemplating([variable]))
|
||||
.whenAsyncActionIsDispatched(changeQueryVariableDataSource(toVariablePayload(variable), 'datasource'), true);
|
||||
|
||||
tester.thenDispatchedActionPredicateShouldEqual(actions => {
|
||||
const [updateDatasource, updateEditor] = actions;
|
||||
const expectedNumberOfActions = 2;
|
||||
|
||||
expect(updateDatasource).toEqual(
|
||||
changeVariableEditorExtended({ propName: 'dataSource', propValue: mocks.datasource })
|
||||
);
|
||||
expect(updateEditor).toEqual(
|
||||
changeVariableEditorExtended({ propName: 'VariableQueryEditor', propValue: editor })
|
||||
);
|
||||
|
||||
return actions.length === expectedNumberOfActions;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when changeQueryVariableQuery is dispatched', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const optionsMetrics = [createMetric('A'), createMetric('B')];
|
||||
const tagsMetrics = [createMetric('tagA'), createMetric('tagB')];
|
||||
const variable = createVariable({ datasource: 'datasource', useTags: true, includeAll: true });
|
||||
|
||||
const query = '$datasource';
|
||||
const definition = 'depends on datasource variable';
|
||||
|
||||
mockDatasourceMetrics({ ...variable, query }, optionsMetrics, tagsMetrics);
|
||||
|
||||
const tester = await reduxTester<{ templating: TemplatingState }>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(initDashboardTemplating([variable]))
|
||||
.whenAsyncActionIsDispatched(changeQueryVariableQuery(toVariablePayload(variable), query, definition), true);
|
||||
|
||||
const option = createOption(ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE);
|
||||
|
||||
tester.thenDispatchedActionPredicateShouldEqual(actions => {
|
||||
const [clearError, changeQuery, changeDefinition, updateOptions, updateTags, setOption] = actions;
|
||||
const expectedNumberOfActions = 6;
|
||||
|
||||
expect(clearError).toEqual(removeVariableEditorError({ errorProp: 'query' }));
|
||||
expect(changeQuery).toEqual(
|
||||
changeVariableProp(toVariablePayload(variable, { propName: 'query', propValue: query }))
|
||||
);
|
||||
expect(changeDefinition).toEqual(
|
||||
changeVariableProp(toVariablePayload(variable, { propName: 'definition', propValue: definition }))
|
||||
);
|
||||
expect(updateOptions).toEqual(updateVariableOptions(toVariablePayload(variable, optionsMetrics)));
|
||||
expect(updateTags).toEqual(updateVariableTags(toVariablePayload(variable, tagsMetrics)));
|
||||
expect(setOption).toEqual(setCurrentVariableValue(toVariablePayload(variable, { option })));
|
||||
|
||||
return actions.length === expectedNumberOfActions;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when changeQueryVariableQuery is dispatched for variable without tags', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const optionsMetrics = [createMetric('A'), createMetric('B')];
|
||||
const variable = createVariable({ datasource: 'datasource', useTags: false, includeAll: true });
|
||||
|
||||
const query = '$datasource';
|
||||
const definition = 'depends on datasource variable';
|
||||
|
||||
mockDatasourceMetrics({ ...variable, query }, optionsMetrics, []);
|
||||
|
||||
const tester = await reduxTester<{ templating: TemplatingState }>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(initDashboardTemplating([variable]))
|
||||
.whenAsyncActionIsDispatched(changeQueryVariableQuery(toVariablePayload(variable), query, definition), true);
|
||||
|
||||
const option = createOption(ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE);
|
||||
|
||||
tester.thenDispatchedActionPredicateShouldEqual(actions => {
|
||||
const [clearError, changeQuery, changeDefinition, updateOptions, setOption] = actions;
|
||||
const expectedNumberOfActions = 5;
|
||||
|
||||
expect(clearError).toEqual(removeVariableEditorError({ errorProp: 'query' }));
|
||||
expect(changeQuery).toEqual(
|
||||
changeVariableProp(toVariablePayload(variable, { propName: 'query', propValue: query }))
|
||||
);
|
||||
expect(changeDefinition).toEqual(
|
||||
changeVariableProp(toVariablePayload(variable, { propName: 'definition', propValue: definition }))
|
||||
);
|
||||
expect(updateOptions).toEqual(updateVariableOptions(toVariablePayload(variable, optionsMetrics)));
|
||||
expect(setOption).toEqual(setCurrentVariableValue(toVariablePayload(variable, { option })));
|
||||
|
||||
return actions.length === expectedNumberOfActions;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when changeQueryVariableQuery is dispatched for variable without tags and all', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const optionsMetrics = [createMetric('A'), createMetric('B')];
|
||||
const variable = createVariable({ datasource: 'datasource', useTags: false, includeAll: false });
|
||||
const query = '$datasource';
|
||||
const definition = 'depends on datasource variable';
|
||||
|
||||
mockDatasourceMetrics({ ...variable, query }, optionsMetrics, []);
|
||||
|
||||
const tester = await reduxTester<{ templating: TemplatingState }>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(initDashboardTemplating([variable]))
|
||||
.whenAsyncActionIsDispatched(changeQueryVariableQuery(toVariablePayload(variable), query, definition), true);
|
||||
|
||||
const option = createOption('A');
|
||||
|
||||
tester.thenDispatchedActionPredicateShouldEqual(actions => {
|
||||
const [clearError, changeQuery, changeDefinition, updateOptions, setOption] = actions;
|
||||
const expectedNumberOfActions = 5;
|
||||
|
||||
expect(clearError).toEqual(removeVariableEditorError({ errorProp: 'query' }));
|
||||
expect(changeQuery).toEqual(
|
||||
changeVariableProp(toVariablePayload(variable, { propName: 'query', propValue: query }))
|
||||
);
|
||||
expect(changeDefinition).toEqual(
|
||||
changeVariableProp(toVariablePayload(variable, { propName: 'definition', propValue: definition }))
|
||||
);
|
||||
expect(updateOptions).toEqual(updateVariableOptions(toVariablePayload(variable, optionsMetrics)));
|
||||
expect(setOption).toEqual(setCurrentVariableValue(toVariablePayload(variable, { option })));
|
||||
|
||||
return actions.length === expectedNumberOfActions;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when changeQueryVariableQuery is dispatched with invalid query', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const variable = createVariable({ datasource: 'datasource', useTags: false, includeAll: false });
|
||||
const query = `$${variable.name}`;
|
||||
const definition = 'depends on datasource variable';
|
||||
|
||||
const tester = await reduxTester<{ templating: TemplatingState }>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(initDashboardTemplating([variable]))
|
||||
.whenAsyncActionIsDispatched(changeQueryVariableQuery(toVariablePayload(variable), query, definition), true);
|
||||
|
||||
const errorText = 'Query cannot contain a reference to itself. Variable: $' + variable.name;
|
||||
|
||||
tester.thenDispatchedActionPredicateShouldEqual(actions => {
|
||||
const [editorError] = actions;
|
||||
const expectedNumberOfActions = 1;
|
||||
|
||||
expect(editorError).toEqual(addVariableEditorError({ errorProp: 'query', errorText }));
|
||||
return actions.length === expectedNumberOfActions;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function mockDatasourceMetrics(variable: QueryVariableModel, optionsMetrics: any[], tagsMetrics: any[]) {
|
||||
const metrics: Record<string, any[]> = {
|
||||
[variable.query]: optionsMetrics,
|
||||
[variable.tagsQuery]: tagsMetrics,
|
||||
};
|
||||
|
||||
const { metricFindQuery } = mocks[variable.datasource!];
|
||||
|
||||
metricFindQuery.mockReset();
|
||||
metricFindQuery.mockImplementation((query: string) => Promise.resolve(metrics[query] ?? []));
|
||||
}
|
||||
|
||||
function createVariable(extend?: Partial<QueryVariableModel>): QueryVariableModel {
|
||||
return {
|
||||
type: 'query',
|
||||
uuid: '0',
|
||||
global: false,
|
||||
current: createOption(''),
|
||||
options: [],
|
||||
query: 'options-query',
|
||||
name: 'Constant',
|
||||
label: '',
|
||||
hide: VariableHide.dontHide,
|
||||
skipUrlSync: false,
|
||||
index: 0,
|
||||
datasource: 'datasource',
|
||||
definition: '',
|
||||
sort: VariableSort.alphabeticalAsc,
|
||||
tags: [],
|
||||
tagsQuery: 'tags-query',
|
||||
tagValuesQuery: '',
|
||||
useTags: true,
|
||||
refresh: VariableRefresh.onDashboardLoad,
|
||||
regex: '',
|
||||
multi: true,
|
||||
includeAll: true,
|
||||
...(extend ?? {}),
|
||||
};
|
||||
}
|
||||
|
||||
function createOption(text: string, value?: string) {
|
||||
const metric = createMetric(text);
|
||||
return {
|
||||
...metric,
|
||||
value: value ?? metric.value,
|
||||
selected: false,
|
||||
};
|
||||
}
|
||||
|
||||
function createMetric(value: string) {
|
||||
return {
|
||||
value: value,
|
||||
text: value,
|
||||
};
|
||||
}
|
||||
115
public/app/features/variables/query/actions.ts
Normal file
115
public/app/features/variables/query/actions.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { AppEvents, DataSourcePluginMeta, DataSourceSelectItem } from '@grafana/data';
|
||||
|
||||
import { validateVariableSelectionState } from '../state/actions';
|
||||
import { QueryVariableModel, VariableRefresh } from '../../templating/variable';
|
||||
import { ThunkResult } from '../../../types';
|
||||
import { getDatasourceSrv } from '../../plugins/datasource_srv';
|
||||
import { getTimeSrv } from '../../dashboard/services/TimeSrv';
|
||||
import appEvents from '../../../core/app_events';
|
||||
import { importDataSourcePlugin } from '../../plugins/plugin_loader';
|
||||
import DefaultVariableQueryEditor from '../../templating/DefaultVariableQueryEditor';
|
||||
import { getVariable } from '../state/selectors';
|
||||
import { addVariableEditorError, changeVariableEditorExtended, removeVariableEditorError } from '../editor/reducer';
|
||||
import { variableAdapters } from '../adapters';
|
||||
import { changeVariableProp } from '../state/sharedReducer';
|
||||
import { updateVariableOptions, updateVariableTags } from './reducer';
|
||||
import { toVariableIdentifier, toVariablePayload, VariableIdentifier } from '../state/types';
|
||||
|
||||
export const updateQueryVariableOptions = (
|
||||
identifier: VariableIdentifier,
|
||||
searchFilter?: string
|
||||
): ThunkResult<void> => {
|
||||
return async (dispatch, getState) => {
|
||||
const variableInState = getVariable<QueryVariableModel>(identifier.uuid!, getState());
|
||||
try {
|
||||
if (getState().templating.editor.id === variableInState.uuid) {
|
||||
dispatch(removeVariableEditorError({ errorProp: 'update' }));
|
||||
}
|
||||
const dataSource = await getDatasourceSrv().get(variableInState.datasource ?? '');
|
||||
const queryOptions: any = { range: undefined, variable: variableInState, searchFilter };
|
||||
if (variableInState.refresh === VariableRefresh.onTimeRangeChanged) {
|
||||
queryOptions.range = getTimeSrv().timeRange();
|
||||
}
|
||||
|
||||
if (!dataSource.metricFindQuery) {
|
||||
return;
|
||||
}
|
||||
|
||||
const results = await dataSource.metricFindQuery(variableInState.query, queryOptions);
|
||||
await dispatch(updateVariableOptions(toVariablePayload(variableInState, results)));
|
||||
|
||||
if (variableInState.useTags) {
|
||||
const tagResults = await dataSource.metricFindQuery(variableInState.tagsQuery, queryOptions);
|
||||
await dispatch(updateVariableTags(toVariablePayload(variableInState, tagResults)));
|
||||
}
|
||||
|
||||
await dispatch(validateVariableSelectionState(toVariableIdentifier(variableInState)));
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
if (err.data && err.data.message) {
|
||||
err.message = err.data.message;
|
||||
}
|
||||
if (getState().templating.editor.id === variableInState.uuid) {
|
||||
dispatch(addVariableEditorError({ errorProp: 'update', errorText: err.message }));
|
||||
}
|
||||
appEvents.emit(AppEvents.alertError, [
|
||||
'Templating',
|
||||
'Template variables could not be initialized: ' + err.message,
|
||||
]);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const initQueryVariableEditor = (identifier: VariableIdentifier): ThunkResult<void> => async (
|
||||
dispatch,
|
||||
getState
|
||||
) => {
|
||||
const dataSources: DataSourceSelectItem[] = getDatasourceSrv()
|
||||
.getMetricSources()
|
||||
.filter(ds => !ds.meta.mixed && ds.value !== null);
|
||||
|
||||
const defaultDatasource: DataSourceSelectItem = { name: '', value: '', meta: {} as DataSourcePluginMeta, sort: '' };
|
||||
const allDataSources = [defaultDatasource].concat(dataSources);
|
||||
dispatch(changeVariableEditorExtended({ propName: 'dataSources', propValue: allDataSources }));
|
||||
|
||||
const variable = getVariable<QueryVariableModel>(identifier.uuid!, getState());
|
||||
if (!variable.datasource) {
|
||||
return;
|
||||
}
|
||||
dispatch(changeQueryVariableDataSource(toVariableIdentifier(variable), variable.datasource));
|
||||
};
|
||||
|
||||
export const changeQueryVariableDataSource = (
|
||||
identifier: VariableIdentifier,
|
||||
name: string | null
|
||||
): ThunkResult<void> => {
|
||||
return async (dispatch, getState) => {
|
||||
try {
|
||||
const dataSource = await getDatasourceSrv().get(name ?? '');
|
||||
const dsPlugin = await importDataSourcePlugin(dataSource.meta!);
|
||||
const VariableQueryEditor = dsPlugin.components.VariableQueryEditor ?? DefaultVariableQueryEditor;
|
||||
dispatch(changeVariableEditorExtended({ propName: 'dataSource', propValue: dataSource }));
|
||||
dispatch(changeVariableEditorExtended({ propName: 'VariableQueryEditor', propValue: VariableQueryEditor }));
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const changeQueryVariableQuery = (
|
||||
identifier: VariableIdentifier,
|
||||
query: any,
|
||||
definition: string
|
||||
): ThunkResult<void> => async (dispatch, getState) => {
|
||||
const variableInState = getVariable<QueryVariableModel>(identifier.uuid!, getState());
|
||||
if (typeof query === 'string' && query.match(new RegExp('\\$' + variableInState.name + '(/| |$)'))) {
|
||||
const errorText = 'Query cannot contain a reference to itself. Variable: $' + variableInState.name;
|
||||
dispatch(addVariableEditorError({ errorProp: 'query', errorText }));
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(removeVariableEditorError({ errorProp: 'query' }));
|
||||
dispatch(changeVariableProp(toVariablePayload(identifier, { propName: 'query', propValue: query })));
|
||||
dispatch(changeVariableProp(toVariablePayload(identifier, { propName: 'definition', propValue: definition })));
|
||||
await variableAdapters.get(identifier.type).updateOptions(variableInState);
|
||||
};
|
||||
49
public/app/features/variables/query/adapter.ts
Normal file
49
public/app/features/variables/query/adapter.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
|
||||
import { containsVariable, QueryVariableModel, VariableRefresh } from '../../templating/variable';
|
||||
import { initialQueryVariableModelState, queryVariableReducer } from './reducer';
|
||||
import { dispatch } from '../../../store/store';
|
||||
import { setOptionAsCurrent, setOptionFromUrl } from '../state/actions';
|
||||
import { VariableAdapter } from '../adapters';
|
||||
import { OptionsPicker } from '../pickers';
|
||||
import { QueryVariableEditor } from './QueryVariableEditor';
|
||||
import { updateQueryVariableOptions } from './actions';
|
||||
import { ALL_VARIABLE_TEXT, toVariableIdentifier } from '../state/types';
|
||||
|
||||
export const createQueryVariableAdapter = (): VariableAdapter<QueryVariableModel> => {
|
||||
return {
|
||||
description: 'Variable values are fetched from a datasource query',
|
||||
label: 'Query',
|
||||
initialState: initialQueryVariableModelState,
|
||||
reducer: queryVariableReducer,
|
||||
picker: OptionsPicker,
|
||||
editor: QueryVariableEditor,
|
||||
dependsOn: (variable, variableToTest) => {
|
||||
return containsVariable(variable.query, variable.datasource, variable.regex, variableToTest.name);
|
||||
},
|
||||
setValue: async (variable, option, emitChanges = false) => {
|
||||
await dispatch(setOptionAsCurrent(toVariableIdentifier(variable), option, emitChanges));
|
||||
},
|
||||
setValueFromUrl: async (variable, urlValue) => {
|
||||
await dispatch(setOptionFromUrl(toVariableIdentifier(variable), urlValue));
|
||||
},
|
||||
updateOptions: async (variable, searchFilter) => {
|
||||
await dispatch(updateQueryVariableOptions(toVariableIdentifier(variable), searchFilter));
|
||||
},
|
||||
getSaveModel: variable => {
|
||||
const { index, uuid, initLock, global, queryValue, ...rest } = cloneDeep(variable);
|
||||
// remove options
|
||||
if (variable.refresh !== VariableRefresh.never) {
|
||||
return { ...rest, options: [] };
|
||||
}
|
||||
|
||||
return rest;
|
||||
},
|
||||
getValueForUrl: variable => {
|
||||
if (variable.current.text === ALL_VARIABLE_TEXT) {
|
||||
return ALL_VARIABLE_TEXT;
|
||||
}
|
||||
return variable.current.value;
|
||||
},
|
||||
};
|
||||
};
|
||||
161
public/app/features/variables/query/reducer.test.ts
Normal file
161
public/app/features/variables/query/reducer.test.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { reducerTester } from '../../../../test/core/redux/reducerTester';
|
||||
import { queryVariableReducer, updateVariableOptions, updateVariableTags } from './reducer';
|
||||
import { QueryVariableModel, VariableOption } from '../../templating/variable';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { VariablesState } from '../state/variablesReducer';
|
||||
import { getVariableTestContext } from '../state/helpers';
|
||||
import { toVariablePayload } from '../state/types';
|
||||
import { createQueryVariableAdapter } from './adapter';
|
||||
|
||||
describe('queryVariableReducer', () => {
|
||||
const adapter = createQueryVariableAdapter();
|
||||
|
||||
describe('when updateVariableOptions is dispatched and includeAll is true', () => {
|
||||
it('then state should be correct', () => {
|
||||
const { initialState } = getVariableTestContext(adapter, { includeAll: true });
|
||||
const options: VariableOption[] = [
|
||||
{ text: 'A', value: 'A', selected: false },
|
||||
{ text: 'B', value: 'B', selected: false },
|
||||
];
|
||||
const payload = toVariablePayload({ uuid: '0', type: 'query' }, options);
|
||||
reducerTester<VariablesState>()
|
||||
.givenReducer(queryVariableReducer, cloneDeep(initialState))
|
||||
.whenActionIsDispatched(updateVariableOptions(payload))
|
||||
.thenStateShouldEqual({
|
||||
...initialState,
|
||||
'0': ({
|
||||
...initialState[0],
|
||||
options: [
|
||||
{ text: 'All', value: '$__all', selected: false },
|
||||
{ text: 'A', value: 'A', selected: false },
|
||||
{ text: 'B', value: 'B', selected: false },
|
||||
],
|
||||
} as unknown) as QueryVariableModel,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when updateVariableOptions is dispatched and includeAll is false', () => {
|
||||
it('then state should be correct', () => {
|
||||
const { initialState } = getVariableTestContext(adapter, { includeAll: false });
|
||||
const options: VariableOption[] = [
|
||||
{ text: 'A', value: 'A', selected: false },
|
||||
{ text: 'B', value: 'B', selected: false },
|
||||
];
|
||||
const payload = toVariablePayload({ uuid: '0', type: 'query' }, options);
|
||||
reducerTester<VariablesState>()
|
||||
.givenReducer(queryVariableReducer, cloneDeep(initialState))
|
||||
.whenActionIsDispatched(updateVariableOptions(payload))
|
||||
.thenStateShouldEqual({
|
||||
...initialState,
|
||||
'0': ({
|
||||
...initialState[0],
|
||||
options: [
|
||||
{ text: 'A', value: 'A', selected: false },
|
||||
{ text: 'B', value: 'B', selected: false },
|
||||
],
|
||||
} as unknown) as QueryVariableModel,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when updateVariableOptions is dispatched and includeAll is true and payload is an empty array', () => {
|
||||
it('then state should be correct', () => {
|
||||
const { initialState } = getVariableTestContext(adapter, { includeAll: true });
|
||||
const payload = toVariablePayload({ uuid: '0', type: 'query' }, []);
|
||||
reducerTester<VariablesState>()
|
||||
.givenReducer(queryVariableReducer, cloneDeep(initialState))
|
||||
.whenActionIsDispatched(updateVariableOptions(payload))
|
||||
.thenStateShouldEqual({
|
||||
...initialState,
|
||||
'0': ({
|
||||
...initialState[0],
|
||||
options: [{ text: 'All', value: '$__all', selected: false }],
|
||||
} as unknown) as QueryVariableModel,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when updateVariableOptions is dispatched and includeAll is false and payload is an empty array', () => {
|
||||
it('then state should be correct', () => {
|
||||
const { initialState } = getVariableTestContext(adapter, { includeAll: false });
|
||||
const payload = toVariablePayload({ uuid: '0', type: 'query' }, []);
|
||||
reducerTester<VariablesState>()
|
||||
.givenReducer(queryVariableReducer, cloneDeep(initialState))
|
||||
.whenActionIsDispatched(updateVariableOptions(payload))
|
||||
.thenStateShouldEqual({
|
||||
...initialState,
|
||||
'0': ({
|
||||
...initialState[0],
|
||||
options: [{ text: 'None', value: '', selected: false, isNone: true }],
|
||||
} as unknown) as QueryVariableModel,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when updateVariableOptions is dispatched and includeAll is true and regex is set', () => {
|
||||
it('then state should be correct', () => {
|
||||
const { initialState } = getVariableTestContext(adapter, { includeAll: true, regex: '/.*(a).*/i' });
|
||||
const options: VariableOption[] = [
|
||||
{ text: 'A', value: 'A', selected: false },
|
||||
{ text: 'B', value: 'B', selected: false },
|
||||
];
|
||||
const payload = toVariablePayload({ uuid: '0', type: 'query' }, options);
|
||||
reducerTester<VariablesState>()
|
||||
.givenReducer(queryVariableReducer, cloneDeep(initialState))
|
||||
.whenActionIsDispatched(updateVariableOptions(payload))
|
||||
.thenStateShouldEqual({
|
||||
...initialState,
|
||||
'0': ({
|
||||
...initialState[0],
|
||||
options: [
|
||||
{ text: 'All', value: '$__all', selected: false },
|
||||
{ text: 'A', value: 'A', selected: false },
|
||||
],
|
||||
} as unknown) as QueryVariableModel,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when updateVariableOptions is dispatched and includeAll is false and regex is set', () => {
|
||||
it('then state should be correct', () => {
|
||||
const { initialState } = getVariableTestContext(adapter, { includeAll: false, regex: '/.*(a).*/i' });
|
||||
const options: VariableOption[] = [
|
||||
{ text: 'A', value: 'A', selected: false },
|
||||
{ text: 'B', value: 'B', selected: false },
|
||||
];
|
||||
const payload = toVariablePayload({ uuid: '0', type: 'query' }, options);
|
||||
reducerTester<VariablesState>()
|
||||
.givenReducer(queryVariableReducer, cloneDeep(initialState))
|
||||
.whenActionIsDispatched(updateVariableOptions(payload))
|
||||
.thenStateShouldEqual({
|
||||
...initialState,
|
||||
'0': ({
|
||||
...initialState[0],
|
||||
options: [{ text: 'A', value: 'A', selected: false }],
|
||||
} as unknown) as QueryVariableModel,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when updateVariableTags is dispatched', () => {
|
||||
it('then state should be correct', () => {
|
||||
const { initialState } = getVariableTestContext(adapter);
|
||||
const tags: any[] = [{ text: 'A' }, { text: 'B' }];
|
||||
const payload = toVariablePayload({ uuid: '0', type: 'query' }, tags);
|
||||
reducerTester<VariablesState>()
|
||||
.givenReducer(queryVariableReducer, cloneDeep(initialState))
|
||||
.whenActionIsDispatched(updateVariableTags(payload))
|
||||
.thenStateShouldEqual({
|
||||
...initialState,
|
||||
'0': ({
|
||||
...initialState[0],
|
||||
tags: [
|
||||
{ text: 'A', selected: false },
|
||||
{ text: 'B', selected: false },
|
||||
],
|
||||
} as unknown) as QueryVariableModel,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
165
public/app/features/variables/query/reducer.ts
Normal file
165
public/app/features/variables/query/reducer.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import _ from 'lodash';
|
||||
import { DataSourceApi, DataSourceSelectItem, stringToJsRegex } from '@grafana/data';
|
||||
|
||||
import {
|
||||
QueryVariableModel,
|
||||
VariableHide,
|
||||
VariableOption,
|
||||
VariableRefresh,
|
||||
VariableSort,
|
||||
VariableTag,
|
||||
} from '../../templating/variable';
|
||||
import templateSrv from '../../templating/template_srv';
|
||||
import {
|
||||
ALL_VARIABLE_TEXT,
|
||||
ALL_VARIABLE_VALUE,
|
||||
EMPTY_UUID,
|
||||
getInstanceState,
|
||||
NONE_VARIABLE_TEXT,
|
||||
NONE_VARIABLE_VALUE,
|
||||
VariablePayload,
|
||||
} from '../state/types';
|
||||
import { ComponentType } from 'react';
|
||||
import { VariableQueryProps } from '../../../types';
|
||||
import { initialVariablesState, VariablesState } from '../state/variablesReducer';
|
||||
|
||||
export interface QueryVariableEditorState {
|
||||
VariableQueryEditor: ComponentType<VariableQueryProps> | null;
|
||||
dataSources: DataSourceSelectItem[];
|
||||
dataSource: DataSourceApi | null;
|
||||
}
|
||||
|
||||
export const initialQueryVariableModelState: QueryVariableModel = {
|
||||
uuid: EMPTY_UUID,
|
||||
global: false,
|
||||
index: -1,
|
||||
type: 'query',
|
||||
name: '',
|
||||
label: null,
|
||||
hide: VariableHide.dontHide,
|
||||
skipUrlSync: false,
|
||||
datasource: null,
|
||||
query: '',
|
||||
regex: '',
|
||||
sort: VariableSort.disabled,
|
||||
refresh: VariableRefresh.never,
|
||||
multi: false,
|
||||
includeAll: false,
|
||||
allValue: null,
|
||||
options: [],
|
||||
current: {} as VariableOption,
|
||||
tags: [],
|
||||
useTags: false,
|
||||
tagsQuery: '',
|
||||
tagValuesQuery: '',
|
||||
definition: '',
|
||||
initLock: null,
|
||||
};
|
||||
|
||||
const sortVariableValues = (options: any[], sortOrder: VariableSort) => {
|
||||
if (sortOrder === VariableSort.disabled) {
|
||||
return options;
|
||||
}
|
||||
|
||||
const sortType = Math.ceil(sortOrder / 2);
|
||||
const reverseSort = sortOrder % 2 === 0;
|
||||
|
||||
if (sortType === 1) {
|
||||
options = _.sortBy(options, 'text');
|
||||
} else if (sortType === 2) {
|
||||
options = _.sortBy(options, opt => {
|
||||
const matches = opt.text.match(/.*?(\d+).*/);
|
||||
if (!matches || matches.length < 2) {
|
||||
return -1;
|
||||
} else {
|
||||
return parseInt(matches[1], 10);
|
||||
}
|
||||
});
|
||||
} else if (sortType === 3) {
|
||||
options = _.sortBy(options, opt => {
|
||||
return _.toLower(opt.text);
|
||||
});
|
||||
}
|
||||
|
||||
if (reverseSort) {
|
||||
options = options.reverse();
|
||||
}
|
||||
|
||||
return options;
|
||||
};
|
||||
|
||||
const metricNamesToVariableValues = (variableRegEx: string, sort: VariableSort, metricNames: any[]) => {
|
||||
let regex, i, matches;
|
||||
let options: VariableOption[] = [];
|
||||
|
||||
if (variableRegEx) {
|
||||
regex = stringToJsRegex(templateSrv.replace(variableRegEx, {}, 'regex'));
|
||||
}
|
||||
for (i = 0; i < metricNames.length; i++) {
|
||||
const item = metricNames[i];
|
||||
let text = item.text === undefined || item.text === null ? item.value : item.text;
|
||||
|
||||
let value = item.value === undefined || item.value === null ? item.text : item.value;
|
||||
|
||||
if (_.isNumber(value)) {
|
||||
value = value.toString();
|
||||
}
|
||||
|
||||
if (_.isNumber(text)) {
|
||||
text = text.toString();
|
||||
}
|
||||
|
||||
if (regex) {
|
||||
matches = regex.exec(value);
|
||||
if (!matches) {
|
||||
continue;
|
||||
}
|
||||
if (matches.length > 1) {
|
||||
value = matches[1];
|
||||
text = matches[1];
|
||||
}
|
||||
}
|
||||
|
||||
options.push({ text: text, value: value, selected: false });
|
||||
}
|
||||
|
||||
options = _.uniqBy(options, 'value');
|
||||
return sortVariableValues(options, sort);
|
||||
};
|
||||
|
||||
export const queryVariableSlice = createSlice({
|
||||
name: 'templating/query',
|
||||
initialState: initialVariablesState,
|
||||
reducers: {
|
||||
updateVariableOptions: (state: VariablesState, action: PayloadAction<VariablePayload<any[]>>) => {
|
||||
const results = action.payload.data;
|
||||
const instanceState = getInstanceState<QueryVariableModel>(state, action.payload.uuid);
|
||||
const { regex, includeAll, sort } = instanceState;
|
||||
const options = metricNamesToVariableValues(regex, sort, results);
|
||||
|
||||
if (includeAll) {
|
||||
options.unshift({ text: ALL_VARIABLE_TEXT, value: ALL_VARIABLE_VALUE, selected: false });
|
||||
}
|
||||
if (!options.length) {
|
||||
options.push({ text: NONE_VARIABLE_TEXT, value: NONE_VARIABLE_VALUE, isNone: true, selected: false });
|
||||
}
|
||||
|
||||
instanceState.options = options;
|
||||
},
|
||||
updateVariableTags: (state: VariablesState, action: PayloadAction<VariablePayload<any[]>>) => {
|
||||
const instanceState = getInstanceState<QueryVariableModel>(state, action.payload.uuid);
|
||||
const results = action.payload.data;
|
||||
const tags: VariableTag[] = [];
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
tags.push({ text: results[i].text, selected: false });
|
||||
}
|
||||
|
||||
instanceState.tags = tags;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const queryVariableReducer = queryVariableSlice.reducer;
|
||||
|
||||
export const { updateVariableOptions, updateVariableTags } = queryVariableSlice.actions;
|
||||
Reference in New Issue
Block a user