108 lines
3.1 KiB
TypeScript
108 lines
3.1 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { useParams, useRouter } from "next/navigation";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { getScenario, scenarioEventsUrl, type ProgressEvent } from "@/lib/api";
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
function ProgressBar({ pct }: { pct: number }) {
|
|
return (
|
|
<div className="w-full bg-muted rounded-full h-3 overflow-hidden">
|
|
<div
|
|
className="bg-primary h-3 rounded-full transition-all duration-500"
|
|
style={{ width: `${pct}%` }}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function ScenarioPage() {
|
|
const params = useParams<{ id: string }>();
|
|
const router = useRouter();
|
|
const id = params.id;
|
|
|
|
const [progress, setProgress] = useState<ProgressEvent | null>(null);
|
|
const [done, setDone] = useState(false);
|
|
|
|
const { data: scenario, refetch } = useQuery({
|
|
queryKey: ["scenario", id],
|
|
queryFn: () => getScenario(id),
|
|
refetchInterval: done ? false : 3000,
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (!id) return;
|
|
|
|
const es = new EventSource(scenarioEventsUrl(id));
|
|
|
|
es.onmessage = (event: MessageEvent<string>) => {
|
|
const data = JSON.parse(event.data) as ProgressEvent;
|
|
setProgress(data);
|
|
if (data.stage === "done") {
|
|
setDone(true);
|
|
es.close();
|
|
void refetch();
|
|
}
|
|
};
|
|
|
|
es.onerror = () => es.close();
|
|
|
|
return () => es.close();
|
|
}, [id, refetch]);
|
|
|
|
const statusColor =
|
|
scenario?.status === "success"
|
|
? "text-green-600"
|
|
: scenario?.status === "failed"
|
|
? "text-red-600"
|
|
: scenario?.status === "running"
|
|
? "text-blue-600"
|
|
: "text-yellow-600";
|
|
|
|
const kpis = scenario?.kpis_json
|
|
? (JSON.parse(scenario.kpis_json) as Record<string, unknown>)
|
|
: null;
|
|
|
|
return (
|
|
<main className="flex-1 container mx-auto px-4 py-8 max-w-3xl">
|
|
<div className="mb-6">
|
|
<Button variant="ghost" size="sm" onClick={() => router.push("/")}>
|
|
← Back
|
|
</Button>
|
|
</div>
|
|
|
|
<h1 className="text-xl font-bold mb-1">
|
|
{scenario?.name ?? "Loading…"}
|
|
</h1>
|
|
<p className={`text-sm font-medium capitalize mb-6 ${statusColor}`}>
|
|
{scenario?.status ?? "—"}
|
|
</p>
|
|
|
|
{(scenario?.status === "queued" || scenario?.status === "running") && (
|
|
<div className="mb-6">
|
|
<div className="flex justify-between text-sm text-muted-foreground mb-2">
|
|
<span>{progress?.stage ?? "waiting…"}</span>
|
|
<span>{progress?.pct ?? 0}%</span>
|
|
</div>
|
|
<ProgressBar pct={progress?.pct ?? 0} />
|
|
</div>
|
|
)}
|
|
|
|
{scenario?.status === "success" && kpis && (
|
|
<div className="border rounded-lg p-6">
|
|
<h2 className="font-semibold mb-4">Result</h2>
|
|
<pre className="text-sm text-muted-foreground bg-muted/50 p-4 rounded overflow-auto">
|
|
{JSON.stringify(kpis, null, 2)}
|
|
</pre>
|
|
</div>
|
|
)}
|
|
|
|
{scenario?.status === "failed" && (
|
|
<div className="border border-red-200 rounded-lg p-6 text-red-600">
|
|
Scenario failed. Check worker logs.
|
|
</div>
|
|
)}
|
|
</main>
|
|
);
|
|
}
|