fix(web): add incident drilldown flow to status chain
This commit is contained in:
@@ -3683,6 +3683,25 @@
|
||||
"learningValue": "KM {km}; AutoRepair {autoRepair}; Ops {ops}",
|
||||
"learningDetail": "verification={verification}; next={nextStep}"
|
||||
},
|
||||
"drilldown": {
|
||||
"title": "單一 Incident 處理流程",
|
||||
"step": "{step}. {label}",
|
||||
"signal": "來源接收",
|
||||
"signalDetail": "inbound={inbound}; outbound={outbound}; source={status}; reason={reason}",
|
||||
"investigation": "MCP 調查",
|
||||
"investigationValue": "success {success}/{total}",
|
||||
"investigationDetail": "tools={tools}; failed={failed}; blocked={blocked}",
|
||||
"playbook": "PlayBook / Ansible",
|
||||
"playbookDetail": "candidates={candidates}; check/apply={check}/{apply}; approval={approval}",
|
||||
"execution": "執行結果",
|
||||
"executionValue": "{executor} / {status}",
|
||||
"executionDetail": "operation={operation}; rc={rc}; mode={mode}",
|
||||
"learning": "KM / Learning",
|
||||
"learningValue": "KM {km}; autoRepair {autoRepair}",
|
||||
"learningDetail": "verification={verification}; next={nextStep}",
|
||||
"handoff": "人工 / 下一步",
|
||||
"handoffDetail": "reason={reason}; next={nextAction}"
|
||||
},
|
||||
"source": {
|
||||
"status": "來源關聯",
|
||||
"verification": "狀態鏈驗證",
|
||||
|
||||
@@ -3683,6 +3683,25 @@
|
||||
"learningValue": "KM {km}; AutoRepair {autoRepair}; Ops {ops}",
|
||||
"learningDetail": "verification={verification}; next={nextStep}"
|
||||
},
|
||||
"drilldown": {
|
||||
"title": "單一 Incident 處理流程",
|
||||
"step": "{step}. {label}",
|
||||
"signal": "來源接收",
|
||||
"signalDetail": "inbound={inbound}; outbound={outbound}; source={status}; reason={reason}",
|
||||
"investigation": "MCP 調查",
|
||||
"investigationValue": "success {success}/{total}",
|
||||
"investigationDetail": "tools={tools}; failed={failed}; blocked={blocked}",
|
||||
"playbook": "PlayBook / Ansible",
|
||||
"playbookDetail": "candidates={candidates}; check/apply={check}/{apply}; approval={approval}",
|
||||
"execution": "執行結果",
|
||||
"executionValue": "{executor} / {status}",
|
||||
"executionDetail": "operation={operation}; rc={rc}; mode={mode}",
|
||||
"learning": "KM / Learning",
|
||||
"learningValue": "KM {km}; autoRepair {autoRepair}",
|
||||
"learningDetail": "verification={verification}; next={nextStep}",
|
||||
"handoff": "人工 / 下一步",
|
||||
"handoffDetail": "reason={reason}; next={nextAction}"
|
||||
},
|
||||
"source": {
|
||||
"status": "來源關聯",
|
||||
"verification": "狀態鏈驗證",
|
||||
|
||||
@@ -286,6 +286,8 @@ export function AwoooPStatusChainPanel({
|
||||
const sourceStatus = String(sourceCorrelation?.status ?? "missing");
|
||||
const sourceVerificationStatus = String(sourceCorrelation?.verification_status ?? sourceStatus);
|
||||
const sourceMissingReason = String(sourceCorrelation?.missing_reason ?? "");
|
||||
const sourceStatusLabel = sourceStatusLabels[sourceStatus] ?? valueOrEmpty(sourceStatus, emptyLabel);
|
||||
const sourceReasonLabel = sourceReasonLabels[sourceMissingReason] ?? valueOrEmpty(sourceMissingReason, emptyLabel);
|
||||
const topAppliedLink = sourceCorrelation?.top_candidates?.find(
|
||||
(item) => item.link_state === "applied"
|
||||
);
|
||||
@@ -356,6 +358,11 @@ export function AwoooPStatusChainPanel({
|
||||
const mcpGateway = chain.mcp?.gateway ?? {};
|
||||
const legacyMcp = chain.mcp?.legacy ?? {};
|
||||
const topTool = chain.mcp?.top_tools?.[0];
|
||||
const topToolNames = chain.mcp?.top_tools
|
||||
?.slice(0, 3)
|
||||
.map((item) => item.tool_name)
|
||||
.filter(Boolean)
|
||||
.join(" / ") || emptyLabel;
|
||||
const execution = chain.execution ?? {};
|
||||
const ansible = execution.ansible ?? {};
|
||||
const candidatePlaybook = ansible.candidate_playbooks?.[0];
|
||||
@@ -399,14 +406,14 @@ export function AwoooPStatusChainPanel({
|
||||
tone: sourceToolchainTone,
|
||||
label: t("toolchain.source"),
|
||||
value: t("toolchain.sourceValue", {
|
||||
status: sourceStatusLabels[sourceStatus] ?? valueOrEmpty(sourceStatus, emptyLabel),
|
||||
status: sourceStatusLabel,
|
||||
direct: directRefTotal,
|
||||
candidate: candidateTotal,
|
||||
applied: appliedLinkTotal,
|
||||
}),
|
||||
detail: t("toolchain.sourceDetail", {
|
||||
providers: sourceProviderSummary || emptyLabel,
|
||||
reason: sourceReasonLabels[sourceMissingReason] ?? valueOrEmpty(sourceMissingReason, emptyLabel),
|
||||
reason: sourceReasonLabel,
|
||||
}),
|
||||
},
|
||||
{
|
||||
@@ -465,6 +472,95 @@ export function AwoooPStatusChainPanel({
|
||||
}),
|
||||
},
|
||||
];
|
||||
const drilldownItems = [
|
||||
{
|
||||
key: "signal",
|
||||
Icon: Link2,
|
||||
tone: (sourceCorrelation ? (providerIngressReady ? "success" : "warning") : "neutral") as SourceFlowTone,
|
||||
title: t("drilldown.signal"),
|
||||
value: valueOrEmpty(chain.source_id ?? chain.incident_ids?.[0], emptyLabel),
|
||||
detail: t("drilldown.signalDetail", {
|
||||
inbound: chain.source_refs?.inbound_total ?? 0,
|
||||
outbound: chain.source_refs?.outbound_total ?? 0,
|
||||
status: sourceStatusLabel,
|
||||
reason: sourceReasonLabel,
|
||||
}),
|
||||
},
|
||||
{
|
||||
key: "investigation",
|
||||
Icon: RadioTower,
|
||||
tone: (mcpGatewayTotal > 0
|
||||
? (mcpGatewayProblemTotal > 0 ? "warning" : "success")
|
||||
: "neutral") as SourceFlowTone,
|
||||
title: t("drilldown.investigation"),
|
||||
value: t("drilldown.investigationValue", {
|
||||
success: mcpGateway.success ?? 0,
|
||||
total: mcpGatewayTotal,
|
||||
}),
|
||||
detail: t("drilldown.investigationDetail", {
|
||||
tools: topToolNames,
|
||||
failed: mcpGateway.failed ?? 0,
|
||||
blocked: mcpGateway.blocked ?? 0,
|
||||
}),
|
||||
},
|
||||
{
|
||||
key: "playbook",
|
||||
Icon: Wrench,
|
||||
tone: (ansible.considered || (ansible.candidate_count ?? 0) > 0
|
||||
? (ansible.applied || (ansible.apply_total ?? 0) > 0 ? "success" : "warning")
|
||||
: "neutral") as SourceFlowTone,
|
||||
title: t("drilldown.playbook"),
|
||||
value: selectedPlaybook,
|
||||
detail: t("drilldown.playbookDetail", {
|
||||
candidates: ansible.candidate_count ?? 0,
|
||||
check: ansible.check_mode_total ?? 0,
|
||||
apply: ansible.apply_total ?? 0,
|
||||
approval: valueOrEmpty(ansible.approval_source, emptyLabel),
|
||||
}),
|
||||
},
|
||||
{
|
||||
key: "execution",
|
||||
Icon: Activity,
|
||||
tone: (executionTotal > 0
|
||||
? (String(execution.latest_status ?? "").toLowerCase().includes("fail") ? "blocked" : "success")
|
||||
: "neutral") as SourceFlowTone,
|
||||
title: t("drilldown.execution"),
|
||||
value: t("drilldown.executionValue", {
|
||||
executor: execution.latest_executor ?? emptyLabel,
|
||||
status: execution.latest_status ?? emptyLabel,
|
||||
}),
|
||||
detail: t("drilldown.executionDetail", {
|
||||
operation: execution.latest_operation_type ?? emptyLabel,
|
||||
rc: valueOrEmpty(ansible.latest_returncode, emptyLabel),
|
||||
mode: valueOrEmpty(ansible.latest_execution_mode, emptyLabel),
|
||||
}),
|
||||
},
|
||||
{
|
||||
key: "learning",
|
||||
Icon: BookOpenCheck,
|
||||
tone: ((evidence.knowledge_entries ?? 0) > 0 ? "success" : "neutral") as SourceFlowTone,
|
||||
title: t("drilldown.learning"),
|
||||
value: t("drilldown.learningValue", {
|
||||
km: evidence.knowledge_entries ?? 0,
|
||||
autoRepair: evidence.auto_repair_records ?? 0,
|
||||
}),
|
||||
detail: t("drilldown.learningDetail", {
|
||||
verification: valueOrEmpty(chain.verification, emptyLabel),
|
||||
nextStep: valueOrEmpty(chain.next_step, emptyLabel),
|
||||
}),
|
||||
},
|
||||
{
|
||||
key: "handoff",
|
||||
Icon: chain.needs_human ? TriangleAlert : CheckCircle2,
|
||||
tone: (chain.needs_human ? "blocked" : "success") as SourceFlowTone,
|
||||
title: t("drilldown.handoff"),
|
||||
value: chain.needs_human ? t("human.yes") : t("human.no"),
|
||||
detail: t("drilldown.handoffDetail", {
|
||||
reason: valueOrEmpty(outcome?.human_action_reason, emptyLabel),
|
||||
nextAction: valueOrEmpty(outcome?.next_action ?? chain.next_step, emptyLabel),
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section className={cn("border border-[#e0ddd4] bg-white", className)}>
|
||||
@@ -663,6 +759,39 @@ export function AwoooPStatusChainPanel({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!compact && (
|
||||
<div>
|
||||
<div className="border-t border-[#e0ddd4] bg-[#faf9f3] px-4 py-2">
|
||||
<p className="text-xs font-semibold text-[#141413]">{t("drilldown.title")}</p>
|
||||
</div>
|
||||
<div className="grid gap-px bg-[#e0ddd4] md:grid-cols-2 xl:grid-cols-3">
|
||||
{drilldownItems.map((item, index) => (
|
||||
<div key={item.key} className="min-w-0 bg-white px-4 py-3">
|
||||
<div className="flex min-w-0 items-start gap-3">
|
||||
<span className={cn(
|
||||
"flex h-8 w-8 shrink-0 items-center justify-center border",
|
||||
sourceFlowToneClass(item.tone)
|
||||
)}>
|
||||
<item.Icon className="h-4 w-4" aria-hidden="true" />
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-semibold text-[#77736a]">
|
||||
{t("drilldown.step", { step: index + 1, label: item.title })}
|
||||
</p>
|
||||
<p className="mt-1 truncate font-mono text-sm font-semibold text-[#141413]" title={item.value}>
|
||||
{item.value}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-2 truncate font-mono text-xs text-[#5f5b52]" title={item.detail}>
|
||||
{item.detail}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{blockers.length > 0 && (
|
||||
<div className="border-t border-[#eee9dd] bg-[#fff7e8] px-4 py-3 text-xs leading-5 text-[#8a5a08]">
|
||||
<span className="font-semibold">{t("blockers")}</span>{" "}
|
||||
|
||||
Reference in New Issue
Block a user