style: api client design overhaul
This commit is contained in:
@@ -50,45 +50,41 @@ export function ResultsGallery({ onNewJob }: ResultsGalleryProps) {
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<header className="mb-8">
|
||||
<span className="text-caption-uppercase text-primary uppercase tracking-widest mb-2 block">Gallery</span>
|
||||
<h2 className="text-display-sm font-serif text-ink mb-2">Results Gallery</h2>
|
||||
<p className="text-body-lg text-body max-w-2xl">
|
||||
Browse and manage scanned batch results. Analyze extraction data, verify outputs, and export structured results.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="flex flex-wrap items-center justify-between gap-4 p-6 bg-surface-soft border-2 border-surface-cream-strong">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative">
|
||||
<span className="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-muted text-sm">search</span>
|
||||
<input
|
||||
type="text"
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
placeholder="Search batches..."
|
||||
className="pl-10 pr-4 py-2 bg-surface-card border-2 border-surface-cream-strong focus:border-primary focus:ring-0 transition-colors text-body-md w-64"
|
||||
/>
|
||||
</div>
|
||||
<select className="px-4 py-2 bg-surface-card border-2 border-surface-cream-strong focus:border-primary focus:ring-0 transition-colors text-body-md">
|
||||
<option>All Templates</option>
|
||||
{Array.from(new Set(runs.map((r) => r.templateName))).map((name) => (
|
||||
<option key={name}>{name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={onNewJob}
|
||||
className="bg-primary text-on-primary px-5 py-2 text-button border-2 border-primary hover:bg-primary-active active:scale-[0.98] transition-all"
|
||||
>
|
||||
New Job
|
||||
</button>
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="text-display-sm font-serif text-ink mb-1">Results Gallery</h2>
|
||||
<p className="text-body-md text-muted max-w-2xl">Browse grading runs, inspect extracted data, and export results.</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={onNewJob}
|
||||
className="px-4 py-2 bg-primary text-on-primary rounded-md text-button font-medium hover:bg-primary-active active:scale-[0.97] transition-all flex items-center gap-2 shadow-sm"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[18px]">add</span>
|
||||
New Job
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-wrap items-center gap-3 p-4 bg-surface-card border border-hairline rounded-lg">
|
||||
<div className="relative">
|
||||
<span className="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-muted text-sm">search</span>
|
||||
<input
|
||||
type="text"
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
placeholder="Search batches..."
|
||||
className="pl-9 pr-3 py-2 bg-canvas border border-hairline rounded-md text-body-sm focus:border-primary focus:ring-1 focus:ring-primary/20 outline-none transition-all w-64"
|
||||
/>
|
||||
</div>
|
||||
<select className="px-3 py-2 bg-canvas border border-hairline rounded-md text-body-sm focus:border-primary focus:ring-1 focus:ring-primary/20 outline-none">
|
||||
<option>All Templates</option>
|
||||
{Array.from(new Set(runs.map((r) => r.templateName))).map((name) => (
|
||||
<option key={name}>{name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
{filteredRuns.map((run) => (
|
||||
<BatchCard
|
||||
key={run.id}
|
||||
@@ -102,17 +98,15 @@ export function ResultsGallery({ onNewJob }: ResultsGalleryProps) {
|
||||
/>
|
||||
))}
|
||||
|
||||
{loadingServer && (
|
||||
<div className="text-caption text-muted text-center py-8">Loading server history...</div>
|
||||
)}
|
||||
{loadingServer && <div className="text-caption text-muted text-center py-8">Loading server history...</div>}
|
||||
|
||||
{filteredRuns.length === 0 && !loadingServer && (
|
||||
<div className="text-center py-12 bg-canvas border-2 border-dashed border-hairline rounded-md">
|
||||
<div className="text-center py-12 rounded-lg border border-dashed border-hairline bg-canvas">
|
||||
<span className="material-symbols-outlined text-4xl text-muted mb-4 block">analytics</span>
|
||||
<p className="text-body-md text-muted mb-4">No grading runs yet.</p>
|
||||
<button
|
||||
onClick={onNewJob}
|
||||
className="bg-primary text-on-primary px-6 py-2 text-button border-2 border-primary hover:bg-primary-active active:scale-[0.98] transition-all"
|
||||
className="px-5 py-2 bg-primary text-on-primary rounded-md text-button font-medium hover:bg-primary-active active:scale-[0.97] transition-all"
|
||||
>
|
||||
Start a Job
|
||||
</button>
|
||||
@@ -121,49 +115,39 @@ export function ResultsGallery({ onNewJob }: ResultsGalleryProps) {
|
||||
</div>
|
||||
|
||||
{serverJobs.length > 0 && (
|
||||
<div className="mt-12 pt-8 border-t-2 border-surface-cream-strong">
|
||||
<div className="mt-8 pt-6 border-t border-hairline">
|
||||
<h3 className="text-title-md font-medium mb-4">Server Job History</h3>
|
||||
<div className="overflow-x-auto custom-scrollbar bg-surface-card border-2 border-surface-cream-strong">
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-canvas border-b-2 border-surface-cream-strong">
|
||||
<th className="p-4 text-[10px] font-mono uppercase text-muted">Job ID</th>
|
||||
<th className="p-4 text-[10px] font-mono uppercase text-muted">Status</th>
|
||||
<th className="p-4 text-[10px] font-mono uppercase text-muted">Created</th>
|
||||
<th className="p-4 text-[10px] font-mono uppercase text-muted text-right">Actions</th>
|
||||
<Table>
|
||||
<thead>
|
||||
<tr className="bg-canvas border-b border-hairline">
|
||||
<th className="p-3 text-[10px] font-mono uppercase text-muted">Job ID</th>
|
||||
<th className="p-3 text-[10px] font-mono uppercase text-muted">Status</th>
|
||||
<th className="p-3 text-[10px] font-mono uppercase text-muted">Created</th>
|
||||
<th className="p-3 text-[10px] font-mono uppercase text-muted text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="font-mono text-code">
|
||||
{serverJobs.slice(0, 10).map((job) => (
|
||||
<tr key={job.job_id} className="border-b border-hairline hover:bg-surface-soft transition-colors">
|
||||
<td className="p-3">{job.job_id.slice(0, 16)}...</td>
|
||||
<td className="p-3">
|
||||
<StatusBadge status={job.status} />
|
||||
</td>
|
||||
<td className="p-3 text-muted-soft">{job.created_at || "—"}</td>
|
||||
<td className="p-3 text-right">
|
||||
<a
|
||||
href={getOutputUrl(serverUrl, job.job_id, "result.json")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="material-symbols-outlined text-lg text-muted hover:text-primary transition-colors"
|
||||
>
|
||||
open_in_new
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="font-mono text-code">
|
||||
{serverJobs.slice(0, 10).map((job) => (
|
||||
<tr key={job.job_id} className="border-b border-hairline hover:bg-surface-soft transition-colors">
|
||||
<td className="p-4">{job.job_id.slice(0, 16)}...</td>
|
||||
<td className="p-4">
|
||||
<span className={`text-caption font-medium uppercase ${
|
||||
job.status === "completed"
|
||||
? "text-success"
|
||||
: job.status === "failed"
|
||||
? "text-error"
|
||||
: "text-warning"
|
||||
}`}>
|
||||
{job.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="p-4 text-muted-soft">{job.created_at || "—"}</td>
|
||||
<td className="p-4 text-right">
|
||||
<a
|
||||
href={getOutputUrl(serverUrl, job.job_id, "result.json")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="material-symbols-outlined text-lg text-muted hover:text-primary transition-colors"
|
||||
>
|
||||
open_in_new
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -193,33 +177,28 @@ function BatchCard({
|
||||
|
||||
const completedCount = run.jobs.filter((j) => j.status === "completed").length
|
||||
const accuracy = overallStatus === "success" ? 99.2 : overallStatus === "partial-error" ? 84.5 : null
|
||||
const speed = "0.4s"
|
||||
|
||||
const statusConfig = {
|
||||
success: { label: "Success", bg: "bg-success/15", text: "text-success", border: "border-success", dot: "bg-success" },
|
||||
"partial-error": { label: "Partial Error", bg: "bg-warning/15", text: "text-warning", border: "border-warning", dot: "bg-warning" },
|
||||
running: { label: "Running", bg: "bg-warning/15", text: "text-warning", border: "border-warning", dot: "bg-warning animate-pulse" },
|
||||
success: { label: "Success", bg: "bg-success/10", text: "text-success", border: "border-success", dot: "bg-success" },
|
||||
"partial-error": { label: "Partial Error", bg: "bg-warning/10", text: "text-warning", border: "border-warning", dot: "bg-warning" },
|
||||
running: { label: "Running", bg: "bg-warning/10", text: "text-warning", border: "border-warning", dot: "bg-warning animate-pulse" },
|
||||
idle: { label: "Idle", bg: "bg-muted/10", text: "text-muted", border: "border-muted", dot: "bg-muted" },
|
||||
}[overallStatus]
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`bg-surface-card border-2 shadow-sm overflow-hidden transition-all ${
|
||||
active ? "border-primary" : "border-surface-cream-strong"
|
||||
}`}
|
||||
>
|
||||
<div className={`bg-surface-card border rounded-lg overflow-hidden transition-all ${active ? "border-primary" : "border-hairline"}`}>
|
||||
<div
|
||||
className={`flex items-center justify-between p-6 border-l-4 ${statusConfig.border} cursor-pointer hover:bg-surface-soft transition-colors`}
|
||||
className={`flex items-center justify-between p-4 border-l-[3px] ${statusConfig.border} cursor-pointer hover:bg-surface-soft transition-colors`}
|
||||
onClick={onActivate}
|
||||
>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<h3 className="text-title-lg font-medium text-ink">{run.templateName}</h3>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="text-title-md font-medium text-ink truncate">{run.templateName}</h3>
|
||||
<span className={`text-[10px] font-mono px-2 py-0.5 rounded uppercase ${statusConfig.bg} ${statusConfig.text} border border-current`}>
|
||||
{statusConfig.label}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-6 text-muted text-sm font-medium">
|
||||
<div className="flex gap-4 text-muted text-sm">
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="material-symbols-outlined text-base">calendar_today</span>
|
||||
{new Date(run.createdAt).toLocaleDateString()}
|
||||
@@ -231,23 +210,17 @@ function BatchCard({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-8 mr-6">
|
||||
<div className="flex items-center gap-6 mr-4 shrink-0">
|
||||
<div className="text-center">
|
||||
<p className="text-[10px] font-mono uppercase text-muted mb-1">Progress</p>
|
||||
<p className="text-title-md font-medium text-ink">
|
||||
{completedCount}/{run.jobs.length || run.scannedPaths.length}
|
||||
</p>
|
||||
<p className="text-[10px] font-mono uppercase text-muted mb-0.5">Progress</p>
|
||||
<p className="text-title-sm font-medium text-ink">{completedCount}/{run.jobs.length || run.scannedPaths.length}</p>
|
||||
</div>
|
||||
{accuracy !== null && (
|
||||
<div className="text-center">
|
||||
<p className="text-[10px] font-mono uppercase text-muted mb-1">Accuracy</p>
|
||||
<p className={`text-title-md font-medium ${statusConfig.text}`}>{accuracy.toFixed(1)}%</p>
|
||||
<p className="text-[10px] font-mono uppercase text-muted mb-0.5">Accuracy</p>
|
||||
<p className={`text-title-sm font-medium ${statusConfig.text}`}>{accuracy.toFixed(1)}%</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="text-center">
|
||||
<p className="text-[10px] font-mono uppercase text-muted mb-1">Speed</p>
|
||||
<p className="text-title-md font-medium text-ink">{speed}<span className="text-sm text-muted">/sheet</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@@ -255,70 +228,62 @@ function BatchCard({
|
||||
e.stopPropagation()
|
||||
onToggle()
|
||||
}}
|
||||
className="p-2 text-muted hover:text-ink hover:bg-surface-cream-strong transition-colors"
|
||||
className="p-1.5 text-muted hover:text-ink hover:bg-surface-cream-strong rounded-md transition-colors shrink-0"
|
||||
>
|
||||
<span className={`material-symbols-outlined transition-transform ${expanded ? "rotate-180" : ""}`}>keyboard_arrow_down</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{expanded && (
|
||||
<div className="border-t-2 border-surface-cream-strong bg-canvas animate-fade-in">
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-end mb-6">
|
||||
<div>
|
||||
<h4 className="text-caption-uppercase text-muted uppercase tracking-wider mb-4">Sheet Inspection</h4>
|
||||
<div className="flex gap-4">
|
||||
<button className="bg-primary text-on-primary px-5 py-2 border-2 border-primary text-button hover:bg-primary-active active:scale-[0.98] transition-all flex items-center gap-2">
|
||||
<span className="material-symbols-outlined text-base">download</span>
|
||||
Export CSV
|
||||
</button>
|
||||
<button className="text-primary border-2 border-primary px-5 py-2 text-button hover:bg-surface-card active:scale-[0.98] transition-all flex items-center gap-2">
|
||||
<span className="material-symbols-outlined text-base">picture_as_pdf</span>
|
||||
Export PDF Report
|
||||
</button>
|
||||
</div>
|
||||
<div className="border-t border-hairline bg-canvas animate-fade-in">
|
||||
<div className="p-4 space-y-5">
|
||||
<div className="flex justify-between items-end">
|
||||
<div className="flex gap-3">
|
||||
<button className="px-4 py-2 bg-primary text-on-primary rounded-md text-button font-medium hover:bg-primary-active active:scale-[0.97] transition-all flex items-center gap-2 shadow-sm">
|
||||
<span className="material-symbols-outlined text-base">download</span>
|
||||
Export CSV
|
||||
</button>
|
||||
<button className="px-4 py-2 border border-primary text-primary rounded-md text-button font-medium hover:bg-primary/5 active:scale-[0.98] transition-all flex items-center gap-2">
|
||||
<span className="material-symbols-outlined text-base">picture_as_pdf</span>
|
||||
Export PDF Report
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={onRemove}
|
||||
className="p-2 text-muted hover:text-error hover:bg-error/5 rounded transition-colors"
|
||||
className="p-1.5 text-muted hover:text-error hover:bg-error/5 rounded-md transition-colors"
|
||||
title="Remove run"
|
||||
>
|
||||
<span className="material-symbols-outlined">delete</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 mb-8">
|
||||
{run.jobs.slice(0, 8).map((job) => (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-3">
|
||||
{run.jobs.slice(0, 12).map((job) => (
|
||||
<JobThumbnail key={job.job_id} job={job} serverUrl={serverUrl} />
|
||||
))}
|
||||
{run.jobs.length > 8 && (
|
||||
<button
|
||||
onClick={() => {}}
|
||||
className="bg-surface-soft border-2 border-dashed border-hairline flex flex-col items-center justify-center p-4 hover:bg-surface-card transition-all"
|
||||
>
|
||||
<span className="material-symbols-outlined text-2xl text-muted mb-1">more_horiz</span>
|
||||
<span className="text-caption text-muted">+{run.jobs.length - 8} more</span>
|
||||
{run.jobs.length > 12 && (
|
||||
<button className="bg-surface-soft border border-dashed border-hairline rounded-lg flex flex-col items-center justify-center p-3 hover:bg-surface-card transition-all">
|
||||
<span className="material-symbols-outlined text-2xl text-muted">more_horiz</span>
|
||||
<span className="text-caption text-muted">+{run.jobs.length - 12} more</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto custom-scrollbar bg-surface-card border-2 border-surface-cream-strong">
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-canvas border-b-2 border-surface-cream-strong">
|
||||
<th className="p-4 text-[10px] font-mono uppercase text-muted">Sheet ID</th>
|
||||
<th className="p-4 text-[10px] font-mono uppercase text-muted">Status</th>
|
||||
<th className="p-4 text-[10px] font-mono uppercase text-muted">Answers</th>
|
||||
<th className="p-4 text-[10px] font-mono uppercase text-muted text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="font-mono text-code">
|
||||
{run.jobs.map((job) => (
|
||||
<JobRow key={job.job_id} job={job} serverUrl={serverUrl} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<Table>
|
||||
<thead>
|
||||
<tr className="bg-canvas border-b border-hairline">
|
||||
<th className="p-3 text-[10px] font-mono uppercase text-muted">Sheet ID</th>
|
||||
<th className="p-3 text-[10px] font-mono uppercase text-muted">Status</th>
|
||||
<th className="p-3 text-[10px] font-mono uppercase text-muted">Answers</th>
|
||||
<th className="p-3 text-[10px] font-mono uppercase text-muted text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="font-mono text-code">
|
||||
{run.jobs.map((job) => (
|
||||
<JobRow key={job.job_id} job={job} serverUrl={serverUrl} />
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -337,10 +302,10 @@ function JobThumbnail({ job, serverUrl }: { job: JobStatusResponse; serverUrl: s
|
||||
const answers = extracted ? Object.keys(extracted).length : 0
|
||||
|
||||
return (
|
||||
<div className="group relative bg-surface-card border-2 border-surface-cream-strong p-2 cursor-pointer hover:border-primary transition-colors">
|
||||
<div className="aspect-[3/4] bg-canvas mb-2 overflow-hidden relative flex items-center justify-center">
|
||||
<div className="group relative bg-surface-card border border-hairline rounded-lg p-2 cursor-pointer hover:border-primary transition-colors">
|
||||
<div className="aspect-[3/4] bg-canvas mb-2 rounded overflow-hidden relative flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<span className="material-symbols-outlined text-3xl text-muted">description</span>
|
||||
<span className="material-symbols-outlined text-2xl text-muted">description</span>
|
||||
<p className="text-[10px] font-mono text-muted mt-1">{answers} answers</p>
|
||||
</div>
|
||||
<div className="absolute inset-0 bg-primary/10 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
|
||||
@@ -348,14 +313,14 @@ function JobThumbnail({ job, serverUrl }: { job: JobStatusResponse; serverUrl: s
|
||||
href={job.output_files?.[0] ? getOutputUrl(serverUrl, job.job_id, job.output_files[0]) : "#"}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="bg-surface-card p-2 material-symbols-outlined text-primary"
|
||||
className="bg-surface-card p-1.5 material-symbols-outlined text-primary rounded"
|
||||
>
|
||||
visibility
|
||||
visibility
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[10px] font-mono text-center truncate">{job.job_id.slice(0, 12)}...</p>
|
||||
<span className={`absolute top-3 right-3 w-2 h-2 rounded-full ${statusColor}`} />
|
||||
<p className="text-[10px] font-mono text-center truncate">{job.job_id.slice(0, 10)}...</p>
|
||||
<span className={`absolute top-2 right-2 w-2 h-2 rounded-full ${statusColor}`} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -365,27 +330,14 @@ function JobRow({ job, serverUrl }: { job: JobStatusResponse; serverUrl: string
|
||||
const extracted = resultData?.ExtractedData as Record<string, unknown> | undefined
|
||||
const answerCount = extracted ? Object.keys(extracted).length : 0
|
||||
|
||||
const statusColor =
|
||||
job.status === "completed" ? "text-success"
|
||||
: job.status === "failed" ? "text-error"
|
||||
: "text-warning"
|
||||
|
||||
const statusDot =
|
||||
job.status === "completed" ? "bg-success"
|
||||
: job.status === "failed" ? "bg-error"
|
||||
: "bg-warning animate-pulse"
|
||||
|
||||
return (
|
||||
<tr className="border-b border-hairline hover:bg-surface-soft transition-colors">
|
||||
<td className="p-4">{job.job_id.slice(0, 20)}...</td>
|
||||
<td className="p-4">
|
||||
<span className={`flex items-center gap-2 ${statusColor}`}>
|
||||
<span className={`w-2 h-2 rounded-full ${statusDot}`} />
|
||||
<span className="text-caption font-medium uppercase">{job.status}</span>
|
||||
</span>
|
||||
<td className="p-3">{job.job_id.slice(0, 18)}...</td>
|
||||
<td className="p-3">
|
||||
<StatusBadge status={job.status} />
|
||||
</td>
|
||||
<td className="p-4 text-muted-soft">{answerCount} extracted</td>
|
||||
<td className="p-4 text-right">
|
||||
<td className="p-3 text-muted-soft">{answerCount} extracted</td>
|
||||
<td className="p-3 text-right">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
{job.output_files?.map((file) => (
|
||||
<a
|
||||
@@ -407,3 +359,25 @@ function JobRow({ job, serverUrl }: { job: JobStatusResponse; serverUrl: string
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
function StatusBadge({ status }: { status: string }) {
|
||||
const config =
|
||||
status === "completed" ? { color: "text-success", dot: "bg-success" }
|
||||
: status === "failed" ? { color: "text-error", dot: "bg-error" }
|
||||
: { color: "text-warning", dot: "bg-warning animate-pulse" }
|
||||
|
||||
return (
|
||||
<span className={`flex items-center gap-1.5 ${config.color}`}>
|
||||
<span className={`w-2 h-2 rounded-full ${config.dot}`} />
|
||||
<span className="text-caption font-medium uppercase">{status}</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function Table({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="overflow-x-auto custom-scrollbar bg-surface-card border border-hairline rounded-lg">
|
||||
<table className="w-full text-left border-collapse">{children}</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user