Prometheus: Fix copy paste behaving as cut and paste (#28622)

This commit is contained in:
Andrej Ocenas 2020-10-30 10:03:05 +01:00 committed by GitHub
parent 05644e7042
commit 43a0167b01
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 131 additions and 90 deletions

View File

@ -600,6 +600,6 @@ export abstract class LanguageProvider {
* Returns startTask that resolves with a task list when main syntax is loaded. * 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. * Task list consists of secondary promises that load more detailed language features.
*/ */
abstract start: () => Promise<any[]>; abstract start: () => Promise<Array<Promise<any>>>;
startTask?: Promise<any[]>; startTask?: Promise<any[]>;
} }

View File

@ -3,7 +3,7 @@ import RCCascader from 'rc-cascader';
import React from 'react'; import React from 'react';
import PromQlLanguageProvider, { DEFAULT_LOOKUP_METRICS_THRESHOLD } from '../language_provider'; import PromQlLanguageProvider, { DEFAULT_LOOKUP_METRICS_THRESHOLD } from '../language_provider';
import PromQueryField, { groupMetricsByPrefix, RECORDING_RULES_GROUP } from './PromQueryField'; import PromQueryField, { groupMetricsByPrefix, RECORDING_RULES_GROUP } from './PromQueryField';
import { DataSourceInstanceSettings } from '@grafana/data'; import { DataSourceInstanceSettings, dateTime } from '@grafana/data';
import { PromOptions } from '../types'; import { PromOptions } from '../types';
import { fireEvent, render, screen } from '@testing-library/react'; import { fireEvent, render, screen } from '@testing-library/react';
@ -51,61 +51,89 @@ describe('PromQueryField', () => {
}); });
it('refreshes metrics when the data source changes', async () => { it('refreshes metrics when the data source changes', async () => {
const defaultProps = {
query: { expr: '', refId: '' },
onRunQuery: () => {},
onChange: () => {},
history: [],
};
const metrics = ['foo', 'bar']; 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( const queryField = render(
<PromQueryField <PromQueryField
// @ts-ignore // @ts-ignore
datasource={{ datasource={{
languageProvider, languageProvider: makeLanguageProvider({ metrics: [metrics] }),
}} }}
query={{ expr: '', refId: '' }} {...defaultProps}
onRunQuery={() => {}}
onChange={() => {}}
history={[]}
/> />
); );
let cascader = await queryField.findByRole('button'); checkMetricsInCascader(await screen.findByRole('button'), metrics);
fireEvent.keyDown(cascader, { keyCode: 40 });
let listNodes = screen.getAllByRole('menuitem');
for (const node of listNodes) {
expect(metrics).toContain(node.innerHTML);
}
const changedMetrics = ['baz', 'moo']; const changedMetrics = ['baz', 'moo'];
queryField.rerender( queryField.rerender(
<PromQueryField <PromQueryField
// @ts-ignore
datasource={{ datasource={{
//@ts-ignore languageProvider: makeLanguageProvider({ metrics: [changedMetrics] }),
languageProvider: {
...languageProvider,
metrics: changedMetrics,
},
}} }}
query={{ expr: '', refId: '' }} {...defaultProps}
onRunQuery={() => {}}
onChange={() => {}}
history={[]}
/> />
); );
cascader = await queryField.findByRole('button'); // If we check the cascader right away it should be in loading state
fireEvent.keyDown(cascader, { keyCode: 40 }); let cascader = screen.getByRole('button');
listNodes = screen.getAllByRole('menuitem'); expect(cascader.textContent).toContain('Loading');
for (const node of listNodes) { checkMetricsInCascader(await screen.findByRole('button'), changedMetrics);
expect(changedMetrics).toContain(node.innerHTML); });
}
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(
<PromQueryField
// @ts-ignore
datasource={{ languageProvider }}
range={{
...range,
raw: range,
}}
{...defaultProps}
/>
);
checkMetricsInCascader(await screen.findByRole('button'), metrics);
const newRange = {
from: dateTime('2020-10-28T01:00:00Z'),
to: dateTime('2020-10-28T02:00:00Z'),
};
queryField.rerender(
<PromQueryField
// @ts-ignore
datasource={{ languageProvider }}
range={{
...newRange,
raw: newRange,
}}
{...defaultProps}
/>
);
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);
}
}

View File

@ -17,14 +17,7 @@ import Prism from 'prismjs';
// dom also includes Element polyfills // dom also includes Element polyfills
import { PromQuery, PromOptions, PromMetricsMetadata } from '../types'; import { PromQuery, PromOptions, PromMetricsMetadata } from '../types';
import { CancelablePromise, makePromiseCancelable } from 'app/core/utils/CancelablePromise'; import { CancelablePromise, makePromiseCancelable } from 'app/core/utils/CancelablePromise';
import { import { ExploreQueryFieldProps, QueryHint, isDataFrame, toLegacyResponseData, HistoryItem } from '@grafana/data';
ExploreQueryFieldProps,
QueryHint,
isDataFrame,
toLegacyResponseData,
HistoryItem,
AbsoluteTimeRange,
} from '@grafana/data';
import { DOMUtil, SuggestionsState } from '@grafana/ui'; import { DOMUtil, SuggestionsState } from '@grafana/ui';
import { PrometheusDatasource } from '../datasource'; import { PrometheusDatasource } from '../datasource';
@ -173,21 +166,27 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
range, range,
} = this.props; } = this.props;
let refreshed = false; const rangeChanged =
if (range && prevProps.range) { range &&
const absoluteRange: AbsoluteTimeRange = { from: range.from.valueOf(), to: range.to.valueOf() }; prevProps.range &&
const prevAbsoluteRange: AbsoluteTimeRange = { !_.isEqual(
from: prevProps.range.from.valueOf(), { from: range.from.valueOf(), to: range.to.valueOf() },
to: prevProps.range.to.valueOf(), {
}; from: prevProps.range.from.valueOf(),
to: prevProps.range.to.valueOf(),
}
);
if (!_.isEqual(absoluteRange, prevAbsoluteRange)) { if (languageProvider !== prevProps.datasource.languageProvider) {
this.refreshMetrics(); // We reset this only on DS change so we do not flesh loading state on every rangeChange which happens on every
refreshed = true; // query run if using relative range.
} this.setState({
metricsOptions: [],
syntaxLoaded: false,
});
} }
if (!refreshed && languageProvider !== prevProps.datasource.languageProvider) { if (languageProvider !== prevProps.datasource.languageProvider || rangeChanged) {
this.refreshMetrics(); this.refreshMetrics();
} }
@ -206,31 +205,35 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
const result = isDataFrame(data.series[0]) ? data.series.map(toLegacyResponseData) : data.series; const result = isDataFrame(data.series[0]) ? data.series.map(toLegacyResponseData) : data.series;
const hints = datasource.getQueryHints(query, result); const hints = datasource.getQueryHints(query, result);
const hint = hints.length > 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 }); this.setState({ hint });
}; };
refreshMetrics = () => { refreshMetrics = async () => {
const { const {
datasource: { languageProvider }, datasource: { languageProvider },
} = this.props; } = this.props;
this.setState({
syntaxLoaded: false,
});
Prism.languages[PRISM_SYNTAX] = languageProvider.syntax; Prism.languages[PRISM_SYNTAX] = languageProvider.syntax;
this.languageProviderInitializationPromise = makePromiseCancelable(languageProvider.start()); this.languageProviderInitializationPromise = makePromiseCancelable(languageProvider.start());
this.languageProviderInitializationPromise.promise
.then(remaining => { try {
remaining.map((task: Promise<any>) => task.then(this.onUpdateLanguage).catch(() => {})); const remainingTasks = await this.languageProviderInitializationPromise.promise;
}) await Promise.all(remainingTasks);
.then(() => this.onUpdateLanguage()) this.onUpdateLanguage();
.catch(err => { } catch (err) {
if (!err.isCanceled) { if (!err.isCanceled) {
throw err; throw err;
} }
}); }
}; };
onChangeMetrics = (values: string[], selectedOptions: CascaderOption[]) => { onChangeMetrics = (values: string[], selectedOptions: CascaderOption[]) => {
@ -278,10 +281,9 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
onUpdateLanguage = () => { onUpdateLanguage = () => {
const { const {
datasource,
datasource: { languageProvider }, datasource: { languageProvider },
} = this.props; } = this.props;
const { histogramMetrics, metrics, metricsMetadata, lookupMetricsThreshold } = languageProvider; const { histogramMetrics, metrics, metricsMetadata } = languageProvider;
if (!metrics) { if (!metrics) {
return; return;
@ -298,17 +300,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
] ]
: metricsByPrefix; : metricsByPrefix;
// Hint for big disabled lookups this.setState({ metricsOptions, syntaxLoaded: true });
let hint: QueryHint | null = null;
if (!datasource.lookupsDisabled && languageProvider.lookupsDisabled) {
hint = {
label: `Dynamic label lookup is disabled for datasources with more than ${lookupMetricsThreshold} metrics.`,
type: 'INFO',
};
}
this.setState({ hint, metricsOptions, syntaxLoaded: true });
}; };
onTypeahead = async (typeahead: TypeaheadInput): Promise<TypeaheadOutput> => { onTypeahead = async (typeahead: TypeaheadInput): Promise<TypeaheadOutput> => {
@ -328,8 +320,6 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
{ history } { history }
); );
// console.log('handleTypeahead', wrapperClasses, text, prefix, labelKey, result.context);
return result; return result;
}; };