Alerting: improve FolderPicker and Evaluation Group Select for Huge lists (#61221)

* Add AsyncVirtualizedSelect component in grafana-ui

* Slice FolderPicker results to 1k and virtualize list using AsyncVirtualizedSelect

* Group list in alert form: Use AsyncSelect and slice results to 1000

* Virtualize and slice always in FolderPicker: set default sliceResults value and let consumers change this value

* Always slice results in FolderPicker

* Limit folder results setting the limit in the api call instead slicing in the FE

* Remove warning about the limit in the Select label
This commit is contained in:
Sonia Aguilar 2023-01-12 11:45:03 +01:00 committed by GitHub
parent 9400ccf478
commit f2d4e24710
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 64 additions and 29 deletions

View File

@ -4,7 +4,13 @@ import { SelectableValue } from '@grafana/data';
import { SelectBase } from './SelectBase';
import { SelectContainer, SelectContainerProps } from './SelectContainer';
import { SelectCommonProps, MultiSelectCommonProps, SelectAsyncProps, VirtualizedSelectProps } from './types';
import {
SelectCommonProps,
MultiSelectCommonProps,
SelectAsyncProps,
VirtualizedSelectProps,
VirtualizedSelectAsyncProps,
} from './types';
export function Select<T>(props: SelectCommonProps<T>) {
return <SelectBase {...props} />;
@ -28,6 +34,10 @@ export function VirtualizedSelect<T>(props: VirtualizedSelectProps<T>) {
return <SelectBase virtualized {...props} />;
}
export function AsyncVirtualizedSelect<T>(props: VirtualizedSelectAsyncProps<T>) {
return <SelectBase virtualized {...props} />;
}
interface AsyncMultiSelectProps<T> extends Omit<MultiSelectCommonProps<T>, 'options'>, SelectAsyncProps<T> {
// AsyncSelect has options stored internally. We cannot enable plain values as we don't have access to the fetched options
value?: Array<SelectableValue<T>>;

View File

@ -112,6 +112,11 @@ export interface VirtualizedSelectProps<T> extends Omit<SelectCommonProps<T>, 'v
options?: Array<Pick<SelectableValue<T>, 'label' | 'value'>>;
}
/** The AsyncVirtualizedSelect component uses a slightly different SelectableValue, description and other props are not supported */
export interface VirtualizedSelectAsyncProps<T>
extends Omit<SelectCommonProps<T>, 'virtualized'>,
SelectAsyncProps<T> {}
export interface MultiSelectCommonProps<T> extends Omit<SelectCommonProps<T>, 'onChange' | 'isMulti' | 'value'> {
value?: Array<SelectableValue<T>> | T[];
onChange: (item: Array<SelectableValue<T>>) => {} | void;

View File

@ -5,7 +5,7 @@ import { useAsync } from 'react-use';
import { AppEvents, SelectableValue, GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { useStyles2, ActionMeta, AsyncSelect, Input, InputActionMeta } from '@grafana/ui';
import { useStyles2, ActionMeta, Input, InputActionMeta, AsyncVirtualizedSelect } from '@grafana/ui';
import appEvents from 'app/core/app_events';
import { t } from 'app/core/internationalization';
import { contextSrv } from 'app/core/services/context_srv';
@ -90,7 +90,8 @@ export function FolderPicker(props: Props) {
const getOptions = useCallback(
async (query: string) => {
const searchHits = await searchFolders(query, permissionLevel, accessControlMetadata);
const options: Array<SelectableValue<string>> = mapSearchHitsToOptions(searchHits, filter);
const resultsAfterMapAndFilter = mapSearchHitsToOptions(searchHits, filter);
const options: Array<SelectableValue<string>> = resultsAfterMapAndFilter;
const hasAccess =
contextSrv.hasAccess(AccessControlAction.DashboardsWrite, contextSrv.isEditor) ||
@ -322,7 +323,7 @@ export function FolderPicker(props: Props) {
return (
<div data-testid={selectors.components.FolderPicker.containerV2}>
<FolderWarningWhenSearching />
<AsyncSelect
<AsyncVirtualizedSelect
inputId={inputId}
aria-label={selectors.components.FolderPicker.input}
loadingMessage={t('folder-picker.loading', 'Loading folders...')}

View File

@ -1,10 +1,11 @@
import { css } from '@emotion/css';
import { debounce } from 'lodash';
import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
import { useFormContext } from 'react-hook-form';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { Stack } from '@grafana/experimental';
import { Field, InputControl, Label, LoadingPlaceholder, useStyles2 } from '@grafana/ui';
import { AsyncSelect, Field, InputControl, Label, useStyles2, LoadingPlaceholder } from '@grafana/ui';
import { FolderPickerFilter } from 'app/core/components/Select/FolderPicker';
import { contextSrv } from 'app/core/core';
import { DashboardSearchHit } from 'app/features/search/types';
@ -20,9 +21,10 @@ import { InfoIcon } from '../InfoIcon';
import { getIntervalForGroup } from './GrafanaEvaluationBehavior';
import { containsSlashes, Folder, RuleFolderPicker } from './RuleFolderPicker';
import { SelectWithAdd } from './SelectWIthAdd';
import { checkForPathSeparator } from './util';
export const SLICE_GROUP_RESULTS_TO = 1000;
const useGetGroups = (groupfoldersForGrafana: RulerRulesConfigDTO | null | undefined, folderName: string) => {
const groupOptions = useMemo(() => {
const groupsForFolderResult: Array<RulerRuleGroupDTO<RulerRuleDTO>> = groupfoldersForGrafana
@ -104,22 +106,20 @@ export function FolderAndGroup({ initialFolder }: FolderAndGroupProps) {
formState: { errors },
watch,
control,
setValue,
} = useFormContext<RuleFormValues>();
const styles = useStyles2(getStyles);
const dispatch = useDispatch();
const folderFilter = useRuleFolderFilter(initialFolder);
const [isAddingGroup, setIsAddingGroup] = useState(false);
const folder = watch('folder');
const group = watch('group');
const [selectedGroup, setSelectedGroup] = useState(group);
const [selectedGroup, setSelectedGroup] = useState<SelectableValue<string>>({ label: group, title: group });
const initialRender = useRef(true);
const { groupOptions, loading } = useGetGroupOptionsFromFolder(folder?.title ?? '');
useEffect(() => setSelectedGroup(group), [group, setSelectedGroup]);
useEffect(() => setSelectedGroup({ label: group, title: group }), [group, setSelectedGroup]);
useEffect(() => {
dispatch(fetchRulerRulesIfNotFetchedYet(GRAFANA_RULES_SOURCE_NAME));
@ -127,21 +127,37 @@ export function FolderAndGroup({ initialFolder }: FolderAndGroupProps) {
const resetGroup = useCallback(() => {
if (group && !initialRender.current && folder?.title) {
setSelectedGroup('');
setSelectedGroup({ label: '', title: '' });
}
initialRender.current = false;
}, [group, folder?.title]);
useEffect(() => {
setValue('group', selectedGroup);
}, [selectedGroup, setValue]);
const groupIsInGroupOptions = useCallback(
(group_: string) => {
return groupOptions.includes((groupInList: SelectableValue<string>) => groupInList.label === group_);
},
[groupOptions]
);
const sliceResults = (list: Array<SelectableValue<string>>) => list.slice(0, SLICE_GROUP_RESULTS_TO);
const getOptions = useCallback(
async (query: string) => {
const results = query
? sliceResults(
groupOptions.filter((el) => {
const label = el.label ?? '';
return label.toLowerCase().includes(query.toLowerCase());
})
)
: sliceResults(groupOptions);
return results;
},
[groupOptions]
);
const debouncedSearch = useMemo(() => {
return debounce(getOptions, 300, { leading: true });
}, [getOptions]);
return (
<div className={styles.container}>
@ -171,11 +187,9 @@ export function FolderAndGroup({ initialFolder }: FolderAndGroupProps) {
enableCreateNew={contextSrv.hasPermission(AccessControlAction.FoldersCreate)}
enableReset={true}
filter={folderFilter}
dissalowSlashes={true}
onChange={({ title, uid }) => {
field.onChange({ title, uid });
if (!groupIsInGroupOptions(selectedGroup)) {
setIsAddingGroup(false);
if (!groupIsInGroupOptions(selectedGroup.value ?? '')) {
resetGroup();
}
}}
@ -204,20 +218,23 @@ export function FolderAndGroup({ initialFolder }: FolderAndGroupProps) {
loading ? (
<LoadingPlaceholder text="Loading..." />
) : (
<SelectWithAdd
<AsyncSelect
disabled={!folder}
key={`my_unique_select_key__${folder?.title ?? ''}`}
inputId="group"
key={`my_unique_select_key__${selectedGroup?.title ?? ''}`}
{...field}
options={groupOptions}
loadOptions={debouncedSearch}
loadingMessage={'Loading groups...'}
defaultOptions={groupOptions}
defaultValue={selectedGroup}
getOptionLabel={(option: SelectableValue<string>) => `${option.label}`}
value={selectedGroup}
custom={isAddingGroup}
onCustomChange={(custom: boolean) => setIsAddingGroup(custom)}
placeholder={isAddingGroup ? 'New evaluation group name' : 'Evaluation group name'}
onChange={(value: string) => {
field.onChange(value);
setSelectedGroup(value);
placeholder={'Evaluation group name'}
onChange={(value) => {
field.onChange(value.label ?? '');
}}
value={selectedGroup}
allowCustomValue
formatCreateLabel={(_) => '+ Add new '}
/>
)
}

View File

@ -16,7 +16,6 @@ export interface Folder {
export interface RuleFolderPickerProps extends Omit<FolderPickerProps, 'initialTitle' | 'initialFolderId'> {
value?: Folder;
dissalowSlashes: boolean;
}
const SlashesWarning = () => {

View File

@ -286,6 +286,8 @@ export function createFolder(payload: any) {
return getBackendSrv().post('/api/folders', payload);
}
export const SLICE_FOLDER_RESULTS_TO = 1000;
export function searchFolders(
query: any,
permission?: PermissionLevelString,
@ -296,6 +298,7 @@ export function searchFolders(
type: 'dash-folder',
permission,
accesscontrol: withAccessControl,
limit: SLICE_FOLDER_RESULTS_TO,
});
}