Templating: removes old Angular variable system and featureToggle (#24779)

* Chore: initial commit

* Tests: fixes MetricsQueryEditor.test.tsx

* Tests: fixes cloudwatch/specs/datasource.test.ts

* Tests: fixes stackdriver/specs/datasource.test.ts

* Tests: remove refrences to CustomVariable

* Refactor: moves DefaultVariableQueryEditor

* Refactor: moves utils

* Refactor: moves types

* Refactor: removes variableSrv

* Refactor: removes feature toggle newVariables

* Refactor: removes valueSelectDropDown

* Chore: removes GeneralTabCtrl

* Chore: migrates RowOptions

* Refactor: adds RowOptionsButton

* Refactor: makes the interface more explicit

* Refactor: small changes

* Refactor: changed type as it can be any variable type

* Tests: fixes broken test

* Refactor: changes after PR comments

* Refactor: adds loading state and call to onChange in componentDidMount
pull/25364/head
Hugo Häggmark 6 years ago committed by GitHub
parent 6b4d1dceb0
commit 00a9af00fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 30
      devenv/dev-dashboards-without-uid/panel_tests_graph.json
  2. 1
      packages/grafana-data/src/types/config.ts
  3. 5
      packages/grafana-e2e/cypress/fixtures/example.json
  4. 9
      packages/grafana-runtime/src/config.ts
  5. 4
      public/app/core/core.ts
  6. 356
      public/app/core/directives/value_select_dropdown.ts
  7. 9
      public/app/core/services/bridge_srv.ts
  8. 270
      public/app/core/specs/value_select_dropdown.test.ts
  9. 182
      public/app/features/dashboard/components/AdHocFilters/AdHocFiltersCtrl.ts
  10. 1
      public/app/features/dashboard/components/AdHocFilters/index.ts
  11. 10
      public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts
  12. 12
      public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts
  13. 8
      public/app/features/dashboard/components/DashboardRow/DashboardRow.test.tsx
  14. 25
      public/app/features/dashboard/components/DashboardRow/DashboardRow.tsx
  15. 2
      public/app/features/dashboard/components/DashboardSettings/SettingsCtrl.ts
  16. 6
      public/app/features/dashboard/components/DashboardSettings/template.html
  17. 2
      public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx
  18. 36
      public/app/features/dashboard/components/PanelEditor/PanelOptionsTab.tsx
  19. 40
      public/app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect.tsx
  20. 37
      public/app/features/dashboard/components/RowOptions/RowOptionsButton.tsx
  21. 39
      public/app/features/dashboard/components/RowOptions/RowOptionsCtrl.ts
  22. 46
      public/app/features/dashboard/components/RowOptions/RowOptionsForm.tsx
  23. 30
      public/app/features/dashboard/components/RowOptions/RowOptionsModal.tsx
  24. 1
      public/app/features/dashboard/components/RowOptions/index.ts
  25. 30
      public/app/features/dashboard/components/RowOptions/template.html
  26. 12
      public/app/features/dashboard/components/ShareModal/ShareSnapshot.tsx
  27. 2
      public/app/features/dashboard/components/SubMenu/SubMenu.tsx
  28. 2
      public/app/features/dashboard/components/SubMenu/SubMenuItems.tsx
  29. 60
      public/app/features/dashboard/components/SubMenu/template.html
  30. 5
      public/app/features/dashboard/containers/DashboardPage.tsx
  31. 4
      public/app/features/dashboard/index.ts
  32. 2
      public/app/features/dashboard/services/ChangeTracker.ts
  33. 4
      public/app/features/dashboard/state/DashboardMigrator.ts
  34. 67
      public/app/features/dashboard/state/DashboardModel.ts
  35. 28
      public/app/features/dashboard/state/initDashboard.test.ts
  36. 4
      public/app/features/dashboard/state/initDashboard.ts
  37. 46
      public/app/features/panel/GeneralTabCtrl.ts
  38. 1
      public/app/features/panel/all.ts
  39. 49
      public/app/features/panel/partials/general_tab.html
  40. 59
      public/app/features/panel/repeat_option.ts
  41. 2
      public/app/features/plugins/datasource_srv.ts
  42. 2
      public/app/features/plugins/variableQueryEditorLoader.tsx
  43. 71
      public/app/features/templating/TextBoxVariable.ts
  44. 101
      public/app/features/templating/adhoc_variable.ts
  45. 21
      public/app/features/templating/all.ts
  46. 70
      public/app/features/templating/constant_variable.ts
  47. 93
      public/app/features/templating/custom_variable.ts
  48. 133
      public/app/features/templating/datasource_variable.ts
  49. 247
      public/app/features/templating/editor_ctrl.ts
  50. 117
      public/app/features/templating/interval_variable.ts
  51. 526
      public/app/features/templating/partials/editor.html
  52. 254
      public/app/features/templating/query_variable.ts
  53. 36
      public/app/features/templating/specs/adhoc_variable.test.ts
  54. 42
      public/app/features/templating/specs/editor_ctrl.test.ts
  55. 133
      public/app/features/templating/specs/query_variable.test.ts
  56. 609
      public/app/features/templating/specs/template_srv.test.ts
  57. 261
      public/app/features/templating/specs/utils.test.ts
  58. 663
      public/app/features/templating/specs/variable_srv.test.ts
  59. 275
      public/app/features/templating/specs/variable_srv_init.test.ts
  60. 21
      public/app/features/templating/template_srv.ts
  61. 427
      public/app/features/templating/variable_srv.ts
  62. 2
      public/app/features/variables/adapters.ts
  63. 2
      public/app/features/variables/adhoc/AdHocVariableEditor.tsx
  64. 6
      public/app/features/variables/adhoc/actions.test.ts
  65. 6
      public/app/features/variables/adhoc/actions.ts
  66. 4
      public/app/features/variables/adhoc/adapter.ts
  67. 4
      public/app/features/variables/adhoc/picker/AdHocFilterBuilder.tsx
  68. 8
      public/app/features/variables/adhoc/picker/AdHocPicker.tsx
  69. 2
      public/app/features/variables/adhoc/reducer.test.ts
  70. 2
      public/app/features/variables/adhoc/reducer.ts
  71. 2
      public/app/features/variables/adhoc/urlParser.test.ts
  72. 2
      public/app/features/variables/adhoc/urlParser.ts
  73. 2
      public/app/features/variables/constant/ConstantVariableEditor.tsx
  74. 4
      public/app/features/variables/constant/actions.test.ts
  75. 2
      public/app/features/variables/constant/adapter.ts
  76. 2
      public/app/features/variables/constant/reducer.test.ts
  77. 2
      public/app/features/variables/constant/reducer.ts
  78. 2
      public/app/features/variables/custom/CustomVariableEditor.tsx
  79. 4
      public/app/features/variables/custom/actions.test.ts
  80. 2
      public/app/features/variables/custom/adapter.ts
  81. 2
      public/app/features/variables/custom/reducer.test.ts
  82. 2
      public/app/features/variables/custom/reducer.ts
  83. 2
      public/app/features/variables/datasource/DataSourceVariableEditor.tsx
  84. 4
      public/app/features/variables/datasource/actions.ts
  85. 4
      public/app/features/variables/datasource/adapter.ts
  86. 2
      public/app/features/variables/datasource/reducer.test.ts
  87. 2
      public/app/features/variables/datasource/reducer.ts
  88. 0
      public/app/features/variables/editor/DefaultVariableQueryEditor.tsx
  89. 2
      public/app/features/variables/editor/SelectionOptionsEditor.tsx
  90. 2
      public/app/features/variables/editor/VariableEditorContainer.tsx
  91. 2
      public/app/features/variables/editor/VariableEditorEditor.tsx
  92. 4
      public/app/features/variables/editor/VariableEditorList.tsx
  93. 2
      public/app/features/variables/editor/VariableValuesPreview.tsx
  94. 8
      public/app/features/variables/editor/actions.ts
  95. 2
      public/app/features/variables/editor/types.ts
  96. 12
      public/app/features/variables/guard.ts
  97. 3
      public/app/features/variables/interval/IntervalVariableEditor.tsx
  98. 2
      public/app/features/variables/interval/actions.ts
  99. 2
      public/app/features/variables/interval/adapter.ts
  100. 2
      public/app/features/variables/interval/reducer.test.ts
  101. Some files were not shown because too many files have changed in this diff Show More

@ -1632,10 +1632,7 @@
"revision": 8,
"schemaVersion": 16,
"style": "dark",
"tags": [
"gdev",
"panel-tests"
],
"tags": ["gdev", "panel-tests"],
"templating": {
"list": []
},
@ -1644,29 +1641,8 @@
"to": "now"
},
"timepicker": {
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
]
"refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"],
"time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"]
},
"timezone": "browser",
"title": "Panel Tests - Graph",

@ -38,7 +38,6 @@ export interface FeatureToggles {
* Available only in Grafana Enterprise
*/
meta: boolean;
newVariables: boolean;
}
/**

@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

@ -1,14 +1,14 @@
import merge from 'lodash/merge';
import { getTheme } from '@grafana/ui';
import {
BuildInfo,
DataSourceInstanceSettings,
FeatureToggles,
GrafanaConfig,
GrafanaTheme,
GrafanaThemeType,
PanelPluginMeta,
GrafanaConfig,
LicenseInfo,
BuildInfo,
FeatureToggles,
PanelPluginMeta,
} from '@grafana/data';
export class GrafanaBootConfig implements GrafanaConfig {
@ -52,7 +52,6 @@ export class GrafanaBootConfig implements GrafanaConfig {
expressions: false,
newEdit: false,
meta: false,
newVariables: true,
};
licenseInfo: LicenseInfo = {} as LicenseInfo;
rendererAvailable = false;

@ -4,7 +4,6 @@ import './directives/metric_segment';
import './directives/misc';
import './directives/ng_model_on_blur';
import './directives/tags';
import './directives/value_select_dropdown';
import './directives/rebuild_on_change';
import './directives/give_focus';
import './directives/diff-view';
@ -39,8 +38,7 @@ import { NavModelSrv } from './nav_model_srv';
import { geminiScrollbar } from './components/scroll/scroll';
import { profiler } from './profiler';
import { registerAngularDirectives } from './angular_wrappers';
import { updateLegendValues } from './time_series2';
import TimeSeries from './time_series2';
import TimeSeries, { updateLegendValues } from './time_series2';
import { NavModel } from '@grafana/data';
export {

@ -1,356 +0,0 @@
import angular, { IScope } from 'angular';
import debounce from 'lodash/debounce';
import each from 'lodash/each';
import filter from 'lodash/filter';
import find from 'lodash/find';
import indexOf from 'lodash/indexOf';
import map from 'lodash/map';
import { selectors } from '@grafana/e2e-selectors';
import coreModule from '../core_module';
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
import { containsSearchFilter } from '../../features/templating/utils';
export class ValueSelectDropdownCtrl {
dropdownVisible: any;
highlightIndex: any;
linkText: any;
oldVariableText: any;
options: any;
search: any;
selectedTags: any;
selectedValues: any;
tags: any;
variable: any;
hide: any;
onUpdated: any;
queryHasSearchFilter: boolean;
debouncedQueryChanged: Function;
selectors: typeof selectors.pages.Dashboard.SubMenu;
/** @ngInject */
constructor(private $scope: IScope) {
this.queryHasSearchFilter = this.variable ? containsSearchFilter(this.variable.query) : false;
this.debouncedQueryChanged = debounce(this.queryChanged.bind(this), 200);
this.selectors = selectors.pages.Dashboard.SubMenu;
}
show() {
this.oldVariableText = this.variable.current.text;
this.highlightIndex = -1;
this.options = this.variable.options;
this.selectedValues = filter(this.options, { selected: true });
this.tags = map(this.variable.tags, value => {
let tag = { text: value, selected: false };
each(this.variable.current.tags, tagObj => {
if (tagObj.text === value) {
tag = tagObj;
}
});
return tag;
});
// new behaviour, if this is a query that uses searchfilter it might be a nicer
// user experience to show the last typed search query in the input field
const query = this.queryHasSearchFilter && this.search && this.search.query ? this.search.query : '';
this.search = {
query,
options: this.options.slice(0, Math.min(this.options.length, 1000)),
};
this.dropdownVisible = true;
}
updateLinkText() {
const current = this.variable.current;
if (current.tags && current.tags.length) {
// filer out values that are in selected tags
const selectedAndNotInTag = filter(this.variable.options, option => {
if (!option.selected) {
return false;
}
for (let i = 0; i < current.tags.length; i++) {
const tag = current.tags[i];
if (indexOf(tag.values, option.value) !== -1) {
return false;
}
}
return true;
});
// convert values to text
const currentTexts = map(selectedAndNotInTag, 'text');
// join texts
this.linkText = currentTexts.join(' + ');
if (this.linkText.length > 0) {
this.linkText += ' + ';
}
} else {
this.linkText = this.variable.current.text;
}
}
clearSelections() {
this.selectedValues = filter(this.options, { selected: true });
if (this.selectedValues.length) {
each(this.options, option => {
option.selected = false;
});
} else {
each(this.search.options, option => {
option.selected = true;
});
}
this.selectionsChanged(false);
}
selectTag(tag: any) {
tag.selected = !tag.selected;
let tagValuesPromise;
if (!tag.values) {
tagValuesPromise = this.variable.getValuesForTag(tag.text);
} else {
tagValuesPromise = Promise.resolve(tag.values);
}
return tagValuesPromise.then((values: any) => {
tag.values = values;
tag.valuesText = values.join(' + ');
each(this.options, option => {
if (indexOf(tag.values, option.value) !== -1) {
option.selected = tag.selected;
}
});
this.selectionsChanged(false);
});
}
keyDown(evt: any) {
if (evt.keyCode === 27) {
this.hide();
}
if (evt.keyCode === 40) {
this.moveHighlight(1);
}
if (evt.keyCode === 38) {
this.moveHighlight(-1);
}
if (evt.keyCode === 13) {
if (this.search.options.length === 0) {
this.commitChanges();
} else {
this.selectValue(this.search.options[this.highlightIndex], {}, true);
}
}
if (evt.keyCode === 32) {
this.selectValue(this.search.options[this.highlightIndex], {}, false);
}
}
moveHighlight(direction: number) {
this.highlightIndex = (this.highlightIndex + direction) % this.search.options.length;
}
selectValue(option: any, event: any, commitChange?: boolean) {
if (!option) {
return;
}
option.selected = this.variable.multi ? !option.selected : true;
commitChange = commitChange || false;
const setAllExceptCurrentTo = (newValue: any) => {
each(this.options, other => {
if (option !== other) {
other.selected = newValue;
}
});
};
// commit action (enter key), should not deselect it
if (commitChange) {
option.selected = true;
}
if (option.text === 'All') {
// always clear search query if all is marked
this.search.query = '';
setAllExceptCurrentTo(false);
commitChange = true;
} else if (!this.variable.multi) {
setAllExceptCurrentTo(false);
commitChange = true;
} else if (event.ctrlKey || event.metaKey || event.shiftKey) {
commitChange = true;
setAllExceptCurrentTo(false);
}
this.selectionsChanged(commitChange);
}
selectionsChanged(commitChange: boolean) {
this.selectedValues = filter(this.options, { selected: true });
if (this.selectedValues.length > 1) {
if (this.selectedValues[0].text === 'All') {
this.selectedValues[0].selected = false;
this.selectedValues = this.selectedValues.slice(1, this.selectedValues.length);
}
}
// validate selected tags
each(this.tags, tag => {
if (tag.selected) {
each(tag.values, value => {
if (!find(this.selectedValues, { value: value })) {
tag.selected = false;
}
});
}
});
this.selectedTags = filter(this.tags, { selected: true });
this.variable.current.value = map(this.selectedValues, 'value');
this.variable.current.text = map(this.selectedValues, 'text').join(' + ');
this.variable.current.tags = this.selectedTags;
if (!this.variable.multi) {
this.variable.current.value = this.selectedValues[0].value;
}
if (commitChange) {
this.commitChanges();
}
}
commitChanges() {
// if we have a search query and no options use that
if (this.search.options.length === 0 && this.search.query.length > 0) {
this.variable.current = { text: this.search.query, value: this.search.query };
} else if (this.selectedValues.length === 0) {
// make sure one option is selected
this.options[0].selected = true;
this.selectionsChanged(false);
}
this.dropdownVisible = false;
this.updateLinkText();
if (this.queryHasSearchFilter) {
this.updateLazyLoadedOptions();
}
if (this.variable.current.text !== this.oldVariableText) {
this.onUpdated();
}
}
async queryChanged() {
if (this.queryHasSearchFilter) {
await this.updateLazyLoadedOptions();
return;
}
const options = filter(this.options, option => {
return option.text.toLowerCase().indexOf(this.search.query.toLowerCase()) !== -1;
});
this.updateUIBoundOptions(this.$scope, options);
}
init() {
this.selectedTags = this.variable.current.tags || [];
this.updateLinkText();
}
async updateLazyLoadedOptions() {
this.options = await this.lazyLoadOptions(this.search.query);
this.updateUIBoundOptions(this.$scope, this.options);
}
async lazyLoadOptions(query: string): Promise<any[]> {
await this.variable.updateOptions(query);
return this.variable.options;
}
updateUIBoundOptions($scope: IScope, options: any[]) {
this.highlightIndex = 0;
this.search.options = options.slice(0, Math.min(options.length, 1000));
$scope.$apply();
}
}
/** @ngInject */
export function valueSelectDropdown($compile: any, $window: any, $timeout: any, $rootScope: GrafanaRootScope) {
return {
scope: { dashboard: '=', variable: '=', onUpdated: '&' },
templateUrl: 'public/app/partials/valueSelectDropdown.html',
controller: 'ValueSelectDropdownCtrl',
controllerAs: 'vm',
bindToController: true,
link: (scope: any, elem: any) => {
const bodyEl = angular.element($window.document.body);
const linkEl = elem.find('.variable-value-link');
const inputEl = elem.find('input');
function openDropdown() {
inputEl.css('width', Math.max(linkEl.width(), 80) + 'px');
inputEl.show();
linkEl.hide();
inputEl.focus();
$timeout(
() => {
bodyEl.on('click', bodyOnClick);
},
0,
false
);
}
function switchToLink() {
inputEl.hide();
linkEl.show();
bodyEl.off('click', bodyOnClick);
}
function bodyOnClick(e: any) {
if (elem.has(e.target).length === 0) {
scope.$apply(() => {
scope.vm.commitChanges();
});
}
}
scope.$watch('vm.dropdownVisible', (newValue: any) => {
if (newValue) {
openDropdown();
} else {
switchToLink();
}
});
scope.vm.dashboard.on(
'template-variable-value-updated',
() => {
scope.vm.updateLinkText();
},
scope
);
scope.vm.init();
},
};
}
coreModule.controller('ValueSelectDropdownCtrl', ValueSelectDropdownCtrl);
coreModule.directive('valueSelectDropdown', valueSelectDropdown);

@ -1,13 +1,13 @@
import coreModule from 'app/core/core_module';
import appEvents from 'app/core/app_events';
import { store } from 'app/store/store';
import { dispatch, store } from 'app/store/store';
import { updateLocation } from 'app/core/actions';
import { ILocationService, ITimeoutService, IWindowService } from 'angular';
import { CoreEvents } from 'app/types';
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
import { locationUtil, UrlQueryMap } from '@grafana/data';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { VariableSrv } from 'app/features/templating/all';
import { templateVarsChangedInUrl } from 'app/features/variables/state/actions';
// Services that handles angular -> redux store sync & other react <-> angular sync
export class BridgeSrv {
@ -22,8 +22,7 @@ export class BridgeSrv {
private $timeout: ITimeoutService,
private $window: IWindowService,
private $rootScope: GrafanaRootScope,
private $route: any,
private variableSrv: VariableSrv
private $route: any
) {
this.fullPageReloadRoutes = ['/logout'];
this.angularUrl = $location.url();
@ -84,7 +83,7 @@ export class BridgeSrv {
if (changes) {
const dash = getDashboardSrv().getCurrent();
if (dash) {
this.variableSrv.templateVarsChangedInUrl(changes);
dispatch(templateVarsChangedInUrl(changes));
}
}
this.lastQuery = state.location.query;

@ -1,270 +0,0 @@
import 'app/core/directives/value_select_dropdown';
import { ValueSelectDropdownCtrl } from '../directives/value_select_dropdown';
import { IScope } from 'angular';
describe('SelectDropdownCtrl', () => {
const tagValuesMap: any = {};
const $scope: IScope = {} as IScope;
ValueSelectDropdownCtrl.prototype.onUpdated = jest.fn();
let ctrl: ValueSelectDropdownCtrl;
describe('Given simple variable', () => {
beforeEach(() => {
ctrl = new ValueSelectDropdownCtrl($scope);
ctrl.variable = {
current: { text: 'hej', value: 'hej' },
getValuesForTag: (key: string) => {
return Promise.resolve(tagValuesMap[key]);
},
};
ctrl.init();
});
it('Should init labelText and linkText', () => {
expect(ctrl.linkText).toBe('hej');
});
});
describe('Given variable with tags and dropdown is opened', () => {
beforeEach(() => {
ctrl = new ValueSelectDropdownCtrl($scope);
ctrl.variable = {
current: { text: 'server-1', value: 'server-1' },
options: [
{ text: 'server-1', value: 'server-1', selected: true },
{ text: 'server-2', value: 'server-2' },
{ text: 'server-3', value: 'server-3' },
],
tags: ['key1', 'key2', 'key3'],
getValuesForTag: (key: string) => {
return Promise.resolve(tagValuesMap[key]);
},
multi: true,
};
tagValuesMap.key1 = ['server-1', 'server-3'];
tagValuesMap.key2 = ['server-2', 'server-3'];
tagValuesMap.key3 = ['server-1', 'server-2', 'server-3'];
ctrl.init();
ctrl.show();
});
it('should init tags model', () => {
expect(ctrl.tags.length).toBe(3);
expect(ctrl.tags[0].text).toBe('key1');
});
it('should init options model', () => {
expect(ctrl.options.length).toBe(3);
});
it('should init selected values array', () => {
expect(ctrl.selectedValues.length).toBe(1);
});
it('should set linkText', () => {
expect(ctrl.linkText).toBe('server-1');
});
describe('after adititional value is selected', () => {
beforeEach(() => {
ctrl.selectValue(ctrl.options[2], {});
ctrl.commitChanges();
});
it('should update link text', () => {
expect(ctrl.linkText).toBe('server-1 + server-3');
});
});
describe('When tag is selected', () => {
beforeEach(async () => {
await ctrl.selectTag(ctrl.tags[0]);
ctrl.commitChanges();
});
it('should select tag', () => {
expect(ctrl.selectedTags.length).toBe(1);
});
it('should select values', () => {
expect(ctrl.options[0].selected).toBe(true);
expect(ctrl.options[2].selected).toBe(true);
});
it('link text should not include tag values', () => {
expect(ctrl.linkText).toBe('');
});
describe('and then dropdown is opened and closed without changes', () => {
beforeEach(() => {
ctrl.show();
ctrl.commitChanges();
});
it('should still have selected tag', () => {
expect(ctrl.selectedTags.length).toBe(1);
});
});
describe('and then unselected', () => {
beforeEach(async () => {
await ctrl.selectTag(ctrl.tags[0]);
});
it('should deselect tag', () => {
expect(ctrl.selectedTags.length).toBe(0);
});
});
describe('and then value is unselected', () => {
beforeEach(() => {
ctrl.selectValue(ctrl.options[0], {});
});
it('should deselect tag', () => {
expect(ctrl.selectedTags.length).toBe(0);
});
});
});
});
describe('Given variable with selected tags', () => {
beforeEach(() => {
ctrl = new ValueSelectDropdownCtrl($scope);
ctrl.variable = {
current: {
text: 'server-1',
value: 'server-1',
tags: [{ text: 'key1', selected: true }],
},
options: [
{ text: 'server-1', value: 'server-1' },
{ text: 'server-2', value: 'server-2' },
{ text: 'server-3', value: 'server-3' },
],
tags: ['key1', 'key2', 'key3'],
getValuesForTag: (key: any) => {
return Promise.resolve(tagValuesMap[key]);
},
multi: true,
};
ctrl.init();
ctrl.show();
});
it('should set tag as selected', () => {
expect(ctrl.tags[0].selected).toBe(true);
});
});
});
describe('queryChanged', () => {
describe('when called and variable query contains search filter', () => {
it('then it should use lazy loading', async () => {
const $scope = {} as IScope;
const ctrl = new ValueSelectDropdownCtrl($scope);
const options = [
{ text: 'server-1', value: 'server-1' },
{ text: 'server-2', value: 'server-2' },
{ text: 'server-3', value: 'server-3' },
];
ctrl.lazyLoadOptions = jest.fn().mockResolvedValue(options);
ctrl.updateUIBoundOptions = jest.fn();
ctrl.search = {
query: 'alpha',
};
ctrl.queryHasSearchFilter = true;
await ctrl.queryChanged();
expect(ctrl.lazyLoadOptions).toBeCalledTimes(1);
expect(ctrl.lazyLoadOptions).toBeCalledWith('alpha');
expect(ctrl.updateUIBoundOptions).toBeCalledTimes(1);
expect(ctrl.updateUIBoundOptions).toBeCalledWith($scope, options);
});
});
describe('when called and variable query does not contain search filter', () => {
it('then it should not use lazy loading', async () => {
const $scope = {} as IScope;
const ctrl = new ValueSelectDropdownCtrl($scope);
ctrl.lazyLoadOptions = jest.fn().mockResolvedValue([]);
ctrl.updateUIBoundOptions = jest.fn();
ctrl.search = {
query: 'alpha',
};
ctrl.queryHasSearchFilter = false;
await ctrl.queryChanged();
expect(ctrl.lazyLoadOptions).toBeCalledTimes(0);
expect(ctrl.updateUIBoundOptions).toBeCalledTimes(1);
});
});
});
describe('lazyLoadOptions', () => {
describe('when called with a query', () => {
it('then the variables updateOptions should be called with the query', async () => {
const $scope = {} as IScope;
const ctrl = new ValueSelectDropdownCtrl($scope);
ctrl.variable = {
updateOptions: jest.fn(),
options: [
{ text: 'server-1', value: 'server-1' },
{ text: 'server-2', value: 'server-2' },
{ text: 'server-3', value: 'server-3' },
],
};
const query = 'server-1';
const result = await ctrl.lazyLoadOptions(query);
expect(ctrl.variable.updateOptions).toBeCalledTimes(1);
expect(ctrl.variable.updateOptions).toBeCalledWith(query);
expect(result).toEqual(ctrl.variable.options);
});
});
});
describe('updateUIBoundOptions', () => {
describe('when called with options', () => {
let options: any[];
let ctrl: ValueSelectDropdownCtrl;
let $scope: IScope;
beforeEach(() => {
$scope = ({
$apply: jest.fn(),
} as any) as IScope;
options = [];
for (let index = 0; index < 1001; index++) {
options.push({ text: `server-${index}`, value: `server-${index}` });
}
ctrl = new ValueSelectDropdownCtrl($scope);
ctrl.highlightIndex = 0;
ctrl.options = [];
ctrl.search = {
options: [],
};
ctrl.updateUIBoundOptions($scope, options);
});
it('then highlightIndex should be reset to first item', () => {
expect(ctrl.highlightIndex).toEqual(0);
});
it('then search.options should be same as options but capped to 1000', () => {
expect(ctrl.search.options.length).toEqual(1000);
for (let index = 0; index < 1000; index++) {
expect(ctrl.search.options[index]).toEqual(options[index]);
}
});
it('then scope apply should be called', () => {
expect($scope.$apply).toBeCalledTimes(1);
});
});
});

@ -1,182 +0,0 @@
import _ from 'lodash';
import angular from 'angular';
import coreModule from 'app/core/core_module';
import { DashboardModel } from 'app/features/dashboard/state';
import DatasourceSrv from 'app/features/plugins/datasource_srv';
import { VariableSrv } from 'app/features/templating/all';
import { CoreEvents } from 'app/types';
export class AdHocFiltersCtrl {
segments: any;
variable: any;
dashboard: DashboardModel;
removeTagFilterSegment: any;
/** @ngInject */
constructor(
private uiSegmentSrv: any,
private datasourceSrv: DatasourceSrv,
private variableSrv: VariableSrv,
$scope: any
) {
this.removeTagFilterSegment = uiSegmentSrv.newSegment({
fake: true,
value: '-- remove filter --',
});
this.buildSegmentModel();
this.dashboard.events.on(CoreEvents.templateVariableValueUpdated, this.buildSegmentModel.bind(this), $scope);
}
buildSegmentModel() {
this.segments = [];
if (this.variable.value && !_.isArray(this.variable.value)) {
}
for (const tag of this.variable.filters) {
if (this.segments.length > 0) {
this.segments.push(this.uiSegmentSrv.newCondition('AND'));
}
if (tag.key !== undefined && tag.value !== undefined) {
this.segments.push(this.uiSegmentSrv.newKey(tag.key));
this.segments.push(this.uiSegmentSrv.newOperator(tag.operator));
this.segments.push(this.uiSegmentSrv.newKeyValue(tag.value));
}
}
this.segments.push(this.uiSegmentSrv.newPlusButton());
}
getOptions(segment: { type: string }, index: number) {
if (segment.type === 'operator') {
return Promise.resolve(this.uiSegmentSrv.newOperators(['=', '!=', '<', '>', '=~', '!~']));
}
if (segment.type === 'condition') {
return Promise.resolve([this.uiSegmentSrv.newSegment('AND')]);
}
return this.datasourceSrv.get(this.variable.datasource).then(ds => {
const options: any = {};
let promise = null;
if (segment.type !== 'value') {
promise = ds.getTagKeys ? ds.getTagKeys() : Promise.resolve([]);
} else {
options.key = this.segments[index - 2].value;
promise = ds.getTagValues ? ds.getTagValues(options) : Promise.resolve([]);
}
return promise.then((results: any) => {
results = _.map(results, segment => {
return this.uiSegmentSrv.newSegment({ value: segment.text });
});
// add remove option for keys
if (segment.type === 'key') {
results.splice(0, 0, angular.copy(this.removeTagFilterSegment));
}
return results;
});
});
}
segmentChanged(segment: { value: any; type: string; cssClass: string }, index: number) {
this.segments[index] = segment;
// handle remove tag condition
if (segment.value === this.removeTagFilterSegment.value) {
this.segments.splice(index, 3);
if (this.segments.length === 0) {
this.segments.push(this.uiSegmentSrv.newPlusButton());
} else if (this.segments.length > 2) {
this.segments.splice(Math.max(index - 1, 0), 1);
if (this.segments[this.segments.length - 1].type !== 'plus-button') {
this.segments.push(this.uiSegmentSrv.newPlusButton());
}
}
} else {
if (segment.type === 'plus-button') {
if (index > 2) {
this.segments.splice(index, 0, this.uiSegmentSrv.newCondition('AND'));
}
this.segments.push(this.uiSegmentSrv.newOperator('='));
this.segments.push(this.uiSegmentSrv.newFake('select value', 'value', 'query-segment-value'));
segment.type = 'key';
segment.cssClass = 'query-segment-key';
}
if (index + 1 === this.segments.length) {
this.segments.push(this.uiSegmentSrv.newPlusButton());
}
}
this.updateVariableModel();
}
updateVariableModel() {
const filters: any[] = [];
let filterIndex = -1;
let hasFakes = false;
this.segments.forEach((segment: any) => {
if (segment.type === 'value' && segment.fake) {
hasFakes = true;
return;
}
switch (segment.type) {
case 'key': {
filters.push({ key: segment.value });
filterIndex += 1;
break;
}
case 'value': {
filters[filterIndex].value = segment.value;
break;
}
case 'operator': {
filters[filterIndex].operator = segment.value;
break;
}
case 'condition': {
filters[filterIndex].condition = segment.value;
break;
}
}
});
if (hasFakes) {
return;
}
this.variable.setFilters(filters);
this.variableSrv.variableUpdated(this.variable, true);
}
}
const template = `
<div class="gf-form-inline">
<div class="gf-form" ng-repeat="segment in ctrl.segments">
<metric-segment segment="segment" get-options="ctrl.getOptions(segment, $index)"
on-change="ctrl.segmentChanged(segment, $index)"></metric-segment>
</div>
</div>
`;
export function adHocFiltersComponent() {
return {
restrict: 'E',
template: template,
controller: AdHocFiltersCtrl,
bindToController: true,
controllerAs: 'ctrl',
scope: {
variable: '=',
dashboard: '=',
},
};
}
coreModule.directive('adHocFilters', adHocFiltersComponent);

@ -1 +0,0 @@
export { AdHocFiltersCtrl } from './AdHocFiltersCtrl';

@ -3,6 +3,10 @@ import config from 'app/core/config';
import { DashboardExporter } from './DashboardExporter';
import { DashboardModel } from '../../state/DashboardModel';
import { PanelPluginMeta } from '@grafana/data';
import { variableAdapters } from '../../../variables/adapters';
import { createConstantVariableAdapter } from '../../../variables/constant/adapter';
import { createQueryVariableAdapter } from '../../../variables/query/adapter';
import { createDataSourceVariableAdapter } from '../../../variables/datasource/adapter';
jest.mock('app/core/store', () => {
return {
@ -25,6 +29,10 @@ jest.mock('@grafana/runtime', () => ({
},
}));
variableAdapters.register(createQueryVariableAdapter());
variableAdapters.register(createConstantVariableAdapter());
variableAdapters.register(createDataSourceVariableAdapter());
describe('given dashboard with repeated panels', () => {
let dash: any, exported: any;
@ -122,7 +130,7 @@ describe('given dashboard with repeated panels', () => {
info: { version: '1.1.2' },
} as PanelPluginMeta;
dash = new DashboardModel(dash, {});
dash = new DashboardModel(dash, {}, () => dash.templating.list);
const exporter = new DashboardExporter();
exporter.makeExportable(dash).then(clean => {
exported = clean;

@ -5,6 +5,8 @@ import { DashboardModel } from '../../state/DashboardModel';
import { PanelModel } from 'app/features/dashboard/state';
import { PanelPluginMeta } from '@grafana/data';
import { getDataSourceSrv } from '@grafana/runtime';
import { VariableOption, VariableRefresh } from '../../../variables/types';
import { isConstant, isQuery } from '../../../variables/guard';
interface Input {
name: string;
@ -144,11 +146,12 @@ export class DashboardExporter {
// templatize template vars
for (const variable of saveModel.getVariables()) {
if (variable.type === 'query') {
if (isQuery(variable)) {
templateizeDatasourceUsage(variable);
variable.options = [];
variable.current = {};
variable.refresh = variable.refresh > 0 ? variable.refresh : 1;
variable.current = ({} as unknown) as VariableOption;
variable.refresh =
variable.refresh !== VariableRefresh.never ? variable.refresh : VariableRefresh.onDashboardLoad;
}
}
@ -173,7 +176,7 @@ export class DashboardExporter {
// templatize constants
for (const variable of saveModel.getVariables()) {
if (variable.type === 'constant') {
if (isConstant(variable)) {
const refName = 'VAR_' + variable.name.replace(' ', '_').toUpperCase();
inputs.push({
name: refName,
@ -187,6 +190,7 @@ export class DashboardExporter {
variable.options[0] = variable.current = {
value: variable.query,
text: variable.query,
selected: false,
};
}
}

@ -1,5 +1,5 @@
import React from 'react';
import { shallow } from 'enzyme';
import { mount } from 'enzyme';
import { DashboardRow } from './DashboardRow';
import { PanelModel } from '../../state/PanelModel';
@ -16,7 +16,7 @@ describe('DashboardRow', () => {
};
panel = new PanelModel({ collapsed: false });
wrapper = shallow(<DashboardRow panel={panel} dashboard={dashboardMock} />);
wrapper = mount(<DashboardRow panel={panel} dashboard={dashboardMock} />);
});
it('Should not have collapsed class when collaped is false', () => {
@ -37,14 +37,14 @@ describe('DashboardRow', () => {
it('should not show row drag handle when cannot edit', () => {
dashboardMock.meta.canEdit = false;
wrapper = shallow(<DashboardRow panel={panel} dashboard={dashboardMock} />);
wrapper = mount(<DashboardRow panel={panel} dashboard={dashboardMock} />);
expect(wrapper.find('.dashboard-row__drag')).toHaveLength(0);
});
it('should have zero actions when cannot edit', () => {
dashboardMock.meta.canEdit = false;
panel = new PanelModel({ collapsed: false });
wrapper = shallow(<DashboardRow panel={panel} dashboard={dashboardMock} />);
wrapper = mount(<DashboardRow panel={panel} dashboard={dashboardMock} />);
expect(wrapper.find('.dashboard-row__actions .pointer')).toHaveLength(0);
});
});

@ -6,6 +6,7 @@ import { DashboardModel } from '../../state/DashboardModel';
import templateSrv from 'app/features/templating/template_srv';
import appEvents from 'app/core/app_events';
import { CoreEvents } from 'app/types';
import { RowOptionsButton } from '../RowOptions/RowOptionsButton';
export interface DashboardRowProps {
panel: PanelModel;
@ -39,22 +40,14 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
});
};
onUpdate = () => {
onUpdate = (title: string | null, repeat: string | null) => {
this.props.panel['title'] = title;
this.props.panel['repeat'] = repeat;
this.props.panel.render();
this.props.dashboard.processRepeats();
this.forceUpdate();
};
onOpenSettings = () => {
appEvents.emit(CoreEvents.showModal, {
templateHtml: `<row-options row="model.row" on-updated="model.onUpdated()" dismiss="dismiss()"></row-options>`,
modalClass: 'modal--narrow',
model: {
row: this.props.panel,
onUpdated: this.onUpdate,
},
});
};
onDelete = () => {
appEvents.emit(CoreEvents.showConfirmModal, {
title: 'Delete Row',
@ -92,9 +85,11 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
</a>
{canEdit && (
<div className="dashboard-row__actions">
<a className="pointer" onClick={this.onOpenSettings}>
<Icon name="cog" />
</a>
<RowOptionsButton
title={this.props.panel.title}
repeat={this.props.panel.repeat}
onUpdate={this.onUpdate}
/>
<a className="pointer" onClick={this.onDelete}>
<Icon name="trash-alt" />
</a>

@ -25,7 +25,6 @@ export class SettingsCtrl {
sections: any[];
hasUnsavedFolderChange: boolean;
selectors: typeof selectors.pages.Dashboard.Settings.General;
useAngularTemplating: boolean;
/** @ngInject */
constructor(
@ -60,7 +59,6 @@ export class SettingsCtrl {
appEvents.on(CoreEvents.dashboardSaved, this.onPostSave.bind(this), $scope);
this.selectors = selectors.pages.Dashboard.Settings.General;
this.useAngularTemplating = !getConfig().featureToggles.newVariables;
}
buildSectionList() {

@ -77,11 +77,7 @@
ng-include="'public/app/features/annotations/partials/editor.html'">
</div>
<div class="dashboard-settings__content" ng-if="ctrl.viewId === 'templating' && ctrl.useAngularTemplating"
ng-include="'public/app/features/templating/partials/editor.html'">
</div>
<div class="dashboard-settings__content" ng-if="ctrl.viewId === 'templating' && !ctrl.useAngularTemplating">
<div class="dashboard-settings__content" ng-if="ctrl.viewId === 'templating'">
<variable-editor-container />
</div>

@ -25,7 +25,7 @@ import { getPanelEditorTabs } from './state/selectors';
import { getPanelStateById } from '../../state/selectors';
import { OptionsPaneContent } from './OptionsPaneContent';
import { DashNavButton } from 'app/features/dashboard/components/DashNav/DashNavButton';
import { VariableModel } from 'app/features/templating/types';
import { VariableModel } from 'app/features/variables/types';
import { getVariables } from 'app/features/variables/state/selectors';
import { SubMenuItems } from 'app/features/dashboard/components/SubMenu/SubMenuItems';
import { BackButton } from 'app/core/components/BackButton/BackButton';

@ -1,13 +1,13 @@
import React, { FC, useMemo, useRef } from 'react';
import React, { FC, useCallback, useMemo, useRef } from 'react';
import { DashboardModel, PanelModel } from '../../state';
import { PanelData, PanelPlugin, SelectableValue } from '@grafana/data';
import { PanelData, PanelPlugin } from '@grafana/data';
import { Counter, DataLinksInlineEditor, Field, Input, RadioButtonGroup, Select, Switch, TextArea } from '@grafana/ui';
import { getPanelLinksVariableSuggestions } from '../../../panel/panellinks/link_srv';
import { getVariables } from '../../../variables/state/selectors';
import { PanelOptionsEditor } from './PanelOptionsEditor';
import { AngularPanelOptions } from './AngularPanelOptions';
import { VisualizationTab } from './VisualizationTab';
import { OptionsGroup } from './OptionsGroup';
import { RepeatRowSelect } from '../RepeatRowSelect/RepeatRowSelect';
interface Props {
panel: PanelModel;
@ -28,10 +28,12 @@ export const PanelOptionsTab: FC<Props> = ({
}) => {
const visTabInputRef = useRef<HTMLInputElement>();
const linkVariablesSuggestions = useMemo(() => getPanelLinksVariableSuggestions(), []);
const onRepeatRowSelectChange = useCallback((value: string | null) => onPanelConfigChange('repeat', value), [
onPanelConfigChange,
]);
const elements: JSX.Element[] = [];
const panelLinksCount = panel && panel.links ? panel.links.length : 0;
const variableOptions = getVariableOptions();
const directionOptions = [
{ label: 'Horizontal', value: 'h' },
{ label: 'Vertical', value: 'v' },
@ -120,11 +122,7 @@ export const PanelOptionsTab: FC<Props> = ({
This is not visible while in edit mode. You need to go back to dashboard and then update the variable or
reload the dashboard."
>
<Select
value={panel.repeat}
onChange={value => onPanelConfigChange('repeat', value.value)}
options={variableOptions}
/>
<RepeatRowSelect repeat={panel.repeat} onChange={onRepeatRowSelectChange} />
</Field>
{panel.repeat && (
<Field label="Repeat direction">
@ -150,23 +148,3 @@ export const PanelOptionsTab: FC<Props> = ({
return <>{elements}</>;
};
function getVariableOptions(): Array<SelectableValue<string>> {
const options = getVariables().map((item: any) => {
return { label: item.name, value: item.name };
});
if (options.length === 0) {
options.unshift({
label: 'No template variables found',
value: null,
});
}
options.unshift({
label: 'Disable repeating',
value: null,
});
return options;
}

@ -0,0 +1,40 @@
import React, { FC, useCallback, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { Select } from '@grafana/ui';
import { SelectableValue } from '@grafana/data';
import { getVariables } from '../../../variables/state/selectors';
import { StoreState } from '../../../../types';
export interface Props {
repeat: string | undefined;
onChange: (name: string) => void;
}
export const RepeatRowSelect: FC<Props> = ({ repeat, onChange }) => {
const variables = useSelector((state: StoreState) => getVariables(state));
const variableOptions = useMemo(() => {
const options = variables.map((item: any) => {
return { label: item.name, value: item.name };
});
if (options.length === 0) {
options.unshift({
label: 'No template variables found',
value: null,
});
}
options.unshift({
label: 'Disable repeating',
value: null,
});
return options;
}, variables);
const onSelectChange = useCallback((option: SelectableValue<string | null>) => onChange(option.value), [onChange]);
return <Select value={repeat} onChange={onSelectChange} options={variableOptions} />;
};

@ -0,0 +1,37 @@
import React, { FC } from 'react';
import { Icon, ModalsController } from '@grafana/ui';
import { RowOptionsModal } from './RowOptionsModal';
import { OnRowOptionsUpdate } from './RowOptionsForm';
export interface RowOptionsButtonProps {
title: string | null;
repeat: string | null;
onUpdate: OnRowOptionsUpdate;
}
export const RowOptionsButton: FC<RowOptionsButtonProps> = ({ repeat, title, onUpdate }) => {
const onUpdateChange = (hideModal: () => void) => (title: string | null, repeat: string | null) => {
onUpdate(title, repeat);
hideModal();
};
return (
<ModalsController>
{({ showModal, hideModal }) => {
return (
<a
className="pointer"
onClick={() => {
showModal(RowOptionsModal, { title, repeat, onDismiss: hideModal, onUpdate: onUpdateChange(hideModal) });
}}
>
<Icon name="cog" />
</a>
);
}}
</ModalsController>
);
};
RowOptionsButton.displayName = 'RowOptionsButton';

@ -1,39 +0,0 @@
import { coreModule } from 'app/core/core';
export class RowOptionsCtrl {
row: any;
source: any;
dismiss: any;
onUpdated: any;
showDelete: boolean;
/** @ngInject */
constructor() {
this.source = this.row;
this.row = this.row.getSaveModel();
}
update() {
this.source.title = this.row.title;
this.source.repeat = this.row.repeat;
this.onUpdated();
this.dismiss();
}
}
export function rowOptionsDirective() {
return {
restrict: 'E',
templateUrl: 'public/app/features/dashboard/components/RowOptions/template.html',
controller: RowOptionsCtrl,
bindToController: true,
controllerAs: 'ctrl',
scope: {
row: '=',
dismiss: '&',
onUpdated: '&',
},
};
}
coreModule.directive('rowOptions', rowOptionsDirective);

@ -0,0 +1,46 @@
import React, { FC, useCallback, useState } from 'react';
import { Button, Field, Form, HorizontalGroup, Input } from '@grafana/ui';
import { RepeatRowSelect } from '../RepeatRowSelect/RepeatRowSelect';
export type OnRowOptionsUpdate = (title: string | null, repeat: string | null) => void;
export interface Props {
title: string | null;
repeat: string | null;
onUpdate: OnRowOptionsUpdate;
onCancel: () => void;
}
export const RowOptionsForm: FC<Props> = ({ repeat, title, onUpdate, onCancel }) => {
const [newRepeat, setNewRepeat] = useState<string | null>(repeat);
const onChangeRepeat = useCallback((name: string) => setNewRepeat(name), [setNewRepeat]);
return (
<Form
defaultValues={{ title }}
onSubmit={(formData: { title: string | null }) => {
onUpdate(formData.title, newRepeat);
}}
>
{({ register }) => (
<>
<Field label="Title">
<Input name="title" ref={register} type="text" />
</Field>
<Field label="Repeat for">
<RepeatRowSelect repeat={newRepeat} onChange={onChangeRepeat} />
</Field>
<HorizontalGroup>
<Button type="submit">Update</Button>
<Button variant="secondary" onClick={onCancel}>
Cancel
</Button>
</HorizontalGroup>
</>
)}
</Form>
);
};

@ -0,0 +1,30 @@
import React, { FC } from 'react';
import { Modal, stylesFactory } from '@grafana/ui';
import { css } from 'emotion';
import { OnRowOptionsUpdate, RowOptionsForm } from './RowOptionsForm';
export interface RowOptionsModalProps {
title: string | null;
repeat: string | null;
onDismiss: () => void;
onUpdate: OnRowOptionsUpdate;
}
export const RowOptionsModal: FC<RowOptionsModalProps> = ({ repeat, title, onDismiss, onUpdate }) => {
const styles = getStyles();
return (
<Modal isOpen={true} title="Row Options" icon="copy" onDismiss={onDismiss} className={styles.modal}>
<RowOptionsForm repeat={repeat} title={title} onCancel={onDismiss} onUpdate={onUpdate} />
</Modal>
);
};
const getStyles = stylesFactory(() => {
return {
modal: css`
label: RowOptionsModal;
width: 500px;
`,
};
});

@ -1 +0,0 @@
export { RowOptionsCtrl } from './RowOptionsCtrl';

@ -1,30 +0,0 @@
<div class="modal-body">
<div class="modal-header">
<h2 class="modal-header-title">
<icon name="'copy'" size="'lg'"></icon>
<span class="p-l-1">Row Options</span>
</h2>
<a class="modal-header-close" ng-click="ctrl.dismiss();">
<icon name="'times'"></icon>
</a>
</div>
<form name="ctrl.saveForm" ng-submit="ctrl.save()" class="modal-content text-center" novalidate>
<div class="section">
<div class="gf-form">
<span class="gf-form-label width-7">Title</span>
<input type="text" class="gf-form-input max-width-13" ng-model='ctrl.row.title'></input>
</div>
<div class="gf-form">
<span class="gf-form-label width-7">Repeat for</span>
<dash-repeat-option panel="ctrl.row"></dash-repeat-option>
</div>
<div class="gf-form-button-row">
<button type="submit" class="btn btn-primary" ng-click="ctrl.update()">Update</button>
<button type="button" class="btn btn-inverse" ng-click="ctrl.dismiss()">Cancel</button>
</div>
</div>
</form>
</div>

@ -1,11 +1,13 @@
import React, { PureComponent } from 'react';
import { Button, ClipboardButton, LinkButton, LegacyForms, Icon } from '@grafana/ui';
const { Select, Input } = LegacyForms;
import { Button, ClipboardButton, Icon, LegacyForms, LinkButton } from '@grafana/ui';
import { AppEvents, SelectableValue } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { appEvents } from 'app/core/core';
import { VariableRefresh } from '../../../variables/types';
const { Select, Input } = LegacyForms;
const snapshotApiUrl = '/api/snapshots';
@ -140,10 +142,10 @@ export class ShareSnapshot extends PureComponent<Props, State> {
});
// remove template queries
dash.getVariables().forEach(variable => {
dash.getVariables().forEach((variable: any) => {
variable.query = '';
variable.options = variable.current;
variable.refresh = false;
variable.options = variable.current ? [variable.current] : [];
variable.refresh = VariableRefresh.never;
});
// snapshot single panel

@ -2,7 +2,7 @@ import React, { PureComponent } from 'react';
import { connect, MapStateToProps } from 'react-redux';
import { StoreState } from '../../../../types';
import { getVariables } from '../../../variables/state/selectors';
import { VariableHide, VariableModel } from '../../../templating/types';
import { VariableHide, VariableModel } from '../../../variables/types';
import { DashboardModel } from '../../state';
import { DashboardLinks } from './DashboardLinks';
import { Annotations } from './Annotations';

@ -1,5 +1,5 @@
import React, { FunctionComponent, useEffect, useState } from 'react';
import { VariableHide, VariableModel } from '../../../templating/types';
import { VariableHide, VariableModel } from '../../../variables/types';
import { selectors } from '@grafana/e2e-selectors';
import { PickerRenderer } from '../../../variables/pickers/PickerRenderer';

@ -1,60 +0,0 @@
<div class="submenu-controls" ng-hide="ctrl.submenuEnabled === false">
<div
ng-repeat="variable in ctrl.variables"
ng-hide="variable.hide === 2"
class="submenu-item gf-form-inline"
aria-label="{{::ctrl.selectors.submenuItem}}"
>
<div class="gf-form">
<label
class="gf-form-label template-variable"
ng-hide="variable.hide === 1"
aria-label="{{ctrl.selectors.submenuItemLabels(variable.label || variable.name)}}"
>{{variable.label || variable.name}}</label
>
<value-select-dropdown
ng-if="variable.type !== 'adhoc' && variable.type !== 'textbox'"
dashboard="ctrl.dashboard"
variable="variable"
on-updated="ctrl.variableUpdated(variable)"
></value-select-dropdown>
<input
type="text"
ng-if="variable.type === 'textbox'"
ng-model="variable.query"
class="gf-form-input width-12"
ng-blur="variable.current.value != variable.query && variable.updateOptions() && ctrl.variableUpdated(variable);"
ng-keydown="$event.keyCode === 13 && variable.current.value != variable.query && variable.updateOptions() && ctrl.variableUpdated(variable);"
/>
</div>
<ad-hoc-filters ng-if="variable.type === 'adhoc'" variable="variable" dashboard="ctrl.dashboard"></ad-hoc-filters>
</div>
<div ng-if="ctrl.dashboard.annotations.list.length > 0">
<div
ng-repeat="annotation in ctrl.dashboard.annotations.list"
ng-hide="annotation.hide"
class="submenu-item"
ng-class="{'annotation-disabled': !annotation.enable}"
>
<gf-form-switch
class="gf-form"
label="{{annotation.name}}"
checked="annotation.enable"
on-change="ctrl.annotationStateChanged()"
></gf-form-switch>
</div>
</div>
<div class="gf-form gf-form--grow"></div>
<div ng-if="ctrl.dashboard.links.length > 0">
<dash-links-container
links="ctrl.dashboard.links"
dashboard="ctrl.dashboard"
class="gf-form-inline"
></dash-links-container>
</div>
<div class="clearfix"></div>
</div>

@ -28,7 +28,6 @@ import {
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
import { InspectTab, PanelInspector } from '../components/Inspector/PanelInspector';
import { getConfig } from '../../../core/config';
import { SubMenu } from '../components/SubMenu/SubMenu';
import { cleanUpDashboardAndVariables } from '../state/actions';
import { cancelVariables } from '../../variables/state/actions';
@ -275,7 +274,6 @@ export class DashboardPage extends PureComponent<Props, State> {
} = this.props;
const { editPanel, viewPanel, scrollTop, updateScrollTop } = this.state;
const { featureToggles } = getConfig();
if (!dashboard) {
if (isInitSlow) {
@ -307,8 +305,7 @@ export class DashboardPage extends PureComponent<Props, State> {
{initError && this.renderInitFailedState()}
<div className={gridWrapperClasses}>
{!featureToggles.newVariables && <SubMenu dashboard={dashboard} />}
{!editPanel && featureToggles.newVariables && <SubMenu dashboard={dashboard} />}
{!editPanel && <SubMenu dashboard={dashboard} />}
<DashboardGrid
dashboard={dashboard}
viewPanel={viewPanel}

@ -2,18 +2,14 @@
import './services/UnsavedChangesSrv';
import './services/DashboardLoaderSrv';
import './services/DashboardSrv';
// Components
import './components/DashLinks';
import './components/DashExportModal';
import './components/DashNav';
import './components/VersionHistory';
import './components/DashboardSettings';
import './components/AdHocFilters';
import './components/RowOptions';
import DashboardPermissions from './components/DashboardPermissions/DashboardPermissions';
// angular wrappers
import { react2AngularDirective } from 'app/core/utils/react2angular';

@ -134,7 +134,7 @@ export class ChangeTracker {
});
// ignore template variable values
_.each(dash.getVariables(), variable => {
_.each(dash.getVariables(), (variable: any) => {
variable.current = null;
variable.options = null;
variable.filters = null;

@ -18,7 +18,7 @@ import {
} from 'app/core/constants';
import { isMulti, isQuery } from 'app/features/variables/guard';
import { alignCurrentWithMulti } from 'app/features/variables/shared/multiOptions';
import { VariableTag } from '../../templating/types';
import { VariableTag } from '../../variables/types';
export class DashboardMigrator {
dashboard: DashboardModel;
@ -240,7 +240,7 @@ export class DashboardMigrator {
if (oldVersion < 12) {
// update template variables
_.each(this.dashboard.getVariables(), templateVariable => {
_.each(this.dashboard.getVariables(), (templateVariable: any) => {
if (templateVariable.refresh) {
templateVariable.refresh = 1;
}

@ -21,7 +21,6 @@ import {
UrlQueryValue,
} from '@grafana/data';
import { CoreEvents, DashboardMeta, KIOSK_MODE_TV } from 'app/types';
import { getConfig } from '../../../core/config';
import { GetVariables, getVariables } from 'app/features/variables/state/selectors';
import { variableAdapters } from 'app/features/variables/adapters';
import { onTimeRangeUpdated } from 'app/features/variables/state/actions';
@ -229,46 +228,6 @@ export class DashboardModel {
private updateTemplatingSaveModelClone(
copy: any,
defaults: { saveTimerange: boolean; saveVariables: boolean } & CloneOptions
) {
if (getConfig().featureToggles.newVariables) {
this.updateTemplatingSaveModel(copy, defaults);
return;
}
this.updateAngularTemplatingSaveModel(copy, defaults);
}
private updateAngularTemplatingSaveModel(
copy: any,
defaults: { saveTimerange: boolean; saveVariables: boolean } & CloneOptions
) {
// get variable save models
copy.templating = {
list: _.map(this.templating.list, (variable: any) =>
variable.getSaveModel ? variable.getSaveModel() : variable
),
};
if (!defaults.saveVariables) {
for (let i = 0; i < copy.templating.list.length; i++) {
const current = copy.templating.list[i];
const original: any = _.find(this.originalTemplating, { name: current.name, type: current.type });
if (!original) {
continue;
}
if (current.type === 'adhoc') {
copy.templating.list[i].filters = original.filters;
} else {
copy.templating.list[i].current = original.current;
}
}
}
}
private updateTemplatingSaveModel(
copy: any,
defaults: { saveTimerange: boolean; saveVariables: boolean } & CloneOptions
) {
const originalVariables = this.originalTemplating;
const currentVariables = this.getVariablesFromState();
@ -297,9 +256,7 @@ export class DashboardModel {
timeRangeUpdated(timeRange: TimeRange) {
this.events.emit(CoreEvents.timeRangeUpdated, timeRange);
if (getConfig().featureToggles.newVariables) {
dispatch(onTimeRangeUpdated(timeRange));
}
dispatch(onTimeRangeUpdated(timeRange));
}
startRefresh() {
@ -965,7 +922,7 @@ export class DashboardModel {
}
resetOriginalVariables(initial = false) {
if (!getConfig().featureToggles.newVariables || initial) {
if (initial) {
this.originalTemplating = this.cloneVariablesFrom(this.templating.list);
return;
}
@ -974,11 +931,7 @@ export class DashboardModel {
}
hasVariableValuesChanged() {
if (getConfig().featureToggles.newVariables) {
return this.hasVariablesChanged(this.originalTemplating, this.getVariablesFromState());
}
return this.hasVariablesChanged(this.originalTemplating, this.templating.list);
return this.hasVariablesChanged(this.originalTemplating, this.getVariablesFromState());
}
autoFitPanels(viewHeight: number, kioskMode?: UrlQueryValue) {
@ -1048,17 +1001,10 @@ export class DashboardModel {
}
getVariables = () => {
if (getConfig().featureToggles.newVariables) {
return this.getVariablesFromState();
}
return this.templating.list;
return this.getVariablesFromState();
};
private getPanelRepeatVariable(panel: PanelModel) {
if (!getConfig().featureToggles.newVariables) {
return _.find(this.templating.list, { name: panel.repeat } as any);
}
return this.getVariablesFromState().find(variable => variable.name === panel.repeat);
}
@ -1067,10 +1013,7 @@ export class DashboardModel {
}
private hasVariables() {
if (getConfig().featureToggles.newVariables) {
return this.getVariablesFromState().length > 0;
}
return this.templating.list.length > 0;
return this.getVariablesFromState().length > 0;
}
private hasVariablesChanged(originalVariables: any[], currentVariables: any[]): boolean {

@ -7,7 +7,6 @@ import { dashboardInitCompleted, dashboardInitFetching, dashboardInitServices }
import { updateLocation } from '../../../core/actions';
import { setEchoSrv } from '@grafana/runtime';
import { Echo } from '../../../core/services/echo/Echo';
import { getConfig } from 'app/core/config';
import { variableAdapters } from 'app/features/variables/adapters';
import { createConstantVariableAdapter } from 'app/features/variables/constant/adapter';
import { constantBuilder } from 'app/features/variables/shared/testing/builders';
@ -38,7 +37,6 @@ interface ScenarioContext {
timeSrv: any;
annotationsSrv: any;
unsavedChangesSrv: any;
variableSrv: any;
dashboardSrv: any;
loaderSrv: any;
keybindingSrv: any;
@ -55,7 +53,6 @@ function describeInitScenario(description: string, scenarioFn: ScenarioFn) {
const timeSrv = { init: jest.fn() };
const annotationsSrv = { init: jest.fn() };
const unsavedChangesSrv = { init: jest.fn() };
const variableSrv = { init: jest.fn() };
const dashboardSrv = { setCurrent: jest.fn() };
const keybindingSrv = { setupDashboardBindings: jest.fn() };
const loaderSrv = {
@ -102,8 +99,6 @@ function describeInitScenario(description: string, scenarioFn: ScenarioFn) {
return unsavedChangesSrv;
case 'dashboardSrv':
return dashboardSrv;
case 'variableSrv':
return variableSrv;
case 'keybindingSrv':
return keybindingSrv;
default:
@ -126,7 +121,6 @@ function describeInitScenario(description: string, scenarioFn: ScenarioFn) {
timeSrv,
annotationsSrv,
unsavedChangesSrv,
variableSrv,
dashboardSrv,
keybindingSrv,
loaderSrv,
@ -201,13 +195,6 @@ describeInitScenario('Initializing new dashboard', ctx => {
expect(ctx.keybindingSrv.setupDashboardBindings).toBeCalled();
expect(ctx.dashboardSrv.setCurrent).toBeCalled();
});
it('Should initialize variableSrv if newVariables is disabled', () => {
if (getConfig().featureToggles.newVariables) {
return expect.assertions(0);
}
expect(ctx.variableSrv.init).toBeCalled();
});
});
describeInitScenario('Initializing home dashboard', ctx => {
@ -260,9 +247,8 @@ describeInitScenario('Initializing existing dashboard', ctx => {
});
it('Should send action dashboardInitCompleted', () => {
const index = getConfig().featureToggles.newVariables ? 6 : 5;
expect(ctx.actions[index].type).toBe(dashboardInitCompleted.type);
expect(ctx.actions[index].payload.title).toBe('My cool dashboard');
expect(ctx.actions[6].type).toBe(dashboardInitCompleted.type);
expect(ctx.actions[6].payload.title).toBe('My cool dashboard');
});
it('Should initialize services', () => {
@ -273,17 +259,7 @@ describeInitScenario('Initializing existing dashboard', ctx => {
expect(ctx.dashboardSrv.setCurrent).toBeCalled();
});
it('Should initialize variableSrv if newVariables is disabled', () => {
if (getConfig().featureToggles.newVariables) {
return expect.assertions(0);
}
expect(ctx.variableSrv.init).toBeCalled();
});
it('Should initialize redux variables if newVariables is enabled', () => {
if (!getConfig().featureToggles.newVariables) {
return expect.assertions(0);
}
expect(ctx.actions[3].type).toBe(variablesInitTransaction.type);
});
});

@ -5,7 +5,6 @@ import { DashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { DashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv';
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { AnnotationsSrv } from 'app/features/annotations/annotations_srv';
import { VariableSrv } from 'app/features/templating/variable_srv';
import { KeybindingSrv } from 'app/core/services/keybindingSrv';
// Actions
import { notifyApp, updateLocation } from 'app/core/actions';
@ -175,7 +174,6 @@ export function initDashboard(args: InitDashboardArgs): ThunkResult<void> {
// init services
const timeSrv: TimeSrv = args.$injector.get('timeSrv');
const annotationsSrv: AnnotationsSrv = args.$injector.get('annotationsSrv');
const variableSrv: VariableSrv = args.$injector.get('variableSrv');
const keybindingSrv: KeybindingSrv = args.$injector.get('keybindingSrv');
const unsavedChangesSrv = args.$injector.get('unsavedChangesSrv');
const dashboardSrv: DashboardSrv = args.$injector.get('dashboardSrv');
@ -189,7 +187,7 @@ export function initDashboard(args: InitDashboardArgs): ThunkResult<void> {
}
// template values service needs to initialize completely before the rest of the dashboard can load
await dispatch(initVariablesTransaction(args.urlUid, dashboard, variableSrv));
await dispatch(initVariablesTransaction(args.urlUid, dashboard));
if (getState().templating.transaction.uid !== args.urlUid) {
// if a previous dashboard has slow running variable queries the batch uid will be the new one

@ -1,46 +0,0 @@
import coreModule from 'app/core/core_module';
const obj2string = (obj: any) => {
return Object.keys(obj)
.reduce((acc, curr) => acc.concat(curr + '=' + obj[curr]), [])
.join();
};
export class GeneralTabCtrl {
panelCtrl: any;
/** @ngInject */
constructor($scope: any) {
this.panelCtrl = $scope.ctrl;
const updatePanel = () => {
console.log('panel.render()');
this.panelCtrl.panel.render();
};
const generateValueFromPanel = (scope: any) => {
const { panel } = scope.ctrl;
const panelPropsToTrack = ['title', 'description', 'transparent', 'repeat', 'repeatDirection', 'minSpan'];
const panelPropsString = panelPropsToTrack
.map(prop => prop + '=' + (panel[prop] && panel[prop].toString ? panel[prop].toString() : panel[prop]))
.join();
const panelLinks = panel.links || [];
const panelLinksString = panelLinks.map(obj2string).join();
return panelPropsString + panelLinksString;
};
$scope.$watch(generateValueFromPanel, updatePanel, true);
}
}
/** @ngInject */
export function generalTab() {
'use strict';
return {
restrict: 'E',
templateUrl: 'public/app/features/panel/partials/general_tab.html',
controller: GeneralTabCtrl,
};
}
coreModule.directive('panelGeneralTab', generalTab);

@ -2,5 +2,4 @@ import './panel_directive';
import './query_ctrl';
import './panel_editor_tab';
import './query_editor_row';
import './repeat_option';
import './panellinks/module';

@ -1,49 +0,0 @@
<div class="panel-options-group">
<!-- <div class="panel&#45;option&#45;section__header">Information</div> -->
<div class="panel-options-group__body">
<div class="section">
<div class="gf-form">
<span class="gf-form-label width-7">Title</span>
<input type="text" class="gf-form-input width-25" ng-model='ctrl.panel.title' ng-model-onblur></input>
</div>
<gf-form-switch class="gf-form" label-class="width-7" switch-class="max-width-6" label="Transparent" checked="ctrl.panel.transparent" on-change="ctrl.render()"></gf-form-switch>
</div>
<div class="section">
<div class="gf-form gf-form--v-stretch">
<span class="gf-form-label width-7">Description</span>
<textarea class="gf-form-input width-25" rows="5" ng-model="ctrl.panel.description" ng-model-onblur placeholder="Panel description, supports markdown & links"></textarea>
</div>
</div>
</div>
</div>
<div class="panel-options-group">
<div class="panel-options-group__header">
<div class="panel-options-group__title">Repeating</div>
</div>
<div class="panel-options-group__body">
<div class="section">
<div class="gf-form">
<span class="gf-form-label width-9">Repeat</span>
<dash-repeat-option panel="ctrl.panel"></dash-repeat-option>
</div>
<div class="gf-form" ng-show="ctrl.panel.repeat">
<span class="gf-form-label width-9">Direction</span>
<select class="gf-form-input" ng-model="ctrl.panel.repeatDirection" ng-options="f.value as f.text for f in [{value: 'v', text: 'Vertical'}, {value: 'h', text: 'Horizontal'}]">
<option value=""></option>
</select>
</div>
<div class="gf-form" ng-show="ctrl.panel.repeat && ctrl.panel.repeatDirection == 'h'">
<span class="gf-form-label width-9">Max per row</span>
<select class="gf-form-input" ng-model="ctrl.panel.maxPerRow" ng-options="f for f in [2,3,4,6,12,24]">
<option value=""></option>
</select>
</div>
<div class="gf-form-hint">
<div class="gf-form-hint-text muted">
Note: You may need to change the variable selection to see this in action.
</div>
</div>
</div>
</div>
</div>

@ -1,59 +0,0 @@
import { coreModule } from 'app/core/core';
import { VariableSrv } from 'app/features/templating/variable_srv';
import { getConfig } from '../../core/config';
import { getVariables } from '../variables/state/selectors';
const template = `
<div class="gf-form-select-wrapper max-width-18">
<select class="gf-form-input" ng-model="panel.repeat" ng-options="f.value as f.text for f in variables" ng-change="optionChanged()">
<option value=""></option>
</div>
`;
/** @ngInject */
function dashRepeatOptionDirective(variableSrv: VariableSrv) {
return {
restrict: 'E',
template: template,
scope: {
panel: '=',
},
link: (scope: any, element: JQuery) => {
element.css({ display: 'block', width: '100%' });
if (getConfig().featureToggles.newVariables) {
scope.variables = getVariables().map((item: any) => {
return { text: item.name, value: item.name };
});
}
if (!getConfig().featureToggles.newVariables) {
scope.variables = variableSrv.variables.map((item: any) => {
return { text: item.name, value: item.name };
});
}
if (scope.variables.length === 0) {
scope.variables.unshift({
text: 'No template variables found',
value: null,
});
}
scope.variables.unshift({ text: 'Disabled', value: null });
// if repeat is set and no direction set to horizontal
if (scope.panel.repeat && !scope.panel.repeatDirection) {
scope.panel.repeatDirection = 'h';
}
scope.optionChanged = () => {
if (scope.panel.repeat) {
scope.panel.repeatDirection = 'h';
}
};
},
};
}
coreModule.directive('dashRepeatOption', dashRepeatOptionDirective);

@ -12,7 +12,7 @@ import { TemplateSrv } from '../templating/template_srv';
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
// Pretend Datasource
import { expressionDatasource } from 'app/features/expressions/ExpressionDatasource';
import { DataSourceVariableModel } from '../templating/types';
import { DataSourceVariableModel } from '../variables/types';
export class DatasourceSrv implements DataSourceService {
datasources: Record<string, DataSourceApi> = {};

@ -2,7 +2,7 @@ import coreModule from 'app/core/core_module';
import { importDataSourcePlugin } from './plugin_loader';
import React from 'react';
import ReactDOM from 'react-dom';
import DefaultVariableQueryEditor from '../templating/DefaultVariableQueryEditor';
import DefaultVariableQueryEditor from '../variables/editor/DefaultVariableQueryEditor';
import { DataSourcePluginMeta } from '@grafana/data';
import { TemplateSrv } from '../templating/template_srv';

@ -1,71 +0,0 @@
import {
assignModelProperties,
TextBoxVariableModel,
VariableActions,
VariableHide,
VariableOption,
variableTypes,
} from './types';
import { VariableSrv } from './variable_srv';
import { VariableType } from '@grafana/data';
export class TextBoxVariable implements TextBoxVariableModel, VariableActions {
type: VariableType;
name: string;
label: string;
hide: VariableHide;
skipUrlSync: boolean;
query: string;
current: VariableOption;
options: VariableOption[];
defaults: TextBoxVariableModel = {
type: 'textbox',
name: '',
label: '',
hide: VariableHide.dontHide,
query: '',
current: {} as VariableOption,
options: [],
skipUrlSync: false,
};
/** @ngInject */
constructor(private model: any, private variableSrv: VariableSrv) {
assignModelProperties(this, model, this.defaults);
}
getSaveModel() {
assignModelProperties(this.model, this, this.defaults);
return this.model;
}
setValue(option: any) {
this.variableSrv.setOptionAsCurrent(this, option);
}
updateOptions() {
this.options = [{ text: this.query.trim(), value: this.query.trim(), selected: false }];
this.current = this.options[0];
return Promise.resolve();
}
dependsOn(variable: any) {
return false;
}
setValueFromUrl(urlValue: string) {
this.query = urlValue;
return this.variableSrv.setOptionFromUrl(this, urlValue);
}
getValueForUrl() {
return this.current.value;
}
}
// @ts-ignore
variableTypes['textbox'] = {
name: 'Text box',
ctor: TextBoxVariable,
description: 'Define a textbox variable, where users can enter any arbitrary string',
};

@ -1,101 +0,0 @@
import _ from 'lodash';
import {
AdHocVariableFilter,
AdHocVariableModel,
assignModelProperties,
VariableActions,
VariableHide,
variableTypes,
} from './types';
import { VariableType } from '@grafana/data';
export class AdhocVariable implements AdHocVariableModel, VariableActions {
type: VariableType;
name: string;
label: string;
hide: VariableHide;
skipUrlSync: boolean;
filters: AdHocVariableFilter[];
datasource: string;
defaults: AdHocVariableModel = {
type: 'adhoc',
name: '',
label: '',
hide: VariableHide.dontHide,
skipUrlSync: false,
datasource: null,
filters: [],
};
/** @ngInject */
constructor(private model: any) {
assignModelProperties(this, model, this.defaults);
}
setValue(option: any) {
return Promise.resolve();
}
getSaveModel() {
assignModelProperties(this.model, this, this.defaults);
return this.model;
}
updateOptions() {
return Promise.resolve();
}
dependsOn(variable: any) {
return false;
}
setValueFromUrl(urlValue: string[] | string[]) {
if (!_.isArray(urlValue)) {
urlValue = [urlValue];
}
this.filters = urlValue.map(item => {
const values = item.split('|').map(value => {
return this.unescapeDelimiter(value);
});
return {
key: values[0],
operator: values[1],
value: values[2],
condition: '',
};
});
return Promise.resolve();
}
getValueForUrl() {
return this.filters.map(filter => {
return [filter.key, filter.operator, filter.value]
.map(value => {
return this.escapeDelimiter(value);
})
.join('|');
});
}
escapeDelimiter(value: string) {
return value.replace(/\|/g, '__gfp__');
}
unescapeDelimiter(value: string) {
return value.replace(/__gfp__/g, '|');
}
setFilters(filters: any[]) {
this.filters = filters;
}
}
variableTypes['adhoc'] = {
name: 'Ad hoc filters',
ctor: AdhocVariable,
description: 'Add key/value filters on the fly',
};

@ -1,25 +1,4 @@
import './editor_ctrl';
import coreModule from 'app/core/core_module';
import templateSrv from './template_srv';
import { VariableSrv } from './variable_srv';
import { IntervalVariable } from './interval_variable';
import { QueryVariable } from './query_variable';
import { DatasourceVariable } from './datasource_variable';
import { CustomVariable } from './custom_variable';
import { ConstantVariable } from './constant_variable';
import { AdhocVariable } from './adhoc_variable';
import { TextBoxVariable } from './TextBoxVariable';
coreModule.factory('templateSrv', () => templateSrv);
export {
VariableSrv,
IntervalVariable,
QueryVariable,
DatasourceVariable,
CustomVariable,
ConstantVariable,
AdhocVariable,
TextBoxVariable,
};

@ -1,70 +0,0 @@
import {
assignModelProperties,
ConstantVariableModel,
VariableActions,
VariableHide,
VariableOption,
variableTypes,
} from './types';
import { VariableSrv } from './all';
import { VariableType } from '@grafana/data';
export class ConstantVariable implements ConstantVariableModel, VariableActions {
type: VariableType;
name: string;
label: string;
hide: VariableHide;
skipUrlSync: boolean;
query: string;
options: VariableOption[];
current: VariableOption;
defaults: ConstantVariableModel = {
type: 'constant',
name: '',
hide: VariableHide.hideVariable,
label: '',
query: '',
current: {} as VariableOption,
options: [],
skipUrlSync: false,
};
/** @ngInject */
constructor(private model: any, private variableSrv: VariableSrv) {
assignModelProperties(this, model, this.defaults);
}
getSaveModel() {
assignModelProperties(this.model, this, this.defaults);
return this.model;
}
setValue(option: any) {
this.variableSrv.setOptionAsCurrent(this, option);
}
updateOptions() {
this.options = [{ text: this.query.trim(), value: this.query.trim(), selected: false }];
this.setValue(this.options[0]);
return Promise.resolve();
}
dependsOn(variable: any) {
return false;
}
setValueFromUrl(urlValue: string) {
return this.variableSrv.setOptionFromUrl(this, urlValue);
}
getValueForUrl() {
return this.current.value;
}
}
variableTypes['constant'] = {
name: 'Constant',
ctor: ConstantVariable,
description: 'Define a hidden constant variable, useful for metric prefixes in dashboards you want to share',
};

@ -1,93 +0,0 @@
import _ from 'lodash';
import {
assignModelProperties,
CustomVariableModel,
VariableActions,
VariableHide,
VariableOption,
variableTypes,
} from './types';
import { VariableSrv } from './variable_srv';
import { VariableType } from '@grafana/data';
export class CustomVariable implements CustomVariableModel, VariableActions {
type: VariableType;
name: string;
label: string;
hide: VariableHide;
skipUrlSync: boolean;
query: string;
options: VariableOption[];
includeAll: boolean;
multi: boolean;
current: VariableOption;
allValue: string;
defaults: CustomVariableModel = {
type: 'custom',
name: '',
label: '',
hide: VariableHide.dontHide,
skipUrlSync: false,
query: '',
options: [],
includeAll: false,
multi: false,
current: {} as VariableOption,
allValue: null,
};
/** @ngInject */
constructor(private model: any, private variableSrv: VariableSrv) {
assignModelProperties(this, model, this.defaults);
}
setValue(option: any) {
return this.variableSrv.setOptionAsCurrent(this, option);
}
getSaveModel() {
assignModelProperties(this.model, this, this.defaults);
return this.model;
}
updateOptions() {
// extract options in comma separated string (use backslash to escape wanted commas)
this.options = _.map(this.query.match(/(?:\\,|[^,])+/g), text => {
text = text.replace(/\\,/g, ',');
return { text: text.trim(), value: text.trim(), selected: false };
});
if (this.includeAll) {
this.addAllOption();
}
return this.variableSrv.validateVariableSelectionState(this);
}
addAllOption() {
this.options.unshift({ text: 'All', value: '$__all', selected: false });
}
dependsOn(variable: any) {
return false;
}
setValueFromUrl(urlValue: string[]) {
return this.variableSrv.setOptionFromUrl(this, urlValue);
}
getValueForUrl() {
if (this.current.text === 'All') {
return 'All';
}
return this.current.value;
}
}
variableTypes['custom'] = {
name: 'Custom',
ctor: CustomVariable,
description: 'Define variable values manually',
supportsMulti: true,
};

@ -1,133 +0,0 @@
import {
assignModelProperties,
DataSourceVariableModel,
VariableActions,
VariableHide,
VariableOption,
VariableRefresh,
variableTypes,
} from './types';
import { VariableType, stringToJsRegex } from '@grafana/data';
import { VariableSrv } from './variable_srv';
import { TemplateSrv } from './template_srv';
import { DatasourceSrv } from '../plugins/datasource_srv';
import { config } from '@grafana/runtime';
import { containsVariable } from './utils';
export class DatasourceVariable implements DataSourceVariableModel, VariableActions {
type: VariableType;
name: string;
label: string;
hide: VariableHide;
regex: any;
query: string;
options: VariableOption[];
current: VariableOption;
multi: boolean;
includeAll: boolean;
refresh: VariableRefresh;
skipUrlSync: boolean;
defaults: DataSourceVariableModel = {
type: 'datasource',
name: '',
hide: 0,
label: '',
current: {} as VariableOption,
regex: '',
options: [],
query: '',
multi: false,
includeAll: false,
refresh: 1,
skipUrlSync: false,
};
/** @ngInject */
constructor(
private model: any,
private datasourceSrv: DatasourceSrv,
private variableSrv: VariableSrv,
private templateSrv: TemplateSrv
) {
assignModelProperties(this, model, this.defaults);
this.refresh = 1;
}
getSaveModel() {
assignModelProperties(this.model, this, this.defaults);
// don't persist options
this.model.options = [];
return this.model;
}
setValue(option: any) {
return this.variableSrv.setOptionAsCurrent(this, option);
}
updateOptions() {
const options: VariableOption[] = [];
const sources = this.datasourceSrv.getMetricSources({ skipVariables: true });
let regex;
if (this.regex) {
regex = this.templateSrv.replace(this.regex, undefined, 'regex');
regex = stringToJsRegex(regex);
}
for (let i = 0; i < sources.length; i++) {
const source = sources[i];
// must match on type
if (source.meta.id !== this.query) {
continue;
}
if (regex && !regex.exec(source.name)) {
continue;
}
options.push({ text: source.name, value: source.name, selected: false });
}
if (options.length === 0) {
options.push({ text: 'No data sources found', value: '', selected: false });
}
this.options = options;
if (this.includeAll) {
this.addAllOption();
}
const { defaultDatasource } = config.bootData.settings;
return this.variableSrv.validateVariableSelectionState(this, defaultDatasource);
}
addAllOption() {
this.options.unshift({ text: 'All', value: '$__all', selected: false });
}
dependsOn(variable: any) {
if (this.regex) {
return containsVariable(this.regex, variable.name);
}
return false;
}
setValueFromUrl(urlValue: string | string[]) {
return this.variableSrv.setOptionFromUrl(this, urlValue);
}
getValueForUrl() {
if (this.current.text === 'All') {
return 'All';
}
return this.current.value;
}
}
variableTypes['datasource'] = {
name: 'Datasource',
ctor: DatasourceVariable,
supportsMulti: true,
description: 'Enabled you to dynamically switch the datasource for multiple panels',
};

@ -1,247 +0,0 @@
import _ from 'lodash';
import { AppEvents } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import coreModule from 'app/core/core_module';
import { variableTypes } from './types';
import appEvents from 'app/core/app_events';
import DatasourceSrv from '../plugins/datasource_srv';
import { VariableSrv } from './all';
import { TemplateSrv } from './template_srv';
import { promiseToDigest } from '../../core/utils/promiseToDigest';
export class VariableEditorCtrl {
/** @ngInject */
constructor($scope: any, datasourceSrv: DatasourceSrv, variableSrv: VariableSrv, templateSrv: TemplateSrv) {
$scope.variableTypes = variableTypes;
$scope.ctrl = {};
$scope.namePattern = /^(?!__).*$/;
$scope._ = _;
$scope.optionsLimit = 20;
$scope.emptyListCta = {
title: 'There are no variables yet',
buttonTitle: 'Add variable',
buttonIcon: 'gicon gicon-variable',
infoBox: {
__html: ` <p>
Variables enable more interactive and dynamic dashboards. Instead of hard-coding things like server or
sensor names in your metric queries you can use variables in their place. Variables are shown as dropdown
select boxes at the top of the dashboard. These dropdowns make it easy to change the data being displayed in
your dashboard. Check out the
<a class="external-link" href="http://docs.grafana.org/reference/templating/" target="_blank">
Templating documentation
</a>
for more information.
</p>`,
infoBoxTitle: 'What do variables do?',
},
};
$scope.refreshOptions = [
{ value: 0, text: 'Never' },
{ value: 1, text: 'On Dashboard Load' },
{ value: 2, text: 'On Time Range Change' },
];
$scope.sortOptions = [
{ value: 0, text: 'Disabled' },
{ value: 1, text: 'Alphabetical (asc)' },
{ value: 2, text: 'Alphabetical (desc)' },
{ value: 3, text: 'Numerical (asc)' },
{ value: 4, text: 'Numerical (desc)' },
{ value: 5, text: 'Alphabetical (case-insensitive, asc)' },
{ value: 6, text: 'Alphabetical (case-insensitive, desc)' },
];
$scope.hideOptions = [
{ value: 0, text: '' },
{ value: 1, text: 'Label' },
{ value: 2, text: 'Variable' },
];
$scope.selectors = {
...selectors.pages.Dashboard.Settings.Variables.List,
...selectors.pages.Dashboard.Settings.Variables.Edit.General,
...selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable,
...selectors.pages.Dashboard.Settings.Variables.Edit.ConstantVariable,
};
$scope.init = () => {
$scope.mode = 'list';
$scope.variables = variableSrv.variables;
$scope.reset();
$scope.$watch('mode', (val: string) => {
if (val === 'new') {
$scope.reset();
}
});
};
$scope.setMode = (mode: any) => {
$scope.mode = mode;
};
$scope.setNewMode = () => {
$scope.setMode('new');
};
$scope.add = () => {
if ($scope.isValid()) {
variableSrv.addVariable($scope.current);
$scope.update();
}
};
$scope.isValid = () => {
if (!$scope.ctrl.form.$valid) {
return false;
}
if (!$scope.current.name.match(/^\w+$/)) {
appEvents.emit(AppEvents.alertWarning, [
'Validation',
'Only word and digit characters are allowed in variable names',
]);
return false;
}
const sameName: any = _.find($scope.variables, { name: $scope.current.name });
if (sameName && sameName !== $scope.current) {
appEvents.emit(AppEvents.alertWarning, ['Validation', 'Variable with the same name already exists']);
return false;
}
if (
$scope.current.type === 'query' &&
_.isString($scope.current.query) &&
$scope.current.query.match(new RegExp('\\$' + $scope.current.name + '(/| |$)'))
) {
appEvents.emit(AppEvents.alertWarning, [
'Validation',
'Query cannot contain a reference to itself. Variable: $' + $scope.current.name,
]);
return false;
}
return true;
};
$scope.validate = () => {
$scope.infoText = '';
if ($scope.current.type === 'adhoc' && $scope.current.datasource !== null) {
$scope.infoText = 'Adhoc filters are applied automatically to all queries that target this datasource';
promiseToDigest($scope)(
datasourceSrv.get($scope.current.datasource).then(ds => {
if (!ds.getTagKeys) {
$scope.infoText = 'This datasource does not support adhoc filters yet.';
}
})
);
}
};
$scope.runQuery = () => {
$scope.optionsLimit = 20;
return variableSrv.updateOptions($scope.current).catch((err: { data: { message: any }; message: string }) => {
if (err.data && err.data.message) {
err.message = err.data.message;
}
appEvents.emit(AppEvents.alertError, [
'Templating',
'Template variables could not be initialized: ' + err.message,
]);
});
};
$scope.onQueryChange = (query: any, definition: any) => {
$scope.current.query = query;
$scope.current.definition = definition;
$scope.runQuery();
};
$scope.edit = (variable: any) => {
$scope.current = variable;
$scope.currentIsNew = false;
$scope.mode = 'edit';
$scope.validate();
promiseToDigest($scope)(
datasourceSrv.get($scope.current.datasource).then(ds => {
$scope.currentDatasource = ds;
})
);
};
$scope.duplicate = (variable: { getSaveModel: () => void; name: string }) => {
const clone = _.cloneDeep(variable.getSaveModel());
$scope.current = variableSrv.createVariableFromModel(clone);
$scope.current.name = 'copy_of_' + variable.name;
variableSrv.addVariable($scope.current);
};
$scope.update = () => {
if ($scope.isValid()) {
promiseToDigest($scope)(
$scope.runQuery().then(() => {
$scope.reset();
$scope.mode = 'list';
templateSrv.updateIndex();
})
);
}
};
$scope.reset = () => {
$scope.currentIsNew = true;
$scope.current = variableSrv.createVariableFromModel({ type: 'query' });
// this is done here in case a new data source type variable was added
$scope.datasources = _.filter(datasourceSrv.getMetricSources(), ds => {
return !ds.meta.mixed && ds.value !== null;
});
$scope.datasourceTypes = _($scope.datasources)
.uniqBy('meta.id')
.map((ds: any) => {
return { text: ds.meta.name, value: ds.meta.id };
})
.value();
};
$scope.typeChanged = function() {
const old = $scope.current;
$scope.current = variableSrv.createVariableFromModel({
type: $scope.current.type,
});
$scope.current.name = old.name;
$scope.current.label = old.label;
const oldIndex = _.indexOf(this.variables, old);
if (oldIndex !== -1) {
this.variables[oldIndex] = $scope.current;
}
$scope.validate();
};
$scope.removeVariable = (variable: any) => {
variableSrv.removeVariable(variable);
};
$scope.showMoreOptions = () => {
$scope.optionsLimit += 20;
};
$scope.datasourceChanged = async () => {
promiseToDigest($scope)(
datasourceSrv.get($scope.current.datasource).then(ds => {
$scope.current.query = '';
$scope.currentDatasource = ds;
})
);
};
}
}
coreModule.controller('VariableEditorCtrl', VariableEditorCtrl);

@ -1,117 +0,0 @@
import _ from 'lodash';
import kbn from 'app/core/utils/kbn';
import {
assignModelProperties,
IntervalVariableModel,
VariableActions,
VariableHide,
VariableOption,
VariableRefresh,
variableTypes,
} from './types';
import { TimeSrv } from '../dashboard/services/TimeSrv';
import { TemplateSrv } from './template_srv';
import { VariableSrv } from './variable_srv';
import { VariableType } from '@grafana/data';
export class IntervalVariable implements IntervalVariableModel, VariableActions {
type: VariableType;
name: string;
label: string;
hide: VariableHide;
skipUrlSync: boolean;
auto_count: number; // eslint-disable-line camelcase
auto_min: string; // eslint-disable-line camelcase
options: VariableOption[];
auto: boolean;
query: string;
refresh: VariableRefresh;
current: VariableOption;
defaults: IntervalVariableModel = {
type: 'interval',
name: '',
label: '',
hide: VariableHide.dontHide,
skipUrlSync: false,
auto_count: 30,
auto_min: '10s',
options: [],
auto: false,
query: '1m,10m,30m,1h,6h,12h,1d,7d,14d,30d',
refresh: VariableRefresh.onTimeRangeChanged,
current: {} as VariableOption,
};
/** @ngInject */
constructor(
private model: any,
private timeSrv: TimeSrv,
private templateSrv: TemplateSrv,
private variableSrv: VariableSrv
) {
assignModelProperties(this, model, this.defaults);
this.refresh = VariableRefresh.onTimeRangeChanged;
}
getSaveModel() {
assignModelProperties(this.model, this, this.defaults);
return this.model;
}
setValue(option: any) {
this.updateAutoValue();
return this.variableSrv.setOptionAsCurrent(this, option);
}
updateAutoValue() {
if (!this.auto) {
return;
}
// add auto option if missing
if (this.options.length && this.options[0].text !== 'auto') {
this.options.unshift({
text: 'auto',
value: '$__auto_interval_' + this.name,
selected: false,
});
}
const res = kbn.calculateInterval(this.timeSrv.timeRange(), this.auto_count, this.auto_min);
this.templateSrv.setGrafanaVariable('$__auto_interval_' + this.name, res.interval);
// for backward compatibility, to be removed eventually
this.templateSrv.setGrafanaVariable('$__auto_interval', res.interval);
}
updateOptions() {
// extract options between quotes and/or comma
this.options = _.map(this.query.match(/(["'])(.*?)\1|\w+/g), text => {
text = text.replace(/["']+/g, '');
return { text: text.trim(), value: text.trim(), selected: false };
});
this.updateAutoValue();
return this.variableSrv.validateVariableSelectionState(this);
}
dependsOn(variable: any) {
return false;
}
setValueFromUrl(urlValue: string | string[]) {
this.updateAutoValue();
return this.variableSrv.setOptionFromUrl(this, urlValue);
}
getValueForUrl() {
return this.current.value;
}
}
// @ts-ignore
variableTypes['interval'] = {
name: 'Interval',
ctor: IntervalVariable,
description: 'Define a timespan interval (ex 1m, 1h, 1d)',
};

@ -1,526 +0,0 @@
<div ng-controller="VariableEditorCtrl" ng-init="init()">
<div class="page-action-bar">
<h3 class="dashboard-settings__header">
<a ng-click="setMode('list')" aria-label="{{::selectors.headerLink}}">Variables</a>
<span ng-show="mode === 'new'"
><icon name="'angle-right'" aria-label="{{::selectors.modeLabelNew}}"></icon> New</span
>
<span ng-show="mode === 'edit'"
><icon name="'angle-right'" aria-label="{{::selectors.modeLabelEdit}}"></icon> Edit</span
>
</h3>
<div class="page-action-bar__spacer"></div>
<a
type="button"
class="btn btn-primary"
ng-click="setMode('new');"
ng-if="variables.length > 0"
ng-hide="mode === 'edit' || mode === 'new'"
aria-label="{{::selectors.newButton}}"
>
New
</a>
</div>
<div ng-if="mode === 'list'">
<div ng-if="variables.length === 0">
<empty-list-cta
on-click="setNewMode"
title="emptyListCta.title"
infoBox="emptyListCta.infoBox"
infoBoxTitle="emptyListCta.infoBoxTitle"
buttonTitle="emptyListCta.buttonTitle"
buttonIcon="emptyListCta.buttonIcon"
/>
</div>
<div ng-if="variables.length">
<table class="filter-table filter-table--hover" aria-label="{{::selectors.table}}">
<thead>
<tr>
<th>Variable</th>
<th>Definition</th>
<th colspan="5"></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="variable in variables">
<td style="width: 1%;">
<span
ng-click="edit(variable)"
class="pointer template-variable"
aria-label="{{::selectors.tableRowNameFields(variable.name)}}"
>
${{ variable.name }}
</span>
</td>
<td
style="max-width: 200px;"
ng-click="edit(variable)"
class="pointer max-width"
aria-label="{{::selectors.tableRowDefinitionFields(variable.name)}}"
>
{{ variable.definition ? variable.definition : variable.query }}
</td>
<td style="width: 1%;">
<icon
ng-click="_.move(variables,$index,$index-1)"
ng-hide="$first"
name="'arrow-up'"
aria-label="{{::selectors.tableRowArrowUpButtons(variable.name)}}"
></icon>
</td>
<td style="width: 1%;">
<icon
ng-click="_.move(variables,$index,$index+1)"
ng-hide="$last"
name="'arrow-down'"
aria-label="{{::selectors.tableRowArrowDownButtons(variable.name)}}"
></icon>
</td>
<td style="width: 1%;">
<a
ng-click="duplicate(variable)"
class="btn btn-inverse btn-small"
aria-label="{{::selectors.tableRowDuplicateButtons(variable.name)}}"
>
Duplicate
</a>
</td>
<td style="width: 1%;">
<a
ng-click="removeVariable(variable)"
class="btn btn-danger btn-small"
aria-label="{{::selectors.tableRowRemoveButtons(variable.name)}}"
>
<icon name="'times'" style="margin-bottom: 0;"></icon>
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<form ng-if="mode === 'edit' || mode === 'new'" name="ctrl.form" aria-label="Variable editor Form">
<h5 class="section-heading">General</h5>
<div class="gf-form-group">
<div class="gf-form-inline">
<div class="gf-form max-width-19">
<span class="gf-form-label width-6">Name</span>
<input
type="text"
class="gf-form-input"
name="name"
placeholder="name"
ng-model="current.name"
required
ng-pattern="namePattern"
aria-label="{{::selectors.generalNameInput}}"
/>
</div>
<div class="gf-form max-width-19">
<span class="gf-form-label width-6">
Type
<info-popover mode="right-normal">
{{ variableTypes[current.type].description }}
</info-popover>
</span>
<div class="gf-form-select-wrapper max-width-17">
<select
class="gf-form-input"
ng-model="current.type"
ng-options="k as v.name for (k, v) in variableTypes"
ng-change="typeChanged()"
aria-label="{{::selectors.generalTypeSelect}}"
></select>
</div>
</div>
</div>
<div class="gf-form" ng-show="ctrl.form.name.$error.pattern">
<span class="gf-form-label gf-form-label--error"
>Template names cannot begin with '__', that's reserved for Grafana's global variables</span
>
</div>
<div class="gf-form-inline">
<div class="gf-form max-width-19">
<span class="gf-form-label width-6">Label</span>
<input
type="text"
class="gf-form-input"
ng-model="current.label"
placeholder="optional display name"
aria-label="{{::selectors.generalLabelInput}}"
/>
</div>
<div class="gf-form max-width-19">
<span class="gf-form-label width-6">Hide</span>
<div class="gf-form-select-wrapper max-width-15">
<select
class="gf-form-input"
ng-model="current.hide"
ng-options="f.value as f.text for f in hideOptions"
aria-label="{{::selectors.generalHideSelect}}"
></select>
</div>
</div>
</div>
</div>
<div ng-if="current.type === 'interval'" class="gf-form-group">
<h5 class="section-heading">Interval Options</h5>
<div class="gf-form">
<span class="gf-form-label width-9">Values</span>
<input
type="text"
class="gf-form-input"
ng-model="current.query"
placeholder="1m,10m,1h,6h,1d,7d"
ng-model-onblur
ng-change="runQuery()"
required
aria-label="Variable editor Form Interval Query field"
/>
</div>
<div class="gf-form-inline">
<gf-form-switch
class="gf-form"
label="Auto Option"
label-class="width-9"
checked="current.auto"
on-change="runQuery()"
aria-label="Variable editor Form Interval AutoOption switch"
>
</gf-form-switch>
<div class="gf-form">
<span class="gf-form-label width-9" ng-show="current.auto">
Step count <tip>How many times should the current time range be divided to calculate the value</tip>
</span>
<div class="gf-form-select-wrapper max-width-10" ng-show="current.auto">
<select
class="gf-form-input"
ng-model="current.auto_count"
ng-options="f for f in [1,2,3,4,5,10,20,30,40,50,100,200,300,400,500]"
ng-change="runQuery()"
aria-label="Variable editor Form Interval AutoCount select"
></select>
</div>
</div>
<div class="gf-form">
<span class="gf-form-label" ng-show="current.auto">
Min interval <tip>The calculated value will not go below this threshold</tip>
</span>
<input
type="text"
class="gf-form-input max-width-10"
ng-show="current.auto"
ng-model="current.auto_min"
ng-change="runQuery()"
placeholder="10s"
aria-label="Variable editor Form Interval AutoMin field"
/>
</div>
</div>
</div>
<div ng-if="current.type === 'custom'" class="gf-form-group">
<h5 class="section-heading">Custom Options</h5>
<div class="gf-form">
<span class="gf-form-label width-14">Values separated by comma</span>
<input
type="text"
class="gf-form-input"
ng-model="current.query"
ng-blur="runQuery()"
placeholder="1, 10, 20, myvalue, escaped\,value"
required
aria-label="Variable editor Form Custom Query field"
/>
</div>
</div>
<div ng-if="current.type === 'constant'" class="gf-form-group">
<h5 class="section-heading">Constant options</h5>
<div class="gf-form">
<span class="gf-form-label">Value</span>
<input
type="text"
class="gf-form-input"
ng-model="current.query"
ng-blur="runQuery()"
placeholder="your metric prefix"
aria-label="{{::selectors.constantOptionsQueryInput}}"
/>
</div>
</div>
<div ng-if="current.type === 'textbox'" class="gf-form-group">
<h5 class="section-heading">Text options</h5>
<div class="gf-form">
<span class="gf-form-label">Default value</span>
<input
type="text"
class="gf-form-input"
ng-model="current.query"
ng-blur="runQuery()"
placeholder="default value, if any"
aria-label="Variable editor Form TextBox Query field"
/>
</div>
</div>
<div ng-if="current.type === 'query'" class="gf-form-group">
<h5 class="section-heading">Query Options</h5>
<div class="gf-form-inline">
<div class="gf-form max-width-21">
<span class="gf-form-label width-10">Data source</span>
<div class="gf-form-select-wrapper max-width-14">
<select
class="gf-form-input"
ng-model="current.datasource"
ng-options="f.value as f.name for f in datasources"
ng-change="datasourceChanged()"
required
aria-label="{{::selectors.queryOptionsDataSourceSelect}}"
>
<option value="" ng-if="false"></option>
</select>
</div>
</div>
<div class="gf-form max-width-22">
<span class="gf-form-label width-10">
Refresh
<info-popover mode="right-normal">
When to update the values of this variable.
</info-popover>
</span>
<div class="gf-form-select-wrapper width-15">
<select
class="gf-form-input"
ng-model="current.refresh"
ng-options="f.value as f.text for f in refreshOptions"
aria-label="{{::selectors.queryOptionsRefreshSelect}}"
></select>
</div>
</div>
</div>
<rebuild-on-change property="currentDatasource">
<variable-query-editor-loader> </variable-query-editor-loader>
</rebuild-on-change>
<div class="gf-form">
<span class="gf-form-label width-10">
Regex
<info-popover mode="right-normal">
Optional, if you want to extract part of a series name or metric node segment.
</info-popover>
</span>
<input
type="text"
class="gf-form-input"
ng-model="current.regex"
placeholder="/.*-(.*)-.*/"
ng-model-onblur
ng-change="runQuery()"
aria-label="{{::selectors.queryOptionsRegExInput}}"
/>
</div>
<div class="gf-form max-width-21">
<span class="gf-form-label width-10">
Sort
<info-popover mode="right-normal">
How to sort the values of this variable.
</info-popover>
</span>
<div class="gf-form-select-wrapper max-width-14">
<select
class="gf-form-input"
ng-model="current.sort"
ng-options="f.value as f.text for f in sortOptions"
ng-change="runQuery()"
aria-label="{{::selectors.queryOptionsSortSelect}}"
></select>
</div>
</div>
</div>
<div ng-show="current.type === 'datasource'" class="gf-form-group">
<h5 class="section-heading">Data source options</h5>
<div class="gf-form">
<label class="gf-form-label width-12">Type</label>
<div class="gf-form-select-wrapper max-width-18">
<select
class="gf-form-input"
ng-model="current.query"
ng-options="f.value as f.text for f in datasourceTypes"
ng-change="runQuery()"
aria-label="Variable editor Form DataSource Query field"
></select>
</div>
</div>
<div class="gf-form">
<label class="gf-form-label width-12">
Instance name filter
<info-popover mode="right-normal">
Regex filter for which data source instances to choose from in the variable value dropdown. Leave empty for
all.
<br /><br />
Example: <code>/^prod/</code>
</info-popover>
</label>
<input
type="text"
class="gf-form-input max-width-18"
ng-model="current.regex"
placeholder="/.*-(.*)-.*/"
ng-model-onblur
ng-change="runQuery()"
aria-label="Variable editor Form DataSource RegEx field"
/>
</div>
</div>
<div ng-if="current.type === 'adhoc'" class="gf-form-group">
<h5 class="section-heading">Options</h5>
<div class="gf-form max-width-21">
<span class="gf-form-label width-8">Data source</span>
<div class="gf-form-select-wrapper max-width-14">
<select
class="gf-form-input"
ng-model="current.datasource"
ng-options="f.value as f.name for f in datasources"
required
ng-change="validate()"
aria-label="Variable editor Form AdHoc DataSource select"
>
<option value="" ng-if="false"></option>
</select>
</div>
</div>
</div>
<div class="section gf-form-group" ng-show="variableTypes[current.type].supportsMulti">
<h5 class="section-heading">Selection Options</h5>
<div class="section">
<gf-form-switch
class="gf-form"
label="Multi-value"
label-class="width-10"
tooltip="Enables multiple values to be selected at the same time"
checked="current.multi"
on-change="runQuery()"
aria-label="{{::selectors.selectionOptionsMultiSwitch}}"
>
</gf-form-switch>
<gf-form-switch
class="gf-form"
label="Include All option"
label-class="width-10"
checked="current.includeAll"
on-change="runQuery()"
aria-label="{{::selectors.selectionOptionsIncludeAllSwitch}}"
>
</gf-form-switch>
</div>
<div class="gf-form" ng-if="current.includeAll">
<span class="gf-form-label width-10">Custom all value</span>
<input
type="text"
class="gf-form-input max-width-15"
ng-model="current.allValue"
placeholder="blank = auto"
aria-label="{{::selectors.selectionOptionsCustomAllInput}}"
/>
</div>
</div>
<div class="gf-form-group" ng-if="current.type === 'query'">
<h5>Value groups/tags (Experimental feature)</h5>
<gf-form-switch
class="gf-form"
label="Enabled"
label-class="width-10"
checked="current.useTags"
on-change="runQuery()"
aria-label="{{::selectors.valueGroupsTagsEnabledSwitch}}"
>
</gf-form-switch>
<div class="gf-form last" ng-if="current.useTags">
<span class="gf-form-label width-10">Tags query</span>
<input
type="text"
class="gf-form-input"
ng-model="current.tagsQuery"
placeholder="metric name or tags query"
ng-model-onblur
aria-label="{{::selectors.valueGroupsTagsTagsQueryInput}}"
/>
</div>
<div class="gf-form" ng-if="current.useTags">
<li class="gf-form-label width-10">Tag values query</li>
<input
type="text"
class="gf-form-input"
ng-model="current.tagValuesQuery"
placeholder="apps.$tag.*"
ng-model-onblur
aria-label="{{::selectors.valueGroupsTagsTagsValuesQueryInput}}"
/>
</div>
</div>
<div class="gf-form-group" ng-show="current.options.length">
<h5>Preview of values</h5>
<div class="gf-form-inline">
<div class="gf-form" ng-repeat="option in current.options | limitTo: optionsLimit">
<span class="gf-form-label" aria-label="{{::selectors.previewOfValuesOption}}">{{ option.text }}</span>
</div>
<div class="gf-form" ng-if="current.options.length > optionsLimit">
<a
class="gf-form-label btn-secondary"
ng-click="showMoreOptions()"
aria-label="Variable editor Preview of Values Show More link"
>
Show more
</a>
</div>
</div>
</div>
<div class="alert alert-info gf-form-group" ng-if="infoText" aria-label="Variable editor Form Alert">
{{ infoText }}
</div>
<div class="gf-form-button-row p-y-0">
<button
type="submit"
class="btn btn-primary"
ng-show="mode === 'edit'"
ng-click="update();"
aria-label="{{::selectors.updateButton}}"
>
Update
</button>
<button
type="submit"
class="btn btn-primary"
ng-show="mode === 'new'"
ng-click="add();"
aria-label="{{::selectors.addButton}}"
>
Add
</button>
</div>
</form>
</div>

@ -1,254 +0,0 @@
import _ from 'lodash';
import {
assignModelProperties,
QueryVariableModel,
VariableActions,
VariableHide,
VariableOption,
VariableRefresh,
VariableSort,
VariableTag,
variableTypes,
} from './types';
import { VariableType, DataSourceApi, stringToJsRegex } from '@grafana/data';
import DatasourceSrv from '../plugins/datasource_srv';
import { TemplateSrv } from './template_srv';
import { VariableSrv } from './variable_srv';
import { TimeSrv } from '../dashboard/services/TimeSrv';
import { containsVariable } from './utils';
function getNoneOption(): VariableOption {
return { text: 'None', value: '', isNone: true, selected: false };
}
export class QueryVariable implements QueryVariableModel, VariableActions {
type: VariableType;
name: string;
label: string | null;
hide: VariableHide;
skipUrlSync: boolean;
datasource: string | null;
query: string;
regex: string;
sort: VariableSort;
options: VariableOption[];
current: VariableOption;
refresh: VariableRefresh;
multi: boolean;
includeAll: boolean;
useTags: boolean;
tagsQuery: string;
tagValuesQuery: string;
tags: VariableTag[];
definition: string;
allValue: string;
index: number;
defaults: QueryVariableModel = {
type: 'query',
name: '',
label: null,
hide: VariableHide.dontHide,
skipUrlSync: false,
datasource: null,
query: '',
regex: '',
sort: VariableSort.disabled,
refresh: VariableRefresh.never,
multi: false,
includeAll: false,
allValue: null,
options: [],
current: {} as VariableOption,
tags: [],
useTags: false,
tagsQuery: '',
tagValuesQuery: '',
definition: '',
index: -1,
};
/** @ngInject */
constructor(
private model: any,
private datasourceSrv: DatasourceSrv,
private templateSrv: TemplateSrv,
private variableSrv: VariableSrv,
private timeSrv: TimeSrv
) {
// copy model properties to this instance
assignModelProperties(this, model, this.defaults);
this.updateOptionsFromMetricFindQuery.bind(this);
}
getSaveModel() {
// copy back model properties to model
assignModelProperties(this.model, this, this.defaults);
// remove options
if (this.refresh !== 0) {
this.model.options = [];
}
return this.model;
}
setValue(option: any) {
return this.variableSrv.setOptionAsCurrent(this, option);
}
setValueFromUrl(urlValue: any) {
return this.variableSrv.setOptionFromUrl(this, urlValue);
}
getValueForUrl() {
if (this.current.text === 'All') {
return 'All';
}
return this.current.value;
}
updateOptions(searchFilter?: string) {
return this.datasourceSrv
.get(this.datasource ?? '')
.then((ds: DataSourceApi) => this.updateOptionsFromMetricFindQuery(ds, searchFilter))
.then(this.updateTags.bind(this))
.then(this.variableSrv.validateVariableSelectionState.bind(this.variableSrv, this));
}
updateTags(datasource: any) {
if (this.useTags) {
return this.metricFindQuery(datasource, this.tagsQuery).then((results: any[]) => {
this.tags = [];
for (let i = 0; i < results.length; i++) {
this.tags.push(results[i].text);
}
return datasource;
});
} else {
delete this.tags;
}
return datasource;
}
getValuesForTag(tagKey: string) {
return this.datasourceSrv.get(this.datasource ?? '').then((datasource: DataSourceApi) => {
const query = this.tagValuesQuery.replace('$tag', tagKey);
return this.metricFindQuery(datasource, query).then((results: any) => {
return _.map(results, value => {
return value.text;
});
});
});
}
updateOptionsFromMetricFindQuery(datasource: any, searchFilter?: string) {
return this.metricFindQuery(datasource, this.query, searchFilter).then((results: any) => {
this.options = this.metricNamesToVariableValues(results);
if (this.includeAll) {
this.addAllOption();
}
if (!this.options.length) {
this.options.push(getNoneOption());
}
return datasource;
});
}
metricFindQuery(datasource: any, query: string, searchFilter?: string) {
const options: any = { range: undefined, variable: this, searchFilter };
if (this.refresh === 2) {
options.range = this.timeSrv.timeRange();
}
return datasource.metricFindQuery(query, options);
}
addAllOption() {
this.options.unshift({ text: 'All', value: '$__all', selected: false });
}
metricNamesToVariableValues(metricNames: any[]) {
let regex, options, i, matches;
options = [];
if (this.regex) {
regex = stringToJsRegex(this.templateSrv.replace(this.regex, {}, 'regex'));
}
for (i = 0; i < metricNames.length; i++) {
const item = metricNames[i];
let text = item.text === undefined || item.text === null ? item.value : item.text;
let value = item.value === undefined || item.value === null ? item.text : item.value;
if (_.isNumber(value)) {
value = value.toString();
}
if (_.isNumber(text)) {
text = text.toString();
}
if (regex) {
matches = regex.exec(value);
if (!matches) {
continue;
}
if (matches.length > 1) {
value = matches[1];
text = matches[1];
}
}
options.push({ text: text, value: value });
}
options = _.uniqBy(options, 'value');
return this.sortVariableValues(options, this.sort);
}
sortVariableValues(options: any[], sortOrder: number) {
if (sortOrder === 0) {
return options;
}
const sortType = Math.ceil(sortOrder / 2);
const reverseSort = sortOrder % 2 === 0;
if (sortType === 1) {
options = _.sortBy(options, 'text');
} else if (sortType === 2) {
options = _.sortBy(options, opt => {
const matches = opt.text.match(/.*?(\d+).*/);
if (!matches || matches.length < 2) {
return -1;
} else {
return parseInt(matches[1], 10);
}
});
} else if (sortType === 3) {
options = _.sortBy(options, opt => {
return _.toLower(opt.text);
});
}
if (reverseSort) {
options = options.reverse();
}
return options;
}
dependsOn(variable: any) {
return containsVariable(this.query, this.datasource, this.regex, variable.name);
}
}
// @ts-ignore
variableTypes['query'] = {
name: 'Query',
ctor: QueryVariable,
description: 'Variable values are fetched from a datasource query',
supportsMulti: true,
};

@ -1,36 +0,0 @@
import { AdhocVariable } from '../adhoc_variable';
describe('AdhocVariable', () => {
describe('when serializing to url', () => {
it('should set return key value and op separated by pipe', () => {
const variable = new AdhocVariable({
filters: [
{ key: 'key1', operator: '=', value: 'value1' },
{ key: 'key2', operator: '!=', value: 'value2' },
{ key: 'key3', operator: '=', value: 'value3a|value3b|value3c' },
],
});
const urlValue = variable.getValueForUrl();
expect(urlValue).toMatchObject(['key1|=|value1', 'key2|!=|value2', 'key3|=|value3a__gfp__value3b__gfp__value3c']);
});
});
describe('when deserializing from url', () => {
it('should restore filters', () => {
const variable = new AdhocVariable({});
variable.setValueFromUrl(['key1|=|value1', 'key2|!=|value2', 'key3|=|value3a__gfp__value3b__gfp__value3c']);
expect(variable.filters[0].key).toBe('key1');
expect(variable.filters[0].operator).toBe('=');
expect(variable.filters[0].value).toBe('value1');
expect(variable.filters[1].key).toBe('key2');
expect(variable.filters[1].operator).toBe('!=');
expect(variable.filters[1].value).toBe('value2');
expect(variable.filters[2].key).toBe('key3');
expect(variable.filters[2].operator).toBe('=');
expect(variable.filters[2].value).toBe('value3a|value3b|value3c');
});
});
});

@ -1,42 +0,0 @@
import { VariableEditorCtrl } from '../editor_ctrl';
import { TemplateSrv } from '../template_srv';
import { AppEvents } from '@grafana/data';
let mockEmit: any;
jest.mock('app/core/app_events', () => {
mockEmit = jest.fn();
return {
emit: mockEmit,
};
});
describe('VariableEditorCtrl', () => {
const scope = {
runQuery: () => {
return Promise.resolve({});
},
};
describe('When running a variable query and the data source returns an error', () => {
beforeEach(() => {
const variableSrv: any = {
updateOptions: () => {
return Promise.reject({
data: { message: 'error' },
});
},
};
return new VariableEditorCtrl(scope, {} as any, variableSrv, {} as TemplateSrv);
});
it('should emit an error', () => {
return scope.runQuery().then(res => {
expect(mockEmit).toBeCalled();
expect(mockEmit.mock.calls[0][0]).toBe(AppEvents.alertError);
expect(mockEmit.mock.calls[0][1][0]).toBe('Templating');
expect(mockEmit.mock.calls[0][1][1]).toBe('Template variables could not be initialized: error');
});
});
});
});

@ -1,133 +0,0 @@
import { QueryVariable } from '../query_variable';
import DatasourceSrv from '../../plugins/datasource_srv';
import { TemplateSrv } from '../template_srv';
import { VariableSrv } from '../variable_srv';
import { TimeSrv } from '../../dashboard/services/TimeSrv';
describe('QueryVariable', () => {
describe('when creating from model', () => {
it('should set defaults', () => {
const variable = new QueryVariable(
{},
(null as unknown) as DatasourceSrv,
(null as unknown) as TemplateSrv,
(null as unknown) as VariableSrv,
(null as unknown) as TimeSrv
);
expect(variable.datasource).toBe(null);
expect(variable.refresh).toBe(0);
expect(variable.sort).toBe(0);
expect(variable.name).toBe('');
expect(variable.hide).toBe(0);
expect(variable.options.length).toBe(0);
expect(variable.multi).toBe(false);
expect(variable.includeAll).toBe(false);
});
it('get model should copy changes back to model', () => {
const variable = new QueryVariable(
{},
(null as unknown) as DatasourceSrv,
(null as unknown) as TemplateSrv,
(null as unknown) as VariableSrv,
(null as unknown) as TimeSrv
);
variable.options = [{ text: 'test', value: '', selected: false }];
variable.datasource = 'google';
variable.regex = 'asd';
variable.sort = 50;
const model = variable.getSaveModel();
expect(model.options.length).toBe(1);
expect(model.options[0].text).toBe('test');
expect(model.datasource).toBe('google');
expect(model.regex).toBe('asd');
expect(model.sort).toBe(50);
});
it('if refresh != 0 then remove options in presisted mode', () => {
const variable = new QueryVariable(
{},
(null as unknown) as DatasourceSrv,
(null as unknown) as TemplateSrv,
(null as unknown) as VariableSrv,
(null as unknown) as TimeSrv
);
variable.options = [{ text: 'test', value: '', selected: false }];
variable.refresh = 1;
const model = variable.getSaveModel();
expect(model.options.length).toBe(0);
});
});
describe('can convert and sort metric names', () => {
const variable = new QueryVariable(
{},
(null as unknown) as DatasourceSrv,
(null as unknown) as TemplateSrv,
(null as unknown) as VariableSrv,
(null as unknown) as TimeSrv
);
let input: any;
beforeEach(() => {
input = [
{ text: '0', value: '0' },
{ text: '1', value: '1' },
{ text: null, value: 3 },
{ text: undefined, value: 4 },
{ text: '5', value: null },
{ text: '6', value: undefined },
{ text: null, value: '7' },
{ text: undefined, value: '8' },
{ text: 9, value: null },
{ text: 10, value: undefined },
{ text: '', value: undefined },
{ text: undefined, value: '' },
];
});
describe('can sort a mixed array of metric variables in numeric order', () => {
let result: any;
beforeEach(() => {
variable.sort = 3; // Numerical (asc)
result = variable.metricNamesToVariableValues(input);
});
it('should return in same order', () => {
let i = 0;
expect(result.length).toBe(11);
expect(result[i++].text).toBe('');
expect(result[i++].text).toBe('0');
expect(result[i++].text).toBe('1');
expect(result[i++].text).toBe('3');
expect(result[i++].text).toBe('4');
expect(result[i++].text).toBe('5');
expect(result[i++].text).toBe('6');
});
});
describe('can sort a mixed array of metric variables in alphabetical order', () => {
let result: any;
beforeEach(() => {
variable.sort = 5; // Alphabetical CI (asc)
result = variable.metricNamesToVariableValues(input);
});
it('should return in same order', () => {
let i = 0;
expect(result.length).toBe(11);
expect(result[i++].text).toBe('');
expect(result[i++].text).toBe('0');
expect(result[i++].text).toBe('1');
expect(result[i++].text).toBe('10');
expect(result[i++].text).toBe('3');
expect(result[i++].text).toBe('4');
expect(result[i++].text).toBe('5');
});
});
});
});

@ -1,609 +0,0 @@
import { TemplateSrv } from '../template_srv';
import { convertToStoreState } from 'test/helpers/convertToStoreState';
import { getTemplateSrvDependencies } from '../../../../test/helpers/getTemplateSrvDependencies';
import { variableAdapters } from '../../variables/adapters';
import { createQueryVariableAdapter } from '../../variables/query/adapter';
describe('templateSrv', () => {
let _templateSrv: any;
function initTemplateSrv(variables: any[]) {
const state = convertToStoreState(variables);
_templateSrv = new TemplateSrv(getTemplateSrvDependencies(state));
_templateSrv.init(variables);
}
describe('init', () => {
beforeEach(() => {
initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'oogle' } }]);
});
it('should initialize template data', () => {
const target = _templateSrv.replace('this.[[test]].filters');
expect(target).toBe('this.oogle.filters');
});
});
describe('replace can pass scoped vars', () => {
beforeEach(() => {
initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'oogle' } }]);
});
it('scoped vars should support objects', () => {
const target = _templateSrv.replace('${series.name} ${series.nested.field}', {
series: { value: { name: 'Server1', nested: { field: 'nested' } } },
});
expect(target).toBe('Server1 nested');
});
it('built in vars should support objects', () => {
_templateSrv.setGlobalVariable('__dashboard', {
value: { name: 'hello' },
});
const target = _templateSrv.replace('${__dashboard.name}');
expect(target).toBe('hello');
});
it('scoped vars should support objects with propert names with dot', () => {
const target = _templateSrv.replace('${series.name} ${series.nested["field.with.dot"]}', {
series: { value: { name: 'Server1', nested: { 'field.with.dot': 'nested' } } },
});
expect(target).toBe('Server1 nested');
});
it('scoped vars should support arrays of objects', () => {
const target = _templateSrv.replace('${series.rows[0].name} ${series.rows[1].name}', {
series: { value: { rows: [{ name: 'first' }, { name: 'second' }] } },
});
expect(target).toBe('first second');
});
it('should replace $test with scoped value', () => {
const target = _templateSrv.replace('this.$test.filters', {
test: { value: 'mupp', text: 'asd' },
});
expect(target).toBe('this.mupp.filters');
});
it('should replace ${test} with scoped value', () => {
const target = _templateSrv.replace('this.${test}.filters', {
test: { value: 'mupp', text: 'asd' },
});
expect(target).toBe('this.mupp.filters');
});
it('should replace ${test:glob} with scoped value', () => {
const target = _templateSrv.replace('this.${test:glob}.filters', {
test: { value: 'mupp', text: 'asd' },
});
expect(target).toBe('this.mupp.filters');
});
it('should replace $test with scoped text', () => {
const target = _templateSrv.replaceWithText('this.$test.filters', {
test: { value: 'mupp', text: 'asd' },
});
expect(target).toBe('this.asd.filters');
});
it('should replace ${test} with scoped text', () => {
const target = _templateSrv.replaceWithText('this.${test}.filters', {
test: { value: 'mupp', text: 'asd' },
});
expect(target).toBe('this.asd.filters');
});
it('should replace ${test:glob} with scoped text', () => {
const target = _templateSrv.replaceWithText('this.${test:glob}.filters', {
test: { value: 'mupp', text: 'asd' },
});
expect(target).toBe('this.asd.filters');
});
});
describe('getAdhocFilters', () => {
beforeEach(() => {
initTemplateSrv([
{
type: 'datasource',
name: 'ds',
current: { value: 'logstash', text: 'logstash' },
},
{ type: 'adhoc', name: 'test', datasource: 'oogle', filters: [1] },
{ type: 'adhoc', name: 'test2', datasource: '$ds', filters: [2] },
]);
});
it('should return filters if datasourceName match', () => {
const filters = _templateSrv.getAdhocFilters('oogle');
expect(filters).toMatchObject([1]);
});
it('should return empty array if datasourceName does not match', () => {
const filters = _templateSrv.getAdhocFilters('oogleasdasd');
expect(filters).toMatchObject([]);
});
it('should return filters when datasourceName match via data source variable', () => {
const filters = _templateSrv.getAdhocFilters('logstash');
expect(filters).toMatchObject([2]);
});
});
describe('replace can pass multi / all format', () => {
beforeEach(() => {
initTemplateSrv([
{
type: 'query',
name: 'test',
current: { value: ['value1', 'value2'] },
},
]);
});
it('should replace $test with globbed value', () => {
const target = _templateSrv.replace('this.$test.filters', {}, 'glob');
expect(target).toBe('this.{value1,value2}.filters');
});
describe('when the globbed variable only has one value', () => {
beforeEach(() => {
initTemplateSrv([
{
type: 'query',
name: 'test',
current: { value: ['value1'] },
},
]);
});
it('should not glob the value', () => {
const target = _templateSrv.replace('this.$test.filters', {}, 'glob');
expect(target).toBe('this.value1.filters');
});
});
it('should replace ${test} with globbed value', () => {
const target = _templateSrv.replace('this.${test}.filters', {}, 'glob');
expect(target).toBe('this.{value1,value2}.filters');
});
it('should replace ${test:glob} with globbed value', () => {
const target = _templateSrv.replace('this.${test:glob}.filters', {});
expect(target).toBe('this.{value1,value2}.filters');
});
it('should replace $test with piped value', () => {
const target = _templateSrv.replace('this=$test', {}, 'pipe');
expect(target).toBe('this=value1|value2');
});
it('should replace ${test} with piped value', () => {
const target = _templateSrv.replace('this=${test}', {}, 'pipe');
expect(target).toBe('this=value1|value2');
});
it('should replace ${test:pipe} with piped value', () => {
const target = _templateSrv.replace('this=${test:pipe}', {});
expect(target).toBe('this=value1|value2');
});
it('should replace ${test:pipe} with piped value and $test with globbed value', () => {
const target = _templateSrv.replace('${test:pipe},$test', {}, 'glob');
expect(target).toBe('value1|value2,{value1,value2}');
});
});
describe('variable with all option', () => {
beforeEach(() => {
initTemplateSrv([
{
type: 'query',
name: 'test',
current: { value: '$__all' },
options: [{ value: '$__all' }, { value: 'value1' }, { value: 'value2' }],
},
]);
});
it('should replace $test with formatted all value', () => {
const target = _templateSrv.replace('this.$test.filters', {}, 'glob');
expect(target).toBe('this.{value1,value2}.filters');
});
it('should replace ${test} with formatted all value', () => {
const target = _templateSrv.replace('this.${test}.filters', {}, 'glob');
expect(target).toBe('this.{value1,value2}.filters');
});
it('should replace ${test:glob} with formatted all value', () => {
const target = _templateSrv.replace('this.${test:glob}.filters', {});
expect(target).toBe('this.{value1,value2}.filters');
});
it('should replace ${test:pipe} with piped value and $test with globbed value', () => {
const target = _templateSrv.replace('${test:pipe},$test', {}, 'glob');
expect(target).toBe('value1|value2,{value1,value2}');
});
});
describe('variable with all option and custom value', () => {
beforeEach(() => {
initTemplateSrv([
{
type: 'query',
name: 'test',
current: { value: '$__all' },
allValue: '*',
options: [{ value: 'value1' }, { value: 'value2' }],
},
]);
});
it('should replace $test with formatted all value', () => {
const target = _templateSrv.replace('this.$test.filters', {}, 'glob');
expect(target).toBe('this.*.filters');
});
it('should replace ${test} with formatted all value', () => {
const target = _templateSrv.replace('this.${test}.filters', {}, 'glob');
expect(target).toBe('this.*.filters');
});
it('should replace ${test:glob} with formatted all value', () => {
const target = _templateSrv.replace('this.${test:glob}.filters', {});
expect(target).toBe('this.*.filters');
});
it('should not escape custom all value', () => {
const target = _templateSrv.replace('this.$test', {}, 'regex');
expect(target).toBe('this.*');
});
});
describe('lucene format', () => {
it('should properly escape $test with lucene escape sequences', () => {
initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'value/4' } }]);
const target = _templateSrv.replace('this:$test', {}, 'lucene');
expect(target).toBe('this:value\\/4');
});
it('should properly escape ${test} with lucene escape sequences', () => {
initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'value/4' } }]);
const target = _templateSrv.replace('this:${test}', {}, 'lucene');
expect(target).toBe('this:value\\/4');
});
it('should properly escape ${test:lucene} with lucene escape sequences', () => {
initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'value/4' } }]);
const target = _templateSrv.replace('this:${test:lucene}', {});
expect(target).toBe('this:value\\/4');
});
});
describe('html format', () => {
it('should encode values html escape sequences', () => {
initTemplateSrv([{ type: 'query', name: 'test', current: { value: '<script>alert(asd)</script>' } }]);
const target = _templateSrv.replace('$test', {}, 'html');
expect(target).toBe('&lt;script&gt;alert(asd)&lt;/script&gt;');
});
});
describe('format variable to string values', () => {
it('single value should return value', () => {
const result = _templateSrv.formatValue('test');
expect(result).toBe('test');
});
it('multi value and glob format should render glob string', () => {
const result = _templateSrv.formatValue(['test', 'test2'], 'glob');
expect(result).toBe('{test,test2}');
});
it('multi value and lucene should render as lucene expr', () => {
const result = _templateSrv.formatValue(['test', 'test2'], 'lucene');
expect(result).toBe('("test" OR "test2")');
});
it('multi value and regex format should render regex string', () => {
const result = _templateSrv.formatValue(['test.', 'test2'], 'regex');
expect(result).toBe('(test\\.|test2)');
});
it('multi value and pipe should render pipe string', () => {
const result = _templateSrv.formatValue(['test', 'test2'], 'pipe');
expect(result).toBe('test|test2');
});
it('multi value and distributed should render distributed string', () => {
const result = _templateSrv.formatValue(['test', 'test2'], 'distributed', {
name: 'build',
});
expect(result).toBe('test,build=test2');
});
it('multi value and distributed should render when not string', () => {
const result = _templateSrv.formatValue(['test'], 'distributed', {
name: 'build',
});
expect(result).toBe('test');
});
it('multi value and csv format should render csv string', () => {
const result = _templateSrv.formatValue(['test', 'test2'], 'csv');
expect(result).toBe('test,test2');
});
it('multi value and percentencode format should render percent-encoded string', () => {
const result = _templateSrv.formatValue(['foo()bar BAZ', 'test2'], 'percentencode');
expect(result).toBe('%7Bfoo%28%29bar%20BAZ%2Ctest2%7D');
});
it('slash should be properly escaped in regex format', () => {
const result = _templateSrv.formatValue('Gi3/14', 'regex');
expect(result).toBe('Gi3\\/14');
});
it('single value and singlequote format should render string with value enclosed in single quotes', () => {
const result = _templateSrv.formatValue('test', 'singlequote');
expect(result).toBe("'test'");
});
it('multi value and singlequote format should render string with values enclosed in single quotes', () => {
const result = _templateSrv.formatValue(['test', "test'2"], 'singlequote');
expect(result).toBe("'test','test\\'2'");
});
it('single value and doublequote format should render string with value enclosed in double quotes', () => {
const result = _templateSrv.formatValue('test', 'doublequote');
expect(result).toBe('"test"');
});
it('multi value and doublequote format should render string with values enclosed in double quotes', () => {
const result = _templateSrv.formatValue(['test', 'test"2'], 'doublequote');
expect(result).toBe('"test","test\\"2"');
});
it('single value and sqlstring format should render string with value enclosed in single quotes', () => {
const result = _templateSrv.formatValue("test'value", 'sqlstring');
expect(result).toBe(`'test''value'`);
});
it('multi value and sqlstring format should render string with values enclosed in single quotes', () => {
const result = _templateSrv.formatValue(['test', "test'value2"], 'sqlstring');
expect(result).toBe(`'test','test''value2'`);
});
});
describe('can check if variable exists', () => {
beforeEach(() => {
initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'oogle' } }]);
});
it('should return true if $test exists', () => {
const result = _templateSrv.variableExists('$test');
expect(result).toBe(true);
});
it('should return true if $test exists in string', () => {
const result = _templateSrv.variableExists('something $test something');
expect(result).toBe(true);
});
it('should return true if [[test]] exists in string', () => {
const result = _templateSrv.variableExists('something [[test]] something');
expect(result).toBe(true);
});
it('should return true if [[test:csv]] exists in string', () => {
const result = _templateSrv.variableExists('something [[test:csv]] something');
expect(result).toBe(true);
});
it('should return true if ${test} exists in string', () => {
const result = _templateSrv.variableExists('something ${test} something');
expect(result).toBe(true);
});
it('should return true if ${test:raw} exists in string', () => {
const result = _templateSrv.variableExists('something ${test:raw} something');
expect(result).toBe(true);
});
it('should return null if there are no variables in string', () => {
const result = _templateSrv.variableExists('string without variables');
expect(result).toBe(null);
});
});
describe('can highlight variables in string', () => {
beforeEach(() => {
initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'oogle' } }]);
});
it('should insert html', () => {
const result = _templateSrv.highlightVariablesAsHtml('$test');
expect(result).toBe('<span class="template-variable">$test</span>');
});
it('should insert html anywhere in string', () => {
const result = _templateSrv.highlightVariablesAsHtml('this $test ok');
expect(result).toBe('this <span class="template-variable">$test</span> ok');
});
it('should ignore if variables does not exist', () => {
const result = _templateSrv.highlightVariablesAsHtml('this $google ok');
expect(result).toBe('this $google ok');
});
});
describe('updateIndex with simple value', () => {
beforeEach(() => {
initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'muuuu' } }]);
});
it('should set current value and update template data', () => {
const target = _templateSrv.replace('this.[[test]].filters');
expect(target).toBe('this.muuuu.filters');
});
});
describe('fillVariableValuesForUrl with multi value', () => {
beforeAll(() => {
variableAdapters.register(createQueryVariableAdapter());
});
beforeEach(() => {
initTemplateSrv([
{
type: 'query',
name: 'test',
current: { value: ['val1', 'val2'] },
getValueForUrl: function() {
return this.current.value;
},
},
]);
});
it('should set multiple url params', () => {
const params: any = {};
_templateSrv.fillVariableValuesForUrl(params);
expect(params['var-test']).toMatchObject(['val1', 'val2']);
});
});
describe('fillVariableValuesForUrl skip url sync', () => {
beforeEach(() => {
initTemplateSrv([
{
name: 'test',
skipUrlSync: true,
current: { value: 'value' },
getValueForUrl: function() {
return this.current.value;
},
},
]);
});
it('should not include template variable value in url', () => {
const params: any = {};
_templateSrv.fillVariableValuesForUrl(params);
expect(params['var-test']).toBe(undefined);
});
});
describe('fillVariableValuesForUrl with multi value with skip url sync', () => {
beforeEach(() => {
initTemplateSrv([
{
type: 'query',
name: 'test',
skipUrlSync: true,
current: { value: ['val1', 'val2'] },
getValueForUrl: function() {
return this.current.value;
},
},
]);
});
it('should not include template variable value in url', () => {
const params: any = {};
_templateSrv.fillVariableValuesForUrl(params);
expect(params['var-test']).toBe(undefined);
});
});
describe('fillVariableValuesForUrl with multi value and scopedVars', () => {
beforeEach(() => {
initTemplateSrv([{ type: 'query', name: 'test', current: { value: ['val1', 'val2'] } }]);
});
it('should set scoped value as url params', () => {
const params: any = {};
_templateSrv.fillVariableValuesForUrl(params, {
test: { value: 'val1' },
});
expect(params['var-test']).toBe('val1');
});
});
describe('fillVariableValuesForUrl with multi value, scopedVars and skip url sync', () => {
beforeEach(() => {
initTemplateSrv([{ type: 'query', name: 'test', current: { value: ['val1', 'val2'] } }]);
});
it('should not set scoped value as url params', () => {
const params: any = {};
_templateSrv.fillVariableValuesForUrl(params, {
test: { name: 'test', value: 'val1', skipUrlSync: true },
});
expect(params['var-test']).toBe(undefined);
});
});
describe('replaceWithText', () => {
beforeEach(() => {
initTemplateSrv([
{
type: 'query',
name: 'server',
current: { value: '{asd,asd2}', text: 'All' },
},
{
type: 'interval',
name: 'period',
current: { value: '$__auto_interval_interval', text: 'auto' },
},
{
type: 'textbox',
name: 'empty_on_init',
current: { value: '', text: '' },
},
{
type: 'custom',
name: 'foo',
current: { value: 'constructor', text: 'constructor' },
},
]);
_templateSrv.setGrafanaVariable('$__auto_interval_interval', '13m');
_templateSrv.updateIndex();
});
it('should replace with text except for grafanaVariables', () => {
const target = _templateSrv.replaceWithText('Server: $server, period: $period');
expect(target).toBe('Server: All, period: 13m');
});
it('should replace empty string-values with an empty string', () => {
const target = _templateSrv.replaceWithText('Hello $empty_on_init');
expect(target).toBe('Hello ');
});
it('should not return a string representation of a constructor property', () => {
const target = _templateSrv.replaceWithText('$foo');
expect(target).not.toBe('function Object() { [native code] }');
expect(target).toBe('constructor');
});
});
describe('built in interval variables', () => {
beforeEach(() => {
initTemplateSrv([]);
});
it('should be possible to fetch value with getBuilInIntervalValue', () => {
const val = _templateSrv.getBuiltInIntervalValue();
expect(val).toBe('1s');
});
it('should replace $__interval_ms with interval milliseconds', () => {
const target = _templateSrv.replace('10 * $__interval_ms', {
__interval_ms: { text: '100', value: '100' },
});
expect(target).toBe('10 * 100');
});
});
});

@ -1,261 +0,0 @@
import { assignModelProperties } from '../types';
import { ScopedVars } from '@grafana/data';
import { containsSearchFilter, containsVariable, getSearchFilterScopedVar, SEARCH_FILTER_VARIABLE } from '../utils';
describe('containsVariable', () => {
describe('when checking if a string contains a variable', () => {
it('should find it with $const syntax', () => {
const contains = containsVariable('this.$test.filters', 'test');
expect(contains).toBe(true);
});
it('should not find it if only part matches with $const syntax', () => {
const contains = containsVariable('this.$serverDomain.filters', 'server');
expect(contains).toBe(false);
});
it('should find it if it ends with variable and passing multiple test strings', () => {
const contains = containsVariable('show field keys from $pgmetric', 'test string2', 'pgmetric');
expect(contains).toBe(true);
});
it('should find it with [[var]] syntax', () => {
const contains = containsVariable('this.[[test]].filters', 'test');
expect(contains).toBe(true);
});
it('should find it with [[var:option]] syntax', () => {
const contains = containsVariable('this.[[test:csv]].filters', 'test');
expect(contains).toBe(true);
});
it('should find it when part of segment', () => {
const contains = containsVariable('metrics.$env.$group-*', 'group');
expect(contains).toBe(true);
});
it('should find it its the only thing', () => {
const contains = containsVariable('$env', 'env');
expect(contains).toBe(true);
});
it('should be able to pass in multiple test strings', () => {
const contains = containsVariable('asd', 'asd2.$env', 'env');
expect(contains).toBe(true);
});
it('should find it with ${var} syntax', () => {
const contains = containsVariable('this.${test}.filters', 'test');
expect(contains).toBe(true);
});
it('should find it with ${var:option} syntax', () => {
const contains = containsVariable('this.${test:csv}.filters', 'test');
expect(contains).toBe(true);
});
});
});
describe('assignModelProperties', () => {
it('only set properties defined in defaults', () => {
const target: any = { test: 'asd' };
assignModelProperties(target, { propA: 1, propB: 2 }, { propB: 0 });
expect(target.propB).toBe(2);
expect(target.test).toBe('asd');
});
it('use default value if not found on source', () => {
const target: any = { test: 'asd' };
assignModelProperties(target, { propA: 1, propB: 2 }, { propC: 10 });
expect(target.propC).toBe(10);
});
});
describe('containsSearchFilter', () => {
describe('when called without query', () => {
it('then it should return false', () => {
const result = containsSearchFilter(null);
expect(result).toBe(false);
});
});
describe('when called with an object', () => {
it('then it should return false', () => {
const result = containsSearchFilter({});
expect(result).toBe(false);
});
});
describe(`when called with a query without ${SEARCH_FILTER_VARIABLE}`, () => {
it('then it should return false', () => {
const result = containsSearchFilter('$app.*');
expect(result).toBe(false);
});
});
describe(`when called with a query with $${SEARCH_FILTER_VARIABLE}`, () => {
it('then it should return true', () => {
const result = containsSearchFilter(`$app.$${SEARCH_FILTER_VARIABLE}`);
expect(result).toBe(true);
});
});
describe(`when called with a query with [[${SEARCH_FILTER_VARIABLE}]]`, () => {
it('then it should return true', () => {
const result = containsSearchFilter(`$app.[[${SEARCH_FILTER_VARIABLE}]]`);
expect(result).toBe(true);
});
});
describe(`when called with a query with \$\{${SEARCH_FILTER_VARIABLE}:regex\}`, () => {
it('then it should return true', () => {
const result = containsSearchFilter(`$app.\$\{${SEARCH_FILTER_VARIABLE}:regex\}`);
expect(result).toBe(true);
});
});
});
interface GetSearchFilterScopedVarScenario {
query: string;
wildcardChar: string;
options: { searchFilter?: string };
expected: ScopedVars;
}
const scenarios: GetSearchFilterScopedVarScenario[] = [
// testing the $__searchFilter notation
{
query: 'abc.$__searchFilter',
wildcardChar: '',
options: { searchFilter: '' },
expected: { __searchFilter: { value: '', text: '' } },
},
{
query: 'abc.$__searchFilter',
wildcardChar: '*',
options: { searchFilter: '' },
expected: { __searchFilter: { value: '*', text: '' } },
},
{
query: 'abc.$__searchFilter',
wildcardChar: '',
options: { searchFilter: 'a' },
expected: { __searchFilter: { value: 'a', text: '' } },
},
{
query: 'abc.$__searchFilter',
wildcardChar: '*',
options: { searchFilter: 'a' },
expected: { __searchFilter: { value: 'a*', text: '' } },
},
// testing the [[__searchFilter]] notation
{
query: 'abc.[[__searchFilter]]',
wildcardChar: '',
options: { searchFilter: '' },
expected: { __searchFilter: { value: '', text: '' } },
},
{
query: 'abc.[[__searchFilter]]',
wildcardChar: '*',
options: { searchFilter: '' },
expected: { __searchFilter: { value: '*', text: '' } },
},
{
query: 'abc.[[__searchFilter]]',
wildcardChar: '',
options: { searchFilter: 'a' },
expected: { __searchFilter: { value: 'a', text: '' } },
},
{
query: 'abc.[[__searchFilter]]',
wildcardChar: '*',
options: { searchFilter: 'a' },
expected: { __searchFilter: { value: 'a*', text: '' } },
},
// testing the ${__searchFilter:fmt} notation
{
query: 'abc.${__searchFilter:regex}',
wildcardChar: '',
options: { searchFilter: '' },
expected: { __searchFilter: { value: '', text: '' } },
},
{
query: 'abc.${__searchFilter:regex}',
wildcardChar: '*',
options: { searchFilter: '' },
expected: { __searchFilter: { value: '*', text: '' } },
},
{
query: 'abc.${__searchFilter:regex}',
wildcardChar: '',
options: { searchFilter: 'a' },
expected: { __searchFilter: { value: 'a', text: '' } },
},
{
query: 'abc.${__searchFilter:regex}',
wildcardChar: '*',
options: { searchFilter: 'a' },
expected: { __searchFilter: { value: 'a*', text: '' } },
},
// testing the no options
{
query: 'abc.$__searchFilter',
wildcardChar: '',
options: null as any,
expected: { __searchFilter: { value: '', text: '' } },
},
{
query: 'abc.$__searchFilter',
wildcardChar: '*',
options: null as any,
expected: { __searchFilter: { value: '*', text: '' } },
},
// testing the no search filter at all
{
query: 'abc.$def',
wildcardChar: '',
options: { searchFilter: '' },
expected: {},
},
{
query: 'abc.$def',
wildcardChar: '*',
options: { searchFilter: '' },
expected: {},
},
{
query: 'abc.$def',
wildcardChar: '',
options: { searchFilter: 'a' },
expected: {},
},
{
query: 'abc.$def',
wildcardChar: '*',
options: { searchFilter: 'a' },
expected: {},
},
];
scenarios.map(scenario => {
describe('getSearchFilterScopedVar', () => {
describe(`when called with query:'${scenario.query}'`, () => {
describe(`and wildcardChar:'${scenario.wildcardChar}'`, () => {
describe(`and options:'${JSON.stringify(scenario.options, null, 0)}'`, () => {
it(`then the result should be ${JSON.stringify(scenario.expected, null, 0)}`, () => {
const { expected, ...args } = scenario;
expect(getSearchFilterScopedVar(args)).toEqual(expected);
});
});
});
});
});
});

@ -1,663 +0,0 @@
import '../all';
import { VariableSrv } from '../variable_srv';
import { DashboardModel } from '../../dashboard/state/DashboardModel';
// @ts-ignore
import $q from 'q';
import { dateTime } from '@grafana/data';
import { CustomVariable } from '../custom_variable';
jest.mock('app/core/core', () => ({
contextSrv: {
user: { orgId: 1, orgName: 'TestOrg' },
},
}));
describe('VariableSrv', function(this: any) {
const ctx = {
datasourceSrv: {},
timeSrv: {
timeRange: () => {
return { from: '2018-01-29', to: '2019-01-29' };
},
},
$rootScope: {
$on: () => {},
},
$injector: {
instantiate: (ctr: any, obj: { model: any }) => new ctr(obj.model),
},
templateSrv: {
setGrafanaVariable: jest.fn(),
init: (vars: any) => {
this.variables = vars;
},
updateIndex: () => {},
setGlobalVariable: (name: string, variable: any) => {},
replace: (str: any) =>
str.replace(this.regex, (match: string) => {
return match;
}),
},
$location: {
search: () => {},
},
} as any;
function describeUpdateVariable(desc: string, fn: Function) {
describe(desc, () => {
const scenario: any = {};
scenario.setup = (setupFn: Function) => {
scenario.setupFn = setupFn;
};
beforeEach(async () => {
scenario.setupFn();
const ds: any = {};
ds.metricFindQuery = () => Promise.resolve(scenario.queryResult);
ctx.variableSrv = new VariableSrv($q, ctx.$location, ctx.$injector, ctx.templateSrv, ctx.timeSrv);
ctx.variableSrv.timeSrv = ctx.timeSrv;
ctx.datasourceSrv = {
get: () => Promise.resolve(ds),
getMetricSources: () => scenario.metricSources,
};
ctx.$injector.instantiate = (ctr: any, model: any) => {
return getVarMockConstructor(ctr, model, ctx);
};
ctx.variableSrv.init(
new DashboardModel({
templating: { list: [] },
updateSubmenuVisibility: () => {},
})
);
scenario.variable = ctx.variableSrv.createVariableFromModel(scenario.variableModel);
ctx.variableSrv.addVariable(scenario.variable);
await ctx.variableSrv.updateOptions(scenario.variable);
});
fn(scenario);
});
}
describeUpdateVariable('interval variable without auto', (scenario: any) => {
scenario.setup(() => {
scenario.variableModel = {
type: 'interval',
query: '1s,2h,5h,1d',
name: 'test',
};
});
it('should update options array', () => {
expect(scenario.variable.options.length).toBe(4);
expect(scenario.variable.options[0].text).toBe('1s');
expect(scenario.variable.options[0].value).toBe('1s');
});
});
//
// Interval variable update
//
describeUpdateVariable('interval variable with auto', (scenario: any) => {
scenario.setup(() => {
scenario.variableModel = {
type: 'interval',
query: '1s,2h,5h,1d',
name: 'test',
auto: true,
auto_count: 10,
};
const range = {
from: dateTime(new Date())
.subtract(7, 'days')
.toDate(),
to: new Date(),
};
ctx.timeSrv.timeRange = () => range;
// ctx.templateSrv.setGrafanaVariable = jest.fn();
});
it('should update options array', () => {
expect(scenario.variable.options.length).toBe(5);
expect(scenario.variable.options[0].text).toBe('auto');
expect(scenario.variable.options[0].value).toBe('$__auto_interval_test');
});
it('should set $__auto_interval_test', () => {
const call = ctx.templateSrv.setGrafanaVariable.mock.calls[0];
expect(call[0]).toBe('$__auto_interval_test');
expect(call[1]).toBe('12h');
});
// updateAutoValue() gets called twice: once directly once via VariableSrv.validateVariableSelectionState()
// So use lastCall instead of a specific call number
it('should set $__auto_interval', () => {
const call = ctx.templateSrv.setGrafanaVariable.mock.calls.pop();
expect(call[0]).toBe('$__auto_interval');
expect(call[1]).toBe('12h');
});
});
//
// Query variable update
//
describeUpdateVariable('query variable with empty current object and refresh', (scenario: any) => {
scenario.setup(() => {
scenario.variableModel = {
type: 'query',
query: '',
name: 'test',
current: {},
};
scenario.queryResult = [{ text: 'backend1' }, { text: 'backend2' }];
});
it('should set current value to first option', () => {
expect(scenario.variable.options.length).toBe(2);
expect(scenario.variable.current.value).toBe('backend1');
});
});
describeUpdateVariable(
'query variable with multi select and new options does not contain some selected values',
(scenario: any) => {
scenario.setup(() => {
scenario.variableModel = {
type: 'query',
query: '',
name: 'test',
current: {
value: ['val1', 'val2', 'val3'],
text: 'val1 + val2 + val3',
},
};
scenario.queryResult = [{ text: 'val2' }, { text: 'val3' }];
});
it('should update current value', () => {
expect(scenario.variable.current.value).toEqual(['val2', 'val3']);
expect(scenario.variable.current.text).toEqual('val2 + val3');
});
}
);
describeUpdateVariable(
'query variable with multi select and new options does not contain any selected values',
(scenario: any) => {
scenario.setup(() => {
scenario.variableModel = {
type: 'query',
query: '',
name: 'test',
current: {
value: ['val1', 'val2', 'val3'],
text: 'val1 + val2 + val3',
},
};
scenario.queryResult = [{ text: 'val5' }, { text: 'val6' }];
});
it('should update current value with first one', () => {
expect(scenario.variable.current.value).toEqual('val5');
expect(scenario.variable.current.text).toEqual('val5');
});
}
);
describeUpdateVariable('query variable with multi select and $__all selected', (scenario: any) => {
scenario.setup(() => {
scenario.variableModel = {
type: 'query',
query: '',
name: 'test',
includeAll: true,
current: {
value: ['$__all'],
text: 'All',
},
};
scenario.queryResult = [{ text: 'val5' }, { text: 'val6' }];
});
it('should keep current All value', () => {
expect(scenario.variable.current.value).toEqual(['$__all']);
expect(scenario.variable.current.text).toEqual('All');
});
});
describeUpdateVariable('query variable with numeric results', (scenario: any) => {
scenario.setup(() => {
scenario.variableModel = {
type: 'query',
query: '',
name: 'test',
current: {},
};
scenario.queryResult = [{ text: 12, value: 12 }];
});
it('should set current value to first option', () => {
expect(scenario.variable.current.value).toBe('12');
expect(scenario.variable.options[0].value).toBe('12');
expect(scenario.variable.options[0].text).toBe('12');
});
});
describeUpdateVariable('basic query variable', (scenario: any) => {
scenario.setup(() => {
scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' };
scenario.queryResult = [{ text: 'backend1' }, { text: 'backend2' }];
});
it('should update options array', () => {
expect(scenario.variable.options.length).toBe(2);
expect(scenario.variable.options[0].text).toBe('backend1');
expect(scenario.variable.options[0].value).toBe('backend1');
expect(scenario.variable.options[1].value).toBe('backend2');
});
it('should select first option as value', () => {
expect(scenario.variable.current.value).toBe('backend1');
});
});
describeUpdateVariable('and existing value still exists in options', (scenario: any) => {
scenario.setup(() => {
scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' };
scenario.variableModel.current = { value: 'backend2', text: 'backend2' };
scenario.queryResult = [{ text: 'backend1' }, { text: 'backend2' }];
});
it('should keep variable value', () => {
expect(scenario.variable.current.text).toBe('backend2');
});
});
describeUpdateVariable('and regex pattern exists', (scenario: any) => {
scenario.setup(() => {
scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' };
scenario.variableModel.regex = '/apps.*(backend_[0-9]+)/';
scenario.queryResult = [
{ text: 'apps.backend.backend_01.counters.req' },
{ text: 'apps.backend.backend_02.counters.req' },
];
});
it('should extract and use match group', () => {
expect(scenario.variable.options[0].value).toBe('backend_01');
});
});
describeUpdateVariable('and regex pattern exists and no match', (scenario: any) => {
scenario.setup(() => {
scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' };
scenario.variableModel.regex = '/apps.*(backendasd[0-9]+)/';
scenario.queryResult = [
{ text: 'apps.backend.backend_01.counters.req' },
{ text: 'apps.backend.backend_02.counters.req' },
];
});
it('should not add non matching items, None option should be added instead', () => {
expect(scenario.variable.options.length).toBe(1);
expect(scenario.variable.options[0].isNone).toBe(true);
});
});
describeUpdateVariable('regex pattern without slashes', (scenario: any) => {
scenario.setup(() => {
scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' };
scenario.variableModel.regex = 'backend_01';
scenario.queryResult = [
{ text: 'apps.backend.backend_01.counters.req' },
{ text: 'apps.backend.backend_02.counters.req' },
];
});
it('should return matches options', () => {
expect(scenario.variable.options.length).toBe(1);
});
});
describeUpdateVariable('regex pattern remove duplicates', (scenario: any) => {
scenario.setup(() => {
scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' };
scenario.variableModel.regex = '/backend_01/';
scenario.queryResult = [
{ text: 'apps.backend.backend_01.counters.req' },
{ text: 'apps.backend.backend_01.counters.req' },
];
});
it('should return matches options', () => {
expect(scenario.variable.options.length).toBe(1);
});
});
describeUpdateVariable('with include All', (scenario: any) => {
scenario.setup(() => {
scenario.variableModel = {
type: 'query',
query: 'apps.*',
name: 'test',
includeAll: true,
};
scenario.queryResult = [{ text: 'backend1' }, { text: 'backend2' }, { text: 'backend3' }];
});
it('should add All option', () => {
expect(scenario.variable.options[0].text).toBe('All');
expect(scenario.variable.options[0].value).toBe('$__all');
});
});
describeUpdateVariable('with include all and custom value', (scenario: any) => {
scenario.setup(() => {
scenario.variableModel = {
type: 'query',
query: 'apps.*',
name: 'test',
includeAll: true,
allValue: '*',
};
scenario.queryResult = [{ text: 'backend1' }, { text: 'backend2' }, { text: 'backend3' }];
});
it('should add All option with custom value', () => {
expect(scenario.variable.options[0].value).toBe('$__all');
});
});
describeUpdateVariable('without sort', (scenario: any) => {
scenario.setup(() => {
scenario.variableModel = {
type: 'query',
query: 'apps.*',
name: 'test',
sort: 0,
};
scenario.queryResult = [{ text: 'bbb2' }, { text: 'aaa10' }, { text: 'ccc3' }];
});
it('should return options without sort', () => {
expect(scenario.variable.options[0].text).toBe('bbb2');
expect(scenario.variable.options[1].text).toBe('aaa10');
expect(scenario.variable.options[2].text).toBe('ccc3');
});
});
describeUpdateVariable('with alphabetical sort (asc)', (scenario: any) => {
scenario.setup(() => {
scenario.variableModel = {
type: 'query',
query: 'apps.*',
name: 'test',
sort: 1,
};
scenario.queryResult = [{ text: 'bbb2' }, { text: 'aaa10' }, { text: 'ccc3' }];
});
it('should return options with alphabetical sort', () => {
expect(scenario.variable.options[0].text).toBe('aaa10');
expect(scenario.variable.options[1].text).toBe('bbb2');
expect(scenario.variable.options[2].text).toBe('ccc3');
});
});
describeUpdateVariable('with alphabetical sort (desc)', (scenario: any) => {
scenario.setup(() => {
scenario.variableModel = {
type: 'query',
query: 'apps.*',
name: 'test',
sort: 2,
};
scenario.queryResult = [{ text: 'bbb2' }, { text: 'aaa10' }, { text: 'ccc3' }];
});
it('should return options with alphabetical sort', () => {
expect(scenario.variable.options[0].text).toBe('ccc3');
expect(scenario.variable.options[1].text).toBe('bbb2');
expect(scenario.variable.options[2].text).toBe('aaa10');
});
});
describeUpdateVariable('with numerical sort (asc)', (scenario: any) => {
scenario.setup(() => {
scenario.variableModel = {
type: 'query',
query: 'apps.*',
name: 'test',
sort: 3,
};
scenario.queryResult = [{ text: 'bbb2' }, { text: 'aaa10' }, { text: 'ccc3' }];
});
it('should return options with numerical sort', () => {
expect(scenario.variable.options[0].text).toBe('bbb2');
expect(scenario.variable.options[1].text).toBe('ccc3');
expect(scenario.variable.options[2].text).toBe('aaa10');
});
});
describeUpdateVariable('with numerical sort (desc)', (scenario: any) => {
scenario.setup(() => {
scenario.variableModel = {
type: 'query',
query: 'apps.*',
name: 'test',
sort: 4,
};
scenario.queryResult = [{ text: 'bbb2' }, { text: 'aaa10' }, { text: 'ccc3' }];
});
it('should return options with numerical sort', () => {
expect(scenario.variable.options[0].text).toBe('aaa10');
expect(scenario.variable.options[1].text).toBe('ccc3');
expect(scenario.variable.options[2].text).toBe('bbb2');
});
});
//
// datasource variable update
//
describeUpdateVariable('datasource variable with regex filter', (scenario: any) => {
scenario.setup(() => {
scenario.variableModel = {
type: 'datasource',
query: 'graphite',
name: 'test',
current: { value: 'backend4_pee', text: 'backend4_pee' },
regex: '/pee$/',
};
scenario.metricSources = [
{ name: 'backend1', meta: { id: 'influx' } },
{ name: 'backend2_pee', meta: { id: 'graphite' } },
{ name: 'backend3', meta: { id: 'graphite' } },
{ name: 'backend4_pee', meta: { id: 'graphite' } },
];
});
it('should set only contain graphite ds and filtered using regex', () => {
expect(scenario.variable.options.length).toBe(2);
expect(scenario.variable.options[0].value).toBe('backend2_pee');
expect(scenario.variable.options[1].value).toBe('backend4_pee');
});
it('should keep current value if available', () => {
expect(scenario.variable.current.value).toBe('backend4_pee');
});
});
//
// Custom variable update
//
describeUpdateVariable('update custom variable', (scenario: any) => {
scenario.setup(() => {
scenario.variableModel = {
type: 'custom',
query: 'hej, hop, asd, escaped\\,var',
name: 'test',
};
});
it('should update options array', () => {
expect(scenario.variable.options.length).toBe(4);
expect(scenario.variable.options[0].text).toBe('hej');
expect(scenario.variable.options[1].value).toBe('hop');
expect(scenario.variable.options[2].value).toBe('asd');
expect(scenario.variable.options[3].value).toBe('escaped,var');
});
});
describe('multiple interval variables with auto', () => {
let variable1: any, variable2: any;
beforeEach(() => {
const range = {
from: dateTime(new Date())
.subtract(7, 'days')
.toDate(),
to: new Date(),
};
ctx.timeSrv.timeRange = () => range;
ctx.templateSrv.setGrafanaVariable = jest.fn();
const variableModel1 = {
type: 'interval',
query: '1s,2h,5h,1d',
name: 'variable1',
auto: true,
auto_count: 10,
};
variable1 = ctx.variableSrv.createVariableFromModel(variableModel1);
ctx.variableSrv.addVariable(variable1);
const variableModel2 = {
type: 'interval',
query: '1s,2h,5h',
name: 'variable2',
auto: true,
auto_count: 1000,
};
variable2 = ctx.variableSrv.createVariableFromModel(variableModel2);
ctx.variableSrv.addVariable(variable2);
ctx.variableSrv.updateOptions(variable1);
ctx.variableSrv.updateOptions(variable2);
// ctx.$rootScope.$digest();
});
it('should update options array', () => {
expect(variable1.options.length).toBe(5);
expect(variable1.options[0].text).toBe('auto');
expect(variable1.options[0].value).toBe('$__auto_interval_variable1');
expect(variable2.options.length).toBe(4);
expect(variable2.options[0].text).toBe('auto');
expect(variable2.options[0].value).toBe('$__auto_interval_variable2');
});
it('should correctly set $__auto_interval_variableX', () => {
let variable1Set,
variable2Set,
legacySet,
unknownSet = false;
// updateAutoValue() gets called repeatedly: once directly once via VariableSrv.validateVariableSelectionState()
// So check that all calls are valid rather than expect a specific number and/or ordering of calls
for (let i = 0; i < ctx.templateSrv.setGrafanaVariable.mock.calls.length; i++) {
const call = ctx.templateSrv.setGrafanaVariable.mock.calls[i];
switch (call[0]) {
case '$__auto_interval_variable1':
expect(call[1]).toBe('12h');
variable1Set = true;
break;
case '$__auto_interval_variable2':
expect(call[1]).toBe('10m');
variable2Set = true;
break;
case '$__auto_interval':
expect(call[1]).toEqual(expect.stringMatching(/^(12h|10m)$/));
legacySet = true;
break;
default:
unknownSet = true;
break;
}
}
expect(variable1Set).toEqual(true);
expect(variable2Set).toEqual(true);
expect(legacySet).toEqual(true);
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: any, 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: any) => variableSrv.setOptionFromUrl(variable, val)];
}
function getVarMockConstructor(variable: any, model: any, ctx: any) {
switch (model.model.type) {
case 'datasource':
return new variable(model.model, ctx.datasourceSrv, ctx.variableSrv, ctx.templateSrv);
case 'query':
return new variable(model.model, ctx.datasourceSrv, ctx.templateSrv, ctx.variableSrv);
case 'interval':
return new variable(model.model, ctx.timeSrv, ctx.templateSrv, ctx.variableSrv);
case 'custom':
return new variable(model.model, ctx.variableSrv);
default:
return new variable(model.model);
}
}

@ -1,275 +0,0 @@
import '../all';
import _ from 'lodash';
import { VariableSrv } from '../variable_srv';
import { DashboardModel } from '../../dashboard/state/DashboardModel';
// @ts-ignore
import $q from 'q';
jest.mock('app/core/core', () => ({
contextSrv: {
user: { orgId: 1, orgName: 'TestOrg' },
},
}));
describe('VariableSrv init', function(this: any) {
const templateSrv = {
init: (vars: any) => {
this.variables = vars;
},
variableInitialized: () => {},
updateIndex: () => {},
setGlobalVariable: (name: string, variable: any) => {},
replace: (str: string) =>
str.replace(this.regex, match => {
return match;
}),
};
const timeSrv = {
timeRange: () => {
return { from: '2018-01-29', to: '2019-01-29' };
},
};
const $injector = {} as any;
let ctx = {} as any;
function describeInitScenario(desc: string, fn: Function) {
describe(desc, () => {
const scenario: any = {
urlParams: {},
setup: (setupFn: Function) => {
scenario.setupFn = setupFn;
},
};
beforeEach(async () => {
scenario.setupFn();
ctx = {
datasource: {
metricFindQuery: jest.fn(() => Promise.resolve(scenario.queryResult)),
},
datasourceSrv: {
get: () => Promise.resolve(ctx.datasource),
getMetricSources: () => scenario.metricSources,
},
templateSrv,
};
// @ts-ignore
ctx.variableSrv = new VariableSrv($q, {}, $injector, templateSrv, timeSrv);
$injector.instantiate = (variable: any, model: any) => {
return getVarMockConstructor(variable, model, ctx);
};
ctx.variableSrv.datasource = ctx.datasource;
ctx.variableSrv.datasourceSrv = ctx.datasourceSrv;
ctx.variableSrv.$location.search = () => scenario.urlParams;
ctx.variableSrv.dashboard = new DashboardModel({
templating: { list: scenario.variables },
});
await ctx.variableSrv.init(ctx.variableSrv.dashboard);
scenario.variables = ctx.variableSrv.variables;
});
fn(scenario);
});
}
['interval', 'custom', 'datasource'].forEach(type => {
describeInitScenario('when setting ' + type + ' variable via url', (scenario: any) => {
scenario.setup(() => {
scenario.variables = [
{
name: 'apps',
type: type,
current: { text: 'Test', value: 'test' },
options: [{ text: 'Test', value: 'test' }],
},
];
scenario.urlParams['var-apps'] = 'new';
scenario.metricSources = [];
});
it('should update current value', () => {
expect(scenario.variables[0].current.value).toBe('new');
expect(scenario.variables[0].current.text).toBe('new');
});
});
});
// this test will moved to redux tests instead
describe('given dependent variables', () => {
const variableList = [
{
name: 'app',
type: 'query',
query: '',
current: { text: 'app1', value: 'app1' },
options: [{ text: 'app1', value: 'app1' }],
},
{
name: 'server',
type: 'query',
refresh: 1,
query: '$app.*',
current: { text: 'server1', value: 'server1' },
options: [{ text: 'server1', value: 'server1' }],
},
];
describeInitScenario('when setting parent const from url', (scenario: any) => {
scenario.setup(() => {
scenario.variables = _.cloneDeep(variableList);
scenario.urlParams['var-app'] = 'google';
scenario.queryResult = [{ text: 'google-server1' }, { text: 'google-server2' }];
});
it('should update child variable', () => {
expect(scenario.variables[1].options.length).toBe(2);
expect(scenario.variables[1].current.text).toBe('google-server1');
});
it('should only update it once', () => {
expect(ctx.variableSrv.datasource.metricFindQuery).toHaveBeenCalledTimes(1);
});
});
});
describeInitScenario('when datasource variable is initialized', (scenario: any) => {
scenario.setup(() => {
scenario.variables = [
{
type: 'datasource',
query: 'graphite',
name: 'test',
current: { value: 'backend4_pee', text: 'backend4_pee' },
regex: '/pee$/',
},
];
scenario.metricSources = [
{ name: 'backend1', meta: { id: 'influx' } },
{ name: 'backend2_pee', meta: { id: 'graphite' } },
{ name: 'backend3', meta: { id: 'graphite' } },
{ name: 'backend4_pee', meta: { id: 'graphite' } },
];
});
it('should update current value', () => {
const variable = ctx.variableSrv.variables[0];
expect(variable.options.length).toBe(2);
});
});
describeInitScenario('when template variable is present in url multiple times', (scenario: any) => {
scenario.setup(() => {
scenario.variables = [
{
name: 'apps',
type: 'custom',
multi: true,
current: { text: 'Val1', value: 'val1' },
options: [
{ text: 'Val1', value: 'val1' },
{ text: 'Val2', value: 'val2' },
{ text: 'Val3', value: 'val3', selected: true },
],
},
];
scenario.urlParams['var-apps'] = ['val2', 'val1'];
});
it('should update current value', () => {
const variable = ctx.variableSrv.variables[0];
expect(variable.current.value.length).toBe(2);
expect(variable.current.value[0]).toBe('val2');
expect(variable.current.value[1]).toBe('val1');
expect(variable.current.text).toBe('Val2 + Val1');
expect(variable.options[0].selected).toBe(true);
expect(variable.options[1].selected).toBe(true);
});
it('should set options that are not in value to selected false', () => {
const variable = ctx.variableSrv.variables[0];
expect(variable.options[2].selected).toBe(false);
});
});
describeInitScenario(
'when template variable is present in url multiple times and variables have no text',
(scenario: any) => {
scenario.setup(() => {
scenario.variables = [
{
name: 'apps',
type: 'custom',
multi: true,
},
];
scenario.urlParams['var-apps'] = ['val1', 'val2'];
});
it('should display concatenated values in text', () => {
const variable = ctx.variableSrv.variables[0];
expect(variable.current.value.length).toBe(2);
expect(variable.current.value[0]).toBe('val1');
expect(variable.current.value[1]).toBe('val2');
expect(variable.current.text).toBe('val1 + val2');
});
}
);
describeInitScenario('when template variable is present in url multiple times using key/values', (scenario: any) => {
scenario.setup(() => {
scenario.variables = [
{
name: 'apps',
type: 'custom',
multi: true,
current: { text: 'Val1', value: 'val1' },
options: [
{ text: 'Val1', value: 'val1' },
{ text: 'Val2', value: 'val2' },
{ text: 'Val3', value: 'val3', selected: true },
],
},
];
scenario.urlParams['var-apps'] = ['val2', 'val1'];
});
it('should update current value', () => {
const variable = ctx.variableSrv.variables[0];
expect(variable.current.value.length).toBe(2);
expect(variable.current.value[0]).toBe('val2');
expect(variable.current.value[1]).toBe('val1');
expect(variable.current.text).toBe('Val2 + Val1');
expect(variable.options[0].selected).toBe(true);
expect(variable.options[1].selected).toBe(true);
});
it('should set options that are not in value to selected false', () => {
const variable = ctx.variableSrv.variables[0];
expect(variable.options[2].selected).toBe(false);
});
});
});
function getVarMockConstructor(variable: any, model: any, ctx: any) {
switch (model.model.type) {
case 'datasource':
return new variable(model.model, ctx.datasourceSrv, ctx.variableSrv, ctx.templateSrv);
case 'query':
return new variable(model.model, ctx.datasourceSrv, ctx.templateSrv, ctx.variableSrv);
case 'interval':
return new variable(model.model, {}, ctx.templateSrv, ctx.variableSrv);
case 'custom':
return new variable(model.model, ctx.variableSrv);
default:
return new variable(model.model);
}
}

@ -2,10 +2,9 @@ import kbn from 'app/core/utils/kbn';
import _ from 'lodash';
import { deprecationWarning, ScopedVars, textUtil, TimeRange } from '@grafana/data';
import { getFilteredVariables, getVariables, getVariableWithName } from '../variables/state/selectors';
import { getConfig } from 'app/core/config';
import { variableRegex } from './utils';
import { variableRegex } from '../variables/utils';
import { isAdHoc } from '../variables/guard';
import { VariableModel } from './types';
import { VariableModel } from '../variables/types';
import { setTemplateSrv, TemplateSrv as BaseTemplateSrv } from '@grafana/runtime';
import { variableAdapters } from '../variables/adapters';
@ -65,11 +64,7 @@ export class TemplateSrv implements BaseTemplateSrv {
}
getVariables(): VariableModel[] {
if (getConfig().featureToggles.newVariables) {
return this.dependencies.getVariables();
}
return this._variables;
return this.dependencies.getVariables();
}
updateIndex() {
@ -431,7 +426,7 @@ export class TemplateSrv implements BaseTemplateSrv {
return;
}
if (getConfig().featureToggles.newVariables && !this.index[name]) {
if (!this.index[name]) {
return this.dependencies.getVariableWithName(name);
}
@ -439,13 +434,7 @@ export class TemplateSrv implements BaseTemplateSrv {
};
private getAdHocVariables = (): any[] => {
if (getConfig().featureToggles.newVariables) {
return this.dependencies.getFilteredVariables(isAdHoc);
}
if (Array.isArray(this._variables)) {
return this._variables.filter(isAdHoc);
}
return [];
return this.dependencies.getFilteredVariables(isAdHoc);
};
}

@ -1,427 +0,0 @@
// Libaries
import angular, { auto, ILocationService, IPromise, IQService } from 'angular';
import _ from 'lodash';
// Utils & Services
import coreModule from 'app/core/core_module';
import { VariableActions, variableTypes } from './types';
import { Graph } from 'app/core/utils/dag';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
// Types
import { AppEvents, TimeRange, UrlQueryMap } from '@grafana/data';
import { CoreEvents } from 'app/types';
import { appEvents, contextSrv } from 'app/core/core';
export class VariableSrv {
dashboard: DashboardModel;
variables: any[] = [];
/** @ngInject */
constructor(
private $q: IQService,
private $location: ILocationService,
private $injector: auto.IInjectorService,
private templateSrv: TemplateSrv,
private timeSrv: TimeSrv
) {}
init(dashboard: DashboardModel) {
this.dashboard = dashboard;
this.dashboard.events.on(CoreEvents.timeRangeUpdated, this.onTimeRangeUpdated.bind(this));
this.dashboard.events.on(
CoreEvents.templateVariableValueUpdated,
this.updateUrlParamsWithCurrentVariables.bind(this)
);
// create working class models representing variables
this.variables = dashboard.templating.list = dashboard.templating.list.map(this.createVariableFromModel.bind(this));
this.templateSrv.init(this.variables, this.timeSrv.timeRange());
// init variables
for (const variable of this.variables) {
variable.initLock = this.$q.defer();
}
const queryParams = this.$location.search();
return this.$q
.all(
this.variables.map(variable => {
return this.processVariable(variable, queryParams);
})
)
.then(() => {
this.templateSrv.updateIndex();
this.templateSrv.setGlobalVariable('__dashboard', {
value: {
name: dashboard.title,
uid: dashboard.uid,
toString: function() {
return this.uid;
},
},
});
this.templateSrv.setGlobalVariable('__org', {
value: {
name: contextSrv.user.orgName,
id: contextSrv.user.orgId,
toString: function() {
return this.id;
},
},
});
this.templateSrv.setGlobalVariable('__user', {
value: {
login: contextSrv.user.login,
id: contextSrv.user.id,
toString: function() {
return this.id;
},
},
});
});
}
onTimeRangeUpdated(timeRange: TimeRange) {
this.templateSrv.updateTimeRange(timeRange);
const promises = this.variables
.filter(variable => variable.refresh === 2)
.map(variable => {
const previousOptions = variable.options.slice();
return variable.updateOptions().then(() => {
if (angular.toJson(previousOptions) !== angular.toJson(variable.options)) {
this.dashboard.templateVariableValueUpdated();
}
});
});
return this.$q
.all(promises)
.then(() => {
this.dashboard.startRefresh();
})
.catch(e => {
appEvents.emit(AppEvents.alertError, ['Template variable service failed', e.message]);
});
}
processVariable(variable: any, queryParams: any) {
const dependencies = [];
for (const otherVariable of this.variables) {
if (variable.dependsOn(otherVariable)) {
dependencies.push(otherVariable.initLock.promise);
}
}
return this.$q
.all(dependencies)
.then(() => {
const urlValue = queryParams['var-' + variable.name];
if (urlValue !== void 0) {
return variable.setValueFromUrl(urlValue).then(variable.initLock.resolve);
}
if (variable.refresh === 1 || variable.refresh === 2) {
return variable.updateOptions().then(variable.initLock.resolve);
}
variable.initLock.resolve();
})
.finally(() => {
this.templateSrv.variableInitialized(variable);
delete variable.initLock;
});
}
createVariableFromModel(model: any) {
// @ts-ignore
const ctor = variableTypes[model.type].ctor;
if (!ctor) {
throw {
message: 'Unable to find variable constructor for ' + model.type,
};
}
const variable = this.$injector.instantiate(ctor, { model: model });
return variable;
}
addVariable(variable: any) {
this.variables.push(variable);
this.templateSrv.updateIndex();
this.dashboard.updateSubmenuVisibility();
}
removeVariable(variable: any) {
const index = _.indexOf(this.variables, variable);
this.variables.splice(index, 1);
this.templateSrv.updateIndex();
this.dashboard.updateSubmenuVisibility();
}
updateOptions(variable: any) {
return variable.updateOptions();
}
variableUpdated(variable: any, emitChangeEvents?: any) {
// if there is a variable lock ignore cascading update because we are in a boot up scenario
if (variable.initLock) {
return this.$q.when();
}
const g = this.createGraph();
const node = g.getNode(variable.name);
let promises = [];
if (node) {
promises = node.getOptimizedInputEdges().map(e => {
return this.updateOptions(this.variables.find(v => v.name === e.inputNode.name));
});
}
return this.$q.all(promises).then(() => {
if (emitChangeEvents) {
this.dashboard.templateVariableValueUpdated();
this.dashboard.startRefresh();
}
});
}
selectOptionsForCurrentValue(variable: any) {
let i, y, value, option;
const selected: any = [];
for (i = 0; i < variable.options.length; i++) {
option = variable.options[i];
option.selected = false;
if (_.isArray(variable.current.value)) {
for (y = 0; y < variable.current.value.length; y++) {
value = variable.current.value[y];
if (option.value === value) {
option.selected = true;
selected.push(option);
}
}
} else if (option.value === variable.current.value) {
option.selected = true;
selected.push(option);
}
}
return selected;
}
validateVariableSelectionState(variable: any, defaultValue?: string) {
if (!variable.current) {
variable.current = {};
}
if (_.isArray(variable.current.value)) {
let selected = this.selectOptionsForCurrentValue(variable);
// if none pick first
if (selected.length === 0) {
selected = variable.options[0];
} else {
selected = {
value: _.map(selected, val => {
return val.value;
}),
text: _.map(selected, val => {
return val.text;
}),
};
}
return variable.setValue(selected);
} else {
let option: any = undefined;
// 1. find the current value
option = _.find(variable.options, {
text: variable.current.text,
});
if (option) {
return variable.setValue(option);
}
// 2. find the default value
if (defaultValue) {
option = _.find(variable.options, {
text: defaultValue,
});
if (option) {
return variable.setValue(option);
}
}
// 3. use the first value
if (variable.options) {
return variable.setValue(variable.options[0]);
}
// 4... give up
return Promise.resolve();
}
}
/**
* 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[]): IPromise<any> {
let promise = this.$q.when();
if (variable.refresh) {
promise = variable.updateOptions();
}
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;
});
// 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;
}, []);
}
// 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 (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 = { text: _.castArray(option.text), value: _.castArray(option.value) };
}
return variable.setValue(option);
});
}
setOptionAsCurrent(variable: any, option: any) {
variable.current = _.cloneDeep(option);
if (_.isArray(variable.current.text) && variable.current.text.length > 0) {
variable.current.text = variable.current.text.join(' + ');
} else if (_.isArray(variable.current.value) && variable.current.value[0] !== '$__all') {
variable.current.text = variable.current.value.join(' + ');
}
this.selectOptionsForCurrentValue(variable);
return this.variableUpdated(variable);
}
templateVarsChangedInUrl(vars: UrlQueryMap) {
const update: Array<Promise<any>> = [];
for (const v of this.variables) {
const key = `var-${v.name}`;
if (vars.hasOwnProperty(key)) {
if (this.isVariableUrlValueDifferentFromCurrent(v, vars[key])) {
update.push(v.setValueFromUrl(vars[key]));
}
}
}
if (update.length) {
Promise.all(update).then(() => {
this.dashboard.templateVariableValueUpdated();
this.dashboard.startRefresh();
});
}
}
isVariableUrlValueDifferentFromCurrent(variable: VariableActions, urlValue: any) {
// lodash _.isEqual handles array of value equality checks as well
return !_.isEqual(variable.getValueForUrl(), urlValue);
}
updateUrlParamsWithCurrentVariables() {
// update url
const params = this.$location.search();
// remove variable params
_.each(params, (value, key) => {
if (key.indexOf('var-') === 0) {
delete params[key];
}
});
// add new values
this.templateSrv.fillVariableValuesForUrl(params);
// update url
this.$location.search(params);
}
setAdhocFilter(options: any) {
let variable: any = _.find(this.variables, {
type: 'adhoc',
datasource: options.datasource,
} as any);
if (!variable) {
variable = this.createVariableFromModel({
name: 'Filters',
type: 'adhoc',
datasource: options.datasource,
});
this.addVariable(variable);
}
const filters = variable.filters;
let filter: any = _.find(filters, { key: options.key, value: options.value });
if (!filter) {
filter = { key: options.key, value: options.value };
filters.push(filter);
}
filter.operator = options.operator;
this.variableUpdated(variable, true);
}
createGraph() {
const g = new Graph();
this.variables.forEach(v => {
g.createNode(v.name);
});
this.variables.forEach(v1 => {
this.variables.forEach(v2 => {
if (v1 === v2) {
return;
}
if (v1.dependsOn(v2)) {
g.link(v1.name, v2.name);
}
});
});
return g;
}
}
coreModule.service('variableSrv', VariableSrv);

@ -12,7 +12,7 @@ import {
TextBoxVariableModel,
VariableModel,
VariableOption,
} from '../templating/types';
} from './types';
import { VariableEditorProps } from './editor/types';
import { VariablesState } from './state/variablesReducer';
import { VariablePickerProps } from './pickers/types';

@ -1,7 +1,7 @@
import React, { PureComponent } from 'react';
import { MapDispatchToProps, MapStateToProps } from 'react-redux';
import { AdHocVariableModel } from '../../templating/types';
import { AdHocVariableModel } from '../types';
import { VariableEditorProps } from '../editor/types';
import { VariableEditorState } from '../editor/reducer';
import { AdHocVariableEditorState } from './reducer';

@ -20,7 +20,7 @@ import { filterAdded, filterRemoved, filtersRestored, filterUpdated } from './re
import { addVariable, changeVariableProp } from '../state/sharedReducer';
import { updateLocation } from 'app/core/actions';
import { DashboardState, LocationState } from 'app/types';
import { VariableModel } from 'app/features/templating/types';
import { VariableModel } from 'app/features/variables/types';
import { changeVariableEditorExtended, setIdInEditor } from '../editor/reducer';
import { adHocBuilder } from '../shared/testing/builders';
@ -421,7 +421,7 @@ describe('adhoc actions', () => {
const tester = await reduxTester<ReducersUsedInContext>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(createAddVariableAction(variable))
.whenActionIsDispatched(setIdInEditor({ id: variable.id! }))
.whenActionIsDispatched(setIdInEditor({ id: variable.id }))
.whenAsyncActionIsDispatched(changeVariableDatasource(datasource), true);
tester.thenDispatchedActionsShouldEqual(
@ -453,7 +453,7 @@ describe('adhoc actions', () => {
const tester = await reduxTester<ReducersUsedInContext>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(createAddVariableAction(variable))
.whenActionIsDispatched(setIdInEditor({ id: variable.id! }))
.whenActionIsDispatched(setIdInEditor({ id: variable.id }))
.whenAsyncActionIsDispatched(changeVariableDatasource(datasource), true);
tester.thenDispatchedActionsShouldEqual(

@ -13,7 +13,7 @@ import {
filterUpdated,
initialAdHocVariableModelState,
} from './reducer';
import { AdHocVariableFilter, AdHocVariableModel } from 'app/features/templating/types';
import { AdHocVariableFilter, AdHocVariableModel } from 'app/features/variables/types';
import { variableUpdated } from '../state/actions';
import { isAdHoc } from '../guard';
@ -40,11 +40,11 @@ export const applyFilterFromTable = (options: AdHocTableOptions): ThunkResult<vo
if (index === -1) {
const { value, key, operator } = options;
const filter = { value, key, operator, condition: '' };
return await dispatch(addFilter(variable.id!, filter));
return await dispatch(addFilter(variable.id, filter));
}
const filter = { ...variable.filters[index], operator: options.operator };
return await dispatch(changeFilter(variable.id!, { index, filter }));
return await dispatch(changeFilter(variable.id, { index, filter }));
};
};

@ -1,6 +1,6 @@
import cloneDeep from 'lodash/cloneDeep';
import { AdHocVariableModel } from '../../templating/types';
import { AdHocVariableModel } from '../types';
import { dispatch } from '../../../store/store';
import { VariableAdapter } from '../adapters';
import { AdHocPicker } from './picker/AdHocPicker';
@ -24,7 +24,7 @@ export const createAdHocVariableAdapter = (): VariableAdapter<AdHocVariableModel
setValue: noop,
setValueFromUrl: async (variable, urlValue) => {
const filters = urlParser.toFilters(urlValue);
await dispatch(setFiltersFromUrl(variable.id!, filters));
await dispatch(setFiltersFromUrl(variable.id, filters));
},
updateOptions: noop,
getSaveModel: variable => {

@ -1,7 +1,7 @@
import React, { FC, ReactElement, useState } from 'react';
import { SegmentAsync, Icon } from '@grafana/ui';
import { Icon, SegmentAsync } from '@grafana/ui';
import { OperatorSegment } from './OperatorSegment';
import { AdHocVariableFilter } from 'app/features/templating/types';
import { AdHocVariableFilter } from 'app/features/variables/types';
import { SelectableValue } from '@grafana/data';
interface Props {

@ -1,7 +1,7 @@
import React, { PureComponent, ReactNode } from 'react';
import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux';
import { StoreState } from 'app/types';
import { AdHocVariableFilter, AdHocVariableModel } from 'app/features/templating/types';
import { AdHocVariableFilter, AdHocVariableModel } from 'app/features/variables/types';
import { SegmentAsync } from '@grafana/ui';
import { VariablePickerProps } from '../../pickers/types';
import { OperatorSegment } from './OperatorSegment';
@ -31,10 +31,10 @@ export class AdHocPickerUnconnected extends PureComponent<Props> {
const { value } = key;
if (key.value === REMOVE_FILTER_KEY) {
return this.props.removeFilter(id!, index);
return this.props.removeFilter(id, index);
}
return this.props.changeFilter(id!, {
return this.props.changeFilter(id, {
index,
filter: {
...filters[index],
@ -45,7 +45,7 @@ export class AdHocPickerUnconnected extends PureComponent<Props> {
appendFilterToVariable = (filter: AdHocVariableFilter) => {
const { id } = this.props.variable;
this.props.addFilter(id!, filter);
this.props.addFilter(id, filter);
};
fetchFilterKeys = async () => {

@ -4,7 +4,7 @@ import { getVariableTestContext } from '../state/helpers';
import { toVariablePayload } from '../state/types';
import { adHocVariableReducer, filterAdded, filterRemoved, filtersRestored, filterUpdated } from './reducer';
import { VariablesState } from '../state/variablesReducer';
import { AdHocVariableFilter, AdHocVariableModel } from '../../templating/types';
import { AdHocVariableFilter, AdHocVariableModel } from '../types';
import { createAdHocVariableAdapter } from './adapter';
describe('adHocVariableReducer', () => {

@ -1,4 +1,4 @@
import { AdHocVariableFilter, AdHocVariableModel, VariableHide } from 'app/features/templating/types';
import { AdHocVariableFilter, AdHocVariableModel, VariableHide } from 'app/features/variables/types';
import { getInstanceState, NEW_VARIABLE_ID, VariablePayload } from '../state/types';
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { initialVariablesState, VariablesState } from '../state/variablesReducer';

@ -1,5 +1,5 @@
import { toFilters, toUrl } from './urlParser';
import { AdHocVariableFilter } from 'app/features/templating/types';
import { AdHocVariableFilter } from 'app/features/variables/types';
import { UrlQueryValue } from '@grafana/data';
describe('urlParser', () => {

@ -1,4 +1,4 @@
import { AdHocVariableFilter } from 'app/features/templating/types';
import { AdHocVariableFilter } from 'app/features/variables/types';
import { UrlQueryValue } from '@grafana/data';
import { isArray, isString } from 'lodash';

@ -1,7 +1,7 @@
import React, { ChangeEvent, FocusEvent, PureComponent } from 'react';
import { selectors } from '@grafana/e2e-selectors';
import { ConstantVariableModel } from '../../templating/types';
import { ConstantVariableModel } from '../types';
import { VariableEditorProps } from '../editor/types';
export interface Props extends VariableEditorProps<ConstantVariableModel> {}

@ -4,10 +4,10 @@ import { reduxTester } from '../../../../test/core/redux/reduxTester';
import { TemplatingState } from 'app/features/variables/state/reducers';
import { updateConstantVariableOptions } from './actions';
import { getRootReducer } from '../state/helpers';
import { ConstantVariableModel, VariableHide, VariableOption } from '../../templating/types';
import { ConstantVariableModel, VariableHide, VariableOption } from '../types';
import { toVariablePayload } from '../state/types';
import { createConstantOptionsFromQuery } from './reducer';
import { setCurrentVariableValue, addVariable } from '../state/sharedReducer';
import { addVariable, setCurrentVariableValue } from '../state/sharedReducer';
describe('constant actions', () => {
variableAdapters.setInit(() => [createConstantVariableAdapter()]);

@ -1,5 +1,5 @@
import cloneDeep from 'lodash/cloneDeep';
import { ConstantVariableModel } from '../../templating/types';
import { ConstantVariableModel } from '../types';
import { dispatch } from '../../../store/store';
import { setOptionAsCurrent, setOptionFromUrl } from '../state/actions';
import { VariableAdapter } from '../adapters';

@ -4,7 +4,7 @@ import { getVariableTestContext } from '../state/helpers';
import { toVariablePayload } from '../state/types';
import { constantVariableReducer, createConstantOptionsFromQuery } from './reducer';
import { VariablesState } from '../state/variablesReducer';
import { ConstantVariableModel } from '../../templating/types';
import { ConstantVariableModel } from '../types';
import { createConstantVariableAdapter } from './adapter';
describe('constantVariableReducer', () => {

@ -1,5 +1,5 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { ConstantVariableModel, VariableHide, VariableOption } from '../../templating/types';
import { ConstantVariableModel, VariableHide, VariableOption } from '../types';
import { getInstanceState, NEW_VARIABLE_ID, VariablePayload } from '../state/types';
import { initialVariablesState, VariablesState } from '../state/variablesReducer';

@ -1,5 +1,5 @@
import React, { ChangeEvent, FocusEvent, PureComponent } from 'react';
import { CustomVariableModel, VariableWithMultiSupport } from '../../templating/types';
import { CustomVariableModel, VariableWithMultiSupport } from '../types';
import { SelectionOptionsEditor } from '../editor/SelectionOptionsEditor';
import { OnPropChangeArguments, VariableEditorProps } from '../editor/types';
import { connectWithStore } from 'app/core/utils/connectWithReduxStore';

@ -3,9 +3,9 @@ import { updateCustomVariableOptions } from './actions';
import { createCustomVariableAdapter } from './adapter';
import { reduxTester } from '../../../../test/core/redux/reduxTester';
import { getRootReducer } from '../state/helpers';
import { CustomVariableModel, VariableHide, VariableOption } from '../../templating/types';
import { CustomVariableModel, VariableHide, VariableOption } from '../types';
import { toVariablePayload } from '../state/types';
import { setCurrentVariableValue, addVariable } from '../state/sharedReducer';
import { addVariable, setCurrentVariableValue } from '../state/sharedReducer';
import { TemplatingState } from '../state/reducers';
import { createCustomOptionsFromQuery } from './reducer';

@ -1,5 +1,5 @@
import cloneDeep from 'lodash/cloneDeep';
import { CustomVariableModel } from '../../templating/types';
import { CustomVariableModel } from '../types';
import { dispatch } from '../../../store/store';
import { setOptionAsCurrent, setOptionFromUrl } from '../state/actions';
import { VariableAdapter } from '../adapters';

@ -5,7 +5,7 @@ import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE, toVariablePayload } from '../sta
import { createCustomOptionsFromQuery, customVariableReducer } from './reducer';
import { createCustomVariableAdapter } from './adapter';
import { VariablesState } from '../state/variablesReducer';
import { CustomVariableModel } from '../../templating/types';
import { CustomVariableModel } from '../types';
describe('customVariableReducer', () => {
const adapter = createCustomVariableAdapter();

@ -1,6 +1,6 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { CustomVariableModel, VariableHide, VariableOption } from '../../templating/types';
import { CustomVariableModel, VariableHide, VariableOption } from '../types';
import {
ALL_VARIABLE_TEXT,
ALL_VARIABLE_VALUE,

@ -1,6 +1,6 @@
import React, { ChangeEvent, FocusEvent, PureComponent } from 'react';
import { DataSourceVariableModel, VariableWithMultiSupport } from '../../templating/types';
import { DataSourceVariableModel, VariableWithMultiSupport } from '../types';
import { OnPropChangeArguments, VariableEditorProps } from '../editor/types';
import { SelectionOptionsEditor } from '../editor/SelectionOptionsEditor';
import { InlineFormLabel } from '@grafana/ui';

@ -5,7 +5,7 @@ import { validateVariableSelectionState } from '../state/actions';
import { DataSourceSelectItem, stringToJsRegex } from '@grafana/data';
import { getDatasourceSrv } from '../../plugins/datasource_srv';
import { getVariable } from '../state/selectors';
import { DataSourceVariableModel } from '../../templating/types';
import { DataSourceVariableModel } from '../types';
import templateSrv from '../../templating/template_srv';
import _ from 'lodash';
import { changeVariableEditorExtended } from '../editor/reducer';
@ -19,7 +19,7 @@ export const updateDataSourceVariableOptions = (
dependencies: DataSourceVariableActionDependencies = { getDatasourceSrv: getDatasourceSrv }
): ThunkResult<void> => async (dispatch, getState) => {
const sources = await dependencies.getDatasourceSrv().getMetricSources({ skipVariables: true });
const variableInState = getVariable<DataSourceVariableModel>(identifier.id!, getState());
const variableInState = getVariable<DataSourceVariableModel>(identifier.id, getState());
let regex;
if (variableInState.regex) {

@ -1,5 +1,5 @@
import cloneDeep from 'lodash/cloneDeep';
import { DataSourceVariableModel } from '../../templating/types';
import { DataSourceVariableModel } from '../types';
import { dispatch } from '../../../store/store';
import { setOptionAsCurrent, setOptionFromUrl } from '../state/actions';
import { VariableAdapter } from '../adapters';
@ -8,7 +8,7 @@ import { OptionsPicker } from '../pickers';
import { ALL_VARIABLE_TEXT, toVariableIdentifier } from '../state/types';
import { DataSourceVariableEditor } from './DataSourceVariableEditor';
import { updateDataSourceVariableOptions } from './actions';
import { containsVariable } from '../../templating/utils';
import { containsVariable } from '../utils';
export const createDataSourceVariableAdapter = (): VariableAdapter<DataSourceVariableModel> => {
return {

@ -1,7 +1,7 @@
import { reducerTester } from '../../../../test/core/redux/reducerTester';
import { VariablesState } from '../state/variablesReducer';
import { createDataSourceOptions, dataSourceVariableReducer } from './reducer';
import { DataSourceVariableModel } from '../../templating/types';
import { DataSourceVariableModel } from '../types';
import { getVariableTestContext } from '../state/helpers';
import cloneDeep from 'lodash/cloneDeep';
import { createDataSourceVariableAdapter } from './adapter';

@ -1,5 +1,5 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { DataSourceVariableModel, VariableHide, VariableOption, VariableRefresh } from '../../templating/types';
import { DataSourceVariableModel, VariableHide, VariableOption, VariableRefresh } from '../types';
import {
ALL_VARIABLE_TEXT,
ALL_VARIABLE_VALUE,

@ -2,7 +2,7 @@ import React, { FunctionComponent, useCallback } from 'react';
import { LegacyForms } from '@grafana/ui';
import { selectors } from '@grafana/e2e-selectors';
import { VariableWithMultiSupport } from '../../templating/types';
import { VariableWithMultiSupport } from '../types';
import { VariableEditorProps } from './types';
import { toVariableIdentifier, VariableIdentifier } from '../state/types';

@ -9,7 +9,7 @@ import { VariableEditorEditor } from './VariableEditorEditor';
import { MapDispatchToProps, MapStateToProps } from 'react-redux';
import { connectWithStore } from '../../../core/utils/connectWithReduxStore';
import { getVariables } from '../state/selectors';
import { VariableModel } from '../../templating/types';
import { VariableModel } from '../types';
import { switchToEditMode, switchToListMode, switchToNewMode } from './actions';
import { changeVariableOrder, duplicateVariable, removeVariable } from '../state/sharedReducer';

@ -6,7 +6,7 @@ import { selectors } from '@grafana/e2e-selectors';
import { variableAdapters } from '../adapters';
import { NEW_VARIABLE_ID, toVariablePayload, VariableIdentifier } from '../state/types';
import { VariableHide, VariableModel } from '../../templating/types';
import { VariableHide, VariableModel } from '../types';
import { appEvents } from '../../../core/core';
import { VariableValuesPreview } from './VariableValuesPreview';
import { changeVariableName, onEditorAdd, onEditorUpdate, variableEditorMount, variableEditorUnMount } from './actions';

@ -3,7 +3,7 @@ import { IconButton } from '@grafana/ui';
import { selectors } from '@grafana/e2e-selectors';
import EmptyListCTA from '../../../core/components/EmptyListCTA/EmptyListCTA';
import { QueryVariableModel, VariableModel } from '../../templating/types';
import { QueryVariableModel, VariableModel } from '../types';
import { toVariableIdentifier, VariableIdentifier } from '../state/types';
export interface Props {
@ -28,7 +28,7 @@ export class VariableEditorList extends PureComponent<Props> {
onChangeVariableOrder = (event: MouseEvent, variable: VariableModel, moveType: MoveType) => {
event.preventDefault();
this.props.onChangeVariableOrder(toVariableIdentifier(variable), variable.index!, variable.index! + moveType);
this.props.onChangeVariableOrder(toVariableIdentifier(variable), variable.index, variable.index + moveType);
};
onDuplicateVariable = (event: MouseEvent, identifier: VariableIdentifier) => {

@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useState } from 'react';
import { VariableModel, VariableOption, VariableWithOptions } from '../../templating/types';
import { VariableModel, VariableOption, VariableWithOptions } from '../types';
import { selectors } from '@grafana/e2e-selectors';
export interface VariableValuesPreviewProps {

@ -22,7 +22,7 @@ import { addVariable, removeVariable, storeNewVariable } from '../state/sharedRe
export const variableEditorMount = (identifier: VariableIdentifier): ThunkResult<void> => {
return async dispatch => {
dispatch(variableEditorMounted({ name: getVariable(identifier.id!).name }));
dispatch(variableEditorMounted({ name: getVariable(identifier.id).name }));
};
};
@ -37,7 +37,7 @@ export const variableEditorUnMount = (identifier: VariableIdentifier): ThunkResu
export const onEditorUpdate = (identifier: VariableIdentifier): ThunkResult<void> => {
return async (dispatch, getState) => {
const variableInState = getVariable(identifier.id!, getState());
const variableInState = getVariable(identifier.id, getState());
await variableAdapters.get(variableInState.type).updateOptions(variableInState);
dispatch(switchToListMode());
};
@ -101,8 +101,8 @@ export const completeChangeVariableName = (identifier: VariableIdentifier, newNa
) => {
const originalVariable = getVariable(identifier.id, getState());
const model = { ...cloneDeep(originalVariable), name: newName, id: newName };
const global = originalVariable.global!; // global is undefined because of old variable system
const index = originalVariable.index!; // index is undefined because of old variable system
const global = originalVariable.global;
const index = originalVariable.index;
const renamedIdentifier = toVariableIdentifier(model);
dispatch(addVariable(toVariablePayload(renamedIdentifier, { global, index, model })));

@ -1,4 +1,4 @@
import { VariableModel } from '../../templating/types';
import { VariableModel } from '../types';
export interface OnPropChangeArguments<Model extends VariableModel = VariableModel> {
propName: keyof Model;

@ -1,4 +1,10 @@
import { QueryVariableModel, VariableModel, AdHocVariableModel, VariableWithMultiSupport } from '../templating/types';
import {
AdHocVariableModel,
ConstantVariableModel,
QueryVariableModel,
VariableModel,
VariableWithMultiSupport,
} from './types';
export const isQuery = (model: VariableModel): model is QueryVariableModel => {
return model.type === 'query';
@ -8,6 +14,10 @@ export const isAdHoc = (model: VariableModel): model is AdHocVariableModel => {
return model.type === 'adhoc';
};
export const isConstant = (model: VariableModel): model is ConstantVariableModel => {
return model.type === 'constant';
};
export const isMulti = (model: VariableModel): model is VariableWithMultiSupport => {
const withMulti = model as VariableWithMultiSupport;
return withMulti.hasOwnProperty('multi') && typeof withMulti.multi === 'boolean';

@ -1,8 +1,9 @@
import React, { ChangeEvent, FocusEvent, PureComponent } from 'react';
import { IntervalVariableModel } from '../../templating/types';
import { IntervalVariableModel } from '../types';
import { VariableEditorProps } from '../editor/types';
import { InlineFormLabel, LegacyForms } from '@grafana/ui';
const { Switch } = LegacyForms;
export interface Props extends VariableEditorProps<IntervalVariableModel> {}

@ -5,7 +5,7 @@ import { ThunkResult } from '../../../types';
import { createIntervalOptions } from './reducer';
import { validateVariableSelectionState } from '../state/actions';
import { getVariable } from '../state/selectors';
import { IntervalVariableModel } from '../../templating/types';
import { IntervalVariableModel } from '../types';
import kbn from '../../../core/utils/kbn';
import { getTimeSrv } from '../../dashboard/services/TimeSrv';
import templateSrv from '../../templating/template_srv';

@ -1,5 +1,5 @@
import cloneDeep from 'lodash/cloneDeep';
import { IntervalVariableModel } from '../../templating/types';
import { IntervalVariableModel } from '../types';
import { dispatch } from '../../../store/store';
import { setOptionAsCurrent, setOptionFromUrl } from '../state/actions';
import { VariableAdapter } from '../adapters';

@ -3,7 +3,7 @@ import cloneDeep from 'lodash/cloneDeep';
import { getVariableTestContext } from '../state/helpers';
import { toVariablePayload } from '../state/types';
import { createIntervalVariableAdapter } from './adapter';
import { IntervalVariableModel } from '../../templating/types';
import { IntervalVariableModel } from '../types';
import { reducerTester } from '../../../../test/core/redux/reducerTester';
import { VariablesState } from '../state/variablesReducer';
import { createIntervalOptions, intervalVariableReducer } from './reducer';

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save