mirror of https://github.com/grafana/grafana
Prometheus Datasource: Improve Prom query variable editor (#58292)
* add prom query var editor with tests and migrations * fix migration, now query not expr * fix label_values migration * remove comments * fix label_values() variables order * update UI and use more clear language * fix tests * use null coalescing operators * allow users to query label values with label and metric if they have not set there flavor and version * use enums instead of numbers for readability * fix label&metrics switch * update type in qv editor * reuse datasource function to get all label names, getLabelNames(), prev named getTagKeys() * use getLabelNames in the query var editor * make label_values() variables label and metric more readable in the migration * fix tooltip for label_values to remove API reference * clean up tooltips and allow newlines in query_result function * change function wording and exprType to query type/qryType for readability * update prometheus query variable docs * Update public/app/plugins/datasource/prometheus/components/VariableQueryEditor.tsx Co-authored-by: Galen Kistler <109082771+gtk-grafana@users.noreply.github.com> --------- Co-authored-by: Galen Kistler <109082771+gtk-grafana@users.noreply.github.com>pull/63236/head
parent
1f984409a2
commit
eedcd7d5b1
@ -0,0 +1,142 @@ |
|||||||
|
import { render, screen, waitFor } from '@testing-library/react'; |
||||||
|
import userEvent from '@testing-library/user-event'; |
||||||
|
import React from 'react'; |
||||||
|
import { selectOptionInTest } from 'test/helpers/selectOptionInTest'; |
||||||
|
|
||||||
|
import { PrometheusDatasource } from '../datasource'; |
||||||
|
|
||||||
|
import { PromVariableQueryEditor, Props } from './VariableQueryEditor'; |
||||||
|
|
||||||
|
const refId = 'PrometheusVariableQueryEditor-VariableQuery'; |
||||||
|
|
||||||
|
describe('PromVariableQueryEditor', () => { |
||||||
|
let props: Props; |
||||||
|
|
||||||
|
beforeEach(() => { |
||||||
|
props = { |
||||||
|
datasource: { |
||||||
|
hasLabelsMatchAPISupport: () => 1, |
||||||
|
languageProvider: { |
||||||
|
start: () => Promise.resolve([]), |
||||||
|
syntax: () => {}, |
||||||
|
getLabelKeys: () => [], |
||||||
|
metrics: [], |
||||||
|
}, |
||||||
|
getInitHints: () => [], |
||||||
|
} as unknown as PrometheusDatasource, |
||||||
|
query: { |
||||||
|
refId: 'test', |
||||||
|
query: 'label_names()', |
||||||
|
}, |
||||||
|
onRunQuery: () => {}, |
||||||
|
onChange: () => {}, |
||||||
|
history: [], |
||||||
|
}; |
||||||
|
}); |
||||||
|
|
||||||
|
test('Displays a group of function options', async () => { |
||||||
|
render(<PromVariableQueryEditor {...props} />); |
||||||
|
|
||||||
|
const select = screen.getByLabelText('Query type').parentElement!; |
||||||
|
await userEvent.click(select); |
||||||
|
|
||||||
|
await waitFor(() => expect(screen.getAllByText('Label names')).toHaveLength(2)); |
||||||
|
await waitFor(() => expect(screen.getByText('Label values')).toBeInTheDocument()); |
||||||
|
await waitFor(() => expect(screen.getByText('Metrics')).toBeInTheDocument()); |
||||||
|
await waitFor(() => expect(screen.getByText('Query result')).toBeInTheDocument()); |
||||||
|
await waitFor(() => expect(screen.getByText('Series query')).toBeInTheDocument()); |
||||||
|
}); |
||||||
|
|
||||||
|
test('Calls onChange for label_names() query', async () => { |
||||||
|
const onChange = jest.fn(); |
||||||
|
|
||||||
|
props.query = { |
||||||
|
refId: 'test', |
||||||
|
query: '', |
||||||
|
}; |
||||||
|
|
||||||
|
render(<PromVariableQueryEditor {...props} onChange={onChange} />); |
||||||
|
|
||||||
|
await selectOptionInTest(screen.getByLabelText('Query type'), 'Label names'); |
||||||
|
|
||||||
|
expect(onChange).toHaveBeenCalledWith({ |
||||||
|
query: 'label_names()', |
||||||
|
refId, |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
test('Does not call onChange for other queries', async () => { |
||||||
|
const onChange = jest.fn(); |
||||||
|
|
||||||
|
render(<PromVariableQueryEditor {...props} onChange={onChange} />); |
||||||
|
|
||||||
|
await selectOptionInTest(screen.getByLabelText('Query type'), 'Metrics'); |
||||||
|
await selectOptionInTest(screen.getByLabelText('Query type'), 'Query result'); |
||||||
|
await selectOptionInTest(screen.getByLabelText('Query type'), 'Series query'); |
||||||
|
|
||||||
|
expect(onChange).not.toHaveBeenCalled(); |
||||||
|
}); |
||||||
|
|
||||||
|
test('Calls onChange for metrics() with argument onBlur', async () => { |
||||||
|
const onChange = jest.fn(); |
||||||
|
|
||||||
|
props.query = { |
||||||
|
refId: 'test', |
||||||
|
query: 'metrics(a)', |
||||||
|
}; |
||||||
|
|
||||||
|
render(<PromVariableQueryEditor {...props} onChange={onChange} />); |
||||||
|
|
||||||
|
const labelSelect = screen.getByLabelText('Metric selector'); |
||||||
|
await userEvent.click(labelSelect); |
||||||
|
const functionSelect = screen.getByLabelText('Query type').parentElement!; |
||||||
|
await userEvent.click(functionSelect); |
||||||
|
|
||||||
|
expect(onChange).toHaveBeenCalledWith({ |
||||||
|
query: 'metrics(a)', |
||||||
|
refId, |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
test('Calls onChange for query_result() with argument onBlur', async () => { |
||||||
|
const onChange = jest.fn(); |
||||||
|
|
||||||
|
props.query = { |
||||||
|
refId: 'test', |
||||||
|
query: 'query_result(a)', |
||||||
|
}; |
||||||
|
|
||||||
|
render(<PromVariableQueryEditor {...props} onChange={onChange} />); |
||||||
|
|
||||||
|
const labelSelect = screen.getByLabelText('Prometheus Query'); |
||||||
|
await userEvent.click(labelSelect); |
||||||
|
const functionSelect = screen.getByLabelText('Query type').parentElement!; |
||||||
|
await userEvent.click(functionSelect); |
||||||
|
|
||||||
|
expect(onChange).toHaveBeenCalledWith({ |
||||||
|
query: 'query_result(a)', |
||||||
|
refId, |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
test('Calls onChange for Match[] series with argument onBlur', async () => { |
||||||
|
const onChange = jest.fn(); |
||||||
|
|
||||||
|
props.query = { |
||||||
|
refId: 'test', |
||||||
|
query: '{a: "example"}', |
||||||
|
}; |
||||||
|
|
||||||
|
render(<PromVariableQueryEditor {...props} onChange={onChange} />); |
||||||
|
|
||||||
|
const labelSelect = screen.getByLabelText('Series Query'); |
||||||
|
await userEvent.click(labelSelect); |
||||||
|
const functionSelect = screen.getByLabelText('Query type').parentElement!; |
||||||
|
await userEvent.click(functionSelect); |
||||||
|
|
||||||
|
expect(onChange).toHaveBeenCalledWith({ |
||||||
|
query: '{a: "example"}', |
||||||
|
refId, |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,257 @@ |
|||||||
|
import React, { FC, FormEvent, useEffect, useState } from 'react'; |
||||||
|
|
||||||
|
import { QueryEditorProps, SelectableValue } from '@grafana/data'; |
||||||
|
import { InlineField, InlineFieldRow, Input, Select, TextArea } from '@grafana/ui'; |
||||||
|
|
||||||
|
import { PrometheusDatasource } from '../datasource'; |
||||||
|
import { |
||||||
|
migrateVariableEditorBackToVariableSupport, |
||||||
|
migrateVariableQueryToEditor, |
||||||
|
} from '../migrations/variableMigration'; |
||||||
|
import { PromOptions, PromQuery, PromVariableQuery, PromVariableQueryType as QueryType } from '../types'; |
||||||
|
|
||||||
|
export const variableOptions = [ |
||||||
|
{ label: 'Label names', value: QueryType.LabelNames }, |
||||||
|
{ label: 'Label values', value: QueryType.LabelValues }, |
||||||
|
{ label: 'Metrics', value: QueryType.MetricNames }, |
||||||
|
{ label: 'Query result', value: QueryType.VarQueryResult }, |
||||||
|
{ label: 'Series query', value: QueryType.SeriesQuery }, |
||||||
|
]; |
||||||
|
|
||||||
|
export type Props = QueryEditorProps<PrometheusDatasource, PromQuery, PromOptions, PromVariableQuery>; |
||||||
|
|
||||||
|
const refId = 'PrometheusVariableQueryEditor-VariableQuery'; |
||||||
|
|
||||||
|
export const PromVariableQueryEditor: FC<Props> = ({ onChange, query, datasource }) => { |
||||||
|
// to select the query type, i.e. label_names, label_values, etc.
|
||||||
|
const [qryType, setQryType] = useState<number | undefined>(undefined); |
||||||
|
|
||||||
|
// list of variables for each function
|
||||||
|
const [label, setLabel] = useState(''); |
||||||
|
// metric is used for both label_values() and metric()
|
||||||
|
// label_values() metric requires a whole/complete metric
|
||||||
|
// metric() is expected to be a part of a metric string
|
||||||
|
const [metric, setMetric] = useState(''); |
||||||
|
// varQuery is a whole query, can include math/rates/etc
|
||||||
|
const [varQuery, setVarQuery] = useState(''); |
||||||
|
// seriesQuery is only a whole
|
||||||
|
const [seriesQuery, setSeriesQuery] = useState(''); |
||||||
|
|
||||||
|
// list of label names for label_values(), /api/v1/labels, contains the same results as label_names() function
|
||||||
|
const [labelOptions, setLabelOptions] = useState<Array<SelectableValue<string>>>([]); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (!query) { |
||||||
|
return; |
||||||
|
} |
||||||
|
// Changing from standard to custom variable editor changes the string attr from expr to query
|
||||||
|
const variableQuery = query.query ? migrateVariableQueryToEditor(query.query) : query; |
||||||
|
|
||||||
|
setQryType(variableQuery.qryType); |
||||||
|
setLabel(variableQuery.label ?? ''); |
||||||
|
setMetric(variableQuery.metric ?? ''); |
||||||
|
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
|
||||||
|
useEffect(() => { |
||||||
|
if (qryType !== QueryType.LabelValues) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
datasource.getLabelNames().then((labelNames: Array<{ text: string }>) => { |
||||||
|
setLabelOptions(labelNames.map(({ text }) => ({ label: text, value: text }))); |
||||||
|
}); |
||||||
|
}, [datasource, qryType]); |
||||||
|
|
||||||
|
const onChangeWithVariableString = (qryType: QueryType) => { |
||||||
|
const queryVar = { |
||||||
|
qryType: qryType, |
||||||
|
label, |
||||||
|
metric, |
||||||
|
varQuery, |
||||||
|
seriesQuery, |
||||||
|
refId: 'PrometheusVariableQueryEditor-VariableQuery', |
||||||
|
}; |
||||||
|
|
||||||
|
const queryString = migrateVariableEditorBackToVariableSupport(queryVar); |
||||||
|
|
||||||
|
onChange({ |
||||||
|
query: queryString, |
||||||
|
refId, |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
const onQueryTypeChange = (newType: SelectableValue<QueryType>) => { |
||||||
|
setQryType(newType.value); |
||||||
|
if (newType.value === QueryType.LabelNames) { |
||||||
|
onChangeWithVariableString(newType.value); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
const onLabelChange = (newLabel: SelectableValue<string>) => { |
||||||
|
setLabel(newLabel.value ?? ''); |
||||||
|
}; |
||||||
|
|
||||||
|
const onMetricChange = (e: FormEvent<HTMLInputElement>) => { |
||||||
|
setMetric(e.currentTarget.value); |
||||||
|
}; |
||||||
|
|
||||||
|
const onVarQueryChange = (e: FormEvent<HTMLTextAreaElement>) => { |
||||||
|
setVarQuery(e.currentTarget.value); |
||||||
|
}; |
||||||
|
|
||||||
|
const onSeriesQueryChange = (e: FormEvent<HTMLInputElement>) => { |
||||||
|
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); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
return ( |
||||||
|
<InlineFieldRow> |
||||||
|
<InlineField |
||||||
|
label="Query Type" |
||||||
|
labelWidth={20} |
||||||
|
tooltip={ |
||||||
|
<div>The Prometheus data source plugin provides the following query types for template variables.</div> |
||||||
|
} |
||||||
|
> |
||||||
|
<Select |
||||||
|
placeholder="Select query type" |
||||||
|
aria-label="Query type" |
||||||
|
onChange={onQueryTypeChange} |
||||||
|
onBlur={handleBlur} |
||||||
|
value={qryType} |
||||||
|
options={variableOptions} |
||||||
|
width={25} |
||||||
|
/> |
||||||
|
</InlineField> |
||||||
|
{qryType === QueryType.LabelValues && ( |
||||||
|
<> |
||||||
|
<InlineField |
||||||
|
label="Label" |
||||||
|
labelWidth={20} |
||||||
|
required |
||||||
|
tooltip={ |
||||||
|
<div> |
||||||
|
Returns a list of label values for the label name in all metrics unless the metric is specified. |
||||||
|
</div> |
||||||
|
} |
||||||
|
> |
||||||
|
<Select |
||||||
|
aria-label="label-select" |
||||||
|
onChange={onLabelChange} |
||||||
|
onBlur={handleBlur} |
||||||
|
value={label} |
||||||
|
options={labelOptions} |
||||||
|
width={25} |
||||||
|
allowCustomValue |
||||||
|
/> |
||||||
|
</InlineField> |
||||||
|
<InlineField |
||||||
|
label="Metric" |
||||||
|
labelWidth={20} |
||||||
|
tooltip={<div>Optional: returns a list of label values for the label name in the specified metric.</div>} |
||||||
|
> |
||||||
|
<Input |
||||||
|
type="text" |
||||||
|
aria-label="Metric selector" |
||||||
|
placeholder="Optional metric selector" |
||||||
|
value={metric} |
||||||
|
onChange={onMetricChange} |
||||||
|
onBlur={handleBlur} |
||||||
|
width={25} |
||||||
|
/> |
||||||
|
</InlineField> |
||||||
|
</> |
||||||
|
)} |
||||||
|
{qryType === QueryType.MetricNames && ( |
||||||
|
<> |
||||||
|
<InlineField |
||||||
|
label="Metric Regex" |
||||||
|
labelWidth={20} |
||||||
|
tooltip={<div>Returns a list of metrics matching the specified metric regex.</div>} |
||||||
|
> |
||||||
|
<Input |
||||||
|
type="text" |
||||||
|
aria-label="Metric selector" |
||||||
|
placeholder="Metric Regex" |
||||||
|
value={metric} |
||||||
|
onChange={onMetricChange} |
||||||
|
onBlur={handleBlur} |
||||||
|
width={25} |
||||||
|
/> |
||||||
|
</InlineField> |
||||||
|
</> |
||||||
|
)} |
||||||
|
{qryType === QueryType.VarQueryResult && ( |
||||||
|
<> |
||||||
|
<InlineField |
||||||
|
label="Query" |
||||||
|
labelWidth={20} |
||||||
|
tooltip={ |
||||||
|
<div> |
||||||
|
Returns a list of Prometheus query results for the query. This can include Prometheus functions, i.e. |
||||||
|
sum(go_goroutines). |
||||||
|
</div> |
||||||
|
} |
||||||
|
> |
||||||
|
<TextArea |
||||||
|
type="text" |
||||||
|
aria-label="Prometheus Query" |
||||||
|
placeholder="Prometheus Query" |
||||||
|
value={varQuery} |
||||||
|
onChange={onVarQueryChange} |
||||||
|
onBlur={handleBlur} |
||||||
|
cols={100} |
||||||
|
/> |
||||||
|
</InlineField> |
||||||
|
</> |
||||||
|
)} |
||||||
|
{qryType === QueryType.SeriesQuery && ( |
||||||
|
<> |
||||||
|
<InlineField |
||||||
|
label="Series Query" |
||||||
|
labelWidth={20} |
||||||
|
tooltip={ |
||||||
|
<div> |
||||||
|
Enter enter a metric with labels, only a metric or only labels, i.e. |
||||||
|
go_goroutines{instance="localhost:9090"}, go_goroutines, or |
||||||
|
{instance="localhost:9090"}. Returns a list of time series associated with the |
||||||
|
entered data. |
||||||
|
</div> |
||||||
|
} |
||||||
|
> |
||||||
|
<Input |
||||||
|
type="text" |
||||||
|
aria-label="Series Query" |
||||||
|
placeholder="Series Query" |
||||||
|
value={seriesQuery} |
||||||
|
onChange={onSeriesQueryChange} |
||||||
|
onBlur={handleBlur} |
||||||
|
width={100} |
||||||
|
/> |
||||||
|
</InlineField> |
||||||
|
</> |
||||||
|
)} |
||||||
|
</InlineFieldRow> |
||||||
|
); |
||||||
|
}; |
@ -0,0 +1,104 @@ |
|||||||
|
import { PromVariableQuery, PromVariableQueryType as QueryType } from '../types'; |
||||||
|
|
||||||
|
const labelNamesRegex = /^label_names\(\)\s*$/; |
||||||
|
const labelValuesRegex = /^label_values\((?:(.+),\s*)?([a-zA-Z_][a-zA-Z0-9_]*)\)\s*$/; |
||||||
|
const metricNamesRegex = /^metrics\((.+)\)\s*$/; |
||||||
|
const queryResultRegex = /^query_result\((.+)\)\s*$/; |
||||||
|
|
||||||
|
export function migrateVariableQueryToEditor(rawQuery: string | PromVariableQuery): PromVariableQuery { |
||||||
|
// If not string, we assume PromVariableQuery
|
||||||
|
if (typeof rawQuery !== 'string') { |
||||||
|
return rawQuery; |
||||||
|
} |
||||||
|
|
||||||
|
const queryBase = { |
||||||
|
refId: 'PrometheusDatasource-VariableQuery', |
||||||
|
qryType: QueryType.LabelNames, |
||||||
|
}; |
||||||
|
|
||||||
|
const labelNames = rawQuery.match(labelNamesRegex); |
||||||
|
if (labelNames) { |
||||||
|
return { |
||||||
|
...queryBase, |
||||||
|
qryType: QueryType.LabelNames, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
const labelValues = rawQuery.match(labelValuesRegex); |
||||||
|
|
||||||
|
if (labelValues) { |
||||||
|
const label = labelValues[2]; |
||||||
|
const metric = labelValues[1]; |
||||||
|
if (metric) { |
||||||
|
return { |
||||||
|
...queryBase, |
||||||
|
qryType: QueryType.LabelValues, |
||||||
|
label, |
||||||
|
metric, |
||||||
|
}; |
||||||
|
} else { |
||||||
|
return { |
||||||
|
...queryBase, |
||||||
|
qryType: QueryType.LabelValues, |
||||||
|
label, |
||||||
|
}; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const metricNames = rawQuery.match(metricNamesRegex); |
||||||
|
if (metricNames) { |
||||||
|
return { |
||||||
|
...queryBase, |
||||||
|
qryType: QueryType.MetricNames, |
||||||
|
metric: metricNames[1], |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
const queryResult = rawQuery.match(queryResultRegex); |
||||||
|
if (queryResult) { |
||||||
|
return { |
||||||
|
...queryBase, |
||||||
|
qryType: QueryType.VarQueryResult, |
||||||
|
varQuery: queryResult[1], |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
// seriesQuery does not have a function and no regex above
|
||||||
|
if (!labelNames && !labelValues && !metricNames && !queryResult) { |
||||||
|
return { |
||||||
|
...queryBase, |
||||||
|
qryType: QueryType.SeriesQuery, |
||||||
|
seriesQuery: rawQuery, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
return queryBase; |
||||||
|
} |
||||||
|
|
||||||
|
// migrate it back to a string with the correct varialbes in place
|
||||||
|
export function migrateVariableEditorBackToVariableSupport(QueryVariable: PromVariableQuery): string { |
||||||
|
switch (QueryVariable.qryType) { |
||||||
|
case QueryType.LabelNames: |
||||||
|
return 'label_names()'; |
||||||
|
case QueryType.LabelValues: |
||||||
|
if (QueryVariable.metric) { |
||||||
|
return `label_values(${QueryVariable.metric},${QueryVariable.label})`; |
||||||
|
} else { |
||||||
|
return `label_values(${QueryVariable.label})`; |
||||||
|
} |
||||||
|
case QueryType.MetricNames: |
||||||
|
return `metrics(${QueryVariable.metric})`; |
||||||
|
case QueryType.VarQueryResult: |
||||||
|
const varQuery = removeLineBreaks(QueryVariable.varQuery); |
||||||
|
return `query_result(${varQuery})`; |
||||||
|
case QueryType.SeriesQuery: |
||||||
|
return '' + QueryVariable.seriesQuery; |
||||||
|
} |
||||||
|
|
||||||
|
return ''; |
||||||
|
} |
||||||
|
|
||||||
|
// allow line breaks in query result textarea
|
||||||
|
function removeLineBreaks(input?: string) { |
||||||
|
return input ? input.replace(/[\r\n]+/gm, '') : ''; |
||||||
|
} |
Loading…
Reference in new issue