feat: Dashboard workflow graph visualization with React Flow (#198)
Phase 1: API + static graph rendering Backend: - GET /workflows/:name now returns descriptor (with graph) from bundle YAML - Graceful fallback to null if YAML missing/invalid Frontend: - New workflow-graph/ component module (7 files) - React Flow + dagre auto-layout (TB direction) - Custom nodes: RoleNode (rounded rect) + TerminalNode (circle for START/END) - Custom edges: dashed for FALLBACK, solid with label for conditions - Self-loop edges supported (e.g. coder → coder) - Node states: default/completed/active with color-coded borders - Active node pulse animation - Collapsible graph panel (300px) above thread records - Dark theme using existing CSS variables Integration: - ThreadDetail extracts workflow name → fetches descriptor → computes node states → renders graph - Node states derived from ThreadRecord[] (completed/active/default)
This commit is contained in:
@@ -1,9 +1,14 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import type { WorkflowDescriptor } from "@uncaged/workflow-protocol";
|
||||
import {
|
||||
getRegisteredWorkflow,
|
||||
listRegisteredWorkflowNames,
|
||||
readWorkflowRegistry,
|
||||
validateWorkflowDescriptor,
|
||||
} from "@uncaged/workflow-register";
|
||||
import { Hono } from "hono";
|
||||
import { parse as parseYaml } from "yaml";
|
||||
|
||||
export function createWorkflowRoutes(storageRoot: string): Hono {
|
||||
const app = new Hono();
|
||||
@@ -35,7 +40,17 @@ export function createWorkflowRoutes(storageRoot: string): Hono {
|
||||
if (entry === null) {
|
||||
return c.json({ error: `workflow not found: ${name}` }, 404);
|
||||
}
|
||||
return c.json({ name, ...entry });
|
||||
let descriptor: WorkflowDescriptor | null = null;
|
||||
try {
|
||||
const yamlPath = join(storageRoot, "bundles", `${entry.hash}.yaml`);
|
||||
const yamlText = await readFile(yamlPath, "utf8");
|
||||
const parsed: unknown = parseYaml(yamlText);
|
||||
const validated = validateWorkflowDescriptor(parsed);
|
||||
descriptor = validated.ok ? validated.value : null;
|
||||
} catch {
|
||||
descriptor = null;
|
||||
}
|
||||
return c.json({ name, ...entry, descriptor });
|
||||
});
|
||||
|
||||
app.get("/:name/history", async (c) => {
|
||||
|
||||
Reference in New Issue
Block a user