فهرست منبع

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
David Kaltschmidt 7 سال پیش
والد
کامیت
4f959648a7

+ 36 - 14
public/app/plugins/datasource/logging/components/LoggingQueryField.tsx

@@ -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">

+ 42 - 38
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<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);

+ 17 - 6
public/app/plugins/datasource/prometheus/components/PromQueryField.tsx

@@ -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">

+ 1 - 1
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([]);
   };

+ 5 - 1
public/app/types/explore.ts

@@ -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 {

+ 10 - 0
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 {