Files
AITrader/app/components/JobHistory.tsx
T

117 lines
4.1 KiB
TypeScript

import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router";
interface Props {
ticker: string;
}
export default function JobHistory({ ticker }: Props) {
const [jobs, setJobs] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [selected, setSelected] = useState<any | null>(null);
const navigate = useNavigate();
const fetchJobs = async () => {
setLoading(true);
try {
const res = await fetch(`/api/jobs?ticker=${encodeURIComponent(ticker)}`);
if (!res.ok) {
setJobs([]);
} else {
const data = await res.json();
setJobs(data.jobs || []);
}
} catch (e) {
console.warn("Failed to fetch jobs:", e);
setJobs([]);
} finally {
setLoading(false);
}
};
const cancel = async (jobId: string) => {
try {
const res = await fetch(`/api/jobs/${jobId}/cancel`, { method: "POST" });
const data = await res.json();
if (res.ok) {
// Refresh list
fetchJobs();
return data.cancelled === true;
}
} catch (e) {
console.warn("Cancel failed:", e);
}
return false;
};
useEffect(() => {
fetchJobs();
const id = setInterval(fetchJobs, 8000);
return () => clearInterval(id);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ticker]);
const fetchDetails = async (jobId: string) => {
try {
const res = await fetch(`/api/jobs/${jobId}`);
if (!res.ok) {
setSelected({ id: jobId, error: true });
return;
}
const data = await res.json();
setSelected(data);
} catch (e) {
console.warn("Failed to fetch job details:", e);
setSelected({ id: jobId, error: true });
}
};
return (
<div className="mt-4">
<h3 className="text-sm font-semibold text-gray-800 mb-2">Job History</h3>
<div className="flex items-center gap-2 mb-2">
<button onClick={fetchJobs} className="text-sm text-blue-600 hover:underline">Refresh</button>
<span className="text-xs text-gray-500">{loading ? "Loading..." : `${jobs.length} jobs`}</span>
</div>
<div className="space-y-2">
{loading ? (
<div className="space-y-2">
<div className="h-10 bg-gray-100 rounded animate-pulse" />
<div className="h-10 bg-gray-100 rounded animate-pulse" />
<div className="h-10 bg-gray-100 rounded animate-pulse" />
</div>
) : jobs.length === 0 ? (
<p className="text-gray-500">No recent jobs for {ticker}</p>
) : (
jobs.map((j: any) => (
<div key={j.id} className="p-3 border border-gray-200 rounded bg-gray-50 text-gray-900">
<div className="flex items-center justify-between">
<div className="text-sm">
<div className="font-medium">Job: <span className="text-blue-600">{j.id}</span></div>
<div className="text-xs text-gray-600">State: <strong className={`px-2 py-0.5 rounded text-xs ${j.state === 'completed' ? 'bg-green-100 text-green-800' : j.state === 'failed' ? 'bg-red-100 text-red-800' : 'bg-yellow-100 text-yellow-800'}`}>{j.state}</strong></div>
</div>
<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={`/jobs/${j.id}`} className="text-sm text-gray-700 underline">Details</a>
{(j.state === 'waiting' || j.state === 'queued') && (
<button
onClick={() => cancel(j.id)}
className="text-sm text-red-600 hover:underline"
>
Cancel
</button>
)}
</div>
</div>
{selected?.id === j.id && (
<pre className="mt-2 text-xs bg-white p-2 rounded overflow-x-auto text-gray-800">{JSON.stringify(selected, null, 2)}</pre>
)}
</div>
))
)}
</div>
</div>
);
}