mirror of
https://github.com/grafana/grafana.git
synced 2025-02-10 23:55:47 -06:00
Prometheus: Query builder label filters dropdown UI overload fix (#58266)
* WIP: try to support removing series endpoint for supported clients * add other filter variables to match param for label values query against filter values, in order to resolve bug in which filter value options would display that aren't relevant in the current query editor context, i.e. options would display that upon select would display no data * clean up console logs * refactor and comment * expanding current unit test coverage to cover calls to new API * fix unit test * whitespace * prettier * WIP: need to merge in other PR * WIP giving up and trying again * WIP: most functionality is working, split out shared loki/prom code * fix bug in which search results wouldn't take other label context into the query * Fix bug in which the previously selected value would conflict with the async search * interpolate the label name string instead of the match promql expression * remove type assertions * remove type assertion * clean up generic confusing types, and add back in a type assertion * remove generic type * make sure to interpolate label names * fix bugs with variables not interpolating before query * remove debug * assert partial properties on QueryBuilderLabelFilter * Force update betterer results :( * update regex so dropdown UX more closely matches current behavior * add eslint ignore * add eslint ignore and update betterer
This commit is contained in:
parent
000b4f878d
commit
ee7348afee
@ -6669,6 +6669,12 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||
],
|
||||
"public/app/plugins/datasource/prometheus/querybuilder/components/LabelFilterItem.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.", "2"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "3"]
|
||||
],
|
||||
"public/app/plugins/datasource/prometheus/querybuilder/components/LabelParamEditor.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
@ -6681,7 +6687,8 @@ 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.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
||||
],
|
||||
"public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderContainer.test.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
|
@ -0,0 +1,166 @@
|
||||
import debounce from 'debounce-promise';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { SelectableValue, toOption } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { AccessoryButton, InputGroup } from '@grafana/experimental';
|
||||
import { AsyncSelect, Select } from '@grafana/ui';
|
||||
|
||||
import { PROMETHEUS_QUERY_BUILDER_MAX_RESULTS } from '../components/MetricSelect';
|
||||
import { QueryBuilderLabelFilter } from '../shared/types';
|
||||
|
||||
export interface Props {
|
||||
defaultOp: string;
|
||||
item: Partial<QueryBuilderLabelFilter>;
|
||||
onChange: (value: QueryBuilderLabelFilter) => void;
|
||||
onGetLabelNames: (forLabel: Partial<QueryBuilderLabelFilter>) => Promise<SelectableValue[]>;
|
||||
onGetLabelValues: (forLabel: Partial<QueryBuilderLabelFilter>) => Promise<SelectableValue[]>;
|
||||
onDelete: () => void;
|
||||
invalidLabel?: boolean;
|
||||
invalidValue?: boolean;
|
||||
getLabelValuesAutofillSuggestions: (query: string, labelName?: string) => Promise<SelectableValue[]>;
|
||||
}
|
||||
|
||||
export function LabelFilterItem({
|
||||
item,
|
||||
defaultOp,
|
||||
onChange,
|
||||
onDelete,
|
||||
onGetLabelNames,
|
||||
onGetLabelValues,
|
||||
invalidLabel,
|
||||
invalidValue,
|
||||
getLabelValuesAutofillSuggestions,
|
||||
}: Props) {
|
||||
const [state, setState] = useState<{
|
||||
labelNames?: SelectableValue[];
|
||||
labelValues?: SelectableValue[];
|
||||
isLoadingLabelNames?: boolean;
|
||||
isLoadingLabelValues?: boolean;
|
||||
}>({});
|
||||
|
||||
const isMultiSelect = (operator = item.op) => {
|
||||
return operators.find((op) => op.label === operator)?.isMultiValue;
|
||||
};
|
||||
|
||||
const getSelectOptionsFromString = (item?: string): string[] => {
|
||||
if (item) {
|
||||
if (item.indexOf('|') > 0) {
|
||||
return item.split('|');
|
||||
}
|
||||
return [item];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const labelValueSearch = debounce((query: string) => getLabelValuesAutofillSuggestions(query, item.label), 350);
|
||||
|
||||
return (
|
||||
<div data-testid="prometheus-dimensions-filter-item">
|
||||
<InputGroup>
|
||||
{/* Label name select, loads all values at once */}
|
||||
<Select
|
||||
placeholder="Select label"
|
||||
aria-label={selectors.components.QueryBuilder.labelSelect}
|
||||
inputId="prometheus-dimensions-filter-item-key"
|
||||
width="auto"
|
||||
value={item.label ? toOption(item.label) : null}
|
||||
allowCustomValue
|
||||
onOpenMenu={async () => {
|
||||
setState({ isLoadingLabelNames: true });
|
||||
const labelNames = await onGetLabelNames(item);
|
||||
setState({ labelNames, isLoadingLabelNames: undefined });
|
||||
}}
|
||||
isLoading={state.isLoadingLabelNames ?? false}
|
||||
options={state.labelNames}
|
||||
onChange={(change) => {
|
||||
if (change.label) {
|
||||
onChange({
|
||||
...item,
|
||||
op: item.op ?? defaultOp,
|
||||
label: change.label,
|
||||
// eslint-ignore
|
||||
} as QueryBuilderLabelFilter);
|
||||
}
|
||||
}}
|
||||
invalid={invalidLabel}
|
||||
/>
|
||||
|
||||
{/* Operator select i.e. = =~ != !~ */}
|
||||
<Select
|
||||
aria-label={selectors.components.QueryBuilder.matchOperatorSelect}
|
||||
value={toOption(item.op ?? defaultOp)}
|
||||
options={operators}
|
||||
width="auto"
|
||||
onChange={(change) => {
|
||||
if (change.value != null) {
|
||||
onChange({
|
||||
...item,
|
||||
op: change.value,
|
||||
value: isMultiSelect(change.value) ? item.value : getSelectOptionsFromString(item?.value)[0],
|
||||
// eslint-ignore
|
||||
} as QueryBuilderLabelFilter);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Label value async select: autocomplete calls prometheus API */}
|
||||
<AsyncSelect
|
||||
placeholder="Select value"
|
||||
aria-label={selectors.components.QueryBuilder.valueSelect}
|
||||
inputId="prometheus-dimensions-filter-item-value"
|
||||
width="auto"
|
||||
value={
|
||||
isMultiSelect()
|
||||
? getSelectOptionsFromString(item?.value).map(toOption)
|
||||
: getSelectOptionsFromString(item?.value).map(toOption)[0]
|
||||
}
|
||||
allowCustomValue
|
||||
onOpenMenu={async () => {
|
||||
setState({ isLoadingLabelValues: true });
|
||||
const labelValues = await onGetLabelValues(item);
|
||||
if (labelValues.length > PROMETHEUS_QUERY_BUILDER_MAX_RESULTS) {
|
||||
labelValues.splice(0, labelValues.length - PROMETHEUS_QUERY_BUILDER_MAX_RESULTS);
|
||||
}
|
||||
setState({
|
||||
...state,
|
||||
labelValues,
|
||||
isLoadingLabelValues: undefined,
|
||||
});
|
||||
}}
|
||||
defaultOptions={state.labelValues}
|
||||
isMulti={isMultiSelect()}
|
||||
isLoading={state.isLoadingLabelValues}
|
||||
loadOptions={labelValueSearch}
|
||||
onChange={(change) => {
|
||||
if (change.value) {
|
||||
onChange({
|
||||
...item,
|
||||
value: change.value,
|
||||
op: item.op ?? defaultOp,
|
||||
// eslint-ignore
|
||||
} as QueryBuilderLabelFilter);
|
||||
} else {
|
||||
const changes = change
|
||||
.map((change: { label?: string }) => {
|
||||
return change.label;
|
||||
})
|
||||
.join('|');
|
||||
// eslint-ignore
|
||||
onChange({ ...item, value: changes, op: item.op ?? defaultOp } as QueryBuilderLabelFilter);
|
||||
}
|
||||
}}
|
||||
invalid={invalidValue}
|
||||
/>
|
||||
<AccessoryButton aria-label="remove" icon="times" variant="secondary" onClick={onDelete} />
|
||||
</InputGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const operators = [
|
||||
{ label: '=~', value: '=~', isMultiValue: true },
|
||||
{ label: '=', value: '=', isMultiValue: false },
|
||||
{ label: '!=', value: '!=', isMultiValue: false },
|
||||
{ label: '!~', value: '!~', isMultiValue: true },
|
||||
];
|
@ -0,0 +1,133 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React, { ComponentProps } from 'react';
|
||||
import { selectOptionInTest } from 'test/helpers/selectOptionInTest';
|
||||
|
||||
import { getLabelSelects } from '../testUtils';
|
||||
|
||||
import { LabelFilters, MISSING_LABEL_FILTER_ERROR_MESSAGE } from './LabelFilters';
|
||||
|
||||
describe('LabelFilters', () => {
|
||||
it('renders empty input without labels', async () => {
|
||||
setup();
|
||||
expect(screen.getAllByText('Select label')).toHaveLength(1);
|
||||
expect(screen.getAllByText('Select value')).toHaveLength(1);
|
||||
expect(screen.getByText(/=/)).toBeInTheDocument();
|
||||
expect(getAddButton()).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders multiple labels', async () => {
|
||||
setup({
|
||||
labelsFilters: [
|
||||
{ label: 'foo', op: '=', value: 'bar' },
|
||||
{ label: 'baz', op: '!=', value: 'qux' },
|
||||
{ label: 'quux', op: '=~', value: 'quuz' },
|
||||
],
|
||||
});
|
||||
expect(screen.getByText(/foo/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/bar/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/baz/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/qux/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/quux/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/quuz/)).toBeInTheDocument();
|
||||
expect(getAddButton()).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders multiple values for regex selectors', async () => {
|
||||
setup({
|
||||
labelsFilters: [
|
||||
{ label: 'bar', op: '!~', value: 'baz|bat|bau' },
|
||||
{ label: 'foo', op: '!~', value: 'fop|for|fos' },
|
||||
],
|
||||
});
|
||||
expect(screen.getByText(/bar/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/baz/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/bat/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/bau/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/foo/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/for/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/fos/)).toBeInTheDocument();
|
||||
expect(getAddButton()).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('adds new label', async () => {
|
||||
const { onChange } = setup({ labelsFilters: [{ label: 'foo', op: '=', value: 'bar' }] });
|
||||
await userEvent.click(getAddButton());
|
||||
expect(screen.getAllByText('Select label')).toHaveLength(1);
|
||||
expect(screen.getAllByText('Select value')).toHaveLength(1);
|
||||
const { name, value } = getLabelSelects(1);
|
||||
await selectOptionInTest(name, 'baz');
|
||||
await selectOptionInTest(value, 'qux');
|
||||
expect(onChange).toBeCalledWith([
|
||||
{ label: 'foo', op: '=', value: 'bar' },
|
||||
{ label: 'baz', op: '=', value: 'qux' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('removes label', async () => {
|
||||
const { onChange } = setup({ labelsFilters: [{ label: 'foo', op: '=', value: 'bar' }] });
|
||||
await userEvent.click(screen.getByLabelText(/remove/));
|
||||
expect(onChange).toBeCalledWith([]);
|
||||
});
|
||||
|
||||
it('renders empty input when labels are deleted from outside ', async () => {
|
||||
const { rerender } = setup({ labelsFilters: [{ label: 'foo', op: '=', value: 'bar' }] });
|
||||
expect(screen.getByText(/foo/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/bar/)).toBeInTheDocument();
|
||||
rerender(
|
||||
<LabelFilters
|
||||
onChange={jest.fn()}
|
||||
onGetLabelNames={jest.fn()}
|
||||
getLabelValuesAutofillSuggestions={jest.fn()}
|
||||
onGetLabelValues={jest.fn()}
|
||||
labelsFilters={[]}
|
||||
/>
|
||||
);
|
||||
expect(screen.getAllByText('Select label')).toHaveLength(1);
|
||||
expect(screen.getAllByText('Select value')).toHaveLength(1);
|
||||
expect(screen.getByText(/=/)).toBeInTheDocument();
|
||||
expect(getAddButton()).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows error when filter with empty strings and label filter is required', async () => {
|
||||
setup({ labelsFilters: [{ label: '', op: '=', value: '' }], labelFilterRequired: true });
|
||||
expect(screen.getByText(MISSING_LABEL_FILTER_ERROR_MESSAGE)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows error when no filter and label filter is required', async () => {
|
||||
setup({ labelsFilters: [], labelFilterRequired: true });
|
||||
expect(screen.getByText(MISSING_LABEL_FILTER_ERROR_MESSAGE)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
function setup(propOverrides?: Partial<ComponentProps<typeof LabelFilters>>) {
|
||||
const defaultProps = {
|
||||
onChange: jest.fn(),
|
||||
getLabelValues: jest.fn(),
|
||||
getLabelValuesAutofillSuggestions: async (query: string, labelName?: string) => [
|
||||
{ label: 'bar', value: 'bar' },
|
||||
{ label: 'qux', value: 'qux' },
|
||||
{ label: 'quux', value: 'quux' },
|
||||
],
|
||||
onGetLabelNames: async () => [
|
||||
{ label: 'foo', value: 'foo' },
|
||||
{ label: 'bar', value: 'bar' },
|
||||
{ label: 'baz', value: 'baz' },
|
||||
],
|
||||
onGetLabelValues: async () => [
|
||||
{ label: 'bar', value: 'bar' },
|
||||
{ label: 'qux', value: 'qux' },
|
||||
{ label: 'quux', value: 'quux' },
|
||||
],
|
||||
labelsFilters: [],
|
||||
};
|
||||
|
||||
const props = { ...defaultProps, ...propOverrides };
|
||||
|
||||
const { rerender } = render(<LabelFilters {...props} />);
|
||||
return { ...props, rerender };
|
||||
}
|
||||
|
||||
function getAddButton() {
|
||||
return screen.getByLabelText(/Add/);
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
import { isEqual } from 'lodash';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { EditorFieldGroup, EditorField, EditorList } from '@grafana/experimental';
|
||||
|
||||
import { QueryBuilderLabelFilter } from '../shared/types';
|
||||
|
||||
import { LabelFilterItem } from './LabelFilterItem';
|
||||
|
||||
export const MISSING_LABEL_FILTER_ERROR_MESSAGE = 'Select at least 1 label filter (label and value)';
|
||||
|
||||
export interface Props {
|
||||
labelsFilters: QueryBuilderLabelFilter[];
|
||||
onChange: (labelFilters: Array<Partial<QueryBuilderLabelFilter>>) => void;
|
||||
onGetLabelNames: (forLabel: Partial<QueryBuilderLabelFilter>) => Promise<SelectableValue[]>;
|
||||
onGetLabelValues: (forLabel: Partial<QueryBuilderLabelFilter>) => Promise<SelectableValue[]>;
|
||||
/** If set to true, component will show error message until at least 1 filter is selected */
|
||||
labelFilterRequired?: boolean;
|
||||
getLabelValuesAutofillSuggestions: (query: string, labelName?: string) => Promise<SelectableValue[]>;
|
||||
}
|
||||
|
||||
export function LabelFilters({
|
||||
labelsFilters,
|
||||
onChange,
|
||||
onGetLabelNames,
|
||||
onGetLabelValues,
|
||||
labelFilterRequired,
|
||||
getLabelValuesAutofillSuggestions,
|
||||
}: Props) {
|
||||
const defaultOp = '=';
|
||||
const [items, setItems] = useState<Array<Partial<QueryBuilderLabelFilter>>>([{ op: defaultOp }]);
|
||||
|
||||
useEffect(() => {
|
||||
if (labelsFilters.length > 0) {
|
||||
setItems(labelsFilters);
|
||||
} else {
|
||||
setItems([{ op: defaultOp }]);
|
||||
}
|
||||
}, [labelsFilters]);
|
||||
|
||||
const onLabelsChange = (newItems: Array<Partial<QueryBuilderLabelFilter>>) => {
|
||||
setItems(newItems);
|
||||
|
||||
// Extract full label filters with both label & value
|
||||
const newLabels = newItems.filter((x) => x.label != null && x.value != null);
|
||||
if (!isEqual(newLabels, labelsFilters)) {
|
||||
onChange(newLabels);
|
||||
}
|
||||
};
|
||||
|
||||
const hasLabelFilter = items.some((item) => item.label && item.value);
|
||||
|
||||
return (
|
||||
<EditorFieldGroup>
|
||||
<EditorField
|
||||
label="Label filters"
|
||||
error={MISSING_LABEL_FILTER_ERROR_MESSAGE}
|
||||
invalid={labelFilterRequired && !hasLabelFilter}
|
||||
>
|
||||
<EditorList
|
||||
items={items}
|
||||
onChange={onLabelsChange}
|
||||
renderItem={(item, onChangeItem, onDelete) => (
|
||||
<LabelFilterItem
|
||||
item={item}
|
||||
defaultOp={defaultOp}
|
||||
onChange={onChangeItem}
|
||||
onDelete={onDelete}
|
||||
onGetLabelNames={onGetLabelNames}
|
||||
onGetLabelValues={onGetLabelValues}
|
||||
invalidLabel={labelFilterRequired && !item.label}
|
||||
invalidValue={labelFilterRequired && !item.value}
|
||||
getLabelValuesAutofillSuggestions={getLabelValuesAutofillSuggestions}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</EditorField>
|
||||
</EditorFieldGroup>
|
||||
);
|
||||
}
|
@ -8,6 +8,7 @@ import { EditorField, EditorFieldGroup } from '@grafana/experimental';
|
||||
import { AsyncSelect, FormatOptionLabelMeta, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { PrometheusDatasource } from '../../datasource';
|
||||
import { regexifyLabelValuesQueryString } from '../shared/parsingUtils';
|
||||
import { QueryBuilderLabelFilter } from '../shared/types';
|
||||
import { PromVisualQuery } from '../types';
|
||||
|
||||
@ -82,14 +83,6 @@ export function MetricSelect({ datasource, query, onChange, onGetMetrics, labels
|
||||
}},__name__)`;
|
||||
};
|
||||
|
||||
/**
|
||||
* There aren't any spaces in the metric names, so let's introduce a wildcard into the regex for each space to better facilitate a fuzzy search
|
||||
*/
|
||||
const regexifyLabelValuesQueryString = (query: string) => {
|
||||
const queryArray = query.split(' ');
|
||||
return queryArray.map((query) => `${query}.*`).join('');
|
||||
};
|
||||
|
||||
/**
|
||||
* Reformat the query string and label filters to return all valid results for current query editor state
|
||||
*/
|
||||
|
@ -8,17 +8,18 @@ import { getMetadataString } from '../../language_provider';
|
||||
import promqlGrammar from '../../promql';
|
||||
import { promQueryModeller } from '../PromQueryModeller';
|
||||
import { buildVisualQueryFromString } from '../parsing';
|
||||
import { LabelFilters } from '../shared/LabelFilters';
|
||||
import { OperationExplainedBox } from '../shared/OperationExplainedBox';
|
||||
import { OperationList } from '../shared/OperationList';
|
||||
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 { PromVisualQuery } from '../types';
|
||||
|
||||
import { MetricSelect } from './MetricSelect';
|
||||
import { LabelFilters } from './LabelFilters';
|
||||
import { MetricSelect, PROMETHEUS_QUERY_BUILDER_MAX_RESULTS } from './MetricSelect';
|
||||
import { NestedQueryList } from './NestedQueryList';
|
||||
import { EXPLAIN_LABEL_FILTER_CONTENT } from './PromQueryBuilderExplained';
|
||||
|
||||
@ -42,7 +43,7 @@ export const PromQueryBuilder = React.memo<Props>((props) => {
|
||||
* Map metric metadata to SelectableValue for Select component and also adds defined template variables to the list.
|
||||
*/
|
||||
const withTemplateVariableOptions = useCallback(
|
||||
async (optionsPromise: Promise<Array<{ value: string; description?: string }>>): Promise<SelectableValue[]> => {
|
||||
async (optionsPromise: Promise<SelectableValue[]>): Promise<SelectableValue[]> => {
|
||||
const variables = datasource.getVariables();
|
||||
const options = await optionsPromise;
|
||||
return [
|
||||
@ -58,7 +59,7 @@ export const PromQueryBuilder = React.memo<Props>((props) => {
|
||||
* Formats a promQL expression and passes that off to helper functions depending on API support
|
||||
* @param forLabel
|
||||
*/
|
||||
const onGetLabelNames = async (forLabel: Partial<QueryBuilderLabelFilter>): Promise<Array<{ value: string }>> => {
|
||||
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!
|
||||
@ -83,18 +84,65 @@ export const PromQueryBuilder = React.memo<Props>((props) => {
|
||||
.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 = async (forLabel: Partial<QueryBuilderLabelFilter>, promQLExpression: string) => {
|
||||
const getLabelValuesFromSeriesAPI = (
|
||||
forLabel: Partial<QueryBuilderLabelFilter>,
|
||||
promQLExpression: string
|
||||
): Promise<SelectableValue[]> => {
|
||||
if (!forLabel.label) {
|
||||
return [];
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
const result = await datasource.languageProvider.fetchSeriesLabels(promQLExpression);
|
||||
const result = datasource.languageProvider.fetchSeries(promQLExpression);
|
||||
const forLabelInterpolated = datasource.interpolateString(forLabel.label);
|
||||
return result[forLabelInterpolated].map((v) => ({ value: v })) ?? [];
|
||||
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 }));
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@ -102,16 +150,19 @@ export const PromQueryBuilder = React.memo<Props>((props) => {
|
||||
* @param forLabel
|
||||
* @param promQLExpression
|
||||
*/
|
||||
const getLabelValuesFromLabelValuesAPI = async (
|
||||
const getLabelValuesFromLabelValuesAPI = (
|
||||
forLabel: Partial<QueryBuilderLabelFilter>,
|
||||
promQLExpression: string
|
||||
) => {
|
||||
): Promise<SelectableValue[]> => {
|
||||
if (!forLabel.label) {
|
||||
return [];
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
return (await datasource.languageProvider.fetchSeriesValues(forLabel.label, promQLExpression)).map((v) => ({
|
||||
value: v,
|
||||
}));
|
||||
return datasource.languageProvider.fetchSeriesValues(forLabel.label, promQLExpression).then((response) => {
|
||||
return response.map((v) => ({
|
||||
value: v,
|
||||
label: v,
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@ -119,7 +170,7 @@ export const PromQueryBuilder = React.memo<Props>((props) => {
|
||||
* Formats a promQL expression and passes that into helper functions depending on API support
|
||||
* @param forLabel
|
||||
*/
|
||||
const onGetLabelValues = async (forLabel: Partial<QueryBuilderLabelFilter>) => {
|
||||
const onGetLabelValues = async (forLabel: Partial<QueryBuilderLabelFilter>): Promise<SelectableValue[]> => {
|
||||
if (!forLabel.label) {
|
||||
return [];
|
||||
}
|
||||
@ -130,7 +181,14 @@ export const PromQueryBuilder = React.memo<Props>((props) => {
|
||||
|
||||
const labelsToConsider = query.labels.filter((x) => x !== forLabel);
|
||||
labelsToConsider.push({ label: '__name__', op: '=', value: query.metric });
|
||||
const expr = promQueryModeller.renderLabels(labelsToConsider);
|
||||
|
||||
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);
|
||||
@ -156,14 +214,12 @@ export const PromQueryBuilder = React.memo<Props>((props) => {
|
||||
labelsFilters={query.labels}
|
||||
/>
|
||||
<LabelFilters
|
||||
getLabelValuesAutofillSuggestions={getLabelValuesAutocompleteSuggestions}
|
||||
labelsFilters={query.labels}
|
||||
onChange={onChangeLabels}
|
||||
onGetLabelNames={(forLabel: Partial<QueryBuilderLabelFilter>) =>
|
||||
withTemplateVariableOptions(onGetLabelNames(forLabel))
|
||||
}
|
||||
onGetLabelValues={(forLabel: Partial<QueryBuilderLabelFilter>) =>
|
||||
withTemplateVariableOptions(onGetLabelValues(forLabel))
|
||||
}
|
||||
// eslint-ignore
|
||||
onChange={onChangeLabels as (labelFilters: Array<Partial<QueryBuilderLabelFilter>>) => void}
|
||||
onGetLabelNames={(forLabel) => withTemplateVariableOptions(onGetLabelNames(forLabel))}
|
||||
onGetLabelValues={(forLabel) => withTemplateVariableOptions(onGetLabelValues(forLabel))}
|
||||
/>
|
||||
</EditorRow>
|
||||
{showExplain && (
|
||||
@ -177,6 +233,7 @@ export const PromQueryBuilder = React.memo<Props>((props) => {
|
||||
<OperationsEditorRow>
|
||||
<OperationList<PromVisualQuery>
|
||||
queryModeller={promQueryModeller}
|
||||
// eslint-ignore
|
||||
datasource={datasource as DataSourceApi}
|
||||
query={query}
|
||||
onChange={onChange}
|
||||
|
@ -200,3 +200,11 @@ function jsonToText(
|
||||
function nodeToString(expr: string, node: SyntaxNode) {
|
||||
return node.name + ': ' + getString(expr, node);
|
||||
}
|
||||
|
||||
/**
|
||||
* There aren't any spaces in the metric names, so let's introduce a wildcard into the regex for each space to better facilitate a fuzzy search
|
||||
*/
|
||||
export const regexifyLabelValuesQueryString = (query: string) => {
|
||||
const queryArray = query.split(' ');
|
||||
return queryArray.map((query) => `${query}.*`).join('');
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user