mirror of
https://github.com/grafana/grafana.git
synced 2025-02-11 16:15:42 -06:00
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:
parent
70dc5610c0
commit
5ed3ddf344
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
|
@ -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 = (
|
||||
|
@ -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;
|
||||
});
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user