diff --git a/plugins/visualizer/src/components/AppVisualizerPage/TreeVisualizer.tsx b/plugins/visualizer/src/components/AppVisualizerPage/TreeVisualizer.tsx index 1260da3c32..d405e1e164 100644 --- a/plugins/visualizer/src/components/AppVisualizerPage/TreeVisualizer.tsx +++ b/plugins/visualizer/src/components/AppVisualizerPage/TreeVisualizer.tsx @@ -18,26 +18,117 @@ import { DependencyGraph, DependencyGraphTypes, } from '@backstage/core-components'; -import { AppTree } from '@backstage/frontend-plugin-api'; +import { AppNode, AppTree } from '@backstage/frontend-plugin-api'; import Box from '@material-ui/core/Box'; -import { useMemo } from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import { useLayoutEffect, useMemo, useRef, useState } from 'react'; -function resolveGraphData(tree: AppTree) { +type NodeType = + | ({ type: 'node'; id: string } & AppNode) + | { type: 'input'; id: string; name: string }; + +function inputId({ node, input }: { node: AppNode; input: string }) { + return `${node.spec.id}$$${input}`; +} + +function resolveGraphData(tree: AppTree): { + nodes: NodeType[]; + edges: { from: string; to: string }[]; +} { const nodes = [...tree.nodes.values()] - .filter(n => n.instance) - .map(n => ({ ...n, id: n.spec.id })); + .filter(node => node.instance) + .map(node => ({ ...node, id: node.spec.id, type: 'node' as const })); + return { - nodes, - edges: nodes - .filter(n => n.edges.attachedTo) - .map(n => ({ - from: n.spec.id, - to: n.edges.attachedTo!.node.spec.id, - label: n.edges.attachedTo!.input, - })), + nodes: [ + ...nodes, + ...nodes.flatMap(node => + [...node.edges.attachments.keys()].map(input => ({ + id: inputId({ node, input }), + type: 'input' as const, + name: input, + })), + ), + ], + edges: [ + ...nodes + .filter(node => node.edges.attachedTo) + .map(node => ({ + from: inputId(node.edges.attachedTo!), + to: node.spec.id, + })), + ...nodes.flatMap(node => + [...node.edges.attachments.keys()].map(input => ({ + from: node.spec.id, + to: inputId({ node, input }), + })), + ), + ], }; } +const useStyles = makeStyles(theme => ({ + node: { + fill: (node: NodeType) => + node.type === 'node' + ? theme.palette.primary.light + : theme.palette.grey[500], + stroke: theme.palette.primary.light, + }, + text: { + fill: theme.palette.primary.contrastText, + }, +})); + +/** @public */ +export function Node(props: { node: NodeType }) { + const { node } = props; + const classes = useStyles(node); + const [width, setWidth] = useState(0); + const [height, setHeight] = useState(0); + const idRef = useRef(null); + + useLayoutEffect(() => { + // set the width to the length of the ID + if (idRef.current) { + let { height: renderedHeight, width: renderedWidth } = + idRef.current.getBBox(); + renderedHeight = Math.round(renderedHeight); + renderedWidth = Math.round(renderedWidth); + + if (renderedHeight !== height || renderedWidth !== width) { + setWidth(renderedWidth); + setHeight(renderedHeight); + } + } + }, [width, height]); + + const padding = 10; + const paddedWidth = width + padding * 2; + const paddedHeight = height + padding * 2; + + return ( + + + + {node.type === 'node' ? node.id : node.name} + + + ); +} + export function TreeVisualizer({ tree }: { tree: AppTree }) { const graphData = useMemo(() => resolveGraphData(tree), [tree]); @@ -50,7 +141,8 @@ export function TreeVisualizer({ tree }: { tree: AppTree }) { nodeMargin={10} edgeMargin={30} rankMargin={100} - direction={DependencyGraphTypes.Direction.BOTTOM_TOP} + renderNode={Node} + direction={DependencyGraphTypes.Direction.TOP_BOTTOM} /> );