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