Prometheus: Refresh query field metrics on data source change (#24116)

... in `componentDidUpdate`, not just `componentDidMount`.

Also unify query field behavior of Explore with Dashboard - when the
data source changes, it doesn't unmount but instead refreshes its
metrics.

Fixes #23162
This commit is contained in:
Andreas Opferkuch
2020-05-04 10:17:57 +02:00
committed by GitHub
parent e9243215a6
commit 827f99f0cb
4 changed files with 97 additions and 21 deletions

View File

@ -20,8 +20,8 @@ export default class QueryRows extends PureComponent<QueryRowsProps> {
const { className = '', exploreEvents, exploreId, queryKeys } = this.props; const { className = '', exploreEvents, exploreId, queryKeys } = this.props;
return ( return (
<div className={className}> <div className={className}>
{queryKeys.map((key, index) => { {queryKeys.map((_, index) => {
return <QueryRow key={key} exploreEvents={exploreEvents} exploreId={exploreId} index={index} />; return <QueryRow key={index} exploreEvents={exploreEvents} exploreId={exploreId} index={index} />;
})} })}
</div> </div>
); );

View File

@ -302,7 +302,6 @@ export const itemReducer = (state: ExploreItemState = makeExploreItemState(), ac
latency: 0, latency: 0,
queryResponse: createEmptyQueryResponse(), queryResponse: createEmptyQueryResponse(),
loading: false, loading: false,
queryKeys: [],
supportedModes, supportedModes,
mode: mode ?? newMode, mode: mode ?? newMode,
originPanelId: state.urlState && state.urlState.originPanelId, originPanelId: state.urlState && state.urlState.originPanelId,

View File

@ -1,4 +1,67 @@
import { groupMetricsByPrefix, RECORDING_RULES_GROUP } from './PromQueryField'; import { mount } from 'enzyme';
// @ts-ignore
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';
describe('PromQueryField', () => {
beforeAll(() => {
// @ts-ignore
window.getSelection = () => {};
});
it('refreshes metrics when the data source changes', async () => {
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 = mount(
<PromQueryField
// @ts-ignore
datasource={{
languageProvider,
}}
query={{ expr: '', refId: '' }}
onRunQuery={() => {}}
onChange={() => {}}
history={[]}
/>
);
await Promise.resolve();
const cascader = queryField.find<RCCascader>(RCCascader);
cascader.simulate('click');
const cascaderNode: HTMLElement = cascader.instance().getPopupDOMNode();
for (const item of Array.from(cascaderNode.getElementsByTagName('li'))) {
expect(metrics.includes(item.innerHTML)).toBe(true);
}
const changedMetrics = ['baz', 'moo'];
queryField.setProps({
datasource: {
languageProvider: {
...languageProvider,
metrics: changedMetrics,
},
},
});
await Promise.resolve();
for (const item of Array.from(cascaderNode.getElementsByTagName('li'))) {
expect(changedMetrics.includes(item.innerHTML)).toBe(true);
}
});
});
describe('groupMetricsByPrefix()', () => { describe('groupMetricsByPrefix()', () => {
it('returns an empty group for no metrics', () => { it('returns an empty group for no metrics', () => {

View File

@ -20,7 +20,6 @@ import { CancelablePromise, makePromiseCancelable } from 'app/core/utils/Cancela
import { ExploreQueryFieldProps, QueryHint, isDataFrame, toLegacyResponseData, HistoryItem } from '@grafana/data'; import { ExploreQueryFieldProps, QueryHint, isDataFrame, toLegacyResponseData, HistoryItem } from '@grafana/data';
import { DOMUtil, SuggestionsState } from '@grafana/ui'; import { DOMUtil, SuggestionsState } from '@grafana/ui';
import { PrometheusDatasource } from '../datasource'; import { PrometheusDatasource } from '../datasource';
import PromQlLanguageProvider from '../language_provider';
const HISTOGRAM_GROUP = '__histograms__'; const HISTOGRAM_GROUP = '__histograms__';
const PRISM_SYNTAX = 'promql'; const PRISM_SYNTAX = 'promql';
@ -121,16 +120,11 @@ interface PromQueryFieldState {
class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryFieldState> { class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryFieldState> {
plugins: Plugin[]; plugins: Plugin[];
languageProvider: PromQlLanguageProvider;
languageProviderInitializationPromise: CancelablePromise<any>; languageProviderInitializationPromise: CancelablePromise<any>;
constructor(props: PromQueryFieldProps, context: React.Context<any>) { constructor(props: PromQueryFieldProps, context: React.Context<any>) {
super(props, context); super(props, context);
if (props.datasource.languageProvider) {
this.languageProvider = props.datasource.languageProvider;
}
this.plugins = [ this.plugins = [
BracesPlugin(), BracesPlugin(),
SlatePrism({ SlatePrism({
@ -147,9 +141,8 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
} }
componentDidMount() { componentDidMount() {
if (this.languageProvider) { if (this.props.datasource.languageProvider) {
Prism.languages[PRISM_SYNTAX] = this.languageProvider.syntax; this.refreshMetrics();
this.refreshMetrics(makePromiseCancelable(this.languageProvider.start()));
} }
this.refreshHint(); this.refreshHint();
} }
@ -161,7 +154,14 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
} }
componentDidUpdate(prevProps: PromQueryFieldProps) { componentDidUpdate(prevProps: PromQueryFieldProps) {
const { data } = this.props; const {
data,
datasource: { languageProvider },
} = this.props;
if (languageProvider !== prevProps.datasource.languageProvider) {
this.refreshMetrics();
}
if (data && prevProps.data && prevProps.data.series !== data.series) { if (data && prevProps.data && prevProps.data.series !== data.series) {
this.refreshHint(); this.refreshHint();
@ -182,8 +182,13 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
this.setState({ hint }); this.setState({ hint });
}; };
refreshMetrics = (cancelablePromise: CancelablePromise<any>) => { refreshMetrics = () => {
this.languageProviderInitializationPromise = cancelablePromise; const {
datasource: { languageProvider },
} = this.props;
Prism.languages[PRISM_SYNTAX] = languageProvider.syntax;
this.languageProviderInitializationPromise = makePromiseCancelable(languageProvider.start());
this.languageProviderInitializationPromise.promise this.languageProviderInitializationPromise.promise
.then(remaining => { .then(remaining => {
remaining.map((task: Promise<any>) => task.then(this.onUpdateLanguage).catch(() => {})); remaining.map((task: Promise<any>) => task.then(this.onUpdateLanguage).catch(() => {}));
@ -246,7 +251,8 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
metricsMetadata, metricsMetadata,
lookupsDisabled, lookupsDisabled,
lookupMetricsThreshold, lookupMetricsThreshold,
} = this.languageProvider; } = this.props.datasource.languageProvider;
if (!metrics) { if (!metrics) {
return; return;
} }
@ -275,14 +281,18 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
}; };
onTypeahead = async (typeahead: TypeaheadInput): Promise<TypeaheadOutput> => { onTypeahead = async (typeahead: TypeaheadInput): Promise<TypeaheadOutput> => {
if (!this.languageProvider) { const {
datasource: { languageProvider },
} = this.props;
if (!languageProvider) {
return { suggestions: [] }; return { suggestions: [] };
} }
const { history } = this.props; const { history } = this.props;
const { prefix, text, value, wrapperClasses, labelKey } = typeahead; const { prefix, text, value, wrapperClasses, labelKey } = typeahead;
const result = await this.languageProvider.provideCompletionItems( const result = await languageProvider.provideCompletionItems(
{ text, value, prefix, wrapperClasses, labelKey }, { text, value, prefix, wrapperClasses, labelKey },
{ history } { history }
); );
@ -293,9 +303,13 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
}; };
render() { render() {
const { query, ExtraFieldElement } = this.props; const {
datasource: { languageProvider },
query,
ExtraFieldElement,
} = this.props;
const { metricsOptions, syntaxLoaded, hint } = this.state; const { metricsOptions, syntaxLoaded, hint } = this.state;
const cleanText = this.languageProvider ? this.languageProvider.cleanText : undefined; const cleanText = languageProvider ? languageProvider.cleanText : undefined;
const chooserText = getChooserText(syntaxLoaded, metricsOptions); const chooserText = getChooserText(syntaxLoaded, metricsOptions);
const buttonDisabled = !(syntaxLoaded && metricsOptions && metricsOptions.length > 0); const buttonDisabled = !(syntaxLoaded && metricsOptions && metricsOptions.length > 0);