diff --git a/packages/workflow-dashboard/src/components/workflow-list.tsx b/packages/workflow-dashboard/src/components/workflow-list.tsx index d2bcb80..651182e 100644 --- a/packages/workflow-dashboard/src/components/workflow-list.tsx +++ b/packages/workflow-dashboard/src/components/workflow-list.tsx @@ -1,5 +1,5 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; -import type { WorkflowDetail } from "../api.ts"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import type { WorkflowDetail, WorkflowRoleDescriptor } from "../api.ts"; import { getWorkflowDetail, listWorkflows } from "../api.ts"; import { useFetch } from "../hooks.ts"; import { type NodeState, WorkflowGraph } from "./workflow-graph/index.ts"; @@ -17,6 +17,88 @@ function versionCount(detail: WorkflowDetail): number { return detail.history.length + 1; } +function schemaPropertiesTable(schema: Record): Array<{ + name: string; + type: string; + description: string; +}> { + const props = (schema.properties ?? {}) as Record>; + const required = new Set( + Array.isArray(schema.required) ? (schema.required as string[]) : [], + ); + return Object.entries(props).map(([name, prop]) => { + let type = String(prop.type ?? "unknown"); + if (!required.has(name)) type += "?"; + const description = String(prop.description ?? ""); + return { name, type, description }; + }); +} + +function RoleCard({ + roleName, + role, +}: { + roleName: string; + role: WorkflowRoleDescriptor; +}) { + const fields = schemaPropertiesTable(role.schema); + return ( +
+

+ {roleName} +

+ {role.description !== "" && ( +

+ {role.description} +

+ )} + {fields.length > 0 && ( +
+

+ Meta Schema +

+ + + + + + + + + + {fields.map((f) => ( + + + + + + ))} + +
FieldTypeDescription
{f.name}{f.type}{f.description || "—"}
+
+ )} + {fields.length === 0 && Object.keys(role.schema).length > 0 && ( +
+          {JSON.stringify(role.schema, null, 2)}
+        
+ )} +
+ ); +} + function ExpandedWorkflowBody({ cacheEntry, staticNodeStates, @@ -24,6 +106,9 @@ function ExpandedWorkflowBody({ cacheEntry: DetailCacheEntry | undefined; staticNodeStates: Map; }) { + const [highlightedRole, setHighlightedRole] = useState(null); + const highlightTimerRef = useRef | null>(null); + if (cacheEntry === undefined || cacheEntry.status === "loading") { return (

@@ -46,65 +131,120 @@ function ExpandedWorkflowBody({ const vc = versionCount(detail); const hasGraph = descriptor !== null && edgeCount > 0; + const roleEntries = + descriptor !== null ? Object.entries(descriptor.roles) : []; + + function handleGraphNodeClick(nodeId: string) { + const el = document.getElementById(`role-${nodeId}`); + if (el === null) return; + el.scrollIntoView({ behavior: "smooth", block: "center" }); + if (highlightTimerRef.current !== null) clearTimeout(highlightTimerRef.current); + setHighlightedRole(nodeId); + highlightTimerRef.current = setTimeout(() => { + setHighlightedRole(null); + highlightTimerRef.current = null; + }, 1500); + } + + // All roles are "completed" (static view, all nodes lit) + const allLitStates = useMemo(() => { + const m = new Map(); + m.set("__start__", "completed"); + m.set("__end__", "completed"); + for (const [name] of roleEntries) { + m.set(name, "completed"); + } + return m; + }, [roleEntries]); return (

-
-
-

- {detail.name} -

-

- Hash -

- - {detail.hash} - + {/* Left: graph sidebar */} + {hasGraph && ( +
+
+
+ Workflow graph + + {edgeCount} edge{edgeCount === 1 ? "" : "s"} + +
+
+ +
+
-

- {vc} version{vc !== 1 ? "s" : ""} -

-
-

- Description -

-

+ )} + + {/* Right: workflow info + role cards */} +

+ {/* Workflow overview */} +
+

+ {detail.name} +

+

{descriptor !== null && descriptor.description !== "" ? descriptor.description : descriptor !== null ? "—" : "No descriptor available for this workflow version."}

-
-
- {hasGraph ? ( -
-
- Workflow graph +
- {edgeCount} edge{edgeCount === 1 ? "" : "s"} + Hash:{" "} + + {detail.hash} + -
-
- + + {vc} version{vc !== 1 ? "s" : ""} + + {roleEntries.length > 0 && ( + + {roleEntries.length} role{roleEntries.length !== 1 ? "s" : ""} + + )}
- ) : null} + + {/* Role cards */} + {roleEntries.map(([name, role]) => ( +
+ +
+ ))} +
); }