feat(web): visualize awooop automation flow
This commit is contained in:
@@ -4181,11 +4181,24 @@
|
||||
"claimReady": "可以宣稱全自動修復",
|
||||
"claimBlocked": "不可宣稱全自動修復",
|
||||
"claimReason": "原因:{reason}",
|
||||
"mapTitle": "Automation Flow Map",
|
||||
"mapSubtitle": "依照真實 truth-chain gate,把告警到 Operator 可見性切成 8 個節點。",
|
||||
"statusCount": "{status} {count}",
|
||||
"attentionTitle": "優先瓶頸",
|
||||
"attentionAction": "下一步:{action}",
|
||||
"heatmapTitle": "Gate Evidence Heatmap",
|
||||
"bottleneckTitle": "目前瓶頸",
|
||||
"coverage": "{percent}% 通過",
|
||||
"counts": "pass {passed} / warn {warning} / miss {missing} / fail {failed}",
|
||||
"nextAction": "下一步:{action}",
|
||||
"example": "例:{incidentId} / {verdict}",
|
||||
"sourceStatuses": "來源 Gate:{statuses}",
|
||||
"heatmap": {
|
||||
"pass": "Pass",
|
||||
"warn": "Warn",
|
||||
"miss": "Miss",
|
||||
"fail": "Fail"
|
||||
},
|
||||
"statuses": {
|
||||
"passed": "Passed",
|
||||
"warning": "Warning",
|
||||
@@ -4204,6 +4217,7 @@
|
||||
"verifiedRepairDetail": "只有修復成功且完成事後驗證才計入。",
|
||||
"blockedGates": "Blocked Gate",
|
||||
"blockedGatesDetail": "有 missing / failed 就不能宣稱完整自動化。",
|
||||
"readiness": "流程健康度",
|
||||
"warningGates": "Warning Gate",
|
||||
"warningGatesDetail": "仍可觀測,但需要補脈絡或人工判斷。"
|
||||
},
|
||||
|
||||
@@ -4181,11 +4181,24 @@
|
||||
"claimReady": "可以宣稱全自動修復",
|
||||
"claimBlocked": "不可宣稱全自動修復",
|
||||
"claimReason": "原因:{reason}",
|
||||
"mapTitle": "Automation Flow Map",
|
||||
"mapSubtitle": "依照真實 truth-chain gate,把告警到可見性的流程畫成 8 個節點。",
|
||||
"statusCount": "{status} {count}",
|
||||
"attentionTitle": "優先處理",
|
||||
"attentionAction": "下一步:{action}",
|
||||
"heatmapTitle": "Gate Evidence Heatmap",
|
||||
"bottleneckTitle": "目前瓶頸",
|
||||
"coverage": "{percent}% 通過",
|
||||
"counts": "pass {passed} / warn {warning} / miss {missing} / fail {failed}",
|
||||
"nextAction": "下一步:{action}",
|
||||
"example": "例:{incidentId} / {verdict}",
|
||||
"sourceStatuses": "來源 Gate:{statuses}",
|
||||
"heatmap": {
|
||||
"pass": "Pass",
|
||||
"warn": "Warn",
|
||||
"miss": "Miss",
|
||||
"fail": "Fail"
|
||||
},
|
||||
"statuses": {
|
||||
"passed": "Passed",
|
||||
"warning": "Warning",
|
||||
@@ -4204,6 +4217,7 @@
|
||||
"verifiedRepairDetail": "只有修復成功且完成事後驗證才計入。",
|
||||
"blockedGates": "Blocked Gate",
|
||||
"blockedGatesDetail": "有 missing / failed 就不能宣稱完整自動化。",
|
||||
"readiness": "流程健康度",
|
||||
"warningGates": "Warning Gate",
|
||||
"warningGatesDetail": "仍可觀測,但需要補脈絡或人工判斷。"
|
||||
},
|
||||
|
||||
@@ -2869,6 +2869,27 @@ function automationFlowStatusClass(status?: AutomationFlowStatus | null) {
|
||||
return "border-[#d8d3c7] bg-[#faf9f3] text-[#5f5b52]";
|
||||
}
|
||||
|
||||
function automationFlowStatusFill(status?: AutomationFlowStatus | null) {
|
||||
if (status === "passed") return "bg-[#4f9d5f]";
|
||||
if (status === "warning") return "bg-[#c58a24]";
|
||||
if (status === "blocked") return "bg-[#c65145]";
|
||||
return "bg-[#b8b2a7]";
|
||||
}
|
||||
|
||||
function automationFlowStatusSurface(status?: AutomationFlowStatus | null) {
|
||||
if (status === "passed") return "border-[#9bc7a4] bg-[#f4fbf5]";
|
||||
if (status === "warning") return "border-[#d9b36f] bg-[#fff8ea]";
|
||||
if (status === "blocked") return "border-[#e2a29b] bg-[#fff5f3]";
|
||||
return "border-[#d8d3c7] bg-[#faf9f3]";
|
||||
}
|
||||
|
||||
function automationFlowStatusText(status?: AutomationFlowStatus | null) {
|
||||
if (status === "passed") return "text-[#17602a]";
|
||||
if (status === "warning") return "text-[#8a5a08]";
|
||||
if (status === "blocked") return "text-[#9f2f25]";
|
||||
return "text-[#5f5b52]";
|
||||
}
|
||||
|
||||
function automationFlowStatusLabelKey(status?: AutomationFlowStatus | null) {
|
||||
if (
|
||||
status === "passed" ||
|
||||
@@ -2911,6 +2932,34 @@ function AutomationFlowGatePanel({
|
||||
const claimReasonLabel = claimReasonKey
|
||||
? t(`claimReasons.${claimReasonKey}` as never)
|
||||
: claimReason;
|
||||
const gates = flow?.gates ?? [];
|
||||
const readiness = gates.length
|
||||
? Math.round(gates.reduce((acc, gate) => acc + (gate.passed_percent ?? 0), 0) / gates.length)
|
||||
: 0;
|
||||
const statusCounts = gates.reduce(
|
||||
(acc, gate) => {
|
||||
if (gate.status === "passed") acc.passed += 1;
|
||||
else if (gate.status === "warning") acc.warning += 1;
|
||||
else if (gate.status === "blocked") acc.blocked += 1;
|
||||
return acc;
|
||||
},
|
||||
{ passed: 0, warning: 0, blocked: 0 }
|
||||
);
|
||||
const blockedGates = gates.filter((gate) => gate.status === "blocked");
|
||||
const warningGates = gates.filter((gate) => gate.status === "warning");
|
||||
const firstAttentionGate = blockedGates[0] ?? warningGates[0] ?? null;
|
||||
const attentionGateLabelKey = firstAttentionGate
|
||||
? AUTOMATION_FLOW_GATE_LABEL_KEYS[firstAttentionGate.gate]
|
||||
: null;
|
||||
const attentionActionKey = firstAttentionGate?.next_action
|
||||
? AUTOMATION_FLOW_ACTION_KEYS[firstAttentionGate.next_action]
|
||||
: null;
|
||||
const attentionGateLabel = attentionGateLabelKey
|
||||
? t(`gates.${attentionGateLabelKey}` as never)
|
||||
: firstAttentionGate?.gate ?? "--";
|
||||
const attentionActionLabel = attentionActionKey
|
||||
? t(`actions.${attentionActionKey}` as never)
|
||||
: firstAttentionGate?.next_action ?? "--";
|
||||
|
||||
return (
|
||||
<section id="automation-flow-gates" className="border border-[#e0ddd4] bg-white">
|
||||
@@ -2950,146 +2999,245 @@ function AutomationFlowGatePanel({
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
"border-b px-4 py-3 text-xs leading-5",
|
||||
claimReady
|
||||
? "border-[#c8dfcb] bg-[#f4fbf5] text-[#17602a]"
|
||||
: "border-[#eed2ce] bg-[#fff6f5] text-[#8f2c22]"
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-2 font-semibold">
|
||||
{claimReady ? (
|
||||
<ShieldCheck className="h-4 w-4" aria-hidden="true" />
|
||||
) : (
|
||||
<AlertCircle className="h-4 w-4" aria-hidden="true" />
|
||||
)}
|
||||
<span>{claimReady ? t("claimReady") : t("claimBlocked")}</span>
|
||||
</div>
|
||||
<p className="mt-1">{t("claimReason", { reason: claimReasonLabel })}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-px bg-[#e0ddd4] md:grid-cols-2 xl:grid-cols-4">
|
||||
{[
|
||||
{
|
||||
label: t("metrics.evaluated"),
|
||||
value: flow.evaluated_total,
|
||||
detail: t("metrics.evaluatedDetail", {
|
||||
incidents: summary.incident_total ?? 0,
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: t("metrics.verifiedRepair"),
|
||||
value: summary.verified_auto_repair_total ?? 0,
|
||||
detail: t("metrics.verifiedRepairDetail"),
|
||||
},
|
||||
{
|
||||
label: t("metrics.blockedGates"),
|
||||
value: flow.blocked_gates?.length ?? 0,
|
||||
detail: t("metrics.blockedGatesDetail"),
|
||||
},
|
||||
{
|
||||
label: t("metrics.warningGates"),
|
||||
value: flow.warning_gates?.length ?? 0,
|
||||
detail: t("metrics.warningGatesDetail"),
|
||||
},
|
||||
].map((item) => (
|
||||
<div key={item.label} className="bg-white px-4 py-3">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{item.label}</p>
|
||||
<p className="mt-2 font-mono text-2xl font-semibold text-[#141413]">
|
||||
{item.value}
|
||||
</p>
|
||||
<p className="mt-1 text-xs leading-5 text-[#5f5b52]">{item.detail}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-px bg-[#e0ddd4] md:grid-cols-2 xl:grid-cols-4">
|
||||
{flow.gates.map((gate) => {
|
||||
const StatusIcon = automationFlowStatusIcon(gate.status);
|
||||
const gateLabelKey = AUTOMATION_FLOW_GATE_LABEL_KEYS[gate.gate];
|
||||
const gateLabel = gateLabelKey
|
||||
? t(`gates.${gateLabelKey}` as never)
|
||||
: gate.gate;
|
||||
const actionKey = gate.next_action
|
||||
? AUTOMATION_FLOW_ACTION_KEYS[gate.next_action]
|
||||
: null;
|
||||
const actionLabel = actionKey
|
||||
? t(`actions.${actionKey}` as never)
|
||||
: gate.next_action ?? "--";
|
||||
const passedPercent = Math.max(
|
||||
0,
|
||||
Math.min(100, Number.isFinite(gate.passed_percent) ? gate.passed_percent : 0)
|
||||
);
|
||||
const example = gate.examples?.[0];
|
||||
const sourceStatuses = Object.entries(example?.source_statuses ?? {})
|
||||
.map(([key, value]) => `${key}:${value}`)
|
||||
.join(", ");
|
||||
|
||||
return (
|
||||
<article key={gate.gate} className="bg-white px-4 py-3">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-semibold text-[#141413]">{gateLabel}</p>
|
||||
<p className="mt-1 font-mono text-[11px] text-[#77736a]">
|
||||
{gate.quality_gates?.join(" / ") || gate.gate}
|
||||
</p>
|
||||
</div>
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex shrink-0 items-center gap-1 border px-2 py-0.5 text-xs font-semibold",
|
||||
automationFlowStatusClass(gate.status)
|
||||
)}
|
||||
>
|
||||
<StatusIcon className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
{t(automationFlowStatusLabelKey(gate.status) as never)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-3 h-1.5 bg-[#ece7dc]">
|
||||
<div
|
||||
className={cn(
|
||||
"h-full",
|
||||
gate.status === "passed"
|
||||
? "bg-[#4f9d5f]"
|
||||
: gate.status === "warning"
|
||||
? "bg-[#c58a24]"
|
||||
: "bg-[#c65145]"
|
||||
)}
|
||||
style={{ width: `${passedPercent}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-2 text-xs leading-5 text-[#5f5b52]">
|
||||
{t("coverage", { percent: passedPercent.toFixed(1) })}
|
||||
<div className="grid gap-px border-b border-[#e0ddd4] bg-[#e0ddd4] xl:grid-cols-[minmax(0,1.85fr)_minmax(330px,0.85fr)]">
|
||||
<div className="bg-white p-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.12em] text-[#77736a]">
|
||||
{t("mapTitle")}
|
||||
</p>
|
||||
<p className="mt-1 text-xs leading-5 text-[#5f5b52]">
|
||||
{t("counts", {
|
||||
passed: gate.passed_total ?? 0,
|
||||
warning: gate.warning_total ?? 0,
|
||||
missing: gate.missing_total ?? 0,
|
||||
failed: gate.failed_total ?? 0,
|
||||
})}
|
||||
{t("mapSubtitle")}
|
||||
</p>
|
||||
<p className="mt-2 text-xs font-semibold leading-5 text-[#141413]">
|
||||
{t("nextAction", { action: actionLabel })}
|
||||
</p>
|
||||
{example ? (
|
||||
<div className="mt-2 space-y-1 text-xs leading-5 text-[#77736a]">
|
||||
<p>
|
||||
{t("example", {
|
||||
incidentId: example.incident_id ?? "--",
|
||||
verdict: example.verdict ?? "--",
|
||||
})}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5 text-xs">
|
||||
{(["passed", "warning", "blocked"] as const).map((status) => (
|
||||
<span
|
||||
key={status}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 border px-2 py-1 font-semibold",
|
||||
automationFlowStatusClass(status)
|
||||
)}
|
||||
>
|
||||
<span className={cn("h-2 w-2", automationFlowStatusFill(status))} />
|
||||
{t("statusCount", {
|
||||
status: t(automationFlowStatusLabelKey(status) as never),
|
||||
count: statusCounts[status],
|
||||
})}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 overflow-x-auto pb-1">
|
||||
<div className="grid min-w-[920px] grid-cols-8 gap-px bg-[#d8d3c7]">
|
||||
{gates.map((gate, index) => {
|
||||
const StatusIcon = automationFlowStatusIcon(gate.status);
|
||||
const gateLabelKey = AUTOMATION_FLOW_GATE_LABEL_KEYS[gate.gate];
|
||||
const gateLabel = gateLabelKey
|
||||
? t(`gates.${gateLabelKey}` as never)
|
||||
: gate.gate;
|
||||
const passedPercent = Math.max(
|
||||
0,
|
||||
Math.min(100, Number.isFinite(gate.passed_percent) ? gate.passed_percent : 0)
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={gate.gate}
|
||||
className={cn(
|
||||
"relative min-h-[150px] border p-3",
|
||||
automationFlowStatusSurface(gate.status)
|
||||
)}
|
||||
>
|
||||
{index < gates.length - 1 ? (
|
||||
<ArrowRight
|
||||
className="absolute -right-3 top-1/2 z-10 h-5 w-5 -translate-y-1/2 bg-white text-[#77736a]"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : null}
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<span className="font-mono text-[11px] text-[#77736a]">
|
||||
{String(index + 1).padStart(2, "0")}
|
||||
</span>
|
||||
<StatusIcon
|
||||
className={cn("h-4 w-4", automationFlowStatusText(gate.status))}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-3 min-h-[34px] text-xs font-semibold leading-4 text-[#141413]">
|
||||
{gateLabel}
|
||||
</p>
|
||||
<div className="mt-3 h-1.5 bg-white/80">
|
||||
<div
|
||||
className={cn("h-full", automationFlowStatusFill(gate.status))}
|
||||
style={{ width: `${passedPercent}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3 flex items-baseline justify-between gap-2">
|
||||
<span className={cn("font-mono text-lg font-semibold", automationFlowStatusText(gate.status))}>
|
||||
{passedPercent.toFixed(0)}%
|
||||
</span>
|
||||
<span className="text-[11px] font-semibold text-[#5f5b52]">
|
||||
{t(automationFlowStatusLabelKey(gate.status) as never)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 grid grid-cols-4 gap-px bg-[#e7e1d6] text-center font-mono text-[10px]">
|
||||
<span className="bg-white px-1 py-1 text-[#17602a]">{gate.passed_total ?? 0}</span>
|
||||
<span className="bg-white px-1 py-1 text-[#8a5a08]">{gate.warning_total ?? 0}</span>
|
||||
<span className="bg-white px-1 py-1 text-[#9f2f25]">{gate.missing_total ?? 0}</span>
|
||||
<span className="bg-white px-1 py-1 text-[#7d2018]">{gate.failed_total ?? 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-4">
|
||||
<div
|
||||
className={cn(
|
||||
"border p-4",
|
||||
claimReady
|
||||
? "border-[#c8dfcb] bg-[#f4fbf5] text-[#17602a]"
|
||||
: "border-[#eed2ce] bg-[#fff6f5] text-[#8f2c22]"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2 text-sm font-semibold">
|
||||
{claimReady ? (
|
||||
<ShieldCheck className="h-4 w-4" aria-hidden="true" />
|
||||
) : (
|
||||
<AlertCircle className="h-4 w-4" aria-hidden="true" />
|
||||
)}
|
||||
<span>{claimReady ? t("claimReady") : t("claimBlocked")}</span>
|
||||
</div>
|
||||
<p className="mt-2 text-xs leading-5">{t("claimReason", { reason: claimReasonLabel })}</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 grid grid-cols-2 gap-px bg-[#e0ddd4]">
|
||||
{[
|
||||
{ label: t("metrics.evaluated"), value: flow.evaluated_total },
|
||||
{ label: t("metrics.verifiedRepair"), value: summary.verified_auto_repair_total ?? 0 },
|
||||
{ label: t("metrics.blockedGates"), value: blockedGates.length },
|
||||
{ label: t("metrics.readiness"), value: `${readiness}%` },
|
||||
].map((item) => (
|
||||
<div key={item.label} className="bg-white px-4 py-3">
|
||||
<p className="text-[11px] font-semibold text-[#77736a]">{item.label}</p>
|
||||
<p className="mt-1 font-mono text-2xl font-semibold text-[#141413]">{item.value}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-3 border border-[#e0ddd4] bg-[#faf9f3] p-3">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("attentionTitle")}</p>
|
||||
<p className="mt-2 text-sm font-semibold text-[#141413]">{attentionGateLabel}</p>
|
||||
<p className="mt-1 text-xs leading-5 text-[#5f5b52]">
|
||||
{t("attentionAction", { action: attentionActionLabel })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-px bg-[#e0ddd4] xl:grid-cols-[minmax(0,1.15fr)_minmax(320px,0.85fr)]">
|
||||
<div className="bg-white p-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.12em] text-[#77736a]">
|
||||
{t("heatmapTitle")}
|
||||
</p>
|
||||
<div className="grid grid-cols-4 gap-px bg-[#e0ddd4] text-center text-[10px] font-semibold text-[#5f5b52]">
|
||||
<span className="bg-white px-2 py-1">{t("heatmap.pass")}</span>
|
||||
<span className="bg-white px-2 py-1">{t("heatmap.warn")}</span>
|
||||
<span className="bg-white px-2 py-1">{t("heatmap.miss")}</span>
|
||||
<span className="bg-white px-2 py-1">{t("heatmap.fail")}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 space-y-2">
|
||||
{gates.map((gate) => {
|
||||
const gateLabelKey = AUTOMATION_FLOW_GATE_LABEL_KEYS[gate.gate];
|
||||
const gateLabel = gateLabelKey
|
||||
? t(`gates.${gateLabelKey}` as never)
|
||||
: gate.gate;
|
||||
const total = Math.max(1, gate.evaluated_total ?? 1);
|
||||
const segments = [
|
||||
{ key: "pass", value: gate.passed_total ?? 0, className: "bg-[#4f9d5f]" },
|
||||
{ key: "warn", value: gate.warning_total ?? 0, className: "bg-[#c58a24]" },
|
||||
{ key: "miss", value: gate.missing_total ?? 0, className: "bg-[#c65145]" },
|
||||
{ key: "fail", value: gate.failed_total ?? 0, className: "bg-[#7d2018]" },
|
||||
];
|
||||
return (
|
||||
<div key={gate.gate} className="grid gap-2 md:grid-cols-[180px_minmax(0,1fr)_110px] md:items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={cn("h-2.5 w-2.5", automationFlowStatusFill(gate.status))} />
|
||||
<span className="text-xs font-semibold text-[#141413]">{gateLabel}</span>
|
||||
</div>
|
||||
<div className="flex h-4 overflow-hidden bg-[#ece7dc]">
|
||||
{segments.map((segment) => (
|
||||
<div
|
||||
key={segment.key}
|
||||
className={segment.className}
|
||||
style={{ width: `${(segment.value / total) * 100}%` }}
|
||||
title={`${segment.key}: ${segment.value}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-px bg-[#e0ddd4] text-center font-mono text-[10px]">
|
||||
<span className="bg-white py-1 text-[#17602a]">{gate.passed_total ?? 0}</span>
|
||||
<span className="bg-white py-1 text-[#8a5a08]">{gate.warning_total ?? 0}</span>
|
||||
<span className="bg-white py-1 text-[#9f2f25]">{gate.missing_total ?? 0}</span>
|
||||
<span className="bg-white py-1 text-[#7d2018]">{gate.failed_total ?? 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-4">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.12em] text-[#77736a]">
|
||||
{t("bottleneckTitle")}
|
||||
</p>
|
||||
<div className="mt-3 space-y-2">
|
||||
{(blockedGates.length ? blockedGates : warningGates).slice(0, 4).map((gate) => {
|
||||
const gateLabelKey = AUTOMATION_FLOW_GATE_LABEL_KEYS[gate.gate];
|
||||
const gateLabel = gateLabelKey
|
||||
? t(`gates.${gateLabelKey}` as never)
|
||||
: gate.gate;
|
||||
const actionKey = gate.next_action
|
||||
? AUTOMATION_FLOW_ACTION_KEYS[gate.next_action]
|
||||
: null;
|
||||
const actionLabel = actionKey
|
||||
? t(`actions.${actionKey}` as never)
|
||||
: gate.next_action ?? "--";
|
||||
const example = gate.examples?.[0];
|
||||
return (
|
||||
<div
|
||||
key={gate.gate}
|
||||
className={cn("border p-3", automationFlowStatusSurface(gate.status))}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<p className="text-xs font-semibold text-[#141413]">{gateLabel}</p>
|
||||
<span
|
||||
className={cn(
|
||||
"border px-2 py-0.5 text-[11px] font-semibold",
|
||||
automationFlowStatusClass(gate.status)
|
||||
)}
|
||||
>
|
||||
{t(automationFlowStatusLabelKey(gate.status) as never)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-2 text-xs leading-5 text-[#5f5b52]">
|
||||
{t("nextAction", { action: actionLabel })}
|
||||
</p>
|
||||
{sourceStatuses ? (
|
||||
<p className="break-words font-mono">
|
||||
{t("sourceStatuses", { statuses: sourceStatuses })}
|
||||
{example ? (
|
||||
<p className="mt-2 truncate font-mono text-[11px] text-[#77736a]">
|
||||
{example.incident_id ?? "--"} / {example.verdict ?? "--"}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user