From f3fe63e87e72a732e55f5beb84d28f6bfcd1df63 Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Mon, 30 Oct 2023 17:00:34 +0100 Subject: [PATCH] Chore: Remove unused code from prometheus datasource (#76776) * Remove unused code * More cleaning * Delete more * betterer * Small import fixes * Fix unit test * Fix unit test * uncomment * Have template_srv from @grafana/runtime --- .betterer.results | 6 +- .../prometheus/components/PromQueryField.tsx | 78 +-- .../datasource/prometheus/datasource.test.ts | 3 +- .../datasource/prometheus/datasource.ts | 3 +- .../prometheus/language_provider.test.ts | 528 +----------------- .../prometheus/language_provider.ts | 322 +---------- .../prometheus/metric_find_query.test.ts | 3 +- .../components/PromQueryBuilder.test.tsx | 9 +- 8 files changed, 21 insertions(+), 931 deletions(-) diff --git a/.betterer.results b/.betterer.results index fcfcd4abf07..49ffde3a4fc 100644 --- a/.betterer.results +++ b/.betterer.results @@ -6461,8 +6461,7 @@ exports[`better eslint`] = { ], "public/app/plugins/datasource/prometheus/components/PromQueryField.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Do not use any type assertions.", "2"] + [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], "public/app/plugins/datasource/prometheus/components/PrometheusMetricsBrowser.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"], @@ -6589,8 +6588,7 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "2"], [0, 0, 0, "Unexpected any. Specify a different type.", "3"], [0, 0, 0, "Unexpected any. Specify a different type.", "4"], - [0, 0, 0, "Unexpected any. Specify a different type.", "5"], - [0, 0, 0, "Do not use any type assertions.", "6"] + [0, 0, 0, "Do not use any type assertions.", "5"] ], "public/app/plugins/datasource/prometheus/language_utils.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], diff --git a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx index 851466a2cb6..4162dfca2e4 100644 --- a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx +++ b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx @@ -1,23 +1,9 @@ import { cx } from '@emotion/css'; -import { LanguageMap, languages as prismLanguages } from 'prismjs'; import React, { ReactNode } from 'react'; -import { Plugin } from 'slate'; -import { Editor } from 'slate-react'; import { isDataFrame, QueryEditorProps, QueryHint, TimeRange, toLegacyResponseData } from '@grafana/data'; -import { reportInteraction } from '@grafana/runtime/src'; -import { - BracesPlugin, - DOMUtil, - Icon, - SlatePrism, - SuggestionsState, - TypeaheadInput, - TypeaheadOutput, - Themeable2, - withTheme2, - clearButtonStyles, -} from '@grafana/ui'; +import { reportInteraction } from '@grafana/runtime'; +import { Icon, Themeable2, withTheme2, clearButtonStyles } from '@grafana/ui'; import { LocalStorageValueProvider } from 'app/core/components/LocalStorageValueProvider'; import { CancelablePromise, @@ -32,7 +18,6 @@ import { PromOptions, PromQuery } from '../types'; import { PrometheusMetricsBrowser } from './PrometheusMetricsBrowser'; import { MonacoQueryFieldWrapper } from './monaco-query-field/MonacoQueryFieldWrapper'; -export const RECORDING_RULES_GROUP = '__recording_rules__'; const LAST_USED_LABELS_KEY = 'grafana.datasources.prometheus.browser.labels'; function getChooserText(metricsLookupDisabled: boolean, hasSyntax: boolean, hasMetrics: boolean) { @@ -51,33 +36,6 @@ function getChooserText(metricsLookupDisabled: boolean, hasSyntax: boolean, hasM return 'Metrics browser'; } -export function willApplySuggestion(suggestion: string, { typeaheadContext, typeaheadText }: SuggestionsState): string { - // Modify suggestion based on context - switch (typeaheadContext) { - case 'context-labels': { - const nextChar = DOMUtil.getNextCharacter(); - if (!nextChar || nextChar === '}' || nextChar === ',') { - suggestion += '='; - } - break; - } - - case 'context-label-values': { - // Always add quotes and remove existing ones instead - if (!typeaheadText.match(/^(!?=~?"|")/)) { - suggestion = `"${suggestion}`; - } - if (DOMUtil.getNextCharacter() !== '"') { - suggestion = `${suggestion}"`; - } - break; - } - - default: - } - return suggestion; -} - interface PromQueryFieldProps extends QueryEditorProps, Themeable2 { ExtraFieldElement?: ReactNode; 'data-testid'?: string; @@ -90,23 +48,11 @@ interface PromQueryFieldState { } class PromQueryField extends React.PureComponent { - plugins: Array>; declare languageProviderInitializationPromise: CancelablePromise; constructor(props: PromQueryFieldProps, context: React.Context) { super(props, context); - this.plugins = [ - BracesPlugin(), - SlatePrism( - { - onlyIn: (node) => 'type' in node && node.type === 'code_block', - getSyntax: (node) => 'promql', - }, - { ...(prismLanguages as LanguageMap), promql: this.props.datasource.languageProvider.syntax } - ), - ]; - this.state = { labelBrowserVisible: false, syntaxLoaded: false, @@ -255,26 +201,6 @@ class PromQueryField extends React.PureComponent => { - const { - datasource: { languageProvider }, - } = this.props; - - if (!languageProvider) { - return { suggestions: [] }; - } - - const { history } = this.props; - const { prefix, text, value, wrapperClasses, labelKey } = typeahead; - - const result = await languageProvider.provideCompletionItems( - { text, value, prefix, wrapperClasses, labelKey }, - { history } - ); - - return result; - }; - render() { const { datasource, diff --git a/public/app/plugins/datasource/prometheus/datasource.test.ts b/public/app/plugins/datasource/prometheus/datasource.test.ts index adf1dd1dd26..60c946dcf5c 100644 --- a/public/app/plugins/datasource/prometheus/datasource.test.ts +++ b/public/app/plugins/datasource/prometheus/datasource.test.ts @@ -18,7 +18,7 @@ import { toDataFrame, VariableHide, } from '@grafana/data'; -import { TemplateSrv } from 'app/features/templating/template_srv'; +import { TemplateSrv } from '@grafana/runtime'; import { QueryOptions } from 'app/types'; import { @@ -39,6 +39,7 @@ jest.mock('@grafana/runtime', () => ({ getBackendSrv: () => ({ fetch: fetchMock, }), + getTemplateSrv: () => templateSrvStub, })); const replaceMock = jest.fn().mockImplementation((a: string, ...rest: unknown[]) => a); diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index a41aedd0d98..12fdd812864 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -38,9 +38,10 @@ import { getBackendSrv, isFetchError, toDataQueryResponse, + getTemplateSrv, + TemplateSrv, } from '@grafana/runtime'; import { safeStringifyValue } from 'app/core/utils/explore'; -import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv'; import { addLabelToQuery } from './add_label_to_query'; import { AnnotationQueryEditor } from './components/AnnotationQueryEditor'; diff --git a/public/app/plugins/datasource/prometheus/language_provider.test.ts b/public/app/plugins/datasource/prometheus/language_provider.test.ts index 8b9deac46a9..574fec92ef0 100644 --- a/public/app/plugins/datasource/prometheus/language_provider.test.ts +++ b/public/app/plugins/datasource/prometheus/language_provider.test.ts @@ -1,14 +1,10 @@ -import { Editor as SlateEditor } from 'slate'; -import Plain from 'slate-plain-serializer'; - -import { AbstractLabelOperator, dateTime, HistoryItem, TimeRange } from '@grafana/data'; -import { SearchFunctionType } from '@grafana/ui'; +import { AbstractLabelOperator, dateTime, TimeRange } from '@grafana/data'; import { Label } from './components/monaco-query-field/monaco-completion-provider/situation'; import { PrometheusDatasource } from './datasource'; import LanguageProvider from './language_provider'; import { getClientCacheDurationInMinutes, getPrometheusTime, getRangeSnapInterval } from './language_utils'; -import { PrometheusCacheLevel, PromQuery } from './types'; +import { PrometheusCacheLevel } from './types'; const now = new Date(1681300293392).getTime(); const timeRangeDurationSeconds = 1; @@ -332,516 +328,7 @@ describe('Language completion provider', () => { }); }); - describe('empty query suggestions', () => { - it('returns no suggestions on empty context', async () => { - const instance = new LanguageProvider(defaultDatasource); - const value = Plain.deserialize(''); - const result = await instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] }); - expect(result.context).toBeUndefined(); - expect(result.suggestions).toMatchObject([]); - }); - - it('returns no suggestions with metrics on empty context even when metrics were provided', async () => { - const instance = new LanguageProvider(defaultDatasource); - instance.metrics = ['foo', 'bar']; - const value = Plain.deserialize(''); - const result = await instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] }); - expect(result.context).toBeUndefined(); - expect(result.suggestions).toMatchObject([]); - }); - - it('returns history on empty context when history was provided', async () => { - const instance = new LanguageProvider(defaultDatasource); - const value = Plain.deserialize(''); - const history: Array> = [ - { - ts: 0, - query: { refId: '1', expr: 'metric' }, - }, - ]; - const result = await instance.provideCompletionItems( - { text: '', prefix: '', value, wrapperClasses: [] }, - { history } - ); - expect(result.context).toBeUndefined(); - - expect(result.suggestions).toMatchObject([ - { - label: 'History', - items: [ - { - label: 'metric', - }, - ], - }, - ]); - }); - }); - - describe('range suggestions', () => { - it('returns range suggestions in range context', async () => { - const instance = new LanguageProvider(defaultDatasource); - const value = Plain.deserialize('1'); - const result = await instance.provideCompletionItems({ - text: '1', - prefix: '1', - value, - wrapperClasses: ['context-range'], - }); - expect(result.context).toBe('context-range'); - expect(result.suggestions).toMatchObject([ - { - items: [ - { label: '$__interval', sortValue: '$__interval' }, - { label: '$__rate_interval', sortValue: '$__rate_interval' }, - { label: '$__range', sortValue: '$__range' }, - { label: '1m', sortValue: '00:01:00' }, - { label: '5m', sortValue: '00:05:00' }, - { label: '10m', sortValue: '00:10:00' }, - { label: '30m', sortValue: '00:30:00' }, - { label: '1h', sortValue: '01:00:00' }, - { label: '1d', sortValue: '24:00:00' }, - ], - label: 'Range vector', - }, - ]); - }); - }); - - describe('metric suggestions', () => { - it('returns history, metrics and function suggestions in an uknown context ', async () => { - const instance = new LanguageProvider(defaultDatasource); - instance.metrics = ['foo', 'bar']; - const history: Array> = [ - { - ts: 0, - query: { refId: '1', expr: 'metric' }, - }, - ]; - let value = Plain.deserialize('m'); - value = value.setSelection({ anchor: { offset: 1 }, focus: { offset: 1 } }); - // Even though no metric with `m` is present, we still get metric completion items, filtering is done by the consumer - const result = await instance.provideCompletionItems( - { text: 'm', prefix: 'm', value, wrapperClasses: [] }, - { history } - ); - expect(result.context).toBeUndefined(); - expect(result.suggestions).toMatchObject([ - { - label: 'History', - items: [ - { - label: 'metric', - }, - ], - }, - { - label: 'Functions', - }, - { - label: 'Metrics', - }, - ]); - }); - - it('returns no suggestions directly after a binary operator', async () => { - const instance = new LanguageProvider(defaultDatasource); - instance.metrics = ['foo', 'bar']; - const value = Plain.deserialize('*'); - const result = await instance.provideCompletionItems({ text: '*', prefix: '', value, wrapperClasses: [] }); - expect(result.context).toBeUndefined(); - expect(result.suggestions).toMatchObject([]); - }); - - it('returns metric suggestions with prefix after a binary operator', async () => { - const instance = new LanguageProvider(defaultDatasource); - instance.metrics = ['foo', 'bar']; - const value = Plain.deserialize('foo + b'); - const ed = new SlateEditor({ value }); - const valueWithSelection = ed.moveForward(7).value; - const result = await instance.provideCompletionItems({ - text: 'foo + b', - prefix: 'b', - value: valueWithSelection, - wrapperClasses: [], - }); - expect(result.context).toBeUndefined(); - expect(result.suggestions).toMatchObject([ - { - label: 'Functions', - }, - { - label: 'Metrics', - }, - ]); - }); - - it('returns no suggestions at the beginning of a non-empty function', async () => { - const instance = new LanguageProvider(defaultDatasource); - const value = Plain.deserialize('sum(up)'); - const ed = new SlateEditor({ value }); - - const valueWithSelection = ed.moveForward(4).value; - const result = await instance.provideCompletionItems({ - text: '', - prefix: '', - value: valueWithSelection, - wrapperClasses: [], - }); - expect(result.context).toBeUndefined(); - expect(result.suggestions.length).toEqual(0); - }); - }); - - describe('label suggestions', () => { - it('returns default label suggestions on label context and no metric', async () => { - const instance = new LanguageProvider(defaultDatasource); - const value = Plain.deserialize('{}'); - const ed = new SlateEditor({ value }); - const valueWithSelection = ed.moveForward(1).value; - const result = await instance.provideCompletionItems({ - text: '', - prefix: '', - wrapperClasses: ['context-labels'], - value: valueWithSelection, - }); - expect(result.context).toBe('context-labels'); - expect(result.suggestions).toEqual([ - { - items: [{ label: 'job' }, { label: 'instance' }], - label: 'Labels', - searchFunctionType: SearchFunctionType.Fuzzy, - }, - ]); - }); - - it('returns label suggestions on label context and metric', async () => { - const datasources: PrometheusDatasource = { - ...defaultDatasource, - metadataRequest: () => ({ data: { data: [{ __name__: 'metric', bar: 'bazinga' }] } }), - } as unknown as PrometheusDatasource; - const instance = new LanguageProvider(datasources); - const value = Plain.deserialize('metric{}'); - const ed = new SlateEditor({ value }); - const valueWithSelection = ed.moveForward(7).value; - const result = await instance.provideCompletionItems({ - text: '', - prefix: '', - wrapperClasses: ['context-labels'], - value: valueWithSelection, - }); - expect(result.context).toBe('context-labels'); - expect(result.suggestions).toEqual([ - { items: [{ label: 'bar' }], label: 'Labels', searchFunctionType: SearchFunctionType.Fuzzy }, - ]); - }); - - it('returns label suggestions on label context but leaves out labels that already exist', async () => { - const testDatasource: PrometheusDatasource = { - ...defaultDatasource, - metadataRequest: () => ({ - data: { - data: [ - { - __name__: 'metric', - bar: 'asdasd', - job1: 'dsadsads', - job2: 'fsfsdfds', - job3: 'dsadsad', - }, - ], - }, - }), - } as unknown as PrometheusDatasource; - const instance = new LanguageProvider(testDatasource); - const value = Plain.deserialize('{job1="foo",job2!="foo",job3=~"foo",__name__="metric",}'); - const ed = new SlateEditor({ value }); - const valueWithSelection = ed.moveForward(54).value; - const result = await instance.provideCompletionItems({ - text: '', - prefix: '', - wrapperClasses: ['context-labels'], - value: valueWithSelection, - }); - expect(result.context).toBe('context-labels'); - expect(result.suggestions).toEqual([ - { items: [{ label: 'bar' }], label: 'Labels', searchFunctionType: SearchFunctionType.Fuzzy }, - ]); - }); - - it('returns label value suggestions inside a label value context after a negated matching operator', async () => { - const instance = new LanguageProvider({ - ...defaultDatasource, - metadataRequest: () => { - return { data: { data: ['value1', 'value2'] } }; - }, - } as unknown as PrometheusDatasource); - const value = Plain.deserialize('{job!=}'); - const ed = new SlateEditor({ value }); - const valueWithSelection = ed.moveForward(6).value; - const result = await instance.provideCompletionItems({ - text: '!=', - prefix: '', - wrapperClasses: ['context-labels'], - labelKey: 'job', - value: valueWithSelection, - }); - expect(result.context).toBe('context-label-values'); - expect(result.suggestions).toEqual([ - { - items: [{ label: 'value1' }, { label: 'value2' }], - label: 'Label values for "job"', - searchFunctionType: SearchFunctionType.Fuzzy, - }, - ]); - }); - - it('returns a refresher on label context and unavailable metric', async () => { - jest.spyOn(console, 'warn').mockImplementation(() => {}); - const instance = new LanguageProvider(defaultDatasource); - const value = Plain.deserialize('metric{}'); - const ed = new SlateEditor({ value }); - const valueWithSelection = ed.moveForward(7).value; - const result = await instance.provideCompletionItems({ - text: '', - prefix: '', - wrapperClasses: ['context-labels'], - value: valueWithSelection, - }); - expect(result.context).toBeUndefined(); - expect(result.suggestions).toEqual([]); - expect(console.warn).toHaveBeenCalledWith('Server did not return any values for selector = {__name__="metric"}'); - }); - - it('returns label values on label context when given a metric and a label key', async () => { - const instance = new LanguageProvider({ - ...defaultDatasource, - metadataRequest: () => simpleMetricLabelsResponse, - } as unknown as PrometheusDatasource); - const value = Plain.deserialize('metric{bar=ba}'); - const ed = new SlateEditor({ value }); - const valueWithSelection = ed.moveForward(13).value; - const result = await instance.provideCompletionItems({ - text: '=ba', - prefix: 'ba', - wrapperClasses: ['context-labels'], - labelKey: 'bar', - value: valueWithSelection, - }); - expect(result.context).toBe('context-label-values'); - expect(result.suggestions).toEqual([ - { items: [{ label: 'baz' }], label: 'Label values for "bar"', searchFunctionType: SearchFunctionType.Fuzzy }, - ]); - }); - - it('returns label suggestions on aggregation context and metric w/ selector', async () => { - const instance = new LanguageProvider({ - ...defaultDatasource, - metadataRequest: () => simpleMetricLabelsResponse, - } as unknown as PrometheusDatasource); - const value = Plain.deserialize('sum(metric{foo="xx"}) by ()'); - const ed = new SlateEditor({ value }); - const valueWithSelection = ed.moveForward(26).value; - const result = await instance.provideCompletionItems({ - text: '', - prefix: '', - wrapperClasses: ['context-aggregation'], - value: valueWithSelection, - }); - expect(result.context).toBe('context-aggregation'); - expect(result.suggestions).toEqual([ - { items: [{ label: 'bar' }], label: 'Labels', searchFunctionType: SearchFunctionType.Fuzzy }, - ]); - }); - - it('returns label suggestions on aggregation context and metric w/o selector', async () => { - const instance = new LanguageProvider({ - ...defaultDatasource, - metadataRequest: () => simpleMetricLabelsResponse, - } as unknown as PrometheusDatasource); - const value = Plain.deserialize('sum(metric) by ()'); - const ed = new SlateEditor({ value }); - const valueWithSelection = ed.moveForward(16).value; - const result = await instance.provideCompletionItems({ - text: '', - prefix: '', - wrapperClasses: ['context-aggregation'], - value: valueWithSelection, - }); - expect(result.context).toBe('context-aggregation'); - expect(result.suggestions).toEqual([ - { items: [{ label: 'bar' }], label: 'Labels', searchFunctionType: SearchFunctionType.Fuzzy }, - ]); - }); - - it('returns label suggestions inside a multi-line aggregation context', async () => { - const instance = new LanguageProvider({ - ...defaultDatasource, - metadataRequest: () => simpleMetricLabelsResponse, - } as unknown as PrometheusDatasource); - const value = Plain.deserialize('sum(\nmetric\n)\nby ()'); - const aggregationTextBlock = value.document.getBlocks().get(3); - const ed = new SlateEditor({ value }); - ed.moveToStartOfNode(aggregationTextBlock); - const valueWithSelection = ed.moveForward(4).value; - const result = await instance.provideCompletionItems({ - text: '', - prefix: '', - wrapperClasses: ['context-aggregation'], - value: valueWithSelection, - }); - expect(result.context).toBe('context-aggregation'); - expect(result.suggestions).toEqual([ - { - items: [{ label: 'bar' }], - label: 'Labels', - searchFunctionType: SearchFunctionType.Fuzzy, - }, - ]); - }); - - it('returns label suggestions inside an aggregation context with a range vector', async () => { - const instance = new LanguageProvider({ - ...defaultDatasource, - metadataRequest: () => simpleMetricLabelsResponse, - } as unknown as PrometheusDatasource); - const value = Plain.deserialize('sum(rate(metric[1h])) by ()'); - const ed = new SlateEditor({ value }); - const valueWithSelection = ed.moveForward(26).value; - const result = await instance.provideCompletionItems({ - text: '', - prefix: '', - wrapperClasses: ['context-aggregation'], - value: valueWithSelection, - }); - expect(result.context).toBe('context-aggregation'); - expect(result.suggestions).toEqual([ - { - items: [{ label: 'bar' }], - label: 'Labels', - searchFunctionType: SearchFunctionType.Fuzzy, - }, - ]); - }); - - it('returns label suggestions inside an aggregation context with a range vector and label', async () => { - const instance = new LanguageProvider({ - ...defaultDatasource, - metadataRequest: () => simpleMetricLabelsResponse, - } as unknown as PrometheusDatasource); - const value = Plain.deserialize('sum(rate(metric{label1="value"}[1h])) by ()'); - const ed = new SlateEditor({ value }); - const valueWithSelection = ed.moveForward(42).value; - const result = await instance.provideCompletionItems({ - text: '', - prefix: '', - wrapperClasses: ['context-aggregation'], - value: valueWithSelection, - }); - expect(result.context).toBe('context-aggregation'); - expect(result.suggestions).toEqual([ - { - items: [{ label: 'bar' }], - label: 'Labels', - searchFunctionType: SearchFunctionType.Fuzzy, - }, - ]); - }); - - it('returns no suggestions inside an unclear aggregation context using alternate syntax', async () => { - const instance = new LanguageProvider(defaultDatasource); - const value = Plain.deserialize('sum by ()'); - const ed = new SlateEditor({ value }); - const valueWithSelection = ed.moveForward(8).value; - const result = await instance.provideCompletionItems({ - text: '', - prefix: '', - wrapperClasses: ['context-aggregation'], - value: valueWithSelection, - }); - expect(result.context).toBe('context-aggregation'); - expect(result.suggestions).toEqual([]); - }); - - it('returns label suggestions inside an aggregation context using alternate syntax', async () => { - const instance = new LanguageProvider({ - ...defaultDatasource, - metadataRequest: () => simpleMetricLabelsResponse, - } as unknown as PrometheusDatasource); - const value = Plain.deserialize('sum by () (metric)'); - const ed = new SlateEditor({ value }); - const valueWithSelection = ed.moveForward(8).value; - const result = await instance.provideCompletionItems({ - text: '', - prefix: '', - wrapperClasses: ['context-aggregation'], - value: valueWithSelection, - }); - expect(result.context).toBe('context-aggregation'); - expect(result.suggestions).toEqual([ - { - items: [{ label: 'bar' }], - label: 'Labels', - searchFunctionType: SearchFunctionType.Fuzzy, - }, - ]); - }); - - it('does not re-fetch default labels', async () => { - const testDatasource: PrometheusDatasource = { - ...defaultDatasource, - metadataRequest: jest.fn(() => ({ data: { data: [] } })), - interpolateString: (string: string) => string, - getQuantizedTimeRangeParams: getMockQuantizedTimeRangeParams, - } as unknown as PrometheusDatasource; - - const mockedMetadataRequest = jest.mocked(testDatasource.metadataRequest); - - const instance = new LanguageProvider(testDatasource); - const value = Plain.deserialize('{}'); - const ed = new SlateEditor({ value }); - const valueWithSelection = ed.moveForward(1).value; - const args = { - text: '', - prefix: '', - wrapperClasses: ['context-labels'], - value: valueWithSelection, - }; - const promise1 = instance.provideCompletionItems(args); - // one call for 2 default labels job, instance - expect(mockedMetadataRequest.mock.calls.length).toBe(2); - const promise2 = instance.provideCompletionItems(args); - expect(mockedMetadataRequest.mock.calls.length).toBe(2); - await Promise.all([promise1, promise2]); - expect(mockedMetadataRequest.mock.calls.length).toBe(2); - }); - }); describe('disabled metrics lookup', () => { - it('does not issue any metadata requests when lookup is disabled', async () => { - jest.spyOn(console, 'warn').mockImplementation(() => {}); - const datasource: PrometheusDatasource = { - metadataRequest: jest.fn(() => ({ data: { data: ['foo', 'bar'] as string[] } })), - lookupsDisabled: true, - } as unknown as PrometheusDatasource; - const mockedMetadataRequest = jest.mocked(datasource.metadataRequest); - const instance = new LanguageProvider(datasource); - const value = Plain.deserialize('{}'); - const ed = new SlateEditor({ value }); - const valueWithSelection = ed.moveForward(1).value; - const args = { - text: '', - prefix: '', - wrapperClasses: ['context-labels'], - value: valueWithSelection, - }; - - expect(mockedMetadataRequest.mock.calls.length).toBe(0); - await instance.start(); - expect(mockedMetadataRequest.mock.calls.length).toBe(0); - await instance.provideCompletionItems(args); - expect(mockedMetadataRequest.mock.calls.length).toBe(0); - expect(console.warn).toHaveBeenCalledWith('Server did not return any values for selector = {}'); - }); it('issues metadata requests when lookup is not disabled', async () => { const datasource: PrometheusDatasource = { ...defaultDatasource, @@ -904,14 +391,3 @@ describe('Language completion provider', () => { }); }); }); - -const simpleMetricLabelsResponse = { - data: { - data: [ - { - __name__: 'metric', - bar: 'baz', - }, - ], - }, -}; diff --git a/public/app/plugins/datasource/prometheus/language_provider.ts b/public/app/plugins/datasource/prometheus/language_provider.ts index a1c3b86c9bd..6bf27153180 100644 --- a/public/app/plugins/datasource/prometheus/language_provider.ts +++ b/public/app/plugins/datasource/prometheus/language_provider.ts @@ -1,48 +1,33 @@ -import { chain, difference, once } from 'lodash'; +import { once } from 'lodash'; import Prism from 'prismjs'; -import { Value } from 'slate'; import { AbstractLabelMatcher, AbstractLabelOperator, AbstractQuery, - dateTime, getDefaultTimeRange, - HistoryItem, LanguageProvider, TimeRange, } from '@grafana/data'; import { BackendSrvRequest } from '@grafana/runtime'; -import { CompletionItem, CompletionItemGroup, SearchFunctionType, TypeaheadInput, TypeaheadOutput } from '@grafana/ui'; import { Label } from './components/monaco-query-field/monaco-completion-provider/situation'; import { PrometheusDatasource } from './datasource'; import { - addLimitInfo, extractLabelMatchers, fixSummariesMetadata, - parseSelector, processHistogramMetrics, processLabels, toPromLikeQuery, } from './language_utils'; -import PromqlSyntax, { FUNCTIONS, RATE_RANGES } from './promql'; +import PromqlSyntax from './promql'; import { PrometheusCacheLevel, PromMetricsMetadata, PromQuery } from './types'; const DEFAULT_KEYS = ['job', 'instance']; const EMPTY_SELECTOR = '{}'; -const HISTORY_ITEM_COUNT = 5; -const HISTORY_COUNT_CUTOFF = 1000 * 60 * 60 * 24; // 24h // Max number of items (metrics, labels, values) that we display as suggestions. Prevents from running out of memory. export const SUGGESTIONS_LIMIT = 10000; -const wrapLabel = (label: string): CompletionItem => ({ label }); - -const setFunctionKind = (suggestion: CompletionItem): CompletionItem => { - suggestion.kind = 'function'; - return suggestion; -}; - const buildCacheHeaders = (durationInSeconds: number) => { return { headers: { @@ -51,32 +36,6 @@ const buildCacheHeaders = (durationInSeconds: number) => { }; }; -export function addHistoryMetadata(item: CompletionItem, history: any[]): CompletionItem { - const cutoffTs = Date.now() - HISTORY_COUNT_CUTOFF; - const historyForItem = history.filter((h) => h.ts > cutoffTs && h.query === item.label); - const count = historyForItem.length; - const recent = historyForItem[0]; - let hint = `Queried ${count} times in the last 24h.`; - - if (recent) { - const lastQueried = dateTime(recent.ts).fromNow(); - hint = `${hint} Last queried ${lastQueried}.`; - } - - return { - ...item, - documentation: hint, - }; -} - -function addMetricsMetadata(metric: string, metadata?: PromMetricsMetadata): CompletionItem { - const item: CompletionItem = { label: metric }; - if (metadata && metadata[metric]) { - item.documentation = getMetadataString(metric, metadata); - } - return item; -} - export function getMetadataString(metric: string, metadata: PromMetricsMetadata): string | undefined { if (!metadata[metric]) { return undefined; @@ -102,10 +61,6 @@ export function getMetadataType(metric: string, metadata: PromMetricsMetadata): const PREFIX_DELIMITER_REGEX = /(="|!="|=~"|!~"|\{|\[|\(|\+|-|\/|\*|%|\^|\band\b|\bor\b|\bunless\b|==|>=|!=|<=|>|<|=|~|,)/; -interface AutocompleteContext { - history?: Array>; -} - const secondsInDay = 86400; export default class PromQlLanguageProvider extends LanguageProvider { histogramMetrics: string[]; @@ -188,279 +143,6 @@ export default class PromQlLanguageProvider extends LanguageProvider { return this.labelKeys; } - provideCompletionItems = async ( - { prefix, text, value, labelKey, wrapperClasses }: TypeaheadInput, - context: AutocompleteContext = {} - ): Promise => { - const emptyResult: TypeaheadOutput = { suggestions: [] }; - - if (!value) { - return emptyResult; - } - - // Local text properties - const empty = value.document.text.length === 0; - const selectedLines = value.document.getTextsAtRange(value.selection); - const currentLine = selectedLines.size === 1 ? selectedLines.first().getText() : null; - - const nextCharacter = currentLine ? currentLine[value.selection.anchor.offset] : null; - - // Syntax spans have 3 classes by default. More indicate a recognized token - const tokenRecognized = wrapperClasses.length > 3; - // Non-empty prefix, but not inside known token - const prefixUnrecognized = prefix && !tokenRecognized; - - // Prevent suggestions in `function(|suffix)` - const noSuffix = !nextCharacter || nextCharacter === ')'; - - // Prefix is safe if it does not immediately follow a complete expression and has no text after it - const safePrefix = prefix && !text.match(/^[\]})\s]+$/) && noSuffix; - - // About to type next operand if preceded by binary operator - const operatorsPattern = /[+\-*/^%]/; - const isNextOperand = text.match(operatorsPattern); - - // Determine candidates by CSS context - if (wrapperClasses.includes('context-range')) { - // Suggestions for metric[|] - return this.getRangeCompletionItems(); - } else if (wrapperClasses.includes('context-labels')) { - // Suggestions for metric{|} and metric{foo=|}, as well as metric-independent label queries like {|} - return this.getLabelCompletionItems({ prefix, text, value, labelKey, wrapperClasses }); - } else if (wrapperClasses.includes('context-aggregation')) { - // Suggestions for sum(metric) by (|) - return this.getAggregationCompletionItems(value); - } else if (empty) { - // Suggestions for empty query field - return this.getEmptyCompletionItems(context); - } else if (prefixUnrecognized && noSuffix && !isNextOperand) { - // Show term suggestions in a couple of scenarios - return this.getBeginningCompletionItems(context); - } else if (prefixUnrecognized && safePrefix) { - // Show term suggestions in a couple of scenarios - return this.getTermCompletionItems(); - } - - return emptyResult; - }; - - getBeginningCompletionItems = (context: AutocompleteContext): TypeaheadOutput => { - return { - suggestions: [...this.getEmptyCompletionItems(context).suggestions, ...this.getTermCompletionItems().suggestions], - }; - }; - - getEmptyCompletionItems = (context: AutocompleteContext): TypeaheadOutput => { - const { history } = context; - const suggestions: CompletionItemGroup[] = []; - - if (history && history.length) { - const historyItems = chain(history) - .map((h) => h.query.expr) - .filter() - .uniq() - .take(HISTORY_ITEM_COUNT) - .map(wrapLabel) - .map((item) => addHistoryMetadata(item, history)) - .value(); - - suggestions.push({ - searchFunctionType: SearchFunctionType.Prefix, - skipSort: true, - label: 'History', - items: historyItems, - }); - } - - return { suggestions }; - }; - - getTermCompletionItems = (): TypeaheadOutput => { - const { metrics, metricsMetadata } = this; - const suggestions: CompletionItemGroup[] = []; - - suggestions.push({ - searchFunctionType: SearchFunctionType.Prefix, - label: 'Functions', - items: FUNCTIONS.map(setFunctionKind), - }); - - if (metrics && metrics.length) { - suggestions.push({ - label: 'Metrics', - items: metrics.map((m) => addMetricsMetadata(m, metricsMetadata)), - searchFunctionType: SearchFunctionType.Fuzzy, - }); - } - - return { suggestions }; - }; - - getRangeCompletionItems(): TypeaheadOutput { - return { - context: 'context-range', - suggestions: [ - { - label: 'Range vector', - items: [...RATE_RANGES], - }, - ], - }; - } - - getAggregationCompletionItems = async (value: Value): Promise => { - const suggestions: CompletionItemGroup[] = []; - - // Stitch all query lines together to support multi-line queries - let queryOffset; - const queryText = value.document.getBlocks().reduce((text, block) => { - if (text === undefined) { - return ''; - } - if (!block) { - return text; - } - - const blockText = block?.getText(); - - if (value.anchorBlock.key === block.key) { - // Newline characters are not accounted for but this is irrelevant - // for the purpose of extracting the selector string - queryOffset = value.selection.anchor.offset + text.length; - } - - return text + blockText; - }, ''); - - // Try search for selector part on the left-hand side, such as `sum (m) by (l)` - const openParensAggregationIndex = queryText.lastIndexOf('(', queryOffset); - let openParensSelectorIndex = queryText.lastIndexOf('(', openParensAggregationIndex - 1); - let closeParensSelectorIndex = queryText.indexOf(')', openParensSelectorIndex); - - // Try search for selector part of an alternate aggregation clause, such as `sum by (l) (m)` - if (openParensSelectorIndex === -1) { - const closeParensAggregationIndex = queryText.indexOf(')', queryOffset); - closeParensSelectorIndex = queryText.indexOf(')', closeParensAggregationIndex + 1); - openParensSelectorIndex = queryText.lastIndexOf('(', closeParensSelectorIndex); - } - - const result = { - suggestions, - context: 'context-aggregation', - }; - - // Suggestions are useless for alternative aggregation clauses without a selector in context - if (openParensSelectorIndex === -1) { - return result; - } - - // Range vector syntax not accounted for by subsequent parse so discard it if present - const selectorString = queryText - .slice(openParensSelectorIndex + 1, closeParensSelectorIndex) - .replace(/\[[^\]]+\]$/, ''); - - const selector = parseSelector(selectorString, selectorString.length - 2).selector; - - const series = await this.getSeries(selector); - const labelKeys = Object.keys(series); - if (labelKeys.length > 0) { - const limitInfo = addLimitInfo(labelKeys); - suggestions.push({ - label: `Labels${limitInfo}`, - items: labelKeys.map(wrapLabel), - searchFunctionType: SearchFunctionType.Fuzzy, - }); - } - return result; - }; - - getLabelCompletionItems = async ({ - text, - wrapperClasses, - labelKey, - value, - }: TypeaheadInput): Promise => { - if (!value) { - return { suggestions: [] }; - } - - const suggestions: CompletionItemGroup[] = []; - const line = value.anchorBlock.getText(); - const cursorOffset = value.selection.anchor.offset; - const suffix = line.substr(cursorOffset); - const prefix = line.substr(0, cursorOffset); - const isValueStart = text.match(/^(=|=~|!=|!~)/); - const isValueEnd = suffix.match(/^"?[,}]|$/); - // Detect cursor in front of value, e.g., {key=|"} - const isPreValue = prefix.match(/(=|=~|!=|!~)$/) && suffix.match(/^"/); - - // Don't suggest anything at the beginning or inside a value - const isValueEmpty = isValueStart && isValueEnd; - const hasValuePrefix = isValueEnd && !isValueStart; - if ((!isValueEmpty && !hasValuePrefix) || isPreValue) { - return { suggestions }; - } - - // Get normalized selector - let selector; - let parsedSelector; - try { - parsedSelector = parseSelector(line, cursorOffset); - selector = parsedSelector.selector; - } catch { - selector = EMPTY_SELECTOR; - } - - const containsMetric = selector.includes('__name__='); - const existingKeys = parsedSelector ? parsedSelector.labelKeys : []; - - let series: Record = {}; - // Query labels for selector - if (selector) { - series = await this.getSeries(selector, !containsMetric); - } - - if (Object.keys(series).length === 0) { - console.warn(`Server did not return any values for selector = ${selector}`); - return { suggestions }; - } - - let context: string | undefined; - - if ((text && isValueStart) || wrapperClasses.includes('attr-value')) { - // Label values - if (labelKey && series[labelKey]) { - context = 'context-label-values'; - const limitInfo = addLimitInfo(series[labelKey]); - suggestions.push({ - label: `Label values for "${labelKey}"${limitInfo}`, - items: series[labelKey].map(wrapLabel), - searchFunctionType: SearchFunctionType.Fuzzy, - }); - } - } else { - // Label keys - const labelKeys = series ? Object.keys(series) : containsMetric ? null : DEFAULT_KEYS; - - if (labelKeys) { - const possibleKeys = difference(labelKeys, existingKeys); - if (possibleKeys.length) { - context = 'context-labels'; - const newItems = possibleKeys.map((key) => ({ label: key })); - const limitInfo = addLimitInfo(newItems); - const newSuggestion: CompletionItemGroup = { - label: `Labels${limitInfo}`, - items: newItems, - searchFunctionType: SearchFunctionType.Fuzzy, - }; - suggestions.push(newSuggestion); - } - } - } - - return { context, suggestions }; - }; - importFromAbstractQuery(labelBasedQuery: AbstractQuery): PromQuery { return toPromLikeQuery(labelBasedQuery); } diff --git a/public/app/plugins/datasource/prometheus/metric_find_query.test.ts b/public/app/plugins/datasource/prometheus/metric_find_query.test.ts index d5890c31148..1a9d8e9179f 100644 --- a/public/app/plugins/datasource/prometheus/metric_find_query.test.ts +++ b/public/app/plugins/datasource/prometheus/metric_find_query.test.ts @@ -2,9 +2,8 @@ import 'whatwg-fetch'; // fetch polyfill needed backendSrv import { of } from 'rxjs'; import { DataSourceInstanceSettings, TimeRange, toUtc } from '@grafana/data'; -import { FetchResponse } from '@grafana/runtime'; +import { FetchResponse, TemplateSrv } from '@grafana/runtime'; import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__ -import { TemplateSrv } from 'app/features/templating/template_srv'; import { PrometheusDatasource } from './datasource'; import PrometheusMetricFindQuery from './metric_find_query'; diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.test.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.test.tsx index b93a56c4d09..8b55a9efaac 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.test.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.test.tsx @@ -11,6 +11,7 @@ import { QueryHint, TimeRange, } from '@grafana/data'; +import { TemplateSrv } from '@grafana/runtime'; import { PrometheusDatasource } from '../../datasource'; import PromQlLanguageProvider from '../../language_provider'; @@ -317,7 +318,7 @@ function createDatasource(options?: Partial, - undefined, + mockTemplateSrv(), languageProvider ); return { datasource, languageProvider }; @@ -355,3 +356,9 @@ async function openLabelNameSelect(index = 0) { const { name } = getLabelSelects(index); await userEvent.click(name); } + +function mockTemplateSrv(): TemplateSrv { + return { + getVariables: () => [], + } as unknown as TemplateSrv; +}