mirror of
https://github.com/rustfs/console.git
synced 2026-08-30 17:14:47 +08:00
fix: correct pool operation console status
This commit is contained in:
@@ -78,6 +78,10 @@ function getPoolStatusBadgeVariant(state: DecommissionDisplayState) {
|
||||
return "secondary"
|
||||
}
|
||||
|
||||
function hasDecommissionProgress(state: DecommissionDisplayState) {
|
||||
return ["running", "canceling", "completed", "failed", "canceled"].includes(state)
|
||||
}
|
||||
|
||||
export default function PoolDecommissionPage() {
|
||||
const { t } = useTranslation()
|
||||
const message = useMessage()
|
||||
@@ -167,6 +171,7 @@ export default function PoolDecommissionPage() {
|
||||
rebalanceState,
|
||||
confirmingPoolId === activePoolId,
|
||||
)
|
||||
const showActiveProgress = Boolean(activeStatus) && hasDecommissionProgress(activeDisplayState)
|
||||
const canCancelActive = activePoolId
|
||||
? getPoolDisplayState(activeStatus, overview.supportState, rebalanceState) === "running"
|
||||
: false
|
||||
@@ -264,7 +269,7 @@ export default function PoolDecommissionPage() {
|
||||
</PageHeader>
|
||||
|
||||
<div className="space-y-6">
|
||||
<PoolsOverviewCard overview={overview} operationLabel={t("Pool Decommission")} />
|
||||
<PoolsOverviewCard overview={overview} operationLabel={t("Pool Decommission")} showDecommissionColumns />
|
||||
|
||||
{overview.supportState === "unsupported" ? (
|
||||
<Alert>
|
||||
@@ -328,11 +333,15 @@ export default function PoolDecommissionPage() {
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">{t("Progress")}</p>
|
||||
<p className="text-sm font-medium">{Math.round(activeStatus?.progressPercent ?? 0)}%</p>
|
||||
<p className="text-sm font-medium">
|
||||
{showActiveProgress ? `${Math.round(activeStatus?.progressPercent ?? 0)}%` : "--"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">{t("Bytes Moved")}</p>
|
||||
<p className="text-sm font-medium">{formatBytesValue(activeStatus?.bytes)}</p>
|
||||
<p className="text-sm font-medium">
|
||||
{showActiveProgress ? formatBytesValue(activeStatus?.bytes) : "--"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -357,7 +366,7 @@ export default function PoolDecommissionPage() {
|
||||
</TableRow>
|
||||
) : (
|
||||
poolRows.map(({ pool, status: rowStatus, displayState: rowState }) => {
|
||||
const hasDecommissionStarted = Boolean(rowStatus)
|
||||
const showProgress = Boolean(rowStatus) && hasDecommissionProgress(rowState)
|
||||
const canRequestStart = !submitting && rowState === "ready"
|
||||
const canConfirm = !submitting && rowState === "confirming"
|
||||
const canCancel = !submitting && rowState === "running"
|
||||
@@ -376,7 +385,7 @@ export default function PoolDecommissionPage() {
|
||||
</TableCell>
|
||||
<TableCell>{formatBytesValue(pool.used)}</TableCell>
|
||||
<TableCell className="min-w-32">
|
||||
{hasDecommissionStarted ? (
|
||||
{showProgress ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Progress value={rowStatus?.progressPercent ?? 0} className="h-2 w-20" />
|
||||
<span className="text-xs text-muted-foreground">
|
||||
@@ -387,8 +396,8 @@ export default function PoolDecommissionPage() {
|
||||
"--"
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>{hasDecommissionStarted ? (rowStatus?.objects ?? "--") : "--"}</TableCell>
|
||||
<TableCell>{hasDecommissionStarted ? formatBytesValue(rowStatus?.bytes) : "--"}</TableCell>
|
||||
<TableCell>{showProgress ? (rowStatus?.objects ?? "--") : "--"}</TableCell>
|
||||
<TableCell>{showProgress ? formatBytesValue(rowStatus?.bytes) : "--"}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex justify-end gap-2">
|
||||
{rowState === "confirming" ? (
|
||||
@@ -423,7 +432,7 @@ export default function PoolDecommissionPage() {
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={!canRequestStart || hasDecommissionStarted}
|
||||
disabled={!canRequestStart || showProgress}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
setActivePoolId(pool.id)
|
||||
|
||||
@@ -19,6 +19,7 @@ import type { PoolSummary, PoolsOverview, RebalanceDisplayState, RebalanceStatus
|
||||
import { useDialog } from "@/lib/feedback/dialog"
|
||||
import { useMessage } from "@/lib/feedback/message"
|
||||
import { niceBytes } from "@/lib/functions"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const POLL_MS = 5000
|
||||
|
||||
@@ -44,6 +45,10 @@ function formatNumberValue(value?: number) {
|
||||
return value === undefined ? "--" : String(value)
|
||||
}
|
||||
|
||||
function isFailedRebalancePool(pool: PoolSummary) {
|
||||
return ["failed", "error"].includes(pool.status.trim().toLowerCase())
|
||||
}
|
||||
|
||||
export default function RebalancePage() {
|
||||
const { t } = useTranslation()
|
||||
const dialog = useDialog()
|
||||
@@ -205,7 +210,7 @@ export default function RebalancePage() {
|
||||
</PageHeader>
|
||||
|
||||
<div className="space-y-6">
|
||||
<PoolsOverviewCard overview={overview} operationLabel={t("Rebalance")} />
|
||||
<PoolsOverviewCard overview={overview} operationLabel={t("Rebalance")} showDecommissionColumns={false} />
|
||||
|
||||
{overview.supportState === "unsupported" ? (
|
||||
<Alert>
|
||||
@@ -285,7 +290,7 @@ export default function RebalancePage() {
|
||||
</TableRow>
|
||||
) : (
|
||||
pools.map((pool) => (
|
||||
<TableRow key={pool.id}>
|
||||
<TableRow key={pool.id} className={cn(isFailedRebalancePool(pool) && "bg-destructive/10")}>
|
||||
<TableCell>{pool.name}</TableCell>
|
||||
<TableCell>{pool.status || "--"}</TableCell>
|
||||
<TableCell>{formatBytesValue(pool.used)}</TableCell>
|
||||
|
||||
@@ -35,7 +35,15 @@ function formatDateTime(value?: string) {
|
||||
return Number.isNaN(date.getTime()) ? value : date.toLocaleString()
|
||||
}
|
||||
|
||||
export function PoolsOverviewCard({ overview, operationLabel }: { overview: PoolsOverview; operationLabel: string }) {
|
||||
export function PoolsOverviewCard({
|
||||
overview,
|
||||
operationLabel,
|
||||
showDecommissionColumns = false,
|
||||
}: {
|
||||
overview: PoolsOverview
|
||||
operationLabel: string
|
||||
showDecommissionColumns?: boolean
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const usedPercent = useMemo(() => {
|
||||
if (!overview.totalCapacity) return 0
|
||||
@@ -77,21 +85,25 @@ export function PoolsOverviewCard({ overview, operationLabel }: { overview: Pool
|
||||
<TableHead>{t("Available")}</TableHead>
|
||||
<TableHead>{t("Usage")}</TableHead>
|
||||
<TableHead>{t("Updated At")}</TableHead>
|
||||
<TableHead>{t("Start Time")}</TableHead>
|
||||
<TableHead>{t("Start Size")}</TableHead>
|
||||
<TableHead>{t("Complete")}</TableHead>
|
||||
<TableHead>{t("Failed Status")}</TableHead>
|
||||
<TableHead>{t("Canceled")}</TableHead>
|
||||
<TableHead>{t("Objects")}</TableHead>
|
||||
<TableHead>{t("Objects Failed")}</TableHead>
|
||||
<TableHead>{t("Bytes Moved")}</TableHead>
|
||||
<TableHead>{t("Bytes Failed")}</TableHead>
|
||||
{showDecommissionColumns ? (
|
||||
<>
|
||||
<TableHead>{t("Start Time")}</TableHead>
|
||||
<TableHead>{t("Start Size")}</TableHead>
|
||||
<TableHead>{t("Complete")}</TableHead>
|
||||
<TableHead>{t("Failed Status")}</TableHead>
|
||||
<TableHead>{t("Canceled")}</TableHead>
|
||||
<TableHead>{t("Objects")}</TableHead>
|
||||
<TableHead>{t("Objects Failed")}</TableHead>
|
||||
<TableHead>{t("Bytes Moved")}</TableHead>
|
||||
<TableHead>{t("Bytes Failed")}</TableHead>
|
||||
</>
|
||||
) : null}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{overview.pools.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={18} className="text-center text-muted-foreground">
|
||||
<TableCell colSpan={showDecommissionColumns ? 18 : 9} className="text-center text-muted-foreground">
|
||||
{t("No Data")}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -109,15 +121,19 @@ export function PoolsOverviewCard({ overview, operationLabel }: { overview: Pool
|
||||
<TableCell>{formatBytesValue(pool.available)}</TableCell>
|
||||
<TableCell>{formatPercentValue(pool.usagePercent)}</TableCell>
|
||||
<TableCell>{formatDateTime(pool.lastUpdate)}</TableCell>
|
||||
<TableCell>{formatDateTime(pool.decommission.startTime)}</TableCell>
|
||||
<TableCell>{formatBytesValue(pool.decommission.startSize)}</TableCell>
|
||||
<TableCell>{pool.decommission.complete ? t("Yes") : t("No")}</TableCell>
|
||||
<TableCell>{pool.decommission.failed ? t("Yes") : t("No")}</TableCell>
|
||||
<TableCell>{pool.decommission.canceled ? t("Yes") : t("No")}</TableCell>
|
||||
<TableCell>{formatNumberValue(pool.decommission.objects)}</TableCell>
|
||||
<TableCell>{formatNumberValue(pool.decommission.objectsFailed)}</TableCell>
|
||||
<TableCell>{formatBytesValue(pool.decommission.bytes)}</TableCell>
|
||||
<TableCell>{formatBytesValue(pool.decommission.bytesFailed)}</TableCell>
|
||||
{showDecommissionColumns ? (
|
||||
<>
|
||||
<TableCell>{formatDateTime(pool.decommission.startTime)}</TableCell>
|
||||
<TableCell>{formatBytesValue(pool.decommission.startSize)}</TableCell>
|
||||
<TableCell>{pool.decommission.complete ? t("Yes") : t("No")}</TableCell>
|
||||
<TableCell>{pool.decommission.failed ? t("Yes") : t("No")}</TableCell>
|
||||
<TableCell>{pool.decommission.canceled ? t("Yes") : t("No")}</TableCell>
|
||||
<TableCell>{formatNumberValue(pool.decommission.objects)}</TableCell>
|
||||
<TableCell>{formatNumberValue(pool.decommission.objectsFailed)}</TableCell>
|
||||
<TableCell>{formatBytesValue(pool.decommission.bytes)}</TableCell>
|
||||
<TableCell>{formatBytesValue(pool.decommission.bytesFailed)}</TableCell>
|
||||
</>
|
||||
) : null}
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
|
||||
+26
-7
@@ -174,6 +174,26 @@ function aggregatePoolProgress(pools: PoolSummary[]): PoolUsageProgress {
|
||||
)
|
||||
}
|
||||
|
||||
function hasExplicitProgressPercent(record: JsonRecord): boolean {
|
||||
return (
|
||||
record.progressPercent !== undefined ||
|
||||
record.ProgressPercent !== undefined ||
|
||||
record.percent !== undefined ||
|
||||
record.Percent !== undefined
|
||||
)
|
||||
}
|
||||
|
||||
function deriveRebalanceProgressPercent(status: string, pools: PoolSummary[]): number {
|
||||
const state = normalizeState(status)
|
||||
if (["completed", "complete", "success", "finished"].includes(state)) return 100
|
||||
if (pools.length === 0) return 0
|
||||
|
||||
const finishedPools = pools.filter((pool) =>
|
||||
["completed", "complete", "success", "finished"].includes(normalizeState(pool.status)),
|
||||
).length
|
||||
return Math.round((finishedPools / pools.length) * 100)
|
||||
}
|
||||
|
||||
function pickStatus(record: JsonRecord): string {
|
||||
return (
|
||||
asString(record.status) ||
|
||||
@@ -256,15 +276,14 @@ export function normalizeRebalanceStatus(value: unknown): RebalanceStatus {
|
||||
: []
|
||||
const pools = poolsSource.map((pool, index) => normalizePool(pool, index))
|
||||
const explicitTotals = normalizeProgress(record.progress || record.Progress || record.totals || record.Totals)
|
||||
const totals = hasProgress(explicitTotals) ? explicitTotals : aggregatePoolProgress(pools)
|
||||
const totalsAreExplicit = hasProgress(explicitTotals)
|
||||
const totals = totalsAreExplicit ? explicitTotals : aggregatePoolProgress(pools)
|
||||
const status = pickStatus(record) || deriveStatusFromPools(pools)
|
||||
const rawProgress =
|
||||
asNumber(record.progressPercent || record.ProgressPercent || record.percent || record.Percent) ||
|
||||
(totals.bytes > 0 && pools.length > 0
|
||||
const rawProgress = hasExplicitProgressPercent(record)
|
||||
? asNumber(record.progressPercent || record.ProgressPercent || record.percent || record.Percent)
|
||||
: totalsAreExplicit && totals.bytes > 0 && pools.length > 0
|
||||
? (pools.reduce((sum, pool) => sum + pool.progress.bytes, 0) / totals.bytes) * 100
|
||||
: ["completed", "complete", "success", "finished"].includes(normalizeState(status))
|
||||
? 100
|
||||
: 0)
|
||||
: deriveRebalanceProgressPercent(status, pools)
|
||||
|
||||
return {
|
||||
id: asString(record.id) || asString(record.ID),
|
||||
|
||||
@@ -18,8 +18,13 @@ test("pool decommission page keeps status panel independent from list clicks and
|
||||
assert.doesNotMatch(source, /{t\("Rebalance Status"\)}/)
|
||||
assert.match(source, /onClick=\{\(\) => setSelectedPoolId\(pool\.id\)\}/)
|
||||
assert.doesNotMatch(source, /onClick=\{\(\) => setActivePoolId\(pool\.id\)\}/)
|
||||
assert.match(source, /const hasDecommissionStarted = Boolean\(rowStatus\)/)
|
||||
assert.match(source, /hasDecommissionStarted \?/)
|
||||
assert.match(source, /function hasDecommissionProgress\(state: DecommissionDisplayState\)/)
|
||||
assert.match(
|
||||
source,
|
||||
/const showActiveProgress = Boolean\(activeStatus\) && hasDecommissionProgress\(activeDisplayState\)/,
|
||||
)
|
||||
assert.match(source, /const showProgress = Boolean\(rowStatus\) && hasDecommissionProgress\(rowState\)/)
|
||||
assert.match(source, /showProgress \?/)
|
||||
assert.match(source, /rowState === "ready"/)
|
||||
assert.doesNotMatch(source, /\["ready", "failed", "canceled", "completed"\]\.includes\(rowState\)/)
|
||||
})
|
||||
|
||||
@@ -197,6 +197,44 @@ test("normalizeRebalanceStatus aggregates pool progress when totals are missing"
|
||||
assert.equal(status.totals.eta, 40)
|
||||
})
|
||||
|
||||
test("normalizeRebalanceStatus does not infer full progress from aggregated bytes", () => {
|
||||
const status = normalizeRebalanceStatus({
|
||||
id: "reb-3",
|
||||
pools: [
|
||||
{
|
||||
id: 0,
|
||||
status: "Completed",
|
||||
progress: {
|
||||
objects: 9,
|
||||
versions: 9,
|
||||
bytes: 18 * 1024 ** 3,
|
||||
elapsed: 53,
|
||||
eta: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
status: "Failed",
|
||||
progress: {
|
||||
objects: 173,
|
||||
versions: 173,
|
||||
bytes: 346 * 1024 ** 3,
|
||||
elapsed: 1072,
|
||||
eta: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
status: "None",
|
||||
progress: null,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
assert.equal(status.status, "failed")
|
||||
assert.equal(status.progressPercent, 33)
|
||||
})
|
||||
|
||||
test("normalizeDecommissionInfo reads nested response", () => {
|
||||
const info = normalizeDecommissionInfo({
|
||||
decommissionInfo: {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import test from "node:test"
|
||||
import assert from "node:assert/strict"
|
||||
import fs from "node:fs"
|
||||
|
||||
test("rebalance page uses basic pool overview columns", () => {
|
||||
const source = fs.readFileSync("app/(dashboard)/rebalance/page.tsx", "utf8")
|
||||
|
||||
assert.match(
|
||||
source,
|
||||
/<PoolsOverviewCard overview=\{overview\} operationLabel=\{t\("Rebalance"\)\} showDecommissionColumns=\{false\} \/>/,
|
||||
)
|
||||
})
|
||||
|
||||
test("rebalance page highlights failed pool rows", () => {
|
||||
const source = fs.readFileSync("app/(dashboard)/rebalance/page.tsx", "utf8")
|
||||
|
||||
assert.match(source, /function isFailedRebalancePool\(pool: PoolSummary\)/)
|
||||
assert.match(source, /\["failed", "error"\]\.includes\(pool\.status\.trim\(\)\.toLowerCase\(\)\)/)
|
||||
assert.match(source, /className=\{cn\(isFailedRebalancePool\(pool\) && "bg-destructive\/10"\)\}/)
|
||||
})
|
||||
|
||||
test("decommission page keeps decommission detail columns", () => {
|
||||
const source = fs.readFileSync("app/(dashboard)/pool-decommission/page.tsx", "utf8")
|
||||
|
||||
assert.match(
|
||||
source,
|
||||
/<PoolsOverviewCard overview=\{overview\} operationLabel=\{t\("Pool Decommission"\)\} showDecommissionColumns \/>/,
|
||||
)
|
||||
})
|
||||
|
||||
test("pool overview card gates decommission-specific columns", () => {
|
||||
const source = fs.readFileSync("components/pools/overview.tsx", "utf8")
|
||||
|
||||
assert.match(source, /showDecommissionColumns = false/)
|
||||
assert.match(source, /showDecommissionColumns \? 18 : 9/)
|
||||
assert.match(source, /showDecommissionColumns \? \(/)
|
||||
})
|
||||
Reference in New Issue
Block a user