mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -1632,10 +1632,7 @@
|
||||
"revision": 8,
|
||||
"schemaVersion": 16,
|
||||
"style": "dark",
|
||||
"tags": [
|
||||
"gdev",
|
||||
"panel-tests"
|
||||
],
|
||||
"tags": ["gdev", "panel-tests"],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
@@ -1644,29 +1641,8 @@
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
"refresh_intervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
],
|
||||
"time_options": [
|
||||
"5m",
|
||||
"15m",
|
||||
"1h",
|
||||
"6h",
|
||||
"12h",
|
||||
"24h",
|
||||
"2d",
|
||||
"7d",
|
||||
"30d"
|
||||
]
|
||||
"refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"],
|
||||
"time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"]
|
||||
},
|
||||
"timezone": "browser",
|
||||
"title": "Panel Tests - Graph",
|
||||
|
||||
@@ -38,7 +38,6 @@ export interface FeatureToggles {
|
||||
* Available only in Grafana Enterprise
|
||||
*/
|
||||
meta: boolean;
|
||||
newVariables: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
5
packages/grafana-e2e/cypress/fixtures/example.json
Normal file
5
packages/grafana-e2e/cypress/fixtures/example.json
Normal 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"
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import merge from 'lodash/merge';
|
||||
import { getTheme } from '@grafana/ui';
|
||||
import {
|
||||
BuildInfo,
|
||||
DataSourceInstanceSettings,
|
||||
FeatureToggles,
|
||||
GrafanaConfig,
|
||||
GrafanaTheme,
|
||||
GrafanaThemeType,
|
||||
PanelPluginMeta,
|
||||
GrafanaConfig,
|
||||
LicenseInfo,
|
||||
BuildInfo,
|
||||
FeatureToggles,
|
||||
PanelPluginMeta,
|
||||
} from '@grafana/data';
|
||||
|
||||
export class GrafanaBootConfig implements GrafanaConfig {
|
||||
@@ -52,7 +52,6 @@ export class GrafanaBootConfig implements GrafanaConfig {
|
||||
expressions: false,
|
||||
newEdit: false,
|
||||
meta: false,
|
||||
newVariables: true,
|
||||
};
|
||||
licenseInfo: LicenseInfo = {} as LicenseInfo;
|
||||
rendererAvailable = false;
|
||||
|
||||
@@ -4,7 +4,6 @@ import './directives/metric_segment';
|
||||
import './directives/misc';
|
||||
import './directives/ng_model_on_blur';
|
||||
import './directives/tags';
|
||||
import './directives/value_select_dropdown';
|
||||
import './directives/rebuild_on_change';
|
||||
import './directives/give_focus';
|
||||
import './directives/diff-view';
|
||||
@@ -39,8 +38,7 @@ import { NavModelSrv } from './nav_model_srv';
|
||||
import { geminiScrollbar } from './components/scroll/scroll';
|
||||
import { profiler } from './profiler';
|
||||
import { registerAngularDirectives } from './angular_wrappers';
|
||||
import { updateLegendValues } from './time_series2';
|
||||
import TimeSeries from './time_series2';
|
||||
import TimeSeries, { updateLegendValues } from './time_series2';
|
||||
import { NavModel } from '@grafana/data';
|
||||
|
||||
export {
|
||||
|
||||
@@ -1,356 +0,0 @@
|
||||
import angular, { IScope } from 'angular';
|
||||
import debounce from 'lodash/debounce';
|
||||
import each from 'lodash/each';
|
||||
import filter from 'lodash/filter';
|
||||
import find from 'lodash/find';
|
||||
import indexOf from 'lodash/indexOf';
|
||||
import map from 'lodash/map';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
import coreModule from '../core_module';
|
||||
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
|
||||
import { containsSearchFilter } from '../../features/templating/utils';
|
||||
|
||||
export class ValueSelectDropdownCtrl {
|
||||
dropdownVisible: any;
|
||||
highlightIndex: any;
|
||||
linkText: any;
|
||||
oldVariableText: any;
|
||||
options: any;
|
||||
search: any;
|
||||
selectedTags: any;
|
||||
selectedValues: any;
|
||||
tags: any;
|
||||
variable: any;
|
||||
|
||||
hide: any;
|
||||
onUpdated: any;
|
||||
queryHasSearchFilter: boolean;
|
||||
debouncedQueryChanged: Function;
|
||||
selectors: typeof selectors.pages.Dashboard.SubMenu;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $scope: IScope) {
|
||||
this.queryHasSearchFilter = this.variable ? containsSearchFilter(this.variable.query) : false;
|
||||
this.debouncedQueryChanged = debounce(this.queryChanged.bind(this), 200);
|
||||
this.selectors = selectors.pages.Dashboard.SubMenu;
|
||||
}
|
||||
|
||||
show() {
|
||||
this.oldVariableText = this.variable.current.text;
|
||||
this.highlightIndex = -1;
|
||||
|
||||
this.options = this.variable.options;
|
||||
this.selectedValues = filter(this.options, { selected: true });
|
||||
|
||||
this.tags = map(this.variable.tags, value => {
|
||||
let tag = { text: value, selected: false };
|
||||
each(this.variable.current.tags, tagObj => {
|
||||
if (tagObj.text === value) {
|
||||
tag = tagObj;
|
||||
}
|
||||
});
|
||||
return tag;
|
||||
});
|
||||
|
||||
// new behaviour, if this is a query that uses searchfilter it might be a nicer
|
||||
// user experience to show the last typed search query in the input field
|
||||
const query = this.queryHasSearchFilter && this.search && this.search.query ? this.search.query : '';
|
||||
|
||||
this.search = {
|
||||
query,
|
||||
options: this.options.slice(0, Math.min(this.options.length, 1000)),
|
||||
};
|
||||
|
||||
this.dropdownVisible = true;
|
||||
}
|
||||
|
||||
updateLinkText() {
|
||||
const current = this.variable.current;
|
||||
|
||||
if (current.tags && current.tags.length) {
|
||||
// filer out values that are in selected tags
|
||||
const selectedAndNotInTag = filter(this.variable.options, option => {
|
||||
if (!option.selected) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < current.tags.length; i++) {
|
||||
const tag = current.tags[i];
|
||||
if (indexOf(tag.values, option.value) !== -1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// convert values to text
|
||||
const currentTexts = map(selectedAndNotInTag, 'text');
|
||||
|
||||
// join texts
|
||||
this.linkText = currentTexts.join(' + ');
|
||||
if (this.linkText.length > 0) {
|
||||
this.linkText += ' + ';
|
||||
}
|
||||
} else {
|
||||
this.linkText = this.variable.current.text;
|
||||
}
|
||||
}
|
||||
|
||||
clearSelections() {
|
||||
this.selectedValues = filter(this.options, { selected: true });
|
||||
|
||||
if (this.selectedValues.length) {
|
||||
each(this.options, option => {
|
||||
option.selected = false;
|
||||
});
|
||||
} else {
|
||||
each(this.search.options, option => {
|
||||
option.selected = true;
|
||||
});
|
||||
}
|
||||
this.selectionsChanged(false);
|
||||
}
|
||||
|
||||
selectTag(tag: any) {
|
||||
tag.selected = !tag.selected;
|
||||
let tagValuesPromise;
|
||||
if (!tag.values) {
|
||||
tagValuesPromise = this.variable.getValuesForTag(tag.text);
|
||||
} else {
|
||||
tagValuesPromise = Promise.resolve(tag.values);
|
||||
}
|
||||
|
||||
return tagValuesPromise.then((values: any) => {
|
||||
tag.values = values;
|
||||
tag.valuesText = values.join(' + ');
|
||||
each(this.options, option => {
|
||||
if (indexOf(tag.values, option.value) !== -1) {
|
||||
option.selected = tag.selected;
|
||||
}
|
||||
});
|
||||
|
||||
this.selectionsChanged(false);
|
||||
});
|
||||
}
|
||||
|
||||
keyDown(evt: any) {
|
||||
if (evt.keyCode === 27) {
|
||||
this.hide();
|
||||
}
|
||||
if (evt.keyCode === 40) {
|
||||
this.moveHighlight(1);
|
||||
}
|
||||
if (evt.keyCode === 38) {
|
||||
this.moveHighlight(-1);
|
||||
}
|
||||
if (evt.keyCode === 13) {
|
||||
if (this.search.options.length === 0) {
|
||||
this.commitChanges();
|
||||
} else {
|
||||
this.selectValue(this.search.options[this.highlightIndex], {}, true);
|
||||
}
|
||||
}
|
||||
if (evt.keyCode === 32) {
|
||||
this.selectValue(this.search.options[this.highlightIndex], {}, false);
|
||||
}
|
||||
}
|
||||
|
||||
moveHighlight(direction: number) {
|
||||
this.highlightIndex = (this.highlightIndex + direction) % this.search.options.length;
|
||||
}
|
||||
|
||||
selectValue(option: any, event: any, commitChange?: boolean) {
|
||||
if (!option) {
|
||||
return;
|
||||
}
|
||||
|
||||
option.selected = this.variable.multi ? !option.selected : true;
|
||||
|
||||
commitChange = commitChange || false;
|
||||
|
||||
const setAllExceptCurrentTo = (newValue: any) => {
|
||||
each(this.options, other => {
|
||||
if (option !== other) {
|
||||
other.selected = newValue;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// commit action (enter key), should not deselect it
|
||||
if (commitChange) {
|
||||
option.selected = true;
|
||||
}
|
||||
|
||||
if (option.text === 'All') {
|
||||
// always clear search query if all is marked
|
||||
this.search.query = '';
|
||||
setAllExceptCurrentTo(false);
|
||||
commitChange = true;
|
||||
} else if (!this.variable.multi) {
|
||||
setAllExceptCurrentTo(false);
|
||||
commitChange = true;
|
||||
} else if (event.ctrlKey || event.metaKey || event.shiftKey) {
|
||||
commitChange = true;
|
||||
setAllExceptCurrentTo(false);
|
||||
}
|
||||
|
||||
this.selectionsChanged(commitChange);
|
||||
}
|
||||
|
||||
selectionsChanged(commitChange: boolean) {
|
||||
this.selectedValues = filter(this.options, { selected: true });
|
||||
|
||||
if (this.selectedValues.length > 1) {
|
||||
if (this.selectedValues[0].text === 'All') {
|
||||
this.selectedValues[0].selected = false;
|
||||
this.selectedValues = this.selectedValues.slice(1, this.selectedValues.length);
|
||||
}
|
||||
}
|
||||
|
||||
// validate selected tags
|
||||
each(this.tags, tag => {
|
||||
if (tag.selected) {
|
||||
each(tag.values, value => {
|
||||
if (!find(this.selectedValues, { value: value })) {
|
||||
tag.selected = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.selectedTags = filter(this.tags, { selected: true });
|
||||
this.variable.current.value = map(this.selectedValues, 'value');
|
||||
this.variable.current.text = map(this.selectedValues, 'text').join(' + ');
|
||||
this.variable.current.tags = this.selectedTags;
|
||||
|
||||
if (!this.variable.multi) {
|
||||
this.variable.current.value = this.selectedValues[0].value;
|
||||
}
|
||||
|
||||
if (commitChange) {
|
||||
this.commitChanges();
|
||||
}
|
||||
}
|
||||
|
||||
commitChanges() {
|
||||
// if we have a search query and no options use that
|
||||
if (this.search.options.length === 0 && this.search.query.length > 0) {
|
||||
this.variable.current = { text: this.search.query, value: this.search.query };
|
||||
} else if (this.selectedValues.length === 0) {
|
||||
// make sure one option is selected
|
||||
this.options[0].selected = true;
|
||||
this.selectionsChanged(false);
|
||||
}
|
||||
|
||||
this.dropdownVisible = false;
|
||||
this.updateLinkText();
|
||||
if (this.queryHasSearchFilter) {
|
||||
this.updateLazyLoadedOptions();
|
||||
}
|
||||
|
||||
if (this.variable.current.text !== this.oldVariableText) {
|
||||
this.onUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
async queryChanged() {
|
||||
if (this.queryHasSearchFilter) {
|
||||
await this.updateLazyLoadedOptions();
|
||||
return;
|
||||
}
|
||||
|
||||
const options = filter(this.options, option => {
|
||||
return option.text.toLowerCase().indexOf(this.search.query.toLowerCase()) !== -1;
|
||||
});
|
||||
|
||||
this.updateUIBoundOptions(this.$scope, options);
|
||||
}
|
||||
|
||||
init() {
|
||||
this.selectedTags = this.variable.current.tags || [];
|
||||
this.updateLinkText();
|
||||
}
|
||||
|
||||
async updateLazyLoadedOptions() {
|
||||
this.options = await this.lazyLoadOptions(this.search.query);
|
||||
this.updateUIBoundOptions(this.$scope, this.options);
|
||||
}
|
||||
|
||||
async lazyLoadOptions(query: string): Promise<any[]> {
|
||||
await this.variable.updateOptions(query);
|
||||
return this.variable.options;
|
||||
}
|
||||
|
||||
updateUIBoundOptions($scope: IScope, options: any[]) {
|
||||
this.highlightIndex = 0;
|
||||
this.search.options = options.slice(0, Math.min(options.length, 1000));
|
||||
$scope.$apply();
|
||||
}
|
||||
}
|
||||
|
||||
/** @ngInject */
|
||||
export function valueSelectDropdown($compile: any, $window: any, $timeout: any, $rootScope: GrafanaRootScope) {
|
||||
return {
|
||||
scope: { dashboard: '=', variable: '=', onUpdated: '&' },
|
||||
templateUrl: 'public/app/partials/valueSelectDropdown.html',
|
||||
controller: 'ValueSelectDropdownCtrl',
|
||||
controllerAs: 'vm',
|
||||
bindToController: true,
|
||||
link: (scope: any, elem: any) => {
|
||||
const bodyEl = angular.element($window.document.body);
|
||||
const linkEl = elem.find('.variable-value-link');
|
||||
const inputEl = elem.find('input');
|
||||
|
||||
function openDropdown() {
|
||||
inputEl.css('width', Math.max(linkEl.width(), 80) + 'px');
|
||||
|
||||
inputEl.show();
|
||||
linkEl.hide();
|
||||
|
||||
inputEl.focus();
|
||||
$timeout(
|
||||
() => {
|
||||
bodyEl.on('click', bodyOnClick);
|
||||
},
|
||||
0,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
function switchToLink() {
|
||||
inputEl.hide();
|
||||
linkEl.show();
|
||||
bodyEl.off('click', bodyOnClick);
|
||||
}
|
||||
|
||||
function bodyOnClick(e: any) {
|
||||
if (elem.has(e.target).length === 0) {
|
||||
scope.$apply(() => {
|
||||
scope.vm.commitChanges();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
scope.$watch('vm.dropdownVisible', (newValue: any) => {
|
||||
if (newValue) {
|
||||
openDropdown();
|
||||
} else {
|
||||
switchToLink();
|
||||
}
|
||||
});
|
||||
|
||||
scope.vm.dashboard.on(
|
||||
'template-variable-value-updated',
|
||||
() => {
|
||||
scope.vm.updateLinkText();
|
||||
},
|
||||
scope
|
||||
);
|
||||
|
||||
scope.vm.init();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
coreModule.controller('ValueSelectDropdownCtrl', ValueSelectDropdownCtrl);
|
||||
coreModule.directive('valueSelectDropdown', valueSelectDropdown);
|
||||
@@ -1,13 +1,13 @@
|
||||
import coreModule from 'app/core/core_module';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { store } from 'app/store/store';
|
||||
import { dispatch, store } from 'app/store/store';
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
import { ILocationService, ITimeoutService, IWindowService } from 'angular';
|
||||
import { CoreEvents } from 'app/types';
|
||||
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
|
||||
import { locationUtil, UrlQueryMap } from '@grafana/data';
|
||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
import { VariableSrv } from 'app/features/templating/all';
|
||||
import { templateVarsChangedInUrl } from 'app/features/variables/state/actions';
|
||||
|
||||
// Services that handles angular -> redux store sync & other react <-> angular sync
|
||||
export class BridgeSrv {
|
||||
@@ -22,8 +22,7 @@ export class BridgeSrv {
|
||||
private $timeout: ITimeoutService,
|
||||
private $window: IWindowService,
|
||||
private $rootScope: GrafanaRootScope,
|
||||
private $route: any,
|
||||
private variableSrv: VariableSrv
|
||||
private $route: any
|
||||
) {
|
||||
this.fullPageReloadRoutes = ['/logout'];
|
||||
this.angularUrl = $location.url();
|
||||
@@ -84,7 +83,7 @@ export class BridgeSrv {
|
||||
if (changes) {
|
||||
const dash = getDashboardSrv().getCurrent();
|
||||
if (dash) {
|
||||
this.variableSrv.templateVarsChangedInUrl(changes);
|
||||
dispatch(templateVarsChangedInUrl(changes));
|
||||
}
|
||||
}
|
||||
this.lastQuery = state.location.query;
|
||||
|
||||
@@ -1,270 +0,0 @@
|
||||
import 'app/core/directives/value_select_dropdown';
|
||||
import { ValueSelectDropdownCtrl } from '../directives/value_select_dropdown';
|
||||
import { IScope } from 'angular';
|
||||
|
||||
describe('SelectDropdownCtrl', () => {
|
||||
const tagValuesMap: any = {};
|
||||
const $scope: IScope = {} as IScope;
|
||||
|
||||
ValueSelectDropdownCtrl.prototype.onUpdated = jest.fn();
|
||||
let ctrl: ValueSelectDropdownCtrl;
|
||||
|
||||
describe('Given simple variable', () => {
|
||||
beforeEach(() => {
|
||||
ctrl = new ValueSelectDropdownCtrl($scope);
|
||||
ctrl.variable = {
|
||||
current: { text: 'hej', value: 'hej' },
|
||||
getValuesForTag: (key: string) => {
|
||||
return Promise.resolve(tagValuesMap[key]);
|
||||
},
|
||||
};
|
||||
ctrl.init();
|
||||
});
|
||||
|
||||
it('Should init labelText and linkText', () => {
|
||||
expect(ctrl.linkText).toBe('hej');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Given variable with tags and dropdown is opened', () => {
|
||||
beforeEach(() => {
|
||||
ctrl = new ValueSelectDropdownCtrl($scope);
|
||||
ctrl.variable = {
|
||||
current: { text: 'server-1', value: 'server-1' },
|
||||
options: [
|
||||
{ text: 'server-1', value: 'server-1', selected: true },
|
||||
{ text: 'server-2', value: 'server-2' },
|
||||
{ text: 'server-3', value: 'server-3' },
|
||||
],
|
||||
tags: ['key1', 'key2', 'key3'],
|
||||
getValuesForTag: (key: string) => {
|
||||
return Promise.resolve(tagValuesMap[key]);
|
||||
},
|
||||
multi: true,
|
||||
};
|
||||
tagValuesMap.key1 = ['server-1', 'server-3'];
|
||||
tagValuesMap.key2 = ['server-2', 'server-3'];
|
||||
tagValuesMap.key3 = ['server-1', 'server-2', 'server-3'];
|
||||
ctrl.init();
|
||||
ctrl.show();
|
||||
});
|
||||
|
||||
it('should init tags model', () => {
|
||||
expect(ctrl.tags.length).toBe(3);
|
||||
expect(ctrl.tags[0].text).toBe('key1');
|
||||
});
|
||||
|
||||
it('should init options model', () => {
|
||||
expect(ctrl.options.length).toBe(3);
|
||||
});
|
||||
|
||||
it('should init selected values array', () => {
|
||||
expect(ctrl.selectedValues.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should set linkText', () => {
|
||||
expect(ctrl.linkText).toBe('server-1');
|
||||
});
|
||||
|
||||
describe('after adititional value is selected', () => {
|
||||
beforeEach(() => {
|
||||
ctrl.selectValue(ctrl.options[2], {});
|
||||
ctrl.commitChanges();
|
||||
});
|
||||
|
||||
it('should update link text', () => {
|
||||
expect(ctrl.linkText).toBe('server-1 + server-3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('When tag is selected', () => {
|
||||
beforeEach(async () => {
|
||||
await ctrl.selectTag(ctrl.tags[0]);
|
||||
ctrl.commitChanges();
|
||||
});
|
||||
|
||||
it('should select tag', () => {
|
||||
expect(ctrl.selectedTags.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should select values', () => {
|
||||
expect(ctrl.options[0].selected).toBe(true);
|
||||
expect(ctrl.options[2].selected).toBe(true);
|
||||
});
|
||||
|
||||
it('link text should not include tag values', () => {
|
||||
expect(ctrl.linkText).toBe('');
|
||||
});
|
||||
|
||||
describe('and then dropdown is opened and closed without changes', () => {
|
||||
beforeEach(() => {
|
||||
ctrl.show();
|
||||
ctrl.commitChanges();
|
||||
});
|
||||
|
||||
it('should still have selected tag', () => {
|
||||
expect(ctrl.selectedTags.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and then unselected', () => {
|
||||
beforeEach(async () => {
|
||||
await ctrl.selectTag(ctrl.tags[0]);
|
||||
});
|
||||
|
||||
it('should deselect tag', () => {
|
||||
expect(ctrl.selectedTags.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and then value is unselected', () => {
|
||||
beforeEach(() => {
|
||||
ctrl.selectValue(ctrl.options[0], {});
|
||||
});
|
||||
|
||||
it('should deselect tag', () => {
|
||||
expect(ctrl.selectedTags.length).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Given variable with selected tags', () => {
|
||||
beforeEach(() => {
|
||||
ctrl = new ValueSelectDropdownCtrl($scope);
|
||||
ctrl.variable = {
|
||||
current: {
|
||||
text: 'server-1',
|
||||
value: 'server-1',
|
||||
tags: [{ text: 'key1', selected: true }],
|
||||
},
|
||||
options: [
|
||||
{ text: 'server-1', value: 'server-1' },
|
||||
{ text: 'server-2', value: 'server-2' },
|
||||
{ text: 'server-3', value: 'server-3' },
|
||||
],
|
||||
tags: ['key1', 'key2', 'key3'],
|
||||
getValuesForTag: (key: any) => {
|
||||
return Promise.resolve(tagValuesMap[key]);
|
||||
},
|
||||
multi: true,
|
||||
};
|
||||
ctrl.init();
|
||||
ctrl.show();
|
||||
});
|
||||
|
||||
it('should set tag as selected', () => {
|
||||
expect(ctrl.tags[0].selected).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('queryChanged', () => {
|
||||
describe('when called and variable query contains search filter', () => {
|
||||
it('then it should use lazy loading', async () => {
|
||||
const $scope = {} as IScope;
|
||||
const ctrl = new ValueSelectDropdownCtrl($scope);
|
||||
const options = [
|
||||
{ text: 'server-1', value: 'server-1' },
|
||||
{ text: 'server-2', value: 'server-2' },
|
||||
{ text: 'server-3', value: 'server-3' },
|
||||
];
|
||||
ctrl.lazyLoadOptions = jest.fn().mockResolvedValue(options);
|
||||
ctrl.updateUIBoundOptions = jest.fn();
|
||||
ctrl.search = {
|
||||
query: 'alpha',
|
||||
};
|
||||
ctrl.queryHasSearchFilter = true;
|
||||
|
||||
await ctrl.queryChanged();
|
||||
|
||||
expect(ctrl.lazyLoadOptions).toBeCalledTimes(1);
|
||||
expect(ctrl.lazyLoadOptions).toBeCalledWith('alpha');
|
||||
expect(ctrl.updateUIBoundOptions).toBeCalledTimes(1);
|
||||
expect(ctrl.updateUIBoundOptions).toBeCalledWith($scope, options);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when called and variable query does not contain search filter', () => {
|
||||
it('then it should not use lazy loading', async () => {
|
||||
const $scope = {} as IScope;
|
||||
const ctrl = new ValueSelectDropdownCtrl($scope);
|
||||
ctrl.lazyLoadOptions = jest.fn().mockResolvedValue([]);
|
||||
ctrl.updateUIBoundOptions = jest.fn();
|
||||
ctrl.search = {
|
||||
query: 'alpha',
|
||||
};
|
||||
ctrl.queryHasSearchFilter = false;
|
||||
|
||||
await ctrl.queryChanged();
|
||||
|
||||
expect(ctrl.lazyLoadOptions).toBeCalledTimes(0);
|
||||
expect(ctrl.updateUIBoundOptions).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('lazyLoadOptions', () => {
|
||||
describe('when called with a query', () => {
|
||||
it('then the variables updateOptions should be called with the query', async () => {
|
||||
const $scope = {} as IScope;
|
||||
const ctrl = new ValueSelectDropdownCtrl($scope);
|
||||
ctrl.variable = {
|
||||
updateOptions: jest.fn(),
|
||||
options: [
|
||||
{ text: 'server-1', value: 'server-1' },
|
||||
{ text: 'server-2', value: 'server-2' },
|
||||
{ text: 'server-3', value: 'server-3' },
|
||||
],
|
||||
};
|
||||
const query = 'server-1';
|
||||
|
||||
const result = await ctrl.lazyLoadOptions(query);
|
||||
|
||||
expect(ctrl.variable.updateOptions).toBeCalledTimes(1);
|
||||
expect(ctrl.variable.updateOptions).toBeCalledWith(query);
|
||||
expect(result).toEqual(ctrl.variable.options);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateUIBoundOptions', () => {
|
||||
describe('when called with options', () => {
|
||||
let options: any[];
|
||||
let ctrl: ValueSelectDropdownCtrl;
|
||||
let $scope: IScope;
|
||||
|
||||
beforeEach(() => {
|
||||
$scope = ({
|
||||
$apply: jest.fn(),
|
||||
} as any) as IScope;
|
||||
options = [];
|
||||
for (let index = 0; index < 1001; index++) {
|
||||
options.push({ text: `server-${index}`, value: `server-${index}` });
|
||||
}
|
||||
ctrl = new ValueSelectDropdownCtrl($scope);
|
||||
ctrl.highlightIndex = 0;
|
||||
ctrl.options = [];
|
||||
ctrl.search = {
|
||||
options: [],
|
||||
};
|
||||
ctrl.updateUIBoundOptions($scope, options);
|
||||
});
|
||||
|
||||
it('then highlightIndex should be reset to first item', () => {
|
||||
expect(ctrl.highlightIndex).toEqual(0);
|
||||
});
|
||||
|
||||
it('then search.options should be same as options but capped to 1000', () => {
|
||||
expect(ctrl.search.options.length).toEqual(1000);
|
||||
|
||||
for (let index = 0; index < 1000; index++) {
|
||||
expect(ctrl.search.options[index]).toEqual(options[index]);
|
||||
}
|
||||
});
|
||||
|
||||
it('then scope apply should be called', () => {
|
||||
expect($scope.$apply).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,182 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import angular from 'angular';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { DashboardModel } from 'app/features/dashboard/state';
|
||||
import DatasourceSrv from 'app/features/plugins/datasource_srv';
|
||||
import { VariableSrv } from 'app/features/templating/all';
|
||||
import { CoreEvents } from 'app/types';
|
||||
|
||||
export class AdHocFiltersCtrl {
|
||||
segments: any;
|
||||
variable: any;
|
||||
dashboard: DashboardModel;
|
||||
removeTagFilterSegment: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(
|
||||
private uiSegmentSrv: any,
|
||||
private datasourceSrv: DatasourceSrv,
|
||||
private variableSrv: VariableSrv,
|
||||
$scope: any
|
||||
) {
|
||||
this.removeTagFilterSegment = uiSegmentSrv.newSegment({
|
||||
fake: true,
|
||||
value: '-- remove filter --',
|
||||
});
|
||||
this.buildSegmentModel();
|
||||
this.dashboard.events.on(CoreEvents.templateVariableValueUpdated, this.buildSegmentModel.bind(this), $scope);
|
||||
}
|
||||
|
||||
buildSegmentModel() {
|
||||
this.segments = [];
|
||||
|
||||
if (this.variable.value && !_.isArray(this.variable.value)) {
|
||||
}
|
||||
|
||||
for (const tag of this.variable.filters) {
|
||||
if (this.segments.length > 0) {
|
||||
this.segments.push(this.uiSegmentSrv.newCondition('AND'));
|
||||
}
|
||||
|
||||
if (tag.key !== undefined && tag.value !== undefined) {
|
||||
this.segments.push(this.uiSegmentSrv.newKey(tag.key));
|
||||
this.segments.push(this.uiSegmentSrv.newOperator(tag.operator));
|
||||
this.segments.push(this.uiSegmentSrv.newKeyValue(tag.value));
|
||||
}
|
||||
}
|
||||
|
||||
this.segments.push(this.uiSegmentSrv.newPlusButton());
|
||||
}
|
||||
|
||||
getOptions(segment: { type: string }, index: number) {
|
||||
if (segment.type === 'operator') {
|
||||
return Promise.resolve(this.uiSegmentSrv.newOperators(['=', '!=', '<', '>', '=~', '!~']));
|
||||
}
|
||||
|
||||
if (segment.type === 'condition') {
|
||||
return Promise.resolve([this.uiSegmentSrv.newSegment('AND')]);
|
||||
}
|
||||
|
||||
return this.datasourceSrv.get(this.variable.datasource).then(ds => {
|
||||
const options: any = {};
|
||||
let promise = null;
|
||||
|
||||
if (segment.type !== 'value') {
|
||||
promise = ds.getTagKeys ? ds.getTagKeys() : Promise.resolve([]);
|
||||
} else {
|
||||
options.key = this.segments[index - 2].value;
|
||||
promise = ds.getTagValues ? ds.getTagValues(options) : Promise.resolve([]);
|
||||
}
|
||||
|
||||
return promise.then((results: any) => {
|
||||
results = _.map(results, segment => {
|
||||
return this.uiSegmentSrv.newSegment({ value: segment.text });
|
||||
});
|
||||
|
||||
// add remove option for keys
|
||||
if (segment.type === 'key') {
|
||||
results.splice(0, 0, angular.copy(this.removeTagFilterSegment));
|
||||
}
|
||||
return results;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
segmentChanged(segment: { value: any; type: string; cssClass: string }, index: number) {
|
||||
this.segments[index] = segment;
|
||||
|
||||
// handle remove tag condition
|
||||
if (segment.value === this.removeTagFilterSegment.value) {
|
||||
this.segments.splice(index, 3);
|
||||
if (this.segments.length === 0) {
|
||||
this.segments.push(this.uiSegmentSrv.newPlusButton());
|
||||
} else if (this.segments.length > 2) {
|
||||
this.segments.splice(Math.max(index - 1, 0), 1);
|
||||
if (this.segments[this.segments.length - 1].type !== 'plus-button') {
|
||||
this.segments.push(this.uiSegmentSrv.newPlusButton());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (segment.type === 'plus-button') {
|
||||
if (index > 2) {
|
||||
this.segments.splice(index, 0, this.uiSegmentSrv.newCondition('AND'));
|
||||
}
|
||||
this.segments.push(this.uiSegmentSrv.newOperator('='));
|
||||
this.segments.push(this.uiSegmentSrv.newFake('select value', 'value', 'query-segment-value'));
|
||||
segment.type = 'key';
|
||||
segment.cssClass = 'query-segment-key';
|
||||
}
|
||||
|
||||
if (index + 1 === this.segments.length) {
|
||||
this.segments.push(this.uiSegmentSrv.newPlusButton());
|
||||
}
|
||||
}
|
||||
|
||||
this.updateVariableModel();
|
||||
}
|
||||
|
||||
updateVariableModel() {
|
||||
const filters: any[] = [];
|
||||
let filterIndex = -1;
|
||||
let hasFakes = false;
|
||||
|
||||
this.segments.forEach((segment: any) => {
|
||||
if (segment.type === 'value' && segment.fake) {
|
||||
hasFakes = true;
|
||||
return;
|
||||
}
|
||||
|
||||
switch (segment.type) {
|
||||
case 'key': {
|
||||
filters.push({ key: segment.value });
|
||||
filterIndex += 1;
|
||||
break;
|
||||
}
|
||||
case 'value': {
|
||||
filters[filterIndex].value = segment.value;
|
||||
break;
|
||||
}
|
||||
case 'operator': {
|
||||
filters[filterIndex].operator = segment.value;
|
||||
break;
|
||||
}
|
||||
case 'condition': {
|
||||
filters[filterIndex].condition = segment.value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (hasFakes) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.variable.setFilters(filters);
|
||||
this.variableSrv.variableUpdated(this.variable, true);
|
||||
}
|
||||
}
|
||||
|
||||
const template = `
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form" ng-repeat="segment in ctrl.segments">
|
||||
<metric-segment segment="segment" get-options="ctrl.getOptions(segment, $index)"
|
||||
on-change="ctrl.segmentChanged(segment, $index)"></metric-segment>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export function adHocFiltersComponent() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: template,
|
||||
controller: AdHocFiltersCtrl,
|
||||
bindToController: true,
|
||||
controllerAs: 'ctrl',
|
||||
scope: {
|
||||
variable: '=',
|
||||
dashboard: '=',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
coreModule.directive('adHocFilters', adHocFiltersComponent);
|
||||
@@ -1 +0,0 @@
|
||||
export { AdHocFiltersCtrl } from './AdHocFiltersCtrl';
|
||||
@@ -3,6 +3,10 @@ import config from 'app/core/config';
|
||||
import { DashboardExporter } from './DashboardExporter';
|
||||
import { DashboardModel } from '../../state/DashboardModel';
|
||||
import { PanelPluginMeta } from '@grafana/data';
|
||||
import { variableAdapters } from '../../../variables/adapters';
|
||||
import { createConstantVariableAdapter } from '../../../variables/constant/adapter';
|
||||
import { createQueryVariableAdapter } from '../../../variables/query/adapter';
|
||||
import { createDataSourceVariableAdapter } from '../../../variables/datasource/adapter';
|
||||
|
||||
jest.mock('app/core/store', () => {
|
||||
return {
|
||||
@@ -25,6 +29,10 @@ jest.mock('@grafana/runtime', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
variableAdapters.register(createQueryVariableAdapter());
|
||||
variableAdapters.register(createConstantVariableAdapter());
|
||||
variableAdapters.register(createDataSourceVariableAdapter());
|
||||
|
||||
describe('given dashboard with repeated panels', () => {
|
||||
let dash: any, exported: any;
|
||||
|
||||
@@ -122,7 +130,7 @@ describe('given dashboard with repeated panels', () => {
|
||||
info: { version: '1.1.2' },
|
||||
} as PanelPluginMeta;
|
||||
|
||||
dash = new DashboardModel(dash, {});
|
||||
dash = new DashboardModel(dash, {}, () => dash.templating.list);
|
||||
const exporter = new DashboardExporter();
|
||||
exporter.makeExportable(dash).then(clean => {
|
||||
exported = clean;
|
||||
|
||||
@@ -5,6 +5,8 @@ import { DashboardModel } from '../../state/DashboardModel';
|
||||
import { PanelModel } from 'app/features/dashboard/state';
|
||||
import { PanelPluginMeta } from '@grafana/data';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import { VariableOption, VariableRefresh } from '../../../variables/types';
|
||||
import { isConstant, isQuery } from '../../../variables/guard';
|
||||
|
||||
interface Input {
|
||||
name: string;
|
||||
@@ -144,11 +146,12 @@ export class DashboardExporter {
|
||||
|
||||
// templatize template vars
|
||||
for (const variable of saveModel.getVariables()) {
|
||||
if (variable.type === 'query') {
|
||||
if (isQuery(variable)) {
|
||||
templateizeDatasourceUsage(variable);
|
||||
variable.options = [];
|
||||
variable.current = {};
|
||||
variable.refresh = variable.refresh > 0 ? variable.refresh : 1;
|
||||
variable.current = ({} as unknown) as VariableOption;
|
||||
variable.refresh =
|
||||
variable.refresh !== VariableRefresh.never ? variable.refresh : VariableRefresh.onDashboardLoad;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,7 +176,7 @@ export class DashboardExporter {
|
||||
|
||||
// templatize constants
|
||||
for (const variable of saveModel.getVariables()) {
|
||||
if (variable.type === 'constant') {
|
||||
if (isConstant(variable)) {
|
||||
const refName = 'VAR_' + variable.name.replace(' ', '_').toUpperCase();
|
||||
inputs.push({
|
||||
name: refName,
|
||||
@@ -187,6 +190,7 @@ export class DashboardExporter {
|
||||
variable.options[0] = variable.current = {
|
||||
value: variable.query,
|
||||
text: variable.query,
|
||||
selected: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { mount } from 'enzyme';
|
||||
import { DashboardRow } from './DashboardRow';
|
||||
import { PanelModel } from '../../state/PanelModel';
|
||||
|
||||
@@ -16,7 +16,7 @@ describe('DashboardRow', () => {
|
||||
};
|
||||
|
||||
panel = new PanelModel({ collapsed: false });
|
||||
wrapper = shallow(<DashboardRow panel={panel} dashboard={dashboardMock} />);
|
||||
wrapper = mount(<DashboardRow panel={panel} dashboard={dashboardMock} />);
|
||||
});
|
||||
|
||||
it('Should not have collapsed class when collaped is false', () => {
|
||||
@@ -37,14 +37,14 @@ describe('DashboardRow', () => {
|
||||
|
||||
it('should not show row drag handle when cannot edit', () => {
|
||||
dashboardMock.meta.canEdit = false;
|
||||
wrapper = shallow(<DashboardRow panel={panel} dashboard={dashboardMock} />);
|
||||
wrapper = mount(<DashboardRow panel={panel} dashboard={dashboardMock} />);
|
||||
expect(wrapper.find('.dashboard-row__drag')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should have zero actions when cannot edit', () => {
|
||||
dashboardMock.meta.canEdit = false;
|
||||
panel = new PanelModel({ collapsed: false });
|
||||
wrapper = shallow(<DashboardRow panel={panel} dashboard={dashboardMock} />);
|
||||
wrapper = mount(<DashboardRow panel={panel} dashboard={dashboardMock} />);
|
||||
expect(wrapper.find('.dashboard-row__actions .pointer')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import { DashboardModel } from '../../state/DashboardModel';
|
||||
import templateSrv from 'app/features/templating/template_srv';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { CoreEvents } from 'app/types';
|
||||
import { RowOptionsButton } from '../RowOptions/RowOptionsButton';
|
||||
|
||||
export interface DashboardRowProps {
|
||||
panel: PanelModel;
|
||||
@@ -39,22 +40,14 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
|
||||
});
|
||||
};
|
||||
|
||||
onUpdate = () => {
|
||||
onUpdate = (title: string | null, repeat: string | null) => {
|
||||
this.props.panel['title'] = title;
|
||||
this.props.panel['repeat'] = repeat;
|
||||
this.props.panel.render();
|
||||
this.props.dashboard.processRepeats();
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
onOpenSettings = () => {
|
||||
appEvents.emit(CoreEvents.showModal, {
|
||||
templateHtml: `<row-options row="model.row" on-updated="model.onUpdated()" dismiss="dismiss()"></row-options>`,
|
||||
modalClass: 'modal--narrow',
|
||||
model: {
|
||||
row: this.props.panel,
|
||||
onUpdated: this.onUpdate,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
onDelete = () => {
|
||||
appEvents.emit(CoreEvents.showConfirmModal, {
|
||||
title: 'Delete Row',
|
||||
@@ -92,9 +85,11 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
|
||||
</a>
|
||||
{canEdit && (
|
||||
<div className="dashboard-row__actions">
|
||||
<a className="pointer" onClick={this.onOpenSettings}>
|
||||
<Icon name="cog" />
|
||||
</a>
|
||||
<RowOptionsButton
|
||||
title={this.props.panel.title}
|
||||
repeat={this.props.panel.repeat}
|
||||
onUpdate={this.onUpdate}
|
||||
/>
|
||||
<a className="pointer" onClick={this.onDelete}>
|
||||
<Icon name="trash-alt" />
|
||||
</a>
|
||||
|
||||
@@ -25,7 +25,6 @@ export class SettingsCtrl {
|
||||
sections: any[];
|
||||
hasUnsavedFolderChange: boolean;
|
||||
selectors: typeof selectors.pages.Dashboard.Settings.General;
|
||||
useAngularTemplating: boolean;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(
|
||||
@@ -60,7 +59,6 @@ export class SettingsCtrl {
|
||||
appEvents.on(CoreEvents.dashboardSaved, this.onPostSave.bind(this), $scope);
|
||||
|
||||
this.selectors = selectors.pages.Dashboard.Settings.General;
|
||||
this.useAngularTemplating = !getConfig().featureToggles.newVariables;
|
||||
}
|
||||
|
||||
buildSectionList() {
|
||||
|
||||
@@ -77,11 +77,7 @@
|
||||
ng-include="'public/app/features/annotations/partials/editor.html'">
|
||||
</div>
|
||||
|
||||
<div class="dashboard-settings__content" ng-if="ctrl.viewId === 'templating' && ctrl.useAngularTemplating"
|
||||
ng-include="'public/app/features/templating/partials/editor.html'">
|
||||
</div>
|
||||
|
||||
<div class="dashboard-settings__content" ng-if="ctrl.viewId === 'templating' && !ctrl.useAngularTemplating">
|
||||
<div class="dashboard-settings__content" ng-if="ctrl.viewId === 'templating'">
|
||||
<variable-editor-container />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ import { getPanelEditorTabs } from './state/selectors';
|
||||
import { getPanelStateById } from '../../state/selectors';
|
||||
import { OptionsPaneContent } from './OptionsPaneContent';
|
||||
import { DashNavButton } from 'app/features/dashboard/components/DashNav/DashNavButton';
|
||||
import { VariableModel } from 'app/features/templating/types';
|
||||
import { VariableModel } from 'app/features/variables/types';
|
||||
import { getVariables } from 'app/features/variables/state/selectors';
|
||||
import { SubMenuItems } from 'app/features/dashboard/components/SubMenu/SubMenuItems';
|
||||
import { BackButton } from 'app/core/components/BackButton/BackButton';
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React, { FC, useMemo, useRef } from 'react';
|
||||
import React, { FC, useCallback, useMemo, useRef } from 'react';
|
||||
import { DashboardModel, PanelModel } from '../../state';
|
||||
import { PanelData, PanelPlugin, SelectableValue } from '@grafana/data';
|
||||
import { PanelData, PanelPlugin } from '@grafana/data';
|
||||
import { Counter, DataLinksInlineEditor, Field, Input, RadioButtonGroup, Select, Switch, TextArea } from '@grafana/ui';
|
||||
import { getPanelLinksVariableSuggestions } from '../../../panel/panellinks/link_srv';
|
||||
import { getVariables } from '../../../variables/state/selectors';
|
||||
import { PanelOptionsEditor } from './PanelOptionsEditor';
|
||||
import { AngularPanelOptions } from './AngularPanelOptions';
|
||||
import { VisualizationTab } from './VisualizationTab';
|
||||
import { OptionsGroup } from './OptionsGroup';
|
||||
import { RepeatRowSelect } from '../RepeatRowSelect/RepeatRowSelect';
|
||||
|
||||
interface Props {
|
||||
panel: PanelModel;
|
||||
@@ -28,10 +28,12 @@ export const PanelOptionsTab: FC<Props> = ({
|
||||
}) => {
|
||||
const visTabInputRef = useRef<HTMLInputElement>();
|
||||
const linkVariablesSuggestions = useMemo(() => getPanelLinksVariableSuggestions(), []);
|
||||
const onRepeatRowSelectChange = useCallback((value: string | null) => onPanelConfigChange('repeat', value), [
|
||||
onPanelConfigChange,
|
||||
]);
|
||||
const elements: JSX.Element[] = [];
|
||||
const panelLinksCount = panel && panel.links ? panel.links.length : 0;
|
||||
|
||||
const variableOptions = getVariableOptions();
|
||||
const directionOptions = [
|
||||
{ label: 'Horizontal', value: 'h' },
|
||||
{ label: 'Vertical', value: 'v' },
|
||||
@@ -120,11 +122,7 @@ export const PanelOptionsTab: FC<Props> = ({
|
||||
This is not visible while in edit mode. You need to go back to dashboard and then update the variable or
|
||||
reload the dashboard."
|
||||
>
|
||||
<Select
|
||||
value={panel.repeat}
|
||||
onChange={value => onPanelConfigChange('repeat', value.value)}
|
||||
options={variableOptions}
|
||||
/>
|
||||
<RepeatRowSelect repeat={panel.repeat} onChange={onRepeatRowSelectChange} />
|
||||
</Field>
|
||||
{panel.repeat && (
|
||||
<Field label="Repeat direction">
|
||||
@@ -150,23 +148,3 @@ export const PanelOptionsTab: FC<Props> = ({
|
||||
|
||||
return <>{elements}</>;
|
||||
};
|
||||
|
||||
function getVariableOptions(): Array<SelectableValue<string>> {
|
||||
const options = getVariables().map((item: any) => {
|
||||
return { label: item.name, value: item.name };
|
||||
});
|
||||
|
||||
if (options.length === 0) {
|
||||
options.unshift({
|
||||
label: 'No template variables found',
|
||||
value: null,
|
||||
});
|
||||
}
|
||||
|
||||
options.unshift({
|
||||
label: 'Disable repeating',
|
||||
value: null,
|
||||
});
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import React, { FC, useCallback, useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Select } from '@grafana/ui';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
|
||||
import { getVariables } from '../../../variables/state/selectors';
|
||||
import { StoreState } from '../../../../types';
|
||||
|
||||
export interface Props {
|
||||
repeat: string | undefined;
|
||||
onChange: (name: string) => void;
|
||||
}
|
||||
|
||||
export const RepeatRowSelect: FC<Props> = ({ repeat, onChange }) => {
|
||||
const variables = useSelector((state: StoreState) => getVariables(state));
|
||||
|
||||
const variableOptions = useMemo(() => {
|
||||
const options = variables.map((item: any) => {
|
||||
return { label: item.name, value: item.name };
|
||||
});
|
||||
|
||||
if (options.length === 0) {
|
||||
options.unshift({
|
||||
label: 'No template variables found',
|
||||
value: null,
|
||||
});
|
||||
}
|
||||
|
||||
options.unshift({
|
||||
label: 'Disable repeating',
|
||||
value: null,
|
||||
});
|
||||
|
||||
return options;
|
||||
}, variables);
|
||||
|
||||
const onSelectChange = useCallback((option: SelectableValue<string | null>) => onChange(option.value), [onChange]);
|
||||
|
||||
return <Select value={repeat} onChange={onSelectChange} options={variableOptions} />;
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import React, { FC } from 'react';
|
||||
import { Icon, ModalsController } from '@grafana/ui';
|
||||
|
||||
import { RowOptionsModal } from './RowOptionsModal';
|
||||
import { OnRowOptionsUpdate } from './RowOptionsForm';
|
||||
|
||||
export interface RowOptionsButtonProps {
|
||||
title: string | null;
|
||||
repeat: string | null;
|
||||
onUpdate: OnRowOptionsUpdate;
|
||||
}
|
||||
|
||||
export const RowOptionsButton: FC<RowOptionsButtonProps> = ({ repeat, title, onUpdate }) => {
|
||||
const onUpdateChange = (hideModal: () => void) => (title: string | null, repeat: string | null) => {
|
||||
onUpdate(title, repeat);
|
||||
hideModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalsController>
|
||||
{({ showModal, hideModal }) => {
|
||||
return (
|
||||
<a
|
||||
className="pointer"
|
||||
onClick={() => {
|
||||
showModal(RowOptionsModal, { title, repeat, onDismiss: hideModal, onUpdate: onUpdateChange(hideModal) });
|
||||
}}
|
||||
>
|
||||
<Icon name="cog" />
|
||||
</a>
|
||||
);
|
||||
}}
|
||||
</ModalsController>
|
||||
);
|
||||
};
|
||||
|
||||
RowOptionsButton.displayName = 'RowOptionsButton';
|
||||
@@ -1,39 +0,0 @@
|
||||
import { coreModule } from 'app/core/core';
|
||||
|
||||
export class RowOptionsCtrl {
|
||||
row: any;
|
||||
source: any;
|
||||
dismiss: any;
|
||||
onUpdated: any;
|
||||
showDelete: boolean;
|
||||
|
||||
/** @ngInject */
|
||||
constructor() {
|
||||
this.source = this.row;
|
||||
this.row = this.row.getSaveModel();
|
||||
}
|
||||
|
||||
update() {
|
||||
this.source.title = this.row.title;
|
||||
this.source.repeat = this.row.repeat;
|
||||
this.onUpdated();
|
||||
this.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
export function rowOptionsDirective() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: 'public/app/features/dashboard/components/RowOptions/template.html',
|
||||
controller: RowOptionsCtrl,
|
||||
bindToController: true,
|
||||
controllerAs: 'ctrl',
|
||||
scope: {
|
||||
row: '=',
|
||||
dismiss: '&',
|
||||
onUpdated: '&',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
coreModule.directive('rowOptions', rowOptionsDirective);
|
||||
@@ -0,0 +1,46 @@
|
||||
import React, { FC, useCallback, useState } from 'react';
|
||||
import { Button, Field, Form, HorizontalGroup, Input } from '@grafana/ui';
|
||||
|
||||
import { RepeatRowSelect } from '../RepeatRowSelect/RepeatRowSelect';
|
||||
|
||||
export type OnRowOptionsUpdate = (title: string | null, repeat: string | null) => void;
|
||||
|
||||
export interface Props {
|
||||
title: string | null;
|
||||
repeat: string | null;
|
||||
onUpdate: OnRowOptionsUpdate;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export const RowOptionsForm: FC<Props> = ({ repeat, title, onUpdate, onCancel }) => {
|
||||
const [newRepeat, setNewRepeat] = useState<string | null>(repeat);
|
||||
const onChangeRepeat = useCallback((name: string) => setNewRepeat(name), [setNewRepeat]);
|
||||
|
||||
return (
|
||||
<Form
|
||||
defaultValues={{ title }}
|
||||
onSubmit={(formData: { title: string | null }) => {
|
||||
onUpdate(formData.title, newRepeat);
|
||||
}}
|
||||
>
|
||||
{({ register }) => (
|
||||
<>
|
||||
<Field label="Title">
|
||||
<Input name="title" ref={register} type="text" />
|
||||
</Field>
|
||||
|
||||
<Field label="Repeat for">
|
||||
<RepeatRowSelect repeat={newRepeat} onChange={onChangeRepeat} />
|
||||
</Field>
|
||||
|
||||
<HorizontalGroup>
|
||||
<Button type="submit">Update</Button>
|
||||
<Button variant="secondary" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import React, { FC } from 'react';
|
||||
import { Modal, stylesFactory } from '@grafana/ui';
|
||||
import { css } from 'emotion';
|
||||
|
||||
import { OnRowOptionsUpdate, RowOptionsForm } from './RowOptionsForm';
|
||||
|
||||
export interface RowOptionsModalProps {
|
||||
title: string | null;
|
||||
repeat: string | null;
|
||||
onDismiss: () => void;
|
||||
onUpdate: OnRowOptionsUpdate;
|
||||
}
|
||||
|
||||
export const RowOptionsModal: FC<RowOptionsModalProps> = ({ repeat, title, onDismiss, onUpdate }) => {
|
||||
const styles = getStyles();
|
||||
return (
|
||||
<Modal isOpen={true} title="Row Options" icon="copy" onDismiss={onDismiss} className={styles.modal}>
|
||||
<RowOptionsForm repeat={repeat} title={title} onCancel={onDismiss} onUpdate={onUpdate} />
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = stylesFactory(() => {
|
||||
return {
|
||||
modal: css`
|
||||
label: RowOptionsModal;
|
||||
width: 500px;
|
||||
`,
|
||||
};
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
export { RowOptionsCtrl } from './RowOptionsCtrl';
|
||||
@@ -1,30 +0,0 @@
|
||||
<div class="modal-body">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-header-title">
|
||||
<icon name="'copy'" size="'lg'"></icon>
|
||||
<span class="p-l-1">Row Options</span>
|
||||
</h2>
|
||||
|
||||
<a class="modal-header-close" ng-click="ctrl.dismiss();">
|
||||
<icon name="'times'"></icon>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<form name="ctrl.saveForm" ng-submit="ctrl.save()" class="modal-content text-center" novalidate>
|
||||
<div class="section">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-7">Title</span>
|
||||
<input type="text" class="gf-form-input max-width-13" ng-model='ctrl.row.title'></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-7">Repeat for</span>
|
||||
<dash-repeat-option panel="ctrl.row"></dash-repeat-option>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row">
|
||||
<button type="submit" class="btn btn-primary" ng-click="ctrl.update()">Update</button>
|
||||
<button type="button" class="btn btn-inverse" ng-click="ctrl.dismiss()">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -1,11 +1,13 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { Button, ClipboardButton, LinkButton, LegacyForms, Icon } from '@grafana/ui';
|
||||
const { Select, Input } = LegacyForms;
|
||||
import { Button, ClipboardButton, Icon, LegacyForms, LinkButton } from '@grafana/ui';
|
||||
import { AppEvents, SelectableValue } from '@grafana/data';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
||||
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
import { appEvents } from 'app/core/core';
|
||||
import { VariableRefresh } from '../../../variables/types';
|
||||
|
||||
const { Select, Input } = LegacyForms;
|
||||
|
||||
const snapshotApiUrl = '/api/snapshots';
|
||||
|
||||
@@ -140,10 +142,10 @@ export class ShareSnapshot extends PureComponent<Props, State> {
|
||||
});
|
||||
|
||||
// remove template queries
|
||||
dash.getVariables().forEach(variable => {
|
||||
dash.getVariables().forEach((variable: any) => {
|
||||
variable.query = '';
|
||||
variable.options = variable.current;
|
||||
variable.refresh = false;
|
||||
variable.options = variable.current ? [variable.current] : [];
|
||||
variable.refresh = VariableRefresh.never;
|
||||
});
|
||||
|
||||
// snapshot single panel
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { PureComponent } from 'react';
|
||||
import { connect, MapStateToProps } from 'react-redux';
|
||||
import { StoreState } from '../../../../types';
|
||||
import { getVariables } from '../../../variables/state/selectors';
|
||||
import { VariableHide, VariableModel } from '../../../templating/types';
|
||||
import { VariableHide, VariableModel } from '../../../variables/types';
|
||||
import { DashboardModel } from '../../state';
|
||||
import { DashboardLinks } from './DashboardLinks';
|
||||
import { Annotations } from './Annotations';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { FunctionComponent, useEffect, useState } from 'react';
|
||||
import { VariableHide, VariableModel } from '../../../templating/types';
|
||||
import { VariableHide, VariableModel } from '../../../variables/types';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { PickerRenderer } from '../../../variables/pickers/PickerRenderer';
|
||||
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
<div class="submenu-controls" ng-hide="ctrl.submenuEnabled === false">
|
||||
<div
|
||||
ng-repeat="variable in ctrl.variables"
|
||||
ng-hide="variable.hide === 2"
|
||||
class="submenu-item gf-form-inline"
|
||||
aria-label="{{::ctrl.selectors.submenuItem}}"
|
||||
>
|
||||
<div class="gf-form">
|
||||
<label
|
||||
class="gf-form-label template-variable"
|
||||
ng-hide="variable.hide === 1"
|
||||
aria-label="{{ctrl.selectors.submenuItemLabels(variable.label || variable.name)}}"
|
||||
>{{variable.label || variable.name}}</label
|
||||
>
|
||||
<value-select-dropdown
|
||||
ng-if="variable.type !== 'adhoc' && variable.type !== 'textbox'"
|
||||
dashboard="ctrl.dashboard"
|
||||
variable="variable"
|
||||
on-updated="ctrl.variableUpdated(variable)"
|
||||
></value-select-dropdown>
|
||||
<input
|
||||
type="text"
|
||||
ng-if="variable.type === 'textbox'"
|
||||
ng-model="variable.query"
|
||||
class="gf-form-input width-12"
|
||||
ng-blur="variable.current.value != variable.query && variable.updateOptions() && ctrl.variableUpdated(variable);"
|
||||
ng-keydown="$event.keyCode === 13 && variable.current.value != variable.query && variable.updateOptions() && ctrl.variableUpdated(variable);"
|
||||
/>
|
||||
</div>
|
||||
<ad-hoc-filters ng-if="variable.type === 'adhoc'" variable="variable" dashboard="ctrl.dashboard"></ad-hoc-filters>
|
||||
</div>
|
||||
|
||||
<div ng-if="ctrl.dashboard.annotations.list.length > 0">
|
||||
<div
|
||||
ng-repeat="annotation in ctrl.dashboard.annotations.list"
|
||||
ng-hide="annotation.hide"
|
||||
class="submenu-item"
|
||||
ng-class="{'annotation-disabled': !annotation.enable}"
|
||||
>
|
||||
<gf-form-switch
|
||||
class="gf-form"
|
||||
label="{{annotation.name}}"
|
||||
checked="annotation.enable"
|
||||
on-change="ctrl.annotationStateChanged()"
|
||||
></gf-form-switch>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form gf-form--grow"></div>
|
||||
|
||||
<div ng-if="ctrl.dashboard.links.length > 0">
|
||||
<dash-links-container
|
||||
links="ctrl.dashboard.links"
|
||||
dashboard="ctrl.dashboard"
|
||||
class="gf-form-inline"
|
||||
></dash-links-container>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
|
||||
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
||||
import { InspectTab, PanelInspector } from '../components/Inspector/PanelInspector';
|
||||
import { getConfig } from '../../../core/config';
|
||||
import { SubMenu } from '../components/SubMenu/SubMenu';
|
||||
import { cleanUpDashboardAndVariables } from '../state/actions';
|
||||
import { cancelVariables } from '../../variables/state/actions';
|
||||
@@ -275,7 +274,6 @@ export class DashboardPage extends PureComponent<Props, State> {
|
||||
} = this.props;
|
||||
|
||||
const { editPanel, viewPanel, scrollTop, updateScrollTop } = this.state;
|
||||
const { featureToggles } = getConfig();
|
||||
|
||||
if (!dashboard) {
|
||||
if (isInitSlow) {
|
||||
@@ -307,8 +305,7 @@ export class DashboardPage extends PureComponent<Props, State> {
|
||||
{initError && this.renderInitFailedState()}
|
||||
|
||||
<div className={gridWrapperClasses}>
|
||||
{!featureToggles.newVariables && <SubMenu dashboard={dashboard} />}
|
||||
{!editPanel && featureToggles.newVariables && <SubMenu dashboard={dashboard} />}
|
||||
{!editPanel && <SubMenu dashboard={dashboard} />}
|
||||
<DashboardGrid
|
||||
dashboard={dashboard}
|
||||
viewPanel={viewPanel}
|
||||
|
||||
@@ -2,18 +2,14 @@
|
||||
import './services/UnsavedChangesSrv';
|
||||
import './services/DashboardLoaderSrv';
|
||||
import './services/DashboardSrv';
|
||||
|
||||
// Components
|
||||
import './components/DashLinks';
|
||||
import './components/DashExportModal';
|
||||
import './components/DashNav';
|
||||
import './components/VersionHistory';
|
||||
import './components/DashboardSettings';
|
||||
import './components/AdHocFilters';
|
||||
import './components/RowOptions';
|
||||
|
||||
import DashboardPermissions from './components/DashboardPermissions/DashboardPermissions';
|
||||
|
||||
// angular wrappers
|
||||
import { react2AngularDirective } from 'app/core/utils/react2angular';
|
||||
|
||||
|
||||
@@ -134,7 +134,7 @@ export class ChangeTracker {
|
||||
});
|
||||
|
||||
// ignore template variable values
|
||||
_.each(dash.getVariables(), variable => {
|
||||
_.each(dash.getVariables(), (variable: any) => {
|
||||
variable.current = null;
|
||||
variable.options = null;
|
||||
variable.filters = null;
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
} from 'app/core/constants';
|
||||
import { isMulti, isQuery } from 'app/features/variables/guard';
|
||||
import { alignCurrentWithMulti } from 'app/features/variables/shared/multiOptions';
|
||||
import { VariableTag } from '../../templating/types';
|
||||
import { VariableTag } from '../../variables/types';
|
||||
|
||||
export class DashboardMigrator {
|
||||
dashboard: DashboardModel;
|
||||
@@ -240,7 +240,7 @@ export class DashboardMigrator {
|
||||
|
||||
if (oldVersion < 12) {
|
||||
// update template variables
|
||||
_.each(this.dashboard.getVariables(), templateVariable => {
|
||||
_.each(this.dashboard.getVariables(), (templateVariable: any) => {
|
||||
if (templateVariable.refresh) {
|
||||
templateVariable.refresh = 1;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
UrlQueryValue,
|
||||
} from '@grafana/data';
|
||||
import { CoreEvents, DashboardMeta, KIOSK_MODE_TV } from 'app/types';
|
||||
import { getConfig } from '../../../core/config';
|
||||
import { GetVariables, getVariables } from 'app/features/variables/state/selectors';
|
||||
import { variableAdapters } from 'app/features/variables/adapters';
|
||||
import { onTimeRangeUpdated } from 'app/features/variables/state/actions';
|
||||
@@ -229,46 +228,6 @@ export class DashboardModel {
|
||||
private updateTemplatingSaveModelClone(
|
||||
copy: any,
|
||||
defaults: { saveTimerange: boolean; saveVariables: boolean } & CloneOptions
|
||||
) {
|
||||
if (getConfig().featureToggles.newVariables) {
|
||||
this.updateTemplatingSaveModel(copy, defaults);
|
||||
return;
|
||||
}
|
||||
this.updateAngularTemplatingSaveModel(copy, defaults);
|
||||
}
|
||||
|
||||
private updateAngularTemplatingSaveModel(
|
||||
copy: any,
|
||||
defaults: { saveTimerange: boolean; saveVariables: boolean } & CloneOptions
|
||||
) {
|
||||
// get variable save models
|
||||
copy.templating = {
|
||||
list: _.map(this.templating.list, (variable: any) =>
|
||||
variable.getSaveModel ? variable.getSaveModel() : variable
|
||||
),
|
||||
};
|
||||
|
||||
if (!defaults.saveVariables) {
|
||||
for (let i = 0; i < copy.templating.list.length; i++) {
|
||||
const current = copy.templating.list[i];
|
||||
const original: any = _.find(this.originalTemplating, { name: current.name, type: current.type });
|
||||
|
||||
if (!original) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current.type === 'adhoc') {
|
||||
copy.templating.list[i].filters = original.filters;
|
||||
} else {
|
||||
copy.templating.list[i].current = original.current;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateTemplatingSaveModel(
|
||||
copy: any,
|
||||
defaults: { saveTimerange: boolean; saveVariables: boolean } & CloneOptions
|
||||
) {
|
||||
const originalVariables = this.originalTemplating;
|
||||
const currentVariables = this.getVariablesFromState();
|
||||
@@ -297,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) {
|
||||
|
||||
@@ -7,7 +7,6 @@ import { dashboardInitCompleted, dashboardInitFetching, dashboardInitServices }
|
||||
import { updateLocation } from '../../../core/actions';
|
||||
import { setEchoSrv } from '@grafana/runtime';
|
||||
import { Echo } from '../../../core/services/echo/Echo';
|
||||
import { getConfig } from 'app/core/config';
|
||||
import { variableAdapters } from 'app/features/variables/adapters';
|
||||
import { createConstantVariableAdapter } from 'app/features/variables/constant/adapter';
|
||||
import { constantBuilder } from 'app/features/variables/shared/testing/builders';
|
||||
@@ -38,7 +37,6 @@ interface ScenarioContext {
|
||||
timeSrv: any;
|
||||
annotationsSrv: any;
|
||||
unsavedChangesSrv: any;
|
||||
variableSrv: any;
|
||||
dashboardSrv: any;
|
||||
loaderSrv: any;
|
||||
keybindingSrv: any;
|
||||
@@ -55,7 +53,6 @@ function describeInitScenario(description: string, scenarioFn: ScenarioFn) {
|
||||
const timeSrv = { init: jest.fn() };
|
||||
const annotationsSrv = { init: jest.fn() };
|
||||
const unsavedChangesSrv = { init: jest.fn() };
|
||||
const variableSrv = { init: jest.fn() };
|
||||
const dashboardSrv = { setCurrent: jest.fn() };
|
||||
const keybindingSrv = { setupDashboardBindings: jest.fn() };
|
||||
const loaderSrv = {
|
||||
@@ -102,8 +99,6 @@ function describeInitScenario(description: string, scenarioFn: ScenarioFn) {
|
||||
return unsavedChangesSrv;
|
||||
case 'dashboardSrv':
|
||||
return dashboardSrv;
|
||||
case 'variableSrv':
|
||||
return variableSrv;
|
||||
case 'keybindingSrv':
|
||||
return keybindingSrv;
|
||||
default:
|
||||
@@ -126,7 +121,6 @@ function describeInitScenario(description: string, scenarioFn: ScenarioFn) {
|
||||
timeSrv,
|
||||
annotationsSrv,
|
||||
unsavedChangesSrv,
|
||||
variableSrv,
|
||||
dashboardSrv,
|
||||
keybindingSrv,
|
||||
loaderSrv,
|
||||
@@ -201,13 +195,6 @@ describeInitScenario('Initializing new dashboard', ctx => {
|
||||
expect(ctx.keybindingSrv.setupDashboardBindings).toBeCalled();
|
||||
expect(ctx.dashboardSrv.setCurrent).toBeCalled();
|
||||
});
|
||||
|
||||
it('Should initialize variableSrv if newVariables is disabled', () => {
|
||||
if (getConfig().featureToggles.newVariables) {
|
||||
return expect.assertions(0);
|
||||
}
|
||||
expect(ctx.variableSrv.init).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describeInitScenario('Initializing home dashboard', ctx => {
|
||||
@@ -260,9 +247,8 @@ describeInitScenario('Initializing existing dashboard', ctx => {
|
||||
});
|
||||
|
||||
it('Should send action dashboardInitCompleted', () => {
|
||||
const index = getConfig().featureToggles.newVariables ? 6 : 5;
|
||||
expect(ctx.actions[index].type).toBe(dashboardInitCompleted.type);
|
||||
expect(ctx.actions[index].payload.title).toBe('My cool dashboard');
|
||||
expect(ctx.actions[6].type).toBe(dashboardInitCompleted.type);
|
||||
expect(ctx.actions[6].payload.title).toBe('My cool dashboard');
|
||||
});
|
||||
|
||||
it('Should initialize services', () => {
|
||||
@@ -273,17 +259,7 @@ describeInitScenario('Initializing existing dashboard', ctx => {
|
||||
expect(ctx.dashboardSrv.setCurrent).toBeCalled();
|
||||
});
|
||||
|
||||
it('Should initialize variableSrv if newVariables is disabled', () => {
|
||||
if (getConfig().featureToggles.newVariables) {
|
||||
return expect.assertions(0);
|
||||
}
|
||||
expect(ctx.variableSrv.init).toBeCalled();
|
||||
});
|
||||
|
||||
it('Should initialize redux variables if newVariables is enabled', () => {
|
||||
if (!getConfig().featureToggles.newVariables) {
|
||||
return expect.assertions(0);
|
||||
}
|
||||
expect(ctx.actions[3].type).toBe(variablesInitTransaction.type);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,6 @@ import { DashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
import { DashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv';
|
||||
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
import { AnnotationsSrv } from 'app/features/annotations/annotations_srv';
|
||||
import { VariableSrv } from 'app/features/templating/variable_srv';
|
||||
import { KeybindingSrv } from 'app/core/services/keybindingSrv';
|
||||
// Actions
|
||||
import { notifyApp, updateLocation } from 'app/core/actions';
|
||||
@@ -175,7 +174,6 @@ export function initDashboard(args: InitDashboardArgs): ThunkResult<void> {
|
||||
// init services
|
||||
const timeSrv: TimeSrv = args.$injector.get('timeSrv');
|
||||
const annotationsSrv: AnnotationsSrv = args.$injector.get('annotationsSrv');
|
||||
const variableSrv: VariableSrv = args.$injector.get('variableSrv');
|
||||
const keybindingSrv: KeybindingSrv = args.$injector.get('keybindingSrv');
|
||||
const unsavedChangesSrv = args.$injector.get('unsavedChangesSrv');
|
||||
const dashboardSrv: DashboardSrv = args.$injector.get('dashboardSrv');
|
||||
@@ -189,7 +187,7 @@ export function initDashboard(args: InitDashboardArgs): ThunkResult<void> {
|
||||
}
|
||||
|
||||
// template values service needs to initialize completely before the rest of the dashboard can load
|
||||
await dispatch(initVariablesTransaction(args.urlUid, dashboard, variableSrv));
|
||||
await dispatch(initVariablesTransaction(args.urlUid, dashboard));
|
||||
|
||||
if (getState().templating.transaction.uid !== args.urlUid) {
|
||||
// if a previous dashboard has slow running variable queries the batch uid will be the new one
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
const obj2string = (obj: any) => {
|
||||
return Object.keys(obj)
|
||||
.reduce((acc, curr) => acc.concat(curr + '=' + obj[curr]), [])
|
||||
.join();
|
||||
};
|
||||
|
||||
export class GeneralTabCtrl {
|
||||
panelCtrl: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor($scope: any) {
|
||||
this.panelCtrl = $scope.ctrl;
|
||||
|
||||
const updatePanel = () => {
|
||||
console.log('panel.render()');
|
||||
this.panelCtrl.panel.render();
|
||||
};
|
||||
|
||||
const generateValueFromPanel = (scope: any) => {
|
||||
const { panel } = scope.ctrl;
|
||||
const panelPropsToTrack = ['title', 'description', 'transparent', 'repeat', 'repeatDirection', 'minSpan'];
|
||||
const panelPropsString = panelPropsToTrack
|
||||
.map(prop => prop + '=' + (panel[prop] && panel[prop].toString ? panel[prop].toString() : panel[prop]))
|
||||
.join();
|
||||
const panelLinks = panel.links || [];
|
||||
const panelLinksString = panelLinks.map(obj2string).join();
|
||||
return panelPropsString + panelLinksString;
|
||||
};
|
||||
|
||||
$scope.$watch(generateValueFromPanel, updatePanel, true);
|
||||
}
|
||||
}
|
||||
|
||||
/** @ngInject */
|
||||
export function generalTab() {
|
||||
'use strict';
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: 'public/app/features/panel/partials/general_tab.html',
|
||||
controller: GeneralTabCtrl,
|
||||
};
|
||||
}
|
||||
|
||||
coreModule.directive('panelGeneralTab', generalTab);
|
||||
@@ -2,5 +2,4 @@ import './panel_directive';
|
||||
import './query_ctrl';
|
||||
import './panel_editor_tab';
|
||||
import './query_editor_row';
|
||||
import './repeat_option';
|
||||
import './panellinks/module';
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
<div class="panel-options-group">
|
||||
<!-- <div class="panel-option-section__header">Information</div> -->
|
||||
<div class="panel-options-group__body">
|
||||
<div class="section">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-7">Title</span>
|
||||
<input type="text" class="gf-form-input width-25" ng-model='ctrl.panel.title' ng-model-onblur></input>
|
||||
</div>
|
||||
<gf-form-switch class="gf-form" label-class="width-7" switch-class="max-width-6" label="Transparent" checked="ctrl.panel.transparent" on-change="ctrl.render()"></gf-form-switch>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="gf-form gf-form--v-stretch">
|
||||
<span class="gf-form-label width-7">Description</span>
|
||||
<textarea class="gf-form-input width-25" rows="5" ng-model="ctrl.panel.description" ng-model-onblur placeholder="Panel description, supports markdown & links"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-options-group">
|
||||
<div class="panel-options-group__header">
|
||||
<div class="panel-options-group__title">Repeating</div>
|
||||
</div>
|
||||
<div class="panel-options-group__body">
|
||||
<div class="section">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9">Repeat</span>
|
||||
<dash-repeat-option panel="ctrl.panel"></dash-repeat-option>
|
||||
</div>
|
||||
<div class="gf-form" ng-show="ctrl.panel.repeat">
|
||||
<span class="gf-form-label width-9">Direction</span>
|
||||
<select class="gf-form-input" ng-model="ctrl.panel.repeatDirection" ng-options="f.value as f.text for f in [{value: 'v', text: 'Vertical'}, {value: 'h', text: 'Horizontal'}]">
|
||||
<option value=""></option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="gf-form" ng-show="ctrl.panel.repeat && ctrl.panel.repeatDirection == 'h'">
|
||||
<span class="gf-form-label width-9">Max per row</span>
|
||||
<select class="gf-form-input" ng-model="ctrl.panel.maxPerRow" ng-options="f for f in [2,3,4,6,12,24]">
|
||||
<option value=""></option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="gf-form-hint">
|
||||
<div class="gf-form-hint-text muted">
|
||||
Note: You may need to change the variable selection to see this in action.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,59 +0,0 @@
|
||||
import { coreModule } from 'app/core/core';
|
||||
import { VariableSrv } from 'app/features/templating/variable_srv';
|
||||
import { getConfig } from '../../core/config';
|
||||
import { getVariables } from '../variables/state/selectors';
|
||||
|
||||
const template = `
|
||||
<div class="gf-form-select-wrapper max-width-18">
|
||||
<select class="gf-form-input" ng-model="panel.repeat" ng-options="f.value as f.text for f in variables" ng-change="optionChanged()">
|
||||
<option value=""></option>
|
||||
</div>
|
||||
`;
|
||||
|
||||
/** @ngInject */
|
||||
function dashRepeatOptionDirective(variableSrv: VariableSrv) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: template,
|
||||
scope: {
|
||||
panel: '=',
|
||||
},
|
||||
link: (scope: any, element: JQuery) => {
|
||||
element.css({ display: 'block', width: '100%' });
|
||||
|
||||
if (getConfig().featureToggles.newVariables) {
|
||||
scope.variables = getVariables().map((item: any) => {
|
||||
return { text: item.name, value: item.name };
|
||||
});
|
||||
}
|
||||
|
||||
if (!getConfig().featureToggles.newVariables) {
|
||||
scope.variables = variableSrv.variables.map((item: any) => {
|
||||
return { text: item.name, value: item.name };
|
||||
});
|
||||
}
|
||||
|
||||
if (scope.variables.length === 0) {
|
||||
scope.variables.unshift({
|
||||
text: 'No template variables found',
|
||||
value: null,
|
||||
});
|
||||
}
|
||||
|
||||
scope.variables.unshift({ text: 'Disabled', value: null });
|
||||
|
||||
// if repeat is set and no direction set to horizontal
|
||||
if (scope.panel.repeat && !scope.panel.repeatDirection) {
|
||||
scope.panel.repeatDirection = 'h';
|
||||
}
|
||||
|
||||
scope.optionChanged = () => {
|
||||
if (scope.panel.repeat) {
|
||||
scope.panel.repeatDirection = 'h';
|
||||
}
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
coreModule.directive('dashRepeatOption', dashRepeatOptionDirective);
|
||||
@@ -12,7 +12,7 @@ import { TemplateSrv } from '../templating/template_srv';
|
||||
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
|
||||
// Pretend Datasource
|
||||
import { expressionDatasource } from 'app/features/expressions/ExpressionDatasource';
|
||||
import { DataSourceVariableModel } from '../templating/types';
|
||||
import { DataSourceVariableModel } from '../variables/types';
|
||||
|
||||
export class DatasourceSrv implements DataSourceService {
|
||||
datasources: Record<string, DataSourceApi> = {};
|
||||
|
||||
@@ -2,7 +2,7 @@ import coreModule from 'app/core/core_module';
|
||||
import { importDataSourcePlugin } from './plugin_loader';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import DefaultVariableQueryEditor from '../templating/DefaultVariableQueryEditor';
|
||||
import DefaultVariableQueryEditor from '../variables/editor/DefaultVariableQueryEditor';
|
||||
import { DataSourcePluginMeta } from '@grafana/data';
|
||||
import { TemplateSrv } from '../templating/template_srv';
|
||||
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import {
|
||||
assignModelProperties,
|
||||
TextBoxVariableModel,
|
||||
VariableActions,
|
||||
VariableHide,
|
||||
VariableOption,
|
||||
variableTypes,
|
||||
} from './types';
|
||||
import { VariableSrv } from './variable_srv';
|
||||
import { VariableType } from '@grafana/data';
|
||||
|
||||
export class TextBoxVariable implements TextBoxVariableModel, VariableActions {
|
||||
type: VariableType;
|
||||
name: string;
|
||||
label: string;
|
||||
hide: VariableHide;
|
||||
skipUrlSync: boolean;
|
||||
query: string;
|
||||
current: VariableOption;
|
||||
options: VariableOption[];
|
||||
|
||||
defaults: TextBoxVariableModel = {
|
||||
type: 'textbox',
|
||||
name: '',
|
||||
label: '',
|
||||
hide: VariableHide.dontHide,
|
||||
query: '',
|
||||
current: {} as VariableOption,
|
||||
options: [],
|
||||
skipUrlSync: false,
|
||||
};
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private model: any, private variableSrv: VariableSrv) {
|
||||
assignModelProperties(this, model, this.defaults);
|
||||
}
|
||||
|
||||
getSaveModel() {
|
||||
assignModelProperties(this.model, this, this.defaults);
|
||||
return this.model;
|
||||
}
|
||||
|
||||
setValue(option: any) {
|
||||
this.variableSrv.setOptionAsCurrent(this, option);
|
||||
}
|
||||
|
||||
updateOptions() {
|
||||
this.options = [{ text: this.query.trim(), value: this.query.trim(), selected: false }];
|
||||
this.current = this.options[0];
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
dependsOn(variable: any) {
|
||||
return false;
|
||||
}
|
||||
|
||||
setValueFromUrl(urlValue: string) {
|
||||
this.query = urlValue;
|
||||
return this.variableSrv.setOptionFromUrl(this, urlValue);
|
||||
}
|
||||
|
||||
getValueForUrl() {
|
||||
return this.current.value;
|
||||
}
|
||||
}
|
||||
// @ts-ignore
|
||||
variableTypes['textbox'] = {
|
||||
name: 'Text box',
|
||||
ctor: TextBoxVariable,
|
||||
description: 'Define a textbox variable, where users can enter any arbitrary string',
|
||||
};
|
||||
@@ -1,101 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import {
|
||||
AdHocVariableFilter,
|
||||
AdHocVariableModel,
|
||||
assignModelProperties,
|
||||
VariableActions,
|
||||
VariableHide,
|
||||
variableTypes,
|
||||
} from './types';
|
||||
|
||||
import { VariableType } from '@grafana/data';
|
||||
|
||||
export class AdhocVariable implements AdHocVariableModel, VariableActions {
|
||||
type: VariableType;
|
||||
name: string;
|
||||
label: string;
|
||||
hide: VariableHide;
|
||||
skipUrlSync: boolean;
|
||||
filters: AdHocVariableFilter[];
|
||||
datasource: string;
|
||||
|
||||
defaults: AdHocVariableModel = {
|
||||
type: 'adhoc',
|
||||
name: '',
|
||||
label: '',
|
||||
hide: VariableHide.dontHide,
|
||||
skipUrlSync: false,
|
||||
datasource: null,
|
||||
filters: [],
|
||||
};
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private model: any) {
|
||||
assignModelProperties(this, model, this.defaults);
|
||||
}
|
||||
|
||||
setValue(option: any) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
getSaveModel() {
|
||||
assignModelProperties(this.model, this, this.defaults);
|
||||
return this.model;
|
||||
}
|
||||
|
||||
updateOptions() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
dependsOn(variable: any) {
|
||||
return false;
|
||||
}
|
||||
|
||||
setValueFromUrl(urlValue: string[] | string[]) {
|
||||
if (!_.isArray(urlValue)) {
|
||||
urlValue = [urlValue];
|
||||
}
|
||||
|
||||
this.filters = urlValue.map(item => {
|
||||
const values = item.split('|').map(value => {
|
||||
return this.unescapeDelimiter(value);
|
||||
});
|
||||
return {
|
||||
key: values[0],
|
||||
operator: values[1],
|
||||
value: values[2],
|
||||
condition: '',
|
||||
};
|
||||
});
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
getValueForUrl() {
|
||||
return this.filters.map(filter => {
|
||||
return [filter.key, filter.operator, filter.value]
|
||||
.map(value => {
|
||||
return this.escapeDelimiter(value);
|
||||
})
|
||||
.join('|');
|
||||
});
|
||||
}
|
||||
|
||||
escapeDelimiter(value: string) {
|
||||
return value.replace(/\|/g, '__gfp__');
|
||||
}
|
||||
|
||||
unescapeDelimiter(value: string) {
|
||||
return value.replace(/__gfp__/g, '|');
|
||||
}
|
||||
|
||||
setFilters(filters: any[]) {
|
||||
this.filters = filters;
|
||||
}
|
||||
}
|
||||
|
||||
variableTypes['adhoc'] = {
|
||||
name: 'Ad hoc filters',
|
||||
ctor: AdhocVariable,
|
||||
description: 'Add key/value filters on the fly',
|
||||
};
|
||||
@@ -1,25 +1,4 @@
|
||||
import './editor_ctrl';
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
import templateSrv from './template_srv';
|
||||
import { VariableSrv } from './variable_srv';
|
||||
import { IntervalVariable } from './interval_variable';
|
||||
import { QueryVariable } from './query_variable';
|
||||
import { DatasourceVariable } from './datasource_variable';
|
||||
import { CustomVariable } from './custom_variable';
|
||||
import { ConstantVariable } from './constant_variable';
|
||||
import { AdhocVariable } from './adhoc_variable';
|
||||
import { TextBoxVariable } from './TextBoxVariable';
|
||||
|
||||
coreModule.factory('templateSrv', () => templateSrv);
|
||||
|
||||
export {
|
||||
VariableSrv,
|
||||
IntervalVariable,
|
||||
QueryVariable,
|
||||
DatasourceVariable,
|
||||
CustomVariable,
|
||||
ConstantVariable,
|
||||
AdhocVariable,
|
||||
TextBoxVariable,
|
||||
};
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import {
|
||||
assignModelProperties,
|
||||
ConstantVariableModel,
|
||||
VariableActions,
|
||||
VariableHide,
|
||||
VariableOption,
|
||||
variableTypes,
|
||||
} from './types';
|
||||
import { VariableSrv } from './all';
|
||||
import { VariableType } from '@grafana/data';
|
||||
|
||||
export class ConstantVariable implements ConstantVariableModel, VariableActions {
|
||||
type: VariableType;
|
||||
name: string;
|
||||
label: string;
|
||||
hide: VariableHide;
|
||||
skipUrlSync: boolean;
|
||||
query: string;
|
||||
options: VariableOption[];
|
||||
current: VariableOption;
|
||||
|
||||
defaults: ConstantVariableModel = {
|
||||
type: 'constant',
|
||||
name: '',
|
||||
hide: VariableHide.hideVariable,
|
||||
label: '',
|
||||
query: '',
|
||||
current: {} as VariableOption,
|
||||
options: [],
|
||||
skipUrlSync: false,
|
||||
};
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private model: any, private variableSrv: VariableSrv) {
|
||||
assignModelProperties(this, model, this.defaults);
|
||||
}
|
||||
|
||||
getSaveModel() {
|
||||
assignModelProperties(this.model, this, this.defaults);
|
||||
return this.model;
|
||||
}
|
||||
|
||||
setValue(option: any) {
|
||||
this.variableSrv.setOptionAsCurrent(this, option);
|
||||
}
|
||||
|
||||
updateOptions() {
|
||||
this.options = [{ text: this.query.trim(), value: this.query.trim(), selected: false }];
|
||||
this.setValue(this.options[0]);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
dependsOn(variable: any) {
|
||||
return false;
|
||||
}
|
||||
|
||||
setValueFromUrl(urlValue: string) {
|
||||
return this.variableSrv.setOptionFromUrl(this, urlValue);
|
||||
}
|
||||
|
||||
getValueForUrl() {
|
||||
return this.current.value;
|
||||
}
|
||||
}
|
||||
|
||||
variableTypes['constant'] = {
|
||||
name: 'Constant',
|
||||
ctor: ConstantVariable,
|
||||
description: 'Define a hidden constant variable, useful for metric prefixes in dashboards you want to share',
|
||||
};
|
||||
@@ -1,93 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import {
|
||||
assignModelProperties,
|
||||
CustomVariableModel,
|
||||
VariableActions,
|
||||
VariableHide,
|
||||
VariableOption,
|
||||
variableTypes,
|
||||
} from './types';
|
||||
import { VariableSrv } from './variable_srv';
|
||||
import { VariableType } from '@grafana/data';
|
||||
|
||||
export class CustomVariable implements CustomVariableModel, VariableActions {
|
||||
type: VariableType;
|
||||
name: string;
|
||||
label: string;
|
||||
hide: VariableHide;
|
||||
skipUrlSync: boolean;
|
||||
query: string;
|
||||
options: VariableOption[];
|
||||
includeAll: boolean;
|
||||
multi: boolean;
|
||||
current: VariableOption;
|
||||
allValue: string;
|
||||
|
||||
defaults: CustomVariableModel = {
|
||||
type: 'custom',
|
||||
name: '',
|
||||
label: '',
|
||||
hide: VariableHide.dontHide,
|
||||
skipUrlSync: false,
|
||||
query: '',
|
||||
options: [],
|
||||
includeAll: false,
|
||||
multi: false,
|
||||
current: {} as VariableOption,
|
||||
allValue: null,
|
||||
};
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private model: any, private variableSrv: VariableSrv) {
|
||||
assignModelProperties(this, model, this.defaults);
|
||||
}
|
||||
|
||||
setValue(option: any) {
|
||||
return this.variableSrv.setOptionAsCurrent(this, option);
|
||||
}
|
||||
|
||||
getSaveModel() {
|
||||
assignModelProperties(this.model, this, this.defaults);
|
||||
return this.model;
|
||||
}
|
||||
|
||||
updateOptions() {
|
||||
// extract options in comma separated string (use backslash to escape wanted commas)
|
||||
this.options = _.map(this.query.match(/(?:\\,|[^,])+/g), text => {
|
||||
text = text.replace(/\\,/g, ',');
|
||||
return { text: text.trim(), value: text.trim(), selected: false };
|
||||
});
|
||||
|
||||
if (this.includeAll) {
|
||||
this.addAllOption();
|
||||
}
|
||||
|
||||
return this.variableSrv.validateVariableSelectionState(this);
|
||||
}
|
||||
|
||||
addAllOption() {
|
||||
this.options.unshift({ text: 'All', value: '$__all', selected: false });
|
||||
}
|
||||
|
||||
dependsOn(variable: any) {
|
||||
return false;
|
||||
}
|
||||
|
||||
setValueFromUrl(urlValue: string[]) {
|
||||
return this.variableSrv.setOptionFromUrl(this, urlValue);
|
||||
}
|
||||
|
||||
getValueForUrl() {
|
||||
if (this.current.text === 'All') {
|
||||
return 'All';
|
||||
}
|
||||
return this.current.value;
|
||||
}
|
||||
}
|
||||
|
||||
variableTypes['custom'] = {
|
||||
name: 'Custom',
|
||||
ctor: CustomVariable,
|
||||
description: 'Define variable values manually',
|
||||
supportsMulti: true,
|
||||
};
|
||||
@@ -1,133 +0,0 @@
|
||||
import {
|
||||
assignModelProperties,
|
||||
DataSourceVariableModel,
|
||||
VariableActions,
|
||||
VariableHide,
|
||||
VariableOption,
|
||||
VariableRefresh,
|
||||
variableTypes,
|
||||
} from './types';
|
||||
import { VariableType, stringToJsRegex } from '@grafana/data';
|
||||
import { VariableSrv } from './variable_srv';
|
||||
import { TemplateSrv } from './template_srv';
|
||||
import { DatasourceSrv } from '../plugins/datasource_srv';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { containsVariable } from './utils';
|
||||
|
||||
export class DatasourceVariable implements DataSourceVariableModel, VariableActions {
|
||||
type: VariableType;
|
||||
name: string;
|
||||
label: string;
|
||||
hide: VariableHide;
|
||||
regex: any;
|
||||
query: string;
|
||||
options: VariableOption[];
|
||||
current: VariableOption;
|
||||
multi: boolean;
|
||||
includeAll: boolean;
|
||||
refresh: VariableRefresh;
|
||||
skipUrlSync: boolean;
|
||||
|
||||
defaults: DataSourceVariableModel = {
|
||||
type: 'datasource',
|
||||
name: '',
|
||||
hide: 0,
|
||||
label: '',
|
||||
current: {} as VariableOption,
|
||||
regex: '',
|
||||
options: [],
|
||||
query: '',
|
||||
multi: false,
|
||||
includeAll: false,
|
||||
refresh: 1,
|
||||
skipUrlSync: false,
|
||||
};
|
||||
|
||||
/** @ngInject */
|
||||
constructor(
|
||||
private model: any,
|
||||
private datasourceSrv: DatasourceSrv,
|
||||
private variableSrv: VariableSrv,
|
||||
private templateSrv: TemplateSrv
|
||||
) {
|
||||
assignModelProperties(this, model, this.defaults);
|
||||
this.refresh = 1;
|
||||
}
|
||||
|
||||
getSaveModel() {
|
||||
assignModelProperties(this.model, this, this.defaults);
|
||||
|
||||
// don't persist options
|
||||
this.model.options = [];
|
||||
return this.model;
|
||||
}
|
||||
|
||||
setValue(option: any) {
|
||||
return this.variableSrv.setOptionAsCurrent(this, option);
|
||||
}
|
||||
|
||||
updateOptions() {
|
||||
const options: VariableOption[] = [];
|
||||
const sources = this.datasourceSrv.getMetricSources({ skipVariables: true });
|
||||
let regex;
|
||||
|
||||
if (this.regex) {
|
||||
regex = this.templateSrv.replace(this.regex, undefined, 'regex');
|
||||
regex = stringToJsRegex(regex);
|
||||
}
|
||||
|
||||
for (let i = 0; i < sources.length; i++) {
|
||||
const source = sources[i];
|
||||
// must match on type
|
||||
if (source.meta.id !== this.query) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (regex && !regex.exec(source.name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
options.push({ text: source.name, value: source.name, selected: false });
|
||||
}
|
||||
|
||||
if (options.length === 0) {
|
||||
options.push({ text: 'No data sources found', value: '', selected: false });
|
||||
}
|
||||
|
||||
this.options = options;
|
||||
if (this.includeAll) {
|
||||
this.addAllOption();
|
||||
}
|
||||
const { defaultDatasource } = config.bootData.settings;
|
||||
return this.variableSrv.validateVariableSelectionState(this, defaultDatasource);
|
||||
}
|
||||
|
||||
addAllOption() {
|
||||
this.options.unshift({ text: 'All', value: '$__all', selected: false });
|
||||
}
|
||||
|
||||
dependsOn(variable: any) {
|
||||
if (this.regex) {
|
||||
return containsVariable(this.regex, variable.name);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
setValueFromUrl(urlValue: string | string[]) {
|
||||
return this.variableSrv.setOptionFromUrl(this, urlValue);
|
||||
}
|
||||
|
||||
getValueForUrl() {
|
||||
if (this.current.text === 'All') {
|
||||
return 'All';
|
||||
}
|
||||
return this.current.value;
|
||||
}
|
||||
}
|
||||
|
||||
variableTypes['datasource'] = {
|
||||
name: 'Datasource',
|
||||
ctor: DatasourceVariable,
|
||||
supportsMulti: true,
|
||||
description: 'Enabled you to dynamically switch the datasource for multiple panels',
|
||||
};
|
||||
@@ -1,247 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import { AppEvents } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { variableTypes } from './types';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import DatasourceSrv from '../plugins/datasource_srv';
|
||||
import { VariableSrv } from './all';
|
||||
import { TemplateSrv } from './template_srv';
|
||||
import { promiseToDigest } from '../../core/utils/promiseToDigest';
|
||||
|
||||
export class VariableEditorCtrl {
|
||||
/** @ngInject */
|
||||
constructor($scope: any, datasourceSrv: DatasourceSrv, variableSrv: VariableSrv, templateSrv: TemplateSrv) {
|
||||
$scope.variableTypes = variableTypes;
|
||||
$scope.ctrl = {};
|
||||
$scope.namePattern = /^(?!__).*$/;
|
||||
$scope._ = _;
|
||||
$scope.optionsLimit = 20;
|
||||
$scope.emptyListCta = {
|
||||
title: 'There are no variables yet',
|
||||
buttonTitle: 'Add variable',
|
||||
buttonIcon: 'gicon gicon-variable',
|
||||
infoBox: {
|
||||
__html: ` <p>
|
||||
Variables enable more interactive and dynamic dashboards. Instead of hard-coding things like server or
|
||||
sensor names in your metric queries you can use variables in their place. Variables are shown as dropdown
|
||||
select boxes at the top of the dashboard. These dropdowns make it easy to change the data being displayed in
|
||||
your dashboard. Check out the
|
||||
<a class="external-link" href="http://docs.grafana.org/reference/templating/" target="_blank">
|
||||
Templating documentation
|
||||
</a>
|
||||
for more information.
|
||||
</p>`,
|
||||
infoBoxTitle: 'What do variables do?',
|
||||
},
|
||||
};
|
||||
|
||||
$scope.refreshOptions = [
|
||||
{ value: 0, text: 'Never' },
|
||||
{ value: 1, text: 'On Dashboard Load' },
|
||||
{ value: 2, text: 'On Time Range Change' },
|
||||
];
|
||||
|
||||
$scope.sortOptions = [
|
||||
{ value: 0, text: 'Disabled' },
|
||||
{ value: 1, text: 'Alphabetical (asc)' },
|
||||
{ value: 2, text: 'Alphabetical (desc)' },
|
||||
{ value: 3, text: 'Numerical (asc)' },
|
||||
{ value: 4, text: 'Numerical (desc)' },
|
||||
{ value: 5, text: 'Alphabetical (case-insensitive, asc)' },
|
||||
{ value: 6, text: 'Alphabetical (case-insensitive, desc)' },
|
||||
];
|
||||
|
||||
$scope.hideOptions = [
|
||||
{ value: 0, text: '' },
|
||||
{ value: 1, text: 'Label' },
|
||||
{ value: 2, text: 'Variable' },
|
||||
];
|
||||
|
||||
$scope.selectors = {
|
||||
...selectors.pages.Dashboard.Settings.Variables.List,
|
||||
...selectors.pages.Dashboard.Settings.Variables.Edit.General,
|
||||
...selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable,
|
||||
...selectors.pages.Dashboard.Settings.Variables.Edit.ConstantVariable,
|
||||
};
|
||||
|
||||
$scope.init = () => {
|
||||
$scope.mode = 'list';
|
||||
|
||||
$scope.variables = variableSrv.variables;
|
||||
$scope.reset();
|
||||
|
||||
$scope.$watch('mode', (val: string) => {
|
||||
if (val === 'new') {
|
||||
$scope.reset();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.setMode = (mode: any) => {
|
||||
$scope.mode = mode;
|
||||
};
|
||||
|
||||
$scope.setNewMode = () => {
|
||||
$scope.setMode('new');
|
||||
};
|
||||
|
||||
$scope.add = () => {
|
||||
if ($scope.isValid()) {
|
||||
variableSrv.addVariable($scope.current);
|
||||
$scope.update();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.isValid = () => {
|
||||
if (!$scope.ctrl.form.$valid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$scope.current.name.match(/^\w+$/)) {
|
||||
appEvents.emit(AppEvents.alertWarning, [
|
||||
'Validation',
|
||||
'Only word and digit characters are allowed in variable names',
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
|
||||
const sameName: any = _.find($scope.variables, { name: $scope.current.name });
|
||||
if (sameName && sameName !== $scope.current) {
|
||||
appEvents.emit(AppEvents.alertWarning, ['Validation', 'Variable with the same name already exists']);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
$scope.current.type === 'query' &&
|
||||
_.isString($scope.current.query) &&
|
||||
$scope.current.query.match(new RegExp('\\$' + $scope.current.name + '(/| |$)'))
|
||||
) {
|
||||
appEvents.emit(AppEvents.alertWarning, [
|
||||
'Validation',
|
||||
'Query cannot contain a reference to itself. Variable: $' + $scope.current.name,
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
$scope.validate = () => {
|
||||
$scope.infoText = '';
|
||||
if ($scope.current.type === 'adhoc' && $scope.current.datasource !== null) {
|
||||
$scope.infoText = 'Adhoc filters are applied automatically to all queries that target this datasource';
|
||||
promiseToDigest($scope)(
|
||||
datasourceSrv.get($scope.current.datasource).then(ds => {
|
||||
if (!ds.getTagKeys) {
|
||||
$scope.infoText = 'This datasource does not support adhoc filters yet.';
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.runQuery = () => {
|
||||
$scope.optionsLimit = 20;
|
||||
return variableSrv.updateOptions($scope.current).catch((err: { data: { message: any }; message: string }) => {
|
||||
if (err.data && err.data.message) {
|
||||
err.message = err.data.message;
|
||||
}
|
||||
appEvents.emit(AppEvents.alertError, [
|
||||
'Templating',
|
||||
'Template variables could not be initialized: ' + err.message,
|
||||
]);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.onQueryChange = (query: any, definition: any) => {
|
||||
$scope.current.query = query;
|
||||
$scope.current.definition = definition;
|
||||
$scope.runQuery();
|
||||
};
|
||||
|
||||
$scope.edit = (variable: any) => {
|
||||
$scope.current = variable;
|
||||
$scope.currentIsNew = false;
|
||||
$scope.mode = 'edit';
|
||||
$scope.validate();
|
||||
promiseToDigest($scope)(
|
||||
datasourceSrv.get($scope.current.datasource).then(ds => {
|
||||
$scope.currentDatasource = ds;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
$scope.duplicate = (variable: { getSaveModel: () => void; name: string }) => {
|
||||
const clone = _.cloneDeep(variable.getSaveModel());
|
||||
$scope.current = variableSrv.createVariableFromModel(clone);
|
||||
$scope.current.name = 'copy_of_' + variable.name;
|
||||
variableSrv.addVariable($scope.current);
|
||||
};
|
||||
|
||||
$scope.update = () => {
|
||||
if ($scope.isValid()) {
|
||||
promiseToDigest($scope)(
|
||||
$scope.runQuery().then(() => {
|
||||
$scope.reset();
|
||||
$scope.mode = 'list';
|
||||
templateSrv.updateIndex();
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.reset = () => {
|
||||
$scope.currentIsNew = true;
|
||||
$scope.current = variableSrv.createVariableFromModel({ type: 'query' });
|
||||
|
||||
// this is done here in case a new data source type variable was added
|
||||
$scope.datasources = _.filter(datasourceSrv.getMetricSources(), ds => {
|
||||
return !ds.meta.mixed && ds.value !== null;
|
||||
});
|
||||
|
||||
$scope.datasourceTypes = _($scope.datasources)
|
||||
.uniqBy('meta.id')
|
||||
.map((ds: any) => {
|
||||
return { text: ds.meta.name, value: ds.meta.id };
|
||||
})
|
||||
.value();
|
||||
};
|
||||
|
||||
$scope.typeChanged = function() {
|
||||
const old = $scope.current;
|
||||
$scope.current = variableSrv.createVariableFromModel({
|
||||
type: $scope.current.type,
|
||||
});
|
||||
$scope.current.name = old.name;
|
||||
$scope.current.label = old.label;
|
||||
|
||||
const oldIndex = _.indexOf(this.variables, old);
|
||||
if (oldIndex !== -1) {
|
||||
this.variables[oldIndex] = $scope.current;
|
||||
}
|
||||
|
||||
$scope.validate();
|
||||
};
|
||||
|
||||
$scope.removeVariable = (variable: any) => {
|
||||
variableSrv.removeVariable(variable);
|
||||
};
|
||||
|
||||
$scope.showMoreOptions = () => {
|
||||
$scope.optionsLimit += 20;
|
||||
};
|
||||
|
||||
$scope.datasourceChanged = async () => {
|
||||
promiseToDigest($scope)(
|
||||
datasourceSrv.get($scope.current.datasource).then(ds => {
|
||||
$scope.current.query = '';
|
||||
$scope.currentDatasource = ds;
|
||||
})
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
coreModule.controller('VariableEditorCtrl', VariableEditorCtrl);
|
||||
@@ -1,117 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import {
|
||||
assignModelProperties,
|
||||
IntervalVariableModel,
|
||||
VariableActions,
|
||||
VariableHide,
|
||||
VariableOption,
|
||||
VariableRefresh,
|
||||
variableTypes,
|
||||
} from './types';
|
||||
import { TimeSrv } from '../dashboard/services/TimeSrv';
|
||||
import { TemplateSrv } from './template_srv';
|
||||
import { VariableSrv } from './variable_srv';
|
||||
import { VariableType } from '@grafana/data';
|
||||
|
||||
export class IntervalVariable implements IntervalVariableModel, VariableActions {
|
||||
type: VariableType;
|
||||
name: string;
|
||||
label: string;
|
||||
hide: VariableHide;
|
||||
skipUrlSync: boolean;
|
||||
auto_count: number; // eslint-disable-line camelcase
|
||||
auto_min: string; // eslint-disable-line camelcase
|
||||
options: VariableOption[];
|
||||
auto: boolean;
|
||||
query: string;
|
||||
refresh: VariableRefresh;
|
||||
current: VariableOption;
|
||||
|
||||
defaults: IntervalVariableModel = {
|
||||
type: 'interval',
|
||||
name: '',
|
||||
label: '',
|
||||
hide: VariableHide.dontHide,
|
||||
skipUrlSync: false,
|
||||
auto_count: 30,
|
||||
auto_min: '10s',
|
||||
options: [],
|
||||
auto: false,
|
||||
query: '1m,10m,30m,1h,6h,12h,1d,7d,14d,30d',
|
||||
refresh: VariableRefresh.onTimeRangeChanged,
|
||||
current: {} as VariableOption,
|
||||
};
|
||||
|
||||
/** @ngInject */
|
||||
constructor(
|
||||
private model: any,
|
||||
private timeSrv: TimeSrv,
|
||||
private templateSrv: TemplateSrv,
|
||||
private variableSrv: VariableSrv
|
||||
) {
|
||||
assignModelProperties(this, model, this.defaults);
|
||||
this.refresh = VariableRefresh.onTimeRangeChanged;
|
||||
}
|
||||
|
||||
getSaveModel() {
|
||||
assignModelProperties(this.model, this, this.defaults);
|
||||
return this.model;
|
||||
}
|
||||
|
||||
setValue(option: any) {
|
||||
this.updateAutoValue();
|
||||
return this.variableSrv.setOptionAsCurrent(this, option);
|
||||
}
|
||||
|
||||
updateAutoValue() {
|
||||
if (!this.auto) {
|
||||
return;
|
||||
}
|
||||
|
||||
// add auto option if missing
|
||||
if (this.options.length && this.options[0].text !== 'auto') {
|
||||
this.options.unshift({
|
||||
text: 'auto',
|
||||
value: '$__auto_interval_' + this.name,
|
||||
selected: false,
|
||||
});
|
||||
}
|
||||
|
||||
const res = kbn.calculateInterval(this.timeSrv.timeRange(), this.auto_count, this.auto_min);
|
||||
this.templateSrv.setGrafanaVariable('$__auto_interval_' + this.name, res.interval);
|
||||
// for backward compatibility, to be removed eventually
|
||||
this.templateSrv.setGrafanaVariable('$__auto_interval', res.interval);
|
||||
}
|
||||
|
||||
updateOptions() {
|
||||
// extract options between quotes and/or comma
|
||||
this.options = _.map(this.query.match(/(["'])(.*?)\1|\w+/g), text => {
|
||||
text = text.replace(/["']+/g, '');
|
||||
return { text: text.trim(), value: text.trim(), selected: false };
|
||||
});
|
||||
|
||||
this.updateAutoValue();
|
||||
return this.variableSrv.validateVariableSelectionState(this);
|
||||
}
|
||||
|
||||
dependsOn(variable: any) {
|
||||
return false;
|
||||
}
|
||||
|
||||
setValueFromUrl(urlValue: string | string[]) {
|
||||
this.updateAutoValue();
|
||||
return this.variableSrv.setOptionFromUrl(this, urlValue);
|
||||
}
|
||||
|
||||
getValueForUrl() {
|
||||
return this.current.value;
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
variableTypes['interval'] = {
|
||||
name: 'Interval',
|
||||
ctor: IntervalVariable,
|
||||
description: 'Define a timespan interval (ex 1m, 1h, 1d)',
|
||||
};
|
||||
@@ -1,526 +0,0 @@
|
||||
<div ng-controller="VariableEditorCtrl" ng-init="init()">
|
||||
<div class="page-action-bar">
|
||||
<h3 class="dashboard-settings__header">
|
||||
<a ng-click="setMode('list')" aria-label="{{::selectors.headerLink}}">Variables</a>
|
||||
<span ng-show="mode === 'new'"
|
||||
><icon name="'angle-right'" aria-label="{{::selectors.modeLabelNew}}"></icon> New</span
|
||||
>
|
||||
<span ng-show="mode === 'edit'"
|
||||
><icon name="'angle-right'" aria-label="{{::selectors.modeLabelEdit}}"></icon> Edit</span
|
||||
>
|
||||
</h3>
|
||||
|
||||
<div class="page-action-bar__spacer"></div>
|
||||
<a
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
ng-click="setMode('new');"
|
||||
ng-if="variables.length > 0"
|
||||
ng-hide="mode === 'edit' || mode === 'new'"
|
||||
aria-label="{{::selectors.newButton}}"
|
||||
>
|
||||
New
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div ng-if="mode === 'list'">
|
||||
<div ng-if="variables.length === 0">
|
||||
<empty-list-cta
|
||||
on-click="setNewMode"
|
||||
title="emptyListCta.title"
|
||||
infoBox="emptyListCta.infoBox"
|
||||
infoBoxTitle="emptyListCta.infoBoxTitle"
|
||||
buttonTitle="emptyListCta.buttonTitle"
|
||||
buttonIcon="emptyListCta.buttonIcon"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div ng-if="variables.length">
|
||||
<table class="filter-table filter-table--hover" aria-label="{{::selectors.table}}">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Variable</th>
|
||||
<th>Definition</th>
|
||||
<th colspan="5"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="variable in variables">
|
||||
<td style="width: 1%;">
|
||||
<span
|
||||
ng-click="edit(variable)"
|
||||
class="pointer template-variable"
|
||||
aria-label="{{::selectors.tableRowNameFields(variable.name)}}"
|
||||
>
|
||||
${{ variable.name }}
|
||||
</span>
|
||||
</td>
|
||||
<td
|
||||
style="max-width: 200px;"
|
||||
ng-click="edit(variable)"
|
||||
class="pointer max-width"
|
||||
aria-label="{{::selectors.tableRowDefinitionFields(variable.name)}}"
|
||||
>
|
||||
{{ variable.definition ? variable.definition : variable.query }}
|
||||
</td>
|
||||
<td style="width: 1%;">
|
||||
<icon
|
||||
ng-click="_.move(variables,$index,$index-1)"
|
||||
ng-hide="$first"
|
||||
name="'arrow-up'"
|
||||
aria-label="{{::selectors.tableRowArrowUpButtons(variable.name)}}"
|
||||
></icon>
|
||||
</td>
|
||||
<td style="width: 1%;">
|
||||
<icon
|
||||
ng-click="_.move(variables,$index,$index+1)"
|
||||
ng-hide="$last"
|
||||
name="'arrow-down'"
|
||||
aria-label="{{::selectors.tableRowArrowDownButtons(variable.name)}}"
|
||||
></icon>
|
||||
</td>
|
||||
<td style="width: 1%;">
|
||||
<a
|
||||
ng-click="duplicate(variable)"
|
||||
class="btn btn-inverse btn-small"
|
||||
aria-label="{{::selectors.tableRowDuplicateButtons(variable.name)}}"
|
||||
>
|
||||
Duplicate
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 1%;">
|
||||
<a
|
||||
ng-click="removeVariable(variable)"
|
||||
class="btn btn-danger btn-small"
|
||||
aria-label="{{::selectors.tableRowRemoveButtons(variable.name)}}"
|
||||
>
|
||||
<icon name="'times'" style="margin-bottom: 0;"></icon>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form ng-if="mode === 'edit' || mode === 'new'" name="ctrl.form" aria-label="Variable editor Form">
|
||||
<h5 class="section-heading">General</h5>
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form max-width-19">
|
||||
<span class="gf-form-label width-6">Name</span>
|
||||
<input
|
||||
type="text"
|
||||
class="gf-form-input"
|
||||
name="name"
|
||||
placeholder="name"
|
||||
ng-model="current.name"
|
||||
required
|
||||
ng-pattern="namePattern"
|
||||
aria-label="{{::selectors.generalNameInput}}"
|
||||
/>
|
||||
</div>
|
||||
<div class="gf-form max-width-19">
|
||||
<span class="gf-form-label width-6">
|
||||
Type
|
||||
<info-popover mode="right-normal">
|
||||
{{ variableTypes[current.type].description }}
|
||||
</info-popover>
|
||||
</span>
|
||||
<div class="gf-form-select-wrapper max-width-17">
|
||||
<select
|
||||
class="gf-form-input"
|
||||
ng-model="current.type"
|
||||
ng-options="k as v.name for (k, v) in variableTypes"
|
||||
ng-change="typeChanged()"
|
||||
aria-label="{{::selectors.generalTypeSelect}}"
|
||||
></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form" ng-show="ctrl.form.name.$error.pattern">
|
||||
<span class="gf-form-label gf-form-label--error"
|
||||
>Template names cannot begin with '__', that's reserved for Grafana's global variables</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form max-width-19">
|
||||
<span class="gf-form-label width-6">Label</span>
|
||||
<input
|
||||
type="text"
|
||||
class="gf-form-input"
|
||||
ng-model="current.label"
|
||||
placeholder="optional display name"
|
||||
aria-label="{{::selectors.generalLabelInput}}"
|
||||
/>
|
||||
</div>
|
||||
<div class="gf-form max-width-19">
|
||||
<span class="gf-form-label width-6">Hide</span>
|
||||
<div class="gf-form-select-wrapper max-width-15">
|
||||
<select
|
||||
class="gf-form-input"
|
||||
ng-model="current.hide"
|
||||
ng-options="f.value as f.text for f in hideOptions"
|
||||
aria-label="{{::selectors.generalHideSelect}}"
|
||||
></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="current.type === 'interval'" class="gf-form-group">
|
||||
<h5 class="section-heading">Interval Options</h5>
|
||||
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9">Values</span>
|
||||
<input
|
||||
type="text"
|
||||
class="gf-form-input"
|
||||
ng-model="current.query"
|
||||
placeholder="1m,10m,1h,6h,1d,7d"
|
||||
ng-model-onblur
|
||||
ng-change="runQuery()"
|
||||
required
|
||||
aria-label="Variable editor Form Interval Query field"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline">
|
||||
<gf-form-switch
|
||||
class="gf-form"
|
||||
label="Auto Option"
|
||||
label-class="width-9"
|
||||
checked="current.auto"
|
||||
on-change="runQuery()"
|
||||
aria-label="Variable editor Form Interval AutoOption switch"
|
||||
>
|
||||
</gf-form-switch>
|
||||
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9" ng-show="current.auto">
|
||||
Step count <tip>How many times should the current time range be divided to calculate the value</tip>
|
||||
</span>
|
||||
<div class="gf-form-select-wrapper max-width-10" ng-show="current.auto">
|
||||
<select
|
||||
class="gf-form-input"
|
||||
ng-model="current.auto_count"
|
||||
ng-options="f for f in [1,2,3,4,5,10,20,30,40,50,100,200,300,400,500]"
|
||||
ng-change="runQuery()"
|
||||
aria-label="Variable editor Form Interval AutoCount select"
|
||||
></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label" ng-show="current.auto">
|
||||
Min interval <tip>The calculated value will not go below this threshold</tip>
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
class="gf-form-input max-width-10"
|
||||
ng-show="current.auto"
|
||||
ng-model="current.auto_min"
|
||||
ng-change="runQuery()"
|
||||
placeholder="10s"
|
||||
aria-label="Variable editor Form Interval AutoMin field"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="current.type === 'custom'" class="gf-form-group">
|
||||
<h5 class="section-heading">Custom Options</h5>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-14">Values separated by comma</span>
|
||||
<input
|
||||
type="text"
|
||||
class="gf-form-input"
|
||||
ng-model="current.query"
|
||||
ng-blur="runQuery()"
|
||||
placeholder="1, 10, 20, myvalue, escaped\,value"
|
||||
required
|
||||
aria-label="Variable editor Form Custom Query field"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="current.type === 'constant'" class="gf-form-group">
|
||||
<h5 class="section-heading">Constant options</h5>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label">Value</span>
|
||||
<input
|
||||
type="text"
|
||||
class="gf-form-input"
|
||||
ng-model="current.query"
|
||||
ng-blur="runQuery()"
|
||||
placeholder="your metric prefix"
|
||||
aria-label="{{::selectors.constantOptionsQueryInput}}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="current.type === 'textbox'" class="gf-form-group">
|
||||
<h5 class="section-heading">Text options</h5>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label">Default value</span>
|
||||
<input
|
||||
type="text"
|
||||
class="gf-form-input"
|
||||
ng-model="current.query"
|
||||
ng-blur="runQuery()"
|
||||
placeholder="default value, if any"
|
||||
aria-label="Variable editor Form TextBox Query field"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="current.type === 'query'" class="gf-form-group">
|
||||
<h5 class="section-heading">Query Options</h5>
|
||||
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form max-width-21">
|
||||
<span class="gf-form-label width-10">Data source</span>
|
||||
<div class="gf-form-select-wrapper max-width-14">
|
||||
<select
|
||||
class="gf-form-input"
|
||||
ng-model="current.datasource"
|
||||
ng-options="f.value as f.name for f in datasources"
|
||||
ng-change="datasourceChanged()"
|
||||
required
|
||||
aria-label="{{::selectors.queryOptionsDataSourceSelect}}"
|
||||
>
|
||||
<option value="" ng-if="false"></option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form max-width-22">
|
||||
<span class="gf-form-label width-10">
|
||||
Refresh
|
||||
<info-popover mode="right-normal">
|
||||
When to update the values of this variable.
|
||||
</info-popover>
|
||||
</span>
|
||||
<div class="gf-form-select-wrapper width-15">
|
||||
<select
|
||||
class="gf-form-input"
|
||||
ng-model="current.refresh"
|
||||
ng-options="f.value as f.text for f in refreshOptions"
|
||||
aria-label="{{::selectors.queryOptionsRefreshSelect}}"
|
||||
></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<rebuild-on-change property="currentDatasource">
|
||||
<variable-query-editor-loader> </variable-query-editor-loader>
|
||||
</rebuild-on-change>
|
||||
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">
|
||||
Regex
|
||||
<info-popover mode="right-normal">
|
||||
Optional, if you want to extract part of a series name or metric node segment.
|
||||
</info-popover>
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
class="gf-form-input"
|
||||
ng-model="current.regex"
|
||||
placeholder="/.*-(.*)-.*/"
|
||||
ng-model-onblur
|
||||
ng-change="runQuery()"
|
||||
aria-label="{{::selectors.queryOptionsRegExInput}}"
|
||||
/>
|
||||
</div>
|
||||
<div class="gf-form max-width-21">
|
||||
<span class="gf-form-label width-10">
|
||||
Sort
|
||||
<info-popover mode="right-normal">
|
||||
How to sort the values of this variable.
|
||||
</info-popover>
|
||||
</span>
|
||||
<div class="gf-form-select-wrapper max-width-14">
|
||||
<select
|
||||
class="gf-form-input"
|
||||
ng-model="current.sort"
|
||||
ng-options="f.value as f.text for f in sortOptions"
|
||||
ng-change="runQuery()"
|
||||
aria-label="{{::selectors.queryOptionsSortSelect}}"
|
||||
></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="current.type === 'datasource'" class="gf-form-group">
|
||||
<h5 class="section-heading">Data source options</h5>
|
||||
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-12">Type</label>
|
||||
<div class="gf-form-select-wrapper max-width-18">
|
||||
<select
|
||||
class="gf-form-input"
|
||||
ng-model="current.query"
|
||||
ng-options="f.value as f.text for f in datasourceTypes"
|
||||
ng-change="runQuery()"
|
||||
aria-label="Variable editor Form DataSource Query field"
|
||||
></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-12">
|
||||
Instance name filter
|
||||
<info-popover mode="right-normal">
|
||||
Regex filter for which data source instances to choose from in the variable value dropdown. Leave empty for
|
||||
all.
|
||||
<br /><br />
|
||||
Example: <code>/^prod/</code>
|
||||
</info-popover>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="gf-form-input max-width-18"
|
||||
ng-model="current.regex"
|
||||
placeholder="/.*-(.*)-.*/"
|
||||
ng-model-onblur
|
||||
ng-change="runQuery()"
|
||||
aria-label="Variable editor Form DataSource RegEx field"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="current.type === 'adhoc'" class="gf-form-group">
|
||||
<h5 class="section-heading">Options</h5>
|
||||
<div class="gf-form max-width-21">
|
||||
<span class="gf-form-label width-8">Data source</span>
|
||||
<div class="gf-form-select-wrapper max-width-14">
|
||||
<select
|
||||
class="gf-form-input"
|
||||
ng-model="current.datasource"
|
||||
ng-options="f.value as f.name for f in datasources"
|
||||
required
|
||||
ng-change="validate()"
|
||||
aria-label="Variable editor Form AdHoc DataSource select"
|
||||
>
|
||||
<option value="" ng-if="false"></option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section gf-form-group" ng-show="variableTypes[current.type].supportsMulti">
|
||||
<h5 class="section-heading">Selection Options</h5>
|
||||
<div class="section">
|
||||
<gf-form-switch
|
||||
class="gf-form"
|
||||
label="Multi-value"
|
||||
label-class="width-10"
|
||||
tooltip="Enables multiple values to be selected at the same time"
|
||||
checked="current.multi"
|
||||
on-change="runQuery()"
|
||||
aria-label="{{::selectors.selectionOptionsMultiSwitch}}"
|
||||
>
|
||||
</gf-form-switch>
|
||||
<gf-form-switch
|
||||
class="gf-form"
|
||||
label="Include All option"
|
||||
label-class="width-10"
|
||||
checked="current.includeAll"
|
||||
on-change="runQuery()"
|
||||
aria-label="{{::selectors.selectionOptionsIncludeAllSwitch}}"
|
||||
>
|
||||
</gf-form-switch>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="current.includeAll">
|
||||
<span class="gf-form-label width-10">Custom all value</span>
|
||||
<input
|
||||
type="text"
|
||||
class="gf-form-input max-width-15"
|
||||
ng-model="current.allValue"
|
||||
placeholder="blank = auto"
|
||||
aria-label="{{::selectors.selectionOptionsCustomAllInput}}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group" ng-if="current.type === 'query'">
|
||||
<h5>Value groups/tags (Experimental feature)</h5>
|
||||
<gf-form-switch
|
||||
class="gf-form"
|
||||
label="Enabled"
|
||||
label-class="width-10"
|
||||
checked="current.useTags"
|
||||
on-change="runQuery()"
|
||||
aria-label="{{::selectors.valueGroupsTagsEnabledSwitch}}"
|
||||
>
|
||||
</gf-form-switch>
|
||||
<div class="gf-form last" ng-if="current.useTags">
|
||||
<span class="gf-form-label width-10">Tags query</span>
|
||||
<input
|
||||
type="text"
|
||||
class="gf-form-input"
|
||||
ng-model="current.tagsQuery"
|
||||
placeholder="metric name or tags query"
|
||||
ng-model-onblur
|
||||
aria-label="{{::selectors.valueGroupsTagsTagsQueryInput}}"
|
||||
/>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="current.useTags">
|
||||
<li class="gf-form-label width-10">Tag values query</li>
|
||||
<input
|
||||
type="text"
|
||||
class="gf-form-input"
|
||||
ng-model="current.tagValuesQuery"
|
||||
placeholder="apps.$tag.*"
|
||||
ng-model-onblur
|
||||
aria-label="{{::selectors.valueGroupsTagsTagsValuesQueryInput}}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group" ng-show="current.options.length">
|
||||
<h5>Preview of values</h5>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form" ng-repeat="option in current.options | limitTo: optionsLimit">
|
||||
<span class="gf-form-label" aria-label="{{::selectors.previewOfValuesOption}}">{{ option.text }}</span>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="current.options.length > optionsLimit">
|
||||
<a
|
||||
class="gf-form-label btn-secondary"
|
||||
ng-click="showMoreOptions()"
|
||||
aria-label="Variable editor Preview of Values Show More link"
|
||||
>
|
||||
Show more
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info gf-form-group" ng-if="infoText" aria-label="Variable editor Form Alert">
|
||||
{{ infoText }}
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row p-y-0">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary"
|
||||
ng-show="mode === 'edit'"
|
||||
ng-click="update();"
|
||||
aria-label="{{::selectors.updateButton}}"
|
||||
>
|
||||
Update
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary"
|
||||
ng-show="mode === 'new'"
|
||||
ng-click="add();"
|
||||
aria-label="{{::selectors.addButton}}"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -1,254 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import {
|
||||
assignModelProperties,
|
||||
QueryVariableModel,
|
||||
VariableActions,
|
||||
VariableHide,
|
||||
VariableOption,
|
||||
VariableRefresh,
|
||||
VariableSort,
|
||||
VariableTag,
|
||||
variableTypes,
|
||||
} from './types';
|
||||
import { VariableType, DataSourceApi, stringToJsRegex } from '@grafana/data';
|
||||
import DatasourceSrv from '../plugins/datasource_srv';
|
||||
import { TemplateSrv } from './template_srv';
|
||||
import { VariableSrv } from './variable_srv';
|
||||
import { TimeSrv } from '../dashboard/services/TimeSrv';
|
||||
import { containsVariable } from './utils';
|
||||
|
||||
function getNoneOption(): VariableOption {
|
||||
return { text: 'None', value: '', isNone: true, selected: false };
|
||||
}
|
||||
|
||||
export class QueryVariable implements QueryVariableModel, VariableActions {
|
||||
type: VariableType;
|
||||
name: string;
|
||||
label: string | null;
|
||||
hide: VariableHide;
|
||||
skipUrlSync: boolean;
|
||||
datasource: string | null;
|
||||
query: string;
|
||||
regex: string;
|
||||
sort: VariableSort;
|
||||
options: VariableOption[];
|
||||
current: VariableOption;
|
||||
refresh: VariableRefresh;
|
||||
multi: boolean;
|
||||
includeAll: boolean;
|
||||
useTags: boolean;
|
||||
tagsQuery: string;
|
||||
tagValuesQuery: string;
|
||||
tags: VariableTag[];
|
||||
definition: string;
|
||||
allValue: string;
|
||||
index: number;
|
||||
|
||||
defaults: QueryVariableModel = {
|
||||
type: 'query',
|
||||
name: '',
|
||||
label: null,
|
||||
hide: VariableHide.dontHide,
|
||||
skipUrlSync: false,
|
||||
datasource: null,
|
||||
query: '',
|
||||
regex: '',
|
||||
sort: VariableSort.disabled,
|
||||
refresh: VariableRefresh.never,
|
||||
multi: false,
|
||||
includeAll: false,
|
||||
allValue: null,
|
||||
options: [],
|
||||
current: {} as VariableOption,
|
||||
tags: [],
|
||||
useTags: false,
|
||||
tagsQuery: '',
|
||||
tagValuesQuery: '',
|
||||
definition: '',
|
||||
index: -1,
|
||||
};
|
||||
|
||||
/** @ngInject */
|
||||
constructor(
|
||||
private model: any,
|
||||
private datasourceSrv: DatasourceSrv,
|
||||
private templateSrv: TemplateSrv,
|
||||
private variableSrv: VariableSrv,
|
||||
private timeSrv: TimeSrv
|
||||
) {
|
||||
// copy model properties to this instance
|
||||
assignModelProperties(this, model, this.defaults);
|
||||
this.updateOptionsFromMetricFindQuery.bind(this);
|
||||
}
|
||||
|
||||
getSaveModel() {
|
||||
// copy back model properties to model
|
||||
assignModelProperties(this.model, this, this.defaults);
|
||||
|
||||
// remove options
|
||||
if (this.refresh !== 0) {
|
||||
this.model.options = [];
|
||||
}
|
||||
|
||||
return this.model;
|
||||
}
|
||||
|
||||
setValue(option: any) {
|
||||
return this.variableSrv.setOptionAsCurrent(this, option);
|
||||
}
|
||||
|
||||
setValueFromUrl(urlValue: any) {
|
||||
return this.variableSrv.setOptionFromUrl(this, urlValue);
|
||||
}
|
||||
|
||||
getValueForUrl() {
|
||||
if (this.current.text === 'All') {
|
||||
return 'All';
|
||||
}
|
||||
return this.current.value;
|
||||
}
|
||||
|
||||
updateOptions(searchFilter?: string) {
|
||||
return this.datasourceSrv
|
||||
.get(this.datasource ?? '')
|
||||
.then((ds: DataSourceApi) => this.updateOptionsFromMetricFindQuery(ds, searchFilter))
|
||||
.then(this.updateTags.bind(this))
|
||||
.then(this.variableSrv.validateVariableSelectionState.bind(this.variableSrv, this));
|
||||
}
|
||||
|
||||
updateTags(datasource: any) {
|
||||
if (this.useTags) {
|
||||
return this.metricFindQuery(datasource, this.tagsQuery).then((results: any[]) => {
|
||||
this.tags = [];
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
this.tags.push(results[i].text);
|
||||
}
|
||||
return datasource;
|
||||
});
|
||||
} else {
|
||||
delete this.tags;
|
||||
}
|
||||
|
||||
return datasource;
|
||||
}
|
||||
|
||||
getValuesForTag(tagKey: string) {
|
||||
return this.datasourceSrv.get(this.datasource ?? '').then((datasource: DataSourceApi) => {
|
||||
const query = this.tagValuesQuery.replace('$tag', tagKey);
|
||||
return this.metricFindQuery(datasource, query).then((results: any) => {
|
||||
return _.map(results, value => {
|
||||
return value.text;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
updateOptionsFromMetricFindQuery(datasource: any, searchFilter?: string) {
|
||||
return this.metricFindQuery(datasource, this.query, searchFilter).then((results: any) => {
|
||||
this.options = this.metricNamesToVariableValues(results);
|
||||
if (this.includeAll) {
|
||||
this.addAllOption();
|
||||
}
|
||||
if (!this.options.length) {
|
||||
this.options.push(getNoneOption());
|
||||
}
|
||||
return datasource;
|
||||
});
|
||||
}
|
||||
|
||||
metricFindQuery(datasource: any, query: string, searchFilter?: string) {
|
||||
const options: any = { range: undefined, variable: this, searchFilter };
|
||||
|
||||
if (this.refresh === 2) {
|
||||
options.range = this.timeSrv.timeRange();
|
||||
}
|
||||
|
||||
return datasource.metricFindQuery(query, options);
|
||||
}
|
||||
|
||||
addAllOption() {
|
||||
this.options.unshift({ text: 'All', value: '$__all', selected: false });
|
||||
}
|
||||
|
||||
metricNamesToVariableValues(metricNames: any[]) {
|
||||
let regex, options, i, matches;
|
||||
options = [];
|
||||
|
||||
if (this.regex) {
|
||||
regex = stringToJsRegex(this.templateSrv.replace(this.regex, {}, 'regex'));
|
||||
}
|
||||
for (i = 0; i < metricNames.length; i++) {
|
||||
const item = metricNames[i];
|
||||
let text = item.text === undefined || item.text === null ? item.value : item.text;
|
||||
|
||||
let value = item.value === undefined || item.value === null ? item.text : item.value;
|
||||
|
||||
if (_.isNumber(value)) {
|
||||
value = value.toString();
|
||||
}
|
||||
|
||||
if (_.isNumber(text)) {
|
||||
text = text.toString();
|
||||
}
|
||||
|
||||
if (regex) {
|
||||
matches = regex.exec(value);
|
||||
if (!matches) {
|
||||
continue;
|
||||
}
|
||||
if (matches.length > 1) {
|
||||
value = matches[1];
|
||||
text = matches[1];
|
||||
}
|
||||
}
|
||||
|
||||
options.push({ text: text, value: value });
|
||||
}
|
||||
|
||||
options = _.uniqBy(options, 'value');
|
||||
return this.sortVariableValues(options, this.sort);
|
||||
}
|
||||
|
||||
sortVariableValues(options: any[], sortOrder: number) {
|
||||
if (sortOrder === 0) {
|
||||
return options;
|
||||
}
|
||||
|
||||
const sortType = Math.ceil(sortOrder / 2);
|
||||
const reverseSort = sortOrder % 2 === 0;
|
||||
|
||||
if (sortType === 1) {
|
||||
options = _.sortBy(options, 'text');
|
||||
} else if (sortType === 2) {
|
||||
options = _.sortBy(options, opt => {
|
||||
const matches = opt.text.match(/.*?(\d+).*/);
|
||||
if (!matches || matches.length < 2) {
|
||||
return -1;
|
||||
} else {
|
||||
return parseInt(matches[1], 10);
|
||||
}
|
||||
});
|
||||
} else if (sortType === 3) {
|
||||
options = _.sortBy(options, opt => {
|
||||
return _.toLower(opt.text);
|
||||
});
|
||||
}
|
||||
|
||||
if (reverseSort) {
|
||||
options = options.reverse();
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
dependsOn(variable: any) {
|
||||
return containsVariable(this.query, this.datasource, this.regex, variable.name);
|
||||
}
|
||||
}
|
||||
// @ts-ignore
|
||||
variableTypes['query'] = {
|
||||
name: 'Query',
|
||||
ctor: QueryVariable,
|
||||
description: 'Variable values are fetched from a datasource query',
|
||||
supportsMulti: true,
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
import { AdhocVariable } from '../adhoc_variable';
|
||||
|
||||
describe('AdhocVariable', () => {
|
||||
describe('when serializing to url', () => {
|
||||
it('should set return key value and op separated by pipe', () => {
|
||||
const variable = new AdhocVariable({
|
||||
filters: [
|
||||
{ key: 'key1', operator: '=', value: 'value1' },
|
||||
{ key: 'key2', operator: '!=', value: 'value2' },
|
||||
{ key: 'key3', operator: '=', value: 'value3a|value3b|value3c' },
|
||||
],
|
||||
});
|
||||
const urlValue = variable.getValueForUrl();
|
||||
expect(urlValue).toMatchObject(['key1|=|value1', 'key2|!=|value2', 'key3|=|value3a__gfp__value3b__gfp__value3c']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when deserializing from url', () => {
|
||||
it('should restore filters', () => {
|
||||
const variable = new AdhocVariable({});
|
||||
variable.setValueFromUrl(['key1|=|value1', 'key2|!=|value2', 'key3|=|value3a__gfp__value3b__gfp__value3c']);
|
||||
|
||||
expect(variable.filters[0].key).toBe('key1');
|
||||
expect(variable.filters[0].operator).toBe('=');
|
||||
expect(variable.filters[0].value).toBe('value1');
|
||||
|
||||
expect(variable.filters[1].key).toBe('key2');
|
||||
expect(variable.filters[1].operator).toBe('!=');
|
||||
expect(variable.filters[1].value).toBe('value2');
|
||||
|
||||
expect(variable.filters[2].key).toBe('key3');
|
||||
expect(variable.filters[2].operator).toBe('=');
|
||||
expect(variable.filters[2].value).toBe('value3a|value3b|value3c');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,42 +0,0 @@
|
||||
import { VariableEditorCtrl } from '../editor_ctrl';
|
||||
import { TemplateSrv } from '../template_srv';
|
||||
import { AppEvents } from '@grafana/data';
|
||||
|
||||
let mockEmit: any;
|
||||
jest.mock('app/core/app_events', () => {
|
||||
mockEmit = jest.fn();
|
||||
return {
|
||||
emit: mockEmit,
|
||||
};
|
||||
});
|
||||
|
||||
describe('VariableEditorCtrl', () => {
|
||||
const scope = {
|
||||
runQuery: () => {
|
||||
return Promise.resolve({});
|
||||
},
|
||||
};
|
||||
|
||||
describe('When running a variable query and the data source returns an error', () => {
|
||||
beforeEach(() => {
|
||||
const variableSrv: any = {
|
||||
updateOptions: () => {
|
||||
return Promise.reject({
|
||||
data: { message: 'error' },
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
return new VariableEditorCtrl(scope, {} as any, variableSrv, {} as TemplateSrv);
|
||||
});
|
||||
|
||||
it('should emit an error', () => {
|
||||
return scope.runQuery().then(res => {
|
||||
expect(mockEmit).toBeCalled();
|
||||
expect(mockEmit.mock.calls[0][0]).toBe(AppEvents.alertError);
|
||||
expect(mockEmit.mock.calls[0][1][0]).toBe('Templating');
|
||||
expect(mockEmit.mock.calls[0][1][1]).toBe('Template variables could not be initialized: error');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,133 +0,0 @@
|
||||
import { QueryVariable } from '../query_variable';
|
||||
import DatasourceSrv from '../../plugins/datasource_srv';
|
||||
import { TemplateSrv } from '../template_srv';
|
||||
import { VariableSrv } from '../variable_srv';
|
||||
import { TimeSrv } from '../../dashboard/services/TimeSrv';
|
||||
|
||||
describe('QueryVariable', () => {
|
||||
describe('when creating from model', () => {
|
||||
it('should set defaults', () => {
|
||||
const variable = new QueryVariable(
|
||||
{},
|
||||
(null as unknown) as DatasourceSrv,
|
||||
(null as unknown) as TemplateSrv,
|
||||
(null as unknown) as VariableSrv,
|
||||
(null as unknown) as TimeSrv
|
||||
);
|
||||
expect(variable.datasource).toBe(null);
|
||||
expect(variable.refresh).toBe(0);
|
||||
expect(variable.sort).toBe(0);
|
||||
expect(variable.name).toBe('');
|
||||
expect(variable.hide).toBe(0);
|
||||
expect(variable.options.length).toBe(0);
|
||||
expect(variable.multi).toBe(false);
|
||||
expect(variable.includeAll).toBe(false);
|
||||
});
|
||||
|
||||
it('get model should copy changes back to model', () => {
|
||||
const variable = new QueryVariable(
|
||||
{},
|
||||
(null as unknown) as DatasourceSrv,
|
||||
(null as unknown) as TemplateSrv,
|
||||
(null as unknown) as VariableSrv,
|
||||
(null as unknown) as TimeSrv
|
||||
);
|
||||
variable.options = [{ text: 'test', value: '', selected: false }];
|
||||
variable.datasource = 'google';
|
||||
variable.regex = 'asd';
|
||||
variable.sort = 50;
|
||||
|
||||
const model = variable.getSaveModel();
|
||||
expect(model.options.length).toBe(1);
|
||||
expect(model.options[0].text).toBe('test');
|
||||
expect(model.datasource).toBe('google');
|
||||
expect(model.regex).toBe('asd');
|
||||
expect(model.sort).toBe(50);
|
||||
});
|
||||
|
||||
it('if refresh != 0 then remove options in presisted mode', () => {
|
||||
const variable = new QueryVariable(
|
||||
{},
|
||||
(null as unknown) as DatasourceSrv,
|
||||
(null as unknown) as TemplateSrv,
|
||||
(null as unknown) as VariableSrv,
|
||||
(null as unknown) as TimeSrv
|
||||
);
|
||||
variable.options = [{ text: 'test', value: '', selected: false }];
|
||||
variable.refresh = 1;
|
||||
|
||||
const model = variable.getSaveModel();
|
||||
expect(model.options.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('can convert and sort metric names', () => {
|
||||
const variable = new QueryVariable(
|
||||
{},
|
||||
(null as unknown) as DatasourceSrv,
|
||||
(null as unknown) as TemplateSrv,
|
||||
(null as unknown) as VariableSrv,
|
||||
(null as unknown) as TimeSrv
|
||||
);
|
||||
let input: any;
|
||||
|
||||
beforeEach(() => {
|
||||
input = [
|
||||
{ text: '0', value: '0' },
|
||||
{ text: '1', value: '1' },
|
||||
{ text: null, value: 3 },
|
||||
{ text: undefined, value: 4 },
|
||||
{ text: '5', value: null },
|
||||
{ text: '6', value: undefined },
|
||||
{ text: null, value: '7' },
|
||||
{ text: undefined, value: '8' },
|
||||
{ text: 9, value: null },
|
||||
{ text: 10, value: undefined },
|
||||
{ text: '', value: undefined },
|
||||
{ text: undefined, value: '' },
|
||||
];
|
||||
});
|
||||
|
||||
describe('can sort a mixed array of metric variables in numeric order', () => {
|
||||
let result: any;
|
||||
|
||||
beforeEach(() => {
|
||||
variable.sort = 3; // Numerical (asc)
|
||||
result = variable.metricNamesToVariableValues(input);
|
||||
});
|
||||
|
||||
it('should return in same order', () => {
|
||||
let i = 0;
|
||||
expect(result.length).toBe(11);
|
||||
expect(result[i++].text).toBe('');
|
||||
expect(result[i++].text).toBe('0');
|
||||
expect(result[i++].text).toBe('1');
|
||||
expect(result[i++].text).toBe('3');
|
||||
expect(result[i++].text).toBe('4');
|
||||
expect(result[i++].text).toBe('5');
|
||||
expect(result[i++].text).toBe('6');
|
||||
});
|
||||
});
|
||||
|
||||
describe('can sort a mixed array of metric variables in alphabetical order', () => {
|
||||
let result: any;
|
||||
|
||||
beforeEach(() => {
|
||||
variable.sort = 5; // Alphabetical CI (asc)
|
||||
result = variable.metricNamesToVariableValues(input);
|
||||
});
|
||||
|
||||
it('should return in same order', () => {
|
||||
let i = 0;
|
||||
expect(result.length).toBe(11);
|
||||
expect(result[i++].text).toBe('');
|
||||
expect(result[i++].text).toBe('0');
|
||||
expect(result[i++].text).toBe('1');
|
||||
expect(result[i++].text).toBe('10');
|
||||
expect(result[i++].text).toBe('3');
|
||||
expect(result[i++].text).toBe('4');
|
||||
expect(result[i++].text).toBe('5');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,609 +0,0 @@
|
||||
import { TemplateSrv } from '../template_srv';
|
||||
import { convertToStoreState } from 'test/helpers/convertToStoreState';
|
||||
import { getTemplateSrvDependencies } from '../../../../test/helpers/getTemplateSrvDependencies';
|
||||
import { variableAdapters } from '../../variables/adapters';
|
||||
import { createQueryVariableAdapter } from '../../variables/query/adapter';
|
||||
|
||||
describe('templateSrv', () => {
|
||||
let _templateSrv: any;
|
||||
|
||||
function initTemplateSrv(variables: any[]) {
|
||||
const state = convertToStoreState(variables);
|
||||
|
||||
_templateSrv = new TemplateSrv(getTemplateSrvDependencies(state));
|
||||
_templateSrv.init(variables);
|
||||
}
|
||||
|
||||
describe('init', () => {
|
||||
beforeEach(() => {
|
||||
initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'oogle' } }]);
|
||||
});
|
||||
|
||||
it('should initialize template data', () => {
|
||||
const target = _templateSrv.replace('this.[[test]].filters');
|
||||
expect(target).toBe('this.oogle.filters');
|
||||
});
|
||||
});
|
||||
|
||||
describe('replace can pass scoped vars', () => {
|
||||
beforeEach(() => {
|
||||
initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'oogle' } }]);
|
||||
});
|
||||
|
||||
it('scoped vars should support objects', () => {
|
||||
const target = _templateSrv.replace('${series.name} ${series.nested.field}', {
|
||||
series: { value: { name: 'Server1', nested: { field: 'nested' } } },
|
||||
});
|
||||
expect(target).toBe('Server1 nested');
|
||||
});
|
||||
|
||||
it('built in vars should support objects', () => {
|
||||
_templateSrv.setGlobalVariable('__dashboard', {
|
||||
value: { name: 'hello' },
|
||||
});
|
||||
const target = _templateSrv.replace('${__dashboard.name}');
|
||||
expect(target).toBe('hello');
|
||||
});
|
||||
|
||||
it('scoped vars should support objects with propert names with dot', () => {
|
||||
const target = _templateSrv.replace('${series.name} ${series.nested["field.with.dot"]}', {
|
||||
series: { value: { name: 'Server1', nested: { 'field.with.dot': 'nested' } } },
|
||||
});
|
||||
expect(target).toBe('Server1 nested');
|
||||
});
|
||||
|
||||
it('scoped vars should support arrays of objects', () => {
|
||||
const target = _templateSrv.replace('${series.rows[0].name} ${series.rows[1].name}', {
|
||||
series: { value: { rows: [{ name: 'first' }, { name: 'second' }] } },
|
||||
});
|
||||
expect(target).toBe('first second');
|
||||
});
|
||||
|
||||
it('should replace $test with scoped value', () => {
|
||||
const target = _templateSrv.replace('this.$test.filters', {
|
||||
test: { value: 'mupp', text: 'asd' },
|
||||
});
|
||||
expect(target).toBe('this.mupp.filters');
|
||||
});
|
||||
|
||||
it('should replace ${test} with scoped value', () => {
|
||||
const target = _templateSrv.replace('this.${test}.filters', {
|
||||
test: { value: 'mupp', text: 'asd' },
|
||||
});
|
||||
expect(target).toBe('this.mupp.filters');
|
||||
});
|
||||
|
||||
it('should replace ${test:glob} with scoped value', () => {
|
||||
const target = _templateSrv.replace('this.${test:glob}.filters', {
|
||||
test: { value: 'mupp', text: 'asd' },
|
||||
});
|
||||
expect(target).toBe('this.mupp.filters');
|
||||
});
|
||||
|
||||
it('should replace $test with scoped text', () => {
|
||||
const target = _templateSrv.replaceWithText('this.$test.filters', {
|
||||
test: { value: 'mupp', text: 'asd' },
|
||||
});
|
||||
expect(target).toBe('this.asd.filters');
|
||||
});
|
||||
|
||||
it('should replace ${test} with scoped text', () => {
|
||||
const target = _templateSrv.replaceWithText('this.${test}.filters', {
|
||||
test: { value: 'mupp', text: 'asd' },
|
||||
});
|
||||
expect(target).toBe('this.asd.filters');
|
||||
});
|
||||
|
||||
it('should replace ${test:glob} with scoped text', () => {
|
||||
const target = _templateSrv.replaceWithText('this.${test:glob}.filters', {
|
||||
test: { value: 'mupp', text: 'asd' },
|
||||
});
|
||||
expect(target).toBe('this.asd.filters');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAdhocFilters', () => {
|
||||
beforeEach(() => {
|
||||
initTemplateSrv([
|
||||
{
|
||||
type: 'datasource',
|
||||
name: 'ds',
|
||||
current: { value: 'logstash', text: 'logstash' },
|
||||
},
|
||||
{ type: 'adhoc', name: 'test', datasource: 'oogle', filters: [1] },
|
||||
{ type: 'adhoc', name: 'test2', datasource: '$ds', filters: [2] },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return filters if datasourceName match', () => {
|
||||
const filters = _templateSrv.getAdhocFilters('oogle');
|
||||
expect(filters).toMatchObject([1]);
|
||||
});
|
||||
|
||||
it('should return empty array if datasourceName does not match', () => {
|
||||
const filters = _templateSrv.getAdhocFilters('oogleasdasd');
|
||||
expect(filters).toMatchObject([]);
|
||||
});
|
||||
|
||||
it('should return filters when datasourceName match via data source variable', () => {
|
||||
const filters = _templateSrv.getAdhocFilters('logstash');
|
||||
expect(filters).toMatchObject([2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('replace can pass multi / all format', () => {
|
||||
beforeEach(() => {
|
||||
initTemplateSrv([
|
||||
{
|
||||
type: 'query',
|
||||
name: 'test',
|
||||
current: { value: ['value1', 'value2'] },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should replace $test with globbed value', () => {
|
||||
const target = _templateSrv.replace('this.$test.filters', {}, 'glob');
|
||||
expect(target).toBe('this.{value1,value2}.filters');
|
||||
});
|
||||
|
||||
describe('when the globbed variable only has one value', () => {
|
||||
beforeEach(() => {
|
||||
initTemplateSrv([
|
||||
{
|
||||
type: 'query',
|
||||
name: 'test',
|
||||
current: { value: ['value1'] },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not glob the value', () => {
|
||||
const target = _templateSrv.replace('this.$test.filters', {}, 'glob');
|
||||
expect(target).toBe('this.value1.filters');
|
||||
});
|
||||
});
|
||||
|
||||
it('should replace ${test} with globbed value', () => {
|
||||
const target = _templateSrv.replace('this.${test}.filters', {}, 'glob');
|
||||
expect(target).toBe('this.{value1,value2}.filters');
|
||||
});
|
||||
|
||||
it('should replace ${test:glob} with globbed value', () => {
|
||||
const target = _templateSrv.replace('this.${test:glob}.filters', {});
|
||||
expect(target).toBe('this.{value1,value2}.filters');
|
||||
});
|
||||
|
||||
it('should replace $test with piped value', () => {
|
||||
const target = _templateSrv.replace('this=$test', {}, 'pipe');
|
||||
expect(target).toBe('this=value1|value2');
|
||||
});
|
||||
|
||||
it('should replace ${test} with piped value', () => {
|
||||
const target = _templateSrv.replace('this=${test}', {}, 'pipe');
|
||||
expect(target).toBe('this=value1|value2');
|
||||
});
|
||||
|
||||
it('should replace ${test:pipe} with piped value', () => {
|
||||
const target = _templateSrv.replace('this=${test:pipe}', {});
|
||||
expect(target).toBe('this=value1|value2');
|
||||
});
|
||||
|
||||
it('should replace ${test:pipe} with piped value and $test with globbed value', () => {
|
||||
const target = _templateSrv.replace('${test:pipe},$test', {}, 'glob');
|
||||
expect(target).toBe('value1|value2,{value1,value2}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('variable with all option', () => {
|
||||
beforeEach(() => {
|
||||
initTemplateSrv([
|
||||
{
|
||||
type: 'query',
|
||||
name: 'test',
|
||||
current: { value: '$__all' },
|
||||
options: [{ value: '$__all' }, { value: 'value1' }, { value: 'value2' }],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should replace $test with formatted all value', () => {
|
||||
const target = _templateSrv.replace('this.$test.filters', {}, 'glob');
|
||||
expect(target).toBe('this.{value1,value2}.filters');
|
||||
});
|
||||
|
||||
it('should replace ${test} with formatted all value', () => {
|
||||
const target = _templateSrv.replace('this.${test}.filters', {}, 'glob');
|
||||
expect(target).toBe('this.{value1,value2}.filters');
|
||||
});
|
||||
|
||||
it('should replace ${test:glob} with formatted all value', () => {
|
||||
const target = _templateSrv.replace('this.${test:glob}.filters', {});
|
||||
expect(target).toBe('this.{value1,value2}.filters');
|
||||
});
|
||||
|
||||
it('should replace ${test:pipe} with piped value and $test with globbed value', () => {
|
||||
const target = _templateSrv.replace('${test:pipe},$test', {}, 'glob');
|
||||
expect(target).toBe('value1|value2,{value1,value2}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('variable with all option and custom value', () => {
|
||||
beforeEach(() => {
|
||||
initTemplateSrv([
|
||||
{
|
||||
type: 'query',
|
||||
name: 'test',
|
||||
current: { value: '$__all' },
|
||||
allValue: '*',
|
||||
options: [{ value: 'value1' }, { value: 'value2' }],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should replace $test with formatted all value', () => {
|
||||
const target = _templateSrv.replace('this.$test.filters', {}, 'glob');
|
||||
expect(target).toBe('this.*.filters');
|
||||
});
|
||||
|
||||
it('should replace ${test} with formatted all value', () => {
|
||||
const target = _templateSrv.replace('this.${test}.filters', {}, 'glob');
|
||||
expect(target).toBe('this.*.filters');
|
||||
});
|
||||
|
||||
it('should replace ${test:glob} with formatted all value', () => {
|
||||
const target = _templateSrv.replace('this.${test:glob}.filters', {});
|
||||
expect(target).toBe('this.*.filters');
|
||||
});
|
||||
|
||||
it('should not escape custom all value', () => {
|
||||
const target = _templateSrv.replace('this.$test', {}, 'regex');
|
||||
expect(target).toBe('this.*');
|
||||
});
|
||||
});
|
||||
|
||||
describe('lucene format', () => {
|
||||
it('should properly escape $test with lucene escape sequences', () => {
|
||||
initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'value/4' } }]);
|
||||
const target = _templateSrv.replace('this:$test', {}, 'lucene');
|
||||
expect(target).toBe('this:value\\/4');
|
||||
});
|
||||
|
||||
it('should properly escape ${test} with lucene escape sequences', () => {
|
||||
initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'value/4' } }]);
|
||||
const target = _templateSrv.replace('this:${test}', {}, 'lucene');
|
||||
expect(target).toBe('this:value\\/4');
|
||||
});
|
||||
|
||||
it('should properly escape ${test:lucene} with lucene escape sequences', () => {
|
||||
initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'value/4' } }]);
|
||||
const target = _templateSrv.replace('this:${test:lucene}', {});
|
||||
expect(target).toBe('this:value\\/4');
|
||||
});
|
||||
});
|
||||
|
||||
describe('html format', () => {
|
||||
it('should encode values html escape sequences', () => {
|
||||
initTemplateSrv([{ type: 'query', name: 'test', current: { value: '<script>alert(asd)</script>' } }]);
|
||||
const target = _templateSrv.replace('$test', {}, 'html');
|
||||
expect(target).toBe('<script>alert(asd)</script>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('format variable to string values', () => {
|
||||
it('single value should return value', () => {
|
||||
const result = _templateSrv.formatValue('test');
|
||||
expect(result).toBe('test');
|
||||
});
|
||||
|
||||
it('multi value and glob format should render glob string', () => {
|
||||
const result = _templateSrv.formatValue(['test', 'test2'], 'glob');
|
||||
expect(result).toBe('{test,test2}');
|
||||
});
|
||||
|
||||
it('multi value and lucene should render as lucene expr', () => {
|
||||
const result = _templateSrv.formatValue(['test', 'test2'], 'lucene');
|
||||
expect(result).toBe('("test" OR "test2")');
|
||||
});
|
||||
|
||||
it('multi value and regex format should render regex string', () => {
|
||||
const result = _templateSrv.formatValue(['test.', 'test2'], 'regex');
|
||||
expect(result).toBe('(test\\.|test2)');
|
||||
});
|
||||
|
||||
it('multi value and pipe should render pipe string', () => {
|
||||
const result = _templateSrv.formatValue(['test', 'test2'], 'pipe');
|
||||
expect(result).toBe('test|test2');
|
||||
});
|
||||
|
||||
it('multi value and distributed should render distributed string', () => {
|
||||
const result = _templateSrv.formatValue(['test', 'test2'], 'distributed', {
|
||||
name: 'build',
|
||||
});
|
||||
expect(result).toBe('test,build=test2');
|
||||
});
|
||||
|
||||
it('multi value and distributed should render when not string', () => {
|
||||
const result = _templateSrv.formatValue(['test'], 'distributed', {
|
||||
name: 'build',
|
||||
});
|
||||
expect(result).toBe('test');
|
||||
});
|
||||
|
||||
it('multi value and csv format should render csv string', () => {
|
||||
const result = _templateSrv.formatValue(['test', 'test2'], 'csv');
|
||||
expect(result).toBe('test,test2');
|
||||
});
|
||||
|
||||
it('multi value and percentencode format should render percent-encoded string', () => {
|
||||
const result = _templateSrv.formatValue(['foo()bar BAZ', 'test2'], 'percentencode');
|
||||
expect(result).toBe('%7Bfoo%28%29bar%20BAZ%2Ctest2%7D');
|
||||
});
|
||||
|
||||
it('slash should be properly escaped in regex format', () => {
|
||||
const result = _templateSrv.formatValue('Gi3/14', 'regex');
|
||||
expect(result).toBe('Gi3\\/14');
|
||||
});
|
||||
|
||||
it('single value and singlequote format should render string with value enclosed in single quotes', () => {
|
||||
const result = _templateSrv.formatValue('test', 'singlequote');
|
||||
expect(result).toBe("'test'");
|
||||
});
|
||||
|
||||
it('multi value and singlequote format should render string with values enclosed in single quotes', () => {
|
||||
const result = _templateSrv.formatValue(['test', "test'2"], 'singlequote');
|
||||
expect(result).toBe("'test','test\\'2'");
|
||||
});
|
||||
|
||||
it('single value and doublequote format should render string with value enclosed in double quotes', () => {
|
||||
const result = _templateSrv.formatValue('test', 'doublequote');
|
||||
expect(result).toBe('"test"');
|
||||
});
|
||||
|
||||
it('multi value and doublequote format should render string with values enclosed in double quotes', () => {
|
||||
const result = _templateSrv.formatValue(['test', 'test"2'], 'doublequote');
|
||||
expect(result).toBe('"test","test\\"2"');
|
||||
});
|
||||
|
||||
it('single value and sqlstring format should render string with value enclosed in single quotes', () => {
|
||||
const result = _templateSrv.formatValue("test'value", 'sqlstring');
|
||||
expect(result).toBe(`'test''value'`);
|
||||
});
|
||||
|
||||
it('multi value and sqlstring format should render string with values enclosed in single quotes', () => {
|
||||
const result = _templateSrv.formatValue(['test', "test'value2"], 'sqlstring');
|
||||
expect(result).toBe(`'test','test''value2'`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('can check if variable exists', () => {
|
||||
beforeEach(() => {
|
||||
initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'oogle' } }]);
|
||||
});
|
||||
|
||||
it('should return true if $test exists', () => {
|
||||
const result = _templateSrv.variableExists('$test');
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if $test exists in string', () => {
|
||||
const result = _templateSrv.variableExists('something $test something');
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if [[test]] exists in string', () => {
|
||||
const result = _templateSrv.variableExists('something [[test]] something');
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if [[test:csv]] exists in string', () => {
|
||||
const result = _templateSrv.variableExists('something [[test:csv]] something');
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if ${test} exists in string', () => {
|
||||
const result = _templateSrv.variableExists('something ${test} something');
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if ${test:raw} exists in string', () => {
|
||||
const result = _templateSrv.variableExists('something ${test:raw} something');
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return null if there are no variables in string', () => {
|
||||
const result = _templateSrv.variableExists('string without variables');
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('can highlight variables in string', () => {
|
||||
beforeEach(() => {
|
||||
initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'oogle' } }]);
|
||||
});
|
||||
|
||||
it('should insert html', () => {
|
||||
const result = _templateSrv.highlightVariablesAsHtml('$test');
|
||||
expect(result).toBe('<span class="template-variable">$test</span>');
|
||||
});
|
||||
|
||||
it('should insert html anywhere in string', () => {
|
||||
const result = _templateSrv.highlightVariablesAsHtml('this $test ok');
|
||||
expect(result).toBe('this <span class="template-variable">$test</span> ok');
|
||||
});
|
||||
|
||||
it('should ignore if variables does not exist', () => {
|
||||
const result = _templateSrv.highlightVariablesAsHtml('this $google ok');
|
||||
expect(result).toBe('this $google ok');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateIndex with simple value', () => {
|
||||
beforeEach(() => {
|
||||
initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'muuuu' } }]);
|
||||
});
|
||||
|
||||
it('should set current value and update template data', () => {
|
||||
const target = _templateSrv.replace('this.[[test]].filters');
|
||||
expect(target).toBe('this.muuuu.filters');
|
||||
});
|
||||
});
|
||||
|
||||
describe('fillVariableValuesForUrl with multi value', () => {
|
||||
beforeAll(() => {
|
||||
variableAdapters.register(createQueryVariableAdapter());
|
||||
});
|
||||
beforeEach(() => {
|
||||
initTemplateSrv([
|
||||
{
|
||||
type: 'query',
|
||||
name: 'test',
|
||||
current: { value: ['val1', 'val2'] },
|
||||
getValueForUrl: function() {
|
||||
return this.current.value;
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should set multiple url params', () => {
|
||||
const params: any = {};
|
||||
_templateSrv.fillVariableValuesForUrl(params);
|
||||
expect(params['var-test']).toMatchObject(['val1', 'val2']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fillVariableValuesForUrl skip url sync', () => {
|
||||
beforeEach(() => {
|
||||
initTemplateSrv([
|
||||
{
|
||||
name: 'test',
|
||||
skipUrlSync: true,
|
||||
current: { value: 'value' },
|
||||
getValueForUrl: function() {
|
||||
return this.current.value;
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not include template variable value in url', () => {
|
||||
const params: any = {};
|
||||
_templateSrv.fillVariableValuesForUrl(params);
|
||||
expect(params['var-test']).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fillVariableValuesForUrl with multi value with skip url sync', () => {
|
||||
beforeEach(() => {
|
||||
initTemplateSrv([
|
||||
{
|
||||
type: 'query',
|
||||
name: 'test',
|
||||
skipUrlSync: true,
|
||||
current: { value: ['val1', 'val2'] },
|
||||
getValueForUrl: function() {
|
||||
return this.current.value;
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not include template variable value in url', () => {
|
||||
const params: any = {};
|
||||
_templateSrv.fillVariableValuesForUrl(params);
|
||||
expect(params['var-test']).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fillVariableValuesForUrl with multi value and scopedVars', () => {
|
||||
beforeEach(() => {
|
||||
initTemplateSrv([{ type: 'query', name: 'test', current: { value: ['val1', 'val2'] } }]);
|
||||
});
|
||||
|
||||
it('should set scoped value as url params', () => {
|
||||
const params: any = {};
|
||||
_templateSrv.fillVariableValuesForUrl(params, {
|
||||
test: { value: 'val1' },
|
||||
});
|
||||
expect(params['var-test']).toBe('val1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('fillVariableValuesForUrl with multi value, scopedVars and skip url sync', () => {
|
||||
beforeEach(() => {
|
||||
initTemplateSrv([{ type: 'query', name: 'test', current: { value: ['val1', 'val2'] } }]);
|
||||
});
|
||||
|
||||
it('should not set scoped value as url params', () => {
|
||||
const params: any = {};
|
||||
_templateSrv.fillVariableValuesForUrl(params, {
|
||||
test: { name: 'test', value: 'val1', skipUrlSync: true },
|
||||
});
|
||||
expect(params['var-test']).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('replaceWithText', () => {
|
||||
beforeEach(() => {
|
||||
initTemplateSrv([
|
||||
{
|
||||
type: 'query',
|
||||
name: 'server',
|
||||
current: { value: '{asd,asd2}', text: 'All' },
|
||||
},
|
||||
{
|
||||
type: 'interval',
|
||||
name: 'period',
|
||||
current: { value: '$__auto_interval_interval', text: 'auto' },
|
||||
},
|
||||
{
|
||||
type: 'textbox',
|
||||
name: 'empty_on_init',
|
||||
current: { value: '', text: '' },
|
||||
},
|
||||
{
|
||||
type: 'custom',
|
||||
name: 'foo',
|
||||
current: { value: 'constructor', text: 'constructor' },
|
||||
},
|
||||
]);
|
||||
_templateSrv.setGrafanaVariable('$__auto_interval_interval', '13m');
|
||||
_templateSrv.updateIndex();
|
||||
});
|
||||
|
||||
it('should replace with text except for grafanaVariables', () => {
|
||||
const target = _templateSrv.replaceWithText('Server: $server, period: $period');
|
||||
expect(target).toBe('Server: All, period: 13m');
|
||||
});
|
||||
|
||||
it('should replace empty string-values with an empty string', () => {
|
||||
const target = _templateSrv.replaceWithText('Hello $empty_on_init');
|
||||
expect(target).toBe('Hello ');
|
||||
});
|
||||
|
||||
it('should not return a string representation of a constructor property', () => {
|
||||
const target = _templateSrv.replaceWithText('$foo');
|
||||
expect(target).not.toBe('function Object() { [native code] }');
|
||||
expect(target).toBe('constructor');
|
||||
});
|
||||
});
|
||||
|
||||
describe('built in interval variables', () => {
|
||||
beforeEach(() => {
|
||||
initTemplateSrv([]);
|
||||
});
|
||||
|
||||
it('should be possible to fetch value with getBuilInIntervalValue', () => {
|
||||
const val = _templateSrv.getBuiltInIntervalValue();
|
||||
expect(val).toBe('1s');
|
||||
});
|
||||
|
||||
it('should replace $__interval_ms with interval milliseconds', () => {
|
||||
const target = _templateSrv.replace('10 * $__interval_ms', {
|
||||
__interval_ms: { text: '100', value: '100' },
|
||||
});
|
||||
expect(target).toBe('10 * 100');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,261 +0,0 @@
|
||||
import { assignModelProperties } from '../types';
|
||||
import { ScopedVars } from '@grafana/data';
|
||||
import { containsSearchFilter, containsVariable, getSearchFilterScopedVar, SEARCH_FILTER_VARIABLE } from '../utils';
|
||||
|
||||
describe('containsVariable', () => {
|
||||
describe('when checking if a string contains a variable', () => {
|
||||
it('should find it with $const syntax', () => {
|
||||
const contains = containsVariable('this.$test.filters', 'test');
|
||||
expect(contains).toBe(true);
|
||||
});
|
||||
|
||||
it('should not find it if only part matches with $const syntax', () => {
|
||||
const contains = containsVariable('this.$serverDomain.filters', 'server');
|
||||
expect(contains).toBe(false);
|
||||
});
|
||||
|
||||
it('should find it if it ends with variable and passing multiple test strings', () => {
|
||||
const contains = containsVariable('show field keys from $pgmetric', 'test string2', 'pgmetric');
|
||||
expect(contains).toBe(true);
|
||||
});
|
||||
|
||||
it('should find it with [[var]] syntax', () => {
|
||||
const contains = containsVariable('this.[[test]].filters', 'test');
|
||||
expect(contains).toBe(true);
|
||||
});
|
||||
|
||||
it('should find it with [[var:option]] syntax', () => {
|
||||
const contains = containsVariable('this.[[test:csv]].filters', 'test');
|
||||
expect(contains).toBe(true);
|
||||
});
|
||||
|
||||
it('should find it when part of segment', () => {
|
||||
const contains = containsVariable('metrics.$env.$group-*', 'group');
|
||||
expect(contains).toBe(true);
|
||||
});
|
||||
|
||||
it('should find it its the only thing', () => {
|
||||
const contains = containsVariable('$env', 'env');
|
||||
expect(contains).toBe(true);
|
||||
});
|
||||
|
||||
it('should be able to pass in multiple test strings', () => {
|
||||
const contains = containsVariable('asd', 'asd2.$env', 'env');
|
||||
expect(contains).toBe(true);
|
||||
});
|
||||
|
||||
it('should find it with ${var} syntax', () => {
|
||||
const contains = containsVariable('this.${test}.filters', 'test');
|
||||
expect(contains).toBe(true);
|
||||
});
|
||||
|
||||
it('should find it with ${var:option} syntax', () => {
|
||||
const contains = containsVariable('this.${test:csv}.filters', 'test');
|
||||
expect(contains).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('assignModelProperties', () => {
|
||||
it('only set properties defined in defaults', () => {
|
||||
const target: any = { test: 'asd' };
|
||||
assignModelProperties(target, { propA: 1, propB: 2 }, { propB: 0 });
|
||||
expect(target.propB).toBe(2);
|
||||
expect(target.test).toBe('asd');
|
||||
});
|
||||
|
||||
it('use default value if not found on source', () => {
|
||||
const target: any = { test: 'asd' };
|
||||
assignModelProperties(target, { propA: 1, propB: 2 }, { propC: 10 });
|
||||
expect(target.propC).toBe(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('containsSearchFilter', () => {
|
||||
describe('when called without query', () => {
|
||||
it('then it should return false', () => {
|
||||
const result = containsSearchFilter(null);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when called with an object', () => {
|
||||
it('then it should return false', () => {
|
||||
const result = containsSearchFilter({});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when called with a query without ${SEARCH_FILTER_VARIABLE}`, () => {
|
||||
it('then it should return false', () => {
|
||||
const result = containsSearchFilter('$app.*');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when called with a query with $${SEARCH_FILTER_VARIABLE}`, () => {
|
||||
it('then it should return true', () => {
|
||||
const result = containsSearchFilter(`$app.$${SEARCH_FILTER_VARIABLE}`);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when called with a query with [[${SEARCH_FILTER_VARIABLE}]]`, () => {
|
||||
it('then it should return true', () => {
|
||||
const result = containsSearchFilter(`$app.[[${SEARCH_FILTER_VARIABLE}]]`);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when called with a query with \$\{${SEARCH_FILTER_VARIABLE}:regex\}`, () => {
|
||||
it('then it should return true', () => {
|
||||
const result = containsSearchFilter(`$app.\$\{${SEARCH_FILTER_VARIABLE}:regex\}`);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
interface GetSearchFilterScopedVarScenario {
|
||||
query: string;
|
||||
wildcardChar: string;
|
||||
options: { searchFilter?: string };
|
||||
expected: ScopedVars;
|
||||
}
|
||||
|
||||
const scenarios: GetSearchFilterScopedVarScenario[] = [
|
||||
// testing the $__searchFilter notation
|
||||
{
|
||||
query: 'abc.$__searchFilter',
|
||||
wildcardChar: '',
|
||||
options: { searchFilter: '' },
|
||||
expected: { __searchFilter: { value: '', text: '' } },
|
||||
},
|
||||
{
|
||||
query: 'abc.$__searchFilter',
|
||||
wildcardChar: '*',
|
||||
options: { searchFilter: '' },
|
||||
expected: { __searchFilter: { value: '*', text: '' } },
|
||||
},
|
||||
{
|
||||
query: 'abc.$__searchFilter',
|
||||
wildcardChar: '',
|
||||
options: { searchFilter: 'a' },
|
||||
expected: { __searchFilter: { value: 'a', text: '' } },
|
||||
},
|
||||
{
|
||||
query: 'abc.$__searchFilter',
|
||||
wildcardChar: '*',
|
||||
options: { searchFilter: 'a' },
|
||||
expected: { __searchFilter: { value: 'a*', text: '' } },
|
||||
},
|
||||
// testing the [[__searchFilter]] notation
|
||||
{
|
||||
query: 'abc.[[__searchFilter]]',
|
||||
wildcardChar: '',
|
||||
options: { searchFilter: '' },
|
||||
expected: { __searchFilter: { value: '', text: '' } },
|
||||
},
|
||||
{
|
||||
query: 'abc.[[__searchFilter]]',
|
||||
wildcardChar: '*',
|
||||
options: { searchFilter: '' },
|
||||
expected: { __searchFilter: { value: '*', text: '' } },
|
||||
},
|
||||
{
|
||||
query: 'abc.[[__searchFilter]]',
|
||||
wildcardChar: '',
|
||||
options: { searchFilter: 'a' },
|
||||
expected: { __searchFilter: { value: 'a', text: '' } },
|
||||
},
|
||||
{
|
||||
query: 'abc.[[__searchFilter]]',
|
||||
wildcardChar: '*',
|
||||
options: { searchFilter: 'a' },
|
||||
expected: { __searchFilter: { value: 'a*', text: '' } },
|
||||
},
|
||||
// testing the ${__searchFilter:fmt} notation
|
||||
{
|
||||
query: 'abc.${__searchFilter:regex}',
|
||||
wildcardChar: '',
|
||||
options: { searchFilter: '' },
|
||||
expected: { __searchFilter: { value: '', text: '' } },
|
||||
},
|
||||
{
|
||||
query: 'abc.${__searchFilter:regex}',
|
||||
wildcardChar: '*',
|
||||
options: { searchFilter: '' },
|
||||
expected: { __searchFilter: { value: '*', text: '' } },
|
||||
},
|
||||
{
|
||||
query: 'abc.${__searchFilter:regex}',
|
||||
wildcardChar: '',
|
||||
options: { searchFilter: 'a' },
|
||||
expected: { __searchFilter: { value: 'a', text: '' } },
|
||||
},
|
||||
{
|
||||
query: 'abc.${__searchFilter:regex}',
|
||||
wildcardChar: '*',
|
||||
options: { searchFilter: 'a' },
|
||||
expected: { __searchFilter: { value: 'a*', text: '' } },
|
||||
},
|
||||
// testing the no options
|
||||
{
|
||||
query: 'abc.$__searchFilter',
|
||||
wildcardChar: '',
|
||||
options: null as any,
|
||||
expected: { __searchFilter: { value: '', text: '' } },
|
||||
},
|
||||
{
|
||||
query: 'abc.$__searchFilter',
|
||||
wildcardChar: '*',
|
||||
options: null as any,
|
||||
expected: { __searchFilter: { value: '*', text: '' } },
|
||||
},
|
||||
// testing the no search filter at all
|
||||
{
|
||||
query: 'abc.$def',
|
||||
wildcardChar: '',
|
||||
options: { searchFilter: '' },
|
||||
expected: {},
|
||||
},
|
||||
{
|
||||
query: 'abc.$def',
|
||||
wildcardChar: '*',
|
||||
options: { searchFilter: '' },
|
||||
expected: {},
|
||||
},
|
||||
{
|
||||
query: 'abc.$def',
|
||||
wildcardChar: '',
|
||||
options: { searchFilter: 'a' },
|
||||
expected: {},
|
||||
},
|
||||
{
|
||||
query: 'abc.$def',
|
||||
wildcardChar: '*',
|
||||
options: { searchFilter: 'a' },
|
||||
expected: {},
|
||||
},
|
||||
];
|
||||
|
||||
scenarios.map(scenario => {
|
||||
describe('getSearchFilterScopedVar', () => {
|
||||
describe(`when called with query:'${scenario.query}'`, () => {
|
||||
describe(`and wildcardChar:'${scenario.wildcardChar}'`, () => {
|
||||
describe(`and options:'${JSON.stringify(scenario.options, null, 0)}'`, () => {
|
||||
it(`then the result should be ${JSON.stringify(scenario.expected, null, 0)}`, () => {
|
||||
const { expected, ...args } = scenario;
|
||||
|
||||
expect(getSearchFilterScopedVar(args)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,663 +0,0 @@
|
||||
import '../all';
|
||||
import { VariableSrv } from '../variable_srv';
|
||||
import { DashboardModel } from '../../dashboard/state/DashboardModel';
|
||||
// @ts-ignore
|
||||
import $q from 'q';
|
||||
import { dateTime } from '@grafana/data';
|
||||
import { CustomVariable } from '../custom_variable';
|
||||
|
||||
jest.mock('app/core/core', () => ({
|
||||
contextSrv: {
|
||||
user: { orgId: 1, orgName: 'TestOrg' },
|
||||
},
|
||||
}));
|
||||
|
||||
describe('VariableSrv', function(this: any) {
|
||||
const ctx = {
|
||||
datasourceSrv: {},
|
||||
timeSrv: {
|
||||
timeRange: () => {
|
||||
return { from: '2018-01-29', to: '2019-01-29' };
|
||||
},
|
||||
},
|
||||
$rootScope: {
|
||||
$on: () => {},
|
||||
},
|
||||
$injector: {
|
||||
instantiate: (ctr: any, obj: { model: any }) => new ctr(obj.model),
|
||||
},
|
||||
templateSrv: {
|
||||
setGrafanaVariable: jest.fn(),
|
||||
init: (vars: any) => {
|
||||
this.variables = vars;
|
||||
},
|
||||
updateIndex: () => {},
|
||||
setGlobalVariable: (name: string, variable: any) => {},
|
||||
replace: (str: any) =>
|
||||
str.replace(this.regex, (match: string) => {
|
||||
return match;
|
||||
}),
|
||||
},
|
||||
$location: {
|
||||
search: () => {},
|
||||
},
|
||||
} as any;
|
||||
|
||||
function describeUpdateVariable(desc: string, fn: Function) {
|
||||
describe(desc, () => {
|
||||
const scenario: any = {};
|
||||
scenario.setup = (setupFn: Function) => {
|
||||
scenario.setupFn = setupFn;
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
scenario.setupFn();
|
||||
|
||||
const ds: any = {};
|
||||
ds.metricFindQuery = () => Promise.resolve(scenario.queryResult);
|
||||
|
||||
ctx.variableSrv = new VariableSrv($q, ctx.$location, ctx.$injector, ctx.templateSrv, ctx.timeSrv);
|
||||
|
||||
ctx.variableSrv.timeSrv = ctx.timeSrv;
|
||||
ctx.datasourceSrv = {
|
||||
get: () => Promise.resolve(ds),
|
||||
getMetricSources: () => scenario.metricSources,
|
||||
};
|
||||
|
||||
ctx.$injector.instantiate = (ctr: any, model: any) => {
|
||||
return getVarMockConstructor(ctr, model, ctx);
|
||||
};
|
||||
|
||||
ctx.variableSrv.init(
|
||||
new DashboardModel({
|
||||
templating: { list: [] },
|
||||
updateSubmenuVisibility: () => {},
|
||||
})
|
||||
);
|
||||
|
||||
scenario.variable = ctx.variableSrv.createVariableFromModel(scenario.variableModel);
|
||||
ctx.variableSrv.addVariable(scenario.variable);
|
||||
|
||||
await ctx.variableSrv.updateOptions(scenario.variable);
|
||||
});
|
||||
|
||||
fn(scenario);
|
||||
});
|
||||
}
|
||||
|
||||
describeUpdateVariable('interval variable without auto', (scenario: any) => {
|
||||
scenario.setup(() => {
|
||||
scenario.variableModel = {
|
||||
type: 'interval',
|
||||
query: '1s,2h,5h,1d',
|
||||
name: 'test',
|
||||
};
|
||||
});
|
||||
|
||||
it('should update options array', () => {
|
||||
expect(scenario.variable.options.length).toBe(4);
|
||||
expect(scenario.variable.options[0].text).toBe('1s');
|
||||
expect(scenario.variable.options[0].value).toBe('1s');
|
||||
});
|
||||
});
|
||||
|
||||
//
|
||||
// Interval variable update
|
||||
//
|
||||
describeUpdateVariable('interval variable with auto', (scenario: any) => {
|
||||
scenario.setup(() => {
|
||||
scenario.variableModel = {
|
||||
type: 'interval',
|
||||
query: '1s,2h,5h,1d',
|
||||
name: 'test',
|
||||
auto: true,
|
||||
auto_count: 10,
|
||||
};
|
||||
|
||||
const range = {
|
||||
from: dateTime(new Date())
|
||||
.subtract(7, 'days')
|
||||
.toDate(),
|
||||
to: new Date(),
|
||||
};
|
||||
|
||||
ctx.timeSrv.timeRange = () => range;
|
||||
// ctx.templateSrv.setGrafanaVariable = jest.fn();
|
||||
});
|
||||
|
||||
it('should update options array', () => {
|
||||
expect(scenario.variable.options.length).toBe(5);
|
||||
expect(scenario.variable.options[0].text).toBe('auto');
|
||||
expect(scenario.variable.options[0].value).toBe('$__auto_interval_test');
|
||||
});
|
||||
|
||||
it('should set $__auto_interval_test', () => {
|
||||
const call = ctx.templateSrv.setGrafanaVariable.mock.calls[0];
|
||||
expect(call[0]).toBe('$__auto_interval_test');
|
||||
expect(call[1]).toBe('12h');
|
||||
});
|
||||
|
||||
// updateAutoValue() gets called twice: once directly once via VariableSrv.validateVariableSelectionState()
|
||||
// So use lastCall instead of a specific call number
|
||||
it('should set $__auto_interval', () => {
|
||||
const call = ctx.templateSrv.setGrafanaVariable.mock.calls.pop();
|
||||
expect(call[0]).toBe('$__auto_interval');
|
||||
expect(call[1]).toBe('12h');
|
||||
});
|
||||
});
|
||||
|
||||
//
|
||||
// Query variable update
|
||||
//
|
||||
describeUpdateVariable('query variable with empty current object and refresh', (scenario: any) => {
|
||||
scenario.setup(() => {
|
||||
scenario.variableModel = {
|
||||
type: 'query',
|
||||
query: '',
|
||||
name: 'test',
|
||||
current: {},
|
||||
};
|
||||
scenario.queryResult = [{ text: 'backend1' }, { text: 'backend2' }];
|
||||
});
|
||||
|
||||
it('should set current value to first option', () => {
|
||||
expect(scenario.variable.options.length).toBe(2);
|
||||
expect(scenario.variable.current.value).toBe('backend1');
|
||||
});
|
||||
});
|
||||
|
||||
describeUpdateVariable(
|
||||
'query variable with multi select and new options does not contain some selected values',
|
||||
(scenario: any) => {
|
||||
scenario.setup(() => {
|
||||
scenario.variableModel = {
|
||||
type: 'query',
|
||||
query: '',
|
||||
name: 'test',
|
||||
current: {
|
||||
value: ['val1', 'val2', 'val3'],
|
||||
text: 'val1 + val2 + val3',
|
||||
},
|
||||
};
|
||||
scenario.queryResult = [{ text: 'val2' }, { text: 'val3' }];
|
||||
});
|
||||
|
||||
it('should update current value', () => {
|
||||
expect(scenario.variable.current.value).toEqual(['val2', 'val3']);
|
||||
expect(scenario.variable.current.text).toEqual('val2 + val3');
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
describeUpdateVariable(
|
||||
'query variable with multi select and new options does not contain any selected values',
|
||||
(scenario: any) => {
|
||||
scenario.setup(() => {
|
||||
scenario.variableModel = {
|
||||
type: 'query',
|
||||
query: '',
|
||||
name: 'test',
|
||||
current: {
|
||||
value: ['val1', 'val2', 'val3'],
|
||||
text: 'val1 + val2 + val3',
|
||||
},
|
||||
};
|
||||
scenario.queryResult = [{ text: 'val5' }, { text: 'val6' }];
|
||||
});
|
||||
|
||||
it('should update current value with first one', () => {
|
||||
expect(scenario.variable.current.value).toEqual('val5');
|
||||
expect(scenario.variable.current.text).toEqual('val5');
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
describeUpdateVariable('query variable with multi select and $__all selected', (scenario: any) => {
|
||||
scenario.setup(() => {
|
||||
scenario.variableModel = {
|
||||
type: 'query',
|
||||
query: '',
|
||||
name: 'test',
|
||||
includeAll: true,
|
||||
current: {
|
||||
value: ['$__all'],
|
||||
text: 'All',
|
||||
},
|
||||
};
|
||||
scenario.queryResult = [{ text: 'val5' }, { text: 'val6' }];
|
||||
});
|
||||
|
||||
it('should keep current All value', () => {
|
||||
expect(scenario.variable.current.value).toEqual(['$__all']);
|
||||
expect(scenario.variable.current.text).toEqual('All');
|
||||
});
|
||||
});
|
||||
|
||||
describeUpdateVariable('query variable with numeric results', (scenario: any) => {
|
||||
scenario.setup(() => {
|
||||
scenario.variableModel = {
|
||||
type: 'query',
|
||||
query: '',
|
||||
name: 'test',
|
||||
current: {},
|
||||
};
|
||||
scenario.queryResult = [{ text: 12, value: 12 }];
|
||||
});
|
||||
|
||||
it('should set current value to first option', () => {
|
||||
expect(scenario.variable.current.value).toBe('12');
|
||||
expect(scenario.variable.options[0].value).toBe('12');
|
||||
expect(scenario.variable.options[0].text).toBe('12');
|
||||
});
|
||||
});
|
||||
|
||||
describeUpdateVariable('basic query variable', (scenario: any) => {
|
||||
scenario.setup(() => {
|
||||
scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' };
|
||||
scenario.queryResult = [{ text: 'backend1' }, { text: 'backend2' }];
|
||||
});
|
||||
|
||||
it('should update options array', () => {
|
||||
expect(scenario.variable.options.length).toBe(2);
|
||||
expect(scenario.variable.options[0].text).toBe('backend1');
|
||||
expect(scenario.variable.options[0].value).toBe('backend1');
|
||||
expect(scenario.variable.options[1].value).toBe('backend2');
|
||||
});
|
||||
|
||||
it('should select first option as value', () => {
|
||||
expect(scenario.variable.current.value).toBe('backend1');
|
||||
});
|
||||
});
|
||||
|
||||
describeUpdateVariable('and existing value still exists in options', (scenario: any) => {
|
||||
scenario.setup(() => {
|
||||
scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' };
|
||||
scenario.variableModel.current = { value: 'backend2', text: 'backend2' };
|
||||
scenario.queryResult = [{ text: 'backend1' }, { text: 'backend2' }];
|
||||
});
|
||||
|
||||
it('should keep variable value', () => {
|
||||
expect(scenario.variable.current.text).toBe('backend2');
|
||||
});
|
||||
});
|
||||
|
||||
describeUpdateVariable('and regex pattern exists', (scenario: any) => {
|
||||
scenario.setup(() => {
|
||||
scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' };
|
||||
scenario.variableModel.regex = '/apps.*(backend_[0-9]+)/';
|
||||
scenario.queryResult = [
|
||||
{ text: 'apps.backend.backend_01.counters.req' },
|
||||
{ text: 'apps.backend.backend_02.counters.req' },
|
||||
];
|
||||
});
|
||||
|
||||
it('should extract and use match group', () => {
|
||||
expect(scenario.variable.options[0].value).toBe('backend_01');
|
||||
});
|
||||
});
|
||||
|
||||
describeUpdateVariable('and regex pattern exists and no match', (scenario: any) => {
|
||||
scenario.setup(() => {
|
||||
scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' };
|
||||
scenario.variableModel.regex = '/apps.*(backendasd[0-9]+)/';
|
||||
scenario.queryResult = [
|
||||
{ text: 'apps.backend.backend_01.counters.req' },
|
||||
{ text: 'apps.backend.backend_02.counters.req' },
|
||||
];
|
||||
});
|
||||
|
||||
it('should not add non matching items, None option should be added instead', () => {
|
||||
expect(scenario.variable.options.length).toBe(1);
|
||||
expect(scenario.variable.options[0].isNone).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describeUpdateVariable('regex pattern without slashes', (scenario: any) => {
|
||||
scenario.setup(() => {
|
||||
scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' };
|
||||
scenario.variableModel.regex = 'backend_01';
|
||||
scenario.queryResult = [
|
||||
{ text: 'apps.backend.backend_01.counters.req' },
|
||||
{ text: 'apps.backend.backend_02.counters.req' },
|
||||
];
|
||||
});
|
||||
|
||||
it('should return matches options', () => {
|
||||
expect(scenario.variable.options.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describeUpdateVariable('regex pattern remove duplicates', (scenario: any) => {
|
||||
scenario.setup(() => {
|
||||
scenario.variableModel = { type: 'query', query: 'apps.*', name: 'test' };
|
||||
scenario.variableModel.regex = '/backend_01/';
|
||||
scenario.queryResult = [
|
||||
{ text: 'apps.backend.backend_01.counters.req' },
|
||||
{ text: 'apps.backend.backend_01.counters.req' },
|
||||
];
|
||||
});
|
||||
|
||||
it('should return matches options', () => {
|
||||
expect(scenario.variable.options.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describeUpdateVariable('with include All', (scenario: any) => {
|
||||
scenario.setup(() => {
|
||||
scenario.variableModel = {
|
||||
type: 'query',
|
||||
query: 'apps.*',
|
||||
name: 'test',
|
||||
includeAll: true,
|
||||
};
|
||||
scenario.queryResult = [{ text: 'backend1' }, { text: 'backend2' }, { text: 'backend3' }];
|
||||
});
|
||||
|
||||
it('should add All option', () => {
|
||||
expect(scenario.variable.options[0].text).toBe('All');
|
||||
expect(scenario.variable.options[0].value).toBe('$__all');
|
||||
});
|
||||
});
|
||||
|
||||
describeUpdateVariable('with include all and custom value', (scenario: any) => {
|
||||
scenario.setup(() => {
|
||||
scenario.variableModel = {
|
||||
type: 'query',
|
||||
query: 'apps.*',
|
||||
name: 'test',
|
||||
includeAll: true,
|
||||
allValue: '*',
|
||||
};
|
||||
scenario.queryResult = [{ text: 'backend1' }, { text: 'backend2' }, { text: 'backend3' }];
|
||||
});
|
||||
|
||||
it('should add All option with custom value', () => {
|
||||
expect(scenario.variable.options[0].value).toBe('$__all');
|
||||
});
|
||||
});
|
||||
|
||||
describeUpdateVariable('without sort', (scenario: any) => {
|
||||
scenario.setup(() => {
|
||||
scenario.variableModel = {
|
||||
type: 'query',
|
||||
query: 'apps.*',
|
||||
name: 'test',
|
||||
sort: 0,
|
||||
};
|
||||
scenario.queryResult = [{ text: 'bbb2' }, { text: 'aaa10' }, { text: 'ccc3' }];
|
||||
});
|
||||
|
||||
it('should return options without sort', () => {
|
||||
expect(scenario.variable.options[0].text).toBe('bbb2');
|
||||
expect(scenario.variable.options[1].text).toBe('aaa10');
|
||||
expect(scenario.variable.options[2].text).toBe('ccc3');
|
||||
});
|
||||
});
|
||||
|
||||
describeUpdateVariable('with alphabetical sort (asc)', (scenario: any) => {
|
||||
scenario.setup(() => {
|
||||
scenario.variableModel = {
|
||||
type: 'query',
|
||||
query: 'apps.*',
|
||||
name: 'test',
|
||||
sort: 1,
|
||||
};
|
||||
scenario.queryResult = [{ text: 'bbb2' }, { text: 'aaa10' }, { text: 'ccc3' }];
|
||||
});
|
||||
|
||||
it('should return options with alphabetical sort', () => {
|
||||
expect(scenario.variable.options[0].text).toBe('aaa10');
|
||||
expect(scenario.variable.options[1].text).toBe('bbb2');
|
||||
expect(scenario.variable.options[2].text).toBe('ccc3');
|
||||
});
|
||||
});
|
||||
|
||||
describeUpdateVariable('with alphabetical sort (desc)', (scenario: any) => {
|
||||
scenario.setup(() => {
|
||||
scenario.variableModel = {
|
||||
type: 'query',
|
||||
query: 'apps.*',
|
||||
name: 'test',
|
||||
sort: 2,
|
||||
};
|
||||
scenario.queryResult = [{ text: 'bbb2' }, { text: 'aaa10' }, { text: 'ccc3' }];
|
||||
});
|
||||
|
||||
it('should return options with alphabetical sort', () => {
|
||||
expect(scenario.variable.options[0].text).toBe('ccc3');
|
||||
expect(scenario.variable.options[1].text).toBe('bbb2');
|
||||
expect(scenario.variable.options[2].text).toBe('aaa10');
|
||||
});
|
||||
});
|
||||
|
||||
describeUpdateVariable('with numerical sort (asc)', (scenario: any) => {
|
||||
scenario.setup(() => {
|
||||
scenario.variableModel = {
|
||||
type: 'query',
|
||||
query: 'apps.*',
|
||||
name: 'test',
|
||||
sort: 3,
|
||||
};
|
||||
scenario.queryResult = [{ text: 'bbb2' }, { text: 'aaa10' }, { text: 'ccc3' }];
|
||||
});
|
||||
|
||||
it('should return options with numerical sort', () => {
|
||||
expect(scenario.variable.options[0].text).toBe('bbb2');
|
||||
expect(scenario.variable.options[1].text).toBe('ccc3');
|
||||
expect(scenario.variable.options[2].text).toBe('aaa10');
|
||||
});
|
||||
});
|
||||
|
||||
describeUpdateVariable('with numerical sort (desc)', (scenario: any) => {
|
||||
scenario.setup(() => {
|
||||
scenario.variableModel = {
|
||||
type: 'query',
|
||||
query: 'apps.*',
|
||||
name: 'test',
|
||||
sort: 4,
|
||||
};
|
||||
scenario.queryResult = [{ text: 'bbb2' }, { text: 'aaa10' }, { text: 'ccc3' }];
|
||||
});
|
||||
|
||||
it('should return options with numerical sort', () => {
|
||||
expect(scenario.variable.options[0].text).toBe('aaa10');
|
||||
expect(scenario.variable.options[1].text).toBe('ccc3');
|
||||
expect(scenario.variable.options[2].text).toBe('bbb2');
|
||||
});
|
||||
});
|
||||
|
||||
//
|
||||
// datasource variable update
|
||||
//
|
||||
describeUpdateVariable('datasource variable with regex filter', (scenario: any) => {
|
||||
scenario.setup(() => {
|
||||
scenario.variableModel = {
|
||||
type: 'datasource',
|
||||
query: 'graphite',
|
||||
name: 'test',
|
||||
current: { value: 'backend4_pee', text: 'backend4_pee' },
|
||||
regex: '/pee$/',
|
||||
};
|
||||
scenario.metricSources = [
|
||||
{ name: 'backend1', meta: { id: 'influx' } },
|
||||
{ name: 'backend2_pee', meta: { id: 'graphite' } },
|
||||
{ name: 'backend3', meta: { id: 'graphite' } },
|
||||
{ name: 'backend4_pee', meta: { id: 'graphite' } },
|
||||
];
|
||||
});
|
||||
|
||||
it('should set only contain graphite ds and filtered using regex', () => {
|
||||
expect(scenario.variable.options.length).toBe(2);
|
||||
expect(scenario.variable.options[0].value).toBe('backend2_pee');
|
||||
expect(scenario.variable.options[1].value).toBe('backend4_pee');
|
||||
});
|
||||
|
||||
it('should keep current value if available', () => {
|
||||
expect(scenario.variable.current.value).toBe('backend4_pee');
|
||||
});
|
||||
});
|
||||
|
||||
//
|
||||
// Custom variable update
|
||||
//
|
||||
describeUpdateVariable('update custom variable', (scenario: any) => {
|
||||
scenario.setup(() => {
|
||||
scenario.variableModel = {
|
||||
type: 'custom',
|
||||
query: 'hej, hop, asd, escaped\\,var',
|
||||
name: 'test',
|
||||
};
|
||||
});
|
||||
|
||||
it('should update options array', () => {
|
||||
expect(scenario.variable.options.length).toBe(4);
|
||||
expect(scenario.variable.options[0].text).toBe('hej');
|
||||
expect(scenario.variable.options[1].value).toBe('hop');
|
||||
expect(scenario.variable.options[2].value).toBe('asd');
|
||||
expect(scenario.variable.options[3].value).toBe('escaped,var');
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiple interval variables with auto', () => {
|
||||
let variable1: any, variable2: any;
|
||||
|
||||
beforeEach(() => {
|
||||
const range = {
|
||||
from: dateTime(new Date())
|
||||
.subtract(7, 'days')
|
||||
.toDate(),
|
||||
to: new Date(),
|
||||
};
|
||||
ctx.timeSrv.timeRange = () => range;
|
||||
ctx.templateSrv.setGrafanaVariable = jest.fn();
|
||||
|
||||
const variableModel1 = {
|
||||
type: 'interval',
|
||||
query: '1s,2h,5h,1d',
|
||||
name: 'variable1',
|
||||
auto: true,
|
||||
auto_count: 10,
|
||||
};
|
||||
variable1 = ctx.variableSrv.createVariableFromModel(variableModel1);
|
||||
ctx.variableSrv.addVariable(variable1);
|
||||
|
||||
const variableModel2 = {
|
||||
type: 'interval',
|
||||
query: '1s,2h,5h',
|
||||
name: 'variable2',
|
||||
auto: true,
|
||||
auto_count: 1000,
|
||||
};
|
||||
variable2 = ctx.variableSrv.createVariableFromModel(variableModel2);
|
||||
ctx.variableSrv.addVariable(variable2);
|
||||
|
||||
ctx.variableSrv.updateOptions(variable1);
|
||||
ctx.variableSrv.updateOptions(variable2);
|
||||
// ctx.$rootScope.$digest();
|
||||
});
|
||||
|
||||
it('should update options array', () => {
|
||||
expect(variable1.options.length).toBe(5);
|
||||
expect(variable1.options[0].text).toBe('auto');
|
||||
expect(variable1.options[0].value).toBe('$__auto_interval_variable1');
|
||||
expect(variable2.options.length).toBe(4);
|
||||
expect(variable2.options[0].text).toBe('auto');
|
||||
expect(variable2.options[0].value).toBe('$__auto_interval_variable2');
|
||||
});
|
||||
|
||||
it('should correctly set $__auto_interval_variableX', () => {
|
||||
let variable1Set,
|
||||
variable2Set,
|
||||
legacySet,
|
||||
unknownSet = false;
|
||||
// updateAutoValue() gets called repeatedly: once directly once via VariableSrv.validateVariableSelectionState()
|
||||
// So check that all calls are valid rather than expect a specific number and/or ordering of calls
|
||||
for (let i = 0; i < ctx.templateSrv.setGrafanaVariable.mock.calls.length; i++) {
|
||||
const call = ctx.templateSrv.setGrafanaVariable.mock.calls[i];
|
||||
switch (call[0]) {
|
||||
case '$__auto_interval_variable1':
|
||||
expect(call[1]).toBe('12h');
|
||||
variable1Set = true;
|
||||
break;
|
||||
case '$__auto_interval_variable2':
|
||||
expect(call[1]).toBe('10m');
|
||||
variable2Set = true;
|
||||
break;
|
||||
case '$__auto_interval':
|
||||
expect(call[1]).toEqual(expect.stringMatching(/^(12h|10m)$/));
|
||||
legacySet = true;
|
||||
break;
|
||||
default:
|
||||
unknownSet = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect(variable1Set).toEqual(true);
|
||||
expect(variable2Set).toEqual(true);
|
||||
expect(legacySet).toEqual(true);
|
||||
expect(unknownSet).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setOptionFromUrl', () => {
|
||||
it('sets single value as string if not multi choice', async () => {
|
||||
const [setValueMock, setFromUrl] = setupSetFromUrlTest(ctx);
|
||||
await setFromUrl('one');
|
||||
expect(setValueMock).toHaveBeenCalledWith({ text: 'one', value: 'one' });
|
||||
});
|
||||
|
||||
it('sets single value as array if multi choice', async () => {
|
||||
const [setValueMock, setFromUrl] = setupSetFromUrlTest(ctx, { multi: true });
|
||||
await setFromUrl('one');
|
||||
expect(setValueMock).toHaveBeenCalledWith({ text: ['one'], value: ['one'] });
|
||||
});
|
||||
|
||||
it('sets both text and value as array if multiple values in url', async () => {
|
||||
const [setValueMock, setFromUrl] = setupSetFromUrlTest(ctx, { multi: true });
|
||||
await setFromUrl(['one', 'two']);
|
||||
expect(setValueMock).toHaveBeenCalledWith({ text: ['one', 'two'], value: ['one', 'two'] });
|
||||
});
|
||||
|
||||
it('sets text and value even if it does not match any option', async () => {
|
||||
const [setValueMock, setFromUrl] = setupSetFromUrlTest(ctx);
|
||||
await setFromUrl('none');
|
||||
expect(setValueMock).toHaveBeenCalledWith({ text: 'none', value: 'none' });
|
||||
});
|
||||
|
||||
it('sets text and value even if it does not match any option and it is array', async () => {
|
||||
const [setValueMock, setFromUrl] = setupSetFromUrlTest(ctx);
|
||||
await setFromUrl(['none', 'none2']);
|
||||
expect(setValueMock).toHaveBeenCalledWith({ text: ['none', 'none2'], value: ['none', 'none2'] });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function setupSetFromUrlTest(ctx: any, model = {}) {
|
||||
const variableSrv = new VariableSrv($q, ctx.$location, ctx.$injector, ctx.templateSrv, ctx.timeSrv);
|
||||
const finalModel = {
|
||||
type: 'custom',
|
||||
options: ['one', 'two', 'three'].map(v => ({ text: v, value: v })),
|
||||
name: 'test',
|
||||
...model,
|
||||
};
|
||||
const variable = new CustomVariable(finalModel, variableSrv);
|
||||
// We are mocking the setValue here instead of just checking the final variable.current value because there is lots
|
||||
// of stuff going when the setValue is called that is hard to mock out.
|
||||
variable.setValue = jest.fn();
|
||||
return [variable.setValue, (val: any) => variableSrv.setOptionFromUrl(variable, val)];
|
||||
}
|
||||
|
||||
function getVarMockConstructor(variable: any, model: any, ctx: any) {
|
||||
switch (model.model.type) {
|
||||
case 'datasource':
|
||||
return new variable(model.model, ctx.datasourceSrv, ctx.variableSrv, ctx.templateSrv);
|
||||
case 'query':
|
||||
return new variable(model.model, ctx.datasourceSrv, ctx.templateSrv, ctx.variableSrv);
|
||||
case 'interval':
|
||||
return new variable(model.model, ctx.timeSrv, ctx.templateSrv, ctx.variableSrv);
|
||||
case 'custom':
|
||||
return new variable(model.model, ctx.variableSrv);
|
||||
default:
|
||||
return new variable(model.model);
|
||||
}
|
||||
}
|
||||
@@ -1,275 +0,0 @@
|
||||
import '../all';
|
||||
|
||||
import _ from 'lodash';
|
||||
import { VariableSrv } from '../variable_srv';
|
||||
import { DashboardModel } from '../../dashboard/state/DashboardModel';
|
||||
// @ts-ignore
|
||||
import $q from 'q';
|
||||
|
||||
jest.mock('app/core/core', () => ({
|
||||
contextSrv: {
|
||||
user: { orgId: 1, orgName: 'TestOrg' },
|
||||
},
|
||||
}));
|
||||
|
||||
describe('VariableSrv init', function(this: any) {
|
||||
const templateSrv = {
|
||||
init: (vars: any) => {
|
||||
this.variables = vars;
|
||||
},
|
||||
variableInitialized: () => {},
|
||||
updateIndex: () => {},
|
||||
setGlobalVariable: (name: string, variable: any) => {},
|
||||
replace: (str: string) =>
|
||||
str.replace(this.regex, match => {
|
||||
return match;
|
||||
}),
|
||||
};
|
||||
|
||||
const timeSrv = {
|
||||
timeRange: () => {
|
||||
return { from: '2018-01-29', to: '2019-01-29' };
|
||||
},
|
||||
};
|
||||
|
||||
const $injector = {} as any;
|
||||
let ctx = {} as any;
|
||||
|
||||
function describeInitScenario(desc: string, fn: Function) {
|
||||
describe(desc, () => {
|
||||
const scenario: any = {
|
||||
urlParams: {},
|
||||
setup: (setupFn: Function) => {
|
||||
scenario.setupFn = setupFn;
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
scenario.setupFn();
|
||||
ctx = {
|
||||
datasource: {
|
||||
metricFindQuery: jest.fn(() => Promise.resolve(scenario.queryResult)),
|
||||
},
|
||||
datasourceSrv: {
|
||||
get: () => Promise.resolve(ctx.datasource),
|
||||
getMetricSources: () => scenario.metricSources,
|
||||
},
|
||||
templateSrv,
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
ctx.variableSrv = new VariableSrv($q, {}, $injector, templateSrv, timeSrv);
|
||||
|
||||
$injector.instantiate = (variable: any, model: any) => {
|
||||
return getVarMockConstructor(variable, model, ctx);
|
||||
};
|
||||
|
||||
ctx.variableSrv.datasource = ctx.datasource;
|
||||
ctx.variableSrv.datasourceSrv = ctx.datasourceSrv;
|
||||
|
||||
ctx.variableSrv.$location.search = () => scenario.urlParams;
|
||||
ctx.variableSrv.dashboard = new DashboardModel({
|
||||
templating: { list: scenario.variables },
|
||||
});
|
||||
|
||||
await ctx.variableSrv.init(ctx.variableSrv.dashboard);
|
||||
|
||||
scenario.variables = ctx.variableSrv.variables;
|
||||
});
|
||||
|
||||
fn(scenario);
|
||||
});
|
||||
}
|
||||
|
||||
['interval', 'custom', 'datasource'].forEach(type => {
|
||||
describeInitScenario('when setting ' + type + ' variable via url', (scenario: any) => {
|
||||
scenario.setup(() => {
|
||||
scenario.variables = [
|
||||
{
|
||||
name: 'apps',
|
||||
type: type,
|
||||
current: { text: 'Test', value: 'test' },
|
||||
options: [{ text: 'Test', value: 'test' }],
|
||||
},
|
||||
];
|
||||
scenario.urlParams['var-apps'] = 'new';
|
||||
scenario.metricSources = [];
|
||||
});
|
||||
|
||||
it('should update current value', () => {
|
||||
expect(scenario.variables[0].current.value).toBe('new');
|
||||
expect(scenario.variables[0].current.text).toBe('new');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// this test will moved to redux tests instead
|
||||
describe('given dependent variables', () => {
|
||||
const variableList = [
|
||||
{
|
||||
name: 'app',
|
||||
type: 'query',
|
||||
query: '',
|
||||
current: { text: 'app1', value: 'app1' },
|
||||
options: [{ text: 'app1', value: 'app1' }],
|
||||
},
|
||||
{
|
||||
name: 'server',
|
||||
type: 'query',
|
||||
refresh: 1,
|
||||
query: '$app.*',
|
||||
current: { text: 'server1', value: 'server1' },
|
||||
options: [{ text: 'server1', value: 'server1' }],
|
||||
},
|
||||
];
|
||||
|
||||
describeInitScenario('when setting parent const from url', (scenario: any) => {
|
||||
scenario.setup(() => {
|
||||
scenario.variables = _.cloneDeep(variableList);
|
||||
scenario.urlParams['var-app'] = 'google';
|
||||
scenario.queryResult = [{ text: 'google-server1' }, { text: 'google-server2' }];
|
||||
});
|
||||
|
||||
it('should update child variable', () => {
|
||||
expect(scenario.variables[1].options.length).toBe(2);
|
||||
expect(scenario.variables[1].current.text).toBe('google-server1');
|
||||
});
|
||||
|
||||
it('should only update it once', () => {
|
||||
expect(ctx.variableSrv.datasource.metricFindQuery).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describeInitScenario('when datasource variable is initialized', (scenario: any) => {
|
||||
scenario.setup(() => {
|
||||
scenario.variables = [
|
||||
{
|
||||
type: 'datasource',
|
||||
query: 'graphite',
|
||||
name: 'test',
|
||||
current: { value: 'backend4_pee', text: 'backend4_pee' },
|
||||
regex: '/pee$/',
|
||||
},
|
||||
];
|
||||
scenario.metricSources = [
|
||||
{ name: 'backend1', meta: { id: 'influx' } },
|
||||
{ name: 'backend2_pee', meta: { id: 'graphite' } },
|
||||
{ name: 'backend3', meta: { id: 'graphite' } },
|
||||
{ name: 'backend4_pee', meta: { id: 'graphite' } },
|
||||
];
|
||||
});
|
||||
|
||||
it('should update current value', () => {
|
||||
const variable = ctx.variableSrv.variables[0];
|
||||
expect(variable.options.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describeInitScenario('when template variable is present in url multiple times', (scenario: any) => {
|
||||
scenario.setup(() => {
|
||||
scenario.variables = [
|
||||
{
|
||||
name: 'apps',
|
||||
type: 'custom',
|
||||
multi: true,
|
||||
current: { text: 'Val1', value: 'val1' },
|
||||
options: [
|
||||
{ text: 'Val1', value: 'val1' },
|
||||
{ text: 'Val2', value: 'val2' },
|
||||
{ text: 'Val3', value: 'val3', selected: true },
|
||||
],
|
||||
},
|
||||
];
|
||||
scenario.urlParams['var-apps'] = ['val2', 'val1'];
|
||||
});
|
||||
|
||||
it('should update current value', () => {
|
||||
const variable = ctx.variableSrv.variables[0];
|
||||
expect(variable.current.value.length).toBe(2);
|
||||
expect(variable.current.value[0]).toBe('val2');
|
||||
expect(variable.current.value[1]).toBe('val1');
|
||||
expect(variable.current.text).toBe('Val2 + Val1');
|
||||
expect(variable.options[0].selected).toBe(true);
|
||||
expect(variable.options[1].selected).toBe(true);
|
||||
});
|
||||
|
||||
it('should set options that are not in value to selected false', () => {
|
||||
const variable = ctx.variableSrv.variables[0];
|
||||
expect(variable.options[2].selected).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describeInitScenario(
|
||||
'when template variable is present in url multiple times and variables have no text',
|
||||
(scenario: any) => {
|
||||
scenario.setup(() => {
|
||||
scenario.variables = [
|
||||
{
|
||||
name: 'apps',
|
||||
type: 'custom',
|
||||
multi: true,
|
||||
},
|
||||
];
|
||||
scenario.urlParams['var-apps'] = ['val1', 'val2'];
|
||||
});
|
||||
|
||||
it('should display concatenated values in text', () => {
|
||||
const variable = ctx.variableSrv.variables[0];
|
||||
expect(variable.current.value.length).toBe(2);
|
||||
expect(variable.current.value[0]).toBe('val1');
|
||||
expect(variable.current.value[1]).toBe('val2');
|
||||
expect(variable.current.text).toBe('val1 + val2');
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
describeInitScenario('when template variable is present in url multiple times using key/values', (scenario: any) => {
|
||||
scenario.setup(() => {
|
||||
scenario.variables = [
|
||||
{
|
||||
name: 'apps',
|
||||
type: 'custom',
|
||||
multi: true,
|
||||
current: { text: 'Val1', value: 'val1' },
|
||||
options: [
|
||||
{ text: 'Val1', value: 'val1' },
|
||||
{ text: 'Val2', value: 'val2' },
|
||||
{ text: 'Val3', value: 'val3', selected: true },
|
||||
],
|
||||
},
|
||||
];
|
||||
scenario.urlParams['var-apps'] = ['val2', 'val1'];
|
||||
});
|
||||
|
||||
it('should update current value', () => {
|
||||
const variable = ctx.variableSrv.variables[0];
|
||||
expect(variable.current.value.length).toBe(2);
|
||||
expect(variable.current.value[0]).toBe('val2');
|
||||
expect(variable.current.value[1]).toBe('val1');
|
||||
expect(variable.current.text).toBe('Val2 + Val1');
|
||||
expect(variable.options[0].selected).toBe(true);
|
||||
expect(variable.options[1].selected).toBe(true);
|
||||
});
|
||||
|
||||
it('should set options that are not in value to selected false', () => {
|
||||
const variable = ctx.variableSrv.variables[0];
|
||||
expect(variable.options[2].selected).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function getVarMockConstructor(variable: any, model: any, ctx: any) {
|
||||
switch (model.model.type) {
|
||||
case 'datasource':
|
||||
return new variable(model.model, ctx.datasourceSrv, ctx.variableSrv, ctx.templateSrv);
|
||||
case 'query':
|
||||
return new variable(model.model, ctx.datasourceSrv, ctx.templateSrv, ctx.variableSrv);
|
||||
case 'interval':
|
||||
return new variable(model.model, {}, ctx.templateSrv, ctx.variableSrv);
|
||||
case 'custom':
|
||||
return new variable(model.model, ctx.variableSrv);
|
||||
default:
|
||||
return new variable(model.model);
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,9 @@ import kbn from 'app/core/utils/kbn';
|
||||
import _ from 'lodash';
|
||||
import { deprecationWarning, ScopedVars, textUtil, TimeRange } from '@grafana/data';
|
||||
import { getFilteredVariables, getVariables, getVariableWithName } from '../variables/state/selectors';
|
||||
import { getConfig } from 'app/core/config';
|
||||
import { variableRegex } from './utils';
|
||||
import { variableRegex } from '../variables/utils';
|
||||
import { isAdHoc } from '../variables/guard';
|
||||
import { VariableModel } from './types';
|
||||
import { VariableModel } from '../variables/types';
|
||||
import { setTemplateSrv, TemplateSrv as BaseTemplateSrv } from '@grafana/runtime';
|
||||
import { variableAdapters } from '../variables/adapters';
|
||||
|
||||
@@ -65,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 [];
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,427 +0,0 @@
|
||||
// Libaries
|
||||
import angular, { auto, ILocationService, IPromise, IQService } from 'angular';
|
||||
import _ from 'lodash';
|
||||
// Utils & Services
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { VariableActions, variableTypes } from './types';
|
||||
import { Graph } from 'app/core/utils/dag';
|
||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
|
||||
|
||||
// Types
|
||||
import { AppEvents, TimeRange, UrlQueryMap } from '@grafana/data';
|
||||
import { CoreEvents } from 'app/types';
|
||||
import { appEvents, contextSrv } from 'app/core/core';
|
||||
|
||||
export class VariableSrv {
|
||||
dashboard: DashboardModel;
|
||||
variables: any[] = [];
|
||||
|
||||
/** @ngInject */
|
||||
constructor(
|
||||
private $q: IQService,
|
||||
private $location: ILocationService,
|
||||
private $injector: auto.IInjectorService,
|
||||
private templateSrv: TemplateSrv,
|
||||
private timeSrv: TimeSrv
|
||||
) {}
|
||||
|
||||
init(dashboard: DashboardModel) {
|
||||
this.dashboard = dashboard;
|
||||
this.dashboard.events.on(CoreEvents.timeRangeUpdated, this.onTimeRangeUpdated.bind(this));
|
||||
this.dashboard.events.on(
|
||||
CoreEvents.templateVariableValueUpdated,
|
||||
this.updateUrlParamsWithCurrentVariables.bind(this)
|
||||
);
|
||||
|
||||
// create working class models representing variables
|
||||
this.variables = dashboard.templating.list = dashboard.templating.list.map(this.createVariableFromModel.bind(this));
|
||||
this.templateSrv.init(this.variables, this.timeSrv.timeRange());
|
||||
|
||||
// init variables
|
||||
for (const variable of this.variables) {
|
||||
variable.initLock = this.$q.defer();
|
||||
}
|
||||
|
||||
const queryParams = this.$location.search();
|
||||
return this.$q
|
||||
.all(
|
||||
this.variables.map(variable => {
|
||||
return this.processVariable(variable, queryParams);
|
||||
})
|
||||
)
|
||||
.then(() => {
|
||||
this.templateSrv.updateIndex();
|
||||
this.templateSrv.setGlobalVariable('__dashboard', {
|
||||
value: {
|
||||
name: dashboard.title,
|
||||
uid: dashboard.uid,
|
||||
toString: function() {
|
||||
return this.uid;
|
||||
},
|
||||
},
|
||||
});
|
||||
this.templateSrv.setGlobalVariable('__org', {
|
||||
value: {
|
||||
name: contextSrv.user.orgName,
|
||||
id: contextSrv.user.orgId,
|
||||
toString: function() {
|
||||
return this.id;
|
||||
},
|
||||
},
|
||||
});
|
||||
this.templateSrv.setGlobalVariable('__user', {
|
||||
value: {
|
||||
login: contextSrv.user.login,
|
||||
id: contextSrv.user.id,
|
||||
toString: function() {
|
||||
return this.id;
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onTimeRangeUpdated(timeRange: TimeRange) {
|
||||
this.templateSrv.updateTimeRange(timeRange);
|
||||
const promises = this.variables
|
||||
.filter(variable => variable.refresh === 2)
|
||||
.map(variable => {
|
||||
const previousOptions = variable.options.slice();
|
||||
|
||||
return variable.updateOptions().then(() => {
|
||||
if (angular.toJson(previousOptions) !== angular.toJson(variable.options)) {
|
||||
this.dashboard.templateVariableValueUpdated();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return this.$q
|
||||
.all(promises)
|
||||
.then(() => {
|
||||
this.dashboard.startRefresh();
|
||||
})
|
||||
.catch(e => {
|
||||
appEvents.emit(AppEvents.alertError, ['Template variable service failed', e.message]);
|
||||
});
|
||||
}
|
||||
|
||||
processVariable(variable: any, queryParams: any) {
|
||||
const dependencies = [];
|
||||
|
||||
for (const otherVariable of this.variables) {
|
||||
if (variable.dependsOn(otherVariable)) {
|
||||
dependencies.push(otherVariable.initLock.promise);
|
||||
}
|
||||
}
|
||||
|
||||
return this.$q
|
||||
.all(dependencies)
|
||||
.then(() => {
|
||||
const urlValue = queryParams['var-' + variable.name];
|
||||
if (urlValue !== void 0) {
|
||||
return variable.setValueFromUrl(urlValue).then(variable.initLock.resolve);
|
||||
}
|
||||
|
||||
if (variable.refresh === 1 || variable.refresh === 2) {
|
||||
return variable.updateOptions().then(variable.initLock.resolve);
|
||||
}
|
||||
|
||||
variable.initLock.resolve();
|
||||
})
|
||||
.finally(() => {
|
||||
this.templateSrv.variableInitialized(variable);
|
||||
delete variable.initLock;
|
||||
});
|
||||
}
|
||||
|
||||
createVariableFromModel(model: any) {
|
||||
// @ts-ignore
|
||||
const ctor = variableTypes[model.type].ctor;
|
||||
if (!ctor) {
|
||||
throw {
|
||||
message: 'Unable to find variable constructor for ' + model.type,
|
||||
};
|
||||
}
|
||||
|
||||
const variable = this.$injector.instantiate(ctor, { model: model });
|
||||
return variable;
|
||||
}
|
||||
|
||||
addVariable(variable: any) {
|
||||
this.variables.push(variable);
|
||||
this.templateSrv.updateIndex();
|
||||
this.dashboard.updateSubmenuVisibility();
|
||||
}
|
||||
|
||||
removeVariable(variable: any) {
|
||||
const index = _.indexOf(this.variables, variable);
|
||||
this.variables.splice(index, 1);
|
||||
this.templateSrv.updateIndex();
|
||||
this.dashboard.updateSubmenuVisibility();
|
||||
}
|
||||
|
||||
updateOptions(variable: any) {
|
||||
return variable.updateOptions();
|
||||
}
|
||||
|
||||
variableUpdated(variable: any, emitChangeEvents?: any) {
|
||||
// if there is a variable lock ignore cascading update because we are in a boot up scenario
|
||||
if (variable.initLock) {
|
||||
return this.$q.when();
|
||||
}
|
||||
|
||||
const g = this.createGraph();
|
||||
const node = g.getNode(variable.name);
|
||||
let promises = [];
|
||||
if (node) {
|
||||
promises = node.getOptimizedInputEdges().map(e => {
|
||||
return this.updateOptions(this.variables.find(v => v.name === e.inputNode.name));
|
||||
});
|
||||
}
|
||||
|
||||
return this.$q.all(promises).then(() => {
|
||||
if (emitChangeEvents) {
|
||||
this.dashboard.templateVariableValueUpdated();
|
||||
this.dashboard.startRefresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
selectOptionsForCurrentValue(variable: any) {
|
||||
let i, y, value, option;
|
||||
const selected: any = [];
|
||||
|
||||
for (i = 0; i < variable.options.length; i++) {
|
||||
option = variable.options[i];
|
||||
option.selected = false;
|
||||
if (_.isArray(variable.current.value)) {
|
||||
for (y = 0; y < variable.current.value.length; y++) {
|
||||
value = variable.current.value[y];
|
||||
if (option.value === value) {
|
||||
option.selected = true;
|
||||
selected.push(option);
|
||||
}
|
||||
}
|
||||
} else if (option.value === variable.current.value) {
|
||||
option.selected = true;
|
||||
selected.push(option);
|
||||
}
|
||||
}
|
||||
|
||||
return selected;
|
||||
}
|
||||
|
||||
validateVariableSelectionState(variable: any, defaultValue?: string) {
|
||||
if (!variable.current) {
|
||||
variable.current = {};
|
||||
}
|
||||
|
||||
if (_.isArray(variable.current.value)) {
|
||||
let selected = this.selectOptionsForCurrentValue(variable);
|
||||
|
||||
// if none pick first
|
||||
if (selected.length === 0) {
|
||||
selected = variable.options[0];
|
||||
} else {
|
||||
selected = {
|
||||
value: _.map(selected, val => {
|
||||
return val.value;
|
||||
}),
|
||||
text: _.map(selected, val => {
|
||||
return val.text;
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
return variable.setValue(selected);
|
||||
} else {
|
||||
let option: any = undefined;
|
||||
|
||||
// 1. find the current value
|
||||
option = _.find(variable.options, {
|
||||
text: variable.current.text,
|
||||
});
|
||||
if (option) {
|
||||
return variable.setValue(option);
|
||||
}
|
||||
|
||||
// 2. find the default value
|
||||
if (defaultValue) {
|
||||
option = _.find(variable.options, {
|
||||
text: defaultValue,
|
||||
});
|
||||
if (option) {
|
||||
return variable.setValue(option);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. use the first value
|
||||
if (variable.options) {
|
||||
return variable.setValue(variable.options[0]);
|
||||
}
|
||||
|
||||
// 4... give up
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current selected option (or options) based on the query params in the url. It is possible for values
|
||||
* in the url to not match current options of the variable. In that case the variables current value will be still set
|
||||
* to that value.
|
||||
* @param variable Instance of Variable
|
||||
* @param urlValue Value of the query parameter
|
||||
*/
|
||||
setOptionFromUrl(variable: any, urlValue: string | string[]): IPromise<any> {
|
||||
let promise = this.$q.when();
|
||||
|
||||
if (variable.refresh) {
|
||||
promise = variable.updateOptions();
|
||||
}
|
||||
|
||||
return promise.then(() => {
|
||||
// Simple case. Value in url matches existing options text or value.
|
||||
let option: any = _.find(variable.options, op => {
|
||||
return op.text === urlValue || op.value === urlValue;
|
||||
});
|
||||
|
||||
// No luck either it is array or value does not exist in the variables options.
|
||||
if (!option) {
|
||||
let defaultText = urlValue;
|
||||
const defaultValue = urlValue;
|
||||
|
||||
if (_.isArray(urlValue)) {
|
||||
// Multiple values in the url. We construct text as a list of texts from all matched options.
|
||||
defaultText = urlValue.reduce((acc, item) => {
|
||||
const t: any = _.find(variable.options, { value: item });
|
||||
if (t) {
|
||||
acc.push(t.text);
|
||||
} else {
|
||||
acc.push(item);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
// It is possible that we did not match the value to any existing option. In that case the url value will be
|
||||
// used anyway for both text and value.
|
||||
option = { text: defaultText, value: defaultValue };
|
||||
}
|
||||
|
||||
if (variable.multi) {
|
||||
// In case variable is multiple choice, we cast to array to preserve the same behaviour as when selecting
|
||||
// the option directly, which will return even single value in an array.
|
||||
option = { text: _.castArray(option.text), value: _.castArray(option.value) };
|
||||
}
|
||||
|
||||
return variable.setValue(option);
|
||||
});
|
||||
}
|
||||
|
||||
setOptionAsCurrent(variable: any, option: any) {
|
||||
variable.current = _.cloneDeep(option);
|
||||
|
||||
if (_.isArray(variable.current.text) && variable.current.text.length > 0) {
|
||||
variable.current.text = variable.current.text.join(' + ');
|
||||
} else if (_.isArray(variable.current.value) && variable.current.value[0] !== '$__all') {
|
||||
variable.current.text = variable.current.value.join(' + ');
|
||||
}
|
||||
|
||||
this.selectOptionsForCurrentValue(variable);
|
||||
return this.variableUpdated(variable);
|
||||
}
|
||||
|
||||
templateVarsChangedInUrl(vars: UrlQueryMap) {
|
||||
const update: Array<Promise<any>> = [];
|
||||
for (const v of this.variables) {
|
||||
const key = `var-${v.name}`;
|
||||
if (vars.hasOwnProperty(key)) {
|
||||
if (this.isVariableUrlValueDifferentFromCurrent(v, vars[key])) {
|
||||
update.push(v.setValueFromUrl(vars[key]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (update.length) {
|
||||
Promise.all(update).then(() => {
|
||||
this.dashboard.templateVariableValueUpdated();
|
||||
this.dashboard.startRefresh();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
isVariableUrlValueDifferentFromCurrent(variable: VariableActions, urlValue: any) {
|
||||
// lodash _.isEqual handles array of value equality checks as well
|
||||
return !_.isEqual(variable.getValueForUrl(), urlValue);
|
||||
}
|
||||
|
||||
updateUrlParamsWithCurrentVariables() {
|
||||
// update url
|
||||
const params = this.$location.search();
|
||||
|
||||
// remove variable params
|
||||
_.each(params, (value, key) => {
|
||||
if (key.indexOf('var-') === 0) {
|
||||
delete params[key];
|
||||
}
|
||||
});
|
||||
|
||||
// add new values
|
||||
this.templateSrv.fillVariableValuesForUrl(params);
|
||||
// update url
|
||||
this.$location.search(params);
|
||||
}
|
||||
|
||||
setAdhocFilter(options: any) {
|
||||
let variable: any = _.find(this.variables, {
|
||||
type: 'adhoc',
|
||||
datasource: options.datasource,
|
||||
} as any);
|
||||
if (!variable) {
|
||||
variable = this.createVariableFromModel({
|
||||
name: 'Filters',
|
||||
type: 'adhoc',
|
||||
datasource: options.datasource,
|
||||
});
|
||||
this.addVariable(variable);
|
||||
}
|
||||
|
||||
const filters = variable.filters;
|
||||
let filter: any = _.find(filters, { key: options.key, value: options.value });
|
||||
|
||||
if (!filter) {
|
||||
filter = { key: options.key, value: options.value };
|
||||
filters.push(filter);
|
||||
}
|
||||
|
||||
filter.operator = options.operator;
|
||||
this.variableUpdated(variable, true);
|
||||
}
|
||||
|
||||
createGraph() {
|
||||
const g = new Graph();
|
||||
|
||||
this.variables.forEach(v => {
|
||||
g.createNode(v.name);
|
||||
});
|
||||
|
||||
this.variables.forEach(v1 => {
|
||||
this.variables.forEach(v2 => {
|
||||
if (v1 === v2) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (v1.dependsOn(v2)) {
|
||||
g.link(v1.name, v2.name);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return g;
|
||||
}
|
||||
}
|
||||
|
||||
coreModule.service('variableSrv', VariableSrv);
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
TextBoxVariableModel,
|
||||
VariableModel,
|
||||
VariableOption,
|
||||
} from '../templating/types';
|
||||
} from './types';
|
||||
import { VariableEditorProps } from './editor/types';
|
||||
import { VariablesState } from './state/variablesReducer';
|
||||
import { VariablePickerProps } from './pickers/types';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { MapDispatchToProps, MapStateToProps } from 'react-redux';
|
||||
|
||||
import { AdHocVariableModel } from '../../templating/types';
|
||||
import { AdHocVariableModel } from '../types';
|
||||
import { VariableEditorProps } from '../editor/types';
|
||||
import { VariableEditorState } from '../editor/reducer';
|
||||
import { AdHocVariableEditorState } from './reducer';
|
||||
|
||||
@@ -20,7 +20,7 @@ import { filterAdded, filterRemoved, filtersRestored, filterUpdated } from './re
|
||||
import { addVariable, changeVariableProp } from '../state/sharedReducer';
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
import { DashboardState, LocationState } from 'app/types';
|
||||
import { VariableModel } from 'app/features/templating/types';
|
||||
import { VariableModel } from 'app/features/variables/types';
|
||||
import { changeVariableEditorExtended, setIdInEditor } from '../editor/reducer';
|
||||
import { adHocBuilder } from '../shared/testing/builders';
|
||||
|
||||
@@ -421,7 +421,7 @@ describe('adhoc actions', () => {
|
||||
const tester = await reduxTester<ReducersUsedInContext>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(createAddVariableAction(variable))
|
||||
.whenActionIsDispatched(setIdInEditor({ id: variable.id! }))
|
||||
.whenActionIsDispatched(setIdInEditor({ id: variable.id }))
|
||||
.whenAsyncActionIsDispatched(changeVariableDatasource(datasource), true);
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
@@ -453,7 +453,7 @@ describe('adhoc actions', () => {
|
||||
const tester = await reduxTester<ReducersUsedInContext>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(createAddVariableAction(variable))
|
||||
.whenActionIsDispatched(setIdInEditor({ id: variable.id! }))
|
||||
.whenActionIsDispatched(setIdInEditor({ id: variable.id }))
|
||||
.whenAsyncActionIsDispatched(changeVariableDatasource(datasource), true);
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
filterUpdated,
|
||||
initialAdHocVariableModelState,
|
||||
} from './reducer';
|
||||
import { AdHocVariableFilter, AdHocVariableModel } from 'app/features/templating/types';
|
||||
import { AdHocVariableFilter, AdHocVariableModel } from 'app/features/variables/types';
|
||||
import { variableUpdated } from '../state/actions';
|
||||
import { isAdHoc } from '../guard';
|
||||
|
||||
@@ -40,11 +40,11 @@ export const applyFilterFromTable = (options: AdHocTableOptions): ThunkResult<vo
|
||||
if (index === -1) {
|
||||
const { value, key, operator } = options;
|
||||
const filter = { value, key, operator, condition: '' };
|
||||
return await dispatch(addFilter(variable.id!, filter));
|
||||
return await dispatch(addFilter(variable.id, filter));
|
||||
}
|
||||
|
||||
const filter = { ...variable.filters[index], operator: options.operator };
|
||||
return await dispatch(changeFilter(variable.id!, { index, filter }));
|
||||
return await dispatch(changeFilter(variable.id, { index, filter }));
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
|
||||
import { AdHocVariableModel } from '../../templating/types';
|
||||
import { AdHocVariableModel } from '../types';
|
||||
import { dispatch } from '../../../store/store';
|
||||
import { VariableAdapter } from '../adapters';
|
||||
import { AdHocPicker } from './picker/AdHocPicker';
|
||||
@@ -24,7 +24,7 @@ export const createAdHocVariableAdapter = (): VariableAdapter<AdHocVariableModel
|
||||
setValue: noop,
|
||||
setValueFromUrl: async (variable, urlValue) => {
|
||||
const filters = urlParser.toFilters(urlValue);
|
||||
await dispatch(setFiltersFromUrl(variable.id!, filters));
|
||||
await dispatch(setFiltersFromUrl(variable.id, filters));
|
||||
},
|
||||
updateOptions: noop,
|
||||
getSaveModel: variable => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { FC, ReactElement, useState } from 'react';
|
||||
import { SegmentAsync, Icon } from '@grafana/ui';
|
||||
import { Icon, SegmentAsync } from '@grafana/ui';
|
||||
import { OperatorSegment } from './OperatorSegment';
|
||||
import { AdHocVariableFilter } from 'app/features/templating/types';
|
||||
import { AdHocVariableFilter } from 'app/features/variables/types';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { PureComponent, ReactNode } from 'react';
|
||||
import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux';
|
||||
import { StoreState } from 'app/types';
|
||||
import { AdHocVariableFilter, AdHocVariableModel } from 'app/features/templating/types';
|
||||
import { AdHocVariableFilter, AdHocVariableModel } from 'app/features/variables/types';
|
||||
import { SegmentAsync } from '@grafana/ui';
|
||||
import { VariablePickerProps } from '../../pickers/types';
|
||||
import { OperatorSegment } from './OperatorSegment';
|
||||
@@ -31,10 +31,10 @@ export class AdHocPickerUnconnected extends PureComponent<Props> {
|
||||
const { value } = key;
|
||||
|
||||
if (key.value === REMOVE_FILTER_KEY) {
|
||||
return this.props.removeFilter(id!, index);
|
||||
return this.props.removeFilter(id, index);
|
||||
}
|
||||
|
||||
return this.props.changeFilter(id!, {
|
||||
return this.props.changeFilter(id, {
|
||||
index,
|
||||
filter: {
|
||||
...filters[index],
|
||||
@@ -45,7 +45,7 @@ export class AdHocPickerUnconnected extends PureComponent<Props> {
|
||||
|
||||
appendFilterToVariable = (filter: AdHocVariableFilter) => {
|
||||
const { id } = this.props.variable;
|
||||
this.props.addFilter(id!, filter);
|
||||
this.props.addFilter(id, filter);
|
||||
};
|
||||
|
||||
fetchFilterKeys = async () => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getVariableTestContext } from '../state/helpers';
|
||||
import { toVariablePayload } from '../state/types';
|
||||
import { adHocVariableReducer, filterAdded, filterRemoved, filtersRestored, filterUpdated } from './reducer';
|
||||
import { VariablesState } from '../state/variablesReducer';
|
||||
import { AdHocVariableFilter, AdHocVariableModel } from '../../templating/types';
|
||||
import { AdHocVariableFilter, AdHocVariableModel } from '../types';
|
||||
import { createAdHocVariableAdapter } from './adapter';
|
||||
|
||||
describe('adHocVariableReducer', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AdHocVariableFilter, AdHocVariableModel, VariableHide } from 'app/features/templating/types';
|
||||
import { AdHocVariableFilter, AdHocVariableModel, VariableHide } from 'app/features/variables/types';
|
||||
import { getInstanceState, NEW_VARIABLE_ID, VariablePayload } from '../state/types';
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { initialVariablesState, VariablesState } from '../state/variablesReducer';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { toFilters, toUrl } from './urlParser';
|
||||
import { AdHocVariableFilter } from 'app/features/templating/types';
|
||||
import { AdHocVariableFilter } from 'app/features/variables/types';
|
||||
import { UrlQueryValue } from '@grafana/data';
|
||||
|
||||
describe('urlParser', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AdHocVariableFilter } from 'app/features/templating/types';
|
||||
import { AdHocVariableFilter } from 'app/features/variables/types';
|
||||
import { UrlQueryValue } from '@grafana/data';
|
||||
import { isArray, isString } from 'lodash';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { ChangeEvent, FocusEvent, PureComponent } from 'react';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
import { ConstantVariableModel } from '../../templating/types';
|
||||
import { ConstantVariableModel } from '../types';
|
||||
import { VariableEditorProps } from '../editor/types';
|
||||
|
||||
export interface Props extends VariableEditorProps<ConstantVariableModel> {}
|
||||
|
||||
@@ -4,10 +4,10 @@ import { reduxTester } from '../../../../test/core/redux/reduxTester';
|
||||
import { TemplatingState } from 'app/features/variables/state/reducers';
|
||||
import { updateConstantVariableOptions } from './actions';
|
||||
import { getRootReducer } from '../state/helpers';
|
||||
import { ConstantVariableModel, VariableHide, VariableOption } from '../../templating/types';
|
||||
import { ConstantVariableModel, VariableHide, VariableOption } from '../types';
|
||||
import { toVariablePayload } from '../state/types';
|
||||
import { createConstantOptionsFromQuery } from './reducer';
|
||||
import { setCurrentVariableValue, addVariable } from '../state/sharedReducer';
|
||||
import { addVariable, setCurrentVariableValue } from '../state/sharedReducer';
|
||||
|
||||
describe('constant actions', () => {
|
||||
variableAdapters.setInit(() => [createConstantVariableAdapter()]);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { ConstantVariableModel } from '../../templating/types';
|
||||
import { ConstantVariableModel } from '../types';
|
||||
import { dispatch } from '../../../store/store';
|
||||
import { setOptionAsCurrent, setOptionFromUrl } from '../state/actions';
|
||||
import { VariableAdapter } from '../adapters';
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getVariableTestContext } from '../state/helpers';
|
||||
import { toVariablePayload } from '../state/types';
|
||||
import { constantVariableReducer, createConstantOptionsFromQuery } from './reducer';
|
||||
import { VariablesState } from '../state/variablesReducer';
|
||||
import { ConstantVariableModel } from '../../templating/types';
|
||||
import { ConstantVariableModel } from '../types';
|
||||
import { createConstantVariableAdapter } from './adapter';
|
||||
|
||||
describe('constantVariableReducer', () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { ConstantVariableModel, VariableHide, VariableOption } from '../../templating/types';
|
||||
import { ConstantVariableModel, VariableHide, VariableOption } from '../types';
|
||||
import { getInstanceState, NEW_VARIABLE_ID, VariablePayload } from '../state/types';
|
||||
import { initialVariablesState, VariablesState } from '../state/variablesReducer';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { ChangeEvent, FocusEvent, PureComponent } from 'react';
|
||||
import { CustomVariableModel, VariableWithMultiSupport } from '../../templating/types';
|
||||
import { CustomVariableModel, VariableWithMultiSupport } from '../types';
|
||||
import { SelectionOptionsEditor } from '../editor/SelectionOptionsEditor';
|
||||
import { OnPropChangeArguments, VariableEditorProps } from '../editor/types';
|
||||
import { connectWithStore } from 'app/core/utils/connectWithReduxStore';
|
||||
|
||||
@@ -3,9 +3,9 @@ import { updateCustomVariableOptions } from './actions';
|
||||
import { createCustomVariableAdapter } from './adapter';
|
||||
import { reduxTester } from '../../../../test/core/redux/reduxTester';
|
||||
import { getRootReducer } from '../state/helpers';
|
||||
import { CustomVariableModel, VariableHide, VariableOption } from '../../templating/types';
|
||||
import { CustomVariableModel, VariableHide, VariableOption } from '../types';
|
||||
import { toVariablePayload } from '../state/types';
|
||||
import { setCurrentVariableValue, addVariable } from '../state/sharedReducer';
|
||||
import { addVariable, setCurrentVariableValue } from '../state/sharedReducer';
|
||||
import { TemplatingState } from '../state/reducers';
|
||||
import { createCustomOptionsFromQuery } from './reducer';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { CustomVariableModel } from '../../templating/types';
|
||||
import { CustomVariableModel } from '../types';
|
||||
import { dispatch } from '../../../store/store';
|
||||
import { setOptionAsCurrent, setOptionFromUrl } from '../state/actions';
|
||||
import { VariableAdapter } from '../adapters';
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE, toVariablePayload } from '../sta
|
||||
import { createCustomOptionsFromQuery, customVariableReducer } from './reducer';
|
||||
import { createCustomVariableAdapter } from './adapter';
|
||||
import { VariablesState } from '../state/variablesReducer';
|
||||
import { CustomVariableModel } from '../../templating/types';
|
||||
import { CustomVariableModel } from '../types';
|
||||
|
||||
describe('customVariableReducer', () => {
|
||||
const adapter = createCustomVariableAdapter();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
import { CustomVariableModel, VariableHide, VariableOption } from '../../templating/types';
|
||||
import { CustomVariableModel, VariableHide, VariableOption } from '../types';
|
||||
import {
|
||||
ALL_VARIABLE_TEXT,
|
||||
ALL_VARIABLE_VALUE,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { ChangeEvent, FocusEvent, PureComponent } from 'react';
|
||||
|
||||
import { DataSourceVariableModel, VariableWithMultiSupport } from '../../templating/types';
|
||||
import { DataSourceVariableModel, VariableWithMultiSupport } from '../types';
|
||||
import { OnPropChangeArguments, VariableEditorProps } from '../editor/types';
|
||||
import { SelectionOptionsEditor } from '../editor/SelectionOptionsEditor';
|
||||
import { InlineFormLabel } from '@grafana/ui';
|
||||
|
||||
@@ -5,7 +5,7 @@ import { validateVariableSelectionState } from '../state/actions';
|
||||
import { DataSourceSelectItem, stringToJsRegex } from '@grafana/data';
|
||||
import { getDatasourceSrv } from '../../plugins/datasource_srv';
|
||||
import { getVariable } from '../state/selectors';
|
||||
import { DataSourceVariableModel } from '../../templating/types';
|
||||
import { DataSourceVariableModel } from '../types';
|
||||
import templateSrv from '../../templating/template_srv';
|
||||
import _ from 'lodash';
|
||||
import { changeVariableEditorExtended } from '../editor/reducer';
|
||||
@@ -19,7 +19,7 @@ export const updateDataSourceVariableOptions = (
|
||||
dependencies: DataSourceVariableActionDependencies = { getDatasourceSrv: getDatasourceSrv }
|
||||
): ThunkResult<void> => async (dispatch, getState) => {
|
||||
const sources = await dependencies.getDatasourceSrv().getMetricSources({ skipVariables: true });
|
||||
const variableInState = getVariable<DataSourceVariableModel>(identifier.id!, getState());
|
||||
const variableInState = getVariable<DataSourceVariableModel>(identifier.id, getState());
|
||||
let regex;
|
||||
|
||||
if (variableInState.regex) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { DataSourceVariableModel } from '../../templating/types';
|
||||
import { DataSourceVariableModel } from '../types';
|
||||
import { dispatch } from '../../../store/store';
|
||||
import { setOptionAsCurrent, setOptionFromUrl } from '../state/actions';
|
||||
import { VariableAdapter } from '../adapters';
|
||||
@@ -8,7 +8,7 @@ import { OptionsPicker } from '../pickers';
|
||||
import { ALL_VARIABLE_TEXT, toVariableIdentifier } from '../state/types';
|
||||
import { DataSourceVariableEditor } from './DataSourceVariableEditor';
|
||||
import { updateDataSourceVariableOptions } from './actions';
|
||||
import { containsVariable } from '../../templating/utils';
|
||||
import { containsVariable } from '../utils';
|
||||
|
||||
export const createDataSourceVariableAdapter = (): VariableAdapter<DataSourceVariableModel> => {
|
||||
return {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { reducerTester } from '../../../../test/core/redux/reducerTester';
|
||||
import { VariablesState } from '../state/variablesReducer';
|
||||
import { createDataSourceOptions, dataSourceVariableReducer } from './reducer';
|
||||
import { DataSourceVariableModel } from '../../templating/types';
|
||||
import { DataSourceVariableModel } from '../types';
|
||||
import { getVariableTestContext } from '../state/helpers';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { createDataSourceVariableAdapter } from './adapter';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { DataSourceVariableModel, VariableHide, VariableOption, VariableRefresh } from '../../templating/types';
|
||||
import { DataSourceVariableModel, VariableHide, VariableOption, VariableRefresh } from '../types';
|
||||
import {
|
||||
ALL_VARIABLE_TEXT,
|
||||
ALL_VARIABLE_VALUE,
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { FunctionComponent, useCallback } from 'react';
|
||||
import { LegacyForms } from '@grafana/ui';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
import { VariableWithMultiSupport } from '../../templating/types';
|
||||
import { VariableWithMultiSupport } from '../types';
|
||||
import { VariableEditorProps } from './types';
|
||||
import { toVariableIdentifier, VariableIdentifier } from '../state/types';
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { VariableEditorEditor } from './VariableEditorEditor';
|
||||
import { MapDispatchToProps, MapStateToProps } from 'react-redux';
|
||||
import { connectWithStore } from '../../../core/utils/connectWithReduxStore';
|
||||
import { getVariables } from '../state/selectors';
|
||||
import { VariableModel } from '../../templating/types';
|
||||
import { VariableModel } from '../types';
|
||||
import { switchToEditMode, switchToListMode, switchToNewMode } from './actions';
|
||||
import { changeVariableOrder, duplicateVariable, removeVariable } from '../state/sharedReducer';
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
import { variableAdapters } from '../adapters';
|
||||
import { NEW_VARIABLE_ID, toVariablePayload, VariableIdentifier } from '../state/types';
|
||||
import { VariableHide, VariableModel } from '../../templating/types';
|
||||
import { VariableHide, VariableModel } from '../types';
|
||||
import { appEvents } from '../../../core/core';
|
||||
import { VariableValuesPreview } from './VariableValuesPreview';
|
||||
import { changeVariableName, onEditorAdd, onEditorUpdate, variableEditorMount, variableEditorUnMount } from './actions';
|
||||
|
||||
@@ -3,7 +3,7 @@ import { IconButton } from '@grafana/ui';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
import EmptyListCTA from '../../../core/components/EmptyListCTA/EmptyListCTA';
|
||||
import { QueryVariableModel, VariableModel } from '../../templating/types';
|
||||
import { QueryVariableModel, VariableModel } from '../types';
|
||||
import { toVariableIdentifier, VariableIdentifier } from '../state/types';
|
||||
|
||||
export interface Props {
|
||||
@@ -28,7 +28,7 @@ export class VariableEditorList extends PureComponent<Props> {
|
||||
|
||||
onChangeVariableOrder = (event: MouseEvent, variable: VariableModel, moveType: MoveType) => {
|
||||
event.preventDefault();
|
||||
this.props.onChangeVariableOrder(toVariableIdentifier(variable), variable.index!, variable.index! + moveType);
|
||||
this.props.onChangeVariableOrder(toVariableIdentifier(variable), variable.index, variable.index + moveType);
|
||||
};
|
||||
|
||||
onDuplicateVariable = (event: MouseEvent, identifier: VariableIdentifier) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { VariableModel, VariableOption, VariableWithOptions } from '../../templating/types';
|
||||
import { VariableModel, VariableOption, VariableWithOptions } from '../types';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
export interface VariableValuesPreviewProps {
|
||||
|
||||
@@ -22,7 +22,7 @@ import { addVariable, removeVariable, storeNewVariable } from '../state/sharedRe
|
||||
|
||||
export const variableEditorMount = (identifier: VariableIdentifier): ThunkResult<void> => {
|
||||
return async dispatch => {
|
||||
dispatch(variableEditorMounted({ name: getVariable(identifier.id!).name }));
|
||||
dispatch(variableEditorMounted({ name: getVariable(identifier.id).name }));
|
||||
};
|
||||
};
|
||||
|
||||
@@ -37,7 +37,7 @@ export const variableEditorUnMount = (identifier: VariableIdentifier): ThunkResu
|
||||
|
||||
export const onEditorUpdate = (identifier: VariableIdentifier): ThunkResult<void> => {
|
||||
return async (dispatch, getState) => {
|
||||
const variableInState = getVariable(identifier.id!, getState());
|
||||
const variableInState = getVariable(identifier.id, getState());
|
||||
await variableAdapters.get(variableInState.type).updateOptions(variableInState);
|
||||
dispatch(switchToListMode());
|
||||
};
|
||||
@@ -101,8 +101,8 @@ export const completeChangeVariableName = (identifier: VariableIdentifier, newNa
|
||||
) => {
|
||||
const originalVariable = getVariable(identifier.id, getState());
|
||||
const model = { ...cloneDeep(originalVariable), name: newName, id: newName };
|
||||
const global = originalVariable.global!; // global is undefined because of old variable system
|
||||
const index = originalVariable.index!; // index is undefined because of old variable system
|
||||
const global = originalVariable.global;
|
||||
const index = originalVariable.index;
|
||||
const renamedIdentifier = toVariableIdentifier(model);
|
||||
|
||||
dispatch(addVariable(toVariablePayload(renamedIdentifier, { global, index, model })));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { VariableModel } from '../../templating/types';
|
||||
import { VariableModel } from '../types';
|
||||
|
||||
export interface OnPropChangeArguments<Model extends VariableModel = VariableModel> {
|
||||
propName: keyof Model;
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { QueryVariableModel, VariableModel, AdHocVariableModel, VariableWithMultiSupport } from '../templating/types';
|
||||
import {
|
||||
AdHocVariableModel,
|
||||
ConstantVariableModel,
|
||||
QueryVariableModel,
|
||||
VariableModel,
|
||||
VariableWithMultiSupport,
|
||||
} from './types';
|
||||
|
||||
export const isQuery = (model: VariableModel): model is QueryVariableModel => {
|
||||
return model.type === 'query';
|
||||
@@ -8,6 +14,10 @@ export const isAdHoc = (model: VariableModel): model is AdHocVariableModel => {
|
||||
return model.type === 'adhoc';
|
||||
};
|
||||
|
||||
export const isConstant = (model: VariableModel): model is ConstantVariableModel => {
|
||||
return model.type === 'constant';
|
||||
};
|
||||
|
||||
export const isMulti = (model: VariableModel): model is VariableWithMultiSupport => {
|
||||
const withMulti = model as VariableWithMultiSupport;
|
||||
return withMulti.hasOwnProperty('multi') && typeof withMulti.multi === 'boolean';
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React, { ChangeEvent, FocusEvent, PureComponent } from 'react';
|
||||
|
||||
import { IntervalVariableModel } from '../../templating/types';
|
||||
import { IntervalVariableModel } from '../types';
|
||||
import { VariableEditorProps } from '../editor/types';
|
||||
import { InlineFormLabel, LegacyForms } from '@grafana/ui';
|
||||
|
||||
const { Switch } = LegacyForms;
|
||||
|
||||
export interface Props extends VariableEditorProps<IntervalVariableModel> {}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ThunkResult } from '../../../types';
|
||||
import { createIntervalOptions } from './reducer';
|
||||
import { validateVariableSelectionState } from '../state/actions';
|
||||
import { getVariable } from '../state/selectors';
|
||||
import { IntervalVariableModel } from '../../templating/types';
|
||||
import { IntervalVariableModel } from '../types';
|
||||
import kbn from '../../../core/utils/kbn';
|
||||
import { getTimeSrv } from '../../dashboard/services/TimeSrv';
|
||||
import templateSrv from '../../templating/template_srv';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IntervalVariableModel } from '../../templating/types';
|
||||
import { IntervalVariableModel } from '../types';
|
||||
import { dispatch } from '../../../store/store';
|
||||
import { setOptionAsCurrent, setOptionFromUrl } from '../state/actions';
|
||||
import { VariableAdapter } from '../adapters';
|
||||
|
||||
@@ -3,7 +3,7 @@ import cloneDeep from 'lodash/cloneDeep';
|
||||
import { getVariableTestContext } from '../state/helpers';
|
||||
import { toVariablePayload } from '../state/types';
|
||||
import { createIntervalVariableAdapter } from './adapter';
|
||||
import { IntervalVariableModel } from '../../templating/types';
|
||||
import { IntervalVariableModel } from '../types';
|
||||
import { reducerTester } from '../../../../test/core/redux/reducerTester';
|
||||
import { VariablesState } from '../state/variablesReducer';
|
||||
import { createIntervalOptions, intervalVariableReducer } from './reducer';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user