Commit 406b6144 by David Committed by GitHub

Merge pull request #13491 from grafana/davkal/explore-perf

Explore: typeahead and render performance improvements
parents 9ae6f685 bdae3993
...@@ -7,6 +7,7 @@ const DEFAULT_EXPLORE_STATE: ExploreState = { ...@@ -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'; ...@@ -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 { ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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 { ...@@ -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 { ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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'; ...@@ -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 { ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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, {}> { ...@@ -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, {}> { ...@@ -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, {}> { ...@@ -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, {}> { ...@@ -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);
});
} }
} }
......
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));
} }
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 { ...@@ -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 { ...@@ -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;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment