diff --git a/packages/grafana-ui/src/types/datasource.ts b/packages/grafana-ui/src/types/datasource.ts index 3b47265de73..2622efd3ed5 100644 --- a/packages/grafana-ui/src/types/datasource.ts +++ b/packages/grafana-ui/src/types/datasource.ts @@ -214,16 +214,10 @@ export interface ExploreQueryFieldProps< DSType extends DataSourceApi, TQuery extends DataQuery = DataQuery, TOptions extends DataSourceJsonData = DataSourceJsonData -> { - datasource: DSType; +> extends QueryEditorProps { datasourceStatus: DataSourceStatus; - query: TQuery; - error?: string | JSX.Element; - hint?: QueryHint; history: any[]; - onExecuteQuery?: () => void; - onQueryChange?: (value: TQuery) => void; - onExecuteHint?: (action: QueryFixAction) => void; + onHint?: (action: QueryFixAction) => void; } export interface ExploreStartPageProps { diff --git a/packages/grafana-ui/src/utils/moment_wrapper.ts b/packages/grafana-ui/src/utils/moment_wrapper.ts index aaf2113f799..063c427372b 100644 --- a/packages/grafana-ui/src/utils/moment_wrapper.ts +++ b/packages/grafana-ui/src/utils/moment_wrapper.ts @@ -47,6 +47,7 @@ export interface DateTimeDuration { export interface DateTime extends Object { add: (amount?: DateTimeInput, unit?: DurationUnit) => DateTime; + diff: (amount: DateTimeInput, unit?: DurationUnit, truncate?: boolean) => number; endOf: (unitOfTime: DurationUnit) => DateTime; format: (formatInput?: FormatInput) => string; fromNow: (withoutSuffix?: boolean) => string; @@ -59,7 +60,6 @@ export interface DateTime extends Object { subtract: (amount?: DateTimeInput, unit?: DurationUnit) => DateTime; toDate: () => Date; toISOString: () => string; - diff: (amount: DateTimeInput, unit?: DurationUnit, truncate?: boolean) => number; valueOf: () => number; unix: () => number; utc: () => DateTime; diff --git a/public/app/core/utils/explore.test.ts b/public/app/core/utils/explore.test.ts index 2f28e60fdbc..3a0752d2a5e 100644 --- a/public/app/core/utils/explore.test.ts +++ b/public/app/core/utils/explore.test.ts @@ -5,10 +5,15 @@ import { updateHistory, clearHistory, hasNonEmptyQuery, + instanceOfDataQueryError, + getValueWithRefId, + getFirstQueryErrorWithoutRefId, + getRefIds, } from './explore'; import { ExploreUrlState } from 'app/types/explore'; import store from 'app/core/store'; import { LogsDedupStrategy } from 'app/core/logs_model'; +import { DataQueryError } from '@grafana/ui'; const DEFAULT_EXPLORE_STATE: ExploreUrlState = { datasource: null, @@ -188,3 +193,164 @@ describe('hasNonEmptyQuery', () => { expect(hasNonEmptyQuery([])).toBeFalsy(); }); }); + +describe('instanceOfDataQueryError', () => { + describe('when called with a DataQueryError', () => { + it('then it should return true', () => { + const error: DataQueryError = { + message: 'A message', + status: '200', + statusText: 'Ok', + }; + const result = instanceOfDataQueryError(error); + + expect(result).toBe(true); + }); + }); + + describe('when called with a non DataQueryError', () => { + it('then it should return false', () => { + const error = {}; + const result = instanceOfDataQueryError(error); + + expect(result).toBe(false); + }); + }); +}); + +describe('hasRefId', () => { + describe('when called with a null value', () => { + it('then it should return null', () => { + const input = null; + const result = getValueWithRefId(input); + + expect(result).toBeNull(); + }); + }); + + describe('when called with a non object value', () => { + it('then it should return null', () => { + const input = 123; + const result = getValueWithRefId(input); + + expect(result).toBeNull(); + }); + }); + + describe('when called with an object that has refId', () => { + it('then it should return the object', () => { + const input = { refId: 'A' }; + const result = getValueWithRefId(input); + + expect(result).toBe(input); + }); + }); + + describe('when called with an array that has refId', () => { + it('then it should return the object', () => { + const input = [123, null, {}, { refId: 'A' }]; + const result = getValueWithRefId(input); + + expect(result).toBe(input[3]); + }); + }); + + describe('when called with an object that has refId somewhere in the object tree', () => { + it('then it should return the object', () => { + const input: any = { data: [123, null, {}, { series: [123, null, {}, { refId: 'A' }] }] }; + const result = getValueWithRefId(input); + + expect(result).toBe(input.data[3].series[3]); + }); + }); +}); + +describe('getFirstQueryErrorWithoutRefId', () => { + describe('when called with a null value', () => { + it('then it should return null', () => { + const errors: DataQueryError[] = null; + const result = getFirstQueryErrorWithoutRefId(errors); + + expect(result).toBeNull(); + }); + }); + + describe('when called with an array with only refIds', () => { + it('then it should return undefined', () => { + const errors: DataQueryError[] = [{ refId: 'A' }, { refId: 'B' }]; + const result = getFirstQueryErrorWithoutRefId(errors); + + expect(result).toBeUndefined(); + }); + }); + + describe('when called with an array with and without refIds', () => { + it('then it should return undefined', () => { + const errors: DataQueryError[] = [ + { refId: 'A' }, + { message: 'A message' }, + { refId: 'B' }, + { message: 'B message' }, + ]; + const result = getFirstQueryErrorWithoutRefId(errors); + + expect(result).toBe(errors[1]); + }); + }); +}); + +describe('getRefIds', () => { + describe('when called with a null value', () => { + it('then it should return empty array', () => { + const input = null; + const result = getRefIds(input); + + expect(result).toEqual([]); + }); + }); + + describe('when called with a non object value', () => { + it('then it should return empty array', () => { + const input = 123; + const result = getRefIds(input); + + expect(result).toEqual([]); + }); + }); + + describe('when called with an object that has refId', () => { + it('then it should return an array with that refId', () => { + const input = { refId: 'A' }; + const result = getRefIds(input); + + expect(result).toEqual(['A']); + }); + }); + + describe('when called with an array that has refIds', () => { + it('then it should return an array with unique refIds', () => { + const input = [123, null, {}, { refId: 'A' }, { refId: 'A' }, { refId: 'B' }]; + const result = getRefIds(input); + + expect(result).toEqual(['A', 'B']); + }); + }); + + describe('when called with an object that has refIds somewhere in the object tree', () => { + it('then it should return return an array with unique refIds', () => { + const input: any = { + data: [ + 123, + null, + { refId: 'B', series: [{ refId: 'X' }] }, + { refId: 'B' }, + {}, + { series: [123, null, {}, { refId: 'A' }] }, + ], + }; + const result = getRefIds(input); + + expect(result).toEqual(['B', 'X', 'A']); + }); + }); +}); diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index d507b6c1a82..7a19fd5a822 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -21,6 +21,7 @@ import { toSeriesData, guessFieldTypes, TimeFragment, + DataQueryError, } from '@grafana/ui'; import TimeSeries from 'app/core/time_series2'; import { @@ -110,8 +111,7 @@ export async function getExploreUrl( } export function buildQueryTransaction( - query: DataQuery, - rowIndex: number, + queries: DataQuery[], resultType: ResultType, queryOptions: QueryOptions, range: TimeRange, @@ -120,12 +120,11 @@ export function buildQueryTransaction( ): QueryTransaction { const { interval, intervalMs } = queryIntervals; - const configuredQueries = [ - { - ...query, - ...queryOptions, - }, - ]; + const configuredQueries = queries.map(query => ({ ...query, ...queryOptions })); + const key = queries.reduce((combinedKey, query) => { + combinedKey += query.key; + return combinedKey; + }, ''); // Clone range for query request // const queryRange: RawTimeRange = { ...range }; @@ -134,7 +133,7 @@ export function buildQueryTransaction( // Using `format` here because it relates to the view panel that the request is for. // However, some datasources don't use `panelId + query.refId`, but only `panelId`. // Therefore panel id has to be unique. - const panelId = `${queryOptions.format}-${query.key}`; + const panelId = `${queryOptions.format}-${key}`; const options = { interval, @@ -151,10 +150,9 @@ export function buildQueryTransaction( }; return { + queries, options, - query, resultType, - rowIndex, scanning, id: generateKey(), // reusing for unique ID done: false, @@ -195,6 +193,20 @@ export const safeParseJson = (text: string) => { } }; +export const safeStringifyValue = (value: any, space?: number) => { + if (!value) { + return ''; + } + + try { + return JSON.stringify(value, null, space); + } catch (error) { + console.error(error); + } + + return ''; +}; + export function parseUrlState(initial: string | undefined): ExploreUrlState { const parsed = safeParseJson(initial); const errorResult = { @@ -265,12 +277,34 @@ export function generateEmptyQuery(queries: DataQuery[], index = 0): DataQuery { return { refId: getNextRefIdChar(queries), key: generateKey(index) }; } +export const generateNewKeyAndAddRefIdIfMissing = (target: DataQuery, queries: DataQuery[], index = 0): DataQuery => { + const key = generateKey(index); + const refId = target.refId || getNextRefIdChar(queries); + + return { ...target, refId, key }; +}; + /** * Ensure at least one target exists and that targets have the necessary keys */ export function ensureQueries(queries?: DataQuery[]): DataQuery[] { if (queries && typeof queries === 'object' && queries.length > 0) { - return queries.map((query, i) => ({ ...query, ...generateEmptyQuery(queries, i) })); + const allQueries = []; + for (let index = 0; index < queries.length; index++) { + const query = queries[index]; + const key = generateKey(index); + let refId = query.refId; + if (!refId) { + refId = getNextRefIdChar(allQueries); + } + + allQueries.push({ + ...query, + refId, + key, + }); + } + return allQueries; } return [{ ...generateEmptyQuery(queries) }]; } @@ -290,26 +324,20 @@ export function hasNonEmptyQuery(queries: TQuery ); } -export function calculateResultsFromQueryTransactions( - queryTransactions: QueryTransaction[], - datasource: any, - graphInterval: number -) { - const graphResult = _.flatten( - queryTransactions.filter(qt => qt.resultType === 'Graph' && qt.done && qt.result).map(qt => qt.result) - ); - const tableResult = mergeTablesIntoModel( - new TableModel(), - ...queryTransactions - .filter(qt => qt.resultType === 'Table' && qt.done && qt.result && qt.result.columns && qt.result.rows) - .map(qt => qt.result) - ); - const logsResult = seriesDataToLogsModel( - _.flatten( - queryTransactions.filter(qt => qt.resultType === 'Logs' && qt.done && qt.result).map(qt => qt.result) - ).map(r => guessFieldTypes(toSeriesData(r))), - graphInterval - ); +export function calculateResultsFromQueryTransactions(result: any, resultType: ResultType, graphInterval: number) { + const flattenedResult: any[] = _.flatten(result); + const graphResult = resultType === 'Graph' && result ? result : null; + const tableResult = + resultType === 'Table' && result + ? mergeTablesIntoModel( + new TableModel(), + ...flattenedResult.filter((r: any) => r.columns && r.rows).map((r: any) => r as TableModel) + ) + : mergeTablesIntoModel(new TableModel()); + const logsResult = + resultType === 'Logs' && result + ? seriesDataToLogsModel(flattenedResult.map(r => guessFieldTypes(toSeriesData(r))), graphInterval) + : null; return { graphResult, @@ -441,3 +469,63 @@ export const getTimeRangeFromUrl = (range: RawTimeRange, timeZone: TimeZone): Ti raw, }; }; + +export const instanceOfDataQueryError = (value: any): value is DataQueryError => { + return value.message !== undefined && value.status !== undefined && value.statusText !== undefined; +}; + +export const getValueWithRefId = (value: any): any | null => { + if (!value) { + return null; + } + + if (typeof value !== 'object') { + return null; + } + + if (value.refId) { + return value; + } + + const keys = Object.keys(value); + for (let index = 0; index < keys.length; index++) { + const key = keys[index]; + const refId = getValueWithRefId(value[key]); + if (refId) { + return refId; + } + } + + return null; +}; + +export const getFirstQueryErrorWithoutRefId = (errors: DataQueryError[]) => { + if (!errors) { + return null; + } + + return errors.filter(error => (error.refId ? false : true))[0]; +}; + +export const getRefIds = (value: any): string[] => { + if (!value) { + return []; + } + + if (typeof value !== 'object') { + return []; + } + + const keys = Object.keys(value); + const refIds = []; + for (let index = 0; index < keys.length; index++) { + const key = keys[index]; + if (key === 'refId') { + refIds.push(value[key]); + continue; + } + refIds.push(getRefIds(value[key])); + } + + return _.uniq(_.flatten(refIds)); +}; diff --git a/public/app/features/explore/ErrorContainer.tsx b/public/app/features/explore/ErrorContainer.tsx new file mode 100644 index 00000000000..ff8ac3d8210 --- /dev/null +++ b/public/app/features/explore/ErrorContainer.tsx @@ -0,0 +1,32 @@ +import React, { FunctionComponent } from 'react'; +import { DataQueryError } from '@grafana/ui'; +import { FadeIn } from 'app/core/components/Animations/FadeIn'; +import { getFirstQueryErrorWithoutRefId, getValueWithRefId } from 'app/core/utils/explore'; + +interface Props { + queryErrors: DataQueryError[]; +} + +export const ErrorContainer: FunctionComponent = props => { + const { queryErrors } = props; + const refId = getValueWithRefId(queryErrors); + const queryError = refId ? null : getFirstQueryErrorWithoutRefId(queryErrors); + const showError = queryError ? true : false; + const duration = showError ? 100 : 10; + const message = queryError ? queryError.message : null; + + return ( + +
+
+
+ +
+
+
{message}
+
+
+
+
+ ); +}; diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 67d7cb9f578..643e1d0d0f9 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -31,7 +31,7 @@ import { } from './state/actions'; // Types -import { RawTimeRange, DataQuery, ExploreStartPageProps, ExploreDataSourceApi } from '@grafana/ui'; +import { RawTimeRange, DataQuery, ExploreStartPageProps, ExploreDataSourceApi, DataQueryError } from '@grafana/ui'; import { ExploreItemState, ExploreUrlState, @@ -54,6 +54,7 @@ import { scanStopAction } from './state/actionTypes'; import { NoDataSourceCallToAction } from './NoDataSourceCallToAction'; import { FadeIn } from 'app/core/components/Animations/FadeIn'; import { getTimeZone } from '../profile/state/selectors'; +import { ErrorContainer } from './ErrorContainer'; interface ExploreProps { StartPage?: ComponentClass; @@ -86,6 +87,7 @@ interface ExploreProps { initialQueries: DataQuery[]; initialRange: RawTimeRange; initialUI: ExploreUIState; + queryErrors: DataQueryError[]; } /** @@ -236,6 +238,7 @@ export class Explore extends React.PureComponent { supportsLogs, supportsTable, queryKeys, + queryErrors, } = this.props; const exploreClass = split ? 'explore explore-split' : 'explore'; @@ -257,6 +260,7 @@ export class Explore extends React.PureComponent { {datasourceInstance && (
+ {({ width }) => { if (width === 0) { @@ -313,6 +317,7 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) { queryKeys, urlState, update, + queryErrors, } = item; const { datasource, queries, range: urlRange, ui } = (urlState || {}) as ExploreUrlState; @@ -339,6 +344,7 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) { initialQueries, initialRange, initialUI, + queryErrors, }; } diff --git a/public/app/features/explore/ExploreToolbar.tsx b/public/app/features/explore/ExploreToolbar.tsx index d4f24b08455..2038224d40f 100644 --- a/public/app/features/explore/ExploreToolbar.tsx +++ b/public/app/features/explore/ExploreToolbar.tsx @@ -203,14 +203,16 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps datasourceInstance, datasourceMissing, exploreDatasources, - queryTransactions, range, refreshInterval, + graphIsLoading, + logIsLoading, + tableIsLoading, } = exploreItem; const selectedDatasource = datasourceInstance ? exploreDatasources.find(datasource => datasource.name === datasourceInstance.name) : undefined; - const loading = queryTransactions.some(qt => !qt.done); + const loading = graphIsLoading || logIsLoading || tableIsLoading; return { datasourceMissing, diff --git a/public/app/features/explore/GraphContainer.tsx b/public/app/features/explore/GraphContainer.tsx index fd0c0b680ec..7033473a33b 100644 --- a/public/app/features/explore/GraphContainer.tsx +++ b/public/app/features/explore/GraphContainer.tsx @@ -71,8 +71,8 @@ function mapStateToProps(state: StoreState, { exploreId }) { const explore = state.explore; const { split } = explore; const item: ExploreItemState = explore[exploreId]; - const { graphResult, queryTransactions, range, showingGraph, showingTable } = item; - const loading = queryTransactions.some(qt => qt.resultType === 'Graph' && !qt.done); + const { graphResult, graphIsLoading, range, showingGraph, showingTable } = item; + const loading = graphIsLoading; return { graphResult, loading, range, showingGraph, showingTable, split, timeZone: getTimeZone(state.user) }; } diff --git a/public/app/features/explore/LogsContainer.tsx b/public/app/features/explore/LogsContainer.tsx index dc2cb88129c..83cce42c7d1 100644 --- a/public/app/features/explore/LogsContainer.tsx +++ b/public/app/features/explore/LogsContainer.tsx @@ -113,8 +113,8 @@ export class LogsContainer extends PureComponent { function mapStateToProps(state: StoreState, { exploreId }) { const explore = state.explore; const item: ExploreItemState = explore[exploreId]; - const { logsHighlighterExpressions, logsResult, queryTransactions, scanning, scanRange, range } = item; - const loading = queryTransactions.some(qt => qt.resultType === 'Logs' && !qt.done); + const { logsHighlighterExpressions, logsResult, logIsLoading, scanning, scanRange, range } = item; + const loading = logIsLoading; const { showingLogs, dedupStrategy } = exploreItemUIStateSelector(item); const hiddenLogLevels = new Set(item.hiddenLogLevels); const dedupedResult = deduplicatedLogsSelector(item); diff --git a/public/app/features/explore/QueryEditor.tsx b/public/app/features/explore/QueryEditor.tsx index 72613cbeb91..0d34ee5249e 100644 --- a/public/app/features/explore/QueryEditor.tsx +++ b/public/app/features/explore/QueryEditor.tsx @@ -12,8 +12,8 @@ import 'app/features/plugins/plugin_loader'; import { dateTime } from '@grafana/ui/src/utils/moment_wrapper'; interface QueryEditorProps { + error?: any; datasource: any; - error?: string | JSX.Element; onExecuteQuery?: () => void; onQueryChange?: (value: DataQuery) => void; initialQuery: DataQuery; @@ -57,6 +57,14 @@ export default class QueryEditor extends PureComponent { this.props.onQueryChange(target); } + componentDidUpdate(prevProps: QueryEditorProps) { + if (prevProps.error !== this.props.error && this.component) { + // Some query controllers listen to data error events and need a digest + // for some reason this needs to be done in next tick + setTimeout(this.component.digest); + } + } + componentWillUnmount() { if (this.component) { this.component.destroy(); diff --git a/public/app/features/explore/QueryField.tsx b/public/app/features/explore/QueryField.tsx index 58d860f54ad..e5d7ff43631 100644 --- a/public/app/features/explore/QueryField.tsx +++ b/public/app/features/explore/QueryField.tsx @@ -36,8 +36,8 @@ export interface QueryFieldProps { cleanText?: (text: string) => string; disabled?: boolean; initialQuery: string | null; - onExecuteQuery?: () => void; - onQueryChange?: (value: string) => void; + onRunQuery?: () => void; + onChange?: (value: string) => void; onTypeahead?: (typeahead: TypeaheadInput) => TypeaheadOutput; onWillApplySuggestion?: (suggestion: string, state: QueryFieldState) => string; placeholder?: string; @@ -149,7 +149,7 @@ export class QueryField extends React.PureComponent { - const { onQueryChange } = this.props; - if (onQueryChange) { - onQueryChange(Plain.serialize(this.state.value)); + const { onChange } = this.props; + if (onChange) { + onChange(Plain.serialize(this.state.value)); } }; - executeOnQueryChangeAndExecuteQueries = () => { + executeOnChangeAndRunQueries = () => { // Send text change to parent - const { onQueryChange, onExecuteQuery } = this.props; - if (onQueryChange) { - onQueryChange(Plain.serialize(this.state.value)); + const { onChange, onRunQuery } = this.props; + if (onChange) { + onChange(Plain.serialize(this.state.value)); } - if (onExecuteQuery) { - onExecuteQuery(); + if (onRunQuery) { + onRunQuery(); this.setState({ lastExecutedValue: this.state.value }); } }; @@ -330,7 +330,7 @@ export class QueryField extends React.PureComponent qt.hints && qt.hints.length > 0); - if (transaction) { - return transaction.hints[0]; - } - return undefined; -} +import QueryStatus from './QueryStatus'; interface QueryRowProps { addQueryRow: typeof addQueryRow; @@ -39,20 +40,22 @@ interface QueryRowProps { index: number; query: DataQuery; modifyQueries: typeof modifyQueries; - queryTransactions: QueryTransaction[]; exploreEvents: Emitter; range: TimeRange; removeQueryRowAction: typeof removeQueryRowAction; runQueries: typeof runQueries; + queryResponse: PanelData; + latency: number; + queryErrors: DataQueryError[]; } export class QueryRow extends PureComponent { - onExecuteQuery = () => { + onRunQuery = () => { const { exploreId } = this.props; this.props.runQueries(exploreId); }; - onChangeQuery = (query: DataQuery, override?: boolean) => { + onChange = (query: DataQuery, override?: boolean) => { const { datasourceInstance, exploreId, index } = this.props; this.props.changeQuery(exploreId, query, index, override); if (query && !override && datasourceInstance.getHighlighterExpression && index === 0) { @@ -71,7 +74,7 @@ export class QueryRow extends PureComponent { }; onClickClearButton = () => { - this.onChangeQuery(null, true); + this.onChange(null, true); }; onClickHintFix = (action: QueryFixAction) => { @@ -85,6 +88,7 @@ export class QueryRow extends PureComponent { onClickRemoveButton = () => { const { exploreId, index } = this.props; this.props.removeQueryRowAction({ exploreId, index }); + this.props.runQueries(exploreId); }; updateLogsHighlights = _.debounce((value: DataQuery) => { @@ -100,24 +104,20 @@ export class QueryRow extends PureComponent { const { datasourceInstance, history, - index, query, - queryTransactions, exploreEvents, range, datasourceStatus, + queryResponse, + latency, + queryErrors, } = this.props; - - const transactions = queryTransactions.filter(t => t.rowIndex === index); - const transactionWithError = transactions.find(t => t.error !== undefined); - const hint = getFirstHintFromTransactions(transactions); - const queryError = transactionWithError ? transactionWithError.error : null; const QueryField = datasourceInstance.components.ExploreQueryField; return (
- +
{QueryField ? ( @@ -125,19 +125,19 @@ export class QueryRow extends PureComponent { datasource={datasourceInstance} datasourceStatus={datasourceStatus} query={query} - error={queryError} - hint={hint} history={history} - onExecuteQuery={this.onExecuteQuery} - onExecuteHint={this.onClickHintFix} - onQueryChange={this.onChangeQuery} + onRunQuery={this.onRunQuery} + onHint={this.onClickHintFix} + onChange={this.onChange} + panelData={null} + queryResponse={queryResponse} /> ) : ( { function mapStateToProps(state: StoreState, { exploreId, index }: QueryRowProps) { const explore = state.explore; const item: ExploreItemState = explore[exploreId]; - const { datasourceInstance, history, queries, queryTransactions, range, datasourceError } = item; + const { + datasourceInstance, + history, + queries, + range, + datasourceError, + graphResult, + graphIsLoading, + tableIsLoading, + logIsLoading, + latency, + queryErrors, + } = item; const query = queries[index]; + const datasourceStatus = datasourceError ? DataSourceStatus.Disconnected : DataSourceStatus.Connected; + const error = queryErrors.filter(queryError => queryError.refId === query.refId)[0]; + const series = graphResult ? graphResult : []; // TODO: use SeriesData + const queryResponseState = + graphIsLoading || tableIsLoading || logIsLoading + ? LoadingState.Loading + : error + ? LoadingState.Error + : LoadingState.Done; + const queryResponse: PanelData = { + series, + state: queryResponseState, + error, + }; + return { datasourceInstance, history, query, - queryTransactions, range, - datasourceStatus: datasourceError ? DataSourceStatus.Disconnected : DataSourceStatus.Connected, + datasourceStatus, + queryResponse, + latency, + queryErrors, }; } diff --git a/public/app/features/explore/QueryStatus.tsx b/public/app/features/explore/QueryStatus.tsx new file mode 100644 index 00000000000..54b3b8129ef --- /dev/null +++ b/public/app/features/explore/QueryStatus.tsx @@ -0,0 +1,47 @@ +import React, { PureComponent } from 'react'; + +import ElapsedTime from './ElapsedTime'; +import { PanelData, LoadingState } from '@grafana/ui'; + +function formatLatency(value) { + return `${(value / 1000).toFixed(1)}s`; +} + +interface QueryStatusItemProps { + queryResponse: PanelData; + latency: number; +} + +class QueryStatusItem extends PureComponent { + render() { + const { queryResponse, latency } = this.props; + const className = + queryResponse.state === LoadingState.Done || LoadingState.Error + ? 'query-transaction' + : 'query-transaction query-transaction--loading'; + return ( +
+ {/*
{transaction.resultType}:
*/} +
+ {queryResponse.state === LoadingState.Done || LoadingState.Error ? formatLatency(latency) : } +
+
+ ); + } +} + +interface QueryStatusProps { + queryResponse: PanelData; + latency: number; +} + +export default class QueryStatus extends PureComponent { + render() { + const { queryResponse, latency } = this.props; + return ( +
+ {queryResponse && } +
+ ); + } +} diff --git a/public/app/features/explore/QueryTransactionStatus.tsx b/public/app/features/explore/QueryTransactionStatus.tsx deleted file mode 100644 index 6f47f147645..00000000000 --- a/public/app/features/explore/QueryTransactionStatus.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React, { PureComponent } from 'react'; - -import { QueryTransaction } from 'app/types/explore'; -import ElapsedTime from './ElapsedTime'; - -function formatLatency(value) { - return `${(value / 1000).toFixed(1)}s`; -} - -interface QueryTransactionStatusItemProps { - transaction: QueryTransaction; -} - -class QueryTransactionStatusItem extends PureComponent { - render() { - const { transaction } = this.props; - const className = transaction.done ? 'query-transaction' : 'query-transaction query-transaction--loading'; - return ( -
-
{transaction.resultType}:
-
- {transaction.done ? formatLatency(transaction.latency) : } -
-
- ); - } -} - -interface QueryTransactionStatusProps { - transactions: QueryTransaction[]; -} - -export default class QueryTransactionStatus extends PureComponent { - render() { - const { transactions } = this.props; - return ( -
- {transactions.map((t, i) => ( - - ))} -
- ); - } -} diff --git a/public/app/features/explore/TableContainer.tsx b/public/app/features/explore/TableContainer.tsx index e24ca070f2f..78f190a05cb 100644 --- a/public/app/features/explore/TableContainer.tsx +++ b/public/app/features/explore/TableContainer.tsx @@ -42,8 +42,8 @@ export class TableContainer extends PureComponent { function mapStateToProps(state: StoreState, { exploreId }) { const explore = state.explore; const item: ExploreItemState = explore[exploreId]; - const { queryTransactions, showingTable, tableResult } = item; - const loading = queryTransactions.some(qt => qt.resultType === 'Table' && !qt.done); + const { tableIsLoading, showingTable, tableResult } = item; + const loading = tableIsLoading; return { loading, showingTable, tableResult }; } diff --git a/public/app/features/explore/state/actionTypes.ts b/public/app/features/explore/state/actionTypes.ts index aec004286a8..cce8d649c9d 100644 --- a/public/app/features/explore/state/actionTypes.ts +++ b/public/app/features/explore/state/actionTypes.ts @@ -8,6 +8,7 @@ import { QueryFixAction, LogLevel, TimeRange, + DataQueryError, } from '@grafana/ui/src/types'; import { ExploreId, @@ -132,22 +133,29 @@ export interface ModifyQueriesPayload { modifier: (query: DataQuery, modification: QueryFixAction) => DataQuery; } -export interface QueryTransactionFailurePayload { +export interface QueryFailurePayload { exploreId: ExploreId; - queryTransactions: QueryTransaction[]; + response: DataQueryError; + resultType: ResultType; } -export interface QueryTransactionStartPayload { +export interface QueryStartPayload { exploreId: ExploreId; resultType: ResultType; rowIndex: number; transaction: QueryTransaction; } -export interface QueryTransactionSuccessPayload { +export interface QuerySuccessPayload { + exploreId: ExploreId; + result: any; + resultType: ResultType; + latency: number; +} + +export interface HistoryUpdatedPayload { exploreId: ExploreId; history: HistoryItem[]; - queryTransactions: QueryTransaction[]; } export interface RemoveQueryRowPayload { @@ -222,6 +230,11 @@ export interface RunQueriesPayload { exploreId: ExploreId; } +export interface ResetQueryErrorPayload { + exploreId: ExploreId; + refIds: string[]; +} + /** * Adds a query row after the row with the given index. */ @@ -310,9 +323,7 @@ export const modifyQueriesAction = actionCreatorFactory('e * Mark a query transaction as failed with an error extracted from the query response. * The transaction will be marked as `done`. */ -export const queryTransactionFailureAction = actionCreatorFactory( - 'explore/QUERY_TRANSACTION_FAILURE' -).create(); +export const queryFailureAction = actionCreatorFactory('explore/QUERY_FAILURE').create(); /** * Start a query transaction for the given result type. @@ -321,9 +332,7 @@ export const queryTransactionFailureAction = actionCreatorFactory( - 'explore/QUERY_TRANSACTION_START' -).create(); +export const queryStartAction = actionCreatorFactory('explore/QUERY_START').create(); /** * Complete a query transaction, mark the transaction as `done` and store query state in URL. @@ -336,9 +345,7 @@ export const queryTransactionStartAction = actionCreatorFactory( - 'explore/QUERY_TRANSACTION_SUCCESS' -).create(); +export const querySuccessAction = actionCreatorFactory('explore/QUERY_SUCCESS').create(); /** * Remove query row of the given index, as well as associated query results. @@ -426,6 +433,10 @@ export const loadExploreDatasources = actionCreatorFactory('explore/HISTORY_UPDATED').create(); + +export const resetQueryErrorAction = actionCreatorFactory('explore/RESET_QUERY_ERROR').create(); + export type HigherOrderAction = | ActionOf | SplitOpenAction diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts index ebf269c4c08..12b1a1b8b69 100644 --- a/public/app/features/explore/state/actions.ts +++ b/public/app/features/explore/state/actions.ts @@ -18,20 +18,21 @@ import { parseUrlState, getTimeRange, getTimeRangeFromUrl, + generateNewKeyAndAddRefIdIfMissing, + instanceOfDataQueryError, + getRefIds, } from 'app/core/utils/explore'; // Actions import { updateLocation } from 'app/core/actions'; // Types -import { ResultGetter } from 'app/types/explore'; import { ThunkResult } from 'app/types'; import { RawTimeRange, DataSourceApi, DataQuery, DataSourceSelectItem, - QueryHint, QueryFixAction, TimeRange, } from '@grafana/ui/src/types'; @@ -61,9 +62,8 @@ import { LoadDatasourceReadyPayload, loadDatasourceReadyAction, modifyQueriesAction, - queryTransactionFailureAction, - queryTransactionStartAction, - queryTransactionSuccessAction, + queryFailureAction, + querySuccessAction, scanRangeAction, scanStartAction, setQueriesAction, @@ -82,11 +82,15 @@ import { testDataSourceSuccessAction, testDataSourceFailureAction, loadExploreDatasources, + queryStartAction, + historyUpdatedAction, + resetQueryErrorAction, } from './actionTypes'; import { ActionOf, ActionCreator } from 'app/core/redux/actionCreatorFactory'; import { LogsDedupStrategy } from 'app/core/logs_model'; import { getTimeZone } from 'app/features/profile/state/selectors'; import { isDateTime } from '@grafana/ui/src/utils/moment_wrapper'; +import { toDataQueryError } from 'app/features/dashboard/state/PanelQueryState'; /** * Updates UI state and save it to the URL @@ -103,7 +107,8 @@ const updateExploreUIState = (exploreId: ExploreId, uiStateFragment: Partial { return (dispatch, getState) => { - const query = generateEmptyQuery(getState().explore[exploreId].queries, index); + const queries = getState().explore[exploreId].queries; + const query = generateEmptyQuery(queries, index); dispatch(addQueryRowAction({ exploreId, index, query })); }; @@ -148,7 +153,9 @@ export function changeQuery( return (dispatch, getState) => { // Null query means reset if (query === null) { - query = { ...generateEmptyQuery(getState().explore[exploreId].queries) }; + const queries = getState().explore[exploreId].queries; + const { refId, key } = queries[index]; + query = generateNewKeyAndAddRefIdIfMissing({ refId, key }, queries, index); } dispatch(changeQueryAction({ exploreId, query, index, override })); @@ -306,10 +313,7 @@ export function importQueries( importedQueries = ensureQueries(); } - const nextQueries = importedQueries.map((q, i) => ({ - ...q, - ...generateEmptyQuery(queries), - })); + const nextQueries = ensureQueries(importedQueries); dispatch(queriesImportedAction({ exploreId, queries: nextQueries })); }; @@ -368,7 +372,11 @@ export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): T } if (instance.init) { - instance.init(); + try { + instance.init(); + } catch (err) { + console.log(err); + } } if (datasourceName !== getState().explore[exploreId].requestedDatasourceName) { @@ -401,140 +409,87 @@ export function modifyQueries( }; } -/** - * Mark a query transaction as failed with an error extracted from the query response. - * The transaction will be marked as `done`. - */ -export function queryTransactionFailure( +export function processQueryErrors( exploreId: ExploreId, - transactionId: string, response: any, + resultType: ResultType, datasourceId: string ): ThunkResult { return (dispatch, getState) => { - const { datasourceInstance, queryTransactions } = getState().explore[exploreId]; + const { datasourceInstance } = getState().explore[exploreId]; + if (datasourceInstance.meta.id !== datasourceId || response.cancelled) { // Navigated away, queries did not matter return; } - // Transaction might have been discarded - if (!queryTransactions.find(qt => qt.id === transactionId)) { - return; + console.error(response); // To help finding problems with query syntax + + if (!instanceOfDataQueryError(response)) { + response = toDataQueryError(response); } - console.error(response); - - let error: string; - let errorDetails: string; - if (response.data) { - if (typeof response.data === 'string') { - error = response.data; - } else if (response.data.error) { - error = response.data.error; - if (response.data.response) { - errorDetails = response.data.response; - } - } else { - throw new Error('Could not handle error response'); - } - } else if (response.message) { - error = response.message; - } else if (typeof response === 'string') { - error = response; - } else { - error = 'Unknown error during query transaction. Please check JS console logs.'; - } - - // Mark transactions as complete - const nextQueryTransactions = queryTransactions.map(qt => { - if (qt.id === transactionId) { - return { - ...qt, - error, - errorDetails, - done: true, - }; - } - return qt; - }); - - dispatch(queryTransactionFailureAction({ exploreId, queryTransactions: nextQueryTransactions })); + dispatch( + queryFailureAction({ + exploreId, + response, + resultType, + }) + ); }; } /** - * Complete a query transaction, mark the transaction as `done` and store query state in URL. - * If the transaction was started by a scanner, it keeps on scanning for more results. - * Side-effect: the query is stored in localStorage. * @param exploreId Explore area - * @param transactionId ID - * @param result Response from `datasourceInstance.query()` + * @param response Response from `datasourceInstance.query()` * @param latency Duration between request and response - * @param queries Queries from all query rows + * @param resultType The type of result * @param datasourceId Origin datasource instance, used to discard results if current datasource is different */ -export function queryTransactionSuccess( +export function processQueryResults( exploreId: ExploreId, - transactionId: string, - result: any, + response: any, latency: number, - queries: DataQuery[], + resultType: ResultType, datasourceId: string ): ThunkResult { return (dispatch, getState) => { - const { datasourceInstance, history, queryTransactions, scanner, scanning } = getState().explore[exploreId]; + const { datasourceInstance, scanning, scanner } = getState().explore[exploreId]; // If datasource already changed, results do not matter if (datasourceInstance.meta.id !== datasourceId) { return; } - // Transaction might have been discarded - const transaction = queryTransactions.find(qt => qt.id === transactionId); - if (!transaction) { - return; - } + const series: any[] = response.data; + const refIds = getRefIds(series); - // Get query hints - let hints: QueryHint[]; - if (datasourceInstance.getQueryHints) { - hints = datasourceInstance.getQueryHints(transaction.query, result); - } + // Clears any previous errors that now have a successful query, important so Angular editors are updated correctly + dispatch( + resetQueryErrorAction({ + exploreId, + refIds, + }) + ); - // Mark transactions as complete and attach result - const nextQueryTransactions = queryTransactions.map(qt => { - if (qt.id === transactionId) { - return { - ...qt, - hints, - latency, - result, - done: true, - }; - } - return qt; - }); - - // Side-effect: Saving history in localstorage - const nextHistory = updateHistory(history, datasourceId, queries); + const resultGetter = + resultType === 'Graph' ? makeTimeSeriesList : resultType === 'Table' ? (data: any[]) => data : null; + const result = resultGetter ? resultGetter(series, null, []) : series; dispatch( - queryTransactionSuccessAction({ + querySuccessAction({ exploreId, - history: nextHistory, - queryTransactions: nextQueryTransactions, + result, + resultType, + latency, }) ); // Keep scanning for results if this was the last scanning transaction if (scanning) { if (_.size(result) === 0) { - const other = nextQueryTransactions.find(qt => qt.scanning && !qt.done); - if (!other) { - const range = scanner(); - dispatch(scanRangeAction({ exploreId, range })); - } + const range = scanner(); + dispatch(scanRangeAction({ exploreId, range })); } else { // We can stop scanning if we have a result dispatch(scanStopAction({ exploreId })); @@ -580,32 +535,22 @@ export function runQueries(exploreId: ExploreId, ignoreUIState = false): ThunkRe // Keep table queries first since they need to return quickly if ((ignoreUIState || showingTable) && supportsTable) { dispatch( - runQueriesForType( - exploreId, - 'Table', - { - interval, - format: 'table', - instant: true, - valueWithRefId: true, - }, - (data: any[]) => data[0] - ) + runQueriesForType(exploreId, 'Table', { + interval, + format: 'table', + instant: true, + valueWithRefId: true, + }) ); } if ((ignoreUIState || showingGraph) && supportsGraph) { dispatch( - runQueriesForType( - exploreId, - 'Graph', - { - interval, - format: 'time_series', - instant: false, - maxDataPoints: containerWidth, - }, - makeTimeSeriesList - ) + runQueriesForType(exploreId, 'Graph', { + interval, + format: 'time_series', + instant: false, + maxDataPoints: containerWidth, + }) ); } if ((ignoreUIState || showingLogs) && supportsLogs) { @@ -626,37 +571,27 @@ export function runQueries(exploreId: ExploreId, ignoreUIState = false): ThunkRe function runQueriesForType( exploreId: ExploreId, resultType: ResultType, - queryOptions: QueryOptions, - resultGetter?: ResultGetter + queryOptions: QueryOptions ): ThunkResult { return async (dispatch, getState) => { - const { datasourceInstance, eventBridge, queries, queryIntervals, range, scanning } = getState().explore[exploreId]; + const { datasourceInstance, eventBridge, queries, queryIntervals, range, scanning, history } = getState().explore[ + exploreId + ]; const datasourceId = datasourceInstance.meta.id; - // Run all queries concurrently - for (let rowIndex = 0; rowIndex < queries.length; rowIndex++) { - const query = queries[rowIndex]; - const transaction = buildQueryTransaction( - query, - rowIndex, - resultType, - queryOptions, - range, - queryIntervals, - scanning - ); - dispatch(queryTransactionStartAction({ exploreId, resultType, rowIndex, transaction })); - try { - const now = Date.now(); - const res = await datasourceInstance.query(transaction.options); - eventBridge.emit('data-received', res.data || []); - const latency = Date.now() - now; - const { queryTransactions } = getState().explore[exploreId]; - const results = resultGetter ? resultGetter(res.data, transaction, queryTransactions) : res.data; - dispatch(queryTransactionSuccess(exploreId, transaction.id, results, latency, queries, datasourceId)); - } catch (response) { - eventBridge.emit('data-error', response); - dispatch(queryTransactionFailure(exploreId, transaction.id, response, datasourceId)); - } + const transaction = buildQueryTransaction(queries, resultType, queryOptions, range, queryIntervals, scanning); + dispatch(queryStartAction({ exploreId, resultType, rowIndex: 0, transaction })); + try { + const now = Date.now(); + const response = await datasourceInstance.query(transaction.options); + eventBridge.emit('data-received', response.data || []); + const latency = Date.now() - now; + // Side-effect: Saving history in localstorage + const nextHistory = updateHistory(history, datasourceId, queries); + dispatch(historyUpdatedAction({ exploreId, history: nextHistory })); + dispatch(processQueryResults(exploreId, response, latency, resultType, datasourceId)); + } catch (err) { + eventBridge.emit('data-error', err); + dispatch(processQueryErrors(exploreId, err, resultType, datasourceId)); } }; } @@ -684,8 +619,9 @@ export function scanStart(exploreId: ExploreId, scanner: RangeScanner): ThunkRes export function setQueries(exploreId: ExploreId, rawQueries: DataQuery[]): ThunkResult { return (dispatch, getState) => { // Inject react keys into query objects - const queries = rawQueries.map(q => ({ ...q, ...generateEmptyQuery(getState().explore[exploreId].queries) })); - dispatch(setQueriesAction({ exploreId, queries })); + const queries = getState().explore[exploreId].queries; + const nextQueries = rawQueries.map((query, index) => generateNewKeyAndAddRefIdIfMissing(query, queries, index)); + dispatch(setQueriesAction({ exploreId, queries: nextQueries })); dispatch(runQueries(exploreId)); }; } @@ -849,7 +785,11 @@ export function refreshExplore(exploreId: ExploreId): ThunkResult { const { urlState, update, containerWidth, eventBridge } = itemState; const { datasource, queries, range: urlRange, ui } = urlState; - const refreshQueries = queries.map(q => ({ ...q, ...generateEmptyQuery(itemState.queries) })); + const refreshQueries: DataQuery[] = []; + for (let index = 0; index < queries.length; index++) { + const query = queries[index]; + refreshQueries.push(generateNewKeyAndAddRefIdIfMissing(query, refreshQueries, index)); + } const timeZone = getTimeZone(getState().user); const range = getTimeRangeFromUrl(urlRange, timeZone); diff --git a/public/app/features/explore/state/reducers.test.ts b/public/app/features/explore/state/reducers.test.ts index f781eb684c0..428d208e17a 100644 --- a/public/app/features/explore/state/reducers.test.ts +++ b/public/app/features/explore/state/reducers.test.ts @@ -97,7 +97,6 @@ describe('Explore item reducer', () => { const queryTransactions: QueryTransaction[] = []; const initalState: Partial = { datasourceError: null, - queryTransactions: [{} as QueryTransaction], graphResult: [], tableResult: {} as TableModel, logsResult: {} as LogsModel, diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index c63b5fe4a1c..9807bcb8ad8 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -1,14 +1,14 @@ import _ from 'lodash'; import { calculateResultsFromQueryTransactions, - generateEmptyQuery, getIntervals, ensureQueries, getQueryKeys, parseUrlState, DEFAULT_UI_STATE, + generateNewKeyAndAddRefIdIfMissing, } from 'app/core/utils/explore'; -import { ExploreItemState, ExploreState, QueryTransaction, ExploreId, ExploreUpdateState } from 'app/types/explore'; +import { ExploreItemState, ExploreState, ExploreId, ExploreUpdateState } from 'app/types/explore'; import { DataQuery } from '@grafana/ui/src/types'; import { HigherOrderAction, @@ -20,6 +20,8 @@ import { SplitCloseActionPayload, loadExploreDatasources, runQueriesAction, + historyUpdatedAction, + resetQueryErrorAction, } from './actionTypes'; import { reducerFactory } from 'app/core/redux'; import { @@ -36,16 +38,14 @@ import { loadDatasourcePendingAction, loadDatasourceReadyAction, modifyQueriesAction, - queryTransactionFailureAction, - queryTransactionStartAction, - queryTransactionSuccessAction, + queryFailureAction, + queryStartAction, + querySuccessAction, removeQueryRowAction, scanRangeAction, scanStartAction, scanStopAction, setQueriesAction, - toggleGraphAction, - toggleLogsAction, toggleTableAction, queriesImportedAction, updateUIStateAction, @@ -53,6 +53,7 @@ import { } from './actionTypes'; import { updateLocation } from 'app/core/actions/location'; import { LocationUpdate } from 'app/types'; +import TableModel from 'app/core/table_model'; export const DEFAULT_RANGE = { from: 'now-6h', @@ -84,7 +85,6 @@ export const makeExploreItemState = (): ExploreItemState => ({ history: [], queries: [], initialized: false, - queryTransactions: [], queryIntervals: { interval: '15s', intervalMs: DEFAULT_GRAPH_INTERVAL }, range: { from: null, @@ -96,12 +96,17 @@ export const makeExploreItemState = (): ExploreItemState => ({ showingGraph: true, showingLogs: true, showingTable: true, + graphIsLoading: false, + logIsLoading: false, + tableIsLoading: false, supportsGraph: null, supportsLogs: null, supportsTable: null, queryKeys: [], urlState: null, update: makeInitialUpdateState(), + queryErrors: [], + latency: 0, }); /** @@ -121,28 +126,16 @@ export const itemReducer = reducerFactory({} as ExploreItemSta .addMapper({ filter: addQueryRowAction, mapper: (state, action): ExploreItemState => { - const { queries, queryTransactions } = state; + const { queries } = state; const { index, query } = action.payload; // Add to queries, which will cause a new row to be rendered const nextQueries = [...queries.slice(0, index + 1), { ...query }, ...queries.slice(index + 1)]; - // Ongoing transactions need to update their row indices - const nextQueryTransactions = queryTransactions.map(qt => { - if (qt.rowIndex > index) { - return { - ...qt, - rowIndex: qt.rowIndex + 1, - }; - } - return qt; - }); - return { ...state, queries: nextQueries, logsHighlighterExpressions: undefined, - queryTransactions: nextQueryTransactions, queryKeys: getQueryKeys(nextQueries, state.datasourceInstance), }; }, @@ -150,21 +143,17 @@ export const itemReducer = reducerFactory({} as ExploreItemSta .addMapper({ filter: changeQueryAction, mapper: (state, action): ExploreItemState => { - const { queries, queryTransactions } = state; + const { queries } = state; const { query, index } = action.payload; // Override path: queries are completely reset - const nextQuery: DataQuery = { ...query, ...generateEmptyQuery(state.queries) }; + const nextQuery: DataQuery = generateNewKeyAndAddRefIdIfMissing(query, queries, index); const nextQueries = [...queries]; nextQueries[index] = nextQuery; - // Discard ongoing transaction related to row query - const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); - return { ...state, queries: nextQueries, - queryTransactions: nextQueryTransactions, queryKeys: getQueryKeys(nextQueries, state.datasourceInstance), }; }, @@ -199,7 +188,6 @@ export const itemReducer = reducerFactory({} as ExploreItemSta return { ...state, queries: queries.slice(), - queryTransactions: [], showingStartPage: Boolean(state.StartPage), queryKeys: getQueryKeys(queries, state.datasourceInstance), }; @@ -244,6 +232,11 @@ export const itemReducer = reducerFactory({} as ExploreItemSta return { ...state, datasourceInstance, + queryErrors: [], + latency: 0, + graphIsLoading: false, + logIsLoading: false, + tableIsLoading: false, supportsGraph, supportsLogs, supportsTable, @@ -284,7 +277,6 @@ export const itemReducer = reducerFactory({} as ExploreItemSta datasourceLoading: false, datasourceMissing: false, logsHighlighterExpressions: undefined, - queryTransactions: [], update: makeInitialUpdateState(), }; }, @@ -292,95 +284,87 @@ export const itemReducer = reducerFactory({} as ExploreItemSta .addMapper({ filter: modifyQueriesAction, mapper: (state, action): ExploreItemState => { - const { queries, queryTransactions } = state; + const { queries } = state; const { modification, index, modifier } = action.payload; let nextQueries: DataQuery[]; - let nextQueryTransactions: QueryTransaction[]; if (index === undefined) { // Modify all queries - nextQueries = queries.map((query, i) => ({ - ...modifier({ ...query }, modification), - ...generateEmptyQuery(state.queries), - })); - // Discard all ongoing transactions - nextQueryTransactions = []; + nextQueries = queries.map((query, i) => { + const nextQuery = modifier({ ...query }, modification); + return generateNewKeyAndAddRefIdIfMissing(nextQuery, queries, i); + }); } else { // Modify query only at index nextQueries = queries.map((query, i) => { - // Synchronize all queries with local query cache to ensure consistency - // TODO still needed? - return i === index - ? { ...modifier({ ...query }, modification), ...generateEmptyQuery(state.queries) } - : query; + if (i === index) { + const nextQuery = modifier({ ...query }, modification); + return generateNewKeyAndAddRefIdIfMissing(nextQuery, queries, i); + } + + return query; }); - nextQueryTransactions = queryTransactions - // Consume the hint corresponding to the action - .map(qt => { - if (qt.hints != null && qt.rowIndex === index) { - qt.hints = qt.hints.filter(hint => hint.fix.action !== modification); - } - return qt; - }) - // Preserve previous row query transaction to keep results visible if next query is incomplete - .filter(qt => modification.preventSubmit || qt.rowIndex !== index); } return { ...state, queries: nextQueries, queryKeys: getQueryKeys(nextQueries, state.datasourceInstance), - queryTransactions: nextQueryTransactions, }; }, }) .addMapper({ - filter: queryTransactionFailureAction, + filter: queryFailureAction, mapper: (state, action): ExploreItemState => { - const { queryTransactions } = action.payload; + const { resultType, response } = action.payload; + const queryErrors = state.queryErrors.concat(response); + return { ...state, - queryTransactions, + graphResult: resultType === 'Graph' ? null : state.graphResult, + tableResult: resultType === 'Table' ? null : state.tableResult, + logsResult: resultType === 'Logs' ? null : state.logsResult, + latency: 0, + queryErrors, + showingStartPage: false, + graphIsLoading: resultType === 'Graph' ? false : state.graphIsLoading, + logIsLoading: resultType === 'Logs' ? false : state.logIsLoading, + tableIsLoading: resultType === 'Table' ? false : state.tableIsLoading, + update: makeInitialUpdateState(), + }; + }, + }) + .addMapper({ + filter: queryStartAction, + mapper: (state, action): ExploreItemState => { + const { resultType } = action.payload; + + return { + ...state, + queryErrors: [], + latency: 0, + graphIsLoading: resultType === 'Graph' ? true : state.graphIsLoading, + logIsLoading: resultType === 'Logs' ? true : state.logIsLoading, + tableIsLoading: resultType === 'Table' ? true : state.tableIsLoading, showingStartPage: false, update: makeInitialUpdateState(), }; }, }) .addMapper({ - filter: queryTransactionStartAction, + filter: querySuccessAction, mapper: (state, action): ExploreItemState => { - const { queryTransactions } = state; - const { resultType, rowIndex, transaction } = action.payload; - // Discarding existing transactions of same type - const remainingTransactions = queryTransactions.filter( - qt => !(qt.resultType === resultType && qt.rowIndex === rowIndex) - ); - - // Append new transaction - const nextQueryTransactions: QueryTransaction[] = [...remainingTransactions, transaction]; + const { queryIntervals } = state; + const { result, resultType, latency } = action.payload; + const results = calculateResultsFromQueryTransactions(result, resultType, queryIntervals.intervalMs); return { ...state, - queryTransactions: nextQueryTransactions, - showingStartPage: false, - update: makeInitialUpdateState(), - }; - }, - }) - .addMapper({ - filter: queryTransactionSuccessAction, - mapper: (state, action): ExploreItemState => { - const { datasourceInstance, queryIntervals } = state; - const { history, queryTransactions } = action.payload; - const results = calculateResultsFromQueryTransactions( - queryTransactions, - datasourceInstance, - queryIntervals.intervalMs - ); - - return { - ...state, - ...results, - history, - queryTransactions, + graphResult: resultType === 'Graph' ? results.graphResult : state.graphResult, + tableResult: resultType === 'Table' ? results.tableResult : state.tableResult, + logsResult: resultType === 'Logs' ? results.logsResult : state.logsResult, + latency, + graphIsLoading: false, + logIsLoading: false, + tableIsLoading: false, showingStartPage: false, update: makeInitialUpdateState(), }; @@ -389,7 +373,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta .addMapper({ filter: removeQueryRowAction, mapper: (state, action): ExploreItemState => { - const { datasourceInstance, queries, queryIntervals, queryTransactions, queryKeys } = state; + const { queries, queryKeys } = state; const { index } = action.payload; if (queries.length <= 1) { @@ -399,20 +383,10 @@ export const itemReducer = reducerFactory({} as ExploreItemSta const nextQueries = [...queries.slice(0, index), ...queries.slice(index + 1)]; const nextQueryKeys = [...queryKeys.slice(0, index), ...queryKeys.slice(index + 1)]; - // Discard transactions related to row query - const nextQueryTransactions = queryTransactions.filter(qt => nextQueries.some(nq => nq.key === qt.query.key)); - const results = calculateResultsFromQueryTransactions( - nextQueryTransactions, - datasourceInstance, - queryIntervals.intervalMs - ); - return { ...state, - ...results, queries: nextQueries, logsHighlighterExpressions: undefined, - queryTransactions: nextQueryTransactions, queryKeys: nextQueryKeys, }; }, @@ -432,11 +406,8 @@ export const itemReducer = reducerFactory({} as ExploreItemSta .addMapper({ filter: scanStopAction, mapper: (state): ExploreItemState => { - const { queryTransactions } = state; - const nextQueryTransactions = queryTransactions.filter(qt => qt.scanning && !qt.done); return { ...state, - queryTransactions: nextQueryTransactions, scanning: false, scanRange: undefined, scanner: undefined, @@ -461,47 +432,15 @@ export const itemReducer = reducerFactory({} as ExploreItemSta return { ...state, ...action.payload }; }, }) - .addMapper({ - filter: toggleGraphAction, - mapper: (state): ExploreItemState => { - const showingGraph = !state.showingGraph; - let nextQueryTransactions = state.queryTransactions; - if (!showingGraph) { - // Discard transactions related to Graph query - nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Graph'); - } - return { ...state, queryTransactions: nextQueryTransactions }; - }, - }) - .addMapper({ - filter: toggleLogsAction, - mapper: (state): ExploreItemState => { - const showingLogs = !state.showingLogs; - let nextQueryTransactions = state.queryTransactions; - if (!showingLogs) { - // Discard transactions related to Logs query - nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Logs'); - } - return { ...state, queryTransactions: nextQueryTransactions }; - }, - }) .addMapper({ filter: toggleTableAction, mapper: (state): ExploreItemState => { const showingTable = !state.showingTable; if (showingTable) { - return { ...state, queryTransactions: state.queryTransactions }; + return { ...state }; } - // Toggle off needs discarding of table queries and results - const nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Table'); - const results = calculateResultsFromQueryTransactions( - nextQueryTransactions, - state.datasourceInstance, - state.queryIntervals.intervalMs - ); - - return { ...state, ...results, queryTransactions: nextQueryTransactions }; + return { ...state, tableResult: new TableModel() }; }, }) .addMapper({ @@ -549,7 +488,6 @@ export const itemReducer = reducerFactory({} as ExploreItemSta return { ...state, datasourceError: action.payload.error, - queryTransactions: [], graphResult: undefined, tableResult: undefined, logsResult: undefined, @@ -581,6 +519,33 @@ export const itemReducer = reducerFactory({} as ExploreItemSta }; }, }) + .addMapper({ + filter: historyUpdatedAction, + mapper: (state, action): ExploreItemState => { + return { + ...state, + history: action.payload.history, + }; + }, + }) + .addMapper({ + filter: resetQueryErrorAction, + mapper: (state, action): ExploreItemState => { + const { refIds } = action.payload; + const queryErrors = state.queryErrors.reduce((allErrors, error) => { + if (error.refId && refIds.indexOf(error.refId) !== -1) { + return allErrors; + } + + return allErrors.concat(error); + }, []); + + return { + ...state, + queryErrors, + }; + }, + }) .create(); export const updateChildRefreshState = ( diff --git a/public/app/plugins/datasource/loki/components/LokiCheatSheet.tsx b/public/app/plugins/datasource/loki/components/LokiCheatSheet.tsx index a7b865cde3f..35df8959e9d 100644 --- a/public/app/plugins/datasource/loki/components/LokiCheatSheet.tsx +++ b/public/app/plugins/datasource/loki/components/LokiCheatSheet.tsx @@ -31,7 +31,7 @@ export default (props: any) => ( {item.expression && (
props.onClickExample({ refId: '1', expr: item.expression })} + onClick={e => props.onClickExample({ refId: 'A', expr: item.expression })} > {item.expression}
diff --git a/public/app/plugins/datasource/loki/components/LokiQueryFieldForm.tsx b/public/app/plugins/datasource/loki/components/LokiQueryFieldForm.tsx index 547ecb90b95..76e94904c1f 100644 --- a/public/app/plugins/datasource/loki/components/LokiQueryFieldForm.tsx +++ b/public/app/plugins/datasource/loki/components/LokiQueryFieldForm.tsx @@ -86,14 +86,14 @@ export class LokiQueryFieldForm extends React.PureComponent node.type === 'code_block', getSyntax: (node: any) => 'promql', }), ]; - this.pluginsSearch = [RunnerPlugin({ handler: props.onExecuteQuery })]; + this.pluginsSearch = [RunnerPlugin({ handler: props.onRunQuery })]; } loadOptions = (selectedOptions: CascaderOption[]) => { @@ -111,24 +111,17 @@ export class LokiQueryFieldForm extends React.PureComponent { // Send text change to parent - const { query, onQueryChange, onExecuteQuery } = this.props; - if (onQueryChange) { + const { query, onChange, onRunQuery } = this.props; + if (onChange) { const nextQuery = { ...query, expr: value }; - onQueryChange(nextQuery); + onChange(nextQuery); - if (override && onExecuteQuery) { - onExecuteQuery(); + if (override && onRunQuery) { + onRunQuery(); } } }; - onClickHintFix = () => { - const { hint, onExecuteHint } = this.props; - if (onExecuteHint && hint && hint.fix) { - onExecuteHint(hint.fix.action); - } - }; - onTypeahead = (typeahead: TypeaheadInput): TypeaheadOutput => { const { datasource } = this.props; if (!datasource.languageProvider) { @@ -156,8 +149,7 @@ export class LokiQueryFieldForm extends React.PureComponent
- {error ?
{error}
: null} - {hint ? ( -
- {hint.label}{' '} - {hint.fix ? ( - - {hint.fix.label} - - ) : null} -
+ {queryResponse && queryResponse.error ? ( +
{queryResponse.error.message}
) : null}
diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index 4f22e41dead..3e7e5f3b396 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -15,10 +15,12 @@ import { SeriesData, DataSourceApi, DataSourceInstanceSettings, + DataQueryError, } from '@grafana/ui/src/types'; import { LokiQuery, LokiOptions } from './types'; import { BackendSrv } from 'app/core/services/backend_srv'; import { TemplateSrv } from 'app/features/templating/template_srv'; +import { safeStringifyValue } from 'app/core/utils/explore'; export const DEFAULT_MAX_LINES = 1000; @@ -65,16 +67,18 @@ export class LokiDatasource extends DataSourceApi { return this.backendSrv.datasourceRequest(req); } - prepareQueryTarget(target, options) { + prepareQueryTarget(target: LokiQuery, options: DataQueryRequest) { const interpolated = this.templateSrv.replace(target.expr); const start = this.getTime(options.range.from, false); const end = this.getTime(options.range.to, true); + const refId = target.refId; return { ...DEFAULT_QUERY_PARAMS, ...parseQuery(interpolated), start, end, limit: this.maxLines, + refId, }; } @@ -87,16 +91,47 @@ export class LokiDatasource extends DataSourceApi { return Promise.resolve({ data: [] }); } - const queries = queryTargets.map(target => this._request('/api/prom/query', target)); + const queries = queryTargets.map(target => + this._request('/api/prom/query', target).catch((err: any) => { + if (err.cancelled) { + return err; + } + + const error: DataQueryError = { + message: 'Unknown error during query transaction. Please check JS console logs.', + refId: target.refId, + }; + + if (err.data) { + if (typeof err.data === 'string') { + error.message = err.data; + } else if (err.data.error) { + error.message = safeStringifyValue(err.data.error); + } + } else if (err.message) { + error.message = err.message; + } else if (typeof err === 'string') { + error.message = err; + } + + error.status = err.status; + error.statusText = err.statusText; + + throw error; + }) + ); return Promise.all(queries).then((results: any[]) => { - const series: SeriesData[] = []; + const series: Array = []; for (let i = 0; i < results.length; i++) { const result = results[i]; + if (result.data) { + const refId = queryTargets[i].refId; for (const stream of result.data.streams || []) { const seriesData = logStreamToSeriesData(stream); + seriesData.refId = refId; seriesData.meta = { search: queryTargets[i].regexp, limit: this.maxLines, diff --git a/public/app/plugins/datasource/prometheus/components/PromCheatSheet.tsx b/public/app/plugins/datasource/prometheus/components/PromCheatSheet.tsx index 41606d198c6..f3b3dcb7f83 100644 --- a/public/app/plugins/datasource/prometheus/components/PromCheatSheet.tsx +++ b/public/app/plugins/datasource/prometheus/components/PromCheatSheet.tsx @@ -27,7 +27,7 @@ export default (props: any) => (
{item.title}
props.onClickExample({ refId: '1', expr: item.expression })} + onClick={e => props.onClickExample({ refId: 'A', expr: item.expression })} > {item.expression}
diff --git a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx index f8b2e79c60a..a63e2ef52b9 100644 --- a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx +++ b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx @@ -16,7 +16,7 @@ import RunnerPlugin from 'app/features/explore/slate-plugins/runner'; import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField'; import { PromQuery } from '../types'; import { CancelablePromise, makePromiseCancelable } from 'app/core/utils/CancelablePromise'; -import { ExploreDataSourceApi, ExploreQueryFieldProps, DataSourceStatus } from '@grafana/ui'; +import { ExploreDataSourceApi, ExploreQueryFieldProps, DataSourceStatus, QueryHint } from '@grafana/ui'; const HISTOGRAM_GROUP = '__histograms__'; const METRIC_MARK = 'metric'; @@ -109,6 +109,7 @@ interface PromQueryFieldProps extends ExploreQueryFieldProps { @@ -125,7 +126,7 @@ class PromQueryField extends React.PureComponent node.type === 'code_block', getSyntax: (node: any) => 'promql', @@ -135,6 +136,7 @@ class PromQueryField extends React.PureComponent 0; + if (currentHasSeries && prevProps.queryResponse.series !== this.props.queryResponse.series) { + this.refreshHint(); + } + const reconnected = prevProps.datasourceStatus === DataSourceStatus.Disconnected && this.props.datasourceStatus === DataSourceStatus.Connected; @@ -167,6 +175,17 @@ class PromQueryField extends React.PureComponent { + const { datasource, query, queryResponse } = this.props; + if (queryResponse.series && queryResponse.series.length === 0) { + return; + } + + const hints = datasource.getQueryHints(query, queryResponse.series); + const hint = hints && hints.length > 0 ? hints[0] : null; + this.setState({ hint }); + }; + refreshMetrics = (cancelablePromise: CancelablePromise) => { this.languageProviderInitializationPromise = cancelablePromise; this.languageProviderInitializationPromise.promise @@ -204,21 +223,22 @@ class PromQueryField extends React.PureComponent { // Send text change to parent - const { query, onQueryChange, onExecuteQuery } = this.props; - if (onQueryChange) { + const { query, onChange, onRunQuery } = this.props; + if (onChange) { const nextQuery: PromQuery = { ...query, expr: value }; - onQueryChange(nextQuery); + onChange(nextQuery); - if (override && onExecuteQuery) { - onExecuteQuery(); + if (override && onRunQuery) { + onRunQuery(); } } }; onClickHintFix = () => { - const { hint, onExecuteHint } = this.props; - if (onExecuteHint && hint && hint.fix) { - onExecuteHint(hint.fix.action); + const { hint } = this.state; + const { onHint } = this.props; + if (onHint && hint && hint.fix) { + onHint(hint.fix.action); } }; @@ -273,8 +293,8 @@ class PromQueryField extends React.PureComponent
- {error ?
{error}
: null} + {queryResponse && queryResponse.error ? ( +
{queryResponse.error.message}
+ ) : null} {hint ? (
{hint.label}{' '} diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 5b2470fbd1f..c80c30fc682 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -15,8 +15,15 @@ import { expandRecordingRules } from './language_utils'; // Types import { PromQuery, PromOptions } from './types'; -import { DataQueryRequest, DataSourceApi, AnnotationEvent, DataSourceInstanceSettings } from '@grafana/ui/src/types'; +import { + DataQueryRequest, + DataSourceApi, + AnnotationEvent, + DataSourceInstanceSettings, + DataQueryError, +} from '@grafana/ui/src/types'; import { ExploreUrlState } from 'app/types/explore'; +import { safeStringifyValue } from 'app/core/utils/explore'; import { TemplateSrv } from 'app/features/templating/template_srv'; import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; @@ -38,7 +45,7 @@ export class PrometheusDatasource extends DataSourceApi /** @ngInject */ constructor( instanceSettings: DataSourceInstanceSettings, - private $q, + private $q: angular.IQService, private backendSrv: BackendSrv, private templateSrv: TemplateSrv, private timeSrv: TimeSrv @@ -134,7 +141,7 @@ export class PrometheusDatasource extends DataSourceApi return this.templateSrv.variableExists(target.expr); } - query(options: DataQueryRequest) { + query(options: DataQueryRequest): Promise<{ data: any }> { const start = this.getPrometheusTime(options.range.from, false); const end = this.getPrometheusTime(options.range.to, true); @@ -154,7 +161,7 @@ export class PrometheusDatasource extends DataSourceApi // No valid targets, return the empty result to save a round trip. if (_.isEmpty(queries)) { - return this.$q.when({ data: [] }); + return this.$q.when({ data: [] }) as Promise<{ data: any }>; } const allQueryPromise = _.map(queries, query => { @@ -165,16 +172,12 @@ export class PrometheusDatasource extends DataSourceApi } }); - return this.$q.all(allQueryPromise).then(responseList => { + const allPromise = this.$q.all(allQueryPromise).then((responseList: any) => { let result = []; _.each(responseList, (response, index) => { - if (response.status === 'error') { - const error = { - index, - ...response.error, - }; - throw error; + if (response.cancelled) { + return; } // Keeping original start/end for transformers @@ -195,6 +198,8 @@ export class PrometheusDatasource extends DataSourceApi return { data: result }; }); + + return allPromise as Promise<{ data: any }>; } createQuery(target, options, start, end) { @@ -241,6 +246,7 @@ export class PrometheusDatasource extends DataSourceApi // Only replace vars in expression after having (possibly) updated interval vars query.expr = this.templateSrv.replace(expr, scopedVars, this.interpolateQueryExpr); query.requestId = options.panelId + target.refId; + query.refId = target.refId; // Align query interval with step to allow query caching and to ensure // that about-same-time query results look the same. @@ -276,7 +282,9 @@ export class PrometheusDatasource extends DataSourceApi if (this.queryTimeout) { data['timeout'] = this.queryTimeout; } - return this._request(url, data, { requestId: query.requestId, headers: query.headers }); + return this._request(url, data, { requestId: query.requestId, headers: query.headers }).catch((err: any) => + this.handleErrors(err, query) + ); } performInstantQuery(query, time) { @@ -288,9 +296,39 @@ export class PrometheusDatasource extends DataSourceApi if (this.queryTimeout) { data['timeout'] = this.queryTimeout; } - return this._request(url, data, { requestId: query.requestId, headers: query.headers }); + return this._request(url, data, { requestId: query.requestId, headers: query.headers }).catch((err: any) => + this.handleErrors(err, query) + ); } + handleErrors = (err: any, target: PromQuery) => { + if (err.cancelled) { + return err; + } + + const error: DataQueryError = { + message: 'Unknown error during query transaction. Please check JS console logs.', + refId: target.refId, + }; + + if (err.data) { + if (typeof err.data === 'string') { + error.message = err.data; + } else if (err.data.error) { + error.message = safeStringifyValue(err.data.error); + } + } else if (err.message) { + error.message = err.message; + } else if (typeof err === 'string') { + error.message = err; + } + + error.status = err.status; + error.statusText = err.statusText; + + throw error; + }; + performSuggestQuery(query, cache = false) { const url = '/api/v1/label/__name__/values'; diff --git a/public/app/plugins/datasource/prometheus/specs/completer.test.ts b/public/app/plugins/datasource/prometheus/specs/completer.test.ts index 693c850dafe..2580b87f6d7 100644 --- a/public/app/plugins/datasource/prometheus/specs/completer.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/completer.test.ts @@ -5,6 +5,7 @@ import { DataSourceInstanceSettings } from '@grafana/ui'; import { PromOptions } from '../types'; import { TemplateSrv } from 'app/features/templating/template_srv'; import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; +import { IQService } from 'angular'; jest.mock('../datasource'); jest.mock('app/core/services/backend_srv'); @@ -22,7 +23,7 @@ describe('Prometheus editor completer', () => { const backendSrv = {} as BackendSrv; const datasourceStub = new PrometheusDatasource( {} as DataSourceInstanceSettings, - {}, + {} as IQService, backendSrv, {} as TemplateSrv, {} as TimeSrv diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.test.ts b/public/app/plugins/datasource/prometheus/specs/datasource.test.ts index a38eb46ac03..275671e0283 100644 --- a/public/app/plugins/datasource/prometheus/specs/datasource.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/datasource.test.ts @@ -401,7 +401,7 @@ describe('PrometheusDatasource', () => { }, }; backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any); await ctx.ds.query(query).then(data => { results = data; @@ -451,7 +451,7 @@ describe('PrometheusDatasource', () => { }; backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any); await ctx.ds.query(query).then(data => { results = data; @@ -512,7 +512,7 @@ describe('PrometheusDatasource', () => { }; backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any); await ctx.ds.query(query).then(data => { results = data; @@ -569,7 +569,7 @@ describe('PrometheusDatasource', () => { beforeEach(async () => { options.annotation.useValueForTime = false; backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any); await ctx.ds.annotationQuery(options).then(data => { results = data; @@ -589,7 +589,7 @@ describe('PrometheusDatasource', () => { beforeEach(async () => { options.annotation.useValueForTime = true; backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any); await ctx.ds.annotationQuery(options).then(data => { results = data; @@ -604,7 +604,7 @@ describe('PrometheusDatasource', () => { describe('step parameter', () => { beforeEach(() => { backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any); }); it('should use default step for short range if no interval is given', () => { @@ -700,7 +700,7 @@ describe('PrometheusDatasource', () => { }; backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any); await ctx.ds.query(query).then(data => { results = data; }); @@ -737,7 +737,7 @@ describe('PrometheusDatasource', () => { const urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=10'; backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any); await ctx.ds.query(query); const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); @@ -753,7 +753,7 @@ describe('PrometheusDatasource', () => { }; const urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=1'; backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any); await ctx.ds.query(query); const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); @@ -774,7 +774,7 @@ describe('PrometheusDatasource', () => { }; const urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=10'; backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any); await ctx.ds.query(query); const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); @@ -791,7 +791,7 @@ describe('PrometheusDatasource', () => { const start = 60 * 60; const urlExpected = 'proxied/api/v1/query_range?query=test&start=' + start + '&end=' + end + '&step=2'; backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any); await ctx.ds.query(query); const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); @@ -813,7 +813,7 @@ describe('PrometheusDatasource', () => { // times get rounded up to interval const urlExpected = 'proxied/api/v1/query_range?query=test&start=50&end=400&step=50'; backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any); await ctx.ds.query(query); const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); @@ -834,7 +834,7 @@ describe('PrometheusDatasource', () => { }; const urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=60&end=420&step=15'; backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any); await ctx.ds.query(query); const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); @@ -856,7 +856,7 @@ describe('PrometheusDatasource', () => { // times get aligned to interval const urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=0&end=400&step=100'; backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any); await ctx.ds.query(query); const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); @@ -878,7 +878,7 @@ describe('PrometheusDatasource', () => { const start = 0; const urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=' + start + '&end=' + end + '&step=100'; backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any); await ctx.ds.query(query); const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); @@ -900,7 +900,7 @@ describe('PrometheusDatasource', () => { const start = 0; const urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=' + start + '&end=' + end + '&step=60'; backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any); await ctx.ds.query(query); const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); @@ -943,7 +943,7 @@ describe('PrometheusDatasource', () => { templateSrv.replace = jest.fn(str => str); backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any); await ctx.ds.query(query); const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); @@ -983,7 +983,7 @@ describe('PrometheusDatasource', () => { '&start=60&end=420&step=10'; backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); templateSrv.replace = jest.fn(str => str); - ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any); await ctx.ds.query(query); const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); @@ -1024,7 +1024,7 @@ describe('PrometheusDatasource', () => { '&start=0&end=400&step=100'; backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); templateSrv.replace = jest.fn(str => str); - ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any); await ctx.ds.query(query); const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); @@ -1071,7 +1071,7 @@ describe('PrometheusDatasource', () => { templateSrv.replace = jest.fn(str => str); backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any); await ctx.ds.query(query); const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); @@ -1112,7 +1112,7 @@ describe('PrometheusDatasource', () => { '&start=60&end=420&step=15'; backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any); await ctx.ds.query(query); const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); @@ -1158,7 +1158,7 @@ describe('PrometheusDatasource', () => { '&step=60'; backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); templateSrv.replace = jest.fn(str => str); - ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any); await ctx.ds.query(query); const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(res.method).toBe('GET'); @@ -1220,7 +1220,7 @@ describe('PrometheusDatasource for POST', () => { }, }; backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any); await ctx.ds.query(query).then(data => { results = data; }); @@ -1245,7 +1245,7 @@ describe('PrometheusDatasource for POST', () => { }; it('with proxy access tracing headers should be added', () => { - ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any); ctx.ds._addTracingHeaders(httpOptions, options); expect(httpOptions.headers['X-Dashboard-Id']).toBe(1); expect(httpOptions.headers['X-Panel-Id']).toBe(2); @@ -1253,7 +1253,7 @@ describe('PrometheusDatasource for POST', () => { it('with direct access tracing headers should not be added', () => { instanceSettings.url = 'http://127.0.0.1:8000'; - ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any); ctx.ds._addTracingHeaders(httpOptions, options); expect(httpOptions.headers['X-Dashboard-Id']).toBe(undefined); expect(httpOptions.headers['X-Panel-Id']).toBe(undefined); diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index c057785bb90..404e53c2832 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -10,6 +10,7 @@ import { ExploreStartPageProps, LogLevel, TimeRange, + DataQueryError, } from '@grafana/ui'; import { Emitter, TimeSeries } from 'app/core/core'; @@ -178,14 +179,6 @@ export interface ExploreItemState { * Needs to be updated when `datasourceInstance` or `containerWidth` is changed. */ queryIntervals: QueryIntervals; - /** - * List of query transaction to track query duration and query result. - * Graph/Logs/Table results are calculated on the fly from the transaction, - * based on the transaction's result types. Transaction also holds the row index - * so that results can be dropped and re-computed without running queries again - * when query rows are removed. - */ - queryTransactions: QueryTransaction[]; /** * Time range for this Explore. Managed by the time picker and used by all query runs. */ @@ -230,6 +223,10 @@ export interface ExploreItemState { * True if `datasourceInstance` supports table queries. */ supportsTable: boolean | null; + + graphIsLoading: boolean; + logIsLoading: boolean; + tableIsLoading: boolean; /** * Table model that combines all query table results into a single table. */ @@ -258,6 +255,9 @@ export interface ExploreItemState { urlState: ExploreUrlState; update: ExploreUpdateState; + + queryErrors: DataQueryError[]; + latency: number; } export interface ExploreUpdateState { @@ -332,10 +332,9 @@ export interface QueryTransaction { hints?: QueryHint[]; latency: number; options: any; - query: DataQuery; + queries: DataQuery[]; result?: any; // Table model / Timeseries[] / Logs resultType: ResultType; - rowIndex: number; scanning?: boolean; }