PanelQueryRunner.ts 7.0 KB

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