From 5ed3ddf3442780018e6fa8d1da6082476ac13657 Mon Sep 17 00:00:00 2001 From: Galen Kistler <109082771+gtk-grafana@users.noreply.github.com> Date: Fri, 25 Aug 2023 11:58:35 -0500 Subject: [PATCH] Prometheus: Fix dropdowns truncating from start of array (#73643) * move truncating to helper function, to start of array * Prometheus: Add 1000 result warning as sticky footer in metric select (#73649) add 1000 result warning as sticky footer in metric select * Prometheus: Only show truncated results warning if results have been truncated (#73650) only show truncated warning if results have been truncated * add test to metric select --------- Co-authored-by: Brendan O'Handley Co-authored-by: bohandley --- .../prometheus/language_utils.test.ts | 13 +++ .../datasource/prometheus/language_utils.ts | 9 ++ .../components/LabelFilterItem.tsx | 7 +- .../components/MetricSelect.test.tsx | 12 +++ .../querybuilder/components/MetricSelect.tsx | 94 +++++++++++++++++-- .../components/MetricsLabelsSection.tsx | 7 +- 6 files changed, 125 insertions(+), 17 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/language_utils.test.ts b/public/app/plugins/datasource/prometheus/language_utils.test.ts index a50e4e3b5b9..60151aa8840 100644 --- a/public/app/plugins/datasource/prometheus/language_utils.test.ts +++ b/public/app/plugins/datasource/prometheus/language_utils.test.ts @@ -11,6 +11,7 @@ import { getRangeSnapInterval, parseSelector, toPromLikeQuery, + truncateResult, } from './language_utils'; import { PrometheusCacheLevel } from './types'; @@ -474,3 +475,15 @@ describe('toPromLikeQuery', () => { }); }); }); + +describe('truncateResult', () => { + it('truncates array longer then 1k from the start of array', () => { + // creates an array of 1k + 1 elements with values from 0 to 1k + const array = Array.from(Array(1001).keys()); + expect(array[1000]).toBe(1000); + truncateResult(array); + expect(array.length).toBe(1000); + expect(array[0]).toBe(0); + expect(array[999]).toBe(999); + }); +}); diff --git a/public/app/plugins/datasource/prometheus/language_utils.ts b/public/app/plugins/datasource/prometheus/language_utils.ts index ba3a4f54fc1..8f024b030af 100644 --- a/public/app/plugins/datasource/prometheus/language_utils.ts +++ b/public/app/plugins/datasource/prometheus/language_utils.ts @@ -14,6 +14,7 @@ import { import { addLabelToQuery } from './add_label_to_query'; import { SUGGESTIONS_LIMIT } from './language_provider'; +import { PROMETHEUS_QUERY_BUILDER_MAX_RESULTS } from './querybuilder/components/MetricSelect'; import { PrometheusCacheLevel, PromMetricsMetadata, PromMetricsMetadataItem } from './types'; export const processHistogramMetrics = (metrics: string[]) => { @@ -416,3 +417,11 @@ export function getPrometheusTime(date: string | DateTime, roundUp: boolean) { return Math.ceil(date.valueOf() / 1000); } + +export function truncateResult(array: T[], limit?: number): T[] { + if (limit === undefined) { + limit = PROMETHEUS_QUERY_BUILDER_MAX_RESULTS; + } + array.length = Math.min(array.length, limit); + return array; +} diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/LabelFilterItem.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/LabelFilterItem.tsx index d6fb3e61995..8492059b533 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/LabelFilterItem.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/LabelFilterItem.tsx @@ -6,10 +6,9 @@ import { selectors } from '@grafana/e2e-selectors'; import { AccessoryButton, InputGroup } from '@grafana/experimental'; import { AsyncSelect, Select } from '@grafana/ui'; +import { truncateResult } from '../../language_utils'; import { QueryBuilderLabelFilter } from '../shared/types'; -import { PROMETHEUS_QUERY_BUILDER_MAX_RESULTS } from './MetricSelect'; - export interface Props { defaultOp: string; item: Partial; @@ -136,9 +135,7 @@ export function LabelFilterItem({ 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); - } + truncateResult(labelValues); setLabelValuesMenuOpen(true); setState({ ...state, diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/MetricSelect.test.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/MetricSelect.test.tsx index ade5c9d2c84..1d9a62cc7f6 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/MetricSelect.test.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/MetricSelect.test.tsx @@ -72,6 +72,18 @@ describe('MetricSelect', () => { await waitFor(() => expect(screen.getAllByLabelText('Select option')).toHaveLength(3)); }); + it('truncates list of metrics to 1000', async () => { + const manyMockValues = [...Array(1001).keys()].map((idx: number) => { + return { label: 'random_metric' + idx }; + }); + + props.onGetMetrics = jest.fn().mockResolvedValue(manyMockValues); + + render(); + await openMetricSelect(); + await waitFor(() => expect(screen.getAllByLabelText('Select option')).toHaveLength(1000)); + }); + it('shows option to set custom value when typing', async () => { render(); await openMetricSelect(); diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/MetricSelect.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/MetricSelect.tsx index 38010e21fdf..e97de0c3267 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/MetricSelect.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/MetricSelect.tsx @@ -1,15 +1,27 @@ import { css } from '@emotion/css'; import debounce from 'debounce-promise'; -import React, { useCallback, useState } from 'react'; +import React, { RefCallback, useCallback, useState } from 'react'; 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, InlineField, InlineFieldRow, useStyles2 } from '@grafana/ui'; +import { + AsyncSelect, + Button, + CustomScrollbar, + FormatOptionLabelMeta, + getSelectStyles, + Icon, + InlineField, + InlineFieldRow, + useStyles2, + useTheme2, +} from '@grafana/ui'; import { SelectMenuOptions } from '@grafana/ui/src/components/Select/SelectMenu'; import { PrometheusDatasource } from '../../datasource'; +import { truncateResult } from '../../language_utils'; import { regexifyLabelValuesQueryString } from '../shared/parsingUtils'; import { QueryBuilderLabelFilter } from '../shared/types'; import { PromVisualQuery } from '../types'; @@ -49,6 +61,7 @@ export function MetricSelect({ isLoading?: boolean; metricsModalOpen?: boolean; initialMetrics?: string[]; + resultsTruncated?: boolean; }>({}); const prometheusMetricEncyclopedia = config.featureToggles.prometheusMetricEncyclopedia; @@ -57,7 +70,7 @@ export function MetricSelect({ { value: 'BrowseMetrics', label: 'Metrics explorer', - description: 'Browse and filter metrics and metadata with a fuzzy search', + description: 'Browse and filter all metrics and metadata with a fuzzy search', }, ]; @@ -74,6 +87,7 @@ export function MetricSelect({ } const searchWords = searchQuery.split(splitSeparator); + return searchWords.reduce((acc, cur) => { const matcheSearch = label.toLowerCase().includes(cur.toLowerCase()); @@ -126,8 +140,13 @@ export function MetricSelect({ // Since some customers can have millions of metrics, whenever the user changes the autocomplete text we want to call the backend and request all metrics that match the current query string const results = datasource.metricFindQuery(formatKeyValueStringsForLabelValuesQuery(query, labelsFilters)); return results.then((results) => { - if (results.length > PROMETHEUS_QUERY_BUILDER_MAX_RESULTS) { - results.splice(0, results.length - PROMETHEUS_QUERY_BUILDER_MAX_RESULTS); + const resultsLength = results.length; + truncateResult(results); + + if (resultsLength > results.length) { + setState({ ...state, resultsTruncated: true }); + } else { + setState({ ...state, resultsTruncated: false }); } const resultsOptions = results.map((result) => { @@ -201,6 +220,42 @@ export function MetricSelect({ return SelectMenuOptions(props); }; + interface SelectMenuProps { + maxHeight: number; + innerRef: RefCallback; + innerProps: {}; + } + + const CustomMenu = ({ children, maxHeight, innerRef, innerProps }: React.PropsWithChildren) => { + const theme = useTheme2(); + const stylesMenu = getSelectStyles(theme); + + // Show the results trucated warning only if the options are loaded and the results are truncated + // The children are a react node(options loading node) or an array(not a valid element) + const optionsLoaded = !React.isValidElement(children) && state.resultsTruncated; + + return ( +
+ + {children} + + {optionsLoaded && ( +
+
+ Only the top 1000 metrics are displayed in the metric select. Use the metrics explorer to view all + metrics. +
+
+ )} +
+ ); + }; + const asyncSelect = () => { return ( m.value); + const resultsLength = metrics.length; + if (metrics.length > PROMETHEUS_QUERY_BUILDER_MAX_RESULTS) { - metrics.splice(0, metrics.length - PROMETHEUS_QUERY_BUILDER_MAX_RESULTS); + truncateResult(metrics); } if (prometheusMetricEncyclopedia) { @@ -230,9 +287,14 @@ export function MetricSelect({ isLoading: undefined, // pass the initial metrics into the metrics explorer initialMetrics: initialMetrics, + resultsTruncated: resultsLength > metrics.length, }); } else { - setState({ metrics, isLoading: undefined }); + setState({ + metrics, + isLoading: undefined, + resultsTruncated: resultsLength > metrics.length, + }); } }} loadOptions={metricLookupDisabled ? metricLookupDisabledSearch : debouncedSearch} @@ -252,7 +314,9 @@ export function MetricSelect({ onChange({ ...query, metric: '' }); } }} - components={prometheusMetricEncyclopedia ? { Option: CustomOption } : {}} + components={ + prometheusMetricEncyclopedia ? { Option: CustomOption, MenuList: CustomMenu } : { MenuList: CustomMenu } + } onBlur={onBlur ? onBlur : () => {}} /> ); @@ -324,6 +388,20 @@ const getStyles = (theme: GrafanaTheme2) => ({ customOptionWidth: css` min-width: 400px; `, + customMenuFooter: css` + flex: 0; + display: flex; + justify-content: space-between; + padding: ${theme.spacing(1.5)}; + border-top: 1px solid ${theme.colors.border.weak}; + color: ${theme.colors.text.secondary}; + `, + customMenuContainer: css` + display: flex; + flex-direction: column; + background: ${theme.colors.background.primary}; + box-shadow: ${theme.shadows.z3}; + `, }); export const formatPrometheusLabelFiltersToString = ( diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/MetricsLabelsSection.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/MetricsLabelsSection.tsx index addba18bc51..f6b4f26f47e 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/MetricsLabelsSection.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/MetricsLabelsSection.tsx @@ -4,13 +4,14 @@ import { SelectableValue } from '@grafana/data'; import { PrometheusDatasource } from '../../datasource'; import { getMetadataString } from '../../language_provider'; +import { truncateResult } from '../../language_utils'; 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'; +import { MetricSelect } from './MetricSelect'; export interface MetricsLabelsSectionProps { query: PromVisualQuery; @@ -108,9 +109,7 @@ export function MetricsLabelsSection({ } return response.then((response: SelectableValue[]) => { - if (response.length > PROMETHEUS_QUERY_BUILDER_MAX_RESULTS) { - response.splice(0, response.length - PROMETHEUS_QUERY_BUILDER_MAX_RESULTS); - } + truncateResult(response); return response; }); };