mirror of
https://github.com/grafana/grafana.git
synced 2024-11-21 16:38:03 -06:00
Prometheus: Fix label names select component when there are too many options (#92026)
* add more doc info for truncate function and how we use it * truncate label names and allow users to search all labels on typing * remove unused import * handle labels select in variable query in addition with truncated list
This commit is contained in:
parent
def8104e74
commit
68f545210d
@ -1,11 +1,13 @@
|
||||
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/components/VariableQueryEditor.tsx
|
||||
import debounce from 'debounce-promise';
|
||||
import { FormEvent, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { QueryEditorProps, SelectableValue } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { InlineField, InlineFieldRow, Input, Select, TextArea } from '@grafana/ui';
|
||||
import { AsyncSelect, InlineField, InlineFieldRow, Input, Select, TextArea } from '@grafana/ui';
|
||||
|
||||
import { PrometheusDatasource } from '../datasource';
|
||||
import { truncateResult } from '../language_utils';
|
||||
import {
|
||||
migrateVariableEditorBackToVariableSupport,
|
||||
migrateVariableQueryToEditor,
|
||||
@ -56,7 +58,20 @@ export const PromVariableQueryEditor = ({ onChange, query, datasource, range }:
|
||||
const [classicQuery, setClassicQuery] = useState('');
|
||||
|
||||
// list of label names for label_values(), /api/v1/labels, contains the same results as label_names() function
|
||||
const [labelOptions, setLabelOptions] = useState<Array<SelectableValue<string>>>([]);
|
||||
const [truncatedLabelOptions, setTruncatedLabelOptions] = useState<Array<SelectableValue<string>>>([]);
|
||||
const [allLabelOptions, setAllLabelOptions] = useState<Array<SelectableValue<string>>>([]);
|
||||
|
||||
/**
|
||||
* Set the both allLabels and truncatedLabels
|
||||
*
|
||||
* @param names
|
||||
* @param variables
|
||||
*/
|
||||
function setLabels(names: SelectableValue[], variables: SelectableValue[]) {
|
||||
setAllLabelOptions([...variables, ...names]);
|
||||
const truncatedNames = truncateResult(names);
|
||||
setTruncatedLabelOptions([...variables, ...truncatedNames]);
|
||||
}
|
||||
|
||||
// label filters have been added as a filter for metrics in label values query type
|
||||
const [labelFilters, setLabelFilters] = useState<QueryBuilderLabelFilter[]>([]);
|
||||
@ -100,7 +115,7 @@ export const PromVariableQueryEditor = ({ onChange, query, datasource, range }:
|
||||
// get all the labels
|
||||
datasource.getTagKeys({ filters: [] }).then((labelNames: Array<{ text: string }>) => {
|
||||
const names = labelNames.map(({ text }) => ({ label: text, value: text }));
|
||||
setLabelOptions([...variables, ...names]);
|
||||
setLabels(names, variables);
|
||||
});
|
||||
} else {
|
||||
// fetch the labels filtered by the metric
|
||||
@ -110,7 +125,7 @@ export const PromVariableQueryEditor = ({ onChange, query, datasource, range }:
|
||||
datasource.languageProvider.fetchLabelsWithMatch(expr).then((labelsIndex: Record<string, string[]>) => {
|
||||
const labelNames = Object.keys(labelsIndex);
|
||||
const names = labelNames.map((value) => ({ label: value, value: value }));
|
||||
setLabelOptions([...variables, ...names]);
|
||||
setLabels(names, variables);
|
||||
});
|
||||
}
|
||||
}, [datasource, qryType, metric]);
|
||||
@ -220,6 +235,18 @@ export const PromVariableQueryEditor = ({ onChange, query, datasource, range }:
|
||||
return { metric: metric, labels: labelFilters, operations: [] };
|
||||
}, [metric, labelFilters]);
|
||||
|
||||
/**
|
||||
* Debounce a search through all the labels possible and truncate by .
|
||||
*/
|
||||
const labelNamesSearch = debounce((query: string) => {
|
||||
// we limit the select to show 1000 options,
|
||||
// but we still search through all the possible options
|
||||
const results = allLabelOptions.filter((label) => {
|
||||
return label.value?.includes(query);
|
||||
});
|
||||
return truncateResult(results);
|
||||
}, 300);
|
||||
|
||||
return (
|
||||
<>
|
||||
<InlineFieldRow>
|
||||
@ -256,14 +283,15 @@ export const PromVariableQueryEditor = ({ onChange, query, datasource, range }:
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Select
|
||||
<AsyncSelect
|
||||
aria-label="label-select"
|
||||
onChange={onLabelChange}
|
||||
value={label}
|
||||
options={labelOptions}
|
||||
defaultOptions={truncatedLabelOptions}
|
||||
width={25}
|
||||
allowCustomValue
|
||||
isClearable={true}
|
||||
loadOptions={labelNamesSearch}
|
||||
data-testid={selectors.components.DataSource.Prometheus.variableQueryEditor.labelValues.labelSelect}
|
||||
/>
|
||||
</InlineField>
|
||||
|
@ -526,6 +526,16 @@ export function getPrometheusTime(date: string | DateTime, roundUp: boolean) {
|
||||
return Math.ceil(date.valueOf() / 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to truncate metrics, label names and label value in the query builder select components
|
||||
* to improve frontend performance. This is best used with an async select component including
|
||||
* the loadOptions property where we should still allow users to search all results with a string.
|
||||
* This can be done either storing the total results or querying an api that allows for matching a query.
|
||||
*
|
||||
* @param array
|
||||
* @param limit
|
||||
* @returns
|
||||
*/
|
||||
export function truncateResult<T>(array: T[], limit?: number): T[] {
|
||||
if (limit === undefined) {
|
||||
limit = PROMETHEUS_QUERY_BUILDER_MAX_RESULTS;
|
||||
|
@ -46,6 +46,7 @@ export function LabelFilterItem({
|
||||
// instead, we explicitly control the menu visibility and prevent showing it until the options have fully loaded
|
||||
const [labelNamesMenuOpen, setLabelNamesMenuOpen] = useState(false);
|
||||
const [labelValuesMenuOpen, setLabelValuesMenuOpen] = useState(false);
|
||||
const [allLabels, setAllLabels] = useState<SelectableValue[]>([]);
|
||||
|
||||
const isMultiSelect = (operator = item.op) => {
|
||||
return operators.find((op) => op.label === operator)?.isMultiValue;
|
||||
@ -73,13 +74,25 @@ export function LabelFilterItem({
|
||||
debounceDuration
|
||||
);
|
||||
|
||||
/**
|
||||
* Debounce a search through all the labels possible and truncate by .
|
||||
*/
|
||||
const labelNamesSearch = debounce((query: string) => {
|
||||
// we limit the select to show 1000 options,
|
||||
// but we still search through all the possible options
|
||||
const results = allLabels.filter((label) => {
|
||||
return label.value.includes(query);
|
||||
});
|
||||
return truncateResult(results);
|
||||
}, debounceDuration);
|
||||
|
||||
const itemValue = item?.value ?? '';
|
||||
|
||||
return (
|
||||
<div key={itemValue} data-testid="prometheus-dimensions-filter-item">
|
||||
<InputGroup>
|
||||
{/* Label name select, loads all values at once */}
|
||||
<Select
|
||||
<AsyncSelect
|
||||
placeholder="Select label"
|
||||
data-testid={selectors.components.QueryBuilder.labelSelect}
|
||||
inputId="prometheus-dimensions-filter-item-key"
|
||||
@ -89,15 +102,20 @@ export function LabelFilterItem({
|
||||
onOpenMenu={async () => {
|
||||
setState({ isLoadingLabelNames: true });
|
||||
const labelNames = await onGetLabelNames(item);
|
||||
// store all label names to allow for full label searching by typing in the select option, see loadOptions function labelNamesSearch
|
||||
setAllLabels(labelNames);
|
||||
setLabelNamesMenuOpen(true);
|
||||
setState({ labelNames, isLoadingLabelNames: undefined });
|
||||
// truncate the results the same amount as the metric select
|
||||
const truncatedLabelNames = truncateResult(labelNames);
|
||||
setState({ labelNames: truncatedLabelNames, isLoadingLabelNames: undefined });
|
||||
}}
|
||||
onCloseMenu={() => {
|
||||
setLabelNamesMenuOpen(false);
|
||||
}}
|
||||
isOpen={labelNamesMenuOpen}
|
||||
isLoading={state.isLoadingLabelNames ?? false}
|
||||
options={state.labelNames}
|
||||
loadOptions={labelNamesSearch}
|
||||
defaultOptions={state.labelNames}
|
||||
onChange={(change) => {
|
||||
if (change.label) {
|
||||
onChange({
|
||||
|
@ -1,14 +1,28 @@
|
||||
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/querybuilder/components/LabelFilters.test.tsx
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { ComponentProps } from 'react';
|
||||
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
import { selectOptionInTest } from '../../test/helpers/selectOptionInTest';
|
||||
import { getLabelSelects } from '../testUtils';
|
||||
|
||||
import { LabelFilters, MISSING_LABEL_FILTER_ERROR_MESSAGE, LabelFiltersProps } from './LabelFilters';
|
||||
|
||||
describe('LabelFilters', () => {
|
||||
it('truncates list of label names to 1000', async () => {
|
||||
const manyMockValues = [...Array(1001).keys()].map((idx: number) => {
|
||||
return { label: 'random_label' + idx };
|
||||
});
|
||||
|
||||
setup({ onGetLabelNames: jest.fn().mockResolvedValue(manyMockValues) });
|
||||
|
||||
await openLabelNamesSelect();
|
||||
|
||||
await waitFor(() => expect(screen.getAllByTestId(selectors.components.Select.option)).toHaveLength(1000));
|
||||
});
|
||||
|
||||
it('renders empty input without labels', async () => {
|
||||
setup();
|
||||
expect(screen.getAllByText('Select label')).toHaveLength(1);
|
||||
@ -162,3 +176,8 @@ function setup(propOverrides?: Partial<ComponentProps<typeof LabelFilters>>) {
|
||||
function getAddButton() {
|
||||
return screen.getByLabelText(/Add/);
|
||||
}
|
||||
|
||||
async function openLabelNamesSelect() {
|
||||
const select = screen.getByText('Select label').parentElement!;
|
||||
await userEvent.click(select);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user