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 = {
datasourceLoading: null,
datasourceMissing: false,
datasourceName: '',
exploreDatasources: [],
graphResult: null,
history: [],
latency: 0,
......
......@@ -156,6 +156,7 @@ interface PromQueryFieldState {
labelValues: { [index: string]: { [index: string]: string[] } }; // metric -> labelKey -> [labelValue,...]
logLabelOptions: any[];
metrics: string[];
metricsOptions: any[];
metricsByPrefix: CascaderOption[];
}
......@@ -167,7 +168,7 @@ interface PromTypeaheadInput {
value?: Value;
}
class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryFieldState> {
class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryFieldState> {
plugins: any[];
constructor(props: PromQueryFieldProps, context) {
......@@ -189,6 +190,7 @@ class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryField
logLabelOptions: [],
metrics: props.metrics || [],
metricsByPrefix: props.metricsByPrefix || [],
metricsOptions: [],
};
}
......@@ -258,10 +260,22 @@ class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryField
};
onReceiveMetrics = () => {
if (!this.state.metrics) {
const { histogramMetrics, metrics, metricsByPrefix } = this.state;
if (!metrics) {
return;
}
// Update global prism config
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 => {
......@@ -453,7 +467,7 @@ class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryField
const histogramSeries = this.state.labelValues[HISTOGRAM_SELECTOR];
if (histogramSeries && histogramSeries['__name__']) {
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() {
const { error, hint, supportsLogs } = this.props;
const { histogramMetrics, logLabelOptions, metricsByPrefix } = this.state;
const histogramOptions = histogramMetrics.map(hm => ({ label: hm, value: hm }));
const metricsOptions = [
{ label: 'Histograms', value: HISTOGRAM_GROUP, children: histogramOptions },
...metricsByPrefix,
];
const { logLabelOptions, metricsOptions } = this.state;
return (
<div className="prom-query-field">
......@@ -575,6 +584,7 @@ class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryField
onWillApplySuggestion={willApplySuggestion}
onValueChanged={this.onChangeQuery}
placeholder="Enter a PromQL query"
portalPrefix="prometheus"
/>
</div>
{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 { makeFragment, makeValue } from './Value';
export const TYPEAHEAD_DEBOUNCE = 300;
export const TYPEAHEAD_DEBOUNCE = 100;
function flattenSuggestions(s: any[]): any[] {
return s ? s.reduce((acc, g) => acc.concat(g.items), []) : [];
function getSuggestionByIndex(suggestions: SuggestionGroup[], index: number): Suggestion {
// 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 {
......@@ -125,7 +132,7 @@ export interface TypeaheadOutput {
suggestions: SuggestionGroup[];
}
class QueryField extends React.Component<TypeaheadFieldProps, TypeaheadFieldState> {
class QueryField extends React.PureComponent<TypeaheadFieldProps, TypeaheadFieldState> {
menuEl: HTMLElement | null;
plugins: any[];
resetTimer: any;
......@@ -154,8 +161,14 @@ class QueryField extends React.Component<TypeaheadFieldProps, TypeaheadFieldStat
clearTimeout(this.resetTimer);
}
componentDidUpdate() {
this.updateMenu();
componentDidUpdate(prevProps, prevState) {
// 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) {
......@@ -216,7 +229,7 @@ class QueryField extends React.Component<TypeaheadFieldProps, TypeaheadFieldStat
wrapperNode,
});
const filteredSuggestions = suggestions
let filteredSuggestions = suggestions
.map(group => {
if (group.items) {
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
// Keep same object for equality checking later
if (_.isEqual(filteredSuggestions, this.state.suggestions)) {
filteredSuggestions = this.state.suggestions;
}
this.setState(
{
suggestions: filteredSuggestions,
......@@ -326,12 +344,7 @@ class QueryField extends React.Component<TypeaheadFieldProps, TypeaheadFieldStat
return undefined;
}
// Get the currently selected suggestion
const flattenedSuggestions = flattenSuggestions(suggestions);
const selected = Math.abs(typeaheadIndex);
const selectedIndex = selected % flattenedSuggestions.length || 0;
const suggestion = flattenedSuggestions[selectedIndex];
const suggestion = getSuggestionByIndex(suggestions, typeaheadIndex);
this.applyTypeahead(change, suggestion);
return true;
}
......@@ -408,8 +421,7 @@ class QueryField extends React.Component<TypeaheadFieldProps, TypeaheadFieldStat
}
// No suggestions or blur, remove menu
const hasSuggesstions = suggestions && suggestions.length > 0;
if (!hasSuggesstions) {
if (!hasSuggestions(suggestions)) {
menu.removeAttribute('style');
return;
}
......@@ -436,18 +448,12 @@ class QueryField extends React.Component<TypeaheadFieldProps, TypeaheadFieldStat
renderMenu = () => {
const { portalPrefix } = this.props;
const { suggestions } = this.state;
const hasSuggesstions = suggestions && suggestions.length > 0;
if (!hasSuggesstions) {
const { suggestions, typeaheadIndex } = this.state;
if (!hasSuggestions(suggestions)) {
return null;
}
// Guard selectedIndex to be within the length of the suggestions
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;
const selectedItem = getSuggestionByIndex(suggestions, typeaheadIndex);
// Create typeahead in DOM root so we can later position it absolutely
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;
constructor(props) {
......
......@@ -44,14 +44,14 @@ class QueryRow extends PureComponent<any, {}> {
};
render() {
const { edited, history, query, queryError, queryHint, request, supportsLogs } = this.props;
const { history, query, queryError, queryHint, request, supportsLogs } = this.props;
return (
<div className="query-row">
<div className="query-row-field">
<QueryField
error={queryError}
hint={queryHint}
initialQuery={edited ? null : query}
initialQuery={query}
history={history}
portalPrefix="explore"
onClickHintFix={this.onClickHintFix}
......@@ -79,7 +79,7 @@ class QueryRow extends PureComponent<any, {}> {
export default class QueryRows extends PureComponent<any, {}> {
render() {
const { className = '', queries, queryErrors = [], queryHints = [], ...handlers } = this.props;
const { className = '', queries, queryErrors, queryHints, ...handlers } = this.props;
return (
<div className={className}>
{queries.map((q, index) => (
......@@ -89,7 +89,6 @@ export default class QueryRows extends PureComponent<any, {}> {
query={q.query}
queryError={queryErrors[index]}
queryHint={queryHints[index]}
edited={q.edited}
{...handlers}
/>
))}
......
......@@ -23,7 +23,9 @@ class TypeaheadItem extends React.PureComponent<TypeaheadItemProps, {}> {
componentDidUpdate(prevProps) {
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}`;
}
export function ensureQueries(queries?) {
export function ensureQueries(queries?: Query[]): Query[] {
if (queries && typeof queries === 'object' && queries.length > 0 && typeof queries[0].query === 'string') {
return queries.map(({ query }, i) => ({ key: generateQueryKey(i), query }));
}
return [{ key: generateQueryKey(), query: '' }];
}
export function hasQuery(queries) {
return queries.some(q => q.query);
export function hasQuery(queries: string[]): boolean {
return queries.some(q => Boolean(q));
}
interface ExploreDatasource {
value: string;
label: string;
}
export interface Range {
from: string;
to: string;
......@@ -5,7 +10,6 @@ export interface Range {
export interface Query {
query: string;
edited?: boolean;
key?: string;
}
......@@ -15,13 +19,25 @@ export interface ExploreState {
datasourceLoading: boolean | null;
datasourceMissing: boolean;
datasourceName?: string;
exploreDatasources: ExploreDatasource[];
graphResult: any;
history: any[];
latency: number;
loading: 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[];
/**
* Errors caused by the running the query row.
*/
queryErrors: any[];
/**
* Hints gathered for the query row.
*/
queryHints: any[];
range: Range;
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