Skip to content

Commit f755469

Browse files
committed
added a new topology view
Signed-off-by: Lars Heinemann <lhein.smx@gmail.com>
1 parent 1b30aa1 commit f755469

35 files changed

Lines changed: 2706 additions & 43 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,4 @@ storybook-static
148148

149149
.bob/notes
150150
.codex
151+
.claude/

packages/ui/src/layout/Navigation.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export const Navigation: FunctionComponent<INavigationSidebar> = (props) => {
2222
children: [
2323
{ title: 'Design', to: Links.Home },
2424
{ title: 'Source Code', to: Links.SourceCode },
25+
{ title: 'Topology', to: Links.Topology },
2526
],
2627
},
2728
{

packages/ui/src/layout/__snapshots__/Navigation.test.tsx.snap

Lines changed: 118 additions & 43 deletions
Large diffs are not rendered by default.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { Point } from '@patternfly/react-topology';
2+
3+
import { LayoutType } from '../../components/Visualization/Canvas/canvas.models';
4+
import { OrthogonalBendpointsEdge } from './OrthogonalBendpointsEdge';
5+
6+
const node = (pos: { x: number; y: number }, size: { width: number; height: number }) => ({
7+
getPosition: () => pos,
8+
getDimensions: () => size,
9+
});
10+
11+
const wireEdge = (edge: OrthogonalBendpointsEdge, layout: string, source: unknown, target: unknown) => {
12+
jest.spyOn(edge, 'getSource').mockReturnValue(source as never);
13+
jest.spyOn(edge, 'getTarget').mockReturnValue(target as never);
14+
jest.spyOn(edge, 'getGraph').mockReturnValue({ getLayout: () => layout } as never);
15+
};
16+
17+
describe('OrthogonalBendpointsEdge', () => {
18+
let edge: OrthogonalBendpointsEdge;
19+
20+
beforeEach(() => {
21+
edge = new OrthogonalBendpointsEdge();
22+
});
23+
24+
afterEach(() => {
25+
jest.restoreAllMocks();
26+
});
27+
28+
it('returns no bendpoints for a self-loop', () => {
29+
const self = {};
30+
wireEdge(edge, LayoutType.DagreHorizontal, self, self);
31+
expect(edge.getBendpoints()).toEqual([]);
32+
});
33+
34+
it('returns no bendpoints when source or target is missing', () => {
35+
wireEdge(edge, LayoutType.DagreHorizontal, undefined, node({ x: 0, y: 0 }, { width: 10, height: 10 }));
36+
expect(edge.getBendpoints()).toEqual([]);
37+
});
38+
39+
it('produces a horizontal step (same Y as source) and a vertical step at midX in horizontal layout', () => {
40+
const source = node({ x: 0, y: 0 }, { width: 100, height: 50 });
41+
const target = node({ x: 300, y: 200 }, { width: 100, height: 50 });
42+
wireEdge(edge, LayoutType.DagreHorizontal, source, target);
43+
44+
// Centers: source = (50, 25), target = (350, 225). midX = 200.
45+
expect(edge.getBendpoints()).toEqual([new Point(200, 25), new Point(200, 225)]);
46+
});
47+
48+
it('produces a vertical step (same X as source) and a horizontal step at midY in vertical layout', () => {
49+
const source = node({ x: 0, y: 0 }, { width: 100, height: 50 });
50+
const target = node({ x: 300, y: 200 }, { width: 100, height: 50 });
51+
wireEdge(edge, LayoutType.DagreVertical, source, target);
52+
53+
// Centers: source = (50, 25), target = (350, 225). midY = 125.
54+
expect(edge.getBendpoints()).toEqual([new Point(50, 125), new Point(350, 125)]);
55+
});
56+
});
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { BaseEdge, Point } from '@patternfly/react-topology';
2+
3+
import { LayoutType } from '../../components/Visualization/Canvas/canvas.models';
4+
5+
/**
6+
* Edge element that bends twice to produce orthogonal (stepped) lines:
7+
* one segment along the layout's primary axis, a perpendicular step in the
8+
* middle, then another segment along the primary axis to the target.
9+
*/
10+
export class OrthogonalBendpointsEdge extends BaseEdge {
11+
getBendpoints(): Point[] {
12+
const source = this.getSource();
13+
const target = this.getTarget();
14+
if (!source || !target || source === target) {
15+
return [];
16+
}
17+
18+
const sourcePos = source.getPosition();
19+
const sourceSize = source.getDimensions();
20+
const targetPos = target.getPosition();
21+
const targetSize = target.getDimensions();
22+
23+
const startX = sourcePos.x + sourceSize.width / 2;
24+
const startY = sourcePos.y + sourceSize.height / 2;
25+
const endX = targetPos.x + targetSize.width / 2;
26+
const endY = targetPos.y + targetSize.height / 2;
27+
28+
// Edges may briefly be queried before they're attached to the graph. Default to
29+
// horizontal so we always return a sensible bend pair even in that window.
30+
const graph = this.getGraph?.();
31+
const isHorizontal = graph?.getLayout?.() !== LayoutType.DagreVertical;
32+
33+
if (isHorizontal) {
34+
const midX = (startX + endX) / 2;
35+
return [new Point(midX, startY), new Point(midX, endY)];
36+
}
37+
const midY = (startY + endY) / 2;
38+
return [new Point(startX, midY), new Point(endX, midY)];
39+
}
40+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
.topology-page {
2+
width: 100%;
3+
height: 100%;
4+
display: flex;
5+
flex: 1 1 auto;
6+
flex-direction: column;
7+
min-height: 0;
8+
9+
.pf-topology-container {
10+
flex: 1 1 auto;
11+
}
12+
13+
// Stepped edge paths enclose an area, which the browser would fill in black
14+
// without an explicit fill: none.
15+
.custom-edge {
16+
&__background,
17+
&__body {
18+
fill: none;
19+
}
20+
21+
&__body {
22+
stroke-width: 2;
23+
}
24+
25+
&__connector {
26+
stroke-width: 2;
27+
}
28+
}
29+
30+
// Let the node labels blend with the canvas background instead of using the
31+
// default solid background coming from CustomNode.scss.
32+
.custom-node {
33+
&__label {
34+
&__text {
35+
background-color: transparent;
36+
}
37+
}
38+
}
39+
}
40+
41+
.topology-collapsed-route {
42+
cursor: pointer;
43+
44+
&:hover {
45+
filter: brightness(1.05);
46+
}
47+
}
48+
49+
.topology-external-endpoint,
50+
.topology-dynamic-endpoint {
51+
// External and dynamic endpoints are read-only — make the visual cue obvious
52+
// by using a dashed border on the container.
53+
.custom-node {
54+
&__container {
55+
&__image {
56+
border-style: dashed;
57+
}
58+
}
59+
}
60+
61+
&__badge {
62+
right: -12px;
63+
}
64+
}

0 commit comments

Comments
 (0)