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 <brendan.ohandley@grafana.com>
Co-authored-by: bohandley <brendan.ohandley@gmail.com>
This commit is contained in:
Galen Kistler 2023-08-25 11:58:35 -05:00 committed by GitHub
parent 70dc5610c0
commit 5ed3ddf344
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 125 additions and 17 deletions

View File

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

View File

@ -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<T>(array: T[], limit?: number): T[] {
if (limit === undefined) {
limit = PROMETHEUS_QUERY_BUILDER_MAX_RESULTS;
}
array.length = Math.min(array.length, limit);
return array;
}

View File

@ -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<QueryBuilderLabelFilter>;
@ -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,

View File

@ -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(<MetricSelect {...props} />);
await openMetricSelect();
await waitFor(() => expect(screen.getAllByLabelText('Select option')).toHaveLength(1000));
});
it('shows option to set custom value when typing', async () => {
render(<MetricSelect {...props} />);
await openMetricSelect();

View File

@ -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<HTMLDivElement>;
innerProps: {};
}
const CustomMenu = ({ children, maxHeight, innerRef, innerProps }: React.PropsWithChildren<SelectMenuProps>) => {
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 (
<div
{...innerProps}
className={`${stylesMenu.menu} ${styles.customMenuContainer}`}
style={{ maxHeight }}
aria-label="Select options menu"
>
<CustomScrollbar scrollRefCallback={innerRef} autoHide={false} autoHeightMax="inherit" hideHorizontalTrack>
{children}
</CustomScrollbar>
{optionsLoaded && (
<div className={styles.customMenuFooter}>
<div>
Only the top 1000 metrics are displayed in the metric select. Use the metrics explorer to view all
metrics.
</div>
</div>
)}
</div>
);
};
const asyncSelect = () => {
return (
<AsyncSelect
@ -219,8 +274,10 @@ export function MetricSelect({
setState({ isLoading: true });
const metrics = await onGetMetrics();
const initialMetrics: string[] = metrics.map((m) => 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 = (

View File

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