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
This commit is contained in:
Hugo Häggmark
2020-06-04 13:44:48 +02:00
committed by GitHub
parent 6b4d1dceb0
commit 00a9af00fc
166 changed files with 678 additions and 5917 deletions

View File

@@ -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",

View File

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

View File

@@ -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"
}

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);
});
});
});

View File

@@ -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);

View File

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

View File

@@ -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;

View File

@@ -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,
};
}
}

View File

@@ -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);
});
});

View File

@@ -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>

View File

@@ -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() {

View File

@@ -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>

View File

@@ -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';

View File

@@ -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;
}

View File

@@ -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} />;
};

View File

@@ -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';

View File

@@ -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);

View File

@@ -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>
);
};

View File

@@ -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;
`,
};
});

View File

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

View File

@@ -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>

View File

@@ -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

View File

@@ -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';

View File

@@ -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';

View File

@@ -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>

View File

@@ -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}

View File

@@ -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';

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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,10 +256,8 @@ export class DashboardModel {
timeRangeUpdated(timeRange: TimeRange) {
this.events.emit(CoreEvents.timeRangeUpdated, timeRange);
if (getConfig().featureToggles.newVariables) {
dispatch(onTimeRangeUpdated(timeRange));
}
}
startRefresh() {
this.events.emit(PanelEvents.refresh);
@@ -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,13 +931,9 @@ export class DashboardModel {
}
hasVariableValuesChanged() {
if (getConfig().featureToggles.newVariables) {
return this.hasVariablesChanged(this.originalTemplating, this.getVariablesFromState());
}
return this.hasVariablesChanged(this.originalTemplating, this.templating.list);
}
autoFitPanels(viewHeight: number, kioskMode?: UrlQueryValue) {
const currentGridHeight = Math.max(
...this.panels.map(panel => {
@@ -1048,17 +1001,10 @@ export class DashboardModel {
}
getVariables = () => {
if (getConfig().featureToggles.newVariables) {
return this.getVariablesFromState();
}
return this.templating.list;
};
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,11 +1013,8 @@ export class DashboardModel {
}
private hasVariables() {
if (getConfig().featureToggles.newVariables) {
return this.getVariablesFromState().length > 0;
}
return this.templating.list.length > 0;
}
private hasVariablesChanged(originalVariables: any[], currentVariables: any[]): boolean {
if (originalVariables.length !== currentVariables.length) {

View File

@@ -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);
});
});

View File

@@ -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

View File

@@ -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);

View File

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

View File

@@ -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>

View File

@@ -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);

View File

@@ -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> = {};

View File

@@ -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';

View File

@@ -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',
};

View File

@@ -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',
};

View File

@@ -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,
};

View File

@@ -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',
};

View File

@@ -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,
};

View File

@@ -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',
};

View File

@@ -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);

View File

@@ -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)',
};

View File

@@ -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>

View File

@@ -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,
};

View File

@@ -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');
});
});
});

View File

@@ -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');
});
});
});
});

View File

@@ -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');
});
});
});
});

View File

@@ -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');
});
});
});

View File

@@ -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);
});
});
});
});
});
});

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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,13 +64,9 @@ export class TemplateSrv implements BaseTemplateSrv {
}
getVariables(): VariableModel[] {
if (getConfig().featureToggles.newVariables) {
return this.dependencies.getVariables();
}
return this._variables;
}
updateIndex() {
const existsOrEmpty = (value: any) => value || value === '';
@@ -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 [];
};
}

View File

@@ -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);

View File

@@ -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';

View File

@@ -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';

View File

@@ -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(

View File

@@ -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 }));
};
};

View File

@@ -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 => {

View File

@@ -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 {

View File

@@ -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 () => {

View File

@@ -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', () => {

View File

@@ -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';

View File

@@ -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', () => {

View File

@@ -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';

View File

@@ -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> {}

View File

@@ -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()]);

View File

@@ -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';

View File

@@ -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', () => {

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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();

View File

@@ -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,

View File

@@ -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';

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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';

View File

@@ -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,

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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) => {

View File

@@ -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 {

View File

@@ -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 })));

View File

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

View File

@@ -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';

View File

@@ -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> {}

View File

@@ -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';

View File

@@ -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';

View File

@@ -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