feat(awooop): show source flow in approvals
All checks were successful
Code Review / ai-code-review (push) Successful in 11s
CD Pipeline / tests (push) Successful in 3m52s
CD Pipeline / build-and-deploy (push) Successful in 3m40s
CD Pipeline / post-deploy-checks (push) Successful in 1m18s

This commit is contained in:
Your Name
2026-05-21 14:20:13 +08:00
parent e89bb267ea
commit 140c9cdaef

View File

@@ -208,6 +208,65 @@ function RemediationEvidenceCell({ summary }: { summary?: RemediationSummary | n
);
}
type ApprovalSourceFlowStatus = "verified" | "applied" | "evidence" | "provider" | "waiting";
function approvalSourceFlowStatus(chain?: AwoooPStatusChain | null): ApprovalSourceFlowStatus {
const correlation = chain?.source_refs?.correlation;
if (!correlation) return "waiting";
const verificationStatus = String(correlation.verification_status ?? correlation.status ?? "missing");
if (verificationStatus === "applied_link_verified" || verificationStatus === "direct_ref_verified") {
return "verified";
}
if ((correlation.applied_link_total ?? 0) > 0) return "applied";
if ((correlation.direct_ref_total ?? 0) > 0 || (correlation.candidate_total ?? 0) > 0) {
return "evidence";
}
if ((correlation.provider_event_total ?? 0) > 0) return "provider";
return "waiting";
}
function approvalSourceFlowClass(status: ApprovalSourceFlowStatus) {
if (status === "verified") return "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]";
if (status === "applied" || status === "evidence") return "border-[#9bb6d9] bg-[#eef5ff] text-[#1f5b9b]";
if (status === "provider") return "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]";
return "border-[#d8d3c7] bg-[#faf9f3] text-[#5f5b52]";
}
function ApprovalSourceFlowCell({ chain }: { chain?: AwoooPStatusChain | null }) {
const t = useTranslations("awooop.listEvidence.sourceFlow");
const status = approvalSourceFlowStatus(chain);
const correlation = chain?.source_refs?.correlation;
const detail = t("detail", {
providers: correlation?.provider_event_total ?? 0,
direct: correlation?.direct_ref_total ?? 0,
candidate: correlation?.candidate_total ?? 0,
applied: correlation?.applied_link_total ?? 0,
});
const Icon = status === "verified" || status === "applied"
? ShieldCheck
: status === "waiting"
? AlertCircle
: SearchCheck;
return (
<div className="min-w-[190px]">
<span
className={cn(
"inline-flex items-center gap-1.5 border px-2 py-1 text-xs font-semibold",
approvalSourceFlowClass(status)
)}
>
<Icon className="h-3.5 w-3.5" aria-hidden="true" />
{t(`statuses.${status}`)}
</span>
<p className="mt-1 truncate font-mono text-xs text-[#77736a]" title={detail}>
{detail}
</p>
</div>
);
}
function TimeoutCell({ timeoutAt }: { timeoutAt: string | null }) {
const [remaining, setRemaining] = useState<number | null>(
getRemainingMs(timeoutAt)
@@ -299,6 +358,9 @@ function ApprovalRow({ approval }: { approval: Approval }) {
<td className="px-4 py-3">
<RemediationEvidenceCell summary={approval.remediation_summary} />
</td>
<td className="px-4 py-3">
<ApprovalSourceFlowCell chain={approval.awooop_status_chain} />
</td>
<td className="min-w-[280px] px-4 py-3">
<AwoooPStatusChainPanel chain={approval.awooop_status_chain} compact />
</td>
@@ -563,6 +625,9 @@ export default function ApprovalsPage() {
<th className="text-left px-4 py-3 text-xs font-medium text-muted-foreground uppercase tracking-wider">
{tEvidence("column")}
</th>
<th className="text-left px-4 py-3 text-xs font-medium text-muted-foreground uppercase tracking-wider">
{tEvidence("sourceFlow.column")}
</th>
<th className="text-left px-4 py-3 text-xs font-medium text-muted-foreground uppercase tracking-wider">
{tStatusChain("title")}
</th>
@@ -578,7 +643,7 @@ export default function ApprovalsPage() {
{loading ? (
Array.from({ length: 5 }).map((_, i) => (
<tr key={i} className="border-b border-border">
{Array.from({ length: 8 }).map((_, j) => (
{Array.from({ length: 9 }).map((_, j) => (
<td key={j} className="px-4 py-3">
<div className="h-5 bg-muted animate-pulse rounded w-20" />
</td>