fix(rebalance): show cleanup warnings and progress state (#132)

This commit is contained in:
cxymds
2026-06-14 19:18:26 +08:00
committed by GitHub
parent 0bd097ad26
commit c34ef4432f
4 changed files with 83 additions and 17 deletions
+24 -2
View File
@@ -49,6 +49,14 @@ function isFailedRebalancePool(pool: PoolSummary) {
return ["failed", "error"].includes(pool.status.trim().toLowerCase())
}
function formatCleanupWarnings(pool: PoolSummary) {
const warnings = pool.cleanupWarnings
if (!warnings.count) return "--"
const context = [warnings.lastBucket, warnings.lastObject].filter(Boolean).join("/")
return context ? `${warnings.count} (${context})` : String(warnings.count)
}
export default function RebalancePage() {
const { t } = useTranslation()
const dialog = useDialog()
@@ -178,6 +186,7 @@ export default function RebalancePage() {
...pool,
status: statusPool.status || pool.status,
progress: statusPool.progress,
cleanupWarnings: statusPool.cleanupWarnings,
}
})
}, [overview.pools, status?.pools])
@@ -279,24 +288,37 @@ export default function RebalancePage() {
<TableHead>{t("Bytes Moved")}</TableHead>
<TableHead>{t("Objects")}</TableHead>
<TableHead>{t("Versions")}</TableHead>
<TableHead>{t("Cleanup Warnings")}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{pools.length === 0 ? (
<TableRow>
<TableCell colSpan={6} className="text-center text-muted-foreground">
<TableCell colSpan={7} className="text-center text-muted-foreground">
{t("No Data")}
</TableCell>
</TableRow>
) : (
pools.map((pool) => (
<TableRow key={pool.id} className={cn(isFailedRebalancePool(pool) && "bg-destructive/10")}>
<TableRow
key={pool.id}
className={cn(isFailedRebalancePool(pool) && "bg-destructive/15 hover:bg-destructive/20")}
>
<TableCell>{pool.name}</TableCell>
<TableCell>{pool.status || "--"}</TableCell>
<TableCell>{formatBytesValue(pool.used)}</TableCell>
<TableCell>{formatBytesValue(pool.progress.bytes)}</TableCell>
<TableCell>{formatNumberValue(pool.progress.objects)}</TableCell>
<TableCell>{formatNumberValue(pool.progress.versions)}</TableCell>
<TableCell>
{pool.cleanupWarnings.count > 0 ? (
<Badge variant="secondary" className="bg-amber-100 text-amber-900 hover:bg-amber-100">
{formatCleanupWarnings(pool)}
</Badge>
) : (
"--"
)}
</TableCell>
</TableRow>
))
)}
+37 -9
View File
@@ -29,6 +29,14 @@ export interface PoolUsageProgress {
elapsed: number
}
export interface RebalanceCleanupWarnings {
count: number
lastMessage?: string
lastBucket?: string
lastObject?: string
lastAt?: string
}
export interface PoolDecommissionSummary {
startTime?: string
startSize: number
@@ -54,6 +62,7 @@ export interface PoolSummary {
lastUpdate?: string
status: string
progress: PoolUsageProgress
cleanupWarnings: RebalanceCleanupWarnings
decommission: PoolDecommissionSummary
}
@@ -138,6 +147,17 @@ function normalizeProgress(value: unknown): PoolUsageProgress {
}
}
function normalizeCleanupWarnings(value: unknown): RebalanceCleanupWarnings {
const record = asRecord(value)
return {
count: asNumber(record.count || record.Count),
lastMessage: asString(record.lastMsg || record.LastMsg || record.lastMessage || record.LastMessage) || undefined,
lastBucket: asString(record.lastBucket || record.LastBucket) || undefined,
lastObject: asString(record.lastObject || record.LastObject) || undefined,
lastAt: asString(record.lastAt || record.LastAt) || undefined,
}
}
function normalizeDecommissionSummary(value: unknown): PoolDecommissionSummary {
const record = asRecord(value)
return {
@@ -185,12 +205,12 @@ function hasExplicitProgressPercent(record: JsonRecord): boolean {
function deriveRebalanceProgressPercent(status: string, pools: PoolSummary[]): number {
const state = normalizeState(status)
if (["completed", "complete", "success", "finished"].includes(state)) return 100
if (["completed", "complete", "success", "finished"].includes(state) && pools.every(isCompletedRebalancePool)) {
return 100
}
if (pools.length === 0) return 0
const finishedPools = pools.filter((pool) =>
["completed", "complete", "success", "finished"].includes(normalizeState(pool.status)),
).length
const finishedPools = pools.filter(isCompletedRebalancePool).length
return Math.round((finishedPools / pools.length) * 100)
}
@@ -224,6 +244,7 @@ function normalizePool(value: unknown, index: number): PoolSummary {
rawUsagePercent > 0 && rawUsagePercent <= 1 ? rawUsagePercent * 100 : rawUsagePercent,
)
const progress = normalizeProgress(record.progress || record.Progress)
const cleanupWarnings = normalizeCleanupWarnings(record.cleanupWarnings || record.CleanupWarnings)
const rawId =
record.id ?? record.poolId ?? record.pool ?? record.index ?? record.ID ?? record.PoolID ?? record.poolIndex
@@ -238,6 +259,7 @@ function normalizePool(value: unknown, index: number): PoolSummary {
lastUpdate: asString(record.lastUpdate || record.LastUpdate || record.updatedAt || record.UpdatedAt) || undefined,
status: pickStatus(record),
progress,
cleanupWarnings,
decommission,
}
}
@@ -327,6 +349,14 @@ function normalizeState(value: string): string {
return value.trim().toLowerCase()
}
function isIdleRebalancePool(pool: PoolSummary): boolean {
return ["", "none", "not_started", "not-started", "idle"].includes(normalizeState(pool.status))
}
function isCompletedRebalancePool(pool: PoolSummary): boolean {
return ["completed", "complete", "success", "finished"].includes(normalizeState(pool.status))
}
function deriveStatusFromPools(pools: PoolSummary[]): string {
if (pools.length === 0) return ""
@@ -340,13 +370,11 @@ function deriveStatusFromPools(pools: PoolSummary[]): string {
) {
return "running"
}
if (
states.every((state) =>
["completed", "complete", "success", "finished", "none", "not_started", "not-started"].includes(state),
)
) {
if (pools.every(isCompletedRebalancePool)) {
return "completed"
}
if (pools.some(isCompletedRebalancePool) && pools.some(isIdleRebalancePool)) return "running"
if (pools.every(isIdleRebalancePool)) return ""
return ""
}
+21 -5
View File
@@ -115,15 +115,31 @@ test("normalizeRebalanceStatus reads progress and pool details", () => {
id: "reb-1",
status: "running",
progress: { bytes: "1000", objects: "10", versions: "3", eta: "12", elapsed: "6" },
pools: [{ id: 1, used: "20", progress: { bytes: "400" } }],
pools: [
{
id: 1,
used: "20",
progress: { bytes: "400" },
cleanupWarnings: {
count: 1,
lastMsg: "cleanup warning",
lastBucket: "test-bucket",
lastObject: "object-a",
lastAt: "2026-06-12T00:00:00Z",
},
},
],
})
assert.equal(status.id, "reb-1")
assert.equal(status.progressPercent, 40)
assert.equal(status.pools[0]?.progress.bytes, 400)
assert.equal(status.pools[0]?.cleanupWarnings.count, 1)
assert.equal(status.pools[0]?.cleanupWarnings.lastMessage, "cleanup warning")
assert.equal(status.pools[0]?.cleanupWarnings.lastBucket, "test-bucket")
})
test("normalizeRebalanceStatus derives status from pool-only response", () => {
test("normalizeRebalanceStatus does not complete mixed completed and idle pools", () => {
const status = normalizeRebalanceStatus({
id: "3be0831f-4315-4adb-9904-3bb0609b3bfc",
pools: [
@@ -154,11 +170,11 @@ test("normalizeRebalanceStatus derives status from pool-only response", () => {
stoppedAt: null,
})
assert.equal(status.status, "completed")
assert.equal(status.progressPercent, 100)
assert.equal(status.status, "running")
assert.equal(status.progressPercent, 50)
assert.equal(status.pools[0]?.status, "Completed")
assert.equal(status.pools[1]?.status, "None")
assert.equal(deriveRebalanceDisplayState(status, "supported"), "completed")
assert.equal(deriveRebalanceDisplayState(status, "supported"), "running")
})
test("normalizeRebalanceStatus aggregates pool progress when totals are missing", () => {
+1 -1
View File
@@ -16,7 +16,7 @@ test("rebalance page highlights failed pool rows", () => {
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"\)\}/)
assert.match(source, /bg-destructive\/15 hover:bg-destructive\/20/)
})
test("decommission page keeps decommission detail columns", () => {