Skip to content

Commit

Permalink
refactor asset search to be in separate component
Browse files Browse the repository at this point in the history
  • Loading branch information
clairelin135 committed Mar 11, 2024
1 parent 8523819 commit 4c23fcf
Show file tree
Hide file tree
Showing 3 changed files with 265 additions and 135 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {displayNameForAssetKey} from '../asset-graph/Utils';
import {TagIcon} from '../graph/OpTags';
import {useDocumentTitle} from '../hooks/useDocumentTitle';
import {useLaunchPadHooks} from '../launchpad/LaunchpadHooksContext';
import {SearchDialog} from '../search/SearchDialog';
import {AssetSearch} from '../search/AssetSearch';
import {ReloadAllButton} from '../workspace/ReloadAllButton';
import {buildRepoPathForHuman} from '../workspace/buildRepoAddress';
import {repoAddressAsHumanString, repoAddressAsURLString} from '../workspace/repoAddressAsString';
Expand Down Expand Up @@ -252,7 +252,7 @@ export const AssetsOverview = ({viewerName}: {viewerName?: string}) => {
<AssetGlobalLineageButton />
</Box>
</Box>
<SearchDialog isAssetSearch={true} displayAsOverlay={false} />
<AssetSearch />
</Box>
</Box>
<Box flex={{direction: 'column'}}>
Expand Down
204 changes: 204 additions & 0 deletions js_modules/dagster-ui/packages/ui-core/src/search/AssetSearch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import {Colors, Icon, Spinner} from '@dagster-io/ui-components';
import Fuse from 'fuse.js';
import debounce from 'lodash/debounce';
import * as React from 'react';
import {useHistory} from 'react-router-dom';
import styled from 'styled-components';

import {SearchBox, SearchInput} from './SearchDialog';
import {SearchResults} from './SearchResults';
import {SearchResult, SearchResultType, isAssetFilterSearchResultType} from './types';
import {useGlobalSearch} from './useGlobalSearch';
import {Trace, createTrace} from '../performance';

const MAX_ASSET_RESULTS = 50;
const MAX_FILTER_RESULTS = 25;

type State = {
queryString: string;
searching: boolean;
secondaryResults: Fuse.FuseResult<SearchResult>[];
highlight: number;
loaded: boolean;
};

type Action =
| {type: 'highlight'; highlight: number}
| {type: 'change-query'; queryString: string}
| {type: 'complete-secondary'; queryString: string; results: Fuse.FuseResult<SearchResult>[]};

const reducer = (state: State, action: Action) => {
switch (action.type) {
case 'highlight':
return {...state, highlight: action.highlight};
case 'change-query':
return {...state, queryString: action.queryString, searching: true, highlight: 0};
case 'complete-secondary':
// If the received results match the current querystring, use them. Otherwise discard.
const secondaryResults =
action.queryString === state.queryString ? action.results : state.secondaryResults;
return {...state, secondaryResults, searching: false};
default:
return state;
}
};

const initialState: State = {
queryString: '',
searching: false,
secondaryResults: [],
highlight: 0,
loaded: false,
};

const DEBOUNCE_MSEC = 100;

type SearchResultGroups = {
assetResults: Fuse.FuseResult<SearchResult>[];
assetFilterResults: Fuse.FuseResult<SearchResult>[];
};

function groupSearchResults(secondaryResults: Fuse.FuseResult<SearchResult>[]): SearchResultGroups {
return {
assetResults: secondaryResults.filter((result) => result.item.type === SearchResultType.Asset),
assetFilterResults: secondaryResults.filter((result) =>
isAssetFilterSearchResultType(result.item.type),
),
};
}

export const AssetSearch = () => {
const history = useHistory();
const {loading, searchSecondary} = useGlobalSearch({
includeAssetFilters: true,
});

const [state, dispatch] = React.useReducer(reducer, initialState);
const {queryString, secondaryResults, highlight} = state;

const {assetResults, assetFilterResults} = groupSearchResults(secondaryResults);

const renderedAssetResults = assetResults.slice(0, MAX_ASSET_RESULTS);
const renderedFilterResults = assetFilterResults.slice(0, MAX_FILTER_RESULTS);

const renderedResults = [...renderedAssetResults, ...renderedFilterResults];
const numRenderedResults = renderedResults.length;

const isFirstSearch = React.useRef(true);
const firstSearchTrace = React.useRef<null | Trace>(null);

React.useEffect(() => {
if (!loading && secondaryResults) {
firstSearchTrace.current?.endTrace();
}
}, [loading, secondaryResults]);

const searchAndHandleSecondary = React.useCallback(
async (queryString: string) => {
const {queryString: queryStringForResults, results} = await searchSecondary(queryString);
dispatch({type: 'complete-secondary', queryString: queryStringForResults, results});
},
[searchSecondary],
);

const debouncedSearch = React.useMemo(() => {
const debouncedSearch = debounce(async (queryString: string) => {
searchAndHandleSecondary(queryString);
}, DEBOUNCE_MSEC);
return (queryString: string) => {
if (!firstSearchTrace.current && isFirstSearch.current) {
isFirstSearch.current = false;
const trace = createTrace('SearchDialog:FirstSearch');
firstSearchTrace.current = trace;
trace.startTrace();
}
return debouncedSearch(queryString);
};
}, [searchAndHandleSecondary]);

const onChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
dispatch({type: 'change-query', queryString: newValue});
debouncedSearch(newValue);
};

const onClickResult = React.useCallback(
(result: Fuse.FuseResult<SearchResult>) => {
history.push(result.item.href);
},
[history],
);

const highlightedResult = renderedResults[highlight] || null;

const onKeyDown = (e: React.KeyboardEvent) => {
const {key} = e;

if (!numRenderedResults) {
return;
}

const lastResult = numRenderedResults - 1;

switch (key) {
case 'ArrowUp':
e.preventDefault();
dispatch({
type: 'highlight',
highlight: highlight === 0 ? lastResult : highlight - 1,
});
break;
case 'ArrowDown':
e.preventDefault();
dispatch({
type: 'highlight',
highlight: highlight === lastResult ? 0 : highlight + 1,
});
break;
case 'Enter':
e.preventDefault();
if (highlightedResult) {
history.push(highlightedResult.item.href);
}
}
};

return (
<SearchInputWrapper>
<SearchBox hasQueryString={!!queryString.length}>
<Icon name="search" color={Colors.accentGray()} size={20} />
<SearchInput
data-search-input="1"
autoFocus
spellCheck={false}
onChange={onChange}
onKeyDown={onKeyDown}
placeholder="Search assets"
type="text"
value={queryString}
/>
{loading ? <Spinner purpose="body-text" /> : null}
</SearchBox>
<SearchResultsWrapper>
<SearchResults
highlight={highlight}
queryString={queryString}
results={renderedAssetResults}
filterResults={renderedFilterResults}
onClickResult={onClickResult}
/>
</SearchResultsWrapper>
</SearchInputWrapper>
);
};

const SearchInputWrapper = styled.div`
position: relative;
`;

const SearchResultsWrapper = styled.div`
top: 60px;
position: absolute;
z-index: 1;
width: 100%;
`;
Loading

0 comments on commit 4c23fcf

Please sign in to comment.