fix: keep demo workflow graph in frame
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import { cleanup, render, screen } from "@testing-library/react";
|
import { cleanup, render, screen } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
import { WorkflowGraphStage } from "./WorkflowGraphStage.js";
|
import { presentationNodes, WorkflowGraphStage } from "./WorkflowGraphStage.js";
|
||||||
|
|
||||||
afterEach(() => cleanup());
|
afterEach(() => cleanup());
|
||||||
|
|
||||||
@@ -58,4 +58,28 @@ describe("WorkflowGraphStage", () => {
|
|||||||
expect(connectors).toHaveLength(4);
|
expect(connectors).toHaveLength(4);
|
||||||
expect(connectors.filter((connector) => connector.dataset.active === "true")).toHaveLength(1);
|
expect(connectors.filter((connector) => connector.dataset.active === "true")).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps all graph nodes inside the visible percentage frame", () => {
|
||||||
|
for (const node of presentationNodes) {
|
||||||
|
expect(node.x).toBeGreaterThanOrEqual(14);
|
||||||
|
expect(node.x).toBeLessThanOrEqual(86);
|
||||||
|
expect(node.y).toBeGreaterThanOrEqual(28);
|
||||||
|
expect(node.y).toBeLessThanOrEqual(72);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders compact run proof inside the graph", () => {
|
||||||
|
render(
|
||||||
|
<WorkflowGraphStage
|
||||||
|
execution={{ completedNodeIds: ["read_docs"], currentNodeId: "build_report" }}
|
||||||
|
selectedNodeId={null}
|
||||||
|
selectNode={vi.fn()}
|
||||||
|
proof={{ runId: "run_recorded_lda_report", traceLabel: "5 nodes", evidenceLabel: "JSON-RPC captured" }}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByLabelText("workflow graph proof")).toHaveTextContent("run_recorded_lda_report");
|
||||||
|
expect(screen.getByLabelText("workflow graph proof")).toHaveTextContent("5 nodes");
|
||||||
|
expect(screen.getByLabelText("workflow graph proof")).toHaveTextContent("JSON-RPC captured");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ export type PresentationNode = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const presentationNodes: ReadonlyArray<PresentationNode> = [
|
export const presentationNodes: ReadonlyArray<PresentationNode> = [
|
||||||
{ id: "read_docs", label: "Read documents", detail: "5 selected", kind: "node", x: 9, y: 57 },
|
{ id: "read_docs", label: "Read documents", detail: "5 selected", kind: "node", x: 14, y: 58 },
|
||||||
{ id: "build_report", label: "Build report", detail: "Markdown", kind: "node", x: 30, y: 35 },
|
{ id: "build_report", label: "Build report", detail: "Markdown", kind: "node", x: 34, y: 36 },
|
||||||
{ id: "review_issues", label: "Issue review", detail: "Typed interrupt", kind: "interrupt", x: 52, y: 57 },
|
{ id: "review_issues", label: "Issue review", detail: "Typed interrupt", kind: "interrupt", x: 52, y: 58 },
|
||||||
{ id: "create_issues", label: "Create issues", detail: "Selected only", kind: "node", x: 74, y: 35 },
|
{ id: "create_issues", label: "Create issues", detail: "Selected only", kind: "node", x: 70, y: 36 },
|
||||||
{ id: "end_completed", label: "Completed", detail: "Persisted run", kind: "end", x: 92, y: 57 },
|
{ id: "end_completed", label: "Completed", detail: "Persisted run", kind: "end", x: 86, y: 58 },
|
||||||
];
|
];
|
||||||
|
|
||||||
type PresentationEdge = readonly [from: string, to: string];
|
type PresentationEdge = readonly [from: string, to: string];
|
||||||
@@ -30,10 +30,17 @@ const presentationEdges: ReadonlyArray<PresentationEdge> = [
|
|||||||
|
|
||||||
type NodeExecutionState = "completed" | "current" | "future";
|
type NodeExecutionState = "completed" | "current" | "future";
|
||||||
|
|
||||||
|
export type WorkflowGraphProof = {
|
||||||
|
readonly runId: string | null;
|
||||||
|
readonly traceLabel: string;
|
||||||
|
readonly evidenceLabel: string;
|
||||||
|
};
|
||||||
|
|
||||||
type WorkflowGraphStageProps = {
|
type WorkflowGraphStageProps = {
|
||||||
readonly execution: GraphExecutionPresentation;
|
readonly execution: GraphExecutionPresentation;
|
||||||
readonly selectedNodeId: string | null;
|
readonly selectedNodeId: string | null;
|
||||||
readonly selectNode: (nodeId: string) => void;
|
readonly selectNode: (nodeId: string) => void;
|
||||||
|
readonly proof?: WorkflowGraphProof;
|
||||||
};
|
};
|
||||||
|
|
||||||
const executionStateForNode = (
|
const executionStateForNode = (
|
||||||
@@ -55,6 +62,7 @@ export const WorkflowGraphStage = ({
|
|||||||
execution,
|
execution,
|
||||||
selectedNodeId,
|
selectedNodeId,
|
||||||
selectNode,
|
selectNode,
|
||||||
|
proof,
|
||||||
}: WorkflowGraphStageProps) => {
|
}: WorkflowGraphStageProps) => {
|
||||||
const markerPrefix = useId().replaceAll(":", "");
|
const markerPrefix = useId().replaceAll(":", "");
|
||||||
const arrowMarkerId = `${markerPrefix}-workflow-arrow`;
|
const arrowMarkerId = `${markerPrefix}-workflow-arrow`;
|
||||||
@@ -68,6 +76,14 @@ export const WorkflowGraphStage = ({
|
|||||||
<span><i data-state="interrupt" />Human boundary</span>
|
<span><i data-state="interrupt" />Human boundary</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{proof && (
|
||||||
|
<div className="workflow-graph-stage__proof" aria-label="workflow graph proof">
|
||||||
|
<span><b>Run</b><code>{proof.runId ?? "run unavailable"}</code></span>
|
||||||
|
<span><b>Trace</b>{proof.traceLabel}</span>
|
||||||
|
<span><b>Evidence</b>{proof.evidenceLabel}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<svg className="workflow-graph-stage__connectors" aria-hidden="true">
|
<svg className="workflow-graph-stage__connectors" aria-hidden="true">
|
||||||
<defs>
|
<defs>
|
||||||
<marker className="workflow-graph-stage__arrow-marker" id={arrowMarkerId} markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
|
<marker className="workflow-graph-stage__arrow-marker" id={arrowMarkerId} markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
|
||||||
|
|||||||
@@ -400,16 +400,53 @@
|
|||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workflow-graph-stage__proof {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 4;
|
||||||
|
left: 0.8rem;
|
||||||
|
bottom: 0.7rem;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.55rem;
|
||||||
|
max-width: calc(100% - 1.6rem);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font: 600 0.58rem/1.2 var(--font-mono, monospace);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-graph-stage__proof span {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
border: 1px solid oklch(0.36 0.04 250 / 0.7);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.18rem 0.45rem;
|
||||||
|
background: oklch(0.09 0.018 250 / 0.78);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-graph-stage__proof b {
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-graph-stage__proof code {
|
||||||
|
max-width: 14rem;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--text-primary);
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.presentation-route .workflow-graph-stage__node {
|
.presentation-route .workflow-graph-stage__node {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
width: clamp(7.2rem, 11vw, 9.5rem);
|
width: clamp(7rem, 10vw, 8.75rem);
|
||||||
min-height: 4.35rem;
|
min-height: 4.15rem;
|
||||||
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.65rem 0.75rem;
|
padding: 0.58rem 0.68rem;
|
||||||
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;
|
||||||
|
|||||||
Reference in New Issue
Block a user