Commit 38caa80a by Torkel Ödegaard Committed by GitHub

DataSourceSrv: Look up data source by uid and name transparently (#29449)

* Datasources: Look up data source by uid or name transparently

* comment tweak

* Removed config

* fixed type issues

* Initialize datasource srv

* Added deprecation notice

* Renamed getSettingsFor to getInstanceSettings

* fixed ts issue
parent b7aa6fed
...@@ -17,6 +17,7 @@ export interface DataSourceSrv { ...@@ -17,6 +17,7 @@ export interface DataSourceSrv {
/** /**
* Returns metadata based on UID. * Returns metadata based on UID.
* @deprecated use getInstanceSettings
*/ */
getDataSourceSettingsByUid(uid: string): DataSourceInstanceSettings | undefined; getDataSourceSettingsByUid(uid: string): DataSourceInstanceSettings | undefined;
...@@ -29,6 +30,11 @@ export interface DataSourceSrv { ...@@ -29,6 +30,11 @@ export interface DataSourceSrv {
* Get all data sources except for internal ones that usually should not be listed like mixed data source. * Get all data sources except for internal ones that usually should not be listed like mixed data source.
*/ */
getExternal(): DataSourceInstanceSettings[]; getExternal(): DataSourceInstanceSettings[];
/**
* Get settings and plugin metadata by name or uid
*/
getInstanceSettings(nameOrUid: string | null | undefined): DataSourceInstanceSettings | undefined;
} }
let singletonInstance: DataSourceSrv; let singletonInstance: DataSourceSrv;
......
...@@ -19,19 +19,10 @@ const backendSrv = ({ ...@@ -19,19 +19,10 @@ const backendSrv = ({
jest.mock('../services', () => ({ jest.mock('../services', () => ({
getBackendSrv: () => backendSrv, getBackendSrv: () => backendSrv,
})); getDataSourceSrv: () => {
jest.mock('..', () => ({ return {
config: { getInstanceSettings: () => ({ id: 8674 }),
bootData: { };
user: {
orgId: 77,
},
},
datasources: {
sample: {
id: 8674,
},
},
}, },
})); }));
...@@ -46,6 +37,7 @@ describe('DataSourceWithBackend', () => { ...@@ -46,6 +37,7 @@ describe('DataSourceWithBackend', () => {
mockDatasourceRequest.mockReset(); mockDatasourceRequest.mockReset();
mockDatasourceRequest.mockReturnValue(Promise.resolve({})); mockDatasourceRequest.mockReturnValue(Promise.resolve({}));
const ds = new MyDataSource(settings); const ds = new MyDataSource(settings);
ds.query({ ds.query({
maxDataPoints: 10, maxDataPoints: 10,
intervalMs: 5000, intervalMs: 5000,
...@@ -64,7 +56,6 @@ describe('DataSourceWithBackend', () => { ...@@ -64,7 +56,6 @@ describe('DataSourceWithBackend', () => {
"datasourceId": 1234, "datasourceId": 1234,
"intervalMs": 5000, "intervalMs": 5000,
"maxDataPoints": 10, "maxDataPoints": 10,
"orgId": 77,
"refId": "A", "refId": "A",
}, },
Object { Object {
...@@ -72,7 +63,6 @@ describe('DataSourceWithBackend', () => { ...@@ -72,7 +63,6 @@ describe('DataSourceWithBackend', () => {
"datasourceId": 8674, "datasourceId": 8674,
"intervalMs": 5000, "intervalMs": 5000,
"maxDataPoints": 10, "maxDataPoints": 10,
"orgId": 77,
"refId": "B", "refId": "B",
}, },
], ],
......
...@@ -9,8 +9,7 @@ import { ...@@ -9,8 +9,7 @@ import {
} from '@grafana/data'; } from '@grafana/data';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { map, catchError } from 'rxjs/operators'; import { map, catchError } from 'rxjs/operators';
import { config } from '..'; import { getBackendSrv, getDataSourceSrv } from '../services';
import { getBackendSrv } from '../services';
import { toDataQueryResponse } from './queryResponse'; import { toDataQueryResponse } from './queryResponse';
const ExpressionDatasourceID = '__expr__'; const ExpressionDatasourceID = '__expr__';
...@@ -57,34 +56,37 @@ export class DataSourceWithBackend< ...@@ -57,34 +56,37 @@ export class DataSourceWithBackend<
*/ */
query(request: DataQueryRequest<TQuery>): Observable<DataQueryResponse> { query(request: DataQueryRequest<TQuery>): Observable<DataQueryResponse> {
const { intervalMs, maxDataPoints, range, requestId } = request; const { intervalMs, maxDataPoints, range, requestId } = request;
const orgId = config.bootData.user.orgId;
let targets = request.targets; let targets = request.targets;
if (this.filterQuery) { if (this.filterQuery) {
targets = targets.filter(q => this.filterQuery!(q)); targets = targets.filter(q => this.filterQuery!(q));
} }
const queries = targets.map(q => { const queries = targets.map(q => {
let datasourceId = this.id; let datasourceId = this.id;
if (q.datasource === ExpressionDatasourceID) { if (q.datasource === ExpressionDatasourceID) {
return { return {
...q, ...q,
datasourceId, datasourceId,
orgId,
}; };
} }
if (q.datasource) { if (q.datasource) {
const dsName = q.datasource === 'default' ? config.defaultDatasource : q.datasource; const ds = getDataSourceSrv().getInstanceSettings(q.datasource);
const ds = config.datasources[dsName];
if (!ds) { if (!ds) {
throw new Error('Unknown Datasource: ' + q.datasource); throw new Error('Unknown Datasource: ' + q.datasource);
} }
datasourceId = ds.id; datasourceId = ds.id;
} }
return { return {
...this.applyTemplateVariables(q, request.scopedVars), ...this.applyTemplateVariables(q, request.scopedVars),
datasourceId, datasourceId,
intervalMs, intervalMs,
maxDataPoints, maxDataPoints,
orgId,
}; };
}); });
...@@ -93,9 +95,8 @@ export class DataSourceWithBackend< ...@@ -93,9 +95,8 @@ export class DataSourceWithBackend<
return of({ data: [] }); return of({ data: [] });
} }
const body: any = { const body: any = { queries };
queries,
};
if (range) { if (range) {
body.range = range; body.range = range;
body.from = range.from.valueOf().toString(); body.from = range.from.valueOf().toString();
......
...@@ -26,6 +26,7 @@ describe('getAlertingValidationMessage', () => { ...@@ -26,6 +26,7 @@ describe('getAlertingValidationMessage', () => {
getExternal(): DataSourceInstanceSettings[] { getExternal(): DataSourceInstanceSettings[] {
return []; return [];
}, },
getInstanceSettings: (() => {}) as any,
getAll(): DataSourceInstanceSettings[] { getAll(): DataSourceInstanceSettings[] {
return []; return [];
}, },
...@@ -66,6 +67,7 @@ describe('getAlertingValidationMessage', () => { ...@@ -66,6 +67,7 @@ describe('getAlertingValidationMessage', () => {
return Promise.resolve(alertingDatasource); return Promise.resolve(alertingDatasource);
}, },
getDataSourceSettingsByUid(): any {}, getDataSourceSettingsByUid(): any {},
getInstanceSettings: (() => {}) as any,
getExternal(): DataSourceInstanceSettings[] { getExternal(): DataSourceInstanceSettings[] {
return []; return [];
}, },
...@@ -96,6 +98,7 @@ describe('getAlertingValidationMessage', () => { ...@@ -96,6 +98,7 @@ describe('getAlertingValidationMessage', () => {
const datasourceSrv: DataSourceSrv = { const datasourceSrv: DataSourceSrv = {
get: getMock, get: getMock,
getDataSourceSettingsByUid(): any {}, getDataSourceSettingsByUid(): any {},
getInstanceSettings: (() => {}) as any,
getExternal(): DataSourceInstanceSettings[] { getExternal(): DataSourceInstanceSettings[] {
return []; return [];
}, },
...@@ -128,6 +131,7 @@ describe('getAlertingValidationMessage', () => { ...@@ -128,6 +131,7 @@ describe('getAlertingValidationMessage', () => {
const datasourceSrv: DataSourceSrv = { const datasourceSrv: DataSourceSrv = {
get: getMock, get: getMock,
getDataSourceSettingsByUid(): any {}, getDataSourceSettingsByUid(): any {},
getInstanceSettings: (() => {}) as any,
getExternal(): DataSourceInstanceSettings[] { getExternal(): DataSourceInstanceSettings[] {
return []; return [];
}, },
...@@ -160,6 +164,7 @@ describe('getAlertingValidationMessage', () => { ...@@ -160,6 +164,7 @@ describe('getAlertingValidationMessage', () => {
const datasourceSrv: DataSourceSrv = { const datasourceSrv: DataSourceSrv = {
get: getMock, get: getMock,
getDataSourceSettingsByUid(): any {}, getDataSourceSettingsByUid(): any {},
getInstanceSettings: (() => {}) as any,
getExternal(): DataSourceInstanceSettings[] { getExternal(): DataSourceInstanceSettings[] {
return []; return [];
}, },
......
...@@ -214,7 +214,7 @@ function updateFrontendSettings() { ...@@ -214,7 +214,7 @@ function updateFrontendSettings() {
.then((settings: any) => { .then((settings: any) => {
config.datasources = settings.datasources; config.datasources = settings.datasources;
config.defaultDatasource = settings.defaultDatasource; config.defaultDatasource = settings.defaultDatasource;
getDatasourceSrv().init(); getDatasourceSrv().init(config.datasources, settings.defaultDatasource);
}); });
} }
......
...@@ -82,7 +82,9 @@ export class GrafanaLiveDataSourceScope extends GrafanaLiveScope { ...@@ -82,7 +82,9 @@ export class GrafanaLiveDataSourceScope extends GrafanaLiveScope {
if (this.names) { if (this.names) {
return Promise.resolve(this.names); return Promise.resolve(this.names);
} }
const names: Array<SelectableValue<string>> = []; const names: Array<SelectableValue<string>> = [];
for (const [key, ds] of Object.entries(config.datasources)) { for (const [key, ds] of Object.entries(config.datasources)) {
if (ds.meta.live) { if (ds.meta.live) {
try { try {
...@@ -99,6 +101,7 @@ export class GrafanaLiveDataSourceScope extends GrafanaLiveScope { ...@@ -99,6 +101,7 @@ export class GrafanaLiveDataSourceScope extends GrafanaLiveScope {
} }
} }
} }
return (this.names = names); return (this.names = names);
} }
} }
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
import sortBy from 'lodash/sortBy'; import sortBy from 'lodash/sortBy';
import coreModule from 'app/core/core_module'; import coreModule from 'app/core/core_module';
// Services & Utils // Services & Utils
import config from 'app/core/config';
import { importDataSourcePlugin } from './plugin_loader'; import { importDataSourcePlugin } from './plugin_loader';
import { import {
DataSourceSrv as DataSourceService, DataSourceSrv as DataSourceService,
...@@ -18,47 +17,74 @@ import { expressionDatasource } from 'app/features/expressions/ExpressionDatasou ...@@ -18,47 +17,74 @@ import { expressionDatasource } from 'app/features/expressions/ExpressionDatasou
import { DataSourceVariableModel } from '../variables/types'; import { DataSourceVariableModel } from '../variables/types';
export class DatasourceSrv implements DataSourceService { export class DatasourceSrv implements DataSourceService {
datasources: Record<string, DataSourceApi> = {}; private datasources: Record<string, DataSourceApi> = {};
private settingsMapByName: Record<string, DataSourceInstanceSettings> = {};
private settingsMapByUid: Record<string, DataSourceInstanceSettings> = {};
private defaultName = '';
/** @ngInject */ /** @ngInject */
constructor( constructor(
private $injector: auto.IInjectorService, private $injector: auto.IInjectorService,
private $rootScope: GrafanaRootScope, private $rootScope: GrafanaRootScope,
private templateSrv: TemplateSrv private templateSrv: TemplateSrv
) { ) {}
this.init();
}
init() { init(settingsMapByName: Record<string, DataSourceInstanceSettings>, defaultName: string) {
this.datasources = {}; this.datasources = {};
this.settingsMapByUid = {};
this.settingsMapByName = settingsMapByName;
this.defaultName = defaultName;
for (const dsSettings of Object.values(settingsMapByName)) {
this.settingsMapByUid[dsSettings.uid] = dsSettings;
}
} }
getDataSourceSettingsByUid(uid: string): DataSourceInstanceSettings | undefined { getDataSourceSettingsByUid(uid: string): DataSourceInstanceSettings | undefined {
return Object.values(config.datasources).find(ds => ds.uid === uid); return this.settingsMapByUid[uid];
} }
get(name?: string | null, scopedVars?: ScopedVars): Promise<DataSourceApi> { getInstanceSettings(nameOrUid: string | null | undefined): DataSourceInstanceSettings | undefined {
if (!name) { if (nameOrUid === 'default' || nameOrUid === null || nameOrUid === undefined) {
return this.get(config.defaultDatasource); return this.settingsMapByName[this.defaultName];
}
return this.settingsMapByUid[nameOrUid] ?? this.settingsMapByName[nameOrUid];
}
get(nameOrUid?: string | null, scopedVars?: ScopedVars): Promise<DataSourceApi> {
if (!nameOrUid) {
return this.get(this.defaultName);
}
// Check if nameOrUid matches a uid and then get the name
const byUid = this.settingsMapByUid[nameOrUid];
if (byUid) {
nameOrUid = byUid.name;
}
// This check is duplicated below, this is here mainly as performance optimization to skip interpolation
if (this.datasources[nameOrUid]) {
return Promise.resolve(this.datasources[nameOrUid]);
} }
// Interpolation here is to support template variable in data source selection // Interpolation here is to support template variable in data source selection
name = this.templateSrv.replace(name, scopedVars, (value: any[]) => { nameOrUid = this.templateSrv.replace(nameOrUid, scopedVars, (value: any[]) => {
if (Array.isArray(value)) { if (Array.isArray(value)) {
return value[0]; return value[0];
} }
return value; return value;
}); });
if (name === 'default') { if (nameOrUid === 'default') {
return this.get(config.defaultDatasource); return this.get(this.defaultName);
} }
if (this.datasources[name]) { if (this.datasources[nameOrUid]) {
return Promise.resolve(this.datasources[name]); return Promise.resolve(this.datasources[nameOrUid]);
} }
return this.loadDatasource(name); return this.loadDatasource(nameOrUid);
} }
async loadDatasource(name: string): Promise<DataSourceApi<any, any>> { async loadDatasource(name: string): Promise<DataSourceApi<any, any>> {
...@@ -68,7 +94,7 @@ export class DatasourceSrv implements DataSourceService { ...@@ -68,7 +94,7 @@ export class DatasourceSrv implements DataSourceService {
return Promise.resolve(expressionDatasource); return Promise.resolve(expressionDatasource);
} }
const dsConfig = config.datasources[name]; const dsConfig = this.settingsMapByName[name];
if (!dsConfig) { if (!dsConfig) {
return Promise.reject({ message: `Datasource named ${name} was not found` }); return Promise.reject({ message: `Datasource named ${name} was not found` });
} }
...@@ -101,8 +127,7 @@ export class DatasourceSrv implements DataSourceService { ...@@ -101,8 +127,7 @@ export class DatasourceSrv implements DataSourceService {
} }
getAll(): DataSourceInstanceSettings[] { getAll(): DataSourceInstanceSettings[] {
const { datasources } = config; return Object.values(this.settingsMapByName);
return Object.keys(datasources).map(name => datasources[name]);
} }
getExternal(): DataSourceInstanceSettings[] { getExternal(): DataSourceInstanceSettings[] {
...@@ -115,7 +140,7 @@ export class DatasourceSrv implements DataSourceService { ...@@ -115,7 +140,7 @@ export class DatasourceSrv implements DataSourceService {
this.addDataSourceVariables(sources); this.addDataSourceVariables(sources);
Object.values(config.datasources).forEach(value => { Object.values(this.settingsMapByName).forEach(value => {
if (value.meta?.annotations) { if (value.meta?.annotations) {
sources.push(value); sources.push(value);
} }
...@@ -127,7 +152,7 @@ export class DatasourceSrv implements DataSourceService { ...@@ -127,7 +152,7 @@ export class DatasourceSrv implements DataSourceService {
getMetricSources(options?: { skipVariables?: boolean }) { getMetricSources(options?: { skipVariables?: boolean }) {
const metricSources: DataSourceSelectItem[] = []; const metricSources: DataSourceSelectItem[] = [];
Object.entries(config.datasources).forEach(([key, value]) => { Object.entries(this.settingsMapByName).forEach(([key, value]) => {
if (value.meta?.metrics) { if (value.meta?.metrics) {
let metricSource: DataSourceSelectItem = { value: key, name: key, meta: value.meta, sort: key }; let metricSource: DataSourceSelectItem = { value: key, name: key, meta: value.meta, sort: key };
...@@ -142,7 +167,7 @@ export class DatasourceSrv implements DataSourceService { ...@@ -142,7 +167,7 @@ export class DatasourceSrv implements DataSourceService {
metricSources.push(metricSource); metricSources.push(metricSource);
if (key === config.defaultDatasource) { if (key === this.defaultName) {
metricSource = { value: null, name: 'default', meta: value.meta, sort: key }; metricSource = { value: null, name: 'default', meta: value.meta, sort: key };
metricSources.push(metricSource); metricSources.push(metricSource);
} }
...@@ -172,9 +197,9 @@ export class DatasourceSrv implements DataSourceService { ...@@ -172,9 +197,9 @@ export class DatasourceSrv implements DataSourceService {
.getVariables() .getVariables()
.filter(variable => variable.type === 'datasource') .filter(variable => variable.type === 'datasource')
.forEach((variable: DataSourceVariableModel) => { .forEach((variable: DataSourceVariableModel) => {
const first = variable.current.value === 'default' ? config.defaultDatasource : variable.current.value; const first = variable.current.value === 'default' ? this.defaultName : variable.current.value;
const index = (first as unknown) as string; const index = (first as unknown) as string;
const ds = config.datasources[index]; const ds = this.settingsMapByName[index];
if (ds) { if (ds) {
const key = `$${variable.name}`; const key = `$${variable.name}`;
......
import config from 'app/core/config';
import 'app/features/plugins/datasource_srv'; import 'app/features/plugins/datasource_srv';
import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
import { DataSourcePluginMeta, PluginMeta } from '@grafana/data'; import { DataSourceInstanceSettings, DataSourcePlugin, DataSourcePluginMeta, PluginMeta } from '@grafana/data';
// Datasource variable $datasource with current value 'BBB' // Datasource variable $datasource with current value 'BBB'
const templateSrv: any = { const templateSrv: any = {
...@@ -14,41 +13,72 @@ const templateSrv: any = { ...@@ -14,41 +13,72 @@ const templateSrv: any = {
}, },
}, },
], ],
replace: (v: string) => v,
}; };
class TestDataSource {
constructor(public instanceSettings: DataSourceInstanceSettings) {}
}
jest.mock('../plugin_loader', () => ({
importDataSourcePlugin: () => {
return Promise.resolve(new DataSourcePlugin(TestDataSource as any));
},
}));
describe('datasource_srv', () => { describe('datasource_srv', () => {
const _datasourceSrv = new DatasourceSrv({} as any, {} as any, templateSrv); const _datasourceSrv = new DatasourceSrv({} as any, {} as any, templateSrv);
const datasources = {
buildIn: {
id: 1,
uid: '1',
type: 'b',
name: 'buildIn',
meta: { builtIn: true } as DataSourcePluginMeta,
jsonData: {},
},
external1: {
id: 2,
uid: '2',
type: 'e',
name: 'external1',
meta: { builtIn: false } as DataSourcePluginMeta,
jsonData: {},
},
external2: {
id: 3,
uid: '3',
type: 'e2',
name: 'external2',
meta: {} as PluginMeta,
jsonData: {},
},
};
describe('when loading external datasources', () => { beforeEach(() => {
beforeEach(() => { _datasourceSrv.init(datasources, 'external1');
config.datasources = { });
buildInDs: {
id: 1, describe('when getting data source class instance', () => {
uid: '1', it('should load plugin and create instance and set meta', async () => {
type: 'b', const ds = (await _datasourceSrv.get('external1')) as any;
name: 'buildIn', expect(ds.meta).toBe(datasources.external1.meta);
meta: { builtIn: true } as DataSourcePluginMeta, expect(ds.instanceSettings).toBe(datasources.external1);
jsonData: {},
}, // validate that it caches instance
nonBuildIn: { const ds2 = await _datasourceSrv.get('external1');
id: 2, expect(ds).toBe(ds2);
uid: '2', });
type: 'e',
name: 'external1', it('should be able to load data source using uid as well', async () => {
meta: { builtIn: false } as DataSourcePluginMeta, const dsByUid = await _datasourceSrv.get('2');
jsonData: {}, const dsByName = await _datasourceSrv.get('external1');
}, expect(dsByUid.meta).toBe(datasources.external1.meta);
nonExplore: { expect(dsByUid).toBe(dsByName);
id: 3,
uid: '3',
type: 'e2',
name: 'external2',
meta: {} as PluginMeta,
jsonData: {},
},
};
}); });
});
describe('when getting external metric sources', () => {
it('should return list of explore sources', () => { it('should return list of explore sources', () => {
const externalSources = _datasourceSrv.getExternal(); const externalSources = _datasourceSrv.getExternal();
expect(externalSources.length).toBe(2); expect(externalSources.length).toBe(2);
...@@ -59,45 +89,48 @@ describe('datasource_srv', () => { ...@@ -59,45 +89,48 @@ describe('datasource_srv', () => {
describe('when loading metric sources', () => { describe('when loading metric sources', () => {
let metricSources: any; let metricSources: any;
const unsortedDatasources = {
mmm: {
type: 'test-db',
meta: { metrics: { m: 1 } },
},
'--Grafana--': {
type: 'grafana',
meta: { builtIn: true, metrics: { m: 1 }, id: 'grafana' },
},
'--Mixed--': {
type: 'test-db',
meta: { builtIn: true, metrics: { m: 1 }, id: 'mixed' },
},
ZZZ: {
type: 'test-db',
meta: { metrics: { m: 1 } },
},
aaa: {
type: 'test-db',
meta: { metrics: { m: 1 } },
},
BBB: {
type: 'test-db',
meta: { metrics: { m: 1 } },
},
};
beforeEach(() => { beforeEach(() => {
config.datasources = unsortedDatasources as any; _datasourceSrv.init(
{
mmm: {
type: 'test-db',
meta: { metrics: true } as any,
},
'--Grafana--': {
type: 'grafana',
meta: { builtIn: true, metrics: true, id: 'grafana' },
},
'--Mixed--': {
type: 'test-db',
meta: { builtIn: true, metrics: true, id: 'mixed' },
},
ZZZ: {
type: 'test-db',
meta: { metrics: true },
},
aaa: {
type: 'test-db',
meta: { metrics: true },
},
BBB: {
type: 'test-db',
meta: { metrics: true },
},
} as any,
'BBB'
);
metricSources = _datasourceSrv.getMetricSources({}); metricSources = _datasourceSrv.getMetricSources({});
config.defaultDatasource = 'BBB';
}); });
it('should return a list of sources sorted case insensitively with builtin sources last', () => { it('should return a list of sources sorted case insensitively with builtin sources last', () => {
expect(metricSources[1].name).toBe('aaa'); expect(metricSources[1].name).toBe('aaa');
expect(metricSources[2].name).toBe('BBB'); expect(metricSources[2].name).toBe('BBB');
expect(metricSources[3].name).toBe('mmm'); expect(metricSources[3].name).toBe('default');
expect(metricSources[4].name).toBe('ZZZ'); expect(metricSources[4].name).toBe('mmm');
expect(metricSources[5].name).toBe('--Grafana--'); expect(metricSources[5].name).toBe('ZZZ');
expect(metricSources[6].name).toBe('--Mixed--'); expect(metricSources[6].name).toBe('--Grafana--');
expect(metricSources[7].name).toBe('--Mixed--');
}); });
it('should set default data source', () => { it('should set default data source', () => {
......
...@@ -62,6 +62,8 @@ export class GrafanaCtrl { ...@@ -62,6 +62,8 @@ export class GrafanaCtrl {
setDashboardSrv(dashboardSrv); setDashboardSrv(dashboardSrv);
setLegacyAngularInjector($injector); setLegacyAngularInjector($injector);
datasourceSrv.init(config.datasources, config.defaultDatasource);
locationUtil.initialize({ locationUtil.initialize({
getConfig: () => config, getConfig: () => config,
getTimeRangeForUrl: getTimeSrv().timeRangeForUrl, getTimeRangeForUrl: getTimeSrv().timeRangeForUrl,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment