Commit 409874b3 by Hugo Häggmark Committed by Torkel Ödegaard

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
parent 6912ed57
......@@ -12,7 +12,6 @@
"build": "grunt",
"start": "grunt watch"
},
"devDependencies": {
"grunt": "^0.4.5",
"grunt-premailer": "^1.1.10",
......
// 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);
......
......@@ -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;
......
......@@ -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,
};
}
......
......@@ -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;
......
......@@ -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);
......
......@@ -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,
};
}
......
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 };
}
......
// 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;
}
......@@ -231,11 +203,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.
*/
export const changeModeAction = actionCreatorFactory<ChangeModePayload>('explore/CHANGE_MODE').create();
......@@ -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();
......
......@@ -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,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) => {
......
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: [],
......@@ -353,48 +367,17 @@ 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(),
};
},
......@@ -527,24 +510,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 => {
return {
......@@ -554,24 +519,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 => {
return {
......@@ -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,
......
......@@ -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>
</>
);
......
......@@ -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}{' '}
......
......@@ -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 {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment