fix: skip decommission status for unsupported topology (#199)

This commit is contained in:
ccccpj
2026-08-07 13:45:08 +08:00
committed by GitHub
parent 37cfdc0fe5
commit 737294c1b8
4 changed files with 145 additions and 12 deletions
+17 -11
View File
@@ -17,6 +17,7 @@ import { usePoolOperations } from "@/hooks/use-pool-operations"
import { useDialog } from "@/lib/feedback/dialog"
import { useMessage } from "@/lib/feedback/message"
import { formatDateTime, formatInteger, niceBytes } from "@/lib/functions"
import { loadPoolDecommissionData } from "@/lib/pool-decommission-load"
import {
deriveDecommissionDisplayState,
deriveRebalanceDisplayState,
@@ -219,18 +220,17 @@ export default function PoolDecommissionPage() {
setError(null)
setRebalanceError(null)
try {
const [overviewResult, rebalanceResult, decommissionResult] = await Promise.allSettled([
getPoolsOverview(),
getRebalanceStatus(),
getDecommissionStatuses(),
])
const {
overview: nextOverview,
rebalanceResult,
decommissionStatuses,
} = await loadPoolDecommissionData({
getPoolsOverview,
getRebalanceStatus,
getDecommissionStatuses,
})
if (!mountedRef.current || requestId !== requestVersionRef.current) return false
if (overviewResult.status === "rejected") throw overviewResult.reason
if (decommissionResult.status === "rejected") throw decommissionResult.reason
const nextOverview = overviewResult.value
const decommissionStatuses = decommissionResult.value
const statusEntries = decommissionStatuses.map((status) => [status.poolId, status])
setOverview(nextOverview)
@@ -661,7 +661,13 @@ export default function PoolDecommissionPage() {
</p>
</div>
<Badge variant={selectionLocked ? "outline" : "secondary"}>
{!dataReady || error || rebalanceError ? t("Unknown") : selectionLocked ? t("Running") : t("Ready")}
{!dataReady || error || rebalanceError
? t("Unknown")
: overview.supportState === "unsupported"
? t("Unsupported")
: selectionLocked
? t("Running")
: t("Ready")}
</Badge>
</CardHeader>
<CardContent className="space-y-5">
+27
View File
@@ -0,0 +1,27 @@
import type { DecommissionInfo, PoolsOverview, RebalanceStatus } from "@/lib/pool-operations"
interface PoolDecommissionLoaders {
getPoolsOverview: () => Promise<PoolsOverview>
getRebalanceStatus: () => Promise<RebalanceStatus | null>
getDecommissionStatuses: () => Promise<DecommissionInfo[]>
}
export async function loadPoolDecommissionData({
getPoolsOverview,
getRebalanceStatus,
getDecommissionStatuses,
}: PoolDecommissionLoaders) {
const overview = await getPoolsOverview()
const [rebalanceResult, decommissionResult] = await Promise.allSettled([
getRebalanceStatus(),
overview.supportState === "supported" ? getDecommissionStatuses() : Promise.resolve([]),
])
if (decommissionResult.status === "rejected") throw decommissionResult.reason
return {
overview,
rebalanceResult,
decommissionStatuses: decommissionResult.value,
}
}
+98
View File
@@ -0,0 +1,98 @@
import test from "node:test"
import assert from "node:assert/strict"
import fs from "node:fs"
import { loadPoolDecommissionData } from "../../lib/pool-decommission-load"
import { normalizePoolsOverview } from "../../lib/pool-operations"
const singlePoolOverview = normalizePoolsOverview({ pools: [{ id: 1, status: "active" }] })
const multiPoolOverview = normalizePoolsOverview({
pools: [
{ id: 1, status: "active" },
{ id: 2, status: "active" },
],
})
test("unsupported single-pool topology skips decommission status and loads as unsupported", async () => {
let statusRequests = 0
const result = await loadPoolDecommissionData({
getPoolsOverview: async () => singlePoolOverview,
getRebalanceStatus: async () => null,
getDecommissionStatuses: async () => {
statusRequests += 1
throw new Error("501 Not Implemented")
},
})
assert.equal(statusRequests, 0)
assert.equal(result.overview.supportState, "unsupported")
assert.deepEqual(result.decommissionStatuses, [])
})
test("supported multi-pool topology requests decommission status", async () => {
let statusRequests = 0
await loadPoolDecommissionData({
getPoolsOverview: async () => multiPoolOverview,
getRebalanceStatus: async () => null,
getDecommissionStatuses: async () => {
statusRequests += 1
return []
},
})
assert.equal(statusRequests, 1)
})
test("overview failure rejects before dependent status reads", async () => {
let rebalanceRequests = 0
let statusRequests = 0
await assert.rejects(
loadPoolDecommissionData({
getPoolsOverview: async () => {
throw new Error("overview unavailable")
},
getRebalanceStatus: async () => {
rebalanceRequests += 1
return null
},
getDecommissionStatuses: async () => {
statusRequests += 1
return []
},
}),
/overview unavailable/,
)
assert.equal(rebalanceRequests, 0)
assert.equal(statusRequests, 0)
})
test("supported topology rejects a decommission status failure", async () => {
await assert.rejects(
loadPoolDecommissionData({
getPoolsOverview: async () => multiPoolOverview,
getRebalanceStatus: async () => null,
getDecommissionStatuses: async () => {
throw new Error("status unavailable")
},
}),
/status unavailable/,
)
})
test("page source keeps unsupported data separate from locked load failures", () => {
const source = fs.readFileSync("app/(dashboard)/pool-decommission/page.tsx", "utf8")
assert.match(source, /\{error \? \([\s\S]*?<AlertTitle>\{t\("Load Failed"\)\}<\/AlertTitle>/)
assert.match(
source,
/\{dataReady && overview\.supportState === "unsupported" \? \([\s\S]*?<AlertTitle>\{t\("Single pool decommission is not supported"\)\}<\/AlertTitle>/,
)
assert.match(source, /catch \(loadError\)[\s\S]*?setError\([\s\S]*?setDataReady\(false\)/)
assert.match(
source,
/const interactionLocked =[\s\S]*?Boolean\(error\)[\s\S]*?Boolean\(rebalanceError\)[\s\S]*?!dataReady/,
)
assert.match(source, /overview\.supportState === "unsupported"[\s\S]*?t\("Unsupported"\)/)
})
+3 -1
View File
@@ -22,8 +22,10 @@ test("pool status reads fail closed instead of becoming idle or ready", () => {
test("decommission keeps pool data visible when only rebalance status fails", () => {
const source = read("app/(dashboard)/pool-decommission/page.tsx")
const loader = read("lib/pool-decommission-load.ts")
assert.match(source, /Promise\.allSettled\(/)
assert.match(source, /loadPoolDecommissionData\(/)
assert.match(loader, /Promise\.allSettled\(/)
assert.match(source, /const \[rebalanceError, setRebalanceError\] = useState<string \| null>\(null\)/)
assert.match(source, /setRebalanceState\("unknown"\)/)
assert.match(source, /setDataReady\(true\)/)