1// Services & Utils
2import { importDataSourcePlugin } from './plugin_loader';
3import {
4  GetDataSourceListFilters,
5  DataSourceSrv as DataSourceService,
6  getDataSourceSrv as getDataSourceService,
7  TemplateSrv,
8  getTemplateSrv,
9  getLegacyAngularInjector,
10} from '@grafana/runtime';
11// Types
12import {
13  AppEvents,
14  DataSourceApi,
15  DataSourceInstanceSettings,
16  DataSourceRef,
17  DataSourceSelectItem,
18  ScopedVars,
19} from '@grafana/data';
20// Pretend Datasource
21import {
22  dataSource as expressionDatasource,
23  ExpressionDatasourceUID,
24  instanceSettings as expressionInstanceSettings,
25} from 'app/features/expressions/ExpressionDatasource';
26import { DataSourceVariableModel } from '../variables/types';
27import { ExpressionDatasourceRef } from '@grafana/runtime/src/utils/DataSourceWithBackend';
28import appEvents from 'app/core/app_events';
29
30export class DatasourceSrv implements DataSourceService {
31  private datasources: Record<string, DataSourceApi> = {}; // UID
32  private settingsMapByName: Record<string, DataSourceInstanceSettings> = {};
33  private settingsMapByUid: Record<string, DataSourceInstanceSettings> = {};
34  private settingsMapById: Record<string, DataSourceInstanceSettings> = {};
35  private defaultName = ''; // actually UID
36
37  constructor(private templateSrv: TemplateSrv = getTemplateSrv()) {}
38
39  init(settingsMapByName: Record<string, DataSourceInstanceSettings>, defaultName: string) {
40    this.datasources = {};
41    this.settingsMapByUid = {};
42    this.settingsMapByName = settingsMapByName;
43    this.defaultName = defaultName;
44
45    for (const dsSettings of Object.values(settingsMapByName)) {
46      if (!dsSettings.uid) {
47        dsSettings.uid = dsSettings.name; // -- Grafana --, -- Mixed etc
48      }
49
50      this.settingsMapByUid[dsSettings.uid] = dsSettings;
51      this.settingsMapById[dsSettings.id] = dsSettings;
52    }
53
54    // Preload expressions
55    this.datasources[ExpressionDatasourceRef.type] = expressionDatasource as any;
56    this.datasources[ExpressionDatasourceUID] = expressionDatasource as any;
57    this.settingsMapByUid[ExpressionDatasourceRef.uid] = expressionInstanceSettings;
58    this.settingsMapByUid[ExpressionDatasourceUID] = expressionInstanceSettings;
59  }
60
61  getDataSourceSettingsByUid(uid: string): DataSourceInstanceSettings | undefined {
62    return this.settingsMapByUid[uid];
63  }
64
65  getInstanceSettings(
66    ref: string | null | undefined | DataSourceRef,
67    scopedVars?: ScopedVars
68  ): DataSourceInstanceSettings | undefined {
69    const isstring = typeof ref === 'string';
70    let nameOrUid = isstring ? (ref as string) : ((ref as any)?.uid as string | undefined);
71
72    if (nameOrUid === 'default' || nameOrUid === null || nameOrUid === undefined) {
73      if (!isstring && ref) {
74        const type = (ref as any)?.type as string;
75        if (type === ExpressionDatasourceRef.type) {
76          return expressionDatasource.instanceSettings;
77        } else if (type) {
78          console.log('FIND Default instance for datasource type?', ref);
79        }
80      }
81      return this.settingsMapByUid[this.defaultName] ?? this.settingsMapByName[this.defaultName];
82    }
83
84    // Complex logic to support template variable data source names
85    // For this we just pick the current or first data source in the variable
86    if (nameOrUid[0] === '$') {
87      const interpolatedName = this.templateSrv.replace(nameOrUid, scopedVars, variableInterpolation);
88
89      let dsSettings;
90
91      if (interpolatedName === 'default') {
92        dsSettings = this.settingsMapByName[this.defaultName];
93      } else {
94        dsSettings = this.settingsMapByUid[interpolatedName] ?? this.settingsMapByName[interpolatedName];
95      }
96
97      if (!dsSettings) {
98        return undefined;
99      }
100
101      // Return an instance with un-interpolated values for name and uid
102      return {
103        ...dsSettings,
104        isDefault: false,
105        name: nameOrUid,
106        uid: nameOrUid,
107        rawRef: { type: dsSettings.type, uid: dsSettings.uid },
108      };
109    }
110
111    return this.settingsMapByUid[nameOrUid] ?? this.settingsMapByName[nameOrUid];
112  }
113
114  get(ref?: string | DataSourceRef | null, scopedVars?: ScopedVars): Promise<DataSourceApi> {
115    let nameOrUid = typeof ref === 'string' ? (ref as string) : ((ref as any)?.uid as string | undefined);
116    if (!nameOrUid) {
117      return this.get(this.defaultName);
118    }
119
120    // Check if nameOrUid matches a uid and then get the name
121    const byName = this.settingsMapByName[nameOrUid];
122    if (byName) {
123      nameOrUid = byName.uid;
124    }
125
126    // This check is duplicated below, this is here mainly as performance optimization to skip interpolation
127    if (this.datasources[nameOrUid]) {
128      return Promise.resolve(this.datasources[nameOrUid]);
129    }
130
131    // Interpolation here is to support template variable in data source selection
132    nameOrUid = this.templateSrv.replace(nameOrUid, scopedVars, variableInterpolation);
133
134    if (nameOrUid === 'default' && this.defaultName !== 'default') {
135      return this.get(this.defaultName);
136    }
137
138    if (this.datasources[nameOrUid]) {
139      return Promise.resolve(this.datasources[nameOrUid]);
140    }
141
142    return this.loadDatasource(nameOrUid);
143  }
144
145  async loadDatasource(key: string): Promise<DataSourceApi<any, any>> {
146    if (this.datasources[key]) {
147      return Promise.resolve(this.datasources[key]);
148    }
149
150    // find the metadata
151    const instanceSettings = this.settingsMapByUid[key] ?? this.settingsMapByName[key] ?? this.settingsMapById[key];
152    if (!instanceSettings) {
153      return Promise.reject({ message: `Datasource ${key} was not found` });
154    }
155
156    try {
157      const dsPlugin = await importDataSourcePlugin(instanceSettings.meta);
158      // check if its in cache now
159      if (this.datasources[key]) {
160        return this.datasources[key];
161      }
162
163      // If there is only one constructor argument it is instanceSettings
164      const useAngular = dsPlugin.DataSourceClass.length !== 1;
165      let instance: DataSourceApi<any, any>;
166
167      if (useAngular) {
168        instance = getLegacyAngularInjector().instantiate(dsPlugin.DataSourceClass, {
169          instanceSettings,
170        });
171      } else {
172        instance = new dsPlugin.DataSourceClass(instanceSettings);
173      }
174
175      instance.components = dsPlugin.components;
176
177      // Some old plugins does not extend DataSourceApi so we need to manually patch them
178      if (!(instance instanceof DataSourceApi)) {
179        const anyInstance = instance as any;
180        anyInstance.name = instanceSettings.name;
181        anyInstance.id = instanceSettings.id;
182        anyInstance.type = instanceSettings.type;
183        anyInstance.meta = instanceSettings.meta;
184        anyInstance.uid = instanceSettings.uid;
185        (instance as any).getRef = DataSourceApi.prototype.getRef;
186      }
187
188      // store in instance cache
189      this.datasources[key] = instance;
190      this.datasources[instance.uid] = instance;
191      return instance;
192    } catch (err) {
193      appEvents.emit(AppEvents.alertError, [instanceSettings.name + ' plugin failed', err.toString()]);
194      return Promise.reject({ message: `Datasource: ${key} was not found` });
195    }
196  }
197
198  getAll(): DataSourceInstanceSettings[] {
199    return Object.values(this.settingsMapByName);
200  }
201
202  getList(filters: GetDataSourceListFilters = {}): DataSourceInstanceSettings[] {
203    const base = Object.values(this.settingsMapByName).filter((x) => {
204      if (x.meta.id === 'grafana' || x.meta.id === 'mixed' || x.meta.id === 'dashboard') {
205        return false;
206      }
207      if (filters.metrics && !x.meta.metrics) {
208        return false;
209      }
210      if (filters.tracing && !x.meta.tracing) {
211        return false;
212      }
213      if (filters.annotations && !x.meta.annotations) {
214        return false;
215      }
216      if (filters.alerting && !x.meta.alerting) {
217        return false;
218      }
219      if (filters.pluginId && x.meta.id !== filters.pluginId) {
220        return false;
221      }
222      if (filters.filter && !filters.filter(x)) {
223        return false;
224      }
225      if (filters.type && (Array.isArray(filters.type) ? !filters.type.includes(x.type) : filters.type !== x.type)) {
226        return false;
227      }
228      if (
229        !filters.all &&
230        x.meta.metrics !== true &&
231        x.meta.annotations !== true &&
232        x.meta.tracing !== true &&
233        x.meta.logs !== true &&
234        x.meta.alerting !== true
235      ) {
236        return false;
237      }
238      return true;
239    });
240
241    if (filters.variables) {
242      for (const variable of this.templateSrv.getVariables().filter((variable) => variable.type === 'datasource')) {
243        const dsVar = variable as DataSourceVariableModel;
244        const first = dsVar.current.value === 'default' ? this.defaultName : dsVar.current.value;
245        const dsName = (first as unknown) as string;
246        const dsSettings = this.settingsMapByName[dsName];
247
248        if (dsSettings) {
249          const key = `$\{${variable.name}\}`;
250          base.push({
251            ...dsSettings,
252            name: key,
253            uid: key,
254          });
255        }
256      }
257    }
258
259    const sorted = base.sort((a, b) => {
260      if (a.name.toLowerCase() > b.name.toLowerCase()) {
261        return 1;
262      }
263      if (a.name.toLowerCase() < b.name.toLowerCase()) {
264        return -1;
265      }
266      return 0;
267    });
268
269    if (!filters.pluginId && !filters.alerting) {
270      if (filters.mixed) {
271        const mixedInstanceSettings = this.getInstanceSettings('-- Mixed --');
272        if (mixedInstanceSettings) {
273          base.push(mixedInstanceSettings);
274        }
275      }
276
277      if (filters.dashboard) {
278        const dashboardInstanceSettings = this.getInstanceSettings('-- Dashboard --');
279        if (dashboardInstanceSettings) {
280          base.push(dashboardInstanceSettings);
281        }
282      }
283
284      if (!filters.tracing) {
285        const grafanaInstanceSettings = this.getInstanceSettings('-- Grafana --');
286        if (grafanaInstanceSettings) {
287          base.push(grafanaInstanceSettings);
288        }
289      }
290    }
291
292    return sorted;
293  }
294
295  /**
296   * @deprecated use getList
297   * */
298  getExternal(): DataSourceInstanceSettings[] {
299    return this.getList();
300  }
301
302  /**
303   * @deprecated use getList
304   * */
305  getAnnotationSources() {
306    return this.getList({ annotations: true, variables: true }).map((x) => {
307      return {
308        name: x.name,
309        value: x.isDefault ? null : x.name,
310        meta: x.meta,
311      };
312    });
313  }
314
315  /**
316   * @deprecated use getList
317   * */
318  getMetricSources(options?: { skipVariables?: boolean }): DataSourceSelectItem[] {
319    return this.getList({ metrics: true, variables: !options?.skipVariables }).map((x) => {
320      return {
321        name: x.name,
322        value: x.isDefault ? null : x.name,
323        meta: x.meta,
324      };
325    });
326  }
327}
328
329export function variableInterpolation(value: any[]) {
330  if (Array.isArray(value)) {
331    return value[0];
332  }
333  return value;
334}
335
336export const getDatasourceSrv = (): DatasourceSrv => {
337  return getDataSourceService() as DatasourceSrv;
338};
339
340export default DatasourceSrv;
341