77 DialogFooter ,
88 Icon ,
99 MiddleTruncate ,
10+ SpinnerWithText ,
1011 Tag ,
1112} from '@dagster-io/ui-components' ;
1213import useResizeObserver from '@react-hook/resize-observer' ;
@@ -15,6 +16,7 @@ import {Link} from 'react-router-dom';
1516
1617import { displayNameForAssetKey , tokenForAssetKey } from '../asset-graph/Utils' ;
1718import {
19+ Check ,
1820 labelForAssetCheck ,
1921 renderItemAssetCheck ,
2022 renderItemAssetKey ,
@@ -31,41 +33,72 @@ import {TagActionsPopover} from '../ui/TagActions';
3133import { VirtualizedItemListForDialog } from '../ui/VirtualizedItemListForDialog' ;
3234import { numberFormatter } from '../ui/formatters' ;
3335
36+ /**
37+ * Describes a selection whose inline tags are a bounded preview, with the full list fetched lazily
38+ * (e.g. the runs feed, where a run can target tens of thousands of assets/checks). When provided,
39+ * the total count drives the labels and the full list is loaded on demand for the "View list"
40+ * dialog and lineage link.
41+ */
42+ export interface TagCollectionLazyLoad < T > {
43+ /** Total number of items; the inline preview list may be truncated. */
44+ totalCount : number ;
45+ /** The full list, once lazily loaded (null until then). */
46+ allItems : T [ ] | null ;
47+ /** Whether the full list is currently being fetched. */
48+ loading : boolean ;
49+ /** Request the full list. Called when the overflow popover opens and when an action is clicked. */
50+ onRequest : ( ) => void ;
51+ }
52+
3453function useShowMoreDialog < T > (
3554 dialogTitle : string ,
3655 items : T [ ] | null ,
3756 renderItem : ( item : T ) => React . ReactNode ,
57+ loading ?: boolean ,
3858) {
3959 const [ showMore , setShowMore ] = React . useState ( false ) ;
4060
41- const dialog =
42- ! ! items && items . length > 1 ? (
43- < Dialog
44- title = { dialogTitle }
45- onClose = { ( ) => setShowMore ( false ) }
46- style = { { minWidth : '400px' , width : '50vw' , maxWidth : '800px' , maxHeight : '70vh' } }
47- isOpen = { showMore }
48- >
49- < div style = { { height : '500px' , overflow : 'hidden' } } >
61+ // Always render the dialog and toggle it with `isOpen` rather than mounting/unmounting it on
62+ // `showMore` — mount/unmount breaks the open/close fade animation.
63+ const dialog = (
64+ < Dialog
65+ title = { dialogTitle }
66+ onClose = { ( ) => setShowMore ( false ) }
67+ style = { { minWidth : '400px' , width : '50vw' , maxWidth : '800px' , maxHeight : '70vh' } }
68+ isOpen = { showMore }
69+ >
70+ < div style = { { height : '500px' , overflow : 'hidden' } } >
71+ { items && items . length ? (
5072 < VirtualizedItemListForDialog items = { items } renderItem = { renderItem } />
51- </ div >
52- < DialogFooter topBorder >
53- < Button intent = "primary" autoFocus onClick = { ( ) => setShowMore ( false ) } >
54- Done
55- </ Button >
56- </ DialogFooter >
57- </ Dialog >
58- ) : undefined ;
73+ ) : (
74+ < Box flex = { { alignItems : 'center' , justifyContent : 'center' } } style = { { height : '100%' } } >
75+ { loading ? < SpinnerWithText label = "Loading…" /> : null }
76+ </ Box >
77+ ) }
78+ </ div >
79+ < DialogFooter topBorder >
80+ < Button intent = "primary" autoFocus onClick = { ( ) => setShowMore ( false ) } >
81+ Done
82+ </ Button >
83+ </ DialogFooter >
84+ </ Dialog >
85+ ) ;
5986
6087 return { dialog, showMore, setShowMore} ;
6188}
6289
6390interface AssetKeyTagCollectionProps {
91+ /**
92+ * The asset keys to render as inline tags. When `lazy` is provided this is only a bounded
93+ * preview — `lazy.totalCount` is the true count and `lazy.allItems` the full list (loaded on
94+ * demand). Without `lazy` it is the complete list.
95+ */
6496 assetKeys : AssetKey [ ] | null ;
6597 dialogTitle ?: string ;
6698 useTags ?: boolean ;
6799 extraTags ?: React . ReactNode [ ] ;
68100 maxRows ?: number ;
101+ lazy ?: TagCollectionLazyLoad < AssetKey > ;
69102}
70103
71104/** This hook returns a containerRef and a moreLabelRef. It expects you to populate
@@ -141,20 +174,45 @@ export function useAdjustChildVisibilityToFill(moreLabelFn: (count: number) => s
141174}
142175
143176export const AssetKeyTagCollection = React . memo ( ( props : AssetKeyTagCollectionProps ) => {
144- const { assetKeys, useTags, extraTags, maxRows, dialogTitle = 'Assets in run' } = props ;
177+ const { assetKeys, useTags, extraTags, maxRows, dialogTitle = 'Assets in run' , lazy } = props ;
145178
146- const count = assetKeys ?. length ?? 0 ;
179+ const count = lazy ? lazy . totalCount : ( assetKeys ?. length ?? 0 ) ;
147180 const rendered = maxRows ? 10 : count === 1 ? 1 : 0 ;
148181
149- const { sortedAssetKeys, slicedSortedAssetKeys} = React . useMemo ( ( ) => {
150- const sortedAssetKeys = assetKeys ?. slice ( ) . sort ( sortItemAssetKey ) ?? [ ] ;
182+ // When the preview already holds the entire selection there is nothing more to fetch, so the
183+ // preview itself is the full list and actions needing it are ready immediately.
184+ const previewComplete = ! lazy || ( assetKeys ?. length ?? 0 ) >= lazy . totalCount ;
185+
186+ const { slicedSortedAssetKeys, fullAssetKeys} = React . useMemo ( ( ) => {
187+ const sortedPreview = assetKeys ?. slice ( ) . sort ( sortItemAssetKey ) ?? [ ] ;
188+ const sortedAll = lazy ?. allItems ? lazy . allItems . slice ( ) . sort ( sortItemAssetKey ) : null ;
151189 return {
152- sortedAssetKeys,
153- slicedSortedAssetKeys : sortedAssetKeys ?. slice ( 0 , rendered ) ?? [ ] ,
190+ slicedSortedAssetKeys : sortedPreview . slice ( 0 , rendered ) ,
191+ // The complete, sorted list: the preview when it already holds everything, otherwise the
192+ // lazily-loaded full list (null until it arrives).
193+ fullAssetKeys : previewComplete ? sortedPreview : sortedAll ,
154194 } ;
155- } , [ assetKeys , rendered ] ) ;
195+ } , [ assetKeys , rendered , lazy , previewComplete ] ) ;
156196
157- const { setShowMore, dialog} = useShowMoreDialog ( dialogTitle , sortedAssetKeys , renderItemAssetKey ) ;
197+ const { setShowMore, dialog} = useShowMoreDialog (
198+ dialogTitle ,
199+ fullAssetKeys ,
200+ renderItemAssetKey ,
201+ lazy ?. loading ,
202+ ) ;
203+
204+ // Kick off the lazy fetch (a no-op when the preview is already complete) when the overflow menu
205+ // opens or "View list" is clicked.
206+ const requestFull = React . useCallback ( ( ) => {
207+ if ( ! previewComplete ) {
208+ lazy ?. onRequest ( ) ;
209+ }
210+ } , [ previewComplete , lazy ] ) ;
211+
212+ const showMore = React . useCallback ( ( ) => {
213+ requestFull ( ) ;
214+ setShowMore ( true ) ;
215+ } , [ requestFull , setShowMore ] ) ;
158216
159217 const moreLabelFn = React . useCallback (
160218 ( displayed : number ) =>
@@ -223,23 +281,32 @@ export const AssetKeyTagCollection = React.memo((props: AssetKeyTagCollectionPro
223281 < span style = { useTags ? { } : { marginBottom : - 4 } } >
224282 < TagActionsPopover
225283 data = { { key : '' , value : '' } }
284+ onOpening = { requestFull }
226285 actions = { [
227286 {
228287 label : 'View list' ,
229- onClick : ( ) => setShowMore ( true ) ,
230- } ,
231- {
232- label : 'View lineage' ,
233- to : globalAssetGraphPathForAssets ( sortedAssetKeys ) ,
288+ onClick : showMore ,
234289 } ,
290+ // Lineage needs the complete set; disable it until the full list is available so we
291+ // never navigate with only the preview.
292+ fullAssetKeys
293+ ? {
294+ label : 'View lineage' ,
295+ to : globalAssetGraphPathForAssets ( fullAssetKeys ) ,
296+ }
297+ : {
298+ label : 'View lineage' ,
299+ to : '' ,
300+ disabled : true ,
301+ } ,
235302 ] }
236303 >
237304 { useTags ? (
238305 < Tag intent = "none" icon = "asset" >
239306 < span ref = { moreLabelRef } > { moreLabelFn ( 0 ) } </ span >
240307 </ Tag >
241308 ) : (
242- < ButtonLink onClick = { ( ) => setShowMore ( true ) } underline = "hover" >
309+ < ButtonLink onClick = { showMore } underline = "hover" >
243310 < Box
244311 flex = { { direction : 'row' , gap : 8 , alignItems : 'center' , display : 'inline-flex' } }
245312 >
@@ -256,34 +323,54 @@ export const AssetKeyTagCollection = React.memo((props: AssetKeyTagCollectionPro
256323 ) ;
257324} ) ;
258325
259- type Check = { name : string ; assetKey : AssetKey } ;
260-
261326interface AssetCheckTagCollectionProps {
327+ /**
328+ * The asset checks to render as inline tags. When `lazy` is provided this is only a bounded
329+ * preview — `lazy.totalCount` is the true count and `lazy.allItems` the full list (loaded on
330+ * demand). Without `lazy` it is the complete list.
331+ */
262332 assetChecks : Check [ ] | null ;
263333 dialogTitle ?: string ;
264334 maxRows ?: number ;
335+ lazy ?: TagCollectionLazyLoad < Check > ;
265336}
266337
267338export const AssetCheckTagCollection = React . memo ( ( props : AssetCheckTagCollectionProps ) => {
268- const { assetChecks, maxRows, dialogTitle = 'Asset checks in run' } = props ;
339+ const { assetChecks, maxRows, dialogTitle = 'Asset checks in run' , lazy } = props ;
269340
270- const count = assetChecks ?. length ?? 0 ;
341+ const count = lazy ? lazy . totalCount : ( assetChecks ?. length ?? 0 ) ;
271342 const rendered = maxRows ? 10 : count === 1 ? 1 : 0 ;
272343
273- const { sortedAssetChecks, slicedSortedAssetChecks} = React . useMemo ( ( ) => {
274- const sortedAssetChecks = assetChecks ?. slice ( ) . sort ( sortItemAssetCheck ) ?? [ ] ;
344+ // When the preview already holds every check there is nothing more to fetch.
345+ const previewComplete = ! lazy || ( assetChecks ?. length ?? 0 ) >= lazy . totalCount ;
346+
347+ const { slicedSortedAssetChecks, fullAssetChecks} = React . useMemo ( ( ) => {
348+ const sortedPreview = assetChecks ?. slice ( ) . sort ( sortItemAssetCheck ) ?? [ ] ;
349+ const sortedAll = lazy ?. allItems ? lazy . allItems . slice ( ) . sort ( sortItemAssetCheck ) : null ;
275350 return {
276- sortedAssetChecks ,
277- slicedSortedAssetChecks : sortedAssetChecks ?. slice ( 0 , rendered ) ?? [ ] ,
351+ slicedSortedAssetChecks : sortedPreview . slice ( 0 , rendered ) ,
352+ fullAssetChecks : previewComplete ? sortedPreview : sortedAll ,
278353 } ;
279- } , [ assetChecks , rendered ] ) ;
354+ } , [ assetChecks , rendered , lazy , previewComplete ] ) ;
280355
281356 const { setShowMore, dialog} = useShowMoreDialog (
282357 dialogTitle ,
283- sortedAssetChecks ,
358+ fullAssetChecks ,
284359 renderItemAssetCheck ,
360+ lazy ?. loading ,
285361 ) ;
286362
363+ const requestFull = React . useCallback ( ( ) => {
364+ if ( ! previewComplete ) {
365+ lazy ?. onRequest ( ) ;
366+ }
367+ } , [ previewComplete , lazy ] ) ;
368+
369+ const showMore = React . useCallback ( ( ) => {
370+ requestFull ( ) ;
371+ setShowMore ( true ) ;
372+ } , [ requestFull , setShowMore ] ) ;
373+
287374 const moreLabelFn = React . useCallback (
288375 ( displayed : number ) =>
289376 displayed === 0
@@ -327,10 +414,11 @@ export const AssetCheckTagCollection = React.memo((props: AssetCheckTagCollectio
327414 < span >
328415 < TagActionsPopover
329416 data = { { key : '' , value : '' } }
417+ onOpening = { requestFull }
330418 actions = { [
331419 {
332420 label : 'View list' ,
333- onClick : ( ) => setShowMore ( true ) ,
421+ onClick : showMore ,
334422 } ,
335423 ] }
336424 >
0 commit comments