mirror of
https://github.com/open-gitagent/langship.sh.git
synced 2026-08-03 07:21:04 +02:00
216 lines
6.6 KiB
TypeScript
216 lines
6.6 KiB
TypeScript
"use client";
|
|
|
|
import { Suspense, useEffect, useState } from "react";
|
|
import Link from "next/link";
|
|
import { useRouter, useSearchParams } from "next/navigation";
|
|
import { Play, Save, Trash2 } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { api, type StoredFlow } from "@/lib/api";
|
|
import { formatDate } from "@/lib/utils";
|
|
|
|
export default function FlowDetailPage() {
|
|
return (
|
|
<Suspense fallback={<div className="text-sm text-muted-foreground">Loading…</div>}>
|
|
<FlowDetail />
|
|
</Suspense>
|
|
);
|
|
}
|
|
|
|
function FlowDetail() {
|
|
const router = useRouter();
|
|
const params = useSearchParams();
|
|
const id = params.get("id") ?? "";
|
|
|
|
const [flow, setFlow] = useState<StoredFlow | null>(null);
|
|
const [name, setName] = useState("");
|
|
const [definition, setDefinition] = useState("");
|
|
const [input, setInput] = useState("[{}]");
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [saving, setSaving] = useState(false);
|
|
const [running, setRunning] = useState(false);
|
|
const [lastExecId, setLastExecId] = useState<string | null>(null);
|
|
|
|
async function load() {
|
|
if (!id) return;
|
|
try {
|
|
const f = await api.getFlow(id);
|
|
setFlow(f);
|
|
setName(f.name);
|
|
setDefinition(JSON.stringify(f.definition, null, 2));
|
|
setError(null);
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : "load failed");
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
load();
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [id]);
|
|
|
|
async function onSave() {
|
|
setSaving(true);
|
|
setError(null);
|
|
try {
|
|
const parsed = JSON.parse(definition);
|
|
await api.updateFlow(id, { name, definition: parsed });
|
|
await load();
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : "save failed");
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
}
|
|
|
|
async function onRun() {
|
|
setRunning(true);
|
|
setError(null);
|
|
try {
|
|
let parsedInput: unknown[] | undefined;
|
|
if (input.trim()) {
|
|
const v = JSON.parse(input);
|
|
if (!Array.isArray(v)) throw new Error("Input must be a JSON array");
|
|
parsedInput = v;
|
|
}
|
|
const res = await api.executeWorkflow({ workflow_id: id, input: parsedInput });
|
|
setLastExecId(res.execution_id);
|
|
router.push(`/executions/view/?id=${encodeURIComponent(res.execution_id)}`);
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : "run failed");
|
|
} finally {
|
|
setRunning(false);
|
|
}
|
|
}
|
|
|
|
async function onDelete() {
|
|
if (!confirm("Delete this flow? This cannot be undone.")) return;
|
|
try {
|
|
await api.deleteFlow(id);
|
|
router.push("/");
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : "delete failed");
|
|
}
|
|
}
|
|
|
|
if (!id) {
|
|
return (
|
|
<div className="text-sm text-muted-foreground">
|
|
Missing <code>id</code> query param.
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{flow && (
|
|
<div className="flex items-center justify-end gap-2 text-xs text-muted-foreground">
|
|
<span className="font-mono">{flow.id}</span>
|
|
<span>·</span>
|
|
<span>updated {formatDate(flow.updatedAt)}</span>
|
|
<Badge variant="secondary">{flow.nodeCount} nodes</Badge>
|
|
</div>
|
|
)}
|
|
|
|
<div>
|
|
<h1 className="text-3xl font-semibold tracking-tight">
|
|
{name || "Untitled flow"}
|
|
</h1>
|
|
<p className="mt-1 text-sm text-muted-foreground">
|
|
Edit, save, and run this workflow against the orchestrator.
|
|
</p>
|
|
</div>
|
|
|
|
{error && (
|
|
<Card className="border-destructive/40">
|
|
<CardContent className="pt-6 text-sm text-destructive">{error}</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
<div className="grid gap-6 lg:grid-cols-3">
|
|
<Card className="lg:col-span-2">
|
|
<CardHeader>
|
|
<CardTitle>Definition</CardTitle>
|
|
<CardDescription>n8n-format JSON</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="name">Name</Label>
|
|
<Input id="name" value={name} onChange={(e) => setName(e.target.value)} />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="def">Workflow JSON</Label>
|
|
<Textarea
|
|
id="def"
|
|
rows={24}
|
|
value={definition}
|
|
onChange={(e) => setDefinition(e.target.value)}
|
|
spellCheck={false}
|
|
className="text-xs"
|
|
/>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<Button variant="outline" onClick={onDelete}>
|
|
<Trash2 />
|
|
Delete
|
|
</Button>
|
|
<Button onClick={onSave} disabled={saving}>
|
|
<Save />
|
|
{saving ? "Saving…" : "Save"}
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Run</CardTitle>
|
|
<CardDescription>Submit to the orchestrator</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="input">Trigger input (JSON array)</Label>
|
|
<Textarea
|
|
id="input"
|
|
rows={8}
|
|
value={input}
|
|
onChange={(e) => setInput(e.target.value)}
|
|
spellCheck={false}
|
|
className="text-xs"
|
|
/>
|
|
<p className="text-xs text-muted-foreground">
|
|
e.g. <code className="font-mono">[{`{"foo":"bar"}`}]</code>
|
|
</p>
|
|
</div>
|
|
<Button className="w-full" onClick={onRun} disabled={running}>
|
|
<Play />
|
|
{running ? "Submitting…" : "Run flow"}
|
|
</Button>
|
|
{lastExecId && (
|
|
<div className="rounded-md border bg-muted/30 p-3 text-xs">
|
|
<div className="font-medium">Last execution</div>
|
|
<Link
|
|
href={`/executions/view/?id=${encodeURIComponent(lastExecId)}`}
|
|
className="font-mono text-primary hover:underline"
|
|
>
|
|
{lastExecId}
|
|
</Link>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|