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

* Wip: Removes queryTransactions from state

* Refactor: Adds back query failures

* Refactor: Moves error parsing to datasources

* Refactor: Adds back hinting for Prometheus

* Refactor: removed commented out code

* Refactor: Adds back QueryStatus

* Refactor: Adds scanning back to Explore

* Fix: Fixes prettier error

* Fix: Makes sure there is an error

* Merge: Merges with master

* Fix: Adds safeStringifyValue to error parsing

* Fix: Fixes table result calculations

* Refactor: Adds ErrorContainer and generic error handling in Explore

* Fix: Fixes so refIds remain consistent

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

* Fix: Fixes digest issue with Angular editors

* Refactor: Adds tests for explore utils

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

* Fix: Fixes Prettier error

* Fix: Adds back console.log for erroneous querys

* Refactor: Changes console.log to console.error
This commit is contained in:
Hugo Häggmark 2019-05-10 14:00:39 +02:00 committed by GitHub
parent 8eb78ea931
commit 6dbaa704bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 871 additions and 549 deletions

View File

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

View File

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

View File

@ -5,10 +5,15 @@ import {
updateHistory, updateHistory,
clearHistory, clearHistory,
hasNonEmptyQuery, hasNonEmptyQuery,
instanceOfDataQueryError,
getValueWithRefId,
getFirstQueryErrorWithoutRefId,
getRefIds,
} from './explore'; } from './explore';
import { ExploreUrlState } from 'app/types/explore'; import { ExploreUrlState } from 'app/types/explore';
import store from 'app/core/store'; import store from 'app/core/store';
import { LogsDedupStrategy } from 'app/core/logs_model'; import { LogsDedupStrategy } from 'app/core/logs_model';
import { DataQueryError } from '@grafana/ui';
const DEFAULT_EXPLORE_STATE: ExploreUrlState = { const DEFAULT_EXPLORE_STATE: ExploreUrlState = {
datasource: null, datasource: null,
@ -188,3 +193,164 @@ describe('hasNonEmptyQuery', () => {
expect(hasNonEmptyQuery([])).toBeFalsy(); 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']);
});
});
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,8 +12,8 @@ import 'app/features/plugins/plugin_loader';
import { dateTime } from '@grafana/ui/src/utils/moment_wrapper'; import { dateTime } from '@grafana/ui/src/utils/moment_wrapper';
interface QueryEditorProps { interface QueryEditorProps {
error?: any;
datasource: any; datasource: any;
error?: string | JSX.Element;
onExecuteQuery?: () => void; onExecuteQuery?: () => void;
onQueryChange?: (value: DataQuery) => void; onQueryChange?: (value: DataQuery) => void;
initialQuery: DataQuery; initialQuery: DataQuery;
@ -57,6 +57,14 @@ export default class QueryEditor extends PureComponent<QueryEditorProps, any> {
this.props.onQueryChange(target); 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() { componentWillUnmount() {
if (this.component) { if (this.component) {
this.component.destroy(); this.component.destroy();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,10 +15,12 @@ import {
SeriesData, SeriesData,
DataSourceApi, DataSourceApi,
DataSourceInstanceSettings, DataSourceInstanceSettings,
DataQueryError,
} from '@grafana/ui/src/types'; } from '@grafana/ui/src/types';
import { LokiQuery, LokiOptions } from './types'; import { LokiQuery, LokiOptions } from './types';
import { BackendSrv } from 'app/core/services/backend_srv'; import { BackendSrv } from 'app/core/services/backend_srv';
import { TemplateSrv } from 'app/features/templating/template_srv'; import { TemplateSrv } from 'app/features/templating/template_srv';
import { safeStringifyValue } from 'app/core/utils/explore';
export const DEFAULT_MAX_LINES = 1000; export const DEFAULT_MAX_LINES = 1000;
@ -65,16 +67,18 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
return this.backendSrv.datasourceRequest(req); return this.backendSrv.datasourceRequest(req);
} }
prepareQueryTarget(target, options) { prepareQueryTarget(target: LokiQuery, options: DataQueryRequest<LokiQuery>) {
const interpolated = this.templateSrv.replace(target.expr); const interpolated = this.templateSrv.replace(target.expr);
const start = this.getTime(options.range.from, false); const start = this.getTime(options.range.from, false);
const end = this.getTime(options.range.to, true); const end = this.getTime(options.range.to, true);
const refId = target.refId;
return { return {
...DEFAULT_QUERY_PARAMS, ...DEFAULT_QUERY_PARAMS,
...parseQuery(interpolated), ...parseQuery(interpolated),
start, start,
end, end,
limit: this.maxLines, limit: this.maxLines,
refId,
}; };
} }
@ -87,16 +91,47 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
return Promise.resolve({ data: [] }); 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[]) => { return Promise.all(queries).then((results: any[]) => {
const series: SeriesData[] = []; const series: Array<SeriesData | DataQueryError> = [];
for (let i = 0; i < results.length; i++) { for (let i = 0; i < results.length; i++) {
const result = results[i]; const result = results[i];
if (result.data) { if (result.data) {
const refId = queryTargets[i].refId;
for (const stream of result.data.streams || []) { for (const stream of result.data.streams || []) {
const seriesData = logStreamToSeriesData(stream); const seriesData = logStreamToSeriesData(stream);
seriesData.refId = refId;
seriesData.meta = { seriesData.meta = {
search: queryTargets[i].regexp, search: queryTargets[i].regexp,
limit: this.maxLines, limit: this.maxLines,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,6 +10,7 @@ import {
ExploreStartPageProps, ExploreStartPageProps,
LogLevel, LogLevel,
TimeRange, TimeRange,
DataQueryError,
} from '@grafana/ui'; } from '@grafana/ui';
import { Emitter, TimeSeries } from 'app/core/core'; import { Emitter, TimeSeries } from 'app/core/core';
@ -178,14 +179,6 @@ export interface ExploreItemState {
* Needs to be updated when `datasourceInstance` or `containerWidth` is changed. * Needs to be updated when `datasourceInstance` or `containerWidth` is changed.
*/ */
queryIntervals: QueryIntervals; 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. * 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. * True if `datasourceInstance` supports table queries.
*/ */
supportsTable: boolean | null; supportsTable: boolean | null;
graphIsLoading: boolean;
logIsLoading: boolean;
tableIsLoading: boolean;
/** /**
* Table model that combines all query table results into a single table. * Table model that combines all query table results into a single table.
*/ */
@ -258,6 +255,9 @@ export interface ExploreItemState {
urlState: ExploreUrlState; urlState: ExploreUrlState;
update: ExploreUpdateState; update: ExploreUpdateState;
queryErrors: DataQueryError[];
latency: number;
} }
export interface ExploreUpdateState { export interface ExploreUpdateState {
@ -332,10 +332,9 @@ export interface QueryTransaction {
hints?: QueryHint[]; hints?: QueryHint[];
latency: number; latency: number;
options: any; options: any;
query: DataQuery; queries: DataQuery[];
result?: any; // Table model / Timeseries[] / Logs result?: any; // Table model / Timeseries[] / Logs
resultType: ResultType; resultType: ResultType;
rowIndex: number;
scanning?: boolean; scanning?: boolean;
} }