jobs: split list into Running & Queued (top) and History (bottom, 70/30)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 15:42:15 +00:00
co-authored by Claude Opus 4.8
parent ba2c2e57ca
commit c277ceead7
@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, type ReactNode } from 'react';
import { useParams, useNavigate } from 'react-router';
import { Search, CheckCircle2, AlertCircle, Clock, Loader2, StopCircle, AlertTriangle, Inbox } from 'lucide-react';
import { WorkspaceLayout } from 'officerdev';
@@ -34,6 +34,12 @@ const StatusIcon = ({ status }: { status: JobSummary['status'] }) => {
}
};
const SectionLabel = ({ children }: { children: ReactNode }) => (
<div className="px-3 py-1.5 text-[10px] font-semibold uppercase tracking-wide text-duck-dark/40 bg-duck-dark/[0.03] border-b border-duck-dark/10 shrink-0">
{children}
</div>
);
// ── Left panel: the jobs list (polls so running/queued statuses stay fresh) ──
const JobsListPanel = () => {
const client = useClient();
@@ -59,10 +65,37 @@ const JobsListPanel = () => {
})
: jobs;
// Active = running first, then the FIFO queue (oldest pending on top — next to run). History = the
// rest (already newest-first from the API).
const active = [
...filtered.filter((j) => j.status === 'running'),
...filtered.filter((j) => j.status === 'pending').sort((a, b) => +new Date(a.createdAt) - +new Date(b.createdAt)),
];
const history = filtered.filter((j) => j.status !== 'running' && j.status !== 'pending');
const renderRow = (job: JobSummary) => (
<button
key={job.id}
onClick={() => navigate(`/jobs/${job.id}`)}
className={`w-full text-left px-4 py-2.5 border-b border-duck-dark/5 transition-colors cursor-pointer flex items-center gap-3 ${
job.id === activeId ? 'bg-duck-teal/10' : 'hover:bg-duck-dark/5'
}`}
>
<StatusIcon status={job.status} />
<div className="flex-1 min-w-0">
<span className="text-sm font-medium text-duck-dark truncate block">{job.taskName}</span>
<div className="flex items-center gap-2 mt-0.5">
<span className="text-xs text-duck-dark/50">{formatDate(job.createdAt)}</span>
{job.error && <span className="text-xs text-red-500 truncate max-w-[180px]">{job.error}</span>}
</div>
</div>
</button>
);
return (
<div className="h-full p-2">
<Card className="h-full flex flex-col overflow-hidden">
<div className="p-3 border-b border-duck-dark/10 flex items-center gap-3">
<div className="p-3 border-b border-duck-dark/10 flex items-center gap-3 shrink-0">
<h2 className="text-sm font-semibold text-duck-dark shrink-0">Jobs</h2>
<div className="relative flex-1">
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-duck-dark/40" />
@@ -76,32 +109,34 @@ const JobsListPanel = () => {
</div>
</div>
<div className="flex-1 overflow-y-auto">
{isLoading && <div className="flex items-center justify-center h-32 text-duck-dark/30 text-sm">Loading</div>}
{!isLoading && filtered.length === 0 && (
<div className="flex items-center justify-center h-32 text-duck-dark/30 text-sm">
{search ? 'No jobs match' : 'No jobs yet'}
</div>
)}
{filtered.map((job) => (
<button
key={job.id}
onClick={() => navigate(`/jobs/${job.id}`)}
className={`w-full text-left px-4 py-3 border-b border-duck-dark/5 transition-colors cursor-pointer flex items-center gap-3 ${
job.id === activeId ? 'bg-duck-teal/10' : 'hover:bg-duck-dark/5'
}`}
>
<StatusIcon status={job.status} />
<div className="flex-1 min-w-0">
<span className="text-sm font-medium text-duck-dark truncate block">{job.taskName}</span>
<div className="flex items-center gap-2 mt-0.5">
<span className="text-xs text-duck-dark/50">{formatDate(job.createdAt)}</span>
{job.error && <span className="text-xs text-red-500 truncate max-w-[180px]">{job.error}</span>}
</div>
{isLoading ? (
<div className="flex-1 flex items-center justify-center text-duck-dark/30 text-sm">Loading</div>
) : (
<div className="flex-1 min-h-0 flex flex-col">
{/* Active (running + queued) — top 70% */}
<div className="flex-[7] min-h-0 flex flex-col border-b border-duck-dark/10">
<SectionLabel>Running &amp; Queued{active.length ? ` · ${active.length}` : ''}</SectionLabel>
<div className="flex-1 overflow-y-auto">
{active.length === 0 ? (
<div className="flex items-center justify-center h-full min-h-16 text-duck-dark/30 text-xs">Nothing running</div>
) : (
active.map(renderRow)
)}
</div>
</button>
))}
</div>
</div>
{/* History — bottom 30% */}
<div className="flex-[3] min-h-0 flex flex-col">
<SectionLabel>History</SectionLabel>
<div className="flex-1 overflow-y-auto">
{history.length === 0 ? (
<div className="flex items-center justify-center h-full min-h-16 text-duck-dark/30 text-xs">{search ? 'No matches' : 'No finished jobs'}</div>
) : (
history.map(renderRow)
)}
</div>
</div>
</div>
)}
</Card>
</div>
);