mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
Add run configuration and graph sub-pages under /runs/:id
Sidebar "Run Configuration" and "Workflow Graph" links now navigate to /runs/:id/configuration and /runs/:id/graph instead of away to the workflow detail page. Extracts CollapsibleFile into a shared component. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1245106cea
commit
d37d2c8f49
7 changed files with 272 additions and 47 deletions
44
apps/arc-web/app/components/collapsible-file.tsx
Normal file
44
apps/arc-web/app/components/collapsible-file.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { useState } from "react";
|
||||
import { ChevronRightIcon } from "@heroicons/react/20/solid";
|
||||
import type { FileContents } from "@pierre/diffs";
|
||||
import { File } from "@pierre/diffs/react";
|
||||
|
||||
export function CollapsibleFile({
|
||||
file,
|
||||
defaultOpen = true,
|
||||
}: {
|
||||
file: FileContents;
|
||||
defaultOpen?: boolean;
|
||||
}) {
|
||||
const [open, setOpen] = useState(defaultOpen);
|
||||
|
||||
const lines = file.contents.split("\n");
|
||||
const lineCount = lines.length;
|
||||
const loc = lines.filter((l) => l.trim().length > 0).length;
|
||||
|
||||
return (
|
||||
<div className="rounded-md border border-white/[0.06] bg-navy-800/50 overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
className="flex w-full items-center gap-2 px-4 py-2.5 text-left hover:bg-white/[0.02] transition-colors"
|
||||
>
|
||||
<ChevronRightIcon
|
||||
className={`size-4 text-navy-600 transition-transform duration-150 ${open ? "rotate-90" : ""}`}
|
||||
/>
|
||||
<span className="font-mono text-xs text-navy-600">{file.name}</span>
|
||||
<span className="ml-auto font-mono text-xs text-navy-600/60">
|
||||
{lineCount} lines ({loc} loc)
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div className={open ? "" : "hidden"}>
|
||||
<div className="border-t border-white/[0.06]" />
|
||||
<File
|
||||
file={file}
|
||||
options={{ theme: "pierre-dark", disableFileHeader: true }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -19,6 +19,8 @@ export default [
|
|||
route("runs/:id", "routes/run-detail.tsx", [
|
||||
index("routes/run-overview.tsx"),
|
||||
route("stages/:stageId", "routes/run-stages.tsx"),
|
||||
route("configuration", "routes/run-configuration.tsx"),
|
||||
route("graph", "routes/run-graph.tsx"),
|
||||
route("files", "routes/run-files-changed.tsx"),
|
||||
route("usage", "routes/run-usage.tsx"),
|
||||
]),
|
||||
|
|
|
|||
101
apps/arc-web/app/routes/run-configuration.tsx
Normal file
101
apps/arc-web/app/routes/run-configuration.tsx
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import { Link, useParams } from "react-router";
|
||||
import { CheckCircleIcon, ArrowPathIcon, PauseCircleIcon, XCircleIcon } from "@heroicons/react/24/solid";
|
||||
import { DocumentTextIcon, MapIcon } from "@heroicons/react/24/outline";
|
||||
import { findRun } from "../data/runs";
|
||||
import { workflowData } from "./workflow-detail";
|
||||
import { CollapsibleFile } from "../components/collapsible-file";
|
||||
|
||||
export const handle = { wide: true };
|
||||
|
||||
type StageStatus = "completed" | "running" | "pending" | "failed";
|
||||
|
||||
interface Stage {
|
||||
id: string;
|
||||
name: string;
|
||||
status: StageStatus;
|
||||
duration: string;
|
||||
}
|
||||
|
||||
const stages: Stage[] = [
|
||||
{ id: "detect-drift", name: "Detect Drift", status: "completed", duration: "1m 12s" },
|
||||
{ id: "propose-changes", name: "Propose Changes", status: "completed", duration: "2m 34s" },
|
||||
{ id: "review-changes", name: "Review Changes", status: "completed", duration: "0m 45s" },
|
||||
{ id: "apply-changes", name: "Apply Changes", status: "running", duration: "1m 58s" },
|
||||
];
|
||||
|
||||
const statusConfig: Record<StageStatus, { icon: typeof CheckCircleIcon; color: string }> = {
|
||||
completed: { icon: CheckCircleIcon, color: "text-mint" },
|
||||
running: { icon: ArrowPathIcon, color: "text-teal-500" },
|
||||
pending: { icon: PauseCircleIcon, color: "text-navy-600" },
|
||||
failed: { icon: XCircleIcon, color: "text-coral" },
|
||||
};
|
||||
|
||||
export default function RunConfiguration() {
|
||||
const { id } = useParams();
|
||||
const run = findRun(id ?? "");
|
||||
const workflow = run ? workflowData[run.workflow] : undefined;
|
||||
|
||||
return (
|
||||
<div className="flex gap-6">
|
||||
<nav className="w-56 shrink-0 space-y-6">
|
||||
<div>
|
||||
<h3 className="px-2 text-xs font-medium uppercase tracking-wider text-navy-600">Stages</h3>
|
||||
<ul className="mt-2 space-y-0.5">
|
||||
{stages.map((stage) => {
|
||||
const config = statusConfig[stage.status];
|
||||
const Icon = config.icon;
|
||||
return (
|
||||
<li key={stage.id}>
|
||||
<Link
|
||||
to={`/runs/${id}/stages/${stage.id}`}
|
||||
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-ice-300 transition-colors hover:bg-white/[0.04] hover:text-white"
|
||||
>
|
||||
<Icon className={`size-4 shrink-0 ${config.color} ${stage.status === "running" ? "animate-spin" : ""}`} />
|
||||
<span className="flex-1 truncate">{stage.name}</span>
|
||||
<span className="font-mono text-xs tabular-nums text-navy-600">{stage.duration}</span>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{workflow && (
|
||||
<div>
|
||||
<h3 className="px-2 text-xs font-medium uppercase tracking-wider text-navy-600">Workflow</h3>
|
||||
<ul className="mt-2 space-y-0.5">
|
||||
<li>
|
||||
<Link
|
||||
to={`/runs/${id}/configuration`}
|
||||
className="flex items-center gap-2 rounded-md bg-white/[0.06] px-2 py-1.5 text-sm text-white transition-colors"
|
||||
>
|
||||
<DocumentTextIcon className="size-4 shrink-0 text-navy-600" />
|
||||
Run Configuration
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
to={`/runs/${id}/graph`}
|
||||
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-ice-300 transition-colors hover:bg-white/[0.04] hover:text-white"
|
||||
>
|
||||
<MapIcon className="size-4 shrink-0 text-navy-600" />
|
||||
Workflow Graph
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
{workflow ? (
|
||||
<CollapsibleFile
|
||||
file={{ name: "task.toml", contents: workflow.config, lang: "toml" }}
|
||||
/>
|
||||
) : (
|
||||
<p className="text-sm text-navy-600">No configuration found.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
119
apps/arc-web/app/routes/run-graph.tsx
Normal file
119
apps/arc-web/app/routes/run-graph.tsx
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { Link, useParams } from "react-router";
|
||||
import type { BundledLanguage } from "@pierre/diffs";
|
||||
import { CheckCircleIcon, ArrowPathIcon, PauseCircleIcon, XCircleIcon } from "@heroicons/react/24/solid";
|
||||
import { DocumentTextIcon, MapIcon } from "@heroicons/react/24/outline";
|
||||
import { findRun } from "../data/runs";
|
||||
import { workflowData } from "./workflow-detail";
|
||||
import { registerDotLanguage } from "../data/register-dot-language";
|
||||
import { CollapsibleFile } from "../components/collapsible-file";
|
||||
|
||||
export const handle = { wide: true };
|
||||
|
||||
type StageStatus = "completed" | "running" | "pending" | "failed";
|
||||
|
||||
interface Stage {
|
||||
id: string;
|
||||
name: string;
|
||||
status: StageStatus;
|
||||
duration: string;
|
||||
}
|
||||
|
||||
const stages: Stage[] = [
|
||||
{ id: "detect-drift", name: "Detect Drift", status: "completed", duration: "1m 12s" },
|
||||
{ id: "propose-changes", name: "Propose Changes", status: "completed", duration: "2m 34s" },
|
||||
{ id: "review-changes", name: "Review Changes", status: "completed", duration: "0m 45s" },
|
||||
{ id: "apply-changes", name: "Apply Changes", status: "running", duration: "1m 58s" },
|
||||
];
|
||||
|
||||
const statusConfig: Record<StageStatus, { icon: typeof CheckCircleIcon; color: string }> = {
|
||||
completed: { icon: CheckCircleIcon, color: "text-mint" },
|
||||
running: { icon: ArrowPathIcon, color: "text-teal-500" },
|
||||
pending: { icon: PauseCircleIcon, color: "text-navy-600" },
|
||||
failed: { icon: XCircleIcon, color: "text-coral" },
|
||||
};
|
||||
|
||||
export default function RunGraph() {
|
||||
const { id } = useParams();
|
||||
const run = findRun(id ?? "");
|
||||
const workflow = run ? workflowData[run.workflow] : undefined;
|
||||
const [dotReady, setDotReady] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
registerDotLanguage().then(() => {
|
||||
if (!cancelled) setDotReady(true);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex gap-6">
|
||||
<nav className="w-56 shrink-0 space-y-6">
|
||||
<div>
|
||||
<h3 className="px-2 text-xs font-medium uppercase tracking-wider text-navy-600">Stages</h3>
|
||||
<ul className="mt-2 space-y-0.5">
|
||||
{stages.map((stage) => {
|
||||
const config = statusConfig[stage.status];
|
||||
const Icon = config.icon;
|
||||
return (
|
||||
<li key={stage.id}>
|
||||
<Link
|
||||
to={`/runs/${id}/stages/${stage.id}`}
|
||||
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-ice-300 transition-colors hover:bg-white/[0.04] hover:text-white"
|
||||
>
|
||||
<Icon className={`size-4 shrink-0 ${config.color} ${stage.status === "running" ? "animate-spin" : ""}`} />
|
||||
<span className="flex-1 truncate">{stage.name}</span>
|
||||
<span className="font-mono text-xs tabular-nums text-navy-600">{stage.duration}</span>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{workflow && (
|
||||
<div>
|
||||
<h3 className="px-2 text-xs font-medium uppercase tracking-wider text-navy-600">Workflow</h3>
|
||||
<ul className="mt-2 space-y-0.5">
|
||||
<li>
|
||||
<Link
|
||||
to={`/runs/${id}/configuration`}
|
||||
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-ice-300 transition-colors hover:bg-white/[0.04] hover:text-white"
|
||||
>
|
||||
<DocumentTextIcon className="size-4 shrink-0 text-navy-600" />
|
||||
Run Configuration
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
to={`/runs/${id}/graph`}
|
||||
className="flex items-center gap-2 rounded-md bg-white/[0.06] px-2 py-1.5 text-sm text-white transition-colors"
|
||||
>
|
||||
<MapIcon className="size-4 shrink-0 text-navy-600" />
|
||||
Workflow Graph
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
{workflow && dotReady ? (
|
||||
<CollapsibleFile
|
||||
file={{
|
||||
name: workflow.filename,
|
||||
contents: workflow.graph,
|
||||
lang: "dot" as BundledLanguage,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<p className="text-sm text-navy-600">No workflow graph available.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -295,7 +295,7 @@ export default function RunOverview() {
|
|||
<ul className="mt-2 space-y-0.5">
|
||||
<li>
|
||||
<Link
|
||||
to={`/workflows/${run?.workflow}`}
|
||||
to={`/runs/${id}/configuration`}
|
||||
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-ice-300 transition-colors hover:bg-white/[0.04] hover:text-white"
|
||||
>
|
||||
<DocumentTextIcon className="size-4 shrink-0 text-navy-600" />
|
||||
|
|
@ -304,7 +304,7 @@ export default function RunOverview() {
|
|||
</li>
|
||||
<li>
|
||||
<Link
|
||||
to={`/workflows/${run?.workflow}/diagram`}
|
||||
to={`/runs/${id}/graph`}
|
||||
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-ice-300 transition-colors hover:bg-white/[0.04] hover:text-white"
|
||||
>
|
||||
<MapIcon className="size-4 shrink-0 text-navy-600" />
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@ export default function RunStages() {
|
|||
<ul className="mt-2 space-y-0.5">
|
||||
<li>
|
||||
<Link
|
||||
to={`/workflows/${run?.workflow}`}
|
||||
to={`/runs/${id}/configuration`}
|
||||
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-ice-300 transition-colors hover:bg-white/[0.04] hover:text-white"
|
||||
>
|
||||
<DocumentTextIcon className="size-4 shrink-0 text-navy-600" />
|
||||
|
|
@ -201,7 +201,7 @@ export default function RunStages() {
|
|||
</li>
|
||||
<li>
|
||||
<Link
|
||||
to={`/workflows/${run?.workflow}/diagram`}
|
||||
to={`/runs/${id}/graph`}
|
||||
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-ice-300 transition-colors hover:bg-white/[0.04] hover:text-white"
|
||||
>
|
||||
<MapIcon className="size-4 shrink-0 text-navy-600" />
|
||||
|
|
|
|||
|
|
@ -1,50 +1,9 @@
|
|||
import { ChevronRightIcon } from "@heroicons/react/20/solid";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams } from "react-router";
|
||||
import type { BundledLanguage, FileContents } from "@pierre/diffs";
|
||||
import { File } from "@pierre/diffs/react";
|
||||
import type { BundledLanguage } from "@pierre/diffs";
|
||||
import { registerDotLanguage } from "../data/register-dot-language";
|
||||
import { workflowData } from "./workflow-detail";
|
||||
|
||||
function CollapsibleFile({
|
||||
file,
|
||||
defaultOpen = true,
|
||||
}: {
|
||||
file: FileContents;
|
||||
defaultOpen?: boolean;
|
||||
}) {
|
||||
const [open, setOpen] = useState(defaultOpen);
|
||||
|
||||
const lines = file.contents.split("\n");
|
||||
const lineCount = lines.length;
|
||||
const loc = lines.filter((l) => l.trim().length > 0).length;
|
||||
|
||||
return (
|
||||
<div className="rounded-md border border-white/[0.06] bg-navy-800/50 overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
className="flex w-full items-center gap-2 px-4 py-2.5 text-left hover:bg-white/[0.02] transition-colors"
|
||||
>
|
||||
<ChevronRightIcon
|
||||
className={`size-4 text-navy-600 transition-transform duration-150 ${open ? "rotate-90" : ""}`}
|
||||
/>
|
||||
<span className="font-mono text-xs text-navy-600">{file.name}</span>
|
||||
<span className="ml-auto font-mono text-xs text-navy-600/60">
|
||||
{lineCount} lines ({loc} loc)
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div className={open ? "" : "hidden"}>
|
||||
<div className="border-t border-white/[0.06]" />
|
||||
<File
|
||||
file={file}
|
||||
options={{ theme: "pierre-dark", disableFileHeader: true }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import { CollapsibleFile } from "../components/collapsible-file";
|
||||
|
||||
export default function WorkflowDefinition() {
|
||||
const { name } = useParams();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue