mirror of
https://github.com/grafana/grafana.git
synced 2025-08-02 06:52:13 +08:00
GrafanaUI: Fix Combobox throwing error with too many items (#102452)
This commit is contained in:
103
packages/grafana-ui/src/components/Combobox/useOptions.test.ts
Normal file
103
packages/grafana-ui/src/components/Combobox/useOptions.test.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import { renderHook, act, waitFor } from '@testing-library/react';
|
||||
|
||||
import { useOptions } from './useOptions';
|
||||
|
||||
describe('useOptions', () => {
|
||||
it('should handle a large number of synchronous options without throwing an error', () => {
|
||||
const largeOptions = Array.from({ length: 1_000_000 }, (_, i) => ({
|
||||
label: `Option ${i + 1}`,
|
||||
value: `${i + 1}`,
|
||||
}));
|
||||
const { result } = renderHook(() => useOptions(largeOptions, false));
|
||||
|
||||
act(() => {
|
||||
result.current.updateOptions('Option 999999');
|
||||
});
|
||||
|
||||
expect(result.current.options).toEqual([{ label: 'Option 999999', value: '999999' }]);
|
||||
});
|
||||
|
||||
it('should return filtered options for synchronous options', () => {
|
||||
const options = [
|
||||
{ label: 'Option 1', value: '1' },
|
||||
{ label: 'Option 2', value: '2' },
|
||||
];
|
||||
const { result } = renderHook(() => useOptions(options, false));
|
||||
|
||||
act(() => {
|
||||
result.current.updateOptions('Option 1');
|
||||
});
|
||||
|
||||
expect(result.current.options).toEqual([{ label: 'Option 1', value: '1' }]);
|
||||
});
|
||||
|
||||
it('should handle asynchronous options', async () => {
|
||||
const asyncOptions = jest.fn().mockResolvedValue([
|
||||
{ label: 'Async Option 1', value: '1' },
|
||||
{ label: 'Async Option 2', value: '2' },
|
||||
]);
|
||||
const { result } = renderHook(() => useOptions(asyncOptions, false));
|
||||
|
||||
act(() => {
|
||||
result.current.updateOptions('Async');
|
||||
});
|
||||
|
||||
expect(result.current.asyncLoading).toBe(true);
|
||||
|
||||
await waitFor(() => expect(result.current.asyncLoading).toBe(false));
|
||||
|
||||
expect(result.current.options).toEqual([
|
||||
{ label: 'Async Option 1', value: '1' },
|
||||
{ label: 'Async Option 2', value: '2' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should add a custom value if enabled', () => {
|
||||
const options = [
|
||||
{ label: 'Apple', value: 'apple' },
|
||||
{ label: 'Carrot', value: 'carrot' },
|
||||
];
|
||||
const { result } = renderHook(() => useOptions(options, true));
|
||||
|
||||
act(() => {
|
||||
result.current.updateOptions('car');
|
||||
});
|
||||
|
||||
expect(result.current.options).toEqual([
|
||||
{ label: 'car', value: 'car', description: 'Use custom value' },
|
||||
{ label: 'Carrot', value: 'carrot' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not add a custom value if it already exists', () => {
|
||||
const options = [
|
||||
{ label: 'Apple', value: 'apple' },
|
||||
{ label: 'Carrot', value: 'carrot' },
|
||||
];
|
||||
const { result } = renderHook(() => useOptions(options, true));
|
||||
|
||||
act(() => {
|
||||
result.current.updateOptions('carrot');
|
||||
});
|
||||
|
||||
expect(result.current.options).toEqual([{ label: 'Carrot', value: 'carrot' }]);
|
||||
});
|
||||
|
||||
it('should handle errors in asynchronous options', async () => {
|
||||
jest.spyOn(console, 'error').mockImplementation();
|
||||
|
||||
const asyncOptions = jest.fn().mockRejectedValue(new Error('Async error'));
|
||||
const { result } = renderHook(() => useOptions(asyncOptions, false));
|
||||
|
||||
act(() => {
|
||||
result.current.updateOptions('Async');
|
||||
});
|
||||
|
||||
expect(result.current.asyncLoading).toBe(true);
|
||||
|
||||
await waitFor(() => expect(result.current.asyncLoading).toBe(false));
|
||||
|
||||
expect(result.current.asyncLoading).toBe(false);
|
||||
expect(result.current.asyncError).toBe(true);
|
||||
});
|
||||
});
|
@ -1,3 +1,6 @@
|
||||
/* Spreading unbound arrays can be very slow or even crash the browser if used for arguments */
|
||||
/* eslint no-restricted-syntax: ["error", "SpreadElement"] */
|
||||
|
||||
import { debounce } from 'lodash';
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
|
||||
@ -14,7 +17,7 @@ type AsyncOptions<T extends string | number> =
|
||||
const asyncNoop = () => Promise.resolve([]);
|
||||
|
||||
/**
|
||||
* Abstracts away sync/async options for MultiCombobox (and later Combobox).
|
||||
* Abstracts away sync/async options for combobox components.
|
||||
* It also filters options based on the user's input.
|
||||
*
|
||||
* Returns:
|
||||
@ -66,14 +69,11 @@ export function useOptions<T extends string | number>(rawOptions: AsyncOptions<T
|
||||
//we just focus on the value to check if the option already exists
|
||||
const customValueExists = opts.some((opt) => opt.value === userTypedSearch);
|
||||
if (!customValueExists) {
|
||||
currentOptions = [
|
||||
{
|
||||
label: userTypedSearch,
|
||||
value: userTypedSearch as T,
|
||||
description: t('combobox.custom-value.description', 'Use custom value'),
|
||||
},
|
||||
...currentOptions,
|
||||
];
|
||||
currentOptions.unshift({
|
||||
label: userTypedSearch,
|
||||
value: userTypedSearch as T,
|
||||
description: t('combobox.custom-value.description', 'Use custom value'),
|
||||
});
|
||||
}
|
||||
}
|
||||
return currentOptions;
|
||||
@ -131,21 +131,21 @@ function sortByGroup<T extends string | number>(options: Array<ComboboxOption<T>
|
||||
let currentIndex = 0;
|
||||
|
||||
// Reorganize options to have groups first, then undefined group
|
||||
const reorganizeOptions = [];
|
||||
let reorganizeOptions: Array<ComboboxOption<T>> = [];
|
||||
for (const [group, groupOptions] of groupedOptions) {
|
||||
if (!group) {
|
||||
continue;
|
||||
}
|
||||
|
||||
groupStartIndices.set(group, currentIndex);
|
||||
reorganizeOptions.push(...groupOptions);
|
||||
reorganizeOptions = reorganizeOptions.concat(groupOptions);
|
||||
currentIndex += groupOptions.length;
|
||||
}
|
||||
|
||||
const undefinedGroupOptions = groupedOptions.get(undefined);
|
||||
if (undefinedGroupOptions) {
|
||||
groupStartIndices.set('undefined', currentIndex);
|
||||
reorganizeOptions.push(...undefinedGroupOptions);
|
||||
reorganizeOptions = reorganizeOptions.concat(undefinedGroupOptions);
|
||||
}
|
||||
|
||||
return {
|
||||
|
Reference in New Issue
Block a user