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]} + + + ); + })}