explore.ts 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. import _ from 'lodash';
  2. import { renderUrl } from 'app/core/utils/url';
  3. import { ExploreState, ExploreUrlState, HistoryItem, QueryTransaction } from 'app/types/explore';
  4. import { DataQuery, RawTimeRange } from 'app/types/series';
  5. import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
  6. import kbn from 'app/core/utils/kbn';
  7. import colors from 'app/core/utils/colors';
  8. import TimeSeries from 'app/core/time_series2';
  9. import { parse as parseDate } from 'app/core/utils/datemath';
  10. import store from 'app/core/store';
  11. export const DEFAULT_RANGE = {
  12. from: 'now-6h',
  13. to: 'now',
  14. };
  15. const MAX_HISTORY_ITEMS = 100;
  16. /**
  17. * Returns an Explore-URL that contains a panel's queries and the dashboard time range.
  18. *
  19. * @param panel Origin panel of the jump to Explore
  20. * @param panelTargets The origin panel's query targets
  21. * @param panelDatasource The origin panel's datasource
  22. * @param datasourceSrv Datasource service to query other datasources in case the panel datasource is mixed
  23. * @param timeSrv Time service to get the current dashboard range from
  24. */
  25. export async function getExploreUrl(
  26. panel: any,
  27. panelTargets: any[],
  28. panelDatasource: any,
  29. datasourceSrv: any,
  30. timeSrv: any
  31. ) {
  32. let exploreDatasource = panelDatasource;
  33. let exploreTargets: DataQuery[] = panelTargets;
  34. let url;
  35. // Mixed datasources need to choose only one datasource
  36. if (panelDatasource.meta.id === 'mixed' && panelTargets) {
  37. // Find first explore datasource among targets
  38. let mixedExploreDatasource;
  39. for (const t of panel.targets) {
  40. const datasource = await datasourceSrv.get(t.datasource);
  41. if (datasource && datasource.meta.explore) {
  42. mixedExploreDatasource = datasource;
  43. break;
  44. }
  45. }
  46. // Add all its targets
  47. if (mixedExploreDatasource) {
  48. exploreDatasource = mixedExploreDatasource;
  49. exploreTargets = panelTargets.filter(t => t.datasource === mixedExploreDatasource.name);
  50. }
  51. }
  52. if (exploreDatasource && exploreDatasource.meta.explore) {
  53. const range = timeSrv.timeRangeForUrl();
  54. const state = {
  55. ...exploreDatasource.getExploreState(exploreTargets),
  56. range,
  57. };
  58. const exploreState = JSON.stringify(state);
  59. url = renderUrl('/explore', { state: exploreState });
  60. }
  61. return url;
  62. }
  63. const clearQueryKeys: ((query: DataQuery) => object) = ({ key, refId, ...rest }) => rest;
  64. export function parseUrlState(initial: string | undefined): ExploreUrlState {
  65. if (initial) {
  66. try {
  67. const parsed = JSON.parse(decodeURI(initial));
  68. if (Array.isArray(parsed)) {
  69. if (parsed.length <= 3) {
  70. throw new Error('Error parsing compact URL state for Explore.');
  71. }
  72. const range = {
  73. from: parsed[0],
  74. to: parsed[1],
  75. };
  76. const datasource = parsed[2];
  77. const queries = parsed.slice(3);
  78. return { datasource, queries, range };
  79. }
  80. return parsed;
  81. } catch (e) {
  82. console.error(e);
  83. }
  84. }
  85. return { datasource: null, queries: [], range: DEFAULT_RANGE };
  86. }
  87. export function serializeStateToUrlParam(state: ExploreState, compact?: boolean): string {
  88. const urlState: ExploreUrlState = {
  89. datasource: state.datasourceName,
  90. queries: state.initialQueries.map(clearQueryKeys),
  91. range: state.range,
  92. };
  93. if (compact) {
  94. return JSON.stringify([urlState.range.from, urlState.range.to, urlState.datasource, ...urlState.queries]);
  95. }
  96. return JSON.stringify(urlState);
  97. }
  98. export function generateKey(index = 0): string {
  99. return `Q-${Date.now()}-${Math.random()}-${index}`;
  100. }
  101. export function generateRefId(index = 0): string {
  102. return `${index + 1}`;
  103. }
  104. export function generateQueryKeys(index = 0): { refId: string; key: string } {
  105. return { refId: generateRefId(index), key: generateKey(index) };
  106. }
  107. /**
  108. * Ensure at least one target exists and that targets have the necessary keys
  109. */
  110. export function ensureQueries(queries?: DataQuery[]): DataQuery[] {
  111. if (queries && typeof queries === 'object' && queries.length > 0) {
  112. return queries.map((query, i) => ({ ...query, ...generateQueryKeys(i) }));
  113. }
  114. return [{ ...generateQueryKeys() }];
  115. }
  116. /**
  117. * A target is non-empty when it has keys (with non-empty values) other than refId and key.
  118. */
  119. export function hasNonEmptyQuery(queries: DataQuery[]): boolean {
  120. return queries.some(
  121. query =>
  122. Object.keys(query)
  123. .map(k => query[k])
  124. .filter(v => v).length > 2
  125. );
  126. }
  127. export function calculateResultsFromQueryTransactions(
  128. queryTransactions: QueryTransaction[],
  129. datasource: any,
  130. graphInterval: number
  131. ) {
  132. const graphResult = _.flatten(
  133. queryTransactions.filter(qt => qt.resultType === 'Graph' && qt.done && qt.result).map(qt => qt.result)
  134. );
  135. const tableResult = mergeTablesIntoModel(
  136. new TableModel(),
  137. ...queryTransactions.filter(qt => qt.resultType === 'Table' && qt.done && qt.result).map(qt => qt.result)
  138. );
  139. const logsResult =
  140. datasource && datasource.mergeStreams
  141. ? datasource.mergeStreams(
  142. _.flatten(
  143. queryTransactions.filter(qt => qt.resultType === 'Logs' && qt.done && qt.result).map(qt => qt.result)
  144. ),
  145. graphInterval
  146. )
  147. : undefined;
  148. return {
  149. graphResult,
  150. tableResult,
  151. logsResult,
  152. };
  153. }
  154. export function getIntervals(
  155. range: RawTimeRange,
  156. datasource,
  157. resolution: number
  158. ): { interval: string; intervalMs: number } {
  159. if (!datasource || !resolution) {
  160. return { interval: '1s', intervalMs: 1000 };
  161. }
  162. const absoluteRange: RawTimeRange = {
  163. from: parseDate(range.from, false),
  164. to: parseDate(range.to, true),
  165. };
  166. return kbn.calculateInterval(absoluteRange, resolution, datasource.interval);
  167. }
  168. export function makeTimeSeriesList(dataList) {
  169. return dataList.map((seriesData, index) => {
  170. const datapoints = seriesData.datapoints || [];
  171. const alias = seriesData.target;
  172. const colorIndex = index % colors.length;
  173. const color = colors[colorIndex];
  174. const series = new TimeSeries({
  175. datapoints,
  176. alias,
  177. color,
  178. unit: seriesData.unit,
  179. });
  180. return series;
  181. });
  182. }
  183. /**
  184. * Update the query history. Side-effect: store history in local storage
  185. */
  186. export function updateHistory(history: HistoryItem[], datasourceId: string, queries: DataQuery[]): HistoryItem[] {
  187. const ts = Date.now();
  188. queries.forEach(query => {
  189. history = [{ query, ts }, ...history];
  190. });
  191. if (history.length > MAX_HISTORY_ITEMS) {
  192. history = history.slice(0, MAX_HISTORY_ITEMS);
  193. }
  194. // Combine all queries of a datasource type into one history
  195. const historyKey = `grafana.explore.history.${datasourceId}`;
  196. store.setObject(historyKey, history);
  197. return history;
  198. }
  199. export function clearHistory(datasourceId: string) {
  200. const historyKey = `grafana.explore.history.${datasourceId}`;
  201. store.delete(historyKey);
  202. }