feat(awooop): expose controlled execution preflight
This commit is contained in:
@@ -4292,6 +4292,144 @@ def _apply_gate_closure_tasks(
|
||||
]
|
||||
|
||||
|
||||
def _apply_gate_controlled_execution_preflight(
|
||||
*,
|
||||
source_ref: str,
|
||||
safe_source_ref: str,
|
||||
catalog_id: str,
|
||||
check_mode_playbook: Any,
|
||||
apply_playbook: Any,
|
||||
dry_run_passed: bool,
|
||||
owner_release_package: dict[str, Any],
|
||||
verifier_package: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
"""Describe the no-write route that would become executable after release gates."""
|
||||
|
||||
owner_release_approved = _safe_int(
|
||||
owner_release_package.get("owner_release_approved_count")
|
||||
)
|
||||
maintenance_approved = _safe_int(
|
||||
owner_release_package.get("maintenance_window_approved_count")
|
||||
)
|
||||
rollback_confirmed = _safe_int(
|
||||
owner_release_package.get("rollback_owner_confirmed_count")
|
||||
)
|
||||
verifier_ready = _safe_int(verifier_package.get("post_release_verifier_ready_count"))
|
||||
route_candidate_ready = (
|
||||
dry_run_passed
|
||||
and bool(catalog_id)
|
||||
and str(apply_playbook or "").strip()
|
||||
and str(apply_playbook or "").strip() != "--"
|
||||
)
|
||||
prerequisites = [
|
||||
{
|
||||
"key": "dry_run_passed",
|
||||
"status": "passed" if dry_run_passed else "blocked_missing_dry_run",
|
||||
"detail": f"dry_run_passed={str(dry_run_passed).lower()}",
|
||||
"required": True,
|
||||
},
|
||||
{
|
||||
"key": "allowlisted_route_candidate",
|
||||
"status": (
|
||||
"candidate_ready_no_runtime_authority"
|
||||
if route_candidate_ready
|
||||
else "route_missing"
|
||||
),
|
||||
"detail": f"catalog={catalog_id}",
|
||||
"required": True,
|
||||
},
|
||||
{
|
||||
"key": "owner_release_receipt",
|
||||
"status": (
|
||||
"passed" if owner_release_approved > 0 else "blocked_missing_owner_release"
|
||||
),
|
||||
"detail": f"approved={owner_release_approved}",
|
||||
"required": True,
|
||||
},
|
||||
{
|
||||
"key": "maintenance_window",
|
||||
"status": (
|
||||
"passed"
|
||||
if maintenance_approved > 0
|
||||
else "blocked_missing_maintenance_window"
|
||||
),
|
||||
"detail": f"approved={maintenance_approved}",
|
||||
"required": True,
|
||||
},
|
||||
{
|
||||
"key": "rollback_owner",
|
||||
"status": (
|
||||
"passed" if rollback_confirmed > 0 else "blocked_missing_rollback_owner"
|
||||
),
|
||||
"detail": f"confirmed={rollback_confirmed}",
|
||||
"required": True,
|
||||
},
|
||||
{
|
||||
"key": "post_apply_verifier",
|
||||
"status": (
|
||||
"passed"
|
||||
if verifier_ready > 0
|
||||
else "blocked_missing_post_apply_verifier"
|
||||
),
|
||||
"detail": f"ready={verifier_ready}",
|
||||
"required": True,
|
||||
},
|
||||
{
|
||||
"key": "km_playbook_writeback",
|
||||
"status": "blocked_until_verified_execution",
|
||||
"detail": "km_write=0; playbook_trust_write=0",
|
||||
"required": True,
|
||||
},
|
||||
]
|
||||
ready_count = sum(
|
||||
1
|
||||
for item in prerequisites
|
||||
if item["status"] in {"passed", "candidate_ready_no_runtime_authority"}
|
||||
)
|
||||
blocked_count = len(prerequisites) - ready_count
|
||||
route_count = 1 if route_candidate_ready else 0
|
||||
return {
|
||||
"schema_version": "awooop_controlled_execution_preflight_v1",
|
||||
"status": "blocked_before_runtime_gate",
|
||||
"source_id": source_ref,
|
||||
"work_item_id": f"controlled-execution-gate:awoooi:{safe_source_ref}",
|
||||
"runtime_execution_authorized": False,
|
||||
"runtime_write_allowed": False,
|
||||
"allowed_route_count": 0,
|
||||
"candidate_route_count": route_count,
|
||||
"ready_count": ready_count,
|
||||
"total_count": len(prerequisites),
|
||||
"blocked_count": blocked_count,
|
||||
"next_action": "collect_owner_release_maintenance_rollback_and_verifier",
|
||||
"blocked_reason": "owner_release_or_verifier_gate_missing",
|
||||
"routes": [
|
||||
{
|
||||
"route_id": f"ansible-allowlisted-apply:{catalog_id}",
|
||||
"transport": "ansible",
|
||||
"status": (
|
||||
"candidate_ready_no_runtime_authority"
|
||||
if route_candidate_ready
|
||||
else "route_missing"
|
||||
),
|
||||
"source_asset_id": f"ansible-apply-candidate:{catalog_id}",
|
||||
"check_mode_playbook_path": check_mode_playbook,
|
||||
"apply_playbook_path": apply_playbook,
|
||||
"allowed": False,
|
||||
"blocker": "runtime_gate_closed_until_owner_release_and_verifier",
|
||||
}
|
||||
],
|
||||
"prerequisites": prerequisites,
|
||||
"forbidden_until_released": [
|
||||
"ansible_apply",
|
||||
"ssh_write",
|
||||
"service_restart",
|
||||
"telegram_send",
|
||||
"km_writeback",
|
||||
"playbook_trust_writeback",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _status_chain_ansible_apply_gate_handoff(
|
||||
*,
|
||||
ansible_dry_run_only: bool,
|
||||
@@ -4406,6 +4544,16 @@ def _status_chain_ansible_apply_gate_handoff(
|
||||
owner_release_package=owner_release_package,
|
||||
verifier_package=verifier_package,
|
||||
)
|
||||
controlled_execution_preflight = _apply_gate_controlled_execution_preflight(
|
||||
source_ref=str(source_ref),
|
||||
safe_source_ref=safe_source_ref,
|
||||
catalog_id=str(catalog_id),
|
||||
check_mode_playbook=check_mode_playbook,
|
||||
apply_playbook=apply_playbook,
|
||||
dry_run_passed=dry_run_passed,
|
||||
owner_release_package=owner_release_package,
|
||||
verifier_package=verifier_package,
|
||||
)
|
||||
|
||||
return {
|
||||
"schema_version": "awooop_automation_handoff_v1",
|
||||
@@ -4473,6 +4621,7 @@ def _status_chain_ansible_apply_gate_handoff(
|
||||
"owner_release_package": owner_release_package,
|
||||
"release_verifier_package": verifier_package,
|
||||
"closure_tasks": closure_tasks,
|
||||
"controlled_execution_preflight": controlled_execution_preflight,
|
||||
},
|
||||
"candidate": {
|
||||
"catalog_id": catalog_id,
|
||||
|
||||
@@ -1901,6 +1901,34 @@ def test_awooop_status_chain_does_not_treat_ansible_check_mode_as_repair() -> No
|
||||
assert closure["closure_tasks"][3]["source_asset_id"] == (
|
||||
"agent-result-capture-release-verifier-preflight-gate:P2-136"
|
||||
)
|
||||
controlled_execution = closure["controlled_execution_preflight"]
|
||||
assert controlled_execution["schema_version"] == (
|
||||
"awooop_controlled_execution_preflight_v1"
|
||||
)
|
||||
assert controlled_execution["status"] == "blocked_before_runtime_gate"
|
||||
assert controlled_execution["runtime_execution_authorized"] is False
|
||||
assert controlled_execution["runtime_write_allowed"] is False
|
||||
assert controlled_execution["candidate_route_count"] == 1
|
||||
assert controlled_execution["allowed_route_count"] == 0
|
||||
assert controlled_execution["ready_count"] == 2
|
||||
assert controlled_execution["total_count"] == 7
|
||||
assert controlled_execution["blocked_count"] == 5
|
||||
assert [item["key"] for item in controlled_execution["prerequisites"]] == [
|
||||
"dry_run_passed",
|
||||
"allowlisted_route_candidate",
|
||||
"owner_release_receipt",
|
||||
"maintenance_window",
|
||||
"rollback_owner",
|
||||
"post_apply_verifier",
|
||||
"km_playbook_writeback",
|
||||
]
|
||||
assert controlled_execution["routes"][0]["route_id"] == (
|
||||
"ansible-allowlisted-apply:ansible:188-ai-web"
|
||||
)
|
||||
assert controlled_execution["routes"][0]["allowed"] is False
|
||||
assert controlled_execution["routes"][0]["apply_playbook_path"] == (
|
||||
"infra/ansible/playbooks/188-ai-web.yml"
|
||||
)
|
||||
assert chain["execution"]["ansible"]["check_mode_total"] == 1
|
||||
assert chain["execution"]["ansible"]["apply_total"] == 0
|
||||
assert chain["execution"]["ansible"]["applied"] is False
|
||||
|
||||
@@ -10303,6 +10303,12 @@
|
||||
"closureTaskBoardTitle": "閉環任務板",
|
||||
"closureTaskOwner": "Owner:{owner}",
|
||||
"runtimeWrite": "runtime_write={value}",
|
||||
"controlledExecutionPreflightTitle": "受控執行前檢",
|
||||
"controlledCandidateRoutes": "候選路由",
|
||||
"controlledAllowedRoutes": "允許路由",
|
||||
"controlledBlockedCount": "阻擋前置",
|
||||
"controlledRouteTitle": "Allowlisted route",
|
||||
"controlledAllowed": "allowed={value}",
|
||||
"checklistTitle": "Owner 審查清單",
|
||||
"forbiddenTitle": "禁止動作",
|
||||
"gates": {
|
||||
@@ -10332,6 +10338,15 @@
|
||||
"postApplyVerifierPreflight": "套用後 Verifier",
|
||||
"kmPlaybookTrustWritebackPlan": "KM / PlayBook 回寫"
|
||||
},
|
||||
"controlledPrerequisites": {
|
||||
"dryRunPassed": "乾跑通過",
|
||||
"allowlistedRouteCandidate": "允許路由候選",
|
||||
"ownerReleaseReceipt": "Owner 放行回執",
|
||||
"maintenanceWindow": "維護窗口",
|
||||
"rollbackOwner": "Rollback Owner",
|
||||
"postApplyVerifier": "套用後 Verifier",
|
||||
"kmPlaybookWriteback": "KM / PlayBook 回寫"
|
||||
},
|
||||
"closureStatuses": {
|
||||
"blockedBeforeOwnerRelease": "Owner 放行前受阻",
|
||||
"noWriteRehearsal": "無寫入演練",
|
||||
@@ -10343,7 +10358,15 @@
|
||||
"blockedByPolicy": "政策阻擋",
|
||||
"blockedBeforeRuntimeGate": "Runtime gate 前受阻",
|
||||
"blockedUntilVerifierPasses": "Verifier 通過前受阻",
|
||||
"snapshotUnavailable": "快照不可用"
|
||||
"snapshotUnavailable": "快照不可用",
|
||||
"candidateReadyNoRuntimeAuthority": "候選就緒但未授權",
|
||||
"routeMissing": "路由缺失",
|
||||
"blockedMissingDryRun": "缺乾跑",
|
||||
"blockedMissingOwnerRelease": "缺 Owner 放行",
|
||||
"blockedMissingMaintenanceWindow": "缺維護窗口",
|
||||
"blockedMissingRollbackOwner": "缺 Rollback Owner",
|
||||
"blockedMissingPostApplyVerifier": "缺套用後 Verifier",
|
||||
"blockedUntilVerifiedExecution": "驗證執行前受阻"
|
||||
},
|
||||
"statuses": {
|
||||
"passed": "已通過",
|
||||
|
||||
@@ -10303,6 +10303,12 @@
|
||||
"closureTaskBoardTitle": "閉環任務板",
|
||||
"closureTaskOwner": "Owner:{owner}",
|
||||
"runtimeWrite": "runtime_write={value}",
|
||||
"controlledExecutionPreflightTitle": "受控執行前檢",
|
||||
"controlledCandidateRoutes": "候選路由",
|
||||
"controlledAllowedRoutes": "允許路由",
|
||||
"controlledBlockedCount": "阻擋前置",
|
||||
"controlledRouteTitle": "Allowlisted route",
|
||||
"controlledAllowed": "allowed={value}",
|
||||
"checklistTitle": "Owner 審查清單",
|
||||
"forbiddenTitle": "禁止動作",
|
||||
"gates": {
|
||||
@@ -10332,6 +10338,15 @@
|
||||
"postApplyVerifierPreflight": "套用後 Verifier",
|
||||
"kmPlaybookTrustWritebackPlan": "KM / PlayBook 回寫"
|
||||
},
|
||||
"controlledPrerequisites": {
|
||||
"dryRunPassed": "乾跑通過",
|
||||
"allowlistedRouteCandidate": "允許路由候選",
|
||||
"ownerReleaseReceipt": "Owner 放行回執",
|
||||
"maintenanceWindow": "維護窗口",
|
||||
"rollbackOwner": "Rollback Owner",
|
||||
"postApplyVerifier": "套用後 Verifier",
|
||||
"kmPlaybookWriteback": "KM / PlayBook 回寫"
|
||||
},
|
||||
"closureStatuses": {
|
||||
"blockedBeforeOwnerRelease": "Owner 放行前受阻",
|
||||
"noWriteRehearsal": "無寫入演練",
|
||||
@@ -10343,7 +10358,15 @@
|
||||
"blockedByPolicy": "政策阻擋",
|
||||
"blockedBeforeRuntimeGate": "Runtime gate 前受阻",
|
||||
"blockedUntilVerifierPasses": "Verifier 通過前受阻",
|
||||
"snapshotUnavailable": "快照不可用"
|
||||
"snapshotUnavailable": "快照不可用",
|
||||
"candidateReadyNoRuntimeAuthority": "候選就緒但未授權",
|
||||
"routeMissing": "路由缺失",
|
||||
"blockedMissingDryRun": "缺乾跑",
|
||||
"blockedMissingOwnerRelease": "缺 Owner 放行",
|
||||
"blockedMissingMaintenanceWindow": "缺維護窗口",
|
||||
"blockedMissingRollbackOwner": "缺 Rollback Owner",
|
||||
"blockedMissingPostApplyVerifier": "缺套用後 Verifier",
|
||||
"blockedUntilVerifiedExecution": "驗證執行前受阻"
|
||||
},
|
||||
"statuses": {
|
||||
"passed": "已通過",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Activity, BookOpenCheck, CheckCircle2, Link2, RadioTower, Route, ShieldAlert, TriangleAlert, Wrench } from "lucide-react";
|
||||
import { Activity, BookOpenCheck, CheckCircle2, Link2, RadioTower, Route, ShieldAlert, ShieldCheck, TriangleAlert, Wrench } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { Link } from "@/i18n/routing";
|
||||
@@ -235,6 +235,38 @@ export interface AwoooPStatusChain {
|
||||
next_step?: string | null;
|
||||
runtime_write_allowed?: boolean | null;
|
||||
}>;
|
||||
controlled_execution_preflight?: {
|
||||
schema_version?: string | null;
|
||||
status?: string | null;
|
||||
source_id?: string | null;
|
||||
work_item_id?: string | null;
|
||||
runtime_execution_authorized?: boolean | null;
|
||||
runtime_write_allowed?: boolean | null;
|
||||
allowed_route_count?: number | null;
|
||||
candidate_route_count?: number | null;
|
||||
ready_count?: number | null;
|
||||
total_count?: number | null;
|
||||
blocked_count?: number | null;
|
||||
next_action?: string | null;
|
||||
blocked_reason?: string | null;
|
||||
routes?: Array<{
|
||||
route_id?: string | null;
|
||||
transport?: string | null;
|
||||
status?: string | null;
|
||||
source_asset_id?: string | null;
|
||||
check_mode_playbook_path?: string | null;
|
||||
apply_playbook_path?: string | null;
|
||||
allowed?: boolean | null;
|
||||
blocker?: string | null;
|
||||
}>;
|
||||
prerequisites?: Array<{
|
||||
key?: string | null;
|
||||
status?: string | null;
|
||||
detail?: string | null;
|
||||
required?: boolean | null;
|
||||
}>;
|
||||
forbidden_until_released?: string[];
|
||||
};
|
||||
};
|
||||
candidate?: {
|
||||
catalog_id?: string | null;
|
||||
@@ -543,6 +575,10 @@ export function AwoooPStatusChainPanel({
|
||||
const closureOwnerPackage = closureReadiness?.owner_release_package;
|
||||
const closureVerifierPackage = closureReadiness?.release_verifier_package;
|
||||
const closureTasks = closureReadiness?.closure_tasks ?? [];
|
||||
const controlledExecutionPreflight = closureReadiness?.controlled_execution_preflight;
|
||||
const controlledExecutionRoutes = controlledExecutionPreflight?.routes ?? [];
|
||||
const controlledExecutionPrerequisites =
|
||||
controlledExecutionPreflight?.prerequisites ?? [];
|
||||
const ownerReviewChecklist = automationHandoff?.owner_review_checklist ?? [];
|
||||
const forbiddenActions = automationHandoff?.forbidden_actions ?? [];
|
||||
const sourceToolchainTone: SourceFlowTone = sourceCorrelation
|
||||
@@ -688,6 +724,14 @@ export function AwoooPStatusChainPanel({
|
||||
blocked_before_runtime_gate: t("applyGate.closureStatuses.blockedBeforeRuntimeGate"),
|
||||
blocked_until_verifier_passes: t("applyGate.closureStatuses.blockedUntilVerifierPasses"),
|
||||
snapshot_unavailable: t("applyGate.closureStatuses.snapshotUnavailable"),
|
||||
candidate_ready_no_runtime_authority: t("applyGate.closureStatuses.candidateReadyNoRuntimeAuthority"),
|
||||
route_missing: t("applyGate.closureStatuses.routeMissing"),
|
||||
blocked_missing_dry_run: t("applyGate.closureStatuses.blockedMissingDryRun"),
|
||||
blocked_missing_owner_release: t("applyGate.closureStatuses.blockedMissingOwnerRelease"),
|
||||
blocked_missing_maintenance_window: t("applyGate.closureStatuses.blockedMissingMaintenanceWindow"),
|
||||
blocked_missing_rollback_owner: t("applyGate.closureStatuses.blockedMissingRollbackOwner"),
|
||||
blocked_missing_post_apply_verifier: t("applyGate.closureStatuses.blockedMissingPostApplyVerifier"),
|
||||
blocked_until_verified_execution: t("applyGate.closureStatuses.blockedUntilVerifiedExecution"),
|
||||
};
|
||||
return labels[String(status ?? "")] ?? handoffStatusLabel(status);
|
||||
};
|
||||
@@ -701,6 +745,18 @@ export function AwoooPStatusChainPanel({
|
||||
};
|
||||
return labels[String(key ?? "")] ?? valueOrEmpty(key, emptyLabel);
|
||||
};
|
||||
const controlledExecutionPrerequisiteLabel = (key: string | null | undefined) => {
|
||||
const labels: Record<string, string> = {
|
||||
dry_run_passed: t("applyGate.controlledPrerequisites.dryRunPassed"),
|
||||
allowlisted_route_candidate: t("applyGate.controlledPrerequisites.allowlistedRouteCandidate"),
|
||||
owner_release_receipt: t("applyGate.controlledPrerequisites.ownerReleaseReceipt"),
|
||||
maintenance_window: t("applyGate.controlledPrerequisites.maintenanceWindow"),
|
||||
rollback_owner: t("applyGate.controlledPrerequisites.rollbackOwner"),
|
||||
post_apply_verifier: t("applyGate.controlledPrerequisites.postApplyVerifier"),
|
||||
km_playbook_writeback: t("applyGate.controlledPrerequisites.kmPlaybookWriteback"),
|
||||
};
|
||||
return labels[String(key ?? "")] ?? valueOrEmpty(key, emptyLabel);
|
||||
};
|
||||
const handoffWorkItemHref = automationHandoff?.work_item_id
|
||||
? `/awooop/work-items?project_id=awoooi&work_item_id=${encodeURIComponent(automationHandoff.work_item_id)}${automationHandoff.source_id ? `&incident_id=${encodeURIComponent(automationHandoff.source_id)}` : ""}`
|
||||
: null;
|
||||
@@ -722,9 +778,10 @@ export function AwoooPStatusChainPanel({
|
||||
return labels[String(status ?? "")] ?? valueOrEmpty(status, emptyLabel);
|
||||
};
|
||||
const handoffTone = (status: string | null | undefined): SourceFlowTone => {
|
||||
if (status === "passed") return "success";
|
||||
if (status === "blocked") return "blocked";
|
||||
if (status === "warning") return "warning";
|
||||
const normalized = String(status ?? "");
|
||||
if (normalized === "passed") return "success";
|
||||
if (normalized === "warning" || normalized.includes("candidate_ready")) return "warning";
|
||||
if (normalized === "blocked" || normalized.startsWith("blocked") || normalized === "route_missing") return "blocked";
|
||||
return "neutral";
|
||||
};
|
||||
const drilldownItems = [
|
||||
@@ -1123,6 +1180,109 @@ export function AwoooPStatusChainPanel({
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{controlledExecutionPreflight ? (
|
||||
<>
|
||||
<div className="border-t border-[#e0ddd4] bg-[#fbfaf5] px-4 py-2">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-semibold text-[#141413]">{t("applyGate.controlledExecutionPreflightTitle")}</p>
|
||||
<p className="mt-1 truncate font-mono text-[11px] text-[#77736a]" title={valueOrEmpty(controlledExecutionPreflight.blocked_reason, emptyLabel)}>
|
||||
{valueOrEmpty(controlledExecutionPreflight.blocked_reason, emptyLabel)}
|
||||
</p>
|
||||
</div>
|
||||
<span className="border border-[#e2a29b] bg-[#fff0ef] px-2 py-0.5 font-mono text-[11px] font-semibold text-[#9f2f25]">
|
||||
{closureStatusLabel(controlledExecutionPreflight.status)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-px bg-[#e0ddd4] md:grid-cols-4">
|
||||
{[
|
||||
{
|
||||
key: "candidate",
|
||||
label: t("applyGate.controlledCandidateRoutes"),
|
||||
value: controlledExecutionPreflight.candidate_route_count ?? 0,
|
||||
},
|
||||
{
|
||||
key: "allowed",
|
||||
label: t("applyGate.controlledAllowedRoutes"),
|
||||
value: controlledExecutionPreflight.allowed_route_count ?? 0,
|
||||
},
|
||||
{
|
||||
key: "blocked",
|
||||
label: t("applyGate.controlledBlockedCount"),
|
||||
value: controlledExecutionPreflight.blocked_count ?? 0,
|
||||
},
|
||||
{
|
||||
key: "runtime",
|
||||
label: t("applyGate.runtimeWrite", {
|
||||
value: boolValue(controlledExecutionPreflight.runtime_write_allowed, emptyLabel),
|
||||
}),
|
||||
value: boolValue(controlledExecutionPreflight.runtime_execution_authorized, emptyLabel),
|
||||
},
|
||||
].map((item) => (
|
||||
<div key={item.key} className="min-w-0 bg-white px-4 py-3">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{item.label}</p>
|
||||
<p className="mt-1 truncate font-mono text-lg font-semibold text-[#141413]" title={String(item.value)}>
|
||||
{item.value}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid gap-px bg-[#e0ddd4] lg:grid-cols-7">
|
||||
{(controlledExecutionPrerequisites.length ? controlledExecutionPrerequisites : [{ key: emptyLabel, status: emptyLabel, detail: emptyLabel, required: true }]).map((item, index) => (
|
||||
<div key={`${item.key}-${index}`} 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(handoffTone(item.status))
|
||||
)}>
|
||||
<ShieldCheck className="h-4 w-4" aria-hidden="true" />
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{controlledExecutionPrerequisiteLabel(item.key)}</p>
|
||||
<p className="mt-1 truncate font-mono text-sm font-semibold text-[#141413]" title={closureStatusLabel(item.status)}>
|
||||
{closureStatusLabel(item.status)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-2 truncate font-mono text-xs text-[#5f5b52]" title={valueOrEmpty(item.detail, emptyLabel)}>
|
||||
{valueOrEmpty(item.detail, emptyLabel)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid gap-px bg-[#e0ddd4]">
|
||||
{(controlledExecutionRoutes.length ? controlledExecutionRoutes : [{ route_id: emptyLabel, transport: emptyLabel, status: emptyLabel, source_asset_id: emptyLabel, check_mode_playbook_path: emptyLabel, apply_playbook_path: emptyLabel, allowed: false, blocker: emptyLabel }]).map((route, index) => (
|
||||
<div key={`${route.route_id}-${index}`} className="min-w-0 bg-white px-4 py-3">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("applyGate.controlledRouteTitle")}</p>
|
||||
<p className="mt-1 break-all font-mono text-sm font-semibold text-[#141413]" title={valueOrEmpty(route.route_id, emptyLabel)}>
|
||||
{valueOrEmpty(route.route_id, emptyLabel)}
|
||||
</p>
|
||||
</div>
|
||||
<span className="shrink-0 border border-[#e2a29b] bg-[#fff0ef] px-2 py-0.5 font-mono text-[11px] text-[#9f2f25]">
|
||||
{t("applyGate.controlledAllowed", {
|
||||
value: boolValue(route.allowed, emptyLabel),
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-3 grid gap-2 lg:grid-cols-3">
|
||||
<span className="min-w-0 border border-[#ece8dd] bg-[#faf9f3] px-2 py-1 font-mono text-[11px] text-[#141413]">
|
||||
{valueOrEmpty(route.transport, emptyLabel)} · {closureStatusLabel(route.status)}
|
||||
</span>
|
||||
<span className="min-w-0 break-all border border-[#ece8dd] bg-[#faf9f3] px-2 py-1 font-mono text-[11px] text-[#141413]">
|
||||
{valueOrEmpty(route.apply_playbook_path, emptyLabel)}
|
||||
</span>
|
||||
<span className="min-w-0 break-all border border-[#ece8dd] bg-[#faf9f3] px-2 py-1 font-mono text-[11px] text-[#141413]">
|
||||
{valueOrEmpty(route.blocker, emptyLabel)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
<div className="grid gap-px bg-[#e0ddd4] lg:grid-cols-2">
|
||||
<div className="min-w-0 bg-white px-4 py-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
|
||||
Reference in New Issue
Block a user