From b84c66abfd4cc0b10fa1cde96349f5edc9862e7f Mon Sep 17 00:00:00 2001 From: Alan Buscaglia Date: Wed, 6 May 2026 08:17:32 +0200 Subject: [PATCH] fix(ui): use horizontal attack path graph layout --- .../graph/nodes/finding-node.test.tsx | 8 +++++--- .../_components/graph/nodes/finding-node.tsx | 13 ++++++------- .../_components/graph/nodes/hidden-handles.tsx | 4 ++-- .../graph/nodes/resource-node.test.tsx | 8 +++++--- .../_components/graph/nodes/resource-node.tsx | 13 ++++++------- .../query-builder/_lib/layout.test.ts | 17 ++++++++++++----- .../(workflow)/query-builder/_lib/layout.ts | 6 +++--- 7 files changed, 39 insertions(+), 30 deletions(-) diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/nodes/finding-node.test.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/nodes/finding-node.test.tsx index d3b8f2c2c2..74a1de7e5d 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/nodes/finding-node.test.tsx +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/nodes/finding-node.test.tsx @@ -32,7 +32,7 @@ const buildNodeProps = (graphNode: GraphNode): NodeProps => }) as unknown as NodeProps; describe("FindingNode", () => { - it("positions graph handles for vertical top-to-bottom edges", () => { + it("positions graph handles for horizontal left-to-right edges", () => { // Given const props = buildNodeProps( buildFindingNode("critical", "Root key exposed"), @@ -44,8 +44,10 @@ describe("FindingNode", () => { // Then expect(hiddenHandlesMock).toHaveBeenCalledWith( expect.objectContaining({ - sourcePosition: Position.Bottom, - targetPosition: Position.Top, + sourcePosition: Position.Right, + sourceStyle: { left: 97, top: 26 }, + targetPosition: Position.Left, + targetStyle: { left: 53, top: 26 }, }), undefined, ); diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/nodes/finding-node.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/nodes/finding-node.tsx index 9bd73e2407..986617def7 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/nodes/finding-node.tsx +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/nodes/finding-node.tsx @@ -21,8 +21,8 @@ const BADGE_SIZE = 44; const BADGE_RADIUS = BADGE_SIZE / 2; const BADGE_CENTER_X = NODE_WIDTH / 2; const BADGE_CENTER_Y = 26; -const BADGE_BOTTOM_Y = BADGE_CENTER_Y + BADGE_RADIUS; -const BADGE_TOP_Y = BADGE_CENTER_Y - BADGE_RADIUS; +const BADGE_LEFT_X = BADGE_CENTER_X - BADGE_RADIUS; +const BADGE_RIGHT_X = BADGE_CENTER_X + BADGE_RADIUS; const ICON_SIZE = 28; const ICON_X = BADGE_CENTER_X - ICON_SIZE / 2; const ICON_Y = BADGE_CENTER_Y - ICON_SIZE / 2; @@ -76,11 +76,10 @@ export const FindingNode = ({ data, selected }: NodeProps) => { return ( <> ( <> diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/nodes/resource-node.test.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/nodes/resource-node.test.tsx index f2c524933e..937e6cb2ac 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/nodes/resource-node.test.tsx +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/nodes/resource-node.test.tsx @@ -32,7 +32,7 @@ const buildNodeProps = (graphNode: GraphNode): NodeProps => }) as unknown as NodeProps; describe("ResourceNode", () => { - it("positions graph handles for vertical top-to-bottom edges", () => { + it("positions graph handles for horizontal left-to-right edges", () => { // Given const props = buildNodeProps(buildGraphNode("S3Bucket", "logs")); @@ -42,8 +42,10 @@ describe("ResourceNode", () => { // Then expect(hiddenHandlesMock).toHaveBeenCalledWith( expect.objectContaining({ - sourcePosition: Position.Bottom, - targetPosition: Position.Top, + sourcePosition: Position.Right, + sourceStyle: { left: 90, top: 26 }, + targetPosition: Position.Left, + targetStyle: { left: 46, top: 26 }, }), undefined, ); diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/nodes/resource-node.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/nodes/resource-node.tsx index b8f77292ea..324d3aa4b9 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/nodes/resource-node.tsx +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/nodes/resource-node.tsx @@ -22,8 +22,8 @@ const BADGE_SIZE = 44; const BADGE_RADIUS = BADGE_SIZE / 2; const BADGE_CENTER_X = NODE_WIDTH / 2; const BADGE_CENTER_Y = 26; -const BADGE_BOTTOM_Y = BADGE_CENTER_Y + BADGE_RADIUS; -const BADGE_TOP_Y = BADGE_CENTER_Y - BADGE_RADIUS; +const BADGE_LEFT_X = BADGE_CENTER_X - BADGE_RADIUS; +const BADGE_RIGHT_X = BADGE_CENTER_X + BADGE_RADIUS; const ICON_SIZE = 28; const ICON_X = BADGE_CENTER_X - ICON_SIZE / 2; const ICON_Y = BADGE_CENTER_Y - ICON_SIZE / 2; @@ -63,11 +63,10 @@ export const ResourceNode = ({ data, selected }: NodeProps) => { return ( <> {glowRadius > 0 && ( diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/layout.test.ts b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/layout.test.ts index 1231430fce..ee60e28103 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/layout.test.ts +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/layout.test.ts @@ -72,7 +72,7 @@ describe("layoutWithDagre", () => { expect(a).toEqual(b); }); - it("spreads sibling nodes horizontally to use wide graph space", () => { + it("places connected children to the right and stacks siblings within the horizontal rank", () => { const rootNode: GraphNode = { id: "root", labels: ["AWSAccount"], @@ -106,6 +106,9 @@ describe("layoutWithDagre", () => { })), ); + const rootPosition = rfNodes.find( + (candidate) => candidate.id === "root", + )?.position; const siblingPositions = siblingNodes.map((node) => { const rfNode = rfNodes.find((candidate) => candidate.id === node.id); @@ -121,10 +124,14 @@ describe("layoutWithDagre", () => { Math.max(...siblingPositions.map((position) => position.y)) - Math.min(...siblingPositions.map((position) => position.y)); - expect(xSpread).toBeGreaterThan(ySpread); + expect(rootPosition).toBeDefined(); + siblingPositions.forEach((position) => { + expect(position.x).toBeGreaterThan(rootPosition?.x ?? 0); + }); + expect(ySpread).toBeGreaterThan(xSpread); }); - it("connects edges through top and bottom node sides for vertical layout", () => { + it("connects edges through right and left node sides for horizontal layout", () => { const { rfNodes } = layoutWithDagre( [findingNode, resourceNode], [ @@ -138,8 +145,8 @@ describe("layoutWithDagre", () => { ); rfNodes.forEach((node) => { - expect(node.sourcePosition).toBe(Position.Bottom); - expect(node.targetPosition).toBe(Position.Top); + expect(node.sourcePosition).toBe(Position.Right); + expect(node.targetPosition).toBe(Position.Left); }); }); diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/layout.ts b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/layout.ts index ef86ee8b1d..3e43c1857d 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/layout.ts +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/layout.ts @@ -65,7 +65,7 @@ export const layoutWithDagre = ( ): { rfNodes: Node[]; rfEdges: Edge[] } => { const g = new Graph(); g.setGraph({ - rankdir: "TB", + rankdir: "LR", nodesep: 80, ranksep: 150, marginx: 50, @@ -112,8 +112,8 @@ export const layoutWithDagre = ( x: dagreNode.x - width / 2, y: dagreNode.y - height / 2, }, - sourcePosition: Position.Bottom, - targetPosition: Position.Top, + sourcePosition: Position.Right, + targetPosition: Position.Left, data: { graphNode: node }, width, height,