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:
Hugo Häggmark
2020-03-16 06:32:04 +01:00
committed by GitHub
parent b30f4c7bb0
commit 1db067396a
74 changed files with 652 additions and 85 deletions

View 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
);

View 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,
};
}

View 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);
};

View 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;
},
};
};

View 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,
});
});
});
});

View 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;