fix(awooop): make approvals mobile queue scannable
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 57s
CD Pipeline / build-and-deploy (push) Successful in 6m36s
CD Pipeline / post-deploy-checks (push) Has been cancelled
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 57s
CD Pipeline / build-and-deploy (push) Successful in 6m36s
CD Pipeline / post-deploy-checks (push) Has been cancelled
This commit is contained in:
@@ -633,6 +633,52 @@ function LogAutomationMainlinePanel({
|
||||
);
|
||||
}
|
||||
|
||||
function legacyRiskClass(risk: string) {
|
||||
return risk === "critical"
|
||||
? "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]"
|
||||
: "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]";
|
||||
}
|
||||
|
||||
function LegacyApprovalMobileCard({ approval }: { approval: LegacyApproval }) {
|
||||
const tLegacy = useTranslations("awooop.approvals.legacyHitl");
|
||||
const risk = approval.risk_level?.toLowerCase() ?? "unknown";
|
||||
|
||||
return (
|
||||
<article className="border border-[#ead9b4] bg-white p-4">
|
||||
<div className="flex min-w-0 items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<span className={cn("inline-flex border px-2 py-0.5 text-xs font-semibold", legacyRiskClass(risk))}>
|
||||
{risk}
|
||||
</span>
|
||||
<p className="mt-2 break-words text-sm font-semibold text-[#141413]">{approval.action}</p>
|
||||
</div>
|
||||
<span className="shrink-0 font-mono text-xs text-[#77736a]">{approval.id.slice(0, 8)}</span>
|
||||
</div>
|
||||
<p className="mt-2 line-clamp-3 break-words text-xs leading-5 text-[#5f5b52]">
|
||||
{approval.description || "--"}
|
||||
</p>
|
||||
<div className="mt-4 grid gap-2 border-t border-[#eee9dd] pt-3 text-xs leading-5 text-[#5f5b52]">
|
||||
<div className="grid grid-cols-[5rem_minmax(0,1fr)] gap-3">
|
||||
<span className="font-semibold text-[#77736a]">{tLegacy("columns.incident")}</span>
|
||||
<span className="min-w-0 break-all font-mono">{approval.incident_id || "--"}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-[5rem_minmax(0,1fr)] gap-3">
|
||||
<span className="font-semibold text-[#77736a]">{tLegacy("columns.source")}</span>
|
||||
<span className="min-w-0 break-words">
|
||||
{approval.telegram_message_id
|
||||
? tLegacy("telegramRef", { id: approval.telegram_message_id })
|
||||
: tLegacy("noTelegram")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-[5rem_minmax(0,1fr)] gap-3">
|
||||
<span className="font-semibold text-[#77736a]">{tLegacy("columns.created")}</span>
|
||||
<span>{formatLocalTime(approval.created_at)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
function LegacyHitlBacklogPanel({
|
||||
approvals,
|
||||
loading,
|
||||
@@ -694,7 +740,32 @@ function LegacyHitlBacklogPanel({
|
||||
)}
|
||||
|
||||
{(loading || approvals.length > 0) && (
|
||||
<div className="overflow-x-auto bg-white">
|
||||
<div className="grid gap-3 bg-white p-3 md:hidden" data-testid="legacy-hitl-mobile-list">
|
||||
{loading ? (
|
||||
Array.from({ length: 3 }).map((_, row) => (
|
||||
<div key={row} className="border border-[#ead9b4] bg-white p-4">
|
||||
<div className="h-5 w-24 animate-pulse rounded bg-[#f0ede5]" />
|
||||
<div className="mt-4 grid gap-3">
|
||||
<div className="h-4 w-full animate-pulse rounded bg-[#f0ede5]" />
|
||||
<div className="h-4 w-3/4 animate-pulse rounded bg-[#f0ede5]" />
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
approvals.slice(0, 8).map((approval) => (
|
||||
<LegacyApprovalMobileCard key={approval.id} approval={approval} />
|
||||
))
|
||||
)}
|
||||
{!loading && approvals.length > 8 && (
|
||||
<div className="border border-[#e0ddd4] bg-[#faf9f3] px-4 py-3 text-xs text-[#5f5b52]">
|
||||
{tLegacy("moreRows", { count: approvals.length - 8 })}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(loading || approvals.length > 0) && (
|
||||
<div className="hidden overflow-x-auto bg-white md:block">
|
||||
<table className="w-full" aria-label={tLegacy("tableLabel")}>
|
||||
<thead>
|
||||
<tr className="border-b border-[#e0ddd4] bg-[#faf9f3]">
|
||||
@@ -719,14 +790,10 @@ function LegacyHitlBacklogPanel({
|
||||
) : (
|
||||
approvals.slice(0, 8).map((approval) => {
|
||||
const risk = approval.risk_level?.toLowerCase() ?? "unknown";
|
||||
const riskClass =
|
||||
risk === "critical"
|
||||
? "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]"
|
||||
: "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]";
|
||||
return (
|
||||
<tr key={approval.id} className="border-b border-[#f0ede5]">
|
||||
<td className="px-4 py-3 align-top">
|
||||
<span className={cn("inline-flex border px-2 py-0.5 text-xs font-semibold", riskClass)}>
|
||||
<span className={cn("inline-flex border px-2 py-0.5 text-xs font-semibold", legacyRiskClass(risk))}>
|
||||
{risk}
|
||||
</span>
|
||||
</td>
|
||||
@@ -1354,6 +1421,95 @@ function ApprovalRow({ approval }: { approval: Approval }) {
|
||||
);
|
||||
}
|
||||
|
||||
function ApprovalMobileCard({ approval }: { approval: Approval }) {
|
||||
const t = useTranslations("awooop.approvals");
|
||||
const tEvidence = useTranslations("awooop.listEvidence");
|
||||
const tStatusChain = useTranslations("awooop.statusChain");
|
||||
const formattedDate = approval.created_at
|
||||
? new Date(approval.created_at).toLocaleDateString("zh-TW", {
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})
|
||||
: "--";
|
||||
const remainingMs = getRemainingMs(approval.timeout_at);
|
||||
const isCritical = remainingMs !== null && remainingMs <= 5 * 60 * 1000;
|
||||
const isGate5Projection = approval.trigger_type === "adr100_runtime_replay_gate5";
|
||||
|
||||
return (
|
||||
<article
|
||||
className={cn(
|
||||
"border border-[#e0ddd4] bg-white p-4 shadow-[0_1px_4px_rgba(0,0,0,0.05)]",
|
||||
isCritical && "border-[#e2a29b] bg-[#fff7f6]"
|
||||
)}
|
||||
>
|
||||
<div className="flex min-w-0 items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("columns.runId")}</p>
|
||||
<Link
|
||||
href={`/awooop/approvals/${approval.run_id}`}
|
||||
className="mt-1 inline-flex max-w-full items-center gap-1.5 border border-brand-accent/20 bg-brand-accent/10 px-2 py-1 font-mono text-xs font-semibold text-brand-accent transition-colors hover:bg-brand-accent/20"
|
||||
aria-label={`開啟審批 ${approval.run_id}`}
|
||||
>
|
||||
<span className="truncate">{approval.run_id.slice(0, 12)}</span>
|
||||
<ArrowRight className="h-3 w-3 shrink-0" aria-hidden="true" />
|
||||
</Link>
|
||||
</div>
|
||||
<DecisionPostureBadge />
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid gap-3 border-t border-[#eee9dd] pt-3 text-xs leading-5 text-[#5f5b52]">
|
||||
<div className="grid grid-cols-[6rem_minmax(0,1fr)] gap-3">
|
||||
<span className="font-semibold text-[#77736a]">{t("columns.projectId")}</span>
|
||||
<span className="min-w-0 break-words font-semibold text-[#141413]">
|
||||
{publicProjectText(approval.project_id)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-[6rem_minmax(0,1fr)] gap-3">
|
||||
<span className="font-semibold text-[#77736a]">{t("columns.agent")}</span>
|
||||
<div className="min-w-0">
|
||||
<span className="break-words font-semibold text-[#141413]">
|
||||
{publicAgentText(approval.agent_id)}
|
||||
</span>
|
||||
{isGate5Projection && <Gate5ProjectionBadge />}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-[6rem_minmax(0,1fr)] gap-3">
|
||||
<span className="font-semibold text-[#77736a]">{t("columns.created")}</span>
|
||||
<span className="font-mono text-[#141413]">{formattedDate}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-[6rem_minmax(0,1fr)] gap-3">
|
||||
<span className="font-semibold text-[#77736a]">{t("columns.remaining")}</span>
|
||||
<TimeoutCell timeoutAt={approval.timeout_at} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid gap-3 border-t border-[#eee9dd] pt-3">
|
||||
<div className="min-w-0 overflow-hidden">
|
||||
<p className="mb-2 text-xs font-semibold text-[#77736a]">{tEvidence("column")}</p>
|
||||
<RemediationEvidenceCell summary={approval.remediation_summary} />
|
||||
</div>
|
||||
<div className="min-w-0 overflow-hidden">
|
||||
<p className="mb-2 text-xs font-semibold text-[#77736a]">{tEvidence("sourceFlow.column")}</p>
|
||||
<ApprovalSourceFlowCell chain={approval.awooop_status_chain} />
|
||||
</div>
|
||||
<div className="min-w-0 overflow-hidden">
|
||||
<p className="mb-2 text-xs font-semibold text-[#77736a]">{t("assetLedger.column")}</p>
|
||||
<AwoooPAutomationAssetLedger
|
||||
chain={approval.awooop_status_chain}
|
||||
remediationSummary={approval.remediation_summary}
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 overflow-hidden">
|
||||
<p className="mb-2 text-xs font-semibold text-[#77736a]">{tStatusChain("title")}</p>
|
||||
<AwoooPStatusChainPanel chain={approval.awooop_status_chain} compact />
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
function SecurityOwnerResponseGatePanel() {
|
||||
const t = useTranslations("awooop.approvals.securityOwnerResponseGate");
|
||||
const metrics = [
|
||||
@@ -2293,7 +2449,28 @@ export default function ApprovalsPage() {
|
||||
|
||||
{/* Table */}
|
||||
{(loading || approvals.length > 0) && (
|
||||
<div className="overflow-hidden border border-[#e0ddd4] bg-white shadow-[0_1px_4px_rgba(0,0,0,0.05)]">
|
||||
<div className="grid gap-3 md:hidden" data-testid="approvals-mobile-list">
|
||||
{loading ? (
|
||||
Array.from({ length: 4 }).map((_, i) => (
|
||||
<div key={i} className="border border-[#e0ddd4] bg-white p-4">
|
||||
<div className="h-5 w-32 animate-pulse rounded bg-muted" />
|
||||
<div className="mt-4 grid gap-3">
|
||||
<div className="h-4 w-full animate-pulse rounded bg-muted" />
|
||||
<div className="h-4 w-3/4 animate-pulse rounded bg-muted" />
|
||||
<div className="h-16 w-full animate-pulse rounded bg-muted" />
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
approvals.map((approval) => (
|
||||
<ApprovalMobileCard key={approval.run_id} approval={approval} />
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(loading || approvals.length > 0) && (
|
||||
<div className="hidden overflow-hidden border border-[#e0ddd4] bg-white shadow-[0_1px_4px_rgba(0,0,0,0.05)] md:block">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full" role="table" aria-label={t("page.title")}>
|
||||
<thead>
|
||||
|
||||
@@ -1550,7 +1550,7 @@ export function AutonomousRuntimeReceiptPanel({
|
||||
<Icon className="h-4 w-4" aria-hidden="true" />
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 line-clamp-2 text-xs leading-5 text-[#5f5b52]">
|
||||
<p className="mt-1 line-clamp-2 break-words text-xs leading-5 text-[#5f5b52]">
|
||||
{card.detail}
|
||||
</p>
|
||||
</div>
|
||||
@@ -1563,7 +1563,7 @@ export function AutonomousRuntimeReceiptPanel({
|
||||
className="border-t border-[#e0ddd4] bg-white"
|
||||
>
|
||||
<div className="flex flex-wrap items-start justify-between gap-3 px-4 py-3">
|
||||
<div className="min-w-0">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Bell className="h-4 w-4 text-[#87867f]" aria-hidden="true" />
|
||||
<h4 className="text-sm font-semibold text-[#141413]">{t("monitoringCoverage.title")}</h4>
|
||||
@@ -1572,7 +1572,7 @@ export function AutonomousRuntimeReceiptPanel({
|
||||
{t("monitoringCoverage.subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
<span className={cn("inline-flex border px-2 py-0.5 text-xs font-semibold", toneClass(
|
||||
<span className={cn("inline-flex max-w-full min-w-0 break-all border px-2 py-0.5 text-left text-xs font-semibold", toneClass(
|
||||
!hasTelegramCoverageReadback
|
||||
? "neutral"
|
||||
: telegramMonitoringCoverageReady ? "ok" : "warn"
|
||||
@@ -1598,7 +1598,7 @@ export function AutonomousRuntimeReceiptPanel({
|
||||
<Icon className="h-4 w-4" aria-hidden="true" />
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 line-clamp-2 text-xs leading-5 text-[#5f5b52]">
|
||||
<p className="mt-1 line-clamp-2 break-words text-xs leading-5 text-[#5f5b52]">
|
||||
{card.detail}
|
||||
</p>
|
||||
</div>
|
||||
@@ -1624,7 +1624,7 @@ export function AutonomousRuntimeReceiptPanel({
|
||||
{telegramCoveragePipeline.slice(0, 7).map((stage) => (
|
||||
<div
|
||||
key={stage.stage_id ?? stage.next_action ?? "stage"}
|
||||
className="flex items-center justify-between gap-3 border border-[#eee9dd] px-2 py-1.5"
|
||||
className="flex min-w-0 items-center justify-between gap-3 border border-[#eee9dd] px-2 py-1.5"
|
||||
>
|
||||
<div className="min-w-0 truncate text-xs font-semibold text-[#141413]">
|
||||
{stage.stage_id ?? "--"}
|
||||
@@ -1656,7 +1656,7 @@ export function AutonomousRuntimeReceiptPanel({
|
||||
className="grid gap-1 border border-[#eee9dd] px-2 py-1.5"
|
||||
>
|
||||
<div className="flex min-w-0 items-center justify-between gap-2">
|
||||
<span className="truncate text-xs font-semibold text-[#141413]">
|
||||
<span className="min-w-0 truncate text-xs font-semibold text-[#141413]">
|
||||
{item.controlled_next_action ?? item.blocker ?? "--"}
|
||||
</span>
|
||||
<span className="font-mono text-xs text-[#77736a]">
|
||||
|
||||
Reference in New Issue
Block a user