Commit 4f959648 by David Kaltschmidt

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
parent 749d7a2f
......@@ -92,25 +92,44 @@ class LoggingQueryField extends React.PureComponent<LoggingQueryFieldProps, Logg
componentDidMount() {
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[]) => {
let query;
if (selectedOptions.length === 1) {
if (selectedOptions[0].children.length === 0) {
query = selectedOptions[0].value;
} else {
// Ignore click on group
return;
}
} else {
if (selectedOptions.length === 2) {
const key = selectedOptions[0].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) => {
......@@ -165,12 +184,15 @@ class LoggingQueryField extends React.PureComponent<LoggingQueryFieldProps, Logg
const { error, hint, initialQuery } = this.props;
const { logLabelOptions, syntaxLoaded } = this.state;
const cleanText = this.languageProvider ? this.languageProvider.cleanText : undefined;
const chooserText = syntaxLoaded ? 'Log labels' : 'Loading labels...';
return (
<div className="prom-query-field">
<div className="prom-query-field-tools">
<Cascader options={logLabelOptions} onChange={this.onChangeLogLabels}>
<button className="btn navbar-button navbar-button--tight">Log labels</button>
<Cascader options={logLabelOptions} onChange={this.onChangeLogLabels} loadData={this.loadOptions}>
<button className="btn navbar-button navbar-button--tight" disabled={!syntaxLoaded}>
{chooserText}
</button>
</Cascader>
</div>
<div className="prom-query-field-wrapper">
......
......@@ -12,9 +12,9 @@ import {
import { parseSelector } from 'app/plugins/datasource/prometheus/language_utils';
import PromqlSyntax from 'app/plugins/datasource/prometheus/promql';
const DEFAULT_KEYS = ['job', 'instance'];
const DEFAULT_KEYS = ['job', 'namespace'];
const EMPTY_SELECTOR = '{}';
const HISTORY_ITEM_COUNT = 5;
const HISTORY_ITEM_COUNT = 10;
const HISTORY_COUNT_CUTOFF = 1000 * 60 * 60 * 24; // 24h
const wrapLabel = (label: string) => ({ label });
......@@ -65,7 +65,7 @@ export default class LoggingLanguageProvider extends LanguageProvider {
start = () => {
if (!this.started) {
this.started = true;
return Promise.all([this.fetchLogLabels()]);
return this.fetchLogLabels();
}
return Promise.resolve([]);
};
......@@ -118,35 +118,36 @@ export default class LoggingLanguageProvider extends LanguageProvider {
getLabelCompletionItems({ text, wrapperClasses, labelKey, value }: TypeaheadInput): TypeaheadOutput {
let context: string;
let refresher: Promise<any> = null;
const suggestions: CompletionItemGroup[] = [];
const line = value.anchorBlock.getText();
const cursorOffset: number = value.anchorOffset;
// Get normalized selector
let selector;
// Use EMPTY_SELECTOR until series API is implemented for facetting
const selector = EMPTY_SELECTOR;
let parsedSelector;
try {
parsedSelector = parseSelector(line, cursorOffset);
selector = parsedSelector.selector;
} catch {
selector = EMPTY_SELECTOR;
}
const containsMetric = selector.indexOf('__name__=') > -1;
} catch {}
const existingKeys = parsedSelector ? parsedSelector.labelKeys : [];
if ((text && text.match(/^!?=~?/)) || _.includes(wrapperClasses, 'attr-value')) {
// Label values
if (labelKey && this.labelValues[selector] && this.labelValues[selector][labelKey]) {
if (labelKey && this.labelValues[selector]) {
const labelValues = this.labelValues[selector][labelKey];
context = 'context-label-values';
suggestions.push({
label: `Label values for "${labelKey}"`,
items: labelValues.map(wrapLabel),
});
if (labelValues) {
context = 'context-label-values';
suggestions.push({
label: `Label values for "${labelKey}"`,
items: labelValues.map(wrapLabel),
});
} else {
refresher = this.fetchLabelValues(labelKey);
}
}
} else {
// Label keys
const labelKeys = this.labelKeys[selector] || (containsMetric ? null : DEFAULT_KEYS);
const labelKeys = this.labelKeys[selector] || DEFAULT_KEYS;
if (labelKeys) {
const possibleKeys = _.difference(labelKeys, existingKeys);
if (possibleKeys.length > 0) {
......@@ -156,7 +157,7 @@ export default class LoggingLanguageProvider extends LanguageProvider {
}
}
return { context, suggestions };
return { context, refresher, suggestions };
}
async fetchLogLabels() {
......@@ -165,29 +166,18 @@ export default class LoggingLanguageProvider extends LanguageProvider {
const res = await this.request(url);
const body = await (res.data || res.json());
const labelKeys = body.data.slice().sort();
const labelKeysBySelector = {
this.labelKeys = {
...this.labelKeys,
[EMPTY_SELECTOR]: labelKeys,
};
const labelValuesByKey = {};
this.logLabelOptions = [];
for (const key of labelKeys) {
const valuesUrl = `/api/prom/label/${key}/values`;
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;
this.logLabelOptions = labelKeys.map(key => ({ label: key, value: key, isLeaf: false }));
// Pre-load values for default labels
return labelKeys.filter(key => DEFAULT_KEYS.indexOf(key) > -1).map(key => this.fetchLabelValues(key));
} catch (e) {
console.error(e);
}
return [];
}
async fetchLabelValues(key: string) {
......@@ -195,14 +185,28 @@ export default class LoggingLanguageProvider extends LanguageProvider {
try {
const res = await this.request(url);
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 values = {
const nextValues = {
...exisingValues,
[key]: body.data,
[key]: values,
};
this.labelValues = {
...this.labelValues,
[EMPTY_SELECTOR]: values,
[EMPTY_SELECTOR]: nextValues,
};
} catch (e) {
console.error(e);
......
......@@ -131,7 +131,12 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
componentDidMount() {
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
const metricsByPrefix = groupMetricsByPrefix(metrics);
const histogramOptions = histogramMetrics.map(hm => ({ label: hm, value: hm }));
const metricsOptions = [
{ label: 'Histograms', value: HISTOGRAM_GROUP, children: histogramOptions },
...metricsByPrefix,
];
const metricsOptions =
histogramMetrics.length > 0
? [
{ label: 'Histograms', value: HISTOGRAM_GROUP, children: histogramOptions, isLeaf: false },
...metricsByPrefix,
]
: metricsByPrefix;
this.setState({ metricsOptions, syntaxLoaded: true });
};
......@@ -222,12 +230,15 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
const { error, hint, initialQuery } = this.props;
const { metricsOptions, syntaxLoaded } = this.state;
const cleanText = this.languageProvider ? this.languageProvider.cleanText : undefined;
const chooserText = syntaxLoaded ? 'Metrics' : 'Loading matrics...';
return (
<div className="prom-query-field">
<div className="prom-query-field-tools">
<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>
</div>
<div className="prom-query-field-wrapper">
......
......@@ -74,7 +74,7 @@ export default class PromQlLanguageProvider extends LanguageProvider {
start = () => {
if (!this.started) {
this.started = true;
return Promise.all([this.fetchMetricNames(), this.fetchHistogramMetrics()]);
return this.fetchMetricNames().then(() => [this.fetchHistogramMetrics()]);
}
return Promise.resolve([]);
};
......
......@@ -85,7 +85,11 @@ export interface HistoryItem {
export abstract class LanguageProvider {
datasource: 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 {
......
......@@ -329,6 +329,16 @@
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
.cheat-sheet-item {
......
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