1import { from, merge, Observable, of } from 'rxjs';
2import { delay } from 'rxjs/operators';
3
4import {
5  AnnotationEvent,
6  ArrayDataFrame,
7  DataFrame,
8  DataQueryRequest,
9  DataQueryResponse,
10  DataSourceInstanceSettings,
11  DataTopic,
12  LiveChannelScope,
13  LoadingState,
14  TimeRange,
15  ScopedVars,
16} from '@grafana/data';
17import { Scenario, TestDataQuery } from './types';
18import { DataSourceWithBackend, getBackendSrv, getGrafanaLiveSrv, getTemplateSrv, TemplateSrv } from '@grafana/runtime';
19import { queryMetricTree } from './metricTree';
20import { runStream } from './runStreams';
21import { getSearchFilterScopedVar } from 'app/features/variables/utils';
22import { TestDataVariableSupport } from './variables';
23import { generateRandomNodes, savedNodesResponse } from './nodeGraphUtils';
24
25export class TestDataDataSource extends DataSourceWithBackend<TestDataQuery> {
26  scenariosCache?: Promise<Scenario[]>;
27
28  constructor(
29    instanceSettings: DataSourceInstanceSettings,
30    private readonly templateSrv: TemplateSrv = getTemplateSrv()
31  ) {
32    super(instanceSettings);
33    this.variables = new TestDataVariableSupport();
34  }
35
36  query(options: DataQueryRequest<TestDataQuery>): Observable<DataQueryResponse> {
37    const backendQueries: TestDataQuery[] = [];
38    const streams: Array<Observable<DataQueryResponse>> = [];
39
40    // Start streams and prepare queries
41    for (const target of options.targets) {
42      if (target.hide) {
43        continue;
44      }
45
46      this.resolveTemplateVariables(target, options.scopedVars);
47
48      switch (target.scenarioId) {
49        case 'live':
50          streams.push(runGrafanaLiveQuery(target, options));
51          break;
52        case 'streaming_client':
53          streams.push(runStream(target, options));
54          break;
55        case 'grafana_api':
56          streams.push(runGrafanaAPI(target, options));
57          break;
58        case 'annotations':
59          streams.push(this.annotationDataTopicTest(target, options));
60          break;
61        case 'variables-query':
62          streams.push(this.variablesQuery(target, options));
63          break;
64        case 'node_graph':
65          streams.push(this.nodesQuery(target, options));
66          break;
67
68        // Unusable since 7, removed in 8
69        case 'manual_entry': {
70          let csvContent = 'Time,Value\n';
71          if ((target as any).points) {
72            for (const point of (target as any).points) {
73              csvContent += `${point[1]},${point[0]}\n`;
74            }
75          }
76          target.scenarioId = 'csv_content';
77          target.csvContent = csvContent;
78        }
79
80        default:
81          if (target.alias) {
82            target.alias = this.templateSrv.replace(target.alias, options.scopedVars);
83          }
84
85          backendQueries.push(target);
86      }
87    }
88
89    if (backendQueries.length) {
90      const backendOpts = {
91        ...options,
92        targets: backendQueries,
93      };
94      streams.push(super.query(backendOpts));
95    }
96
97    if (streams.length === 0) {
98      return of({ data: [] });
99    }
100
101    return merge(...streams);
102  }
103
104  resolveTemplateVariables(query: TestDataQuery, scopedVars: ScopedVars) {
105    query.labels = this.templateSrv.replace(query.labels!, scopedVars);
106  }
107
108  annotationDataTopicTest(target: TestDataQuery, req: DataQueryRequest<TestDataQuery>): Observable<DataQueryResponse> {
109    const events = this.buildFakeAnnotationEvents(req.range, 50);
110    const dataFrame = new ArrayDataFrame(events);
111    dataFrame.meta = { dataTopic: DataTopic.Annotations };
112
113    return of({ key: target.refId, data: [dataFrame] }).pipe(delay(100));
114  }
115
116  buildFakeAnnotationEvents(range: TimeRange, count: number): AnnotationEvent[] {
117    let timeWalker = range.from.valueOf();
118    const to = range.to.valueOf();
119    const events = [];
120    const step = (to - timeWalker) / count;
121
122    for (let i = 0; i < count; i++) {
123      events.push({
124        time: timeWalker,
125        text: 'This is the text, <a href="https://grafana.com">Grafana.com</a>',
126        tags: ['text', 'server'],
127      });
128      timeWalker += step;
129    }
130
131    return events;
132  }
133
134  annotationQuery(options: any) {
135    return Promise.resolve(this.buildFakeAnnotationEvents(options.range, 10));
136  }
137
138  getQueryDisplayText(query: TestDataQuery) {
139    if (query.alias) {
140      return query.scenarioId + ' as ' + query.alias;
141    }
142    return query.scenarioId;
143  }
144
145  testDatasource() {
146    return Promise.resolve({
147      status: 'success',
148      message: 'Data source is working',
149    });
150  }
151
152  getScenarios(): Promise<Scenario[]> {
153    if (!this.scenariosCache) {
154      this.scenariosCache = this.getResource('scenarios');
155    }
156
157    return this.scenariosCache;
158  }
159
160  variablesQuery(target: TestDataQuery, options: DataQueryRequest<TestDataQuery>): Observable<DataQueryResponse> {
161    const query = target.stringInput ?? '';
162    const interpolatedQuery = this.templateSrv.replace(
163      query,
164      getSearchFilterScopedVar({ query, wildcardChar: '*', options: options.scopedVars })
165    );
166    const children = queryMetricTree(interpolatedQuery);
167    const items = children.map((item) => ({ value: item.name, text: item.name }));
168    const dataFrame = new ArrayDataFrame(items);
169
170    return of({ data: [dataFrame] }).pipe(delay(100));
171  }
172
173  nodesQuery(target: TestDataQuery, options: DataQueryRequest<TestDataQuery>): Observable<DataQueryResponse> {
174    const type = target.nodes?.type || 'random';
175    let frames: DataFrame[];
176    switch (type) {
177      case 'random':
178        frames = generateRandomNodes(target.nodes?.count);
179        break;
180      case 'response':
181        frames = savedNodesResponse();
182        break;
183      default:
184        throw new Error(`Unknown node_graph sub type ${type}`);
185    }
186
187    return of({ data: frames }).pipe(delay(100));
188  }
189}
190
191function runGrafanaAPI(target: TestDataQuery, req: DataQueryRequest<TestDataQuery>): Observable<DataQueryResponse> {
192  const url = `/api/${target.stringInput}`;
193  return from(
194    getBackendSrv()
195      .get(url)
196      .then((res) => {
197        const frame = new ArrayDataFrame(res);
198        return {
199          state: LoadingState.Done,
200          data: [frame],
201        };
202      })
203  );
204}
205
206let liveQueryCounter = 1000;
207
208function runGrafanaLiveQuery(
209  target: TestDataQuery,
210  req: DataQueryRequest<TestDataQuery>
211): Observable<DataQueryResponse> {
212  if (!target.channel) {
213    throw new Error(`Missing channel config`);
214  }
215  return getGrafanaLiveSrv().getDataStream({
216    addr: {
217      scope: LiveChannelScope.Plugin,
218      namespace: 'testdata',
219      path: target.channel,
220    },
221    key: `testStream.${liveQueryCounter++}`,
222  });
223}
224