diff --git a/packages/grafana-data/src/types/datasource.ts b/packages/grafana-data/src/types/datasource.ts index c07100f8ee4..bef708a7020 100644 --- a/packages/grafana-data/src/types/datasource.ts +++ b/packages/grafana-data/src/types/datasource.ts @@ -600,6 +600,6 @@ export abstract class LanguageProvider { * Returns startTask that resolves with a task list when main syntax is loaded. * Task list consists of secondary promises that load more detailed language features. */ - abstract start: () => Promise; + abstract start: () => Promise>>; startTask?: Promise; } diff --git a/public/app/plugins/datasource/prometheus/components/PromQueryField.test.tsx b/public/app/plugins/datasource/prometheus/components/PromQueryField.test.tsx index bbd835ee458..3bc79ea44d0 100644 --- a/public/app/plugins/datasource/prometheus/components/PromQueryField.test.tsx +++ b/public/app/plugins/datasource/prometheus/components/PromQueryField.test.tsx @@ -3,7 +3,7 @@ import RCCascader from 'rc-cascader'; import React from 'react'; import PromQlLanguageProvider, { DEFAULT_LOOKUP_METRICS_THRESHOLD } from '../language_provider'; import PromQueryField, { groupMetricsByPrefix, RECORDING_RULES_GROUP } from './PromQueryField'; -import { DataSourceInstanceSettings } from '@grafana/data'; +import { DataSourceInstanceSettings, dateTime } from '@grafana/data'; import { PromOptions } from '../types'; import { fireEvent, render, screen } from '@testing-library/react'; @@ -51,61 +51,89 @@ describe('PromQueryField', () => { }); it('refreshes metrics when the data source changes', async () => { + const defaultProps = { + query: { expr: '', refId: '' }, + onRunQuery: () => {}, + onChange: () => {}, + history: [], + }; const metrics = ['foo', 'bar']; - const languageProvider = ({ - histogramMetrics: [] as any, - metrics, - metricsMetadata: {}, - lookupsDisabled: false, - lookupMetricsThreshold: DEFAULT_LOOKUP_METRICS_THRESHOLD, - start: () => { - return Promise.resolve([]); - }, - } as unknown) as PromQlLanguageProvider; - const queryField = render( {}} - onChange={() => {}} - history={[]} + {...defaultProps} /> ); - let cascader = await queryField.findByRole('button'); - fireEvent.keyDown(cascader, { keyCode: 40 }); - let listNodes = screen.getAllByRole('menuitem'); - for (const node of listNodes) { - expect(metrics).toContain(node.innerHTML); - } + checkMetricsInCascader(await screen.findByRole('button'), metrics); const changedMetrics = ['baz', 'moo']; queryField.rerender( {}} - onChange={() => {}} - history={[]} + {...defaultProps} /> ); - cascader = await queryField.findByRole('button'); - fireEvent.keyDown(cascader, { keyCode: 40 }); - listNodes = screen.getAllByRole('menuitem'); - for (const node of listNodes) { - expect(changedMetrics).toContain(node.innerHTML); - } + // If we check the cascader right away it should be in loading state + let cascader = screen.getByRole('button'); + expect(cascader.textContent).toContain('Loading'); + checkMetricsInCascader(await screen.findByRole('button'), changedMetrics); + }); + + it('refreshes metrics when time range changes but dont show loading state', async () => { + const defaultProps = { + query: { expr: '', refId: '' }, + onRunQuery: () => {}, + onChange: () => {}, + history: [], + }; + const metrics = ['foo', 'bar']; + const changedMetrics = ['baz', 'moo']; + const range = { + from: dateTime('2020-10-28T00:00:00Z'), + to: dateTime('2020-10-28T01:00:00Z'), + }; + + const languageProvider = makeLanguageProvider({ metrics: [metrics, changedMetrics] }); + const queryField = render( + + ); + checkMetricsInCascader(await screen.findByRole('button'), metrics); + + const newRange = { + from: dateTime('2020-10-28T01:00:00Z'), + to: dateTime('2020-10-28T02:00:00Z'), + }; + queryField.rerender( + + ); + let cascader = screen.getByRole('button'); + // Should not show loading + expect(cascader.textContent).toContain('Metrics'); + checkMetricsInCascader(cascader, metrics); }); }); @@ -162,3 +190,26 @@ describe('groupMetricsByPrefix()', () => { ]); }); }); + +function makeLanguageProvider(options: { metrics: string[][] }) { + const metricsStack = [...options.metrics]; + return ({ + histogramMetrics: [] as any, + metrics: [], + metricsMetadata: {}, + lookupsDisabled: false, + lookupMetricsThreshold: DEFAULT_LOOKUP_METRICS_THRESHOLD, + start() { + this.metrics = metricsStack.shift(); + return Promise.resolve([]); + }, + } as any) as PromQlLanguageProvider; +} + +function checkMetricsInCascader(cascader: HTMLElement, metrics: string[]) { + fireEvent.keyDown(cascader, { keyCode: 40 }); + let listNodes = screen.getAllByRole('menuitem'); + for (const node of listNodes) { + expect(metrics).toContain(node.innerHTML); + } +} diff --git a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx index ef774adb034..b94322277a6 100644 --- a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx +++ b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx @@ -17,14 +17,7 @@ import Prism from 'prismjs'; // dom also includes Element polyfills import { PromQuery, PromOptions, PromMetricsMetadata } from '../types'; import { CancelablePromise, makePromiseCancelable } from 'app/core/utils/CancelablePromise'; -import { - ExploreQueryFieldProps, - QueryHint, - isDataFrame, - toLegacyResponseData, - HistoryItem, - AbsoluteTimeRange, -} from '@grafana/data'; +import { ExploreQueryFieldProps, QueryHint, isDataFrame, toLegacyResponseData, HistoryItem } from '@grafana/data'; import { DOMUtil, SuggestionsState } from '@grafana/ui'; import { PrometheusDatasource } from '../datasource'; @@ -173,21 +166,27 @@ class PromQueryField extends React.PureComponent 0 ? hints[0] : null; + let hint = hints.length > 0 ? hints[0] : null; + + // Hint for big disabled lookups + if (!hint && !datasource.lookupsDisabled && datasource.languageProvider.lookupsDisabled) { + hint = { + label: `Dynamic label lookup is disabled for datasources with more than ${datasource.languageProvider.lookupMetricsThreshold} metrics.`, + type: 'INFO', + }; + } this.setState({ hint }); }; - refreshMetrics = () => { + refreshMetrics = async () => { const { datasource: { languageProvider }, } = this.props; - this.setState({ - syntaxLoaded: false, - }); - Prism.languages[PRISM_SYNTAX] = languageProvider.syntax; this.languageProviderInitializationPromise = makePromiseCancelable(languageProvider.start()); - this.languageProviderInitializationPromise.promise - .then(remaining => { - remaining.map((task: Promise) => task.then(this.onUpdateLanguage).catch(() => {})); - }) - .then(() => this.onUpdateLanguage()) - .catch(err => { - if (!err.isCanceled) { - throw err; - } - }); + + try { + const remainingTasks = await this.languageProviderInitializationPromise.promise; + await Promise.all(remainingTasks); + this.onUpdateLanguage(); + } catch (err) { + if (!err.isCanceled) { + throw err; + } + } }; onChangeMetrics = (values: string[], selectedOptions: CascaderOption[]) => { @@ -278,10 +281,9 @@ class PromQueryField extends React.PureComponent { const { - datasource, datasource: { languageProvider }, } = this.props; - const { histogramMetrics, metrics, metricsMetadata, lookupMetricsThreshold } = languageProvider; + const { histogramMetrics, metrics, metricsMetadata } = languageProvider; if (!metrics) { return; @@ -298,17 +300,7 @@ class PromQueryField extends React.PureComponent => { @@ -328,8 +320,6 @@ class PromQueryField extends React.PureComponent