HistoryCard.tsx
1 import { useHistory } from '@/hooks/useDashboard' 2 import { StatusBadge } from '@/components/ui' 3 import { formatTimeAgo, formatDuration } from '@/lib/utils' 4 5 const INCOMPLETE = new Set(['running', 'queued', 'pending', 'unknown']) 6 const HISTORY_LIMIT = 25 7 8 export function HistoryCard() { 9 const { data, isLoading, isError } = useHistory() 10 11 const completed = (data?.jobs ?? data?.history ?? []) 12 .filter(j => !INCOMPLETE.has((j.result ?? j.status ?? 'unknown').toLowerCase())) 13 .sort((a, b) => { 14 const ta = a.finished_at ?? a.completed_at ?? a.started_at ?? '' 15 const tb = b.finished_at ?? b.completed_at ?? b.started_at ?? '' 16 return tb.localeCompare(ta) 17 }) 18 .slice(0, HISTORY_LIMIT) 19 20 return ( 21 <section 22 className="rounded-lg overflow-hidden" 23 style={{ background: 'var(--bg-secondary)', border: '1px solid var(--border-color)' }} 24 > 25 <h2 26 className="px-6 py-4 text-base font-semibold" 27 style={{ background: 'var(--bg-tertiary)', borderBottom: '1px solid var(--border-color)', color: 'var(--text-primary)' }} 28 > 29 Recent Runs 30 </h2> 31 <div style={{ maxHeight: '400px', overflowY: 'auto' }}> 32 {isLoading && ( 33 <p className="text-center py-8" style={{ color: 'var(--text-muted)' }}>Loading...</p> 34 )} 35 {isError && ( 36 <p className="text-center py-8" style={{ color: 'var(--text-muted)' }}>Failed to load history</p> 37 )} 38 {!isLoading && !isError && ( 39 <table className="w-full border-collapse text-sm"> 40 <thead> 41 <tr> 42 {['Repo', 'Branch', 'Job', 'Result', 'Duration', 'Finished'].map(h => ( 43 <th 44 key={h} 45 className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider" 46 style={{ color: 'var(--text-secondary)', borderBottom: '1px solid var(--border-color)' }} 47 > 48 {h} 49 </th> 50 ))} 51 </tr> 52 </thead> 53 <tbody> 54 {completed.length === 0 ? ( 55 <tr> 56 <td colSpan={6} className="px-4 py-8 text-center" style={{ color: 'var(--text-muted)' }}> 57 No completed runs 58 </td> 59 </tr> 60 ) : ( 61 completed.map((job, i) => { 62 const finishedAt = job.finished_at ?? job.completed_at ?? job.timestamp 63 return ( 64 <tr 65 key={i} 66 className="transition-colors" 67 style={{ borderBottom: '1px solid var(--border-color)' }} 68 onMouseEnter={e => (e.currentTarget.style.background = 'var(--bg-tertiary)')} 69 onMouseLeave={e => (e.currentTarget.style.background = '')} 70 > 71 <td className="px-4 py-3 font-medium" style={{ color: 'var(--text-primary)' }}> 72 {job.repo ?? job.repository ?? '-'} 73 </td> 74 <td className="px-4 py-3"> 75 <span 76 className="font-mono text-xs px-2 py-0.5 rounded" 77 style={{ 78 background: 'var(--bg-tertiary)', 79 color: 'var(--text-primary)', 80 border: '1px solid var(--border-color)', 81 }} 82 > 83 {job.branch ?? 'main'} 84 </span> 85 </td> 86 <td className="px-4 py-3" style={{ color: 'var(--text-primary)' }}> 87 {job.job ?? job.name ?? '-'} 88 </td> 89 <td className="px-4 py-3"> 90 <StatusBadge status={job.result ?? job.status ?? 'unknown'} /> 91 </td> 92 <td className="px-4 py-3 font-mono text-xs" style={{ color: 'var(--text-secondary)' }}> 93 {formatDuration(job.duration)} 94 </td> 95 <td className="px-4 py-3 text-xs" style={{ color: 'var(--text-secondary)' }}> 96 {formatTimeAgo(finishedAt)} 97 </td> 98 </tr> 99 ) 100 }) 101 )} 102 </tbody> 103 </table> 104 )} 105 </div> 106 </section> 107 ) 108 }