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 { useParams, useNavigate } from 'react-router';
|
||||||
import { Search, CheckCircle2, AlertCircle, Clock, Loader2, StopCircle, AlertTriangle, Inbox } from 'lucide-react';
|
import { Search, CheckCircle2, AlertCircle, Clock, Loader2, StopCircle, AlertTriangle, Inbox } from 'lucide-react';
|
||||||
import { WorkspaceLayout } from 'officerdev';
|
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) ──
|
// ── Left panel: the jobs list (polls so running/queued statuses stay fresh) ──
|
||||||
const JobsListPanel = () => {
|
const JobsListPanel = () => {
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
@@ -59,10 +65,37 @@ const JobsListPanel = () => {
|
|||||||
})
|
})
|
||||||
: jobs;
|
: 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 (
|
return (
|
||||||
<div className="h-full p-2">
|
<div className="h-full p-2">
|
||||||
<Card className="h-full flex flex-col overflow-hidden">
|
<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>
|
<h2 className="text-sm font-semibold text-duck-dark shrink-0">Jobs</h2>
|
||||||
<div className="relative flex-1">
|
<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" />
|
<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>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto">
|
{isLoading ? (
|
||||||
{isLoading && <div className="flex items-center justify-center h-32 text-duck-dark/30 text-sm">Loading…</div>}
|
<div className="flex-1 flex items-center justify-center 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">
|
<div className="flex-1 min-h-0 flex flex-col">
|
||||||
{search ? 'No jobs match' : 'No jobs yet'}
|
{/* Active (running + queued) — top 70% */}
|
||||||
</div>
|
<div className="flex-[7] min-h-0 flex flex-col border-b border-duck-dark/10">
|
||||||
)}
|
<SectionLabel>Running & Queued{active.length ? ` · ${active.length}` : ''}</SectionLabel>
|
||||||
{filtered.map((job) => (
|
<div className="flex-1 overflow-y-auto">
|
||||||
<button
|
{active.length === 0 ? (
|
||||||
key={job.id}
|
<div className="flex items-center justify-center h-full min-h-16 text-duck-dark/30 text-xs">Nothing running</div>
|
||||||
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 ${
|
active.map(renderRow)
|
||||||
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>
|
</div>
|
||||||
</button>
|
</div>
|
||||||
))}
|
{/* History — bottom 30% */}
|
||||||
</div>
|
<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>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user