mirror of https://github.com/grafana/grafana
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 componentDidMountpull/25364/head
parent
6b4d1dceb0
commit
00a9af00fc
@ -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,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,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'; |
||||
@ -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,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> |
||||
@ -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); |
||||
@ -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); |
||||
@ -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,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); |
||||
} |
||||
} |
||||
@ -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); |
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue