From 409874b35df698dd9058db4bce497b35863343c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 3 Sep 2019 09:55:20 +0200 Subject: [PATCH] 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 --- emails/package.json | 1 - public/app/core/utils/explore.ts | 30 +-- .../dashboard/state/PanelQueryState.ts | 7 +- public/app/features/explore/Explore.tsx | 23 +-- .../app/features/explore/ExploreToolbar.tsx | 5 +- public/app/features/explore/LogsContainer.tsx | 4 +- public/app/features/explore/QueryRow.tsx | 29 +-- .../app/features/explore/TableContainer.tsx | 8 +- .../app/features/explore/state/actionTypes.ts | 68 +----- public/app/features/explore/state/actions.ts | 193 +++--------------- .../features/explore/state/reducers.test.ts | 32 +-- public/app/features/explore/state/reducers.ts | 190 +++++++++-------- .../loki/components/LokiQueryFieldForm.tsx | 8 +- .../prometheus/components/PromQueryField.tsx | 6 +- public/app/types/explore.ts | 9 +- 15 files changed, 179 insertions(+), 434 deletions(-) diff --git a/emails/package.json b/emails/package.json index 0ac005a395e..20e5783b7ca 100644 --- a/emails/package.json +++ b/emails/package.json @@ -12,7 +12,6 @@ "build": "grunt", "start": "grunt watch" }, - "devDependencies": { "grunt": "^0.4.5", "grunt-premailer": "^1.1.10", diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index 9b9164c8f6d..b22ab4f9e65 100644 --- a/public/app/core/utils/explore.ts +++ b/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(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, - options: DataQueryRequest, - observer?: DataStreamObserver -) => { - return from(datasourceInstance.query(options, observer)); -}; - export const stopQueryState = (queryState: PanelQueryState, reason: string) => { if (queryState && queryState.isStarted()) { queryState.cancel(reason); diff --git a/public/app/features/dashboard/state/PanelQueryState.ts b/public/app/features/dashboard/state/PanelQueryState.ts index 77be08179c1..47e8df6ff66 100644 --- a/public/app/features/dashboard/state/PanelQueryState.ts +++ b/public/app/features/dashboard/state/PanelQueryState.ts @@ -95,7 +95,10 @@ export class PanelQueryState { } execute(ds: DataSourceApi, req: DataQueryRequest): Promise { - 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((resolve, reject) => { this.rejector = reject; diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 1e1ab20ae3c..d4f966c152a 100644 --- a/public/app/features/explore/Explore.tsx +++ b/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 { showingStartPage, split, queryKeys, - queryErrors, mode, graphResult, loading, @@ -251,6 +244,7 @@ export class Explore extends React.PureComponent { showingGraph, showingTable, timeZone, + queryResponse, } = this.props; const exploreClass = split ? 'explore explore-split' : 'explore'; @@ -272,7 +266,7 @@ export class Explore extends React.PureComponent { {datasourceInstance && (
- + {({ 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, }; } diff --git a/public/app/features/explore/ExploreToolbar.tsx b/public/app/features/explore/ExploreToolbar.tsx index 32fffb284c7..2f94b9b0c97 100644 --- a/public/app/features/explore/ExploreToolbar.tsx +++ b/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; diff --git a/public/app/features/explore/LogsContainer.tsx b/public/app/features/explore/LogsContainer.tsx index bdf0c14bb57..a8baf98cca4 100644 --- a/public/app/features/explore/LogsContainer.tsx +++ b/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); diff --git a/public/app/features/explore/QueryRow.tsx b/public/app/features/explore/QueryRow.tsx index 12f8f013fe0..c97d2284d19 100644 --- a/public/app/features/explore/QueryRow.tsx +++ b/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 { 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 { } } -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, }; } diff --git a/public/app/features/explore/TableContainer.tsx b/public/app/features/explore/TableContainer.tsx index 937e830a768..f87128796ed 100644 --- a/public/app/features/explore/TableContainer.tsx +++ b/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 }; } diff --git a/public/app/features/explore/state/actionTypes.ts b/public/app/features/explore/state/actionTypes.ts index 5698b673c28..a05bb3bfb55 100644 --- a/public/app/features/explore/state/actionTypes.ts +++ b/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('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('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('explore/QUERY_FAILURE').create(); - export const queryStartAction = actionCreatorFactory('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('explore/QUERY_SUCCESS').create(); +export const queryEndedAction = actionCreatorFactory('explore/QUERY_ENDED').create(); + +export const queryStreamUpdatedAction = actionCreatorFactory( + 'explore/QUERY_STREAM_UPDATED' +).create(); /** * Remove query row of the given index, as well as associated query results. */ export const removeQueryRowAction = actionCreatorFactory('explore/REMOVE_QUERY_ROW').create(); -export const runQueriesAction = actionCreatorFactory('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('explore/HISTORY_UPDATED').create(); -export const resetQueryErrorAction = actionCreatorFactory('explore/RESET_QUERY_ERROR').create(); - export const setUrlReplacedAction = actionCreatorFactory('explore/SET_URL_REPLACED').create(); export const changeRangeAction = actionCreatorFactory('explore/CHANGE_RANGE').create(); diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts index 29c8b16a71b..69123285b52 100644 --- a/public/app/features/explore/state/actions.ts +++ b/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 { 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 { 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( - processQueryResults({ - exploreId, - latency, - datasourceId, - loadingState: LoadingState.Done, - series: isLokiDataSource ? series : legacy, - }) - ); + 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(processErrorResults({ exploreId, response: error, datasourceId })); + dispatch( + queryEndedAction({ + exploreId, + response: { error, legacy: [], series: [], request: transaction.options, state: LoadingState.Error }, + }) + ); }); }; } -export const limitMessageRate = ( - exploreId: ExploreId, - series: DataFrame[] | any[], - datasourceId: string -): ThunkResult => { - 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 => { - 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 => { - 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)) { diff --git a/public/app/features/explore/state/reducers.test.ts b/public/app/features/explore/state/reducers.test.ts index 10ef8dc362b..18005d2def4 100644 --- a/public/app/features/explore/state/reducers.test.ts +++ b/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 = { - 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) => { diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index 2a83b74b94e..b775fd1f5e9 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/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({} 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({} 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({} 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({} 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({} 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({} 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({} 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 +): 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, payload: LocationUpdate, diff --git a/public/app/plugins/datasource/loki/components/LokiQueryFieldForm.tsx b/public/app/plugins/datasource/loki/components/LokiQueryFieldForm.tsx index 95d1b3d4cb3..070332e249b 100644 --- a/public/app/plugins/datasource/loki/components/LokiQueryFieldForm.tsx +++ b/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 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
- {queryResponse && queryResponse.error ? ( -
{queryResponse.error.message}
- ) : null} + {showError ?
{queryResponse.error.message}
: null}
); diff --git a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx index d4e2b74b00d..8f317799ffb 100644 --- a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx +++ b/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 @@ -329,9 +329,7 @@ class PromQueryField extends React.PureComponent - {queryResponse && queryResponse.error ? ( -
{queryResponse.error.message}
- ) : null} + {showError ?
{queryResponse.error.message}
: null} {hint ? (
{hint.label}{' '} diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index 8c7d5036da1..8723fa202bb 100644 --- a/public/app/types/explore.ts +++ b/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 {