From e9e6e1b190114fc4898eee7ae47c3ab8b867eec0 Mon Sep 17 00:00:00 2001 From: Marco Salazar Date: Tue, 12 Sep 2023 09:49:32 -0400 Subject: [PATCH 01/15] rfc asset graph sidebar for navigation --- .../src/asset-graph/AssetGraphExplorer.tsx | 408 ++++++++++-------- .../asset-graph/AssetGraphExplorerSidebar.tsx | 260 +++++++++++ .../ui-core/src/asset-graph/AssetNode.tsx | 2 +- 3 files changed, 484 insertions(+), 186 deletions(-) create mode 100644 js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorerSidebar.tsx diff --git a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorer.tsx b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorer.tsx index 495215bbecca4..6ef9e46bcfd78 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorer.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorer.tsx @@ -38,6 +38,7 @@ import {GraphQueryInput} from '../ui/GraphQueryInput'; import {Loading} from '../ui/Loading'; import {AssetEdges} from './AssetEdges'; +import {AssetGraphExplorerSidebar} from './AssetGraphExplorerSidebar'; import {AssetGraphJobSidebar} from './AssetGraphJobSidebar'; import {AssetGroupNode} from './AssetGroupNode'; import {AssetNode, AssetNodeMinimal} from './AssetNode'; @@ -161,6 +162,7 @@ const AssetGraphExplorerWithData: React.FC = ({ assetKey: {path: string[]}, node: GraphNode | null, ) => { + // todo (Toggle sidebar) e.stopPropagation(); const token = tokenForAssetKey(assetKey); @@ -269,200 +271,236 @@ const AssetGraphExplorerWithData: React.FC = ({ } }; + const selectNode = (e: React.MouseEvent, nodeId: string) => { + const node = assetGraphData.nodes[nodeId]; + if (node && viewportEl.current) { + onSelectNode(e, node.assetKey, node); + if (layout) { + viewportEl.current.zoomToSVGBox(layout.nodes[nodeId]!.bounds, true); + } + } + }; + const allowGroupsOnlyZoomLevel = !!(layout && Object.keys(layout.groups).length); return ( - {graphQueryItems.length === 0 ? ( - - ) : applyingEmptyDefault ? ( - - ) : Object.keys(assetGraphData.nodes).length === 0 ? ( - - ) : undefined} - {loading || !layout ? ( - - ) : ( - (viewportEl.current = r || undefined)} - defaultZoom={flagHorizontalDAGs ? 'zoom-to-fit-width' : 'zoom-to-fit'} - interactor={SVGViewport.Interactors.PanAndZoom} - graphWidth={layout.width} - graphHeight={layout.height} - graphHasNoMinimumZoom={allowGroupsOnlyZoomLevel} - onClick={onClickBackground} - onArrowKeyDown={onArrowKeyDown} - onDoubleClick={(e) => { - viewportEl.current?.autocenter(true); - e.stopPropagation(); - }} - maxZoom={1.2} - maxAutocenterZoom={1.0} - > - {({scale}) => ( - - + } + second={ + + {graphQueryItems.length === 0 ? ( + + ) : applyingEmptyDefault ? ( + + ) : Object.keys(assetGraphData.nodes).length === 0 ? ( + + ) : undefined} + {loading || !layout ? ( + + ) : ( + (viewportEl.current = r || undefined)} + defaultZoom={flagHorizontalDAGs ? 'zoom-to-fit-width' : 'zoom-to-fit'} + interactor={SVGViewport.Interactors.PanAndZoom} + graphWidth={layout.width} + graphHeight={layout.height} + graphHasNoMinimumZoom={allowGroupsOnlyZoomLevel} + onClick={onClickBackground} + onArrowKeyDown={onArrowKeyDown} + onDoubleClick={(e) => { + viewportEl.current?.autocenter(true); + e.stopPropagation(); + }} + maxZoom={1.2} + maxAutocenterZoom={1.0} + > + {({scale}) => ( + + + + {Object.values(layout.groups) + .sort((a, b) => a.id.length - b.id.length) + .map((group) => ( + { + if (!viewportEl.current) { + return; + } + const targetScale = viewportEl.current.scaleForSVGBounds( + group.bounds.width, + group.bounds.height, + ); + viewportEl.current.zoomToSVGBox( + group.bounds, + true, + targetScale * 0.9, + ); + e.stopPropagation(); + }} + > + + + ))} + + {Object.values(layout.nodes).map(({id, bounds}) => { + const graphNode = assetGraphData.nodes[id]!; + const path = JSON.parse(id); + if (allowGroupsOnlyZoomLevel && scale < GROUPS_ONLY_SCALE) { + return; + } + return ( + setHighlighted(id)} + onMouseLeave={() => setHighlighted(null)} + onClick={(e) => onSelectNode(e, {path}, graphNode)} + onDoubleClick={(e) => { + viewportEl.current?.zoomToSVGBox(bounds, true, 1.2); + e.stopPropagation(); + }} + style={{overflow: 'visible'}} + > + {!graphNode ? ( + + ) : scale < MINIMAL_SCALE ? ( + + ) : ( + + )} + + ); + })} + + )} + + )} + {setOptions && ( + + { + onChangeExplorerPath( + {...explorerPath, opNames: selectedDefinitions[0]?.opNames || []}, + 'replace', + ); + setOptions({ + ...options, + preferAssetRendering: !options.preferAssetRendering, + }); + }} /> + + )} - {Object.values(layout.groups) - .sort((a, b) => a.id.length - b.id.length) - .map((group) => ( - { - if (!viewportEl.current) { - return; - } - const targetScale = viewportEl.current.scaleForSVGBounds( - group.bounds.width, - group.bounds.height, - ); - viewportEl.current.zoomToSVGBox(group.bounds, true, targetScale * 0.9); - e.stopPropagation(); - }} - > - - - ))} - - {Object.values(layout.nodes).map(({id, bounds}) => { - const graphNode = assetGraphData.nodes[id]!; - const path = JSON.parse(id); - if (allowGroupsOnlyZoomLevel && scale < GROUPS_ONLY_SCALE) { - return; + + + + a.isObservable)} + : {all: allDefinitionsForMaterialize.filter((a) => a.isObservable)} } - return ( - setHighlighted(id)} - onMouseLeave={() => setHighlighted(null)} - onClick={(e) => onSelectNode(e, {path}, graphNode)} - onDoubleClick={(e) => { - viewportEl.current?.zoomToSVGBox(bounds, true, 1.2); - e.stopPropagation(); - }} - style={{overflow: 'visible'}} - > - {!graphNode ? ( - - ) : scale < MINIMAL_SCALE ? ( - - ) : ( - - )} - - ); - })} - - )} - - )} - {setOptions && ( - - { - onChangeExplorerPath( - {...explorerPath, opNames: selectedDefinitions[0]?.opNames || []}, - 'replace', - ); - setOptions({ - ...options, - preferAssetRendering: !options.preferAssetRendering, - }); - }} - /> - - )} - - - - - a.isObservable)} - : {all: allDefinitionsForMaterialize.filter((a) => a.isObservable)} - } - /> - - - - - {fetchOptionFilters} - - onChangeExplorerPath({...explorerPath, opsQuery}, 'replace')} - popoverPosition="bottom-left" - /> - - - } - second={ - selectedGraphNodes.length === 1 && selectedGraphNodes[0] ? ( - - - - + + + + + {fetchOptionFilters} + + + onChangeExplorerPath({...explorerPath, opsQuery}, 'replace') + } + popoverPosition="bottom-left" /> - - - - ) : fetchOptions.pipelineSelector ? ( - - - - - - - - ) : null + + + } + second={ + selectedGraphNodes.length === 1 && selectedGraphNodes[0] ? ( + + + + + + + + ) : fetchOptions.pipelineSelector ? ( + + + + + + + + ) : null + } + /> } /> ); diff --git a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorerSidebar.tsx b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorerSidebar.tsx new file mode 100644 index 0000000000000..256fc6e54207c --- /dev/null +++ b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorerSidebar.tsx @@ -0,0 +1,260 @@ +import {Box, ButtonLink, Colors, Icon, useViewport} from '@dagster-io/ui-components'; +import {useVirtualizer} from '@tanstack/react-virtual'; +import React from 'react'; + +import {withMiddleTruncation} from '../app/Util'; +import {Container, Inner, Row} from '../ui/VirtualizedTable'; + +import {NameTooltipStyle} from './AssetNode'; +import {GraphData, GraphNode} from './Utils'; + +const COLLATOR = new Intl.Collator(navigator.language, {sensitivity: 'base', numeric: true}); + +type GroupNode = { + nodes: string[]; + groupName: string | null; + repositoryLocationName: string; + repositoryName: string; + id: string; +}; + +export const AssetGraphExplorerSidebar = ({ + assetGraphData, + lastSelectedNode, + selectNode, +}: { + assetGraphData: GraphData; + lastSelectedNode: GraphNode; + selectNode: (e: React.MouseEvent, nodeId: string) => void; +}) => { + const [openNodes, setOpenNodes] = React.useState>(() => new Set()); + const {rootGroupsWithRootNodes, nodeToGroupId} = React.useMemo(() => { + const nodeToGroupId: Record = {}; + const groups: Record = {}; + Object.entries(assetGraphData.nodes).forEach(([_, node]) => { + const groupName = node.definition.groupName; + const repositoryLocationName = node.definition.repository.location.name; + const repositoryName = node.definition.repository.name; + const groupId = `${groupName}@${repositoryName}@${repositoryLocationName}`; + groups[groupId] = groups[groupId] || { + nodes: [], + groupName, + repositoryLocationName, + repositoryName, + id: groupId, + }; + groups[groupId]!.nodes.push(node.id); + nodeToGroupId[node.id] = groupId; + }); + Object.entries(groups).forEach(([_, group]) => { + group.nodes = group.nodes + .filter((nodeId) => !assetGraphData.upstream[nodeId]) + .sort((nodeA, nodeB) => COLLATOR.compare(nodeA, nodeB)); + }); + return {rootGroupsWithRootNodes: groups, nodeToGroupId}; + }, [assetGraphData]); + + const renderedNodes = React.useMemo(() => { + const queue = Object.entries(rootGroupsWithRootNodes).map(([_, {id}]) => ({level: 1, id})); + + const renderedNodes: {level: number; id: string}[] = []; + while (queue.length) { + const node = queue.shift()!; + renderedNodes.push(node); + if (openNodes.has(node.id)) { + const groupNode = rootGroupsWithRootNodes[node.id]; + if (groupNode) { + const downstream = groupNode.nodes; + if (downstream.length) { + queue.unshift(...downstream.map((id) => ({level: 2, id}))); + } + } else { + const downstream = assetGraphData.downstream[node.id]; + if (downstream) { + queue.unshift(...Object.keys(downstream).map((id) => ({level: node.level + 1, id}))); + } + } + } + } + return renderedNodes; + }, [assetGraphData.downstream, openNodes, rootGroupsWithRootNodes]); + + const containerRef = React.useRef(null); + + const rowVirtualizer = useVirtualizer({ + count: renderedNodes.length, + getScrollElement: () => containerRef.current, + estimateSize: () => 32, + overscan: 5, + }); + + const totalHeight = rowVirtualizer.getTotalSize(); + const items = rowVirtualizer.getVirtualItems(); + + React.useLayoutEffect(() => { + requestAnimationFrame(() => { + rowVirtualizer.measure(); + }); + }, [rowVirtualizer]); + + const indexOfLastSelectedNode = React.useMemo( + () => + lastSelectedNode ? renderedNodes.findIndex((node) => node.id === lastSelectedNode.id) : -1, + [renderedNodes, lastSelectedNode], + ); + + React.useLayoutEffect(() => { + if (indexOfLastSelectedNode !== -1) { + rowVirtualizer.scrollToIndex(indexOfLastSelectedNode); + } + }, [indexOfLastSelectedNode, rowVirtualizer]); + + React.useEffect(() => { + if (lastSelectedNode) { + setOpenNodes((nodes) => { + const nextOpenNodes = new Set(nodes); + nextOpenNodes.add(lastSelectedNode.id); + const upstreamQueue = Object.keys(assetGraphData.upstream[lastSelectedNode.id] ?? {}); + while (upstreamQueue.length) { + const next = upstreamQueue.pop()!; + const nextUpstream = Object.keys(assetGraphData.upstream[next] ?? {}); + upstreamQueue.push(...nextUpstream); + nextOpenNodes.add(next); + } + nextOpenNodes.add(nodeToGroupId[lastSelectedNode.id]!); + return nextOpenNodes; + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [lastSelectedNode, nodeToGroupId]); + + const {containerProps, viewport} = useViewport(); + + React.useLayoutEffect(() => { + rowVirtualizer.measure(); + console.log('measuring'); + }, [viewport.width, viewport.height, rowVirtualizer]); + + return ( + { + if (el) { + containerProps.ref(el); + containerRef.current = el; + } + }} + > + + {items.map(({index, key, size, start, measureElement}) => { + const node = renderedNodes[index]!; + return ( + + { + selectNode(e, node.id); + setOpenNodes((nodes) => { + const openNodes = new Set(nodes); + const isOpen = openNodes.has(node.id); + if (isOpen) { + openNodes.delete(node.id); + } else { + openNodes.add(node.id); + } + return openNodes; + }); + }} + measureElement={measureElement} + /> + + ); + })} + + + ); +}; + +const Node = ({ + assetGraphData, + node, + level, + toggleOpen, + isOpen, + measureElement, + isSelected, +}: { + assetGraphData: GraphData; + node: GraphNode | GroupNode; + level: number; + toggleOpen: (e: React.MouseEvent) => void; + isOpen: boolean; + measureElement: (el: HTMLDivElement) => void; + isSelected: boolean; +}) => { + const isGroupNode = 'groupName' in node; + + const displayName = isGroupNode + ? `${node.groupName ?? 'default'} in ${node.repositoryName}@${node.repositoryLocationName}` + : node.assetKey.path[node.assetKey.path.length - 1]!; + + const upstream = isGroupNode ? [] : Object.keys(assetGraphData.upstream[node.id] ?? {}); + const downstream = isGroupNode + ? node.nodes + : Object.keys(assetGraphData.downstream[node.id] ?? {}); + const elementRef = React.useRef(null); + React.useLayoutEffect(() => { + if (elementRef.current) { + measureElement(elementRef.current); + } + }, [measureElement, isOpen]); + + return ( + + {downstream.length ? ( + + ) : null} + +
+ {withMiddleTruncation(displayName, {maxLength: 30})} +
+ {upstream.length > 1 ? {upstream.length} : null} +
+
+ ); +}; diff --git a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetNode.tsx b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetNode.tsx index f498ef177ff03..f005c137e3d33 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetNode.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetNode.tsx @@ -334,7 +334,7 @@ const NameTooltipCSS: CSSObject = { fontSize: 16.8, }; -const NameTooltipStyle = JSON.stringify({ +export const NameTooltipStyle = JSON.stringify({ ...NameTooltipCSS, background: Colors.Blue50, border: `1px solid ${Colors.Blue100}`, From a81fae1452bd61f5fe4c473a9527adbcce36fb09 Mon Sep 17 00:00:00 2001 From: Marco Salazar Date: Tue, 12 Sep 2023 09:55:00 -0400 Subject: [PATCH 02/15] rm comment --- .../packages/ui-core/src/asset-graph/AssetGraphExplorer.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorer.tsx b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorer.tsx index 6ef9e46bcfd78..a71d5eb59a3d7 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorer.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorer.tsx @@ -162,7 +162,6 @@ const AssetGraphExplorerWithData: React.FC = ({ assetKey: {path: string[]}, node: GraphNode | null, ) => { - // todo (Toggle sidebar) e.stopPropagation(); const token = tokenForAssetKey(assetKey); From 2b86ca8bed108757a51b305bae6d5dde9304dd65 Mon Sep 17 00:00:00 2001 From: Marco Salazar Date: Tue, 12 Sep 2023 10:30:49 -0400 Subject: [PATCH 03/15] consolidate select node logic --- .../ui-core/src/asset-graph/AssetGraphExplorer.tsx | 11 +++++------ .../src/asset-graph/AssetGraphExplorerSidebar.tsx | 1 - 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorer.tsx b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorer.tsx index a71d5eb59a3d7..c0b6c8e1a3939 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorer.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorer.tsx @@ -263,14 +263,13 @@ const AssetGraphExplorerWithData: React.FC = ({ const layoutWithoutExternalLinks = {...layout, nodes: pickBy(layout.nodes, hasDefinition)}; const nextId = closestNodeInDirection(layoutWithoutExternalLinks, lastSelectedNode.id, dir); - const node = nextId && assetGraphData.nodes[nextId]; - if (node && viewportEl.current) { - onSelectNode(e, node.assetKey, node); - viewportEl.current.zoomToSVGBox(layout.nodes[nextId]!.bounds, true); - } + selectNodeById(e, nextId); }; - const selectNode = (e: React.MouseEvent, nodeId: string) => { + const selectNodeById = (e: React.MouseEvent | React.KeyboardEvent, nodeId?: string) => { + if (!nodeId) { + return; + } const node = assetGraphData.nodes[nodeId]; if (node && viewportEl.current) { onSelectNode(e, node.assetKey, node); diff --git a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorerSidebar.tsx b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorerSidebar.tsx index 256fc6e54207c..65a825560666a 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorerSidebar.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorerSidebar.tsx @@ -132,7 +132,6 @@ export const AssetGraphExplorerSidebar = ({ React.useLayoutEffect(() => { rowVirtualizer.measure(); - console.log('measuring'); }, [viewport.width, viewport.height, rowVirtualizer]); return ( From 69d1bd900821cf52f7eebcde0f88d1a79b4e412b Mon Sep 17 00:00:00 2001 From: Marco Salazar Date: Tue, 12 Sep 2023 10:31:23 -0400 Subject: [PATCH 04/15] by id --- .../packages/ui-core/src/asset-graph/AssetGraphExplorer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorer.tsx b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorer.tsx index c0b6c8e1a3939..94fea9a25ae77 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorer.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorer.tsx @@ -290,7 +290,7 @@ const AssetGraphExplorerWithData: React.FC = ({ } second={ From ae378c4fec8563829eb8984d7c9abccef87cff69 Mon Sep 17 00:00:00 2001 From: Marco Salazar Date: Tue, 12 Sep 2023 12:50:05 -0400 Subject: [PATCH 05/15] popover --- .../packages/ui-components/src/index.ts | 1 + .../src/asset-graph/AssetGraphExplorer.tsx | 10 +- .../asset-graph/AssetGraphExplorerSidebar.tsx | 185 +++++++++++++++--- 3 files changed, 165 insertions(+), 31 deletions(-) diff --git a/js_modules/dagster-ui/packages/ui-components/src/index.ts b/js_modules/dagster-ui/packages/ui-components/src/index.ts index 4c209b68e80ca..ae0b7cd48f0a2 100644 --- a/js_modules/dagster-ui/packages/ui-components/src/index.ts +++ b/js_modules/dagster-ui/packages/ui-components/src/index.ts @@ -50,6 +50,7 @@ export * from './components/styles'; export * from './components/useSuggestionsForString'; export * from './components/ErrorBoundary'; export * from './components/useViewport'; +export * from './components/UnstyledButton'; export * from './components/StyledRawCodeMirror'; // Global font styles, exported as styled-component components to render in diff --git a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorer.tsx b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorer.tsx index 94fea9a25ae77..3873d92748fc5 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorer.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorer.tsx @@ -271,13 +271,19 @@ const AssetGraphExplorerWithData: React.FC = ({ return; } const node = assetGraphData.nodes[nodeId]; - if (node && viewportEl.current) { + if (node) { onSelectNode(e, node.assetKey, node); - if (layout) { + if (layout && viewportEl.current) { viewportEl.current.zoomToSVGBox(layout.nodes[nodeId]!.bounds, true); } } }; + React.useEffect(() => { + if (layout && lastSelectedNode) { + selectNodeById({stopPropagation: () => {}} as any, lastSelectedNode.id); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [!!layout]); const allowGroupsOnlyZoomLevel = !!(layout && Object.keys(layout.groups).length); diff --git a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorerSidebar.tsx b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorerSidebar.tsx index 65a825560666a..1d60c1d8ba497 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorerSidebar.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorerSidebar.tsx @@ -1,6 +1,20 @@ -import {Box, ButtonLink, Colors, Icon, useViewport} from '@dagster-io/ui-components'; +import { + Box, + Button, + Colors, + Dialog, + DialogBody, + DialogFooter, + Icon, + Menu, + MenuItem, + Popover, + useViewport, + UnstyledButton, +} from '@dagster-io/ui-components'; import {useVirtualizer} from '@tanstack/react-virtual'; import React from 'react'; +import styled from 'styled-components'; import {withMiddleTruncation} from '../app/Util'; import {Container, Inner, Row} from '../ui/VirtualizedTable'; @@ -18,6 +32,14 @@ type GroupNode = { id: string; }; +const indentColors = [ + Colors.Blue100, + Colors.LightPurple, + Colors.Yellow200, + Colors.Gray300, + Colors.KeylineGray, +]; + export const AssetGraphExplorerSidebar = ({ assetGraphData, lastSelectedNode, @@ -126,7 +148,7 @@ export const AssetGraphExplorerSidebar = ({ }); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [lastSelectedNode, nodeToGroupId]); + }, [lastSelectedNode, nodeToGroupId[lastSelectedNode?.id ?? 0]]); const {containerProps, viewport} = useViewport(); @@ -161,8 +183,7 @@ export const AssetGraphExplorerSidebar = ({ node={(assetGraphData.nodes[node.id] ?? rootGroupsWithRootNodes[node.id])!} level={node.level} isSelected={lastSelectedNode?.id === node.id} - toggleOpen={(e: React.MouseEvent) => { - selectNode(e, node.id); + toggleOpen={() => { setOpenNodes((nodes) => { const openNodes = new Set(nodes); const isOpen = openNodes.has(node.id); @@ -174,6 +195,7 @@ export const AssetGraphExplorerSidebar = ({ return openNodes; }); }} + selectNode={selectNode} measureElement={measureElement} /> @@ -189,6 +211,7 @@ const Node = ({ node, level, toggleOpen, + selectNode, isOpen, measureElement, isSelected, @@ -196,7 +219,8 @@ const Node = ({ assetGraphData: GraphData; node: GraphNode | GroupNode; level: number; - toggleOpen: (e: React.MouseEvent) => void; + toggleOpen: () => void; + selectNode: (e: React.MouseEvent, nodeId: string) => void; isOpen: boolean; measureElement: (el: HTMLDivElement) => void; isSelected: boolean; @@ -218,42 +242,145 @@ const Node = ({ } }, [measureElement, isOpen]); + const [showDownstream, setShowDownstream] = React.useState(false); + const [showUpstream, setShowUpstream] = React.useState(false); + return ( selectNode(e, node.id)} border={{side: 'bottom', width: 1, color: Colors.KeylineGray}} style={{ - ...(downstream ? {cursor: 'pointer'} : {}), + ...(downstream && !isGroupNode ? {cursor: 'pointer'} : {}), ...(isSelected ? {background: Colors.LightPurple} : {}), }} - padding={{vertical: 8, left: (8 * level + (downstream.length ? 0 : 20)) as any, right: 24}} - flex={{direction: 'row', gap: 4, alignItems: 'center'}} + padding={{left: (8 * level) as any, right: 24}} > - {downstream.length ? ( - - ) : null} -
+ +
+ ) : null} + - {withMiddleTruncation(displayName, {maxLength: 30})} - - {upstream.length > 1 ? {upstream.length} : null} +
+ {withMiddleTruncation(displayName, {maxLength: 30})} +
+ assetGraphData.nodes[id]!)} + isOpen={showDownstream} + close={() => { + setShowDownstream(false); + }} + selectNode={selectNode} + /> + assetGraphData.nodes[id]!)} + isOpen={showUpstream} + close={() => { + setShowUpstream(false); + }} + selectNode={selectNode} + /> + + {upstream.length ? ( + { + setShowUpstream(true); + }} + /> + ) : null} + {downstream.length ? ( + { + setShowDownstream(true); + }} + /> + ) : null} + + } + hoverOpenDelay={100} + hoverCloseDelay={100} + placement="top" + shouldReturnFocusOnClose + > +
+ +
+
+
); }; + +const UpstreamDownstreamDialog = ({ + title, + assets, + isOpen, + close, + selectNode, +}: { + title: string; + assets: GraphNode[]; + isOpen: boolean; + close: () => void; + selectNode: (e: React.MouseEvent, nodeId: string) => void; +}) => { + return ( + + + {assets.map((asset) => ( + { + selectNode(e, asset.id); + close(); + }} + > + + {asset.assetKey.path[asset.assetKey.path.length - 1]} + + + ))} + + + + + + ); +}; + +const DialogAssetButton = styled(Box)` + border-radius: 8px; + &:hover { + background: ${Colors.Gray100}; + } +`; From e804ba1dfa53b3d4f8c5377fe1fb2dbae5d7ffcd Mon Sep 17 00:00:00 2001 From: Marco Salazar Date: Tue, 12 Sep 2023 12:52:07 -0400 Subject: [PATCH 06/15] measure in setTimeout cuz it less janky --- .../ui-core/src/asset-graph/AssetGraphExplorerSidebar.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorerSidebar.tsx b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorerSidebar.tsx index 1d60c1d8ba497..048ad9d89c176 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorerSidebar.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorerSidebar.tsx @@ -237,9 +237,11 @@ const Node = ({ : Object.keys(assetGraphData.downstream[node.id] ?? {}); const elementRef = React.useRef(null); React.useLayoutEffect(() => { - if (elementRef.current) { - measureElement(elementRef.current); - } + setTimeout(() => { + if (elementRef.current) { + measureElement(elementRef.current); + } + }, 100); }, [measureElement, isOpen]); const [showDownstream, setShowDownstream] = React.useState(false); From 53e48b330933c2d00ed1785497a8460d98a63e3c Mon Sep 17 00:00:00 2001 From: Marco Salazar Date: Tue, 12 Sep 2023 19:37:12 -0400 Subject: [PATCH 07/15] sofar --- .../asset-graph/AssetGraphExplorerSidebar.tsx | 574 ++++++++++-------- .../ui-core/src/assets/AssetGroupRoot.tsx | 2 +- 2 files changed, 326 insertions(+), 250 deletions(-) diff --git a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorerSidebar.tsx b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorerSidebar.tsx index 048ad9d89c176..3b3b15f25c7e8 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorerSidebar.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorerSidebar.tsx @@ -11,6 +11,7 @@ import { Popover, useViewport, UnstyledButton, + TextInput, } from '@dagster-io/ui-components'; import {useVirtualizer} from '@tanstack/react-virtual'; import React from 'react'; @@ -32,179 +33,200 @@ type GroupNode = { id: string; }; -const indentColors = [ - Colors.Blue100, - Colors.LightPurple, - Colors.Yellow200, - Colors.Gray300, - Colors.KeylineGray, -]; +export const AssetGraphExplorerSidebar = React.memo( + ({ + assetGraphData, + lastSelectedNode, + selectNode, + }: { + assetGraphData: GraphData; + lastSelectedNode: GraphNode; + selectNode: (e: React.MouseEvent, nodeId: string) => void; + }) => { + const {rootGroupsWithRootNodes, nodeToGroupId} = React.useMemo(() => { + const nodeToGroupId: Record = {}; + const groups: Record = {}; + Object.entries(assetGraphData.nodes).forEach(([_, node]) => { + const groupName = node.definition.groupName; + const repositoryLocationName = node.definition.repository.location.name; + const repositoryName = node.definition.repository.name; + const groupId = `${groupName}@${repositoryName}@${repositoryLocationName}`; + groups[groupId] = groups[groupId] || { + nodes: [], + groupName, + repositoryLocationName, + repositoryName, + id: groupId, + }; + groups[groupId]!.nodes.push(node.id); + nodeToGroupId[node.id] = groupId; + }); + Object.entries(groups).forEach(([_, group]) => { + group.nodes = group.nodes + // Exclude nodes without data, these are nodes from another group which we don't have graph data for, + // We don't want those nodes to show up as roots in the sidebar + .filter( + (nodeId) => + Object.keys(assetGraphData.upstream[nodeId] || {}).filter( + (id) => !!assetGraphData.nodes[id], + ).length === 0, + ) + .sort((nodeA, nodeB) => COLLATOR.compare(nodeA, nodeB)); + }); + return {rootGroupsWithRootNodes: groups, nodeToGroupId}; + }, [assetGraphData]); -export const AssetGraphExplorerSidebar = ({ - assetGraphData, - lastSelectedNode, - selectNode, -}: { - assetGraphData: GraphData; - lastSelectedNode: GraphNode; - selectNode: (e: React.MouseEvent, nodeId: string) => void; -}) => { - const [openNodes, setOpenNodes] = React.useState>(() => new Set()); - const {rootGroupsWithRootNodes, nodeToGroupId} = React.useMemo(() => { - const nodeToGroupId: Record = {}; - const groups: Record = {}; - Object.entries(assetGraphData.nodes).forEach(([_, node]) => { - const groupName = node.definition.groupName; - const repositoryLocationName = node.definition.repository.location.name; - const repositoryName = node.definition.repository.name; - const groupId = `${groupName}@${repositoryName}@${repositoryLocationName}`; - groups[groupId] = groups[groupId] || { - nodes: [], - groupName, - repositoryLocationName, - repositoryName, - id: groupId, - }; - groups[groupId]!.nodes.push(node.id); - nodeToGroupId[node.id] = groupId; - }); - Object.entries(groups).forEach(([_, group]) => { - group.nodes = group.nodes - .filter((nodeId) => !assetGraphData.upstream[nodeId]) - .sort((nodeA, nodeB) => COLLATOR.compare(nodeA, nodeB)); + const [openNodes, setOpenNodes] = React.useState>(() => { + const set = new Set(); + if (Object.keys(rootGroupsWithRootNodes).length === 1) { + set.add(Object.keys(rootGroupsWithRootNodes)[0]!); + } + return set; }); - return {rootGroupsWithRootNodes: groups, nodeToGroupId}; - }, [assetGraphData]); - const renderedNodes = React.useMemo(() => { - const queue = Object.entries(rootGroupsWithRootNodes).map(([_, {id}]) => ({level: 1, id})); + const renderedNodes = React.useMemo(() => { + const queue = Object.entries(rootGroupsWithRootNodes).map(([_, {id}]) => ({level: 1, id})); - const renderedNodes: {level: number; id: string}[] = []; - while (queue.length) { - const node = queue.shift()!; - renderedNodes.push(node); - if (openNodes.has(node.id)) { - const groupNode = rootGroupsWithRootNodes[node.id]; - if (groupNode) { - const downstream = groupNode.nodes; - if (downstream.length) { - queue.unshift(...downstream.map((id) => ({level: 2, id}))); + const renderedNodes: {level: number; id: string}[] = []; + while (queue.length) { + const node = queue.shift()!; + renderedNodes.push(node); + if (openNodes.has(node.id)) { + const groupNode = rootGroupsWithRootNodes[node.id]; + let downstream; + if (groupNode) { + downstream = groupNode.nodes; + } else { + downstream = Object.keys(assetGraphData.downstream[node.id] || {}); } - } else { - const downstream = assetGraphData.downstream[node.id]; if (downstream) { - queue.unshift(...Object.keys(downstream).map((id) => ({level: node.level + 1, id}))); + queue.unshift(...downstream.map((id) => ({level: node.level + 1, id}))); } } } - } - return renderedNodes; - }, [assetGraphData.downstream, openNodes, rootGroupsWithRootNodes]); - - const containerRef = React.useRef(null); + return renderedNodes.filter( + // Exclude nodes without data, these are nodes from another group which we don't have graph data for, + // We don't want those nodes to show up as leaf nodes in the sidebar + ({id}) => !!assetGraphData.nodes[id] || !!rootGroupsWithRootNodes[id], + ); + }, [assetGraphData, openNodes, rootGroupsWithRootNodes]); - const rowVirtualizer = useVirtualizer({ - count: renderedNodes.length, - getScrollElement: () => containerRef.current, - estimateSize: () => 32, - overscan: 5, - }); + const containerRef = React.useRef(null); + const {containerProps, viewport} = useViewport(); - const totalHeight = rowVirtualizer.getTotalSize(); - const items = rowVirtualizer.getVirtualItems(); + const rowVirtualizer = useVirtualizer({ + count: renderedNodes.length, + getScrollElement: () => containerRef.current, + estimateSize: () => 34, - React.useLayoutEffect(() => { - requestAnimationFrame(() => { - rowVirtualizer.measure(); + // TODO: Figure out why virtualizer isn't filling up all of the space automatically... + overscan: viewport.height / 34, }); - }, [rowVirtualizer]); - const indexOfLastSelectedNode = React.useMemo( - () => - lastSelectedNode ? renderedNodes.findIndex((node) => node.id === lastSelectedNode.id) : -1, - [renderedNodes, lastSelectedNode], - ); + const totalHeight = rowVirtualizer.getTotalSize(); + const items = rowVirtualizer.getVirtualItems(); - React.useLayoutEffect(() => { - if (indexOfLastSelectedNode !== -1) { - rowVirtualizer.scrollToIndex(indexOfLastSelectedNode); - } - }, [indexOfLastSelectedNode, rowVirtualizer]); - - React.useEffect(() => { - if (lastSelectedNode) { - setOpenNodes((nodes) => { - const nextOpenNodes = new Set(nodes); - nextOpenNodes.add(lastSelectedNode.id); - const upstreamQueue = Object.keys(assetGraphData.upstream[lastSelectedNode.id] ?? {}); - while (upstreamQueue.length) { - const next = upstreamQueue.pop()!; - const nextUpstream = Object.keys(assetGraphData.upstream[next] ?? {}); - upstreamQueue.push(...nextUpstream); - nextOpenNodes.add(next); - } - nextOpenNodes.add(nodeToGroupId[lastSelectedNode.id]!); - return nextOpenNodes; - }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [lastSelectedNode, nodeToGroupId[lastSelectedNode?.id ?? 0]]); + const indexOfLastSelectedNode = React.useMemo( + () => + lastSelectedNode ? renderedNodes.findIndex((node) => node.id === lastSelectedNode.id) : -1, + [renderedNodes, lastSelectedNode], + ); - const {containerProps, viewport} = useViewport(); + React.useLayoutEffect(() => { + if (indexOfLastSelectedNode !== -1) { + rowVirtualizer.scrollToIndex(indexOfLastSelectedNode); + } + }, [indexOfLastSelectedNode, rowVirtualizer]); - React.useLayoutEffect(() => { - rowVirtualizer.measure(); - }, [viewport.width, viewport.height, rowVirtualizer]); + React.useEffect(() => { + if (lastSelectedNode) { + setOpenNodes((nodes) => { + const nextOpenNodes = new Set(nodes); + nextOpenNodes.add(lastSelectedNode.id); + const upstreamQueue = Object.keys(assetGraphData.upstream[lastSelectedNode.id] ?? {}); + while (upstreamQueue.length) { + const next = upstreamQueue.pop()!; + const nextUpstream = Object.keys(assetGraphData.upstream[next] ?? {}); + upstreamQueue.push(...nextUpstream); + nextOpenNodes.add(next); + } + nextOpenNodes.add(nodeToGroupId[lastSelectedNode.id]!); + return nextOpenNodes; + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [lastSelectedNode, nodeToGroupId[lastSelectedNode?.id ?? 0]]); - return ( - { - if (el) { - containerProps.ref(el); - containerRef.current = el; - } - }} - > - - {items.map(({index, key, size, start, measureElement}) => { - const node = renderedNodes[index]!; - return ( - - { - setOpenNodes((nodes) => { - const openNodes = new Set(nodes); - const isOpen = openNodes.has(node.id); - if (isOpen) { - openNodes.delete(node.id); - } else { - openNodes.add(node.id); - } - return openNodes; - }); - }} - selectNode={selectNode} - measureElement={measureElement} - /> - - ); - })} - - - ); -}; + return ( + + + + + + + + + + + + + { + if (el) { + console.log({el}); + containerProps.ref(el); + containerRef.current = el; + } + }} + > + + {items.map(({index, key, size, start, measureElement}) => { + const node = renderedNodes[index]!; + const row = assetGraphData.nodes[node.id] ?? rootGroupsWithRootNodes[node.id]; + setTimeout(measureElement, 100); + return ( + + {row ? ( + { + setOpenNodes((nodes) => { + const openNodes = new Set(nodes); + const isOpen = openNodes.has(node.id); + if (isOpen) { + openNodes.delete(node.id); + } else { + openNodes.add(node.id); + } + return openNodes; + }); + }} + selectNode={selectNode} + measureElement={measureElement} + /> + ) : null} + + ); + })} + + + + + ); + }, +); const Node = ({ assetGraphData, @@ -231,10 +253,16 @@ const Node = ({ ? `${node.groupName ?? 'default'} in ${node.repositoryName}@${node.repositoryLocationName}` : node.assetKey.path[node.assetKey.path.length - 1]!; - const upstream = isGroupNode ? [] : Object.keys(assetGraphData.upstream[node.id] ?? {}); + const upstream = isGroupNode + ? [] + : Object.keys(assetGraphData.upstream[node.id] ?? {}).filter( + (id) => !!assetGraphData.nodes[id], + ); const downstream = isGroupNode ? node.nodes - : Object.keys(assetGraphData.downstream[node.id] ?? {}); + : Object.keys(assetGraphData.downstream[node.id] ?? {}).filter( + (id) => !!assetGraphData.nodes[id], + ); const elementRef = React.useRef(null); React.useLayoutEffect(() => { setTimeout(() => { @@ -251,92 +279,97 @@ const Node = ({ selectNode(e, node.id)} - border={{side: 'bottom', width: 1, color: Colors.KeylineGray}} style={{ ...(downstream && !isGroupNode ? {cursor: 'pointer'} : {}), - ...(isSelected ? {background: Colors.LightPurple} : {}), }} - padding={{left: (8 * level) as any, right: 24}} > - - {downstream.length ? ( -
- -
- ) : null} - -
- {withMiddleTruncation(displayName, {maxLength: 30})} -
- assetGraphData.nodes[id]!)} - isOpen={showDownstream} - close={() => { - setShowDownstream(false); - }} - selectNode={selectNode} - /> - assetGraphData.nodes[id]!)} - isOpen={showUpstream} - close={() => { - setShowUpstream(false); + + + {downstream.length ? ( +
{ + e.stopPropagation(); + toggleOpen(); + }} + style={{cursor: 'pointer'}} + > + +
+ ) : null} + - - {upstream.length ? ( - { - setShowUpstream(true); - }} - /> - ) : null} - {downstream.length ? ( - { - setShowDownstream(true); - }} - /> - ) : null} - - } - hoverOpenDelay={100} - hoverCloseDelay={100} - placement="top" - shouldReturnFocusOnClose + padding={{horizontal: 12, vertical: 8}} + style={{borderRadius: '8px', ...(isSelected ? {background: Colors.Gray100} : {})}} > -
- +
+ {withMiddleTruncation(displayName, {maxLength: 30})}
- + { + setShowDownstream(false); + }} + selectNode={selectNode} + /> + { + setShowUpstream(false); + }} + selectNode={selectNode} + /> + + {upstream.length ? ( + { + setShowUpstream(true); + }} + /> + ) : null} + {downstream.length ? ( + { + setShowDownstream(true); + }} + /> + ) : null} + + } + hoverOpenDelay={100} + hoverCloseDelay={100} + placement="right" + shouldReturnFocusOnClose + > +
+ +
+
+ - + ); }; @@ -344,12 +377,14 @@ const Node = ({ const UpstreamDownstreamDialog = ({ title, assets, + assetGraphData, isOpen, close, selectNode, }: { title: string; - assets: GraphNode[]; + assets: string[]; + assetGraphData: GraphData; isOpen: boolean; close: () => void; selectNode: (e: React.MouseEvent, nodeId: string) => void; @@ -357,19 +392,25 @@ const UpstreamDownstreamDialog = ({ return ( - {assets.map((asset) => ( - { - selectNode(e, asset.id); - close(); - }} - > - - {asset.assetKey.path[asset.assetKey.path.length - 1]} - - - ))} + {assets.map((assetId) => { + const asset = assetGraphData.nodes[assetId]; + if (!asset) { + return null; + } + return ( + { + selectNode(e, asset.id); + close(); + }} + > + + {asset.assetKey.path[asset.assetKey.path.length - 1]} + + + ); + })}
+ { - return Object.entries(assetGraphData.nodes).map(([id, node]) => ({ - value: id, - label: getDisplayName(node), + return allAssetKeys.map((key) => ({ + value: JSON.stringify(key.path), + label: key.path[key.path.length - 1]!, })); - }, [assetGraphData.nodes])} + }, [allAssetKeys])} onSelectValue={selectNode} /> @@ -143,7 +330,10 @@ export const AssetGraphExplorerSidebar = React.memo( {items.map(({index, key, size, start, measureElement}) => { const node = renderedNodes[index]!; - const row = assetGraphData.nodes[node.id]; + const isCodelocationNode = 'locationName' in node; + const isGroupNode = 'groupName' in node; + const row = + !isCodelocationNode && !isGroupNode ? assetGraphData.nodes[node.id] : node; return ( {row ? ( { setSelectedNode(node); setOpenNodes((nodes) => { const openNodes = new Set(nodes); - const isOpen = openNodes.has(node.path); + const isOpen = openNodes.has(nodeId(node)); if (isOpen) { - openNodes.delete(node.path); + openNodes.delete(nodeId(node)); } else { - openNodes.add(node.path); + openNodes.add(nodeId(node)); } return openNodes; }); @@ -179,6 +374,8 @@ export const AssetGraphExplorerSidebar = React.memo( selectNode(e, node.id); setSelectedNode(node); }} + explorerPath={explorerPath} + onChangeExplorerPath={onChangeExplorerPath} /> ) : null} @@ -201,58 +398,103 @@ const Node = ({ isOpen, isSelected, selectThisNode, + explorerPath, + onChangeExplorerPath, + viewType, }: { assetGraphData: GraphData; - node: GraphNode; + node: GraphNode | FolderNodeNonAssetType; level: number; toggleOpen: () => void; selectThisNode: (e: React.MouseEvent | React.KeyboardEvent) => void; selectNode: (e: React.MouseEvent | React.KeyboardEvent, nodeId: string) => void; isOpen: boolean; isSelected: boolean; + explorerPath: ExplorerPath; + onChangeExplorerPath: (path: ExplorerPath, mode: 'replace' | 'push') => void; + viewType: 'tree' | 'folder'; }) => { - const displayName = getDisplayName(node); + const isGroupNode = 'groupName' in node; + const isLocationNode = 'locationName' in node; + const isAssetNode = !isGroupNode && !isLocationNode; - const upstream = Object.keys(assetGraphData.upstream[node.id] ?? {}).filter( - (id) => !!assetGraphData.nodes[id], - ); - const downstream = Object.keys(assetGraphData.downstream[node.id] ?? {}).filter( - (id) => !!assetGraphData.nodes[id], - ); + const displayName = React.useMemo(() => { + if (isAssetNode) { + return getDisplayName(node); + } else if (isGroupNode) { + return node.groupName; + } else { + return node.locationName; + } + }, [isAssetNode, isGroupNode, node]); + + const upstream = Object.keys(assetGraphData.upstream[node.id] ?? {}); + const downstream = Object.keys(assetGraphData.downstream[node.id] ?? {}); const elementRef = React.useRef(null); - const [showDownstream, setShowDownstream] = React.useState(false); - const [showUpstream, setShowUpstream] = React.useState(false); + const [showDownstreamDialog, setShowDownstreamDialog] = React.useState(false); + const [showUpstreamDialog, setShowUpstreamDialog] = React.useState(false); + + function showDownstreamGraph() { + const path = JSON.parse(node.id); + const newQuery = `\"${path[path.length - 1]}\"*`; + const nextOpsQuery = explorerPath.opsQuery.includes(newQuery) + ? explorerPath.opsQuery + : `${explorerPath.opsQuery} ${newQuery}`; + onChangeExplorerPath( + { + ...explorerPath, + opsQuery: nextOpsQuery, + }, + 'push', + ); + } + + function showUpstreamGraph() { + const path = JSON.parse(node.id); + const newQuery = `*\"${path[path.length - 1]}\"`; + const nextOpsQuery = explorerPath.opsQuery.includes(newQuery) + ? explorerPath.opsQuery + : `${explorerPath.opsQuery} ${newQuery}`; + onChangeExplorerPath( + { + ...explorerPath, + opsQuery: nextOpsQuery, + }, + 'push', + ); + } + + const {onClick, loading, launchpadElement} = useMaterializationAction(); return ( <> + {launchpadElement} { - setShowDownstream(false); + setShowDownstreamDialog(false); + }} + selectNode={(e, id) => { + selectNode(e, id); }} - selectNode={selectNode} /> { - setShowUpstream(false); + setShowUpstreamDialog(false); }} selectNode={selectNode} /> - - {downstream.length ? ( + + {!isAssetNode || + (viewType === 'tree' && downstream.filter((id) => assetGraphData.nodes[id]).length) ? (
{ e.stopPropagation(); @@ -271,49 +513,104 @@ const Node = ({ direction: 'row', alignItems: 'center', justifyContent: 'space-between', - gap: 12, + gap: 6, grow: 1, shrink: 1, }} - padding={{horizontal: 12, vertical: 8}} + padding={{horizontal: 8, vertical: 5 as any}} style={{ width: '100%', borderRadius: '8px', ...(isSelected ? {background: Colors.LightPurple} : {}), }} > - - {upstream.length || downstream.length ? ( - - {upstream.length ? ( - { - setShowUpstream(true); - }} - /> - ) : null} - {downstream.length ? ( +
+ {isGroupNode ? : null} + {isLocationNode ? : null} + +
+ {isAssetNode ? ( +
{ + // stop propagation outside of the popover to prevent parent onClick from being selected + e.stopPropagation(); + }} + > + { - setShowDownstream(true); + icon="materialization" + text={ + + Materialize + {loading ? : null} + + } + onClick={async (e) => { + await showSharedToaster({ + intent: 'primary', + message: 'Initiating materialization', + icon: 'materialization', + }); + onClick([node.assetKey], e, false); }} /> - ) : null} - - } - hoverOpenDelay={100} - hoverCloseDelay={100} - placement="right" - shouldReturnFocusOnClose - > - - - - + {upstream.length || downstream.length ? : null} + {upstream.length ? ( + { + // TODO: Hook up selecting the nodes + showUpstreamGraph(); + }} + /> + ) : null} + {downstream.length ? ( + { + // TODO: Hook up selecting the nodes + showDownstreamGraph(); + }} + /> + ) : null} + {upstream.length || downstream.length ? : null} + {upstream.length ? ( + + ) : null} + {downstream.length ? ( + + ) : null} + + } + hoverOpenDelay={100} + hoverCloseDelay={100} + placement="right" + shouldReturnFocusOnClose + > + + + + +
) : null} @@ -326,14 +623,12 @@ const Node = ({ const UpstreamDownstreamDialog = ({ title, assets, - assetGraphData, isOpen, close, selectNode, }: { title: string; assets: string[]; - assetGraphData: GraphData; isOpen: boolean; close: () => void; selectNode: (e: React.MouseEvent | React.KeyboardEvent, nodeId: string) => void; @@ -343,16 +638,14 @@ const UpstreamDownstreamDialog = ({ {assets.map((assetId) => { - const asset = assetGraphData.nodes[assetId]; - if (!asset) { - return null; - } + const path = JSON.parse(assetId); return ( { - selectNode(e, asset.id); + selectNode(e, assetId); close(); }} /> @@ -374,7 +667,11 @@ const BoxWrapper = ({level, children}: {level: number; children: React.ReactNode let sofar = children; for (let i = 0; i < level; i++) { sofar = ( - + {sofar} ); @@ -471,3 +768,17 @@ const GrayOnHoverBox = styled(Box)` visibility: hidden; } `; + +const ButtonGroupWrapper = styled.div` + > * { + display: grid; + grid-template-columns: 1fr 1fr; + > * { + place-content: center; + } + } +`; + +function nodeId(node: {path: string; id: string} | {id: string}) { + return 'path' in node ? node.path : node.id; +} diff --git a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/Utils.tsx b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/Utils.tsx index 8412b2b3bd8dc..4574b7705fbde 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/Utils.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/Utils.tsx @@ -261,3 +261,21 @@ export const itemWithAssetKey = (key: {path: string[]}) => { const token = tokenForAssetKey(key); return (asset: {assetKey: {path: string[]}}) => tokenForAssetKey(asset.assetKey) === token; }; + +export function walkTreeUpwards( + nodeId: string, + graphData: GraphData, + callback: (nodeId: string) => void, +) { + // TODO + console.log({nodeId, graphData, callback}); +} + +export function walkTreeDownwards( + nodeId: string, + graphData: GraphData, + callback: (nodeId: string) => void, +) { + // TODO + console.log({nodeId, graphData, callback}); +} diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/AssetNodeLineageGraph.tsx b/js_modules/dagster-ui/packages/ui-core/src/assets/AssetNodeLineageGraph.tsx index d729dd4656a50..3eab93656a98a 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/assets/AssetNodeLineageGraph.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/assets/AssetNodeLineageGraph.tsx @@ -9,7 +9,7 @@ import {AssetGroupNode} from '../asset-graph/AssetGroupNode'; import {AssetNodeMinimal, AssetNode} from '../asset-graph/AssetNode'; import {AssetNodeLink} from '../asset-graph/ForeignNode'; import {GraphData, LiveData, toGraphId} from '../asset-graph/Utils'; -import {SVGViewport} from '../graph/SVGViewport'; +import {DEFAULT_MAX_ZOOM, SVGViewport} from '../graph/SVGViewport'; import {useAssetLayout} from '../graph/asyncGraphLayout'; import {AssetKeyInput} from '../graphql/types'; import {getJSONForKey} from '../hooks/useStateWithStorage'; @@ -64,8 +64,8 @@ export const AssetNodeLineageGraph: React.FC<{ viewportEl.current?.autocenter(true); e.stopPropagation(); }} - maxZoom={1.2} - maxAutocenterZoom={1.2} + maxZoom={DEFAULT_MAX_ZOOM} + maxAutocenterZoom={DEFAULT_MAX_ZOOM} > {({scale}) => ( diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/AssetsGroupsGlobalGraphRoot.tsx b/js_modules/dagster-ui/packages/ui-core/src/assets/AssetsGroupsGlobalGraphRoot.tsx index 0cda0623eb016..1f348adccfc18 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/assets/AssetsGroupsGlobalGraphRoot.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/assets/AssetsGroupsGlobalGraphRoot.tsx @@ -4,17 +4,17 @@ import * as React from 'react'; import {useHistory, useParams} from 'react-router-dom'; import {AssetGraphExplorer} from '../asset-graph/AssetGraphExplorer'; +import {AssetGraphExplorerFilters} from '../asset-graph/AssetGraphExplorerFilters'; import {AssetGraphFetchScope} from '../asset-graph/useAssetGraphData'; import {AssetLocation} from '../asset-graph/useFindAssetLocation'; import {AssetGroupSelector} from '../graphql/types'; import {useDocumentTitle} from '../hooks/useDocumentTitle'; import {useQueryPersistedState} from '../hooks/useQueryPersistedState'; -import {RepoFilterButton} from '../instance/RepoFilterButton'; import {ExplorerPath} from '../pipelines/PipelinePathUtils'; import {ReloadAllButton} from '../workspace/ReloadAllButton'; import {WorkspaceContext} from '../workspace/WorkspaceContext'; -import {AssetGroupSuggest, buildAssetGroupSelector} from './AssetGroupSuggest'; +import {buildAssetGroupSelector} from './AssetGroupSuggest'; import {assetDetailsPathForKey} from './assetDetailsPathForKey'; import { globalAssetGraphPathFromString, @@ -27,7 +27,7 @@ interface AssetGroupRootParams { export const AssetsGroupsGlobalGraphRoot: React.FC = () => { const {0: path} = useParams(); - const {allRepos, visibleRepos} = React.useContext(WorkspaceContext); + const {visibleRepos} = React.useContext(WorkspaceContext); const history = useHistory(); const [filters, setFilters] = useQueryPersistedState<{groups: AssetGroupSelector[]}>({ @@ -103,14 +103,14 @@ export const AssetsGroupsGlobalGraphRoot: React.FC = () => { - {allRepos.length > 1 && } - setFilters({...filters, groups})} - /> - + filters.groups || [], [filters.groups])} + setGroupFilters={React.useCallback((groups) => setFilters({...filters, groups}), [ + filters, + setFilters, + ])} + /> } options={{preferAssetRendering: true, explodeComposites: true}} explorerPath={globalAssetGraphPathFromString(path)} diff --git a/js_modules/dagster-ui/packages/ui-core/src/graph/OpGraph.tsx b/js_modules/dagster-ui/packages/ui-core/src/graph/OpGraph.tsx index 72fcfb6d581f7..ebca923fcd8b8 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/graph/OpGraph.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/graph/OpGraph.tsx @@ -8,7 +8,7 @@ import {OpNameOrPath} from '../ops/OpNameOrPath'; import {OpEdges} from './OpEdges'; import {OpNode, OP_NODE_DEFINITION_FRAGMENT, OP_NODE_INVOCATION_FRAGMENT} from './OpNode'; import {ParentOpNode, SVGLabeledParentRect} from './ParentOpNode'; -import {DETAIL_ZOOM, SVGViewport, SVGViewportInteractor} from './SVGViewport'; +import {DEFAULT_MAX_ZOOM, DETAIL_ZOOM, SVGViewport, SVGViewportInteractor} from './SVGViewport'; import {OpGraphLayout} from './asyncGraphLayout'; import { Edge, @@ -199,7 +199,7 @@ export class OpGraph extends React.Component { ` white-space: nowrap; `; -const LabelTooltipStyles = JSON.stringify({ +export const LabelTooltipStyles = JSON.stringify({ background: Colors.Gray100, filter: `brightness(97%)`, color: Colors.Gray900, @@ -140,11 +140,18 @@ const TruncatingName = styled.div` export const TruncatedTextWithFullTextOnHover = React.forwardRef( ( - {text, tooltipStyle, ...rest}: {text: string; tooltipStyle?: string}, + { + text, + tooltipStyle, + tooltipText, + ...rest + }: + | {text: string; tooltipStyle?: string; tooltipText?: null} + | {text: React.ReactNode; tooltipStyle?: string; tooltipText: string}, ref: React.ForwardedRef, ) => ( Date: Wed, 20 Sep 2023 12:28:58 -0400 Subject: [PATCH 12/15] min/max zoom --- .../dagster-ui/packages/ui-core/src/graph/SVGViewport.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js_modules/dagster-ui/packages/ui-core/src/graph/SVGViewport.tsx b/js_modules/dagster-ui/packages/ui-core/src/graph/SVGViewport.tsx index 0372883053d06..43dd6bd872647 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/graph/SVGViewport.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/graph/SVGViewport.tsx @@ -438,7 +438,7 @@ export class SVGViewport extends React.Component Date: Wed, 20 Sep 2023 12:31:44 -0400 Subject: [PATCH 13/15] remove select upstream/downstream --- .../asset-graph/AssetGraphExplorerSidebar.tsx | 24 +------------------ 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorerSidebar.tsx b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorerSidebar.tsx index 5f82cb5d0cab1..3beddd93f346c 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorerSidebar.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorerSidebar.tsx @@ -21,7 +21,6 @@ import {useVirtualizer} from '@tanstack/react-virtual'; import React from 'react'; import styled from 'styled-components'; - import {showSharedToaster} from '../app/DomUtils'; import {useMaterializationAction} from '../assets/LaunchAssetExecutionButton'; import {AssetKey} from '../assets/types'; @@ -259,7 +258,7 @@ export const AssetGraphExplorerSidebar = React.memo( assetGraphData, viewType, // eslint-disable-next-line react-hooks/exhaustive-deps - + lastSelectedNode && renderedNodes.findIndex((node) => nodeId(lastSelectedNode) === nodeId(node)), ]); @@ -564,27 +563,6 @@ const Node = ({ }} /> {upstream.length || downstream.length ? : null} - {upstream.length ? ( - { - // TODO: Hook up selecting the nodes - showUpstreamGraph(); - }} - /> - ) : null} - {downstream.length ? ( - { - // TODO: Hook up selecting the nodes - showDownstreamGraph(); - }} - /> - ) : null} - {upstream.length || downstream.length ? : null} {upstream.length ? ( Date: Wed, 20 Sep 2023 12:46:45 -0400 Subject: [PATCH 14/15] learn more link on feature flag --- .../ui-core/src/app/UserSettingsDialog.tsx | 7 ++++--- .../src/app/getVisibleFeatureFlagRows.tsx | 19 ++++++++++++++++++- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/js_modules/dagster-ui/packages/ui-core/src/app/UserSettingsDialog.tsx b/js_modules/dagster-ui/packages/ui-core/src/app/UserSettingsDialog.tsx index e06ff306a0862..77602d8154686 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/app/UserSettingsDialog.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/app/UserSettingsDialog.tsx @@ -21,7 +21,7 @@ import {TimezoneSelect} from './time/TimezoneSelect'; import {automaticLabel} from './time/browserTimezone'; type OnCloseFn = (event: React.SyntheticEvent) => void; -type VisibleFlag = {key: string; flagType: FeatureFlagType}; +type VisibleFlag = {key: string; label?: React.ReactNode; flagType: FeatureFlagType}; interface DialogProps { isOpen: boolean; @@ -44,7 +44,7 @@ export const UserSettingsDialog: React.FC = ({isOpen, onClose, visi interface DialogContentProps { onClose: OnCloseFn; - visibleFlags: {key: string; flagType: FeatureFlagType}[]; + visibleFlags: {key: string; label?: React.ReactNode; flagType: FeatureFlagType}[]; } /** @@ -139,8 +139,9 @@ const UserSettingsDialogContent: React.FC = ({onClose, visib Experimental features ({ + rows={visibleFlags.map(({key, label, flagType}) => ({ key, + label, value: ( [ flagType: FeatureFlag.flagHorizontalDAGs, }, { - key: 'Experimental DAG sidebar', + key: 'New asset lineage sidebar', + label: ( + + New asset lineage sidebar, + + + ), flagType: FeatureFlag.flagDAGSidebar, }, ]; From 224deddacb7c970a001dc8ec1809a57651aaccf0 Mon Sep 17 00:00:00 2001 From: Marco Salazar Date: Wed, 20 Sep 2023 12:48:24 -0400 Subject: [PATCH 15/15] lint --- .../ui-core/src/asset-graph/AssetGraphExplorerSidebar.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorerSidebar.tsx b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorerSidebar.tsx index 3beddd93f346c..52fa3d7b1c7b7 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorerSidebar.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorerSidebar.tsx @@ -258,7 +258,6 @@ export const AssetGraphExplorerSidebar = React.memo( assetGraphData, viewType, // eslint-disable-next-line react-hooks/exhaustive-deps - lastSelectedNode && renderedNodes.findIndex((node) => nodeId(lastSelectedNode) === nodeId(node)), ]);