PanelQueryRunner.ts 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
  2. import { Subject, Unsubscribable, PartialObserver } from 'rxjs';
  3. import {
  4. guessFieldTypes,
  5. toSeriesData,
  6. PanelData,
  7. LoadingState,
  8. DataQuery,
  9. TimeRange,
  10. ScopedVars,
  11. DataRequestInfo,
  12. SeriesData,
  13. DataQueryError,
  14. toLegacyResponseData,
  15. isSeriesData,
  16. DataSourceApi,
  17. } from '@grafana/ui';
  18. import cloneDeep from 'lodash/cloneDeep';
  19. import kbn from 'app/core/utils/kbn';
  20. export interface QueryRunnerOptions<TQuery extends DataQuery = DataQuery> {
  21. ds?: DataSourceApi<TQuery>; // if they already have the datasource, don't look it up
  22. datasource: string | null;
  23. queries: TQuery[];
  24. panelId: number;
  25. dashboardId?: number;
  26. timezone?: string;
  27. timeRange?: TimeRange;
  28. timeInfo?: string; // String description of time range for display
  29. widthPixels: number;
  30. minInterval?: string;
  31. maxDataPoints?: number;
  32. scopedVars?: ScopedVars;
  33. cacheTimeout?: string;
  34. delayStateNotification?: number; // default 100ms.
  35. }
  36. export enum PanelQueryRunnerFormat {
  37. series = 'series',
  38. legacy = 'legacy',
  39. both = 'both',
  40. }
  41. export class PanelQueryRunner {
  42. private subject?: Subject<PanelData>;
  43. private sendSeries = false;
  44. private sendLegacy = false;
  45. private data = {
  46. state: LoadingState.NotStarted,
  47. series: [],
  48. } as PanelData;
  49. /**
  50. * Listen for updates to the PanelData. If a query has already run for this panel,
  51. * the results will be immediatly passed to the observer
  52. */
  53. subscribe(observer: PartialObserver<PanelData>, format = PanelQueryRunnerFormat.series): Unsubscribable {
  54. if (!this.subject) {
  55. this.subject = new Subject(); // Delay creating a subject until someone is listening
  56. }
  57. if (format === PanelQueryRunnerFormat.legacy) {
  58. this.sendLegacy = true;
  59. } else if (format === PanelQueryRunnerFormat.both) {
  60. this.sendSeries = true;
  61. this.sendLegacy = true;
  62. } else {
  63. this.sendSeries = true;
  64. }
  65. // Send the last result
  66. if (this.data.state !== LoadingState.NotStarted) {
  67. // TODO: make sure it has legacy if necessary
  68. observer.next(this.data);
  69. }
  70. return this.subject.subscribe(observer);
  71. }
  72. async run(options: QueryRunnerOptions): Promise<PanelData> {
  73. if (!this.subject) {
  74. this.subject = new Subject();
  75. }
  76. const {
  77. queries,
  78. timezone,
  79. datasource,
  80. panelId,
  81. dashboardId,
  82. timeRange,
  83. timeInfo,
  84. cacheTimeout,
  85. widthPixels,
  86. maxDataPoints,
  87. scopedVars,
  88. delayStateNotification,
  89. } = options;
  90. const request: DataRequestInfo = {
  91. timezone,
  92. panelId,
  93. dashboardId,
  94. range: timeRange,
  95. rangeRaw: timeRange.raw,
  96. timeInfo,
  97. interval: '',
  98. intervalMs: 0,
  99. targets: cloneDeep(queries),
  100. maxDataPoints: maxDataPoints || widthPixels,
  101. scopedVars: scopedVars || {},
  102. cacheTimeout,
  103. startTime: Date.now(),
  104. };
  105. if (!queries) {
  106. this.data = {
  107. state: LoadingState.Done,
  108. series: [], // Clear the data
  109. legacy: [],
  110. request,
  111. };
  112. this.subject.next(this.data);
  113. return this.data;
  114. }
  115. let loadingStateTimeoutId = 0;
  116. try {
  117. const ds = options.ds ? options.ds : await getDatasourceSrv().get(datasource, request.scopedVars);
  118. const minInterval = options.minInterval || ds.interval;
  119. const norm = kbn.calculateInterval(timeRange, widthPixels, minInterval);
  120. // make shallow copy of scoped vars,
  121. // and add built in variables interval and interval_ms
  122. request.scopedVars = Object.assign({}, request.scopedVars, {
  123. __interval: { text: norm.interval, value: norm.interval },
  124. __interval_ms: { text: norm.intervalMs, value: norm.intervalMs },
  125. });
  126. request.interval = norm.interval;
  127. request.intervalMs = norm.intervalMs;
  128. // Send a loading status event on slower queries
  129. loadingStateTimeoutId = window.setTimeout(() => {
  130. this.publishUpdate({ state: LoadingState.Loading });
  131. }, delayStateNotification || 500);
  132. const resp = await ds.query(request);
  133. request.endTime = Date.now();
  134. // Make sure the response is in a supported format
  135. const series = this.sendSeries ? getProcessedSeriesData(resp.data) : [];
  136. const legacy = this.sendLegacy
  137. ? resp.data.map(v => {
  138. if (isSeriesData(v)) {
  139. return toLegacyResponseData(v);
  140. }
  141. return v;
  142. })
  143. : undefined;
  144. // Make sure the delayed loading state timeout is cleared
  145. clearTimeout(loadingStateTimeoutId);
  146. // Publish the result
  147. return this.publishUpdate({
  148. state: LoadingState.Done,
  149. series,
  150. legacy,
  151. request,
  152. });
  153. } catch (err) {
  154. const error = err as DataQueryError;
  155. if (!error.message) {
  156. let message = 'Query error';
  157. if (error.message) {
  158. message = error.message;
  159. } else if (error.data && error.data.message) {
  160. message = error.data.message;
  161. } else if (error.data && error.data.error) {
  162. message = error.data.error;
  163. } else if (error.status) {
  164. message = `Query error: ${error.status} ${error.statusText}`;
  165. }
  166. error.message = message;
  167. }
  168. // Make sure the delayed loading state timeout is cleared
  169. clearTimeout(loadingStateTimeoutId);
  170. return this.publishUpdate({
  171. state: LoadingState.Error,
  172. error: error,
  173. });
  174. }
  175. }
  176. publishUpdate(update: Partial<PanelData>): PanelData {
  177. this.data = {
  178. ...this.data,
  179. ...update,
  180. };
  181. this.subject.next(this.data);
  182. return this.data;
  183. }
  184. }
  185. /**
  186. * All panels will be passed tables that have our best guess at colum type set
  187. *
  188. * This is also used by PanelChrome for snapshot support
  189. */
  190. export function getProcessedSeriesData(results?: any[]): SeriesData[] {
  191. if (!results) {
  192. return [];
  193. }
  194. const series: SeriesData[] = [];
  195. for (const r of results) {
  196. if (r) {
  197. series.push(guessFieldTypes(toSeriesData(r)));
  198. }
  199. }
  200. return series;
  201. }