mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Explore: query transactions
Existing querying was grouped together before handed over to the datasource. This slowed down result display to however long the slowest query took. - create one query transaction per result viewer (graph, table, etc.) and query row - track latencies for each transaction - show results as soon as they are being received - loading indicator on graph and query button to indicate that queries are still running and that results are incomplete - properly discard transactions when removing or changing queries
This commit is contained in:
@@ -8,23 +8,18 @@ const DEFAULT_EXPLORE_STATE: ExploreState = {
|
||||
datasourceMissing: false,
|
||||
datasourceName: '',
|
||||
exploreDatasources: [],
|
||||
graphResult: null,
|
||||
graphRange: DEFAULT_RANGE,
|
||||
history: [],
|
||||
latency: 0,
|
||||
loading: false,
|
||||
logsResult: null,
|
||||
queries: [],
|
||||
queryErrors: [],
|
||||
queryHints: [],
|
||||
queryTransactions: [],
|
||||
range: DEFAULT_RANGE,
|
||||
requestOptions: null,
|
||||
showingGraph: true,
|
||||
showingLogs: true,
|
||||
showingTable: true,
|
||||
supportsGraph: null,
|
||||
supportsLogs: null,
|
||||
supportsTable: null,
|
||||
tableResult: null,
|
||||
};
|
||||
|
||||
describe('state functions', () => {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React from 'react';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import Select from 'react-select';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { ExploreState, ExploreUrlState, Query } from 'app/types/explore';
|
||||
import { ExploreState, ExploreUrlState, HistoryItem, Query, QueryTransaction, Range } from 'app/types/explore';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import colors from 'app/core/utils/colors';
|
||||
import store from 'app/core/store';
|
||||
@@ -15,7 +16,6 @@ import IndicatorsContainer from 'app/core/components/Picker/IndicatorsContainer'
|
||||
import NoOptionsMessage from 'app/core/components/Picker/NoOptionsMessage';
|
||||
import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
|
||||
|
||||
import ElapsedTime from './ElapsedTime';
|
||||
import QueryRows from './QueryRows';
|
||||
import Graph from './Graph';
|
||||
import Logs from './Logs';
|
||||
@@ -53,6 +53,25 @@ function makeTimeSeriesList(dataList, options) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the query history. Side-effect: store history in local storage
|
||||
*/
|
||||
function updateHistory(history: HistoryItem[], datasourceId: string, queries: string[]): HistoryItem[] {
|
||||
const ts = Date.now();
|
||||
queries.forEach(query => {
|
||||
history = [{ query, ts }, ...history];
|
||||
});
|
||||
|
||||
if (history.length > MAX_HISTORY_ITEMS) {
|
||||
history = history.slice(0, MAX_HISTORY_ITEMS);
|
||||
}
|
||||
|
||||
// Combine all queries of a datasource type into one history
|
||||
const historyKey = `grafana.explore.history.${datasourceId}`;
|
||||
store.setObject(historyKey, history);
|
||||
return history;
|
||||
}
|
||||
|
||||
interface ExploreProps {
|
||||
datasourceSrv: any;
|
||||
onChangeSplit: (split: boolean, state?: ExploreState) => void;
|
||||
@@ -83,6 +102,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
} else {
|
||||
const { datasource, queries, range } = props.urlState as ExploreUrlState;
|
||||
initialQueries = ensureQueries(queries);
|
||||
const initialRange = range || { ...DEFAULT_RANGE };
|
||||
this.state = {
|
||||
datasource: null,
|
||||
datasourceError: null,
|
||||
@@ -90,23 +110,18 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
datasourceMissing: false,
|
||||
datasourceName: datasource,
|
||||
exploreDatasources: [],
|
||||
graphResult: null,
|
||||
graphRange: initialRange,
|
||||
history: [],
|
||||
latency: 0,
|
||||
loading: false,
|
||||
logsResult: null,
|
||||
queries: initialQueries,
|
||||
queryErrors: [],
|
||||
queryHints: [],
|
||||
range: range || { ...DEFAULT_RANGE },
|
||||
requestOptions: null,
|
||||
queryTransactions: [],
|
||||
range: initialRange,
|
||||
showingGraph: true,
|
||||
showingLogs: true,
|
||||
showingTable: true,
|
||||
supportsGraph: null,
|
||||
supportsLogs: null,
|
||||
supportsTable: null,
|
||||
tableResult: null,
|
||||
};
|
||||
}
|
||||
this.queryExpressions = initialQueries.map(q => q.query);
|
||||
@@ -200,14 +215,30 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
};
|
||||
|
||||
onAddQueryRow = index => {
|
||||
const { queries } = this.state;
|
||||
const { queries, queryTransactions } = this.state;
|
||||
|
||||
// Local cache
|
||||
this.queryExpressions[index + 1] = '';
|
||||
|
||||
// Add row by generating new react key
|
||||
const nextQueries = [
|
||||
...queries.slice(0, index + 1),
|
||||
{ query: '', key: generateQueryKey() },
|
||||
...queries.slice(index + 1),
|
||||
];
|
||||
this.setState({ queries: nextQueries });
|
||||
|
||||
// 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;
|
||||
});
|
||||
|
||||
this.setState({ queries: nextQueries, queryTransactions: nextQueryTransactions });
|
||||
};
|
||||
|
||||
onChangeDatasource = async option => {
|
||||
@@ -215,12 +246,8 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
datasource: null,
|
||||
datasourceError: null,
|
||||
datasourceLoading: true,
|
||||
graphResult: null,
|
||||
latency: 0,
|
||||
logsResult: null,
|
||||
queryErrors: [],
|
||||
queryHints: [],
|
||||
tableResult: null,
|
||||
queryTransactions: [],
|
||||
});
|
||||
const datasourceName = option.value;
|
||||
const datasource = await this.props.datasourceSrv.get(datasourceName);
|
||||
@@ -231,9 +258,9 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
// Keep current value in local cache
|
||||
this.queryExpressions[index] = value;
|
||||
|
||||
// Replace query row on override
|
||||
if (override) {
|
||||
const { queries } = this.state;
|
||||
// Replace query row
|
||||
const { queries, queryTransactions } = this.state;
|
||||
const nextQuery: Query = {
|
||||
key: generateQueryKey(index),
|
||||
query: value,
|
||||
@@ -241,11 +268,14 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
const nextQueries = [...queries];
|
||||
nextQueries[index] = nextQuery;
|
||||
|
||||
// Discard ongoing transaction related to row query
|
||||
const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index);
|
||||
|
||||
this.setState(
|
||||
{
|
||||
queryErrors: [],
|
||||
queryHints: [],
|
||||
queries: nextQueries,
|
||||
queryHints: [],
|
||||
queryTransactions: nextQueryTransactions,
|
||||
},
|
||||
this.onSubmit
|
||||
);
|
||||
@@ -264,13 +294,9 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
this.queryExpressions = [''];
|
||||
this.setState(
|
||||
{
|
||||
graphResult: null,
|
||||
logsResult: null,
|
||||
latency: 0,
|
||||
queries: ensureQueries(),
|
||||
queryErrors: [],
|
||||
queryHints: [],
|
||||
tableResult: null,
|
||||
queryTransactions: [],
|
||||
},
|
||||
this.saveState
|
||||
);
|
||||
@@ -308,15 +334,18 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
};
|
||||
|
||||
onModifyQueries = (action: object, index?: number) => {
|
||||
const { datasource, queries } = this.state;
|
||||
const { datasource, queries, queryTransactions } = this.state;
|
||||
if (datasource && datasource.modifyQuery) {
|
||||
let nextQueries;
|
||||
let nextQueryTransactions;
|
||||
if (index === undefined) {
|
||||
// Modify all queries
|
||||
nextQueries = queries.map((q, i) => ({
|
||||
key: generateQueryKey(i),
|
||||
query: datasource.modifyQuery(this.queryExpressions[i], action),
|
||||
}));
|
||||
// Discard all ongoing transactions
|
||||
nextQueryTransactions = [];
|
||||
} else {
|
||||
// Modify query only at index
|
||||
nextQueries = [
|
||||
@@ -327,20 +356,41 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
},
|
||||
...queries.slice(index + 1),
|
||||
];
|
||||
// Discard transactions related to row query
|
||||
nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index);
|
||||
}
|
||||
this.queryExpressions = nextQueries.map(q => q.query);
|
||||
this.setState({ queries: nextQueries }, () => this.onSubmit());
|
||||
this.setState(
|
||||
{
|
||||
queries: nextQueries,
|
||||
queryTransactions: nextQueryTransactions,
|
||||
},
|
||||
() => this.onSubmit()
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
onRemoveQueryRow = index => {
|
||||
const { queries } = this.state;
|
||||
const { queries, queryTransactions } = this.state;
|
||||
if (queries.length <= 1) {
|
||||
return;
|
||||
}
|
||||
// Remove from local cache
|
||||
this.queryExpressions = [...this.queryExpressions.slice(0, index), ...this.queryExpressions.slice(index + 1)];
|
||||
|
||||
// Remove row from react state
|
||||
const nextQueries = [...queries.slice(0, index), ...queries.slice(index + 1)];
|
||||
this.queryExpressions = nextQueries.map(q => q.query);
|
||||
this.setState({ queries: nextQueries }, () => this.onSubmit());
|
||||
|
||||
// Discard transactions related to row query
|
||||
const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index);
|
||||
|
||||
this.setState(
|
||||
{
|
||||
queries: nextQueries,
|
||||
queryTransactions: nextQueryTransactions,
|
||||
},
|
||||
() => this.onSubmit()
|
||||
);
|
||||
};
|
||||
|
||||
onSubmit = () => {
|
||||
@@ -349,7 +399,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
this.runTableQuery();
|
||||
}
|
||||
if (showingGraph && supportsGraph) {
|
||||
this.runGraphQuery();
|
||||
this.runGraphQueries();
|
||||
}
|
||||
if (showingLogs && supportsLogs) {
|
||||
this.runLogsQuery();
|
||||
@@ -357,32 +407,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
this.saveState();
|
||||
};
|
||||
|
||||
onQuerySuccess(datasourceId: string, queries: string[]): void {
|
||||
// save queries to history
|
||||
let { history } = this.state;
|
||||
const { datasource } = this.state;
|
||||
|
||||
if (datasource.meta.id !== datasourceId) {
|
||||
// Navigated away, queries did not matter
|
||||
return;
|
||||
}
|
||||
|
||||
const ts = Date.now();
|
||||
queries.forEach(query => {
|
||||
history = [{ query, ts }, ...history];
|
||||
});
|
||||
|
||||
if (history.length > MAX_HISTORY_ITEMS) {
|
||||
history = history.slice(0, MAX_HISTORY_ITEMS);
|
||||
}
|
||||
|
||||
// Combine all queries of a datasource type into one history
|
||||
const historyKey = `grafana.explore.history.${datasourceId}`;
|
||||
store.setObject(historyKey, history);
|
||||
this.setState({ history });
|
||||
}
|
||||
|
||||
buildQueryOptions(targetOptions: { format: string; hinting?: boolean; instant?: boolean }) {
|
||||
buildQueryOptions(query: string, rowIndex: number, targetOptions: { format: string; hinting?: boolean; instant?: boolean }) {
|
||||
const { datasource, range } = this.state;
|
||||
const resolution = this.el.offsetWidth;
|
||||
const absoluteRange = {
|
||||
@@ -390,90 +415,215 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
to: parseDate(range.to, true),
|
||||
};
|
||||
const { interval } = kbn.calculateInterval(absoluteRange, resolution, datasource.interval);
|
||||
const targets = this.queryExpressions.map((q, i) => ({
|
||||
...targetOptions,
|
||||
// Target identifier is needed for table transformations
|
||||
refId: i + 1,
|
||||
expr: q,
|
||||
}));
|
||||
const targets = [
|
||||
{
|
||||
...targetOptions,
|
||||
// Target identifier is needed for table transformations
|
||||
refId: rowIndex + 1,
|
||||
expr: query,
|
||||
},
|
||||
];
|
||||
|
||||
// Clone range for query request
|
||||
const queryRange: Range = { ...range };
|
||||
|
||||
return {
|
||||
interval,
|
||||
range,
|
||||
targets,
|
||||
range: queryRange,
|
||||
};
|
||||
}
|
||||
|
||||
async runGraphQuery() {
|
||||
startQueryTransaction(query: string, rowIndex: number, resultType: string, options: any): QueryTransaction {
|
||||
const queryOptions = this.buildQueryOptions(query, rowIndex, options);
|
||||
const transaction: QueryTransaction = {
|
||||
query,
|
||||
resultType,
|
||||
rowIndex,
|
||||
id: generateQueryKey(),
|
||||
done: false,
|
||||
latency: 0,
|
||||
options: queryOptions,
|
||||
};
|
||||
|
||||
// Using updater style because we might be modifying queryTransactions in quick succession
|
||||
this.setState(state => {
|
||||
const { queryTransactions } = state;
|
||||
// Discarding existing transactions of same type
|
||||
const remainingTransactions = queryTransactions.filter(
|
||||
qt => !(qt.resultType === resultType && qt.rowIndex === rowIndex)
|
||||
);
|
||||
|
||||
// Append new transaction
|
||||
const nextQueryTransactions = [...remainingTransactions, transaction];
|
||||
|
||||
return {
|
||||
queryHints: [],
|
||||
queryTransactions: nextQueryTransactions,
|
||||
};
|
||||
});
|
||||
|
||||
return transaction;
|
||||
}
|
||||
|
||||
completeQueryTransaction(
|
||||
transactionId: string,
|
||||
result: any,
|
||||
latency: number,
|
||||
hints: any[],
|
||||
queries: string[],
|
||||
datasourceId: string
|
||||
) {
|
||||
const { datasource } = this.state;
|
||||
if (datasource.meta.id !== datasourceId) {
|
||||
// Navigated away, queries did not matter
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState(state => {
|
||||
const { history, queryTransactions } = state;
|
||||
|
||||
// Transaction might have been discarded
|
||||
if (!queryTransactions.find(qt => qt.id === transactionId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Mark transactions as complete
|
||||
const nextQueryTransactions = queryTransactions.map(qt => {
|
||||
if (qt.id === transactionId) {
|
||||
return {
|
||||
...qt,
|
||||
latency,
|
||||
result,
|
||||
done: true,
|
||||
};
|
||||
}
|
||||
return qt;
|
||||
});
|
||||
|
||||
const nextHistory = updateHistory(history, datasourceId, queries);
|
||||
|
||||
return {
|
||||
history: nextHistory,
|
||||
queryHints: hints,
|
||||
queryTransactions: nextQueryTransactions,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
failQueryTransaction(transactionId: string, error: string, datasourceId: string) {
|
||||
const { datasource } = this.state;
|
||||
if (datasource.meta.id !== datasourceId) {
|
||||
// Navigated away, queries did not matter
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState(state => {
|
||||
// Transaction might have been discarded
|
||||
if (!state.queryTransactions.find(qt => qt.id === transactionId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Mark transactions as complete
|
||||
const nextQueryTransactions = state.queryTransactions.map(qt => {
|
||||
if (qt.id === transactionId) {
|
||||
return {
|
||||
...qt,
|
||||
error,
|
||||
done: true,
|
||||
};
|
||||
}
|
||||
return qt;
|
||||
});
|
||||
|
||||
return {
|
||||
queryTransactions: nextQueryTransactions,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async runGraphQueries() {
|
||||
const queries = [...this.queryExpressions];
|
||||
if (!hasQuery(queries)) {
|
||||
return;
|
||||
}
|
||||
this.setState({ latency: 0, loading: true, graphResult: null, queryErrors: [], queryHints: [] });
|
||||
const now = Date.now();
|
||||
const options = this.buildQueryOptions({ format: 'time_series', instant: false, hinting: true });
|
||||
try {
|
||||
const res = await datasource.query(options);
|
||||
const result = makeTimeSeriesList(res.data, options);
|
||||
const queryHints = res.hints ? makeHints(res.hints) : [];
|
||||
const latency = Date.now() - now;
|
||||
this.setState({ latency, loading: false, graphResult: result, queryHints, requestOptions: options });
|
||||
this.onQuerySuccess(datasource.meta.id, queries);
|
||||
} catch (response) {
|
||||
console.error(response);
|
||||
const queryError = response.data ? response.data.error : response;
|
||||
this.setState({ loading: false, queryErrors: [queryError] });
|
||||
}
|
||||
const { datasource } = this.state;
|
||||
const datasourceId = datasource.meta.id;
|
||||
// Run all queries concurrently
|
||||
queries.forEach(async (query, rowIndex) => {
|
||||
if (query) {
|
||||
const transaction = this.startQueryTransaction(query, rowIndex, 'Graph', {
|
||||
format: 'time_series',
|
||||
instant: false,
|
||||
hinting: true,
|
||||
});
|
||||
try {
|
||||
const now = Date.now();
|
||||
const res = await datasource.query(transaction.options);
|
||||
const latency = Date.now() - now;
|
||||
const results = makeTimeSeriesList(res.data, transaction.options);
|
||||
const queryHints = res.hints ? makeHints(res.hints) : [];
|
||||
this.completeQueryTransaction(transaction.id, results, latency, queryHints, queries, datasourceId);
|
||||
this.setState({ graphRange: transaction.options.range });
|
||||
} catch (response) {
|
||||
console.error(response);
|
||||
const queryError = response.data ? response.data.error : response;
|
||||
this.failQueryTransaction(transaction.id, queryError, datasourceId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async runTableQuery() {
|
||||
const queries = [...this.queryExpressions];
|
||||
const { datasource } = this.state;
|
||||
if (!hasQuery(queries)) {
|
||||
return;
|
||||
}
|
||||
this.setState({ latency: 0, loading: true, queryErrors: [], queryHints: [], tableResult: null });
|
||||
const now = Date.now();
|
||||
const options = this.buildQueryOptions({
|
||||
format: 'table',
|
||||
instant: true,
|
||||
const { datasource } = this.state;
|
||||
const datasourceId = datasource.meta.id;
|
||||
// Run all queries concurrently
|
||||
queries.forEach(async (query, rowIndex) => {
|
||||
if (query) {
|
||||
const transaction = this.startQueryTransaction(query, rowIndex, 'Table', { format: 'table', instant: true });
|
||||
try {
|
||||
const now = Date.now();
|
||||
const res = await datasource.query(transaction.options);
|
||||
const latency = Date.now() - now;
|
||||
const results = mergeTablesIntoModel(new TableModel(), ...res.data);
|
||||
this.completeQueryTransaction(transaction.id, results, latency, [], queries, datasourceId);
|
||||
} catch (response) {
|
||||
console.error(response);
|
||||
const queryError = response.data ? response.data.error : response;
|
||||
this.failQueryTransaction(transaction.id, queryError, datasourceId);
|
||||
}
|
||||
}
|
||||
});
|
||||
try {
|
||||
const res = await datasource.query(options);
|
||||
const tableModel = mergeTablesIntoModel(new TableModel(), ...res.data);
|
||||
const latency = Date.now() - now;
|
||||
this.setState({ latency, loading: false, tableResult: tableModel, requestOptions: options });
|
||||
this.onQuerySuccess(datasource.meta.id, queries);
|
||||
} catch (response) {
|
||||
console.error(response);
|
||||
const queryError = response.data ? response.data.error : response;
|
||||
this.setState({ loading: false, queryErrors: [queryError] });
|
||||
}
|
||||
}
|
||||
|
||||
async runLogsQuery() {
|
||||
const queries = [...this.queryExpressions];
|
||||
const { datasource } = this.state;
|
||||
if (!hasQuery(queries)) {
|
||||
return;
|
||||
}
|
||||
this.setState({ latency: 0, loading: true, queryErrors: [], queryHints: [], logsResult: null });
|
||||
const now = Date.now();
|
||||
const options = this.buildQueryOptions({
|
||||
format: 'logs',
|
||||
const { datasource } = this.state;
|
||||
const datasourceId = datasource.meta.id;
|
||||
// Run all queries concurrently
|
||||
queries.forEach(async (query, rowIndex) => {
|
||||
if (query) {
|
||||
const transaction = this.startQueryTransaction(query, rowIndex, 'Logs', { format: 'logs' });
|
||||
try {
|
||||
const now = Date.now();
|
||||
const res = await datasource.query(transaction.options);
|
||||
const latency = Date.now() - now;
|
||||
const results = res.data;
|
||||
this.completeQueryTransaction(transaction.id, results, latency, [], queries, datasourceId);
|
||||
} catch (response) {
|
||||
console.error(response);
|
||||
const queryError = response.data ? response.data.error : response;
|
||||
this.failQueryTransaction(transaction.id, queryError, datasourceId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const res = await datasource.query(options);
|
||||
const logsData = res.data;
|
||||
const latency = Date.now() - now;
|
||||
this.setState({ latency, loading: false, logsResult: logsData, requestOptions: options });
|
||||
this.onQuerySuccess(datasource.meta.id, queries);
|
||||
} catch (response) {
|
||||
console.error(response);
|
||||
const queryError = response.data ? response.data.error : response;
|
||||
this.setState({ loading: false, queryErrors: [queryError] });
|
||||
}
|
||||
}
|
||||
|
||||
request = url => {
|
||||
@@ -502,23 +652,18 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
datasourceLoading,
|
||||
datasourceMissing,
|
||||
exploreDatasources,
|
||||
graphResult,
|
||||
graphRange,
|
||||
history,
|
||||
latency,
|
||||
loading,
|
||||
logsResult,
|
||||
queries,
|
||||
queryErrors,
|
||||
queryHints,
|
||||
queryTransactions,
|
||||
range,
|
||||
requestOptions,
|
||||
showingGraph,
|
||||
showingLogs,
|
||||
showingTable,
|
||||
supportsGraph,
|
||||
supportsLogs,
|
||||
supportsTable,
|
||||
tableResult,
|
||||
} = this.state;
|
||||
const showingBoth = showingGraph && showingTable;
|
||||
const graphHeight = showingBoth ? '200px' : '400px';
|
||||
@@ -527,6 +672,17 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
const tableButtonActive = showingBoth || showingTable ? 'active' : '';
|
||||
const exploreClass = split ? 'explore explore-split' : 'explore';
|
||||
const selectedDatasource = datasource ? exploreDatasources.find(d => d.label === datasource.name) : undefined;
|
||||
const graphLoading = queryTransactions.some(qt => qt.resultType === 'Graph' && !qt.done);
|
||||
const tableLoading = queryTransactions.some(qt => qt.resultType === 'Table' && !qt.done);
|
||||
const logsLoading = queryTransactions.some(qt => qt.resultType === 'Logs' && !qt.done);
|
||||
const graphResult = _.flatten(
|
||||
queryTransactions.filter(qt => qt.resultType === 'Graph' && qt.done && qt.result).map(qt => qt.result)
|
||||
);
|
||||
const tableResult = queryTransactions.filter(qt => qt.resultType === 'Table' && qt.done).map(qt => qt.result)[0];
|
||||
const logsResult = _.flatten(
|
||||
queryTransactions.filter(qt => qt.resultType === 'Logs' && qt.done).map(qt => qt.result)
|
||||
);
|
||||
const loading = queryTransactions.some(qt => !qt.done);
|
||||
|
||||
return (
|
||||
<div className={exploreClass} ref={this.getRef}>
|
||||
@@ -539,12 +695,12 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
<div className="navbar-buttons explore-first-button">
|
||||
<button className="btn navbar-button" onClick={this.onClickCloseSplit}>
|
||||
Close Split
|
||||
<div className="navbar-buttons explore-first-button">
|
||||
<button className="btn navbar-button" onClick={this.onClickCloseSplit}>
|
||||
Close Split
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!datasourceMissing ? (
|
||||
<div className="navbar-buttons">
|
||||
<Select
|
||||
@@ -584,9 +740,9 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
</div>
|
||||
<div className="navbar-buttons relative">
|
||||
<button className="btn navbar-button--primary" onClick={this.onSubmit}>
|
||||
Run Query <i className="fa fa-level-down run-icon" />
|
||||
Run Query{' '}
|
||||
{loading ? <i className="fa fa-spinner fa-spin run-icon" /> : <i className="fa fa-level-down run-icon" />}
|
||||
</button>
|
||||
{loading || latency ? <ElapsedTime time={latency} className="text-info" /> : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -605,7 +761,6 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
<QueryRows
|
||||
history={history}
|
||||
queries={queries}
|
||||
queryErrors={queryErrors}
|
||||
queryHints={queryHints}
|
||||
request={this.request}
|
||||
onAddQueryRow={this.onAddQueryRow}
|
||||
@@ -614,6 +769,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
onExecuteQuery={this.onSubmit}
|
||||
onRemoveQueryRow={this.onRemoveQueryRow}
|
||||
supportsLogs={supportsLogs}
|
||||
transactions={queryTransactions}
|
||||
/>
|
||||
<div className="result-options">
|
||||
{supportsGraph ? (
|
||||
@@ -635,23 +791,22 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
|
||||
<main className="m-t-2">
|
||||
{supportsGraph &&
|
||||
showingGraph &&
|
||||
graphResult && (
|
||||
showingGraph && (
|
||||
<Graph
|
||||
data={graphResult}
|
||||
height={graphHeight}
|
||||
loading={loading}
|
||||
loading={graphLoading}
|
||||
id={`explore-graph-${position}`}
|
||||
options={requestOptions}
|
||||
range={graphRange}
|
||||
split={split}
|
||||
/>
|
||||
)}
|
||||
{supportsTable && showingTable ? (
|
||||
<div className="panel-container">
|
||||
<Table data={tableResult} loading={loading} onClickCell={this.onClickTableCell} />
|
||||
<div className="panel-container m-t-2">
|
||||
<Table data={tableResult} loading={tableLoading} onClickCell={this.onClickTableCell} />
|
||||
</div>
|
||||
) : null}
|
||||
{supportsLogs && showingLogs ? <Logs data={logsResult} loading={loading} /> : null}
|
||||
{supportsLogs && showingLogs ? <Logs data={logsResult} loading={logsLoading} /> : null}
|
||||
</main>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -4,24 +4,11 @@ import { Graph } from './Graph';
|
||||
import { mockData } from './__mocks__/mockData';
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props = Object.assign(
|
||||
{
|
||||
data: mockData().slice(0, 19),
|
||||
options: {
|
||||
interval: '20s',
|
||||
range: { from: 'now-6h', to: 'now' },
|
||||
targets: [
|
||||
{
|
||||
format: 'time_series',
|
||||
instant: false,
|
||||
hinting: true,
|
||||
expr: 'prometheus_http_request_duration_seconds_bucket',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
propOverrides
|
||||
);
|
||||
const props = {
|
||||
data: mockData().slice(0, 19),
|
||||
range: { from: 'now-6h', to: 'now' },
|
||||
...propOverrides,
|
||||
};
|
||||
|
||||
// Enzyme.shallow did not work well with jquery.flop. Mocking the draw function.
|
||||
Graph.prototype.draw = jest.fn();
|
||||
|
||||
@@ -5,6 +5,8 @@ import { withSize } from 'react-sizeme';
|
||||
|
||||
import 'vendor/flot/jquery.flot';
|
||||
import 'vendor/flot/jquery.flot.time';
|
||||
|
||||
import { Range } from 'app/types/explore';
|
||||
import * as dateMath from 'app/core/utils/datemath';
|
||||
import TimeSeries from 'app/core/time_series2';
|
||||
|
||||
@@ -74,7 +76,7 @@ interface GraphProps {
|
||||
height?: string; // e.g., '200px'
|
||||
id?: string;
|
||||
loading?: boolean;
|
||||
options: any;
|
||||
range: Range;
|
||||
split?: boolean;
|
||||
size?: { width: number; height: number };
|
||||
}
|
||||
@@ -101,7 +103,7 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
|
||||
componentDidUpdate(prevProps: GraphProps) {
|
||||
if (
|
||||
prevProps.data !== this.props.data ||
|
||||
prevProps.options !== this.props.options ||
|
||||
prevProps.range !== this.props.range ||
|
||||
prevProps.split !== this.props.split ||
|
||||
prevProps.height !== this.props.height ||
|
||||
(prevProps.size && prevProps.size.width !== this.props.size.width)
|
||||
@@ -120,22 +122,22 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
|
||||
};
|
||||
|
||||
draw() {
|
||||
const { options: userOptions, size } = this.props;
|
||||
const { range, size } = this.props;
|
||||
const data = this.getGraphData();
|
||||
|
||||
const $el = $(`#${this.props.id}`);
|
||||
if (!data) {
|
||||
$el.empty();
|
||||
return;
|
||||
let series = [{ data: [[0, 0]] }];
|
||||
|
||||
if (data && data.length > 0) {
|
||||
series = data.map((ts: TimeSeries) => ({
|
||||
color: ts.color,
|
||||
label: ts.label,
|
||||
data: ts.getFlotPairs('null'),
|
||||
}));
|
||||
}
|
||||
const series = data.map((ts: TimeSeries) => ({
|
||||
color: ts.color,
|
||||
label: ts.label,
|
||||
data: ts.getFlotPairs('null'),
|
||||
}));
|
||||
|
||||
const ticks = (size.width || 0) / 100;
|
||||
let { from, to } = userOptions.range;
|
||||
let { from, to } = range;
|
||||
if (!moment.isMoment(from)) {
|
||||
from = dateMath.parse(from, false);
|
||||
}
|
||||
@@ -157,7 +159,6 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
|
||||
const options = {
|
||||
...FLOT_OPTIONS,
|
||||
...dynamicOptions,
|
||||
...userOptions,
|
||||
};
|
||||
$.plot($el, series, options);
|
||||
}
|
||||
@@ -166,16 +167,10 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
|
||||
const { height = '100px', id = 'graph', loading = false } = this.props;
|
||||
const data = this.getGraphData();
|
||||
|
||||
if (!loading && data.length === 0) {
|
||||
return (
|
||||
<div className="panel-container">
|
||||
<div className="muted m-a-1">The queries returned no time series to graph.</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
{this.props.data.length > MAX_NUMBER_OF_TIME_SERIES &&
|
||||
<>
|
||||
{this.props.data &&
|
||||
this.props.data.length > MAX_NUMBER_OF_TIME_SERIES &&
|
||||
!this.state.showAllTimeSeries && (
|
||||
<div className="time-series-disclaimer">
|
||||
<i className="fa fa-fw fa-warning disclaimer-icon" />
|
||||
@@ -186,10 +181,11 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
|
||||
</div>
|
||||
)}
|
||||
<div className="panel-container">
|
||||
{loading && <div className="explore-graph__loader" />}
|
||||
<div id={id} className="explore-graph" style={{ height }} />
|
||||
<Legend data={data} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { PureComponent } from 'react';
|
||||
|
||||
// TODO make this datasource-plugin-dependent
|
||||
import QueryField from './PromQueryField';
|
||||
import QueryTransactions from './QueryTransactions';
|
||||
|
||||
class QueryRow extends PureComponent<any, {}> {
|
||||
onChangeQuery = (value, override?: boolean) => {
|
||||
@@ -44,9 +45,14 @@ class QueryRow extends PureComponent<any, {}> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { history, query, queryError, queryHint, request, supportsLogs } = this.props;
|
||||
const { history, query, queryHint, request, supportsLogs, transactions } = this.props;
|
||||
const transactionWithError = transactions.find(t => t.error);
|
||||
const queryError = transactionWithError ? transactionWithError.error : null;
|
||||
return (
|
||||
<div className="query-row">
|
||||
<div className="query-row-status">
|
||||
<QueryTransactions transactions={transactions} />
|
||||
</div>
|
||||
<div className="query-row-field">
|
||||
<QueryField
|
||||
error={queryError}
|
||||
@@ -78,7 +84,7 @@ class QueryRow extends PureComponent<any, {}> {
|
||||
|
||||
export default class QueryRows extends PureComponent<any, {}> {
|
||||
render() {
|
||||
const { className = '', queries, queryErrors, queryHints, ...handlers } = this.props;
|
||||
const { className = '', queries, queryHints, transactions, ...handlers } = this.props;
|
||||
return (
|
||||
<div className={className}>
|
||||
{queries.map((q, index) => (
|
||||
@@ -86,7 +92,7 @@ export default class QueryRows extends PureComponent<any, {}> {
|
||||
key={q.key}
|
||||
index={index}
|
||||
query={q.query}
|
||||
queryError={queryErrors[index]}
|
||||
transactions={transactions.filter(t => t.rowIndex === index)}
|
||||
queryHint={queryHints[index]}
|
||||
{...handlers}
|
||||
/>
|
||||
|
||||
42
public/app/features/explore/QueryTransactions.tsx
Normal file
42
public/app/features/explore/QueryTransactions.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
import { QueryTransaction as QueryTransactionModel } from 'app/types/explore';
|
||||
import ElapsedTime from './ElapsedTime';
|
||||
|
||||
function formatLatency(value) {
|
||||
return `${(value / 1000).toFixed(1)}s`;
|
||||
}
|
||||
|
||||
interface QueryTransactionProps {
|
||||
transaction: QueryTransactionModel;
|
||||
}
|
||||
|
||||
class QueryTransaction extends PureComponent<QueryTransactionProps> {
|
||||
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 QueryTransactionsProps {
|
||||
transactions: QueryTransactionModel[];
|
||||
}
|
||||
|
||||
export default class QueryTransactions extends PureComponent<QueryTransactionsProps> {
|
||||
render() {
|
||||
const { transactions } = this.props;
|
||||
return (
|
||||
<div className="query-transactions">
|
||||
{transactions.map((t, i) => <QueryTransaction key={`${t.query}:${t.resultType}`} transaction={t} />)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ export default class Table extends PureComponent<TableProps> {
|
||||
minRows={0}
|
||||
noDataText={noDataText}
|
||||
resolveData={data => prepareRows(data, columnNames)}
|
||||
showPagination={data}
|
||||
showPagination={Boolean(data)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Render should render component 1`] = `
|
||||
<div>
|
||||
<Fragment>
|
||||
<div
|
||||
className="panel-container"
|
||||
>
|
||||
@@ -458,11 +458,11 @@ exports[`Render should render component 1`] = `
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`Render should render component with disclaimer 1`] = `
|
||||
<div>
|
||||
<Fragment>
|
||||
<div
|
||||
className="time-series-disclaimer"
|
||||
>
|
||||
@@ -956,17 +956,26 @@ exports[`Render should render component with disclaimer 1`] = `
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`Render should show query return no time series 1`] = `
|
||||
<div
|
||||
className="panel-container"
|
||||
>
|
||||
<Fragment>
|
||||
<div
|
||||
className="muted m-a-1"
|
||||
className="panel-container"
|
||||
>
|
||||
The queries returned no time series to graph.
|
||||
<div
|
||||
className="explore-graph"
|
||||
id="graph"
|
||||
style={
|
||||
Object {
|
||||
"height": "100px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Legend
|
||||
data={Array []}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
@@ -3,6 +3,11 @@ interface ExploreDatasource {
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface HistoryItem {
|
||||
ts: number;
|
||||
query: string;
|
||||
}
|
||||
|
||||
export interface Range {
|
||||
from: string;
|
||||
to: string;
|
||||
@@ -13,6 +18,18 @@ export interface Query {
|
||||
key?: string;
|
||||
}
|
||||
|
||||
export interface QueryTransaction {
|
||||
id: string;
|
||||
done: boolean;
|
||||
error?: string;
|
||||
latency: number;
|
||||
options: any;
|
||||
query: string;
|
||||
result?: any; // Table / Timeseries / Logs
|
||||
resultType: string;
|
||||
rowIndex: number;
|
||||
}
|
||||
|
||||
export interface TextMatch {
|
||||
text: string;
|
||||
start: number;
|
||||
@@ -27,34 +44,26 @@ export interface ExploreState {
|
||||
datasourceMissing: boolean;
|
||||
datasourceName?: string;
|
||||
exploreDatasources: ExploreDatasource[];
|
||||
graphResult: any;
|
||||
history: any[];
|
||||
latency: number;
|
||||
loading: any;
|
||||
logsResult: any;
|
||||
graphRange: Range;
|
||||
history: HistoryItem[];
|
||||
/**
|
||||
* Initial rows of queries to push down the tree.
|
||||
* Modifications do not end up here, but in `this.queryExpressions`.
|
||||
* The only way to reset a query is to change its `key`.
|
||||
*/
|
||||
queries: Query[];
|
||||
/**
|
||||
* Errors caused by the running the query row.
|
||||
*/
|
||||
queryErrors: any[];
|
||||
/**
|
||||
* Hints gathered for the query row.
|
||||
*/
|
||||
queryHints: any[];
|
||||
queryTransactions: QueryTransaction[];
|
||||
range: Range;
|
||||
requestOptions: any;
|
||||
showingGraph: boolean;
|
||||
showingLogs: boolean;
|
||||
showingTable: boolean;
|
||||
supportsGraph: boolean | null;
|
||||
supportsLogs: boolean | null;
|
||||
supportsTable: boolean | null;
|
||||
tableResult: any;
|
||||
}
|
||||
|
||||
export interface ExploreUrlState {
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.elapsed-time {
|
||||
.navbar .elapsed-time {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
@@ -87,6 +87,37 @@
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.explore-graph__loader {
|
||||
height: 2px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: $table-border;
|
||||
margin: $panel-margin / 2;
|
||||
}
|
||||
|
||||
.explore-graph__loader:after {
|
||||
content: ' ';
|
||||
display: block;
|
||||
width: 25%;
|
||||
top: 0;
|
||||
top: -50%;
|
||||
height: 250%;
|
||||
position: absolute;
|
||||
animation: loader 2s cubic-bezier(0.17, 0.67, 0.83, 0.67);
|
||||
animation-iteration-count: 100;
|
||||
z-index: 2;
|
||||
background: $blue;
|
||||
}
|
||||
|
||||
@keyframes loader {
|
||||
from {
|
||||
left: -25%;
|
||||
}
|
||||
to {
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.datasource-picker {
|
||||
min-width: 200px;
|
||||
}
|
||||
@@ -119,6 +150,7 @@
|
||||
|
||||
.query-row {
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
& + & {
|
||||
margin-top: 0.5rem;
|
||||
@@ -129,11 +161,53 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.query-row-status {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 90px;
|
||||
z-index: 1024;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.query-row-field {
|
||||
margin-right: 3px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.query-transactions {
|
||||
display: table;
|
||||
}
|
||||
|
||||
.query-transaction {
|
||||
display: table-row;
|
||||
color: $text-color-faint;
|
||||
line-height: 1.44;
|
||||
}
|
||||
|
||||
.query-transaction--loading {
|
||||
animation: query-loading-color-change 1s alternate 100;
|
||||
}
|
||||
|
||||
@keyframes query-loading-color-change {
|
||||
from {
|
||||
color: $text-color-faint;
|
||||
}
|
||||
to {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
.query-transaction__type,
|
||||
.query-transaction__duration {
|
||||
display: table-cell;
|
||||
font-size: $font-size-xs;
|
||||
text-align: right;
|
||||
padding-right: 0.25em;
|
||||
}
|
||||
|
||||
.explore {
|
||||
.logs {
|
||||
.logs-entries {
|
||||
|
||||
Reference in New Issue
Block a user