fix(web): clarify incident flow stage on dashboard
This commit is contained in:
@@ -417,6 +417,19 @@
|
||||
"signalCount": "Signals",
|
||||
"statusLabel": "Status",
|
||||
"aiProposal": "AI Proposal",
|
||||
"aiProposalPreview": "AI Proposal: {action}",
|
||||
"flowCurrentLabel": "Current stage",
|
||||
"flowNextLabel": "Next step",
|
||||
"flowComplete": "Complete",
|
||||
"flowStages": {
|
||||
"alert": "Alert received",
|
||||
"detection": "AI detection",
|
||||
"analysis": "AI analysis",
|
||||
"proposal": "Proposal generated",
|
||||
"approval": "Waiting approval",
|
||||
"execution": "Repair execution",
|
||||
"resolved": "Complete"
|
||||
},
|
||||
"processingTimeline": "Processing Timeline",
|
||||
"timelineLoading": "Loading processing timeline...",
|
||||
"timelineEvents": "Event Details",
|
||||
|
||||
@@ -418,6 +418,19 @@
|
||||
"signalCount": "信號數",
|
||||
"statusLabel": "狀態",
|
||||
"aiProposal": "AI 提案",
|
||||
"aiProposalPreview": "AI 提案:{action}",
|
||||
"flowCurrentLabel": "目前階段",
|
||||
"flowNextLabel": "下一步",
|
||||
"flowComplete": "已完成",
|
||||
"flowStages": {
|
||||
"alert": "告警收到",
|
||||
"detection": "AI 偵測",
|
||||
"analysis": "AI 分析",
|
||||
"proposal": "提案生成",
|
||||
"approval": "等待授權",
|
||||
"execution": "執行修復",
|
||||
"resolved": "完成"
|
||||
},
|
||||
"processingTimeline": "處理歷程",
|
||||
"timelineLoading": "載入處理歷程...",
|
||||
"timelineEvents": "事件明細",
|
||||
|
||||
@@ -46,6 +46,16 @@ const SEV_CONFIG = {
|
||||
P3: { barColor: '#22C55E', label: 'P3', labelBg: 'rgba(34,197,94,0.1)', labelColor: '#16a34a' },
|
||||
} as const
|
||||
|
||||
const FLOW_STAGE_ORDER: FlowStage[] = [
|
||||
'alert',
|
||||
'detection',
|
||||
'analysis',
|
||||
'proposal',
|
||||
'approval',
|
||||
'execution',
|
||||
'resolved',
|
||||
]
|
||||
|
||||
/** 根據 incident + decision evidence 對應 FlowStage */
|
||||
function toFlowStage(status: string, severity: string, decision?: DecisionInfo | null): FlowStage {
|
||||
const normalizedStatus = status.toLowerCase()
|
||||
@@ -66,6 +76,13 @@ function toFlowStage(status: string, severity: string, decision?: DecisionInfo |
|
||||
return severity === 'P0' ? 'alert' : 'detection'
|
||||
}
|
||||
|
||||
function nextFlowStage(stage: FlowStage, isResolved: boolean): FlowStage | null {
|
||||
if (isResolved) return null
|
||||
const index = FLOW_STAGE_ORDER.indexOf(stage)
|
||||
if (index < 0 || index >= FLOW_STAGE_ORDER.length - 1) return null
|
||||
return FLOW_STAGE_ORDER[index + 1]
|
||||
}
|
||||
|
||||
/** 格式化持續時間 */
|
||||
function formatDuration(createdAt: string | undefined): string {
|
||||
if (!createdAt) return '--'
|
||||
@@ -217,6 +234,16 @@ export function IncidentCard({ incident, decision, onApprovalChange }: IncidentC
|
||||
const sevCfg = SEV_CONFIG[sev] ?? SEV_CONFIG.P3
|
||||
const flowStage = toFlowStage(incidentStatus, incident.severity, decision)
|
||||
const isResolved = incidentStatus === 'resolved' || incidentStatus === 'closed'
|
||||
const nextStage = nextFlowStage(flowStage, isResolved)
|
||||
const flowStageLabels: Record<FlowStage, string> = {
|
||||
alert: t('flowStages.alert'),
|
||||
detection: t('flowStages.detection'),
|
||||
analysis: t('flowStages.analysis'),
|
||||
proposal: t('flowStages.proposal'),
|
||||
approval: t('flowStages.approval'),
|
||||
execution: t('flowStages.execution'),
|
||||
resolved: t('flowStages.resolved'),
|
||||
}
|
||||
|
||||
const serviceName = incident.affected_services?.[0] ?? '--'
|
||||
const duration = formatDuration(incident.created_at)
|
||||
@@ -315,9 +342,9 @@ export function IncidentCard({ incident, decision, onApprovalChange }: IncidentC
|
||||
</span>
|
||||
)
|
||||
case 'approved':
|
||||
return <span style={{ fontSize: 12, color: '#22C55E', fontWeight: 700 }}>✓ {t('approved')}</span>
|
||||
return <span style={{ fontSize: 12, color: '#22C55E', fontWeight: 700 }}>{t('approved')}</span>
|
||||
case 'rejected':
|
||||
return <span style={{ fontSize: 12, color: '#cc2200', fontWeight: 700 }}>✗ {t('rejected')}</span>
|
||||
return <span style={{ fontSize: 12, color: '#cc2200', fontWeight: 700 }}>{t('rejected')}</span>
|
||||
case 'error':
|
||||
case 'timeout':
|
||||
return (
|
||||
@@ -352,7 +379,7 @@ export function IncidentCard({ incident, decision, onApprovalChange }: IncidentC
|
||||
}}
|
||||
title={isAnalyzing ? t('analyzing') : decisionAction || t('authorizeExecution')}
|
||||
>
|
||||
✓ {t('authorize')}
|
||||
{t('authorize')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleReject}
|
||||
@@ -369,9 +396,20 @@ export function IncidentCard({ incident, decision, onApprovalChange }: IncidentC
|
||||
fontFamily: 'inherit',
|
||||
}}
|
||||
>
|
||||
✗ {t('reject')}
|
||||
{t('reject')}
|
||||
</button>
|
||||
{isAnalyzing && <span style={{ fontSize: 12, color: '#F59E0B' }}>⏳</span>}
|
||||
{isAnalyzing && (
|
||||
<span
|
||||
aria-label={t('analyzing')}
|
||||
style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: '50%',
|
||||
background: '#F59E0B',
|
||||
display: 'inline-block',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -425,6 +463,31 @@ export function IncidentCard({ incident, decision, onApprovalChange }: IncidentC
|
||||
{/* 流程狀態圖 */}
|
||||
<FlowPipeline activeStage={flowStage} isResolved={isResolved} severity={sev} />
|
||||
|
||||
<div
|
||||
data-testid="incident-flow-summary"
|
||||
style={{
|
||||
margin: '0 14px 8px',
|
||||
padding: '7px 9px',
|
||||
border: '0.5px solid #e0ddd4',
|
||||
borderRadius: 6,
|
||||
background: '#fbfaf6',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
flexWrap: 'wrap',
|
||||
fontSize: 11,
|
||||
color: '#555550',
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
{t('flowCurrentLabel')}: <strong style={{ color: sevCfg.labelColor }}>{flowStageLabels[flowStage]}</strong>
|
||||
</span>
|
||||
<span style={{ color: '#b0ad9f' }}>/</span>
|
||||
<span>
|
||||
{t('flowNextLabel')}: <strong style={{ color: '#141413' }}>{nextStage ? flowStageLabels[nextStage] : t('flowComplete')}</strong>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Impact 指標列 */}
|
||||
<div style={{
|
||||
margin: '0 14px 8px',
|
||||
@@ -646,7 +709,7 @@ export function IncidentCard({ incident, decision, onApprovalChange }: IncidentC
|
||||
fontFamily: 'inherit',
|
||||
}}
|
||||
>
|
||||
<span>▸ {t('aiProposal')}: {decisionAction.slice(0, 50)}{decisionAction.length > 50 ? '...' : ''}</span>
|
||||
<span>{t('aiProposalPreview', { action: compactTimelineText(decisionAction, '') })}</span>
|
||||
</button>
|
||||
{aiExpanded && (
|
||||
<div style={{
|
||||
|
||||
Reference in New Issue
Block a user