mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
OptionsPicker: Use fuzzy search and improve ranking of matches (#79286)
This commit is contained in:
@@ -774,6 +774,39 @@ describe('optionsPickerReducer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when similar data for updateOptionsAndFilter', () => {
|
||||
it('should properly rank by match quality', () => {
|
||||
const searchQuery = 'C';
|
||||
|
||||
const options: VariableOption[] = 'A AA AB AC BC C CD'.split(' ').map((v) => ({
|
||||
selected: false,
|
||||
text: v,
|
||||
value: v,
|
||||
}));
|
||||
|
||||
const expect: VariableOption[] = 'C CD AC BC'.split(' ').map((v) => ({
|
||||
selected: false,
|
||||
text: v,
|
||||
value: v,
|
||||
}));
|
||||
|
||||
const { initialState } = getVariableTestContext({
|
||||
queryValue: searchQuery,
|
||||
});
|
||||
|
||||
reducerTester<OptionsPickerState>()
|
||||
.givenReducer(optionsPickerReducer, cloneDeep(initialState))
|
||||
.whenActionIsDispatched(updateOptionsAndFilter(options))
|
||||
.thenStateShouldEqual({
|
||||
...cloneDeep(initialState),
|
||||
options: expect,
|
||||
selectedValues: [],
|
||||
queryValue: 'C',
|
||||
highlightIndex: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when large data for updateOptionsFromSearch is dispatched and variable has searchFilter', () => {
|
||||
it('then state should be correct', () => {
|
||||
const searchQuery = '__searchFilter';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import uFuzzy from '@leeoniya/ufuzzy';
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { cloneDeep, isString, trimStart } from 'lodash';
|
||||
import { cloneDeep, isString } from 'lodash';
|
||||
|
||||
import { containsSearchFilter } from '@grafana/data';
|
||||
|
||||
@@ -34,6 +35,14 @@ export const initialOptionPickerState: OptionsPickerState = {
|
||||
|
||||
export const OPTIONS_LIMIT = 1000;
|
||||
|
||||
const ufuzzy = new uFuzzy({
|
||||
intraMode: 1,
|
||||
intraIns: 1,
|
||||
intraSub: 1,
|
||||
intraTrn: 1,
|
||||
intraDel: 1,
|
||||
});
|
||||
|
||||
const optionsToRecord = (options: VariableOption[]): Record<string, VariableOption> => {
|
||||
if (!Array.isArray(options)) {
|
||||
return {};
|
||||
@@ -237,14 +246,32 @@ const optionsPickerSlice = createSlice({
|
||||
return state;
|
||||
},
|
||||
updateOptionsAndFilter: (state, action: PayloadAction<VariableOption[]>): OptionsPickerState => {
|
||||
const searchQuery = trimStart((state.queryValue ?? '').toLowerCase());
|
||||
const needle = state.queryValue.trim();
|
||||
|
||||
state.options = action.payload.filter((option) => {
|
||||
const optionsText = option.text ?? '';
|
||||
const text = Array.isArray(optionsText) ? optionsText.toString() : optionsText;
|
||||
return text.toLowerCase().indexOf(searchQuery) !== -1;
|
||||
});
|
||||
let opts: VariableOption[] = [];
|
||||
|
||||
if (needle === '') {
|
||||
opts = action.payload;
|
||||
} else {
|
||||
// with current API, not seeing a way to cache this on state using action.payload's uniqueness
|
||||
// since it's recreated and includes selected state on each item :(
|
||||
const haystack = action.payload.map(({ text }) => (Array.isArray(text) ? text.toString() : text));
|
||||
|
||||
const [idxs, info, order] = ufuzzy.search(haystack, needle, 5);
|
||||
|
||||
if (idxs?.length) {
|
||||
if (info && order) {
|
||||
opts = order.map((idx) => action.payload[info.idx[idx]]);
|
||||
} else {
|
||||
opts = idxs!.map((idx) => action.payload[idx]);
|
||||
}
|
||||
|
||||
// always sort $__all to the top, even if exact match exists?
|
||||
opts.sort((a, b) => (a.value === '$__all' ? -1 : 0) - (b.value === '$__all' ? -1 : 0));
|
||||
}
|
||||
}
|
||||
|
||||
state.options = opts;
|
||||
state.highlightIndex = 0;
|
||||
|
||||
return applyStateChanges(state, updateDefaultSelection, updateOptions);
|
||||
|
||||
Reference in New Issue
Block a user