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:
Galen Kistler 2022-11-09 13:54:51 -06:00 committed by GitHub
parent 000b4f878d
commit ee7348afee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 477 additions and 32 deletions

View File

@ -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"],

View File

@ -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 },
];

View File

@ -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/);
}

View File

@ -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>
);
}

View File

@ -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
*/

View File

@ -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}

View File

@ -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('');
};