Variables: replaces UUID with name for a more performant lookup in TemplateSrv (#22858)

* Refactor: renames uuid to id

* Refactor: misc renames

* Refactor: fixes renaming of variable

* Refactor: changes method accessed by templateSrv

* Refactor: fixes for NEW_VARIABLE_ID

* Refactor: rename flow refactor

* Tests: adds missing reducer and action tests

* Refactor: keeping tests consitent

* Chore: reorder imports

* Chore: removes uuid package

* Refactor: fixes imports
This commit is contained in:
Hugo Häggmark
2020-03-23 13:45:08 +01:00
committed by GitHub
parent 3798ac903d
commit 9af04a49ea
62 changed files with 834 additions and 747 deletions

View File

@@ -211,7 +211,6 @@
"@types/md5": "^2.1.33",
"@types/react-loadable": "5.5.2",
"@types/react-virtualized-auto-sizer": "1.0.0",
"@types/uuid": "3.4.7",
"abortcontroller-polyfill": "1.4.0",
"angular": "1.6.9",
"angular-bindonce": "0.3.1",
@@ -275,7 +274,6 @@
"tether-drop": "https://github.com/torkelo/drop/tarball/master",
"tinycolor2": "1.4.1",
"tti-polyfill": "0.2.2",
"uuid": "3.4.0",
"whatwg-fetch": "3.0.0",
"xss": "1.0.6"
},

View File

@@ -22,7 +22,7 @@ export const SubMenuItems: FunctionComponent<Props> = ({ variables }) => {
{visibleVariables.map(variable => {
return (
<div
key={variable.uuid}
key={variable.id}
className="submenu-item gf-form-inline"
aria-label={e2e.pages.Dashboard.SubMenu.selectors.submenuItem}
>

View File

@@ -4,7 +4,6 @@ import { variableRegex } from 'app/features/templating/variable';
import { escapeHtml } from 'app/core/utils/text';
import { ScopedVars, TimeRange } from '@grafana/data';
import { getVariableWithName, getFilteredVariables } from '../variables/state/selectors';
import { getState } from '../../store/store';
import { getConfig } from 'app/core/config';
import { isAdHoc } from '../variables/guard';
@@ -378,7 +377,7 @@ export class TemplateSrv {
}
if (getConfig().featureToggles.newVariables && !this.index[name]) {
return getVariableWithName(name, getState());
return getVariableWithName(name);
}
return this.index[name];

View File

@@ -139,7 +139,7 @@ export interface VariableWithOptions extends VariableModel {
}
export interface VariableModel {
uuid?: string; // only exists for variables in redux state
id?: string; // only exists for variables in redux state
global?: boolean; // only exists for variables in redux state
type: VariableType;
name: string;

View File

@@ -1,36 +1,32 @@
import { DataSourcePluginMeta, DataSourceSelectItem } from '@grafana/data';
import { variableAdapters } from '../adapters';
import { createAdHocVariableAdapter } from './adapter';
import { reduxTester } from '../../../../test/core/redux/reduxTester';
import { TemplatingState } from 'app/features/variables/state/reducers';
import { getRootReducer } from '../state/helpers';
import { toVariablePayload, toVariableIdentifier } from '../state/types';
import * as variableBuilder from '../shared/testing/builders';
import { toVariableIdentifier, toVariablePayload } from '../state/types';
import {
applyFilterFromTable,
AdHocTableOptions,
changeFilter,
addFilter,
AdHocTableOptions,
applyFilterFromTable,
changeFilter,
changeVariableDatasource,
initAdHocVariableEditor,
removeFilter,
setFiltersFromUrl,
initAdHocVariableEditor,
changeVariableDatasource,
} from './actions';
import { filterAdded, filterUpdated, filterRemoved, filtersRestored } from './reducer';
import { filterAdded, filterRemoved, filtersRestored, filterUpdated } from './reducer';
import { addVariable, changeVariableProp } from '../state/sharedReducer';
import { updateLocation } from 'app/core/actions';
import { DashboardState, LocationState } from 'app/types';
import { VariableModel } from 'app/features/templating/variable';
import { changeVariableEditorExtended, setIdInEditor } from '../editor/reducer';
import { DataSourceSelectItem, DataSourcePluginMeta } from '@grafana/data';
import { adHocBuilder } from '../shared/testing/builders';
const uuid = '0';
const getMetricSources = jest.fn().mockReturnValue([]);
const getDatasource = jest.fn().mockResolvedValue({});
jest.mock('uuid', () => ({
v4: jest.fn(() => uuid),
}));
jest.mock('app/features/plugins/datasource_srv', () => ({
getDatasourceSrv: jest.fn(() => ({
get: getDatasource,
@@ -63,11 +59,10 @@ describe('adhoc actions', () => {
condition: '',
};
const variable = variableBuilder
.adHoc()
const variable = adHocBuilder()
.withId('Filters')
.withName('Filters')
.withFilters([existingFilter])
.withUUID(uuid)
.withDatasource(options.datasource)
.build();
@@ -79,15 +74,10 @@ describe('adhoc actions', () => {
const expectedQuery = { 'var-Filters': ['filter-key|!=|filter-existing', 'filter-key|=|filter-value'] };
const expectedFilter = { key: 'filter-key', value: 'filter-value', operator: '=', condition: '' };
tester.thenDispatchedActionsPredicateShouldEqual(actions => {
const [addFilterAction, updateLocationAction] = actions;
const expectedNumberOfActions = 2;
expect(addFilterAction).toEqual(filterAdded(toVariablePayload(variable, expectedFilter)));
expect(updateLocationAction).toEqual(updateLocation({ query: expectedQuery }));
return actions.length === expectedNumberOfActions;
});
tester.thenDispatchedActionsShouldEqual(
filterAdded(toVariablePayload(variable, expectedFilter)),
updateLocation({ query: expectedQuery })
);
});
});
@@ -104,26 +94,20 @@ describe('adhoc actions', () => {
.givenRootReducer(getRootReducer())
.whenAsyncActionIsDispatched(applyFilterFromTable(options), true);
const variable = variableBuilder
.adHoc()
const variable = adHocBuilder()
.withId('Filters')
.withName('Filters')
.withUUID(uuid)
.withDatasource(options.datasource)
.build();
const expectedQuery = { 'var-Filters': ['filter-key|=|filter-value'] };
const expectedFilter = { key: 'filter-key', value: 'filter-value', operator: '=', condition: '' };
tester.thenDispatchedActionsPredicateShouldEqual(actions => {
const [addVariableAction, addFilterAction, updateLocationAction] = actions;
const expectedNumberOfActions = 3;
expect(addVariableAction).toEqual(createAddVariableAction(variable));
expect(addFilterAction).toEqual(filterAdded(toVariablePayload(variable, expectedFilter)));
expect(updateLocationAction).toEqual(updateLocation({ query: expectedQuery }));
return actions.length === expectedNumberOfActions;
});
tester.thenDispatchedActionsShouldEqual(
createAddVariableAction(variable),
filterAdded(toVariablePayload(variable, expectedFilter)),
updateLocation({ query: expectedQuery })
);
});
});
@@ -136,10 +120,9 @@ describe('adhoc actions', () => {
operator: '=',
};
const variable = variableBuilder
.adHoc()
const variable = adHocBuilder()
.withId('Filters')
.withName('Filters')
.withUUID(uuid)
.withFilters([])
.withDatasource(options.datasource)
.build();
@@ -152,15 +135,10 @@ describe('adhoc actions', () => {
const expectedFilter = { key: 'filter-key', value: 'filter-value', operator: '=', condition: '' };
const expectedQuery = { 'var-Filters': ['filter-key|=|filter-value'] };
tester.thenDispatchedActionsPredicateShouldEqual(actions => {
const [addFilterAction, updateLocationAction] = actions;
const expectedNumberOfActions = 2;
expect(addFilterAction).toEqual(filterAdded(toVariablePayload(variable, expectedFilter)));
expect(updateLocationAction).toEqual(updateLocation({ query: expectedQuery }));
return actions.length === expectedNumberOfActions;
});
tester.thenDispatchedActionsShouldEqual(
filterAdded(toVariablePayload(variable, expectedFilter)),
updateLocation({ query: expectedQuery })
);
});
});
@@ -173,16 +151,15 @@ describe('adhoc actions', () => {
operator: '=',
};
const existing = variableBuilder
.adHoc()
const existing = adHocBuilder()
.withId('elastic-filter')
.withName('elastic-filter')
.withDatasource('elasticsearch')
.build();
const variable = variableBuilder
.adHoc()
const variable = adHocBuilder()
.withId('Filters')
.withName('Filters')
.withUUID(uuid)
.withDatasource(options.datasource)
.build();
@@ -192,18 +169,13 @@ describe('adhoc actions', () => {
.whenAsyncActionIsDispatched(applyFilterFromTable(options), true);
const expectedFilter = { key: 'filter-key', value: 'filter-value', operator: '=', condition: '' };
const expectedQuery = { 'var-Filters': ['filter-key|=|filter-value'] };
const expectedQuery = { 'var-elastic-filter': [] as string[], 'var-Filters': ['filter-key|=|filter-value'] };
tester.thenDispatchedActionsPredicateShouldEqual(actions => {
const [addVariableAction, addFilterAction, updateLocationAction] = actions;
const expectedNumberOfActions = 3;
expect(addVariableAction).toEqual(createAddVariableAction(variable, 1));
expect(addFilterAction).toEqual(filterAdded(toVariablePayload(variable, expectedFilter)));
expect(updateLocationAction).toEqual(updateLocation({ query: expectedQuery }));
return actions.length === expectedNumberOfActions;
});
tester.thenDispatchedActionsShouldEqual(
createAddVariableAction(variable, 1),
filterAdded(toVariablePayload(variable, expectedFilter)),
updateLocation({ query: expectedQuery })
);
});
});
@@ -221,11 +193,10 @@ describe('adhoc actions', () => {
operator: '!=',
};
const variable = variableBuilder
.adHoc()
.withUUID(uuid)
.withFilters([existing])
const variable = adHocBuilder()
.withId('elastic-filter')
.withName('elastic-filter')
.withFilters([existing])
.withDatasource('elasticsearch')
.build();
@@ -234,20 +205,15 @@ describe('adhoc actions', () => {
const tester = await reduxTester<ReducersUsedInContext>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(createAddVariableAction(variable))
.whenAsyncActionIsDispatched(changeFilter(uuid, update), true);
.whenAsyncActionIsDispatched(changeFilter('elastic-filter', update), true);
const expectedQuery = { 'var-elastic-filter': ['key|!=|value'] };
const expectedUpdate = { index: 0, filter: updated };
tester.thenDispatchedActionsPredicateShouldEqual(actions => {
const [filterUpdatedAction, updateLocationAction] = actions;
const expectedNumberOfActions = 2;
expect(filterUpdatedAction).toEqual(filterUpdated(toVariablePayload(variable, expectedUpdate)));
expect(updateLocationAction).toEqual(updateLocation({ query: expectedQuery }));
return actions.length === expectedNumberOfActions;
});
tester.thenDispatchedActionsShouldEqual(
filterUpdated(toVariablePayload(variable, expectedUpdate)),
updateLocation({ query: expectedQuery })
);
});
});
@@ -265,31 +231,25 @@ describe('adhoc actions', () => {
operator: '!=',
};
const variable = variableBuilder
.adHoc()
.withUUID(uuid)
.withFilters([existing])
const variable = adHocBuilder()
.withId('elastic-filter')
.withName('elastic-filter')
.withFilters([existing])
.withDatasource('elasticsearch')
.build();
const tester = await reduxTester<ReducersUsedInContext>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(createAddVariableAction(variable))
.whenAsyncActionIsDispatched(addFilter(uuid, adding), true);
.whenAsyncActionIsDispatched(addFilter('elastic-filter', adding), true);
const expectedQuery = { 'var-elastic-filter': ['key|=|value', 'key|!=|value'] };
const expectedFilter = { key: 'key', value: 'value', operator: '!=', condition: '' };
tester.thenDispatchedActionsPredicateShouldEqual(actions => {
const [filterAddAction, updateLocationAction] = actions;
const expectedNumberOfActions = 2;
expect(filterAddAction).toEqual(filterAdded(toVariablePayload(variable, expectedFilter)));
expect(updateLocationAction).toEqual(updateLocation({ query: expectedQuery }));
return actions.length === expectedNumberOfActions;
});
tester.thenDispatchedActionsShouldEqual(
filterAdded(toVariablePayload(variable, expectedFilter)),
updateLocation({ query: expectedQuery })
);
});
});
@@ -302,59 +262,47 @@ describe('adhoc actions', () => {
condition: '',
};
const variable = variableBuilder
.adHoc()
.withUUID(uuid)
.withFilters([])
const variable = adHocBuilder()
.withId('elastic-filter')
.withName('elastic-filter')
.withFilters([])
.withDatasource('elasticsearch')
.build();
const tester = await reduxTester<ReducersUsedInContext>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(createAddVariableAction(variable))
.whenAsyncActionIsDispatched(addFilter(uuid, adding), true);
.whenAsyncActionIsDispatched(addFilter('elastic-filter', adding), true);
const expectedQuery = { 'var-elastic-filter': ['key|=|value'] };
tester.thenDispatchedActionsPredicateShouldEqual(actions => {
const [filterAddAction, updateLocationAction] = actions;
const expectedNumberOfActions = 2;
expect(filterAddAction).toEqual(filterAdded(toVariablePayload(variable, adding)));
expect(updateLocationAction).toEqual(updateLocation({ query: expectedQuery }));
return actions.length === expectedNumberOfActions;
});
tester.thenDispatchedActionsShouldEqual(
filterAdded(toVariablePayload(variable, adding)),
updateLocation({ query: expectedQuery })
);
});
});
describe('when removeFilter is dispatched on variable with no existing filter', () => {
it('then correct actions are dispatched', async () => {
const variable = variableBuilder
.adHoc()
.withUUID(uuid)
.withFilters([])
const variable = adHocBuilder()
.withId('elastic-filter')
.withName('elastic-filter')
.withFilters([])
.withDatasource('elasticsearch')
.build();
const tester = await reduxTester<ReducersUsedInContext>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(createAddVariableAction(variable))
.whenAsyncActionIsDispatched(removeFilter(uuid, 0), true);
.whenAsyncActionIsDispatched(removeFilter('elastic-filter', 0), true);
const expectedQuery = { 'var-elastic-filter': [] as string[] };
tester.thenDispatchedActionsPredicateShouldEqual(actions => {
const [filterRemoveAction, updateLocationAction] = actions;
const expectedNumberOfActions = 2;
expect(filterRemoveAction).toEqual(filterRemoved(toVariablePayload(variable, 0)));
expect(updateLocationAction).toEqual(updateLocation({ query: expectedQuery }));
return actions.length === expectedNumberOfActions;
});
tester.thenDispatchedActionsShouldEqual(
filterRemoved(toVariablePayload(variable, 0)),
updateLocation({ query: expectedQuery })
);
});
});
@@ -367,30 +315,24 @@ describe('adhoc actions', () => {
condition: '',
};
const variable = variableBuilder
.adHoc()
.withUUID(uuid)
.withFilters([filter])
const variable = adHocBuilder()
.withId('elastic-filter')
.withName('elastic-filter')
.withFilters([filter])
.withDatasource('elasticsearch')
.build();
const tester = await reduxTester<ReducersUsedInContext>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(createAddVariableAction(variable))
.whenAsyncActionIsDispatched(removeFilter(uuid, 0), true);
.whenAsyncActionIsDispatched(removeFilter('elastic-filter', 0), true);
const expectedQuery = { 'var-elastic-filter': [] as string[] };
tester.thenDispatchedActionsPredicateShouldEqual(actions => {
const [filterRemoveAction, updateLocationAction] = actions;
const expectedNumberOfActions = 2;
expect(filterRemoveAction).toEqual(filterRemoved(toVariablePayload(variable, 0)));
expect(updateLocationAction).toEqual(updateLocation({ query: expectedQuery }));
return actions.length === expectedNumberOfActions;
});
tester.thenDispatchedActionsShouldEqual(
filterRemoved(toVariablePayload(variable, 0)),
updateLocation({ query: expectedQuery })
);
});
});
@@ -403,11 +345,10 @@ describe('adhoc actions', () => {
condition: '',
};
const variable = variableBuilder
.adHoc()
.withUUID(uuid)
.withFilters([existing])
const variable = adHocBuilder()
.withId('elastic-filter')
.withName('elastic-filter')
.withFilters([existing])
.withDatasource('elasticsearch')
.build();
@@ -419,7 +360,7 @@ describe('adhoc actions', () => {
const tester = await reduxTester<ReducersUsedInContext>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(createAddVariableAction(variable))
.whenAsyncActionIsDispatched(setFiltersFromUrl(uuid, fromUrl), true);
.whenAsyncActionIsDispatched(setFiltersFromUrl('elastic-filter', fromUrl), true);
const expectedQuery = { 'var-elastic-filter': ['key|=|value', 'key|=|value'] };
const expectedFilters = [
@@ -427,15 +368,10 @@ describe('adhoc actions', () => {
{ key: 'key', value: 'value', operator: '=', condition: '', name: 'value-2' },
];
tester.thenDispatchedActionsPredicateShouldEqual(actions => {
const [filterRestoredAction, updateLocationAction] = actions;
const expectedNumberOfActions = 2;
expect(filterRestoredAction).toEqual(filtersRestored(toVariablePayload(variable, expectedFilters)));
expect(updateLocationAction).toEqual(updateLocation({ query: expectedQuery }));
return actions.length === expectedNumberOfActions;
});
tester.thenDispatchedActionsShouldEqual(
filtersRestored(toVariablePayload(variable, expectedFilters)),
updateLocation({ query: expectedQuery })
);
});
});
@@ -463,19 +399,9 @@ describe('adhoc actions', () => {
{ text: 'elasticsearch-v7', value: 'elasticsearch-v7' },
];
tester.thenDispatchedActionsPredicateShouldEqual(actions => {
const [changeEditorAction] = actions;
const expectedNumberOfActions = 1;
expect(changeEditorAction).toEqual(
changeVariableEditorExtended({
propName: 'dataSources',
propValue: expectedDatasources,
})
);
return actions.length === expectedNumberOfActions;
});
tester.thenDispatchedActionsShouldEqual(
changeVariableEditorExtended({ propName: 'dataSources', propValue: expectedDatasources })
);
});
});
@@ -483,9 +409,9 @@ describe('adhoc actions', () => {
it('then correct actions are dispatched', async () => {
const datasource = 'mysql';
const loadingText = 'Adhoc filters are applied automatically to all queries that target this datasource';
const variable = variableBuilder
.adHoc()
.withUUID(uuid)
const variable = adHocBuilder()
.withId('Filters')
.withName('Filters')
.withDatasource('influxdb')
.build();
@@ -495,28 +421,17 @@ describe('adhoc actions', () => {
const tester = await reduxTester<ReducersUsedInContext>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(createAddVariableAction(variable))
.whenActionIsDispatched(setIdInEditor({ id: variable.uuid! }))
.whenActionIsDispatched(setIdInEditor({ id: variable.id! }))
.whenAsyncActionIsDispatched(changeVariableDatasource(datasource), true);
tester.thenDispatchedActionsPredicateShouldEqual(actions => {
const [loadingTextAction, changePropAction, unsupportedTextAction] = actions;
const expectedNumberOfActions = 3;
expect(loadingTextAction).toEqual(
changeVariableEditorExtended({ propName: 'infoText', propValue: loadingText })
);
expect(changePropAction).toEqual(
changeVariableProp(toVariablePayload(variable, { propName: 'datasource', propValue: datasource }))
);
expect(unsupportedTextAction).toEqual(
changeVariableEditorExtended({
propName: 'infoText',
propValue: 'This datasource does not support adhoc filters yet.',
})
);
return actions.length === expectedNumberOfActions;
});
tester.thenDispatchedActionsShouldEqual(
changeVariableEditorExtended({ propName: 'infoText', propValue: loadingText }),
changeVariableProp(toVariablePayload(variable, { propName: 'datasource', propValue: datasource })),
changeVariableEditorExtended({
propName: 'infoText',
propValue: 'This datasource does not support adhoc filters yet.',
})
);
});
});
@@ -524,9 +439,9 @@ describe('adhoc actions', () => {
it('then correct actions are dispatched', async () => {
const datasource = 'elasticsearch';
const loadingText = 'Adhoc filters are applied automatically to all queries that target this datasource';
const variable = variableBuilder
.adHoc()
.withUUID(uuid)
const variable = adHocBuilder()
.withId('Filters')
.withName('Filters')
.withDatasource('influxdb')
.build();
@@ -538,22 +453,13 @@ describe('adhoc actions', () => {
const tester = await reduxTester<ReducersUsedInContext>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(createAddVariableAction(variable))
.whenActionIsDispatched(setIdInEditor({ id: variable.uuid! }))
.whenActionIsDispatched(setIdInEditor({ id: variable.id! }))
.whenAsyncActionIsDispatched(changeVariableDatasource(datasource), true);
tester.thenDispatchedActionsPredicateShouldEqual(actions => {
const [loadingTextAction, changePropAction] = actions;
const expectedNumberOfActions = 2;
expect(loadingTextAction).toEqual(
changeVariableEditorExtended({ propName: 'infoText', propValue: loadingText })
);
expect(changePropAction).toEqual(
changeVariableProp(toVariablePayload(variable, { propName: 'datasource', propValue: datasource }))
);
return actions.length === expectedNumberOfActions;
});
tester.thenDispatchedActionsShouldEqual(
changeVariableEditorExtended({ propName: 'infoText', propValue: loadingText }),
changeVariableProp(toVariablePayload(variable, { propName: 'datasource', propValue: datasource }))
);
});
});
});

View File

@@ -1,17 +1,16 @@
import { v4 } from 'uuid';
import { cloneDeep } from 'lodash';
import { ThunkResult, StoreState } from 'app/types';
import { StoreState, ThunkResult } from 'app/types';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { changeVariableEditorExtended } from '../editor/reducer';
import { changeVariableProp, addVariable } from '../state/sharedReducer';
import { getVariable, getNewVariabelIndex } from '../state/selectors';
import { toVariablePayload, toVariableIdentifier, AddVariable, VariableIdentifier } from '../state/types';
import { addVariable, changeVariableProp } from '../state/sharedReducer';
import { getNewVariabelIndex, getVariable } from '../state/selectors';
import { AddVariable, toVariableIdentifier, toVariablePayload, VariableIdentifier } from '../state/types';
import {
AdHocVariabelFilterUpdate,
filterRemoved,
filterUpdated,
filterAdded,
filterRemoved,
filtersRestored,
filterUpdated,
initialAdHocVariableModelState,
} from './reducer';
import { AdHocVariableFilter, AdHocVariableModel } from 'app/features/templating/variable';
@@ -41,41 +40,41 @@ export const applyFilterFromTable = (options: AdHocTableOptions): ThunkResult<vo
if (index === -1) {
const { value, key, operator } = options;
const filter = { value, key, operator, condition: '' };
return await dispatch(addFilter(variable.uuid!, filter));
return await dispatch(addFilter(variable.id!, filter));
}
const filter = { ...variable.filters[index], operator: options.operator };
return await dispatch(changeFilter(variable.uuid!, { index, filter }));
return await dispatch(changeFilter(variable.id!, { index, filter }));
};
};
export const changeFilter = (uuid: string, update: AdHocVariabelFilterUpdate): ThunkResult<void> => {
export const changeFilter = (id: string, update: AdHocVariabelFilterUpdate): ThunkResult<void> => {
return async (dispatch, getState) => {
const variable = getVariable(uuid, getState());
const variable = getVariable(id, getState());
dispatch(filterUpdated(toVariablePayload(variable, update)));
await dispatch(variableUpdated(toVariableIdentifier(variable), true));
};
};
export const removeFilter = (uuid: string, index: number): ThunkResult<void> => {
export const removeFilter = (id: string, index: number): ThunkResult<void> => {
return async (dispatch, getState) => {
const variable = getVariable(uuid, getState());
const variable = getVariable(id, getState());
dispatch(filterRemoved(toVariablePayload(variable, index)));
await dispatch(variableUpdated(toVariableIdentifier(variable), true));
};
};
export const addFilter = (uuid: string, filter: AdHocVariableFilter): ThunkResult<void> => {
export const addFilter = (id: string, filter: AdHocVariableFilter): ThunkResult<void> => {
return async (dispatch, getState) => {
const variable = getVariable(uuid, getState());
const variable = getVariable(id, getState());
dispatch(filterAdded(toVariablePayload(variable, filter)));
await dispatch(variableUpdated(toVariableIdentifier(variable), true));
};
};
export const setFiltersFromUrl = (uuid: string, filters: AdHocVariableFilter[]): ThunkResult<void> => {
export const setFiltersFromUrl = (id: string, filters: AdHocVariableFilter[]): ThunkResult<void> => {
return async (dispatch, getState) => {
const variable = getVariable(uuid, getState());
const variable = getVariable(id, getState());
dispatch(filtersRestored(toVariablePayload(variable, filters)));
await dispatch(variableUpdated(toVariableIdentifier(variable), true));
};
@@ -141,12 +140,12 @@ const createAdHocVariable = (options: AdHocTableOptions): ThunkResult<void> => {
...cloneDeep(initialAdHocVariableModelState),
datasource: options.datasource,
name: filterTableName,
uuid: v4(),
id: filterTableName,
};
const global = false;
const index = getNewVariabelIndex(getState());
const identifier: VariableIdentifier = { type: 'adhoc', uuid: model.uuid };
const identifier: VariableIdentifier = { type: 'adhoc', id: model.id };
dispatch(
addVariable(

View File

@@ -22,11 +22,11 @@ export const createAdHocVariableAdapter = (): VariableAdapter<AdHocVariableModel
setValue: noop,
setValueFromUrl: async (variable, urlValue) => {
const filters = urlParser.toFilters(urlValue);
await dispatch(setFiltersFromUrl(variable.uuid!, filters));
await dispatch(setFiltersFromUrl(variable.id!, filters));
},
updateOptions: noop,
getSaveModel: variable => {
const { index, uuid, initLock, global, ...rest } = cloneDeep(variable);
const { index, id, initLock, global, ...rest } = cloneDeep(variable);
return rest;
},
getValueForUrl: variable => {

View File

@@ -1,15 +1,15 @@
import React, { PureComponent, ReactNode } from 'react';
import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux';
import { StoreState } from 'app/types';
import { AdHocVariableModel, AdHocVariableFilter } from 'app/features/templating/variable';
import { AdHocVariableFilter, AdHocVariableModel } from 'app/features/templating/variable';
import { SegmentAsync } from '@grafana/ui';
import { VariablePickerProps } from '../../pickers/types';
import { OperatorSegment } from './OperatorSegment';
import { SelectableValue, MetricFindValue } from '@grafana/data';
import { MetricFindValue, SelectableValue } from '@grafana/data';
import { AdHocFilterBuilder } from './AdHocFilterBuilder';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { ConditionSegment } from './ConditionSegment';
import { addFilter, removeFilter, changeFilter } from '../actions';
import { addFilter, changeFilter, removeFilter } from '../actions';
interface OwnProps extends VariablePickerProps<AdHocVariableModel> {}
@@ -27,14 +27,14 @@ const REMOVE_FILTER_KEY = '-- remove filter --';
const REMOVE_VALUE = { label: REMOVE_FILTER_KEY, value: REMOVE_FILTER_KEY };
export class AdHocPickerUnconnected extends PureComponent<Props> {
onChange = (index: number, prop: string) => (key: SelectableValue<string>) => {
const { uuid, filters } = this.props.variable;
const { id, filters } = this.props.variable;
const { value } = key;
if (key.value === REMOVE_FILTER_KEY) {
return this.props.removeFilter(uuid!, index);
return this.props.removeFilter(id!, index);
}
return this.props.changeFilter(uuid!, {
return this.props.changeFilter(id!, {
index,
filter: {
...filters[index],
@@ -44,8 +44,8 @@ export class AdHocPickerUnconnected extends PureComponent<Props> {
};
appendFilterToVariable = (filter: AdHocVariableFilter) => {
const { uuid } = this.props.variable;
this.props.addFilter(uuid!, filter);
const { id } = this.props.variable;
this.props.addFilter(id!, filter);
};
fetchFilterKeys = async () => {

View File

@@ -2,9 +2,9 @@ import { reducerTester } from '../../../../test/core/redux/reducerTester';
import cloneDeep from 'lodash/cloneDeep';
import { getVariableTestContext } from '../state/helpers';
import { toVariablePayload } from '../state/types';
import { adHocVariableReducer, filterAdded, filterRemoved, filterUpdated, filtersRestored } from './reducer';
import { adHocVariableReducer, filterAdded, filterRemoved, filtersRestored, filterUpdated } from './reducer';
import { VariablesState } from '../state/variablesReducer';
import { AdHocVariableModel, AdHocVariableFilter } from '../../templating/variable';
import { AdHocVariableFilter, AdHocVariableModel } from '../../templating/variable';
import { createAdHocVariableAdapter } from './adapter';
describe('adHocVariableReducer', () => {
@@ -12,17 +12,17 @@ describe('adHocVariableReducer', () => {
describe('when filterAdded is dispatched', () => {
it('then state should be correct', () => {
const uuid = '0';
const { initialState } = getVariableTestContext(adapter, { uuid });
const id = '0';
const { initialState } = getVariableTestContext(adapter, { id });
const filter = createFilter('a');
const payload = toVariablePayload({ uuid, type: 'adhoc' }, filter);
const payload = toVariablePayload({ id, type: 'adhoc' }, filter);
reducerTester<VariablesState>()
.givenReducer(adHocVariableReducer, cloneDeep(initialState))
.whenActionIsDispatched(filterAdded(payload))
.thenStateShouldEqual({
[uuid]: {
...initialState[uuid],
[id]: {
...initialState[id],
filters: [{ value: 'a', operator: '=', condition: '', key: 'a' }],
} as AdHocVariableModel,
});
@@ -31,18 +31,18 @@ describe('adHocVariableReducer', () => {
describe('when filterAdded is dispatched and filter already exists', () => {
it('then state should be correct', () => {
const uuid = '0';
const id = '0';
const filterA = createFilter('a');
const filterB = createFilter('b');
const { initialState } = getVariableTestContext(adapter, { uuid, filters: [filterA] });
const payload = toVariablePayload({ uuid, type: 'adhoc' }, filterB);
const { initialState } = getVariableTestContext(adapter, { id, filters: [filterA] });
const payload = toVariablePayload({ id, type: 'adhoc' }, filterB);
reducerTester<VariablesState>()
.givenReducer(adHocVariableReducer, cloneDeep(initialState))
.whenActionIsDispatched(filterAdded(payload))
.thenStateShouldEqual({
[uuid]: {
...initialState[uuid],
[id]: {
...initialState[id],
filters: [
{ value: 'a', operator: '=', condition: '', key: 'a' },
{ value: 'b', operator: '=', condition: '', key: 'b' },
@@ -54,19 +54,19 @@ describe('adHocVariableReducer', () => {
describe('when filterRemoved is dispatched to remove second filter', () => {
it('then state should be correct', () => {
const uuid = '0';
const id = '0';
const filterA = createFilter('a');
const filterB = createFilter('b');
const index = 1;
const { initialState } = getVariableTestContext(adapter, { uuid, filters: [filterA, filterB] });
const payload = toVariablePayload({ uuid, type: 'adhoc' }, index);
const { initialState } = getVariableTestContext(adapter, { id, filters: [filterA, filterB] });
const payload = toVariablePayload({ id, type: 'adhoc' }, index);
reducerTester<VariablesState>()
.givenReducer(adHocVariableReducer, cloneDeep(initialState))
.whenActionIsDispatched(filterRemoved(payload))
.thenStateShouldEqual({
[uuid]: {
...initialState[uuid],
[id]: {
...initialState[id],
filters: [{ value: 'a', operator: '=', condition: '', key: 'a' }],
} as AdHocVariableModel,
});
@@ -75,19 +75,19 @@ describe('adHocVariableReducer', () => {
describe('when filterRemoved is dispatched to remove first filter', () => {
it('then state should be correct', () => {
const uuid = '0';
const id = '0';
const filterA = createFilter('a');
const filterB = createFilter('b');
const index = 0;
const { initialState } = getVariableTestContext(adapter, { uuid, filters: [filterA, filterB] });
const payload = toVariablePayload({ uuid, type: 'adhoc' }, index);
const { initialState } = getVariableTestContext(adapter, { id, filters: [filterA, filterB] });
const payload = toVariablePayload({ id, type: 'adhoc' }, index);
reducerTester<VariablesState>()
.givenReducer(adHocVariableReducer, cloneDeep(initialState))
.whenActionIsDispatched(filterRemoved(payload))
.thenStateShouldEqual({
[uuid]: {
...initialState[uuid],
[id]: {
...initialState[id],
filters: [{ value: 'b', operator: '=', condition: '', key: 'b' }],
} as AdHocVariableModel,
});
@@ -96,18 +96,18 @@ describe('adHocVariableReducer', () => {
describe('when filterRemoved is dispatched to all filters', () => {
it('then state should be correct', () => {
const uuid = '0';
const id = '0';
const filterA = createFilter('a');
const index = 0;
const { initialState } = getVariableTestContext(adapter, { uuid, filters: [filterA] });
const payload = toVariablePayload({ uuid, type: 'adhoc' }, index);
const { initialState } = getVariableTestContext(adapter, { id, filters: [filterA] });
const payload = toVariablePayload({ id, type: 'adhoc' }, index);
reducerTester<VariablesState>()
.givenReducer(adHocVariableReducer, cloneDeep(initialState))
.whenActionIsDispatched(filterRemoved(payload))
.thenStateShouldEqual({
[uuid]: {
...initialState[uuid],
[id]: {
...initialState[id],
filters: [] as AdHocVariableFilter[],
} as AdHocVariableModel,
});
@@ -116,20 +116,20 @@ describe('adHocVariableReducer', () => {
describe('when filterUpdated is dispatched', () => {
it('then state should be correct', () => {
const uuid = '0';
const id = '0';
const original = createFilter('a');
const other = createFilter('b');
const filter = createFilter('aa');
const index = 1;
const { initialState } = getVariableTestContext(adapter, { uuid, filters: [other, original] });
const payload = toVariablePayload({ uuid, type: 'adhoc' }, { index, filter });
const { initialState } = getVariableTestContext(adapter, { id, filters: [other, original] });
const payload = toVariablePayload({ id, type: 'adhoc' }, { index, filter });
reducerTester<VariablesState>()
.givenReducer(adHocVariableReducer, cloneDeep(initialState))
.whenActionIsDispatched(filterUpdated(payload))
.thenStateShouldEqual({
[uuid]: {
...initialState[uuid],
[id]: {
...initialState[id],
filters: [
{ value: 'b', operator: '=', condition: '', key: 'b' },
{ value: 'aa', operator: '=', condition: '', key: 'aa' },
@@ -141,20 +141,20 @@ describe('adHocVariableReducer', () => {
describe('when filterUpdated is dispatched to update operator', () => {
it('then state should be correct', () => {
const uuid = '0';
const id = '0';
const original = createFilter('a');
const other = createFilter('b');
const filter = createFilter('aa', '>');
const index = 1;
const { initialState } = getVariableTestContext(adapter, { uuid, filters: [other, original] });
const payload = toVariablePayload({ uuid, type: 'adhoc' }, { index, filter });
const { initialState } = getVariableTestContext(adapter, { id, filters: [other, original] });
const payload = toVariablePayload({ id, type: 'adhoc' }, { index, filter });
reducerTester<VariablesState>()
.givenReducer(adHocVariableReducer, cloneDeep(initialState))
.whenActionIsDispatched(filterUpdated(payload))
.thenStateShouldEqual({
[uuid]: {
...initialState[uuid],
[id]: {
...initialState[id],
filters: [
{ value: 'b', operator: '=', condition: '', key: 'b' },
{ value: 'aa', operator: '>', condition: '', key: 'aa' },
@@ -166,18 +166,18 @@ describe('adHocVariableReducer', () => {
describe('when filtersRestored is dispatched', () => {
it('then state should be correct', () => {
const uuid = '0';
const id = '0';
const original = [createFilter('a'), createFilter('b')];
const restored = [createFilter('aa'), createFilter('bb')];
const { initialState } = getVariableTestContext(adapter, { uuid, filters: original });
const payload = toVariablePayload({ uuid, type: 'adhoc' }, restored);
const { initialState } = getVariableTestContext(adapter, { id, filters: original });
const payload = toVariablePayload({ id, type: 'adhoc' }, restored);
reducerTester<VariablesState>()
.givenReducer(adHocVariableReducer, cloneDeep(initialState))
.whenActionIsDispatched(filtersRestored(payload))
.thenStateShouldEqual({
[uuid]: {
...initialState[uuid],
[id]: {
...initialState[id],
filters: [
{ value: 'aa', operator: '=', condition: '', key: 'aa' },
{ value: 'bb', operator: '=', condition: '', key: 'bb' },
@@ -189,17 +189,17 @@ describe('adHocVariableReducer', () => {
describe('when filtersRestored is dispatched on variabel with no filters', () => {
it('then state should be correct', () => {
const uuid = '0';
const id = '0';
const restored = [createFilter('aa'), createFilter('bb')];
const { initialState } = getVariableTestContext(adapter, { uuid });
const payload = toVariablePayload({ uuid, type: 'adhoc' }, restored);
const { initialState } = getVariableTestContext(adapter, { id });
const payload = toVariablePayload({ id, type: 'adhoc' }, restored);
reducerTester<VariablesState>()
.givenReducer(adHocVariableReducer, cloneDeep(initialState))
.whenActionIsDispatched(filtersRestored(payload))
.thenStateShouldEqual({
[uuid]: {
...initialState[uuid],
[id]: {
...initialState[id],
filters: [
{ value: 'aa', operator: '=', condition: '', key: 'aa' },
{ value: 'bb', operator: '=', condition: '', key: 'bb' },

View File

@@ -1,7 +1,7 @@
import { AdHocVariableModel, VariableHide, AdHocVariableFilter } from 'app/features/templating/variable';
import { EMPTY_UUID, getInstanceState, VariablePayload } from '../state/types';
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
import { VariablesState, initialVariablesState } from '../state/variablesReducer';
import { AdHocVariableFilter, AdHocVariableModel, VariableHide } from 'app/features/templating/variable';
import { getInstanceState, NEW_VARIABLE_ID, VariablePayload } from '../state/types';
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { initialVariablesState, VariablesState } from '../state/variablesReducer';
export interface AdHocVariabelFilterUpdate {
index: number;
@@ -13,7 +13,7 @@ export interface AdHocVariableEditorState {
}
export const initialAdHocVariableModelState: AdHocVariableModel = {
uuid: EMPTY_UUID,
id: NEW_VARIABLE_ID,
global: false,
type: 'adhoc',
name: '',
@@ -31,23 +31,23 @@ export const adHocVariableSlice = createSlice({
initialState: initialVariablesState,
reducers: {
filterAdded: (state: VariablesState, action: PayloadAction<VariablePayload<AdHocVariableFilter>>) => {
const instanceState = getInstanceState<AdHocVariableModel>(state, action.payload.uuid);
const instanceState = getInstanceState<AdHocVariableModel>(state, action.payload.id);
instanceState.filters.push(action.payload.data);
},
filterRemoved: (state: VariablesState, action: PayloadAction<VariablePayload<number>>) => {
const instanceState = getInstanceState<AdHocVariableModel>(state, action.payload.uuid);
const instanceState = getInstanceState<AdHocVariableModel>(state, action.payload.id);
const index = action.payload.data;
instanceState.filters.splice(index, 1);
},
filterUpdated: (state: VariablesState, action: PayloadAction<VariablePayload<AdHocVariabelFilterUpdate>>) => {
const instanceState = getInstanceState<AdHocVariableModel>(state, action.payload.uuid);
const instanceState = getInstanceState<AdHocVariableModel>(state, action.payload.id);
const { filter, index } = action.payload.data;
instanceState.filters[index] = filter;
},
filtersRestored: (state: VariablesState, action: PayloadAction<VariablePayload<AdHocVariableFilter[]>>) => {
const instanceState = getInstanceState<AdHocVariableModel>(state, action.payload.uuid);
const instanceState = getInstanceState<AdHocVariableModel>(state, action.payload.id);
instanceState.filters = action.payload.data;
},
},

View File

@@ -23,7 +23,7 @@ describe('constant actions', () => {
const variable: ConstantVariableModel = {
type: 'constant',
uuid: '0',
id: '0',
global: false,
current: {
value: '',

View File

@@ -30,7 +30,7 @@ export const createConstantVariableAdapter = (): VariableAdapter<ConstantVariabl
await dispatch(updateConstantVariableOptions(toVariableIdentifier(variable)));
},
getSaveModel: variable => {
const { index, uuid, initLock, global, ...rest } = cloneDeep(variable);
const { index, id, initLock, global, ...rest } = cloneDeep(variable);
return rest;
},
getValueForUrl: variable => {

View File

@@ -13,16 +13,16 @@ describe('constantVariableReducer', () => {
describe('when createConstantOptionsFromQuery is dispatched', () => {
it('then state should be correct', () => {
const query = 'ABC';
const uuid = '0';
const { initialState } = getVariableTestContext(adapter, { uuid, query });
const payload = toVariablePayload({ uuid: '0', type: 'constant' });
const id = '0';
const { initialState } = getVariableTestContext(adapter, { id, query });
const payload = toVariablePayload({ id: '0', type: 'constant' });
reducerTester<VariablesState>()
.givenReducer(constantVariableReducer, cloneDeep(initialState))
.whenActionIsDispatched(createConstantOptionsFromQuery(payload))
.thenStateShouldEqual({
[uuid]: {
...initialState[uuid],
[id]: {
...initialState[id],
options: [
{
text: query,
@@ -38,16 +38,16 @@ describe('constantVariableReducer', () => {
describe('when createConstantOptionsFromQuery is dispatched and query contains spaces', () => {
it('then state should be correct', () => {
const query = ' ABC ';
const uuid = '0';
const { initialState } = getVariableTestContext(adapter, { uuid, query });
const payload = toVariablePayload({ uuid: '0', type: 'constant' });
const id = '0';
const { initialState } = getVariableTestContext(adapter, { id, query });
const payload = toVariablePayload({ id: '0', type: 'constant' });
reducerTester<VariablesState>()
.givenReducer(constantVariableReducer, cloneDeep(initialState))
.whenActionIsDispatched(createConstantOptionsFromQuery(payload))
.thenStateShouldEqual({
[uuid]: {
...initialState[uuid],
[id]: {
...initialState[id],
options: [
{
text: query.trim(),

View File

@@ -1,10 +1,10 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { ConstantVariableModel, VariableHide, VariableOption } from '../../templating/variable';
import { EMPTY_UUID, getInstanceState, VariablePayload } from '../state/types';
import { getInstanceState, NEW_VARIABLE_ID, VariablePayload } from '../state/types';
import { initialVariablesState, VariablesState } from '../state/variablesReducer';
export const initialConstantVariableModelState: ConstantVariableModel = {
uuid: EMPTY_UUID,
id: NEW_VARIABLE_ID,
global: false,
type: 'constant',
name: '',
@@ -23,7 +23,7 @@ export const constantVariableSlice = createSlice({
initialState: initialVariablesState,
reducers: {
createConstantOptionsFromQuery: (state: VariablesState, action: PayloadAction<VariablePayload>) => {
const instanceState = getInstanceState<ConstantVariableModel>(state, action.payload.uuid);
const instanceState = getInstanceState<ConstantVariableModel>(state, action.payload.id);
instanceState.options = [
{ text: instanceState.query.trim(), value: instanceState.query.trim(), selected: false },
];

View File

@@ -23,7 +23,7 @@ describe('custom actions', () => {
const variable: CustomVariableModel = {
type: 'custom',
uuid: '0',
id: '0',
global: false,
current: {
value: '',

View File

@@ -30,7 +30,7 @@ export const createCustomVariableAdapter = (): VariableAdapter<CustomVariableMod
await dispatch(updateCustomVariableOptions(toVariableIdentifier(variable)));
},
getSaveModel: variable => {
const { index, uuid, initLock, global, ...rest } = cloneDeep(variable);
const { index, id, initLock, global, ...rest } = cloneDeep(variable);
return rest;
},
getValueForUrl: variable => {

View File

@@ -13,16 +13,16 @@ describe('customVariableReducer', () => {
describe('when createCustomOptionsFromQuery is dispatched', () => {
it('then state should be correct', () => {
const query = 'a,b,c';
const uuid = '0';
const { initialState } = getVariableTestContext(adapter, { uuid, query });
const payload = toVariablePayload({ uuid: '0', type: 'custom' });
const id = '0';
const { initialState } = getVariableTestContext(adapter, { id, query });
const payload = toVariablePayload({ id: '0', type: 'custom' });
reducerTester<VariablesState>()
.givenReducer(customVariableReducer, cloneDeep(initialState))
.whenActionIsDispatched(createCustomOptionsFromQuery(payload))
.thenStateShouldEqual({
[uuid]: {
...initialState[uuid],
[id]: {
...initialState[id],
options: [
{
text: 'a',
@@ -48,16 +48,16 @@ describe('customVariableReducer', () => {
describe('when createCustomOptionsFromQuery is dispatched and query contains spaces', () => {
it('then state should be correct', () => {
const query = 'a, b, c';
const uuid = '0';
const { initialState } = getVariableTestContext(adapter, { uuid, query });
const payload = toVariablePayload({ uuid: '0', type: 'constant' });
const id = '0';
const { initialState } = getVariableTestContext(adapter, { id, query });
const payload = toVariablePayload({ id: '0', type: 'constant' });
reducerTester<VariablesState>()
.givenReducer(customVariableReducer, cloneDeep(initialState))
.whenActionIsDispatched(createCustomOptionsFromQuery(payload))
.thenStateShouldEqual({
[uuid]: {
...initialState[uuid],
[id]: {
...initialState[id],
options: [
{
text: 'a',
@@ -83,16 +83,16 @@ describe('customVariableReducer', () => {
describe('when createCustomOptionsFromQuery is dispatched and includeAll is true', () => {
it('then state should be correct', () => {
const query = 'a,b,c';
const uuid = '0';
const { initialState } = getVariableTestContext(adapter, { uuid, query, includeAll: true });
const payload = toVariablePayload({ uuid: '0', type: 'constant' });
const id = '0';
const { initialState } = getVariableTestContext(adapter, { id, query, includeAll: true });
const payload = toVariablePayload({ id: '0', type: 'constant' });
reducerTester<VariablesState>()
.givenReducer(customVariableReducer, cloneDeep(initialState))
.whenActionIsDispatched(createCustomOptionsFromQuery(payload))
.thenStateShouldEqual({
[uuid]: {
...initialState[uuid],
[id]: {
...initialState[id],
options: [
{
text: ALL_VARIABLE_TEXT,

View File

@@ -1,11 +1,17 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { CustomVariableModel, VariableHide, VariableOption } from '../../templating/variable';
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE, EMPTY_UUID, getInstanceState, VariablePayload } from '../state/types';
import {
ALL_VARIABLE_TEXT,
ALL_VARIABLE_VALUE,
getInstanceState,
NEW_VARIABLE_ID,
VariablePayload,
} from '../state/types';
import { initialVariablesState, VariablesState } from '../state/variablesReducer';
export const initialCustomVariableModelState: CustomVariableModel = {
uuid: EMPTY_UUID,
id: NEW_VARIABLE_ID,
global: false,
multi: false,
includeAll: false,
@@ -27,7 +33,7 @@ export const customVariableSlice = createSlice({
initialState: initialVariablesState,
reducers: {
createCustomOptionsFromQuery: (state: VariablesState, action: PayloadAction<VariablePayload>) => {
const instanceState = getInstanceState<CustomVariableModel>(state, action.payload.uuid);
const instanceState = getInstanceState<CustomVariableModel>(state, action.payload.id);
const { includeAll, query } = instanceState;
const match = query.match(/(?:\\,|[^,])+/g) ?? [];

View File

@@ -15,7 +15,7 @@ import { getMockPlugin } from '../../plugins/__mocks__/pluginMocks';
import { createDataSourceOptions } from './reducer';
import { setCurrentVariableValue } from '../state/sharedReducer';
import { changeVariableEditorExtended } from '../editor/reducer';
import * as variableBuilder from '../shared/testing/builders';
import { datasourceBuilder } from '../shared/testing/builders';
describe('data source actions', () => {
variableAdapters.set('datasource', createDataSourceVariableAdapter());
@@ -41,9 +41,8 @@ describe('data source actions', () => {
const getMetricSourcesMock = jest.fn().mockResolvedValue(sources);
const getDatasourceSrvMock = jest.fn().mockReturnValue({ getMetricSources: getMetricSourcesMock });
const dependencies: DataSourceVariableActionDependencies = { getDatasourceSrv: getDatasourceSrvMock };
const datasource = variableBuilder
.datasource()
.withUUID('0')
const datasource = datasourceBuilder()
.withId('0')
.withQuery('mock-data-id')
.build();
@@ -57,11 +56,11 @@ describe('data source actions', () => {
await tester.thenDispatchedActionsShouldEqual(
createDataSourceOptions(
toVariablePayload({ type: 'datasource', uuid: '0' }, { sources, regex: (undefined as unknown) as RegExp })
toVariablePayload({ type: 'datasource', id: '0' }, { sources, regex: (undefined as unknown) as RegExp })
),
setCurrentVariableValue(
toVariablePayload(
{ type: 'datasource', uuid: '0' },
{ type: 'datasource', id: '0' },
{ option: { text: 'first-name', value: 'first-name', selected: false } }
)
)
@@ -93,9 +92,8 @@ describe('data source actions', () => {
const getMetricSourcesMock = jest.fn().mockResolvedValue(sources);
const getDatasourceSrvMock = jest.fn().mockReturnValue({ getMetricSources: getMetricSourcesMock });
const dependencies: DataSourceVariableActionDependencies = { getDatasourceSrv: getDatasourceSrvMock };
const datasource = variableBuilder
.datasource()
.withUUID('0')
const datasource = datasourceBuilder()
.withId('0')
.withQuery('mock-data-id')
.withRegEx('/.*(second-name).*/')
.build();
@@ -109,11 +107,11 @@ describe('data source actions', () => {
await tester.thenDispatchedActionsShouldEqual(
createDataSourceOptions(
toVariablePayload({ type: 'datasource', uuid: '0' }, { sources, regex: /.*(second-name).*/ })
toVariablePayload({ type: 'datasource', id: '0' }, { sources, regex: /.*(second-name).*/ })
),
setCurrentVariableValue(
toVariablePayload(
{ type: 'datasource', uuid: '0' },
{ type: 'datasource', id: '0' },
{ option: { text: 'second-name', value: 'second-name', selected: false } }
)
)

View File

@@ -19,7 +19,7 @@ export const updateDataSourceVariableOptions = (
dependencies: DataSourceVariableActionDependencies = { getDatasourceSrv: getDatasourceSrv }
): ThunkResult<void> => async (dispatch, getState) => {
const sources = await dependencies.getDatasourceSrv().getMetricSources({ skipVariables: true });
const variableInState = getVariable<DataSourceVariableModel>(identifier.uuid!, getState());
const variableInState = getVariable<DataSourceVariableModel>(identifier.id!, getState());
let regex;
if (variableInState.regex) {

View File

@@ -33,7 +33,7 @@ export const createDataSourceVariableAdapter = (): VariableAdapter<DataSourceVar
await dispatch(updateDataSourceVariableOptions(toVariableIdentifier(variable)));
},
getSaveModel: variable => {
const { index, uuid, initLock, global, ...rest } = cloneDeep(variable);
const { index, id, initLock, global, ...rest } = cloneDeep(variable);
return { ...rest, options: [] };
},
getValueForUrl: variable => {

View File

@@ -46,7 +46,7 @@ describe('dataSourceVariableReducer', () => {
"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 });
const payload = toVariablePayload({ id: '0', type: 'datasource' }, { sources, regex });
reducerTester<VariablesState>()
.givenReducer(dataSourceVariableReducer, cloneDeep(initialState))

View File

@@ -1,6 +1,12 @@
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 {
ALL_VARIABLE_TEXT,
ALL_VARIABLE_VALUE,
getInstanceState,
NEW_VARIABLE_ID,
VariablePayload,
} from '../state/types';
import { initialVariablesState, VariablesState } from '../state/variablesReducer';
import { DataSourceSelectItem } from '@grafana/data';
@@ -9,7 +15,7 @@ export interface DataSourceVariableEditorState {
}
export const initialDataSourceVariableModelState: DataSourceVariableModel = {
uuid: EMPTY_UUID,
id: NEW_VARIABLE_ID,
global: false,
type: 'datasource',
name: '',
@@ -37,7 +43,7 @@ export const dataSourceVariableSlice = createSlice({
) => {
const { sources, regex } = action.payload.data;
const options: VariableOption[] = [];
const instanceState = getInstanceState<DataSourceVariableModel>(state, action.payload.uuid);
const instanceState = getInstanceState<DataSourceVariableModel>(state, action.payload.id);
for (let i = 0; i < sources.length; i++) {
const source = sources[i];
// must match on type

View File

@@ -1,5 +1,5 @@
import React, { MouseEvent, PureComponent } from 'react';
import { EMPTY_UUID, toVariableIdentifier, toVariablePayload, VariableIdentifier } from '../state/types';
import { NEW_VARIABLE_ID, toVariableIdentifier, toVariablePayload, VariableIdentifier } from '../state/types';
import { StoreState } from '../../../types';
import { e2e } from '@grafana/e2e';
import { VariableEditorList } from './VariableEditorList';
@@ -53,7 +53,7 @@ class VariableEditorContainerUnconnected extends PureComponent<Props> {
};
onDuplicateVariable = (identifier: VariableIdentifier) => {
this.props.duplicateVariable(toVariablePayload(identifier, { newUuid: (undefined as unknown) as string }));
this.props.duplicateVariable(toVariablePayload(identifier, { newId: (undefined as unknown) as string }));
};
onRemoveVariable = (identifier: VariableIdentifier) => {
@@ -61,7 +61,7 @@ class VariableEditorContainerUnconnected extends PureComponent<Props> {
};
render() {
const variableToEdit = this.props.variables.find(s => s.uuid === this.props.idInEditor) ?? null;
const variableToEdit = this.props.variables.find(s => s.id === this.props.idInEditor) ?? null;
return (
<div>
<div className="page-action-bar">
@@ -72,7 +72,7 @@ class VariableEditorContainerUnconnected extends PureComponent<Props> {
>
Variables
</a>
{this.props.idInEditor === EMPTY_UUID && (
{this.props.idInEditor === NEW_VARIABLE_ID && (
<span>
<i
className="fa fa-fw fa-chevron-right"
@@ -81,7 +81,7 @@ class VariableEditorContainerUnconnected extends PureComponent<Props> {
New
</span>
)}
{this.props.idInEditor && this.props.idInEditor !== EMPTY_UUID && (
{this.props.idInEditor && this.props.idInEditor !== NEW_VARIABLE_ID && (
<span>
<i
className="fa fa-fw fa-chevron-right"

View File

@@ -4,7 +4,7 @@ import { AppEvents } from '@grafana/data';
import { FormLabel } from '@grafana/ui';
import { e2e } from '@grafana/e2e';
import { variableAdapters } from '../adapters';
import { EMPTY_UUID, toVariablePayload, VariableIdentifier } from '../state/types';
import { NEW_VARIABLE_ID, toVariablePayload, VariableIdentifier } from '../state/types';
import { VariableHide, VariableModel, VariableType } from '../../templating/variable';
import { appEvents } from '../../../core/core';
import { VariableValuesPreview } from './VariableValuesPreview';
@@ -97,11 +97,11 @@ export class VariableEditorEditorUnConnected extends PureComponent<Props> {
return;
}
if (this.props.variable.uuid !== EMPTY_UUID) {
if (this.props.variable.id !== NEW_VARIABLE_ID) {
await this.props.onEditorUpdate(this.props.identifier);
}
if (this.props.variable.uuid === EMPTY_UUID) {
if (this.props.variable.id === NEW_VARIABLE_ID) {
await this.props.onEditorAdd(this.props.identifier);
}
};
@@ -111,7 +111,7 @@ export class VariableEditorEditorUnConnected extends PureComponent<Props> {
if (!EditorToRender) {
return null;
}
const newVariable = this.props.variable.uuid && this.props.variable.uuid === EMPTY_UUID;
const newVariable = this.props.variable.id && this.props.variable.id === NEW_VARIABLE_ID;
return (
<div>
@@ -227,7 +227,7 @@ export class VariableEditorEditorUnConnected extends PureComponent<Props> {
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (state, ownProps) => ({
editor: state.templating.editor,
variable: getVariable(ownProps.identifier.uuid!, state),
variable: getVariable(ownProps.identifier.id, state, false), // we could be renaming a variable and we don't want this to throw
});
const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = {

View File

@@ -9,30 +9,35 @@ import {
variableEditorUnMounted,
} from './reducer';
import { variableAdapters } from '../adapters';
import { v4 } from 'uuid';
import { AddVariable, EMPTY_UUID, toVariablePayload, VariableIdentifier } from '../state/types';
import {
AddVariable,
NEW_VARIABLE_ID,
toVariableIdentifier,
toVariablePayload,
VariableIdentifier,
} from '../state/types';
import cloneDeep from 'lodash/cloneDeep';
import { VariableType } from '../../templating/variable';
import { addVariable, removeVariable, storeNewVariable } from '../state/sharedReducer';
export const variableEditorMount = (identifier: VariableIdentifier): ThunkResult<void> => {
return async dispatch => {
dispatch(variableEditorMounted({ name: getVariable(identifier.uuid!).name }));
dispatch(variableEditorMounted({ name: getVariable(identifier.id!).name }));
};
};
export const variableEditorUnMount = (identifier: VariableIdentifier): ThunkResult<void> => {
return async (dispatch, getState) => {
dispatch(variableEditorUnMounted(toVariablePayload(identifier)));
if (getState().templating.variables[EMPTY_UUID]) {
dispatch(removeVariable(toVariablePayload({ type: identifier.type, uuid: EMPTY_UUID }, { reIndex: false })));
if (getState().templating.variables[NEW_VARIABLE_ID]) {
dispatch(removeVariable(toVariablePayload({ type: identifier.type, id: NEW_VARIABLE_ID }, { reIndex: false })));
}
};
};
export const onEditorUpdate = (identifier: VariableIdentifier): ThunkResult<void> => {
return async (dispatch, getState) => {
const variableInState = getVariable(identifier.uuid!, getState());
const variableInState = getVariable(identifier.id!, getState());
await variableAdapters.get(variableInState.type).updateOptions(variableInState);
dispatch(switchToListMode());
};
@@ -40,17 +45,23 @@ export const onEditorUpdate = (identifier: VariableIdentifier): ThunkResult<void
export const onEditorAdd = (identifier: VariableIdentifier): ThunkResult<void> => {
return async (dispatch, getState) => {
const uuid = v4();
dispatch(storeNewVariable(toVariablePayload({ type: identifier.type, uuid })));
const variableInState = getVariable(uuid, getState());
const newVariableInState = getVariable(NEW_VARIABLE_ID, getState());
const id = newVariableInState.name;
dispatch(storeNewVariable(toVariablePayload({ type: identifier.type, id })));
const variableInState = getVariable(id, getState());
await variableAdapters.get(variableInState.type).updateOptions(variableInState);
dispatch(switchToListMode());
dispatch(removeVariable(toVariablePayload({ type: identifier.type, uuid: EMPTY_UUID }, { reIndex: false })));
dispatch(removeVariable(toVariablePayload({ type: identifier.type, id: NEW_VARIABLE_ID }, { reIndex: false })));
};
};
export const changeVariableName = (identifier: VariableIdentifier, newName: string): ThunkResult<void> => {
return (dispatch, getState) => {
const variableInState = getVariable(identifier.id, getState());
if (newName === variableInState.name) {
return;
}
let errorText = null;
if (!newName.match(/^(?!__).*$/)) {
errorText = "Template names cannot begin with '__', that's reserved for Grafana's global variables";
@@ -61,39 +72,62 @@ export const changeVariableName = (identifier: VariableIdentifier, newName: stri
}
const variables = getVariables(getState());
const stateVariables = variables.filter(v => v.name === newName && v.uuid !== identifier.uuid);
const foundVariables = variables.filter(v => v.name === newName && v.id !== identifier.id);
if (stateVariables.length) {
if (foundVariables.length) {
errorText = 'Variable with the same name already exists';
}
if (errorText) {
dispatch(changeVariableNameFailed({ newName, errorText }));
return;
}
if (!errorText) {
dispatch(changeVariableNameSucceeded(toVariablePayload(identifier, newName)));
}
const thunkToCall = identifier.id === NEW_VARIABLE_ID ? completeChangeNewVariableName : completeChangeVariableName;
dispatch(thunkToCall(identifier, newName));
};
};
export const completeChangeNewVariableName = (
identifier: VariableIdentifier,
newName: string
): ThunkResult<void> => dispatch => {
dispatch(changeVariableNameSucceeded(toVariablePayload(identifier, { newName })));
};
export const completeChangeVariableName = (identifier: VariableIdentifier, newName: string): ThunkResult<void> => (
dispatch,
getState
) => {
const originalVariable = getVariable(identifier.id, getState());
const model = { ...cloneDeep(originalVariable), name: newName, id: newName };
const global = originalVariable.global;
const index = originalVariable.index;
const renamedIdentifier = toVariableIdentifier(model);
dispatch(addVariable(toVariablePayload(renamedIdentifier, { global, index, model })));
dispatch(changeVariableNameSucceeded(toVariablePayload(renamedIdentifier, { newName })));
dispatch(switchToEditMode(renamedIdentifier));
dispatch(removeVariable(toVariablePayload(identifier, { reIndex: false })));
};
export const switchToNewMode = (): ThunkResult<void> => (dispatch, getState) => {
const type: VariableType = 'query';
const uuid = EMPTY_UUID;
const id = NEW_VARIABLE_ID;
const global = false;
const model = cloneDeep(variableAdapters.get(type).initialState);
const index = getNewVariabelIndex(getState());
const identifier = { type, uuid };
const identifier = { type, id };
dispatch(
addVariable(
toVariablePayload<AddVariable>(identifier, { global, model, index })
)
);
dispatch(setIdInEditor({ id: identifier.uuid }));
dispatch(setIdInEditor({ id: identifier.id }));
};
export const switchToEditMode = (identifier: VariableIdentifier): ThunkResult<void> => dispatch => {
dispatch(setIdInEditor({ id: identifier.uuid }));
dispatch(setIdInEditor({ id: identifier.id }));
};
export const switchToListMode = (): ThunkResult<void> => dispatch => {

View File

@@ -63,7 +63,7 @@ describe('variableEditorReducer', () => {
errors: { update: 'Something wrong' },
extended: { prop: 1000 },
};
const payload = toVariablePayload({ uuid: '0', type: 'textbox' });
const payload = toVariablePayload({ id: '0', type: 'textbox' });
reducerTester<VariableEditorState>()
.givenReducer(variableEditorReducer, initialState)
.whenActionIsDispatched(variableEditorUnMounted(payload))
@@ -79,7 +79,7 @@ describe('variableEditorReducer', () => {
isValid: false,
errors: { name: 'Duplicate', update: 'Update failed' },
};
const payload = toVariablePayload({ uuid: '0', type: 'textbox' }, 'New Name');
const payload = toVariablePayload({ id: '0', type: 'textbox' }, { newName: 'New Name' });
reducerTester<VariableEditorState>()
.givenReducer(variableEditorReducer, initialState)
.whenActionIsDispatched(changeVariableNameSucceeded(payload))
@@ -100,7 +100,7 @@ describe('variableEditorReducer', () => {
isValid: false,
errors: { name: 'Duplicate' },
};
const payload = toVariablePayload({ uuid: '0', type: 'textbox' }, 'New Name');
const payload = toVariablePayload({ id: '0', type: 'textbox' }, { newName: 'New Name' });
reducerTester<VariableEditorState>()
.givenReducer(variableEditorReducer, initialState)
.whenActionIsDispatched(changeVariableNameSucceeded(payload))

View File

@@ -34,8 +34,11 @@ const variableEditorReducerSlice = createSlice({
variableEditorUnMounted: (state: VariableEditorState, action: PayloadAction<VariablePayload>) => {
return initialVariableEditorState;
},
changeVariableNameSucceeded: (state: VariableEditorState, action: PayloadAction<VariablePayload<string>>) => {
state.name = action.payload.data;
changeVariableNameSucceeded: (
state: VariableEditorState,
action: PayloadAction<VariablePayload<{ newName: string }>>
) => {
state.name = action.payload.data.newName;
delete state.errors['name'];
state.isValid = Object.keys(state.errors).length === 0;
},

View File

@@ -17,15 +17,14 @@ import { Emitter } from 'app/core/core';
import { AppEvents, dateTime } from '@grafana/data';
import { getTimeSrv, setTimeSrv, TimeSrv } from '../../dashboard/services/TimeSrv';
import { TemplateSrv } from '../../templating/template_srv';
import * as variableBuilder from '../shared/testing/builders';
import { intervalBuilder } from '../shared/testing/builders';
describe('interval actions', () => {
variableAdapters.set('interval', createIntervalVariableAdapter());
describe('when updateIntervalVariableOptions is dispatched', () => {
it('then correct actions are dispatched', async () => {
const interval = variableBuilder
.interval()
.withUUID('0')
const interval = intervalBuilder()
.withId('0')
.withQuery('1s,1m,1h,1d')
.withAuto(false)
.build();
@@ -36,10 +35,10 @@ describe('interval actions', () => {
.whenAsyncActionIsDispatched(updateIntervalVariableOptions(toVariableIdentifier(interval)), true);
tester.thenDispatchedActionsShouldEqual(
createIntervalOptions({ type: 'interval', uuid: '0', data: undefined }),
createIntervalOptions({ type: 'interval', id: '0', data: undefined }),
setCurrentVariableValue({
type: 'interval',
uuid: '0',
id: '0',
data: { option: { text: '1s', value: '1s', selected: false } },
})
);
@@ -62,9 +61,8 @@ describe('interval actions', () => {
} as unknown) as TimeSrv;
const originalTimeSrv = getTimeSrv();
setTimeSrv(timeSrvMock);
const interval = variableBuilder
.interval()
.withUUID('0')
const interval = intervalBuilder()
.withId('0')
.withQuery('1s,1m,1h,1d')
.withAuto(true)
.withAutoMin('1') // illegal interval string
@@ -91,9 +89,8 @@ describe('interval actions', () => {
describe('when updateAutoValue is dispatched', () => {
describe('and auto is false', () => {
it('then no dependencies are called', async () => {
const interval = variableBuilder
.interval()
.withUUID('0')
const interval = intervalBuilder()
.withId('0')
.withAuto(false)
.build();
@@ -131,9 +128,8 @@ describe('interval actions', () => {
describe('and auto is true', () => {
it('then correct dependencies are called', async () => {
const interval = variableBuilder
.interval()
.withUUID('0')
const interval = intervalBuilder()
.withId('0')
.withName('intervalName')
.withAuto(true)
.withAutoCount(33)

View File

@@ -42,7 +42,7 @@ export const updateAutoValue = (
templateSrv: templateSrv,
}
): ThunkResult<void> => (dispatch, getState) => {
const variableInState = getVariable<IntervalVariableModel>(identifier.uuid, getState());
const variableInState = getVariable<IntervalVariableModel>(identifier.id, getState());
if (variableInState.auto) {
const res = dependencies.kbn.calculateInterval(
dependencies.getTimeSrv().timeRange(),

View File

@@ -32,7 +32,7 @@ export const createIntervalVariableAdapter = (): VariableAdapter<IntervalVariabl
await dispatch(updateIntervalVariableOptions(toVariableIdentifier(variable)));
},
getSaveModel: variable => {
const { index, uuid, initLock, global, ...rest } = cloneDeep(variable);
const { index, id, initLock, global, ...rest } = cloneDeep(variable);
return rest;
},
getValueForUrl: variable => {

View File

@@ -13,11 +13,11 @@ describe('intervalVariableReducer', () => {
describe('when createIntervalOptions is dispatched', () => {
describe('and auto is false', () => {
it('then state should be correct', () => {
const uuid = '0';
const id = '0';
const query = '1s,1m,1h,1d';
const auto = false;
const { initialState } = getVariableTestContext<IntervalVariableModel>(adapter, { uuid, query, auto });
const payload = toVariablePayload({ uuid: '0', type: 'interval' });
const { initialState } = getVariableTestContext<IntervalVariableModel>(adapter, { id, query, auto });
const payload = toVariablePayload({ id: '0', type: 'interval' });
reducerTester<VariablesState>()
.givenReducer(intervalVariableReducer, cloneDeep(initialState))
@@ -25,7 +25,7 @@ describe('intervalVariableReducer', () => {
.thenStateShouldEqual({
'0': {
...initialState['0'],
uuid: '0',
id: '0',
query: '1s,1m,1h,1d',
auto: false,
options: [
@@ -41,11 +41,11 @@ describe('intervalVariableReducer', () => {
describe('and auto is true', () => {
it('then state should be correct', () => {
const uuid = '0';
const id = '0';
const query = '1s,1m,1h,1d';
const auto = true;
const { initialState } = getVariableTestContext<IntervalVariableModel>(adapter, { uuid, query, auto });
const payload = toVariablePayload({ uuid: '0', type: 'interval' });
const { initialState } = getVariableTestContext<IntervalVariableModel>(adapter, { id, query, auto });
const payload = toVariablePayload({ id: '0', type: 'interval' });
reducerTester<VariablesState>()
.givenReducer(intervalVariableReducer, cloneDeep(initialState))
@@ -53,7 +53,7 @@ describe('intervalVariableReducer', () => {
.thenStateShouldEqual({
'0': {
...initialState['0'],
uuid: '0',
id: '0',
query: '1s,1m,1h,1d',
auto: true,
options: [
@@ -70,11 +70,11 @@ describe('intervalVariableReducer', () => {
describe('and query contains "', () => {
it('then state should be correct', () => {
const uuid = '0';
const id = '0';
const query = '"kalle, anka","donald, duck"';
const auto = false;
const { initialState } = getVariableTestContext<IntervalVariableModel>(adapter, { uuid, query, auto });
const payload = toVariablePayload({ uuid: '0', type: 'interval' });
const { initialState } = getVariableTestContext<IntervalVariableModel>(adapter, { id, query, auto });
const payload = toVariablePayload({ id: '0', type: 'interval' });
reducerTester<VariablesState>()
.givenReducer(intervalVariableReducer, cloneDeep(initialState))
@@ -82,7 +82,7 @@ describe('intervalVariableReducer', () => {
.thenStateShouldEqual({
'0': {
...initialState['0'],
uuid: '0',
id: '0',
query: '"kalle, anka","donald, duck"',
auto: false,
options: [
@@ -96,11 +96,11 @@ describe('intervalVariableReducer', () => {
describe("and query contains '", () => {
it('then state should be correct', () => {
const uuid = '0';
const id = '0';
const query = "'kalle, anka','donald, duck'";
const auto = false;
const { initialState } = getVariableTestContext<IntervalVariableModel>(adapter, { uuid, query, auto });
const payload = toVariablePayload({ uuid: '0', type: 'interval' });
const { initialState } = getVariableTestContext<IntervalVariableModel>(adapter, { id, query, auto });
const payload = toVariablePayload({ id: '0', type: 'interval' });
reducerTester<VariablesState>()
.givenReducer(intervalVariableReducer, cloneDeep(initialState))
@@ -108,7 +108,7 @@ describe('intervalVariableReducer', () => {
.thenStateShouldEqual({
'0': {
...initialState['0'],
uuid: '0',
id: '0',
query: "'kalle, anka','donald, duck'",
auto: false,
options: [

View File

@@ -1,11 +1,11 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { IntervalVariableModel, VariableHide, VariableOption, VariableRefresh } from '../../templating/variable';
import { EMPTY_UUID, getInstanceState, VariablePayload } from '../state/types';
import { getInstanceState, NEW_VARIABLE_ID, VariablePayload } from '../state/types';
import { initialVariablesState, VariablesState } from '../state/variablesReducer';
import _ from 'lodash';
export const initialIntervalVariableModelState: IntervalVariableModel = {
uuid: EMPTY_UUID,
id: NEW_VARIABLE_ID,
global: false,
type: 'interval',
name: '',
@@ -28,7 +28,7 @@ export const intervalVariableSlice = createSlice({
initialState: initialVariablesState,
reducers: {
createIntervalOptions: (state: VariablesState, action: PayloadAction<VariablePayload>) => {
const instanceState = getInstanceState<IntervalVariableModel>(state, action.payload.uuid!);
const instanceState = getInstanceState<IntervalVariableModel>(state, action.payload.id!);
const options: VariableOption[] = _.map(instanceState.query.match(/(["'])(.*?)\1|\w+/g), text => {
text = text.replace(/["']+/g, '');
return { text: text.trim(), value: text.trim(), selected: false };

View File

@@ -54,7 +54,7 @@ export class OptionsPickerUnconnected extends PureComponent<Props> {
render() {
const { variable, picker } = this.props;
const showOptions = picker.uuid === variable.uuid;
const showOptions = picker.id === variable.id;
return (
<div className="variable-link-wrapper">

View File

@@ -397,7 +397,7 @@ describe('options picker actions', () => {
function createVariable(extend?: Partial<QueryVariableModel>): QueryVariableModel {
return {
type: 'query',
uuid: '0',
id: '0',
global: false,
current: createOption(''),
options: [],

View File

@@ -56,8 +56,8 @@ export const navigateOptions = (key: NavigationKey, clearOthers: boolean): Thunk
export const filterOrSearchOptions = (searchQuery: string): ThunkResult<void> => {
return async (dispatch, getState) => {
const { uuid } = getState().templating.optionsPicker;
const { query, options } = getVariable<VariableWithOptions>(uuid!, getState());
const { id } = getState().templating.optionsPicker;
const { query, options } = getVariable<VariableWithOptions>(id!, getState());
dispatch(updateSearchQuery(searchQuery));
if (containsSearchFilter(query)) {
@@ -70,13 +70,13 @@ export const filterOrSearchOptions = (searchQuery: string): ThunkResult<void> =>
export const commitChangesToVariable = (): ThunkResult<void> => {
return async (dispatch, getState) => {
const picker = getState().templating.optionsPicker;
const existing = getVariable<VariableWithMultiSupport>(picker.uuid, getState());
const existing = getVariable<VariableWithMultiSupport>(picker.id, getState());
const currentPayload = { option: mapToCurrent(picker) };
const searchQueryPayload = { propName: 'queryValue', propValue: picker.queryValue };
dispatch(setCurrentVariableValue(toVariablePayload(existing, currentPayload)));
dispatch(changeVariableProp(toVariablePayload(existing, searchQueryPayload)));
const updated = getVariable<VariableWithMultiSupport>(picker.uuid, getState());
const updated = getVariable<VariableWithMultiSupport>(picker.id, getState());
if (existing.current.text === updated.current.text) {
return dispatch(hideOptions());
@@ -90,8 +90,8 @@ export const commitChangesToVariable = (): ThunkResult<void> => {
export const toggleOptionByHighlight = (clearOthers: boolean): ThunkResult<void> => {
return (dispatch, getState) => {
const { uuid, highlightIndex } = getState().templating.optionsPicker;
const variable = getVariable<VariableWithMultiSupport>(uuid, getState());
const { id, highlightIndex } = getState().templating.optionsPicker;
const variable = getVariable<VariableWithMultiSupport>(id, getState());
const option = variable.options[highlightIndex];
dispatch(toggleOption({ option, forceSelect: false, clearOthers }));
};
@@ -111,7 +111,7 @@ export const toggleAndFetchTag = (tag: VariableTag): ThunkResult<void> => {
const fetchTagValues = (tagText: string): ThunkResult<Promise<string[]>> => {
return async (dispatch, getState) => {
const picker = getState().templating.optionsPicker;
const variable = getVariable<QueryVariableModel>(picker.uuid, getState());
const variable = getVariable<QueryVariableModel>(picker.id, getState());
const datasource = await getDataSourceSrv().get(variable.datasource ?? '');
const query = variable.tagValuesQuery.replace('$tag', tagText);
@@ -139,13 +139,13 @@ const getTimeRange = (variable: QueryVariableModel) => {
const searchForOptions = async (dispatch: ThunkDispatch, getState: () => StoreState, searchQuery: string) => {
try {
const { uuid } = getState().templating.optionsPicker;
const existing = getVariable<VariableWithOptions>(uuid, getState());
const { id } = getState().templating.optionsPicker;
const existing = getVariable<VariableWithOptions>(id, getState());
const adapter = variableAdapters.get(existing.type);
await adapter.updateOptions(existing, searchQuery);
const updated = getVariable<VariableWithOptions>(uuid, getState());
const updated = getVariable<VariableWithOptions>(id, getState());
dispatch(updateOptionsFromSearch(updated.options));
} catch (error) {
console.error(error);

View File

@@ -283,7 +283,7 @@ describe('optionsPickerReducer', () => {
query,
options: [selected, { text: 'A', value: 'A', selected: false }, { text: 'B', value: 'B', selected: false }],
multi: false,
uuid: '0',
id: '0',
queryValue,
} as QueryVariableModel;
@@ -294,7 +294,7 @@ describe('optionsPickerReducer', () => {
...initialState,
options: payload.options,
queryValue,
uuid: payload.uuid!,
id: payload.id!,
multi: payload.multi,
selectedValues: [selected],
});
@@ -312,14 +312,14 @@ describe('optionsPickerReducer', () => {
{ text: 'B', value: 'B', selected: false },
];
const { initialState } = getVariableTestContext({});
const payload = { type: 'query', uuid: '0', current, query, options, queryValue } as QueryVariableModel;
const payload = { type: 'query', id: '0', current, query, options, queryValue } as QueryVariableModel;
reducerTester<OptionsPickerState>()
.givenReducer(optionsPickerReducer, cloneDeep(initialState))
.whenActionIsDispatched(showOptions(payload))
.thenStateShouldEqual({
...initialState,
uuid: '0',
id: '0',
queryValue: '',
selectedValues: [
{
@@ -343,7 +343,7 @@ describe('optionsPickerReducer', () => {
],
queryValue: 'a search',
highlightIndex: 1,
uuid: '0',
id: '0',
});
reducerTester<OptionsPickerState>()

View File

@@ -17,7 +17,7 @@ export interface ToggleOption {
}
export interface OptionsPickerState {
uuid: string;
id: string;
selectedValues: VariableOption[];
selectedTags: VariableTag[];
queryValue: string | null;
@@ -28,7 +28,7 @@ export interface OptionsPickerState {
}
export const initialState: OptionsPickerState = {
uuid: '',
id: '',
highlightIndex: -1,
queryValue: null,
selectedTags: [],
@@ -76,7 +76,7 @@ const optionsPickerSlice = createSlice({
state.options = cloneDeep(options);
state.tags = getTags(action.payload);
state.multi = multi ?? false;
state.uuid = action.payload.uuid!;
state.id = action.payload.id!;
state.queryValue = '';
if (isQuery(action.payload)) {

View File

@@ -163,7 +163,7 @@ describe('query actions', () => {
const tester = await reduxTester<{ templating: TemplatingState }>()
.givenRootReducer(getTemplatingRootReducer())
.whenActionIsDispatched(initDashboardTemplating([variable]))
.whenActionIsDispatched(setIdInEditor({ id: variable.uuid! }))
.whenActionIsDispatched(setIdInEditor({ id: variable.id! }))
.whenAsyncActionIsDispatched(updateQueryVariableOptions(toVariablePayload(variable)), true);
const option = createOption(ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE);
@@ -190,7 +190,7 @@ describe('query actions', () => {
const tester = await reduxTester<{ templating: TemplatingState }>()
.givenRootReducer(getTemplatingRootReducer())
.whenActionIsDispatched(initDashboardTemplating([variable]))
.whenActionIsDispatched(setIdInEditor({ id: variable.uuid! }))
.whenActionIsDispatched(setIdInEditor({ id: variable.id! }))
.whenAsyncActionIsDispatched(updateQueryVariableOptions(toVariablePayload(variable)), true);
tester.thenDispatchedActionsPredicateShouldEqual(actions => {
@@ -530,7 +530,7 @@ function mockDatasourceMetrics(variable: QueryVariableModel, optionsMetrics: any
function createVariable(extend?: Partial<QueryVariableModel>): QueryVariableModel {
return {
type: 'query',
uuid: '0',
id: '0',
global: false,
current: createOption(''),
options: [],

View File

@@ -20,9 +20,9 @@ export const updateQueryVariableOptions = (
searchFilter?: string
): ThunkResult<void> => {
return async (dispatch, getState) => {
const variableInState = getVariable<QueryVariableModel>(identifier.uuid!, getState());
const variableInState = getVariable<QueryVariableModel>(identifier.id!, getState());
try {
if (getState().templating.editor.id === variableInState.uuid) {
if (getState().templating.editor.id === variableInState.id) {
dispatch(removeVariableEditorError({ errorProp: 'update' }));
}
const dataSource = await getDatasourceSrv().get(variableInState.datasource ?? '');
@@ -49,7 +49,7 @@ export const updateQueryVariableOptions = (
if (err.data && err.data.message) {
err.message = err.data.message;
}
if (getState().templating.editor.id === variableInState.uuid) {
if (getState().templating.editor.id === variableInState.id) {
dispatch(addVariableEditorError({ errorProp: 'update', errorText: err.message }));
}
appEvents.emit(AppEvents.alertError, [
@@ -72,7 +72,7 @@ export const initQueryVariableEditor = (identifier: VariableIdentifier): ThunkRe
const allDataSources = [defaultDatasource].concat(dataSources);
dispatch(changeVariableEditorExtended({ propName: 'dataSources', propValue: allDataSources }));
const variable = getVariable<QueryVariableModel>(identifier.uuid!, getState());
const variable = getVariable<QueryVariableModel>(identifier.id!, getState());
if (!variable.datasource) {
return;
}
@@ -101,7 +101,7 @@ export const changeQueryVariableQuery = (
query: any,
definition: string
): ThunkResult<void> => async (dispatch, getState) => {
const variableInState = getVariable<QueryVariableModel>(identifier.uuid!, getState());
const variableInState = getVariable<QueryVariableModel>(identifier.id!, 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 }));

View File

@@ -31,7 +31,7 @@ export const createQueryVariableAdapter = (): VariableAdapter<QueryVariableModel
await dispatch(updateQueryVariableOptions(toVariableIdentifier(variable), searchFilter));
},
getSaveModel: variable => {
const { index, uuid, initLock, global, queryValue, ...rest } = cloneDeep(variable);
const { index, id, initLock, global, queryValue, ...rest } = cloneDeep(variable);
// remove options
if (variable.refresh !== VariableRefresh.never) {
return { ...rest, options: [] };

View File

@@ -17,7 +17,7 @@ describe('queryVariableReducer', () => {
{ text: 'A', value: 'A', selected: false },
{ text: 'B', value: 'B', selected: false },
];
const payload = toVariablePayload({ uuid: '0', type: 'query' }, options);
const payload = toVariablePayload({ id: '0', type: 'query' }, options);
reducerTester<VariablesState>()
.givenReducer(queryVariableReducer, cloneDeep(initialState))
.whenActionIsDispatched(updateVariableOptions(payload))
@@ -42,7 +42,7 @@ describe('queryVariableReducer', () => {
{ text: 'A', value: 'A', selected: false },
{ text: 'B', value: 'B', selected: false },
];
const payload = toVariablePayload({ uuid: '0', type: 'query' }, options);
const payload = toVariablePayload({ id: '0', type: 'query' }, options);
reducerTester<VariablesState>()
.givenReducer(queryVariableReducer, cloneDeep(initialState))
.whenActionIsDispatched(updateVariableOptions(payload))
@@ -62,7 +62,7 @@ describe('queryVariableReducer', () => {
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' }, []);
const payload = toVariablePayload({ id: '0', type: 'query' }, []);
reducerTester<VariablesState>()
.givenReducer(queryVariableReducer, cloneDeep(initialState))
.whenActionIsDispatched(updateVariableOptions(payload))
@@ -79,7 +79,7 @@ describe('queryVariableReducer', () => {
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' }, []);
const payload = toVariablePayload({ id: '0', type: 'query' }, []);
reducerTester<VariablesState>()
.givenReducer(queryVariableReducer, cloneDeep(initialState))
.whenActionIsDispatched(updateVariableOptions(payload))
@@ -100,7 +100,7 @@ describe('queryVariableReducer', () => {
{ text: 'A', value: 'A', selected: false },
{ text: 'B', value: 'B', selected: false },
];
const payload = toVariablePayload({ uuid: '0', type: 'query' }, options);
const payload = toVariablePayload({ id: '0', type: 'query' }, options);
reducerTester<VariablesState>()
.givenReducer(queryVariableReducer, cloneDeep(initialState))
.whenActionIsDispatched(updateVariableOptions(payload))
@@ -124,7 +124,7 @@ describe('queryVariableReducer', () => {
{ text: 'A', value: 'A', selected: false },
{ text: 'B', value: 'B', selected: false },
];
const payload = toVariablePayload({ uuid: '0', type: 'query' }, options);
const payload = toVariablePayload({ id: '0', type: 'query' }, options);
reducerTester<VariablesState>()
.givenReducer(queryVariableReducer, cloneDeep(initialState))
.whenActionIsDispatched(updateVariableOptions(payload))
@@ -142,7 +142,7 @@ describe('queryVariableReducer', () => {
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);
const payload = toVariablePayload({ id: '0', type: 'query' }, tags);
reducerTester<VariablesState>()
.givenReducer(queryVariableReducer, cloneDeep(initialState))
.whenActionIsDispatched(updateVariableTags(payload))

View File

@@ -14,8 +14,8 @@ import templateSrv from '../../templating/template_srv';
import {
ALL_VARIABLE_TEXT,
ALL_VARIABLE_VALUE,
EMPTY_UUID,
getInstanceState,
NEW_VARIABLE_ID,
NONE_VARIABLE_TEXT,
NONE_VARIABLE_VALUE,
VariablePayload,
@@ -31,7 +31,7 @@ export interface QueryVariableEditorState {
}
export const initialQueryVariableModelState: QueryVariableModel = {
uuid: EMPTY_UUID,
id: NEW_VARIABLE_ID,
global: false,
index: -1,
type: 'query',
@@ -134,7 +134,7 @@ export const queryVariableSlice = createSlice({
reducers: {
updateVariableOptions: (state: VariablesState, action: PayloadAction<VariablePayload<any[]>>) => {
const results = action.payload.data;
const instanceState = getInstanceState<QueryVariableModel>(state, action.payload.uuid);
const instanceState = getInstanceState<QueryVariableModel>(state, action.payload.id);
const { regex, includeAll, sort } = instanceState;
const options = metricNamesToVariableValues(regex, sort, results);
@@ -148,7 +148,7 @@ export const queryVariableSlice = createSlice({
instanceState.options = options;
},
updateVariableTags: (state: VariablesState, action: PayloadAction<VariablePayload<any[]>>) => {
const instanceState = getInstanceState<QueryVariableModel>(state, action.payload.uuid);
const instanceState = getInstanceState<QueryVariableModel>(state, action.payload.id);
const results = action.payload.data;
const tags: VariableTag[] = [];
for (let i = 0; i < results.length; i++) {

View File

@@ -11,10 +11,10 @@ import { initialCustomVariableModelState } from '../../custom/reducer';
import { MultiVariableBuilder } from './multiVariableBuilder';
import { initialConstantVariableModelState } from '../../constant/reducer';
export const adHoc = () => new AdHocVariableBuilder(initialAdHocVariableModelState);
export const interval = () => new IntervalVariableBuilder(initialIntervalVariableModelState);
export const datasource = () => new DatasourceVariableBuilder(initialDataSourceVariableModelState);
export const query = () => new DatasourceVariableBuilder(initialQueryVariableModelState);
export const textbox = () => new OptionsVariableBuilder(initialTextBoxVariableModelState);
export const custom = () => new MultiVariableBuilder(initialCustomVariableModelState);
export const constant = () => new OptionsVariableBuilder(initialConstantVariableModelState);
export const adHocBuilder = () => new AdHocVariableBuilder(initialAdHocVariableModelState);
export const intervalBuilder = () => new IntervalVariableBuilder(initialIntervalVariableModelState);
export const datasourceBuilder = () => new DatasourceVariableBuilder(initialDataSourceVariableModelState);
export const queryBuilder = () => new DatasourceVariableBuilder(initialQueryVariableModelState);
export const textboxBuilder = () => new OptionsVariableBuilder(initialTextBoxVariableModelState);
export const customBuilder = () => new MultiVariableBuilder(initialCustomVariableModelState);
export const constantBuilder = () => new OptionsVariableBuilder(initialConstantVariableModelState);

View File

@@ -5,7 +5,7 @@ export class VariableBuilder<T extends VariableModel> {
protected variable: T;
constructor(initialState: T) {
const { uuid, index, global, ...rest } = initialState;
const { id, index, global, ...rest } = initialState;
this.variable = cloneDeep({ ...rest, name: rest.type }) as T;
}
@@ -14,8 +14,8 @@ export class VariableBuilder<T extends VariableModel> {
return this;
}
withUUID(uuid: string) {
this.variable.uuid = uuid;
withId(id: string) {
this.variable.id = id;
return this;
}

View File

@@ -1,5 +1,6 @@
import { AnyAction } from 'redux';
import { UrlQueryMap } from '@grafana/runtime';
import { dateTime, TimeRange } from '@grafana/data';
import { getTemplatingAndLocationRootReducer, getTemplatingRootReducer } from './helpers';
import { variableAdapters } from '../adapters';
@@ -7,6 +8,7 @@ import { createQueryVariableAdapter } from '../query/adapter';
import { createCustomVariableAdapter } from '../custom/adapter';
import { createTextBoxVariableAdapter } from '../textbox/adapter';
import { createConstantVariableAdapter } from '../constant/adapter';
import { createIntervalVariableAdapter } from '../interval/adapter';
import { reduxTester } from '../../../../test/core/redux/reduxTester';
import { TemplatingState } from 'app/features/variables/state/reducers';
import {
@@ -17,16 +19,30 @@ import {
setOptionFromUrl,
validateVariableSelectionState,
} from './actions';
import { addInitLock, addVariable, removeInitLock, resolveInitLock, setCurrentVariableValue } from './sharedReducer';
import { toVariableIdentifier, toVariablePayload } from './types';
import {
addInitLock,
addVariable,
removeInitLock,
removeVariable,
resolveInitLock,
setCurrentVariableValue,
} from './sharedReducer';
import { NEW_VARIABLE_ID, toVariableIdentifier, toVariablePayload } from './types';
import { changeVariableName } from '../editor/actions';
import { changeVariableNameFailed, changeVariableNameSucceeded, setIdInEditor } from '../editor/reducer';
import { TemplateSrv } from '../../templating/template_srv';
import { Emitter } from '../../../core/core';
import { createIntervalVariableAdapter } from '../interval/adapter';
import { VariableRefresh } from '../../templating/variable';
import { DashboardModel } from '../../dashboard/state';
import { DashboardState } from '../../../types';
import { dateTime, TimeRange } from '@grafana/data';
import * as variableBuilder from '../shared/testing/builders';
import {
constantBuilder,
customBuilder,
datasourceBuilder,
intervalBuilder,
queryBuilder,
textboxBuilder,
} from '../shared/testing/builders';
describe('shared actions', () => {
describe('when initDashboardTemplating is dispatched', () => {
@@ -35,11 +51,11 @@ describe('shared actions', () => {
variableAdapters.set('custom', createCustomVariableAdapter());
variableAdapters.set('textbox', createTextBoxVariableAdapter());
variableAdapters.set('constant', createConstantVariableAdapter());
const query = variableBuilder.query().build();
const constant = variableBuilder.constant().build();
const datasource = variableBuilder.datasource().build();
const custom = variableBuilder.custom().build();
const textbox = variableBuilder.textbox().build();
const query = queryBuilder().build();
const constant = constantBuilder().build();
const datasource = datasourceBuilder().build();
const custom = customBuilder().build();
const textbox = textboxBuilder().build();
const list = [query, constant, datasource, custom, textbox];
reduxTester<{ templating: TemplatingState }>()
@@ -63,16 +79,16 @@ describe('shared actions', () => {
// because uuid are dynamic we need to get the uuid from the resulting state
// an alternative would be to add our own uuids in the model above instead
expect(dispatchedActions[4]).toEqual(
addInitLock(toVariablePayload({ ...query, uuid: dispatchedActions[4].payload.uuid }))
addInitLock(toVariablePayload({ ...query, id: dispatchedActions[4].payload.id }))
);
expect(dispatchedActions[5]).toEqual(
addInitLock(toVariablePayload({ ...constant, uuid: dispatchedActions[5].payload.uuid }))
addInitLock(toVariablePayload({ ...constant, id: dispatchedActions[5].payload.id }))
);
expect(dispatchedActions[6]).toEqual(
addInitLock(toVariablePayload({ ...custom, uuid: dispatchedActions[6].payload.uuid }))
addInitLock(toVariablePayload({ ...custom, id: dispatchedActions[6].payload.id }))
);
expect(dispatchedActions[7]).toEqual(
addInitLock(toVariablePayload({ ...textbox, uuid: dispatchedActions[7].payload.uuid }))
addInitLock(toVariablePayload({ ...textbox, id: dispatchedActions[7].payload.id }))
);
return true;
@@ -86,11 +102,11 @@ describe('shared actions', () => {
variableAdapters.set('custom', createCustomVariableAdapter());
variableAdapters.set('textbox', createTextBoxVariableAdapter());
variableAdapters.set('constant', createConstantVariableAdapter());
const query = variableBuilder.query().build();
const constant = variableBuilder.constant().build();
const datasource = variableBuilder.datasource().build();
const custom = variableBuilder.custom().build();
const textbox = variableBuilder.textbox().build();
const query = queryBuilder().build();
const constant = constantBuilder().build();
const datasource = datasourceBuilder().build();
const custom = customBuilder().build();
const textbox = textboxBuilder().build();
const list = [query, constant, datasource, custom, textbox];
const tester = await reduxTester<{ templating: TemplatingState; location: { query: UrlQueryMap } }>({
@@ -104,29 +120,29 @@ describe('shared actions', () => {
expect(dispatchedActions.length).toEqual(8);
expect(dispatchedActions[0]).toEqual(
resolveInitLock(toVariablePayload({ ...query, uuid: dispatchedActions[0].payload.uuid }))
resolveInitLock(toVariablePayload({ ...query, id: dispatchedActions[0].payload.id }))
);
expect(dispatchedActions[1]).toEqual(
resolveInitLock(toVariablePayload({ ...constant, uuid: dispatchedActions[1].payload.uuid }))
resolveInitLock(toVariablePayload({ ...constant, id: dispatchedActions[1].payload.id }))
);
expect(dispatchedActions[2]).toEqual(
resolveInitLock(toVariablePayload({ ...custom, uuid: dispatchedActions[2].payload.uuid }))
resolveInitLock(toVariablePayload({ ...custom, id: dispatchedActions[2].payload.id }))
);
expect(dispatchedActions[3]).toEqual(
resolveInitLock(toVariablePayload({ ...textbox, uuid: dispatchedActions[3].payload.uuid }))
resolveInitLock(toVariablePayload({ ...textbox, id: dispatchedActions[3].payload.id }))
);
expect(dispatchedActions[4]).toEqual(
removeInitLock(toVariablePayload({ ...query, uuid: dispatchedActions[4].payload.uuid }))
removeInitLock(toVariablePayload({ ...query, id: dispatchedActions[4].payload.id }))
);
expect(dispatchedActions[5]).toEqual(
removeInitLock(toVariablePayload({ ...constant, uuid: dispatchedActions[5].payload.uuid }))
removeInitLock(toVariablePayload({ ...constant, id: dispatchedActions[5].payload.id }))
);
expect(dispatchedActions[6]).toEqual(
removeInitLock(toVariablePayload({ ...custom, uuid: dispatchedActions[6].payload.uuid }))
removeInitLock(toVariablePayload({ ...custom, id: dispatchedActions[6].payload.id }))
);
expect(dispatchedActions[7]).toEqual(
removeInitLock(toVariablePayload({ ...textbox, uuid: dispatchedActions[7].payload.uuid }))
removeInitLock(toVariablePayload({ ...textbox, id: dispatchedActions[7].payload.id }))
);
return true;
@@ -146,9 +162,8 @@ describe('shared actions', () => {
${undefined} | ${[undefined]}
`('and urlValue is $urlValue then correct actions are dispatched', async ({ urlValue, expected }) => {
variableAdapters.set('custom', createCustomVariableAdapter());
const custom = variableBuilder
.custom()
.withUUID('0')
const custom = customBuilder()
.withId('0')
.withOptions('A', 'B', 'C')
.withCurrent('A')
.build();
@@ -161,7 +176,7 @@ describe('shared actions', () => {
await tester.thenDispatchedActionsShouldEqual(
setCurrentVariableValue(
toVariablePayload(
{ type: 'custom', uuid: '0' },
{ type: 'custom', id: '0' },
{ option: { text: expected, value: expected, selected: false } }
)
)
@@ -184,16 +199,14 @@ describe('shared actions', () => {
let custom;
if (!withOptions) {
custom = variableBuilder
.custom()
.withUUID('0')
custom = customBuilder()
.withId('0')
.withCurrent(withCurrent)
.withoutOptions()
.build();
} else {
custom = variableBuilder
.custom()
.withUUID('0')
custom = customBuilder()
.withId('0')
.withOptions(...withOptions)
.withCurrent(withCurrent)
.build();
@@ -213,7 +226,7 @@ describe('shared actions', () => {
: [
setCurrentVariableValue(
toVariablePayload(
{ type: 'custom', uuid: '0' },
{ type: 'custom', id: '0' },
{ option: { text: expected, value: expected, selected: false } }
)
),
@@ -240,17 +253,15 @@ describe('shared actions', () => {
let custom;
if (!withOptions) {
custom = variableBuilder
.custom()
.withUUID('0')
custom = customBuilder()
.withId('0')
.withMulti()
.withCurrent(withCurrent)
.withoutOptions()
.build();
} else {
custom = variableBuilder
.custom()
.withUUID('0')
custom = customBuilder()
.withId('0')
.withMulti()
.withOptions(...withOptions)
.withCurrent(withCurrent)
@@ -271,7 +282,7 @@ describe('shared actions', () => {
: [
setCurrentVariableValue(
toVariablePayload(
{ type: 'custom', uuid: '0' },
{ type: 'custom', id: '0' },
{ option: { text: expectedText, value: expectedText, selected: expectedSelected } }
)
),
@@ -316,9 +327,8 @@ describe('shared actions', () => {
variableAdapters.set('constant', createConstantVariableAdapter());
// initial variable state
const initialVariable = variableBuilder
.interval()
.withUUID('0')
const initialVariable = intervalBuilder()
.withId('interval-0')
.withName('interval-0')
.withOptions('1m', '10m', '30m', '1h', '6h', '12h', '1d', '7d', '14d', '30d')
.withCurrent('1m')
@@ -326,22 +336,20 @@ describe('shared actions', () => {
.build();
// the constant variable should be filtered out
const constant = variableBuilder
.constant()
.withUUID('1')
const constant = constantBuilder()
.withId('constant-1')
.withName('constant-1')
.withOptions('a constant')
.withCurrent('a constant')
.build();
const initialState = {
templating: { variables: { '0': { ...initialVariable }, '1': { ...constant } } },
templating: { variables: { 'interval-0': { ...initialVariable }, 'constant-1': { ...constant } } },
dashboard,
};
// updated variable state
const updatedVariable = variableBuilder
.interval()
.withUUID('0')
const updatedVariable = intervalBuilder()
.withId('interval-0')
.withName('interval-0')
.withOptions('1m')
.withCurrent('1m')
@@ -349,7 +357,7 @@ describe('shared actions', () => {
.build();
const variable = args.update ? { ...updatedVariable } : { ...initialVariable };
const state = { templating: { variables: { '0': variable, '1': { ...constant } } }, dashboard };
const state = { templating: { variables: { 'interval-0': variable, 'constant-1': { ...constant } } }, dashboard };
const getStateMock = jest
.fn()
.mockReturnValueOnce(initialState)
@@ -443,4 +451,154 @@ describe('shared actions', () => {
});
});
});
describe('when changeVariableName is dispatched with the same name', () => {
it('then no actions are dispatched', () => {
const textbox = textboxBuilder()
.withId('textbox')
.withName('textbox')
.build();
const constant = constantBuilder()
.withId('constant')
.withName('constant')
.build();
reduxTester<{ templating: TemplatingState }>()
.givenRootReducer(getTemplatingRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(textbox, { global: false, index: 0, model: textbox })))
.whenActionIsDispatched(addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant })))
.whenActionIsDispatched(changeVariableName(toVariableIdentifier(constant), constant.name), true)
.thenNoActionsWhereDispatched();
});
});
describe('when changeVariableName is dispatched with an unique name', () => {
it('then the correct actions are dispatched', () => {
const textbox = textboxBuilder()
.withId('textbox')
.withName('textbox')
.build();
const constant = constantBuilder()
.withId('constant')
.withName('constant')
.build();
reduxTester<{ templating: TemplatingState }>()
.givenRootReducer(getTemplatingRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(textbox, { global: false, index: 0, model: textbox })))
.whenActionIsDispatched(addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant })))
.whenActionIsDispatched(changeVariableName(toVariableIdentifier(constant), 'constant1'), true)
.thenDispatchedActionsShouldEqual(
addVariable({
type: 'constant',
id: 'constant1',
data: {
global: false,
index: 1,
model: { ...constant, name: 'constant1', id: 'constant1', global: false, index: 1 },
},
}),
changeVariableNameSucceeded({ type: 'constant', id: 'constant1', data: { newName: 'constant1' } }),
setIdInEditor({ id: 'constant1' }),
removeVariable({ type: 'constant', id: 'constant', data: { reIndex: false } })
);
});
});
describe('when changeVariableName is dispatched with an unique name for a new variable', () => {
it('then the correct actions are dispatched', () => {
const textbox = textboxBuilder()
.withId('textbox')
.withName('textbox')
.build();
const constant = constantBuilder()
.withId(NEW_VARIABLE_ID)
.withName('constant')
.build();
reduxTester<{ templating: TemplatingState }>()
.givenRootReducer(getTemplatingRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(textbox, { global: false, index: 0, model: textbox })))
.whenActionIsDispatched(addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant })))
.whenActionIsDispatched(changeVariableName(toVariableIdentifier(constant), 'constant1'), true)
.thenDispatchedActionsShouldEqual(
changeVariableNameSucceeded({ type: 'constant', id: NEW_VARIABLE_ID, data: { newName: 'constant1' } })
);
});
});
describe('when changeVariableName is dispatched with __newName', () => {
it('then the correct actions are dispatched', () => {
const textbox = textboxBuilder()
.withId('textbox')
.withName('textbox')
.build();
const constant = constantBuilder()
.withId('constant')
.withName('constant')
.build();
reduxTester<{ templating: TemplatingState }>()
.givenRootReducer(getTemplatingRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(textbox, { global: false, index: 0, model: textbox })))
.whenActionIsDispatched(addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant })))
.whenActionIsDispatched(changeVariableName(toVariableIdentifier(constant), '__newName'), true)
.thenDispatchedActionsShouldEqual(
changeVariableNameFailed({
newName: '__newName',
errorText: "Template names cannot begin with '__', that's reserved for Grafana's global variables",
})
);
});
});
describe('when changeVariableName is dispatched with illegal characters', () => {
it('then the correct actions are dispatched', () => {
const textbox = textboxBuilder()
.withId('textbox')
.withName('textbox')
.build();
const constant = constantBuilder()
.withId('constant')
.withName('constant')
.build();
reduxTester<{ templating: TemplatingState }>()
.givenRootReducer(getTemplatingRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(textbox, { global: false, index: 0, model: textbox })))
.whenActionIsDispatched(addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant })))
.whenActionIsDispatched(changeVariableName(toVariableIdentifier(constant), '#constant!'), true)
.thenDispatchedActionsShouldEqual(
changeVariableNameFailed({
newName: '#constant!',
errorText: 'Only word and digit characters are allowed in variable names',
})
);
});
});
describe('when changeVariableName is dispatched with a name that is already used', () => {
it('then the correct actions are dispatched', () => {
const textbox = textboxBuilder()
.withId('textbox')
.withName('textbox')
.build();
const constant = constantBuilder()
.withId('constant')
.withName('constant')
.build();
reduxTester<{ templating: TemplatingState }>()
.givenRootReducer(getTemplatingRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(textbox, { global: false, index: 0, model: textbox })))
.whenActionIsDispatched(addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant })))
.whenActionIsDispatched(changeVariableName(toVariableIdentifier(constant), 'textbox'), true)
.thenDispatchedActionsShouldEqual(
changeVariableNameFailed({
newName: 'textbox',
errorText: 'Variable with the same name already exists',
})
);
});
});
});

View File

@@ -88,7 +88,7 @@ export const processVariableDependencies = async (variable: VariableModel, state
export const processVariable = (identifier: VariableIdentifier, queryParams: UrlQueryMap): ThunkResult<void> => {
return async (dispatch, getState) => {
const variable = getVariable(identifier.uuid!, getState());
const variable = getVariable(identifier.id!, getState());
await processVariableDependencies(variable, getState());
const urlValue = queryParams['var-' + variable.name];
@@ -131,14 +131,14 @@ export const processVariables = (): ThunkResult<void> => {
export const setOptionFromUrl = (identifier: VariableIdentifier, urlValue: UrlQueryValue): ThunkResult<void> => {
return async (dispatch, getState) => {
const variable = getVariable(identifier.uuid!, getState());
const variable = getVariable(identifier.id!, getState());
if (variable.hasOwnProperty('refresh') && (variable as QueryVariableModel).refresh !== VariableRefresh.never) {
// updates options
await variableAdapters.get(variable.type).updateOptions(variable);
}
// get variable from state
const variableFromState = getVariable<VariableWithOptions>(variable.uuid!, getState());
const variableFromState = getVariable<VariableWithOptions>(variable.id!, getState());
if (!variableFromState) {
throw new Error(`Couldn't find variable with name: ${variable.name}`);
}
@@ -212,7 +212,7 @@ export const validateVariableSelectionState = (
defaultValue?: string
): ThunkResult<void> => {
return async (dispatch, getState) => {
const variableInState = getVariable<VariableWithOptions>(identifier.uuid!, getState());
const variableInState = getVariable<VariableWithOptions>(identifier.id!, getState());
const current = variableInState.current || (({} as unknown) as VariableOption);
const setValue = variableAdapters.get(variableInState.type).setValue;
@@ -296,7 +296,7 @@ const createGraph = (variables: VariableModel[]) => {
export const variableUpdated = (identifier: VariableIdentifier, emitChangeEvents: boolean): ThunkResult<void> => {
return (dispatch, getState) => {
// if there is a variable lock ignore cascading update because we are in a boot up scenario
const variable = getVariable(identifier.uuid!, getState());
const variable = getVariable(identifier.id!, getState());
if (variable.initLock) {
return Promise.resolve();
}
@@ -349,7 +349,7 @@ export const onTimeRangeUpdated = (
const promises = variablesThatNeedRefresh.map(async (variable: VariableWithOptions) => {
const previousOptions = variable.options.slice();
await variableAdapters.get(variable.type).updateOptions(variable);
const updatedVariable = getVariable<VariableWithOptions>(variable.uuid!, getState());
const updatedVariable = getVariable<VariableWithOptions>(variable.id!, getState());
if (angular.toJson(previousOptions) !== angular.toJson(updatedVariable.options)) {
const dashboard = getState().dashboard.getModel();
dashboard?.templateVariableValueUpdated();

View File

@@ -1,6 +1,6 @@
import { combineReducers } from '@reduxjs/toolkit';
import { EMPTY_UUID } from './types';
import { NEW_VARIABLE_ID } from './types';
import { VariableHide, VariableModel } from '../../templating/variable';
import { variablesReducer, VariablesState } from './variablesReducer';
import { optionsPickerReducer } from '../pickers/OptionsPicker/reducer';
@@ -18,7 +18,7 @@ export const getVariableState = (
for (let index = 0; index < noOfVariables; index++) {
variables[index] = {
uuid: index.toString(),
id: index.toString(),
type: 'query',
name: `Name-${index}`,
hide: VariableHide.dontHide,
@@ -29,13 +29,13 @@ export const getVariableState = (
}
if (includeEmpty) {
variables[EMPTY_UUID] = {
uuid: EMPTY_UUID,
variables[NEW_VARIABLE_ID] = {
id: NEW_VARIABLE_ID,
type: 'query',
name: `Name-${EMPTY_UUID}`,
name: `Name-${NEW_VARIABLE_ID}`,
hide: VariableHide.dontHide,
index: noOfVariables,
label: `Label-${EMPTY_UUID}`,
label: `Label-${NEW_VARIABLE_ID}`,
skipUrlSync: false,
};
}
@@ -49,7 +49,7 @@ export const getVariableTestContext = <Model extends VariableModel>(
) => {
const defaultVariable = {
...adapter.initialState,
uuid: '0',
id: '0',
index: 0,
name: '0',
};

View File

@@ -11,7 +11,9 @@ import { resolveInitLock, setCurrentVariableValue } from './sharedReducer';
import { toVariableIdentifier, toVariablePayload } from './types';
import { VariableRefresh } from '../../templating/variable';
import { updateVariableOptions } from '../query/reducer';
import * as variableBuilder from '../shared/testing/builders';
import { customBuilder, queryBuilder } from '../shared/testing/builders';
'../shared/testing/builders';
jest.mock('app/features/dashboard/services/TimeSrv', () => ({
getTimeSrv: jest.fn().mockReturnValue({
@@ -68,26 +70,24 @@ describe('processVariable', () => {
const getAndSetupProcessVariableContext = () => {
variableAdapters.set('custom', createCustomVariableAdapter());
variableAdapters.set('query', createQueryVariableAdapter());
const custom = variableBuilder
.custom()
.withUUID('0')
const custom = customBuilder()
.withId('custom')
.withName('custom')
.withQuery('A,B,C')
.withOptions('A', 'B', 'C')
.withCurrent('A')
.build();
const queryDependsOnCustom = variableBuilder
.query()
.withUUID('1')
const queryDependsOnCustom = queryBuilder()
.withId('queryDependsOnCustom')
.withName('queryDependsOnCustom')
.withQuery('$custom.*')
.withOptions('AA', 'AB', 'AC')
.withCurrent('AA')
.build();
const queryNoDepends = variableBuilder
.query()
.withUUID('2')
const queryNoDepends = queryBuilder()
.withId('queryNoDepends')
.withName('queryNoDepends')
.withQuery('*')
.withOptions('A', 'B', 'C')
@@ -116,7 +116,7 @@ describe('processVariable', () => {
.whenAsyncActionIsDispatched(processVariable(toVariableIdentifier(custom), queryParams), true);
await tester.thenDispatchedActionsShouldEqual(
resolveInitLock(toVariablePayload({ type: 'custom', uuid: '0' }))
resolveInitLock(toVariablePayload({ type: 'custom', id: 'custom' }))
);
});
});
@@ -132,9 +132,12 @@ describe('processVariable', () => {
await tester.thenDispatchedActionsShouldEqual(
setCurrentVariableValue(
toVariablePayload({ type: 'custom', uuid: '0' }, { option: { text: ['B'], value: ['B'], selected: false } })
toVariablePayload(
{ type: 'custom', id: 'custom' },
{ option: { text: ['B'], value: ['B'], selected: false } }
)
),
resolveInitLock(toVariablePayload({ type: 'custom', uuid: '0' }))
resolveInitLock(toVariablePayload({ type: 'custom', id: 'custom' }))
);
});
});
@@ -156,7 +159,7 @@ describe('processVariable', () => {
.whenAsyncActionIsDispatched(processVariable(toVariableIdentifier(queryNoDepends), queryParams), true);
await tester.thenDispatchedActionsShouldEqual(
resolveInitLock(toVariablePayload({ type: 'query', uuid: '2' }))
resolveInitLock(toVariablePayload({ type: 'query', id: 'queryNoDepends' }))
);
});
});
@@ -173,16 +176,19 @@ describe('processVariable', () => {
await tester.thenDispatchedActionsShouldEqual(
updateVariableOptions(
toVariablePayload({ type: 'query', uuid: '2' }, [
toVariablePayload({ type: 'query', id: 'queryNoDepends' }, [
{ value: 'A', text: 'A' },
{ value: 'B', text: 'B' },
{ value: 'C', text: 'C' },
])
),
setCurrentVariableValue(
toVariablePayload({ type: 'query', uuid: '2' }, { option: { text: 'A', value: 'A', selected: false } })
toVariablePayload(
{ type: 'query', id: 'queryNoDepends' },
{ option: { text: 'A', value: 'A', selected: false } }
)
),
resolveInitLock(toVariablePayload({ type: 'query', uuid: '2' }))
resolveInitLock(toVariablePayload({ type: 'query', id: 'queryNoDepends' }))
);
});
});
@@ -205,11 +211,11 @@ describe('processVariable', () => {
await tester.thenDispatchedActionsShouldEqual(
setCurrentVariableValue(
toVariablePayload(
{ type: 'query', uuid: '2' },
{ type: 'query', id: 'queryNoDepends' },
{ option: { text: ['B'], value: ['B'], selected: false } }
)
),
resolveInitLock(toVariablePayload({ type: 'query', uuid: '2' }))
resolveInitLock(toVariablePayload({ type: 'query', id: 'queryNoDepends' }))
);
});
});
@@ -230,22 +236,25 @@ describe('processVariable', () => {
await tester.thenDispatchedActionsShouldEqual(
updateVariableOptions(
toVariablePayload({ type: 'query', uuid: '2' }, [
toVariablePayload({ type: 'query', id: 'queryNoDepends' }, [
{ value: 'A', text: 'A' },
{ value: 'B', text: 'B' },
{ value: 'C', text: 'C' },
])
),
setCurrentVariableValue(
toVariablePayload({ type: 'query', uuid: '2' }, { option: { text: 'A', value: 'A', selected: false } })
toVariablePayload(
{ type: 'query', id: 'queryNoDepends' },
{ option: { text: 'A', value: 'A', selected: false } }
)
),
setCurrentVariableValue(
toVariablePayload(
{ type: 'query', uuid: '2' },
{ type: 'query', id: 'queryNoDepends' },
{ option: { text: ['B'], value: ['B'], selected: false } }
)
),
resolveInitLock(toVariablePayload({ type: 'query', uuid: '2' }))
resolveInitLock(toVariablePayload({ type: 'query', id: 'queryNoDepends' }))
);
});
});
@@ -274,7 +283,7 @@ describe('processVariable', () => {
);
await tester.thenDispatchedActionsShouldEqual(
resolveInitLock(toVariablePayload({ type: 'query', uuid: '1' }))
resolveInitLock(toVariablePayload({ type: 'query', id: 'queryDependsOnCustom' }))
);
});
});
@@ -296,7 +305,7 @@ describe('processVariable', () => {
await tester.thenDispatchedActionsShouldEqual(
updateVariableOptions(
toVariablePayload({ type: 'query', uuid: '1' }, [
toVariablePayload({ type: 'query', id: 'queryDependsOnCustom' }, [
{ value: 'AA', text: 'AA' },
{ value: 'AB', text: 'AB' },
{ value: 'AC', text: 'AC' },
@@ -304,11 +313,11 @@ describe('processVariable', () => {
),
setCurrentVariableValue(
toVariablePayload(
{ type: 'query', uuid: '1' },
{ type: 'query', id: 'queryDependsOnCustom' },
{ option: { text: 'AA', value: 'AA', selected: false } }
)
),
resolveInitLock(toVariablePayload({ type: 'query', uuid: '1' }))
resolveInitLock(toVariablePayload({ type: 'query', id: 'queryDependsOnCustom' }))
);
});
});
@@ -336,11 +345,11 @@ describe('processVariable', () => {
await tester.thenDispatchedActionsShouldEqual(
setCurrentVariableValue(
toVariablePayload(
{ type: 'query', uuid: '1' },
{ type: 'query', id: 'queryDependsOnCustom' },
{ option: { text: ['AB'], value: ['AB'], selected: false } }
)
),
resolveInitLock(toVariablePayload({ type: 'query', uuid: '1' }))
resolveInitLock(toVariablePayload({ type: 'query', id: 'queryDependsOnCustom' }))
);
});
});
@@ -366,7 +375,7 @@ describe('processVariable', () => {
await tester.thenDispatchedActionsShouldEqual(
updateVariableOptions(
toVariablePayload({ type: 'query', uuid: '1' }, [
toVariablePayload({ type: 'query', id: 'queryDependsOnCustom' }, [
{ value: 'AA', text: 'AA' },
{ value: 'AB', text: 'AB' },
{ value: 'AC', text: 'AC' },
@@ -374,17 +383,17 @@ describe('processVariable', () => {
),
setCurrentVariableValue(
toVariablePayload(
{ type: 'query', uuid: '1' },
{ type: 'query', id: 'queryDependsOnCustom' },
{ option: { text: 'AA', value: 'AA', selected: false } }
)
),
setCurrentVariableValue(
toVariablePayload(
{ type: 'query', uuid: '1' },
{ type: 'query', id: 'queryDependsOnCustom' },
{ option: { text: ['AB'], value: ['AB'], selected: false } }
)
),
resolveInitLock(toVariablePayload({ type: 'query', uuid: '1' }))
resolveInitLock(toVariablePayload({ type: 'query', id: 'queryDependsOnCustom' }))
);
});
});

View File

@@ -11,7 +11,7 @@ describe('variablesReducer', () => {
it('then all variables except global variables should be removed', () => {
const initialState: VariablesState = {
'0': {
uuid: '0',
id: '0',
type: 'query',
name: 'Name-0',
hide: VariableHide.dontHide,
@@ -20,7 +20,7 @@ describe('variablesReducer', () => {
skipUrlSync: false,
},
'1': {
uuid: '1',
id: '1',
type: 'query',
name: 'Name-1',
hide: VariableHide.dontHide,
@@ -30,7 +30,7 @@ describe('variablesReducer', () => {
global: true,
},
'2': {
uuid: '2',
id: '2',
type: 'query',
name: 'Name-2',
hide: VariableHide.dontHide,
@@ -39,7 +39,7 @@ describe('variablesReducer', () => {
skipUrlSync: false,
},
'3': {
uuid: '3',
id: '3',
type: 'query',
name: 'Name-3',
hide: VariableHide.dontHide,
@@ -55,7 +55,7 @@ describe('variablesReducer', () => {
.whenActionIsDispatched(cleanUpDashboard())
.thenStateShouldEqual({
'1': {
uuid: '1',
id: '1',
type: 'query',
name: 'Name-1',
hide: VariableHide.dontHide,
@@ -65,7 +65,7 @@ describe('variablesReducer', () => {
global: true,
},
'3': {
uuid: '3',
id: '3',
type: 'query',
name: 'Name-3',
hide: VariableHide.dontHide,
@@ -82,7 +82,7 @@ describe('variablesReducer', () => {
it('then the reducer for that variableAdapter should be invoked', () => {
const initialState: VariablesState = {
'0': {
uuid: '0',
id: '0',
type: 'query',
name: 'Name-0',
hide: VariableHide.dontHide,
@@ -109,12 +109,12 @@ describe('variablesReducer', () => {
const mockAction = createAction<VariablePayload>('mockAction');
reducerTester<VariablesState>()
.givenReducer(variablesReducer, initialState)
.whenActionIsDispatched(mockAction(toVariablePayload({ type: 'query', uuid: '0' })))
.whenActionIsDispatched(mockAction(toVariablePayload({ type: 'query', id: '0' })))
.thenStateShouldEqual(initialState);
expect(variableAdapter.reducer).toHaveBeenCalledTimes(1);
expect(variableAdapter.reducer).toHaveBeenCalledWith(
initialState,
mockAction(toVariablePayload({ type: 'query', uuid: '0' }))
mockAction(toVariablePayload({ type: 'query', id: '0' }))
);
});
});
@@ -123,7 +123,7 @@ describe('variablesReducer', () => {
it('then the reducer for that variableAdapter should be invoked', () => {
const initialState: VariablesState = {
'0': {
uuid: '0',
id: '0',
type: 'query',
name: 'Name-0',
hide: VariableHide.dontHide,
@@ -150,7 +150,7 @@ describe('variablesReducer', () => {
const mockAction = createAction<VariablePayload>('mockAction');
reducerTester<VariablesState>()
.givenReducer(variablesReducer, initialState)
.whenActionIsDispatched(mockAction(toVariablePayload({ type: 'adhoc', uuid: '0' })))
.whenActionIsDispatched(mockAction(toVariablePayload({ type: 'adhoc', id: '0' })))
.thenStateShouldEqual(initialState);
expect(variableAdapter.reducer).toHaveBeenCalledTimes(0);
});
@@ -160,7 +160,7 @@ describe('variablesReducer', () => {
it('then the reducer for that variableAdapter should be invoked', () => {
const initialState: VariablesState = {
'0': {
uuid: '0',
id: '0',
type: 'query',
name: 'Name-0',
hide: VariableHide.dontHide,

View File

@@ -3,34 +3,38 @@ import { cloneDeep } from 'lodash';
import { StoreState } from '../../../types';
import { VariableModel } from '../../templating/variable';
import { getState } from '../../../store/store';
import { EMPTY_UUID } from './types';
import { NEW_VARIABLE_ID } from './types';
export const getVariable = <T extends VariableModel = VariableModel>(
uuid: string,
state: StoreState = getState()
id: string,
state: StoreState = getState(),
throwWhenMissing = true
): T => {
if (!state.templating.variables[uuid]) {
throw new Error(`Couldn't find variable with uuid:${uuid}`);
if (!state.templating.variables[id]) {
if (throwWhenMissing) {
throw new Error(`Couldn't find variable with id:${id}`);
}
return undefined;
}
return state.templating.variables[uuid] as T;
return state.templating.variables[id] as T;
};
export const getFilteredVariables = (filter: (model: VariableModel) => boolean, state: StoreState = getState()) => {
return Object.values(state.templating.variables).filter(filter);
};
export const getVariableWithName = (name: string, state: StoreState = getState()) => {
return Object.values(state.templating.variables).find(variable => variable.name === name);
export const getVariableWithName = (name: string) => {
return getVariable(name, getState(), false);
};
export const getVariables = (state: StoreState = getState()): VariableModel[] => {
return getFilteredVariables(variable => variable.uuid! !== EMPTY_UUID, state);
return getFilteredVariables(variable => variable.id! !== NEW_VARIABLE_ID, state);
};
export const getVariableClones = (state: StoreState = getState(), includeEmptyUuid = false): VariableModel[] => {
const variables = getFilteredVariables(
variable => (includeEmptyUuid ? true : variable.uuid !== EMPTY_UUID),
variable => (includeEmptyUuid ? true : variable.id! !== NEW_VARIABLE_ID),
state
).map(variable => cloneDeep(variable));
return variables.sort((s1, s2) => s1.index! - s2.index!);

View File

@@ -15,19 +15,20 @@ import {
storeNewVariable,
} from './sharedReducer';
import { QueryVariableModel, VariableHide } from '../../templating/variable';
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE, EMPTY_UUID, toVariablePayload } from './types';
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE, NEW_VARIABLE_ID, toVariablePayload } from './types';
import { variableAdapters } from '../adapters';
import { createQueryVariableAdapter } from '../query/adapter';
import { initialQueryVariableModelState } from '../query/reducer';
import { Deferred } from '../../../core/utils/deferred';
import { getVariableState, getVariableTestContext } from './helpers';
import { initialVariablesState, VariablesState } from './variablesReducer';
import { changeVariableNameSucceeded } from '../editor/reducer';
describe('sharedReducer', () => {
describe('when addVariable is dispatched', () => {
it('then state should be correct', () => {
const model = ({ name: 'name from model', type: 'type from model' } as unknown) as QueryVariableModel;
const payload = toVariablePayload({ uuid: '0', type: 'query' }, { global: true, index: 0, model });
const payload = toVariablePayload({ id: '0', type: 'query' }, { global: true, index: 0, model });
variableAdapters.set('query', createQueryVariableAdapter());
reducerTester<VariablesState>()
.givenReducer(sharedReducer, { ...initialVariablesState })
@@ -36,7 +37,7 @@ describe('sharedReducer', () => {
[0]: {
...initialQueryVariableModelState,
...model,
uuid: '0',
id: '0',
global: true,
index: 0,
},
@@ -47,13 +48,13 @@ describe('sharedReducer', () => {
describe('when removeVariable is dispatched and reIndex is true', () => {
it('then state should be correct', () => {
const initialState: VariablesState = getVariableState(3);
const payload = toVariablePayload({ uuid: '1', type: 'query' }, { reIndex: true });
const payload = toVariablePayload({ id: '1', type: 'query' }, { reIndex: true });
reducerTester<VariablesState>()
.givenReducer(sharedReducer, initialState)
.whenActionIsDispatched(removeVariable(payload))
.thenStateShouldEqual({
'0': {
uuid: '0',
id: '0',
type: 'query',
name: 'Name-0',
hide: VariableHide.dontHide,
@@ -62,7 +63,7 @@ describe('sharedReducer', () => {
skipUrlSync: false,
},
'2': {
uuid: '2',
id: '2',
type: 'query',
name: 'Name-2',
hide: VariableHide.dontHide,
@@ -77,13 +78,13 @@ describe('sharedReducer', () => {
describe('when removeVariable is dispatched and reIndex is false', () => {
it('then state should be correct', () => {
const initialState: VariablesState = getVariableState(3);
const payload = toVariablePayload({ uuid: '1', type: 'query' }, { reIndex: false });
const payload = toVariablePayload({ id: '1', type: 'query' }, { reIndex: false });
reducerTester<VariablesState>()
.givenReducer(sharedReducer, initialState)
.whenActionIsDispatched(removeVariable(payload))
.thenStateShouldEqual({
'0': {
uuid: '0',
id: '0',
type: 'query',
name: 'Name-0',
hide: VariableHide.dontHide,
@@ -92,7 +93,7 @@ describe('sharedReducer', () => {
skipUrlSync: false,
},
'2': {
uuid: '2',
id: '2',
type: 'query',
name: 'Name-2',
hide: VariableHide.dontHide,
@@ -108,13 +109,13 @@ describe('sharedReducer', () => {
it('then state should be correct', () => {
variableAdapters.set('query', createQueryVariableAdapter());
const initialState: VariablesState = getVariableState(3);
const payload = toVariablePayload({ uuid: '1', type: 'query' }, { newUuid: '11' });
const payload = toVariablePayload({ id: '1', type: 'query' }, { newId: '11' });
reducerTester<VariablesState>()
.givenReducer(sharedReducer, initialState)
.whenActionIsDispatched(duplicateVariable(payload))
.thenStateShouldEqual({
'0': {
uuid: '0',
id: '0',
type: 'query',
name: 'Name-0',
hide: VariableHide.dontHide,
@@ -123,7 +124,7 @@ describe('sharedReducer', () => {
skipUrlSync: false,
},
'1': {
uuid: '1',
id: '1',
type: 'query',
name: 'Name-1',
hide: VariableHide.dontHide,
@@ -132,7 +133,7 @@ describe('sharedReducer', () => {
skipUrlSync: false,
},
'2': {
uuid: '2',
id: '2',
type: 'query',
name: 'Name-2',
hide: VariableHide.dontHide,
@@ -142,7 +143,7 @@ describe('sharedReducer', () => {
},
'11': {
...initialQueryVariableModelState,
uuid: '11',
id: '11',
name: 'copy_of_Name-1',
index: 3,
label: 'Label-1',
@@ -154,13 +155,13 @@ describe('sharedReducer', () => {
describe('when changeVariableOrder is dispatched', () => {
it('then state should be correct', () => {
const initialState: VariablesState = getVariableState(3);
const payload = toVariablePayload({ uuid: '1', type: 'query' }, { fromIndex: 1, toIndex: 0 });
const payload = toVariablePayload({ id: '1', type: 'query' }, { fromIndex: 1, toIndex: 0 });
reducerTester<VariablesState>()
.givenReducer(sharedReducer, initialState)
.whenActionIsDispatched(changeVariableOrder(payload))
.thenStateShouldEqual({
'0': {
uuid: '0',
id: '0',
type: 'query',
name: 'Name-0',
hide: VariableHide.dontHide,
@@ -169,7 +170,7 @@ describe('sharedReducer', () => {
skipUrlSync: false,
},
'1': {
uuid: '1',
id: '1',
type: 'query',
name: 'Name-1',
hide: VariableHide.dontHide,
@@ -178,7 +179,7 @@ describe('sharedReducer', () => {
skipUrlSync: false,
},
'2': {
uuid: '2',
id: '2',
type: 'query',
name: 'Name-2',
hide: VariableHide.dontHide,
@@ -194,13 +195,13 @@ describe('sharedReducer', () => {
it('then state should be correct', () => {
variableAdapters.set('query', createQueryVariableAdapter());
const initialState: VariablesState = getVariableState(3, -1, true);
const payload = toVariablePayload({ uuid: '11', type: 'query' });
const payload = toVariablePayload({ id: '11', type: 'query' });
reducerTester<VariablesState>()
.givenReducer(sharedReducer, initialState)
.whenActionIsDispatched(storeNewVariable(payload))
.thenStateShouldEqual({
'0': {
uuid: '0',
id: '0',
type: 'query',
name: 'Name-0',
hide: VariableHide.dontHide,
@@ -209,7 +210,7 @@ describe('sharedReducer', () => {
skipUrlSync: false,
},
'1': {
uuid: '1',
id: '1',
type: 'query',
name: 'Name-1',
hide: VariableHide.dontHide,
@@ -218,7 +219,7 @@ describe('sharedReducer', () => {
skipUrlSync: false,
},
'2': {
uuid: '2',
id: '2',
type: 'query',
name: 'Name-2',
hide: VariableHide.dontHide,
@@ -226,21 +227,21 @@ describe('sharedReducer', () => {
label: 'Label-2',
skipUrlSync: false,
},
[EMPTY_UUID]: {
uuid: EMPTY_UUID,
[NEW_VARIABLE_ID]: {
id: NEW_VARIABLE_ID,
type: 'query',
name: `Name-${EMPTY_UUID}`,
name: `Name-${NEW_VARIABLE_ID}`,
hide: VariableHide.dontHide,
index: 3,
label: `Label-${EMPTY_UUID}`,
label: `Label-${NEW_VARIABLE_ID}`,
skipUrlSync: false,
},
[11]: {
...initialQueryVariableModelState,
uuid: '11',
name: `Name-${EMPTY_UUID}`,
id: '11',
name: `Name-${NEW_VARIABLE_ID}`,
index: 3,
label: `Label-${EMPTY_UUID}`,
label: `Label-${NEW_VARIABLE_ID}`,
},
});
});
@@ -257,7 +258,7 @@ describe('sharedReducer', () => {
],
});
const current = { text: ['A', 'B'], selected: true, value: ['A', 'B'] };
const payload = toVariablePayload({ uuid: '0', type: 'query' }, { option: current });
const payload = toVariablePayload({ id: '0', type: 'query' }, { option: current });
reducerTester<VariablesState>()
.givenReducer(sharedReducer, cloneDeep(initialState))
.whenActionIsDispatched(setCurrentVariableValue(payload))
@@ -287,7 +288,7 @@ describe('sharedReducer', () => {
],
});
const current = { text: 'A + B', selected: true, value: ['A', 'B'] };
const payload = toVariablePayload({ uuid: '0', type: 'query' }, { option: current });
const payload = toVariablePayload({ id: '0', type: 'query' }, { option: current });
reducerTester<VariablesState>()
.givenReducer(sharedReducer, cloneDeep(initialState))
.whenActionIsDispatched(setCurrentVariableValue(payload))
@@ -317,7 +318,7 @@ describe('sharedReducer', () => {
],
});
const current = { text: ALL_VARIABLE_TEXT, selected: true, value: [ALL_VARIABLE_VALUE] };
const payload = toVariablePayload({ uuid: '0', type: 'query' }, { option: current });
const payload = toVariablePayload({ id: '0', type: 'query' }, { option: current });
reducerTester<VariablesState>()
.givenReducer(sharedReducer, cloneDeep(initialState))
.whenActionIsDispatched(setCurrentVariableValue(payload))
@@ -340,7 +341,7 @@ describe('sharedReducer', () => {
it('then state should be correct', () => {
const adapter = createQueryVariableAdapter();
const { initialState } = getVariableTestContext(adapter, {});
const payload = toVariablePayload({ uuid: '0', type: 'query' });
const payload = toVariablePayload({ id: '0', type: 'query' });
reducerTester<VariablesState>()
.givenReducer(sharedReducer, cloneDeep(initialState))
.whenActionIsDispatched(addInitLock(payload))
@@ -369,7 +370,7 @@ describe('sharedReducer', () => {
} as unknown) as Deferred;
const adapter = createQueryVariableAdapter();
const { initialState } = getVariableTestContext(adapter, { initLock });
const payload = toVariablePayload({ uuid: '0', type: 'query' });
const payload = toVariablePayload({ id: '0', type: 'query' });
reducerTester<VariablesState>()
.givenReducer(sharedReducer, cloneDeep(initialState))
.whenActionIsDispatched(resolveInitLock(payload))
@@ -399,7 +400,7 @@ describe('sharedReducer', () => {
} as unknown) as Deferred;
const adapter = createQueryVariableAdapter();
const { initialState } = getVariableTestContext(adapter, { initLock });
const payload = toVariablePayload({ uuid: '0', type: 'query' });
const payload = toVariablePayload({ id: '0', type: 'query' });
reducerTester<VariablesState>()
.givenReducer(sharedReducer, cloneDeep(initialState))
.whenActionIsDispatched(removeInitLock(payload))
@@ -417,9 +418,9 @@ describe('sharedReducer', () => {
it('then state should be correct', () => {
const adapter = createQueryVariableAdapter();
const { initialState } = getVariableTestContext(adapter);
const propName = 'name';
const propValue = 'Updated name';
const payload = toVariablePayload({ uuid: '0', type: 'query' }, { propName, propValue });
const propName = 'label';
const propValue = 'Updated label';
const payload = toVariablePayload({ id: '0', type: 'query' }, { propName, propValue });
reducerTester<VariablesState>()
.givenReducer(sharedReducer, cloneDeep(initialState))
.whenActionIsDispatched(changeVariableProp(payload))
@@ -427,7 +428,26 @@ describe('sharedReducer', () => {
...initialState,
'0': {
...initialState[0],
name: 'Updated name',
label: 'Updated label',
},
});
});
});
describe('when changeVariableNameSucceeded is dispatched', () => {
it('then state should be correct', () => {
const adapter = createQueryVariableAdapter();
const { initialState } = getVariableTestContext(adapter);
const newName = 'A new name';
const payload = toVariablePayload({ id: '0', type: 'query' }, { newName });
reducerTester<VariablesState>()
.givenReducer(sharedReducer, cloneDeep(initialState))
.whenActionIsDispatched(changeVariableNameSucceeded(payload))
.thenStateShouldEqual({
...initialState,
'0': {
...initialState[0],
name: 'A new name',
},
});
});

View File

@@ -2,42 +2,41 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import cloneDeep from 'lodash/cloneDeep';
import { VariableModel, VariableOption, VariableType, VariableWithOptions } from '../../templating/variable';
import { AddVariable, ALL_VARIABLE_VALUE, EMPTY_UUID, getInstanceState, VariablePayload } from './types';
import { AddVariable, ALL_VARIABLE_VALUE, getInstanceState, NEW_VARIABLE_ID, VariablePayload } from './types';
import { variableAdapters } from '../adapters';
import { changeVariableNameSucceeded } from '../editor/reducer';
import { Deferred } from '../../../core/utils/deferred';
import { initialVariablesState, VariablesState } from './variablesReducer';
import { isQuery } from '../guard';
import { v4 } from 'uuid';
const sharedReducerSlice = createSlice({
name: 'templating/shared',
initialState: initialVariablesState,
reducers: {
addVariable: (state: VariablesState, action: PayloadAction<VariablePayload<AddVariable>>) => {
const uuid = action.payload.uuid ?? v4(); // for testing purposes we can call this with an uuid
state[uuid] = {
const id = action.payload.id ?? action.payload.data.model.name; // for testing purposes we can call this with an id
state[id] = {
...cloneDeep(variableAdapters.get(action.payload.type).initialState),
...action.payload.data.model,
};
state[uuid].uuid = uuid;
state[uuid].index = action.payload.data.index;
state[uuid].global = action.payload.data.global;
state[id].id = id;
state[id].index = action.payload.data.index;
state[id].global = action.payload.data.global;
},
addInitLock: (state: VariablesState, action: PayloadAction<VariablePayload>) => {
const instanceState = getInstanceState(state, action.payload.uuid!);
const instanceState = getInstanceState(state, action.payload.id!);
instanceState.initLock = new Deferred();
},
resolveInitLock: (state: VariablesState, action: PayloadAction<VariablePayload>) => {
const instanceState = getInstanceState(state, action.payload.uuid!);
const instanceState = getInstanceState(state, action.payload.id!);
instanceState.initLock?.resolve();
},
removeInitLock: (state: VariablesState, action: PayloadAction<VariablePayload>) => {
const instanceState = getInstanceState(state, action.payload.uuid!);
const instanceState = getInstanceState(state, action.payload.id!);
instanceState.initLock = null;
},
removeVariable: (state: VariablesState, action: PayloadAction<VariablePayload<{ reIndex: boolean }>>) => {
delete state[action.payload.uuid!];
delete state[action.payload.id!];
if (!action.payload.data.reIndex) {
return;
}
@@ -47,15 +46,15 @@ const sharedReducerSlice = createSlice({
variableStates[index].index = index;
}
},
duplicateVariable: (state: VariablesState, action: PayloadAction<VariablePayload<{ newUuid: string }>>) => {
const newUuid = action.payload.data?.newUuid ?? v4();
const original = cloneDeep<VariableModel>(state[action.payload.uuid]);
const index = Object.keys(state).length;
duplicateVariable: (state: VariablesState, action: PayloadAction<VariablePayload<{ newId: string }>>) => {
const original = cloneDeep<VariableModel>(state[action.payload.id]);
const name = `copy_of_${original.name}`;
state[newUuid] = {
const newId = action.payload.data?.newId ?? name;
const index = Object.keys(state).length;
state[newId] = {
...cloneDeep(variableAdapters.get(action.payload.type).initialState),
...original,
uuid: newUuid,
id: newId,
name,
index,
};
@@ -69,30 +68,30 @@ const sharedReducerSlice = createSlice({
const toVariable = variables.find(v => v.index === action.payload.data.toIndex);
if (fromVariable) {
state[fromVariable.uuid!].index = action.payload.data.toIndex;
state[fromVariable.id!].index = action.payload.data.toIndex;
}
if (toVariable) {
state[toVariable.uuid!].index = action.payload.data.fromIndex;
state[toVariable.id!].index = action.payload.data.fromIndex;
}
},
storeNewVariable: (state: VariablesState, action: PayloadAction<VariablePayload>) => {
const uuid = action.payload.uuid!;
const emptyVariable = cloneDeep<VariableModel>(state[EMPTY_UUID]);
state[uuid!] = {
const id = action.payload.id!;
const emptyVariable = cloneDeep<VariableModel>(state[NEW_VARIABLE_ID]);
state[id!] = {
...cloneDeep(variableAdapters.get(action.payload.type).initialState),
...emptyVariable,
uuid,
id,
index: emptyVariable.index,
};
},
changeVariableType: (state: VariablesState, action: PayloadAction<VariablePayload<{ newType: VariableType }>>) => {
const { uuid } = action.payload;
const { label, name, index } = state[uuid!];
const { id } = action.payload;
const { label, name, index } = state[id!];
state[uuid!] = {
state[id!] = {
...cloneDeep(variableAdapters.get(action.payload.data.newType).initialState),
uuid,
id: id,
label,
name,
index,
@@ -106,7 +105,7 @@ const sharedReducerSlice = createSlice({
return;
}
const instanceState = getInstanceState<VariableWithOptions>(state, action.payload.uuid);
const instanceState = getInstanceState<VariableWithOptions>(state, action.payload.id);
const current = { ...action.payload.data.option };
if (Array.isArray(current.text) && current.text.length > 0) {
@@ -150,14 +149,14 @@ const sharedReducerSlice = createSlice({
state: VariablesState,
action: PayloadAction<VariablePayload<{ propName: string; propValue: any }>>
) => {
const instanceState = getInstanceState(state, action.payload.uuid!);
const instanceState = getInstanceState(state, action.payload.id!);
(instanceState as Record<string, any>)[action.payload.data.propName] = action.payload.data.propValue;
},
},
extraReducers: builder =>
builder.addCase(changeVariableNameSucceeded, (state, action) => {
const instanceState = getInstanceState(state, action.payload.uuid);
instanceState.name = action.payload.data;
const instanceState = getInstanceState(state, action.payload.id);
instanceState.name = action.payload.data.newName;
}),
});

View File

@@ -1,19 +1,19 @@
import { VariableModel, VariableType } from '../../templating/variable';
import { VariablesState } from './variablesReducer';
export const EMPTY_UUID = '00000000-0000-0000-0000-000000000000';
export const NEW_VARIABLE_ID = '00000000-0000-0000-0000-000000000000';
export const ALL_VARIABLE_TEXT = 'All';
export const ALL_VARIABLE_VALUE = '$__all';
export const NONE_VARIABLE_TEXT = 'None';
export const NONE_VARIABLE_VALUE = '';
export const getInstanceState = <Model extends VariableModel = VariableModel>(state: VariablesState, uuid: string) => {
return state[uuid] as Model;
export const getInstanceState = <Model extends VariableModel = VariableModel>(state: VariablesState, id: string) => {
return state[id] as Model;
};
export interface VariableIdentifier {
type: VariableType;
uuid: string;
id: string;
}
export interface VariablePayload<T extends any = undefined> extends VariableIdentifier {
@@ -27,7 +27,7 @@ export interface AddVariable<T extends VariableModel = VariableModel> {
}
export const toVariableIdentifier = (variable: VariableModel): VariableIdentifier => {
return { type: variable.type, uuid: variable.uuid! };
return { type: variable.type, id: variable.id! };
};
export function toVariablePayload<T extends any = undefined>(
@@ -39,5 +39,5 @@ export function toVariablePayload<T extends any = undefined>(
obj: VariableIdentifier | VariableModel,
data?: T
): VariablePayload<T> {
return { type: obj.type, uuid: obj.uuid!, data: data as T };
return { type: obj.type, id: obj.id!, data: data as T };
}

View File

@@ -20,7 +20,7 @@ export const variablesReducer = (
}
const variables = globalVariables.reduce((allVariables, state) => {
allVariables[state.uuid!] = state;
allVariables[state.id!] = state;
return allVariables;
}, {} as Record<string, VariableModel>);

View File

@@ -23,7 +23,7 @@ describe('textbox actions', () => {
const variable: TextBoxVariableModel = {
type: 'textbox',
uuid: '0',
id: '0',
global: false,
current: {
value: '',

View File

@@ -8,7 +8,7 @@ import { toVariablePayload, VariableIdentifier } from '../state/types';
export const updateTextBoxVariableOptions = (identifier: VariableIdentifier): ThunkResult<void> => {
return async (dispatch, getState) => {
await dispatch(createTextBoxOptions(toVariablePayload(identifier)));
const variableInState = getVariable<TextBoxVariableModel>(identifier.uuid!, getState());
const variableInState = getVariable<TextBoxVariableModel>(identifier.id!, getState());
await variableAdapters.get(identifier.type).setValue(variableInState, variableInState.options[0], true);
};
};

View File

@@ -31,7 +31,7 @@ export const createTextBoxVariableAdapter = (): VariableAdapter<TextBoxVariableM
await dispatch(updateTextBoxVariableOptions(toVariableIdentifier(variable)));
},
getSaveModel: variable => {
const { index, uuid, initLock, global, ...rest } = cloneDeep(variable);
const { index, id, initLock, global, ...rest } = cloneDeep(variable);
return rest;
},
getValueForUrl: variable => {

View File

@@ -13,16 +13,16 @@ describe('textBoxVariableReducer', () => {
describe('when createTextBoxOptions is dispatched', () => {
it('then state should be correct', () => {
const query = 'ABC';
const uuid = '0';
const { initialState } = getVariableTestContext(adapter, { uuid, query });
const payload = toVariablePayload({ uuid: '0', type: 'textbox' });
const id = '0';
const { initialState } = getVariableTestContext(adapter, { id, query });
const payload = toVariablePayload({ id: '0', type: 'textbox' });
reducerTester<VariablesState>()
.givenReducer(textBoxVariableReducer, cloneDeep(initialState))
.whenActionIsDispatched(createTextBoxOptions(payload))
.thenStateShouldEqual({
[uuid]: {
...initialState[uuid],
[id]: {
...initialState[id],
options: [
{
text: query,
@@ -43,16 +43,16 @@ describe('textBoxVariableReducer', () => {
describe('when createTextBoxOptions is dispatched and query contains spaces', () => {
it('then state should be correct', () => {
const query = ' ABC ';
const uuid = '0';
const { initialState } = getVariableTestContext(adapter, { uuid, query });
const payload = toVariablePayload({ uuid: '0', type: 'textbox' });
const id = '0';
const { initialState } = getVariableTestContext(adapter, { id, query });
const payload = toVariablePayload({ id: '0', type: 'textbox' });
reducerTester<VariablesState>()
.givenReducer(textBoxVariableReducer, cloneDeep(initialState))
.whenActionIsDispatched(createTextBoxOptions(payload))
.thenStateShouldEqual({
[uuid]: {
...initialState[uuid],
[id]: {
...initialState[id],
options: [
{
text: query.trim(),

View File

@@ -1,11 +1,11 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { TextBoxVariableModel, VariableHide, VariableOption } from '../../templating/variable';
import { EMPTY_UUID, getInstanceState, VariablePayload } from '../state/types';
import { getInstanceState, NEW_VARIABLE_ID, VariablePayload } from '../state/types';
import { initialVariablesState, VariablesState } from '../state/variablesReducer';
export const initialTextBoxVariableModelState: TextBoxVariableModel = {
uuid: EMPTY_UUID,
id: NEW_VARIABLE_ID,
global: false,
index: -1,
type: 'textbox',
@@ -24,7 +24,7 @@ export const textBoxVariableSlice = createSlice({
initialState: initialVariablesState,
reducers: {
createTextBoxOptions: (state: VariablesState, action: PayloadAction<VariablePayload>) => {
const instanceState = getInstanceState<TextBoxVariableModel>(state, action.payload.uuid!);
const instanceState = getInstanceState<TextBoxVariableModel>(state, action.payload.id!);
instanceState.options = [
{ text: instanceState.query.trim(), value: instanceState.query.trim(), selected: false },
];

View File

@@ -1,8 +1,8 @@
import { AnyAction, configureStore, EnhancedStore, Reducer } from '@reduxjs/toolkit';
import { Dispatch, Middleware, MiddlewareAPI } from 'redux';
import thunk from 'redux-thunk';
import { AnyAction, configureStore, EnhancedStore, Reducer } from '@reduxjs/toolkit';
import { StoreState } from '../../../app/types';
import thunk from 'redux-thunk';
import { setStore } from '../../../app/store/store';
export interface ReduxTesterGiven<State> {
@@ -25,6 +25,7 @@ export interface ReduxTesterThen<State> {
thenDispatchedActionsPredicateShouldEqual: (
predicate: (dispatchedActions: AnyAction[]) => boolean
) => ReduxTesterWhen<State>;
thenNoActionsWhereDispatched: () => ReduxTesterWhen<State>;
}
export interface ReduxTesterArguments<State> {
@@ -117,12 +118,22 @@ export const reduxTester = <State>(args?: ReduxTesterArguments<State>): ReduxTes
return instance;
};
const thenNoActionsWhereDispatched = (): ReduxTesterWhen<State> => {
if (debug) {
console.log('Dispatched Actions', JSON.stringify(dispatchedActions, null, 2));
}
expect(dispatchedActions.length).toBe(0);
return instance;
};
const instance = {
givenRootReducer,
whenActionIsDispatched,
whenAsyncActionIsDispatched,
thenDispatchedActionsShouldEqual,
thenDispatchedActionsPredicateShouldEqual,
thenNoActionsWhereDispatched,
};
return instance;

View File

@@ -5928,11 +5928,6 @@
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e"
integrity sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ==
"@types/uuid@3.4.7":
version "3.4.7"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-3.4.7.tgz#51d42247473bc00e38cc8dfaf70d936842a36c03"
integrity sha512-C2j2FWgQkF1ru12SjZJyMaTPxs/f6n90+5G5qNakBxKXjTBc/YTSelHh4Pz1HUDwxFXD9WvpQhOGCDC+/Y4mIQ==
"@types/webpack-dev-server@*":
version "3.1.7"
resolved "https://registry.yarnpkg.com/@types/webpack-dev-server/-/webpack-dev-server-3.1.7.tgz#a3e7a20366e68bc9853c730b56e994634cb78dac"
@@ -11975,16 +11970,6 @@ find-cache-dir@^3.2.0:
make-dir "^3.0.0"
pkg-dir "^4.1.0"
find-parent-dir@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/find-parent-dir/-/find-parent-dir-0.3.0.tgz#33c44b429ab2b2f0646299c5f9f718f376ff8d54"
integrity sha1-M8RLQpqysvBkYpnF+fcY83b/jVQ=
find-npm-prefix@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/find-npm-prefix/-/find-npm-prefix-1.0.2.tgz#8d8ce2c78b3b4b9e66c8acc6a37c231eb841cfdf"
integrity sha512-KEftzJ+H90x6pcKtdXZEPsQse8/y/UnvzRKrOSQFprnrGaFuJ62fVkP34Iu2IYuMvyauCyoLTNkJZgrrGA2wkA==
find-replace@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/find-replace/-/find-replace-2.0.1.tgz#6d9683a7ca20f8f9aabeabad07e4e2580f528550"
@@ -17364,14 +17349,7 @@ npm-packlist@^1.1.6, npm-packlist@^1.4.4:
ignore-walk "^3.0.1"
npm-bundled "^1.0.1"
npm-path@^2.0.2:
version "2.0.4"
resolved "https://registry.yarnpkg.com/npm-path/-/npm-path-2.0.4.tgz#c641347a5ff9d6a09e4d9bce5580c4f505278e64"
integrity sha512-IFsj0R9C7ZdR5cP+ET342q77uSRdtWOlWpih5eC+lu29tIDbNEgDbzgVJ5UFvYHWhxDZ5TFkJafFioO0pPQjCw==
dependencies:
which "^1.2.10"
npm-pick-manifest@^3.0.0, npm-pick-manifest@^3.0.2:
npm-pick-manifest@^3.0.0:
version "3.0.2"
resolved "https://registry.yarnpkg.com/npm-pick-manifest/-/npm-pick-manifest-3.0.2.tgz#f4d9e5fd4be2153e5f4e5f9b7be8dc419a99abb7"
integrity sha512-wNprTNg+X5nf+tDi+hbjdHhM4bX+mKqv6XmPh7B5eG+QY9VARfQPfCEH013H5GqfNj6ee8Ij2fg8yk0mzps1Vw==
@@ -17394,15 +17372,6 @@ npm-run-path@^4.0.0:
dependencies:
path-key "^3.0.0"
npm-which@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/npm-which/-/npm-which-3.0.1.tgz#9225f26ec3a285c209cae67c3b11a6b4ab7140aa"
integrity sha1-kiXybsOihcIJyuZ8OxGmtKtxQKo=
dependencies:
commander "^2.9.0"
npm-path "^2.0.2"
which "^1.2.10"
"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0, npmlog@^4.0.1, npmlog@^4.0.2, npmlog@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
@@ -21777,7 +21746,7 @@ semver-regex@^2.0.0:
resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-2.0.0.tgz#a93c2c5844539a770233379107b38c7b4ac9d338"
integrity sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw==
"semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", "semver@^2.3.0 || 3.x || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1:
"semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1:
version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
@@ -22660,11 +22629,6 @@ strict-uri-encode@^1.0.0:
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
integrity sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=
strict-uri-encode@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546"
integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY=
string-argv@0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da"
@@ -24316,11 +24280,6 @@ utils-merge@1.0.1:
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
uuid@3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
uuid@^3.0.1, uuid@^3.1.0, uuid@^3.3.2:
version "3.3.3"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866"
@@ -25207,24 +25166,6 @@ yargs@6.6.0:
y18n "^3.2.1"
yargs-parser "^4.2.0"
yargs@^11.0.0:
version "11.1.1"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-11.1.1.tgz#5052efe3446a4df5ed669c995886cc0f13702766"
integrity sha512-PRU7gJrJaXv3q3yQZ/+/X6KBswZiaQ+zOmdprZcouPYtQgvNU35i+68M4b1ZHLZtYFT5QObFLV+ZkmJYcwKdiw==
dependencies:
cliui "^4.0.0"
decamelize "^1.1.1"
find-up "^2.1.0"
get-caller-file "^1.0.1"
os-locale "^3.1.0"
require-directory "^2.1.1"
require-main-filename "^1.0.1"
set-blocking "^2.0.0"
string-width "^2.0.0"
which-module "^2.0.0"
y18n "^3.2.1"
yargs-parser "^9.0.2"
yargs@^14.2.2:
version "14.2.2"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-14.2.2.tgz#2769564379009ff8597cdd38fba09da9b493c4b5"