diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/AssetsOverview.tsx b/js_modules/dagster-ui/packages/ui-core/src/assets/AssetsOverview.tsx
index 8287cb5f84fa8..2e2134eec3484 100644
--- a/js_modules/dagster-ui/packages/ui-core/src/assets/AssetsOverview.tsx
+++ b/js_modules/dagster-ui/packages/ui-core/src/assets/AssetsOverview.tsx
@@ -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';
@@ -252,7 +252,7 @@ export const AssetsOverview = ({viewerName}: {viewerName?: string}) => {
-
+
diff --git a/js_modules/dagster-ui/packages/ui-core/src/search/AssetSearch.tsx b/js_modules/dagster-ui/packages/ui-core/src/search/AssetSearch.tsx
new file mode 100644
index 0000000000000..97a34eefdc701
--- /dev/null
+++ b/js_modules/dagster-ui/packages/ui-core/src/search/AssetSearch.tsx
@@ -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[];
+ highlight: number;
+ loaded: boolean;
+};
+
+type Action =
+ | {type: 'highlight'; highlight: number}
+ | {type: 'change-query'; queryString: string}
+ | {type: 'complete-secondary'; queryString: string; results: Fuse.FuseResult[]};
+
+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[];
+ assetFilterResults: Fuse.FuseResult[];
+};
+
+function groupSearchResults(secondaryResults: Fuse.FuseResult[]): 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);
+
+ 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) => {
+ const newValue = e.target.value;
+ dispatch({type: 'change-query', queryString: newValue});
+ debouncedSearch(newValue);
+ };
+
+ const onClickResult = React.useCallback(
+ (result: Fuse.FuseResult) => {
+ 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 (
+
+
+
+
+ {loading ? : null}
+
+
+
+
+
+ );
+};
+
+const SearchInputWrapper = styled.div`
+ position: relative;
+`;
+
+const SearchResultsWrapper = styled.div`
+ top: 60px;
+ position: absolute;
+ z-index: 1;
+ width: 100%;
+`;
diff --git a/js_modules/dagster-ui/packages/ui-core/src/search/SearchDialog.tsx b/js_modules/dagster-ui/packages/ui-core/src/search/SearchDialog.tsx
index 18d42f77785e7..96447ada98516 100644
--- a/js_modules/dagster-ui/packages/ui-core/src/search/SearchDialog.tsx
+++ b/js_modules/dagster-ui/packages/ui-core/src/search/SearchDialog.tsx
@@ -8,7 +8,7 @@ import {useHistory} from 'react-router-dom';
import styled from 'styled-components';
import {SearchResults} from './SearchResults';
-import {SearchResult, SearchResultType, isAssetFilterSearchResultType} from './types';
+import {SearchResult} from './types';
import {useGlobalSearch} from './useGlobalSearch';
import {__updateSearchVisibility} from './useSearchVisibility';
import {ShortcutHandler} from '../app/ShortcutHandler';
@@ -72,47 +72,7 @@ const initialState: State = {
const DEBOUNCE_MSEC = 100;
-type SearchResultGroups = {
- primaryResults: Fuse.FuseResult[];
- assetResults: Fuse.FuseResult[];
- assetFilterResults: Fuse.FuseResult[];
-};
-
-function groupSearchResults(
- primaryResults: Fuse.FuseResult[],
- secondaryResults: Fuse.FuseResult[],
- isAssetSearch: boolean,
-): SearchResultGroups {
- if (isAssetSearch) {
- return {
- primaryResults: [],
- assetResults: secondaryResults.filter(
- (result) => result.item.type === SearchResultType.Asset,
- ),
- assetFilterResults: secondaryResults.filter((result) =>
- isAssetFilterSearchResultType(result.item.type),
- ),
- };
- } else {
- return {
- primaryResults,
- assetResults: secondaryResults.filter(
- (result) => result.item.type === SearchResultType.Asset,
- ),
- assetFilterResults: [],
- };
- }
-}
-
-export const SearchDialog = ({
- searchPlaceholder,
- isAssetSearch,
- displayAsOverlay = true,
-}: {
- searchPlaceholder?: string;
- isAssetSearch: boolean;
- displayAsOverlay?: boolean;
-}) => {
+export const SearchDialog = ({searchPlaceholder}: {searchPlaceholder: string}) => {
const history = useHistory();
const {initialize, loading, searchPrimary, searchSecondary} = useGlobalSearch({
includeAssetFilters: false,
@@ -122,13 +82,7 @@ export const SearchDialog = ({
const [state, dispatch] = React.useReducer(reducer, initialState);
const {shown, queryString, primaryResults, secondaryResults, highlight} = state;
- const {
- primaryResults: primaryResultsGroup,
- assetResults,
- assetFilterResults,
- } = groupSearchResults(primaryResults, secondaryResults, isAssetSearch);
- const results = [...primaryResultsGroup, ...assetResults];
-
+ const results = [...primaryResults, ...secondaryResults];
const renderedResults = results.slice(0, MAX_DISPLAYED_RESULTS);
const numRenderedResults = renderedResults.length;
@@ -254,78 +208,61 @@ export const SearchDialog = ({
}
};
- const searchResults = (
-
- );
-
- const searchInput = (
-
-
-
- {loading ? : null}
-
- );
-
- if (displayAsOverlay) {
- return (
- <>
-
-
-
-
-
-
-
- {searchPlaceholder}
-
- /
+ return (
+ <>
+
+
+
+
+
+
+
+ {searchPlaceholder}
-
-
- dispatch({type: 'hide-dialog'})}
- transitionDuration={100}
- >
-
- {searchInput}
- {searchResults}
-
-
- >
- );
- } else {
- return (
-
- {searchInput}
- {searchResults}
-
- );
- }
+ /
+
+
+
+ dispatch({type: 'hide-dialog'})}
+ transitionDuration={100}
+ >
+
+
+
+
+ {loading ? : null}
+
+
+
+
+ >
+ );
};
const SearchTrigger = styled.button`
@@ -366,22 +303,11 @@ const Container = styled.div`
}
`;
-const SearchInputWrapper = styled.div`
- position: relative;
-`;
-
-const SearchResultsWrapper = styled.div`
- top: 60px;
- position: absolute;
- z-index: 1;
- width: 100%;
-`;
-
interface SearchBoxProps {
readonly hasQueryString: boolean;
}
-const SearchBox = styled.div`
+export const SearchBox = styled.div`
border-radius: 12px;
box-shadow: 2px 2px 8px ${Colors.shadowDefault()};
@@ -392,7 +318,7 @@ const SearchBox = styled.div`
padding: 12px 20px 12px 12px;
`;
-const SearchInput = styled.input`
+export const SearchInput = styled.input`
border: none;
color: ${Colors.textLight()};
font-family: ${FontFamily.default};