From 9af04a49eac2e9818f443193bcaadd96cc23fc04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Mon, 23 Mar 2020 13:45:08 +0100 Subject: [PATCH] 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 --- package.json | 2 - .../components/SubMenu/SubMenuItems.tsx | 2 +- .../app/features/templating/template_srv.ts | 3 +- public/app/features/templating/variable.ts | 2 +- .../features/variables/adhoc/actions.test.ts | 314 ++++++------------ .../app/features/variables/adhoc/actions.ts | 37 +-- .../app/features/variables/adhoc/adapter.ts | 4 +- .../variables/adhoc/picker/AdHocPicker.tsx | 16 +- .../features/variables/adhoc/reducer.test.ts | 94 +++--- .../app/features/variables/adhoc/reducer.ts | 18 +- .../variables/constant/actions.test.ts | 2 +- .../features/variables/constant/adapter.ts | 2 +- .../variables/constant/reducer.test.ts | 20 +- .../features/variables/constant/reducer.ts | 6 +- .../features/variables/custom/actions.test.ts | 2 +- .../app/features/variables/custom/adapter.ts | 2 +- .../features/variables/custom/reducer.test.ts | 30 +- .../app/features/variables/custom/reducer.ts | 12 +- .../variables/datasource/actions.test.ts | 20 +- .../features/variables/datasource/actions.ts | 2 +- .../features/variables/datasource/adapter.ts | 2 +- .../variables/datasource/reducer.test.ts | 2 +- .../features/variables/datasource/reducer.ts | 12 +- .../editor/VariableEditorContainer.tsx | 10 +- .../variables/editor/VariableEditorEditor.tsx | 10 +- .../app/features/variables/editor/actions.ts | 72 ++-- .../features/variables/editor/reducer.test.ts | 6 +- .../app/features/variables/editor/reducer.ts | 7 +- .../variables/interval/actions.test.ts | 26 +- .../features/variables/interval/actions.ts | 2 +- .../features/variables/interval/adapter.ts | 2 +- .../variables/interval/reducer.test.ts | 32 +- .../features/variables/interval/reducer.ts | 6 +- .../pickers/OptionsPicker/OptionsPicker.tsx | 2 +- .../pickers/OptionsPicker/actions.test.ts | 2 +- .../pickers/OptionsPicker/actions.ts | 20 +- .../pickers/OptionsPicker/reducer.test.ts | 10 +- .../pickers/OptionsPicker/reducer.ts | 6 +- .../features/variables/query/actions.test.ts | 6 +- .../app/features/variables/query/actions.ts | 10 +- .../app/features/variables/query/adapter.ts | 2 +- .../features/variables/query/reducer.test.ts | 14 +- .../app/features/variables/query/reducer.ts | 8 +- .../variables/shared/testing/builders.ts | 14 +- .../shared/testing/variableBuilder.ts | 6 +- .../features/variables/state/actions.test.ts | 270 +++++++++++---- .../app/features/variables/state/actions.ts | 12 +- .../app/features/variables/state/helpers.ts | 14 +- .../variables/state/processVariable.test.ts | 75 +++-- .../features/variables/state/reducers.test.ts | 24 +- .../app/features/variables/state/selectors.ts | 24 +- .../variables/state/sharedReducer.test.ts | 98 +++--- .../features/variables/state/sharedReducer.ts | 61 ++-- public/app/features/variables/state/types.ts | 12 +- .../variables/state/variablesReducer.ts | 2 +- .../variables/textbox/actions.test.ts | 2 +- .../app/features/variables/textbox/actions.ts | 2 +- .../app/features/variables/textbox/adapter.ts | 2 +- .../variables/textbox/reducer.test.ts | 20 +- .../app/features/variables/textbox/reducer.ts | 6 +- public/test/core/redux/reduxTester.ts | 15 +- yarn.lock | 63 +--- 62 files changed, 834 insertions(+), 747 deletions(-) diff --git a/package.json b/package.json index 263d09fcd27..d318c8249e8 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/public/app/features/dashboard/components/SubMenu/SubMenuItems.tsx b/public/app/features/dashboard/components/SubMenu/SubMenuItems.tsx index 78d035cbcd6..3dd44509aaa 100644 --- a/public/app/features/dashboard/components/SubMenu/SubMenuItems.tsx +++ b/public/app/features/dashboard/components/SubMenu/SubMenuItems.tsx @@ -22,7 +22,7 @@ export const SubMenuItems: FunctionComponent = ({ variables }) => { {visibleVariables.map(variable => { return (
diff --git a/public/app/features/templating/template_srv.ts b/public/app/features/templating/template_srv.ts index 38efa06d816..2a8e6f9d0df 100644 --- a/public/app/features/templating/template_srv.ts +++ b/public/app/features/templating/template_srv.ts @@ -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]; diff --git a/public/app/features/templating/variable.ts b/public/app/features/templating/variable.ts index 8aa7efbe7cb..d2ec672b483 100644 --- a/public/app/features/templating/variable.ts +++ b/public/app/features/templating/variable.ts @@ -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; diff --git a/public/app/features/variables/adhoc/actions.test.ts b/public/app/features/variables/adhoc/actions.test.ts index 633686975e5..f57e089e8e7 100644 --- a/public/app/features/variables/adhoc/actions.test.ts +++ b/public/app/features/variables/adhoc/actions.test.ts @@ -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() .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() .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() .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() .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() .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() .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() .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() .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 })) + ); }); }); }); diff --git a/public/app/features/variables/adhoc/actions.ts b/public/app/features/variables/adhoc/actions.ts index c9b826f5bf6..40be5f10ecd 100644 --- a/public/app/features/variables/adhoc/actions.ts +++ b/public/app/features/variables/adhoc/actions.ts @@ -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 => { +export const changeFilter = (id: string, update: AdHocVariabelFilterUpdate): ThunkResult => { 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 => { +export const removeFilter = (id: string, index: number): ThunkResult => { 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 => { +export const addFilter = (id: string, filter: AdHocVariableFilter): ThunkResult => { 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 => { +export const setFiltersFromUrl = (id: string, filters: AdHocVariableFilter[]): ThunkResult => { 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 => { ...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( diff --git a/public/app/features/variables/adhoc/adapter.ts b/public/app/features/variables/adhoc/adapter.ts index 16b1f753f7a..9849b5457d0 100644 --- a/public/app/features/variables/adhoc/adapter.ts +++ b/public/app/features/variables/adhoc/adapter.ts @@ -22,11 +22,11 @@ export const createAdHocVariableAdapter = (): VariableAdapter { 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 => { diff --git a/public/app/features/variables/adhoc/picker/AdHocPicker.tsx b/public/app/features/variables/adhoc/picker/AdHocPicker.tsx index a779321ee8d..6c68b915e3a 100644 --- a/public/app/features/variables/adhoc/picker/AdHocPicker.tsx +++ b/public/app/features/variables/adhoc/picker/AdHocPicker.tsx @@ -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 {} @@ -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 { onChange = (index: number, prop: string) => (key: SelectableValue) => { - 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 { }; 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 () => { diff --git a/public/app/features/variables/adhoc/reducer.test.ts b/public/app/features/variables/adhoc/reducer.test.ts index c06334dc606..0198b1fd47b 100644 --- a/public/app/features/variables/adhoc/reducer.test.ts +++ b/public/app/features/variables/adhoc/reducer.test.ts @@ -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() .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() .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() .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() .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() .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() .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() .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() .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() .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' }, diff --git a/public/app/features/variables/adhoc/reducer.ts b/public/app/features/variables/adhoc/reducer.ts index 54a09736289..2657fb3ea86 100644 --- a/public/app/features/variables/adhoc/reducer.ts +++ b/public/app/features/variables/adhoc/reducer.ts @@ -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>) => { - const instanceState = getInstanceState(state, action.payload.uuid); + const instanceState = getInstanceState(state, action.payload.id); instanceState.filters.push(action.payload.data); }, filterRemoved: (state: VariablesState, action: PayloadAction>) => { - const instanceState = getInstanceState(state, action.payload.uuid); + const instanceState = getInstanceState(state, action.payload.id); const index = action.payload.data; instanceState.filters.splice(index, 1); }, filterUpdated: (state: VariablesState, action: PayloadAction>) => { - const instanceState = getInstanceState(state, action.payload.uuid); + const instanceState = getInstanceState(state, action.payload.id); const { filter, index } = action.payload.data; instanceState.filters[index] = filter; }, filtersRestored: (state: VariablesState, action: PayloadAction>) => { - const instanceState = getInstanceState(state, action.payload.uuid); + const instanceState = getInstanceState(state, action.payload.id); instanceState.filters = action.payload.data; }, }, diff --git a/public/app/features/variables/constant/actions.test.ts b/public/app/features/variables/constant/actions.test.ts index 97985ea128f..019a3d0823e 100644 --- a/public/app/features/variables/constant/actions.test.ts +++ b/public/app/features/variables/constant/actions.test.ts @@ -23,7 +23,7 @@ describe('constant actions', () => { const variable: ConstantVariableModel = { type: 'constant', - uuid: '0', + id: '0', global: false, current: { value: '', diff --git a/public/app/features/variables/constant/adapter.ts b/public/app/features/variables/constant/adapter.ts index d930a24dda4..e31061360ed 100644 --- a/public/app/features/variables/constant/adapter.ts +++ b/public/app/features/variables/constant/adapter.ts @@ -30,7 +30,7 @@ export const createConstantVariableAdapter = (): VariableAdapter { - const { index, uuid, initLock, global, ...rest } = cloneDeep(variable); + const { index, id, initLock, global, ...rest } = cloneDeep(variable); return rest; }, getValueForUrl: variable => { diff --git a/public/app/features/variables/constant/reducer.test.ts b/public/app/features/variables/constant/reducer.test.ts index add0528cc4a..0e63cb8c1d8 100644 --- a/public/app/features/variables/constant/reducer.test.ts +++ b/public/app/features/variables/constant/reducer.test.ts @@ -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() .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() .givenReducer(constantVariableReducer, cloneDeep(initialState)) .whenActionIsDispatched(createConstantOptionsFromQuery(payload)) .thenStateShouldEqual({ - [uuid]: { - ...initialState[uuid], + [id]: { + ...initialState[id], options: [ { text: query.trim(), diff --git a/public/app/features/variables/constant/reducer.ts b/public/app/features/variables/constant/reducer.ts index 9d209982a52..5fe2c3772f9 100644 --- a/public/app/features/variables/constant/reducer.ts +++ b/public/app/features/variables/constant/reducer.ts @@ -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) => { - const instanceState = getInstanceState(state, action.payload.uuid); + const instanceState = getInstanceState(state, action.payload.id); instanceState.options = [ { text: instanceState.query.trim(), value: instanceState.query.trim(), selected: false }, ]; diff --git a/public/app/features/variables/custom/actions.test.ts b/public/app/features/variables/custom/actions.test.ts index 6a362b7fd0e..65424eb3d48 100644 --- a/public/app/features/variables/custom/actions.test.ts +++ b/public/app/features/variables/custom/actions.test.ts @@ -23,7 +23,7 @@ describe('custom actions', () => { const variable: CustomVariableModel = { type: 'custom', - uuid: '0', + id: '0', global: false, current: { value: '', diff --git a/public/app/features/variables/custom/adapter.ts b/public/app/features/variables/custom/adapter.ts index a4ee7bad3cd..50bd28ecbd6 100644 --- a/public/app/features/variables/custom/adapter.ts +++ b/public/app/features/variables/custom/adapter.ts @@ -30,7 +30,7 @@ export const createCustomVariableAdapter = (): VariableAdapter { - const { index, uuid, initLock, global, ...rest } = cloneDeep(variable); + const { index, id, initLock, global, ...rest } = cloneDeep(variable); return rest; }, getValueForUrl: variable => { diff --git a/public/app/features/variables/custom/reducer.test.ts b/public/app/features/variables/custom/reducer.test.ts index 42108042859..93e420a7be1 100644 --- a/public/app/features/variables/custom/reducer.test.ts +++ b/public/app/features/variables/custom/reducer.test.ts @@ -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() .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() .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() .givenReducer(customVariableReducer, cloneDeep(initialState)) .whenActionIsDispatched(createCustomOptionsFromQuery(payload)) .thenStateShouldEqual({ - [uuid]: { - ...initialState[uuid], + [id]: { + ...initialState[id], options: [ { text: ALL_VARIABLE_TEXT, diff --git a/public/app/features/variables/custom/reducer.ts b/public/app/features/variables/custom/reducer.ts index cffcd8d089d..d1c6185a724 100644 --- a/public/app/features/variables/custom/reducer.ts +++ b/public/app/features/variables/custom/reducer.ts @@ -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) => { - const instanceState = getInstanceState(state, action.payload.uuid); + const instanceState = getInstanceState(state, action.payload.id); const { includeAll, query } = instanceState; const match = query.match(/(?:\\,|[^,])+/g) ?? []; diff --git a/public/app/features/variables/datasource/actions.test.ts b/public/app/features/variables/datasource/actions.test.ts index 87039451b14..6d9d8a0996e 100644 --- a/public/app/features/variables/datasource/actions.test.ts +++ b/public/app/features/variables/datasource/actions.test.ts @@ -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 } } ) ) diff --git a/public/app/features/variables/datasource/actions.ts b/public/app/features/variables/datasource/actions.ts index 27e3534bd52..a8a31a03188 100644 --- a/public/app/features/variables/datasource/actions.ts +++ b/public/app/features/variables/datasource/actions.ts @@ -19,7 +19,7 @@ export const updateDataSourceVariableOptions = ( dependencies: DataSourceVariableActionDependencies = { getDatasourceSrv: getDatasourceSrv } ): ThunkResult => async (dispatch, getState) => { const sources = await dependencies.getDatasourceSrv().getMetricSources({ skipVariables: true }); - const variableInState = getVariable(identifier.uuid!, getState()); + const variableInState = getVariable(identifier.id!, getState()); let regex; if (variableInState.regex) { diff --git a/public/app/features/variables/datasource/adapter.ts b/public/app/features/variables/datasource/adapter.ts index 2991b137d92..d8b05bd8ee3 100644 --- a/public/app/features/variables/datasource/adapter.ts +++ b/public/app/features/variables/datasource/adapter.ts @@ -33,7 +33,7 @@ export const createDataSourceVariableAdapter = (): VariableAdapter { - const { index, uuid, initLock, global, ...rest } = cloneDeep(variable); + const { index, id, initLock, global, ...rest } = cloneDeep(variable); return { ...rest, options: [] }; }, getValueForUrl: variable => { diff --git a/public/app/features/variables/datasource/reducer.test.ts b/public/app/features/variables/datasource/reducer.test.ts index f27d3ce3ce5..ad300405e97 100644 --- a/public/app/features/variables/datasource/reducer.test.ts +++ b/public/app/features/variables/datasource/reducer.test.ts @@ -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(adapter, { query, includeAll }); - const payload = toVariablePayload({ uuid: '0', type: 'datasource' }, { sources, regex }); + const payload = toVariablePayload({ id: '0', type: 'datasource' }, { sources, regex }); reducerTester() .givenReducer(dataSourceVariableReducer, cloneDeep(initialState)) diff --git a/public/app/features/variables/datasource/reducer.ts b/public/app/features/variables/datasource/reducer.ts index 2ded552ef40..91130caeece 100644 --- a/public/app/features/variables/datasource/reducer.ts +++ b/public/app/features/variables/datasource/reducer.ts @@ -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(state, action.payload.uuid); + const instanceState = getInstanceState(state, action.payload.id); for (let i = 0; i < sources.length; i++) { const source = sources[i]; // must match on type diff --git a/public/app/features/variables/editor/VariableEditorContainer.tsx b/public/app/features/variables/editor/VariableEditorContainer.tsx index ca4d37f3e25..92555f1f0fe 100644 --- a/public/app/features/variables/editor/VariableEditorContainer.tsx +++ b/public/app/features/variables/editor/VariableEditorContainer.tsx @@ -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 { }; 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 { }; 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 (
@@ -72,7 +72,7 @@ class VariableEditorContainerUnconnected extends PureComponent { > Variables - {this.props.idInEditor === EMPTY_UUID && ( + {this.props.idInEditor === NEW_VARIABLE_ID && ( { New )} - {this.props.idInEditor && this.props.idInEditor !== EMPTY_UUID && ( + {this.props.idInEditor && this.props.idInEditor !== NEW_VARIABLE_ID && ( { 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 { 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 (
@@ -227,7 +227,7 @@ export class VariableEditorEditorUnConnected extends PureComponent { const mapStateToProps: MapStateToProps = (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 = { diff --git a/public/app/features/variables/editor/actions.ts b/public/app/features/variables/editor/actions.ts index 141a502ce04..4851fb17b71 100644 --- a/public/app/features/variables/editor/actions.ts +++ b/public/app/features/variables/editor/actions.ts @@ -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 => { return async dispatch => { - dispatch(variableEditorMounted({ name: getVariable(identifier.uuid!).name })); + dispatch(variableEditorMounted({ name: getVariable(identifier.id!).name })); }; }; export const variableEditorUnMount = (identifier: VariableIdentifier): ThunkResult => { 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 => { 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 => { 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 => { 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 => dispatch => { + dispatch(changeVariableNameSucceeded(toVariablePayload(identifier, { newName }))); +}; + +export const completeChangeVariableName = (identifier: VariableIdentifier, newName: string): ThunkResult => ( + 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 => (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(identifier, { global, model, index }) ) ); - dispatch(setIdInEditor({ id: identifier.uuid })); + dispatch(setIdInEditor({ id: identifier.id })); }; export const switchToEditMode = (identifier: VariableIdentifier): ThunkResult => dispatch => { - dispatch(setIdInEditor({ id: identifier.uuid })); + dispatch(setIdInEditor({ id: identifier.id })); }; export const switchToListMode = (): ThunkResult => dispatch => { diff --git a/public/app/features/variables/editor/reducer.test.ts b/public/app/features/variables/editor/reducer.test.ts index a956f3cd050..7609e1fd3c6 100644 --- a/public/app/features/variables/editor/reducer.test.ts +++ b/public/app/features/variables/editor/reducer.test.ts @@ -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() .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() .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() .givenReducer(variableEditorReducer, initialState) .whenActionIsDispatched(changeVariableNameSucceeded(payload)) diff --git a/public/app/features/variables/editor/reducer.ts b/public/app/features/variables/editor/reducer.ts index 6998cb216a6..d38a16f11f0 100644 --- a/public/app/features/variables/editor/reducer.ts +++ b/public/app/features/variables/editor/reducer.ts @@ -34,8 +34,11 @@ const variableEditorReducerSlice = createSlice({ variableEditorUnMounted: (state: VariableEditorState, action: PayloadAction) => { return initialVariableEditorState; }, - changeVariableNameSucceeded: (state: VariableEditorState, action: PayloadAction>) => { - state.name = action.payload.data; + changeVariableNameSucceeded: ( + state: VariableEditorState, + action: PayloadAction> + ) => { + state.name = action.payload.data.newName; delete state.errors['name']; state.isValid = Object.keys(state.errors).length === 0; }, diff --git a/public/app/features/variables/interval/actions.test.ts b/public/app/features/variables/interval/actions.test.ts index 006e9d4b14a..1f8a1719cb7 100644 --- a/public/app/features/variables/interval/actions.test.ts +++ b/public/app/features/variables/interval/actions.test.ts @@ -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) diff --git a/public/app/features/variables/interval/actions.ts b/public/app/features/variables/interval/actions.ts index 334d09afbf4..3888410e756 100644 --- a/public/app/features/variables/interval/actions.ts +++ b/public/app/features/variables/interval/actions.ts @@ -42,7 +42,7 @@ export const updateAutoValue = ( templateSrv: templateSrv, } ): ThunkResult => (dispatch, getState) => { - const variableInState = getVariable(identifier.uuid, getState()); + const variableInState = getVariable(identifier.id, getState()); if (variableInState.auto) { const res = dependencies.kbn.calculateInterval( dependencies.getTimeSrv().timeRange(), diff --git a/public/app/features/variables/interval/adapter.ts b/public/app/features/variables/interval/adapter.ts index dca4db2c9b9..9228fb272f8 100644 --- a/public/app/features/variables/interval/adapter.ts +++ b/public/app/features/variables/interval/adapter.ts @@ -32,7 +32,7 @@ export const createIntervalVariableAdapter = (): VariableAdapter { - const { index, uuid, initLock, global, ...rest } = cloneDeep(variable); + const { index, id, initLock, global, ...rest } = cloneDeep(variable); return rest; }, getValueForUrl: variable => { diff --git a/public/app/features/variables/interval/reducer.test.ts b/public/app/features/variables/interval/reducer.test.ts index 6d43aab0de3..64a669e6849 100644 --- a/public/app/features/variables/interval/reducer.test.ts +++ b/public/app/features/variables/interval/reducer.test.ts @@ -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(adapter, { uuid, query, auto }); - const payload = toVariablePayload({ uuid: '0', type: 'interval' }); + const { initialState } = getVariableTestContext(adapter, { id, query, auto }); + const payload = toVariablePayload({ id: '0', type: 'interval' }); reducerTester() .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(adapter, { uuid, query, auto }); - const payload = toVariablePayload({ uuid: '0', type: 'interval' }); + const { initialState } = getVariableTestContext(adapter, { id, query, auto }); + const payload = toVariablePayload({ id: '0', type: 'interval' }); reducerTester() .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(adapter, { uuid, query, auto }); - const payload = toVariablePayload({ uuid: '0', type: 'interval' }); + const { initialState } = getVariableTestContext(adapter, { id, query, auto }); + const payload = toVariablePayload({ id: '0', type: 'interval' }); reducerTester() .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(adapter, { uuid, query, auto }); - const payload = toVariablePayload({ uuid: '0', type: 'interval' }); + const { initialState } = getVariableTestContext(adapter, { id, query, auto }); + const payload = toVariablePayload({ id: '0', type: 'interval' }); reducerTester() .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: [ diff --git a/public/app/features/variables/interval/reducer.ts b/public/app/features/variables/interval/reducer.ts index 47830089691..9162a9af4b4 100644 --- a/public/app/features/variables/interval/reducer.ts +++ b/public/app/features/variables/interval/reducer.ts @@ -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) => { - const instanceState = getInstanceState(state, action.payload.uuid!); + const instanceState = getInstanceState(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 }; diff --git a/public/app/features/variables/pickers/OptionsPicker/OptionsPicker.tsx b/public/app/features/variables/pickers/OptionsPicker/OptionsPicker.tsx index 2242a9bc25c..f755ca7ced2 100644 --- a/public/app/features/variables/pickers/OptionsPicker/OptionsPicker.tsx +++ b/public/app/features/variables/pickers/OptionsPicker/OptionsPicker.tsx @@ -54,7 +54,7 @@ export class OptionsPickerUnconnected extends PureComponent { render() { const { variable, picker } = this.props; - const showOptions = picker.uuid === variable.uuid; + const showOptions = picker.id === variable.id; return (
diff --git a/public/app/features/variables/pickers/OptionsPicker/actions.test.ts b/public/app/features/variables/pickers/OptionsPicker/actions.test.ts index 023fd4f9e83..2ab8b2f40a8 100644 --- a/public/app/features/variables/pickers/OptionsPicker/actions.test.ts +++ b/public/app/features/variables/pickers/OptionsPicker/actions.test.ts @@ -397,7 +397,7 @@ describe('options picker actions', () => { function createVariable(extend?: Partial): QueryVariableModel { return { type: 'query', - uuid: '0', + id: '0', global: false, current: createOption(''), options: [], diff --git a/public/app/features/variables/pickers/OptionsPicker/actions.ts b/public/app/features/variables/pickers/OptionsPicker/actions.ts index 97346726b90..aed60b87abc 100644 --- a/public/app/features/variables/pickers/OptionsPicker/actions.ts +++ b/public/app/features/variables/pickers/OptionsPicker/actions.ts @@ -56,8 +56,8 @@ export const navigateOptions = (key: NavigationKey, clearOthers: boolean): Thunk export const filterOrSearchOptions = (searchQuery: string): ThunkResult => { return async (dispatch, getState) => { - const { uuid } = getState().templating.optionsPicker; - const { query, options } = getVariable(uuid!, getState()); + const { id } = getState().templating.optionsPicker; + const { query, options } = getVariable(id!, getState()); dispatch(updateSearchQuery(searchQuery)); if (containsSearchFilter(query)) { @@ -70,13 +70,13 @@ export const filterOrSearchOptions = (searchQuery: string): ThunkResult => export const commitChangesToVariable = (): ThunkResult => { return async (dispatch, getState) => { const picker = getState().templating.optionsPicker; - const existing = getVariable(picker.uuid, getState()); + const existing = getVariable(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(picker.uuid, getState()); + const updated = getVariable(picker.id, getState()); if (existing.current.text === updated.current.text) { return dispatch(hideOptions()); @@ -90,8 +90,8 @@ export const commitChangesToVariable = (): ThunkResult => { export const toggleOptionByHighlight = (clearOthers: boolean): ThunkResult => { return (dispatch, getState) => { - const { uuid, highlightIndex } = getState().templating.optionsPicker; - const variable = getVariable(uuid, getState()); + const { id, highlightIndex } = getState().templating.optionsPicker; + const variable = getVariable(id, getState()); const option = variable.options[highlightIndex]; dispatch(toggleOption({ option, forceSelect: false, clearOthers })); }; @@ -111,7 +111,7 @@ export const toggleAndFetchTag = (tag: VariableTag): ThunkResult => { const fetchTagValues = (tagText: string): ThunkResult> => { return async (dispatch, getState) => { const picker = getState().templating.optionsPicker; - const variable = getVariable(picker.uuid, getState()); + const variable = getVariable(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(uuid, getState()); + const { id } = getState().templating.optionsPicker; + const existing = getVariable(id, getState()); const adapter = variableAdapters.get(existing.type); await adapter.updateOptions(existing, searchQuery); - const updated = getVariable(uuid, getState()); + const updated = getVariable(id, getState()); dispatch(updateOptionsFromSearch(updated.options)); } catch (error) { console.error(error); diff --git a/public/app/features/variables/pickers/OptionsPicker/reducer.test.ts b/public/app/features/variables/pickers/OptionsPicker/reducer.test.ts index 4e4eaeadc89..19f94489400 100644 --- a/public/app/features/variables/pickers/OptionsPicker/reducer.test.ts +++ b/public/app/features/variables/pickers/OptionsPicker/reducer.test.ts @@ -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() .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() diff --git a/public/app/features/variables/pickers/OptionsPicker/reducer.ts b/public/app/features/variables/pickers/OptionsPicker/reducer.ts index 7d99df621fe..5f1fcdefab6 100644 --- a/public/app/features/variables/pickers/OptionsPicker/reducer.ts +++ b/public/app/features/variables/pickers/OptionsPicker/reducer.ts @@ -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)) { diff --git a/public/app/features/variables/query/actions.test.ts b/public/app/features/variables/query/actions.test.ts index 4b79aaef279..8133f8c630c 100644 --- a/public/app/features/variables/query/actions.test.ts +++ b/public/app/features/variables/query/actions.test.ts @@ -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 { return { type: 'query', - uuid: '0', + id: '0', global: false, current: createOption(''), options: [], diff --git a/public/app/features/variables/query/actions.ts b/public/app/features/variables/query/actions.ts index 3db647700ad..b9e4efa2254 100644 --- a/public/app/features/variables/query/actions.ts +++ b/public/app/features/variables/query/actions.ts @@ -20,9 +20,9 @@ export const updateQueryVariableOptions = ( searchFilter?: string ): ThunkResult => { return async (dispatch, getState) => { - const variableInState = getVariable(identifier.uuid!, getState()); + const variableInState = getVariable(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(identifier.uuid!, getState()); + const variable = getVariable(identifier.id!, getState()); if (!variable.datasource) { return; } @@ -101,7 +101,7 @@ export const changeQueryVariableQuery = ( query: any, definition: string ): ThunkResult => async (dispatch, getState) => { - const variableInState = getVariable(identifier.uuid!, getState()); + const variableInState = getVariable(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 })); diff --git a/public/app/features/variables/query/adapter.ts b/public/app/features/variables/query/adapter.ts index d6f9bf94b89..abba5da0025 100644 --- a/public/app/features/variables/query/adapter.ts +++ b/public/app/features/variables/query/adapter.ts @@ -31,7 +31,7 @@ export const createQueryVariableAdapter = (): VariableAdapter { - 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: [] }; diff --git a/public/app/features/variables/query/reducer.test.ts b/public/app/features/variables/query/reducer.test.ts index 94ba78b5a95..d8b0cad7801 100644 --- a/public/app/features/variables/query/reducer.test.ts +++ b/public/app/features/variables/query/reducer.test.ts @@ -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() .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() .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() .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() .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() .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() .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() .givenReducer(queryVariableReducer, cloneDeep(initialState)) .whenActionIsDispatched(updateVariableTags(payload)) diff --git a/public/app/features/variables/query/reducer.ts b/public/app/features/variables/query/reducer.ts index f8cad8227a7..7025c39eeb7 100644 --- a/public/app/features/variables/query/reducer.ts +++ b/public/app/features/variables/query/reducer.ts @@ -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>) => { const results = action.payload.data; - const instanceState = getInstanceState(state, action.payload.uuid); + const instanceState = getInstanceState(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>) => { - const instanceState = getInstanceState(state, action.payload.uuid); + const instanceState = getInstanceState(state, action.payload.id); const results = action.payload.data; const tags: VariableTag[] = []; for (let i = 0; i < results.length; i++) { diff --git a/public/app/features/variables/shared/testing/builders.ts b/public/app/features/variables/shared/testing/builders.ts index 92c57747d72..492e04049d4 100644 --- a/public/app/features/variables/shared/testing/builders.ts +++ b/public/app/features/variables/shared/testing/builders.ts @@ -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); diff --git a/public/app/features/variables/shared/testing/variableBuilder.ts b/public/app/features/variables/shared/testing/variableBuilder.ts index 1e69da0a76c..105d1b8aa58 100644 --- a/public/app/features/variables/shared/testing/variableBuilder.ts +++ b/public/app/features/variables/shared/testing/variableBuilder.ts @@ -5,7 +5,7 @@ export class VariableBuilder { 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 { return this; } - withUUID(uuid: string) { - this.variable.uuid = uuid; + withId(id: string) { + this.variable.id = id; return this; } diff --git a/public/app/features/variables/state/actions.test.ts b/public/app/features/variables/state/actions.test.ts index 5a038a10bd1..15cd0f6ff8f 100644 --- a/public/app/features/variables/state/actions.test.ts +++ b/public/app/features/variables/state/actions.test.ts @@ -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', + }) + ); + }); + }); }); diff --git a/public/app/features/variables/state/actions.ts b/public/app/features/variables/state/actions.ts index 50062f21808..2dfb3b26c13 100644 --- a/public/app/features/variables/state/actions.ts +++ b/public/app/features/variables/state/actions.ts @@ -88,7 +88,7 @@ export const processVariableDependencies = async (variable: VariableModel, state export const processVariable = (identifier: VariableIdentifier, queryParams: UrlQueryMap): ThunkResult => { 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 => { export const setOptionFromUrl = (identifier: VariableIdentifier, urlValue: UrlQueryValue): ThunkResult => { 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(variable.uuid!, getState()); + const variableFromState = getVariable(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 => { return async (dispatch, getState) => { - const variableInState = getVariable(identifier.uuid!, getState()); + const variableInState = getVariable(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 => { 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(variable.uuid!, getState()); + const updatedVariable = getVariable(variable.id!, getState()); if (angular.toJson(previousOptions) !== angular.toJson(updatedVariable.options)) { const dashboard = getState().dashboard.getModel(); dashboard?.templateVariableValueUpdated(); diff --git a/public/app/features/variables/state/helpers.ts b/public/app/features/variables/state/helpers.ts index 64c85a8e042..bbe00e57283 100644 --- a/public/app/features/variables/state/helpers.ts +++ b/public/app/features/variables/state/helpers.ts @@ -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 = ( ) => { const defaultVariable = { ...adapter.initialState, - uuid: '0', + id: '0', index: 0, name: '0', }; diff --git a/public/app/features/variables/state/processVariable.test.ts b/public/app/features/variables/state/processVariable.test.ts index ee2bfe1509b..481ad2eaf0c 100644 --- a/public/app/features/variables/state/processVariable.test.ts +++ b/public/app/features/variables/state/processVariable.test.ts @@ -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' })) ); }); }); diff --git a/public/app/features/variables/state/reducers.test.ts b/public/app/features/variables/state/reducers.test.ts index aa9dc686635..593e3137912 100644 --- a/public/app/features/variables/state/reducers.test.ts +++ b/public/app/features/variables/state/reducers.test.ts @@ -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('mockAction'); reducerTester() .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('mockAction'); reducerTester() .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, diff --git a/public/app/features/variables/state/selectors.ts b/public/app/features/variables/state/selectors.ts index 7fb77f743c0..3f879345da7 100644 --- a/public/app/features/variables/state/selectors.ts +++ b/public/app/features/variables/state/selectors.ts @@ -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 = ( - 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!); diff --git a/public/app/features/variables/state/sharedReducer.test.ts b/public/app/features/variables/state/sharedReducer.test.ts index 8e22d5dad21..dc85a898331 100644 --- a/public/app/features/variables/state/sharedReducer.test.ts +++ b/public/app/features/variables/state/sharedReducer.test.ts @@ -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() .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() .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() .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() .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() .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() .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() .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() .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() .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() .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() .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() .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() .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() + .givenReducer(sharedReducer, cloneDeep(initialState)) + .whenActionIsDispatched(changeVariableNameSucceeded(payload)) + .thenStateShouldEqual({ + ...initialState, + '0': { + ...initialState[0], + name: 'A new name', }, }); }); diff --git a/public/app/features/variables/state/sharedReducer.ts b/public/app/features/variables/state/sharedReducer.ts index 4764d70b00c..5ba332d17f7 100644 --- a/public/app/features/variables/state/sharedReducer.ts +++ b/public/app/features/variables/state/sharedReducer.ts @@ -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>) => { - 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) => { - const instanceState = getInstanceState(state, action.payload.uuid!); + const instanceState = getInstanceState(state, action.payload.id!); instanceState.initLock = new Deferred(); }, resolveInitLock: (state: VariablesState, action: PayloadAction) => { - const instanceState = getInstanceState(state, action.payload.uuid!); + const instanceState = getInstanceState(state, action.payload.id!); instanceState.initLock?.resolve(); }, removeInitLock: (state: VariablesState, action: PayloadAction) => { - const instanceState = getInstanceState(state, action.payload.uuid!); + const instanceState = getInstanceState(state, action.payload.id!); instanceState.initLock = null; }, removeVariable: (state: VariablesState, action: PayloadAction>) => { - 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>) => { - const newUuid = action.payload.data?.newUuid ?? v4(); - const original = cloneDeep(state[action.payload.uuid]); - const index = Object.keys(state).length; + duplicateVariable: (state: VariablesState, action: PayloadAction>) => { + const original = cloneDeep(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) => { - const uuid = action.payload.uuid!; - const emptyVariable = cloneDeep(state[EMPTY_UUID]); - state[uuid!] = { + const id = action.payload.id!; + const emptyVariable = cloneDeep(state[NEW_VARIABLE_ID]); + state[id!] = { ...cloneDeep(variableAdapters.get(action.payload.type).initialState), ...emptyVariable, - uuid, + id, index: emptyVariable.index, }; }, changeVariableType: (state: VariablesState, action: PayloadAction>) => { - 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(state, action.payload.uuid); + const instanceState = getInstanceState(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> ) => { - const instanceState = getInstanceState(state, action.payload.uuid!); + const instanceState = getInstanceState(state, action.payload.id!); (instanceState as Record)[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; }), }); diff --git a/public/app/features/variables/state/types.ts b/public/app/features/variables/state/types.ts index 3bad148b500..0841c2c4d37 100644 --- a/public/app/features/variables/state/types.ts +++ b/public/app/features/variables/state/types.ts @@ -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 = (state: VariablesState, uuid: string) => { - return state[uuid] as Model; +export const getInstanceState = (state: VariablesState, id: string) => { + return state[id] as Model; }; export interface VariableIdentifier { type: VariableType; - uuid: string; + id: string; } export interface VariablePayload extends VariableIdentifier { @@ -27,7 +27,7 @@ export interface AddVariable { } export const toVariableIdentifier = (variable: VariableModel): VariableIdentifier => { - return { type: variable.type, uuid: variable.uuid! }; + return { type: variable.type, id: variable.id! }; }; export function toVariablePayload( @@ -39,5 +39,5 @@ export function toVariablePayload( obj: VariableIdentifier | VariableModel, data?: T ): VariablePayload { - return { type: obj.type, uuid: obj.uuid!, data: data as T }; + return { type: obj.type, id: obj.id!, data: data as T }; } diff --git a/public/app/features/variables/state/variablesReducer.ts b/public/app/features/variables/state/variablesReducer.ts index d1625ebf4c5..a2a6eead163 100644 --- a/public/app/features/variables/state/variablesReducer.ts +++ b/public/app/features/variables/state/variablesReducer.ts @@ -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); diff --git a/public/app/features/variables/textbox/actions.test.ts b/public/app/features/variables/textbox/actions.test.ts index 68139e62b67..e7465953195 100644 --- a/public/app/features/variables/textbox/actions.test.ts +++ b/public/app/features/variables/textbox/actions.test.ts @@ -23,7 +23,7 @@ describe('textbox actions', () => { const variable: TextBoxVariableModel = { type: 'textbox', - uuid: '0', + id: '0', global: false, current: { value: '', diff --git a/public/app/features/variables/textbox/actions.ts b/public/app/features/variables/textbox/actions.ts index b7141f0f803..a1639a5759a 100644 --- a/public/app/features/variables/textbox/actions.ts +++ b/public/app/features/variables/textbox/actions.ts @@ -8,7 +8,7 @@ import { toVariablePayload, VariableIdentifier } from '../state/types'; export const updateTextBoxVariableOptions = (identifier: VariableIdentifier): ThunkResult => { return async (dispatch, getState) => { await dispatch(createTextBoxOptions(toVariablePayload(identifier))); - const variableInState = getVariable(identifier.uuid!, getState()); + const variableInState = getVariable(identifier.id!, getState()); await variableAdapters.get(identifier.type).setValue(variableInState, variableInState.options[0], true); }; }; diff --git a/public/app/features/variables/textbox/adapter.ts b/public/app/features/variables/textbox/adapter.ts index 62388de283f..66d2f998b9c 100644 --- a/public/app/features/variables/textbox/adapter.ts +++ b/public/app/features/variables/textbox/adapter.ts @@ -31,7 +31,7 @@ export const createTextBoxVariableAdapter = (): VariableAdapter { - const { index, uuid, initLock, global, ...rest } = cloneDeep(variable); + const { index, id, initLock, global, ...rest } = cloneDeep(variable); return rest; }, getValueForUrl: variable => { diff --git a/public/app/features/variables/textbox/reducer.test.ts b/public/app/features/variables/textbox/reducer.test.ts index b253a62e0a4..e64a35c22a2 100644 --- a/public/app/features/variables/textbox/reducer.test.ts +++ b/public/app/features/variables/textbox/reducer.test.ts @@ -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() .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() .givenReducer(textBoxVariableReducer, cloneDeep(initialState)) .whenActionIsDispatched(createTextBoxOptions(payload)) .thenStateShouldEqual({ - [uuid]: { - ...initialState[uuid], + [id]: { + ...initialState[id], options: [ { text: query.trim(), diff --git a/public/app/features/variables/textbox/reducer.ts b/public/app/features/variables/textbox/reducer.ts index 247fa753485..f6702c7a769 100644 --- a/public/app/features/variables/textbox/reducer.ts +++ b/public/app/features/variables/textbox/reducer.ts @@ -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) => { - const instanceState = getInstanceState(state, action.payload.uuid!); + const instanceState = getInstanceState(state, action.payload.id!); instanceState.options = [ { text: instanceState.query.trim(), value: instanceState.query.trim(), selected: false }, ]; diff --git a/public/test/core/redux/reduxTester.ts b/public/test/core/redux/reduxTester.ts index 3184d283525..1d2c664ea41 100644 --- a/public/test/core/redux/reduxTester.ts +++ b/public/test/core/redux/reduxTester.ts @@ -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 { @@ -25,6 +25,7 @@ export interface ReduxTesterThen { thenDispatchedActionsPredicateShouldEqual: ( predicate: (dispatchedActions: AnyAction[]) => boolean ) => ReduxTesterWhen; + thenNoActionsWhereDispatched: () => ReduxTesterWhen; } export interface ReduxTesterArguments { @@ -117,12 +118,22 @@ export const reduxTester = (args?: ReduxTesterArguments): ReduxTes return instance; }; + const thenNoActionsWhereDispatched = (): ReduxTesterWhen => { + 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; diff --git a/yarn.lock b/yarn.lock index a80672acef9..8938d76305e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"