mirror of
https://github.com/rustfs/console.git
synced 2026-10-29 18:44:56 +08:00
fix: skip decommission status for unsupported topology (#199)
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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"\)/)
|
||||
})
|
||||
@@ -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\)/)
|
||||
|
||||
Reference in New Issue
Block a user