mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge pull request #13787 from grafana/davkal/explore-transactions
Explore: query transactions for faster result display
This commit is contained in:
commit
7d0eccdd23
@ -8,23 +8,17 @@ const DEFAULT_EXPLORE_STATE: ExploreState = {
|
|||||||
datasourceMissing: false,
|
datasourceMissing: false,
|
||||||
datasourceName: '',
|
datasourceName: '',
|
||||||
exploreDatasources: [],
|
exploreDatasources: [],
|
||||||
graphResult: null,
|
graphRange: DEFAULT_RANGE,
|
||||||
history: [],
|
history: [],
|
||||||
latency: 0,
|
|
||||||
loading: false,
|
|
||||||
logsResult: null,
|
|
||||||
queries: [],
|
queries: [],
|
||||||
queryErrors: [],
|
queryTransactions: [],
|
||||||
queryHints: [],
|
|
||||||
range: DEFAULT_RANGE,
|
range: DEFAULT_RANGE,
|
||||||
requestOptions: null,
|
|
||||||
showingGraph: true,
|
showingGraph: true,
|
||||||
showingLogs: true,
|
showingLogs: true,
|
||||||
showingTable: true,
|
showingTable: true,
|
||||||
supportsGraph: null,
|
supportsGraph: null,
|
||||||
supportsLogs: null,
|
supportsLogs: null,
|
||||||
supportsTable: null,
|
supportsTable: null,
|
||||||
tableResult: null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('state functions', () => {
|
describe('state functions', () => {
|
||||||
|
@ -1,8 +1,17 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { hot } from 'react-hot-loader';
|
import { hot } from 'react-hot-loader';
|
||||||
import Select from 'react-select';
|
import Select from 'react-select';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
import { ExploreState, ExploreUrlState, Query } from 'app/types/explore';
|
import {
|
||||||
|
ExploreState,
|
||||||
|
ExploreUrlState,
|
||||||
|
HistoryItem,
|
||||||
|
Query,
|
||||||
|
QueryTransaction,
|
||||||
|
Range,
|
||||||
|
ResultType,
|
||||||
|
} from 'app/types/explore';
|
||||||
import kbn from 'app/core/utils/kbn';
|
import kbn from 'app/core/utils/kbn';
|
||||||
import colors from 'app/core/utils/colors';
|
import colors from 'app/core/utils/colors';
|
||||||
import store from 'app/core/store';
|
import store from 'app/core/store';
|
||||||
@ -15,7 +24,6 @@ import IndicatorsContainer from 'app/core/components/Picker/IndicatorsContainer'
|
|||||||
import NoOptionsMessage from 'app/core/components/Picker/NoOptionsMessage';
|
import NoOptionsMessage from 'app/core/components/Picker/NoOptionsMessage';
|
||||||
import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
|
import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
|
||||||
|
|
||||||
import ElapsedTime from './ElapsedTime';
|
|
||||||
import QueryRows from './QueryRows';
|
import QueryRows from './QueryRows';
|
||||||
import Graph from './Graph';
|
import Graph from './Graph';
|
||||||
import Logs from './Logs';
|
import Logs from './Logs';
|
||||||
@ -25,16 +33,6 @@ import { ensureQueries, generateQueryKey, hasQuery } from './utils/query';
|
|||||||
|
|
||||||
const MAX_HISTORY_ITEMS = 100;
|
const MAX_HISTORY_ITEMS = 100;
|
||||||
|
|
||||||
function makeHints(hints) {
|
|
||||||
const hintsByIndex = [];
|
|
||||||
hints.forEach(hint => {
|
|
||||||
if (hint) {
|
|
||||||
hintsByIndex[hint.index] = hint;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return hintsByIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeTimeSeriesList(dataList, options) {
|
function makeTimeSeriesList(dataList, options) {
|
||||||
return dataList.map((seriesData, index) => {
|
return dataList.map((seriesData, index) => {
|
||||||
const datapoints = seriesData.datapoints || [];
|
const datapoints = seriesData.datapoints || [];
|
||||||
@ -53,6 +51,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 {
|
interface ExploreProps {
|
||||||
datasourceSrv: any;
|
datasourceSrv: any;
|
||||||
onChangeSplit: (split: boolean, state?: ExploreState) => void;
|
onChangeSplit: (split: boolean, state?: ExploreState) => void;
|
||||||
@ -83,6 +100,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
} else {
|
} else {
|
||||||
const { datasource, queries, range } = props.urlState as ExploreUrlState;
|
const { datasource, queries, range } = props.urlState as ExploreUrlState;
|
||||||
initialQueries = ensureQueries(queries);
|
initialQueries = ensureQueries(queries);
|
||||||
|
const initialRange = range || { ...DEFAULT_RANGE };
|
||||||
this.state = {
|
this.state = {
|
||||||
datasource: null,
|
datasource: null,
|
||||||
datasourceError: null,
|
datasourceError: null,
|
||||||
@ -90,23 +108,17 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
datasourceMissing: false,
|
datasourceMissing: false,
|
||||||
datasourceName: datasource,
|
datasourceName: datasource,
|
||||||
exploreDatasources: [],
|
exploreDatasources: [],
|
||||||
graphResult: null,
|
graphRange: initialRange,
|
||||||
history: [],
|
history: [],
|
||||||
latency: 0,
|
|
||||||
loading: false,
|
|
||||||
logsResult: null,
|
|
||||||
queries: initialQueries,
|
queries: initialQueries,
|
||||||
queryErrors: [],
|
queryTransactions: [],
|
||||||
queryHints: [],
|
range: initialRange,
|
||||||
range: range || { ...DEFAULT_RANGE },
|
|
||||||
requestOptions: null,
|
|
||||||
showingGraph: true,
|
showingGraph: true,
|
||||||
showingLogs: true,
|
showingLogs: true,
|
||||||
showingTable: true,
|
showingTable: true,
|
||||||
supportsGraph: null,
|
supportsGraph: null,
|
||||||
supportsLogs: null,
|
supportsLogs: null,
|
||||||
supportsTable: null,
|
supportsTable: null,
|
||||||
tableResult: null,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
this.queryExpressions = initialQueries.map(q => q.query);
|
this.queryExpressions = initialQueries.map(q => q.query);
|
||||||
@ -200,14 +212,32 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onAddQueryRow = index => {
|
onAddQueryRow = index => {
|
||||||
const { queries } = this.state;
|
// Local cache
|
||||||
this.queryExpressions[index + 1] = '';
|
this.queryExpressions[index + 1] = '';
|
||||||
|
|
||||||
|
this.setState(state => {
|
||||||
|
const { queries, queryTransactions } = state;
|
||||||
|
|
||||||
|
// Add row by generating new react key
|
||||||
const nextQueries = [
|
const nextQueries = [
|
||||||
...queries.slice(0, index + 1),
|
...queries.slice(0, index + 1),
|
||||||
{ query: '', key: generateQueryKey() },
|
{ query: '', key: generateQueryKey() },
|
||||||
...queries.slice(index + 1),
|
...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;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { queries: nextQueries, queryTransactions: nextQueryTransactions };
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onChangeDatasource = async option => {
|
onChangeDatasource = async option => {
|
||||||
@ -215,12 +245,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
datasource: null,
|
datasource: null,
|
||||||
datasourceError: null,
|
datasourceError: null,
|
||||||
datasourceLoading: true,
|
datasourceLoading: true,
|
||||||
graphResult: null,
|
queryTransactions: [],
|
||||||
latency: 0,
|
|
||||||
logsResult: null,
|
|
||||||
queryErrors: [],
|
|
||||||
queryHints: [],
|
|
||||||
tableResult: null,
|
|
||||||
});
|
});
|
||||||
const datasourceName = option.value;
|
const datasourceName = option.value;
|
||||||
const datasource = await this.props.datasourceSrv.get(datasourceName);
|
const datasource = await this.props.datasourceSrv.get(datasourceName);
|
||||||
@ -231,9 +256,10 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
// Keep current value in local cache
|
// Keep current value in local cache
|
||||||
this.queryExpressions[index] = value;
|
this.queryExpressions[index] = value;
|
||||||
|
|
||||||
// Replace query row on override
|
|
||||||
if (override) {
|
if (override) {
|
||||||
const { queries } = this.state;
|
this.setState(state => {
|
||||||
|
// Replace query row
|
||||||
|
const { queries, queryTransactions } = state;
|
||||||
const nextQuery: Query = {
|
const nextQuery: Query = {
|
||||||
key: generateQueryKey(index),
|
key: generateQueryKey(index),
|
||||||
query: value,
|
query: value,
|
||||||
@ -241,14 +267,14 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
const nextQueries = [...queries];
|
const nextQueries = [...queries];
|
||||||
nextQueries[index] = nextQuery;
|
nextQueries[index] = nextQuery;
|
||||||
|
|
||||||
this.setState(
|
// Discard ongoing transaction related to row query
|
||||||
{
|
const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index);
|
||||||
queryErrors: [],
|
|
||||||
queryHints: [],
|
return {
|
||||||
queries: nextQueries,
|
queries: nextQueries,
|
||||||
},
|
queryTransactions: nextQueryTransactions,
|
||||||
this.onSubmit
|
};
|
||||||
);
|
}, this.onSubmit);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -264,13 +290,8 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
this.queryExpressions = [''];
|
this.queryExpressions = [''];
|
||||||
this.setState(
|
this.setState(
|
||||||
{
|
{
|
||||||
graphResult: null,
|
|
||||||
logsResult: null,
|
|
||||||
latency: 0,
|
|
||||||
queries: ensureQueries(),
|
queries: ensureQueries(),
|
||||||
queryErrors: [],
|
queryTransactions: [],
|
||||||
queryHints: [],
|
|
||||||
tableResult: null,
|
|
||||||
},
|
},
|
||||||
this.saveState
|
this.saveState
|
||||||
);
|
);
|
||||||
@ -284,11 +305,41 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onClickGraphButton = () => {
|
onClickGraphButton = () => {
|
||||||
this.setState(state => ({ showingGraph: !state.showingGraph }));
|
this.setState(
|
||||||
|
state => {
|
||||||
|
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 { queryTransactions: nextQueryTransactions, showingGraph };
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
if (this.state.showingGraph) {
|
||||||
|
this.onSubmit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
onClickLogsButton = () => {
|
onClickLogsButton = () => {
|
||||||
this.setState(state => ({ showingLogs: !state.showingLogs }));
|
this.setState(
|
||||||
|
state => {
|
||||||
|
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 { queryTransactions: nextQueryTransactions, showingLogs };
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
if (this.state.showingLogs) {
|
||||||
|
this.onSubmit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
onClickSplit = () => {
|
onClickSplit = () => {
|
||||||
@ -300,7 +351,22 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onClickTableButton = () => {
|
onClickTableButton = () => {
|
||||||
this.setState(state => ({ showingTable: !state.showingTable }));
|
this.setState(
|
||||||
|
state => {
|
||||||
|
const showingTable = !state.showingTable;
|
||||||
|
let nextQueryTransactions = state.queryTransactions;
|
||||||
|
if (!showingTable) {
|
||||||
|
// Discard transactions related to Table query
|
||||||
|
nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Table');
|
||||||
|
}
|
||||||
|
return { queryTransactions: nextQueryTransactions, showingTable };
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
if (this.state.showingTable) {
|
||||||
|
this.onSubmit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
onClickTableCell = (columnKey: string, rowValue: string) => {
|
onClickTableCell = (columnKey: string, rowValue: string) => {
|
||||||
@ -308,15 +374,21 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onModifyQueries = (action: object, index?: number) => {
|
onModifyQueries = (action: object, index?: number) => {
|
||||||
const { datasource, queries } = this.state;
|
const { datasource } = this.state;
|
||||||
if (datasource && datasource.modifyQuery) {
|
if (datasource && datasource.modifyQuery) {
|
||||||
|
this.setState(
|
||||||
|
state => {
|
||||||
|
const { queries, queryTransactions } = state;
|
||||||
let nextQueries;
|
let nextQueries;
|
||||||
|
let nextQueryTransactions;
|
||||||
if (index === undefined) {
|
if (index === undefined) {
|
||||||
// Modify all queries
|
// Modify all queries
|
||||||
nextQueries = queries.map((q, i) => ({
|
nextQueries = queries.map((q, i) => ({
|
||||||
key: generateQueryKey(i),
|
key: generateQueryKey(i),
|
||||||
query: datasource.modifyQuery(this.queryExpressions[i], action),
|
query: datasource.modifyQuery(this.queryExpressions[i], action),
|
||||||
}));
|
}));
|
||||||
|
// Discard all ongoing transactions
|
||||||
|
nextQueryTransactions = [];
|
||||||
} else {
|
} else {
|
||||||
// Modify query only at index
|
// Modify query only at index
|
||||||
nextQueries = [
|
nextQueries = [
|
||||||
@ -327,20 +399,43 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
},
|
},
|
||||||
...queries.slice(index + 1),
|
...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.queryExpressions = nextQueries.map(q => q.query);
|
||||||
this.setState({ queries: nextQueries }, () => this.onSubmit());
|
return {
|
||||||
|
queries: nextQueries,
|
||||||
|
queryTransactions: nextQueryTransactions,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
() => this.onSubmit()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onRemoveQueryRow = index => {
|
onRemoveQueryRow = index => {
|
||||||
const { queries } = this.state;
|
// Remove from local cache
|
||||||
|
this.queryExpressions = [...this.queryExpressions.slice(0, index), ...this.queryExpressions.slice(index + 1)];
|
||||||
|
|
||||||
|
this.setState(
|
||||||
|
state => {
|
||||||
|
const { queries, queryTransactions } = state;
|
||||||
if (queries.length <= 1) {
|
if (queries.length <= 1) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
// Remove row from react state
|
||||||
const nextQueries = [...queries.slice(0, index), ...queries.slice(index + 1)];
|
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);
|
||||||
|
|
||||||
|
return {
|
||||||
|
queries: nextQueries,
|
||||||
|
queryTransactions: nextQueryTransactions,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
() => this.onSubmit()
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
onSubmit = () => {
|
onSubmit = () => {
|
||||||
@ -349,7 +444,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
this.runTableQuery();
|
this.runTableQuery();
|
||||||
}
|
}
|
||||||
if (showingGraph && supportsGraph) {
|
if (showingGraph && supportsGraph) {
|
||||||
this.runGraphQuery();
|
this.runGraphQueries();
|
||||||
}
|
}
|
||||||
if (showingLogs && supportsLogs) {
|
if (showingLogs && supportsLogs) {
|
||||||
this.runLogsQuery();
|
this.runLogsQuery();
|
||||||
@ -357,32 +452,11 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
this.saveState();
|
this.saveState();
|
||||||
};
|
};
|
||||||
|
|
||||||
onQuerySuccess(datasourceId: string, queries: string[]): void {
|
buildQueryOptions(
|
||||||
// save queries to history
|
query: string,
|
||||||
let { history } = this.state;
|
rowIndex: number,
|
||||||
const { datasource } = this.state;
|
targetOptions: { format: string; hinting?: boolean; instant?: boolean }
|
||||||
|
) {
|
||||||
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 }) {
|
|
||||||
const { datasource, range } = this.state;
|
const { datasource, range } = this.state;
|
||||||
const resolution = this.el.offsetWidth;
|
const resolution = this.el.offsetWidth;
|
||||||
const absoluteRange = {
|
const absoluteRange = {
|
||||||
@ -390,90 +464,235 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
to: parseDate(range.to, true),
|
to: parseDate(range.to, true),
|
||||||
};
|
};
|
||||||
const { interval } = kbn.calculateInterval(absoluteRange, resolution, datasource.interval);
|
const { interval } = kbn.calculateInterval(absoluteRange, resolution, datasource.interval);
|
||||||
const targets = this.queryExpressions.map((q, i) => ({
|
const targets = [
|
||||||
|
{
|
||||||
...targetOptions,
|
...targetOptions,
|
||||||
// Target identifier is needed for table transformations
|
// Target identifier is needed for table transformations
|
||||||
refId: i + 1,
|
refId: rowIndex + 1,
|
||||||
expr: q,
|
expr: query,
|
||||||
}));
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Clone range for query request
|
||||||
|
const queryRange: Range = { ...range };
|
||||||
|
|
||||||
return {
|
return {
|
||||||
interval,
|
interval,
|
||||||
range,
|
|
||||||
targets,
|
targets,
|
||||||
|
range: queryRange,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async runGraphQuery() {
|
startQueryTransaction(query: string, rowIndex: number, resultType: ResultType, 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 {
|
||||||
|
queryTransactions: nextQueryTransactions,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
completeQueryTransaction(
|
||||||
|
transactionId: string,
|
||||||
|
result: any,
|
||||||
|
latency: number,
|
||||||
|
queries: string[],
|
||||||
|
datasourceId: string
|
||||||
|
) {
|
||||||
const { datasource } = this.state;
|
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
|
||||||
|
const transaction = queryTransactions.find(qt => qt.id === transactionId);
|
||||||
|
if (!transaction) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get query hints
|
||||||
|
let hints;
|
||||||
|
if (datasource.getQueryHints) {
|
||||||
|
hints = datasource.getQueryHints(transaction.query, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark transactions as complete
|
||||||
|
const nextQueryTransactions = queryTransactions.map(qt => {
|
||||||
|
if (qt.id === transactionId) {
|
||||||
|
return {
|
||||||
|
...qt,
|
||||||
|
hints,
|
||||||
|
latency,
|
||||||
|
result,
|
||||||
|
done: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return qt;
|
||||||
|
});
|
||||||
|
|
||||||
|
const nextHistory = updateHistory(history, datasourceId, queries);
|
||||||
|
|
||||||
|
return {
|
||||||
|
history: nextHistory,
|
||||||
|
queryTransactions: nextQueryTransactions,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
discardTransactions(rowIndex: number) {
|
||||||
|
this.setState(state => {
|
||||||
|
const remainingTransactions = state.queryTransactions.filter(qt => qt.rowIndex !== rowIndex);
|
||||||
|
return { queryTransactions: remainingTransactions };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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];
|
const queries = [...this.queryExpressions];
|
||||||
if (!hasQuery(queries)) {
|
if (!hasQuery(queries)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setState({ latency: 0, loading: true, graphResult: null, queryErrors: [], queryHints: [] });
|
const { datasource } = this.state;
|
||||||
const now = Date.now();
|
const datasourceId = datasource.meta.id;
|
||||||
const options = this.buildQueryOptions({ format: 'time_series', instant: false, hinting: true });
|
// Run all queries concurrently
|
||||||
|
queries.forEach(async (query, rowIndex) => {
|
||||||
|
if (query) {
|
||||||
|
const transaction = this.startQueryTransaction(query, rowIndex, 'Graph', {
|
||||||
|
format: 'time_series',
|
||||||
|
instant: false,
|
||||||
|
});
|
||||||
try {
|
try {
|
||||||
const res = await datasource.query(options);
|
const now = Date.now();
|
||||||
const result = makeTimeSeriesList(res.data, options);
|
const res = await datasource.query(transaction.options);
|
||||||
const queryHints = res.hints ? makeHints(res.hints) : [];
|
|
||||||
const latency = Date.now() - now;
|
const latency = Date.now() - now;
|
||||||
this.setState({ latency, loading: false, graphResult: result, queryHints, requestOptions: options });
|
const results = makeTimeSeriesList(res.data, transaction.options);
|
||||||
this.onQuerySuccess(datasource.meta.id, queries);
|
this.completeQueryTransaction(transaction.id, results, latency, queries, datasourceId);
|
||||||
|
this.setState({ graphRange: transaction.options.range });
|
||||||
} catch (response) {
|
} catch (response) {
|
||||||
console.error(response);
|
console.error(response);
|
||||||
const queryError = response.data ? response.data.error : response;
|
const queryError = response.data ? response.data.error : response;
|
||||||
this.setState({ loading: false, queryErrors: [queryError] });
|
this.failQueryTransaction(transaction.id, queryError, datasourceId);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
this.discardTransactions(rowIndex);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async runTableQuery() {
|
async runTableQuery() {
|
||||||
const queries = [...this.queryExpressions];
|
const queries = [...this.queryExpressions];
|
||||||
const { datasource } = this.state;
|
|
||||||
if (!hasQuery(queries)) {
|
if (!hasQuery(queries)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setState({ latency: 0, loading: true, queryErrors: [], queryHints: [], tableResult: null });
|
const { datasource } = this.state;
|
||||||
const now = Date.now();
|
const datasourceId = datasource.meta.id;
|
||||||
const options = this.buildQueryOptions({
|
// Run all queries concurrently
|
||||||
|
queries.forEach(async (query, rowIndex) => {
|
||||||
|
if (query) {
|
||||||
|
const transaction = this.startQueryTransaction(query, rowIndex, 'Table', {
|
||||||
format: 'table',
|
format: 'table',
|
||||||
instant: true,
|
instant: true,
|
||||||
|
valueWithRefId: true,
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
const res = await datasource.query(options);
|
const now = Date.now();
|
||||||
const tableModel = mergeTablesIntoModel(new TableModel(), ...res.data);
|
const res = await datasource.query(transaction.options);
|
||||||
const latency = Date.now() - now;
|
const latency = Date.now() - now;
|
||||||
this.setState({ latency, loading: false, tableResult: tableModel, requestOptions: options });
|
const results = res.data[0];
|
||||||
this.onQuerySuccess(datasource.meta.id, queries);
|
this.completeQueryTransaction(transaction.id, results, latency, queries, datasourceId);
|
||||||
} catch (response) {
|
} catch (response) {
|
||||||
console.error(response);
|
console.error(response);
|
||||||
const queryError = response.data ? response.data.error : response;
|
const queryError = response.data ? response.data.error : response;
|
||||||
this.setState({ loading: false, queryErrors: [queryError] });
|
this.failQueryTransaction(transaction.id, queryError, datasourceId);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
this.discardTransactions(rowIndex);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async runLogsQuery() {
|
async runLogsQuery() {
|
||||||
const queries = [...this.queryExpressions];
|
const queries = [...this.queryExpressions];
|
||||||
const { datasource } = this.state;
|
|
||||||
if (!hasQuery(queries)) {
|
if (!hasQuery(queries)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setState({ latency: 0, loading: true, queryErrors: [], queryHints: [], logsResult: null });
|
const { datasource } = this.state;
|
||||||
const now = Date.now();
|
const datasourceId = datasource.meta.id;
|
||||||
const options = this.buildQueryOptions({
|
// Run all queries concurrently
|
||||||
format: 'logs',
|
queries.forEach(async (query, rowIndex) => {
|
||||||
});
|
if (query) {
|
||||||
|
const transaction = this.startQueryTransaction(query, rowIndex, 'Logs', { format: 'logs' });
|
||||||
try {
|
try {
|
||||||
const res = await datasource.query(options);
|
const now = Date.now();
|
||||||
const logsData = res.data;
|
const res = await datasource.query(transaction.options);
|
||||||
const latency = Date.now() - now;
|
const latency = Date.now() - now;
|
||||||
this.setState({ latency, loading: false, logsResult: logsData, requestOptions: options });
|
const results = res.data;
|
||||||
this.onQuerySuccess(datasource.meta.id, queries);
|
this.completeQueryTransaction(transaction.id, results, latency, queries, datasourceId);
|
||||||
} catch (response) {
|
} catch (response) {
|
||||||
console.error(response);
|
console.error(response);
|
||||||
const queryError = response.data ? response.data.error : response;
|
const queryError = response.data ? response.data.error : response;
|
||||||
this.setState({ loading: false, queryErrors: [queryError] });
|
this.failQueryTransaction(transaction.id, queryError, datasourceId);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
this.discardTransactions(rowIndex);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
request = url => {
|
request = url => {
|
||||||
@ -485,6 +704,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
// Copy state, but copy queries including modifications
|
// Copy state, but copy queries including modifications
|
||||||
return {
|
return {
|
||||||
...this.state,
|
...this.state,
|
||||||
|
queryTransactions: [],
|
||||||
queries: ensureQueries(this.queryExpressions.map(query => ({ query }))),
|
queries: ensureQueries(this.queryExpressions.map(query => ({ query }))),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -502,23 +722,17 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
datasourceLoading,
|
datasourceLoading,
|
||||||
datasourceMissing,
|
datasourceMissing,
|
||||||
exploreDatasources,
|
exploreDatasources,
|
||||||
graphResult,
|
graphRange,
|
||||||
history,
|
history,
|
||||||
latency,
|
|
||||||
loading,
|
|
||||||
logsResult,
|
|
||||||
queries,
|
queries,
|
||||||
queryErrors,
|
queryTransactions,
|
||||||
queryHints,
|
|
||||||
range,
|
range,
|
||||||
requestOptions,
|
|
||||||
showingGraph,
|
showingGraph,
|
||||||
showingLogs,
|
showingLogs,
|
||||||
showingTable,
|
showingTable,
|
||||||
supportsGraph,
|
supportsGraph,
|
||||||
supportsLogs,
|
supportsLogs,
|
||||||
supportsTable,
|
supportsTable,
|
||||||
tableResult,
|
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const showingBoth = showingGraph && showingTable;
|
const showingBoth = showingGraph && showingTable;
|
||||||
const graphHeight = showingBoth ? '200px' : '400px';
|
const graphHeight = showingBoth ? '200px' : '400px';
|
||||||
@ -527,6 +741,20 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
const tableButtonActive = showingBoth || showingTable ? 'active' : '';
|
const tableButtonActive = showingBoth || showingTable ? 'active' : '';
|
||||||
const exploreClass = split ? 'explore explore-split' : 'explore';
|
const exploreClass = split ? 'explore explore-split' : 'explore';
|
||||||
const selectedDatasource = datasource ? exploreDatasources.find(d => d.label === datasource.name) : undefined;
|
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 = mergeTablesIntoModel(
|
||||||
|
new TableModel(),
|
||||||
|
...queryTransactions.filter(qt => qt.resultType === 'Table' && qt.done).map(qt => qt.result)
|
||||||
|
);
|
||||||
|
const logsResult = _.flatten(
|
||||||
|
queryTransactions.filter(qt => qt.resultType === 'Logs' && qt.done).map(qt => qt.result)
|
||||||
|
);
|
||||||
|
const loading = queryTransactions.some(qt => !qt.done);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={exploreClass} ref={this.getRef}>
|
<div className={exploreClass} ref={this.getRef}>
|
||||||
@ -584,9 +812,9 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
</div>
|
</div>
|
||||||
<div className="navbar-buttons relative">
|
<div className="navbar-buttons relative">
|
||||||
<button className="btn navbar-button--primary" onClick={this.onSubmit}>
|
<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>
|
</button>
|
||||||
{loading || latency ? <ElapsedTime time={latency} className="text-info" /> : null}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -605,8 +833,6 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
<QueryRows
|
<QueryRows
|
||||||
history={history}
|
history={history}
|
||||||
queries={queries}
|
queries={queries}
|
||||||
queryErrors={queryErrors}
|
|
||||||
queryHints={queryHints}
|
|
||||||
request={this.request}
|
request={this.request}
|
||||||
onAddQueryRow={this.onAddQueryRow}
|
onAddQueryRow={this.onAddQueryRow}
|
||||||
onChangeQuery={this.onChangeQuery}
|
onChangeQuery={this.onChangeQuery}
|
||||||
@ -614,6 +840,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
onExecuteQuery={this.onSubmit}
|
onExecuteQuery={this.onSubmit}
|
||||||
onRemoveQueryRow={this.onRemoveQueryRow}
|
onRemoveQueryRow={this.onRemoveQueryRow}
|
||||||
supportsLogs={supportsLogs}
|
supportsLogs={supportsLogs}
|
||||||
|
transactions={queryTransactions}
|
||||||
/>
|
/>
|
||||||
<div className="result-options">
|
<div className="result-options">
|
||||||
{supportsGraph ? (
|
{supportsGraph ? (
|
||||||
@ -635,23 +862,22 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
|
|
||||||
<main className="m-t-2">
|
<main className="m-t-2">
|
||||||
{supportsGraph &&
|
{supportsGraph &&
|
||||||
showingGraph &&
|
showingGraph && (
|
||||||
graphResult && (
|
|
||||||
<Graph
|
<Graph
|
||||||
data={graphResult}
|
data={graphResult}
|
||||||
height={graphHeight}
|
height={graphHeight}
|
||||||
loading={loading}
|
loading={graphLoading}
|
||||||
id={`explore-graph-${position}`}
|
id={`explore-graph-${position}`}
|
||||||
options={requestOptions}
|
range={graphRange}
|
||||||
split={split}
|
split={split}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{supportsTable && showingTable ? (
|
{supportsTable && showingTable ? (
|
||||||
<div className="panel-container">
|
<div className="panel-container m-t-2">
|
||||||
<Table data={tableResult} loading={loading} onClickCell={this.onClickTableCell} />
|
<Table data={tableResult} loading={tableLoading} onClickCell={this.onClickTableCell} />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{supportsLogs && showingLogs ? <Logs data={logsResult} loading={loading} /> : null}
|
{supportsLogs && showingLogs ? <Logs data={logsResult} loading={logsLoading} /> : null}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -4,24 +4,11 @@ import { Graph } from './Graph';
|
|||||||
import { mockData } from './__mocks__/mockData';
|
import { mockData } from './__mocks__/mockData';
|
||||||
|
|
||||||
const setup = (propOverrides?: object) => {
|
const setup = (propOverrides?: object) => {
|
||||||
const props = Object.assign(
|
const props = {
|
||||||
{
|
|
||||||
data: mockData().slice(0, 19),
|
data: mockData().slice(0, 19),
|
||||||
options: {
|
|
||||||
interval: '20s',
|
|
||||||
range: { from: 'now-6h', to: 'now' },
|
range: { from: 'now-6h', to: 'now' },
|
||||||
targets: [
|
...propOverrides,
|
||||||
{
|
};
|
||||||
format: 'time_series',
|
|
||||||
instant: false,
|
|
||||||
hinting: true,
|
|
||||||
expr: 'prometheus_http_request_duration_seconds_bucket',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
propOverrides
|
|
||||||
);
|
|
||||||
|
|
||||||
// Enzyme.shallow did not work well with jquery.flop. Mocking the draw function.
|
// Enzyme.shallow did not work well with jquery.flop. Mocking the draw function.
|
||||||
Graph.prototype.draw = jest.fn();
|
Graph.prototype.draw = jest.fn();
|
||||||
|
@ -5,6 +5,8 @@ import { withSize } from 'react-sizeme';
|
|||||||
|
|
||||||
import 'vendor/flot/jquery.flot';
|
import 'vendor/flot/jquery.flot';
|
||||||
import 'vendor/flot/jquery.flot.time';
|
import 'vendor/flot/jquery.flot.time';
|
||||||
|
|
||||||
|
import { Range } from 'app/types/explore';
|
||||||
import * as dateMath from 'app/core/utils/datemath';
|
import * as dateMath from 'app/core/utils/datemath';
|
||||||
import TimeSeries from 'app/core/time_series2';
|
import TimeSeries from 'app/core/time_series2';
|
||||||
|
|
||||||
@ -74,7 +76,7 @@ interface GraphProps {
|
|||||||
height?: string; // e.g., '200px'
|
height?: string; // e.g., '200px'
|
||||||
id?: string;
|
id?: string;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
options: any;
|
range: Range;
|
||||||
split?: boolean;
|
split?: boolean;
|
||||||
size?: { width: number; height: number };
|
size?: { width: number; height: number };
|
||||||
}
|
}
|
||||||
@ -101,7 +103,7 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
|
|||||||
componentDidUpdate(prevProps: GraphProps) {
|
componentDidUpdate(prevProps: GraphProps) {
|
||||||
if (
|
if (
|
||||||
prevProps.data !== this.props.data ||
|
prevProps.data !== this.props.data ||
|
||||||
prevProps.options !== this.props.options ||
|
prevProps.range !== this.props.range ||
|
||||||
prevProps.split !== this.props.split ||
|
prevProps.split !== this.props.split ||
|
||||||
prevProps.height !== this.props.height ||
|
prevProps.height !== this.props.height ||
|
||||||
(prevProps.size && prevProps.size.width !== this.props.size.width)
|
(prevProps.size && prevProps.size.width !== this.props.size.width)
|
||||||
@ -120,22 +122,22 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
draw() {
|
draw() {
|
||||||
const { options: userOptions, size } = this.props;
|
const { range, size } = this.props;
|
||||||
const data = this.getGraphData();
|
const data = this.getGraphData();
|
||||||
|
|
||||||
const $el = $(`#${this.props.id}`);
|
const $el = $(`#${this.props.id}`);
|
||||||
if (!data) {
|
let series = [{ data: [[0, 0]] }];
|
||||||
$el.empty();
|
|
||||||
return;
|
if (data && data.length > 0) {
|
||||||
}
|
series = data.map((ts: TimeSeries) => ({
|
||||||
const series = data.map((ts: TimeSeries) => ({
|
|
||||||
color: ts.color,
|
color: ts.color,
|
||||||
label: ts.label,
|
label: ts.label,
|
||||||
data: ts.getFlotPairs('null'),
|
data: ts.getFlotPairs('null'),
|
||||||
}));
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
const ticks = (size.width || 0) / 100;
|
const ticks = (size.width || 0) / 100;
|
||||||
let { from, to } = userOptions.range;
|
let { from, to } = range;
|
||||||
if (!moment.isMoment(from)) {
|
if (!moment.isMoment(from)) {
|
||||||
from = dateMath.parse(from, false);
|
from = dateMath.parse(from, false);
|
||||||
}
|
}
|
||||||
@ -157,7 +159,6 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
|
|||||||
const options = {
|
const options = {
|
||||||
...FLOT_OPTIONS,
|
...FLOT_OPTIONS,
|
||||||
...dynamicOptions,
|
...dynamicOptions,
|
||||||
...userOptions,
|
|
||||||
};
|
};
|
||||||
$.plot($el, series, options);
|
$.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 { height = '100px', id = 'graph', loading = false } = this.props;
|
||||||
const data = this.getGraphData();
|
const data = this.getGraphData();
|
||||||
|
|
||||||
if (!loading && data.length === 0) {
|
|
||||||
return (
|
return (
|
||||||
<div className="panel-container">
|
<>
|
||||||
<div className="muted m-a-1">The queries returned no time series to graph.</div>
|
{this.props.data &&
|
||||||
</div>
|
this.props.data.length > MAX_NUMBER_OF_TIME_SERIES &&
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{this.props.data.length > MAX_NUMBER_OF_TIME_SERIES &&
|
|
||||||
!this.state.showAllTimeSeries && (
|
!this.state.showAllTimeSeries && (
|
||||||
<div className="time-series-disclaimer">
|
<div className="time-series-disclaimer">
|
||||||
<i className="fa fa-fw fa-warning disclaimer-icon" />
|
<i className="fa fa-fw fa-warning disclaimer-icon" />
|
||||||
@ -186,10 +181,11 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="panel-container">
|
<div className="panel-container">
|
||||||
|
{loading && <div className="explore-graph__loader" />}
|
||||||
<div id={id} className="explore-graph" style={{ height }} />
|
<div id={id} className="explore-graph" style={{ height }} />
|
||||||
<Legend data={data} />
|
<Legend data={data} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,18 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
|
|
||||||
|
import { QueryTransaction } from 'app/types/explore';
|
||||||
|
|
||||||
// TODO make this datasource-plugin-dependent
|
// TODO make this datasource-plugin-dependent
|
||||||
import QueryField from './PromQueryField';
|
import QueryField from './PromQueryField';
|
||||||
|
import QueryTransactions from './QueryTransactions';
|
||||||
|
|
||||||
|
function getFirstHintFromTransactions(transactions: QueryTransaction[]) {
|
||||||
|
const transaction = transactions.find(qt => qt.hints && qt.hints.length > 0);
|
||||||
|
if (transaction) {
|
||||||
|
return transaction.hints[0];
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
class QueryRow extends PureComponent<any, {}> {
|
class QueryRow extends PureComponent<any, {}> {
|
||||||
onChangeQuery = (value, override?: boolean) => {
|
onChangeQuery = (value, override?: boolean) => {
|
||||||
@ -44,13 +55,19 @@ class QueryRow extends PureComponent<any, {}> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { history, query, queryError, queryHint, request, supportsLogs } = this.props;
|
const { history, query, request, supportsLogs, transactions } = this.props;
|
||||||
|
const transactionWithError = transactions.find(t => t.error);
|
||||||
|
const hint = getFirstHintFromTransactions(transactions);
|
||||||
|
const queryError = transactionWithError ? transactionWithError.error : null;
|
||||||
return (
|
return (
|
||||||
<div className="query-row">
|
<div className="query-row">
|
||||||
|
<div className="query-row-status">
|
||||||
|
<QueryTransactions transactions={transactions} />
|
||||||
|
</div>
|
||||||
<div className="query-row-field">
|
<div className="query-row-field">
|
||||||
<QueryField
|
<QueryField
|
||||||
error={queryError}
|
error={queryError}
|
||||||
hint={queryHint}
|
hint={hint}
|
||||||
initialQuery={query}
|
initialQuery={query}
|
||||||
history={history}
|
history={history}
|
||||||
onClickHintFix={this.onClickHintFix}
|
onClickHintFix={this.onClickHintFix}
|
||||||
@ -78,7 +95,7 @@ class QueryRow extends PureComponent<any, {}> {
|
|||||||
|
|
||||||
export default class QueryRows extends PureComponent<any, {}> {
|
export default class QueryRows extends PureComponent<any, {}> {
|
||||||
render() {
|
render() {
|
||||||
const { className = '', queries, queryErrors, queryHints, ...handlers } = this.props;
|
const { className = '', queries, queryHints, transactions, ...handlers } = this.props;
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
{queries.map((q, index) => (
|
{queries.map((q, index) => (
|
||||||
@ -86,8 +103,7 @@ export default class QueryRows extends PureComponent<any, {}> {
|
|||||||
key={q.key}
|
key={q.key}
|
||||||
index={index}
|
index={index}
|
||||||
query={q.query}
|
query={q.query}
|
||||||
queryError={queryErrors[index]}
|
transactions={transactions.filter(t => t.rowIndex === index)}
|
||||||
queryHint={queryHints[index]}
|
|
||||||
{...handlers}
|
{...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}
|
minRows={0}
|
||||||
noDataText={noDataText}
|
noDataText={noDataText}
|
||||||
resolveData={data => prepareRows(data, columnNames)}
|
resolveData={data => prepareRows(data, columnNames)}
|
||||||
showPagination={data}
|
showPagination={Boolean(data)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`Render should render component 1`] = `
|
exports[`Render should render component 1`] = `
|
||||||
<div>
|
<Fragment>
|
||||||
<div
|
<div
|
||||||
className="panel-container"
|
className="panel-container"
|
||||||
>
|
>
|
||||||
@ -458,11 +458,11 @@ exports[`Render should render component 1`] = `
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Fragment>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Render should render component with disclaimer 1`] = `
|
exports[`Render should render component with disclaimer 1`] = `
|
||||||
<div>
|
<Fragment>
|
||||||
<div
|
<div
|
||||||
className="time-series-disclaimer"
|
className="time-series-disclaimer"
|
||||||
>
|
>
|
||||||
@ -956,17 +956,26 @@ exports[`Render should render component with disclaimer 1`] = `
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Fragment>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Render should show query return no time series 1`] = `
|
exports[`Render should show query return no time series 1`] = `
|
||||||
<div
|
<Fragment>
|
||||||
className="panel-container"
|
|
||||||
>
|
|
||||||
<div
|
<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>
|
||||||
</div>
|
</Fragment>
|
||||||
`;
|
`;
|
||||||
|
@ -176,7 +176,6 @@ export class PrometheusDatasource {
|
|||||||
|
|
||||||
return this.$q.all(allQueryPromise).then(responseList => {
|
return this.$q.all(allQueryPromise).then(responseList => {
|
||||||
let result = [];
|
let result = [];
|
||||||
let hints = [];
|
|
||||||
|
|
||||||
_.each(responseList, (response, index) => {
|
_.each(responseList, (response, index) => {
|
||||||
if (response.status === 'error') {
|
if (response.status === 'error') {
|
||||||
@ -196,19 +195,14 @@ export class PrometheusDatasource {
|
|||||||
end: queries[index].end,
|
end: queries[index].end,
|
||||||
query: queries[index].expr,
|
query: queries[index].expr,
|
||||||
responseListLength: responseList.length,
|
responseListLength: responseList.length,
|
||||||
responseIndex: index,
|
|
||||||
refId: activeTargets[index].refId,
|
refId: activeTargets[index].refId,
|
||||||
|
valueWithRefId: activeTargets[index].valueWithRefId,
|
||||||
};
|
};
|
||||||
const series = this.resultTransformer.transform(response, transformerOptions);
|
const series = this.resultTransformer.transform(response, transformerOptions);
|
||||||
result = [...result, ...series];
|
result = [...result, ...series];
|
||||||
|
|
||||||
if (queries[index].hinting) {
|
|
||||||
const queryHints = getQueryHints(series, this);
|
|
||||||
hints = [...hints, ...queryHints];
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return { data: result, hints };
|
return { data: result };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -437,6 +431,10 @@ export class PrometheusDatasource {
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getQueryHints(query: string, result: any[]) {
|
||||||
|
return getQueryHints(query, result, this);
|
||||||
|
}
|
||||||
|
|
||||||
loadRules() {
|
loadRules() {
|
||||||
this.metadataRequest('/api/v1/rules')
|
this.metadataRequest('/api/v1/rules')
|
||||||
.then(res => res.data || res.json())
|
.then(res => res.data || res.json())
|
||||||
|
@ -1,32 +1,28 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
export function getQueryHints(series: any[], datasource?: any): any[] {
|
export function getQueryHints(query: string, series?: any[], datasource?: any): any[] {
|
||||||
const hints = series.map((s, i) => {
|
const hints = [];
|
||||||
const query: string = s.query;
|
|
||||||
const index: number = s.responseIndex;
|
|
||||||
if (query === undefined || index === undefined) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ..._bucket metric needs a histogram_quantile()
|
// ..._bucket metric needs a histogram_quantile()
|
||||||
const histogramMetric = query.trim().match(/^\w+_bucket$/);
|
const histogramMetric = query.trim().match(/^\w+_bucket$/);
|
||||||
if (histogramMetric) {
|
if (histogramMetric) {
|
||||||
const label = 'Time series has buckets, you probably wanted a histogram.';
|
const label = 'Time series has buckets, you probably wanted a histogram.';
|
||||||
return {
|
hints.push({
|
||||||
index,
|
type: 'HISTOGRAM_QUANTILE',
|
||||||
label,
|
label,
|
||||||
fix: {
|
fix: {
|
||||||
label: 'Fix by adding histogram_quantile().',
|
label: 'Fix by adding histogram_quantile().',
|
||||||
action: {
|
action: {
|
||||||
type: 'ADD_HISTOGRAM_QUANTILE',
|
type: 'ADD_HISTOGRAM_QUANTILE',
|
||||||
query,
|
query,
|
||||||
index,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for monotony
|
// Check for monotony on series (table results are being ignored here)
|
||||||
|
if (series && series.length > 0) {
|
||||||
|
series.forEach(s => {
|
||||||
const datapoints: number[][] = s.datapoints;
|
const datapoints: number[][] = s.datapoints;
|
||||||
if (query.indexOf('rate(') === -1 && datapoints.length > 1) {
|
if (query.indexOf('rate(') === -1 && datapoints.length > 1) {
|
||||||
let increasing = false;
|
let increasing = false;
|
||||||
@ -49,19 +45,20 @@ export function getQueryHints(series: any[], datasource?: any): any[] {
|
|||||||
action: {
|
action: {
|
||||||
type: 'ADD_RATE',
|
type: 'ADD_RATE',
|
||||||
query,
|
query,
|
||||||
index,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
label = `${label} Try applying a rate() function.`;
|
label = `${label} Try applying a rate() function.`;
|
||||||
}
|
}
|
||||||
return {
|
hints.push({
|
||||||
|
type: 'APPLY_RATE',
|
||||||
label,
|
label,
|
||||||
index,
|
|
||||||
fix,
|
fix,
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Check for recording rules expansion
|
// Check for recording rules expansion
|
||||||
if (datasource && datasource.ruleMappings) {
|
if (datasource && datasource.ruleMappings) {
|
||||||
@ -77,24 +74,19 @@ export function getQueryHints(series: any[], datasource?: any): any[] {
|
|||||||
}, {});
|
}, {});
|
||||||
if (_.size(mappingForQuery) > 0) {
|
if (_.size(mappingForQuery) > 0) {
|
||||||
const label = 'Query contains recording rules.';
|
const label = 'Query contains recording rules.';
|
||||||
return {
|
hints.push({
|
||||||
|
type: 'EXPAND_RULES',
|
||||||
label,
|
label,
|
||||||
index,
|
|
||||||
fix: {
|
fix: {
|
||||||
label: 'Expand rules',
|
label: 'Expand rules',
|
||||||
action: {
|
action: {
|
||||||
type: 'EXPAND_RULES',
|
type: 'EXPAND_RULES',
|
||||||
query,
|
query,
|
||||||
index,
|
|
||||||
mapping: mappingForQuery,
|
mapping: mappingForQuery,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// No hint found
|
|
||||||
return null;
|
|
||||||
});
|
});
|
||||||
return hints;
|
}
|
||||||
|
}
|
||||||
|
return hints.length > 0 ? hints : null;
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,14 @@ export class ResultTransformer {
|
|||||||
const prometheusResult = response.data.data.result;
|
const prometheusResult = response.data.data.result;
|
||||||
|
|
||||||
if (options.format === 'table') {
|
if (options.format === 'table') {
|
||||||
return [this.transformMetricDataToTable(prometheusResult, options.responseListLength, options.refId)];
|
return [
|
||||||
|
this.transformMetricDataToTable(
|
||||||
|
prometheusResult,
|
||||||
|
options.responseListLength,
|
||||||
|
options.refId,
|
||||||
|
options.valueWithRefId
|
||||||
|
),
|
||||||
|
];
|
||||||
} else if (options.format === 'heatmap') {
|
} else if (options.format === 'heatmap') {
|
||||||
let seriesList = [];
|
let seriesList = [];
|
||||||
prometheusResult.sort(sortSeriesByLabel);
|
prometheusResult.sort(sortSeriesByLabel);
|
||||||
@ -66,12 +73,11 @@ export class ResultTransformer {
|
|||||||
return {
|
return {
|
||||||
datapoints: dps,
|
datapoints: dps,
|
||||||
query: options.query,
|
query: options.query,
|
||||||
responseIndex: options.responseIndex,
|
|
||||||
target: metricLabel,
|
target: metricLabel,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
transformMetricDataToTable(md, resultCount: number, refId: string) {
|
transformMetricDataToTable(md, resultCount: number, refId: string, valueWithRefId?: boolean) {
|
||||||
const table = new TableModel();
|
const table = new TableModel();
|
||||||
let i, j;
|
let i, j;
|
||||||
const metricLabels = {};
|
const metricLabels = {};
|
||||||
@ -96,7 +102,7 @@ export class ResultTransformer {
|
|||||||
metricLabels[label] = labelIndex + 1;
|
metricLabels[label] = labelIndex + 1;
|
||||||
table.columns.push({ text: label, filterable: !label.startsWith('__') });
|
table.columns.push({ text: label, filterable: !label.startsWith('__') });
|
||||||
});
|
});
|
||||||
const valueText = resultCount > 1 ? `Value #${refId}` : 'Value';
|
const valueText = resultCount > 1 || valueWithRefId ? `Value #${refId}` : 'Value';
|
||||||
table.columns.push({ text: valueText });
|
table.columns.push({ text: valueText });
|
||||||
|
|
||||||
// Populate rows, set value to empty string when label not present.
|
// Populate rows, set value to empty string when label not present.
|
||||||
|
@ -2,34 +2,31 @@ import { getQueryHints } from '../query_hints';
|
|||||||
|
|
||||||
describe('getQueryHints()', () => {
|
describe('getQueryHints()', () => {
|
||||||
it('returns no hints for no series', () => {
|
it('returns no hints for no series', () => {
|
||||||
expect(getQueryHints([])).toEqual([]);
|
expect(getQueryHints('', [])).toEqual(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns no hints for empty series', () => {
|
it('returns no hints for empty series', () => {
|
||||||
expect(getQueryHints([{ datapoints: [], query: '' }])).toEqual([null]);
|
expect(getQueryHints('', [{ datapoints: [] }])).toEqual(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns no hint for a monotonously decreasing series', () => {
|
it('returns no hint for a monotonously decreasing series', () => {
|
||||||
const series = [{ datapoints: [[23, 1000], [22, 1001]], query: 'metric', responseIndex: 0 }];
|
const series = [{ datapoints: [[23, 1000], [22, 1001]] }];
|
||||||
const hints = getQueryHints(series);
|
const hints = getQueryHints('metric', series);
|
||||||
expect(hints).toEqual([null]);
|
expect(hints).toEqual(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns no hint for a flat series', () => {
|
it('returns no hint for a flat series', () => {
|
||||||
const series = [
|
const series = [{ datapoints: [[null, 1000], [23, 1001], [null, 1002], [23, 1003]] }];
|
||||||
{ datapoints: [[null, 1000], [23, 1001], [null, 1002], [23, 1003]], query: 'metric', responseIndex: 0 },
|
const hints = getQueryHints('metric', series);
|
||||||
];
|
expect(hints).toEqual(null);
|
||||||
const hints = getQueryHints(series);
|
|
||||||
expect(hints).toEqual([null]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns a rate hint for a monotonously increasing series', () => {
|
it('returns a rate hint for a monotonously increasing series', () => {
|
||||||
const series = [{ datapoints: [[23, 1000], [24, 1001]], query: 'metric', responseIndex: 0 }];
|
const series = [{ datapoints: [[23, 1000], [24, 1001]] }];
|
||||||
const hints = getQueryHints(series);
|
const hints = getQueryHints('metric', series);
|
||||||
expect(hints.length).toBe(1);
|
expect(hints.length).toBe(1);
|
||||||
expect(hints[0]).toMatchObject({
|
expect(hints[0]).toMatchObject({
|
||||||
label: 'Time series is monotonously increasing.',
|
label: 'Time series is monotonously increasing.',
|
||||||
index: 0,
|
|
||||||
fix: {
|
fix: {
|
||||||
action: {
|
action: {
|
||||||
type: 'ADD_RATE',
|
type: 'ADD_RATE',
|
||||||
@ -40,26 +37,25 @@ describe('getQueryHints()', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('returns no rate hint for a monotonously increasing series that already has a rate', () => {
|
it('returns no rate hint for a monotonously increasing series that already has a rate', () => {
|
||||||
const series = [{ datapoints: [[23, 1000], [24, 1001]], query: 'rate(metric[1m])', responseIndex: 0 }];
|
const series = [{ datapoints: [[23, 1000], [24, 1001]] }];
|
||||||
const hints = getQueryHints(series);
|
const hints = getQueryHints('rate(metric[1m])', series);
|
||||||
expect(hints).toEqual([null]);
|
expect(hints).toEqual(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns a rate hint w/o action for a complex monotonously increasing series', () => {
|
it('returns a rate hint w/o action for a complex monotonously increasing series', () => {
|
||||||
const series = [{ datapoints: [[23, 1000], [24, 1001]], query: 'sum(metric)', responseIndex: 0 }];
|
const series = [{ datapoints: [[23, 1000], [24, 1001]] }];
|
||||||
const hints = getQueryHints(series);
|
const hints = getQueryHints('sum(metric)', series);
|
||||||
expect(hints.length).toBe(1);
|
expect(hints.length).toBe(1);
|
||||||
expect(hints[0].label).toContain('rate()');
|
expect(hints[0].label).toContain('rate()');
|
||||||
expect(hints[0].fix).toBeUndefined();
|
expect(hints[0].fix).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns a rate hint for a monotonously increasing series with missing data', () => {
|
it('returns a rate hint for a monotonously increasing series with missing data', () => {
|
||||||
const series = [{ datapoints: [[23, 1000], [null, 1001], [24, 1002]], query: 'metric', responseIndex: 0 }];
|
const series = [{ datapoints: [[23, 1000], [null, 1001], [24, 1002]] }];
|
||||||
const hints = getQueryHints(series);
|
const hints = getQueryHints('metric', series);
|
||||||
expect(hints.length).toBe(1);
|
expect(hints.length).toBe(1);
|
||||||
expect(hints[0]).toMatchObject({
|
expect(hints[0]).toMatchObject({
|
||||||
label: 'Time series is monotonously increasing.',
|
label: 'Time series is monotonously increasing.',
|
||||||
index: 0,
|
|
||||||
fix: {
|
fix: {
|
||||||
action: {
|
action: {
|
||||||
type: 'ADD_RATE',
|
type: 'ADD_RATE',
|
||||||
@ -70,12 +66,11 @@ describe('getQueryHints()', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('returns a histogram hint for a bucket series', () => {
|
it('returns a histogram hint for a bucket series', () => {
|
||||||
const series = [{ datapoints: [[23, 1000]], query: 'metric_bucket', responseIndex: 0 }];
|
const series = [{ datapoints: [[23, 1000]] }];
|
||||||
const hints = getQueryHints(series);
|
const hints = getQueryHints('metric_bucket', series);
|
||||||
expect(hints.length).toBe(1);
|
expect(hints.length).toBe(1);
|
||||||
expect(hints[0]).toMatchObject({
|
expect(hints[0]).toMatchObject({
|
||||||
label: 'Time series has buckets, you probably wanted a histogram.',
|
label: 'Time series has buckets, you probably wanted a histogram.',
|
||||||
index: 0,
|
|
||||||
fix: {
|
fix: {
|
||||||
action: {
|
action: {
|
||||||
type: 'ADD_HISTOGRAM_QUANTILE',
|
type: 'ADD_HISTOGRAM_QUANTILE',
|
||||||
|
@ -3,6 +3,11 @@ interface ExploreDatasource {
|
|||||||
label: string;
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface HistoryItem {
|
||||||
|
ts: number;
|
||||||
|
query: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Range {
|
export interface Range {
|
||||||
from: string;
|
from: string;
|
||||||
to: string;
|
to: string;
|
||||||
@ -13,6 +18,19 @@ export interface Query {
|
|||||||
key?: string;
|
key?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface QueryTransaction {
|
||||||
|
id: string;
|
||||||
|
done: boolean;
|
||||||
|
error?: string;
|
||||||
|
hints?: any[];
|
||||||
|
latency: number;
|
||||||
|
options: any;
|
||||||
|
query: string;
|
||||||
|
result?: any; // Table model / Timeseries[] / Logs
|
||||||
|
resultType: ResultType;
|
||||||
|
rowIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TextMatch {
|
export interface TextMatch {
|
||||||
text: string;
|
text: string;
|
||||||
start: number;
|
start: number;
|
||||||
@ -27,34 +45,25 @@ export interface ExploreState {
|
|||||||
datasourceMissing: boolean;
|
datasourceMissing: boolean;
|
||||||
datasourceName?: string;
|
datasourceName?: string;
|
||||||
exploreDatasources: ExploreDatasource[];
|
exploreDatasources: ExploreDatasource[];
|
||||||
graphResult: any;
|
graphRange: Range;
|
||||||
history: any[];
|
history: HistoryItem[];
|
||||||
latency: number;
|
|
||||||
loading: any;
|
|
||||||
logsResult: any;
|
|
||||||
/**
|
/**
|
||||||
* Initial rows of queries to push down the tree.
|
* Initial rows of queries to push down the tree.
|
||||||
* Modifications do not end up here, but in `this.queryExpressions`.
|
* Modifications do not end up here, but in `this.queryExpressions`.
|
||||||
* The only way to reset a query is to change its `key`.
|
* The only way to reset a query is to change its `key`.
|
||||||
*/
|
*/
|
||||||
queries: Query[];
|
queries: Query[];
|
||||||
/**
|
|
||||||
* Errors caused by the running the query row.
|
|
||||||
*/
|
|
||||||
queryErrors: any[];
|
|
||||||
/**
|
/**
|
||||||
* Hints gathered for the query row.
|
* Hints gathered for the query row.
|
||||||
*/
|
*/
|
||||||
queryHints: any[];
|
queryTransactions: QueryTransaction[];
|
||||||
range: Range;
|
range: Range;
|
||||||
requestOptions: any;
|
|
||||||
showingGraph: boolean;
|
showingGraph: boolean;
|
||||||
showingLogs: boolean;
|
showingLogs: boolean;
|
||||||
showingTable: boolean;
|
showingTable: boolean;
|
||||||
supportsGraph: boolean | null;
|
supportsGraph: boolean | null;
|
||||||
supportsLogs: boolean | null;
|
supportsLogs: boolean | null;
|
||||||
supportsTable: boolean | null;
|
supportsTable: boolean | null;
|
||||||
tableResult: any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExploreUrlState {
|
export interface ExploreUrlState {
|
||||||
@ -62,3 +71,5 @@ export interface ExploreUrlState {
|
|||||||
queries: Query[];
|
queries: Query[];
|
||||||
range: Range;
|
range: Range;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ResultType = 'Graph' | 'Logs' | 'Table';
|
||||||
|
@ -74,7 +74,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.elapsed-time {
|
.navbar .elapsed-time {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
@ -87,6 +87,37 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.explore-graph__loader {
|
||||||
|
height: 2px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background: $text-color-faint;
|
||||||
|
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 {
|
.datasource-picker {
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
}
|
}
|
||||||
@ -119,6 +150,7 @@
|
|||||||
|
|
||||||
.query-row {
|
.query-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
& + & {
|
& + & {
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
@ -129,11 +161,53 @@
|
|||||||
white-space: nowrap;
|
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 {
|
.query-row-field {
|
||||||
margin-right: 3px;
|
margin-right: 3px;
|
||||||
width: 100%;
|
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 {
|
.explore {
|
||||||
.logs {
|
.logs {
|
||||||
.logs-entries {
|
.logs-entries {
|
||||||
|
Loading…
Reference in New Issue
Block a user