Explore: async starts of language provider

- changed `start()` to return promise on main language feature task
- promise resolves to list of secondary tasks
- speeds up time to interaction of metric selector
- lazy loading of certain metric selector and log label selector items
- loading indication of metric and log label selectors
This commit is contained in:
David Kaltschmidt 2018-10-31 17:48:36 +01:00
parent 749d7a2f0c
commit 4f959648a7
6 changed files with 111 additions and 60 deletions

View File

@ -92,25 +92,44 @@ class LoggingQueryField extends React.PureComponent<LoggingQueryFieldProps, Logg
componentDidMount() { componentDidMount() {
if (this.languageProvider) { if (this.languageProvider) {
this.languageProvider.start().then(() => this.onReceiveMetrics()); this.languageProvider
.start()
.then(remaining => {
remaining.map(task => task.then(this.onReceiveMetrics).catch(() => {}));
})
.then(() => this.onReceiveMetrics());
} }
} }
loadOptions = (selectedOptions: CascaderOption[]) => {
const targetOption = selectedOptions[selectedOptions.length - 1];
this.setState(state => {
const nextOptions = state.logLabelOptions.map(option => {
if (option.value === targetOption.value) {
return {
...option,
loading: true,
};
}
return option;
});
return { logLabelOptions: nextOptions };
});
this.languageProvider
.fetchLabelValues(targetOption.value)
.then(this.onReceiveMetrics)
.catch(() => {});
};
onChangeLogLabels = (values: string[], selectedOptions: CascaderOption[]) => { onChangeLogLabels = (values: string[], selectedOptions: CascaderOption[]) => {
let query; if (selectedOptions.length === 2) {
if (selectedOptions.length === 1) {
if (selectedOptions[0].children.length === 0) {
query = selectedOptions[0].value;
} else {
// Ignore click on group
return;
}
} else {
const key = selectedOptions[0].value; const key = selectedOptions[0].value;
const value = selectedOptions[1].value; const value = selectedOptions[1].value;
query = `{${key}="${value}"}`; const query = `{${key}="${value}"}`;
this.onChangeQuery(query, true);
} }
this.onChangeQuery(query, true);
}; };
onChangeQuery = (value: string, override?: boolean) => { onChangeQuery = (value: string, override?: boolean) => {
@ -165,12 +184,15 @@ class LoggingQueryField extends React.PureComponent<LoggingQueryFieldProps, Logg
const { error, hint, initialQuery } = this.props; const { error, hint, initialQuery } = this.props;
const { logLabelOptions, syntaxLoaded } = this.state; const { logLabelOptions, syntaxLoaded } = this.state;
const cleanText = this.languageProvider ? this.languageProvider.cleanText : undefined; const cleanText = this.languageProvider ? this.languageProvider.cleanText : undefined;
const chooserText = syntaxLoaded ? 'Log labels' : 'Loading labels...';
return ( return (
<div className="prom-query-field"> <div className="prom-query-field">
<div className="prom-query-field-tools"> <div className="prom-query-field-tools">
<Cascader options={logLabelOptions} onChange={this.onChangeLogLabels}> <Cascader options={logLabelOptions} onChange={this.onChangeLogLabels} loadData={this.loadOptions}>
<button className="btn navbar-button navbar-button--tight">Log labels</button> <button className="btn navbar-button navbar-button--tight" disabled={!syntaxLoaded}>
{chooserText}
</button>
</Cascader> </Cascader>
</div> </div>
<div className="prom-query-field-wrapper"> <div className="prom-query-field-wrapper">

View File

@ -12,9 +12,9 @@ import {
import { parseSelector } from 'app/plugins/datasource/prometheus/language_utils'; import { parseSelector } from 'app/plugins/datasource/prometheus/language_utils';
import PromqlSyntax from 'app/plugins/datasource/prometheus/promql'; import PromqlSyntax from 'app/plugins/datasource/prometheus/promql';
const DEFAULT_KEYS = ['job', 'instance']; const DEFAULT_KEYS = ['job', 'namespace'];
const EMPTY_SELECTOR = '{}'; const EMPTY_SELECTOR = '{}';
const HISTORY_ITEM_COUNT = 5; const HISTORY_ITEM_COUNT = 10;
const HISTORY_COUNT_CUTOFF = 1000 * 60 * 60 * 24; // 24h const HISTORY_COUNT_CUTOFF = 1000 * 60 * 60 * 24; // 24h
const wrapLabel = (label: string) => ({ label }); const wrapLabel = (label: string) => ({ label });
@ -65,7 +65,7 @@ export default class LoggingLanguageProvider extends LanguageProvider {
start = () => { start = () => {
if (!this.started) { if (!this.started) {
this.started = true; this.started = true;
return Promise.all([this.fetchLogLabels()]); return this.fetchLogLabels();
} }
return Promise.resolve([]); return Promise.resolve([]);
}; };
@ -118,35 +118,36 @@ export default class LoggingLanguageProvider extends LanguageProvider {
getLabelCompletionItems({ text, wrapperClasses, labelKey, value }: TypeaheadInput): TypeaheadOutput { getLabelCompletionItems({ text, wrapperClasses, labelKey, value }: TypeaheadInput): TypeaheadOutput {
let context: string; let context: string;
let refresher: Promise<any> = null;
const suggestions: CompletionItemGroup[] = []; const suggestions: CompletionItemGroup[] = [];
const line = value.anchorBlock.getText(); const line = value.anchorBlock.getText();
const cursorOffset: number = value.anchorOffset; const cursorOffset: number = value.anchorOffset;
// Get normalized selector // Use EMPTY_SELECTOR until series API is implemented for facetting
let selector; const selector = EMPTY_SELECTOR;
let parsedSelector; let parsedSelector;
try { try {
parsedSelector = parseSelector(line, cursorOffset); parsedSelector = parseSelector(line, cursorOffset);
selector = parsedSelector.selector; } catch {}
} catch {
selector = EMPTY_SELECTOR;
}
const containsMetric = selector.indexOf('__name__=') > -1;
const existingKeys = parsedSelector ? parsedSelector.labelKeys : []; const existingKeys = parsedSelector ? parsedSelector.labelKeys : [];
if ((text && text.match(/^!?=~?/)) || _.includes(wrapperClasses, 'attr-value')) { if ((text && text.match(/^!?=~?/)) || _.includes(wrapperClasses, 'attr-value')) {
// Label values // Label values
if (labelKey && this.labelValues[selector] && this.labelValues[selector][labelKey]) { if (labelKey && this.labelValues[selector]) {
const labelValues = this.labelValues[selector][labelKey]; const labelValues = this.labelValues[selector][labelKey];
context = 'context-label-values'; if (labelValues) {
suggestions.push({ context = 'context-label-values';
label: `Label values for "${labelKey}"`, suggestions.push({
items: labelValues.map(wrapLabel), label: `Label values for "${labelKey}"`,
}); items: labelValues.map(wrapLabel),
});
} else {
refresher = this.fetchLabelValues(labelKey);
}
} }
} else { } else {
// Label keys // Label keys
const labelKeys = this.labelKeys[selector] || (containsMetric ? null : DEFAULT_KEYS); const labelKeys = this.labelKeys[selector] || DEFAULT_KEYS;
if (labelKeys) { if (labelKeys) {
const possibleKeys = _.difference(labelKeys, existingKeys); const possibleKeys = _.difference(labelKeys, existingKeys);
if (possibleKeys.length > 0) { if (possibleKeys.length > 0) {
@ -156,7 +157,7 @@ export default class LoggingLanguageProvider extends LanguageProvider {
} }
} }
return { context, suggestions }; return { context, refresher, suggestions };
} }
async fetchLogLabels() { async fetchLogLabels() {
@ -165,29 +166,18 @@ export default class LoggingLanguageProvider extends LanguageProvider {
const res = await this.request(url); const res = await this.request(url);
const body = await (res.data || res.json()); const body = await (res.data || res.json());
const labelKeys = body.data.slice().sort(); const labelKeys = body.data.slice().sort();
const labelKeysBySelector = { this.labelKeys = {
...this.labelKeys, ...this.labelKeys,
[EMPTY_SELECTOR]: labelKeys, [EMPTY_SELECTOR]: labelKeys,
}; };
const labelValuesByKey = {}; this.logLabelOptions = labelKeys.map(key => ({ label: key, value: key, isLeaf: false }));
this.logLabelOptions = [];
for (const key of labelKeys) { // Pre-load values for default labels
const valuesUrl = `/api/prom/label/${key}/values`; return labelKeys.filter(key => DEFAULT_KEYS.indexOf(key) > -1).map(key => this.fetchLabelValues(key));
const res = await this.request(valuesUrl);
const body = await (res.data || res.json());
const values = body.data.slice().sort();
labelValuesByKey[key] = values;
this.logLabelOptions.push({
label: key,
value: key,
children: values.map(value => ({ label: value, value })),
});
}
this.labelValues = { [EMPTY_SELECTOR]: labelValuesByKey };
this.labelKeys = labelKeysBySelector;
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
return [];
} }
async fetchLabelValues(key: string) { async fetchLabelValues(key: string) {
@ -195,14 +185,28 @@ export default class LoggingLanguageProvider extends LanguageProvider {
try { try {
const res = await this.request(url); const res = await this.request(url);
const body = await (res.data || res.json()); const body = await (res.data || res.json());
const values = body.data.slice().sort();
// Add to label options
this.logLabelOptions = this.logLabelOptions.map(keyOption => {
if (keyOption.value === key) {
return {
...keyOption,
children: values.map(value => ({ label: value, value })),
};
}
return keyOption;
});
// Add to key map
const exisingValues = this.labelValues[EMPTY_SELECTOR]; const exisingValues = this.labelValues[EMPTY_SELECTOR];
const values = { const nextValues = {
...exisingValues, ...exisingValues,
[key]: body.data, [key]: values,
}; };
this.labelValues = { this.labelValues = {
...this.labelValues, ...this.labelValues,
[EMPTY_SELECTOR]: values, [EMPTY_SELECTOR]: nextValues,
}; };
} catch (e) { } catch (e) {
console.error(e); console.error(e);

View File

@ -131,7 +131,12 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
componentDidMount() { componentDidMount() {
if (this.languageProvider) { if (this.languageProvider) {
this.languageProvider.start().then(() => this.onReceiveMetrics()); this.languageProvider
.start()
.then(remaining => {
remaining.map(task => task.then(this.onReceiveMetrics).catch(() => {}));
})
.then(() => this.onReceiveMetrics());
} }
} }
@ -186,10 +191,13 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
// Build metrics tree // Build metrics tree
const metricsByPrefix = groupMetricsByPrefix(metrics); const metricsByPrefix = groupMetricsByPrefix(metrics);
const histogramOptions = histogramMetrics.map(hm => ({ label: hm, value: hm })); const histogramOptions = histogramMetrics.map(hm => ({ label: hm, value: hm }));
const metricsOptions = [ const metricsOptions =
{ label: 'Histograms', value: HISTOGRAM_GROUP, children: histogramOptions }, histogramMetrics.length > 0
...metricsByPrefix, ? [
]; { label: 'Histograms', value: HISTOGRAM_GROUP, children: histogramOptions, isLeaf: false },
...metricsByPrefix,
]
: metricsByPrefix;
this.setState({ metricsOptions, syntaxLoaded: true }); this.setState({ metricsOptions, syntaxLoaded: true });
}; };
@ -222,12 +230,15 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
const { error, hint, initialQuery } = this.props; const { error, hint, initialQuery } = this.props;
const { metricsOptions, syntaxLoaded } = this.state; const { metricsOptions, syntaxLoaded } = this.state;
const cleanText = this.languageProvider ? this.languageProvider.cleanText : undefined; const cleanText = this.languageProvider ? this.languageProvider.cleanText : undefined;
const chooserText = syntaxLoaded ? 'Metrics' : 'Loading matrics...';
return ( return (
<div className="prom-query-field"> <div className="prom-query-field">
<div className="prom-query-field-tools"> <div className="prom-query-field-tools">
<Cascader options={metricsOptions} onChange={this.onChangeMetrics}> <Cascader options={metricsOptions} onChange={this.onChangeMetrics}>
<button className="btn navbar-button navbar-button--tight">Metrics</button> <button className="btn navbar-button navbar-button--tight" disabled={!syntaxLoaded}>
{chooserText}
</button>
</Cascader> </Cascader>
</div> </div>
<div className="prom-query-field-wrapper"> <div className="prom-query-field-wrapper">

View File

@ -74,7 +74,7 @@ export default class PromQlLanguageProvider extends LanguageProvider {
start = () => { start = () => {
if (!this.started) { if (!this.started) {
this.started = true; this.started = true;
return Promise.all([this.fetchMetricNames(), this.fetchHistogramMetrics()]); return this.fetchMetricNames().then(() => [this.fetchHistogramMetrics()]);
} }
return Promise.resolve([]); return Promise.resolve([]);
}; };

View File

@ -85,7 +85,11 @@ export interface HistoryItem {
export abstract class LanguageProvider { export abstract class LanguageProvider {
datasource: any; datasource: any;
request: (url) => Promise<any>; request: (url) => Promise<any>;
start: () => Promise<any>; /**
* Returns a promise that resolves with a task list when main syntax is loaded.
* Task list consists of secondary promises that load more detailed language features.
*/
start: () => Promise<any[]>;
} }
export interface TypeaheadInput { export interface TypeaheadInput {

View File

@ -329,6 +329,16 @@
text-align: right; text-align: right;
} }
// React-component cascade fix: show "loading" even though item can expand
.rc-cascader-menu-item-loading:after {
position: absolute;
right: 12px;
content: 'loading';
color: #767980;
font-style: italic;
}
// TODO Experimental // TODO Experimental
.cheat-sheet-item { .cheat-sheet-item {