mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Prometheus: Variable query editor improvements (#69884)
* refactor metric select and label filters, add variables to label names dropdown * use MetricsLabelsSection in variable editor * use variable editor UI with MetricsLabelsSection * filter label names by optional metric and allow metric to be cleared * fix tests * testing types for onChangeLabels * testing types for onChangeLabels * testing types for onChangeLabels * Ismails review, remove comment and indent * wrap label filters * improved behavior for selects and inputs, add tests and document behavior
This commit is contained in:
committed by
GitHub
parent
160ff360c4
commit
3425012ee4
@@ -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"]
|
||||
|
@@ -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(<PromVariableQueryEditor {...props} onChange={onChange} />);
|
||||
|
||||
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(<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);
|
||||
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(<PromVariableQueryEditor {...props} onChange={onChange} />);
|
||||
|
||||
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(<PromVariableQueryEditor {...props} onChange={onChange} />);
|
||||
|
||||
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,
|
||||
});
|
||||
});
|
||||
|
@@ -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<Array<SelectableValue<string>>>([]);
|
||||
|
||||
// label filters have been added as a filter for metrics in label values query type
|
||||
const [labelFilters, setLabelFilters] = useState<QueryBuilderLabelFilter[]>([]);
|
||||
|
||||
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<string, string[]>) => {
|
||||
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<string, string[]>) => {
|
||||
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<QueryType>) => {
|
||||
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<string>) => {
|
||||
setLabel(newLabel.value ?? '');
|
||||
const newLabelvalue = newLabel && newLabel.value ? newLabel.value : '';
|
||||
setLabel(newLabelvalue);
|
||||
if (qryType === QueryType.LabelValues && newLabelvalue) {
|
||||
onChangeWithVariableString({ label: newLabelvalue });
|
||||
}
|
||||
};
|
||||
|
||||
const onMetricChange = (e: FormEvent<HTMLInputElement>) => {
|
||||
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<HTMLTextAreaElement>) => {
|
||||
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<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);
|
||||
}
|
||||
};
|
||||
const promVisualQuery = useCallback(() => {
|
||||
return { metric: metric, labels: labelFilters, operations: [] };
|
||||
}, [metric, labelFilters]);
|
||||
|
||||
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>
|
||||
<>
|
||||
<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}
|
||||
value={qryType}
|
||||
options={variableOptions}
|
||||
width={25}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
|
||||
{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>
|
||||
<InlineFieldRow>
|
||||
<InlineField
|
||||
label="Label"
|
||||
labelWidth={20}
|
||||
required
|
||||
aria-labelledby="label-select"
|
||||
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}
|
||||
value={label}
|
||||
options={labelOptions}
|
||||
width={25}
|
||||
allowCustomValue
|
||||
isClearable={true}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
{/* Used to select an optional metric with optional label filters */}
|
||||
<MetricsLabelsSection
|
||||
query={promVisualQuery()}
|
||||
datasource={datasource}
|
||||
onChange={metricsLabelsChange}
|
||||
variableEditor={true}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{qryType === QueryType.MetricNames && (
|
||||
<>
|
||||
<InlineFieldRow>
|
||||
<InlineField
|
||||
label="Metric Regex"
|
||||
label="Metric regex"
|
||||
labelWidth={20}
|
||||
tooltip={<div>Returns a list of metrics matching the specified metric regex.</div>}
|
||||
>
|
||||
@@ -202,15 +264,18 @@ export const PromVariableQueryEditor = ({ onChange, query, datasource }: Props)
|
||||
aria-label="Metric selector"
|
||||
placeholder="Metric Regex"
|
||||
value={metric}
|
||||
onChange={onMetricChange}
|
||||
onBlur={handleBlur}
|
||||
onChange={(e) => {
|
||||
setMetric(e.currentTarget.value);
|
||||
onMetricChange(e.currentTarget.value);
|
||||
}}
|
||||
width={25}
|
||||
/>
|
||||
</InlineField>
|
||||
</>
|
||||
</InlineFieldRow>
|
||||
)}
|
||||
|
||||
{qryType === QueryType.VarQueryResult && (
|
||||
<>
|
||||
<InlineFieldRow>
|
||||
<InlineField
|
||||
label="Query"
|
||||
labelWidth={20}
|
||||
@@ -227,14 +292,19 @@ export const PromVariableQueryEditor = ({ onChange, query, datasource }: Props)
|
||||
placeholder="Prometheus Query"
|
||||
value={varQuery}
|
||||
onChange={onVarQueryChange}
|
||||
onBlur={handleBlur}
|
||||
onBlur={() => {
|
||||
if (qryType === QueryType.VarQueryResult && varQuery) {
|
||||
onChangeWithVariableString({ qryType });
|
||||
}
|
||||
}}
|
||||
cols={100}
|
||||
/>
|
||||
</InlineField>
|
||||
</>
|
||||
</InlineFieldRow>
|
||||
)}
|
||||
|
||||
{qryType === QueryType.SeriesQuery && (
|
||||
<>
|
||||
<InlineFieldRow>
|
||||
<InlineField
|
||||
label="Series Query"
|
||||
labelWidth={20}
|
||||
@@ -253,13 +323,17 @@ export const PromVariableQueryEditor = ({ onChange, query, datasource }: Props)
|
||||
placeholder="Series Query"
|
||||
value={seriesQuery}
|
||||
onChange={onSeriesQueryChange}
|
||||
onBlur={handleBlur}
|
||||
onBlur={() => {
|
||||
if (qryType === QueryType.SeriesQuery && seriesQuery) {
|
||||
onChangeWithVariableString({ qryType });
|
||||
}
|
||||
}}
|
||||
width={100}
|
||||
/>
|
||||
</InlineField>
|
||||
</>
|
||||
</InlineFieldRow>
|
||||
)}
|
||||
</InlineFieldRow>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@@ -1,8 +1,10 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { isEqual } from 'lodash';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { EditorFieldGroup, EditorField, EditorList } from '@grafana/experimental';
|
||||
import { InlineFieldRow, InlineLabel } from '@grafana/ui';
|
||||
|
||||
import { QueryBuilderLabelFilter } from '../shared/types';
|
||||
|
||||
@@ -19,6 +21,7 @@ export interface Props {
|
||||
labelFilterRequired?: boolean;
|
||||
getLabelValuesAutofillSuggestions: (query: string, labelName?: string) => Promise<SelectableValue[]>;
|
||||
debounceDuration: number;
|
||||
variableEditor?: boolean;
|
||||
}
|
||||
|
||||
export function LabelFilters({
|
||||
@@ -29,6 +32,7 @@ export function LabelFilters({
|
||||
labelFilterRequired,
|
||||
getLabelValuesAutofillSuggestions,
|
||||
debounceDuration,
|
||||
variableEditor,
|
||||
}: Props) {
|
||||
const defaultOp = '=';
|
||||
const [items, setItems] = useState<Array<Partial<QueryBuilderLabelFilter>>>([{ op: defaultOp }]);
|
||||
@@ -53,32 +57,60 @@ export function LabelFilters({
|
||||
|
||||
const hasLabelFilter = items.some((item) => item.label && item.value);
|
||||
|
||||
const editorList = () => {
|
||||
return (
|
||||
<EditorList
|
||||
items={items}
|
||||
onChange={onLabelsChange}
|
||||
renderItem={(item: Partial<QueryBuilderLabelFilter>, onChangeItem, onDelete) => (
|
||||
<LabelFilterItem
|
||||
debounceDuration={debounceDuration}
|
||||
item={item}
|
||||
defaultOp={defaultOp}
|
||||
onChange={onChangeItem}
|
||||
onDelete={onDelete}
|
||||
onGetLabelNames={onGetLabelNames}
|
||||
onGetLabelValues={onGetLabelValues}
|
||||
invalidLabel={labelFilterRequired && !item.label}
|
||||
invalidValue={labelFilterRequired && !item.value}
|
||||
getLabelValuesAutofillSuggestions={getLabelValuesAutofillSuggestions}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<EditorFieldGroup>
|
||||
<EditorField
|
||||
label="Label filters"
|
||||
error={MISSING_LABEL_FILTER_ERROR_MESSAGE}
|
||||
invalid={labelFilterRequired && !hasLabelFilter}
|
||||
>
|
||||
<EditorList
|
||||
items={items}
|
||||
onChange={onLabelsChange}
|
||||
renderItem={(item: Partial<QueryBuilderLabelFilter>, onChangeItem, onDelete) => (
|
||||
<LabelFilterItem
|
||||
debounceDuration={debounceDuration}
|
||||
item={item}
|
||||
defaultOp={defaultOp}
|
||||
onChange={onChangeItem}
|
||||
onDelete={onDelete}
|
||||
onGetLabelNames={onGetLabelNames}
|
||||
onGetLabelValues={onGetLabelValues}
|
||||
invalidLabel={labelFilterRequired && !item.label}
|
||||
invalidValue={labelFilterRequired && !item.value}
|
||||
getLabelValuesAutofillSuggestions={getLabelValuesAutofillSuggestions}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</EditorField>
|
||||
</EditorFieldGroup>
|
||||
<>
|
||||
{variableEditor ? (
|
||||
<InlineFieldRow>
|
||||
<div
|
||||
className={cx(
|
||||
css`
|
||||
display: flex;
|
||||
`
|
||||
)}
|
||||
>
|
||||
<InlineLabel
|
||||
width={20}
|
||||
tooltip={<div>Optional: used to filter the metric select for this query type.</div>}
|
||||
>
|
||||
Label filters
|
||||
</InlineLabel>
|
||||
{editorList()}
|
||||
</div>
|
||||
</InlineFieldRow>
|
||||
) : (
|
||||
<EditorFieldGroup>
|
||||
<EditorField
|
||||
label="Label filters"
|
||||
error={MISSING_LABEL_FILTER_ERROR_MESSAGE}
|
||||
invalid={labelFilterRequired && !hasLabelFilter}
|
||||
>
|
||||
{editorList()}
|
||||
</EditorField>
|
||||
</EditorFieldGroup>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@@ -6,7 +6,7 @@ import Highlighter from 'react-highlight-words';
|
||||
import { GrafanaTheme2, SelectableValue, toOption } from '@grafana/data';
|
||||
import { EditorField, EditorFieldGroup } from '@grafana/experimental';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { AsyncSelect, Button, FormatOptionLabelMeta, Icon, useStyles2 } from '@grafana/ui';
|
||||
import { AsyncSelect, Button, FormatOptionLabelMeta, Icon, InlineField, InlineFieldRow, useStyles2 } from '@grafana/ui';
|
||||
import { SelectMenuOptions } from '@grafana/ui/src/components/Select/SelectMenu';
|
||||
|
||||
import { PrometheusDatasource } from '../../datasource';
|
||||
@@ -27,12 +27,12 @@ export interface Props {
|
||||
onGetMetrics: () => Promise<SelectableValue[]>;
|
||||
datasource: PrometheusDatasource;
|
||||
labelsFilters: QueryBuilderLabelFilter[];
|
||||
onBlur?: () => void;
|
||||
variableEditor?: boolean;
|
||||
}
|
||||
|
||||
export const PROMETHEUS_QUERY_BUILDER_MAX_RESULTS = 1000;
|
||||
|
||||
const prometheusMetricEncyclopedia = config.featureToggles.prometheusMetricEncyclopedia;
|
||||
|
||||
export function MetricSelect({
|
||||
datasource,
|
||||
query,
|
||||
@@ -40,6 +40,8 @@ export function MetricSelect({
|
||||
onGetMetrics,
|
||||
labelsFilters,
|
||||
metricLookupDisabled,
|
||||
onBlur,
|
||||
variableEditor,
|
||||
}: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const [state, setState] = useState<{
|
||||
@@ -49,6 +51,8 @@ export function MetricSelect({
|
||||
initialMetrics?: string[];
|
||||
}>({});
|
||||
|
||||
const prometheusMetricEncyclopedia = config.featureToggles.prometheusMetricEncyclopedia;
|
||||
|
||||
const metricsModalOption: SelectableValue[] = [
|
||||
{
|
||||
value: 'BrowseMetrics',
|
||||
@@ -57,29 +61,32 @@ export function MetricSelect({
|
||||
},
|
||||
];
|
||||
|
||||
const customFilterOption = useCallback((option: SelectableValue<any>, searchQuery: string) => {
|
||||
const label = option.label ?? option.value;
|
||||
if (!label) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// custom value is not a string label but a react node
|
||||
if (!label.toLowerCase) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const searchWords = searchQuery.split(splitSeparator);
|
||||
return searchWords.reduce((acc, cur) => {
|
||||
const matcheSearch = label.toLowerCase().includes(cur.toLowerCase());
|
||||
|
||||
let browseOption = false;
|
||||
if (prometheusMetricEncyclopedia) {
|
||||
browseOption = label === 'Metrics explorer';
|
||||
const customFilterOption = useCallback(
|
||||
(option: SelectableValue<any>, searchQuery: string) => {
|
||||
const label = option.label ?? option.value;
|
||||
if (!label) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return acc && (matcheSearch || browseOption);
|
||||
}, true);
|
||||
}, []);
|
||||
// custom value is not a string label but a react node
|
||||
if (!label.toLowerCase) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const searchWords = searchQuery.split(splitSeparator);
|
||||
return searchWords.reduce((acc, cur) => {
|
||||
const matcheSearch = label.toLowerCase().includes(cur.toLowerCase());
|
||||
|
||||
let browseOption = false;
|
||||
if (prometheusMetricEncyclopedia) {
|
||||
browseOption = label === 'Metrics explorer';
|
||||
}
|
||||
|
||||
return acc && (matcheSearch || browseOption);
|
||||
}, true);
|
||||
},
|
||||
[prometheusMetricEncyclopedia]
|
||||
);
|
||||
|
||||
const formatOptionLabel = useCallback(
|
||||
(option: SelectableValue<any>, meta: FormatOptionLabelMeta<any>) => {
|
||||
@@ -192,6 +199,63 @@ export function MetricSelect({
|
||||
return SelectMenuOptions(props);
|
||||
};
|
||||
|
||||
const asyncSelect = () => {
|
||||
return (
|
||||
<AsyncSelect
|
||||
isClearable={variableEditor ? true : false}
|
||||
inputId="prometheus-metric-select"
|
||||
className={styles.select}
|
||||
value={query.metric ? toOption(query.metric) : undefined}
|
||||
placeholder={'Select metric'}
|
||||
allowCustomValue
|
||||
formatOptionLabel={formatOptionLabel}
|
||||
filterOption={customFilterOption}
|
||||
onOpenMenu={async () => {
|
||||
if (metricLookupDisabled) {
|
||||
return;
|
||||
}
|
||||
setState({ isLoading: true });
|
||||
const metrics = await onGetMetrics();
|
||||
const initialMetrics: string[] = metrics.map((m) => m.value);
|
||||
if (metrics.length > PROMETHEUS_QUERY_BUILDER_MAX_RESULTS) {
|
||||
metrics.splice(0, metrics.length - PROMETHEUS_QUERY_BUILDER_MAX_RESULTS);
|
||||
}
|
||||
|
||||
if (prometheusMetricEncyclopedia) {
|
||||
setState({
|
||||
// add the modal butoon option to the options
|
||||
metrics: [...metricsModalOption, ...metrics],
|
||||
isLoading: undefined,
|
||||
// pass the initial metrics into the Metrics Modal
|
||||
initialMetrics: initialMetrics,
|
||||
});
|
||||
} else {
|
||||
setState({ metrics, isLoading: undefined });
|
||||
}
|
||||
}}
|
||||
loadOptions={metricLookupDisabled ? metricLookupDisabledSearch : debouncedSearch}
|
||||
isLoading={state.isLoading}
|
||||
defaultOptions={state.metrics}
|
||||
onChange={(input) => {
|
||||
const value = input?.value;
|
||||
if (value) {
|
||||
// if there is no metric and the m.e. is enabled, open the modal
|
||||
if (prometheusMetricEncyclopedia && value === 'BrowseMetrics') {
|
||||
tracking('grafana_prometheus_metric_encyclopedia_open', null, '', query);
|
||||
setState({ ...state, metricsModalOpen: true });
|
||||
} else {
|
||||
onChange({ ...query, metric: value });
|
||||
}
|
||||
} else {
|
||||
onChange({ ...query, metric: '' });
|
||||
}
|
||||
}}
|
||||
components={prometheusMetricEncyclopedia ? { Option: CustomOption } : {}}
|
||||
onBlur={onBlur ? onBlur : () => {}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{prometheusMetricEncyclopedia && !datasource.lookupsDisabled && state.metricsModalOpen && (
|
||||
@@ -204,57 +268,22 @@ export function MetricSelect({
|
||||
initialMetrics={state.initialMetrics ?? []}
|
||||
/>
|
||||
)}
|
||||
<EditorFieldGroup>
|
||||
<EditorField label="Metric">
|
||||
<AsyncSelect
|
||||
inputId="prometheus-metric-select"
|
||||
className={styles.select}
|
||||
value={query.metric ? toOption(query.metric) : undefined}
|
||||
placeholder={'Select metric'}
|
||||
allowCustomValue
|
||||
formatOptionLabel={formatOptionLabel}
|
||||
filterOption={customFilterOption}
|
||||
onOpenMenu={async () => {
|
||||
if (metricLookupDisabled) {
|
||||
return;
|
||||
}
|
||||
setState({ isLoading: true });
|
||||
const metrics = await onGetMetrics();
|
||||
const initialMetrics: string[] = metrics.map((m) => m.value);
|
||||
if (metrics.length > PROMETHEUS_QUERY_BUILDER_MAX_RESULTS) {
|
||||
metrics.splice(0, metrics.length - PROMETHEUS_QUERY_BUILDER_MAX_RESULTS);
|
||||
}
|
||||
|
||||
if (prometheusMetricEncyclopedia) {
|
||||
setState({
|
||||
// add the modal butoon option to the options
|
||||
metrics: [...metricsModalOption, ...metrics],
|
||||
isLoading: undefined,
|
||||
// pass the initial metrics into the Metrics Modal
|
||||
initialMetrics: initialMetrics,
|
||||
});
|
||||
} else {
|
||||
setState({ metrics, isLoading: undefined });
|
||||
}
|
||||
}}
|
||||
loadOptions={metricLookupDisabled ? metricLookupDisabledSearch : debouncedSearch}
|
||||
isLoading={state.isLoading}
|
||||
defaultOptions={state.metrics}
|
||||
onChange={({ value }) => {
|
||||
if (value) {
|
||||
// if there is no metric and the m.e. is enabled, open the modal
|
||||
if (prometheusMetricEncyclopedia && value === 'BrowseMetrics') {
|
||||
tracking('grafana_prometheus_metric_encyclopedia_open', null, '', query);
|
||||
setState({ ...state, metricsModalOpen: true });
|
||||
} else {
|
||||
onChange({ ...query, metric: value });
|
||||
}
|
||||
}
|
||||
}}
|
||||
components={prometheusMetricEncyclopedia ? { Option: CustomOption } : {}}
|
||||
/>
|
||||
</EditorField>
|
||||
</EditorFieldGroup>
|
||||
{/* format the ui for either the query editor or the variable editor */}
|
||||
{variableEditor ? (
|
||||
<InlineFieldRow>
|
||||
<InlineField
|
||||
label="Metric"
|
||||
labelWidth={20}
|
||||
tooltip={<div>Optional: returns a list of label values for the label name in the specified metric.</div>}
|
||||
>
|
||||
{asyncSelect()}
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
) : (
|
||||
<EditorFieldGroup>
|
||||
<EditorField label="Metric">{asyncSelect()}</EditorField>
|
||||
</EditorFieldGroup>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@@ -0,0 +1,258 @@
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
|
||||
import { PrometheusDatasource } from '../../datasource';
|
||||
import { getMetadataString } from '../../language_provider';
|
||||
import { promQueryModeller } from '../PromQueryModeller';
|
||||
import { regexifyLabelValuesQueryString } from '../shared/parsingUtils';
|
||||
import { QueryBuilderLabelFilter } from '../shared/types';
|
||||
import { PromVisualQuery } from '../types';
|
||||
|
||||
import { LabelFilters } from './LabelFilters';
|
||||
import { MetricSelect, PROMETHEUS_QUERY_BUILDER_MAX_RESULTS } from './MetricSelect';
|
||||
|
||||
export interface MetricsLabelsSectionProps {
|
||||
query: PromVisualQuery;
|
||||
datasource: PrometheusDatasource;
|
||||
onChange: (update: PromVisualQuery) => void;
|
||||
variableEditor?: boolean;
|
||||
onBlur?: () => void;
|
||||
}
|
||||
|
||||
export function MetricsLabelsSection({
|
||||
datasource,
|
||||
query,
|
||||
onChange,
|
||||
onBlur,
|
||||
variableEditor,
|
||||
}: MetricsLabelsSectionProps) {
|
||||
// fixing the use of 'as' from refactoring
|
||||
// @ts-ignore
|
||||
const onChangeLabels = (labels) => {
|
||||
onChange({ ...query, labels });
|
||||
};
|
||||
/**
|
||||
* Map metric metadata to SelectableValue for Select component and also adds defined template variables to the list.
|
||||
*/
|
||||
const withTemplateVariableOptions = useCallback(
|
||||
async (optionsPromise: Promise<SelectableValue[]>): Promise<SelectableValue[]> => {
|
||||
const variables = datasource.getVariables();
|
||||
const options = await optionsPromise;
|
||||
return [
|
||||
...variables.map((value: string) => ({ label: value, value })),
|
||||
...options.map((option: SelectableValue) => ({
|
||||
label: option.value,
|
||||
value: option.value,
|
||||
title: option.description,
|
||||
})),
|
||||
];
|
||||
},
|
||||
[datasource]
|
||||
);
|
||||
|
||||
/**
|
||||
* Function kicked off when user interacts with label in label filters.
|
||||
* Formats a promQL expression and passes that off to helper functions depending on API support
|
||||
* @param forLabel
|
||||
*/
|
||||
const onGetLabelNames = async (forLabel: Partial<QueryBuilderLabelFilter>): Promise<SelectableValue[]> => {
|
||||
// If no metric we need to use a different method
|
||||
if (!query.metric) {
|
||||
await datasource.languageProvider.fetchLabels();
|
||||
return datasource.languageProvider.getLabelKeys().map((k) => ({ value: k }));
|
||||
}
|
||||
|
||||
const labelsToConsider = query.labels.filter((x) => x !== forLabel);
|
||||
labelsToConsider.push({ label: '__name__', op: '=', value: query.metric });
|
||||
const expr = promQueryModeller.renderLabels(labelsToConsider);
|
||||
|
||||
let labelsIndex: Record<string, string[]>;
|
||||
if (datasource.hasLabelsMatchAPISupport()) {
|
||||
labelsIndex = await datasource.languageProvider.fetchSeriesLabelsMatch(expr);
|
||||
} else {
|
||||
labelsIndex = await datasource.languageProvider.fetchSeriesLabels(expr);
|
||||
}
|
||||
|
||||
// filter out already used labels
|
||||
return Object.keys(labelsIndex)
|
||||
.filter((labelName) => !labelsToConsider.find((filter) => filter.label === labelName))
|
||||
.map((k) => ({ value: k }));
|
||||
};
|
||||
|
||||
const getLabelValuesAutocompleteSuggestions = (
|
||||
queryString?: string,
|
||||
labelName?: string
|
||||
): Promise<SelectableValue[]> => {
|
||||
const forLabel = {
|
||||
label: labelName ?? '__name__',
|
||||
op: '=~',
|
||||
value: regexifyLabelValuesQueryString(`.*${queryString}`),
|
||||
};
|
||||
const labelsToConsider = query.labels.filter((x) => x.label !== forLabel.label);
|
||||
labelsToConsider.push(forLabel);
|
||||
if (query.metric) {
|
||||
labelsToConsider.push({ label: '__name__', op: '=', value: query.metric });
|
||||
}
|
||||
const interpolatedLabelsToConsider = labelsToConsider.map((labelObject) => ({
|
||||
...labelObject,
|
||||
label: datasource.interpolateString(labelObject.label),
|
||||
value: datasource.interpolateString(labelObject.value),
|
||||
}));
|
||||
const expr = promQueryModeller.renderLabels(interpolatedLabelsToConsider);
|
||||
let response: Promise<SelectableValue[]>;
|
||||
if (datasource.hasLabelsMatchAPISupport()) {
|
||||
response = getLabelValuesFromLabelValuesAPI(forLabel, expr);
|
||||
} else {
|
||||
response = getLabelValuesFromSeriesAPI(forLabel, expr);
|
||||
}
|
||||
|
||||
return response.then((response: SelectableValue[]) => {
|
||||
if (response.length > PROMETHEUS_QUERY_BUILDER_MAX_RESULTS) {
|
||||
response.splice(0, response.length - PROMETHEUS_QUERY_BUILDER_MAX_RESULTS);
|
||||
}
|
||||
return response;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to fetch and format label value results from legacy API
|
||||
* @param forLabel
|
||||
* @param promQLExpression
|
||||
*/
|
||||
const getLabelValuesFromSeriesAPI = (
|
||||
forLabel: Partial<QueryBuilderLabelFilter>,
|
||||
promQLExpression: string
|
||||
): Promise<SelectableValue[]> => {
|
||||
if (!forLabel.label) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
const result = datasource.languageProvider.fetchSeries(promQLExpression);
|
||||
const forLabelInterpolated = datasource.interpolateString(forLabel.label);
|
||||
return result.then((result) => {
|
||||
// This query returns duplicate values, scrub them out
|
||||
const set = new Set<string>();
|
||||
result.forEach((labelValue) => {
|
||||
const labelNameString = labelValue[forLabelInterpolated];
|
||||
set.add(labelNameString);
|
||||
});
|
||||
|
||||
return Array.from(set).map((labelValues: string) => ({ label: labelValues, value: labelValues }));
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to fetch label values from a promql string expression and a label
|
||||
* @param forLabel
|
||||
* @param promQLExpression
|
||||
*/
|
||||
const getLabelValuesFromLabelValuesAPI = (
|
||||
forLabel: Partial<QueryBuilderLabelFilter>,
|
||||
promQLExpression: string
|
||||
): Promise<SelectableValue[]> => {
|
||||
if (!forLabel.label) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
return datasource.languageProvider.fetchSeriesValuesWithMatch(forLabel.label, promQLExpression).then((response) => {
|
||||
return response.map((v) => ({
|
||||
value: v,
|
||||
label: v,
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Function kicked off when users interact with the value of the label filters
|
||||
* Formats a promQL expression and passes that into helper functions depending on API support
|
||||
* @param forLabel
|
||||
*/
|
||||
const onGetLabelValues = async (forLabel: Partial<QueryBuilderLabelFilter>): Promise<SelectableValue[]> => {
|
||||
if (!forLabel.label) {
|
||||
return [];
|
||||
}
|
||||
// If no metric is selected, we can get the raw list of labels
|
||||
if (!query.metric) {
|
||||
return (await datasource.languageProvider.getLabelValues(forLabel.label)).map((v) => ({ value: v }));
|
||||
}
|
||||
|
||||
const labelsToConsider = query.labels.filter((x) => x !== forLabel);
|
||||
labelsToConsider.push({ label: '__name__', op: '=', value: query.metric });
|
||||
|
||||
const interpolatedLabelsToConsider = labelsToConsider.map((labelObject) => ({
|
||||
...labelObject,
|
||||
label: datasource.interpolateString(labelObject.label),
|
||||
value: datasource.interpolateString(labelObject.value),
|
||||
}));
|
||||
|
||||
const expr = promQueryModeller.renderLabels(interpolatedLabelsToConsider);
|
||||
|
||||
if (datasource.hasLabelsMatchAPISupport()) {
|
||||
return getLabelValuesFromLabelValuesAPI(forLabel, expr);
|
||||
} else {
|
||||
return getLabelValuesFromSeriesAPI(forLabel, expr);
|
||||
}
|
||||
};
|
||||
|
||||
const onGetMetrics = useCallback(() => {
|
||||
return withTemplateVariableOptions(getMetrics(datasource, query));
|
||||
}, [datasource, query, withTemplateVariableOptions]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MetricSelect
|
||||
query={query}
|
||||
onChange={onChange}
|
||||
onGetMetrics={onGetMetrics}
|
||||
datasource={datasource}
|
||||
labelsFilters={query.labels}
|
||||
metricLookupDisabled={datasource.lookupsDisabled}
|
||||
onBlur={onBlur ? onBlur : () => {}}
|
||||
variableEditor={variableEditor}
|
||||
/>
|
||||
<LabelFilters
|
||||
debounceDuration={datasource.getDebounceTimeInMilliseconds()}
|
||||
getLabelValuesAutofillSuggestions={getLabelValuesAutocompleteSuggestions}
|
||||
labelsFilters={query.labels}
|
||||
onChange={onChangeLabels}
|
||||
onGetLabelNames={(forLabel) => withTemplateVariableOptions(onGetLabelNames(forLabel))}
|
||||
onGetLabelValues={(forLabel) => withTemplateVariableOptions(onGetLabelValues(forLabel))}
|
||||
variableEditor={variableEditor}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of metrics, either all or filtered by query param. It also adds description string to each metric if it
|
||||
* exists.
|
||||
* @param datasource
|
||||
* @param query
|
||||
*/
|
||||
async function getMetrics(
|
||||
datasource: PrometheusDatasource,
|
||||
query: PromVisualQuery
|
||||
): Promise<Array<{ value: string; description?: string }>> {
|
||||
// Makes sure we loaded the metadata for metrics. Usually this is done in the start() method of the provider but we
|
||||
// don't use it with the visual builder and there is no need to run all the start() setup anyway.
|
||||
if (!datasource.languageProvider.metricsMetadata) {
|
||||
await datasource.languageProvider.loadMetricsMetadata();
|
||||
}
|
||||
|
||||
// Error handling for when metrics metadata returns as undefined
|
||||
if (!datasource.languageProvider.metricsMetadata) {
|
||||
datasource.languageProvider.metricsMetadata = {};
|
||||
}
|
||||
|
||||
let metrics: string[];
|
||||
if (query.labels.length > 0) {
|
||||
const expr = promQueryModeller.renderLabels(query.labels);
|
||||
metrics = (await datasource.languageProvider.getSeries(expr, true))['__name__'] ?? [];
|
||||
} else {
|
||||
metrics = (await datasource.languageProvider.getLabelValues('__name__')) ?? [];
|
||||
}
|
||||
|
||||
return metrics.map((m) => ({
|
||||
value: m,
|
||||
description: getMetadataString(m, datasource.languageProvider.metricsMetadata!),
|
||||
}));
|
||||
}
|
@@ -1,10 +1,9 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { DataSourceApi, PanelData, SelectableValue } from '@grafana/data';
|
||||
import { DataSourceApi, PanelData } from '@grafana/data';
|
||||
import { EditorRow } from '@grafana/experimental';
|
||||
|
||||
import { PrometheusDatasource } from '../../datasource';
|
||||
import { getMetadataString } from '../../language_provider';
|
||||
import promqlGrammar from '../../promql';
|
||||
import { promQueryModeller } from '../PromQueryModeller';
|
||||
import { buildVisualQueryFromString } from '../parsing';
|
||||
@@ -14,12 +13,10 @@ import { OperationListExplained } from '../shared/OperationListExplained';
|
||||
import { OperationsEditorRow } from '../shared/OperationsEditorRow';
|
||||
import { QueryBuilderHints } from '../shared/QueryBuilderHints';
|
||||
import { RawQuery } from '../shared/RawQuery';
|
||||
import { regexifyLabelValuesQueryString } from '../shared/parsingUtils';
|
||||
import { QueryBuilderLabelFilter, QueryBuilderOperation } from '../shared/types';
|
||||
import { QueryBuilderOperation } from '../shared/types';
|
||||
import { PromVisualQuery } from '../types';
|
||||
|
||||
import { LabelFilters } from './LabelFilters';
|
||||
import { MetricSelect, PROMETHEUS_QUERY_BUILDER_MAX_RESULTS } from './MetricSelect';
|
||||
import { MetricsLabelsSection } from './MetricsLabelsSection';
|
||||
import { NestedQueryList } from './NestedQueryList';
|
||||
import { EXPLAIN_LABEL_FILTER_CONTENT } from './PromQueryBuilderExplained';
|
||||
|
||||
@@ -35,171 +32,6 @@ export interface Props {
|
||||
export const PromQueryBuilder = React.memo<Props>((props) => {
|
||||
const { datasource, query, onChange, onRunQuery, data, showExplain } = props;
|
||||
const [highlightedOp, setHighlightedOp] = useState<QueryBuilderOperation | undefined>();
|
||||
const onChangeLabels = (labels: QueryBuilderLabelFilter[]) => {
|
||||
onChange({ ...query, labels });
|
||||
};
|
||||
|
||||
/**
|
||||
* Map metric metadata to SelectableValue for Select component and also adds defined template variables to the list.
|
||||
*/
|
||||
const withTemplateVariableOptions = useCallback(
|
||||
async (optionsPromise: Promise<SelectableValue[]>): Promise<SelectableValue[]> => {
|
||||
const variables = datasource.getVariables();
|
||||
const options = await optionsPromise;
|
||||
return [
|
||||
...variables.map((value) => ({ label: value, value })),
|
||||
...options.map((option) => ({ label: option.value, value: option.value, title: option.description })),
|
||||
];
|
||||
},
|
||||
[datasource]
|
||||
);
|
||||
|
||||
/**
|
||||
* Function kicked off when user interacts with label in label filters.
|
||||
* Formats a promQL expression and passes that off to helper functions depending on API support
|
||||
* @param forLabel
|
||||
*/
|
||||
const onGetLabelNames = async (forLabel: Partial<QueryBuilderLabelFilter>): Promise<SelectableValue[]> => {
|
||||
// If no metric we need to use a different method
|
||||
if (!query.metric) {
|
||||
// Todo add caching but inside language provider!
|
||||
await datasource.languageProvider.fetchLabels();
|
||||
return datasource.languageProvider.getLabelKeys().map((k) => ({ value: k }));
|
||||
}
|
||||
|
||||
const labelsToConsider = query.labels.filter((x) => x !== forLabel);
|
||||
labelsToConsider.push({ label: '__name__', op: '=', value: query.metric });
|
||||
const expr = promQueryModeller.renderLabels(labelsToConsider);
|
||||
|
||||
let labelsIndex;
|
||||
if (datasource.hasLabelsMatchAPISupport()) {
|
||||
labelsIndex = await datasource.languageProvider.fetchSeriesLabelsMatch(expr);
|
||||
} else {
|
||||
labelsIndex = await datasource.languageProvider.fetchSeriesLabels(expr);
|
||||
}
|
||||
|
||||
// filter out already used labels
|
||||
return Object.keys(labelsIndex)
|
||||
.filter((labelName) => !labelsToConsider.find((filter) => filter.label === labelName))
|
||||
.map((k) => ({ value: k }));
|
||||
};
|
||||
|
||||
const getLabelValuesAutocompleteSuggestions = (
|
||||
queryString?: string,
|
||||
labelName?: string
|
||||
): Promise<SelectableValue[]> => {
|
||||
const forLabel = {
|
||||
label: labelName ?? '__name__',
|
||||
op: '=~',
|
||||
value: regexifyLabelValuesQueryString(`.*${queryString}`),
|
||||
};
|
||||
const labelsToConsider = query.labels.filter((x) => x.label !== forLabel.label);
|
||||
labelsToConsider.push(forLabel);
|
||||
if (query.metric) {
|
||||
labelsToConsider.push({ label: '__name__', op: '=', value: query.metric });
|
||||
}
|
||||
const interpolatedLabelsToConsider = labelsToConsider.map((labelObject) => ({
|
||||
...labelObject,
|
||||
label: datasource.interpolateString(labelObject.label),
|
||||
value: datasource.interpolateString(labelObject.value),
|
||||
}));
|
||||
const expr = promQueryModeller.renderLabels(interpolatedLabelsToConsider);
|
||||
let response;
|
||||
if (datasource.hasLabelsMatchAPISupport()) {
|
||||
response = getLabelValuesFromLabelValuesAPI(forLabel, expr);
|
||||
} else {
|
||||
response = getLabelValuesFromSeriesAPI(forLabel, expr);
|
||||
}
|
||||
|
||||
return response.then((response: SelectableValue[]) => {
|
||||
if (response.length > PROMETHEUS_QUERY_BUILDER_MAX_RESULTS) {
|
||||
response.splice(0, response.length - PROMETHEUS_QUERY_BUILDER_MAX_RESULTS);
|
||||
}
|
||||
return response;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to fetch and format label value results from legacy API
|
||||
* @param forLabel
|
||||
* @param promQLExpression
|
||||
*/
|
||||
const getLabelValuesFromSeriesAPI = (
|
||||
forLabel: Partial<QueryBuilderLabelFilter>,
|
||||
promQLExpression: string
|
||||
): Promise<SelectableValue[]> => {
|
||||
if (!forLabel.label) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
const result = datasource.languageProvider.fetchSeries(promQLExpression);
|
||||
const forLabelInterpolated = datasource.interpolateString(forLabel.label);
|
||||
return result.then((result) => {
|
||||
// This query returns duplicate values, scrub them out
|
||||
const set = new Set<string>();
|
||||
result.forEach((labelValue) => {
|
||||
const labelNameString = labelValue[forLabelInterpolated];
|
||||
set.add(labelNameString);
|
||||
});
|
||||
|
||||
return Array.from(set).map((labelValues: string) => ({ label: labelValues, value: labelValues }));
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to fetch label values from a promql string expression and a label
|
||||
* @param forLabel
|
||||
* @param promQLExpression
|
||||
*/
|
||||
const getLabelValuesFromLabelValuesAPI = (
|
||||
forLabel: Partial<QueryBuilderLabelFilter>,
|
||||
promQLExpression: string
|
||||
): Promise<SelectableValue[]> => {
|
||||
if (!forLabel.label) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
return datasource.languageProvider.fetchSeriesValuesWithMatch(forLabel.label, promQLExpression).then((response) => {
|
||||
return response.map((v) => ({
|
||||
value: v,
|
||||
label: v,
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Function kicked off when users interact with the value of the label filters
|
||||
* Formats a promQL expression and passes that into helper functions depending on API support
|
||||
* @param forLabel
|
||||
*/
|
||||
const onGetLabelValues = async (forLabel: Partial<QueryBuilderLabelFilter>): Promise<SelectableValue[]> => {
|
||||
if (!forLabel.label) {
|
||||
return [];
|
||||
}
|
||||
// If no metric is selected, we can get the raw list of labels
|
||||
if (!query.metric) {
|
||||
return (await datasource.languageProvider.getLabelValues(forLabel.label)).map((v) => ({ value: v }));
|
||||
}
|
||||
|
||||
const labelsToConsider = query.labels.filter((x) => x !== forLabel);
|
||||
labelsToConsider.push({ label: '__name__', op: '=', value: query.metric });
|
||||
|
||||
const interpolatedLabelsToConsider = labelsToConsider.map((labelObject) => ({
|
||||
...labelObject,
|
||||
label: datasource.interpolateString(labelObject.label),
|
||||
value: datasource.interpolateString(labelObject.value),
|
||||
}));
|
||||
|
||||
const expr = promQueryModeller.renderLabels(interpolatedLabelsToConsider);
|
||||
|
||||
if (datasource.hasLabelsMatchAPISupport()) {
|
||||
return getLabelValuesFromLabelValuesAPI(forLabel, expr);
|
||||
} else {
|
||||
return getLabelValuesFromSeriesAPI(forLabel, expr);
|
||||
}
|
||||
};
|
||||
|
||||
const onGetMetrics = useCallback(() => {
|
||||
return withTemplateVariableOptions(getMetrics(datasource, query));
|
||||
}, [datasource, query, withTemplateVariableOptions]);
|
||||
|
||||
const lang = { grammar: promqlGrammar, name: 'promql' };
|
||||
|
||||
@@ -208,23 +40,7 @@ export const PromQueryBuilder = React.memo<Props>((props) => {
|
||||
return (
|
||||
<>
|
||||
<EditorRow>
|
||||
<MetricSelect
|
||||
query={query}
|
||||
onChange={onChange}
|
||||
onGetMetrics={onGetMetrics}
|
||||
datasource={datasource}
|
||||
labelsFilters={query.labels}
|
||||
metricLookupDisabled={datasource.lookupsDisabled}
|
||||
/>
|
||||
<LabelFilters
|
||||
debounceDuration={datasource.getDebounceTimeInMilliseconds()}
|
||||
getLabelValuesAutofillSuggestions={getLabelValuesAutocompleteSuggestions}
|
||||
labelsFilters={query.labels}
|
||||
// eslint-ignore
|
||||
onChange={onChangeLabels as (labelFilters: Array<Partial<QueryBuilderLabelFilter>>) => void}
|
||||
onGetLabelNames={(forLabel) => withTemplateVariableOptions(onGetLabelNames(forLabel))}
|
||||
onGetLabelValues={(forLabel) => withTemplateVariableOptions(onGetLabelValues(forLabel))}
|
||||
/>
|
||||
<MetricsLabelsSection query={query} onChange={onChange} datasource={datasource} />
|
||||
</EditorRow>
|
||||
{initHints.length ? (
|
||||
<div className="query-row-break">
|
||||
@@ -288,39 +104,4 @@ export const PromQueryBuilder = React.memo<Props>((props) => {
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns list of metrics, either all or filtered by query param. It also adds description string to each metric if it
|
||||
* exists.
|
||||
* @param datasource
|
||||
* @param query
|
||||
*/
|
||||
async function getMetrics(
|
||||
datasource: PrometheusDatasource,
|
||||
query: PromVisualQuery
|
||||
): Promise<Array<{ value: string; description?: string }>> {
|
||||
// Makes sure we loaded the metadata for metrics. Usually this is done in the start() method of the provider but we
|
||||
// don't use it with the visual builder and there is no need to run all the start() setup anyway.
|
||||
if (!datasource.languageProvider.metricsMetadata) {
|
||||
await datasource.languageProvider.loadMetricsMetadata();
|
||||
}
|
||||
|
||||
// Error handling for when metrics metadata returns as undefined
|
||||
if (!datasource.languageProvider.metricsMetadata) {
|
||||
datasource.languageProvider.metricsMetadata = {};
|
||||
}
|
||||
|
||||
let metrics;
|
||||
if (query.labels.length > 0) {
|
||||
const expr = promQueryModeller.renderLabels(query.labels);
|
||||
metrics = (await datasource.languageProvider.getSeries(expr, true))['__name__'] ?? [];
|
||||
} else {
|
||||
metrics = (await datasource.languageProvider.getLabelValues('__name__')) ?? [];
|
||||
}
|
||||
|
||||
return metrics.map((m) => ({
|
||||
value: m,
|
||||
description: getMetadataString(m, datasource.languageProvider.metricsMetadata!),
|
||||
}));
|
||||
}
|
||||
|
||||
PromQueryBuilder.displayName = 'PromQueryBuilder';
|
||||
|
@@ -4,7 +4,7 @@ import { DataQuery } from '@grafana/schema';
|
||||
import { PromApplication } from '../../../types/unified-alerting-dto';
|
||||
|
||||
import { Prometheus as GenPromQuery } from './dataquery.gen';
|
||||
import { QueryEditorMode } from './querybuilder/shared/types';
|
||||
import { QueryBuilderLabelFilter, QueryEditorMode } from './querybuilder/shared/types';
|
||||
|
||||
export interface PromQuery extends GenPromQuery, DataQuery {
|
||||
/**
|
||||
@@ -191,6 +191,7 @@ export interface PromVariableQuery extends DataQuery {
|
||||
metric?: string;
|
||||
varQuery?: string;
|
||||
seriesQuery?: string;
|
||||
labelFilters?: QueryBuilderLabelFilter[];
|
||||
}
|
||||
|
||||
export type StandardPromVariableQuery = {
|
||||
|
Reference in New Issue
Block a user