Merge pull request 'fix(dashboard): restore graph visual preferences (#247)' (#250) from fix/dashboard-graph-visual-247 into main

This commit is contained in:
2026-05-14 03:43:32 +00:00
4 changed files with 38 additions and 27 deletions
@@ -7,25 +7,23 @@ const FEEDBACK_OFFSET_X = 100;
const FEEDBACK_RADIUS = 16;
/**
* Build an SVG path for a feedback (back) edge that routes to the right of the nodes.
* The path goes: source right → arc → vertical up → arc → target right
* Build an SVG path for a feedback (back) edge that routes to the given side of the nodes.
* The path goes: source → arc → vertical up → arc → target
*/
function feedbackPath(sourceX: number, sourceY: number, targetX: number, targetY: number): string {
const rightX = Math.max(sourceX, targetX) + FEEDBACK_OFFSET_X;
function feedbackPath(sourceX: number, sourceY: number, targetX: number, targetY: number, side: "right" | "left"): string {
const d = side === "right" ? 1 : -1;
const offsetX =
side === "right"
? Math.max(sourceX, targetX) + FEEDBACK_OFFSET_X
: Math.min(sourceX, targetX) - FEEDBACK_OFFSET_X;
const r = FEEDBACK_RADIUS;
// Start from source right side, go right, then up, then left to target right side
const segments = [
`M ${sourceX} ${sourceY}`,
// Horizontal to the right
`L ${rightX - r} ${sourceY}`,
// Arc turning upward
`Q ${rightX} ${sourceY} ${rightX} ${sourceY - r}`,
// Vertical upward
`L ${rightX} ${targetY + r}`,
// Arc turning left
`Q ${rightX} ${targetY} ${rightX - r} ${targetY}`,
// Horizontal left to target
`L ${offsetX - d * r} ${sourceY}`,
`Q ${offsetX} ${sourceY} ${offsetX} ${sourceY - r}`,
`L ${offsetX} ${targetY + r}`,
`Q ${offsetX} ${targetY} ${offsetX - d * r} ${targetY}`,
`L ${targetX} ${targetY}`,
];
@@ -57,10 +55,13 @@ export function ConditionEdge(props: EdgeProps) {
let defaultLabelY: number;
if (isFeedback) {
// Custom feedback path routed to the right
path = feedbackPath(sourceX, sourceY, targetX, targetY);
const rightX = Math.max(sourceX, targetX) + FEEDBACK_OFFSET_X;
defaultLabelX = rightX;
const side = edgeData?.feedbackSide ?? "right";
path = feedbackPath(sourceX, sourceY, targetX, targetY, side);
const offsetX =
side === "right"
? Math.max(sourceX, targetX) + FEEDBACK_OFFSET_X
: Math.min(sourceX, targetX) - FEEDBACK_OFFSET_X;
defaultLabelX = offsetX;
defaultLabelY = (sourceY + targetY) / 2;
} else {
const result = getSmoothStepPath({
@@ -78,9 +79,8 @@ export function ConditionEdge(props: EdgeProps) {
defaultLabelY = result[2];
}
const stroke = isFallback ? "var(--color-text-muted)" : "var(--color-accent)";
const strokeDasharray = isFallback ? "5 4" : undefined;
const label = edgeData?.condition ?? "";
const stroke = "var(--color-accent)";
const label = isFallback ? "" : (edgeData?.condition ?? "");
// Use pre-computed label position if available, otherwise fall back to default
const labelX = edgeData?.labelX ?? defaultLabelX;
@@ -92,7 +92,7 @@ export function ConditionEdge(props: EdgeProps) {
id={id}
path={path}
markerEnd={markerEnd}
style={{ stroke, strokeWidth: 1.5, strokeDasharray }}
style={{ stroke, strokeWidth: 1.5 }}
/>
{label !== "" && (
<EdgeLabelRenderer>
@@ -102,7 +102,7 @@ export function ConditionEdge(props: EdgeProps) {
transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`,
background: "var(--color-surface)",
border: "1px solid var(--color-border)",
color: isFallback ? "var(--color-text-muted)" : "var(--color-text)",
color: "var(--color-text)",
whiteSpace: "nowrap",
zIndex: 10,
}}
@@ -31,7 +31,7 @@ export function RoleNode(props: NodeProps) {
return (
<div
className={`px-3 py-2 rounded-md border-2 text-xs font-medium cursor-pointer ${isActive ? "wf-node-pulse" : ""}`}
className={`px-3 py-2 rounded-md border-2 text-xs font-medium ${t.state !== "default" ? "cursor-pointer" : ""} ${isActive ? "wf-node-pulse" : ""}`}
style={{
width: 180,
height: 60,
@@ -23,6 +23,7 @@ export type ConditionEdgeData = {
isFallback: boolean;
isFeedback: boolean;
isSelfLoop: boolean;
feedbackSide: "right" | "left" | null;
labelX: number | null;
labelY: number | null;
[key: string]: unknown;
@@ -173,6 +173,8 @@ function computeLayout(input: LayoutInput): LayoutResult {
// Build edges with label positions
// For feedback edges (target rank < source rank), we'll compute label at midpoint
// of the right-side arc. The actual SVG path is drawn by ConditionEdge component.
// Track feedback edge count per target node for alternating sides
const feedbackCountByTarget = new Map<string, number>();
const edges: Edge[] = input.edges.map((e) => {
const isFallback = e.condition === "FALLBACK";
const isSelfLoop = e.from === e.to;
@@ -185,13 +187,20 @@ function computeLayout(input: LayoutInput): LayoutResult {
let labelX: number | null = null;
let labelY: number | null = null;
let feedbackSide: "right" | "left" | null = null;
if (sourcePos !== undefined && targetPos !== undefined) {
if (isFeedback) {
// Label on the right side of the feedback arc
const rightX = centerX + ROLE_NODE_WIDTH / 2 + FEEDBACK_OFFSET_X;
// Alternate feedback edges left/right per target node
const count = feedbackCountByTarget.get(e.to) ?? 0;
feedbackCountByTarget.set(e.to, count + 1);
feedbackSide = count % 2 === 0 ? "right" : "left";
const offsetX =
feedbackSide === "right"
? centerX + ROLE_NODE_WIDTH / 2 + FEEDBACK_OFFSET_X
: centerX - ROLE_NODE_WIDTH / 2 - FEEDBACK_OFFSET_X;
const midY = (sourcePos.y + sourcePos.h / 2 + targetPos.y + targetPos.h / 2) / 2;
labelX = rightX;
labelX = offsetX;
labelY = midY;
} else if (!isSelfLoop) {
// Forward edge: label between source bottom and target top
@@ -214,6 +223,7 @@ function computeLayout(input: LayoutInput): LayoutResult {
isFallback,
isFeedback,
isSelfLoop,
feedbackSide,
labelX,
labelY,
},