Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions app/(dashboard)/rebalance/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -178,6 +186,7 @@ export default function RebalancePage() {
...pool,
status: statusPool.status || pool.status,
progress: statusPool.progress,
cleanupWarnings: statusPool.cleanupWarnings,
}
})
}, [overview.pools, status?.pools])
Expand Down Expand Up @@ -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>
))
)}
Expand Down
46 changes: 37 additions & 9 deletions lib/pool-operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -54,6 +62,7 @@ export interface PoolSummary {
lastUpdate?: string
status: string
progress: PoolUsageProgress
cleanupWarnings: RebalanceCleanupWarnings
decommission: PoolDecommissionSummary
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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

Expand All @@ -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,
}
}
Expand Down Expand Up @@ -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 ""

Expand All @@ -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 ""
}

Expand Down
26 changes: 21 additions & 5 deletions tests/lib/pool-operations.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down Expand Up @@ -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", () => {
Expand Down
2 changes: 1 addition & 1 deletion tests/lib/pool-overview-source.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down