浏览代码

Explore: Introduces PanelData to ExploreItemState (#18804)

* WIP: inital POC

* Wip: Moving forward

* Wip

* Refactor: Makes loading indicator work for Prometheus

* Refactor: Reverts prom observable queries because they did not work for multiple targets

* Refactor: Transforms all epics into thunks

* Fix: Fixes scanning

* Fix: Fixes so that Instant and TimeSeries Prom query loads in parallel

* Fix: Fixes negation logic error

* Wip: Introduces PanelData as a carries for query responses

* Refactor: Makes errors work again

* Refactor: Simplifies code somewhat and removes comments

* Tests: Fixes broken tests

* Fix query latency

* Remove unused code
Hugo Häggmark 6 年之前
父节点
当前提交
409874b35d

+ 0 - 1
emails/package.json

@@ -12,7 +12,6 @@
     "build": "grunt",
     "start": "grunt watch"
   },
-
   "devDependencies": {
     "grunt": "^0.4.5",
     "grunt-premailer": "^1.1.10",

+ 2 - 28
public/app/core/utils/explore.ts

@@ -1,6 +1,5 @@
 // Libraries
 import _ from 'lodash';
-import { from } from 'rxjs';
 import { isLive } from '@grafana/ui/src/components/RefreshPicker/RefreshPicker';
 // Services & Utils
 import {
@@ -9,25 +8,16 @@ import {
   TimeRange,
   RawTimeRange,
   TimeZone,
-  IntervalValues,
   TimeFragment,
   LogRowModel,
   LogsModel,
   LogsDedupStrategy,
 } from '@grafana/data';
 import { renderUrl } from 'app/core/utils/url';
-import kbn from 'app/core/utils/kbn';
 import store from 'app/core/store';
 import { getNextRefIdChar } from './query';
 // Types
-import {
-  DataQuery,
-  DataSourceApi,
-  DataQueryError,
-  DataSourceJsonData,
-  DataQueryRequest,
-  DataStreamObserver,
-} from '@grafana/ui';
+import { DataQuery, DataSourceApi, DataQueryError } from '@grafana/ui';
 import {
   ExploreUrlState,
   HistoryItem,
@@ -321,14 +311,6 @@ export function hasNonEmptyQuery<TQuery extends DataQuery = any>(queries: TQuery
   );
 }
 
-export function getIntervals(range: TimeRange, lowLimit: string, resolution: number): IntervalValues {
-  if (!resolution) {
-    return { interval: '1s', intervalMs: 1000 };
-  }
-
-  return kbn.calculateInterval(range, resolution, lowLimit);
-}
-
 /**
  * Update the query history. Side-effect: store history in local storage
  */
@@ -448,7 +430,7 @@ export const getFirstQueryErrorWithoutRefId = (errors: DataQueryError[]) => {
     return null;
   }
 
-  return errors.filter(error => (error.refId ? false : true))[0];
+  return errors.filter(error => (error && error.refId ? false : true))[0];
 };
 
 export const getRefIds = (value: any): string[] => {
@@ -523,14 +505,6 @@ export const convertToWebSocketUrl = (url: string) => {
   return `${backend}${url}`;
 };
 
-export const getQueryResponse = (
-  datasourceInstance: DataSourceApi<DataQuery, DataSourceJsonData>,
-  options: DataQueryRequest<DataQuery>,
-  observer?: DataStreamObserver
-) => {
-  return from(datasourceInstance.query(options, observer));
-};
-
 export const stopQueryState = (queryState: PanelQueryState, reason: string) => {
   if (queryState && queryState.isStarted()) {
     queryState.cancel(reason);

+ 5 - 2
public/app/features/dashboard/state/PanelQueryState.ts

@@ -95,7 +95,10 @@ export class PanelQueryState {
   }
 
   execute(ds: DataSourceApi, req: DataQueryRequest): Promise<PanelData> {
-    this.request = req;
+    this.request = {
+      ...req,
+      startTime: Date.now(),
+    };
     this.datasource = ds;
 
     // Return early if there are no queries to run
@@ -112,7 +115,7 @@ export class PanelQueryState {
       );
     }
 
-    // Set the loading state immediatly
+    // Set the loading state immediately
     this.response.state = LoadingState.Loading;
     this.executor = new Promise<PanelData>((resolve, reject) => {
       this.rejector = reject;

+ 8 - 15
public/app/features/explore/Explore.tsx

@@ -3,20 +3,17 @@ import React, { ComponentClass } from 'react';
 import { hot } from 'react-hot-loader';
 // @ts-ignore
 import { connect } from 'react-redux';
-import _ from 'lodash';
 import { AutoSizer } from 'react-virtualized';
 import memoizeOne from 'memoize-one';
 
 // Services & Utils
 import store from 'app/core/store';
-
 // Components
-import { Alert } from '@grafana/ui';
+import { Alert, DataQuery, ExploreStartPageProps, DataSourceApi, PanelData } from '@grafana/ui';
 import { ErrorBoundary } from './ErrorBoundary';
 import LogsContainer from './LogsContainer';
 import QueryRows from './QueryRows';
 import TableContainer from './TableContainer';
-
 // Actions
 import {
   changeSize,
@@ -29,11 +26,8 @@ import {
   updateTimeRange,
   toggleGraph,
 } from './state/actions';
-
 // Types
-import { RawTimeRange, GraphSeriesXY, LoadingState, TimeZone, AbsoluteTimeRange } from '@grafana/data';
-
-import { DataQuery, ExploreStartPageProps, DataSourceApi, DataQueryError } from '@grafana/ui';
+import { RawTimeRange, GraphSeriesXY, TimeZone, AbsoluteTimeRange } from '@grafana/data';
 import {
   ExploreItemState,
   ExploreUrlState,
@@ -86,7 +80,6 @@ interface ExploreProps {
   initialRange: RawTimeRange;
   mode: ExploreMode;
   initialUI: ExploreUIState;
-  queryErrors: DataQueryError[];
   isLive: boolean;
   updateTimeRange: typeof updateTimeRange;
   graphResult?: GraphSeriesXY[];
@@ -97,6 +90,7 @@ interface ExploreProps {
   timeZone?: TimeZone;
   onHiddenSeriesChanged?: (hiddenSeries: string[]) => void;
   toggleGraph: typeof toggleGraph;
+  queryResponse: PanelData;
 }
 
 /**
@@ -243,7 +237,6 @@ export class Explore extends React.PureComponent<ExploreProps> {
       showingStartPage,
       split,
       queryKeys,
-      queryErrors,
       mode,
       graphResult,
       loading,
@@ -251,6 +244,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
       showingGraph,
       showingTable,
       timeZone,
+      queryResponse,
     } = this.props;
     const exploreClass = split ? 'explore explore-split' : 'explore';
 
@@ -272,7 +266,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
         {datasourceInstance && (
           <div className="explore-container">
             <QueryRows exploreEvents={this.exploreEvents} exploreId={exploreId} queryKeys={queryKeys} />
-            <ErrorContainer queryErrors={queryErrors} />
+            <ErrorContainer queryErrors={[queryResponse.error]} />
             <AutoSizer onResize={this.onResize} disableHeight>
               {({ width }) => {
                 if (width === 0) {
@@ -347,15 +341,15 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) {
     queryKeys,
     urlState,
     update,
-    queryErrors,
     isLive,
     supportedModes,
     mode,
     graphResult,
-    loadingState,
+    loading,
     showingGraph,
     showingTable,
     absoluteRange,
+    queryResponse,
   } = item;
 
   const { datasource, queries, range: urlRange, mode: urlMode, ui } = (urlState || {}) as ExploreUrlState;
@@ -380,7 +374,6 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) {
   }
 
   const initialUI = ui || DEFAULT_UI_STATE;
-  const loading = loadingState === LoadingState.Loading || loadingState === LoadingState.Streaming;
 
   return {
     StartPage,
@@ -398,13 +391,13 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) {
     initialRange,
     mode: newMode,
     initialUI,
-    queryErrors,
     isLive,
     graphResult,
     loading,
     showingGraph,
     showingTable,
     absoluteRange,
+    queryResponse,
   };
 }
 

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

@@ -5,7 +5,7 @@ import memoizeOne from 'memoize-one';
 
 import { ExploreId, ExploreMode } from 'app/types/explore';
 import { DataSourceSelectItem, ToggleButtonGroup, ToggleButton } from '@grafana/ui';
-import { RawTimeRange, TimeZone, TimeRange, LoadingState, SelectableValue } from '@grafana/data';
+import { RawTimeRange, TimeZone, TimeRange, SelectableValue } from '@grafana/data';
 import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
 import { StoreState } from 'app/types/store';
 import {
@@ -281,7 +281,7 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps
     exploreDatasources,
     range,
     refreshInterval,
-    loadingState,
+    loading,
     supportedModes,
     mode,
     isLive,
@@ -289,7 +289,6 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps
   const selectedDatasource = datasourceInstance
     ? exploreDatasources.find(datasource => datasource.name === datasourceInstance.name)
     : undefined;
-  const loading = loadingState === LoadingState.Loading || loadingState === LoadingState.Streaming;
   const hasLiveOption =
     datasourceInstance && datasourceInstance.meta && datasourceInstance.meta.streaming ? true : false;
 

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

@@ -11,7 +11,6 @@ import {
   LogsModel,
   LogRowModel,
   LogsDedupStrategy,
-  LoadingState,
   TimeRange,
 } from '@grafana/data';
 
@@ -143,14 +142,13 @@ function mapStateToProps(state: StoreState, { exploreId }: { exploreId: string }
   const {
     logsHighlighterExpressions,
     logsResult,
-    loadingState,
+    loading,
     scanning,
     datasourceInstance,
     isLive,
     range,
     absoluteRange,
   } = item;
-  const loading = loadingState === LoadingState.Loading || loadingState === LoadingState.Streaming;
   const { dedupStrategy } = exploreItemUIStateSelector(item);
   const dedupedResult = deduplicatedLogsSelector(item);
   const timeZone = getTimeZone(state.user);

+ 4 - 25
public/app/features/explore/QueryRow.tsx

@@ -2,20 +2,16 @@
 import React, { PureComponent } from 'react';
 import _ from 'lodash';
 import { hot } from 'react-hot-loader';
-import memoizeOne from 'memoize-one';
 // @ts-ignore
 import { connect } from 'react-redux';
-
 // Components
 import QueryEditor from './QueryEditor';
-
 // Actions
 import { changeQuery, modifyQueries, runQueries, addQueryRow } from './state/actions';
-
 // Types
 import { StoreState } from 'app/types';
-import { TimeRange, AbsoluteTimeRange, toDataFrame, guessFieldTypes, GraphSeriesXY, LoadingState } from '@grafana/data';
-import { DataQuery, DataSourceApi, QueryFixAction, DataSourceStatus, PanelData, DataQueryError } from '@grafana/ui';
+import { TimeRange, AbsoluteTimeRange } from '@grafana/data';
+import { DataQuery, DataSourceApi, QueryFixAction, DataSourceStatus, PanelData } from '@grafana/ui';
 import { HistoryItem, ExploreItemState, ExploreId, ExploreMode } from 'app/types/explore';
 import { Emitter } from 'app/core/utils/emitter';
 import { highlightLogsExpressionAction, removeQueryRowAction } from './state/actionTypes';
@@ -44,7 +40,6 @@ interface QueryRowProps extends PropsFromParent {
   runQueries: typeof runQueries;
   queryResponse: PanelData;
   latency: number;
-  queryErrors: DataQueryError[];
   mode: ExploreMode;
 }
 
@@ -122,11 +117,11 @@ export class QueryRow extends PureComponent<QueryRowProps, QueryRowState> {
       datasourceStatus,
       queryResponse,
       latency,
-      queryErrors,
       mode,
     } = this.props;
     const canToggleEditorModes =
       mode === ExploreMode.Metrics && _.has(datasourceInstance, 'components.QueryCtrl.prototype.toggleEditorMode');
+    const queryErrors = queryResponse.error && queryResponse.error.refId === query.refId ? [queryResponse.error] : [];
     let QueryField;
 
     if (mode === ExploreMode.Metrics && datasourceInstance.components.ExploreMetricsQueryField) {
@@ -199,17 +194,6 @@ export class QueryRow extends PureComponent<QueryRowProps, QueryRowState> {
   }
 }
 
-const makeQueryResponseMemoized = memoizeOne(
-  (graphResult: GraphSeriesXY[], error: DataQueryError, loadingState: LoadingState): PanelData => {
-    const series = graphResult ? graphResult.map(serie => guessFieldTypes(toDataFrame(serie))) : []; // TODO: use DataFrame
-    return {
-      series,
-      state: loadingState,
-      error,
-    };
-  }
-);
-
 function mapStateToProps(state: StoreState, { exploreId, index }: QueryRowProps) {
   const explore = state.explore;
   const item: ExploreItemState = explore[exploreId];
@@ -220,16 +204,12 @@ function mapStateToProps(state: StoreState, { exploreId, index }: QueryRowProps)
     range,
     absoluteRange,
     datasourceError,
-    graphResult,
-    loadingState,
     latency,
-    queryErrors,
     mode,
+    queryResponse,
   } = item;
   const query = queries[index];
   const datasourceStatus = datasourceError ? DataSourceStatus.Disconnected : DataSourceStatus.Connected;
-  const error = queryErrors.filter(queryError => queryError.refId === query.refId)[0];
-  const queryResponse = makeQueryResponseMemoized(graphResult, error, loadingState);
 
   return {
     datasourceInstance,
@@ -240,7 +220,6 @@ function mapStateToProps(state: StoreState, { exploreId, index }: QueryRowProps)
     datasourceStatus,
     queryResponse,
     latency,
-    queryErrors,
     mode,
   };
 }

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

@@ -1,7 +1,6 @@
 import React, { PureComponent } from 'react';
 import { hot } from 'react-hot-loader';
 import { connect } from 'react-redux';
-import { LoadingState } from '@grafana/data';
 import { Collapse } from '@grafana/ui';
 
 import { ExploreId, ExploreItemState } from 'app/types/explore';
@@ -40,11 +39,8 @@ function mapStateToProps(state: StoreState, { exploreId }: { exploreId: string }
   const explore = state.explore;
   // @ts-ignore
   const item: ExploreItemState = explore[exploreId];
-  const { loadingState, showingTable, tableResult } = item;
-  const loading =
-    tableResult && tableResult.rows.length > 0
-      ? false
-      : loadingState === LoadingState.Loading || loadingState === LoadingState.Streaming;
+  const { loading: loadingInState, showingTable, tableResult } = item;
+  const loading = tableResult && tableResult.rows.length > 0 ? false : loadingInState;
   return { loading, showingTable, tableResult };
 }
 

+ 9 - 59
public/app/features/explore/state/actionTypes.ts

@@ -1,11 +1,10 @@
 // Types
 import { Emitter } from 'app/core/core';
-import { DataQuery, DataSourceSelectItem, DataSourceApi, QueryFixAction, DataQueryError } from '@grafana/ui';
+import { DataQuery, DataSourceSelectItem, DataSourceApi, QueryFixAction, PanelData } from '@grafana/ui';
 
-import { LogLevel, TimeRange, LogsModel, LoadingState, AbsoluteTimeRange, GraphSeriesXY } from '@grafana/data';
+import { LogLevel, TimeRange, LoadingState, AbsoluteTimeRange } from '@grafana/data';
 import { ExploreId, ExploreItemState, HistoryItem, ExploreUIState, ExploreMode } from 'app/types/explore';
 import { actionCreatorFactory, noPayloadActionCreatorFactory, ActionOf } from 'app/core/redux/actionCreatorFactory';
-import TableModel from 'app/core/table_model';
 
 /**  Higher order actions
  *
@@ -62,10 +61,6 @@ export interface ClearQueriesPayload {
   exploreId: ExploreId;
 }
 
-export interface ClearRefreshIntervalPayload {
-  exploreId: ExploreId;
-}
-
 export interface HighlightLogsExpressionPayload {
   exploreId: ExploreId;
   expressions: string[];
@@ -81,11 +76,6 @@ export interface InitializeExplorePayload {
   ui: ExploreUIState;
 }
 
-export interface LoadDatasourceFailurePayload {
-  exploreId: ExploreId;
-  error: string;
-}
-
 export interface LoadDatasourceMissingPayload {
   exploreId: ExploreId;
 }
@@ -120,22 +110,13 @@ export interface ModifyQueriesPayload {
   modifier: (query: DataQuery, modification: QueryFixAction) => DataQuery;
 }
 
-export interface QueryFailurePayload {
-  exploreId: ExploreId;
-  response: DataQueryError;
-}
-
 export interface QueryStartPayload {
   exploreId: ExploreId;
 }
 
-export interface QuerySuccessPayload {
+export interface QueryEndedPayload {
   exploreId: ExploreId;
-  latency: number;
-  loadingState: LoadingState;
-  graphResult: GraphSeriesXY[];
-  tableResult: TableModel;
-  logsResult: LogsModel;
+  response: PanelData;
 }
 
 export interface HistoryUpdatedPayload {
@@ -201,15 +182,6 @@ export interface LoadExploreDataSourcesPayload {
   exploreDatasources: DataSourceSelectItem[];
 }
 
-export interface RunQueriesPayload {
-  exploreId: ExploreId;
-}
-
-export interface ResetQueryErrorPayload {
-  exploreId: ExploreId;
-  refIds: string[];
-}
-
 export interface SetUrlReplacedPayload {
   exploreId: ExploreId;
 }
@@ -230,11 +202,6 @@ export interface ChangeLoadingStatePayload {
  */
 export const addQueryRowAction = actionCreatorFactory<AddQueryRowPayload>('explore/ADD_QUERY_ROW').create();
 
-/**
- * Loads a new datasource identified by the given name.
- */
-export const changeDatasourceAction = noPayloadActionCreatorFactory('explore/CHANGE_DATASOURCE').create();
-
 /**
  * Change the mode of Explore.
  */
@@ -309,34 +276,19 @@ export const loadDatasourceReadyAction = actionCreatorFactory<LoadDatasourceRead
  */
 export const modifyQueriesAction = actionCreatorFactory<ModifyQueriesPayload>('explore/MODIFY_QUERIES').create();
 
-/**
- * Mark a query transaction as failed with an error extracted from the query response.
- * The transaction will be marked as `done`.
- */
-export const queryFailureAction = actionCreatorFactory<QueryFailurePayload>('explore/QUERY_FAILURE').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.
- * 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 latency Duration between request and response
- * @param queries Queries from all query rows
- * @param datasourceId Origin datasource instance, used to discard results if current datasource is different
- */
-export const querySuccessAction = actionCreatorFactory<QuerySuccessPayload>('explore/QUERY_SUCCESS').create();
+export const queryEndedAction = actionCreatorFactory<QueryEndedPayload>('explore/QUERY_ENDED').create();
+
+export const queryStreamUpdatedAction = actionCreatorFactory<QueryEndedPayload>(
+  'explore/QUERY_STREAM_UPDATED'
+).create();
 
 /**
  * Remove query row of the given index, as well as associated query results.
  */
 export const removeQueryRowAction = actionCreatorFactory<RemoveQueryRowPayload>('explore/REMOVE_QUERY_ROW').create();
 
-export const runQueriesAction = actionCreatorFactory<RunQueriesPayload>('explore/RUN_QUERIES').create();
-
 /**
  * Start a scan for more results using the given scanner.
  * @param exploreId Explore area
@@ -411,8 +363,6 @@ export const loadExploreDatasources = actionCreatorFactory<LoadExploreDataSource
 
 export const historyUpdatedAction = actionCreatorFactory<HistoryUpdatedPayload>('explore/HISTORY_UPDATED').create();
 
-export const resetQueryErrorAction = actionCreatorFactory<ResetQueryErrorPayload>('explore/RESET_QUERY_ERROR').create();
-
 export const setUrlReplacedAction = actionCreatorFactory<SetUrlReplacedPayload>('explore/SET_URL_REPLACED').create();
 
 export const changeRangeAction = actionCreatorFactory<ChangeRangePayload>('explore/CHANGE_RANGE').create();

+ 30 - 161
public/app/features/explore/state/actions.ts

@@ -13,30 +13,20 @@ import {
   lastUsedDatasourceKeyForOrgId,
   hasNonEmptyQuery,
   buildQueryTransaction,
-  updateHistory,
-  getRefIds,
-  instanceOfDataQueryError,
   clearQueryKeys,
   serializeStateToUrlParam,
   stopQueryState,
+  updateHistory,
 } from 'app/core/utils/explore';
 // Types
 import { ThunkResult, ExploreUrlState } from 'app/types';
-import {
-  DataSourceApi,
-  DataQuery,
-  DataSourceSelectItem,
-  QueryFixAction,
-  PanelData,
-  DataQueryResponseData,
-} from '@grafana/ui';
+import { DataSourceApi, DataQuery, DataSourceSelectItem, QueryFixAction, PanelData } from '@grafana/ui';
 
 import {
   RawTimeRange,
   LogsDedupStrategy,
   AbsoluteTimeRange,
   LoadingState,
-  DataFrame,
   TimeRange,
   isDateTime,
   dateTimeForTimeZone,
@@ -73,22 +63,18 @@ import {
   loadExploreDatasources,
   changeModeAction,
   scanStopAction,
-  changeLoadingStateAction,
-  historyUpdatedAction,
   queryStartAction,
-  resetQueryErrorAction,
-  querySuccessAction,
-  queryFailureAction,
   setUrlReplacedAction,
   changeRangeAction,
+  historyUpdatedAction,
+  queryEndedAction,
+  queryStreamUpdatedAction,
 } from './actionTypes';
 import { ActionOf, ActionCreator } from 'app/core/redux/actionCreatorFactory';
 import { getTimeZone } from 'app/features/profile/state/selectors';
 import { offOption } from '@grafana/ui/src/components/RefreshPicker/RefreshPicker';
 import { getShiftedTimeRange } from 'app/core/utils/timePicker';
-import { ResultProcessor } from '../utils/ResultProcessor';
 import _ from 'lodash';
-import { toDataQueryError } from '../../dashboard/state/PanelQueryState';
 import { updateLocation } from '../../../core/actions';
 import { getTimeSrv } from '../../dashboard/services/TimeSrv';
 
@@ -466,36 +452,15 @@ export function runQueries(exploreId: ExploreId): ThunkResult<void> {
     stopQueryState(queryState, 'New request issued');
 
     queryState.sendFrames = true;
-    queryState.sendLegacy = true; // temporary hack until we switch to PanelData
+    queryState.sendLegacy = true;
 
     const queryOptions = { interval, maxDataPoints: containerWidth, live };
     const datasourceId = datasourceInstance.meta.id;
-    const now = Date.now();
     const transaction = buildQueryTransaction(queries, queryOptions, range, queryIntervals, scanning);
 
-    // temporary hack until we switch to PanelData, Loki already converts to DataFrame so using legacy will destroy the format
-    const isLokiDataSource = datasourceInstance.meta.name === 'Loki';
-
     queryState.onStreamingDataUpdated = () => {
-      const data = queryState.validateStreamsAndGetPanelData();
-      const { state, error, legacy, series } = data;
-      if (!data && !error && !legacy && !series) {
-        return;
-      }
-
-      if (state === LoadingState.Error) {
-        dispatch(processErrorResults({ exploreId, response: error, datasourceId }));
-        return;
-      }
-
-      if (state === LoadingState.Streaming) {
-        dispatch(limitMessageRate(exploreId, isLokiDataSource ? series : legacy, datasourceId));
-        return;
-      }
-
-      if (state === LoadingState.Done) {
-        dispatch(changeLoadingStateAction({ exploreId, loadingState: state }));
-      }
+      const response = queryState.validateStreamsAndGetPanelData();
+      dispatch(queryStreamUpdatedAction({ exploreId, response }));
     };
 
     dispatch(queryStartAction({ exploreId }));
@@ -503,134 +468,38 @@ export function runQueries(exploreId: ExploreId): ThunkResult<void> {
     queryState
       .execute(datasourceInstance, transaction.options)
       .then((response: PanelData) => {
-        const { legacy, error, series } = response;
-        if (error) {
-          dispatch(processErrorResults({ exploreId, response: error, datasourceId }));
-          return;
+        if (!response.error) {
+          // Side-effect: Saving history in localstorage
+          const nextHistory = updateHistory(history, datasourceId, queries);
+          dispatch(historyUpdatedAction({ exploreId, history: nextHistory }));
         }
 
-        const latency = Date.now() - now;
-        // Side-effect: Saving history in localstorage
-        const nextHistory = updateHistory(history, datasourceId, queries);
-        dispatch(historyUpdatedAction({ exploreId, history: nextHistory }));
+        dispatch(queryEndedAction({ exploreId, response }));
+        dispatch(stateSave());
+
+        // Keep scanning for results if this was the last scanning transaction
+        if (getState().explore[exploreId].scanning) {
+          if (_.size(response.series) === 0) {
+            const range = getShiftedTimeRange(-1, getState().explore[exploreId].range);
+            dispatch(updateTime({ exploreId, absoluteRange: range }));
+            dispatch(runQueries(exploreId));
+          } else {
+            // We can stop scanning if we have a result
+            dispatch(scanStopAction({ exploreId }));
+          }
+        }
+      })
+      .catch(error => {
         dispatch(
-          processQueryResults({
+          queryEndedAction({
             exploreId,
-            latency,
-            datasourceId,
-            loadingState: LoadingState.Done,
-            series: isLokiDataSource ? series : legacy,
+            response: { error, legacy: [], series: [], request: transaction.options, state: LoadingState.Error },
           })
         );
-        dispatch(stateSave());
-      })
-      .catch(error => {
-        dispatch(processErrorResults({ exploreId, response: error, datasourceId }));
       });
   };
 }
 
-export const limitMessageRate = (
-  exploreId: ExploreId,
-  series: DataFrame[] | any[],
-  datasourceId: string
-): ThunkResult<void> => {
-  return (dispatch, getState) => {
-    dispatch(
-      processQueryResults({
-        exploreId,
-        latency: 0,
-        datasourceId,
-        loadingState: LoadingState.Streaming,
-        series,
-      })
-    );
-  };
-};
-
-export const processQueryResults = (config: {
-  exploreId: ExploreId;
-  latency: number;
-  datasourceId: string;
-  loadingState: LoadingState;
-  series?: DataQueryResponseData[];
-}): ThunkResult<void> => {
-  return (dispatch, getState) => {
-    const { exploreId, datasourceId, latency, loadingState, series } = config;
-    const { datasourceInstance, scanning, eventBridge } = getState().explore[exploreId];
-
-    // If datasource already changed, results do not matter
-    if (datasourceInstance.meta.id !== datasourceId) {
-      return;
-    }
-
-    const result = series || [];
-    const replacePreviousResults = loadingState === LoadingState.Done && series ? true : false;
-    const resultProcessor = new ResultProcessor(getState().explore[exploreId], replacePreviousResults, result);
-    const graphResult = resultProcessor.getGraphResult();
-    const tableResult = resultProcessor.getTableResult();
-    const logsResult = resultProcessor.getLogsResult();
-    const refIds = getRefIds(result);
-
-    // For Angular editors
-    eventBridge.emit('data-received', resultProcessor.getRawData());
-
-    // Clears any previous errors that now have a successful query, important so Angular editors are updated correctly
-    dispatch(resetQueryErrorAction({ exploreId, refIds }));
-
-    dispatch(
-      querySuccessAction({
-        exploreId,
-        latency,
-        loadingState,
-        graphResult,
-        tableResult,
-        logsResult,
-      })
-    );
-
-    // Keep scanning for results if this was the last scanning transaction
-    if (scanning) {
-      if (_.size(result) === 0) {
-        const range = getShiftedTimeRange(-1, getState().explore[exploreId].range);
-        dispatch(updateTime({ exploreId, absoluteRange: range }));
-        dispatch(runQueries(exploreId));
-      } else {
-        // We can stop scanning if we have a result
-        dispatch(scanStopAction({ exploreId }));
-      }
-    }
-  };
-};
-
-export const processErrorResults = (config: {
-  exploreId: ExploreId;
-  response: any;
-  datasourceId: string;
-}): ThunkResult<void> => {
-  return (dispatch, getState) => {
-    const { exploreId, datasourceId } = config;
-    let { response } = config;
-    const { datasourceInstance, eventBridge } = getState().explore[exploreId];
-
-    if (datasourceInstance.meta.id !== datasourceId || response.cancelled) {
-      // Navigated away, queries did not matter
-      return;
-    }
-
-    // For Angular editors
-    eventBridge.emit('data-error', response);
-
-    console.error(response); // To help finding problems with query syntax
-
-    if (!instanceOfDataQueryError(response)) {
-      response = toDataQueryError(response);
-    }
-
-    dispatch(queryFailureAction({ exploreId, response }));
-  };
-};
-
 const toRawTimeRange = (range: TimeRange): RawTimeRange => {
   let from = range.raw.from;
   if (isDateTime(from)) {

+ 4 - 28
public/app/features/explore/state/reducers.test.ts

@@ -4,6 +4,7 @@ import {
   exploreReducer,
   makeInitialUpdateState,
   initialExploreState,
+  createEmptyQueryResponse,
 } from './reducers';
 import { ExploreId, ExploreItemState, ExploreUrlState, ExploreState, ExploreMode } from 'app/types/explore';
 import { reducerTester } from 'test/core/redux/reducerTester';
@@ -17,7 +18,6 @@ import {
   splitCloseAction,
   changeModeAction,
   scanStopAction,
-  runQueriesAction,
 } from './actionTypes';
 import { Reducer } from 'redux';
 import { ActionOf } from 'app/core/redux/actionCreatorFactory';
@@ -25,7 +25,7 @@ import { updateLocation } from 'app/core/actions/location';
 import { serializeStateToUrlParam } from 'app/core/utils/explore';
 import TableModel from 'app/core/table_model';
 import { DataSourceApi, DataQuery } from '@grafana/ui';
-import { LogsModel, LogsDedupStrategy, LoadingState } from '@grafana/data';
+import { LogsModel, LogsDedupStrategy } from '@grafana/data';
 import { PanelQueryState } from '../../dashboard/state/PanelQueryState';
 
 describe('Explore item reducer', () => {
@@ -162,9 +162,9 @@ describe('Explore item reducer', () => {
             tableResult: null,
             supportedModes: [ExploreMode.Metrics, ExploreMode.Logs],
             mode: ExploreMode.Metrics,
-            loadingState: LoadingState.NotStarted,
             latency: 0,
-            queryErrors: [],
+            loading: false,
+            queryResponse: createEmptyQueryResponse(),
           };
 
           reducerTester()
@@ -175,30 +175,6 @@ describe('Explore item reducer', () => {
       });
     });
   });
-
-  describe('run queries', () => {
-    describe('when runQueriesAction is dispatched', () => {
-      it('then it should set correct state', () => {
-        const initalState: Partial<ExploreItemState> = {
-          showingStartPage: true,
-          range: null,
-        };
-        const expectedState: any = {
-          queryIntervals: {
-            interval: '1s',
-            intervalMs: 1000,
-          },
-          showingStartPage: false,
-          range: null,
-        };
-
-        reducerTester()
-          .givenReducer(itemReducer, initalState)
-          .whenActionIsDispatched(runQueriesAction({ exploreId: ExploreId.left }))
-          .thenStateShouldEqual(expectedState);
-      });
-    });
-  });
 });
 
 export const setup = (urlStateOverrides?: any) => {

+ 103 - 87
public/app/features/explore/state/reducers.ts

@@ -1,6 +1,5 @@
 import _ from 'lodash';
 import {
-  getIntervals,
   ensureQueries,
   getQueryKeys,
   parseUrlState,
@@ -9,10 +8,11 @@ import {
   sortLogsResult,
   stopQueryState,
   refreshIntervalToSortOrder,
+  instanceOfDataQueryError,
 } from 'app/core/utils/explore';
 import { ExploreItemState, ExploreState, ExploreId, ExploreUpdateState, ExploreMode } from 'app/types/explore';
 import { LoadingState } from '@grafana/data';
-import { DataQuery } from '@grafana/ui';
+import { DataQuery, PanelData } from '@grafana/ui';
 import {
   HigherOrderAction,
   ActionTypes,
@@ -24,13 +24,9 @@ import {
   loadExploreDatasources,
   historyUpdatedAction,
   changeModeAction,
-  queryFailureAction,
   setUrlReplacedAction,
-  querySuccessAction,
   scanStopAction,
-  resetQueryErrorAction,
   queryStartAction,
-  runQueriesAction,
   changeRangeAction,
   addQueryRowAction,
   changeQueryAction,
@@ -53,13 +49,17 @@ import {
   toggleLogLevelAction,
   changeLoadingStateAction,
   resetExploreAction,
+  queryEndedAction,
+  queryStreamUpdatedAction,
+  QueryEndedPayload,
 } from './actionTypes';
-import { reducerFactory } from 'app/core/redux';
+import { reducerFactory, ActionOf } from 'app/core/redux';
 import { updateLocation } from 'app/core/actions/location';
 import { LocationUpdate } from '@grafana/runtime';
 import TableModel from 'app/core/table_model';
 import { isLive } from '@grafana/ui/src/components/RefreshPicker/RefreshPicker';
-import { PanelQueryState } from '../../dashboard/state/PanelQueryState';
+import { PanelQueryState, toDataQueryError } from '../../dashboard/state/PanelQueryState';
+import { ResultProcessor } from '../utils/ResultProcessor';
 
 export const DEFAULT_RANGE = {
   from: 'now-6h',
@@ -106,17 +106,25 @@ export const makeExploreItemState = (): ExploreItemState => ({
   scanRange: null,
   showingGraph: true,
   showingTable: true,
-  loadingState: LoadingState.NotStarted,
+  loading: false,
   queryKeys: [],
   urlState: null,
   update: makeInitialUpdateState(),
-  queryErrors: [],
   latency: 0,
   supportedModes: [],
   mode: null,
   isLive: false,
   urlReplaced: false,
   queryState: new PanelQueryState(),
+  queryResponse: createEmptyQueryResponse(),
+});
+
+export const createEmptyQueryResponse = (): PanelData => ({
+  state: LoadingState.NotStarted,
+  request: null,
+  series: [],
+  legacy: null,
+  error: null,
 });
 
 /**
@@ -196,8 +204,12 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
       return {
         ...state,
         refreshInterval,
-        loadingState: live ? LoadingState.Streaming : LoadingState.NotStarted,
+        queryResponse: {
+          ...state.queryResponse,
+          state: live ? LoadingState.Streaming : LoadingState.NotStarted,
+        },
         isLive: live,
+        loading: live,
         logsResult,
       };
     },
@@ -215,6 +227,8 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
         logsResult: null,
         showingStartPage: Boolean(state.StartPage),
         queryKeys: getQueryKeys(queries, state.datasourceInstance),
+        queryResponse: createEmptyQueryResponse(),
+        loading: false,
       };
     },
   })
@@ -273,12 +287,12 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
       return {
         ...state,
         datasourceInstance,
-        queryErrors: [],
         graphResult: null,
         tableResult: null,
         logsResult: null,
         latency: 0,
-        loadingState: LoadingState.NotStarted,
+        queryResponse: createEmptyQueryResponse(),
+        loading: false,
         StartPage,
         showingStartPage: Boolean(StartPage),
         queryKeys: [],
@@ -352,49 +366,18 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
       };
     },
   })
-  .addMapper({
-    filter: queryFailureAction,
-    mapper: (state, action): ExploreItemState => {
-      const { response } = action.payload;
-      const queryErrors = state.queryErrors.concat(response);
-
-      return {
-        ...state,
-        graphResult: null,
-        tableResult: null,
-        logsResult: null,
-        latency: 0,
-        queryErrors,
-        loadingState: LoadingState.Error,
-        update: makeInitialUpdateState(),
-      };
-    },
-  })
   .addMapper({
     filter: queryStartAction,
     mapper: (state): ExploreItemState => {
       return {
         ...state,
-        queryErrors: [],
         latency: 0,
-        loadingState: LoadingState.Loading,
-        update: makeInitialUpdateState(),
-      };
-    },
-  })
-  .addMapper({
-    filter: querySuccessAction,
-    mapper: (state, action): ExploreItemState => {
-      const { latency, loadingState, graphResult, tableResult, logsResult } = action.payload;
-
-      return {
-        ...state,
-        loadingState,
-        graphResult,
-        tableResult,
-        logsResult,
-        latency,
-        showingStartPage: false,
+        queryResponse: {
+          ...state.queryResponse,
+          state: LoadingState.Loading,
+          error: null,
+        },
+        loading: true,
         update: makeInitialUpdateState(),
       };
     },
@@ -526,24 +509,6 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
       };
     },
   })
-  .addMapper({
-    filter: runQueriesAction,
-    mapper: (state): ExploreItemState => {
-      const { range } = state;
-      const { datasourceInstance, containerWidth } = state;
-      let interval = '1s';
-      if (datasourceInstance && datasourceInstance.interval) {
-        interval = datasourceInstance.interval;
-      }
-      const queryIntervals = getIntervals(range, interval, containerWidth);
-      return {
-        ...state,
-        range,
-        queryIntervals,
-        showingStartPage: false,
-      };
-    },
-  })
   .addMapper({
     filter: historyUpdatedAction,
     mapper: (state, action): ExploreItemState => {
@@ -553,24 +518,6 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
       };
     },
   })
-  .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,
-      };
-    },
-  })
   .addMapper({
     filter: setUrlReplacedAction,
     mapper: (state): ExploreItemState => {
@@ -597,12 +544,81 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
       const { loadingState } = action.payload;
       return {
         ...state,
-        loadingState,
+        queryResponse: {
+          ...state.queryResponse,
+          state: loadingState,
+        },
+        loading: loadingState === LoadingState.Loading || loadingState === LoadingState.Streaming,
       };
     },
   })
+  .addMapper({
+    //queryStreamUpdatedAction
+    filter: queryEndedAction,
+    mapper: (state, action): ExploreItemState => {
+      return processQueryResponse(state, action);
+    },
+  })
+  .addMapper({
+    filter: queryStreamUpdatedAction,
+    mapper: (state, action): ExploreItemState => {
+      return processQueryResponse(state, action);
+    },
+  })
   .create();
 
+export const processQueryResponse = (
+  state: ExploreItemState,
+  action: ActionOf<QueryEndedPayload>
+): ExploreItemState => {
+  const { response } = action.payload;
+  const { request, state: loadingState, series, legacy, error } = response;
+  const replacePreviousResults = action.type === queryEndedAction.type;
+
+  if (error) {
+    // For Angular editors
+    state.eventBridge.emit('data-error', error);
+
+    console.error(error); // To help finding problems with query syntax
+
+    if (!instanceOfDataQueryError(error)) {
+      response.error = toDataQueryError(error);
+    }
+
+    return {
+      ...state,
+      loading: false,
+      queryResponse: response,
+      graphResult: null,
+      tableResult: null,
+      logsResult: null,
+      showingStartPage: false,
+      update: makeInitialUpdateState(),
+    };
+  }
+
+  const latency = request.endTime - request.startTime;
+
+  // temporary hack until we switch to PanelData, Loki already converts to DataFrame so using legacy will destroy the format
+  const isLokiDataSource = state.datasourceInstance.meta.name === 'Loki';
+  const processor = new ResultProcessor(state, replacePreviousResults, isLokiDataSource ? series : legacy);
+
+  // For Angular editors
+  state.eventBridge.emit('data-received', processor.getRawData());
+
+  return {
+    ...state,
+    latency,
+    queryResponse: response,
+    graphResult: processor.getGraphResult(),
+    tableResult: processor.getTableResult(),
+    logsResult: processor.getLogsResult(),
+    loading: loadingState === LoadingState.Loading || loadingState === LoadingState.Streaming,
+    showingStartPage: false,
+    update: makeInitialUpdateState(),
+  };
+};
+
 export const updateChildRefreshState = (
   state: Readonly<ExploreItemState>,
   payload: LocationUpdate,

+ 2 - 6
public/app/plugins/datasource/loki/components/LokiQueryFieldForm.tsx

@@ -4,14 +4,11 @@ import React from 'react';
 import Cascader from 'rc-cascader';
 // @ts-ignore
 import PluginPrism from 'slate-prism';
-
 // Components
 import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField';
-
 // Utils & Services
 // dom also includes Element polyfills
 import BracesPlugin from 'app/features/explore/slate-plugins/braces';
-
 // Types
 import { LokiQuery } from '../types';
 import { TypeaheadOutput, HistoryItem } from 'app/types/explore';
@@ -158,6 +155,7 @@ export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormPr
     const hasLogLabels = logLabelOptions && logLabelOptions.length > 0;
     const chooserText = getChooserText(syntaxLoaded, hasLogLabels, datasourceStatus);
     const buttonDisabled = !syntaxLoaded || datasourceStatus === DataSourceStatus.Disconnected;
+    const showError = queryResponse && queryResponse.error && queryResponse.error.refId === query.refId;
 
     return (
       <>
@@ -194,9 +192,7 @@ export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormPr
           </div>
         </div>
         <div>
-          {queryResponse && queryResponse.error ? (
-            <div className="prom-query-field-info text-error">{queryResponse.error.message}</div>
-          ) : null}
+          {showError ? <div className="prom-query-field-info text-error">{queryResponse.error.message}</div> : null}
         </div>
       </>
     );

+ 2 - 4
public/app/plugins/datasource/prometheus/components/PromQueryField.tsx

@@ -8,7 +8,6 @@ import PluginPrism from 'slate-prism';
 import Prism from 'prismjs';
 
 import { TypeaheadOutput, HistoryItem } from 'app/types/explore';
-
 // dom also includes Element polyfills
 import BracesPlugin from 'app/features/explore/slate-plugins/braces';
 import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField';
@@ -303,6 +302,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
     const cleanText = this.languageProvider ? this.languageProvider.cleanText : undefined;
     const chooserText = getChooserText(syntaxLoaded, datasourceStatus);
     const buttonDisabled = !syntaxLoaded || datasourceStatus === DataSourceStatus.Disconnected;
+    const showError = queryResponse && queryResponse.error && queryResponse.error.refId === query.refId;
 
     return (
       <>
@@ -329,9 +329,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
             />
           </div>
         </div>
-        {queryResponse && queryResponse.error ? (
-          <div className="prom-query-field-info text-error">{queryResponse.error.message}</div>
-        ) : null}
+        {showError ? <div className="prom-query-field-info text-error">{queryResponse.error.message}</div> : null}
         {hint ? (
           <div className="prom-query-field-info text-warning">
             {hint.label}{' '}

+ 4 - 5
public/app/types/explore.ts

@@ -5,7 +5,7 @@ import {
   DataSourceApi,
   QueryHint,
   ExploreStartPageProps,
-  DataQueryError,
+  PanelData,
 } from '@grafana/ui';
 
 import {
@@ -14,7 +14,6 @@ import {
   TimeRange,
   LogsModel,
   LogsDedupStrategy,
-  LoadingState,
   AbsoluteTimeRange,
   GraphSeriesXY,
 } from '@grafana/data';
@@ -218,7 +217,7 @@ export interface ExploreItemState {
    */
   showingTable: boolean;
 
-  loadingState: LoadingState;
+  loading: boolean;
   /**
    * Table model that combines all query table results into a single table.
    */
@@ -248,8 +247,6 @@ export interface ExploreItemState {
 
   update: ExploreUpdateState;
 
-  queryErrors: DataQueryError[];
-
   latency: number;
   supportedModes: ExploreMode[];
   mode: ExploreMode;
@@ -258,6 +255,8 @@ export interface ExploreItemState {
   urlReplaced: boolean;
 
   queryState: PanelQueryState;
+
+  queryResponse: PanelData;
 }
 
 export interface ExploreUpdateState {