diff --git a/.betterer.results b/.betterer.results index bd5f5151634..d69333d6934 100644 --- a/.betterer.results +++ b/.betterer.results @@ -4587,8 +4587,7 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "3"] ], "public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"] + [0, 0, 0, "Do not use any type assertions.", "0"] ], "public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderContainer.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] diff --git a/public/app/plugins/datasource/prometheus/components/VariableQueryEditor.test.tsx b/public/app/plugins/datasource/prometheus/components/VariableQueryEditor.test.tsx index 28325ff4d7b..b68df23183f 100644 --- a/public/app/plugins/datasource/prometheus/components/VariableQueryEditor.test.tsx +++ b/public/app/plugins/datasource/prometheus/components/VariableQueryEditor.test.tsx @@ -68,8 +68,15 @@ describe('PromVariableQueryEditor', () => { syntax: () => {}, getLabelKeys: () => [], metrics: [], + metricsMetadata: {}, + getLabelValues: jest.fn().mockImplementation(() => ['that']), + fetchSeriesLabelsMatch: jest.fn().mockImplementation(() => Promise.resolve({ those: 'those' })), }, getInitHints: () => [], + getDebounceTimeInMilliseconds: jest.fn(), + getTagKeys: jest.fn().mockImplementation(() => Promise.resolve(['this'])), + getVariables: jest.fn().mockImplementation(() => []), + metricFindQuery: jest.fn().mockImplementation(() => Promise.resolve(['that'])), } as unknown as PrometheusDatasource, query: { refId: 'test', @@ -108,6 +115,7 @@ describe('PromVariableQueryEditor', () => { expect(onChange).toHaveBeenCalledWith({ query: 'label_names()', + labelFilters: [], refId, }); }); @@ -117,6 +125,7 @@ describe('PromVariableQueryEditor', () => { render(); + await selectOptionInTest(screen.getByLabelText('Query type'), 'Label values'); await selectOptionInTest(screen.getByLabelText('Query type'), 'Metrics'); await selectOptionInTest(screen.getByLabelText('Query type'), 'Query result'); await selectOptionInTest(screen.getByLabelText('Query type'), 'Series query'); @@ -124,25 +133,79 @@ describe('PromVariableQueryEditor', () => { expect(onChange).not.toHaveBeenCalled(); }); - test('Calls onChange for metrics() with argument onBlur', async () => { + test('Calls onChange for metrics() after input', async () => { const onChange = jest.fn(); props.query = { refId: 'test', - query: 'metrics(a)', + query: 'label_names()', }; render(); - const labelSelect = screen.getByLabelText('Metric selector'); - await userEvent.click(labelSelect); - const functionSelect = screen.getByLabelText('Query type').parentElement!; - await userEvent.click(functionSelect); + await selectOptionInTest(screen.getByLabelText('Query type'), 'Metrics'); + const metricInput = screen.getByLabelText('Metric selector'); + await userEvent.type(metricInput, 'a'); - expect(onChange).toHaveBeenCalledWith({ - query: 'metrics(a)', - refId, - }); + waitFor(() => + expect(onChange).toHaveBeenCalledWith({ + query: 'metrics(a)', + labelFilters: [], + refId, + }) + ); + }); + + test('Calls onChange for label_values() after selecting label', async () => { + const onChange = jest.fn(); + + props.query = { + refId: 'test', + query: 'label_names()', + }; + + render(); + + await selectOptionInTest(screen.getByLabelText('Query type'), 'Label values'); + const labelSelect = screen.getByLabelText('label-select'); + await userEvent.type(labelSelect, 'this'); + await selectOptionInTest(labelSelect, 'this'); + + waitFor(() => + expect(onChange).toHaveBeenCalledWith({ + query: 'label_values(this)', + labelFilters: [], + refId, + }) + ); + }); + + test('Calls onChange for label_values() after selecting metric', async () => { + const onChange = jest.fn(); + + props.query = { + refId: 'test', + query: 'label_names()', + }; + + render(); + + await selectOptionInTest(screen.getByLabelText('Query type'), 'Label values'); + const labelSelect = screen.getByLabelText('label-select'); + await userEvent.type(labelSelect, 'this'); + await selectOptionInTest(labelSelect, 'this'); + + const metricSelect = screen.getByLabelText('Metric'); + await userEvent.type(metricSelect, 'that'); + await selectOptionInTest(metricSelect, 'that'); + + waitFor(() => + expect(onChange).toHaveBeenCalledWith({ + query: 'label_values(that,this)', + labelFilters: [], + refId, + }) + ); }); test('Calls onChange for query_result() with argument onBlur', async () => { @@ -162,6 +225,7 @@ describe('PromVariableQueryEditor', () => { expect(onChange).toHaveBeenCalledWith({ query: 'query_result(a)', + labelFilters: [], refId, }); }); @@ -183,6 +247,7 @@ describe('PromVariableQueryEditor', () => { expect(onChange).toHaveBeenCalledWith({ query: '{a: "example"}', + labelFilters: [], refId, }); }); diff --git a/public/app/plugins/datasource/prometheus/components/VariableQueryEditor.tsx b/public/app/plugins/datasource/prometheus/components/VariableQueryEditor.tsx index 167b1f9608e..a576155c412 100644 --- a/public/app/plugins/datasource/prometheus/components/VariableQueryEditor.tsx +++ b/public/app/plugins/datasource/prometheus/components/VariableQueryEditor.tsx @@ -1,4 +1,5 @@ -import React, { FormEvent, useEffect, useState } from 'react'; +import { debounce } from 'lodash'; +import React, { FormEvent, useCallback, useEffect, useState } from 'react'; import { QueryEditorProps, SelectableValue } from '@grafana/data'; import { InlineField, InlineFieldRow, Input, Select, TextArea } from '@grafana/ui'; @@ -8,6 +9,10 @@ import { migrateVariableEditorBackToVariableSupport, migrateVariableQueryToEditor, } from '../migrations/variableMigration'; +import { promQueryModeller } from '../querybuilder/PromQueryModeller'; +import { MetricsLabelsSection } from '../querybuilder/components/MetricsLabelsSection'; +import { QueryBuilderLabelFilter } from '../querybuilder/shared/types'; +import { PromVisualQuery } from '../querybuilder/types'; import { PromOptions, PromQuery, @@ -46,6 +51,9 @@ export const PromVariableQueryEditor = ({ onChange, query, datasource }: Props) // list of label names for label_values(), /api/v1/labels, contains the same results as label_names() function const [labelOptions, setLabelOptions] = useState>>([]); + // label filters have been added as a filter for metrics in label values query type + const [labelFilters, setLabelFilters] = useState([]); + useEffect(() => { if (!query) { return; @@ -57,13 +65,9 @@ export const PromVariableQueryEditor = ({ onChange, query, datasource }: Props) setQryType(variableQuery.qryType); setLabel(variableQuery.label ?? ''); setMetric(variableQuery.metric ?? ''); + setLabelFilters(query.labelFilters ?? []); setVarQuery(variableQuery.varQuery ?? ''); setSeriesQuery(variableQuery.seriesQuery ?? ''); - - // set the migrated label in the label options - if (variableQuery.label) { - setLabelOptions([{ label: variableQuery.label, value: variableQuery.label }]); - } }, [query]); // set the label names options for the label values var query @@ -71,15 +75,40 @@ export const PromVariableQueryEditor = ({ onChange, query, datasource }: Props) if (qryType !== QueryType.LabelValues) { return; } + const variables = datasource.getVariables().map((variable: string) => ({ label: variable, value: variable })); + if (!metric) { + // get all the labels + datasource.getTagKeys().then((labelNames: Array<{ text: string }>) => { + const names = labelNames.map(({ text }) => ({ label: text, value: text })); + setLabelOptions([...variables, ...names]); + }); + } else { + // fetch the labels filtered by the metric + const labelToConsider = [{ label: '__name__', op: '=', value: metric }]; + const expr = promQueryModeller.renderLabels(labelToConsider); - datasource.getTagKeys().then((labelNames: Array<{ text: string }>) => { - setLabelOptions(labelNames.map(({ text }) => ({ label: text, value: text }))); - }); - }, [datasource, qryType]); + if (datasource.hasLabelsMatchAPISupport()) { + datasource.languageProvider.fetchSeriesLabelsMatch(expr).then((labelsIndex: Record) => { + const labelNames = Object.keys(labelsIndex); + const names = labelNames.map((value) => ({ label: value, value: value })); + setLabelOptions([...variables, ...names]); + }); + } else { + datasource.languageProvider.fetchSeriesLabels(expr).then((labelsIndex: Record) => { + const labelNames = Object.keys(labelsIndex); + const names = labelNames.map((value) => ({ label: value, value: value })); + setLabelOptions([...variables, ...names]); + }); + } + } + }, [datasource, qryType, metric]); - const onChangeWithVariableString = (qryType: QueryType) => { + const onChangeWithVariableString = ( + updateVar: { [key: string]: QueryType | string }, + updLabelFilters?: QueryBuilderLabelFilter[] + ) => { const queryVar = { - qryType: qryType, + qryType, label, metric, varQuery, @@ -87,113 +116,146 @@ export const PromVariableQueryEditor = ({ onChange, query, datasource }: Props) refId: 'PrometheusVariableQueryEditor-VariableQuery', }; - const queryString = migrateVariableEditorBackToVariableSupport(queryVar); + const updatedVar = { ...queryVar, ...updateVar }; + const queryString = migrateVariableEditorBackToVariableSupport(updatedVar); + + const lblFltrs = updLabelFilters ? updLabelFilters : labelFilters; + + // setting query.query property allows for update of variable definition onChange({ query: queryString, + labelFilters: lblFltrs, refId, }); }; + /** Call onchange for label names query type change */ const onQueryTypeChange = (newType: SelectableValue) => { setQryType(newType.value); if (newType.value === QueryType.LabelNames) { - onChangeWithVariableString(newType.value); + onChangeWithVariableString({ qryType: newType.value }); } }; + /** Call onchange for label select when query type is label values */ const onLabelChange = (newLabel: SelectableValue) => { - setLabel(newLabel.value ?? ''); + const newLabelvalue = newLabel && newLabel.value ? newLabel.value : ''; + setLabel(newLabelvalue); + if (qryType === QueryType.LabelValues && newLabelvalue) { + onChangeWithVariableString({ label: newLabelvalue }); + } }; - const onMetricChange = (e: FormEvent) => { - setMetric(e.currentTarget.value); + /** + * Call onChange for MetricsLabels component change for label values query type + * if there is a label (required) and + * if the labels or metric are updated. + */ + const metricsLabelsChange = (update: PromVisualQuery) => { + setMetric(update.metric); + setLabelFilters(update.labels); + + const updMetric = update.metric; + const updLabelFilters = update.labels ?? []; + + if (qryType === QueryType.LabelValues && label && (updMetric || updLabelFilters)) { + onChangeWithVariableString({ qryType, metric: updMetric }, updLabelFilters); + } }; + /** + * Call onchange for metric change if metrics names (regex) query type + * Debounce this because to not call the API for every keystroke. + */ + const onMetricChange = debounce((value: string) => { + if (qryType === QueryType.MetricNames && value) { + onChangeWithVariableString({ metric: value }); + } + }, 300); + + /** + * Do not call onchange for variable query result when query type is var query result + * because the query may not be finished typing and an error is returned + * for incorrectly formatted series. Call onchange for blur instead. + */ const onVarQueryChange = (e: FormEvent) => { setVarQuery(e.currentTarget.value); }; + /** + * Do not call onchange for seriesQuery when query type is series query + * because the series may not be finished typing and an error is returned + * for incorrectly formatted series. Call onchange for blur instead. + */ const onSeriesQueryChange = (e: FormEvent) => { setSeriesQuery(e.currentTarget.value); }; - const handleBlur = () => { - if (qryType === QueryType.LabelNames) { - onChangeWithVariableString(qryType); - } else if (qryType === QueryType.LabelValues && label) { - onChangeWithVariableString(qryType); - } else if (qryType === QueryType.MetricNames && metric) { - onChangeWithVariableString(qryType); - } else if (qryType === QueryType.VarQueryResult && varQuery) { - onChangeWithVariableString(qryType); - } else if (qryType === QueryType.SeriesQuery && seriesQuery) { - onChangeWithVariableString(qryType); - } - }; + const promVisualQuery = useCallback(() => { + return { metric: metric, labels: labelFilters, operations: [] }; + }, [metric, labelFilters]); return ( - - The Prometheus data source plugin provides the following query types for template variables. - } - > - + + + {qryType === QueryType.LabelValues && ( <> - - Returns a list of label values for the label name in all metrics unless the metric is specified. - - } - > - - + + + Returns a list of label values for the label name in all metrics unless the metric is specified. + + } + > +