From 88ae7edcc95801a1ee25aabf5f661cb7ab2a7b41 Mon Sep 17 00:00:00 2001 From: "grafana-delivery-bot[bot]" <132647405+grafana-delivery-bot[bot]@users.noreply.github.com> Date: Fri, 16 Aug 2024 22:14:02 +0300 Subject: [PATCH] [v11.2.x] Prometheus: Fix label names select component when there are too many options (#92031) 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 (cherry picked from commit 68f545210d18c8b7b1d5c24b725ba8b58f94b7b9) Co-authored-by: Brendan O'Handley --- .../src/components/VariableQueryEditor.tsx | 40 ++++++++++++++++--- .../grafana-prometheus/src/language_utils.ts | 10 +++++ .../components/LabelFilterItem.tsx | 24 +++++++++-- .../components/LabelFilters.test.tsx | 21 +++++++++- 4 files changed, 85 insertions(+), 10 deletions(-) diff --git a/packages/grafana-prometheus/src/components/VariableQueryEditor.tsx b/packages/grafana-prometheus/src/components/VariableQueryEditor.tsx index ad6a2c5386b..6c1c470c69d 100644 --- a/packages/grafana-prometheus/src/components/VariableQueryEditor.tsx +++ b/packages/grafana-prometheus/src/components/VariableQueryEditor.tsx @@ -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>>([]); + const [truncatedLabelOptions, setTruncatedLabelOptions] = useState>>([]); + const [allLabelOptions, setAllLabelOptions] = useState>>([]); + + /** + * 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([]); @@ -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) => { 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 ( <> @@ -256,14 +283,15 @@ export const PromVariableQueryEditor = ({ onChange, query, datasource, range }: } > - { 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({ diff --git a/packages/grafana-prometheus/src/querybuilder/components/LabelFilters.test.tsx b/packages/grafana-prometheus/src/querybuilder/components/LabelFilters.test.tsx index 8647feee695..00bb07331e1 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/LabelFilters.test.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/LabelFilters.test.tsx @@ -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>) { function getAddButton() { return screen.getByLabelText(/Add/); } + +async function openLabelNamesSelect() { + const select = screen.getByText('Select label').parentElement!; + await userEvent.click(select); +}