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

View File

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

View File

@ -5,10 +5,15 @@ import {
updateHistory,
clearHistory,
hasNonEmptyQuery,
instanceOfDataQueryError,
getValueWithRefId,
getFirstQueryErrorWithoutRefId,
getRefIds,
} from './explore';
import { ExploreUrlState } from 'app/types/explore';
import store from 'app/core/store';
import { LogsDedupStrategy } from 'app/core/logs_model';
import { DataQueryError } from '@grafana/ui';
const DEFAULT_EXPLORE_STATE: ExploreUrlState = {
datasource: null,
@ -188,3 +193,164 @@ describe('hasNonEmptyQuery', () => {
expect(hasNonEmptyQuery([])).toBeFalsy();
});
});
describe('instanceOfDataQueryError', () => {
describe('when called with a DataQueryError', () => {
it('then it should return true', () => {
const error: DataQueryError = {
message: 'A message',
status: '200',
statusText: 'Ok',
};
const result = instanceOfDataQueryError(error);
expect(result).toBe(true);
});
});
describe('when called with a non DataQueryError', () => {
it('then it should return false', () => {
const error = {};
const result = instanceOfDataQueryError(error);
expect(result).toBe(false);
});
});
});
describe('hasRefId', () => {
describe('when called with a null value', () => {
it('then it should return null', () => {
const input = null;
const result = getValueWithRefId(input);
expect(result).toBeNull();
});
});
describe('when called with a non object value', () => {
it('then it should return null', () => {
const input = 123;
const result = getValueWithRefId(input);
expect(result).toBeNull();
});
});
describe('when called with an object that has refId', () => {
it('then it should return the object', () => {
const input = { refId: 'A' };
const result = getValueWithRefId(input);
expect(result).toBe(input);
});
});
describe('when called with an array that has refId', () => {
it('then it should return the object', () => {
const input = [123, null, {}, { refId: 'A' }];
const result = getValueWithRefId(input);
expect(result).toBe(input[3]);
});
});
describe('when called with an object that has refId somewhere in the object tree', () => {
it('then it should return the object', () => {
const input: any = { data: [123, null, {}, { series: [123, null, {}, { refId: 'A' }] }] };
const result = getValueWithRefId(input);
expect(result).toBe(input.data[3].series[3]);
});
});
});
describe('getFirstQueryErrorWithoutRefId', () => {
describe('when called with a null value', () => {
it('then it should return null', () => {
const errors: DataQueryError[] = null;
const result = getFirstQueryErrorWithoutRefId(errors);
expect(result).toBeNull();
});
});
describe('when called with an array with only refIds', () => {
it('then it should return undefined', () => {
const errors: DataQueryError[] = [{ refId: 'A' }, { refId: 'B' }];
const result = getFirstQueryErrorWithoutRefId(errors);
expect(result).toBeUndefined();
});
});
describe('when called with an array with and without refIds', () => {
it('then it should return undefined', () => {
const errors: DataQueryError[] = [
{ refId: 'A' },
{ message: 'A message' },
{ refId: 'B' },
{ message: 'B message' },
];
const result = getFirstQueryErrorWithoutRefId(errors);
expect(result).toBe(errors[1]);
});
});
});
describe('getRefIds', () => {
describe('when called with a null value', () => {
it('then it should return empty array', () => {
const input = null;
const result = getRefIds(input);
expect(result).toEqual([]);
});
});
describe('when called with a non object value', () => {
it('then it should return empty array', () => {
const input = 123;
const result = getRefIds(input);
expect(result).toEqual([]);
});
});
describe('when called with an object that has refId', () => {
it('then it should return an array with that refId', () => {
const input = { refId: 'A' };
const result = getRefIds(input);
expect(result).toEqual(['A']);
});
});
describe('when called with an array that has refIds', () => {
it('then it should return an array with unique refIds', () => {
const input = [123, null, {}, { refId: 'A' }, { refId: 'A' }, { refId: 'B' }];
const result = getRefIds(input);
expect(result).toEqual(['A', 'B']);
});
});
describe('when called with an object that has refIds somewhere in the object tree', () => {
it('then it should return return an array with unique refIds', () => {
const input: any = {
data: [
123,
null,
{ refId: 'B', series: [{ refId: 'X' }] },
{ refId: 'B' },
{},
{ series: [123, null, {}, { refId: 'A' }] },
],
};
const result = getRefIds(input);
expect(result).toEqual(['B', 'X', 'A']);
});
});
});

View File

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

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

View File

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

View File

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

View File

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

View File

@ -12,8 +12,8 @@ import 'app/features/plugins/plugin_loader';
import { dateTime } from '@grafana/ui/src/utils/moment_wrapper';
interface QueryEditorProps {
error?: any;
datasource: any;
error?: string | JSX.Element;
onExecuteQuery?: () => void;
onQueryChange?: (value: DataQuery) => void;
initialQuery: DataQuery;
@ -57,6 +57,14 @@ export default class QueryEditor extends PureComponent<QueryEditorProps, any> {
this.props.onQueryChange(target);
}
componentDidUpdate(prevProps: QueryEditorProps) {
if (prevProps.error !== this.props.error && this.component) {
// Some query controllers listen to data error events and need a digest
// for some reason this needs to be done in next tick
setTimeout(this.component.digest);
}
}
componentWillUnmount() {
if (this.component) {
this.component.destroy();

View File

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

View File

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

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 }) {
const explore = state.explore;
const item: ExploreItemState = explore[exploreId];
const { queryTransactions, showingTable, tableResult } = item;
const loading = queryTransactions.some(qt => qt.resultType === 'Table' && !qt.done);
const { tableIsLoading, showingTable, tableResult } = item;
const loading = tableIsLoading;
return { loading, showingTable, tableResult };
}

View File

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

View File

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

View File

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

View File

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

View File

@ -31,7 +31,7 @@ export default (props: any) => (
{item.expression && (
<div
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>
</div>

View File

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

View File

@ -15,10 +15,12 @@ import {
SeriesData,
DataSourceApi,
DataSourceInstanceSettings,
DataQueryError,
} from '@grafana/ui/src/types';
import { LokiQuery, LokiOptions } from './types';
import { BackendSrv } from 'app/core/services/backend_srv';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { safeStringifyValue } from 'app/core/utils/explore';
export const DEFAULT_MAX_LINES = 1000;
@ -65,16 +67,18 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
return this.backendSrv.datasourceRequest(req);
}
prepareQueryTarget(target, options) {
prepareQueryTarget(target: LokiQuery, options: DataQueryRequest<LokiQuery>) {
const interpolated = this.templateSrv.replace(target.expr);
const start = this.getTime(options.range.from, false);
const end = this.getTime(options.range.to, true);
const refId = target.refId;
return {
...DEFAULT_QUERY_PARAMS,
...parseQuery(interpolated),
start,
end,
limit: this.maxLines,
refId,
};
}
@ -87,16 +91,47 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
return Promise.resolve({ data: [] });
}
const queries = queryTargets.map(target => this._request('/api/prom/query', target));
const queries = queryTargets.map(target =>
this._request('/api/prom/query', target).catch((err: any) => {
if (err.cancelled) {
return err;
}
const error: DataQueryError = {
message: 'Unknown error during query transaction. Please check JS console logs.',
refId: target.refId,
};
if (err.data) {
if (typeof err.data === 'string') {
error.message = err.data;
} else if (err.data.error) {
error.message = safeStringifyValue(err.data.error);
}
} else if (err.message) {
error.message = err.message;
} else if (typeof err === 'string') {
error.message = err;
}
error.status = err.status;
error.statusText = err.statusText;
throw error;
})
);
return Promise.all(queries).then((results: any[]) => {
const series: SeriesData[] = [];
const series: Array<SeriesData | DataQueryError> = [];
for (let i = 0; i < results.length; i++) {
const result = results[i];
if (result.data) {
const refId = queryTargets[i].refId;
for (const stream of result.data.streams || []) {
const seriesData = logStreamToSeriesData(stream);
seriesData.refId = refId;
seriesData.meta = {
search: queryTargets[i].regexp,
limit: this.maxLines,

View File

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

View File

@ -15,8 +15,15 @@ import { expandRecordingRules } from './language_utils';
// Types
import { PromQuery, PromOptions } from './types';
import { DataQueryRequest, DataSourceApi, AnnotationEvent, DataSourceInstanceSettings } from '@grafana/ui/src/types';
import {
DataQueryRequest,
DataSourceApi,
AnnotationEvent,
DataSourceInstanceSettings,
DataQueryError,
} from '@grafana/ui/src/types';
import { ExploreUrlState } from 'app/types/explore';
import { safeStringifyValue } from 'app/core/utils/explore';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
@ -38,7 +45,7 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
/** @ngInject */
constructor(
instanceSettings: DataSourceInstanceSettings<PromOptions>,
private $q,
private $q: angular.IQService,
private backendSrv: BackendSrv,
private templateSrv: TemplateSrv,
private timeSrv: TimeSrv
@ -134,7 +141,7 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
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 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.
if (_.isEmpty(queries)) {
return this.$q.when({ data: [] });
return this.$q.when({ data: [] }) as Promise<{ data: any }>;
}
const allQueryPromise = _.map(queries, query => {
@ -165,16 +172,12 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
}
});
return this.$q.all(allQueryPromise).then(responseList => {
const allPromise = this.$q.all(allQueryPromise).then((responseList: any) => {
let result = [];
_.each(responseList, (response, index) => {
if (response.status === 'error') {
const error = {
index,
...response.error,
};
throw error;
if (response.cancelled) {
return;
}
// Keeping original start/end for transformers
@ -195,6 +198,8 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
return { data: result };
});
return allPromise as Promise<{ data: any }>;
}
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
query.expr = this.templateSrv.replace(expr, scopedVars, this.interpolateQueryExpr);
query.requestId = options.panelId + target.refId;
query.refId = target.refId;
// Align query interval with step to allow query caching and to ensure
// that about-same-time query results look the same.
@ -276,7 +282,9 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
if (this.queryTimeout) {
data['timeout'] = this.queryTimeout;
}
return this._request(url, data, { requestId: query.requestId, headers: query.headers });
return this._request(url, data, { requestId: query.requestId, headers: query.headers }).catch((err: any) =>
this.handleErrors(err, query)
);
}
performInstantQuery(query, time) {
@ -288,9 +296,39 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
if (this.queryTimeout) {
data['timeout'] = this.queryTimeout;
}
return this._request(url, data, { requestId: query.requestId, headers: query.headers });
return this._request(url, data, { requestId: query.requestId, headers: query.headers }).catch((err: any) =>
this.handleErrors(err, query)
);
}
handleErrors = (err: any, target: PromQuery) => {
if (err.cancelled) {
return err;
}
const error: DataQueryError = {
message: 'Unknown error during query transaction. Please check JS console logs.',
refId: target.refId,
};
if (err.data) {
if (typeof err.data === 'string') {
error.message = err.data;
} else if (err.data.error) {
error.message = safeStringifyValue(err.data.error);
}
} else if (err.message) {
error.message = err.message;
} else if (typeof err === 'string') {
error.message = err;
}
error.status = err.status;
error.statusText = err.statusText;
throw error;
};
performSuggestQuery(query, cache = false) {
const url = '/api/v1/label/__name__/values';

View File

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

View File

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

View File

@ -10,6 +10,7 @@ import {
ExploreStartPageProps,
LogLevel,
TimeRange,
DataQueryError,
} from '@grafana/ui';
import { Emitter, TimeSeries } from 'app/core/core';
@ -178,14 +179,6 @@ export interface ExploreItemState {
* Needs to be updated when `datasourceInstance` or `containerWidth` is changed.
*/
queryIntervals: QueryIntervals;
/**
* List of query transaction to track query duration and query result.
* Graph/Logs/Table results are calculated on the fly from the transaction,
* based on the transaction's result types. Transaction also holds the row index
* so that results can be dropped and re-computed without running queries again
* when query rows are removed.
*/
queryTransactions: QueryTransaction[];
/**
* Time range for this Explore. Managed by the time picker and used by all query runs.
*/
@ -230,6 +223,10 @@ export interface ExploreItemState {
* True if `datasourceInstance` supports table queries.
*/
supportsTable: boolean | null;
graphIsLoading: boolean;
logIsLoading: boolean;
tableIsLoading: boolean;
/**
* Table model that combines all query table results into a single table.
*/
@ -258,6 +255,9 @@ export interface ExploreItemState {
urlState: ExploreUrlState;
update: ExploreUpdateState;
queryErrors: DataQueryError[];
latency: number;
}
export interface ExploreUpdateState {
@ -332,10 +332,9 @@ export interface QueryTransaction {
hints?: QueryHint[];
latency: number;
options: any;
query: DataQuery;
queries: DataQuery[];
result?: any; // Table model / Timeseries[] / Logs
resultType: ResultType;
rowIndex: number;
scanning?: boolean;
}