mirror of
https://github.com/grafana/grafana.git
synced 2025-01-19 13:03:32 -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() {
|
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">
|
||||||
|
@ -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);
|
||||||
|
@ -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">
|
||||||
|
@ -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([]);
|
||||||
};
|
};
|
||||||
|
@ -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 {
|
||||||
|
@ -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 {
|
||||||
|
Loading…
Reference in New Issue
Block a user