mirror of https://github.com/grafana/grafana
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 errorspull/22144/head^2
parent
b30f4c7bb0
commit
1db067396a
@ -1,7 +1,7 @@ |
||||
import React, { ChangeEvent, FocusEvent, PureComponent } from 'react'; |
||||
import { e2e } from '@grafana/e2e'; |
||||
|
||||
import { ConstantVariableModel } from '../variable'; |
||||
import { ConstantVariableModel } from '../../templating/variable'; |
||||
import { VariableEditorProps } from '../editor/types'; |
||||
|
||||
export interface Props extends VariableEditorProps<ConstantVariableModel> {} |
@ -1,10 +1,10 @@ |
||||
import { variableAdapters } from '../adapters'; |
||||
import { createConstantVariableAdapter } from '../constant/adapter'; |
||||
import { createConstantVariableAdapter } from './adapter'; |
||||
import { reduxTester } from '../../../../test/core/redux/reduxTester'; |
||||
import { TemplatingState } from 'app/features/templating/state/reducers'; |
||||
import { TemplatingState } from 'app/features/variables/state/reducers'; |
||||
import { updateConstantVariableOptions } from './actions'; |
||||
import { getTemplatingRootReducer } from '../state/helpers'; |
||||
import { ConstantVariableModel, VariableOption, VariableHide } from '../variable'; |
||||
import { ConstantVariableModel, VariableHide, VariableOption } from '../../templating/variable'; |
||||
import { toVariablePayload } from '../state/types'; |
||||
import { createConstantOptionsFromQuery } from './reducer'; |
||||
import { setCurrentVariableValue } from '../state/sharedReducer'; |
@ -1,5 +1,5 @@ |
||||
import cloneDeep from 'lodash/cloneDeep'; |
||||
import { ConstantVariableModel } from '../variable'; |
||||
import { ConstantVariableModel } from '../../templating/variable'; |
||||
import { dispatch } from '../../../store/store'; |
||||
import { setOptionAsCurrent, setOptionFromUrl } from '../state/actions'; |
||||
import { VariableAdapter } from '../adapters'; |
@ -1,5 +1,5 @@ |
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'; |
||||
import { ConstantVariableModel, VariableHide, VariableOption } from '../variable'; |
||||
import { ConstantVariableModel, VariableHide, VariableOption } from '../../templating/variable'; |
||||
import { EMPTY_UUID, getInstanceState, VariablePayload } from '../state/types'; |
||||
import { initialVariablesState, VariablesState } from '../state/variablesReducer'; |
||||
|
@ -1,5 +1,5 @@ |
||||
import React, { ChangeEvent, FocusEvent, PureComponent } from 'react'; |
||||
import { CustomVariableModel, VariableWithMultiSupport } from '../variable'; |
||||
import { CustomVariableModel, VariableWithMultiSupport } from '../../templating/variable'; |
||||
import { SelectionOptionsEditor } from '../editor/SelectionOptionsEditor'; |
||||
import { OnPropChangeArguments, VariableEditorProps } from '../editor/types'; |
||||
|
@ -1,5 +1,5 @@ |
||||
import cloneDeep from 'lodash/cloneDeep'; |
||||
import { CustomVariableModel } from '../variable'; |
||||
import { CustomVariableModel } from '../../templating/variable'; |
||||
import { dispatch } from '../../../store/store'; |
||||
import { setOptionAsCurrent, setOptionFromUrl } from '../state/actions'; |
||||
import { VariableAdapter } from '../adapters'; |
@ -1,11 +1,11 @@ |
||||
import { reducerTester } from '../../../../test/core/redux/reducerTester'; |
||||
import cloneDeep from 'lodash/cloneDeep'; |
||||
import { getVariableTestContext } from '../state/helpers'; |
||||
import { toVariablePayload, ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from '../state/types'; |
||||
import { customVariableReducer, createCustomOptionsFromQuery } from './reducer'; |
||||
import { createCustomVariableAdapter } from '../custom/adapter'; |
||||
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE, toVariablePayload } from '../state/types'; |
||||
import { createCustomOptionsFromQuery, customVariableReducer } from './reducer'; |
||||
import { createCustomVariableAdapter } from './adapter'; |
||||
import { VariablesState } from '../state/variablesReducer'; |
||||
import { CustomVariableModel } from '../variable'; |
||||
import { CustomVariableModel } from '../../templating/variable'; |
||||
|
||||
describe('customVariableReducer', () => { |
||||
const adapter = createCustomVariableAdapter(); |
@ -1,6 +1,6 @@ |
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'; |
||||
|
||||
import { CustomVariableModel, VariableHide, VariableOption } from '../variable'; |
||||
import { CustomVariableModel, VariableHide, VariableOption } from '../../templating/variable'; |
||||
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE, EMPTY_UUID, getInstanceState, VariablePayload } from '../state/types'; |
||||
import { initialVariablesState, VariablesState } from '../state/variablesReducer'; |
||||
|
@ -0,0 +1,131 @@ |
||||
import React, { ChangeEvent, FocusEvent, PureComponent } from 'react'; |
||||
|
||||
import { DataSourceVariableModel, VariableWithMultiSupport } from '../../templating/variable'; |
||||
import { OnPropChangeArguments, VariableEditorProps } from '../editor/types'; |
||||
import { SelectionOptionsEditor } from '../editor/SelectionOptionsEditor'; |
||||
import { FormLabel } from '@grafana/ui'; |
||||
import { VariableEditorState } from '../editor/reducer'; |
||||
import { DataSourceVariableEditorState } from './reducer'; |
||||
import { initDataSourceVariableEditor } from './actions'; |
||||
import { MapDispatchToProps, MapStateToProps } from 'react-redux'; |
||||
import { StoreState } from '../../../types'; |
||||
import { connectWithStore } from '../../../core/utils/connectWithReduxStore'; |
||||
|
||||
export interface OwnProps extends VariableEditorProps<DataSourceVariableModel> {} |
||||
|
||||
interface ConnectedProps { |
||||
editor: VariableEditorState<DataSourceVariableEditorState>; |
||||
} |
||||
|
||||
interface DispatchProps { |
||||
initDataSourceVariableEditor: typeof initDataSourceVariableEditor; |
||||
} |
||||
|
||||
type Props = OwnProps & ConnectedProps & DispatchProps; |
||||
|
||||
export class DataSourceVariableEditorUnConnected extends PureComponent<Props> { |
||||
async componentDidMount() { |
||||
await this.props.initDataSourceVariableEditor(); |
||||
} |
||||
|
||||
onRegExChange = (event: ChangeEvent<HTMLInputElement>) => { |
||||
this.props.onPropChange({ |
||||
propName: 'regex', |
||||
propValue: event.target.value, |
||||
}); |
||||
}; |
||||
|
||||
onRegExBlur = (event: FocusEvent<HTMLInputElement>) => { |
||||
this.props.onPropChange({ |
||||
propName: 'regex', |
||||
propValue: event.target.value, |
||||
updateOptions: true, |
||||
}); |
||||
}; |
||||
|
||||
onSelectionOptionsChange = async ({ propValue, propName }: OnPropChangeArguments<VariableWithMultiSupport>) => { |
||||
this.props.onPropChange({ propName, propValue, updateOptions: true }); |
||||
}; |
||||
|
||||
getSelectedDataSourceTypeValue = (): string => { |
||||
if (!this.props.editor.extended?.dataSourceTypes?.length) { |
||||
return ''; |
||||
} |
||||
const foundItem = this.props.editor.extended?.dataSourceTypes.find(ds => ds.value === this.props.variable.query); |
||||
const value = foundItem ? foundItem.value : this.props.editor.extended?.dataSourceTypes[0].value; |
||||
return value ?? ''; |
||||
}; |
||||
|
||||
onDataSourceTypeChanged = (event: ChangeEvent<HTMLSelectElement>) => { |
||||
this.props.onPropChange({ propName: 'query', propValue: event.target.value, updateOptions: true }); |
||||
}; |
||||
|
||||
render() { |
||||
return ( |
||||
<> |
||||
<div className="gf-form-group"> |
||||
<h5 className="section-heading">Data source options</h5> |
||||
|
||||
<div className="gf-form"> |
||||
<label className="gf-form-label width-12">Type</label> |
||||
<div className="gf-form-select-wrapper max-width-18"> |
||||
<select |
||||
className="gf-form-input" |
||||
value={this.getSelectedDataSourceTypeValue()} |
||||
onChange={this.onDataSourceTypeChanged} |
||||
> |
||||
{this.props.editor.extended?.dataSourceTypes?.length && |
||||
this.props.editor.extended?.dataSourceTypes?.map(ds => ( |
||||
<option key={ds.value ?? ''} value={ds.value ?? ''} label={ds.text}> |
||||
{ds.text} |
||||
</option> |
||||
))} |
||||
</select> |
||||
</div> |
||||
</div> |
||||
|
||||
<div className="gf-form"> |
||||
<FormLabel |
||||
width={12} |
||||
tooltip={ |
||||
<div> |
||||
Regex filter for which data source instances to choose from in the variable value dropdown. Leave |
||||
empty for all. |
||||
<br /> |
||||
<br /> |
||||
Example: <code>/^prod/</code> |
||||
</div> |
||||
} |
||||
> |
||||
Instance name filter |
||||
</FormLabel> |
||||
<input |
||||
type="text" |
||||
className="gf-form-input max-width-18" |
||||
placeholder="/.*-(.*)-.*/" |
||||
value={this.props.variable.regex} |
||||
onChange={this.onRegExChange} |
||||
onBlur={this.onRegExBlur} |
||||
/> |
||||
</div> |
||||
</div> |
||||
|
||||
<SelectionOptionsEditor variable={this.props.variable} onPropChange={this.onSelectionOptionsChange} /> |
||||
</> |
||||
); |
||||
} |
||||
} |
||||
|
||||
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (state, ownProps) => ({ |
||||
editor: state.templating.editor as VariableEditorState<DataSourceVariableEditorState>, |
||||
}); |
||||
|
||||
const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = { |
||||
initDataSourceVariableEditor, |
||||
}; |
||||
|
||||
export const DataSourceVariableEditor = connectWithStore( |
||||
DataSourceVariableEditorUnConnected, |
||||
mapStateToProps, |
||||
mapDispatchToProps |
||||
); |
@ -0,0 +1,175 @@ |
||||
import { reduxTester } from '../../../../test/core/redux/reduxTester'; |
||||
import { TemplatingState } from '../state/reducers'; |
||||
import { getTemplatingRootReducer, variableMockBuilder } from '../state/helpers'; |
||||
import { initDashboardTemplating } from '../state/actions'; |
||||
import { toVariableIdentifier, toVariablePayload } from '../state/types'; |
||||
import { variableAdapters } from '../adapters'; |
||||
import { createDataSourceVariableAdapter } from './adapter'; |
||||
import { |
||||
DataSourceVariableActionDependencies, |
||||
initDataSourceVariableEditor, |
||||
updateDataSourceVariableOptions, |
||||
} from './actions'; |
||||
import { DataSourcePluginMeta, DataSourceSelectItem } from '@grafana/data'; |
||||
import { getMockPlugin } from '../../plugins/__mocks__/pluginMocks'; |
||||
import { createDataSourceOptions } from './reducer'; |
||||
import { setCurrentVariableValue } from '../state/sharedReducer'; |
||||
import { changeVariableEditorExtended } from '../editor/reducer'; |
||||
|
||||
describe('data source actions', () => { |
||||
variableAdapters.set('datasource', createDataSourceVariableAdapter()); |
||||
|
||||
describe('when updateDataSourceVariableOptions is dispatched', () => { |
||||
describe('and there is no regex', () => { |
||||
it('then the correct actions are dispatched', async () => { |
||||
const sources: DataSourceSelectItem[] = [ |
||||
{ |
||||
name: 'first-name', |
||||
value: 'first-value', |
||||
meta: getMockPlugin({ name: 'mock-data-name', id: 'mock-data-id' }), |
||||
sort: '', |
||||
}, |
||||
{ |
||||
name: 'second-name', |
||||
value: 'second-value', |
||||
meta: getMockPlugin({ name: 'mock-data-name', id: 'mock-data-id' }), |
||||
sort: '', |
||||
}, |
||||
]; |
||||
|
||||
const getMetricSourcesMock = jest.fn().mockResolvedValue(sources); |
||||
const getDatasourceSrvMock = jest.fn().mockReturnValue({ getMetricSources: getMetricSourcesMock }); |
||||
const dependencies: DataSourceVariableActionDependencies = { getDatasourceSrv: getDatasourceSrvMock }; |
||||
const datasource = variableMockBuilder('datasource') |
||||
.withUuid('0') |
||||
.withQuery('mock-data-id') |
||||
.create(); |
||||
const tester = await reduxTester<{ templating: TemplatingState }>() |
||||
.givenRootReducer(getTemplatingRootReducer()) |
||||
.whenActionIsDispatched(initDashboardTemplating([datasource])) |
||||
.whenAsyncActionIsDispatched( |
||||
updateDataSourceVariableOptions(toVariableIdentifier(datasource), dependencies), |
||||
true |
||||
); |
||||
|
||||
await tester.thenDispatchedActionShouldEqual( |
||||
createDataSourceOptions( |
||||
toVariablePayload({ type: 'datasource', uuid: '0' }, { sources, regex: (undefined as unknown) as RegExp }) |
||||
), |
||||
setCurrentVariableValue( |
||||
toVariablePayload( |
||||
{ type: 'datasource', uuid: '0' }, |
||||
{ option: { text: 'first-name', value: 'first-name', selected: false } } |
||||
) |
||||
) |
||||
); |
||||
|
||||
expect(getMetricSourcesMock).toHaveBeenCalledTimes(1); |
||||
expect(getMetricSourcesMock).toHaveBeenCalledWith({ skipVariables: true }); |
||||
expect(getDatasourceSrvMock).toHaveBeenCalledTimes(1); |
||||
}); |
||||
}); |
||||
|
||||
describe('and there is a regex', () => { |
||||
it('then the correct actions are dispatched', async () => { |
||||
const sources: DataSourceSelectItem[] = [ |
||||
{ |
||||
name: 'first-name', |
||||
value: 'first-value', |
||||
meta: getMockPlugin({ name: 'mock-data-name', id: 'mock-data-id' }), |
||||
sort: '', |
||||
}, |
||||
{ |
||||
name: 'second-name', |
||||
value: 'second-value', |
||||
meta: getMockPlugin({ name: 'mock-data-name', id: 'mock-data-id' }), |
||||
sort: '', |
||||
}, |
||||
]; |
||||
|
||||
const getMetricSourcesMock = jest.fn().mockResolvedValue(sources); |
||||
const getDatasourceSrvMock = jest.fn().mockReturnValue({ getMetricSources: getMetricSourcesMock }); |
||||
const dependencies: DataSourceVariableActionDependencies = { getDatasourceSrv: getDatasourceSrvMock }; |
||||
const datasource = variableMockBuilder('datasource') |
||||
.withUuid('0') |
||||
.withQuery('mock-data-id') |
||||
.withRegEx('/.*(second-name).*/') |
||||
.create(); |
||||
const tester = await reduxTester<{ templating: TemplatingState }>() |
||||
.givenRootReducer(getTemplatingRootReducer()) |
||||
.whenActionIsDispatched(initDashboardTemplating([datasource])) |
||||
.whenAsyncActionIsDispatched( |
||||
updateDataSourceVariableOptions(toVariableIdentifier(datasource), dependencies), |
||||
true |
||||
); |
||||
|
||||
await tester.thenDispatchedActionShouldEqual( |
||||
createDataSourceOptions( |
||||
toVariablePayload({ type: 'datasource', uuid: '0' }, { sources, regex: /.*(second-name).*/ }) |
||||
), |
||||
setCurrentVariableValue( |
||||
toVariablePayload( |
||||
{ type: 'datasource', uuid: '0' }, |
||||
{ option: { text: 'second-name', value: 'second-name', selected: false } } |
||||
) |
||||
) |
||||
); |
||||
|
||||
expect(getMetricSourcesMock).toHaveBeenCalledTimes(1); |
||||
expect(getMetricSourcesMock).toHaveBeenCalledWith({ skipVariables: true }); |
||||
expect(getDatasourceSrvMock).toHaveBeenCalledTimes(1); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('when initDataSourceVariableEditor is dispatched', () => { |
||||
it('then the correct actions are dispatched', async () => { |
||||
const sources: DataSourceSelectItem[] = [ |
||||
{ |
||||
name: 'first-name', |
||||
value: 'first-value', |
||||
meta: getMockPlugin({ name: 'mock-data-name', id: 'mock-data-id' }), |
||||
sort: '', |
||||
}, |
||||
{ |
||||
name: 'second-name', |
||||
value: 'second-value', |
||||
meta: getMockPlugin({ name: 'mock-data-name', id: 'mock-data-id' }), |
||||
sort: '', |
||||
}, |
||||
{ |
||||
name: 'mixed-name', |
||||
value: 'mixed-value', |
||||
meta: getMockPlugin(({ |
||||
name: 'mixed-data-name', |
||||
id: 'mixed-data-id', |
||||
mixed: true, |
||||
} as unknown) as DataSourcePluginMeta), |
||||
sort: '', |
||||
}, |
||||
]; |
||||
|
||||
const getMetricSourcesMock = jest.fn().mockResolvedValue(sources); |
||||
const getDatasourceSrvMock = jest.fn().mockReturnValue({ getMetricSources: getMetricSourcesMock }); |
||||
const dependencies: DataSourceVariableActionDependencies = { getDatasourceSrv: getDatasourceSrvMock }; |
||||
|
||||
const tester = await reduxTester<{ templating: TemplatingState }>() |
||||
.givenRootReducer(getTemplatingRootReducer()) |
||||
.whenAsyncActionIsDispatched(initDataSourceVariableEditor(dependencies)); |
||||
|
||||
await tester.thenDispatchedActionShouldEqual( |
||||
changeVariableEditorExtended({ |
||||
propName: 'dataSourceTypes', |
||||
propValue: [ |
||||
{ text: '', value: '' }, |
||||
{ text: 'mock-data-name', value: 'mock-data-id' }, |
||||
], |
||||
}) |
||||
); |
||||
|
||||
expect(getMetricSourcesMock).toHaveBeenCalledTimes(1); |
||||
expect(getMetricSourcesMock).toHaveBeenCalledWith(); |
||||
expect(getDatasourceSrvMock).toHaveBeenCalledTimes(1); |
||||
}); |
||||
}); |
||||
}); |
@ -0,0 +1,54 @@ |
||||
import { toVariablePayload, VariableIdentifier } from '../state/types'; |
||||
import { ThunkResult } from '../../../types'; |
||||
import { createDataSourceOptions } from './reducer'; |
||||
import { validateVariableSelectionState } from '../state/actions'; |
||||
import { DataSourceSelectItem, stringToJsRegex } from '@grafana/data'; |
||||
import { getDatasourceSrv } from '../../plugins/datasource_srv'; |
||||
import { getVariable } from '../state/selectors'; |
||||
import { DataSourceVariableModel } from '../../templating/variable'; |
||||
import templateSrv from '../../templating/template_srv'; |
||||
import _ from 'lodash'; |
||||
import { changeVariableEditorExtended } from '../editor/reducer'; |
||||
|
||||
export interface DataSourceVariableActionDependencies { |
||||
getDatasourceSrv: typeof getDatasourceSrv; |
||||
} |
||||
|
||||
export const updateDataSourceVariableOptions = ( |
||||
identifier: VariableIdentifier, |
||||
dependencies: DataSourceVariableActionDependencies = { getDatasourceSrv: getDatasourceSrv } |
||||
): ThunkResult<void> => async (dispatch, getState) => { |
||||
const sources = await dependencies.getDatasourceSrv().getMetricSources({ skipVariables: true }); |
||||
const variableInState = getVariable<DataSourceVariableModel>(identifier.uuid!, getState()); |
||||
let regex; |
||||
|
||||
if (variableInState.regex) { |
||||
regex = templateSrv.replace(variableInState.regex, undefined, 'regex'); |
||||
regex = stringToJsRegex(regex); |
||||
} |
||||
|
||||
await dispatch(createDataSourceOptions(toVariablePayload(identifier, { sources, regex }))); |
||||
await dispatch(validateVariableSelectionState(identifier)); |
||||
}; |
||||
|
||||
export const initDataSourceVariableEditor = ( |
||||
dependencies: DataSourceVariableActionDependencies = { getDatasourceSrv: getDatasourceSrv } |
||||
): ThunkResult<void> => async dispatch => { |
||||
const dataSources: DataSourceSelectItem[] = await dependencies.getDatasourceSrv().getMetricSources(); |
||||
const filtered = dataSources.filter(ds => !ds.meta.mixed && ds.value !== null); |
||||
const dataSourceTypes = _(filtered) |
||||
.uniqBy('meta.id') |
||||
.map((ds: any) => { |
||||
return { text: ds.meta.name, value: ds.meta.id }; |
||||
}) |
||||
.value(); |
||||
|
||||
dataSourceTypes.unshift({ text: '', value: '' }); |
||||
|
||||
dispatch( |
||||
changeVariableEditorExtended({ |
||||
propName: 'dataSourceTypes', |
||||
propValue: dataSourceTypes, |
||||
}) |
||||
); |
||||
}; |
@ -0,0 +1,46 @@ |
||||
import cloneDeep from 'lodash/cloneDeep'; |
||||
import { containsVariable, DataSourceVariableModel } from '../../templating/variable'; |
||||
import { dispatch } from '../../../store/store'; |
||||
import { setOptionAsCurrent, setOptionFromUrl } from '../state/actions'; |
||||
import { VariableAdapter } from '../adapters'; |
||||
import { dataSourceVariableReducer, initialDataSourceVariableModelState } from './reducer'; |
||||
import { OptionsPicker } from '../pickers'; |
||||
import { ALL_VARIABLE_TEXT, toVariableIdentifier } from '../state/types'; |
||||
import { DataSourceVariableEditor } from './DataSourceVariableEditor'; |
||||
import { updateDataSourceVariableOptions } from './actions'; |
||||
|
||||
export const createDataSourceVariableAdapter = (): VariableAdapter<DataSourceVariableModel> => { |
||||
return { |
||||
description: 'Enabled you to dynamically switch the datasource for multiple panels', |
||||
label: 'Datasource', |
||||
initialState: initialDataSourceVariableModelState, |
||||
reducer: dataSourceVariableReducer, |
||||
picker: OptionsPicker, |
||||
editor: DataSourceVariableEditor, |
||||
dependsOn: (variable, variableToTest) => { |
||||
if (variable.regex) { |
||||
return containsVariable(variable.regex, variableToTest.name); |
||||
} |
||||
return false; |
||||
}, |
||||
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 => { |
||||
await dispatch(updateDataSourceVariableOptions(toVariableIdentifier(variable))); |
||||
}, |
||||
getSaveModel: variable => { |
||||
const { index, uuid, initLock, global, ...rest } = cloneDeep(variable); |
||||
return { ...rest, options: [] }; |
||||
}, |
||||
getValueForUrl: variable => { |
||||
if (variable.current.text === ALL_VARIABLE_TEXT) { |
||||
return ALL_VARIABLE_TEXT; |
||||
} |
||||
return variable.current.value; |
||||
}, |
||||
}; |
||||
}; |
@ -0,0 +1,64 @@ |
||||
import { reducerTester } from '../../../../test/core/redux/reducerTester'; |
||||
import { VariablesState } from '../state/variablesReducer'; |
||||
import { createDataSourceOptions, dataSourceVariableReducer } from './reducer'; |
||||
import { DataSourceVariableModel } from '../../templating/variable'; |
||||
import { getVariableTestContext } from '../state/helpers'; |
||||
import cloneDeep from 'lodash/cloneDeep'; |
||||
import { createDataSourceVariableAdapter } from './adapter'; |
||||
import { DataSourceSelectItem } from '@grafana/data'; |
||||
import { toVariablePayload } from '../state/types'; |
||||
import { getMockPlugins } from '../../plugins/__mocks__/pluginMocks'; |
||||
|
||||
describe('dataSourceVariableReducer', () => { |
||||
const adapter = createDataSourceVariableAdapter(); |
||||
describe('when createDataSourceOptions is dispatched', () => { |
||||
const plugins = getMockPlugins(3); |
||||
const sources: DataSourceSelectItem[] = plugins.map(p => ({ |
||||
name: p.name, |
||||
value: `${p.name} value`, |
||||
meta: p, |
||||
sort: '', |
||||
})); |
||||
|
||||
it.each` |
||||
query | regex | includeAll | expected |
||||
${sources[1].meta.id} | ${undefined} | ${false} | ${[{ text: 'pretty cool plugin-1', value: 'pretty cool plugin-1', selected: false }]} |
||||
${'not-found-plugin'} | ${undefined} | ${false} | ${[{ text: 'No data sources found', value: '', selected: false }]} |
||||
${sources[1].meta.id} | ${/.*(pretty cool plugin-1).*/} | ${false} | ${[{ text: 'pretty cool plugin-1', value: 'pretty cool plugin-1', selected: false }]} |
||||
${sources[1].meta.id} | ${/.*(pretty cool plugin-2).*/} | ${false} | ${[{ text: 'No data sources found', value: '', selected: false }]} |
||||
${sources[1].meta.id} | ${undefined} | ${true} | ${[ |
||||
{ text: 'All', value: '$__all', selected: false }, |
||||
{ text: 'pretty cool plugin-1', value: 'pretty cool plugin-1', selected: false }, |
||||
]} |
||||
${'not-found-plugin'} | ${undefined} | ${true} | ${[ |
||||
{ text: 'All', value: '$__all', selected: false }, |
||||
{ text: 'No data sources found', value: '', selected: false }, |
||||
]} |
||||
${sources[1].meta.id} | ${/.*(pretty cool plugin-1).*/} | ${true} | ${[ |
||||
{ text: 'All', value: '$__all', selected: false }, |
||||
{ text: 'pretty cool plugin-1', value: 'pretty cool plugin-1', selected: false }, |
||||
]} |
||||
${sources[1].meta.id} | ${/.*(pretty cool plugin-2).*/} | ${true} | ${[ |
||||
{ text: 'All', value: '$__all', selected: false }, |
||||
{ text: 'No data sources found', value: '', selected: false }, |
||||
]} |
||||
`(
|
||||
"when called with query: '$query' and regex: '$regex' and includeAll: '$includeAll' then state should be correct", |
||||
({ query, regex, includeAll, expected }) => { |
||||
const { initialState } = getVariableTestContext<DataSourceVariableModel>(adapter, { query, includeAll }); |
||||
const payload = toVariablePayload({ uuid: '0', type: 'datasource' }, { sources, regex }); |
||||
|
||||
reducerTester<VariablesState>() |
||||
.givenReducer(dataSourceVariableReducer, cloneDeep(initialState)) |
||||
.whenActionIsDispatched(createDataSourceOptions(payload)) |
||||
.thenStateShouldEqual({ |
||||
...initialState, |
||||
['0']: ({ |
||||
...initialState['0'], |
||||
options: expected, |
||||
} as unknown) as DataSourceVariableModel, |
||||
}); |
||||
} |
||||
); |
||||
}); |
||||
}); |
@ -0,0 +1,70 @@ |
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'; |
||||
import { DataSourceVariableModel, VariableHide, VariableOption, VariableRefresh } from '../../templating/variable'; |
||||
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE, EMPTY_UUID, getInstanceState, VariablePayload } from '../state/types'; |
||||
import { initialVariablesState, VariablesState } from '../state/variablesReducer'; |
||||
import { DataSourceSelectItem } from '@grafana/data'; |
||||
|
||||
export interface DataSourceVariableEditorState { |
||||
dataSourceTypes: Array<{ text: string; value: string }>; |
||||
} |
||||
|
||||
export const initialDataSourceVariableModelState: DataSourceVariableModel = { |
||||
uuid: EMPTY_UUID, |
||||
global: false, |
||||
type: 'datasource', |
||||
name: '', |
||||
hide: VariableHide.dontHide, |
||||
label: '', |
||||
current: {} as VariableOption, |
||||
regex: '', |
||||
options: [], |
||||
query: '', |
||||
multi: false, |
||||
includeAll: false, |
||||
refresh: VariableRefresh.onDashboardLoad, |
||||
skipUrlSync: false, |
||||
index: -1, |
||||
initLock: null, |
||||
}; |
||||
|
||||
export const dataSourceVariableSlice = createSlice({ |
||||
name: 'templating/datasource', |
||||
initialState: initialVariablesState, |
||||
reducers: { |
||||
createDataSourceOptions: ( |
||||
state: VariablesState, |
||||
action: PayloadAction<VariablePayload<{ sources: DataSourceSelectItem[]; regex: RegExp | undefined }>> |
||||
) => { |
||||
const { sources, regex } = action.payload.data; |
||||
const options: VariableOption[] = []; |
||||
const instanceState = getInstanceState<DataSourceVariableModel>(state, action.payload.uuid); |
||||
for (let i = 0; i < sources.length; i++) { |
||||
const source = sources[i]; |
||||
// must match on type
|
||||
if (source.meta.id !== instanceState.query) { |
||||
continue; |
||||
} |
||||
|
||||
if (regex && !regex.exec(source.name)) { |
||||
continue; |
||||
} |
||||
|
||||
options.push({ text: source.name, value: source.name, selected: false }); |
||||
} |
||||
|
||||
if (options.length === 0) { |
||||
options.push({ text: 'No data sources found', value: '', selected: false }); |
||||
} |
||||
|
||||
if (instanceState.includeAll) { |
||||
options.unshift({ text: ALL_VARIABLE_TEXT, value: ALL_VARIABLE_VALUE, selected: false }); |
||||
} |
||||
|
||||
instanceState.options = options; |
||||
}, |
||||
}, |
||||
}); |
||||
|
||||
export const dataSourceVariableReducer = dataSourceVariableSlice.reducer; |
||||
|
||||
export const { createDataSourceOptions } = dataSourceVariableSlice.actions; |
@ -1,7 +1,7 @@ |
||||
import React, { MouseEvent, PureComponent } from 'react'; |
||||
import { e2e } from '@grafana/e2e'; |
||||
import EmptyListCTA from '../../../core/components/EmptyListCTA/EmptyListCTA'; |
||||
import { QueryVariableModel, VariableModel } from '../variable'; |
||||
import { QueryVariableModel, VariableModel } from '../../templating/variable'; |
||||
import { toVariableIdentifier, VariableIdentifier } from '../state/types'; |
||||
|
||||
export interface Props { |
@ -1,5 +1,5 @@ |
||||
import React, { useCallback, useEffect, useState } from 'react'; |
||||
import { VariableModel, VariableOption, VariableWithOptions } from '../variable'; |
||||
import { VariableModel, VariableOption, VariableWithOptions } from '../../templating/variable'; |
||||
import { e2e } from '@grafana/e2e'; |
||||
|
||||
export interface VariableValuesPreviewProps { |
@ -1,4 +1,4 @@ |
||||
import { VariableModel } from '../variable'; |
||||
import { VariableModel } from '../../templating/variable'; |
||||
|
||||
export interface OnPropChangeArguments<Model extends VariableModel = VariableModel> { |
||||
propName: keyof Model; |
@ -1,4 +1,4 @@ |
||||
import { VariableModel, QueryVariableModel } from './variable'; |
||||
import { QueryVariableModel, VariableModel } from '../templating/variable'; |
||||
|
||||
export const isQuery = (model: VariableModel): model is QueryVariableModel => { |
||||
return model.type === 'query'; |
@ -1,6 +1,11 @@ |
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'; |
||||
import { cloneDeep } from 'lodash'; |
||||
import { containsSearchFilter, VariableOption, VariableTag, VariableWithMultiSupport } from '../../variable'; |
||||
import { |
||||
containsSearchFilter, |
||||
VariableOption, |
||||
VariableTag, |
||||
VariableWithMultiSupport, |
||||
} from '../../../templating/variable'; |
||||
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from '../../state/types'; |
||||
import { isQuery } from '../../guard'; |
||||
import { applyStateChanges } from '../../../../core/utils/applyStateChanges'; |
@ -1,5 +1,5 @@ |
||||
import React, { FunctionComponent, useMemo } from 'react'; |
||||
import { VariableHide, VariableModel } from '../variable'; |
||||
import { VariableHide, VariableModel } from '../../templating/variable'; |
||||
import { e2e } from '@grafana/e2e'; |
||||
import { variableAdapters } from '../adapters'; |
||||
|
@ -1,7 +1,7 @@ |
||||
import React, { PureComponent } from 'react'; |
||||
import { getTagColorsFromName } from '@grafana/ui'; |
||||
import { e2e } from '@grafana/e2e'; |
||||
import { VariableTag } from '../../variable'; |
||||
import { VariableTag } from '../../../templating/variable'; |
||||
|
||||
interface Props { |
||||
onClick: () => void; |
@ -1,7 +1,7 @@ |
||||
import React, { PureComponent } from 'react'; |
||||
import { getTagColorsFromName, Tooltip } from '@grafana/ui'; |
||||
import { e2e } from '@grafana/e2e'; |
||||
import { VariableOption, VariableTag } from '../../variable'; |
||||
import { VariableOption, VariableTag } from '../../../templating/variable'; |
||||
|
||||
export interface Props { |
||||
multi: boolean; |
@ -1,4 +1,4 @@ |
||||
import { VariableModel } from '../variable'; |
||||
import { VariableModel } from '../../templating/variable'; |
||||
|
||||
export interface VariablePickerProps<Model extends VariableModel = VariableModel> { |
||||
variable: Model; |
@ -1,13 +1,13 @@ |
||||
import { AppEvents, DataSourcePluginMeta, DataSourceSelectItem } from '@grafana/data'; |
||||
|
||||
import { validateVariableSelectionState } from '../state/actions'; |
||||
import { QueryVariableModel, VariableRefresh } from '../variable'; |
||||
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 '../DefaultVariableQueryEditor'; |
||||
import DefaultVariableQueryEditor from '../../templating/DefaultVariableQueryEditor'; |
||||
import { getVariable } from '../state/selectors'; |
||||
import { addVariableEditorError, changeVariableEditorExtended, removeVariableEditorError } from '../editor/reducer'; |
||||
import { variableAdapters } from '../adapters'; |
@ -1,6 +1,6 @@ |
||||
import cloneDeep from 'lodash/cloneDeep'; |
||||
|
||||
import { containsVariable, QueryVariableModel, VariableRefresh } from '../variable'; |
||||
import { containsVariable, QueryVariableModel, VariableRefresh } from '../../templating/variable'; |
||||
import { initialQueryVariableModelState, queryVariableReducer } from './reducer'; |
||||
import { dispatch } from '../../../store/store'; |
||||
import { setOptionAsCurrent, setOptionFromUrl } from '../state/actions'; |
@ -1,6 +1,6 @@ |
||||
import { reducerTester } from '../../../../test/core/redux/reducerTester'; |
||||
import { queryVariableReducer, updateVariableOptions, updateVariableTags } from './reducer'; |
||||
import { QueryVariableModel, VariableOption } from '../variable'; |
||||
import { QueryVariableModel, VariableOption } from '../../templating/variable'; |
||||
import cloneDeep from 'lodash/cloneDeep'; |
||||
import { VariablesState } from '../state/variablesReducer'; |
||||
import { getVariableTestContext } from '../state/helpers'; |
@ -1,7 +1,13 @@ |
||||
import castArray from 'lodash/castArray'; |
||||
import { UrlQueryMap, UrlQueryValue } from '@grafana/runtime'; |
||||
|
||||
import { QueryVariableModel, VariableModel, VariableOption, VariableRefresh, VariableWithOptions } from '../variable'; |
||||
import { |
||||
QueryVariableModel, |
||||
VariableModel, |
||||
VariableOption, |
||||
VariableRefresh, |
||||
VariableWithOptions, |
||||
} from '../../templating/variable'; |
||||
import { StoreState, ThunkResult } from '../../../types'; |
||||
import { getVariable, getVariables } from './selectors'; |
||||
import { variableAdapters } from '../adapters'; |
@ -1,6 +1,6 @@ |
||||
import { reducerTester } from '../../../../test/core/redux/reducerTester'; |
||||
import { cleanUpDashboard } from 'app/features/dashboard/state/reducers'; |
||||
import { VariableHide, VariableModel } from '../variable'; |
||||
import { VariableHide, VariableModel } from '../../templating/variable'; |
||||
import { VariableAdapter, variableAdapters } from '../adapters'; |
||||
import { createAction } from '@reduxjs/toolkit'; |
||||
import { variablesReducer, VariablesState } from './variablesReducer'; |
@ -1,7 +1,7 @@ |
||||
import { cloneDeep } from 'lodash'; |
||||
|
||||
import { StoreState } from '../../../types'; |
||||
import { VariableModel } from '../variable'; |
||||
import { VariableModel } from '../../templating/variable'; |
||||
import { getState } from '../../../store/store'; |
||||
import { EMPTY_UUID } from './types'; |
||||
|
@ -1,4 +1,4 @@ |
||||
import { VariableModel, VariableType } from '../variable'; |
||||
import { VariableModel, VariableType } from '../../templating/variable'; |
||||
import { VariablesState } from './variablesReducer'; |
||||
|
||||
export const EMPTY_UUID = '00000000-0000-0000-0000-000000000000'; |
@ -1,5 +1,5 @@ |
||||
import React, { ChangeEvent, PureComponent } from 'react'; |
||||
import { TextBoxVariableModel } from '../variable'; |
||||
import { TextBoxVariableModel } from '../../templating/variable'; |
||||
import { VariableEditorProps } from '../editor/types'; |
||||
|
||||
export interface Props extends VariableEditorProps<TextBoxVariableModel> {} |
@ -1,6 +1,6 @@ |
||||
import React, { ChangeEvent, FocusEvent, KeyboardEvent, PureComponent } from 'react'; |
||||
|
||||
import { TextBoxVariableModel } from '../variable'; |
||||
import { TextBoxVariableModel } from '../../templating/variable'; |
||||
import { toVariablePayload } from '../state/types'; |
||||
import { dispatch } from '../../../store/store'; |
||||
import { variableAdapters } from '../adapters'; |
@ -1,10 +1,10 @@ |
||||
import { variableAdapters } from '../adapters'; |
||||
import { createTextBoxVariableAdapter } from './adapter'; |
||||
import { reduxTester } from '../../../../test/core/redux/reduxTester'; |
||||
import { TemplatingState } from 'app/features/templating/state/reducers'; |
||||
import { TemplatingState } from 'app/features/variables/state/reducers'; |
||||
import { updateTextBoxVariableOptions } from './actions'; |
||||
import { getTemplatingRootReducer } from '../state/helpers'; |
||||
import { VariableOption, VariableHide, TextBoxVariableModel } from '../variable'; |
||||
import { TextBoxVariableModel, VariableHide, VariableOption } from '../../templating/variable'; |
||||
import { toVariablePayload } from '../state/types'; |
||||
import { createTextBoxOptions } from './reducer'; |
||||
import { setCurrentVariableValue } from '../state/sharedReducer'; |
@ -1,4 +1,4 @@ |
||||
import { TextBoxVariableModel } from '../variable'; |
||||
import { TextBoxVariableModel } from '../../templating/variable'; |
||||
import { ThunkResult } from '../../../types'; |
||||
import { getVariable } from '../state/selectors'; |
||||
import { variableAdapters } from '../adapters'; |
@ -1,6 +1,6 @@ |
||||
import cloneDeep from 'lodash/cloneDeep'; |
||||
|
||||
import { TextBoxVariableModel } from '../variable'; |
||||
import { TextBoxVariableModel } from '../../templating/variable'; |
||||
import { initialTextBoxVariableModelState, textBoxVariableReducer } from './reducer'; |
||||
import { dispatch } from '../../../store/store'; |
||||
import { setOptionAsCurrent, setOptionFromUrl } from '../state/actions'; |
@ -1,6 +1,6 @@ |
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'; |
||||
|
||||
import { TextBoxVariableModel, VariableHide, VariableOption } from '../variable'; |
||||
import { TextBoxVariableModel, VariableHide, VariableOption } from '../../templating/variable'; |
||||
import { EMPTY_UUID, getInstanceState, VariablePayload } from '../state/types'; |
||||
import { initialVariablesState, VariablesState } from '../state/variablesReducer'; |
||||
|
Loading…
Reference in new issue