/ frontend / src / components / overview / HistoryCard.tsx
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  }