fix(web): condense tenants asset cockpit
This commit is contained in:
@@ -10749,6 +10749,9 @@
|
||||
"loading": "讀取中",
|
||||
"heatmapTitle": "產品納管熱力圖",
|
||||
"heatmapDetail": "每個產品以入口、來源與 owner 狀態呈現納管成熟度。",
|
||||
"matrixTitle": "決策支援矩陣",
|
||||
"matrixDetail": "先判斷全域資產是否納入、哪些還缺證據、下一步該往哪個工作區下鑽。",
|
||||
"matrixBadge": "首屏判讀",
|
||||
"remainingProducts": "+{count} 個產品",
|
||||
"drilldownTitle": "關聯工作區",
|
||||
"boundaryTitle": "目前不可誤讀",
|
||||
@@ -10778,6 +10781,23 @@
|
||||
"sources": "來源",
|
||||
"owner": "owner"
|
||||
},
|
||||
"matrix": {
|
||||
"products": "產品 / 專案",
|
||||
"productsStatus": "已進同一張資產地圖",
|
||||
"productsDetail": "所有前後台產品、公開網站與平台工具先收斂到單一納管視圖。",
|
||||
"routes": "網站入口",
|
||||
"routesStatus": "待驗證 {pending}",
|
||||
"routesDetail": "有入口就必須能追到可觀測性、smoke 與 owner 證據。",
|
||||
"sources": "來源範圍",
|
||||
"sourcesStatus": "缺口 {blockers}",
|
||||
"sourcesDetail": "原始碼範圍只顯示脫敏代號,未完成證據前不得切主來源。",
|
||||
"owner": "Owner gate",
|
||||
"ownerStatus": "待回覆 {waiting}",
|
||||
"ownerDetail": "沒有負責人接受紀錄,就不把候選範圍當正式核准。",
|
||||
"runtime": "Runtime gate",
|
||||
"runtimeStatus": "操作入口 {actions}",
|
||||
"runtimeDetail": "這頁只作納管判讀,不提供掃描、部署、修復或主機操作入口。"
|
||||
},
|
||||
"drilldowns": {
|
||||
"observability": "可觀測性",
|
||||
"observabilityDetail": "主機、服務、網站入口、告警與接收證據。",
|
||||
@@ -10855,6 +10875,11 @@
|
||||
"runtimeGate": "執行閘門",
|
||||
"actionButtons": "操作入口"
|
||||
},
|
||||
"details": {
|
||||
"productsSummary": "{count} 個產品 / 專案完整明細,預設收合避免干擾首屏判讀。",
|
||||
"routesSummary": "{count} 個網站 / 服務入口;待驗證 {pending} 個。",
|
||||
"reposSummary": "{count} 個脫敏來源範圍;待補證據 {blockers} 個。"
|
||||
},
|
||||
"columns": {
|
||||
"domain": "網站 / 服務入口",
|
||||
"product": "產品 / 專案",
|
||||
|
||||
@@ -10749,6 +10749,9 @@
|
||||
"loading": "讀取中",
|
||||
"heatmapTitle": "產品納管熱力圖",
|
||||
"heatmapDetail": "每個產品以入口、來源與 owner 狀態呈現納管成熟度。",
|
||||
"matrixTitle": "決策支援矩陣",
|
||||
"matrixDetail": "先判斷全域資產是否納入、哪些還缺證據、下一步該往哪個工作區下鑽。",
|
||||
"matrixBadge": "首屏判讀",
|
||||
"remainingProducts": "+{count} 個產品",
|
||||
"drilldownTitle": "關聯工作區",
|
||||
"boundaryTitle": "目前不可誤讀",
|
||||
@@ -10778,6 +10781,23 @@
|
||||
"sources": "來源",
|
||||
"owner": "owner"
|
||||
},
|
||||
"matrix": {
|
||||
"products": "產品 / 專案",
|
||||
"productsStatus": "已進同一張資產地圖",
|
||||
"productsDetail": "所有前後台產品、公開網站與平台工具先收斂到單一納管視圖。",
|
||||
"routes": "網站入口",
|
||||
"routesStatus": "待驗證 {pending}",
|
||||
"routesDetail": "有入口就必須能追到可觀測性、smoke 與 owner 證據。",
|
||||
"sources": "來源範圍",
|
||||
"sourcesStatus": "缺口 {blockers}",
|
||||
"sourcesDetail": "原始碼範圍只顯示脫敏代號,未完成證據前不得切主來源。",
|
||||
"owner": "Owner gate",
|
||||
"ownerStatus": "待回覆 {waiting}",
|
||||
"ownerDetail": "沒有負責人接受紀錄,就不把候選範圍當正式核准。",
|
||||
"runtime": "Runtime gate",
|
||||
"runtimeStatus": "操作入口 {actions}",
|
||||
"runtimeDetail": "這頁只作納管判讀,不提供掃描、部署、修復或主機操作入口。"
|
||||
},
|
||||
"drilldowns": {
|
||||
"observability": "可觀測性",
|
||||
"observabilityDetail": "主機、服務、網站入口、告警與接收證據。",
|
||||
@@ -10855,6 +10875,11 @@
|
||||
"runtimeGate": "執行閘門",
|
||||
"actionButtons": "操作入口"
|
||||
},
|
||||
"details": {
|
||||
"productsSummary": "{count} 個產品 / 專案完整明細,預設收合避免干擾首屏判讀。",
|
||||
"routesSummary": "{count} 個網站 / 服務入口;待驗證 {pending} 個。",
|
||||
"reposSummary": "{count} 個脫敏來源範圍;待補證據 {blockers} 個。"
|
||||
},
|
||||
"columns": {
|
||||
"domain": "網站 / 服務入口",
|
||||
"product": "產品 / 專案",
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
AlertCircle,
|
||||
DollarSign,
|
||||
Ban,
|
||||
ChevronDown,
|
||||
CheckCircle2,
|
||||
GitBranch,
|
||||
Globe2,
|
||||
@@ -588,7 +589,6 @@ function ProductCommandMap({
|
||||
loading: boolean;
|
||||
}) {
|
||||
const t = useTranslations("awooop.tenants.productCommandMap");
|
||||
const assetT = useTranslations("awooop.tenants.globalAssets");
|
||||
const products = inventory?.products ?? [];
|
||||
const publicRoutes = inventory?.public_routes ?? [];
|
||||
const sourceRepos = inventory?.source_repos ?? [];
|
||||
@@ -604,6 +604,9 @@ function ProductCommandMap({
|
||||
const acceptedOwnerCount = summary?.owner_response_accepted_count ?? 0;
|
||||
const runtimeGateCount = summary?.runtime_gate_count ?? 0;
|
||||
const actionButtonCount = summary?.action_button_count ?? 0;
|
||||
const routeReadyCount = publicRoutes.filter((item) => item.route_smoke_accepted).length;
|
||||
const sourcePrimaryReadyCount = summary?.source_primary_ready_count ?? 0;
|
||||
const sourceCandidateCount = summary?.source_candidate_repo_count ?? sourceRepos.length;
|
||||
const topologyRows = [
|
||||
{
|
||||
key: "products",
|
||||
@@ -668,8 +671,43 @@ function ProductCommandMap({
|
||||
tone: runtimeGateCount === 0 ? "locked" as const : "warn" as const,
|
||||
},
|
||||
];
|
||||
const productPreview = products.slice(0, 12);
|
||||
const remainingProducts = Math.max(products.length - productPreview.length, 0);
|
||||
const decisionRows = [
|
||||
{
|
||||
key: "products",
|
||||
value: `${products.length}`,
|
||||
status: t("matrix.productsStatus"),
|
||||
detail: t("matrix.productsDetail"),
|
||||
tone: "steady" as const,
|
||||
},
|
||||
{
|
||||
key: "routes",
|
||||
value: `${routeReadyCount}/${publicRoutes.length}`,
|
||||
status: t("matrix.routesStatus", { pending: routePendingCount }),
|
||||
detail: t("matrix.routesDetail"),
|
||||
tone: routePendingCount > 0 ? "warn" as const : "steady" as const,
|
||||
},
|
||||
{
|
||||
key: "sources",
|
||||
value: `${sourcePrimaryReadyCount}/${sourceCandidateCount}`,
|
||||
status: t("matrix.sourcesStatus", { blockers: sourceBlockerCount }),
|
||||
detail: t("matrix.sourcesDetail"),
|
||||
tone: sourceBlockerCount > 0 ? "warn" as const : "steady" as const,
|
||||
},
|
||||
{
|
||||
key: "owner",
|
||||
value: `${acceptedOwnerCount}/${products.length}`,
|
||||
status: t("matrix.ownerStatus", { waiting: ownerWaitingCount }),
|
||||
detail: t("matrix.ownerDetail"),
|
||||
tone: "warn" as const,
|
||||
},
|
||||
{
|
||||
key: "runtime",
|
||||
value: `${runtimeGateCount}`,
|
||||
status: t("matrix.runtimeStatus", { actions: actionButtonCount }),
|
||||
detail: t("matrix.runtimeDetail"),
|
||||
tone: "locked" as const,
|
||||
},
|
||||
];
|
||||
const drilldowns = [
|
||||
{
|
||||
href: "/observability",
|
||||
@@ -754,52 +792,32 @@ function ProductCommandMap({
|
||||
<div className="min-w-0 bg-[#faf9f3] p-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("heatmapTitle")}</p>
|
||||
<p className="mt-1 text-sm font-semibold text-[#141413]">{t("heatmapDetail")}</p>
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("matrixTitle")}</p>
|
||||
<p className="mt-1 text-sm font-semibold text-[#141413]">{t("matrixDetail")}</p>
|
||||
</div>
|
||||
{remainingProducts > 0 && (
|
||||
<span className="border border-[#d8d3c7] bg-white px-2 py-1 text-xs font-semibold text-[#5f5b52]">
|
||||
{t("remainingProducts", { count: remainingProducts })}
|
||||
</span>
|
||||
)}
|
||||
<span className="border border-[#d8d3c7] bg-white px-2 py-1 text-xs font-semibold text-[#5f5b52]">
|
||||
{t("matrixBadge")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid gap-2 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{productPreview.map((item, index) => {
|
||||
const statusTone = ASSET_STATUS_TONES[item.coverage_status] ?? "locked";
|
||||
return (
|
||||
<article
|
||||
key={`${item.product_id}:${index}`}
|
||||
className="min-w-0 border border-[#e0ddd4] bg-white px-3 py-3"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<p className="break-words text-sm font-semibold text-[#141413]">
|
||||
{assetPublicProductName(item, index)}
|
||||
</p>
|
||||
<p className="mt-1 font-mono text-[11px] text-[#77736a]">
|
||||
{assetPublicCode(index)} · {assetCategoryLabel(item.category, assetT)}
|
||||
</p>
|
||||
</div>
|
||||
<span className={cn("h-3 w-3 shrink-0 border", assetToneClass(statusTone))} aria-hidden="true" />
|
||||
</div>
|
||||
<div className="mt-3 grid grid-cols-3 gap-1.5 text-[11px]">
|
||||
<div className="border border-[#eee9dd] bg-[#faf9f3] px-2 py-1">
|
||||
<p className="text-[#77736a]">{t("mini.routes")}</p>
|
||||
<p className="font-mono font-semibold text-[#141413]">{item.public_route_count}</p>
|
||||
</div>
|
||||
<div className="border border-[#eee9dd] bg-[#faf9f3] px-2 py-1">
|
||||
<p className="text-[#77736a]">{t("mini.sources")}</p>
|
||||
<p className="font-mono font-semibold text-[#141413]">{item.source_repo_count}</p>
|
||||
</div>
|
||||
<div className="border border-[#eee9dd] bg-[#faf9f3] px-2 py-1">
|
||||
<p className="text-[#77736a]">{t("mini.owner")}</p>
|
||||
<p className="font-mono font-semibold text-[#141413]">{item.owner_response_accepted_count}</p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
<div className="mt-4 grid gap-2">
|
||||
{decisionRows.map(({ key, value, status, detail, tone }) => (
|
||||
<div
|
||||
key={key}
|
||||
className="grid gap-3 border border-[#e0ddd4] bg-white px-3 py-3 sm:grid-cols-[112px_minmax(0,1fr)_minmax(0,1.2fr)] sm:items-center"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<p className="text-[11px] font-semibold text-[#77736a]">
|
||||
{t(`matrix.${key}` as never)}
|
||||
</p>
|
||||
<p className="mt-1 font-mono text-2xl font-semibold text-[#141413]">{value}</p>
|
||||
</div>
|
||||
<div className={cn("min-w-0 border px-2 py-1 text-xs font-semibold", assetToneClass(tone))}>
|
||||
{status}
|
||||
</div>
|
||||
<p className="min-w-0 text-xs leading-5 text-[#5f5b52]">{detail}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1438,13 +1456,19 @@ function GlobalAssetCoveragePanel({ inventory }: { inventory: TenantAssetInvento
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 p-4 xl:grid-cols-[minmax(0,1.05fr)_minmax(0,0.95fr)]">
|
||||
<div className="min-w-0 border border-[#e0ddd4] bg-white">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-[#e0ddd4] bg-[#faf9f3] px-4 py-3">
|
||||
<p className="text-xs font-semibold text-[#141413]">{t("productsTitle")}</p>
|
||||
<span className="font-mono text-xs text-[#77736a]">
|
||||
{products.length} {t("itemsUnit")}
|
||||
<details className="group min-w-0 overflow-hidden border border-[#e0ddd4] bg-white">
|
||||
<summary className="flex cursor-pointer list-none flex-wrap items-center justify-between gap-2 border-b border-[#e0ddd4] bg-[#faf9f3] px-4 py-3 [&::-webkit-details-marker]:hidden">
|
||||
<span className="min-w-0">
|
||||
<span className="block text-xs font-semibold text-[#141413]">{t("productsTitle")}</span>
|
||||
<span className="mt-1 block text-[11px] text-[#77736a]">
|
||||
{t("details.productsSummary", { count: products.length })}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<span className="inline-flex items-center gap-2 font-mono text-xs font-semibold text-[#5f5b52]">
|
||||
{products.length} {t("itemsUnit")}
|
||||
<ChevronDown className="h-4 w-4 transition-transform group-open:rotate-180" aria-hidden="true" />
|
||||
</span>
|
||||
</summary>
|
||||
<div className="grid gap-px bg-[#eee9dd] md:grid-cols-2">
|
||||
{products.map((item, index) => (
|
||||
<article key={item.product_id} className="min-w-0 bg-white px-4 py-3">
|
||||
@@ -1479,7 +1503,7 @@ function GlobalAssetCoveragePanel({ inventory }: { inventory: TenantAssetInvento
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div className="min-w-0 border border-[#e0ddd4] bg-[#faf9f3] px-4 py-3">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("boundaryTitle")}</p>
|
||||
@@ -1501,10 +1525,16 @@ function GlobalAssetCoveragePanel({ inventory }: { inventory: TenantAssetInvento
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 border-t border-[#e0ddd4] bg-[#faf9f3] p-4 xl:grid-cols-2">
|
||||
<div className="min-w-0 overflow-hidden border border-[#e0ddd4] bg-white">
|
||||
<div className="border-b border-[#e0ddd4] px-4 py-3 text-xs font-semibold text-[#141413]">
|
||||
{t("routesTitle")}
|
||||
</div>
|
||||
<details className="group min-w-0 overflow-hidden border border-[#e0ddd4] bg-white">
|
||||
<summary className="flex cursor-pointer list-none flex-wrap items-center justify-between gap-2 border-b border-[#e0ddd4] px-4 py-3 [&::-webkit-details-marker]:hidden">
|
||||
<span className="min-w-0">
|
||||
<span className="block text-xs font-semibold text-[#141413]">{t("routesTitle")}</span>
|
||||
<span className="mt-1 block text-[11px] text-[#77736a]">
|
||||
{t("details.routesSummary", { count: publicRoutes.length, pending: pendingRouteSmokeCount })}
|
||||
</span>
|
||||
</span>
|
||||
<ChevronDown className="h-4 w-4 text-[#5f5b52] transition-transform group-open:rotate-180" aria-hidden="true" />
|
||||
</summary>
|
||||
<div data-testid="awooop-route-card-grid" className="grid gap-2 p-3 xl:hidden">
|
||||
{publicRoutes.map((route, index) => (
|
||||
<article key={route.domain} className="border border-[#eee9dd] bg-[#faf9f3] px-3 py-3">
|
||||
@@ -1568,12 +1598,18 @@ function GlobalAssetCoveragePanel({ inventory }: { inventory: TenantAssetInvento
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div className="min-w-0 overflow-hidden border border-[#e0ddd4] bg-white">
|
||||
<div className="border-b border-[#e0ddd4] px-4 py-3 text-xs font-semibold text-[#141413]">
|
||||
{t("reposTitle")}
|
||||
</div>
|
||||
<details className="group min-w-0 overflow-hidden border border-[#e0ddd4] bg-white">
|
||||
<summary className="flex cursor-pointer list-none flex-wrap items-center justify-between gap-2 border-b border-[#e0ddd4] px-4 py-3 [&::-webkit-details-marker]:hidden">
|
||||
<span className="min-w-0">
|
||||
<span className="block text-xs font-semibold text-[#141413]">{t("reposTitle")}</span>
|
||||
<span className="mt-1 block text-[11px] text-[#77736a]">
|
||||
{t("details.reposSummary", { count: sourceRepos.length, blockers: sourceBlockerCount })}
|
||||
</span>
|
||||
</span>
|
||||
<ChevronDown className="h-4 w-4 text-[#5f5b52] transition-transform group-open:rotate-180" aria-hidden="true" />
|
||||
</summary>
|
||||
<div data-testid="awooop-source-card-grid" className="grid gap-2 p-3 xl:hidden">
|
||||
{sourceRepos.map((repo, index) => (
|
||||
<article
|
||||
@@ -1660,7 +1696,7 @@ function GlobalAssetCoveragePanel({ inventory }: { inventory: TenantAssetInvento
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user