Add workflow detail page with tabs at /workflows/:name

Detail page shows title, filename, description, and horizontal tabs
for Definition, Diagram, and Runs. Workflow names in the list are now
links to their detail pages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-02-28 13:50:06 -05:00
parent 00c470297b
commit 4dac57685c
3 changed files with 166 additions and 9 deletions

View file

@ -10,6 +10,7 @@ export default [
layout("layouts/app-shell.tsx", [
route("start", "routes/start.tsx"),
route("workflows", "routes/workflows.tsx"),
route("workflows/:name", "routes/workflow-detail.tsx"),
route("runs", "routes/pipelines.tsx"),
route("insights", "routes/insights.tsx"),
route("settings", "routes/settings.tsx"),

View file

@ -0,0 +1,88 @@
import { useState } from "react";
import { useParams } from "react-router";
import type { Route } from "./+types/workflow-detail";
const workflowData: Record<string, { title: string; description: string; filename: string }> = {
fix_build: {
title: "Fix Build",
filename: "fix_build.dot",
description: "Automatically diagnoses and fixes CI build failures by analyzing error logs, identifying root causes, and applying targeted code changes.",
},
implement: {
title: "Implement Feature",
filename: "implement.dot",
description: "Generates production-ready code from a technical blueprint, including tests, documentation, and a pull request ready for review.",
},
sync_drift: {
title: "Sync Drift",
filename: "sync_drift.dot",
description: "Detects configuration and code drift between environments, then generates reconciliation patches to bring everything back in sync.",
},
expand: {
title: "Expand Product",
filename: "expand.dot",
description: "Evolves the product by analyzing usage patterns and specifications to propose and implement incremental improvements.",
},
};
const tabs = ["Definition", "Diagram", "Runs"] as const;
type Tab = (typeof tabs)[number];
export function meta({ params }: Route.MetaArgs) {
const workflow = workflowData[params.name ?? ""];
const title = workflow?.title ?? params.name;
return [{ title: `${title} — Arc` }];
}
export default function WorkflowDetail() {
const { name } = useParams();
const workflow = workflowData[name ?? ""];
const [activeTab, setActiveTab] = useState<Tab>("Definition");
if (workflow == null) {
return <p className="text-sm text-ice-300">Workflow not found.</p>;
}
return (
<div>
<div className="mb-6">
<div className="flex items-center gap-3">
<h2 className="text-xl font-semibold text-white">{workflow.title}</h2>
<span className="font-mono text-xs text-navy-600">{workflow.filename}</span>
</div>
<p className="mt-2 text-sm leading-relaxed text-ice-300">{workflow.description}</p>
</div>
<div className="border-b border-white/[0.06]">
<nav className="-mb-px flex gap-6">
{tabs.map((tab) => (
<button
key={tab}
type="button"
onClick={() => setActiveTab(tab)}
className={`border-b-2 pb-3 text-sm font-medium transition-colors ${
activeTab === tab
? "border-teal-500 text-white"
: "border-transparent text-navy-600 hover:border-white/10 hover:text-ice-300"
}`}
>
{tab}
</button>
))}
</nav>
</div>
<div className="mt-6">
{activeTab === "Definition" && (
<p className="text-sm text-navy-600">Workflow definition will appear here.</p>
)}
{activeTab === "Diagram" && (
<p className="text-sm text-navy-600">Workflow diagram will appear here.</p>
)}
{activeTab === "Runs" && (
<p className="text-sm text-navy-600">Recent runs will appear here.</p>
)}
</div>
</div>
);
}

View file

@ -1,20 +1,66 @@
import { useState } from "react";
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
import { ChevronDownIcon, PlusIcon } from "@heroicons/react/20/solid";
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import { Link } from "react-router";
import type { Route } from "./+types/workflows";
export function meta({}: Route.MetaArgs) {
return [{ title: "Workflows — Arc" }];
}
export const handle = {
headerExtra: (
<div className="relative inline-flex rounded-md">
<button
type="button"
className="inline-flex items-center gap-2 rounded-l-md bg-teal-700 px-3.5 py-2 text-sm font-semibold text-white transition-colors hover:bg-teal-500"
>
<PlusIcon className="size-4" aria-hidden="true" />
Create Workflow
</button>
<Menu as="div" className="relative -ml-px flex">
<MenuButton className="inline-flex items-center rounded-r-md border-l border-teal-500/30 bg-teal-700 px-2 text-white transition-colors hover:bg-teal-500">
<ChevronDownIcon className="size-4" aria-hidden="true" />
</MenuButton>
<MenuItems
transition
className="absolute right-0 top-full z-10 mt-2 w-48 origin-top-right rounded-md bg-navy-800 py-1 outline-1 -outline-offset-1 outline-white/10 transition data-closed:scale-95 data-closed:transform data-closed:opacity-0 data-enter:duration-100 data-enter:ease-out data-leave:duration-75 data-leave:ease-in"
>
<MenuItem>
<button
type="button"
className="block w-full px-4 py-2 text-left text-sm text-ice-300 data-focus:bg-white/5 data-focus:outline-hidden"
>
Import from file
</button>
</MenuItem>
<MenuItem>
<button
type="button"
className="block w-full px-4 py-2 text-left text-sm text-ice-300 data-focus:bg-white/5 data-focus:outline-hidden"
>
Duplicate existing
</button>
</MenuItem>
</MenuItems>
</Menu>
</div>
),
};
interface Workflow {
name: string;
slug: string;
filename: string;
lastRun: string;
}
const workflows: Workflow[] = [
{ name: "Fix Build", filename: "fix_build.dot", lastRun: "2 hours ago" },
{ name: "Implement Feature", filename: "implement.dot", lastRun: "4 days ago" },
{ name: "Sync Drift", filename: "sync_drift.dot", lastRun: "1 day ago" },
{ name: "Expand Product", filename: "expand.dot", lastRun: "2 weeks ago" },
{ name: "Fix Build", slug: "fix_build", filename: "fix_build.dot", lastRun: "2 hours ago" },
{ name: "Implement Feature", slug: "implement", filename: "implement.dot", lastRun: "4 days ago" },
{ name: "Sync Drift", slug: "sync_drift", filename: "sync_drift.dot", lastRun: "1 day ago" },
{ name: "Expand Product", slug: "expand", filename: "expand.dot", lastRun: "2 weeks ago" },
];
function PlayIcon({ className }: { className?: string }) {
@ -46,7 +92,7 @@ function WorkflowCard({ workflow }: { workflow: Workflow }) {
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-ice-100">{workflow.name}</span>
<Link to={`/workflows/${workflow.slug}`} className="text-sm font-medium text-ice-100 hover:text-white">{workflow.name}</Link>
<span className="font-mono text-xs text-navy-600">{workflow.filename}</span>
</div>
<p className="mt-1 text-xs text-navy-600">Last run {workflow.lastRun}</p>
@ -64,11 +110,33 @@ function WorkflowCard({ workflow }: { workflow: Workflow }) {
}
export default function Workflows() {
const [query, setQuery] = useState("");
const filtered = workflows.filter(
(w) =>
w.name.toLowerCase().includes(query.toLowerCase()) ||
w.filename.toLowerCase().includes(query.toLowerCase()),
);
return (
<div className="mx-auto max-w-3xl space-y-3">
{workflows.map((workflow) => (
<WorkflowCard key={workflow.filename} workflow={workflow} />
))}
<div className="space-y-4">
<div className="relative">
<MagnifyingGlassIcon className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-navy-600" />
<input
type="text"
placeholder="Search workflows..."
value={query}
onChange={(e) => setQuery(e.target.value)}
className="w-full rounded-lg border border-white/[0.06] bg-navy-800/80 py-2 pl-9 pr-3 text-sm text-ice-100 placeholder-navy-600 outline-none transition-colors focus:border-teal-500/40 focus:ring-0"
/>
</div>
<div className="space-y-3">
{filtered.map((workflow) => (
<WorkflowCard key={workflow.filename} workflow={workflow} />
))}
{filtered.length === 0 && (
<p className="py-8 text-center text-sm text-navy-600">No workflows match "{query}"</p>
)}
</div>
</div>
);
}