diff --git a/js_modules/dagster-ui/packages/ui-components/src/components/MiddleTruncate.tsx b/js_modules/dagster-ui/packages/ui-components/src/components/MiddleTruncate.tsx index f15c801eb79dd..9f0123b90c1fd 100644 --- a/js_modules/dagster-ui/packages/ui-components/src/components/MiddleTruncate.tsx +++ b/js_modules/dagster-ui/packages/ui-components/src/components/MiddleTruncate.tsx @@ -68,6 +68,8 @@ const MeasureWidth = styled.div` height: 0; overflow: hidden; white-space: nowrap; + user-select: none; + visibility: hidden; `; const Container = styled.div` diff --git a/js_modules/dagster-ui/packages/ui-components/src/components/TextInput.tsx b/js_modules/dagster-ui/packages/ui-components/src/components/TextInput.tsx index c50ce17e8a121..7b8e09869668a 100644 --- a/js_modules/dagster-ui/packages/ui-components/src/components/TextInput.tsx +++ b/js_modules/dagster-ui/packages/ui-components/src/components/TextInput.tsx @@ -56,7 +56,7 @@ export const TextInputContainerStyles = css` position: relative; `; -export const TextInputContainer = styled.div<{$disabled: boolean}>` +export const TextInputContainer = styled.div<{$disabled?: boolean}>` ${TextInputContainerStyles} > ${IconWrapper}:first-child { 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 3873d92748fc5..f9ecca719f592 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 @@ -220,13 +220,13 @@ const AssetGraphExplorerWithData: React.FC = ({ ); }, [ + assetGraphData, explorerPath, + findAssetLocation, + layout?.nodes, onChangeExplorerPath, onNavigateToSourceAssetNode, - findAssetLocation, selectedGraphNodes, - assetGraphData, - layout, ], ); @@ -266,18 +266,22 @@ const AssetGraphExplorerWithData: React.FC = ({ selectNodeById(e, nextId); }; - const selectNodeById = (e: React.MouseEvent | React.KeyboardEvent, nodeId?: string) => { - if (!nodeId) { - return; - } - const node = assetGraphData.nodes[nodeId]; - if (node) { - onSelectNode(e, node.assetKey, node); - if (layout && viewportEl.current) { - viewportEl.current.zoomToSVGBox(layout.nodes[nodeId]!.bounds, true); + const selectNodeById = React.useCallback( + (e: React.MouseEvent | React.KeyboardEvent, nodeId?: string) => { + if (!nodeId) { + return; } - } - }; + const node = assetGraphData.nodes[nodeId]; + if (node) { + onSelectNode(e, node.assetKey, node); + if (layout && viewportEl.current) { + viewportEl.current.zoomToSVGBox(layout.nodes[nodeId]!.bounds, true); + } + } + }, + [assetGraphData.nodes, layout, onSelectNode], + ); + React.useEffect(() => { if (layout && lastSelectedNode) { selectNodeById({stopPropagation: () => {}} as any, lastSelectedNode.id); @@ -296,7 +300,12 @@ const AssetGraphExplorerWithData: React.FC = ({ { + selectNodeById(e, nodeId); + }, + [selectNodeById], + )} /> } second={ 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 3b3b15f25c7e8..98d925b71dffe 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 @@ -9,30 +9,20 @@ import { Menu, MenuItem, Popover, - useViewport, - UnstyledButton, TextInput, + MiddleTruncate, + useViewport, } 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'; -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 = React.memo( ({ assetGraphData, @@ -41,95 +31,92 @@ export const AssetGraphExplorerSidebar = React.memo( }: { assetGraphData: GraphData; lastSelectedNode: GraphNode; - selectNode: (e: React.MouseEvent, nodeId: string) => void; + selectNode: (e: React.MouseEvent | React.KeyboardEvent, 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]); + const [openNodes, setOpenNodes] = React.useState>(new Set()); + const [selectedNode, setSelectedNode] = React.useState(null); - const [openNodes, setOpenNodes] = React.useState>(() => { - const set = new Set(); - if (Object.keys(rootGroupsWithRootNodes).length === 1) { - set.add(Object.keys(rootGroupsWithRootNodes)[0]!); - } - return set; - }); + const rootNodes = React.useMemo( + () => + Object.keys(assetGraphData.nodes) + .filter((id) => !assetGraphData.upstream[id]) + .sort((a, b) => + COLLATOR.compare( + getDisplayName(assetGraphData.nodes[a]!), + getDisplayName(assetGraphData.nodes[b]!), + ), + ), + [assetGraphData], + ); const renderedNodes = React.useMemo(() => { - const queue = Object.entries(rootGroupsWithRootNodes).map(([_, {id}]) => ({level: 1, id})); + const queue = rootNodes.map((id) => ({level: 1, id, path: id})); - const renderedNodes: {level: number; id: string}[] = []; + const renderedNodes: {level: number; id: string; path: 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] || {}); - } + if (openNodes.has(node.path)) { + const downstream = Object.keys(assetGraphData.downstream[node.id] || {}); if (downstream) { - queue.unshift(...downstream.map((id) => ({level: node.level + 1, id}))); + queue.unshift( + ...downstream + .filter((id) => assetGraphData.nodes[id]) + .map((id) => ({level: node.level + 1, id, path: `${node.path}:${id}`})), + ); } } } - 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]); + return renderedNodes; + }, [assetGraphData.downstream, assetGraphData.nodes, openNodes, rootNodes]); const containerRef = React.useRef(null); - const {containerProps, viewport} = useViewport(); const rowVirtualizer = useVirtualizer({ count: renderedNodes.length, getScrollElement: () => containerRef.current, - estimateSize: () => 34, - - // TODO: Figure out why virtualizer isn't filling up all of the space automatically... - overscan: viewport.height / 34, + estimateSize: () => 38, + overscan: 10, }); const totalHeight = rowVirtualizer.getTotalSize(); const items = rowVirtualizer.getVirtualItems(); + React.useLayoutEffect(() => { + if (lastSelectedNode && lastSelectedNode.id !== selectedNode?.id) { + setOpenNodes((prevOpenNodes) => { + let path = lastSelectedNode.id; + let currentId = lastSelectedNode.id; + let next: string | undefined; + while ((next = Object.keys(assetGraphData.upstream[currentId] ?? {})[0])) { + path = `${next}:${path}`; + currentId = next; + } + + const nextOpenNodes = new Set(prevOpenNodes); + + const nodesInPath = path.split(':'); + let currentPath = nodesInPath[0]!; + + nextOpenNodes.add(currentPath); + for (let i = 1; i < nodesInPath.length; i++) { + currentPath = `${currentPath}:${nodesInPath[i]}`; + nextOpenNodes.add(currentPath); + } + setSelectedNode({id: lastSelectedNode.id, path: currentPath}); + return nextOpenNodes; + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [lastSelectedNode]); + const indexOfLastSelectedNode = React.useMemo( () => - lastSelectedNode ? renderedNodes.findIndex((node) => node.id === lastSelectedNode.id) : -1, - [renderedNodes, lastSelectedNode], + selectedNode?.path + ? renderedNodes.findIndex((node) => node.path === selectedNode?.path) + : -1, + // eslint-disable-next-line react-hooks/exhaustive-deps + [renderedNodes, selectedNode?.path], ); React.useLayoutEffect(() => { @@ -138,54 +125,25 @@ export const AssetGraphExplorerSidebar = React.memo( } }, [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]]); - return ( - +
- - - - - - - - + { + return Object.entries(assetGraphData.nodes).map(([id, node]) => ({ + value: id, + label: getDisplayName(node), + })); + }, [assetGraphData.nodes])} + onSelectValue={selectNode} + /> - - { - 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); + const row = assetGraphData.nodes[node.id]; return ( { + setSelectedNode(node); setOpenNodes((nodes) => { const openNodes = new Set(nodes); - const isOpen = openNodes.has(node.id); + const isOpen = openNodes.has(node.path); if (isOpen) { - openNodes.delete(node.id); + openNodes.delete(node.path); } else { - openNodes.add(node.id); + openNodes.add(node.path); } return openNodes; }); }} - selectNode={selectNode} - measureElement={measureElement} + selectNode={(e, id) => { + selectNode(e, id); + }} + selectThisNode={(e) => { + selectNode(e, node.id); + setSelectedNode(node); + }} /> ) : null} @@ -222,8 +186,8 @@ export const AssetGraphExplorerSidebar = React.memo( })} - - +
+
); }, ); @@ -235,142 +199,127 @@ const Node = ({ toggleOpen, selectNode, isOpen, - measureElement, isSelected, + selectThisNode, }: { assetGraphData: GraphData; - node: GraphNode | GroupNode; + node: GraphNode; level: number; toggleOpen: () => void; - selectNode: (e: React.MouseEvent, nodeId: string) => void; + selectThisNode: (e: React.MouseEvent | React.KeyboardEvent) => void; + selectNode: (e: React.MouseEvent | React.KeyboardEvent, nodeId: string) => void; isOpen: boolean; - measureElement: (el: HTMLDivElement) => void; isSelected: boolean; }) => { - const isGroupNode = 'groupName' in node; + const displayName = getDisplayName(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] ?? {}).filter( - (id) => !!assetGraphData.nodes[id], - ); - const downstream = isGroupNode - ? node.nodes - : Object.keys(assetGraphData.downstream[node.id] ?? {}).filter( - (id) => !!assetGraphData.nodes[id], - ); + 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 elementRef = React.useRef(null); - React.useLayoutEffect(() => { - setTimeout(() => { - if (elementRef.current) { - measureElement(elementRef.current); - } - }, 100); - }, [measureElement, isOpen]); const [showDownstream, setShowDownstream] = React.useState(false); const [showUpstream, setShowUpstream] = React.useState(false); return ( - selectNode(e, node.id)} - style={{ - ...(downstream && !isGroupNode ? {cursor: 'pointer'} : {}), - }} - > - - - {downstream.length ? ( -
{ - e.stopPropagation(); - toggleOpen(); - }} - style={{cursor: 'pointer'}} - > - -
- ) : null} + <> + { + setShowDownstream(false); + }} + selectNode={selectNode} + /> + { + setShowUpstream(false); + }} + selectNode={selectNode} + /> + + -
- {withMiddleTruncation(displayName, {maxLength: 30})} -
- { - setShowDownstream(false); + {downstream.length ? ( +
{ + e.stopPropagation(); + toggleOpen(); + }} + style={{cursor: 'pointer'}} + > + +
+ ) : null} + - { - setShowUpstream(false); + padding={{horizontal: 12, vertical: 8}} + style={{ + width: '100%', + borderRadius: '8px', + ...(isSelected ? {background: Colors.LightPurple} : {}), }} - selectNode={selectNode} - /> - - {upstream.length ? ( - { - setShowUpstream(true); - }} - /> - ) : null} - {downstream.length ? ( - { - setShowDownstream(true); - }} - /> - ) : null} - - } - hoverOpenDelay={100} - hoverCloseDelay={100} - placement="right" - shouldReturnFocusOnClose > -
- -
-
+ + {upstream.length || downstream.length ? ( + + {upstream.length ? ( + { + setShowUpstream(true); + }} + /> + ) : null} + {downstream.length ? ( + { + setShowDownstream(true); + }} + /> + ) : null} + + } + hoverOpenDelay={100} + hoverCloseDelay={100} + placement="right" + shouldReturnFocusOnClose + > + + + + + ) : null} +
-
-
-
+ +
+ ); }; @@ -387,30 +336,29 @@ const UpstreamDownstreamDialog = ({ assetGraphData: GraphData; isOpen: boolean; close: () => void; - selectNode: (e: React.MouseEvent, nodeId: string) => void; + selectNode: (e: React.MouseEvent | React.KeyboardEvent, nodeId: string) => void; }) => { return ( - {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]} - - - ); - })} + + {assets.map((assetId) => { + const asset = assetGraphData.nodes[assetId]; + if (!asset) { + return null; + } + return ( + { + selectNode(e, asset.id); + close(); + }} + /> + ); + })} +