Переглянути джерело

Explore: Align Explore with Dashboards and Panels (#16823)

* Wip: Removes queryTransactions from state

* Refactor: Adds back query failures

* Refactor: Moves error parsing to datasources

* Refactor: Adds back hinting for Prometheus

* Refactor: removed commented out code

* Refactor: Adds back QueryStatus

* Refactor: Adds scanning back to Explore

* Fix: Fixes prettier error

* Fix: Makes sure there is an error

* Merge: Merges with master

* Fix: Adds safeStringifyValue to error parsing

* Fix: Fixes table result calculations

* Refactor: Adds ErrorContainer and generic error handling in Explore

* Fix: Fixes so refIds remain consistent

* Refactor: Makes it possible to return result even when there are errors

* Fix: Fixes digest issue with Angular editors

* Refactor: Adds tests for explore utils

* Refactor: Breakes current behaviour of always returning a result even if Query fails

* Fix: Fixes Prettier error

* Fix: Adds back console.log for erroneous querys

* Refactor: Changes console.log to console.error
Hugo Häggmark 6 роки тому
батько
коміт
6dbaa704bc
28 змінених файлів з 861 додано та 539 видалено
  1. 2 8
      packages/grafana-ui/src/types/datasource.ts
  2. 1 1
      packages/grafana-ui/src/utils/moment_wrapper.ts
  3. 166 0
      public/app/core/utils/explore.test.ts
  4. 120 32
      public/app/core/utils/explore.ts
  5. 32 0
      public/app/features/explore/ErrorContainer.tsx
  6. 7 1
      public/app/features/explore/Explore.tsx
  7. 4 2
      public/app/features/explore/ExploreToolbar.tsx
  8. 2 2
      public/app/features/explore/GraphContainer.tsx
  9. 2 2
      public/app/features/explore/LogsContainer.tsx
  10. 9 1
      public/app/features/explore/QueryEditor.tsx
  11. 14 14
      public/app/features/explore/QueryField.tsx
  12. 63 34
      public/app/features/explore/QueryRow.tsx
  13. 47 0
      public/app/features/explore/QueryStatus.tsx
  14. 0 44
      public/app/features/explore/QueryTransactionStatus.tsx
  15. 2 2
      public/app/features/explore/TableContainer.tsx
  16. 25 14
      public/app/features/explore/state/actionTypes.ts
  17. 97 157
      public/app/features/explore/state/actions.ts
  18. 0 1
      public/app/features/explore/state/reducers.test.ts
  19. 92 127
      public/app/features/explore/state/reducers.ts
  20. 1 1
      public/app/plugins/datasource/loki/components/LokiCheatSheet.tsx
  21. 12 28
      public/app/plugins/datasource/loki/components/LokiQueryFieldForm.tsx
  22. 38 3
      public/app/plugins/datasource/loki/datasource.ts
  23. 1 1
      public/app/plugins/datasource/prometheus/components/PromCheatSheet.tsx
  24. 37 15
      public/app/plugins/datasource/prometheus/components/PromQueryField.tsx
  25. 51 13
      public/app/plugins/datasource/prometheus/datasource.ts
  26. 2 1
      public/app/plugins/datasource/prometheus/specs/completer.test.ts
  27. 25 25
      public/app/plugins/datasource/prometheus/specs/datasource.test.ts
  28. 9 10
      public/app/types/explore.ts

+ 2 - 8
packages/grafana-ui/src/types/datasource.ts

@@ -214,16 +214,10 @@ export interface ExploreQueryFieldProps<
   DSType extends DataSourceApi<TQuery, TOptions>,
   TQuery extends DataQuery = DataQuery,
   TOptions extends DataSourceJsonData = DataSourceJsonData
-> {
-  datasource: DSType;
+> extends QueryEditorProps<DSType, TQuery, TOptions> {
   datasourceStatus: DataSourceStatus;
-  query: TQuery;
-  error?: string | JSX.Element;
-  hint?: QueryHint;
   history: any[];
-  onExecuteQuery?: () => void;
-  onQueryChange?: (value: TQuery) => void;
-  onExecuteHint?: (action: QueryFixAction) => void;
+  onHint?: (action: QueryFixAction) => void;
 }
 
 export interface ExploreStartPageProps {

+ 1 - 1
packages/grafana-ui/src/utils/moment_wrapper.ts

@@ -47,6 +47,7 @@ export interface DateTimeDuration {
 
 export interface DateTime extends Object {
   add: (amount?: DateTimeInput, unit?: DurationUnit) => DateTime;
+  diff: (amount: DateTimeInput, unit?: DurationUnit, truncate?: boolean) => number;
   endOf: (unitOfTime: DurationUnit) => DateTime;
   format: (formatInput?: FormatInput) => string;
   fromNow: (withoutSuffix?: boolean) => string;
@@ -59,7 +60,6 @@ export interface DateTime extends Object {
   subtract: (amount?: DateTimeInput, unit?: DurationUnit) => DateTime;
   toDate: () => Date;
   toISOString: () => string;
-  diff: (amount: DateTimeInput, unit?: DurationUnit, truncate?: boolean) => number;
   valueOf: () => number;
   unix: () => number;
   utc: () => DateTime;

+ 166 - 0
public/app/core/utils/explore.test.ts

@@ -5,10 +5,15 @@ import {
   updateHistory,
   clearHistory,
   hasNonEmptyQuery,
+  instanceOfDataQueryError,
+  getValueWithRefId,
+  getFirstQueryErrorWithoutRefId,
+  getRefIds,
 } from './explore';
 import { ExploreUrlState } from 'app/types/explore';
 import store from 'app/core/store';
 import { LogsDedupStrategy } from 'app/core/logs_model';
+import { DataQueryError } from '@grafana/ui';
 
 const DEFAULT_EXPLORE_STATE: ExploreUrlState = {
   datasource: null,
@@ -188,3 +193,164 @@ describe('hasNonEmptyQuery', () => {
     expect(hasNonEmptyQuery([])).toBeFalsy();
   });
 });
+
+describe('instanceOfDataQueryError', () => {
+  describe('when called with a DataQueryError', () => {
+    it('then it should return true', () => {
+      const error: DataQueryError = {
+        message: 'A message',
+        status: '200',
+        statusText: 'Ok',
+      };
+      const result = instanceOfDataQueryError(error);
+
+      expect(result).toBe(true);
+    });
+  });
+
+  describe('when called with a non DataQueryError', () => {
+    it('then it should return false', () => {
+      const error = {};
+      const result = instanceOfDataQueryError(error);
+
+      expect(result).toBe(false);
+    });
+  });
+});
+
+describe('hasRefId', () => {
+  describe('when called with a null value', () => {
+    it('then it should return null', () => {
+      const input = null;
+      const result = getValueWithRefId(input);
+
+      expect(result).toBeNull();
+    });
+  });
+
+  describe('when called with a non object value', () => {
+    it('then it should return null', () => {
+      const input = 123;
+      const result = getValueWithRefId(input);
+
+      expect(result).toBeNull();
+    });
+  });
+
+  describe('when called with an object that has refId', () => {
+    it('then it should return the object', () => {
+      const input = { refId: 'A' };
+      const result = getValueWithRefId(input);
+
+      expect(result).toBe(input);
+    });
+  });
+
+  describe('when called with an array that has refId', () => {
+    it('then it should return the object', () => {
+      const input = [123, null, {}, { refId: 'A' }];
+      const result = getValueWithRefId(input);
+
+      expect(result).toBe(input[3]);
+    });
+  });
+
+  describe('when called with an object that has refId somewhere in the object tree', () => {
+    it('then it should return the object', () => {
+      const input: any = { data: [123, null, {}, { series: [123, null, {}, { refId: 'A' }] }] };
+      const result = getValueWithRefId(input);
+
+      expect(result).toBe(input.data[3].series[3]);
+    });
+  });
+});
+
+describe('getFirstQueryErrorWithoutRefId', () => {
+  describe('when called with a null value', () => {
+    it('then it should return null', () => {
+      const errors: DataQueryError[] = null;
+      const result = getFirstQueryErrorWithoutRefId(errors);
+
+      expect(result).toBeNull();
+    });
+  });
+
+  describe('when called with an array with only refIds', () => {
+    it('then it should return undefined', () => {
+      const errors: DataQueryError[] = [{ refId: 'A' }, { refId: 'B' }];
+      const result = getFirstQueryErrorWithoutRefId(errors);
+
+      expect(result).toBeUndefined();
+    });
+  });
+
+  describe('when called with an array with and without refIds', () => {
+    it('then it should return undefined', () => {
+      const errors: DataQueryError[] = [
+        { refId: 'A' },
+        { message: 'A message' },
+        { refId: 'B' },
+        { message: 'B message' },
+      ];
+      const result = getFirstQueryErrorWithoutRefId(errors);
+
+      expect(result).toBe(errors[1]);
+    });
+  });
+});
+
+describe('getRefIds', () => {
+  describe('when called with a null value', () => {
+    it('then it should return empty array', () => {
+      const input = null;
+      const result = getRefIds(input);
+
+      expect(result).toEqual([]);
+    });
+  });
+
+  describe('when called with a non object value', () => {
+    it('then it should return empty array', () => {
+      const input = 123;
+      const result = getRefIds(input);
+
+      expect(result).toEqual([]);
+    });
+  });
+
+  describe('when called with an object that has refId', () => {
+    it('then it should return an array with that refId', () => {
+      const input = { refId: 'A' };
+      const result = getRefIds(input);
+
+      expect(result).toEqual(['A']);
+    });
+  });
+
+  describe('when called with an array that has refIds', () => {
+    it('then it should return an array with unique refIds', () => {
+      const input = [123, null, {}, { refId: 'A' }, { refId: 'A' }, { refId: 'B' }];
+      const result = getRefIds(input);
+
+      expect(result).toEqual(['A', 'B']);
+    });
+  });
+
+  describe('when called with an object that has refIds somewhere in the object tree', () => {
+    it('then it should return return an array with unique refIds', () => {
+      const input: any = {
+        data: [
+          123,
+          null,
+          { refId: 'B', series: [{ refId: 'X' }] },
+          { refId: 'B' },
+          {},
+          { series: [123, null, {}, { refId: 'A' }] },
+        ],
+      };
+      const result = getRefIds(input);
+
+      expect(result).toEqual(['B', 'X', 'A']);
+    });
+  });
+});

+ 120 - 32
public/app/core/utils/explore.ts

@@ -21,6 +21,7 @@ import {
   toSeriesData,
   guessFieldTypes,
   TimeFragment,
+  DataQueryError,
 } from '@grafana/ui';
 import TimeSeries from 'app/core/time_series2';
 import {
@@ -110,8 +111,7 @@ export async function getExploreUrl(
 }
 
 export function buildQueryTransaction(
-  query: DataQuery,
-  rowIndex: number,
+  queries: DataQuery[],
   resultType: ResultType,
   queryOptions: QueryOptions,
   range: TimeRange,
@@ -120,12 +120,11 @@ export function buildQueryTransaction(
 ): QueryTransaction {
   const { interval, intervalMs } = queryIntervals;
 
-  const configuredQueries = [
-    {
-      ...query,
-      ...queryOptions,
-    },
-  ];
+  const configuredQueries = queries.map(query => ({ ...query, ...queryOptions }));
+  const key = queries.reduce((combinedKey, query) => {
+    combinedKey += query.key;
+    return combinedKey;
+  }, '');
 
   // Clone range for query request
   // const queryRange: RawTimeRange = { ...range };
@@ -134,7 +133,7 @@ export function buildQueryTransaction(
   // Using `format` here because it relates to the view panel that the request is for.
   // However, some datasources don't use `panelId + query.refId`, but only `panelId`.
   // Therefore panel id has to be unique.
-  const panelId = `${queryOptions.format}-${query.key}`;
+  const panelId = `${queryOptions.format}-${key}`;
 
   const options = {
     interval,
@@ -151,10 +150,9 @@ export function buildQueryTransaction(
   };
 
   return {
+    queries,
     options,
-    query,
     resultType,
-    rowIndex,
     scanning,
     id: generateKey(), // reusing for unique ID
     done: false,
@@ -195,6 +193,20 @@ export const safeParseJson = (text: string) => {
   }
 };
 
+export const safeStringifyValue = (value: any, space?: number) => {
+  if (!value) {
+    return '';
+  }
+
+  try {
+    return JSON.stringify(value, null, space);
+  } catch (error) {
+    console.error(error);
+  }
+
+  return '';
+};
+
 export function parseUrlState(initial: string | undefined): ExploreUrlState {
   const parsed = safeParseJson(initial);
   const errorResult = {
@@ -265,12 +277,34 @@ export function generateEmptyQuery(queries: DataQuery[], index = 0): DataQuery {
   return { refId: getNextRefIdChar(queries), key: generateKey(index) };
 }
 
+export const generateNewKeyAndAddRefIdIfMissing = (target: DataQuery, queries: DataQuery[], index = 0): DataQuery => {
+  const key = generateKey(index);
+  const refId = target.refId || getNextRefIdChar(queries);
+
+  return { ...target, refId, key };
+};
+
 /**
  * Ensure at least one target exists and that targets have the necessary keys
  */
 export function ensureQueries(queries?: DataQuery[]): DataQuery[] {
   if (queries && typeof queries === 'object' && queries.length > 0) {
-    return queries.map((query, i) => ({ ...query, ...generateEmptyQuery(queries, i) }));
+    const allQueries = [];
+    for (let index = 0; index < queries.length; index++) {
+      const query = queries[index];
+      const key = generateKey(index);
+      let refId = query.refId;
+      if (!refId) {
+        refId = getNextRefIdChar(allQueries);
+      }
+
+      allQueries.push({
+        ...query,
+        refId,
+        key,
+      });
+    }
+    return allQueries;
   }
   return [{ ...generateEmptyQuery(queries) }];
 }
@@ -290,26 +324,20 @@ export function hasNonEmptyQuery<TQuery extends DataQuery = any>(queries: TQuery
   );
 }
 
-export function calculateResultsFromQueryTransactions(
-  queryTransactions: QueryTransaction[],
-  datasource: any,
-  graphInterval: number
-) {
-  const graphResult = _.flatten(
-    queryTransactions.filter(qt => qt.resultType === 'Graph' && qt.done && qt.result).map(qt => qt.result)
-  );
-  const tableResult = mergeTablesIntoModel(
-    new TableModel(),
-    ...queryTransactions
-      .filter(qt => qt.resultType === 'Table' && qt.done && qt.result && qt.result.columns && qt.result.rows)
-      .map(qt => qt.result)
-  );
-  const logsResult = seriesDataToLogsModel(
-    _.flatten(
-      queryTransactions.filter(qt => qt.resultType === 'Logs' && qt.done && qt.result).map(qt => qt.result)
-    ).map(r => guessFieldTypes(toSeriesData(r))),
-    graphInterval
-  );
+export function calculateResultsFromQueryTransactions(result: any, resultType: ResultType, graphInterval: number) {
+  const flattenedResult: any[] = _.flatten(result);
+  const graphResult = resultType === 'Graph' && result ? result : null;
+  const tableResult =
+    resultType === 'Table' && result
+      ? mergeTablesIntoModel(
+          new TableModel(),
+          ...flattenedResult.filter((r: any) => r.columns && r.rows).map((r: any) => r as TableModel)
+        )
+      : mergeTablesIntoModel(new TableModel());
+  const logsResult =
+    resultType === 'Logs' && result
+      ? seriesDataToLogsModel(flattenedResult.map(r => guessFieldTypes(toSeriesData(r))), graphInterval)
+      : null;
 
   return {
     graphResult,
@@ -441,3 +469,63 @@ export const getTimeRangeFromUrl = (range: RawTimeRange, timeZone: TimeZone): Ti
     raw,
   };
 };
+
+export const instanceOfDataQueryError = (value: any): value is DataQueryError => {
+  return value.message !== undefined && value.status !== undefined && value.statusText !== undefined;
+};
+
+export const getValueWithRefId = (value: any): any | null => {
+  if (!value) {
+    return null;
+  }
+
+  if (typeof value !== 'object') {
+    return null;
+  }
+
+  if (value.refId) {
+    return value;
+  }
+
+  const keys = Object.keys(value);
+  for (let index = 0; index < keys.length; index++) {
+    const key = keys[index];
+    const refId = getValueWithRefId(value[key]);
+    if (refId) {
+      return refId;
+    }
+  }
+
+  return null;
+};
+
+export const getFirstQueryErrorWithoutRefId = (errors: DataQueryError[]) => {
+  if (!errors) {
+    return null;
+  }
+
+  return errors.filter(error => (error.refId ? false : true))[0];
+};
+
+export const getRefIds = (value: any): string[] => {
+  if (!value) {
+    return [];
+  }
+
+  if (typeof value !== 'object') {
+    return [];
+  }
+
+  const keys = Object.keys(value);
+  const refIds = [];
+  for (let index = 0; index < keys.length; index++) {
+    const key = keys[index];
+    if (key === 'refId') {
+      refIds.push(value[key]);
+      continue;
+    }
+    refIds.push(getRefIds(value[key]));
+  }
+
+  return _.uniq(_.flatten(refIds));
+};

+ 32 - 0
public/app/features/explore/ErrorContainer.tsx

@@ -0,0 +1,32 @@
+import React, { FunctionComponent } from 'react';
+import { DataQueryError } from '@grafana/ui';
+import { FadeIn } from 'app/core/components/Animations/FadeIn';
+import { getFirstQueryErrorWithoutRefId, getValueWithRefId } from 'app/core/utils/explore';
+
+interface Props {
+  queryErrors: DataQueryError[];
+}
+
+export const ErrorContainer: FunctionComponent<Props> = props => {
+  const { queryErrors } = props;
+  const refId = getValueWithRefId(queryErrors);
+  const queryError = refId ? null : getFirstQueryErrorWithoutRefId(queryErrors);
+  const showError = queryError ? true : false;
+  const duration = showError ? 100 : 10;
+  const message = queryError ? queryError.message : null;
+
+  return (
+    <FadeIn in={showError} duration={duration}>
+      <div className="alert-container">
+        <div className="alert-error alert">
+          <div className="alert-icon">
+            <i className="fa fa-exclamation-triangle" />
+          </div>
+          <div className="alert-body">
+            <div className="alert-title">{message}</div>
+          </div>
+        </div>
+      </div>
+    </FadeIn>
+  );
+};

+ 7 - 1
public/app/features/explore/Explore.tsx

@@ -31,7 +31,7 @@ import {
 } from './state/actions';
 
 // Types
-import { RawTimeRange, DataQuery, ExploreStartPageProps, ExploreDataSourceApi } from '@grafana/ui';
+import { RawTimeRange, DataQuery, ExploreStartPageProps, ExploreDataSourceApi, DataQueryError } from '@grafana/ui';
 import {
   ExploreItemState,
   ExploreUrlState,
@@ -54,6 +54,7 @@ import { scanStopAction } from './state/actionTypes';
 import { NoDataSourceCallToAction } from './NoDataSourceCallToAction';
 import { FadeIn } from 'app/core/components/Animations/FadeIn';
 import { getTimeZone } from '../profile/state/selectors';
+import { ErrorContainer } from './ErrorContainer';
 
 interface ExploreProps {
   StartPage?: ComponentClass<ExploreStartPageProps>;
@@ -86,6 +87,7 @@ interface ExploreProps {
   initialQueries: DataQuery[];
   initialRange: RawTimeRange;
   initialUI: ExploreUIState;
+  queryErrors: DataQueryError[];
 }
 
 /**
@@ -236,6 +238,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
       supportsLogs,
       supportsTable,
       queryKeys,
+      queryErrors,
     } = this.props;
     const exploreClass = split ? 'explore explore-split' : 'explore';
 
@@ -257,6 +260,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
         {datasourceInstance && (
           <div className="explore-container">
             <QueryRows exploreEvents={this.exploreEvents} exploreId={exploreId} queryKeys={queryKeys} />
+            <ErrorContainer queryErrors={queryErrors} />
             <AutoSizer onResize={this.onResize} disableHeight>
               {({ width }) => {
                 if (width === 0) {
@@ -313,6 +317,7 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) {
     queryKeys,
     urlState,
     update,
+    queryErrors,
   } = item;
 
   const { datasource, queries, range: urlRange, ui } = (urlState || {}) as ExploreUrlState;
@@ -339,6 +344,7 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) {
     initialQueries,
     initialRange,
     initialUI,
+    queryErrors,
   };
 }
 

+ 4 - 2
public/app/features/explore/ExploreToolbar.tsx

@@ -203,14 +203,16 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps
     datasourceInstance,
     datasourceMissing,
     exploreDatasources,
-    queryTransactions,
     range,
     refreshInterval,
+    graphIsLoading,
+    logIsLoading,
+    tableIsLoading,
   } = exploreItem;
   const selectedDatasource = datasourceInstance
     ? exploreDatasources.find(datasource => datasource.name === datasourceInstance.name)
     : undefined;
-  const loading = queryTransactions.some(qt => !qt.done);
+  const loading = graphIsLoading || logIsLoading || tableIsLoading;
 
   return {
     datasourceMissing,

+ 2 - 2
public/app/features/explore/GraphContainer.tsx

@@ -71,8 +71,8 @@ function mapStateToProps(state: StoreState, { exploreId }) {
   const explore = state.explore;
   const { split } = explore;
   const item: ExploreItemState = explore[exploreId];
-  const { graphResult, queryTransactions, range, showingGraph, showingTable } = item;
-  const loading = queryTransactions.some(qt => qt.resultType === 'Graph' && !qt.done);
+  const { graphResult, graphIsLoading, range, showingGraph, showingTable } = item;
+  const loading = graphIsLoading;
   return { graphResult, loading, range, showingGraph, showingTable, split, timeZone: getTimeZone(state.user) };
 }
 

+ 2 - 2
public/app/features/explore/LogsContainer.tsx

@@ -113,8 +113,8 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
 function mapStateToProps(state: StoreState, { exploreId }) {
   const explore = state.explore;
   const item: ExploreItemState = explore[exploreId];
-  const { logsHighlighterExpressions, logsResult, queryTransactions, scanning, scanRange, range } = item;
-  const loading = queryTransactions.some(qt => qt.resultType === 'Logs' && !qt.done);
+  const { logsHighlighterExpressions, logsResult, logIsLoading, scanning, scanRange, range } = item;
+  const loading = logIsLoading;
   const { showingLogs, dedupStrategy } = exploreItemUIStateSelector(item);
   const hiddenLogLevels = new Set(item.hiddenLogLevels);
   const dedupedResult = deduplicatedLogsSelector(item);

+ 9 - 1
public/app/features/explore/QueryEditor.tsx

@@ -12,8 +12,8 @@ import 'app/features/plugins/plugin_loader';
 import { dateTime } from '@grafana/ui/src/utils/moment_wrapper';
 
 interface QueryEditorProps {
+  error?: any;
   datasource: any;
-  error?: string | JSX.Element;
   onExecuteQuery?: () => void;
   onQueryChange?: (value: DataQuery) => void;
   initialQuery: DataQuery;
@@ -57,6 +57,14 @@ export default class QueryEditor extends PureComponent<QueryEditorProps, any> {
     this.props.onQueryChange(target);
   }
 
+  componentDidUpdate(prevProps: QueryEditorProps) {
+    if (prevProps.error !== this.props.error && this.component) {
+      // Some query controllers listen to data error events and need a digest
+      // for some reason this needs to be done in next tick
+      setTimeout(this.component.digest);
+    }
+  }
+
   componentWillUnmount() {
     if (this.component) {
       this.component.destroy();

+ 14 - 14
public/app/features/explore/QueryField.tsx

@@ -36,8 +36,8 @@ export interface QueryFieldProps {
   cleanText?: (text: string) => string;
   disabled?: boolean;
   initialQuery: string | null;
-  onExecuteQuery?: () => void;
-  onQueryChange?: (value: string) => void;
+  onRunQuery?: () => void;
+  onChange?: (value: string) => void;
   onTypeahead?: (typeahead: TypeaheadInput) => TypeaheadOutput;
   onWillApplySuggestion?: (suggestion: string, state: QueryFieldState) => string;
   placeholder?: string;
@@ -149,7 +149,7 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
       if (documentChanged) {
         const textChanged = Plain.serialize(prevValue) !== Plain.serialize(value);
         if (textChanged && invokeParentOnValueChanged) {
-          this.executeOnQueryChangeAndExecuteQueries();
+          this.executeOnChangeAndRunQueries();
         }
         if (textChanged && !invokeParentOnValueChanged) {
           this.updateLogsHighlights();
@@ -167,21 +167,21 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
   };
 
   updateLogsHighlights = () => {
-    const { onQueryChange } = this.props;
-    if (onQueryChange) {
-      onQueryChange(Plain.serialize(this.state.value));
+    const { onChange } = this.props;
+    if (onChange) {
+      onChange(Plain.serialize(this.state.value));
     }
   };
 
-  executeOnQueryChangeAndExecuteQueries = () => {
+  executeOnChangeAndRunQueries = () => {
     // Send text change to parent
-    const { onQueryChange, onExecuteQuery } = this.props;
-    if (onQueryChange) {
-      onQueryChange(Plain.serialize(this.state.value));
+    const { onChange, onRunQuery } = this.props;
+    if (onChange) {
+      onChange(Plain.serialize(this.state.value));
     }
 
-    if (onExecuteQuery) {
-      onExecuteQuery();
+    if (onRunQuery) {
+      onRunQuery();
       this.setState({ lastExecutedValue: this.state.value });
     }
   };
@@ -330,7 +330,7 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
 
       return true;
     } else {
-      this.executeOnQueryChangeAndExecuteQueries();
+      this.executeOnChangeAndRunQueries();
 
       return undefined;
     }
@@ -413,7 +413,7 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
     this.placeholdersBuffer.clearPlaceholders();
 
     if (previousValue !== currentValue) {
-      this.executeOnQueryChangeAndExecuteQueries();
+      this.executeOnChangeAndRunQueries();
     }
   };
 

+ 63 - 34
public/app/features/explore/QueryRow.tsx

@@ -7,25 +7,26 @@ import { connect } from 'react-redux';
 
 // Components
 import QueryEditor from './QueryEditor';
-import QueryTransactionStatus from './QueryTransactionStatus';
 
 // Actions
 import { changeQuery, modifyQueries, runQueries, addQueryRow } from './state/actions';
 
 // Types
 import { StoreState } from 'app/types';
-import { DataQuery, ExploreDataSourceApi, QueryHint, QueryFixAction, DataSourceStatus, TimeRange } from '@grafana/ui';
-import { QueryTransaction, HistoryItem, ExploreItemState, ExploreId } from 'app/types/explore';
+import {
+  TimeRange,
+  DataQuery,
+  ExploreDataSourceApi,
+  QueryFixAction,
+  DataSourceStatus,
+  PanelData,
+  LoadingState,
+  DataQueryError,
+} from '@grafana/ui';
+import { HistoryItem, ExploreItemState, ExploreId } from 'app/types/explore';
 import { Emitter } from 'app/core/utils/emitter';
 import { highlightLogsExpressionAction, removeQueryRowAction } from './state/actionTypes';
-
-function getFirstHintFromTransactions(transactions: QueryTransaction[]): QueryHint {
-  const transaction = transactions.find(qt => qt.hints && qt.hints.length > 0);
-  if (transaction) {
-    return transaction.hints[0];
-  }
-  return undefined;
-}
+import QueryStatus from './QueryStatus';
 
 interface QueryRowProps {
   addQueryRow: typeof addQueryRow;
@@ -39,20 +40,22 @@ interface QueryRowProps {
   index: number;
   query: DataQuery;
   modifyQueries: typeof modifyQueries;
-  queryTransactions: QueryTransaction[];
   exploreEvents: Emitter;
   range: TimeRange;
   removeQueryRowAction: typeof removeQueryRowAction;
   runQueries: typeof runQueries;
+  queryResponse: PanelData;
+  latency: number;
+  queryErrors: DataQueryError[];
 }
 
 export class QueryRow extends PureComponent<QueryRowProps> {
-  onExecuteQuery = () => {
+  onRunQuery = () => {
     const { exploreId } = this.props;
     this.props.runQueries(exploreId);
   };
 
-  onChangeQuery = (query: DataQuery, override?: boolean) => {
+  onChange = (query: DataQuery, override?: boolean) => {
     const { datasourceInstance, exploreId, index } = this.props;
     this.props.changeQuery(exploreId, query, index, override);
     if (query && !override && datasourceInstance.getHighlighterExpression && index === 0) {
@@ -71,7 +74,7 @@ export class QueryRow extends PureComponent<QueryRowProps> {
   };
 
   onClickClearButton = () => {
-    this.onChangeQuery(null, true);
+    this.onChange(null, true);
   };
 
   onClickHintFix = (action: QueryFixAction) => {
@@ -85,6 +88,7 @@ export class QueryRow extends PureComponent<QueryRowProps> {
   onClickRemoveButton = () => {
     const { exploreId, index } = this.props;
     this.props.removeQueryRowAction({ exploreId, index });
+    this.props.runQueries(exploreId);
   };
 
   updateLogsHighlights = _.debounce((value: DataQuery) => {
@@ -100,24 +104,20 @@ export class QueryRow extends PureComponent<QueryRowProps> {
     const {
       datasourceInstance,
       history,
-      index,
       query,
-      queryTransactions,
       exploreEvents,
       range,
       datasourceStatus,
+      queryResponse,
+      latency,
+      queryErrors,
     } = this.props;
-
-    const transactions = queryTransactions.filter(t => t.rowIndex === index);
-    const transactionWithError = transactions.find(t => t.error !== undefined);
-    const hint = getFirstHintFromTransactions(transactions);
-    const queryError = transactionWithError ? transactionWithError.error : null;
     const QueryField = datasourceInstance.components.ExploreQueryField;
 
     return (
       <div className="query-row">
         <div className="query-row-status">
-          <QueryTransactionStatus transactions={transactions} />
+          <QueryStatus queryResponse={queryResponse} latency={latency} />
         </div>
         <div className="query-row-field flex-shrink-1">
           {QueryField ? (
@@ -125,19 +125,19 @@ export class QueryRow extends PureComponent<QueryRowProps> {
               datasource={datasourceInstance}
               datasourceStatus={datasourceStatus}
               query={query}
-              error={queryError}
-              hint={hint}
               history={history}
-              onExecuteQuery={this.onExecuteQuery}
-              onExecuteHint={this.onClickHintFix}
-              onQueryChange={this.onChangeQuery}
+              onRunQuery={this.onRunQuery}
+              onHint={this.onClickHintFix}
+              onChange={this.onChange}
+              panelData={null}
+              queryResponse={queryResponse}
             />
           ) : (
             <QueryEditor
+              error={queryErrors}
               datasource={datasourceInstance}
-              error={queryError}
-              onQueryChange={this.onChangeQuery}
-              onExecuteQuery={this.onExecuteQuery}
+              onQueryChange={this.onChange}
+              onExecuteQuery={this.onRunQuery}
               initialQuery={query}
               exploreEvents={exploreEvents}
               range={range}
@@ -169,15 +169,44 @@ export class QueryRow extends PureComponent<QueryRowProps> {
 function mapStateToProps(state: StoreState, { exploreId, index }: QueryRowProps) {
   const explore = state.explore;
   const item: ExploreItemState = explore[exploreId];
-  const { datasourceInstance, history, queries, queryTransactions, range, datasourceError } = item;
+  const {
+    datasourceInstance,
+    history,
+    queries,
+    range,
+    datasourceError,
+    graphResult,
+    graphIsLoading,
+    tableIsLoading,
+    logIsLoading,
+    latency,
+    queryErrors,
+  } = item;
   const query = queries[index];
+  const datasourceStatus = datasourceError ? DataSourceStatus.Disconnected : DataSourceStatus.Connected;
+  const error = queryErrors.filter(queryError => queryError.refId === query.refId)[0];
+  const series = graphResult ? graphResult : []; // TODO: use SeriesData
+  const queryResponseState =
+    graphIsLoading || tableIsLoading || logIsLoading
+      ? LoadingState.Loading
+      : error
+      ? LoadingState.Error
+      : LoadingState.Done;
+  const queryResponse: PanelData = {
+    series,
+    state: queryResponseState,
+    error,
+  };
+
   return {
     datasourceInstance,
     history,
     query,
-    queryTransactions,
     range,
-    datasourceStatus: datasourceError ? DataSourceStatus.Disconnected : DataSourceStatus.Connected,
+    datasourceStatus,
+    queryResponse,
+    latency,
+    queryErrors,
   };
 }
 

+ 47 - 0
public/app/features/explore/QueryStatus.tsx

@@ -0,0 +1,47 @@
+import React, { PureComponent } from 'react';
+
+import ElapsedTime from './ElapsedTime';
+import { PanelData, LoadingState } from '@grafana/ui';
+
+function formatLatency(value) {
+  return `${(value / 1000).toFixed(1)}s`;
+}
+
+interface QueryStatusItemProps {
+  queryResponse: PanelData;
+  latency: number;
+}
+
+class QueryStatusItem extends PureComponent<QueryStatusItemProps> {
+  render() {
+    const { queryResponse, latency } = this.props;
+    const className =
+      queryResponse.state === LoadingState.Done || LoadingState.Error
+        ? 'query-transaction'
+        : 'query-transaction query-transaction--loading';
+    return (
+      <div className={className}>
+        {/* <div className="query-transaction__type">{transaction.resultType}:</div> */}
+        <div className="query-transaction__duration">
+          {queryResponse.state === LoadingState.Done || LoadingState.Error ? formatLatency(latency) : <ElapsedTime />}
+        </div>
+      </div>
+    );
+  }
+}
+
+interface QueryStatusProps {
+  queryResponse: PanelData;
+  latency: number;
+}
+
+export default class QueryStatus extends PureComponent<QueryStatusProps> {
+  render() {
+    const { queryResponse, latency } = this.props;
+    return (
+      <div className="query-transactions">
+        {queryResponse && <QueryStatusItem queryResponse={queryResponse} latency={latency} />}
+      </div>
+    );
+  }
+}

+ 0 - 44
public/app/features/explore/QueryTransactionStatus.tsx

@@ -1,44 +0,0 @@
-import React, { PureComponent } from 'react';
-
-import { QueryTransaction } from 'app/types/explore';
-import ElapsedTime from './ElapsedTime';
-
-function formatLatency(value) {
-  return `${(value / 1000).toFixed(1)}s`;
-}
-
-interface QueryTransactionStatusItemProps {
-  transaction: QueryTransaction;
-}
-
-class QueryTransactionStatusItem extends PureComponent<QueryTransactionStatusItemProps> {
-  render() {
-    const { transaction } = this.props;
-    const className = transaction.done ? 'query-transaction' : 'query-transaction query-transaction--loading';
-    return (
-      <div className={className}>
-        <div className="query-transaction__type">{transaction.resultType}:</div>
-        <div className="query-transaction__duration">
-          {transaction.done ? formatLatency(transaction.latency) : <ElapsedTime />}
-        </div>
-      </div>
-    );
-  }
-}
-
-interface QueryTransactionStatusProps {
-  transactions: QueryTransaction[];
-}
-
-export default class QueryTransactionStatus extends PureComponent<QueryTransactionStatusProps> {
-  render() {
-    const { transactions } = this.props;
-    return (
-      <div className="query-transactions">
-        {transactions.map((t, i) => (
-          <QueryTransactionStatusItem key={`${t.rowIndex}:${t.resultType}`} transaction={t} />
-        ))}
-      </div>
-    );
-  }
-}

+ 2 - 2
public/app/features/explore/TableContainer.tsx

@@ -42,8 +42,8 @@ export class TableContainer extends PureComponent<TableContainerProps> {
 function mapStateToProps(state: StoreState, { exploreId }) {
   const explore = state.explore;
   const item: ExploreItemState = explore[exploreId];
-  const { queryTransactions, showingTable, tableResult } = item;
-  const loading = queryTransactions.some(qt => qt.resultType === 'Table' && !qt.done);
+  const { tableIsLoading, showingTable, tableResult } = item;
+  const loading = tableIsLoading;
   return { loading, showingTable, tableResult };
 }
 

+ 25 - 14
public/app/features/explore/state/actionTypes.ts

@@ -8,6 +8,7 @@ import {
   QueryFixAction,
   LogLevel,
   TimeRange,
+  DataQueryError,
 } from '@grafana/ui/src/types';
 import {
   ExploreId,
@@ -132,22 +133,29 @@ export interface ModifyQueriesPayload {
   modifier: (query: DataQuery, modification: QueryFixAction) => DataQuery;
 }
 
-export interface QueryTransactionFailurePayload {
+export interface QueryFailurePayload {
   exploreId: ExploreId;
-  queryTransactions: QueryTransaction[];
+  response: DataQueryError;
+  resultType: ResultType;
 }
 
-export interface QueryTransactionStartPayload {
+export interface QueryStartPayload {
   exploreId: ExploreId;
   resultType: ResultType;
   rowIndex: number;
   transaction: QueryTransaction;
 }
 
-export interface QueryTransactionSuccessPayload {
+export interface QuerySuccessPayload {
+  exploreId: ExploreId;
+  result: any;
+  resultType: ResultType;
+  latency: number;
+}
+
+export interface HistoryUpdatedPayload {
   exploreId: ExploreId;
   history: HistoryItem[];
-  queryTransactions: QueryTransaction[];
 }
 
 export interface RemoveQueryRowPayload {
@@ -222,6 +230,11 @@ export interface RunQueriesPayload {
   exploreId: ExploreId;
 }
 
+export interface ResetQueryErrorPayload {
+  exploreId: ExploreId;
+  refIds: string[];
+}
+
 /**
  * Adds a query row after the row with the given index.
  */
@@ -310,9 +323,7 @@ export const modifyQueriesAction = actionCreatorFactory<ModifyQueriesPayload>('e
  * Mark a query transaction as failed with an error extracted from the query response.
  * The transaction will be marked as `done`.
  */
-export const queryTransactionFailureAction = actionCreatorFactory<QueryTransactionFailurePayload>(
-  'explore/QUERY_TRANSACTION_FAILURE'
-).create();
+export const queryFailureAction = actionCreatorFactory<QueryFailurePayload>('explore/QUERY_FAILURE').create();
 
 /**
  * Start a query transaction for the given result type.
@@ -321,9 +332,7 @@ export const queryTransactionFailureAction = actionCreatorFactory<QueryTransacti
  * @param resultType Associate the transaction with a result viewer, e.g., Graph
  * @param rowIndex Index is used to associate latency for this transaction with a query row
  */
-export const queryTransactionStartAction = actionCreatorFactory<QueryTransactionStartPayload>(
-  'explore/QUERY_TRANSACTION_START'
-).create();
+export const queryStartAction = actionCreatorFactory<QueryStartPayload>('explore/QUERY_START').create();
 
 /**
  * Complete a query transaction, mark the transaction as `done` and store query state in URL.
@@ -336,9 +345,7 @@ export const queryTransactionStartAction = actionCreatorFactory<QueryTransaction
  * @param queries Queries from all query rows
  * @param datasourceId Origin datasource instance, used to discard results if current datasource is different
  */
-export const queryTransactionSuccessAction = actionCreatorFactory<QueryTransactionSuccessPayload>(
-  'explore/QUERY_TRANSACTION_SUCCESS'
-).create();
+export const querySuccessAction = actionCreatorFactory<QuerySuccessPayload>('explore/QUERY_SUCCESS').create();
 
 /**
  * Remove query row of the given index, as well as associated query results.
@@ -426,6 +433,10 @@ export const loadExploreDatasources = actionCreatorFactory<LoadExploreDataSource
   'explore/LOAD_EXPLORE_DATASOURCES'
 ).create();
 
+export const historyUpdatedAction = actionCreatorFactory<HistoryUpdatedPayload>('explore/HISTORY_UPDATED').create();
+
+export const resetQueryErrorAction = actionCreatorFactory<ResetQueryErrorPayload>('explore/RESET_QUERY_ERROR').create();
+
 export type HigherOrderAction =
   | ActionOf<SplitCloseActionPayload>
   | SplitOpenAction

+ 97 - 157
public/app/features/explore/state/actions.ts

@@ -18,20 +18,21 @@ import {
   parseUrlState,
   getTimeRange,
   getTimeRangeFromUrl,
+  generateNewKeyAndAddRefIdIfMissing,
+  instanceOfDataQueryError,
+  getRefIds,
 } from 'app/core/utils/explore';
 
 // Actions
 import { updateLocation } from 'app/core/actions';
 
 // Types
-import { ResultGetter } from 'app/types/explore';
 import { ThunkResult } from 'app/types';
 import {
   RawTimeRange,
   DataSourceApi,
   DataQuery,
   DataSourceSelectItem,
-  QueryHint,
   QueryFixAction,
   TimeRange,
 } from '@grafana/ui/src/types';
@@ -61,9 +62,8 @@ import {
   LoadDatasourceReadyPayload,
   loadDatasourceReadyAction,
   modifyQueriesAction,
-  queryTransactionFailureAction,
-  queryTransactionStartAction,
-  queryTransactionSuccessAction,
+  queryFailureAction,
+  querySuccessAction,
   scanRangeAction,
   scanStartAction,
   setQueriesAction,
@@ -82,11 +82,15 @@ import {
   testDataSourceSuccessAction,
   testDataSourceFailureAction,
   loadExploreDatasources,
+  queryStartAction,
+  historyUpdatedAction,
+  resetQueryErrorAction,
 } from './actionTypes';
 import { ActionOf, ActionCreator } from 'app/core/redux/actionCreatorFactory';
 import { LogsDedupStrategy } from 'app/core/logs_model';
 import { getTimeZone } from 'app/features/profile/state/selectors';
 import { isDateTime } from '@grafana/ui/src/utils/moment_wrapper';
+import { toDataQueryError } from 'app/features/dashboard/state/PanelQueryState';
 
 /**
  * Updates UI state and save it to the URL
@@ -103,7 +107,8 @@ const updateExploreUIState = (exploreId: ExploreId, uiStateFragment: Partial<Exp
  */
 export function addQueryRow(exploreId: ExploreId, index: number): ThunkResult<void> {
   return (dispatch, getState) => {
-    const query = generateEmptyQuery(getState().explore[exploreId].queries, index);
+    const queries = getState().explore[exploreId].queries;
+    const query = generateEmptyQuery(queries, index);
 
     dispatch(addQueryRowAction({ exploreId, index, query }));
   };
@@ -148,7 +153,9 @@ export function changeQuery(
   return (dispatch, getState) => {
     // Null query means reset
     if (query === null) {
-      query = { ...generateEmptyQuery(getState().explore[exploreId].queries) };
+      const queries = getState().explore[exploreId].queries;
+      const { refId, key } = queries[index];
+      query = generateNewKeyAndAddRefIdIfMissing({ refId, key }, queries, index);
     }
 
     dispatch(changeQueryAction({ exploreId, query, index, override }));
@@ -306,10 +313,7 @@ export function importQueries(
       importedQueries = ensureQueries();
     }
 
-    const nextQueries = importedQueries.map((q, i) => ({
-      ...q,
-      ...generateEmptyQuery(queries),
-    }));
+    const nextQueries = ensureQueries(importedQueries);
 
     dispatch(queriesImportedAction({ exploreId, queries: nextQueries }));
   };
@@ -368,7 +372,11 @@ export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): T
     }
 
     if (instance.init) {
-      instance.init();
+      try {
+        instance.init();
+      } catch (err) {
+        console.log(err);
+      }
     }
 
     if (datasourceName !== getState().explore[exploreId].requestedDatasourceName) {
@@ -401,140 +409,87 @@ export function modifyQueries(
   };
 }
 
-/**
- * Mark a query transaction as failed with an error extracted from the query response.
- * The transaction will be marked as `done`.
- */
-export function queryTransactionFailure(
+export function processQueryErrors(
   exploreId: ExploreId,
-  transactionId: string,
   response: any,
+  resultType: ResultType,
   datasourceId: string
 ): ThunkResult<void> {
   return (dispatch, getState) => {
-    const { datasourceInstance, queryTransactions } = getState().explore[exploreId];
+    const { datasourceInstance } = getState().explore[exploreId];
+
     if (datasourceInstance.meta.id !== datasourceId || response.cancelled) {
       // Navigated away, queries did not matter
       return;
     }
 
-    // Transaction might have been discarded
-    if (!queryTransactions.find(qt => qt.id === transactionId)) {
-      return;
-    }
+    console.error(response); // To help finding problems with query syntax
 
-    console.error(response);
-
-    let error: string;
-    let errorDetails: string;
-    if (response.data) {
-      if (typeof response.data === 'string') {
-        error = response.data;
-      } else if (response.data.error) {
-        error = response.data.error;
-        if (response.data.response) {
-          errorDetails = response.data.response;
-        }
-      } else {
-        throw new Error('Could not handle error response');
-      }
-    } else if (response.message) {
-      error = response.message;
-    } else if (typeof response === 'string') {
-      error = response;
-    } else {
-      error = 'Unknown error during query transaction. Please check JS console logs.';
-    }
-
-    // Mark transactions as complete
-    const nextQueryTransactions = queryTransactions.map(qt => {
-      if (qt.id === transactionId) {
-        return {
-          ...qt,
-          error,
-          errorDetails,
-          done: true,
-        };
-      }
-      return qt;
-    });
+    if (!instanceOfDataQueryError(response)) {
+      response = toDataQueryError(response);
+    }
 
-    dispatch(queryTransactionFailureAction({ exploreId, queryTransactions: nextQueryTransactions }));
+    dispatch(
+      queryFailureAction({
+        exploreId,
+        response,
+        resultType,
+      })
+    );
   };
 }
 
 /**
- * Complete a query transaction, mark the transaction as `done` and store query state in URL.
- * If the transaction was started by a scanner, it keeps on scanning for more results.
- * Side-effect: the query is stored in localStorage.
  * @param exploreId Explore area
- * @param transactionId ID
- * @param result Response from `datasourceInstance.query()`
+ * @param response Response from `datasourceInstance.query()`
  * @param latency Duration between request and response
- * @param queries Queries from all query rows
+ * @param resultType The type of result
  * @param datasourceId Origin datasource instance, used to discard results if current datasource is different
  */
-export function queryTransactionSuccess(
+export function processQueryResults(
   exploreId: ExploreId,
-  transactionId: string,
-  result: any,
+  response: any,
   latency: number,
-  queries: DataQuery[],
+  resultType: ResultType,
   datasourceId: string
 ): ThunkResult<void> {
   return (dispatch, getState) => {
-    const { datasourceInstance, history, queryTransactions, scanner, scanning } = getState().explore[exploreId];
+    const { datasourceInstance, scanning, scanner } = getState().explore[exploreId];
 
     // If datasource already changed, results do not matter
     if (datasourceInstance.meta.id !== datasourceId) {
       return;
     }
 
-    // Transaction might have been discarded
-    const transaction = queryTransactions.find(qt => qt.id === transactionId);
-    if (!transaction) {
-      return;
-    }
-
-    // Get query hints
-    let hints: QueryHint[];
-    if (datasourceInstance.getQueryHints) {
-      hints = datasourceInstance.getQueryHints(transaction.query, result);
-    }
+    const series: any[] = response.data;
+    const refIds = getRefIds(series);
 
-    // Mark transactions as complete and attach result
-    const nextQueryTransactions = queryTransactions.map(qt => {
-      if (qt.id === transactionId) {
-        return {
-          ...qt,
-          hints,
-          latency,
-          result,
-          done: true,
-        };
-      }
-      return qt;
-    });
+    // Clears any previous errors that now have a successful query, important so Angular editors are updated correctly
+    dispatch(
+      resetQueryErrorAction({
+        exploreId,
+        refIds,
+      })
+    );
 
-    // Side-effect: Saving history in localstorage
-    const nextHistory = updateHistory(history, datasourceId, queries);
+    const resultGetter =
+      resultType === 'Graph' ? makeTimeSeriesList : resultType === 'Table' ? (data: any[]) => data : null;
+    const result = resultGetter ? resultGetter(series, null, []) : series;
 
     dispatch(
-      queryTransactionSuccessAction({
+      querySuccessAction({
         exploreId,
-        history: nextHistory,
-        queryTransactions: nextQueryTransactions,
+        result,
+        resultType,
+        latency,
       })
     );
 
     // Keep scanning for results if this was the last scanning transaction
     if (scanning) {
       if (_.size(result) === 0) {
-        const other = nextQueryTransactions.find(qt => qt.scanning && !qt.done);
-        if (!other) {
-          const range = scanner();
-          dispatch(scanRangeAction({ exploreId, range }));
-        }
+        const range = scanner();
+        dispatch(scanRangeAction({ exploreId, range }));
       } else {
         // We can stop scanning if we have a result
         dispatch(scanStopAction({ exploreId }));
@@ -580,32 +535,22 @@ export function runQueries(exploreId: ExploreId, ignoreUIState = false): ThunkRe
     // Keep table queries first since they need to return quickly
     if ((ignoreUIState || showingTable) && supportsTable) {
       dispatch(
-        runQueriesForType(
-          exploreId,
-          'Table',
-          {
-            interval,
-            format: 'table',
-            instant: true,
-            valueWithRefId: true,
-          },
-          (data: any[]) => data[0]
-        )
+        runQueriesForType(exploreId, 'Table', {
+          interval,
+          format: 'table',
+          instant: true,
+          valueWithRefId: true,
+        })
       );
     }
     if ((ignoreUIState || showingGraph) && supportsGraph) {
       dispatch(
-        runQueriesForType(
-          exploreId,
-          'Graph',
-          {
-            interval,
-            format: 'time_series',
-            instant: false,
-            maxDataPoints: containerWidth,
-          },
-          makeTimeSeriesList
-        )
+        runQueriesForType(exploreId, 'Graph', {
+          interval,
+          format: 'time_series',
+          instant: false,
+          maxDataPoints: containerWidth,
+        })
       );
     }
     if ((ignoreUIState || showingLogs) && supportsLogs) {
@@ -626,37 +571,27 @@ export function runQueries(exploreId: ExploreId, ignoreUIState = false): ThunkRe
 function runQueriesForType(
   exploreId: ExploreId,
   resultType: ResultType,
-  queryOptions: QueryOptions,
-  resultGetter?: ResultGetter
+  queryOptions: QueryOptions
 ): ThunkResult<void> {
   return async (dispatch, getState) => {
-    const { datasourceInstance, eventBridge, queries, queryIntervals, range, scanning } = getState().explore[exploreId];
+    const { datasourceInstance, eventBridge, queries, queryIntervals, range, scanning, history } = getState().explore[
+      exploreId
+    ];
     const datasourceId = datasourceInstance.meta.id;
-    // Run all queries concurrently
-    for (let rowIndex = 0; rowIndex < queries.length; rowIndex++) {
-      const query = queries[rowIndex];
-      const transaction = buildQueryTransaction(
-        query,
-        rowIndex,
-        resultType,
-        queryOptions,
-        range,
-        queryIntervals,
-        scanning
-      );
-      dispatch(queryTransactionStartAction({ exploreId, resultType, rowIndex, transaction }));
-      try {
-        const now = Date.now();
-        const res = await datasourceInstance.query(transaction.options);
-        eventBridge.emit('data-received', res.data || []);
-        const latency = Date.now() - now;
-        const { queryTransactions } = getState().explore[exploreId];
-        const results = resultGetter ? resultGetter(res.data, transaction, queryTransactions) : res.data;
-        dispatch(queryTransactionSuccess(exploreId, transaction.id, results, latency, queries, datasourceId));
-      } catch (response) {
-        eventBridge.emit('data-error', response);
-        dispatch(queryTransactionFailure(exploreId, transaction.id, response, datasourceId));
-      }
+    const transaction = buildQueryTransaction(queries, resultType, queryOptions, range, queryIntervals, scanning);
+    dispatch(queryStartAction({ exploreId, resultType, rowIndex: 0, transaction }));
+    try {
+      const now = Date.now();
+      const response = await datasourceInstance.query(transaction.options);
+      eventBridge.emit('data-received', response.data || []);
+      const latency = Date.now() - now;
+      // Side-effect: Saving history in localstorage
+      const nextHistory = updateHistory(history, datasourceId, queries);
+      dispatch(historyUpdatedAction({ exploreId, history: nextHistory }));
+      dispatch(processQueryResults(exploreId, response, latency, resultType, datasourceId));
+    } catch (err) {
+      eventBridge.emit('data-error', err);
+      dispatch(processQueryErrors(exploreId, err, resultType, datasourceId));
     }
   };
 }
@@ -684,8 +619,9 @@ export function scanStart(exploreId: ExploreId, scanner: RangeScanner): ThunkRes
 export function setQueries(exploreId: ExploreId, rawQueries: DataQuery[]): ThunkResult<void> {
   return (dispatch, getState) => {
     // Inject react keys into query objects
-    const queries = rawQueries.map(q => ({ ...q, ...generateEmptyQuery(getState().explore[exploreId].queries) }));
-    dispatch(setQueriesAction({ exploreId, queries }));
+    const queries = getState().explore[exploreId].queries;
+    const nextQueries = rawQueries.map((query, index) => generateNewKeyAndAddRefIdIfMissing(query, queries, index));
+    dispatch(setQueriesAction({ exploreId, queries: nextQueries }));
     dispatch(runQueries(exploreId));
   };
 }
@@ -849,7 +785,11 @@ export function refreshExplore(exploreId: ExploreId): ThunkResult<void> {
 
     const { urlState, update, containerWidth, eventBridge } = itemState;
     const { datasource, queries, range: urlRange, ui } = urlState;
-    const refreshQueries = queries.map(q => ({ ...q, ...generateEmptyQuery(itemState.queries) }));
+    const refreshQueries: DataQuery[] = [];
+    for (let index = 0; index < queries.length; index++) {
+      const query = queries[index];
+      refreshQueries.push(generateNewKeyAndAddRefIdIfMissing(query, refreshQueries, index));
+    }
     const timeZone = getTimeZone(getState().user);
     const range = getTimeRangeFromUrl(urlRange, timeZone);
 

+ 0 - 1
public/app/features/explore/state/reducers.test.ts

@@ -97,7 +97,6 @@ describe('Explore item reducer', () => {
         const queryTransactions: QueryTransaction[] = [];
         const initalState: Partial<ExploreItemState> = {
           datasourceError: null,
-          queryTransactions: [{} as QueryTransaction],
           graphResult: [],
           tableResult: {} as TableModel,
           logsResult: {} as LogsModel,

+ 92 - 127
public/app/features/explore/state/reducers.ts

@@ -1,14 +1,14 @@
 import _ from 'lodash';
 import {
   calculateResultsFromQueryTransactions,
-  generateEmptyQuery,
   getIntervals,
   ensureQueries,
   getQueryKeys,
   parseUrlState,
   DEFAULT_UI_STATE,
+  generateNewKeyAndAddRefIdIfMissing,
 } from 'app/core/utils/explore';
-import { ExploreItemState, ExploreState, QueryTransaction, ExploreId, ExploreUpdateState } from 'app/types/explore';
+import { ExploreItemState, ExploreState, ExploreId, ExploreUpdateState } from 'app/types/explore';
 import { DataQuery } from '@grafana/ui/src/types';
 import {
   HigherOrderAction,
@@ -20,6 +20,8 @@ import {
   SplitCloseActionPayload,
   loadExploreDatasources,
   runQueriesAction,
+  historyUpdatedAction,
+  resetQueryErrorAction,
 } from './actionTypes';
 import { reducerFactory } from 'app/core/redux';
 import {
@@ -36,16 +38,14 @@ import {
   loadDatasourcePendingAction,
   loadDatasourceReadyAction,
   modifyQueriesAction,
-  queryTransactionFailureAction,
-  queryTransactionStartAction,
-  queryTransactionSuccessAction,
+  queryFailureAction,
+  queryStartAction,
+  querySuccessAction,
   removeQueryRowAction,
   scanRangeAction,
   scanStartAction,
   scanStopAction,
   setQueriesAction,
-  toggleGraphAction,
-  toggleLogsAction,
   toggleTableAction,
   queriesImportedAction,
   updateUIStateAction,
@@ -53,6 +53,7 @@ import {
 } from './actionTypes';
 import { updateLocation } from 'app/core/actions/location';
 import { LocationUpdate } from 'app/types';
+import TableModel from 'app/core/table_model';
 
 export const DEFAULT_RANGE = {
   from: 'now-6h',
@@ -84,7 +85,6 @@ export const makeExploreItemState = (): ExploreItemState => ({
   history: [],
   queries: [],
   initialized: false,
-  queryTransactions: [],
   queryIntervals: { interval: '15s', intervalMs: DEFAULT_GRAPH_INTERVAL },
   range: {
     from: null,
@@ -96,12 +96,17 @@ export const makeExploreItemState = (): ExploreItemState => ({
   showingGraph: true,
   showingLogs: true,
   showingTable: true,
+  graphIsLoading: false,
+  logIsLoading: false,
+  tableIsLoading: false,
   supportsGraph: null,
   supportsLogs: null,
   supportsTable: null,
   queryKeys: [],
   urlState: null,
   update: makeInitialUpdateState(),
+  queryErrors: [],
+  latency: 0,
 });
 
 /**
@@ -121,28 +126,16 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
   .addMapper({
     filter: addQueryRowAction,
     mapper: (state, action): ExploreItemState => {
-      const { queries, queryTransactions } = state;
+      const { queries } = state;
       const { index, query } = action.payload;
 
       // Add to queries, which will cause a new row to be rendered
       const nextQueries = [...queries.slice(0, index + 1), { ...query }, ...queries.slice(index + 1)];
 
-      // Ongoing transactions need to update their row indices
-      const nextQueryTransactions = queryTransactions.map(qt => {
-        if (qt.rowIndex > index) {
-          return {
-            ...qt,
-            rowIndex: qt.rowIndex + 1,
-          };
-        }
-        return qt;
-      });
-
       return {
         ...state,
         queries: nextQueries,
         logsHighlighterExpressions: undefined,
-        queryTransactions: nextQueryTransactions,
         queryKeys: getQueryKeys(nextQueries, state.datasourceInstance),
       };
     },
@@ -150,21 +143,17 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
   .addMapper({
     filter: changeQueryAction,
     mapper: (state, action): ExploreItemState => {
-      const { queries, queryTransactions } = state;
+      const { queries } = state;
       const { query, index } = action.payload;
 
       // Override path: queries are completely reset
-      const nextQuery: DataQuery = { ...query, ...generateEmptyQuery(state.queries) };
+      const nextQuery: DataQuery = generateNewKeyAndAddRefIdIfMissing(query, queries, index);
       const nextQueries = [...queries];
       nextQueries[index] = nextQuery;
 
-      // Discard ongoing transaction related to row query
-      const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index);
-
       return {
         ...state,
         queries: nextQueries,
-        queryTransactions: nextQueryTransactions,
         queryKeys: getQueryKeys(nextQueries, state.datasourceInstance),
       };
     },
@@ -199,7 +188,6 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
       return {
         ...state,
         queries: queries.slice(),
-        queryTransactions: [],
         showingStartPage: Boolean(state.StartPage),
         queryKeys: getQueryKeys(queries, state.datasourceInstance),
       };
@@ -244,6 +232,11 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
       return {
         ...state,
         datasourceInstance,
+        queryErrors: [],
+        latency: 0,
+        graphIsLoading: false,
+        logIsLoading: false,
+        tableIsLoading: false,
         supportsGraph,
         supportsLogs,
         supportsTable,
@@ -284,7 +277,6 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
         datasourceLoading: false,
         datasourceMissing: false,
         logsHighlighterExpressions: undefined,
-        queryTransactions: [],
         update: makeInitialUpdateState(),
       };
     },
@@ -292,95 +284,87 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
   .addMapper({
     filter: modifyQueriesAction,
     mapper: (state, action): ExploreItemState => {
-      const { queries, queryTransactions } = state;
+      const { queries } = state;
       const { modification, index, modifier } = action.payload;
       let nextQueries: DataQuery[];
-      let nextQueryTransactions: QueryTransaction[];
       if (index === undefined) {
         // Modify all queries
-        nextQueries = queries.map((query, i) => ({
-          ...modifier({ ...query }, modification),
-          ...generateEmptyQuery(state.queries),
-        }));
-        // Discard all ongoing transactions
-        nextQueryTransactions = [];
+        nextQueries = queries.map((query, i) => {
+          const nextQuery = modifier({ ...query }, modification);
+          return generateNewKeyAndAddRefIdIfMissing(nextQuery, queries, i);
+        });
       } else {
         // Modify query only at index
         nextQueries = queries.map((query, i) => {
-          // Synchronize all queries with local query cache to ensure consistency
-          // TODO still needed?
-          return i === index
-            ? { ...modifier({ ...query }, modification), ...generateEmptyQuery(state.queries) }
-            : query;
+          if (i === index) {
+            const nextQuery = modifier({ ...query }, modification);
+            return generateNewKeyAndAddRefIdIfMissing(nextQuery, queries, i);
+          }
+
+          return query;
         });
-        nextQueryTransactions = queryTransactions
-          // Consume the hint corresponding to the action
-          .map(qt => {
-            if (qt.hints != null && qt.rowIndex === index) {
-              qt.hints = qt.hints.filter(hint => hint.fix.action !== modification);
-            }
-            return qt;
-          })
-          // Preserve previous row query transaction to keep results visible if next query is incomplete
-          .filter(qt => modification.preventSubmit || qt.rowIndex !== index);
       }
       return {
         ...state,
         queries: nextQueries,
         queryKeys: getQueryKeys(nextQueries, state.datasourceInstance),
-        queryTransactions: nextQueryTransactions,
       };
     },
   })
   .addMapper({
-    filter: queryTransactionFailureAction,
+    filter: queryFailureAction,
     mapper: (state, action): ExploreItemState => {
-      const { queryTransactions } = action.payload;
+      const { resultType, response } = action.payload;
+      const queryErrors = state.queryErrors.concat(response);
+
       return {
         ...state,
-        queryTransactions,
+        graphResult: resultType === 'Graph' ? null : state.graphResult,
+        tableResult: resultType === 'Table' ? null : state.tableResult,
+        logsResult: resultType === 'Logs' ? null : state.logsResult,
+        latency: 0,
+        queryErrors,
         showingStartPage: false,
+        graphIsLoading: resultType === 'Graph' ? false : state.graphIsLoading,
+        logIsLoading: resultType === 'Logs' ? false : state.logIsLoading,
+        tableIsLoading: resultType === 'Table' ? false : state.tableIsLoading,
         update: makeInitialUpdateState(),
       };
     },
   })
   .addMapper({
-    filter: queryTransactionStartAction,
+    filter: queryStartAction,
     mapper: (state, action): ExploreItemState => {
-      const { queryTransactions } = state;
-      const { resultType, rowIndex, transaction } = action.payload;
-      // Discarding existing transactions of same type
-      const remainingTransactions = queryTransactions.filter(
-        qt => !(qt.resultType === resultType && qt.rowIndex === rowIndex)
-      );
-
-      // Append new transaction
-      const nextQueryTransactions: QueryTransaction[] = [...remainingTransactions, transaction];
+      const { resultType } = action.payload;
 
       return {
         ...state,
-        queryTransactions: nextQueryTransactions,
+        queryErrors: [],
+        latency: 0,
+        graphIsLoading: resultType === 'Graph' ? true : state.graphIsLoading,
+        logIsLoading: resultType === 'Logs' ? true : state.logIsLoading,
+        tableIsLoading: resultType === 'Table' ? true : state.tableIsLoading,
         showingStartPage: false,
         update: makeInitialUpdateState(),
       };
     },
   })
   .addMapper({
-    filter: queryTransactionSuccessAction,
+    filter: querySuccessAction,
     mapper: (state, action): ExploreItemState => {
-      const { datasourceInstance, queryIntervals } = state;
-      const { history, queryTransactions } = action.payload;
-      const results = calculateResultsFromQueryTransactions(
-        queryTransactions,
-        datasourceInstance,
-        queryIntervals.intervalMs
-      );
+      const { queryIntervals } = state;
+      const { result, resultType, latency } = action.payload;
+      const results = calculateResultsFromQueryTransactions(result, resultType, queryIntervals.intervalMs);
 
       return {
         ...state,
-        ...results,
-        history,
-        queryTransactions,
+        graphResult: resultType === 'Graph' ? results.graphResult : state.graphResult,
+        tableResult: resultType === 'Table' ? results.tableResult : state.tableResult,
+        logsResult: resultType === 'Logs' ? results.logsResult : state.logsResult,
+        latency,
+        graphIsLoading: false,
+        logIsLoading: false,
+        tableIsLoading: false,
         showingStartPage: false,
         update: makeInitialUpdateState(),
       };
@@ -389,7 +373,7 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
   .addMapper({
     filter: removeQueryRowAction,
     mapper: (state, action): ExploreItemState => {
-      const { datasourceInstance, queries, queryIntervals, queryTransactions, queryKeys } = state;
+      const { queries, queryKeys } = state;
       const { index } = action.payload;
 
       if (queries.length <= 1) {
@@ -399,20 +383,10 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
       const nextQueries = [...queries.slice(0, index), ...queries.slice(index + 1)];
       const nextQueryKeys = [...queryKeys.slice(0, index), ...queryKeys.slice(index + 1)];
 
-      // Discard transactions related to row query
-      const nextQueryTransactions = queryTransactions.filter(qt => nextQueries.some(nq => nq.key === qt.query.key));
-      const results = calculateResultsFromQueryTransactions(
-        nextQueryTransactions,
-        datasourceInstance,
-        queryIntervals.intervalMs
-      );
-
       return {
         ...state,
-        ...results,
         queries: nextQueries,
         logsHighlighterExpressions: undefined,
-        queryTransactions: nextQueryTransactions,
         queryKeys: nextQueryKeys,
       };
     },
@@ -432,11 +406,8 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
   .addMapper({
     filter: scanStopAction,
     mapper: (state): ExploreItemState => {
-      const { queryTransactions } = state;
-      const nextQueryTransactions = queryTransactions.filter(qt => qt.scanning && !qt.done);
       return {
         ...state,
-        queryTransactions: nextQueryTransactions,
         scanning: false,
         scanRange: undefined,
         scanner: undefined,
@@ -461,47 +432,15 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
       return { ...state, ...action.payload };
     },
   })
-  .addMapper({
-    filter: toggleGraphAction,
-    mapper: (state): ExploreItemState => {
-      const showingGraph = !state.showingGraph;
-      let nextQueryTransactions = state.queryTransactions;
-      if (!showingGraph) {
-        // Discard transactions related to Graph query
-        nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Graph');
-      }
-      return { ...state, queryTransactions: nextQueryTransactions };
-    },
-  })
-  .addMapper({
-    filter: toggleLogsAction,
-    mapper: (state): ExploreItemState => {
-      const showingLogs = !state.showingLogs;
-      let nextQueryTransactions = state.queryTransactions;
-      if (!showingLogs) {
-        // Discard transactions related to Logs query
-        nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Logs');
-      }
-      return { ...state, queryTransactions: nextQueryTransactions };
-    },
-  })
   .addMapper({
     filter: toggleTableAction,
     mapper: (state): ExploreItemState => {
       const showingTable = !state.showingTable;
       if (showingTable) {
-        return { ...state, queryTransactions: state.queryTransactions };
+        return { ...state };
       }
 
-      // Toggle off needs discarding of table queries and results
-      const nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Table');
-      const results = calculateResultsFromQueryTransactions(
-        nextQueryTransactions,
-        state.datasourceInstance,
-        state.queryIntervals.intervalMs
-      );
-
-      return { ...state, ...results, queryTransactions: nextQueryTransactions };
+      return { ...state, tableResult: new TableModel() };
     },
   })
   .addMapper({
@@ -549,7 +488,6 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
       return {
         ...state,
         datasourceError: action.payload.error,
-        queryTransactions: [],
         graphResult: undefined,
         tableResult: undefined,
         logsResult: undefined,
@@ -581,6 +519,33 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
       };
     },
   })
+  .addMapper({
+    filter: historyUpdatedAction,
+    mapper: (state, action): ExploreItemState => {
+      return {
+        ...state,
+        history: action.payload.history,
+      };
+    },
+  })
+  .addMapper({
+    filter: resetQueryErrorAction,
+    mapper: (state, action): ExploreItemState => {
+      const { refIds } = action.payload;
+      const queryErrors = state.queryErrors.reduce((allErrors, error) => {
+        if (error.refId && refIds.indexOf(error.refId) !== -1) {
+          return allErrors;
+        }
+
+        return allErrors.concat(error);
+      }, []);
+
+      return {
+        ...state,
+        queryErrors,
+      };
+    },
+  })
   .create();
 
 export const updateChildRefreshState = (

+ 1 - 1
public/app/plugins/datasource/loki/components/LokiCheatSheet.tsx

@@ -31,7 +31,7 @@ export default (props: any) => (
         {item.expression && (
           <div
             className="cheat-sheet-item__expression"
-            onClick={e => props.onClickExample({ refId: '1', expr: item.expression })}
+            onClick={e => props.onClickExample({ refId: 'A', expr: item.expression })}
           >
             <code>{item.expression}</code>
           </div>

+ 12 - 28
public/app/plugins/datasource/loki/components/LokiQueryFieldForm.tsx

@@ -86,14 +86,14 @@ export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormPr
 
     this.plugins = [
       BracesPlugin(),
-      RunnerPlugin({ handler: props.onExecuteQuery }),
+      RunnerPlugin({ handler: props.onRunQuery }),
       PluginPrism({
         onlyIn: (node: any) => node.type === 'code_block',
         getSyntax: (node: any) => 'promql',
       }),
     ];
 
-    this.pluginsSearch = [RunnerPlugin({ handler: props.onExecuteQuery })];
+    this.pluginsSearch = [RunnerPlugin({ handler: props.onRunQuery })];
   }
 
   loadOptions = (selectedOptions: CascaderOption[]) => {
@@ -111,24 +111,17 @@ export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormPr
 
   onChangeQuery = (value: string, override?: boolean) => {
     // Send text change to parent
-    const { query, onQueryChange, onExecuteQuery } = this.props;
-    if (onQueryChange) {
+    const { query, onChange, onRunQuery } = this.props;
+    if (onChange) {
       const nextQuery = { ...query, expr: value };
-      onQueryChange(nextQuery);
+      onChange(nextQuery);
 
-      if (override && onExecuteQuery) {
-        onExecuteQuery();
+      if (override && onRunQuery) {
+        onRunQuery();
       }
     }
   };
 
-  onClickHintFix = () => {
-    const { hint, onExecuteHint } = this.props;
-    if (onExecuteHint && hint && hint.fix) {
-      onExecuteHint(hint.fix.action);
-    }
-  };
-
   onTypeahead = (typeahead: TypeaheadInput): TypeaheadOutput => {
     const { datasource } = this.props;
     if (!datasource.languageProvider) {
@@ -156,8 +149,7 @@ export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormPr
 
   render() {
     const {
-      error,
-      hint,
+      queryResponse,
       query,
       syntaxLoaded,
       logLabelOptions,
@@ -197,8 +189,8 @@ export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormPr
               initialQuery={query.expr}
               onTypeahead={this.onTypeahead}
               onWillApplySuggestion={willApplySuggestion}
-              onQueryChange={this.onChangeQuery}
-              onExecuteQuery={this.props.onExecuteQuery}
+              onChange={this.onChangeQuery}
+              onRunQuery={this.props.onRunQuery}
               placeholder="Enter a Loki query"
               portalOrigin="loki"
               syntaxLoaded={syntaxLoaded}
@@ -206,16 +198,8 @@ export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormPr
           </div>
         </div>
         <div>
-          {error ? <div className="prom-query-field-info text-error">{error}</div> : null}
-          {hint ? (
-            <div className="prom-query-field-info text-warning">
-              {hint.label}{' '}
-              {hint.fix ? (
-                <a className="text-link muted" onClick={this.onClickHintFix}>
-                  {hint.fix.label}
-                </a>
-              ) : null}
-            </div>
+          {queryResponse && queryResponse.error ? (
+            <div className="prom-query-field-info text-error">{queryResponse.error.message}</div>
           ) : null}
         </div>
       </>

+ 38 - 3
public/app/plugins/datasource/loki/datasource.ts

@@ -15,10 +15,12 @@ import {
   SeriesData,
   DataSourceApi,
   DataSourceInstanceSettings,
+  DataQueryError,
 } from '@grafana/ui/src/types';
 import { LokiQuery, LokiOptions } from './types';
 import { BackendSrv } from 'app/core/services/backend_srv';
 import { TemplateSrv } from 'app/features/templating/template_srv';
+import { safeStringifyValue } from 'app/core/utils/explore';
 
 export const DEFAULT_MAX_LINES = 1000;
 
@@ -65,16 +67,18 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
     return this.backendSrv.datasourceRequest(req);
   }
 
-  prepareQueryTarget(target, options) {
+  prepareQueryTarget(target: LokiQuery, options: DataQueryRequest<LokiQuery>) {
     const interpolated = this.templateSrv.replace(target.expr);
     const start = this.getTime(options.range.from, false);
     const end = this.getTime(options.range.to, true);
+    const refId = target.refId;
     return {
       ...DEFAULT_QUERY_PARAMS,
       ...parseQuery(interpolated),
       start,
       end,
       limit: this.maxLines,
+      refId,
     };
   }
 
@@ -87,16 +91,47 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
       return Promise.resolve({ data: [] });
     }
 
-    const queries = queryTargets.map(target => this._request('/api/prom/query', target));
+    const queries = queryTargets.map(target =>
+      this._request('/api/prom/query', target).catch((err: any) => {
+        if (err.cancelled) {
+          return err;
+        }
+
+        const error: DataQueryError = {
+          message: 'Unknown error during query transaction. Please check JS console logs.',
+          refId: target.refId,
+        };
+
+        if (err.data) {
+          if (typeof err.data === 'string') {
+            error.message = err.data;
+          } else if (err.data.error) {
+            error.message = safeStringifyValue(err.data.error);
+          }
+        } else if (err.message) {
+          error.message = err.message;
+        } else if (typeof err === 'string') {
+          error.message = err;
+        }
+
+        error.status = err.status;
+        error.statusText = err.statusText;
+
+        throw error;
+      })
+    );
 
     return Promise.all(queries).then((results: any[]) => {
-      const series: SeriesData[] = [];
+      const series: Array<SeriesData | DataQueryError> = [];
 
       for (let i = 0; i < results.length; i++) {
         const result = results[i];
+
         if (result.data) {
+          const refId = queryTargets[i].refId;
           for (const stream of result.data.streams || []) {
             const seriesData = logStreamToSeriesData(stream);
+            seriesData.refId = refId;
             seriesData.meta = {
               search: queryTargets[i].regexp,
               limit: this.maxLines,

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

@@ -27,7 +27,7 @@ export default (props: any) => (
         <div className="cheat-sheet-item__title">{item.title}</div>
         <div
           className="cheat-sheet-item__expression"
-          onClick={e => props.onClickExample({ refId: '1', expr: item.expression })}
+          onClick={e => props.onClickExample({ refId: 'A', expr: item.expression })}
         >
           <code>{item.expression}</code>
         </div>

+ 37 - 15
public/app/plugins/datasource/prometheus/components/PromQueryField.tsx

@@ -16,7 +16,7 @@ import RunnerPlugin from 'app/features/explore/slate-plugins/runner';
 import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField';
 import { PromQuery } from '../types';
 import { CancelablePromise, makePromiseCancelable } from 'app/core/utils/CancelablePromise';
-import { ExploreDataSourceApi, ExploreQueryFieldProps, DataSourceStatus } from '@grafana/ui';
+import { ExploreDataSourceApi, ExploreQueryFieldProps, DataSourceStatus, QueryHint } from '@grafana/ui';
 
 const HISTOGRAM_GROUP = '__histograms__';
 const METRIC_MARK = 'metric';
@@ -109,6 +109,7 @@ interface PromQueryFieldProps extends ExploreQueryFieldProps<ExploreDataSourceAp
 interface PromQueryFieldState {
   metricsOptions: any[];
   syntaxLoaded: boolean;
+  hint: QueryHint;
 }
 
 class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryFieldState> {
@@ -125,7 +126,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
 
     this.plugins = [
       BracesPlugin(),
-      RunnerPlugin({ handler: props.onExecuteQuery }),
+      RunnerPlugin({ handler: props.onRunQuery }),
       PluginPrism({
         onlyIn: (node: any) => node.type === 'code_block',
         getSyntax: (node: any) => 'promql',
@@ -135,6 +136,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
     this.state = {
       metricsOptions: [],
       syntaxLoaded: false,
+      hint: null,
     };
   }
 
@@ -142,6 +144,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
     if (this.languageProvider) {
       this.refreshMetrics(makePromiseCancelable(this.languageProvider.start()));
     }
+    this.refreshHint();
   }
 
   componentWillUnmount() {
@@ -151,6 +154,11 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
   }
 
   componentDidUpdate(prevProps: PromQueryFieldProps) {
+    const currentHasSeries = this.props.queryResponse.series && this.props.queryResponse.series.length > 0;
+    if (currentHasSeries && prevProps.queryResponse.series !== this.props.queryResponse.series) {
+      this.refreshHint();
+    }
+
     const reconnected =
       prevProps.datasourceStatus === DataSourceStatus.Disconnected &&
       this.props.datasourceStatus === DataSourceStatus.Connected;
@@ -167,6 +175,17 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
     }
   }
 
+  refreshHint = () => {
+    const { datasource, query, queryResponse } = this.props;
+    if (queryResponse.series && queryResponse.series.length === 0) {
+      return;
+    }
+
+    const hints = datasource.getQueryHints(query, queryResponse.series);
+    const hint = hints && hints.length > 0 ? hints[0] : null;
+    this.setState({ hint });
+  };
+
   refreshMetrics = (cancelablePromise: CancelablePromise<any>) => {
     this.languageProviderInitializationPromise = cancelablePromise;
     this.languageProviderInitializationPromise.promise
@@ -204,21 +223,22 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
 
   onChangeQuery = (value: string, override?: boolean) => {
     // Send text change to parent
-    const { query, onQueryChange, onExecuteQuery } = this.props;
-    if (onQueryChange) {
+    const { query, onChange, onRunQuery } = this.props;
+    if (onChange) {
       const nextQuery: PromQuery = { ...query, expr: value };
-      onQueryChange(nextQuery);
+      onChange(nextQuery);
 
-      if (override && onExecuteQuery) {
-        onExecuteQuery();
+      if (override && onRunQuery) {
+        onRunQuery();
       }
     }
   };
 
   onClickHintFix = () => {
-    const { hint, onExecuteHint } = this.props;
-    if (onExecuteHint && hint && hint.fix) {
-      onExecuteHint(hint.fix.action);
+    const { hint } = this.state;
+    const { onHint } = this.props;
+    if (onHint && hint && hint.fix) {
+      onHint(hint.fix.action);
     }
   };
 
@@ -273,8 +293,8 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
   };
 
   render() {
-    const { error, hint, query, datasourceStatus } = this.props;
-    const { metricsOptions, syntaxLoaded } = this.state;
+    const { queryResponse, query, datasourceStatus } = this.props;
+    const { metricsOptions, syntaxLoaded, hint } = this.state;
     const cleanText = this.languageProvider ? this.languageProvider.cleanText : undefined;
     const chooserText = getChooserText(syntaxLoaded, datasourceStatus);
     const buttonDisabled = !syntaxLoaded || datasourceStatus === DataSourceStatus.Disconnected;
@@ -296,15 +316,17 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
               initialQuery={query.expr}
               onTypeahead={this.onTypeahead}
               onWillApplySuggestion={willApplySuggestion}
-              onQueryChange={this.onChangeQuery}
-              onExecuteQuery={this.props.onExecuteQuery}
+              onChange={this.onChangeQuery}
+              onRunQuery={this.props.onRunQuery}
               placeholder="Enter a PromQL query"
               portalOrigin="prometheus"
               syntaxLoaded={syntaxLoaded}
             />
           </div>
         </div>
-        {error ? <div className="prom-query-field-info text-error">{error}</div> : null}
+        {queryResponse && queryResponse.error ? (
+          <div className="prom-query-field-info text-error">{queryResponse.error.message}</div>
+        ) : null}
         {hint ? (
           <div className="prom-query-field-info text-warning">
             {hint.label}{' '}

+ 51 - 13
public/app/plugins/datasource/prometheus/datasource.ts

@@ -15,8 +15,15 @@ import { expandRecordingRules } from './language_utils';
 
 // Types
 import { PromQuery, PromOptions } from './types';
-import { DataQueryRequest, DataSourceApi, AnnotationEvent, DataSourceInstanceSettings } from '@grafana/ui/src/types';
+import {
+  DataQueryRequest,
+  DataSourceApi,
+  AnnotationEvent,
+  DataSourceInstanceSettings,
+  DataQueryError,
+} from '@grafana/ui/src/types';
 import { ExploreUrlState } from 'app/types/explore';
+import { safeStringifyValue } from 'app/core/utils/explore';
 import { TemplateSrv } from 'app/features/templating/template_srv';
 import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
 
@@ -38,7 +45,7 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
   /** @ngInject */
   constructor(
     instanceSettings: DataSourceInstanceSettings<PromOptions>,
-    private $q,
+    private $q: angular.IQService,
     private backendSrv: BackendSrv,
     private templateSrv: TemplateSrv,
     private timeSrv: TimeSrv
@@ -134,7 +141,7 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
     return this.templateSrv.variableExists(target.expr);
   }
 
-  query(options: DataQueryRequest<PromQuery>) {
+  query(options: DataQueryRequest<PromQuery>): Promise<{ data: any }> {
     const start = this.getPrometheusTime(options.range.from, false);
     const end = this.getPrometheusTime(options.range.to, true);
 
@@ -154,7 +161,7 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
 
     // No valid targets, return the empty result to save a round trip.
     if (_.isEmpty(queries)) {
-      return this.$q.when({ data: [] });
+      return this.$q.when({ data: [] }) as Promise<{ data: any }>;
     }
 
     const allQueryPromise = _.map(queries, query => {
@@ -165,16 +172,12 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
       }
     });
 
-    return this.$q.all(allQueryPromise).then(responseList => {
+    const allPromise = this.$q.all(allQueryPromise).then((responseList: any) => {
       let result = [];
 
       _.each(responseList, (response, index) => {
-        if (response.status === 'error') {
-          const error = {
-            index,
-            ...response.error,
-          };
-          throw error;
+        if (response.cancelled) {
+          return;
         }
 
         // Keeping original start/end for transformers
@@ -195,6 +198,8 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
 
       return { data: result };
     });
+
+    return allPromise as Promise<{ data: any }>;
   }
 
   createQuery(target, options, start, end) {
@@ -241,6 +246,7 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
     // Only replace vars in expression after having (possibly) updated interval vars
     query.expr = this.templateSrv.replace(expr, scopedVars, this.interpolateQueryExpr);
     query.requestId = options.panelId + target.refId;
+    query.refId = target.refId;
 
     // Align query interval with step to allow query caching and to ensure
     // that about-same-time query results look the same.
@@ -276,7 +282,9 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
     if (this.queryTimeout) {
       data['timeout'] = this.queryTimeout;
     }
-    return this._request(url, data, { requestId: query.requestId, headers: query.headers });
+    return this._request(url, data, { requestId: query.requestId, headers: query.headers }).catch((err: any) =>
+      this.handleErrors(err, query)
+    );
   }
 
   performInstantQuery(query, time) {
@@ -288,9 +296,39 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
     if (this.queryTimeout) {
       data['timeout'] = this.queryTimeout;
     }
-    return this._request(url, data, { requestId: query.requestId, headers: query.headers });
+    return this._request(url, data, { requestId: query.requestId, headers: query.headers }).catch((err: any) =>
+      this.handleErrors(err, query)
+    );
   }
 
+  handleErrors = (err: any, target: PromQuery) => {
+    if (err.cancelled) {
+      return err;
+    }
+
+    const error: DataQueryError = {
+      message: 'Unknown error during query transaction. Please check JS console logs.',
+      refId: target.refId,
+    };
+
+    if (err.data) {
+      if (typeof err.data === 'string') {
+        error.message = err.data;
+      } else if (err.data.error) {
+        error.message = safeStringifyValue(err.data.error);
+      }
+    } else if (err.message) {
+      error.message = err.message;
+    } else if (typeof err === 'string') {
+      error.message = err;
+    }
+
+    error.status = err.status;
+    error.statusText = err.statusText;
+
+    throw error;
+  };
+
   performSuggestQuery(query, cache = false) {
     const url = '/api/v1/label/__name__/values';
 

+ 2 - 1
public/app/plugins/datasource/prometheus/specs/completer.test.ts

@@ -5,6 +5,7 @@ import { DataSourceInstanceSettings } from '@grafana/ui';
 import { PromOptions } from '../types';
 import { TemplateSrv } from 'app/features/templating/template_srv';
 import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
+import { IQService } from 'angular';
 jest.mock('../datasource');
 jest.mock('app/core/services/backend_srv');
 
@@ -22,7 +23,7 @@ describe('Prometheus editor completer', () => {
   const backendSrv = {} as BackendSrv;
   const datasourceStub = new PrometheusDatasource(
     {} as DataSourceInstanceSettings<PromOptions>,
-    {},
+    {} as IQService,
     backendSrv,
     {} as TemplateSrv,
     {} as TimeSrv

+ 25 - 25
public/app/plugins/datasource/prometheus/specs/datasource.test.ts

@@ -401,7 +401,7 @@ describe('PrometheusDatasource', () => {
         },
       };
       backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
-      ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv);
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
 
       await ctx.ds.query(query).then(data => {
         results = data;
@@ -451,7 +451,7 @@ describe('PrometheusDatasource', () => {
       };
 
       backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
-      ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv);
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
 
       await ctx.ds.query(query).then(data => {
         results = data;
@@ -512,7 +512,7 @@ describe('PrometheusDatasource', () => {
       };
 
       backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
-      ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv);
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
 
       await ctx.ds.query(query).then(data => {
         results = data;
@@ -569,7 +569,7 @@ describe('PrometheusDatasource', () => {
       beforeEach(async () => {
         options.annotation.useValueForTime = false;
         backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
-        ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv);
+        ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
 
         await ctx.ds.annotationQuery(options).then(data => {
           results = data;
@@ -589,7 +589,7 @@ describe('PrometheusDatasource', () => {
       beforeEach(async () => {
         options.annotation.useValueForTime = true;
         backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
-        ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv);
+        ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
 
         await ctx.ds.annotationQuery(options).then(data => {
           results = data;
@@ -604,7 +604,7 @@ describe('PrometheusDatasource', () => {
     describe('step parameter', () => {
       beforeEach(() => {
         backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
-        ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv);
+        ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
       });
 
       it('should use default step for short range if no interval is given', () => {
@@ -700,7 +700,7 @@ describe('PrometheusDatasource', () => {
       };
 
       backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
-      ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv);
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
       await ctx.ds.query(query).then(data => {
         results = data;
       });
@@ -737,7 +737,7 @@ describe('PrometheusDatasource', () => {
       const urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=10';
 
       backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
-      ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv);
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
       await ctx.ds.query(query);
       const res = backendSrv.datasourceRequest.mock.calls[0][0];
       expect(res.method).toBe('GET');
@@ -753,7 +753,7 @@ describe('PrometheusDatasource', () => {
       };
       const urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=1';
       backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
-      ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv);
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
       await ctx.ds.query(query);
       const res = backendSrv.datasourceRequest.mock.calls[0][0];
       expect(res.method).toBe('GET');
@@ -774,7 +774,7 @@ describe('PrometheusDatasource', () => {
       };
       const urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=10';
       backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
-      ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv);
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
       await ctx.ds.query(query);
       const res = backendSrv.datasourceRequest.mock.calls[0][0];
       expect(res.method).toBe('GET');
@@ -791,7 +791,7 @@ describe('PrometheusDatasource', () => {
       const start = 60 * 60;
       const urlExpected = 'proxied/api/v1/query_range?query=test&start=' + start + '&end=' + end + '&step=2';
       backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
-      ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv);
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
       await ctx.ds.query(query);
       const res = backendSrv.datasourceRequest.mock.calls[0][0];
       expect(res.method).toBe('GET');
@@ -813,7 +813,7 @@ describe('PrometheusDatasource', () => {
       // times get rounded up to interval
       const urlExpected = 'proxied/api/v1/query_range?query=test&start=50&end=400&step=50';
       backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
-      ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv);
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
       await ctx.ds.query(query);
       const res = backendSrv.datasourceRequest.mock.calls[0][0];
       expect(res.method).toBe('GET');
@@ -834,7 +834,7 @@ describe('PrometheusDatasource', () => {
       };
       const urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=60&end=420&step=15';
       backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
-      ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv);
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
       await ctx.ds.query(query);
       const res = backendSrv.datasourceRequest.mock.calls[0][0];
       expect(res.method).toBe('GET');
@@ -856,7 +856,7 @@ describe('PrometheusDatasource', () => {
       // times get aligned to interval
       const urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=0&end=400&step=100';
       backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
-      ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv);
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
       await ctx.ds.query(query);
       const res = backendSrv.datasourceRequest.mock.calls[0][0];
       expect(res.method).toBe('GET');
@@ -878,7 +878,7 @@ describe('PrometheusDatasource', () => {
       const start = 0;
       const urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=' + start + '&end=' + end + '&step=100';
       backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
-      ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv);
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
       await ctx.ds.query(query);
       const res = backendSrv.datasourceRequest.mock.calls[0][0];
       expect(res.method).toBe('GET');
@@ -900,7 +900,7 @@ describe('PrometheusDatasource', () => {
       const start = 0;
       const urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=' + start + '&end=' + end + '&step=60';
       backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
-      ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv);
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
       await ctx.ds.query(query);
       const res = backendSrv.datasourceRequest.mock.calls[0][0];
       expect(res.method).toBe('GET');
@@ -943,7 +943,7 @@ describe('PrometheusDatasource', () => {
 
       templateSrv.replace = jest.fn(str => str);
       backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
-      ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv);
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
       await ctx.ds.query(query);
       const res = backendSrv.datasourceRequest.mock.calls[0][0];
       expect(res.method).toBe('GET');
@@ -983,7 +983,7 @@ describe('PrometheusDatasource', () => {
         '&start=60&end=420&step=10';
       backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
       templateSrv.replace = jest.fn(str => str);
-      ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv);
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
       await ctx.ds.query(query);
       const res = backendSrv.datasourceRequest.mock.calls[0][0];
       expect(res.method).toBe('GET');
@@ -1024,7 +1024,7 @@ describe('PrometheusDatasource', () => {
         '&start=0&end=400&step=100';
       backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
       templateSrv.replace = jest.fn(str => str);
-      ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv);
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
       await ctx.ds.query(query);
       const res = backendSrv.datasourceRequest.mock.calls[0][0];
       expect(res.method).toBe('GET');
@@ -1071,7 +1071,7 @@ describe('PrometheusDatasource', () => {
 
       templateSrv.replace = jest.fn(str => str);
       backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
-      ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv);
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
       await ctx.ds.query(query);
       const res = backendSrv.datasourceRequest.mock.calls[0][0];
       expect(res.method).toBe('GET');
@@ -1112,7 +1112,7 @@ describe('PrometheusDatasource', () => {
         '&start=60&end=420&step=15';
 
       backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
-      ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv);
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
       await ctx.ds.query(query);
       const res = backendSrv.datasourceRequest.mock.calls[0][0];
       expect(res.method).toBe('GET');
@@ -1158,7 +1158,7 @@ describe('PrometheusDatasource', () => {
         '&step=60';
       backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
       templateSrv.replace = jest.fn(str => str);
-      ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv);
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
       await ctx.ds.query(query);
       const res = backendSrv.datasourceRequest.mock.calls[0][0];
       expect(res.method).toBe('GET');
@@ -1220,7 +1220,7 @@ describe('PrometheusDatasource for POST', () => {
         },
       };
       backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
-      ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv);
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
       await ctx.ds.query(query).then(data => {
         results = data;
       });
@@ -1245,7 +1245,7 @@ describe('PrometheusDatasource for POST', () => {
     };
 
     it('with proxy access tracing headers should be added', () => {
-      ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv);
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
       ctx.ds._addTracingHeaders(httpOptions, options);
       expect(httpOptions.headers['X-Dashboard-Id']).toBe(1);
       expect(httpOptions.headers['X-Panel-Id']).toBe(2);
@@ -1253,7 +1253,7 @@ describe('PrometheusDatasource for POST', () => {
 
     it('with direct access tracing headers should not be added', () => {
       instanceSettings.url = 'http://127.0.0.1:8000';
-      ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv);
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
       ctx.ds._addTracingHeaders(httpOptions, options);
       expect(httpOptions.headers['X-Dashboard-Id']).toBe(undefined);
       expect(httpOptions.headers['X-Panel-Id']).toBe(undefined);

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

@@ -10,6 +10,7 @@ import {
   ExploreStartPageProps,
   LogLevel,
   TimeRange,
+  DataQueryError,
 } from '@grafana/ui';
 
 import { Emitter, TimeSeries } from 'app/core/core';
@@ -178,14 +179,6 @@ export interface ExploreItemState {
    * Needs to be updated when `datasourceInstance` or `containerWidth` is changed.
    */
   queryIntervals: QueryIntervals;
-  /**
-   * List of query transaction to track query duration and query result.
-   * Graph/Logs/Table results are calculated on the fly from the transaction,
-   * based on the transaction's result types. Transaction also holds the row index
-   * so that results can be dropped and re-computed without running queries again
-   * when query rows are removed.
-   */
-  queryTransactions: QueryTransaction[];
   /**
    * Time range for this Explore. Managed by the time picker and used by all query runs.
    */
@@ -230,6 +223,10 @@ export interface ExploreItemState {
    * True if `datasourceInstance` supports table queries.
    */
   supportsTable: boolean | null;
+
+  graphIsLoading: boolean;
+  logIsLoading: boolean;
+  tableIsLoading: boolean;
   /**
    * Table model that combines all query table results into a single table.
    */
@@ -258,6 +255,9 @@ export interface ExploreItemState {
   urlState: ExploreUrlState;
 
   update: ExploreUpdateState;
+
+  queryErrors: DataQueryError[];
+  latency: number;
 }
 
 export interface ExploreUpdateState {
@@ -332,10 +332,9 @@ export interface QueryTransaction {
   hints?: QueryHint[];
   latency: number;
   options: any;
-  query: DataQuery;
+  queries: DataQuery[];
   result?: any; // Table model / Timeseries[] / Logs
   resultType: ResultType;
-  rowIndex: number;
   scanning?: boolean;
 }