mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge pull request #13491 from grafana/davkal/explore-perf
Explore: typeahead and render performance improvements
This commit is contained in:
commit
406b6144a5
@ -7,6 +7,7 @@ const DEFAULT_EXPLORE_STATE: ExploreState = {
|
|||||||
datasourceLoading: null,
|
datasourceLoading: null,
|
||||||
datasourceMissing: false,
|
datasourceMissing: false,
|
||||||
datasourceName: '',
|
datasourceName: '',
|
||||||
|
exploreDatasources: [],
|
||||||
graphResult: null,
|
graphResult: null,
|
||||||
history: [],
|
history: [],
|
||||||
latency: 0,
|
latency: 0,
|
||||||
|
@ -2,7 +2,7 @@ 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 { ExploreState, ExploreUrlState } from 'app/types/explore';
|
import { ExploreState, ExploreUrlState, Query } 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';
|
||||||
@ -61,37 +61,50 @@ interface ExploreProps {
|
|||||||
|
|
||||||
export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||||
el: any;
|
el: any;
|
||||||
|
/**
|
||||||
|
* Current query expressions of the rows including their modifications, used for running queries.
|
||||||
|
* Not kept in component state to prevent edit-render roundtrips.
|
||||||
|
*/
|
||||||
|
queryExpressions: string[];
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
// Split state overrides everything
|
|
||||||
const splitState: ExploreState = props.splitState;
|
const splitState: ExploreState = props.splitState;
|
||||||
const { datasource, queries, range } = props.urlState;
|
let initialQueries: Query[];
|
||||||
this.state = {
|
if (splitState) {
|
||||||
datasource: null,
|
// Split state overrides everything
|
||||||
datasourceError: null,
|
this.state = splitState;
|
||||||
datasourceLoading: null,
|
initialQueries = splitState.queries;
|
||||||
datasourceMissing: false,
|
} else {
|
||||||
datasourceName: datasource,
|
const { datasource, queries, range } = props.urlState as ExploreUrlState;
|
||||||
graphResult: null,
|
initialQueries = ensureQueries(queries);
|
||||||
history: [],
|
this.state = {
|
||||||
latency: 0,
|
datasource: null,
|
||||||
loading: false,
|
datasourceError: null,
|
||||||
logsResult: null,
|
datasourceLoading: null,
|
||||||
queries: ensureQueries(queries),
|
datasourceMissing: false,
|
||||||
queryErrors: [],
|
datasourceName: datasource,
|
||||||
queryHints: [],
|
exploreDatasources: [],
|
||||||
range: range || { ...DEFAULT_RANGE },
|
graphResult: null,
|
||||||
requestOptions: null,
|
history: [],
|
||||||
showingGraph: true,
|
latency: 0,
|
||||||
showingLogs: true,
|
loading: false,
|
||||||
showingTable: true,
|
logsResult: null,
|
||||||
supportsGraph: null,
|
queries: initialQueries,
|
||||||
supportsLogs: null,
|
queryErrors: [],
|
||||||
supportsTable: null,
|
queryHints: [],
|
||||||
tableResult: null,
|
range: range || { ...DEFAULT_RANGE },
|
||||||
...splitState,
|
requestOptions: null,
|
||||||
};
|
showingGraph: true,
|
||||||
|
showingLogs: true,
|
||||||
|
showingTable: true,
|
||||||
|
supportsGraph: null,
|
||||||
|
supportsLogs: null,
|
||||||
|
supportsTable: null,
|
||||||
|
tableResult: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
this.queryExpressions = initialQueries.map(q => q.query);
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
@ -101,8 +114,13 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
throw new Error('No datasource service passed as props.');
|
throw new Error('No datasource service passed as props.');
|
||||||
}
|
}
|
||||||
const datasources = datasourceSrv.getExploreSources();
|
const datasources = datasourceSrv.getExploreSources();
|
||||||
|
const exploreDatasources = datasources.map(ds => ({
|
||||||
|
value: ds.name,
|
||||||
|
label: ds.name,
|
||||||
|
}));
|
||||||
|
|
||||||
if (datasources.length > 0) {
|
if (datasources.length > 0) {
|
||||||
this.setState({ datasourceLoading: true });
|
this.setState({ datasourceLoading: true, exploreDatasources });
|
||||||
// Priority: datasource in url, default datasource, first explore datasource
|
// Priority: datasource in url, default datasource, first explore datasource
|
||||||
let datasource;
|
let datasource;
|
||||||
if (datasourceName) {
|
if (datasourceName) {
|
||||||
@ -146,9 +164,10 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Keep queries but reset edit state
|
// Keep queries but reset edit state
|
||||||
const nextQueries = this.state.queries.map(q => ({
|
const nextQueries = this.state.queries.map((q, i) => ({
|
||||||
...q,
|
...q,
|
||||||
edited: false,
|
key: generateQueryKey(i),
|
||||||
|
query: this.queryExpressions[i],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.setState(
|
this.setState(
|
||||||
@ -177,6 +196,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
|
|
||||||
onAddQueryRow = index => {
|
onAddQueryRow = index => {
|
||||||
const { queries } = this.state;
|
const { queries } = this.state;
|
||||||
|
this.queryExpressions[index + 1] = '';
|
||||||
const nextQueries = [
|
const nextQueries = [
|
||||||
...queries.slice(0, index + 1),
|
...queries.slice(0, index + 1),
|
||||||
{ query: '', key: generateQueryKey() },
|
{ query: '', key: generateQueryKey() },
|
||||||
@ -203,29 +223,28 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onChangeQuery = (value: string, index: number, override?: boolean) => {
|
onChangeQuery = (value: string, index: number, override?: boolean) => {
|
||||||
const { queries } = this.state;
|
// Keep current value in local cache
|
||||||
let { queryErrors, queryHints } = this.state;
|
this.queryExpressions[index] = value;
|
||||||
const prevQuery = queries[index];
|
|
||||||
const edited = override ? false : prevQuery.query !== value;
|
// Replace query row on override
|
||||||
const nextQuery = {
|
|
||||||
...queries[index],
|
|
||||||
edited,
|
|
||||||
query: value,
|
|
||||||
};
|
|
||||||
const nextQueries = [...queries];
|
|
||||||
nextQueries[index] = nextQuery;
|
|
||||||
if (override) {
|
if (override) {
|
||||||
queryErrors = [];
|
const { queries } = this.state;
|
||||||
queryHints = [];
|
const nextQuery: Query = {
|
||||||
|
key: generateQueryKey(index),
|
||||||
|
query: value,
|
||||||
|
};
|
||||||
|
const nextQueries = [...queries];
|
||||||
|
nextQueries[index] = nextQuery;
|
||||||
|
|
||||||
|
this.setState(
|
||||||
|
{
|
||||||
|
queryErrors: [],
|
||||||
|
queryHints: [],
|
||||||
|
queries: nextQueries,
|
||||||
|
},
|
||||||
|
this.onSubmit
|
||||||
|
);
|
||||||
}
|
}
|
||||||
this.setState(
|
|
||||||
{
|
|
||||||
queryErrors,
|
|
||||||
queryHints,
|
|
||||||
queries: nextQueries,
|
|
||||||
},
|
|
||||||
override ? () => this.onSubmit() : undefined
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onChangeTime = nextRange => {
|
onChangeTime = nextRange => {
|
||||||
@ -237,6 +256,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onClickClear = () => {
|
onClickClear = () => {
|
||||||
|
this.queryExpressions = [''];
|
||||||
this.setState(
|
this.setState(
|
||||||
{
|
{
|
||||||
graphResult: null,
|
graphResult: null,
|
||||||
@ -269,9 +289,8 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
|
|
||||||
onClickSplit = () => {
|
onClickSplit = () => {
|
||||||
const { onChangeSplit } = this.props;
|
const { onChangeSplit } = this.props;
|
||||||
const state = { ...this.state };
|
|
||||||
state.queries = state.queries.map(({ edited, ...rest }) => rest);
|
|
||||||
if (onChangeSplit) {
|
if (onChangeSplit) {
|
||||||
|
const state = this.cloneState();
|
||||||
onChangeSplit(true, state);
|
onChangeSplit(true, state);
|
||||||
this.saveState();
|
this.saveState();
|
||||||
}
|
}
|
||||||
@ -291,23 +310,22 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
let nextQueries;
|
let nextQueries;
|
||||||
if (index === undefined) {
|
if (index === undefined) {
|
||||||
// Modify all queries
|
// Modify all queries
|
||||||
nextQueries = queries.map(q => ({
|
nextQueries = queries.map((q, i) => ({
|
||||||
...q,
|
key: generateQueryKey(i),
|
||||||
edited: false,
|
query: datasource.modifyQuery(this.queryExpressions[i], action),
|
||||||
query: datasource.modifyQuery(q.query, action),
|
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
// Modify query only at index
|
// Modify query only at index
|
||||||
nextQueries = [
|
nextQueries = [
|
||||||
...queries.slice(0, index),
|
...queries.slice(0, index),
|
||||||
{
|
{
|
||||||
...queries[index],
|
key: generateQueryKey(index),
|
||||||
edited: false,
|
query: datasource.modifyQuery(this.queryExpressions[index], action),
|
||||||
query: datasource.modifyQuery(queries[index].query, action),
|
|
||||||
},
|
},
|
||||||
...queries.slice(index + 1),
|
...queries.slice(index + 1),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
this.queryExpressions = nextQueries.map(q => q.query);
|
||||||
this.setState({ queries: nextQueries }, () => this.onSubmit());
|
this.setState({ queries: nextQueries }, () => this.onSubmit());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -318,6 +336,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
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());
|
this.setState({ queries: nextQueries }, () => this.onSubmit());
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -335,7 +354,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
this.saveState();
|
this.saveState();
|
||||||
};
|
};
|
||||||
|
|
||||||
onQuerySuccess(datasourceId: string, queries: any[]): void {
|
onQuerySuccess(datasourceId: string, queries: string[]): void {
|
||||||
// save queries to history
|
// save queries to history
|
||||||
let { history } = this.state;
|
let { history } = this.state;
|
||||||
const { datasource } = this.state;
|
const { datasource } = this.state;
|
||||||
@ -346,8 +365,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ts = Date.now();
|
const ts = Date.now();
|
||||||
queries.forEach(q => {
|
queries.forEach(query => {
|
||||||
const { query } = q;
|
|
||||||
history = [{ query, ts }, ...history];
|
history = [{ query, ts }, ...history];
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -362,16 +380,16 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildQueryOptions(targetOptions: { format: string; hinting?: boolean; instant?: boolean }) {
|
buildQueryOptions(targetOptions: { format: string; hinting?: boolean; instant?: boolean }) {
|
||||||
const { datasource, queries, range } = this.state;
|
const { datasource, range } = this.state;
|
||||||
const resolution = this.el.offsetWidth;
|
const resolution = this.el.offsetWidth;
|
||||||
const absoluteRange = {
|
const absoluteRange = {
|
||||||
from: parseDate(range.from, false),
|
from: parseDate(range.from, false),
|
||||||
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 = queries.map(q => ({
|
const targets = this.queryExpressions.map(q => ({
|
||||||
...targetOptions,
|
...targetOptions,
|
||||||
expr: q.query,
|
expr: q,
|
||||||
}));
|
}));
|
||||||
return {
|
return {
|
||||||
interval,
|
interval,
|
||||||
@ -381,7 +399,8 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async runGraphQuery() {
|
async runGraphQuery() {
|
||||||
const { datasource, queries } = this.state;
|
const { datasource } = this.state;
|
||||||
|
const queries = [...this.queryExpressions];
|
||||||
if (!hasQuery(queries)) {
|
if (!hasQuery(queries)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -403,7 +422,8 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async runTableQuery() {
|
async runTableQuery() {
|
||||||
const { datasource, queries } = this.state;
|
const queries = [...this.queryExpressions];
|
||||||
|
const { datasource } = this.state;
|
||||||
if (!hasQuery(queries)) {
|
if (!hasQuery(queries)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -427,7 +447,8 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async runLogsQuery() {
|
async runLogsQuery() {
|
||||||
const { datasource, queries } = this.state;
|
const queries = [...this.queryExpressions];
|
||||||
|
const { datasource } = this.state;
|
||||||
if (!hasQuery(queries)) {
|
if (!hasQuery(queries)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -455,18 +476,27 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
return datasource.metadataRequest(url);
|
return datasource.metadataRequest(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
cloneState(): ExploreState {
|
||||||
|
// Copy state, but copy queries including modifications
|
||||||
|
return {
|
||||||
|
...this.state,
|
||||||
|
queries: ensureQueries(this.queryExpressions.map(query => ({ query }))),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
saveState = () => {
|
saveState = () => {
|
||||||
const { stateKey, onSaveState } = this.props;
|
const { stateKey, onSaveState } = this.props;
|
||||||
onSaveState(stateKey, this.state);
|
onSaveState(stateKey, this.cloneState());
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { datasourceSrv, position, split } = this.props;
|
const { position, split } = this.props;
|
||||||
const {
|
const {
|
||||||
datasource,
|
datasource,
|
||||||
datasourceError,
|
datasourceError,
|
||||||
datasourceLoading,
|
datasourceLoading,
|
||||||
datasourceMissing,
|
datasourceMissing,
|
||||||
|
exploreDatasources,
|
||||||
graphResult,
|
graphResult,
|
||||||
history,
|
history,
|
||||||
latency,
|
latency,
|
||||||
@ -491,10 +521,6 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
const logsButtonActive = showingLogs ? 'active' : '';
|
const logsButtonActive = showingLogs ? 'active' : '';
|
||||||
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 datasources = datasourceSrv.getExploreSources().map(ds => ({
|
|
||||||
value: ds.name,
|
|
||||||
label: ds.name,
|
|
||||||
}));
|
|
||||||
const selectedDatasource = datasource ? datasource.name : undefined;
|
const selectedDatasource = datasource ? datasource.name : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -508,19 +534,19 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="navbar-buttons explore-first-button">
|
<div className="navbar-buttons explore-first-button">
|
||||||
<button className="btn navbar-button" onClick={this.onClickCloseSplit}>
|
<button className="btn navbar-button" onClick={this.onClickCloseSplit}>
|
||||||
Close Split
|
Close Split
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!datasourceMissing ? (
|
{!datasourceMissing ? (
|
||||||
<div className="navbar-buttons">
|
<div className="navbar-buttons">
|
||||||
<Select
|
<Select
|
||||||
clearable={false}
|
clearable={false}
|
||||||
className="gf-form-input gf-form-input--form-dropdown datasource-picker"
|
className="gf-form-input gf-form-input--form-dropdown datasource-picker"
|
||||||
onChange={this.onChangeDatasource}
|
onChange={this.onChangeDatasource}
|
||||||
options={datasources}
|
options={exploreDatasources}
|
||||||
isOpen={true}
|
isOpen={true}
|
||||||
placeholder="Loading datasources..."
|
placeholder="Loading datasources..."
|
||||||
value={selectedDatasource}
|
value={selectedDatasource}
|
||||||
|
@ -156,6 +156,7 @@ interface PromQueryFieldState {
|
|||||||
labelValues: { [index: string]: { [index: string]: string[] } }; // metric -> labelKey -> [labelValue,...]
|
labelValues: { [index: string]: { [index: string]: string[] } }; // metric -> labelKey -> [labelValue,...]
|
||||||
logLabelOptions: any[];
|
logLabelOptions: any[];
|
||||||
metrics: string[];
|
metrics: string[];
|
||||||
|
metricsOptions: any[];
|
||||||
metricsByPrefix: CascaderOption[];
|
metricsByPrefix: CascaderOption[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -167,7 +168,7 @@ interface PromTypeaheadInput {
|
|||||||
value?: Value;
|
value?: Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryFieldState> {
|
class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryFieldState> {
|
||||||
plugins: any[];
|
plugins: any[];
|
||||||
|
|
||||||
constructor(props: PromQueryFieldProps, context) {
|
constructor(props: PromQueryFieldProps, context) {
|
||||||
@ -189,6 +190,7 @@ class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryField
|
|||||||
logLabelOptions: [],
|
logLabelOptions: [],
|
||||||
metrics: props.metrics || [],
|
metrics: props.metrics || [],
|
||||||
metricsByPrefix: props.metricsByPrefix || [],
|
metricsByPrefix: props.metricsByPrefix || [],
|
||||||
|
metricsOptions: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -258,10 +260,22 @@ class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryField
|
|||||||
};
|
};
|
||||||
|
|
||||||
onReceiveMetrics = () => {
|
onReceiveMetrics = () => {
|
||||||
if (!this.state.metrics) {
|
const { histogramMetrics, metrics, metricsByPrefix } = this.state;
|
||||||
|
if (!metrics) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update global prism config
|
||||||
setPrismTokens(PRISM_SYNTAX, METRIC_MARK, this.state.metrics);
|
setPrismTokens(PRISM_SYNTAX, METRIC_MARK, this.state.metrics);
|
||||||
|
|
||||||
|
// Build metrics tree
|
||||||
|
const histogramOptions = histogramMetrics.map(hm => ({ label: hm, value: hm }));
|
||||||
|
const metricsOptions = [
|
||||||
|
{ label: 'Histograms', value: HISTOGRAM_GROUP, children: histogramOptions },
|
||||||
|
...metricsByPrefix,
|
||||||
|
];
|
||||||
|
|
||||||
|
this.setState({ metricsOptions });
|
||||||
};
|
};
|
||||||
|
|
||||||
onTypeahead = (typeahead: TypeaheadInput): TypeaheadOutput => {
|
onTypeahead = (typeahead: TypeaheadInput): TypeaheadOutput => {
|
||||||
@ -453,7 +467,7 @@ class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryField
|
|||||||
const histogramSeries = this.state.labelValues[HISTOGRAM_SELECTOR];
|
const histogramSeries = this.state.labelValues[HISTOGRAM_SELECTOR];
|
||||||
if (histogramSeries && histogramSeries['__name__']) {
|
if (histogramSeries && histogramSeries['__name__']) {
|
||||||
const histogramMetrics = histogramSeries['__name__'].slice().sort();
|
const histogramMetrics = histogramSeries['__name__'].slice().sort();
|
||||||
this.setState({ histogramMetrics });
|
this.setState({ histogramMetrics }, this.onReceiveMetrics);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -545,12 +559,7 @@ class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryField
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { error, hint, supportsLogs } = this.props;
|
const { error, hint, supportsLogs } = this.props;
|
||||||
const { histogramMetrics, logLabelOptions, metricsByPrefix } = this.state;
|
const { logLabelOptions, metricsOptions } = this.state;
|
||||||
const histogramOptions = histogramMetrics.map(hm => ({ label: hm, value: hm }));
|
|
||||||
const metricsOptions = [
|
|
||||||
{ label: 'Histograms', value: HISTOGRAM_GROUP, children: histogramOptions },
|
|
||||||
...metricsByPrefix,
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="prom-query-field">
|
<div className="prom-query-field">
|
||||||
@ -575,6 +584,7 @@ class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryField
|
|||||||
onWillApplySuggestion={willApplySuggestion}
|
onWillApplySuggestion={willApplySuggestion}
|
||||||
onValueChanged={this.onChangeQuery}
|
onValueChanged={this.onChangeQuery}
|
||||||
placeholder="Enter a PromQL query"
|
placeholder="Enter a PromQL query"
|
||||||
|
portalPrefix="prometheus"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{error ? <div className="prom-query-field-info text-error">{error}</div> : null}
|
{error ? <div className="prom-query-field-info text-error">{error}</div> : null}
|
||||||
|
@ -11,10 +11,17 @@ import NewlinePlugin from './slate-plugins/newline';
|
|||||||
import Typeahead from './Typeahead';
|
import Typeahead from './Typeahead';
|
||||||
import { makeFragment, makeValue } from './Value';
|
import { makeFragment, makeValue } from './Value';
|
||||||
|
|
||||||
export const TYPEAHEAD_DEBOUNCE = 300;
|
export const TYPEAHEAD_DEBOUNCE = 100;
|
||||||
|
|
||||||
function flattenSuggestions(s: any[]): any[] {
|
function getSuggestionByIndex(suggestions: SuggestionGroup[], index: number): Suggestion {
|
||||||
return s ? s.reduce((acc, g) => acc.concat(g.items), []) : [];
|
// Flatten suggestion groups
|
||||||
|
const flattenedSuggestions = suggestions.reduce((acc, g) => acc.concat(g.items), []);
|
||||||
|
const correctedIndex = Math.max(index, 0) % flattenedSuggestions.length;
|
||||||
|
return flattenedSuggestions[correctedIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasSuggestions(suggestions: SuggestionGroup[]): boolean {
|
||||||
|
return suggestions && suggestions.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Suggestion {
|
export interface Suggestion {
|
||||||
@ -125,7 +132,7 @@ export interface TypeaheadOutput {
|
|||||||
suggestions: SuggestionGroup[];
|
suggestions: SuggestionGroup[];
|
||||||
}
|
}
|
||||||
|
|
||||||
class QueryField extends React.Component<TypeaheadFieldProps, TypeaheadFieldState> {
|
class QueryField extends React.PureComponent<TypeaheadFieldProps, TypeaheadFieldState> {
|
||||||
menuEl: HTMLElement | null;
|
menuEl: HTMLElement | null;
|
||||||
plugins: any[];
|
plugins: any[];
|
||||||
resetTimer: any;
|
resetTimer: any;
|
||||||
@ -154,8 +161,14 @@ class QueryField extends React.Component<TypeaheadFieldProps, TypeaheadFieldStat
|
|||||||
clearTimeout(this.resetTimer);
|
clearTimeout(this.resetTimer);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentDidUpdate(prevProps, prevState) {
|
||||||
this.updateMenu();
|
// Only update menu location when suggestion existence or text/selection changed
|
||||||
|
if (
|
||||||
|
this.state.value !== prevState.value ||
|
||||||
|
hasSuggestions(this.state.suggestions) !== hasSuggestions(prevState.suggestions)
|
||||||
|
) {
|
||||||
|
this.updateMenu();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
@ -216,7 +229,7 @@ class QueryField extends React.Component<TypeaheadFieldProps, TypeaheadFieldStat
|
|||||||
wrapperNode,
|
wrapperNode,
|
||||||
});
|
});
|
||||||
|
|
||||||
const filteredSuggestions = suggestions
|
let filteredSuggestions = suggestions
|
||||||
.map(group => {
|
.map(group => {
|
||||||
if (group.items) {
|
if (group.items) {
|
||||||
if (prefix) {
|
if (prefix) {
|
||||||
@ -241,6 +254,11 @@ class QueryField extends React.Component<TypeaheadFieldProps, TypeaheadFieldStat
|
|||||||
})
|
})
|
||||||
.filter(group => group.items && group.items.length > 0); // Filter out empty groups
|
.filter(group => group.items && group.items.length > 0); // Filter out empty groups
|
||||||
|
|
||||||
|
// Keep same object for equality checking later
|
||||||
|
if (_.isEqual(filteredSuggestions, this.state.suggestions)) {
|
||||||
|
filteredSuggestions = this.state.suggestions;
|
||||||
|
}
|
||||||
|
|
||||||
this.setState(
|
this.setState(
|
||||||
{
|
{
|
||||||
suggestions: filteredSuggestions,
|
suggestions: filteredSuggestions,
|
||||||
@ -326,12 +344,7 @@ class QueryField extends React.Component<TypeaheadFieldProps, TypeaheadFieldStat
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the currently selected suggestion
|
const suggestion = getSuggestionByIndex(suggestions, typeaheadIndex);
|
||||||
const flattenedSuggestions = flattenSuggestions(suggestions);
|
|
||||||
const selected = Math.abs(typeaheadIndex);
|
|
||||||
const selectedIndex = selected % flattenedSuggestions.length || 0;
|
|
||||||
const suggestion = flattenedSuggestions[selectedIndex];
|
|
||||||
|
|
||||||
this.applyTypeahead(change, suggestion);
|
this.applyTypeahead(change, suggestion);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -408,8 +421,7 @@ class QueryField extends React.Component<TypeaheadFieldProps, TypeaheadFieldStat
|
|||||||
}
|
}
|
||||||
|
|
||||||
// No suggestions or blur, remove menu
|
// No suggestions or blur, remove menu
|
||||||
const hasSuggesstions = suggestions && suggestions.length > 0;
|
if (!hasSuggestions(suggestions)) {
|
||||||
if (!hasSuggesstions) {
|
|
||||||
menu.removeAttribute('style');
|
menu.removeAttribute('style');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -436,18 +448,12 @@ class QueryField extends React.Component<TypeaheadFieldProps, TypeaheadFieldStat
|
|||||||
|
|
||||||
renderMenu = () => {
|
renderMenu = () => {
|
||||||
const { portalPrefix } = this.props;
|
const { portalPrefix } = this.props;
|
||||||
const { suggestions } = this.state;
|
const { suggestions, typeaheadIndex } = this.state;
|
||||||
const hasSuggesstions = suggestions && suggestions.length > 0;
|
if (!hasSuggestions(suggestions)) {
|
||||||
if (!hasSuggesstions) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Guard selectedIndex to be within the length of the suggestions
|
const selectedItem = getSuggestionByIndex(suggestions, typeaheadIndex);
|
||||||
let selectedIndex = Math.max(this.state.typeaheadIndex, 0);
|
|
||||||
const flattenedSuggestions = flattenSuggestions(suggestions);
|
|
||||||
selectedIndex = selectedIndex % flattenedSuggestions.length || 0;
|
|
||||||
const selectedItem: Suggestion | null =
|
|
||||||
flattenedSuggestions.length > 0 ? flattenedSuggestions[selectedIndex] : null;
|
|
||||||
|
|
||||||
// Create typeahead in DOM root so we can later position it absolutely
|
// Create typeahead in DOM root so we can later position it absolutely
|
||||||
return (
|
return (
|
||||||
@ -482,7 +488,7 @@ class QueryField extends React.Component<TypeaheadFieldProps, TypeaheadFieldStat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Portal extends React.Component<{ index?: number; prefix: string }, {}> {
|
class Portal extends React.PureComponent<{ index?: number; prefix: string }, {}> {
|
||||||
node: HTMLElement;
|
node: HTMLElement;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -44,14 +44,14 @@ class QueryRow extends PureComponent<any, {}> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { edited, history, query, queryError, queryHint, request, supportsLogs } = this.props;
|
const { history, query, queryError, queryHint, request, supportsLogs } = this.props;
|
||||||
return (
|
return (
|
||||||
<div className="query-row">
|
<div className="query-row">
|
||||||
<div className="query-row-field">
|
<div className="query-row-field">
|
||||||
<QueryField
|
<QueryField
|
||||||
error={queryError}
|
error={queryError}
|
||||||
hint={queryHint}
|
hint={queryHint}
|
||||||
initialQuery={edited ? null : query}
|
initialQuery={query}
|
||||||
history={history}
|
history={history}
|
||||||
portalPrefix="explore"
|
portalPrefix="explore"
|
||||||
onClickHintFix={this.onClickHintFix}
|
onClickHintFix={this.onClickHintFix}
|
||||||
@ -79,7 +79,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, queryErrors, queryHints, ...handlers } = this.props;
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
{queries.map((q, index) => (
|
{queries.map((q, index) => (
|
||||||
@ -89,7 +89,6 @@ export default class QueryRows extends PureComponent<any, {}> {
|
|||||||
query={q.query}
|
query={q.query}
|
||||||
queryError={queryErrors[index]}
|
queryError={queryErrors[index]}
|
||||||
queryHint={queryHints[index]}
|
queryHint={queryHints[index]}
|
||||||
edited={q.edited}
|
|
||||||
{...handlers}
|
{...handlers}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
@ -23,7 +23,9 @@ class TypeaheadItem extends React.PureComponent<TypeaheadItemProps, {}> {
|
|||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (this.props.isSelected && !prevProps.isSelected) {
|
if (this.props.isSelected && !prevProps.isSelected) {
|
||||||
scrollIntoView(this.el);
|
requestAnimationFrame(() => {
|
||||||
|
scrollIntoView(this.el);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
export function generateQueryKey(index = 0) {
|
import { Query } from 'app/types/explore';
|
||||||
|
|
||||||
|
export function generateQueryKey(index = 0): string {
|
||||||
return `Q-${Date.now()}-${Math.random()}-${index}`;
|
return `Q-${Date.now()}-${Math.random()}-${index}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ensureQueries(queries?) {
|
export function ensureQueries(queries?: Query[]): Query[] {
|
||||||
if (queries && typeof queries === 'object' && queries.length > 0 && typeof queries[0].query === 'string') {
|
if (queries && typeof queries === 'object' && queries.length > 0 && typeof queries[0].query === 'string') {
|
||||||
return queries.map(({ query }, i) => ({ key: generateQueryKey(i), query }));
|
return queries.map(({ query }, i) => ({ key: generateQueryKey(i), query }));
|
||||||
}
|
}
|
||||||
return [{ key: generateQueryKey(), query: '' }];
|
return [{ key: generateQueryKey(), query: '' }];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasQuery(queries) {
|
export function hasQuery(queries: string[]): boolean {
|
||||||
return queries.some(q => q.query);
|
return queries.some(q => Boolean(q));
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,8 @@
|
|||||||
|
interface ExploreDatasource {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Range {
|
export interface Range {
|
||||||
from: string;
|
from: string;
|
||||||
to: string;
|
to: string;
|
||||||
@ -5,7 +10,6 @@ export interface Range {
|
|||||||
|
|
||||||
export interface Query {
|
export interface Query {
|
||||||
query: string;
|
query: string;
|
||||||
edited?: boolean;
|
|
||||||
key?: string;
|
key?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -15,13 +19,25 @@ export interface ExploreState {
|
|||||||
datasourceLoading: boolean | null;
|
datasourceLoading: boolean | null;
|
||||||
datasourceMissing: boolean;
|
datasourceMissing: boolean;
|
||||||
datasourceName?: string;
|
datasourceName?: string;
|
||||||
|
exploreDatasources: ExploreDatasource[];
|
||||||
graphResult: any;
|
graphResult: any;
|
||||||
history: any[];
|
history: any[];
|
||||||
latency: number;
|
latency: number;
|
||||||
loading: any;
|
loading: any;
|
||||||
logsResult: any;
|
logsResult: any;
|
||||||
|
/**
|
||||||
|
* 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[];
|
queries: Query[];
|
||||||
|
/**
|
||||||
|
* Errors caused by the running the query row.
|
||||||
|
*/
|
||||||
queryErrors: any[];
|
queryErrors: any[];
|
||||||
|
/**
|
||||||
|
* Hints gathered for the query row.
|
||||||
|
*/
|
||||||
queryHints: any[];
|
queryHints: any[];
|
||||||
range: Range;
|
range: Range;
|
||||||
requestOptions: any;
|
requestOptions: any;
|
||||||
|
Loading…
Reference in New Issue
Block a user