From 12e7d8db7690c0e19421e6fd3644005f46d4dea2 Mon Sep 17 00:00:00 2001 From: Claire Lin Date: Thu, 7 Mar 2024 17:31:36 -0800 Subject: [PATCH 1/8] claire/styling-display-search-on-asset-catalog-page --- .../ui-core/src/assets/AssetsOverview.tsx | 7 +- .../ui-core/src/search/SearchDialog.tsx | 194 +++++++++++++----- .../ui-core/src/search/SearchResults.tsx | 6 +- 3 files changed, 154 insertions(+), 53 deletions(-) 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 0e1953b40d6bd..4dcf5e56da81c 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 @@ -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'; @@ -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 {SearchDialog} from '../search/SearchDialog'; import {ReloadAllButton} from '../workspace/ReloadAllButton'; import {buildRepoPathForHuman} from '../workspace/buildRepoAddress'; import {repoAddressAsHumanString, repoAddressAsURLString} from '../workspace/repoAddressAsString'; @@ -234,7 +235,7 @@ export const AssetsOverview = ({viewerName}: {viewerName?: string}) => { /> - + {getGreeting(timezone)} @@ -245,7 +246,7 @@ export const AssetsOverview = ({viewerName}: {viewerName?: string}) => { - + 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 8d72f75e363e1..e20a775ee9804 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} from './types'; +import {SearchResult, SearchResultType} from './types'; import {useGlobalSearch} from './useGlobalSearch'; import {__updateSearchVisibility} from './useSearchVisibility'; import {ShortcutHandler} from '../app/ShortcutHandler'; @@ -72,7 +72,50 @@ const initialState: State = { const DEBOUNCE_MSEC = 100; -export const SearchDialog = ({searchPlaceholder}: {searchPlaceholder: string}) => { +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) => + result.item.type === SearchResultType.AssetGroup || + result.item.type === SearchResultType.ComputeKind || + result.item.type === SearchResultType.CodeLocation, + ), + }; + } 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; +}) => { const history = useHistory(); const {initialize, loading, searchPrimary, searchSecondary} = useGlobalSearch({ includeAssetFilters: false, @@ -82,7 +125,13 @@ export const SearchDialog = ({searchPlaceholder}: {searchPlaceholder: string}) = const [state, dispatch] = React.useReducer(reducer, initialState); const {shown, queryString, primaryResults, secondaryResults, highlight} = state; - const results = [...primaryResults, ...secondaryResults]; + const { + primaryResults: primaryResultsGroup, + assetResults, + assetFilterResults, + } = groupSearchResults(primaryResults, secondaryResults, isAssetSearch); + const results = [...primaryResultsGroup, ...assetResults, ...assetFilterResults]; + const renderedResults = results.slice(0, MAX_DISPLAYED_RESULTS); const numRenderedResults = renderedResults.length; @@ -208,60 +257,93 @@ export const SearchDialog = ({searchPlaceholder}: {searchPlaceholder: string}) = } }; - return ( - <> - - - - -
- -
-
{searchPlaceholder}
+ if (displayAsOverlay) { + return ( + <> + + + + +
+ +
+
{searchPlaceholder}
+
+ /
- / -
-
-
- dispatch({type: 'hide-dialog'})} - transitionDuration={100} - > - - - - + + dispatch({type: 'hide-dialog'})} + transitionDuration={100} + > + + + + + {loading ? : null} + + - {loading ? : null} - + + + + ); + } else { + return ( + + + + + {loading ? : null} + + - - - - ); + + + ); + } }; const SearchTrigger = styled.button` @@ -302,11 +384,25 @@ const Container = styled.div` } `; +const InPageSearchContainer = 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` + border-radius: 12px; + box-shadow: 2px 2px 8px ${Colors.shadowDefault()}; + align-items: center; border-bottom: ${({hasQueryString}) => hasQueryString ? `1px solid ${Colors.keylineDefault()}` : 'none'}; diff --git a/js_modules/dagster-ui/packages/ui-core/src/search/SearchResults.tsx b/js_modules/dagster-ui/packages/ui-core/src/search/SearchResults.tsx index 5d78c7a743b7e..a4f225a3ccf24 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/search/SearchResults.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/search/SearchResults.tsx @@ -118,6 +118,9 @@ const List = styled.ul` padding: ${({hasResults}) => (hasResults ? '4px 0' : 'none')}; list-style: none; overflow-y: auto; + background-color: ${Colors.backgroundDefault()}; + box-shadow: 2px 2px 8px ${Colors.shadowDefault()}; + border-radius: 4px; `; interface HighlightableTextProps { @@ -126,7 +129,8 @@ interface HighlightableTextProps { const Item = styled.li` align-items: center; - background-color: ${({isHighlight}) => (isHighlight ? Colors.backgroundLight() : 'transparent')}; + background-color: ${({isHighlight}) => + isHighlight ? Colors.backgroundLight() : Colors.backgroundDefault()}; box-shadow: ${({isHighlight}) => (isHighlight ? Colors.accentLime() : 'transparent')} 4px 0 0 inset; color: ${Colors.textLight()}; From 991601fde6276b0afb3becba8c74736769ed8c31 Mon Sep 17 00:00:00 2001 From: Claire Lin Date: Fri, 8 Mar 2024 12:16:38 -0800 Subject: [PATCH 2/8] display filter results & bold matches --- .../ui-core/src/search/SearchDialog.tsx | 4 +- .../ui-core/src/search/SearchResults.tsx | 108 +++++++++++++++--- .../ui-core/src/search/useGlobalSearch.tsx | 1 + 3 files changed, 98 insertions(+), 15 deletions(-) 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 e20a775ee9804..63fef6ecccd96 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 @@ -130,7 +130,7 @@ export const SearchDialog = ({ assetResults, assetFilterResults, } = groupSearchResults(primaryResults, secondaryResults, isAssetSearch); - const results = [...primaryResultsGroup, ...assetResults, ...assetFilterResults]; + const results = [...primaryResultsGroup, ...assetResults]; const renderedResults = results.slice(0, MAX_DISPLAYED_RESULTS); const numRenderedResults = renderedResults.length; @@ -309,6 +309,7 @@ export const SearchDialog = ({ queryString={queryString} results={renderedResults} onClickResult={onClickResult} + filterResults={[]} /> @@ -338,6 +339,7 @@ export const SearchDialog = ({ highlight={highlight} queryString={queryString} results={renderedResults} + filterResults={assetFilterResults} onClickResult={onClickResult} /> diff --git a/js_modules/dagster-ui/packages/ui-core/src/search/SearchResults.tsx b/js_modules/dagster-ui/packages/ui-core/src/search/SearchResults.tsx index a4f225a3ccf24..3ce18e78ea4a8 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/search/SearchResults.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/search/SearchResults.tsx @@ -1,4 +1,4 @@ -import {Colors, Icon, IconName} from '@dagster-io/ui-components'; +import {Colors, Icon, IconName, StyledTag} from '@dagster-io/ui-components'; import Fuse from 'fuse.js'; import * as React from 'react'; import {Link} from 'react-router-dom'; @@ -38,6 +38,51 @@ interface ItemProps { result: Fuse.FuseResult; } +function buildSearchLabel(result: Fuse.FuseResult): JSX.Element[] { + const labelComponents = []; + let parsedString = ''; + // Match indices can overlap, i.e. [0, 4] and [1, 1] can both be matches + // So we merge them to be non-overlapping + const mergedIndices: Fuse.RangeTuple[] = []; + + if (result.matches && result.matches.length > 0) { + const match = result.matches[0]!; // Only one match per row, since we only match by label + + // The indices should be returned in sorted order, but we sort just in case + const sortedIndices = Array.from(match.indices).sort((a, b) => (a[0] < b[0] ? -1 : 1)); + if (sortedIndices.length > 0) { + mergedIndices.push(sortedIndices[0]!); + + for (let i = 1; i < sortedIndices.length; i++) { + const last = mergedIndices[mergedIndices.length - 1]!; + const current = sortedIndices[i]!; + + if (current[0] <= last[1]) { + last[1] = Math.max(last[1], current[1]); + } else { + mergedIndices.push(current); + } + } + } + } + + mergedIndices.forEach((indices) => { + const stringBeforeMatch = result.item.label.slice(parsedString.length, indices[0]); + labelComponents.push({stringBeforeMatch}); + parsedString += stringBeforeMatch; + + const match = result.item.label.slice(indices[0], indices[1] + 1); + labelComponents.push({match}); + parsedString += match; + }); + + const stringAfterMatch = result.item.label.substring(parsedString.length); + labelComponents.push({stringAfterMatch}); + parsedString += stringAfterMatch; + + return labelComponents; +} + const SearchResultItem = React.memo(({isHighlight, onClickResult, result}: ItemProps) => { const {item} = result; const element = React.useRef(null); @@ -58,15 +103,23 @@ const SearchResultItem = React.memo(({isHighlight, onClickResult, result}: ItemP [onClickResult, result], ); + const labelComponents = buildSearchLabel(result); + return ( - + + + {labelComponents.map((component) => component)} +
- {item.description}
@@ -79,17 +132,18 @@ interface Props { onClickResult: (result: Fuse.FuseResult) => void; queryString: string; results: Fuse.FuseResult[]; + filterResults: Fuse.FuseResult[]; } export const SearchResults = (props: Props) => { - const {highlight, onClickResult, queryString, results} = props; + const {highlight, onClickResult, queryString, results, filterResults} = props; - if (!results.length && queryString) { + if (!results.length && !filterResults.length && queryString) { return No results; } return ( - + {results.map((result, ii) => ( { onClickResult={onClickResult} /> ))} + {filterResults.length > 0 ? ( + <> + Matching filters + {filterResults.map((result, ii) => ( + + ))} + + ) : ( + <> + )} ); }; @@ -127,6 +196,22 @@ interface HighlightableTextProps { readonly isHighlight: boolean; } +const MatchingFiltersHeader = styled.li` + background-color: ${Colors.backgroundDefault()}; + padding: 8px 12px; + border-bottom: 1px solid ${Colors.backgroundGray()}; + color: ${Colors.textLight()}; + font-weight: 500; +`; + +const SearchResultBoldedLabel = styled.span` + font-weight: 800; +`; + +const SearchResultLabel = styled.span` + font-weight: 300; +`; + const Item = styled.li` align-items: center; background-color: ${({isHighlight}) => @@ -159,11 +244,6 @@ const ResultLink = styled(Link)` } `; -const Label = styled.div` - color: ${({isHighlight}) => (isHighlight ? Colors.textDefault() : Colors.textLight())}; - font-weight: 500; -`; - const Description = styled.div` color: ${({isHighlight}) => (isHighlight ? Colors.textDefault() : Colors.textLight())}; font-size: 12px; diff --git a/js_modules/dagster-ui/packages/ui-core/src/search/useGlobalSearch.tsx b/js_modules/dagster-ui/packages/ui-core/src/search/useGlobalSearch.tsx index f92d0574a42e5..a63787f329966 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/search/useGlobalSearch.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/search/useGlobalSearch.tsx @@ -249,6 +249,7 @@ const fuseOptions = { keys: ['label', 'segments', 'tags', 'type'], threshold: 0.3, useExtendedSearch: true, + includeMatches: true, }; const EMPTY_RESPONSE = {queryString: '', results: []}; From 0ff1bebd0e83c154f902f61606755696d686a506 Mon Sep 17 00:00:00 2001 From: Claire Lin Date: Fri, 8 Mar 2024 15:59:26 -0800 Subject: [PATCH 3/8] add different styling for filter results --- .../ui-core/src/assets/AssetsOverview.tsx | 2 +- .../ui-core/src/search/SearchDialog.tsx | 96 ++++++++----------- .../ui-core/src/search/SearchResults.tsx | 81 ++++++++++++---- .../packages/ui-core/src/search/types.ts | 11 +++ 4 files changed, 111 insertions(+), 79 deletions(-) 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 4dcf5e56da81c..a42eac2730795 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 @@ -246,7 +246,7 @@ export const AssetsOverview = ({viewerName}: {viewerName?: string}) => {
- + 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 63fef6ecccd96..18d42f77785e7 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} from './types'; +import {SearchResult, SearchResultType, isAssetFilterSearchResultType} from './types'; import {useGlobalSearch} from './useGlobalSearch'; import {__updateSearchVisibility} from './useSearchVisibility'; import {ShortcutHandler} from '../app/ShortcutHandler'; @@ -89,11 +89,8 @@ function groupSearchResults( assetResults: secondaryResults.filter( (result) => result.item.type === SearchResultType.Asset, ), - assetFilterResults: secondaryResults.filter( - (result) => - result.item.type === SearchResultType.AssetGroup || - result.item.type === SearchResultType.ComputeKind || - result.item.type === SearchResultType.CodeLocation, + assetFilterResults: secondaryResults.filter((result) => + isAssetFilterSearchResultType(result.item.type), ), }; } else { @@ -112,7 +109,7 @@ export const SearchDialog = ({ isAssetSearch, displayAsOverlay = true, }: { - searchPlaceholder: string; + searchPlaceholder?: string; isAssetSearch: boolean; displayAsOverlay?: boolean; }) => { @@ -257,6 +254,33 @@ export const SearchDialog = ({ } }; + const searchResults = ( + + ); + + const searchInput = ( + + + + {loading ? : null} + + ); + if (displayAsOverlay) { return ( <> @@ -288,62 +312,18 @@ export const SearchDialog = ({ transitionDuration={100} > - - - - {loading ? : null} - - + {searchInput} + {searchResults} ); } else { return ( - - - - - {loading ? : null} - - - - - + + {searchInput} + {searchResults} + ); } }; @@ -386,7 +366,7 @@ const Container = styled.div` } `; -const InPageSearchContainer = styled.div` +const SearchInputWrapper = styled.div` position: relative; `; diff --git a/js_modules/dagster-ui/packages/ui-core/src/search/SearchResults.tsx b/js_modules/dagster-ui/packages/ui-core/src/search/SearchResults.tsx index 3ce18e78ea4a8..f87bd1baa5382 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/search/SearchResults.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/search/SearchResults.tsx @@ -1,10 +1,15 @@ -import {Colors, Icon, IconName, StyledTag} from '@dagster-io/ui-components'; +import {Box, Colors, Icon, IconName, StyledTag} from '@dagster-io/ui-components'; import Fuse from 'fuse.js'; import * as React from 'react'; import {Link} from 'react-router-dom'; import styled from 'styled-components'; -import {AssetFilterSearchResultType, SearchResult, SearchResultType} from './types'; +import { + AssetFilterSearchResultType, + SearchResult, + SearchResultType, + isAssetFilterSearchResultType, +} from './types'; const iconForType = (type: SearchResultType | AssetFilterSearchResultType): IconName => { switch (type) { @@ -27,11 +32,32 @@ const iconForType = (type: SearchResultType | AssetFilterSearchResultType): Icon return 'op'; case SearchResultType.Resource: return 'resource'; + case AssetFilterSearchResultType.CodeLocation: + return 'folder'; + case AssetFilterSearchResultType.Owner: + return 'account_circle'; + case AssetFilterSearchResultType.AssetGroup: + return 'asset_group'; default: return 'source'; } }; +const assetFilterPrefixString = (type: AssetFilterSearchResultType): string => { + switch (type) { + case AssetFilterSearchResultType.CodeLocation: + return 'Code location'; + case AssetFilterSearchResultType.ComputeKind: + return 'Compute kind'; + case AssetFilterSearchResultType.Owner: + return 'Owner'; + case AssetFilterSearchResultType.AssetGroup: + return 'Group'; + default: + return ''; + } +}; + interface ItemProps { isHighlight: boolean; onClickResult: (result: Fuse.FuseResult) => void; @@ -39,24 +65,23 @@ interface ItemProps { } function buildSearchLabel(result: Fuse.FuseResult): JSX.Element[] { - const labelComponents = []; - let parsedString = ''; + // Fuse provides indices of the label that match the query string. + // Use these match indices to display the label with the matching parts bolded. + // Match indices can overlap, i.e. [0, 4] and [1, 1] can both be matches // So we merge them to be non-overlapping const mergedIndices: Fuse.RangeTuple[] = []; - if (result.matches && result.matches.length > 0) { const match = result.matches[0]!; // Only one match per row, since we only match by label // The indices should be returned in sorted order, but we sort just in case const sortedIndices = Array.from(match.indices).sort((a, b) => (a[0] < b[0] ? -1 : 1)); + // Merge overlapping indices if (sortedIndices.length > 0) { mergedIndices.push(sortedIndices[0]!); - for (let i = 1; i < sortedIndices.length; i++) { const last = mergedIndices[mergedIndices.length - 1]!; const current = sortedIndices[i]!; - if (current[0] <= last[1]) { last[1] = Math.max(last[1], current[1]); } else { @@ -66,6 +91,8 @@ function buildSearchLabel(result: Fuse.FuseResult): JSX.Element[] } } + const labelComponents = []; + let parsedString = ''; mergedIndices.forEach((indices) => { const stringBeforeMatch = result.item.label.slice(parsedString.length, indices[0]); labelComponents.push({stringBeforeMatch}); @@ -108,20 +135,34 @@ const SearchResultItem = React.memo(({isHighlight, onClickResult, result}: ItemP return ( - - - {labelComponents.map((component) => component)} - -
- {item.description} -
+ + + + {isAssetFilterSearchResultType(item.type) ? ( + {assetFilterPrefixString(item.type)}:  + ) : ( + <> + )} + {labelComponents.map((component) => component)} + +
+ + {item.numResults ? `${item.numResults} assets` : item.description} + +
+
+
); diff --git a/js_modules/dagster-ui/packages/ui-core/src/search/types.ts b/js_modules/dagster-ui/packages/ui-core/src/search/types.ts index 0d466b84570e3..7016114c04d5c 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/search/types.ts +++ b/js_modules/dagster-ui/packages/ui-core/src/search/types.ts @@ -23,6 +23,17 @@ export enum AssetFilterSearchResultType { AssetGroup = 'AssetFilterSearchResultType.AssetGroup', } +export function isAssetFilterSearchResultType( + type: SearchResultType | AssetFilterSearchResultType, +): type is AssetFilterSearchResultType { + return ( + type === AssetFilterSearchResultType.AssetGroup || + type === AssetFilterSearchResultType.CodeLocation || + type === AssetFilterSearchResultType.ComputeKind || + type === AssetFilterSearchResultType.Owner + ); +} + export type SearchResult = { label: string; description: string; From 764276af82708fe5e1688c613b2f0ce07b436f97 Mon Sep 17 00:00:00 2001 From: Claire Lin Date: Fri, 8 Mar 2024 12:16:38 -0800 Subject: [PATCH 4/8] display filter results & bold matches --- .../ui-core/src/search/SearchDialog.tsx | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) 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..8fb55d110b642 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 @@ -312,18 +312,74 @@ export const SearchDialog = ({ transitionDuration={100} > +<<<<<<< HEAD {searchInput} {searchResults} +======= + + + + {loading ? : null} + + +>>>>>>> 60e496c9bd (display filter results & bold matches) ); } else { return ( +<<<<<<< HEAD {searchInput} {searchResults} +======= + + + + + {loading ? : null} + + + + + +>>>>>>> 60e496c9bd (display filter results & bold matches) ); } }; From 6608100f2bcd2f510decd8b388ca665e9b956d56 Mon Sep 17 00:00:00 2001 From: Claire Lin Date: Fri, 8 Mar 2024 15:59:26 -0800 Subject: [PATCH 5/8] add different styling for filter results --- .../ui-core/src/search/SearchDialog.tsx | 56 ------------------- .../ui-core/src/search/SearchResults.tsx | 47 +++++++--------- 2 files changed, 20 insertions(+), 83 deletions(-) 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 8fb55d110b642..18d42f77785e7 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 @@ -312,74 +312,18 @@ export const SearchDialog = ({ transitionDuration={100} > -<<<<<<< HEAD {searchInput} {searchResults} -======= - - - - {loading ? : null} - - ->>>>>>> 60e496c9bd (display filter results & bold matches) ); } else { return ( -<<<<<<< HEAD {searchInput} {searchResults} -======= - - - - - {loading ? : null} - - - - - ->>>>>>> 60e496c9bd (display filter results & bold matches) ); } }; diff --git a/js_modules/dagster-ui/packages/ui-core/src/search/SearchResults.tsx b/js_modules/dagster-ui/packages/ui-core/src/search/SearchResults.tsx index f87bd1baa5382..dc5a0188c539e 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/search/SearchResults.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/search/SearchResults.tsx @@ -135,33 +135,26 @@ const SearchResultItem = React.memo(({isHighlight, onClickResult, result}: ItemP return ( - - - - - {isAssetFilterSearchResultType(item.type) ? ( - {assetFilterPrefixString(item.type)}:  - ) : ( - <> - )} - {labelComponents.map((component) => component)} - -
- - {item.numResults ? `${item.numResults} assets` : item.description} - -
-
+ + + + {isAssetFilterSearchResultType(item.type) && ( + {assetFilterPrefixString(item.type)}:  + )} + {labelComponents.map((component) => component)} + +
+ + {item.numResults ? `${item.numResults} assets` : item.description} + +
From d4da341617f1d9ab641f441d6cce3684e60473e2 Mon Sep 17 00:00:00 2001 From: Claire Lin Date: Mon, 11 Mar 2024 12:07:41 -0700 Subject: [PATCH 6/8] refactor asset search to be in separate component --- .../ui-core/src/assets/AssetsOverview.tsx | 4 +- .../ui-core/src/search/AssetSearch.tsx | 204 ++++++++++++++++++ .../ui-core/src/search/SearchDialog.tsx | 192 +++++------------ 3 files changed, 265 insertions(+), 135 deletions(-) create mode 100644 js_modules/dagster-ui/packages/ui-core/src/search/AssetSearch.tsx 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 a42eac2730795..0ee81d2c9d089 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 @@ -20,7 +20,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'; @@ -246,7 +246,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}; From 47bfc9fd7fc2e2dc22464849a680446c9c65d882 Mon Sep 17 00:00:00 2001 From: Claire Lin Date: Mon, 11 Mar 2024 16:19:42 -0700 Subject: [PATCH 7/8] initialize upon load --- .../dagster-ui/packages/ui-core/src/search/AssetSearch.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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 index 97a34eefdc701..9e6eb256f0b13 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/search/AssetSearch.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/search/AssetSearch.tsx @@ -19,7 +19,6 @@ type State = { searching: boolean; secondaryResults: Fuse.FuseResult[]; highlight: number; - loaded: boolean; }; type Action = @@ -48,7 +47,6 @@ const initialState: State = { searching: false, secondaryResults: [], highlight: 0, - loaded: false, }; const DEBOUNCE_MSEC = 100; @@ -69,7 +67,7 @@ function groupSearchResults(secondaryResults: Fuse.FuseResult[]): export const AssetSearch = () => { const history = useHistory(); - const {loading, searchSecondary} = useGlobalSearch({ + const {loading, searchSecondary, initialize} = useGlobalSearch({ includeAssetFilters: true, }); @@ -88,10 +86,11 @@ export const AssetSearch = () => { const firstSearchTrace = React.useRef(null); React.useEffect(() => { + initialize(); if (!loading && secondaryResults) { firstSearchTrace.current?.endTrace(); } - }, [loading, secondaryResults]); + }, [loading, secondaryResults, initialize]); const searchAndHandleSecondary = React.useCallback( async (queryString: string) => { From 457bb2556f7778286a30a5ca11df38f5f69a2079 Mon Sep 17 00:00:00 2001 From: Claire Lin Date: Tue, 12 Mar 2024 12:10:46 -0700 Subject: [PATCH 8/8] add font styling to text file --- .../ui-components/src/components/Text.tsx | 6 ++++ .../ui-core/src/search/AssetSearch.tsx | 2 +- .../ui-core/src/search/SearchResults.tsx | 28 +++++++++---------- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/js_modules/dagster-ui/packages/ui-components/src/components/Text.tsx b/js_modules/dagster-ui/packages/ui-components/src/components/Text.tsx index bc6208ef2b6ea..7bf750b287a15 100644 --- a/js_modules/dagster-ui/packages/ui-components/src/components/Text.tsx +++ b/js_modules/dagster-ui/packages/ui-components/src/components/Text.tsx @@ -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; +`; + export const Code = styled(Text)` background-color: ${Colors.backgroundBlue()}; border-radius: 2px; 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 index 9e6eb256f0b13..6aebe37a5d507 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/search/AssetSearch.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/search/AssetSearch.tsx @@ -107,7 +107,7 @@ export const AssetSearch = () => { return (queryString: string) => { if (!firstSearchTrace.current && isFirstSearch.current) { isFirstSearch.current = false; - const trace = createTrace('SearchDialog:FirstSearch'); + const trace = createTrace('AssetSearch:FirstSearch'); firstSearchTrace.current = trace; trace.startTrace(); } diff --git a/js_modules/dagster-ui/packages/ui-core/src/search/SearchResults.tsx b/js_modules/dagster-ui/packages/ui-core/src/search/SearchResults.tsx index dc5a0188c539e..eff3934695156 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/search/SearchResults.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/search/SearchResults.tsx @@ -1,4 +1,12 @@ -import {Box, Colors, Icon, IconName, StyledTag} from '@dagster-io/ui-components'; +import { + Box, + Caption, + CaptionBolded, + Colors, + Icon, + IconName, + StyledTag, +} from '@dagster-io/ui-components'; import Fuse from 'fuse.js'; import * as React from 'react'; import {Link} from 'react-router-dom'; @@ -95,16 +103,16 @@ function buildSearchLabel(result: Fuse.FuseResult): JSX.Element[] let parsedString = ''; mergedIndices.forEach((indices) => { const stringBeforeMatch = result.item.label.slice(parsedString.length, indices[0]); - labelComponents.push({stringBeforeMatch}); + labelComponents.push({stringBeforeMatch}); parsedString += stringBeforeMatch; const match = result.item.label.slice(indices[0], indices[1] + 1); - labelComponents.push({match}); + labelComponents.push({match}); parsedString += match; }); const stringAfterMatch = result.item.label.substring(parsedString.length); - labelComponents.push({stringAfterMatch}); + labelComponents.push({stringAfterMatch}); parsedString += stringAfterMatch; return labelComponents; @@ -135,7 +143,7 @@ const SearchResultItem = React.memo(({isHighlight, onClickResult, result}: ItemP return ( - + {isAssetFilterSearchResultType(item.type) && ( - {assetFilterPrefixString(item.type)}:  + {assetFilterPrefixString(item.type)}:  )} {labelComponents.map((component) => component)} @@ -238,14 +246,6 @@ const MatchingFiltersHeader = styled.li` font-weight: 500; `; -const SearchResultBoldedLabel = styled.span` - font-weight: 800; -`; - -const SearchResultLabel = styled.span` - font-weight: 300; -`; - const Item = styled.li` align-items: center; background-color: ${({isHighlight}) =>