Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[search ui] Display asset search on asset landing page #20373

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ export const CaptionSubtitle = styled(Text)`
line-height: 16px;
`;

export const CaptionBolded = styled(Text)`
font-family: ${FontFamily.default};
font-size: 12px;
font-weight: 900;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this comes from figma?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

`;

export const Code = styled(Text)`
background-color: ${Colors.backgroundBlue()};
border-radius: 2px;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {useQuery} from '@apollo/client';
import {Box, Colors, Heading, Icon, Page, Spinner, TextInput} from '@dagster-io/ui-components';
import {Box, Colors, Heading, Icon, Page, Spinner} from '@dagster-io/ui-components';
import qs from 'qs';
import {useContext} from 'react';
import {Link, useParams} from 'react-router-dom';
Expand All @@ -20,6 +20,7 @@ import {displayNameForAssetKey} from '../asset-graph/Utils';
import {TagIcon} from '../graph/OpTags';
import {useDocumentTitle} from '../hooks/useDocumentTitle';
import {useLaunchPadHooks} from '../launchpad/LaunchpadHooksContext';
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 @@ -234,7 +235,7 @@ export const AssetsOverview = ({viewerName}: {viewerName?: string}) => {
/>
<Box flex={{direction: 'column'}} style={{height: '100%', overflow: 'auto'}}>
<Box padding={64} flex={{justifyContent: 'center', alignItems: 'center'}}>
<Box style={{width: '60%'}} flex={{direction: 'column', gap: 16}}>
<Box style={{width: '60%', minWidth: '600px'}} flex={{direction: 'column', gap: 16}}>
<Box flex={{direction: 'row', alignItems: 'center', justifyContent: 'space-between'}}>
<Heading>
{getGreeting(timezone)}
Expand All @@ -245,7 +246,7 @@ export const AssetsOverview = ({viewerName}: {viewerName?: string}) => {
<AssetGlobalLineageButton />
</Box>
</Box>
<TextInput />
<AssetSearch />
</Box>
</Box>
<Box flex={{direction: 'column'}}>
Expand Down
203 changes: 203 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,203 @@
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;
};

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,
};

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, initialize} = 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(() => {
initialize();
if (!loading && secondaryResults) {
firstSearchTrace.current?.endTrace();
}
}, [loading, secondaryResults, initialize]);

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) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure what this trace logic is, but it existed in the global search dialog so I added similar logic here?

isFirstSearch.current = false;
const trace = createTrace('AssetSearch: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%;
`;
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ export const SearchDialog = ({searchPlaceholder}: {searchPlaceholder: string}) =
highlight={highlight}
queryString={queryString}
results={renderedResults}
filterResults={[]}
onClickResult={onClickResult}
/>
</Container>
Expand Down Expand Up @@ -306,15 +307,18 @@ interface SearchBoxProps {
readonly hasQueryString: boolean;
}

const SearchBox = styled.div<SearchBoxProps>`
export const SearchBox = styled.div<SearchBoxProps>`
border-radius: 12px;
box-shadow: 2px 2px 8px ${Colors.shadowDefault()};

align-items: center;
border-bottom: ${({hasQueryString}) =>
hasQueryString ? `1px solid ${Colors.keylineDefault()}` : 'none'};
display: flex;
padding: 12px 20px 12px 12px;
`;

const SearchInput = styled.input`
export const SearchInput = styled.input`
border: none;
color: ${Colors.textLight()};
font-family: ${FontFamily.default};
Expand Down
Loading
Loading