瀏覽代碼

Explore: Introduce DataQuery interface for query handling

- Queries in Explore have been string based
- This PR introduces the use of the DataQuery type to denote all queries handled in Explore
- Within Explore all handling of DataQueries is transparent
- Modifying DataQueries is left to the datasource
- Using `target` as variable names for DataQueries to be consistent with the rest of Grafana
David Kaltschmidt 7 年之前
父節點
當前提交
b3161bea5a

+ 28 - 21
public/app/core/utils/explore.test.ts

@@ -10,7 +10,7 @@ const DEFAULT_EXPLORE_STATE: ExploreState = {
   exploreDatasources: [],
   exploreDatasources: [],
   graphRange: DEFAULT_RANGE,
   graphRange: DEFAULT_RANGE,
   history: [],
   history: [],
-  queries: [],
+  initialTargets: [],
   queryTransactions: [],
   queryTransactions: [],
   range: DEFAULT_RANGE,
   range: DEFAULT_RANGE,
   showingGraph: true,
   showingGraph: true,
@@ -26,17 +26,17 @@ describe('state functions', () => {
     it('returns default state on empty string', () => {
     it('returns default state on empty string', () => {
       expect(parseUrlState('')).toMatchObject({
       expect(parseUrlState('')).toMatchObject({
         datasource: null,
         datasource: null,
-        queries: [],
+        targets: [],
         range: DEFAULT_RANGE,
         range: DEFAULT_RANGE,
       });
       });
     });
     });
 
 
     it('returns a valid Explore state from URL parameter', () => {
     it('returns a valid Explore state from URL parameter', () => {
       const paramValue =
       const paramValue =
-        '%7B"datasource":"Local","queries":%5B%7B"query":"metric"%7D%5D,"range":%7B"from":"now-1h","to":"now"%7D%7D';
+        '%7B"datasource":"Local","targets":%5B%7B"expr":"metric"%7D%5D,"range":%7B"from":"now-1h","to":"now"%7D%7D';
       expect(parseUrlState(paramValue)).toMatchObject({
       expect(parseUrlState(paramValue)).toMatchObject({
         datasource: 'Local',
         datasource: 'Local',
-        queries: [{ query: 'metric' }],
+        targets: [{ expr: 'metric' }],
         range: {
         range: {
           from: 'now-1h',
           from: 'now-1h',
           to: 'now',
           to: 'now',
@@ -45,10 +45,10 @@ describe('state functions', () => {
     });
     });
 
 
     it('returns a valid Explore state from a compact URL parameter', () => {
     it('returns a valid Explore state from a compact URL parameter', () => {
-      const paramValue = '%5B"now-1h","now","Local","metric"%5D';
+      const paramValue = '%5B"now-1h","now","Local",%7B"expr":"metric"%7D%5D';
       expect(parseUrlState(paramValue)).toMatchObject({
       expect(parseUrlState(paramValue)).toMatchObject({
         datasource: 'Local',
         datasource: 'Local',
-        queries: [{ query: 'metric' }],
+        targets: [{ expr: 'metric' }],
         range: {
         range: {
           from: 'now-1h',
           from: 'now-1h',
           to: 'now',
           to: 'now',
@@ -66,18 +66,20 @@ describe('state functions', () => {
           from: 'now-5h',
           from: 'now-5h',
           to: 'now',
           to: 'now',
         },
         },
-        queries: [
+        initialTargets: [
           {
           {
-            query: 'metric{test="a/b"}',
+            refId: '1',
+            expr: 'metric{test="a/b"}',
           },
           },
           {
           {
-            query: 'super{foo="x/z"}',
+            refId: '2',
+            expr: 'super{foo="x/z"}',
           },
           },
         ],
         ],
       };
       };
       expect(serializeStateToUrlParam(state)).toBe(
       expect(serializeStateToUrlParam(state)).toBe(
-        '{"datasource":"foo","queries":[{"query":"metric{test=\\"a/b\\"}"},' +
-          '{"query":"super{foo=\\"x/z\\"}"}],"range":{"from":"now-5h","to":"now"}}'
+        '{"datasource":"foo","targets":[{"expr":"metric{test=\\"a/b\\"}"},' +
+          '{"expr":"super{foo=\\"x/z\\"}"}],"range":{"from":"now-5h","to":"now"}}'
       );
       );
     });
     });
 
 
@@ -89,17 +91,19 @@ describe('state functions', () => {
           from: 'now-5h',
           from: 'now-5h',
           to: 'now',
           to: 'now',
         },
         },
-        queries: [
+        initialTargets: [
           {
           {
-            query: 'metric{test="a/b"}',
+            refId: '1',
+            expr: 'metric{test="a/b"}',
           },
           },
           {
           {
-            query: 'super{foo="x/z"}',
+            refId: '2',
+            expr: 'super{foo="x/z"}',
           },
           },
         ],
         ],
       };
       };
       expect(serializeStateToUrlParam(state, true)).toBe(
       expect(serializeStateToUrlParam(state, true)).toBe(
-        '["now-5h","now","foo","metric{test=\\"a/b\\"}","super{foo=\\"x/z\\"}"]'
+        '["now-5h","now","foo",{"expr":"metric{test=\\"a/b\\"}"},{"expr":"super{foo=\\"x/z\\"}"}]'
       );
       );
     });
     });
   });
   });
@@ -113,12 +117,14 @@ describe('state functions', () => {
           from: 'now - 5h',
           from: 'now - 5h',
           to: 'now',
           to: 'now',
         },
         },
-        queries: [
+        initialTargets: [
           {
           {
-            query: 'metric{test="a/b"}',
+            refId: '1',
+            expr: 'metric{test="a/b"}',
           },
           },
           {
           {
-            query: 'super{foo="x/z"}',
+            refId: '2',
+            expr: 'super{foo="x/z"}',
           },
           },
         ],
         ],
       };
       };
@@ -126,14 +132,15 @@ describe('state functions', () => {
       const parsed = parseUrlState(serialized);
       const parsed = parseUrlState(serialized);
 
 
       // Account for datasource vs datasourceName
       // Account for datasource vs datasourceName
-      const { datasource, ...rest } = parsed;
-      const sameState = {
+      const { datasource, targets, ...rest } = parsed;
+      const resultState = {
         ...rest,
         ...rest,
         datasource: DEFAULT_EXPLORE_STATE.datasource,
         datasource: DEFAULT_EXPLORE_STATE.datasource,
         datasourceName: datasource,
         datasourceName: datasource,
+        initialTargets: targets,
       };
       };
 
 
-      expect(state).toMatchObject(sameState);
+      expect(state).toMatchObject(resultState);
     });
     });
   });
   });
 });
 });

+ 96 - 11
public/app/core/utils/explore.ts

@@ -1,11 +1,20 @@
 import { renderUrl } from 'app/core/utils/url';
 import { renderUrl } from 'app/core/utils/url';
-import { ExploreState, ExploreUrlState } from 'app/types/explore';
+import { ExploreState, ExploreUrlState, HistoryItem } from 'app/types/explore';
+import { DataQuery, RawTimeRange } from 'app/types/series';
+
+import kbn from 'app/core/utils/kbn';
+import colors from 'app/core/utils/colors';
+import TimeSeries from 'app/core/time_series2';
+import { parse as parseDate } from 'app/core/utils/datemath';
+import store from 'app/core/store';
 
 
 export const DEFAULT_RANGE = {
 export const DEFAULT_RANGE = {
   from: 'now-6h',
   from: 'now-6h',
   to: 'now',
   to: 'now',
 };
 };
 
 
+const MAX_HISTORY_ITEMS = 100;
+
 /**
 /**
  * Returns an Explore-URL that contains a panel's queries and the dashboard time range.
  * Returns an Explore-URL that contains a panel's queries and the dashboard time range.
  *
  *
@@ -70,30 +79,106 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState {
           to: parsed[1],
           to: parsed[1],
         };
         };
         const datasource = parsed[2];
         const datasource = parsed[2];
-        const queries = parsed.slice(3).map(query => ({ query }));
-        return { datasource, queries, range };
+        const targets = parsed.slice(3);
+        return { datasource, targets, range };
       }
       }
       return parsed;
       return parsed;
     } catch (e) {
     } catch (e) {
       console.error(e);
       console.error(e);
     }
     }
   }
   }
-  return { datasource: null, queries: [], range: DEFAULT_RANGE };
+  return { datasource: null, targets: [], range: DEFAULT_RANGE };
 }
 }
 
 
 export function serializeStateToUrlParam(state: ExploreState, compact?: boolean): string {
 export function serializeStateToUrlParam(state: ExploreState, compact?: boolean): string {
   const urlState: ExploreUrlState = {
   const urlState: ExploreUrlState = {
     datasource: state.datasourceName,
     datasource: state.datasourceName,
-    queries: state.queries.map(q => ({ query: q.query })),
+    targets: state.initialTargets.map(({ key, refId, ...rest }) => rest),
     range: state.range,
     range: state.range,
   };
   };
   if (compact) {
   if (compact) {
-    return JSON.stringify([
-      urlState.range.from,
-      urlState.range.to,
-      urlState.datasource,
-      ...urlState.queries.map(q => q.query),
-    ]);
+    return JSON.stringify([urlState.range.from, urlState.range.to, urlState.datasource, ...urlState.targets]);
   }
   }
   return JSON.stringify(urlState);
   return JSON.stringify(urlState);
 }
 }
+
+export function generateKey(index = 0): string {
+  return `Q-${Date.now()}-${Math.random()}-${index}`;
+}
+
+export function generateRefId(index = 0): string {
+  return `${index + 1}`;
+}
+
+export function generateTargetKeys(index = 0): { refId: string; key: string } {
+  return { refId: generateRefId(index), key: generateKey(index) };
+}
+
+/**
+ * Ensure at least one target exists and that targets have the necessary keys
+ */
+export function ensureTargets(targets?: DataQuery[]): DataQuery[] {
+  if (targets && typeof targets === 'object' && targets.length > 0) {
+    return targets.map((target, i) => ({ ...target, ...generateTargetKeys(i) }));
+  }
+  return [{ ...generateTargetKeys() }];
+}
+
+/**
+ * A target is non-empty when it has keys other than refId and key.
+ */
+export function hasNonEmptyTarget(targets: DataQuery[]): boolean {
+  return targets.some(target => Object.keys(target).length > 2);
+}
+
+export function getIntervals(
+  range: RawTimeRange,
+  datasource,
+  resolution: number
+): { interval: string; intervalMs: number } {
+  if (!datasource || !resolution) {
+    return { interval: '1s', intervalMs: 1000 };
+  }
+  const absoluteRange: RawTimeRange = {
+    from: parseDate(range.from, false),
+    to: parseDate(range.to, true),
+  };
+  return kbn.calculateInterval(absoluteRange, resolution, datasource.interval);
+}
+
+export function makeTimeSeriesList(dataList, options) {
+  return dataList.map((seriesData, index) => {
+    const datapoints = seriesData.datapoints || [];
+    const alias = seriesData.target;
+    const colorIndex = index % colors.length;
+    const color = colors[colorIndex];
+
+    const series = new TimeSeries({
+      datapoints,
+      alias,
+      color,
+      unit: seriesData.unit,
+    });
+
+    return series;
+  });
+}
+
+/**
+ * Update the query history. Side-effect: store history in local storage
+ */
+export function updateHistory(history: HistoryItem[], datasourceId: string, targets: DataQuery[]): HistoryItem[] {
+  const ts = Date.now();
+  targets.forEach(target => {
+    history = [{ target, ts }, ...history];
+  });
+
+  if (history.length > MAX_HISTORY_ITEMS) {
+    history = history.slice(0, MAX_HISTORY_ITEMS);
+  }
+
+  // Combine all queries of a datasource type into one history
+  const historyKey = `grafana.explore.history.${datasourceId}`;
+  store.setObject(historyKey, history);
+  return history;
+}

+ 146 - 199
public/app/features/explore/Explore.tsx

@@ -4,14 +4,26 @@ import Select from 'react-select';
 import _ from 'lodash';
 import _ from 'lodash';
 
 
 import { DataSource } from 'app/types/datasources';
 import { DataSource } from 'app/types/datasources';
-import { ExploreState, ExploreUrlState, HistoryItem, Query, QueryTransaction, ResultType } from 'app/types/explore';
+import {
+  ExploreState,
+  ExploreUrlState,
+  QueryTransaction,
+  ResultType,
+  QueryHintGetter,
+  QueryHint,
+} from 'app/types/explore';
 import { RawTimeRange, DataQuery } from 'app/types/series';
 import { RawTimeRange, DataQuery } from 'app/types/series';
-import kbn from 'app/core/utils/kbn';
-import colors from 'app/core/utils/colors';
 import store from 'app/core/store';
 import store from 'app/core/store';
-import TimeSeries from 'app/core/time_series2';
-import { parse as parseDate } from 'app/core/utils/datemath';
-import { DEFAULT_RANGE } from 'app/core/utils/explore';
+import {
+  DEFAULT_RANGE,
+  ensureTargets,
+  getIntervals,
+  generateKey,
+  generateTargetKeys,
+  hasNonEmptyTarget,
+  makeTimeSeriesList,
+  updateHistory,
+} from 'app/core/utils/explore';
 import ResetStyles from 'app/core/components/Picker/ResetStyles';
 import ResetStyles from 'app/core/components/Picker/ResetStyles';
 import PickerOption from 'app/core/components/Picker/PickerOption';
 import PickerOption from 'app/core/components/Picker/PickerOption';
 import IndicatorsContainer from 'app/core/components/Picker/IndicatorsContainer';
 import IndicatorsContainer from 'app/core/components/Picker/IndicatorsContainer';
@@ -26,57 +38,6 @@ import Logs from './Logs';
 import Table from './Table';
 import Table from './Table';
 import ErrorBoundary from './ErrorBoundary';
 import ErrorBoundary from './ErrorBoundary';
 import TimePicker from './TimePicker';
 import TimePicker from './TimePicker';
-import { ensureQueries, generateQueryKey, hasQuery } from './utils/query';
-
-const MAX_HISTORY_ITEMS = 100;
-
-function getIntervals(range: RawTimeRange, datasource, resolution: number): { interval: string; intervalMs: number } {
-  if (!datasource || !resolution) {
-    return { interval: '1s', intervalMs: 1000 };
-  }
-  const absoluteRange: RawTimeRange = {
-    from: parseDate(range.from, false),
-    to: parseDate(range.to, true),
-  };
-  return kbn.calculateInterval(absoluteRange, resolution, datasource.interval);
-}
-
-function makeTimeSeriesList(dataList, options) {
-  return dataList.map((seriesData, index) => {
-    const datapoints = seriesData.datapoints || [];
-    const alias = seriesData.target;
-    const colorIndex = index % colors.length;
-    const color = colors[colorIndex];
-
-    const series = new TimeSeries({
-      datapoints,
-      alias,
-      color,
-      unit: seriesData.unit,
-    });
-
-    return series;
-  });
-}
-
-/**
- * Update the query history. Side-effect: store history in local storage
- */
-function updateHistory(history: HistoryItem[], datasourceId: string, queries: string[]): HistoryItem[] {
-  const ts = Date.now();
-  queries.forEach(query => {
-    history = [{ query, ts }, ...history];
-  });
-
-  if (history.length > MAX_HISTORY_ITEMS) {
-    history = history.slice(0, MAX_HISTORY_ITEMS);
-  }
-
-  // Combine all queries of a datasource type into one history
-  const historyKey = `grafana.explore.history.${datasourceId}`;
-  store.setObject(historyKey, history);
-  return history;
-}
 
 
 interface ExploreProps {
 interface ExploreProps {
   datasourceSrv: DatasourceSrv;
   datasourceSrv: DatasourceSrv;
@@ -89,14 +50,20 @@ interface ExploreProps {
   urlState: ExploreUrlState;
   urlState: ExploreUrlState;
 }
 }
 
 
+/**
+ * Explore provides an area for quick query iteration for a given datasource.
+ * Once a datasource is selected it populates the query section at the top.
+ * When queries are run, their results are being displayed in the main section.
+ * The datasource determines what kind of query editor it brings, and what kind
+ * of results viewers it supports.
+ */
 export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
 export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
   el: any;
   el: any;
   /**
   /**
    * Current query expressions of the rows including their modifications, used for running queries.
    * Current query expressions of the rows including their modifications, used for running queries.
    * Not kept in component state to prevent edit-render roundtrips.
    * Not kept in component state to prevent edit-render roundtrips.
-   * TODO: make this generic (other datasources might not have string representations of current query state)
    */
    */
-  queryExpressions: string[];
+  modifiedTargets: DataQuery[];
   /**
   /**
    * Local ID cache to compare requested vs selected datasource
    * Local ID cache to compare requested vs selected datasource
    */
    */
@@ -105,14 +72,14 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
     const splitState: ExploreState = props.splitState;
     const splitState: ExploreState = props.splitState;
-    let initialQueries: Query[];
+    let initialTargets: DataQuery[];
     if (splitState) {
     if (splitState) {
       // Split state overrides everything
       // Split state overrides everything
       this.state = splitState;
       this.state = splitState;
-      initialQueries = splitState.queries;
+      initialTargets = splitState.initialTargets;
     } else {
     } else {
-      const { datasource, queries, range } = props.urlState as ExploreUrlState;
-      initialQueries = ensureQueries(queries);
+      const { datasource, targets, range } = props.urlState as ExploreUrlState;
+      initialTargets = ensureTargets(targets);
       const initialRange = range || { ...DEFAULT_RANGE };
       const initialRange = range || { ...DEFAULT_RANGE };
       this.state = {
       this.state = {
         datasource: null,
         datasource: null,
@@ -122,8 +89,8 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
         datasourceName: datasource,
         datasourceName: datasource,
         exploreDatasources: [],
         exploreDatasources: [],
         graphRange: initialRange,
         graphRange: initialRange,
+        initialTargets,
         history: [],
         history: [],
-        queries: initialQueries,
         queryTransactions: [],
         queryTransactions: [],
         range: initialRange,
         range: initialRange,
         showingGraph: true,
         showingGraph: true,
@@ -135,7 +102,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
         supportsTable: null,
         supportsTable: null,
       };
       };
     }
     }
-    this.queryExpressions = initialQueries.map(q => q.query);
+    this.modifiedTargets = initialTargets.slice();
   }
   }
 
 
   async componentDidMount() {
   async componentDidMount() {
@@ -198,32 +165,26 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
     }
     }
 
 
     // Check if queries can be imported from previously selected datasource
     // Check if queries can be imported from previously selected datasource
-    let queryExpressions = this.queryExpressions;
+    let modifiedTargets = this.modifiedTargets;
     if (origin) {
     if (origin) {
       if (origin.meta.id === datasource.meta.id) {
       if (origin.meta.id === datasource.meta.id) {
         // Keep same queries if same type of datasource
         // Keep same queries if same type of datasource
-        queryExpressions = [...this.queryExpressions];
+        modifiedTargets = [...this.modifiedTargets];
       } else if (datasource.importQueries) {
       } else if (datasource.importQueries) {
-        // Datasource-specific importers, wrapping to satisfy interface
-        const wrappedQueries: DataQuery[] = this.queryExpressions.map((query, index) => ({
-          refId: String(index),
-          expr: query,
-        }));
-        const modifiedQueries: DataQuery[] = await datasource.importQueries(wrappedQueries, origin.meta);
-        queryExpressions = modifiedQueries.map(({ expr }) => expr);
+        // Datasource-specific importers
+        modifiedTargets = await datasource.importQueries(this.modifiedTargets, origin.meta);
       } else {
       } else {
         // Default is blank queries
         // Default is blank queries
-        queryExpressions = this.queryExpressions.map(() => '');
+        modifiedTargets = ensureTargets();
       }
       }
     }
     }
 
 
     // Reset edit state with new queries
     // Reset edit state with new queries
-    const nextQueries = this.state.queries.map((q, i) => ({
-      ...q,
-      key: generateQueryKey(i),
-      query: queryExpressions[i],
+    const nextTargets = this.state.initialTargets.map((q, i) => ({
+      ...modifiedTargets[i],
+      ...generateTargetKeys(i),
     }));
     }));
-    this.queryExpressions = queryExpressions;
+    this.modifiedTargets = modifiedTargets;
 
 
     // Custom components
     // Custom components
     const StartPage = datasource.pluginExports.ExploreStartPage;
     const StartPage = datasource.pluginExports.ExploreStartPage;
@@ -239,7 +200,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
         supportsTable,
         supportsTable,
         datasourceLoading: false,
         datasourceLoading: false,
         datasourceName: datasource.name,
         datasourceName: datasource.name,
-        queries: nextQueries,
+        initialTargets: nextTargets,
         showingStartPage: Boolean(StartPage),
         showingStartPage: Boolean(StartPage),
       },
       },
       () => {
       () => {
@@ -256,16 +217,15 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
 
 
   onAddQueryRow = index => {
   onAddQueryRow = index => {
     // Local cache
     // Local cache
-    this.queryExpressions[index + 1] = '';
+    this.modifiedTargets[index + 1] = { ...generateTargetKeys(index + 1) };
 
 
     this.setState(state => {
     this.setState(state => {
-      const { queries, queryTransactions } = state;
+      const { initialTargets, queryTransactions } = state;
 
 
-      // Add row by generating new react key
-      const nextQueries = [
-        ...queries.slice(0, index + 1),
-        { query: '', key: generateQueryKey() },
-        ...queries.slice(index + 1),
+      const nextTargets = [
+        ...initialTargets.slice(0, index + 1),
+        { ...this.modifiedTargets[index + 1] },
+        ...initialTargets.slice(index + 1),
       ];
       ];
 
 
       // Ongoing transactions need to update their row indices
       // Ongoing transactions need to update their row indices
@@ -279,7 +239,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
         return qt;
         return qt;
       });
       });
 
 
-      return { queries: nextQueries, queryTransactions: nextQueryTransactions };
+      return { initialTargets: nextTargets, queryTransactions: nextQueryTransactions };
     });
     });
   };
   };
 
 
@@ -296,26 +256,26 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
     this.setDatasource(datasource as any, origin);
     this.setDatasource(datasource as any, origin);
   };
   };
 
 
-  onChangeQuery = (value: string, index: number, override?: boolean) => {
+  onChangeQuery = (value: DataQuery, index: number, override?: boolean) => {
     // Keep current value in local cache
     // Keep current value in local cache
-    this.queryExpressions[index] = value;
+    this.modifiedTargets[index] = value;
 
 
     if (override) {
     if (override) {
       this.setState(state => {
       this.setState(state => {
         // Replace query row
         // Replace query row
-        const { queries, queryTransactions } = state;
-        const nextQuery: Query = {
-          key: generateQueryKey(index),
-          query: value,
+        const { initialTargets, queryTransactions } = state;
+        const target: DataQuery = {
+          ...value,
+          ...generateTargetKeys(index),
         };
         };
-        const nextQueries = [...queries];
-        nextQueries[index] = nextQuery;
+        const nextTargets = [...initialTargets];
+        nextTargets[index] = target;
 
 
         // Discard ongoing transaction related to row query
         // Discard ongoing transaction related to row query
         const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index);
         const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index);
 
 
         return {
         return {
-          queries: nextQueries,
+          initialTargets: nextTargets,
           queryTransactions: nextQueryTransactions,
           queryTransactions: nextQueryTransactions,
         };
         };
       }, this.onSubmit);
       }, this.onSubmit);
@@ -330,10 +290,10 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
   };
   };
 
 
   onClickClear = () => {
   onClickClear = () => {
-    this.queryExpressions = [''];
+    this.modifiedTargets = ensureTargets();
     this.setState(
     this.setState(
       prevState => ({
       prevState => ({
-        queries: ensureQueries(),
+        initialTargets: [...this.modifiedTargets],
         queryTransactions: [],
         queryTransactions: [],
         showingStartPage: Boolean(prevState.StartPage),
         showingStartPage: Boolean(prevState.StartPage),
       }),
       }),
@@ -387,10 +347,10 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
   };
   };
 
 
   // Use this in help pages to set page to a single query
   // Use this in help pages to set page to a single query
-  onClickQuery = query => {
-    const nextQueries = [{ query, key: generateQueryKey() }];
-    this.queryExpressions = nextQueries.map(q => q.query);
-    this.setState({ queries: nextQueries }, this.onSubmit);
+  onClickExample = (target: DataQuery) => {
+    const nextTargets = [{ ...target, ...generateTargetKeys() }];
+    this.modifiedTargets = [...nextTargets];
+    this.setState({ initialTargets: nextTargets }, this.onSubmit);
   };
   };
 
 
   onClickSplit = () => {
   onClickSplit = () => {
@@ -430,28 +390,28 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
       const preventSubmit = action.preventSubmit;
       const preventSubmit = action.preventSubmit;
       this.setState(
       this.setState(
         state => {
         state => {
-          const { queries, queryTransactions } = state;
-          let nextQueries;
+          const { initialTargets, queryTransactions } = state;
+          let nextTargets: DataQuery[];
           let nextQueryTransactions;
           let nextQueryTransactions;
           if (index === undefined) {
           if (index === undefined) {
             // Modify all queries
             // Modify all queries
-            nextQueries = queries.map((q, i) => ({
-              key: generateQueryKey(i),
-              query: datasource.modifyQuery(this.queryExpressions[i], action),
+            nextTargets = initialTargets.map((target, i) => ({
+              ...datasource.modifyQuery(this.modifiedTargets[i], action),
+              ...generateTargetKeys(i),
             }));
             }));
             // Discard all ongoing transactions
             // Discard all ongoing transactions
             nextQueryTransactions = [];
             nextQueryTransactions = [];
           } else {
           } else {
             // Modify query only at index
             // Modify query only at index
-            nextQueries = queries.map((q, i) => {
+            nextTargets = initialTargets.map((target, i) => {
               // Synchronise all queries with local query cache to ensure consistency
               // Synchronise all queries with local query cache to ensure consistency
-              q.query = this.queryExpressions[i];
+              // TODO still needed?
               return i === index
               return i === index
                 ? {
                 ? {
-                    key: generateQueryKey(index),
-                    query: datasource.modifyQuery(q.query, action),
+                    ...datasource.modifyQuery(this.modifiedTargets[i], action),
+                    ...generateTargetKeys(i),
                   }
                   }
-                : q;
+                : target;
             });
             });
             nextQueryTransactions = queryTransactions
             nextQueryTransactions = queryTransactions
               // Consume the hint corresponding to the action
               // Consume the hint corresponding to the action
@@ -464,9 +424,9 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
               // Preserve previous row query transaction to keep results visible if next query is incomplete
               // Preserve previous row query transaction to keep results visible if next query is incomplete
               .filter(qt => preventSubmit || qt.rowIndex !== index);
               .filter(qt => preventSubmit || qt.rowIndex !== index);
           }
           }
-          this.queryExpressions = nextQueries.map(q => q.query);
+          this.modifiedTargets = [...nextTargets];
           return {
           return {
-            queries: nextQueries,
+            initialTargets: nextTargets,
             queryTransactions: nextQueryTransactions,
             queryTransactions: nextQueryTransactions,
           };
           };
         },
         },
@@ -478,22 +438,22 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
 
 
   onRemoveQueryRow = index => {
   onRemoveQueryRow = index => {
     // Remove from local cache
     // Remove from local cache
-    this.queryExpressions = [...this.queryExpressions.slice(0, index), ...this.queryExpressions.slice(index + 1)];
+    this.modifiedTargets = [...this.modifiedTargets.slice(0, index), ...this.modifiedTargets.slice(index + 1)];
 
 
     this.setState(
     this.setState(
       state => {
       state => {
-        const { queries, queryTransactions } = state;
-        if (queries.length <= 1) {
+        const { initialTargets, queryTransactions } = state;
+        if (initialTargets.length <= 1) {
           return null;
           return null;
         }
         }
         // Remove row from react state
         // Remove row from react state
-        const nextQueries = [...queries.slice(0, index), ...queries.slice(index + 1)];
+        const nextTargets = [...initialTargets.slice(0, index), ...initialTargets.slice(index + 1)];
 
 
         // Discard transactions related to row query
         // Discard transactions related to row query
         const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index);
         const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index);
 
 
         return {
         return {
-          queries: nextQueries,
+          initialTargets: nextTargets,
           queryTransactions: nextQueryTransactions,
           queryTransactions: nextQueryTransactions,
         };
         };
       },
       },
@@ -515,40 +475,39 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
     this.saveState();
     this.saveState();
   };
   };
 
 
-  buildQueryOptions(
-    query: string,
-    rowIndex: number,
-    targetOptions: { format: string; hinting?: boolean; instant?: boolean }
-  ) {
+  buildQueryOptions(target: DataQuery, targetOptions: { format: string; hinting?: boolean; instant?: boolean }) {
     const { datasource, range } = this.state;
     const { datasource, range } = this.state;
     const { interval, intervalMs } = getIntervals(range, datasource, this.el.offsetWidth);
     const { interval, intervalMs } = getIntervals(range, datasource, this.el.offsetWidth);
     const targets = [
     const targets = [
       {
       {
         ...targetOptions,
         ...targetOptions,
-        // Target identifier is needed for table transformations
-        refId: rowIndex + 1,
-        expr: query,
+        ...target,
       },
       },
     ];
     ];
 
 
     // Clone range for query request
     // Clone range for query request
     const queryRange: RawTimeRange = { ...range };
     const queryRange: RawTimeRange = { ...range };
 
 
+    // Datasource is using `panelId + target.refId` for cancellation logic.
+    // Using `format` here because it relates to the view panel that the request is for.
+    const panelId = targetOptions.format;
+
     return {
     return {
       interval,
       interval,
       intervalMs,
       intervalMs,
+      panelId,
       targets,
       targets,
       range: queryRange,
       range: queryRange,
     };
     };
   }
   }
 
 
-  startQueryTransaction(query: string, rowIndex: number, resultType: ResultType, options: any): QueryTransaction {
-    const queryOptions = this.buildQueryOptions(query, rowIndex, options);
+  startQueryTransaction(target: DataQuery, rowIndex: number, resultType: ResultType, options: any): QueryTransaction {
+    const queryOptions = this.buildQueryOptions(target, options);
     const transaction: QueryTransaction = {
     const transaction: QueryTransaction = {
-      query,
+      target,
       resultType,
       resultType,
       rowIndex,
       rowIndex,
-      id: generateQueryKey(),
+      id: generateKey(), // reusing for unique ID
       done: false,
       done: false,
       latency: 0,
       latency: 0,
       options: queryOptions,
       options: queryOptions,
@@ -578,7 +537,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
     transactionId: string,
     transactionId: string,
     result: any,
     result: any,
     latency: number,
     latency: number,
-    queries: string[],
+    targets: DataQuery[],
     datasourceId: string
     datasourceId: string
   ) {
   ) {
     const { datasource } = this.state;
     const { datasource } = this.state;
@@ -597,9 +556,9 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
       }
       }
 
 
       // Get query hints
       // Get query hints
-      let hints;
-      if (datasource.getQueryHints) {
-        hints = datasource.getQueryHints(transaction.query, result);
+      let hints: QueryHint[];
+      if (datasource.getQueryHints as QueryHintGetter) {
+        hints = datasource.getQueryHints(transaction.target, result);
       }
       }
 
 
       // Mark transactions as complete
       // Mark transactions as complete
@@ -616,7 +575,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
         return qt;
         return qt;
       });
       });
 
 
-      const nextHistory = updateHistory(history, datasourceId, queries);
+      const nextHistory = updateHistory(history, datasourceId, targets);
 
 
       return {
       return {
         history: nextHistory,
         history: nextHistory,
@@ -634,7 +593,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
 
 
   failQueryTransaction(transactionId: string, response: any, datasourceId: string) {
   failQueryTransaction(transactionId: string, response: any, datasourceId: string) {
     const { datasource } = this.state;
     const { datasource } = this.state;
-    if (datasource.meta.id !== datasourceId) {
+    if (datasource.meta.id !== datasourceId || response.cancelled) {
       // Navigated away, queries did not matter
       // Navigated away, queries did not matter
       return;
       return;
     }
     }
@@ -679,87 +638,75 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
   }
   }
 
 
   async runGraphQueries() {
   async runGraphQueries() {
-    const queries = [...this.queryExpressions];
-    if (!hasQuery(queries)) {
+    const targets = [...this.modifiedTargets];
+    if (!hasNonEmptyTarget(targets)) {
       return;
       return;
     }
     }
     const { datasource } = this.state;
     const { datasource } = this.state;
     const datasourceId = datasource.meta.id;
     const datasourceId = datasource.meta.id;
     // Run all queries concurrently
     // Run all queries concurrently
-    queries.forEach(async (query, rowIndex) => {
-      if (query) {
-        const transaction = this.startQueryTransaction(query, rowIndex, 'Graph', {
-          format: 'time_series',
-          instant: false,
-        });
-        try {
-          const now = Date.now();
-          const res = await datasource.query(transaction.options);
-          const latency = Date.now() - now;
-          const results = makeTimeSeriesList(res.data, transaction.options);
-          this.completeQueryTransaction(transaction.id, results, latency, queries, datasourceId);
-          this.setState({ graphRange: transaction.options.range });
-        } catch (response) {
-          this.failQueryTransaction(transaction.id, response, datasourceId);
-        }
-      } else {
-        this.discardTransactions(rowIndex);
+    targets.forEach(async (target, rowIndex) => {
+      const transaction = this.startQueryTransaction(target, rowIndex, 'Graph', {
+        format: 'time_series',
+        instant: false,
+      });
+      try {
+        const now = Date.now();
+        const res = await datasource.query(transaction.options);
+        const latency = Date.now() - now;
+        const results = makeTimeSeriesList(res.data, transaction.options);
+        this.completeQueryTransaction(transaction.id, results, latency, targets, datasourceId);
+        this.setState({ graphRange: transaction.options.range });
+      } catch (response) {
+        this.failQueryTransaction(transaction.id, response, datasourceId);
       }
       }
     });
     });
   }
   }
 
 
   async runTableQuery() {
   async runTableQuery() {
-    const queries = [...this.queryExpressions];
-    if (!hasQuery(queries)) {
+    const targets = [...this.modifiedTargets];
+    if (!hasNonEmptyTarget(targets)) {
       return;
       return;
     }
     }
     const { datasource } = this.state;
     const { datasource } = this.state;
     const datasourceId = datasource.meta.id;
     const datasourceId = datasource.meta.id;
     // Run all queries concurrently
     // Run all queries concurrently
-    queries.forEach(async (query, rowIndex) => {
-      if (query) {
-        const transaction = this.startQueryTransaction(query, rowIndex, 'Table', {
-          format: 'table',
-          instant: true,
-          valueWithRefId: true,
-        });
-        try {
-          const now = Date.now();
-          const res = await datasource.query(transaction.options);
-          const latency = Date.now() - now;
-          const results = res.data[0];
-          this.completeQueryTransaction(transaction.id, results, latency, queries, datasourceId);
-        } catch (response) {
-          this.failQueryTransaction(transaction.id, response, datasourceId);
-        }
-      } else {
-        this.discardTransactions(rowIndex);
+    targets.forEach(async (target, rowIndex) => {
+      const transaction = this.startQueryTransaction(target, rowIndex, 'Table', {
+        format: 'table',
+        instant: true,
+        valueWithRefId: true,
+      });
+      try {
+        const now = Date.now();
+        const res = await datasource.query(transaction.options);
+        const latency = Date.now() - now;
+        const results = res.data[0];
+        this.completeQueryTransaction(transaction.id, results, latency, targets, datasourceId);
+      } catch (response) {
+        this.failQueryTransaction(transaction.id, response, datasourceId);
       }
       }
     });
     });
   }
   }
 
 
   async runLogsQuery() {
   async runLogsQuery() {
-    const queries = [...this.queryExpressions];
-    if (!hasQuery(queries)) {
+    const targets = [...this.modifiedTargets];
+    if (!hasNonEmptyTarget(targets)) {
       return;
       return;
     }
     }
     const { datasource } = this.state;
     const { datasource } = this.state;
     const datasourceId = datasource.meta.id;
     const datasourceId = datasource.meta.id;
     // Run all queries concurrently
     // Run all queries concurrently
-    queries.forEach(async (query, rowIndex) => {
-      if (query) {
-        const transaction = this.startQueryTransaction(query, rowIndex, 'Logs', { format: 'logs' });
-        try {
-          const now = Date.now();
-          const res = await datasource.query(transaction.options);
-          const latency = Date.now() - now;
-          const results = res.data;
-          this.completeQueryTransaction(transaction.id, results, latency, queries, datasourceId);
-        } catch (response) {
-          this.failQueryTransaction(transaction.id, response, datasourceId);
-        }
-      } else {
-        this.discardTransactions(rowIndex);
+    targets.forEach(async (target, rowIndex) => {
+      const transaction = this.startQueryTransaction(target, rowIndex, 'Logs', { format: 'logs' });
+      try {
+        const now = Date.now();
+        const res = await datasource.query(transaction.options);
+        const latency = Date.now() - now;
+        const results = res.data;
+        this.completeQueryTransaction(transaction.id, results, latency, targets, datasourceId);
+      } catch (response) {
+        this.failQueryTransaction(transaction.id, response, datasourceId);
       }
       }
     });
     });
   }
   }
@@ -769,7 +716,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
     return {
     return {
       ...this.state,
       ...this.state,
       queryTransactions: [],
       queryTransactions: [],
-      queries: ensureQueries(this.queryExpressions.map(query => ({ query }))),
+      initialTargets: [...this.modifiedTargets],
     };
     };
   }
   }
 
 
@@ -789,7 +736,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
       exploreDatasources,
       exploreDatasources,
       graphRange,
       graphRange,
       history,
       history,
-      queries,
+      initialTargets,
       queryTransactions,
       queryTransactions,
       range,
       range,
       showingGraph,
       showingGraph,
@@ -903,7 +850,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
             <QueryRows
             <QueryRows
               datasource={datasource}
               datasource={datasource}
               history={history}
               history={history}
-              queries={queries}
+              initialTargets={initialTargets}
               onAddQueryRow={this.onAddQueryRow}
               onAddQueryRow={this.onAddQueryRow}
               onChangeQuery={this.onChangeQuery}
               onChangeQuery={this.onChangeQuery}
               onClickHintFix={this.onModifyQueries}
               onClickHintFix={this.onModifyQueries}
@@ -913,7 +860,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
             />
             />
             <main className="m-t-2">
             <main className="m-t-2">
               <ErrorBoundary>
               <ErrorBoundary>
-                {showingStartPage && <StartPage onClickQuery={this.onClickQuery} />}
+                {showingStartPage && <StartPage onClickExample={this.onClickExample} />}
                 {!showingStartPage && (
                 {!showingStartPage && (
                   <>
                   <>
                     {supportsGraph && (
                     {supportsGraph && (

+ 12 - 6
public/app/features/explore/QueryField.tsx

@@ -27,14 +27,14 @@ function hasSuggestions(suggestions: CompletionItemGroup[]): boolean {
   return suggestions && suggestions.length > 0;
   return suggestions && suggestions.length > 0;
 }
 }
 
 
-interface QueryFieldProps {
+export interface QueryFieldProps {
   additionalPlugins?: any[];
   additionalPlugins?: any[];
   cleanText?: (text: string) => string;
   cleanText?: (text: string) => string;
-  initialValue: string | null;
+  initialQuery: string | null;
   onBlur?: () => void;
   onBlur?: () => void;
   onFocus?: () => void;
   onFocus?: () => void;
   onTypeahead?: (typeahead: TypeaheadInput) => TypeaheadOutput;
   onTypeahead?: (typeahead: TypeaheadInput) => TypeaheadOutput;
-  onValueChanged?: (value: Value) => void;
+  onValueChanged?: (value: string) => void;
   onWillApplySuggestion?: (suggestion: string, state: QueryFieldState) => string;
   onWillApplySuggestion?: (suggestion: string, state: QueryFieldState) => string;
   placeholder?: string;
   placeholder?: string;
   portalOrigin?: string;
   portalOrigin?: string;
@@ -60,16 +60,22 @@ export interface TypeaheadInput {
   wrapperNode: Element;
   wrapperNode: Element;
 }
 }
 
 
+/**
+ * Renders an editor field.
+ * Pass initial value as initialQuery and listen to changes in props.onValueChanged.
+ * This component can only process strings. Internally it uses Slate Value.
+ * Implement props.onTypeahead to use suggestions, see PromQueryField.tsx as an example.
+ */
 export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldState> {
 export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldState> {
   menuEl: HTMLElement | null;
   menuEl: HTMLElement | null;
   placeholdersBuffer: PlaceholdersBuffer;
   placeholdersBuffer: PlaceholdersBuffer;
   plugins: any[];
   plugins: any[];
   resetTimer: any;
   resetTimer: any;
 
 
-  constructor(props, context) {
+  constructor(props: QueryFieldProps, context) {
     super(props, context);
     super(props, context);
 
 
-    this.placeholdersBuffer = new PlaceholdersBuffer(props.initialValue || '');
+    this.placeholdersBuffer = new PlaceholdersBuffer(props.initialQuery || '');
 
 
     // Base plugins
     // Base plugins
     this.plugins = [ClearPlugin(), NewlinePlugin(), ...props.additionalPlugins].filter(p => p);
     this.plugins = [ClearPlugin(), NewlinePlugin(), ...props.additionalPlugins].filter(p => p);
@@ -92,7 +98,7 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
     clearTimeout(this.resetTimer);
     clearTimeout(this.resetTimer);
   }
   }
 
 
-  componentDidUpdate(prevProps, prevState) {
+  componentDidUpdate(prevProps: QueryFieldProps, prevState: QueryFieldState) {
     // Only update menu location when suggestion existence or text/selection changed
     // Only update menu location when suggestion existence or text/selection changed
     if (
     if (
       this.state.value !== prevState.value ||
       this.state.value !== prevState.value ||

+ 13 - 13
public/app/features/explore/QueryRows.tsx

@@ -1,10 +1,10 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
 
 
-import { QueryTransaction, HistoryItem, Query, QueryHint } from 'app/types/explore';
+import { QueryTransaction, HistoryItem, QueryHint } from 'app/types/explore';
 
 
 import DefaultQueryField from './QueryField';
 import DefaultQueryField from './QueryField';
 import QueryTransactionStatus from './QueryTransactionStatus';
 import QueryTransactionStatus from './QueryTransactionStatus';
-import { DataSource } from 'app/types';
+import { DataSource, DataQuery } from 'app/types';
 
 
 function getFirstHintFromTransactions(transactions: QueryTransaction[]): QueryHint {
 function getFirstHintFromTransactions(transactions: QueryTransaction[]): QueryHint {
   const transaction = transactions.find(qt => qt.hints && qt.hints.length > 0);
   const transaction = transactions.find(qt => qt.hints && qt.hints.length > 0);
@@ -16,7 +16,7 @@ function getFirstHintFromTransactions(transactions: QueryTransaction[]): QueryHi
 
 
 interface QueryRowEventHandlers {
 interface QueryRowEventHandlers {
   onAddQueryRow: (index: number) => void;
   onAddQueryRow: (index: number) => void;
-  onChangeQuery: (value: string, index: number, override?: boolean) => void;
+  onChangeQuery: (value: DataQuery, index: number, override?: boolean) => void;
   onClickHintFix: (action: object, index?: number) => void;
   onClickHintFix: (action: object, index?: number) => void;
   onExecuteQuery: () => void;
   onExecuteQuery: () => void;
   onRemoveQueryRow: (index: number) => void;
   onRemoveQueryRow: (index: number) => void;
@@ -32,11 +32,11 @@ interface QueryRowCommonProps {
 type QueryRowProps = QueryRowCommonProps &
 type QueryRowProps = QueryRowCommonProps &
   QueryRowEventHandlers & {
   QueryRowEventHandlers & {
     index: number;
     index: number;
-    query: string;
+    initialTarget: DataQuery;
   };
   };
 
 
 class QueryRow extends PureComponent<QueryRowProps> {
 class QueryRow extends PureComponent<QueryRowProps> {
-  onChangeQuery = (value, override?: boolean) => {
+  onChangeQuery = (value: DataQuery, override?: boolean) => {
     const { index, onChangeQuery } = this.props;
     const { index, onChangeQuery } = this.props;
     if (onChangeQuery) {
     if (onChangeQuery) {
       onChangeQuery(value, index, override);
       onChangeQuery(value, index, override);
@@ -51,7 +51,7 @@ class QueryRow extends PureComponent<QueryRowProps> {
   };
   };
 
 
   onClickClearButton = () => {
   onClickClearButton = () => {
-    this.onChangeQuery('', true);
+    this.onChangeQuery(null, true);
   };
   };
 
 
   onClickHintFix = action => {
   onClickHintFix = action => {
@@ -76,7 +76,7 @@ class QueryRow extends PureComponent<QueryRowProps> {
   };
   };
 
 
   render() {
   render() {
-    const { datasource, history, query, transactions } = this.props;
+    const { datasource, history, initialTarget, transactions } = this.props;
     const transactionWithError = transactions.find(t => t.error !== undefined);
     const transactionWithError = transactions.find(t => t.error !== undefined);
     const hint = getFirstHintFromTransactions(transactions);
     const hint = getFirstHintFromTransactions(transactions);
     const queryError = transactionWithError ? transactionWithError.error : null;
     const queryError = transactionWithError ? transactionWithError.error : null;
@@ -91,7 +91,7 @@ class QueryRow extends PureComponent<QueryRowProps> {
             datasource={datasource}
             datasource={datasource}
             error={queryError}
             error={queryError}
             hint={hint}
             hint={hint}
-            initialQuery={query}
+            initialTarget={initialTarget}
             history={history}
             history={history}
             onClickHintFix={this.onClickHintFix}
             onClickHintFix={this.onClickHintFix}
             onPressEnter={this.onPressEnter}
             onPressEnter={this.onPressEnter}
@@ -116,19 +116,19 @@ class QueryRow extends PureComponent<QueryRowProps> {
 
 
 type QueryRowsProps = QueryRowCommonProps &
 type QueryRowsProps = QueryRowCommonProps &
   QueryRowEventHandlers & {
   QueryRowEventHandlers & {
-    queries: Query[];
+    initialTargets: DataQuery[];
   };
   };
 
 
 export default class QueryRows extends PureComponent<QueryRowsProps> {
 export default class QueryRows extends PureComponent<QueryRowsProps> {
   render() {
   render() {
-    const { className = '', queries, transactions, ...handlers } = this.props;
+    const { className = '', initialTargets, transactions, ...handlers } = this.props;
     return (
     return (
       <div className={className}>
       <div className={className}>
-        {queries.map((q, index) => (
+        {initialTargets.map((target, index) => (
           <QueryRow
           <QueryRow
-            key={q.key}
+            key={target.key}
             index={index}
             index={index}
-            query={q.query}
+            initialTarget={target}
             transactions={transactions.filter(t => t.rowIndex === index)}
             transactions={transactions.filter(t => t.rowIndex === index)}
             {...handlers}
             {...handlers}
           />
           />

+ 3 - 1
public/app/features/explore/QueryTransactionStatus.tsx

@@ -35,7 +35,9 @@ export default class QueryTransactionStatus extends PureComponent<QueryTransacti
     const { transactions } = this.props;
     const { transactions } = this.props;
     return (
     return (
       <div className="query-transactions">
       <div className="query-transactions">
-        {transactions.map((t, i) => <QueryTransactionStatusItem key={`${t.query}:${t.resultType}`} transaction={t} />)}
+        {transactions.map((t, i) => (
+          <QueryTransactionStatusItem key={`${t.rowIndex}:${t.resultType}`} transaction={t} />
+        ))}
       </div>
       </div>
     );
     );
   }
   }

+ 0 - 16
public/app/features/explore/utils/query.ts

@@ -1,16 +0,0 @@
-import { Query } from 'app/types/explore';
-
-export function generateQueryKey(index = 0): string {
-  return `Q-${Date.now()}-${Math.random()}-${index}`;
-}
-
-export function ensureQueries(queries?: Query[]): Query[] {
-  if (queries && typeof queries === 'object' && queries.length > 0 && typeof queries[0].query === 'string') {
-    return queries.map(({ query }, i) => ({ key: generateQueryKey(i), query }));
-  }
-  return [{ key: generateQueryKey(), query: '' }];
-}
-
-export function hasQuery(queries: string[]): boolean {
-  return queries.some(q => Boolean(q));
-}

+ 4 - 1
public/app/plugins/datasource/logging/components/LoggingCheatSheet.tsx

@@ -19,7 +19,10 @@ export default (props: any) => (
     {CHEAT_SHEET_ITEMS.map(item => (
     {CHEAT_SHEET_ITEMS.map(item => (
       <div className="cheat-sheet-item" key={item.expression}>
       <div className="cheat-sheet-item" key={item.expression}>
         <div className="cheat-sheet-item__title">{item.title}</div>
         <div className="cheat-sheet-item__title">{item.title}</div>
-        <div className="cheat-sheet-item__expression" onClick={e => props.onClickQuery(item.expression)}>
+        <div
+          className="cheat-sheet-item__expression"
+          onClick={e => props.onClickExample({ refId: '1', expr: item.expression })}
+        >
           <code>{item.expression}</code>
           <code>{item.expression}</code>
         </div>
         </div>
         <div className="cheat-sheet-item__label">{item.label}</div>
         <div className="cheat-sheet-item__label">{item.label}</div>

+ 15 - 10
public/app/plugins/datasource/logging/components/LoggingQueryField.tsx

@@ -10,7 +10,8 @@ import { TypeaheadOutput } from 'app/types/explore';
 import { getNextCharacter, getPreviousCousin } from 'app/features/explore/utils/dom';
 import { getNextCharacter, getPreviousCousin } from 'app/features/explore/utils/dom';
 import BracesPlugin from 'app/features/explore/slate-plugins/braces';
 import BracesPlugin from 'app/features/explore/slate-plugins/braces';
 import RunnerPlugin from 'app/features/explore/slate-plugins/runner';
 import RunnerPlugin from 'app/features/explore/slate-plugins/runner';
-import TypeaheadField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField';
+import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField';
+import { DataQuery } from 'app/types';
 
 
 const PRISM_SYNTAX = 'promql';
 const PRISM_SYNTAX = 'promql';
 
 
@@ -53,10 +54,10 @@ interface LoggingQueryFieldProps {
   error?: string | JSX.Element;
   error?: string | JSX.Element;
   hint?: any;
   hint?: any;
   history?: any[];
   history?: any[];
-  initialQuery?: string | null;
+  initialTarget?: DataQuery;
   onClickHintFix?: (action: any) => void;
   onClickHintFix?: (action: any) => void;
   onPressEnter?: () => void;
   onPressEnter?: () => void;
-  onQueryChange?: (value: string, override?: boolean) => void;
+  onQueryChange?: (value: DataQuery, override?: boolean) => void;
 }
 }
 
 
 interface LoggingQueryFieldState {
 interface LoggingQueryFieldState {
@@ -134,9 +135,13 @@ class LoggingQueryField extends React.PureComponent<LoggingQueryFieldProps, Logg
 
 
   onChangeQuery = (value: string, override?: boolean) => {
   onChangeQuery = (value: string, override?: boolean) => {
     // Send text change to parent
     // Send text change to parent
-    const { onQueryChange } = this.props;
+    const { initialTarget, onQueryChange } = this.props;
     if (onQueryChange) {
     if (onQueryChange) {
-      onQueryChange(value, override);
+      const target = {
+        ...initialTarget,
+        expr: value,
+      };
+      onQueryChange(target, override);
     }
     }
   };
   };
 
 
@@ -181,7 +186,7 @@ class LoggingQueryField extends React.PureComponent<LoggingQueryFieldProps, Logg
   };
   };
 
 
   render() {
   render() {
-    const { error, hint, initialQuery } = this.props;
+    const { error, hint, initialTarget } = this.props;
     const { logLabelOptions, syntaxLoaded } = this.state;
     const { logLabelOptions, syntaxLoaded } = this.state;
     const cleanText = this.languageProvider ? this.languageProvider.cleanText : undefined;
     const cleanText = this.languageProvider ? this.languageProvider.cleanText : undefined;
     const chooserText = syntaxLoaded ? 'Log labels' : 'Loading labels...';
     const chooserText = syntaxLoaded ? 'Log labels' : 'Loading labels...';
@@ -196,15 +201,15 @@ class LoggingQueryField extends React.PureComponent<LoggingQueryFieldProps, Logg
           </Cascader>
           </Cascader>
         </div>
         </div>
         <div className="prom-query-field-wrapper">
         <div className="prom-query-field-wrapper">
-          <TypeaheadField
+          <QueryField
             additionalPlugins={this.plugins}
             additionalPlugins={this.plugins}
             cleanText={cleanText}
             cleanText={cleanText}
-            initialValue={initialQuery}
+            initialQuery={initialTarget.expr}
             onTypeahead={this.onTypeahead}
             onTypeahead={this.onTypeahead}
             onWillApplySuggestion={willApplySuggestion}
             onWillApplySuggestion={willApplySuggestion}
             onValueChanged={this.onChangeQuery}
             onValueChanged={this.onChangeQuery}
-            placeholder="Enter a PromQL query"
-            portalOrigin="prometheus"
+            placeholder="Enter a Logging query"
+            portalOrigin="logging"
             syntaxLoaded={syntaxLoaded}
             syntaxLoaded={syntaxLoaded}
           />
           />
           {error ? <div className="prom-query-field-info text-error">{error}</div> : null}
           {error ? <div className="prom-query-field-info text-error">{error}</div> : null}

+ 1 - 1
public/app/plugins/datasource/logging/components/LoggingStartPage.tsx

@@ -52,7 +52,7 @@ export default class LoggingStartPage extends PureComponent<any, { active: strin
           </div>
           </div>
         </div>
         </div>
         <div className="page-container page-body">
         <div className="page-container page-body">
-          {active === 'start' && <LoggingCheatSheet onClickQuery={this.props.onClickQuery} />}
+          {active === 'start' && <LoggingCheatSheet onClickExample={this.props.onClickExample} />}
         </div>
         </div>
       </div>
       </div>
     );
     );

+ 2 - 2
public/app/plugins/datasource/logging/datasource.ts

@@ -112,8 +112,8 @@ export default class LoggingDatasource {
     });
     });
   }
   }
 
 
-  async importQueries(queries: DataQuery[], originMeta: PluginMeta): Promise<DataQuery[]> {
-    return this.languageProvider.importQueries(queries, originMeta.id);
+  async importQueries(targets: DataQuery[], originMeta: PluginMeta): Promise<DataQuery[]> {
+    return this.languageProvider.importQueries(targets, originMeta.id);
   }
   }
 
 
   metadataRequest(url) {
   metadataRequest(url) {

+ 11 - 7
public/app/plugins/datasource/logging/language_provider.ts

@@ -158,25 +158,29 @@ export default class LoggingLanguageProvider extends LanguageProvider {
     return { context, refresher, suggestions };
     return { context, refresher, suggestions };
   }
   }
 
 
-  async importQueries(queries: DataQuery[], datasourceType: string): Promise<DataQuery[]> {
+  async importQueries(targets: DataQuery[], datasourceType: string): Promise<DataQuery[]> {
     if (datasourceType === 'prometheus') {
     if (datasourceType === 'prometheus') {
       return Promise.all(
       return Promise.all(
-        queries.map(async query => {
-          const expr = await this.importPrometheusQuery(query.expr);
+        targets.map(async target => {
+          const expr = await this.importPrometheusQuery(target.expr);
           return {
           return {
-            ...query,
+            ...target,
             expr,
             expr,
           };
           };
         })
         })
       );
       );
     }
     }
-    return queries.map(query => ({
-      ...query,
+    return targets.map(target => ({
+      ...target,
       expr: '',
       expr: '',
     }));
     }));
   }
   }
 
 
   async importPrometheusQuery(query: string): Promise<string> {
   async importPrometheusQuery(query: string): Promise<string> {
+    if (!query) {
+      return '';
+    }
+
     // Consider only first selector in query
     // Consider only first selector in query
     const selectorMatch = query.match(selectorRegexp);
     const selectorMatch = query.match(selectorRegexp);
     if (selectorMatch) {
     if (selectorMatch) {
@@ -192,7 +196,7 @@ export default class LoggingLanguageProvider extends LanguageProvider {
       const commonLabels = {};
       const commonLabels = {};
       for (const key in labels) {
       for (const key in labels) {
         const existingKeys = this.labelKeys[EMPTY_SELECTOR];
         const existingKeys = this.labelKeys[EMPTY_SELECTOR];
-        if (existingKeys.indexOf(key) > -1) {
+        if (existingKeys && existingKeys.indexOf(key) > -1) {
           // Should we check for label value equality here?
           // Should we check for label value equality here?
           commonLabels[key] = labels[key];
           commonLabels[key] = labels[key];
         }
         }

+ 4 - 1
public/app/plugins/datasource/prometheus/components/PromCheatSheet.tsx

@@ -25,7 +25,10 @@ export default (props: any) => (
     {CHEAT_SHEET_ITEMS.map(item => (
     {CHEAT_SHEET_ITEMS.map(item => (
       <div className="cheat-sheet-item" key={item.expression}>
       <div className="cheat-sheet-item" key={item.expression}>
         <div className="cheat-sheet-item__title">{item.title}</div>
         <div className="cheat-sheet-item__title">{item.title}</div>
-        <div className="cheat-sheet-item__expression" onClick={e => props.onClickQuery(item.expression)}>
+        <div
+          className="cheat-sheet-item__expression"
+          onClick={e => props.onClickExample({ refId: '1', expr: item.expression })}
+        >
           <code>{item.expression}</code>
           <code>{item.expression}</code>
         </div>
         </div>
         <div className="cheat-sheet-item__label">{item.label}</div>
         <div className="cheat-sheet-item__label">{item.label}</div>

+ 17 - 12
public/app/plugins/datasource/prometheus/components/PromQueryField.tsx

@@ -10,7 +10,8 @@ import { TypeaheadOutput } from 'app/types/explore';
 import { getNextCharacter, getPreviousCousin } from 'app/features/explore/utils/dom';
 import { getNextCharacter, getPreviousCousin } from 'app/features/explore/utils/dom';
 import BracesPlugin from 'app/features/explore/slate-plugins/braces';
 import BracesPlugin from 'app/features/explore/slate-plugins/braces';
 import RunnerPlugin from 'app/features/explore/slate-plugins/runner';
 import RunnerPlugin from 'app/features/explore/slate-plugins/runner';
-import TypeaheadField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField';
+import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField';
+import { DataQuery } from 'app/types';
 
 
 const HISTOGRAM_GROUP = '__histograms__';
 const HISTOGRAM_GROUP = '__histograms__';
 const METRIC_MARK = 'metric';
 const METRIC_MARK = 'metric';
@@ -84,17 +85,17 @@ interface CascaderOption {
   disabled?: boolean;
   disabled?: boolean;
 }
 }
 
 
-interface PromQueryFieldProps {
+type PromQueryFieldProps = {
   datasource: any;
   datasource: any;
   error?: string | JSX.Element;
   error?: string | JSX.Element;
+  initialTarget: DataQuery;
   hint?: any;
   hint?: any;
   history?: any[];
   history?: any[];
-  initialQuery?: string | null;
   metricsByPrefix?: CascaderOption[];
   metricsByPrefix?: CascaderOption[];
   onClickHintFix?: (action: any) => void;
   onClickHintFix?: (action: any) => void;
   onPressEnter?: () => void;
   onPressEnter?: () => void;
-  onQueryChange?: (value: string, override?: boolean) => void;
-}
+  onQueryChange?: (value: DataQuery, override?: boolean) => void;
+};
 
 
 interface PromQueryFieldState {
 interface PromQueryFieldState {
   metricsOptions: any[];
   metricsOptions: any[];
@@ -161,11 +162,15 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
     this.onChangeQuery(query, true);
     this.onChangeQuery(query, true);
   };
   };
 
 
-  onChangeQuery = (value: string, override?: boolean) => {
+  onChangeQuery = (query: string, override?: boolean) => {
     // Send text change to parent
     // Send text change to parent
-    const { onQueryChange } = this.props;
+    const { initialTarget, onQueryChange } = this.props;
     if (onQueryChange) {
     if (onQueryChange) {
-      onQueryChange(value, override);
+      const target: DataQuery = {
+        ...initialTarget,
+        expr: query,
+      };
+      onQueryChange(target, override);
     }
     }
   };
   };
 
 
@@ -227,10 +232,10 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
   };
   };
 
 
   render() {
   render() {
-    const { error, hint, initialQuery } = this.props;
+    const { error, hint, initialTarget } = this.props;
     const { metricsOptions, syntaxLoaded } = this.state;
     const { metricsOptions, syntaxLoaded } = this.state;
     const cleanText = this.languageProvider ? this.languageProvider.cleanText : undefined;
     const cleanText = this.languageProvider ? this.languageProvider.cleanText : undefined;
-    const chooserText = syntaxLoaded ? 'Metrics' : 'Loading matrics...';
+    const chooserText = syntaxLoaded ? 'Metrics' : 'Loading metrics...';
 
 
     return (
     return (
       <div className="prom-query-field">
       <div className="prom-query-field">
@@ -242,10 +247,10 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
           </Cascader>
           </Cascader>
         </div>
         </div>
         <div className="prom-query-field-wrapper">
         <div className="prom-query-field-wrapper">
-          <TypeaheadField
+          <QueryField
             additionalPlugins={this.plugins}
             additionalPlugins={this.plugins}
             cleanText={cleanText}
             cleanText={cleanText}
-            initialValue={initialQuery}
+            initialQuery={initialTarget.expr}
             onTypeahead={this.onTypeahead}
             onTypeahead={this.onTypeahead}
             onWillApplySuggestion={willApplySuggestion}
             onWillApplySuggestion={willApplySuggestion}
             onValueChanged={this.onChangeQuery}
             onValueChanged={this.onChangeQuery}

+ 1 - 1
public/app/plugins/datasource/prometheus/components/PromStart.tsx

@@ -52,7 +52,7 @@ export default class PromStart extends PureComponent<any, { active: string }> {
           </div>
           </div>
         </div>
         </div>
         <div className="page-container page-body">
         <div className="page-container page-body">
-          {active === 'start' && <PromCheatSheet onClickQuery={this.props.onClickQuery} />}
+          {active === 'start' && <PromCheatSheet onClickExample={this.props.onClickExample} />}
         </div>
         </div>
       </div>
       </div>
     );
     );

+ 24 - 16
public/app/plugins/datasource/prometheus/datasource.ts

@@ -11,6 +11,8 @@ import { BackendSrv } from 'app/core/services/backend_srv';
 import addLabelToQuery from './add_label_to_query';
 import addLabelToQuery from './add_label_to_query';
 import { getQueryHints } from './query_hints';
 import { getQueryHints } from './query_hints';
 import { expandRecordingRules } from './language_utils';
 import { expandRecordingRules } from './language_utils';
+import { DataQuery } from 'app/types';
+import { ExploreUrlState } from 'app/types/explore';
 
 
 export function alignRange(start, end, step) {
 export function alignRange(start, end, step) {
   const alignedEnd = Math.ceil(end / step) * step;
   const alignedEnd = Math.ceil(end / step) * step;
@@ -419,24 +421,23 @@ export class PrometheusDatasource {
     });
     });
   }
   }
 
 
-  getExploreState(targets: any[]) {
-    let state = {};
+  getExploreState(targets: DataQuery[]): Partial<ExploreUrlState> {
+    let state: Partial<ExploreUrlState> = { datasource: this.name };
     if (targets && targets.length > 0) {
     if (targets && targets.length > 0) {
-      const queries = targets.map(t => ({
-        query: this.templateSrv.replace(t.expr, {}, this.interpolateQueryExpr),
-        format: t.format,
+      const expandedTargets = targets.map(target => ({
+        ...target,
+        expr: this.templateSrv.replace(target.expr, {}, this.interpolateQueryExpr),
       }));
       }));
       state = {
       state = {
         ...state,
         ...state,
-        queries,
-        datasource: this.name,
+        targets: expandedTargets,
       };
       };
     }
     }
     return state;
     return state;
   }
   }
 
 
-  getQueryHints(query: string, result: any[]) {
-    return getQueryHints(query, result, this);
+  getQueryHints(target: DataQuery, result: any[]) {
+    return getQueryHints(target.expr, result, this);
   }
   }
 
 
   loadRules() {
   loadRules() {
@@ -454,28 +455,35 @@ export class PrometheusDatasource {
       });
       });
   }
   }
 
 
-  modifyQuery(query: string, action: any): string {
+  modifyQuery(target: DataQuery, action: any): DataQuery {
+    let query = target.expr;
     switch (action.type) {
     switch (action.type) {
       case 'ADD_FILTER': {
       case 'ADD_FILTER': {
-        return addLabelToQuery(query, action.key, action.value);
+        query = addLabelToQuery(query, action.key, action.value);
+        break;
       }
       }
       case 'ADD_HISTOGRAM_QUANTILE': {
       case 'ADD_HISTOGRAM_QUANTILE': {
-        return `histogram_quantile(0.95, sum(rate(${query}[5m])) by (le))`;
+        query = `histogram_quantile(0.95, sum(rate(${query}[5m])) by (le))`;
+        break;
       }
       }
       case 'ADD_RATE': {
       case 'ADD_RATE': {
-        return `rate(${query}[5m])`;
+        query = `rate(${query}[5m])`;
+        break;
       }
       }
       case 'ADD_SUM': {
       case 'ADD_SUM': {
-        return `sum(${query.trim()}) by ($1)`;
+        query = `sum(${query.trim()}) by ($1)`;
+        break;
       }
       }
       case 'EXPAND_RULES': {
       case 'EXPAND_RULES': {
         if (action.mapping) {
         if (action.mapping) {
-          return expandRecordingRules(query, action.mapping);
+          query = expandRecordingRules(query, action.mapping);
         }
         }
+        break;
       }
       }
       default:
       default:
-        return query;
+        break;
     }
     }
+    return { ...target, expr: query };
   }
   }
 
 
   getPrometheusTime(date, roundUp) {
   getPrometheusTime(date, roundUp) {

+ 9 - 18
public/app/types/explore.ts

@@ -1,6 +1,6 @@
 import { Value } from 'slate';
 import { Value } from 'slate';
 
 
-import { RawTimeRange } from './series';
+import { DataQuery, RawTimeRange } from './series';
 
 
 export interface CompletionItem {
 export interface CompletionItem {
   /**
   /**
@@ -79,7 +79,7 @@ interface ExploreDatasource {
 
 
 export interface HistoryItem {
 export interface HistoryItem {
   ts: number;
   ts: number;
-  query: string;
+  target: DataQuery;
 }
 }
 
 
 export abstract class LanguageProvider {
 export abstract class LanguageProvider {
@@ -107,11 +107,6 @@ export interface TypeaheadOutput {
   suggestions: CompletionItemGroup[];
   suggestions: CompletionItemGroup[];
 }
 }
 
 
-export interface Query {
-  query: string;
-  key?: string;
-}
-
 export interface QueryFix {
 export interface QueryFix {
   type: string;
   type: string;
   label: string;
   label: string;
@@ -130,6 +125,10 @@ export interface QueryHint {
   fix?: QueryFix;
   fix?: QueryFix;
 }
 }
 
 
+export interface QueryHintGetter {
+  (target: DataQuery, results: any[], ...rest: any): QueryHint[];
+}
+
 export interface QueryTransaction {
 export interface QueryTransaction {
   id: string;
   id: string;
   done: boolean;
   done: boolean;
@@ -137,10 +136,10 @@ export interface QueryTransaction {
   hints?: QueryHint[];
   hints?: QueryHint[];
   latency: number;
   latency: number;
   options: any;
   options: any;
-  query: string;
   result?: any; // Table model / Timeseries[] / Logs
   result?: any; // Table model / Timeseries[] / Logs
   resultType: ResultType;
   resultType: ResultType;
   rowIndex: number;
   rowIndex: number;
+  target: DataQuery;
 }
 }
 
 
 export interface TextMatch {
 export interface TextMatch {
@@ -160,15 +159,7 @@ export interface ExploreState {
   exploreDatasources: ExploreDatasource[];
   exploreDatasources: ExploreDatasource[];
   graphRange: RawTimeRange;
   graphRange: RawTimeRange;
   history: HistoryItem[];
   history: HistoryItem[];
-  /**
-   * Initial rows of queries to push down the tree.
-   * Modifications do not end up here, but in `this.queryExpressions`.
-   * The only way to reset a query is to change its `key`.
-   */
-  queries: Query[];
-  /**
-   * Hints gathered for the query row.
-   */
+  initialTargets: DataQuery[];
   queryTransactions: QueryTransaction[];
   queryTransactions: QueryTransaction[];
   range: RawTimeRange;
   range: RawTimeRange;
   showingGraph: boolean;
   showingGraph: boolean;
@@ -182,7 +173,7 @@ export interface ExploreState {
 
 
 export interface ExploreUrlState {
 export interface ExploreUrlState {
   datasource: string;
   datasource: string;
-  queries: Query[];
+  targets: any[]; // Should be a DataQuery, but we're going to strip refIds, so typing makes less sense
   range: RawTimeRange;
   range: RawTimeRange;
 }
 }