UI: add job details page and auto-refresh in JobHistory\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
ticker: string;
|
ticker: string;
|
||||||
@@ -29,6 +30,8 @@ export default function JobHistory({ ticker }: Props) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchJobs();
|
fetchJobs();
|
||||||
|
const id = setInterval(fetchJobs, 8000);
|
||||||
|
return () => clearInterval(id);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [ticker]);
|
}, [ticker]);
|
||||||
|
|
||||||
@@ -68,13 +71,7 @@ export default function JobHistory({ ticker }: Props) {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<a href={`/api/jobs/${j.id}`} target="_blank" rel="noreferrer" className="text-sm text-blue-600 hover:underline">API</a>
|
<a href={`/api/jobs/${j.id}`} target="_blank" rel="noreferrer" className="text-sm text-blue-600 hover:underline">API</a>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => navigate(`/jobs/${j.id}`)}
|
||||||
if (selected?.id === j.id) {
|
|
||||||
setSelected(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
fetchDetails(j.id);
|
|
||||||
}}
|
|
||||||
className="text-sm text-gray-700 underline"
|
className="text-sm text-gray-700 underline"
|
||||||
>
|
>
|
||||||
Details
|
Details
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useLoaderData } from "react-router";
|
||||||
|
import Navbar from "../../components/Navbar";
|
||||||
|
|
||||||
|
export const meta = () => [{ title: "Job Detail - AITrader" }];
|
||||||
|
|
||||||
|
export async function loader({ params, request }: { params: { jobId: string }; request: Request }) {
|
||||||
|
const jobId = params.jobId;
|
||||||
|
if (!jobId) return Response.json({ error: "jobId required" }, { status: 400 });
|
||||||
|
const reqUrl = new URL(request.url);
|
||||||
|
const host = request.headers.get("host") || reqUrl.host;
|
||||||
|
const protocol = reqUrl.protocol;
|
||||||
|
const baseUrl = `${protocol}//${host}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${baseUrl}/api/jobs/${jobId}`);
|
||||||
|
const body = res.ok ? await res.json() : null;
|
||||||
|
return Response.json(body);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("/jobs loader error:", err);
|
||||||
|
return Response.json({ error: "internal" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function JobDetail() {
|
||||||
|
const initial = useLoaderData() as any;
|
||||||
|
const [job, setJob] = useState<any>(initial);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
const poll = async () => {
|
||||||
|
try {
|
||||||
|
const id = job?.id || initial?.id;
|
||||||
|
if (!id) return;
|
||||||
|
const res = await fetch(`/api/jobs/${id}`);
|
||||||
|
if (!res.ok) return;
|
||||||
|
const j = await res.json();
|
||||||
|
if (cancelled) return;
|
||||||
|
setJob(j);
|
||||||
|
if (j.state === "completed" || j.state === "failed") return;
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
setTimeout(poll, 3000);
|
||||||
|
};
|
||||||
|
poll();
|
||||||
|
return () => { cancelled = true; };
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-blue-50">
|
||||||
|
<Navbar />
|
||||||
|
<div className="mx-auto max-w-4xl px-6 py-8">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900 mb-4">Job {job?.id}</h1>
|
||||||
|
<div className="bg-white rounded-xl shadow-lg p-6 border border-gray-200">
|
||||||
|
<div className="mb-3 text-sm text-gray-700">State: <strong className="ml-2">{job?.state}</strong></div>
|
||||||
|
{job?.failedReason && (
|
||||||
|
<div className="mb-3 text-red-600">Failed: {job.failedReason}</div>
|
||||||
|
)}
|
||||||
|
<div className="text-sm text-gray-700 mb-3">Data:</div>
|
||||||
|
<pre className="bg-gray-50 p-3 rounded text-xs overflow-x-auto">{JSON.stringify(job?.data || job?.returnValue || job, null, 2)}</pre>
|
||||||
|
|
||||||
|
<div className="mt-4">
|
||||||
|
<a href={`/api/jobs/${job?.id}`} target="_blank" rel="noreferrer" className="text-sm text-blue-600 hover:underline">Open API</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user