diff --git a/public/app/plugins/datasource/logging/components/LoggingQueryField.tsx b/public/app/plugins/datasource/logging/components/LoggingQueryField.tsx index 6bf1a38245a..82ebc444c62 100644 --- a/public/app/plugins/datasource/logging/components/LoggingQueryField.tsx +++ b/public/app/plugins/datasource/logging/components/LoggingQueryField.tsx @@ -92,25 +92,44 @@ class LoggingQueryField extends React.PureComponent 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
- - + +
diff --git a/public/app/plugins/datasource/logging/language_provider.ts b/public/app/plugins/datasource/logging/language_provider.ts index 411a1f9439c..32f4f8a0cd7 100644 --- a/public/app/plugins/datasource/logging/language_provider.ts +++ b/public/app/plugins/datasource/logging/language_provider.ts @@ -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 = 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); diff --git a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx index 5af540965b6..649b17ad8cf 100644 --- a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx +++ b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx @@ -131,7 +131,12 @@ class PromQueryField extends React.PureComponent 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 ({ 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
- +
diff --git a/public/app/plugins/datasource/prometheus/language_provider.ts b/public/app/plugins/datasource/prometheus/language_provider.ts index 615d7b2c2c5..5f97ebfa7b5 100644 --- a/public/app/plugins/datasource/prometheus/language_provider.ts +++ b/public/app/plugins/datasource/prometheus/language_provider.ts @@ -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([]); }; diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index a4fb74634fe..a96d6c084fb 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -85,7 +85,11 @@ export interface HistoryItem { export abstract class LanguageProvider { datasource: any; request: (url) => Promise; - start: () => Promise; + /** + * 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; } export interface TypeaheadInput { diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss index be2083b6e5c..b70b058879c 100644 --- a/public/sass/pages/_explore.scss +++ b/public/sass/pages/_explore.scss @@ -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 {