mirror of
https://github.com/grafana/grafana.git
synced 2025-02-11 08:05:43 -06:00
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:
parent
9400ccf478
commit
f2d4e24710
@ -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>>;
|
||||
|
@ -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;
|
||||
|
@ -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...')}
|
||||
|
@ -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 '}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -16,7 +16,6 @@ export interface Folder {
|
||||
|
||||
export interface RuleFolderPickerProps extends Omit<FolderPickerProps, 'initialTitle' | 'initialFolderId'> {
|
||||
value?: Folder;
|
||||
dissalowSlashes: boolean;
|
||||
}
|
||||
|
||||
const SlashesWarning = () => {
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user