import { Tooltip } from 'antd' import { useNavigate } from 'react-router-dom' import type { CampaignTimelineEntry } from '../api' import { colors, fontSizes, statusColors, statusLabels } from '../tokens' import { fmtTick } from './WindowTimeline' import { shortDateTime } from '../utils/date' // Expanded-row view of a campaign: one lane per scenario under a shared window // axis, each child Run a status-coloured node. For still-active campaigns a // dashed line marks how far the window has progressed (nowOffsetSeconds). interface CampaignRunTimelineProps { windowSeconds: number entries: CampaignTimelineEntry[] nowOffsetSeconds?: number | null } const TICK_COUNT = 6 const LANE_HEIGHT = 30 const GUTTER = 140 const TRACK_RIGHT = 12 export default function CampaignRunTimeline({ windowSeconds, entries, nowOffsetSeconds, }: CampaignRunTimelineProps) { const navigate = useNavigate() const window = windowSeconds > 0 ? windowSeconds : 1 const clampPct = (offset: number) => Math.min(1, Math.max(0, offset / window)) * 100 // Entries arrive sorted by offset — lanes appear in chronological first-use order. const lanes: { key: string; name: string; items: CampaignTimelineEntry[] }[] = [] for (const e of entries) { let lane = lanes.find((l) => l.key === e.scenario_id) if (!lane) { lane = { key: e.scenario_id, name: e.scenario_name, items: [] } lanes.push(lane) } lane.items.push(e) } const presentStatuses = Array.from(new Set(entries.map((e) => e.status))).sort() const ticks = Array.from({ length: TICK_COUNT + 1 }, (_, i) => i / TICK_COUNT) const nowPct = nowOffsetSeconds == null ? null : clampPct(nowOffsetSeconds) return (
{ticks.map((t) => ( {fmtTick(t * window)} ))}
{nowPct != null && (
)} {lanes.map((lane) => (
{lane.name}
{lane.items.map((e) => (
{e.scenario_name} · {statusLabels[e.status] ?? e.status}
通过率:{e.pass_rate == null ? '—' : `${(e.pass_rate * 100).toFixed(1)}%`}
时延:{e.avg_latency_ms == null ? '—' : `${Math.round(e.avg_latency_ms)}ms`}
开始:{shortDateTime(e.started_at)}
} > navigate(`/reports?run=${e.run_id}`)} style={{ position: 'absolute', left: `${clampPct(e.offset_seconds)}%`, top: '50%', transform: 'translate(-50%, -50%)', width: 12, height: 12, borderRadius: '50%', background: statusColors[e.status] ?? colors.textMuted, border: '2px solid #fff', boxShadow: '0 0 0 1px rgba(0,0,0,0.1)', cursor: 'pointer', }} /> ))}
))}
{presentStatuses.map((s) => ( {statusLabels[s] ?? s} ))} {nowPct != null && ( 当前进度 )} 点击节点查看单次报告
) }