visualizer: split inputs to separate nodes in tree

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2023-11-04 21:49:16 +01:00
parent 4b3d0ec9e7
commit 5ad1a062e3
@@ -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<SVGTextElement | null>(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 (
<g>
<rect
className={classes.node}
width={paddedWidth}
height={paddedHeight}
rx={10}
/>
<text
ref={idRef}
className={classes.text}
y={paddedHeight / 2}
x={paddedWidth / 2}
textAnchor="middle"
alignmentBaseline="middle"
>
{node.type === 'node' ? node.id : node.name}
</text>
</g>
);
}
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}
/>
</Box>
);