feat(frontend): expand incident timeline event details
This commit is contained in:
@@ -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/teardown:loading 狀態、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>
|
||||
|
||||
Reference in New Issue
Block a user