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