explore.ts 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. import _ from 'lodash';
  2. import { colors, RawTimeRange, IntervalValues } from '@grafana/ui';
  3. import * as dateMath from 'app/core/utils/datemath';
  4. import { renderUrl } from 'app/core/utils/url';
  5. import kbn from 'app/core/utils/kbn';
  6. import store from 'app/core/store';
  7. import { parse as parseDate } from 'app/core/utils/datemath';
  8. import TimeSeries from 'app/core/time_series2';
  9. import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
  10. import {
  11. ExploreUrlState,
  12. HistoryItem,
  13. QueryTransaction,
  14. ResultType,
  15. QueryIntervals,
  16. QueryOptions,
  17. } from 'app/types/explore';
  18. import { DataQuery } from 'app/types/series';
  19. export const DEFAULT_RANGE = {
  20. from: 'now-6h',
  21. to: 'now',
  22. };
  23. const MAX_HISTORY_ITEMS = 100;
  24. export const LAST_USED_DATASOURCE_KEY = 'grafana.explore.datasource';
  25. /**
  26. * Returns an Explore-URL that contains a panel's queries and the dashboard time range.
  27. *
  28. * @param panel Origin panel of the jump to Explore
  29. * @param panelTargets The origin panel's query targets
  30. * @param panelDatasource The origin panel's datasource
  31. * @param datasourceSrv Datasource service to query other datasources in case the panel datasource is mixed
  32. * @param timeSrv Time service to get the current dashboard range from
  33. */
  34. export async function getExploreUrl(
  35. panel: any,
  36. panelTargets: any[],
  37. panelDatasource: any,
  38. datasourceSrv: any,
  39. timeSrv: any
  40. ) {
  41. let exploreDatasource = panelDatasource;
  42. let exploreTargets: DataQuery[] = panelTargets;
  43. let url;
  44. // Mixed datasources need to choose only one datasource
  45. if (panelDatasource.meta.id === 'mixed' && panelTargets) {
  46. // Find first explore datasource among targets
  47. let mixedExploreDatasource;
  48. for (const t of panel.targets) {
  49. const datasource = await datasourceSrv.get(t.datasource);
  50. if (datasource && datasource.meta.explore) {
  51. mixedExploreDatasource = datasource;
  52. break;
  53. }
  54. }
  55. // Add all its targets
  56. if (mixedExploreDatasource) {
  57. exploreDatasource = mixedExploreDatasource;
  58. exploreTargets = panelTargets.filter(t => t.datasource === mixedExploreDatasource.name);
  59. }
  60. }
  61. if (panelDatasource) {
  62. const range = timeSrv.timeRangeForUrl();
  63. let state: Partial<ExploreUrlState> = { range };
  64. if (exploreDatasource.getExploreState) {
  65. state = { ...state, ...exploreDatasource.getExploreState(exploreTargets) };
  66. } else {
  67. state = {
  68. ...state,
  69. datasource: panelDatasource.name,
  70. queries: exploreTargets.map(t => ({ ...t, datasource: panelDatasource.name })),
  71. };
  72. }
  73. const exploreState = JSON.stringify(state);
  74. url = renderUrl('/explore', { state: exploreState });
  75. }
  76. return url;
  77. }
  78. export function buildQueryTransaction(
  79. query: DataQuery,
  80. rowIndex: number,
  81. resultType: ResultType,
  82. queryOptions: QueryOptions,
  83. range: RawTimeRange,
  84. queryIntervals: QueryIntervals,
  85. scanning: boolean
  86. ): QueryTransaction {
  87. const { interval, intervalMs } = queryIntervals;
  88. const configuredQueries = [
  89. {
  90. ...query,
  91. ...queryOptions,
  92. },
  93. ];
  94. // Clone range for query request
  95. // const queryRange: RawTimeRange = { ...range };
  96. // const { from, to, raw } = this.timeSrv.timeRange();
  97. // Most datasource is using `panelId + query.refId` for cancellation logic.
  98. // Using `format` here because it relates to the view panel that the request is for.
  99. // However, some datasources don't use `panelId + query.refId`, but only `panelId`.
  100. // Therefore panel id has to be unique.
  101. const panelId = `${queryOptions.format}-${query.key}`;
  102. const options = {
  103. interval,
  104. intervalMs,
  105. panelId,
  106. targets: configuredQueries, // Datasources rely on DataQueries being passed under the targets key.
  107. range: {
  108. from: dateMath.parse(range.from, false),
  109. to: dateMath.parse(range.to, true),
  110. raw: range,
  111. },
  112. rangeRaw: range,
  113. scopedVars: {
  114. __interval: { text: interval, value: interval },
  115. __interval_ms: { text: intervalMs, value: intervalMs },
  116. },
  117. };
  118. return {
  119. options,
  120. query,
  121. resultType,
  122. rowIndex,
  123. scanning,
  124. id: generateKey(), // reusing for unique ID
  125. done: false,
  126. latency: 0,
  127. };
  128. }
  129. export const clearQueryKeys: ((query: DataQuery) => object) = ({ key, refId, ...rest }) => rest;
  130. export function parseUrlState(initial: string | undefined): ExploreUrlState {
  131. if (initial) {
  132. try {
  133. const parsed = JSON.parse(decodeURI(initial));
  134. if (Array.isArray(parsed)) {
  135. if (parsed.length <= 3) {
  136. throw new Error('Error parsing compact URL state for Explore.');
  137. }
  138. const range = {
  139. from: parsed[0],
  140. to: parsed[1],
  141. };
  142. const datasource = parsed[2];
  143. const queries = parsed.slice(3);
  144. return { datasource, queries, range };
  145. }
  146. return parsed;
  147. } catch (e) {
  148. console.error(e);
  149. }
  150. }
  151. return { datasource: null, queries: [], range: DEFAULT_RANGE };
  152. }
  153. export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: boolean): string {
  154. if (compact) {
  155. return JSON.stringify([urlState.range.from, urlState.range.to, urlState.datasource, ...urlState.queries]);
  156. }
  157. return JSON.stringify(urlState);
  158. }
  159. export function generateKey(index = 0): string {
  160. return `Q-${Date.now()}-${Math.random()}-${index}`;
  161. }
  162. export function generateRefId(index = 0): string {
  163. return `${index + 1}`;
  164. }
  165. export function generateEmptyQuery(index = 0): { refId: string; key: string } {
  166. return { refId: generateRefId(index), key: generateKey(index) };
  167. }
  168. /**
  169. * Ensure at least one target exists and that targets have the necessary keys
  170. */
  171. export function ensureQueries(queries?: DataQuery[]): DataQuery[] {
  172. if (queries && typeof queries === 'object' && queries.length > 0) {
  173. return queries.map((query, i) => ({ ...query, ...generateEmptyQuery(i) }));
  174. }
  175. return [{ ...generateEmptyQuery() }];
  176. }
  177. /**
  178. * A target is non-empty when it has keys (with non-empty values) other than refId and key.
  179. */
  180. export function hasNonEmptyQuery(queries: DataQuery[]): boolean {
  181. return (
  182. queries &&
  183. queries.some(
  184. query =>
  185. Object.keys(query)
  186. .map(k => query[k])
  187. .filter(v => v).length > 2
  188. )
  189. );
  190. }
  191. export function calculateResultsFromQueryTransactions(
  192. queryTransactions: QueryTransaction[],
  193. datasource: any,
  194. graphInterval: number
  195. ) {
  196. const graphResult = _.flatten(
  197. queryTransactions.filter(qt => qt.resultType === 'Graph' && qt.done && qt.result).map(qt => qt.result)
  198. );
  199. const tableResult = mergeTablesIntoModel(
  200. new TableModel(),
  201. ...queryTransactions
  202. .filter(qt => qt.resultType === 'Table' && qt.done && qt.result && qt.result.columns && qt.result.rows)
  203. .map(qt => qt.result)
  204. );
  205. const logsResult =
  206. datasource && datasource.mergeStreams
  207. ? datasource.mergeStreams(
  208. _.flatten(
  209. queryTransactions.filter(qt => qt.resultType === 'Logs' && qt.done && qt.result).map(qt => qt.result)
  210. ),
  211. graphInterval
  212. )
  213. : undefined;
  214. return {
  215. graphResult,
  216. tableResult,
  217. logsResult,
  218. };
  219. }
  220. export function getIntervals(range: RawTimeRange, lowLimit: string, resolution: number): IntervalValues {
  221. if (!resolution) {
  222. return { interval: '1s', intervalMs: 1000 };
  223. }
  224. const absoluteRange: RawTimeRange = {
  225. from: parseDate(range.from, false),
  226. to: parseDate(range.to, true),
  227. };
  228. return kbn.calculateInterval(absoluteRange, resolution, lowLimit);
  229. }
  230. export function makeTimeSeriesList(dataList) {
  231. return dataList.map((seriesData, index) => {
  232. const datapoints = seriesData.datapoints || [];
  233. const alias = seriesData.target;
  234. const colorIndex = index % colors.length;
  235. const color = colors[colorIndex];
  236. const series = new TimeSeries({
  237. datapoints,
  238. alias,
  239. color,
  240. unit: seriesData.unit,
  241. });
  242. return series;
  243. });
  244. }
  245. /**
  246. * Update the query history. Side-effect: store history in local storage
  247. */
  248. export function updateHistory(history: HistoryItem[], datasourceId: string, queries: DataQuery[]): HistoryItem[] {
  249. const ts = Date.now();
  250. queries.forEach(query => {
  251. history = [{ query, ts }, ...history];
  252. });
  253. if (history.length > MAX_HISTORY_ITEMS) {
  254. history = history.slice(0, MAX_HISTORY_ITEMS);
  255. }
  256. // Combine all queries of a datasource type into one history
  257. const historyKey = `grafana.explore.history.${datasourceId}`;
  258. store.setObject(historyKey, history);
  259. return history;
  260. }
  261. export function clearHistory(datasourceId: string) {
  262. const historyKey = `grafana.explore.history.${datasourceId}`;
  263. store.delete(historyKey);
  264. }