diff --git a/public/app/features/variables/pickers/OptionsPicker/reducer.test.ts b/public/app/features/variables/pickers/OptionsPicker/reducer.test.ts index e5d1fc4678d..9a2dfe161af 100644 --- a/public/app/features/variables/pickers/OptionsPicker/reducer.test.ts +++ b/public/app/features/variables/pickers/OptionsPicker/reducer.test.ts @@ -569,8 +569,11 @@ describe('optionsPickerReducer', () => { .whenActionIsDispatched(updateOptionsAndFilter(options)) .thenStateShouldEqual({ ...initialState, - options: [{ text: 'All', value: '$__all', selected: true }], - selectedValues: [{ text: 'All', value: '$__all', selected: true }], + options: [ + { text: '> A', value: 'A', selected: false }, + { text: 'All', value: '$__all', selected: false }, + ], + selectedValues: [], queryValue: 'A', highlightIndex: 0, }); @@ -815,6 +818,45 @@ describe('optionsPickerReducer', () => { highlightIndex: 0, }); }); + + it('should offer as-typed option even when matches exist', () => { + const searchQuery = 'a.*'; + + const options: VariableOption[] = 'A AA AB C'.split(' ').map((v) => ({ + selected: false, + text: v, + value: v, + })); + + const expect: VariableOption[] = [ + { + selected: false, + text: '> ' + searchQuery, + value: searchQuery, + }, + ].concat( + 'A AA AB'.split(' ').map((v) => ({ + selected: false, + text: v, + value: v, + })) + ); + + const { initialState } = getVariableTestContext({ + queryValue: searchQuery, + }); + + reducerTester() + .givenReducer(optionsPickerReducer, cloneDeep(initialState)) + .whenActionIsDispatched(updateOptionsAndFilter(options)) + .thenStateShouldEqual({ + ...cloneDeep(initialState), + options: expect, + selectedValues: [], + queryValue: searchQuery, + highlightIndex: 1, + }); + }); }); describe('when large data for updateOptionsFromSearch is dispatched and variable has searchFilter', () => { diff --git a/public/app/features/variables/pickers/OptionsPicker/reducer.ts b/public/app/features/variables/pickers/OptionsPicker/reducer.ts index c9d53cf89bd..57342c9010e 100644 --- a/public/app/features/variables/pickers/OptionsPicker/reducer.ts +++ b/public/app/features/variables/pickers/OptionsPicker/reducer.ts @@ -267,12 +267,30 @@ const optionsPickerSlice = createSlice({ } // 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)); + opts.sort((a, b) => (a.value === ALL_VARIABLE_VALUE ? -1 : 0) - (b.value === ALL_VARIABLE_VALUE ? -1 : 0)); + } + } + + state.highlightIndex = 0; + + if (needle !== '') { + // top ranked match index + let firstMatchIdx = opts.findIndex((o) => o.value !== ALL_VARIABLE_VALUE); + + // if there's no match or no exact match, prepend as-typed option + if (firstMatchIdx === -1 || opts[firstMatchIdx].value !== needle) { + opts.unshift({ + selected: false, + text: '> ' + needle, + value: needle, + }); + + // if no match at all, select as-typed, else select best match + state.highlightIndex = firstMatchIdx === -1 ? 0 : firstMatchIdx + 1; } } state.options = opts; - state.highlightIndex = 0; return applyStateChanges(state, updateDefaultSelection, updateOptions); },