mirror of
https://github.com/rustfs/console.git
synced 2026-08-28 19:47:21 +08:00
fix: preserve pool status before rebalance starts (#166)
This commit is contained in:
@@ -184,6 +184,7 @@ export default function PoolDecommissionPage() {
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [dataReady, setDataReady] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [rebalanceError, setRebalanceError] = useState<string | null>(null)
|
||||
const [selectedPoolId, setSelectedPoolId] = useState("")
|
||||
const [overview, setOverview] = useState<PoolsOverview>({
|
||||
pools: [] as PoolSummary[],
|
||||
@@ -216,19 +217,30 @@ export default function PoolDecommissionPage() {
|
||||
if (showSpinner) setLoading(true)
|
||||
else setRefreshing(true)
|
||||
setError(null)
|
||||
setRebalanceError(null)
|
||||
try {
|
||||
const [nextOverview, rebalanceStatus, decommissionStatuses] = await Promise.all([
|
||||
const [overviewResult, rebalanceResult, decommissionResult] = await Promise.allSettled([
|
||||
getPoolsOverview(),
|
||||
getRebalanceStatus(),
|
||||
getDecommissionStatuses(),
|
||||
])
|
||||
if (!mountedRef.current || requestId !== requestVersionRef.current) return false
|
||||
const nextRebalanceState = deriveRebalanceDisplayState(rebalanceStatus, nextOverview.supportState)
|
||||
|
||||
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)
|
||||
setStatuses(Object.fromEntries(statusEntries) as Record<string, DecommissionInfo | null>)
|
||||
setRebalanceState(nextRebalanceState)
|
||||
if (rebalanceResult.status === "fulfilled") {
|
||||
setRebalanceState(deriveRebalanceDisplayState(rebalanceResult.value, nextOverview.supportState))
|
||||
} else {
|
||||
setRebalanceState("unknown")
|
||||
setRebalanceError(rebalanceResult.reason instanceof Error ? rebalanceResult.reason.message : t("Load Failed"))
|
||||
}
|
||||
setSelectedPoolId((current) =>
|
||||
current && nextOverview.pools.some((pool) => pool.id === current)
|
||||
? current
|
||||
@@ -283,7 +295,8 @@ export default function PoolDecommissionPage() {
|
||||
const selectionLocked = isTaskLocked(activeTask)
|
||||
const activePoolCount = poolRows.filter((row) => isActivePool(row.pool)).length
|
||||
const trackedProgress = trackedTask ? getProgressPercent(trackedTask.status, trackedTask.pool) : 0
|
||||
const interactionLocked = loading || refreshing || submitting || Boolean(error) || !dataReady
|
||||
const interactionLocked =
|
||||
loading || refreshing || submitting || Boolean(error) || Boolean(rebalanceError) || !dataReady
|
||||
|
||||
useEffect(() => {
|
||||
shouldPollRef.current = dataReady && poolRows.some((row) => shouldPoll(row.displayState))
|
||||
@@ -515,6 +528,21 @@ export default function PoolDecommissionPage() {
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{rebalanceError ? (
|
||||
<Alert variant="destructive">
|
||||
<AlertTitle>
|
||||
{t("Current Rebalance Status")}: {t("Unavailable")}
|
||||
</AlertTitle>
|
||||
<AlertDescription className="flex flex-col items-start gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<span className="break-words">{rebalanceError}</span>
|
||||
<Button type="button" variant="outline" size="sm" onClick={() => void loadData()}>
|
||||
<RiRefreshLine className="size-4" aria-hidden />
|
||||
{t("Refresh")}
|
||||
</Button>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{initialLoading ? (
|
||||
<div className="flex min-h-64 items-center justify-center gap-2 text-sm text-muted-foreground" role="status">
|
||||
<Spinner className="size-5" aria-hidden />
|
||||
@@ -633,7 +661,7 @@ export default function PoolDecommissionPage() {
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant={selectionLocked ? "outline" : "secondary"}>
|
||||
{!dataReady || error ? t("Unknown") : selectionLocked ? t("Running") : t("Ready")}
|
||||
{!dataReady || error || rebalanceError ? t("Unknown") : selectionLocked ? t("Running") : t("Ready")}
|
||||
</Badge>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-5">
|
||||
|
||||
@@ -14,6 +14,7 @@ const eslintConfig = defineConfig([
|
||||
".nuxt/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
".worktrees/**",
|
||||
"next-env.d.ts",
|
||||
]),
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useApi } from "@/contexts/api-context"
|
||||
import {
|
||||
deriveDecommissionDisplayState,
|
||||
deriveRebalanceDisplayState,
|
||||
isRebalanceNotStartedError,
|
||||
normalizeDecommissionInfo,
|
||||
normalizeDecommissionStatus,
|
||||
normalizePoolsOverview,
|
||||
@@ -38,8 +39,15 @@ export function usePoolOperations() {
|
||||
}, [api])
|
||||
|
||||
const getRebalanceStatus = useCallback(async () => {
|
||||
const response = await api.get("/rebalance/status")
|
||||
return normalizeRebalanceStatus(response)
|
||||
try {
|
||||
const response = await api.get("/rebalance/status")
|
||||
return normalizeRebalanceStatus(response)
|
||||
} catch (error) {
|
||||
if (isRebalanceNotStartedError(error)) {
|
||||
return normalizeRebalanceStatus({ status: "idle" })
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}, [api])
|
||||
|
||||
const startRebalance = useCallback(async () => {
|
||||
|
||||
@@ -473,6 +473,10 @@ function normalizeState(value: string): string {
|
||||
return value.trim().toLowerCase()
|
||||
}
|
||||
|
||||
export function isRebalanceNotStartedError(error: unknown): boolean {
|
||||
return error instanceof Error && normalizeState(error.message) === "pool rebalance is not started"
|
||||
}
|
||||
|
||||
function deriveDecommissionStatus(info: JsonRecord): string {
|
||||
if (asBoolean(info.complete || info.Complete)) return "complete"
|
||||
if (asBoolean(info.failed || info.Failed)) return "failed"
|
||||
|
||||
@@ -16,6 +16,20 @@ test("pool status reads fail closed instead of becoming idle or ready", () => {
|
||||
assert.match(decommission, /const \[dataReady, setDataReady\] = useState\(false\)/)
|
||||
assert.match(rebalance, /const interactionLocked =/)
|
||||
assert.match(decommission, /const interactionLocked =/)
|
||||
assert.match(hook, /isRebalanceNotStartedError\(error\)/)
|
||||
assert.match(hook, /normalizeRebalanceStatus\(\{ status: "idle" \}\)/)
|
||||
})
|
||||
|
||||
test("decommission keeps pool data visible when only rebalance status fails", () => {
|
||||
const source = read("app/(dashboard)/pool-decommission/page.tsx")
|
||||
|
||||
assert.match(source, /Promise\.allSettled\(/)
|
||||
assert.match(source, /const \[rebalanceError, setRebalanceError\] = useState<string \| null>\(null\)/)
|
||||
assert.match(source, /setRebalanceState\("unknown"\)/)
|
||||
assert.match(source, /setDataReady\(true\)/)
|
||||
assert.match(source, /Boolean\(rebalanceError\)/)
|
||||
assert.match(source, /\{rebalanceError \? \(/)
|
||||
assert.match(source, /t\("Unavailable"\)/)
|
||||
})
|
||||
|
||||
test("pool operation requests reject stale responses and mutations use synchronous locks", () => {
|
||||
|
||||
@@ -3,11 +3,19 @@ import assert from "node:assert/strict"
|
||||
import {
|
||||
deriveDecommissionDisplayState,
|
||||
deriveRebalanceDisplayState,
|
||||
isRebalanceNotStartedError,
|
||||
normalizeDecommissionInfo,
|
||||
normalizePoolsOverview,
|
||||
normalizeRebalanceStatus,
|
||||
} from "../../lib/pool-operations.ts"
|
||||
|
||||
test("isRebalanceNotStartedError recognizes only the expected idle response", () => {
|
||||
assert.equal(isRebalanceNotStartedError(new Error("pool rebalance is not started")), true)
|
||||
assert.equal(isRebalanceNotStartedError(new Error(" Pool Rebalance Is Not Started ")), true)
|
||||
assert.equal(isRebalanceNotStartedError(new Error("failed to read rebalance status")), false)
|
||||
assert.equal(isRebalanceNotStartedError("pool rebalance is not started"), false)
|
||||
})
|
||||
|
||||
test("normalizePoolsOverview computes support and capacities", () => {
|
||||
const overview = normalizePoolsOverview({
|
||||
pools: [
|
||||
|
||||
Reference in New Issue
Block a user