mirror of
https://github.com/grafana/grafana.git
synced 2025-01-01 11:47:05 -06:00
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:
parent
749d7a2f0c
commit
4f959648a7
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user