feat: clarify workflow graph presentation
This commit is contained in:
@@ -88,6 +88,22 @@ describe("DemoWorkflowScene", () => {
|
|||||||
expect(screen.queryByText("Retry live service")).not.toBeInTheDocument();
|
expect(screen.queryByText("Retry live service")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("presents Scene 10 graph as a ten-node horizontal workflow diagram", () => {
|
||||||
|
renderBeat("graph");
|
||||||
|
|
||||||
|
expect(screen.getByLabelText("demo workflow stage")).toHaveAttribute("data-graph-layout", "horizontal");
|
||||||
|
const graph = screen.getByRole("group", { name: /workflow graph/i });
|
||||||
|
expect(graph).toHaveAttribute("data-graph-layout", "horizontal");
|
||||||
|
expect(document.querySelectorAll('button[aria-label^="workflow node:"]')).toHaveLength(10);
|
||||||
|
expect(screen.getByText(/revision requested/i)).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText(/cancel: no submitted output/i)).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
const legend = screen.getByLabelText("workflow graph node types");
|
||||||
|
expect(legend).toHaveTextContent("Action");
|
||||||
|
expect(legend).toHaveTextContent("Human boundary");
|
||||||
|
expect(legend).toHaveTextContent("Outcome");
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps the run receipt visible when the graph takes over", () => {
|
it("keeps the run receipt visible when the graph takes over", () => {
|
||||||
renderBeat("graph");
|
renderBeat("graph");
|
||||||
|
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ export const DemoWorkflowScene = ({
|
|||||||
className="demo-workflow-stage"
|
className="demo-workflow-stage"
|
||||||
data-beat={beat.id}
|
data-beat={beat.id}
|
||||||
data-demo-layout={layout}
|
data-demo-layout={layout}
|
||||||
|
data-graph-layout={beat.id === "graph" ? "horizontal" : undefined}
|
||||||
data-primary-surface={surface.primarySurface}
|
data-primary-surface={surface.primarySurface}
|
||||||
data-support-surface={surface.supportSurface}
|
data-support-surface={surface.supportSurface}
|
||||||
aria-label="demo workflow stage"
|
aria-label="demo workflow stage"
|
||||||
|
|||||||
@@ -66,6 +66,26 @@ describe("WorkflowGraphStage", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("exposes exactly the ten real workflow nodes as accessible controls", () => {
|
||||||
|
render(
|
||||||
|
<WorkflowGraphStage
|
||||||
|
execution={{ completedNodeIds: [], currentNodeId: null }}
|
||||||
|
selectedNodeId={null}
|
||||||
|
selectNode={vi.fn()}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const graph = screen.getByRole("group", { name: /workflow graph/i });
|
||||||
|
// React Flow hides unmeasured node wrappers in jsdom, so assert the
|
||||||
|
// rendered button contract directly rather than depending on its role tree.
|
||||||
|
const workflowNodes = document.querySelectorAll<HTMLButtonElement>(
|
||||||
|
'button[aria-label^="workflow node:"]',
|
||||||
|
);
|
||||||
|
expect(workflowNodes).toHaveLength(10);
|
||||||
|
expect(screen.queryByText(/cancel: no submitted output/i)).not.toBeInTheDocument();
|
||||||
|
expect(graph).toHaveTextContent("Revision requested");
|
||||||
|
});
|
||||||
|
|
||||||
it("does not render graph proof count chips", () => {
|
it("does not render graph proof count chips", () => {
|
||||||
render(
|
render(
|
||||||
<WorkflowGraphStage
|
<WorkflowGraphStage
|
||||||
@@ -127,7 +147,9 @@ describe("WorkflowGraphStage", () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByRole("group", { name: "workflow graph" })).toHaveAttribute("data-graph-direction", "horizontal");
|
const graph = screen.getByRole("group", { name: "workflow graph" });
|
||||||
|
expect(graph).toHaveAttribute("data-graph-direction", "horizontal");
|
||||||
|
expect(graph).toHaveAttribute("data-graph-layout", "horizontal");
|
||||||
expect(screen.getByRole("button", { name: /zoom in/i })).toBeInTheDocument();
|
expect(screen.getByRole("button", { name: /zoom in/i })).toBeInTheDocument();
|
||||||
expect(screen.getByRole("button", { name: /zoom out/i })).toBeInTheDocument();
|
expect(screen.getByRole("button", { name: /zoom out/i })).toBeInTheDocument();
|
||||||
expect(screen.getByRole("button", { name: /fit view/i })).toBeInTheDocument();
|
expect(screen.getByRole("button", { name: /fit view/i })).toBeInTheDocument();
|
||||||
|
|||||||
@@ -34,6 +34,20 @@ type PresentationNodeData = WorkflowGraphNodeData & {
|
|||||||
readonly selectNode: (nodeId: string) => void;
|
readonly selectNode: (nodeId: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fullGraphLayout = {
|
||||||
|
nodeWidth: 176,
|
||||||
|
nodeHeight: 76,
|
||||||
|
nodesep: 72,
|
||||||
|
ranksep: 56,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const compactGraphLayout = {
|
||||||
|
nodeWidth: 132,
|
||||||
|
nodeHeight: 64,
|
||||||
|
nodesep: 42,
|
||||||
|
ranksep: 52,
|
||||||
|
} as const;
|
||||||
|
|
||||||
const edgeActive = (edge: { source: string; target: string }, execution: GraphExecutionPresentation): boolean =>
|
const edgeActive = (edge: { source: string; target: string }, execution: GraphExecutionPresentation): boolean =>
|
||||||
execution.completedNodeIds.includes(edge.source)
|
execution.completedNodeIds.includes(edge.source)
|
||||||
&& (execution.completedNodeIds.includes(edge.target) || execution.currentNodeId === edge.target);
|
&& (execution.completedNodeIds.includes(edge.target) || execution.currentNodeId === edge.target);
|
||||||
@@ -47,7 +61,7 @@ const PresentationFlowNode = ({ data }: NodeProps<Node<PresentationNodeData>>) =
|
|||||||
data-kind={data.kind}
|
data-kind={data.kind}
|
||||||
data-selected={data.selected}
|
data-selected={data.selected}
|
||||||
aria-pressed={data.selected}
|
aria-pressed={data.selected}
|
||||||
aria-label={`${data.label}${data.detail ? `, ${data.detail}` : ""}`}
|
aria-label={`workflow node: ${data.label}${data.detail ? `, ${data.detail}` : ""}`}
|
||||||
title={data.nodeRef ?? undefined}
|
title={data.nodeRef ?? undefined}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@@ -73,17 +87,16 @@ const WorkflowGraphStageInner = ({
|
|||||||
const model = useMemo(
|
const model = useMemo(
|
||||||
() => buildWorkflowGraph(presentationWorkflowPlan, {
|
() => buildWorkflowGraph(presentationWorkflowPlan, {
|
||||||
direction: "LR",
|
direction: "LR",
|
||||||
nodeWidth: 150,
|
// Keep Dagre's dimensions in lockstep with the CSS node boxes so React
|
||||||
nodeHeight: 64,
|
// Flow's measured handles stay attached when the graph is fit to view.
|
||||||
nodesep: 35,
|
...(variant === "compact" ? compactGraphLayout : fullGraphLayout),
|
||||||
ranksep: 80,
|
|
||||||
label: (node) => {
|
label: (node) => {
|
||||||
if (typeof node.label === "string") return node.label;
|
if (typeof node.label === "string") return node.label;
|
||||||
if (node.id === "review_issues") return "Review issues";
|
if (node.id === "review_issues") return "Review issues";
|
||||||
return undefined;
|
return undefined;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[],
|
[variant],
|
||||||
);
|
);
|
||||||
|
|
||||||
const nodes: Node<PresentationNodeData>[] = useMemo(
|
const nodes: Node<PresentationNodeData>[] = useMemo(
|
||||||
@@ -131,6 +144,7 @@ const WorkflowGraphStageInner = ({
|
|||||||
aria-label="workflow graph"
|
aria-label="workflow graph"
|
||||||
data-graph-variant={variant}
|
data-graph-variant={variant}
|
||||||
data-graph-direction="horizontal"
|
data-graph-direction="horizontal"
|
||||||
|
data-graph-layout="horizontal"
|
||||||
>
|
>
|
||||||
<div className="workflow-graph-stage__legend" aria-label="workflow graph node types">
|
<div className="workflow-graph-stage__legend" aria-label="workflow graph node types">
|
||||||
<span data-kind="use"><i aria-hidden="true" />Action</span>
|
<span data-kind="use"><i aria-hidden="true" />Action</span>
|
||||||
@@ -149,7 +163,7 @@ const WorkflowGraphStageInner = ({
|
|||||||
nodeTypes={nodeTypes}
|
nodeTypes={nodeTypes}
|
||||||
onNodeClick={handleNodeClick}
|
onNodeClick={handleNodeClick}
|
||||||
fitView
|
fitView
|
||||||
fitViewOptions={{ padding: 0.02, minZoom: 0.5, maxZoom: 1 }}
|
fitViewOptions={{ padding: 0.08, minZoom: 0.42, maxZoom: 1 }}
|
||||||
minZoom={0.25}
|
minZoom={0.25}
|
||||||
maxZoom={1.5}
|
maxZoom={1.5}
|
||||||
nodesDraggable={false}
|
nodesDraggable={false}
|
||||||
|
|||||||
@@ -366,6 +366,22 @@
|
|||||||
stroke-dasharray: 5 7;
|
stroke-dasharray: 5 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workflow-graph-stage .react-flow__edge-text {
|
||||||
|
paint-order: stroke;
|
||||||
|
stroke: oklch(0.105 0.02 250);
|
||||||
|
stroke-width: 0.38rem;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
fill: var(--text-primary);
|
||||||
|
font: 700 0.68rem/1 var(--font-mono, monospace);
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-graph-stage .react-flow__edge-textbg {
|
||||||
|
fill: oklch(0.105 0.02 250);
|
||||||
|
stroke: oklch(0.105 0.02 250);
|
||||||
|
}
|
||||||
|
|
||||||
.workflow-graph-stage__edge--active .react-flow__edge-path {
|
.workflow-graph-stage__edge--active .react-flow__edge-path {
|
||||||
stroke: var(--accent-cyan);
|
stroke: var(--accent-cyan);
|
||||||
stroke-width: 2.6;
|
stroke-width: 2.6;
|
||||||
@@ -425,6 +441,23 @@
|
|||||||
background: var(--stage-line);
|
background: var(--stage-line);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workflow-graph-stage__legend span[data-kind="use"] i {
|
||||||
|
border-radius: 0.12rem;
|
||||||
|
background: oklch(0.62 0.12 195);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-graph-stage__legend span[data-kind="interrupt"] i {
|
||||||
|
border-radius: 0.1rem;
|
||||||
|
background: var(--accent-amber);
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-graph-stage__legend span[data-kind="end"] i {
|
||||||
|
width: 0.66rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: oklch(0.68 0.025 250);
|
||||||
|
}
|
||||||
|
|
||||||
.workflow-graph-stage__legend i[data-state="current"] {
|
.workflow-graph-stage__legend i[data-state="current"] {
|
||||||
background: var(--accent-cyan);
|
background: var(--accent-cyan);
|
||||||
}
|
}
|
||||||
@@ -502,12 +535,12 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
width: 9.2rem;
|
width: 11rem;
|
||||||
min-height: 4.15rem;
|
min-height: 4.75rem;
|
||||||
transform: none;
|
transform: none;
|
||||||
border: 1px solid var(--stage-line);
|
border: 1px solid var(--stage-line);
|
||||||
border-radius: 0.7rem;
|
border-radius: 0.7rem;
|
||||||
padding: 0.58rem 0.68rem;
|
padding: 0.68rem 0.78rem;
|
||||||
background: oklch(0.15 0.025 250 / 0.96);
|
background: oklch(0.15 0.025 250 / 0.96);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
@@ -516,14 +549,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.presentation-route .workflow-graph-stage__node[data-kind="end"] {
|
.presentation-route .workflow-graph-stage__node[data-kind="end"] {
|
||||||
min-height: 3.65rem;
|
min-height: 4.5rem;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
place-content: center;
|
place-content: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workflow-graph-stage__node strong {
|
.workflow-graph-stage__node strong {
|
||||||
font-size: 0.84rem;
|
font-size: 0.9rem;
|
||||||
line-height: 1.05;
|
line-height: 1.05;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
@@ -534,7 +567,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.workflow-graph-stage__node small {
|
.workflow-graph-stage__node small {
|
||||||
font-size: 0.62rem;
|
font-size: 0.67rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workflow-graph-stage__node-state {
|
.workflow-graph-stage__node-state {
|
||||||
@@ -928,7 +961,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.presentation-route .workflow-graph-stage__node {
|
.presentation-route .workflow-graph-stage__node {
|
||||||
width: 7rem;
|
width: 11rem;
|
||||||
|
padding-inline: 0.78rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-graph-stage[data-graph-variant="compact"] .workflow-graph-stage__node {
|
||||||
|
width: 8.25rem;
|
||||||
|
min-height: 4rem;
|
||||||
padding-inline: 0.55rem;
|
padding-inline: 0.55rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user