diff --git a/public/app/features/templating/specs/variable_srv.test.ts b/public/app/features/templating/specs/variable_srv.test.ts index eac11d524c2..1dcaa242dfe 100644 --- a/public/app/features/templating/specs/variable_srv.test.ts +++ b/public/app/features/templating/specs/variable_srv.test.ts @@ -3,6 +3,7 @@ import { VariableSrv } from '../variable_srv'; import { DashboardModel } from '../../dashboard/state/DashboardModel'; import $q from 'q'; import { dateTime } from '@grafana/ui/src/utils/moment_wrapper'; +import { CustomVariable } from '../custom_variable'; describe('VariableSrv', function(this: any) { const ctx = { @@ -589,8 +590,55 @@ describe('VariableSrv', function(this: any) { expect(unknownSet).toEqual(false); }); }); + + describe('setOptionFromUrl', () => { + it('sets single value as string if not multi choice', async () => { + const [setValueMock, setFromUrl] = setupSetFromUrlTest(ctx); + await setFromUrl('one'); + expect(setValueMock).toHaveBeenCalledWith({ text: 'one', value: 'one' }); + }); + + it('sets single value as array if multi choice', async () => { + const [setValueMock, setFromUrl] = setupSetFromUrlTest(ctx, { multi: true }); + await setFromUrl('one'); + expect(setValueMock).toHaveBeenCalledWith({ text: 'one', value: ['one'] }); + }); + + it('sets both text and value as array if multiple values in url', async () => { + const [setValueMock, setFromUrl] = setupSetFromUrlTest(ctx, { multi: true }); + await setFromUrl(['one', 'two']); + expect(setValueMock).toHaveBeenCalledWith({ text: ['one', 'two'], value: ['one', 'two'] }); + }); + + it('sets text and value even if it does not match any option', async () => { + const [setValueMock, setFromUrl] = setupSetFromUrlTest(ctx); + await setFromUrl('none'); + expect(setValueMock).toHaveBeenCalledWith({ text: 'none', value: 'none' }); + }); + + it('sets text and value even if it does not match any option and it is array', async () => { + const [setValueMock, setFromUrl] = setupSetFromUrlTest(ctx); + await setFromUrl(['none', 'none2']); + expect(setValueMock).toHaveBeenCalledWith({ text: ['none', 'none2'], value: ['none', 'none2'] }); + }); + }); }); +function setupSetFromUrlTest(ctx, model = {}) { + const variableSrv = new VariableSrv($q, ctx.$location, ctx.$injector, ctx.templateSrv, ctx.timeSrv); + const finalModel = { + type: 'custom', + options: ['one', 'two', 'three'].map(v => ({ text: v, value: v })), + name: 'test', + ...model, + }; + const variable = new CustomVariable(finalModel, variableSrv); + // We are mocking the setValue here instead of just checking the final variable.current value because there is lots + // of stuff going when the setValue is called that is hard to mock out. + variable.setValue = jest.fn(); + return [variable.setValue, val => variableSrv.setOptionFromUrl(variable, val)]; +} + function getVarMockConstructor(variable, model, ctx) { switch (model.model.type) { case 'datasource': diff --git a/public/app/features/templating/variable_srv.ts b/public/app/features/templating/variable_srv.ts index b69ad3c3075..d33e100f276 100644 --- a/public/app/features/templating/variable_srv.ts +++ b/public/app/features/templating/variable_srv.ts @@ -214,7 +214,14 @@ export class VariableSrv { } } - setOptionFromUrl(variable, urlValue) { + /** + * Sets the current selected option (or options) based on the query params in the url. It is possible for values + * in the url to not match current options of the variable. In that case the variables current value will be still set + * to that value. + * @param variable Instance of Variable + * @param urlValue Value of the query parameter + */ + setOptionFromUrl(variable: any, urlValue: string | string[]): Promise { let promise = this.$q.when(); if (variable.refresh) { @@ -222,28 +229,41 @@ export class VariableSrv { } return promise.then(() => { + // Simple case. Value in url matches existing options text or value. let option: any = _.find(variable.options, op => { return op.text === urlValue || op.value === urlValue; }); - let defaultText = urlValue; - const defaultValue = urlValue; - - if (!option && _.isArray(urlValue)) { - defaultText = []; + // No luck either it is array or value does not exist in the variables options. + if (!option) { + let defaultText = urlValue; + const defaultValue = urlValue; + + if (_.isArray(urlValue)) { + // Multiple values in the url. We construct text as a list of texts from all matched options. + defaultText = urlValue.reduce((acc, item) => { + const t: any = _.find(variable.options, { value: item }); + if (t) { + acc.push(t.text); + } else { + acc.push(item); + } + + return acc; + }, []); + } - for (let n = 0; n < urlValue.length; n++) { - const t: any = _.find(variable.options, op => { - return op.value === urlValue[n]; - }); + // It is possible that we did not match the value to any existing option. In that case the url value will be + // used anyway for both text and value. + option = { text: defaultText, value: defaultValue }; + } - if (t) { - defaultText.push(t.text); - } - } + if (variable.multi) { + // In case variable is multiple choice, we cast to array to preserve the same behaviour as when selecting + // the option directly, which will return even single value in an array. + option = { ...option, value: _.castArray(option.value) }; } - option = option || { text: defaultText, value: defaultValue }; return variable.setValue(option); }); }