mirror of
https://github.com/grafana/grafana.git
synced 2025-08-02 06:12:59 +08:00
Combobox: Refactor sortByGroup for performance (#102664)
* first pass at improving perf of sortByGroup * polish up
This commit is contained in:
@ -1,6 +1,6 @@
|
||||
import { renderHook, act, waitFor } from '@testing-library/react';
|
||||
|
||||
import { useOptions } from './useOptions';
|
||||
import { sortByGroup, useOptions } from './useOptions';
|
||||
|
||||
describe('useOptions', () => {
|
||||
it('should handle a large number of synchronous options without throwing an error', () => {
|
||||
@ -101,3 +101,56 @@ describe('useOptions', () => {
|
||||
expect(result.current.asyncError).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sortByGroup', () => {
|
||||
it('should return original array when no groups exist', () => {
|
||||
const options = [
|
||||
{ label: 'Apple', value: 'apple' },
|
||||
{ label: 'Banana', value: 'banana' },
|
||||
{ label: 'Carrot', value: 'carrot' },
|
||||
];
|
||||
|
||||
const { options: sortedOptions, groupStartIndices } = sortByGroup(options);
|
||||
|
||||
expect(sortedOptions).toBe(options); // Check reference equality
|
||||
expect(groupStartIndices.size).toBe(0);
|
||||
});
|
||||
|
||||
it('should return original array when only one group exists', () => {
|
||||
const options = [
|
||||
{ label: 'Apple', value: 'apple', group: 'fruits' },
|
||||
{ label: 'Banana', value: 'banana', group: 'fruits' },
|
||||
{ label: 'Tomato', value: 'tomato', group: 'fruits' },
|
||||
];
|
||||
|
||||
const { options: sortedOptions, groupStartIndices } = sortByGroup(options);
|
||||
|
||||
expect(sortedOptions).toEqual(options);
|
||||
expect(groupStartIndices.size).toBe(1);
|
||||
expect(groupStartIndices.get('fruits')).toBe(0);
|
||||
});
|
||||
|
||||
it('should group options and track group start indices', () => {
|
||||
const options = [
|
||||
{ label: 'Apple', value: 'apple', group: 'fruits' },
|
||||
{ label: 'Carrot', value: 'carrot', group: 'vegetables' },
|
||||
{ label: 'Banana', value: 'banana', group: 'fruits' },
|
||||
{ label: 'Celery', value: 'celery', group: 'vegetables' },
|
||||
{ label: 'Other', value: 'other' }, // Ungrouped
|
||||
];
|
||||
|
||||
const { options: sortedOptions, groupStartIndices } = sortByGroup(options);
|
||||
|
||||
expect(sortedOptions).toEqual([
|
||||
{ label: 'Apple', value: 'apple', group: 'fruits' },
|
||||
{ label: 'Banana', value: 'banana', group: 'fruits' },
|
||||
{ label: 'Carrot', value: 'carrot', group: 'vegetables' },
|
||||
{ label: 'Celery', value: 'celery', group: 'vegetables' },
|
||||
{ label: 'Other', value: 'other' },
|
||||
]);
|
||||
|
||||
expect(groupStartIndices.size).toBe(2);
|
||||
expect(groupStartIndices.get('fruits')).toBe(0);
|
||||
expect(groupStartIndices.get('vegetables')).toBe(2);
|
||||
});
|
||||
});
|
||||
|
@ -115,41 +115,61 @@ export function useOptions<T extends string | number>(rawOptions: AsyncOptions<T
|
||||
return { options: finalOptions, groupStartIndices, updateOptions, asyncLoading, asyncError };
|
||||
}
|
||||
|
||||
function sortByGroup<T extends string | number>(options: Array<ComboboxOption<T>>) {
|
||||
/**
|
||||
* Sorts options by group and returns the sorted options and the starting index of each group
|
||||
*/
|
||||
export function sortByGroup<T extends string | number>(options: Array<ComboboxOption<T>>) {
|
||||
// Group options by their group
|
||||
const groupedOptions = new Map<string | undefined, Array<ComboboxOption<T>>>();
|
||||
const groupStartIndices = new Map<string | undefined, number>();
|
||||
|
||||
for (const option of options) {
|
||||
const groupExists = groupedOptions.has(option.group);
|
||||
if (groupExists) {
|
||||
groupedOptions.get(option.group)?.push(option);
|
||||
const group = option.group;
|
||||
const existing = groupedOptions.get(group);
|
||||
if (existing) {
|
||||
existing.push(option);
|
||||
} else {
|
||||
groupedOptions.set(option.group, [option]);
|
||||
groupedOptions.set(group, [option]);
|
||||
}
|
||||
}
|
||||
|
||||
// Create a map to track the starting index of each group
|
||||
const groupStartIndices = new Map<string, number>();
|
||||
// If we only have one group (either the undefined group, or a single group), return the original array
|
||||
if (groupedOptions.size <= 1) {
|
||||
if (options[0]?.group) {
|
||||
groupStartIndices.set(options[0]?.group, 0);
|
||||
}
|
||||
|
||||
return {
|
||||
options,
|
||||
groupStartIndices,
|
||||
};
|
||||
}
|
||||
|
||||
// 'Preallocate' result array with same size as input - very minor optimization
|
||||
const result: Array<ComboboxOption<T>> = new Array(options.length);
|
||||
|
||||
let currentIndex = 0;
|
||||
|
||||
// Reorganize options to have groups first, then undefined group
|
||||
let reorganizeOptions: Array<ComboboxOption<T>> = [];
|
||||
// Fill result array with grouped options
|
||||
for (const [group, groupOptions] of groupedOptions) {
|
||||
if (!group) {
|
||||
continue;
|
||||
if (group) {
|
||||
groupStartIndices.set(group, currentIndex);
|
||||
for (const option of groupOptions) {
|
||||
result[currentIndex++] = option;
|
||||
}
|
||||
}
|
||||
|
||||
groupStartIndices.set(group, currentIndex);
|
||||
reorganizeOptions = reorganizeOptions.concat(groupOptions);
|
||||
currentIndex += groupOptions.length;
|
||||
}
|
||||
|
||||
const undefinedGroupOptions = groupedOptions.get(undefined);
|
||||
if (undefinedGroupOptions) {
|
||||
groupStartIndices.set('undefined', currentIndex);
|
||||
reorganizeOptions = reorganizeOptions.concat(undefinedGroupOptions);
|
||||
// Add ungrouped options at the end
|
||||
const ungrouped = groupedOptions.get(undefined);
|
||||
if (ungrouped) {
|
||||
for (const option of ungrouped) {
|
||||
result[currentIndex++] = option;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
options: reorganizeOptions,
|
||||
options: result,
|
||||
groupStartIndices,
|
||||
};
|
||||
}
|
||||
|
Reference in New Issue
Block a user