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:
@@ -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 & 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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user