diff --git a/js_modules/dagster-ui/packages/ui-core/src/runs/RunsFilterInput.tsx b/js_modules/dagster-ui/packages/ui-core/src/runs/RunsFilterInput.tsx index 380a95c4198fb..13b7055f3ffb7 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/runs/RunsFilterInput.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/runs/RunsFilterInput.tsx @@ -490,11 +490,11 @@ export const useRunsFilterInput = ({tokens, onChange, enabledFilters}: RunsFilte matchType: 'any-of', }); - const launchedByFilter = useStaticSetFilter({ + const launchedByFilter = useSuggestionFilter({ name: 'Launched by', - allowMultipleSelections: false, icon: 'add_circle', - allValues: createdByValues, + initialSuggestions: createdByValues, + allowMultipleSelections: false, renderLabel: ({value}) => { let icon; let labelValue = value.value; @@ -522,16 +522,38 @@ export const useRunsFilterInput = ({tokens, onChange, enabledFilters}: RunsFilte return x.value!; }, state: useMemo(() => { - return new Set( - tokens - .filter( - ({token, value}) => - token === 'tag' && CREATED_BY_TAGS.includes(value.split('=')[0] as DagsterTag), - ) - .map(({value}) => tagValueToFilterObject(value)), - ); + return tokens + .filter( + ({token, value}) => + token === 'tag' && CREATED_BY_TAGS.includes(value.split('=')[0] as DagsterTag), + ) + .map(({value}) => tagValueToFilterObject(value)); }, [tokens]), - onStateChanged: (values) => { + freeformSearchResult(query, suggestionPath) { + if (suggestionPath.length === 0) { + return [ + { + value: { + key: `${DagsterTag.ScheduleName}-freeform`, + type: DagsterTag.ScheduleName, + value: query, + }, + final: true, + }, + { + value: { + key: `${DagsterTag.SensorName}-freeform`, + type: DagsterTag.SensorName, + value: query, + }, + final: true, + }, + ]; + } + return null; + }, + freeformResultPosition: 'end', + setState: (values) => { onChange([ ...tokens.filter((token) => { if (token.token !== 'tag') { @@ -545,6 +567,12 @@ export const useRunsFilterInput = ({tokens, onChange, enabledFilters}: RunsFilte })), ]); }, + getKey: ({value}) => value, + isMatch: ({value}, query) => value.toLowerCase().includes(query.toLowerCase()), + matchType: 'any-of', + onSuggestionClicked: async (value) => { + return [{value}]; + }, }); const createdDateFilter = useTimeRangeFilter({ diff --git a/js_modules/dagster-ui/packages/ui-core/src/ui/BaseFilters/__tests__/useSuggestionFilter.test.tsx b/js_modules/dagster-ui/packages/ui-core/src/ui/BaseFilters/__tests__/useSuggestionFilter.test.tsx index f0bf988441f4c..74016ed6759d4 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/ui/BaseFilters/__tests__/useSuggestionFilter.test.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/ui/BaseFilters/__tests__/useSuggestionFilter.test.tsx @@ -1,5 +1,6 @@ import {waitFor} from '@testing-library/dom'; -import {act, renderHook} from '@testing-library/react-hooks'; +import {renderHook} from '@testing-library/react-hooks'; +import {act} from 'react'; import {useSuggestionFilter} from '../useSuggestionFilter'; @@ -193,4 +194,61 @@ describe('useSuggestionFilter', () => { expect(result.current.activeJSX).toBeTruthy(); }); + + it('should correctly handle allowMultipleSelections `false`', () => { + let state: string[] = ['']; + const setState = (newState: string[]) => { + state = newState; + }; + + const {result, rerender} = renderHook(() => + useSuggestionFilter({...hookArgs, allowMultipleSelections: false, state, setState}), + ); + + expect(result.current.state).toEqual(['']); + + act(() => { + result.current.onSelect({value: {final: true, value: 'apple'}} as any); + }); + rerender(); + + expect(result.current.state).toEqual(['apple']); + + act(() => { + result.current.onSelect({value: {final: true, value: 'banana'}} as any); + }); + rerender(); + + // Replaced `apple` with `banana` + expect(result.current.state).toEqual(['banana']); + }); + + it('should correctly handle freeformResultPosition `end`', () => { + let state: string[] = ['']; + const setState = (newState: string[]) => { + state = newState; + }; + + const freeformSearchResult = (query: string) => ({final: true, value: `Custom: ${query}`}); + + const {result} = renderHook(() => + useSuggestionFilter({ + ...hookArgs, + freeformSearchResult, + freeformResultPosition: 'end', + state, + setState, + }), + ); + + // Freeform result applied after `apple` + const expectedResult = [ + ...initialSuggestions.filter(({value}) => isMatch(value, 'ap')), + {final: true, value: 'Custom: ap'}, + ]; + + expect(result.current.getResults('ap').map((suggestion) => suggestion.value)).toEqual( + expectedResult, + ); + }); }); diff --git a/js_modules/dagster-ui/packages/ui-core/src/ui/BaseFilters/useSuggestionFilter.tsx b/js_modules/dagster-ui/packages/ui-core/src/ui/BaseFilters/useSuggestionFilter.tsx index 9c6b8841c2c1b..9e9a87c8f4e55 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/ui/BaseFilters/useSuggestionFilter.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/ui/BaseFilters/useSuggestionFilter.tsx @@ -15,11 +15,13 @@ type Args = { freeformSearchResult?: ( query: string, suggestionPath: TValue[], - ) => SuggestionFilterSuggestion | null; + ) => SuggestionFilterSuggestion | SuggestionFilterSuggestion[] | null; + freeformResultPosition?: 'start' | 'end'; state: TValue[]; // Active suggestions setState: (state: TValue[]) => void; + allowMultipleSelections?: boolean; initialSuggestions: SuggestionFilterSuggestion[]; getNoSuggestionsPlaceholder?: (query: string) => string; onSuggestionClicked: (value: TValue) => Promise[]> | void; @@ -45,6 +47,8 @@ export function useSuggestionFilter({ state, setState, initialSuggestions, + allowMultipleSelections = true, + freeformResultPosition = 'start', onSuggestionClicked, getNoSuggestionsPlaceholder, getStringValue, @@ -122,17 +126,27 @@ export function useSuggestionFilter({ if (!hasExactMatch && freeformSearchResult && query.length) { const suggestion = freeformSearchResult(query, suggestionPath); if (suggestion) { - results.unshift({ - label: ( - - ), - key: getKey?.(suggestion.value) || 'freeform', - value: suggestion, - }); + const suggestions = Array.isArray(suggestion) ? suggestion : [suggestion]; + const freeformResults = + suggestions + .filter((s): s is SuggestionFilterSuggestion => s !== null) + .map((suggestion) => ({ + label: ( + + ), + key: getKey?.(suggestion.value) || 'freeform', + value: suggestion, + })) || []; + + if (freeformResultPosition === 'start') { + results.unshift(...freeformResults); + } else { + results.push(...freeformResults); + } } } return results; @@ -140,10 +154,14 @@ export function useSuggestionFilter({ onSelect: async ({value, clearSearch}) => { if (value.final) { - if (state.includes(value.value)) { - setState(state.filter((v) => v !== value.value)); + if (!allowMultipleSelections) { + const result = state.includes(value.value) ? [] : [value.value]; + setState(result); } else { - setState([...state, value.value]); + const result = state.includes(value.value) + ? state.filter((v) => v !== value.value) + : [...state, value.value]; + setState(result); } } else { clearSearch();