feat(frontend): expand incident timeline event details
All checks were successful
Code Review / ai-code-review (push) Successful in 12s
CD Pipeline / tests (push) Successful in 1m7s
CD Pipeline / build-and-deploy (push) Successful in 3m37s
CD Pipeline / post-deploy-checks (push) Successful in 1m23s

This commit is contained in:
Your Name
2026-05-14 23:09:12 +08:00
parent d9d119ede2
commit 475f2e452d
4 changed files with 151 additions and 4 deletions

View File

@@ -101,6 +101,40 @@ function formatTimelineTime(value: string | null): string {
}
}
function asRecord(value: unknown): Record<string, unknown> {
return value && typeof value === 'object' && !Array.isArray(value)
? value as Record<string, unknown>
: {}
}
function eventContext(event: IncidentTimelineResponse['events'][number]): Record<string, unknown> {
const data = asRecord(event.data)
return asRecord(data.context)
}
function eventMcpRoute(event: IncidentTimelineResponse['events'][number]): string | null {
const context = eventContext(event)
const route = asRecord(context.mcp_route)
const agent = route.agent_id
const tool = route.tool_name
const scope = route.required_scope
if (!agent && !tool && !scope) return null
return [agent, tool, scope].filter(Boolean).map(String).join('/')
}
function eventWriteSummary(event: IncidentTimelineResponse['events'][number]): string | null {
const context = eventContext(event)
const incident = context.writes_incident_state
const autoRepair = context.writes_auto_repair_result
if (incident == null && autoRepair == null) return null
return `incident=${String(incident)} autoRepair=${String(autoRepair)}`
}
function compactTimelineText(value: string | null | undefined, fallback = '--'): string {
if (!value) return fallback
return value.length > 96 ? `${value.slice(0, 96)}...` : value
}
// =============================================================================
// 2026-04-02 Claude Code: Phase R-UI2 handleApprove/Reject 重複邏輯抽取
// useApprovalAction — 統一 setup/teardownloading 狀態、timeout、error 處理
@@ -425,7 +459,7 @@ export function IncidentCard({ incident, decision, onApprovalChange }: IncidentC
fontFamily: 'inherit',
}}
>
<span>{timelineExpanded ? '▾' : '▸'} </span>
<span>{timelineExpanded ? '▾' : '▸'} {t('processingTimeline')}</span>
{timelineData?.ascii_timeline && (
<span style={{
color: '#87867f',
@@ -448,7 +482,7 @@ export function IncidentCard({ incident, decision, onApprovalChange }: IncidentC
borderRadius: 6,
}}>
{timelineLoading && (
<div style={{ fontSize: 12, color: '#87867f' }}>...</div>
<div style={{ fontSize: 12, color: '#87867f' }}>{t('timelineLoading')}</div>
)}
{timelineError && (
<div style={{ fontSize: 12, color: '#cc2200' }}>{timelineError}</div>
@@ -502,6 +536,82 @@ export function IncidentCard({ incident, decision, onApprovalChange }: IncidentC
</div>
))}
</div>
{timelineData.events.length > 0 && (
<div style={{ marginTop: 10, paddingTop: 9, borderTop: '0.5px solid #e0ddd4' }}>
<div style={{
fontSize: 10,
color: '#87867f',
fontWeight: 700,
textTransform: 'uppercase',
marginBottom: 6,
}}>
{t('timelineEvents')}
</div>
<div style={{ display: 'grid', gap: 6 }}>
{timelineData.events.slice(-8).reverse().map((event, index) => {
const route = eventMcpRoute(event)
const writes = eventWriteSummary(event)
return (
<div
key={`${event.source_table ?? 'event'}-${event.timestamp ?? index}-${event.title}`}
style={{
display: 'grid',
gap: 3,
minWidth: 0,
padding: '6px 7px',
border: '0.5px solid #e0ddd4',
borderRadius: 6,
background: '#faf9f3',
}}
>
<div style={{
display: 'flex',
alignItems: 'center',
gap: 6,
minWidth: 0,
fontSize: 11,
}}>
<span style={{ color: statusColor(event.status), fontWeight: 700, flexShrink: 0 }}>
{event.stage}
</span>
<span style={{
color: '#141413',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}} title={event.title}>
{event.title}
</span>
</div>
<div style={{
color: '#87867f',
fontSize: 10,
fontFamily: 'var(--font-body), monospace',
overflowWrap: 'anywhere',
}}>
{t('timelineSource')}: {event.source_table ?? '--'} · {formatTimelineTime(event.timestamp)}
</div>
{event.description && (
<div style={{ color: '#5f5d56', fontSize: 10, lineHeight: 1.35, overflowWrap: 'anywhere' }}>
{compactTimelineText(event.description)}
</div>
)}
{route && (
<div style={{ color: '#4A90D9', fontSize: 10, lineHeight: 1.35, overflowWrap: 'anywhere' }}>
{t('timelineRoute')}: {route}
</div>
)}
{writes && (
<div style={{ color: '#166534', fontSize: 10, lineHeight: 1.35, overflowWrap: 'anywhere' }}>
{t('timelineWrites')}: {writes}
</div>
)}
</div>
)
})}
</div>
</div>
)}
</>
)}
</div>