diff --git a/packages/grafana-runtime/src/services/dataSourceSrv.ts b/packages/grafana-runtime/src/services/dataSourceSrv.ts index 0abf84952cc..f06d8408a2c 100644 --- a/packages/grafana-runtime/src/services/dataSourceSrv.ts +++ b/packages/grafana-runtime/src/services/dataSourceSrv.ts @@ -17,6 +17,7 @@ export interface DataSourceSrv { /** * Returns metadata based on UID. + * @deprecated use getInstanceSettings */ getDataSourceSettingsByUid(uid: string): DataSourceInstanceSettings | undefined; @@ -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. */ getExternal(): DataSourceInstanceSettings[]; + + /** + * Get settings and plugin metadata by name or uid + */ + getInstanceSettings(nameOrUid: string | null | undefined): DataSourceInstanceSettings | undefined; } let singletonInstance: DataSourceSrv; diff --git a/packages/grafana-runtime/src/utils/DataSourceWithBackend.test.ts b/packages/grafana-runtime/src/utils/DataSourceWithBackend.test.ts index 622fcd4bd93..9cc83b45edb 100644 --- a/packages/grafana-runtime/src/utils/DataSourceWithBackend.test.ts +++ b/packages/grafana-runtime/src/utils/DataSourceWithBackend.test.ts @@ -19,19 +19,10 @@ const backendSrv = ({ jest.mock('../services', () => ({ getBackendSrv: () => backendSrv, -})); -jest.mock('..', () => ({ - config: { - bootData: { - user: { - orgId: 77, - }, - }, - datasources: { - sample: { - id: 8674, - }, - }, + getDataSourceSrv: () => { + return { + getInstanceSettings: () => ({ id: 8674 }), + }; }, })); @@ -46,6 +37,7 @@ describe('DataSourceWithBackend', () => { mockDatasourceRequest.mockReset(); mockDatasourceRequest.mockReturnValue(Promise.resolve({})); const ds = new MyDataSource(settings); + ds.query({ maxDataPoints: 10, intervalMs: 5000, @@ -64,7 +56,6 @@ describe('DataSourceWithBackend', () => { "datasourceId": 1234, "intervalMs": 5000, "maxDataPoints": 10, - "orgId": 77, "refId": "A", }, Object { @@ -72,7 +63,6 @@ describe('DataSourceWithBackend', () => { "datasourceId": 8674, "intervalMs": 5000, "maxDataPoints": 10, - "orgId": 77, "refId": "B", }, ], diff --git a/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts b/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts index 4c8e719a8f7..ebe0b12774d 100644 --- a/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts +++ b/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts @@ -9,8 +9,7 @@ import { } from '@grafana/data'; import { Observable, of } from 'rxjs'; import { map, catchError } from 'rxjs/operators'; -import { config } from '..'; -import { getBackendSrv } from '../services'; +import { getBackendSrv, getDataSourceSrv } from '../services'; import { toDataQueryResponse } from './queryResponse'; const ExpressionDatasourceID = '__expr__'; @@ -57,34 +56,37 @@ export class DataSourceWithBackend< */ query(request: DataQueryRequest): Observable { const { intervalMs, maxDataPoints, range, requestId } = request; - const orgId = config.bootData.user.orgId; let targets = request.targets; + if (this.filterQuery) { targets = targets.filter(q => this.filterQuery!(q)); } + const queries = targets.map(q => { let datasourceId = this.id; + if (q.datasource === ExpressionDatasourceID) { return { ...q, datasourceId, - orgId, }; } + if (q.datasource) { - const dsName = q.datasource === 'default' ? config.defaultDatasource : q.datasource; - const ds = config.datasources[dsName]; + const ds = getDataSourceSrv().getInstanceSettings(q.datasource); + if (!ds) { throw new Error('Unknown Datasource: ' + q.datasource); } + datasourceId = ds.id; } + return { ...this.applyTemplateVariables(q, request.scopedVars), datasourceId, intervalMs, maxDataPoints, - orgId, }; }); @@ -93,9 +95,8 @@ export class DataSourceWithBackend< return of({ data: [] }); } - const body: any = { - queries, - }; + const body: any = { queries }; + if (range) { body.range = range; body.from = range.from.valueOf().toString(); diff --git a/public/app/features/alerting/getAlertingValidationMessage.test.ts b/public/app/features/alerting/getAlertingValidationMessage.test.ts index f98a765975c..57c06cdf507 100644 --- a/public/app/features/alerting/getAlertingValidationMessage.test.ts +++ b/public/app/features/alerting/getAlertingValidationMessage.test.ts @@ -26,6 +26,7 @@ describe('getAlertingValidationMessage', () => { getExternal(): DataSourceInstanceSettings[] { return []; }, + getInstanceSettings: (() => {}) as any, getAll(): DataSourceInstanceSettings[] { return []; }, @@ -66,6 +67,7 @@ describe('getAlertingValidationMessage', () => { return Promise.resolve(alertingDatasource); }, getDataSourceSettingsByUid(): any {}, + getInstanceSettings: (() => {}) as any, getExternal(): DataSourceInstanceSettings[] { return []; }, @@ -96,6 +98,7 @@ describe('getAlertingValidationMessage', () => { const datasourceSrv: DataSourceSrv = { get: getMock, getDataSourceSettingsByUid(): any {}, + getInstanceSettings: (() => {}) as any, getExternal(): DataSourceInstanceSettings[] { return []; }, @@ -128,6 +131,7 @@ describe('getAlertingValidationMessage', () => { const datasourceSrv: DataSourceSrv = { get: getMock, getDataSourceSettingsByUid(): any {}, + getInstanceSettings: (() => {}) as any, getExternal(): DataSourceInstanceSettings[] { return []; }, @@ -160,6 +164,7 @@ describe('getAlertingValidationMessage', () => { const datasourceSrv: DataSourceSrv = { get: getMock, getDataSourceSettingsByUid(): any {}, + getInstanceSettings: (() => {}) as any, getExternal(): DataSourceInstanceSettings[] { return []; }, diff --git a/public/app/features/datasources/state/actions.ts b/public/app/features/datasources/state/actions.ts index 9d66b201791..e09696dd312 100644 --- a/public/app/features/datasources/state/actions.ts +++ b/public/app/features/datasources/state/actions.ts @@ -214,7 +214,7 @@ function updateFrontendSettings() { .then((settings: any) => { config.datasources = settings.datasources; config.defaultDatasource = settings.defaultDatasource; - getDatasourceSrv().init(); + getDatasourceSrv().init(config.datasources, settings.defaultDatasource); }); } diff --git a/public/app/features/live/scopes.ts b/public/app/features/live/scopes.ts index 2097db5c3bd..21a68ce76d5 100644 --- a/public/app/features/live/scopes.ts +++ b/public/app/features/live/scopes.ts @@ -82,7 +82,9 @@ export class GrafanaLiveDataSourceScope extends GrafanaLiveScope { if (this.names) { return Promise.resolve(this.names); } + const names: Array> = []; + for (const [key, ds] of Object.entries(config.datasources)) { if (ds.meta.live) { try { @@ -99,6 +101,7 @@ export class GrafanaLiveDataSourceScope extends GrafanaLiveScope { } } } + return (this.names = names); } } diff --git a/public/app/features/plugins/datasource_srv.ts b/public/app/features/plugins/datasource_srv.ts index d892d171cbd..76d86c6000d 100644 --- a/public/app/features/plugins/datasource_srv.ts +++ b/public/app/features/plugins/datasource_srv.ts @@ -2,7 +2,6 @@ import sortBy from 'lodash/sortBy'; import coreModule from 'app/core/core_module'; // Services & Utils -import config from 'app/core/config'; import { importDataSourcePlugin } from './plugin_loader'; import { DataSourceSrv as DataSourceService, @@ -18,47 +17,74 @@ import { expressionDatasource } from 'app/features/expressions/ExpressionDatasou import { DataSourceVariableModel } from '../variables/types'; export class DatasourceSrv implements DataSourceService { - datasources: Record = {}; + private datasources: Record = {}; + private settingsMapByName: Record = {}; + private settingsMapByUid: Record = {}; + private defaultName = ''; /** @ngInject */ constructor( private $injector: auto.IInjectorService, private $rootScope: GrafanaRootScope, private templateSrv: TemplateSrv - ) { - this.init(); - } + ) {} - init() { + init(settingsMapByName: Record, defaultName: string) { 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 { - return Object.values(config.datasources).find(ds => ds.uid === uid); + return this.settingsMapByUid[uid]; } - get(name?: string | null, scopedVars?: ScopedVars): Promise { - if (!name) { - return this.get(config.defaultDatasource); + getInstanceSettings(nameOrUid: string | null | undefined): DataSourceInstanceSettings | undefined { + if (nameOrUid === 'default' || nameOrUid === null || nameOrUid === undefined) { + return this.settingsMapByName[this.defaultName]; + } + + return this.settingsMapByUid[nameOrUid] ?? this.settingsMapByName[nameOrUid]; + } + + get(nameOrUid?: string | null, scopedVars?: ScopedVars): Promise { + 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 - name = this.templateSrv.replace(name, scopedVars, (value: any[]) => { + nameOrUid = this.templateSrv.replace(nameOrUid, scopedVars, (value: any[]) => { if (Array.isArray(value)) { return value[0]; } return value; }); - if (name === 'default') { - return this.get(config.defaultDatasource); + if (nameOrUid === 'default') { + return this.get(this.defaultName); } - if (this.datasources[name]) { - return Promise.resolve(this.datasources[name]); + if (this.datasources[nameOrUid]) { + return Promise.resolve(this.datasources[nameOrUid]); } - return this.loadDatasource(name); + return this.loadDatasource(nameOrUid); } async loadDatasource(name: string): Promise> { @@ -68,7 +94,7 @@ export class DatasourceSrv implements DataSourceService { return Promise.resolve(expressionDatasource); } - const dsConfig = config.datasources[name]; + const dsConfig = this.settingsMapByName[name]; if (!dsConfig) { return Promise.reject({ message: `Datasource named ${name} was not found` }); } @@ -101,8 +127,7 @@ export class DatasourceSrv implements DataSourceService { } getAll(): DataSourceInstanceSettings[] { - const { datasources } = config; - return Object.keys(datasources).map(name => datasources[name]); + return Object.values(this.settingsMapByName); } getExternal(): DataSourceInstanceSettings[] { @@ -115,7 +140,7 @@ export class DatasourceSrv implements DataSourceService { this.addDataSourceVariables(sources); - Object.values(config.datasources).forEach(value => { + Object.values(this.settingsMapByName).forEach(value => { if (value.meta?.annotations) { sources.push(value); } @@ -127,7 +152,7 @@ export class DatasourceSrv implements DataSourceService { getMetricSources(options?: { skipVariables?: boolean }) { const metricSources: DataSourceSelectItem[] = []; - Object.entries(config.datasources).forEach(([key, value]) => { + Object.entries(this.settingsMapByName).forEach(([key, value]) => { if (value.meta?.metrics) { let metricSource: DataSourceSelectItem = { value: key, name: key, meta: value.meta, sort: key }; @@ -142,7 +167,7 @@ export class DatasourceSrv implements DataSourceService { metricSources.push(metricSource); - if (key === config.defaultDatasource) { + if (key === this.defaultName) { metricSource = { value: null, name: 'default', meta: value.meta, sort: key }; metricSources.push(metricSource); } @@ -172,9 +197,9 @@ export class DatasourceSrv implements DataSourceService { .getVariables() .filter(variable => variable.type === 'datasource') .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 ds = config.datasources[index]; + const ds = this.settingsMapByName[index]; if (ds) { const key = `$${variable.name}`; diff --git a/public/app/features/plugins/specs/datasource_srv.test.ts b/public/app/features/plugins/specs/datasource_srv.test.ts index 36b59e9f6e9..ccd3e631394 100644 --- a/public/app/features/plugins/specs/datasource_srv.test.ts +++ b/public/app/features/plugins/specs/datasource_srv.test.ts @@ -1,7 +1,6 @@ -import config from 'app/core/config'; import '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' 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', () => { 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(() => { - config.datasources = { - buildInDs: { - id: 1, - uid: '1', - type: 'b', - name: 'buildIn', - meta: { builtIn: true } as DataSourcePluginMeta, - jsonData: {}, - }, - nonBuildIn: { - id: 2, - uid: '2', - type: 'e', - name: 'external1', - meta: { builtIn: false } as DataSourcePluginMeta, - jsonData: {}, - }, - nonExplore: { - id: 3, - uid: '3', - type: 'e2', - name: 'external2', - meta: {} as PluginMeta, - jsonData: {}, - }, - }; + beforeEach(() => { + _datasourceSrv.init(datasources, 'external1'); + }); + + describe('when getting data source class instance', () => { + it('should load plugin and create instance and set meta', async () => { + const ds = (await _datasourceSrv.get('external1')) as any; + expect(ds.meta).toBe(datasources.external1.meta); + expect(ds.instanceSettings).toBe(datasources.external1); + + // validate that it caches instance + const ds2 = await _datasourceSrv.get('external1'); + expect(ds).toBe(ds2); }); + it('should be able to load data source using uid as well', async () => { + const dsByUid = await _datasourceSrv.get('2'); + const dsByName = await _datasourceSrv.get('external1'); + expect(dsByUid.meta).toBe(datasources.external1.meta); + expect(dsByUid).toBe(dsByName); + }); + }); + + describe('when getting external metric sources', () => { it('should return list of explore sources', () => { const externalSources = _datasourceSrv.getExternal(); expect(externalSources.length).toBe(2); @@ -59,45 +89,48 @@ describe('datasource_srv', () => { describe('when loading metric sources', () => { 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(() => { - 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({}); - config.defaultDatasource = 'BBB'; }); it('should return a list of sources sorted case insensitively with builtin sources last', () => { expect(metricSources[1].name).toBe('aaa'); expect(metricSources[2].name).toBe('BBB'); - expect(metricSources[3].name).toBe('mmm'); - expect(metricSources[4].name).toBe('ZZZ'); - expect(metricSources[5].name).toBe('--Grafana--'); - expect(metricSources[6].name).toBe('--Mixed--'); + expect(metricSources[3].name).toBe('default'); + expect(metricSources[4].name).toBe('mmm'); + expect(metricSources[5].name).toBe('ZZZ'); + expect(metricSources[6].name).toBe('--Grafana--'); + expect(metricSources[7].name).toBe('--Mixed--'); }); it('should set default data source', () => { diff --git a/public/app/routes/GrafanaCtrl.ts b/public/app/routes/GrafanaCtrl.ts index d51c2b9d4df..a6c58fa9aec 100644 --- a/public/app/routes/GrafanaCtrl.ts +++ b/public/app/routes/GrafanaCtrl.ts @@ -62,6 +62,8 @@ export class GrafanaCtrl { setDashboardSrv(dashboardSrv); setLegacyAngularInjector($injector); + datasourceSrv.init(config.datasources, config.defaultDatasource); + locationUtil.initialize({ getConfig: () => config, getTimeRangeForUrl: getTimeSrv().timeRangeForUrl,