mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
8eb78ea931
commit
6dbaa704bc
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -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']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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));
|
||||
};
|
||||
|
32
public/app/features/explore/ErrorContainer.tsx
Normal file
32
public/app/features/explore/ErrorContainer.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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) };
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
47
public/app/features/explore/QueryStatus.tsx
Normal file
47
public/app/features/explore/QueryStatus.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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 };
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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 = (
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
</>
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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}{' '}
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user