feat: expand pool details for rebalance and decommission

This commit is contained in:
马登山
2026-05-09 17:05:23 +08:00
parent 70e6cd3a6f
commit a95bb5b89d
17 changed files with 284 additions and 29 deletions
+79
View File
@@ -4,6 +4,7 @@ import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { niceBytes } from "@/lib/functions"
import type { PoolsOverview } from "@/lib/pool-operations"
@@ -16,6 +17,24 @@ function Stat({ label, value }: { label: string; value: string }) {
)
}
function formatBytesValue(value?: number) {
return value === undefined ? "--" : niceBytes(String(value))
}
function formatNumberValue(value?: number) {
return value === undefined ? "--" : value.toLocaleString()
}
function formatPercentValue(value?: number) {
return value === undefined ? "--" : `${value.toFixed(2)}%`
}
function formatDateTime(value?: string) {
if (!value) return "--"
const date = new Date(value)
return Number.isNaN(date.getTime()) ? value : date.toLocaleString()
}
export function PoolsOverviewCard({ overview, operationLabel }: { overview: PoolsOverview; operationLabel: string }) {
const { t } = useTranslation()
const usedPercent = useMemo(() => {
@@ -45,6 +64,66 @@ export function PoolsOverviewCard({ overview, operationLabel }: { overview: Pool
value={overview.totalUsedCapacity ? niceBytes(String(overview.totalUsedCapacity)) : "--"}
/>
<Stat label={t("Usage")} value={`${usedPercent}%`} />
<div className="md:col-span-4">
<Table className="min-w-[1120px]">
<TableHeader>
<TableRow>
<TableHead>{t("ID")}</TableHead>
<TableHead>{t("Pool")}</TableHead>
<TableHead>{t("Status")}</TableHead>
<TableHead>{t("Total Capacity")}</TableHead>
<TableHead>{t("Current Size")}</TableHead>
<TableHead>{t("Used Capacity")}</TableHead>
<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>
</TableRow>
</TableHeader>
<TableBody>
{overview.pools.length === 0 ? (
<TableRow>
<TableCell colSpan={18} className="text-center text-muted-foreground">
{t("No Data")}
</TableCell>
</TableRow>
) : (
overview.pools.map((pool) => (
<TableRow key={pool.id}>
<TableCell>{pool.id}</TableCell>
<TableCell className="max-w-[360px] truncate" title={pool.name}>
{pool.name}
</TableCell>
<TableCell>{pool.status || "--"}</TableCell>
<TableCell>{formatBytesValue(pool.total)}</TableCell>
<TableCell>{formatBytesValue(pool.currentSize)}</TableCell>
<TableCell>{formatBytesValue(pool.used)}</TableCell>
<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>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
)
+8 -1
View File
@@ -1307,5 +1307,12 @@
"username length cannot be less than 8 characters and greater than 16 characters": "لا يمكن أن يكون طول اسم المستخدم أقل من 8 أحرف وأكثر من 16 حرفًا",
"waiting": "في الانتظار",
"Completed Status": "مكتمل",
"Active Pool": "التجمع النشط"
"Active Pool": "التجمع النشط",
"Bytes Failed": "Bytes Failed",
"Complete": "Complete",
"Current Size": "Current Size",
"ID": "ID",
"Objects Failed": "Objects Failed",
"Start Size": "Start Size",
"Start Time": "Start Time"
}
+8 -1
View File
@@ -1307,5 +1307,12 @@
"username length cannot be less than 8 characters and greater than 16 characters": "Benutzernamenlänge darf nicht weniger als 8 Zeichen und nicht mehr als 16 Zeichen sein",
"waiting": "Warten",
"Completed Status": "Abgeschlossen",
"Active Pool": "Aktiver Pool"
"Active Pool": "Aktiver Pool",
"Bytes Failed": "Bytes Failed",
"Complete": "Complete",
"Current Size": "Current Size",
"ID": "ID",
"Objects Failed": "Objects Failed",
"Start Size": "Start Size",
"Start Time": "Start Time"
}
+8 -1
View File
@@ -1307,5 +1307,12 @@
"username length cannot be less than 8 characters and greater than 16 characters": "username length cannot be less than 8 characters and greater than 16 characters",
"waiting": "Waiting",
"Completed Status": "Completed",
"Active Pool": "Active Pool"
"Active Pool": "Active Pool",
"Bytes Failed": "Bytes Failed",
"Complete": "Complete",
"Current Size": "Current Size",
"ID": "ID",
"Objects Failed": "Objects Failed",
"Start Size": "Start Size",
"Start Time": "Start Time"
}
+8 -1
View File
@@ -1307,5 +1307,12 @@
"username length cannot be less than 8 characters and greater than 16 characters": "La longitud del nombre de usuario no puede ser menor de 8 caracteres y mayor de 16 caracteres",
"waiting": "Esperando",
"Completed Status": "Completada",
"Active Pool": "Pool activo"
"Active Pool": "Pool activo",
"Bytes Failed": "Bytes Failed",
"Complete": "Complete",
"Current Size": "Current Size",
"ID": "ID",
"Objects Failed": "Objects Failed",
"Start Size": "Start Size",
"Start Time": "Start Time"
}
+8 -1
View File
@@ -1307,5 +1307,12 @@
"username length cannot be less than 8 characters and greater than 16 characters": "la longueur du nom d'utilisateur ne peut pas être inférieure à 8 caractères et supérieure à 16 caractères",
"waiting": "En attente",
"Completed Status": "Terminé",
"Active Pool": "Pool actif"
"Active Pool": "Pool actif",
"Bytes Failed": "Bytes Failed",
"Complete": "Complete",
"Current Size": "Current Size",
"ID": "ID",
"Objects Failed": "Objects Failed",
"Start Size": "Start Size",
"Start Time": "Start Time"
}
+8 -1
View File
@@ -1307,5 +1307,12 @@
"username length cannot be less than 8 characters and greater than 16 characters": "panjang nama pengguna tidak boleh kurang dari 8 karakter dan lebih dari 16 karakter",
"waiting": "Menunggu",
"Completed Status": "Selesai",
"Active Pool": "Pool aktif"
"Active Pool": "Pool aktif",
"Bytes Failed": "Bytes Failed",
"Complete": "Complete",
"Current Size": "Current Size",
"ID": "ID",
"Objects Failed": "Objects Failed",
"Start Size": "Start Size",
"Start Time": "Start Time"
}
+8 -1
View File
@@ -1307,5 +1307,12 @@
"username length cannot be less than 8 characters and greater than 16 characters": "La lunghezza del nome utente non può essere inferiore a 8 caratteri e superiore a 16 caratteri",
"waiting": "In attesa",
"Completed Status": "Completato",
"Active Pool": "Pool attivo"
"Active Pool": "Pool attivo",
"Bytes Failed": "Bytes Failed",
"Complete": "Complete",
"Current Size": "Current Size",
"ID": "ID",
"Objects Failed": "Objects Failed",
"Start Size": "Start Size",
"Start Time": "Start Time"
}
+8 -1
View File
@@ -1307,5 +1307,12 @@
"username length cannot be less than 8 characters and greater than 16 characters": "ユーザー名の長さは8文字未満、16文字を超えることはできません",
"waiting": "待機中",
"Completed Status": "完了しました",
"Active Pool": "アクティブなプール"
"Active Pool": "アクティブなプール",
"Bytes Failed": "Bytes Failed",
"Complete": "Complete",
"Current Size": "Current Size",
"ID": "ID",
"Objects Failed": "Objects Failed",
"Start Size": "Start Size",
"Start Time": "Start Time"
}
+8 -1
View File
@@ -1307,5 +1307,12 @@
"username length cannot be less than 8 characters and greater than 16 characters": "사용자 이름 길이는 8자 미만, 16자 초과일 수 없습니다",
"waiting": "대기 중",
"Completed Status": "완료",
"Active Pool": "활성 풀"
"Active Pool": "활성 풀",
"Bytes Failed": "Bytes Failed",
"Complete": "Complete",
"Current Size": "Current Size",
"ID": "ID",
"Objects Failed": "Objects Failed",
"Start Size": "Start Size",
"Start Time": "Start Time"
}
+8 -1
View File
@@ -1307,5 +1307,12 @@
"username length cannot be less than 8 characters and greater than 16 characters": "O comprimento do nome de usuário não pode ser menor que 8 caracteres e maior que 16 caracteres",
"waiting": "Aguardando",
"Completed Status": "Concluído",
"Active Pool": "Pool ativo"
"Active Pool": "Pool ativo",
"Bytes Failed": "Bytes Failed",
"Complete": "Complete",
"Current Size": "Current Size",
"ID": "ID",
"Objects Failed": "Objects Failed",
"Start Size": "Start Size",
"Start Time": "Start Time"
}
+8 -1
View File
@@ -1307,5 +1307,12 @@
"username length cannot be less than 8 characters and greater than 16 characters": "Длина имени пользователя не может быть менее 8 символов и более 16 символов",
"waiting": "Ожидание",
"Completed Status": "Завершено",
"Active Pool": "Активный пул"
"Active Pool": "Активный пул",
"Bytes Failed": "Bytes Failed",
"Complete": "Complete",
"Current Size": "Current Size",
"ID": "ID",
"Objects Failed": "Objects Failed",
"Start Size": "Start Size",
"Start Time": "Start Time"
}
+8 -1
View File
@@ -1307,5 +1307,12 @@
"username length cannot be less than 8 characters and greater than 16 characters": "Kullanıcı adı uzunluğu 8 karakterden az 16 karakterden büyük olamaz",
"waiting": "Bekliyor",
"Completed Status": "Tamamlandı",
"Active Pool": "Etkin pool"
"Active Pool": "Etkin pool",
"Bytes Failed": "Bytes Failed",
"Complete": "Complete",
"Current Size": "Current Size",
"ID": "ID",
"Objects Failed": "Objects Failed",
"Start Size": "Start Size",
"Start Time": "Start Time"
}
+8 -1
View File
@@ -1307,5 +1307,12 @@
"username length cannot be less than 8 characters and greater than 16 characters": "độ dài tên người dùng phải từ 8 đến 16 ký tự",
"waiting": "Đang chờ",
"Completed Status": "Đã hoàn thành",
"Active Pool": "Pool đang hoạt động"
"Active Pool": "Pool đang hoạt động",
"Bytes Failed": "Bytes Failed",
"Complete": "Complete",
"Current Size": "Current Size",
"ID": "ID",
"Objects Failed": "Objects Failed",
"Start Size": "Start Size",
"Start Time": "Start Time"
}
+8 -1
View File
@@ -1307,5 +1307,12 @@
"username length cannot be less than 8 characters and greater than 16 characters": "用户名长度不能少于8个字符且不能大于16个字符",
"waiting": "等待中",
"Completed Status": "已完成",
"Active Pool": "当前存储池"
"Active Pool": "当前存储池",
"Bytes Failed": "失败字节数",
"Complete": "完成",
"Current Size": "当前大小",
"ID": "ID",
"Objects Failed": "失败对象数",
"Start Size": "开始大小",
"Start Time": "开始时间"
}
+52 -15
View File
@@ -29,14 +29,32 @@ export interface PoolUsageProgress {
elapsed: number
}
export interface PoolDecommissionSummary {
startTime?: string
startSize: number
totalSize: number
currentSize: number
complete: boolean
failed: boolean
canceled: boolean
objects: number
objectsFailed: number
bytes: number
bytesFailed: number
}
export interface PoolSummary {
id: string
name: string
total: number
used: number
available: number
currentSize: number
usagePercent: number
lastUpdate?: string
status: string
progress: PoolUsageProgress
decommission: PoolDecommissionSummary
}
export interface PoolsOverview {
@@ -120,6 +138,25 @@ function normalizeProgress(value: unknown): PoolUsageProgress {
}
}
function normalizeDecommissionSummary(value: unknown): PoolDecommissionSummary {
const record = asRecord(value)
return {
startTime: asString(record.startTime || record.StartTime) || undefined,
startSize: asNumber(record.startSize || record.StartSize),
totalSize: asNumber(record.totalSize || record.TotalSize),
currentSize: asNumber(record.currentSize || record.CurrentSize),
complete: asBoolean(record.complete || record.Complete),
failed: asBoolean(record.failed || record.Failed),
canceled: asBoolean(record.canceled || record.Canceled),
objects: asNumber(record.objectsDecommissioned || record.ObjectsDecommissioned || record.objects || record.Objects),
objectsFailed: asNumber(
record.objectsDecommissionedFailed || record.ObjectsDecommissionedFailed || record.objectsFailed,
),
bytes: asNumber(record.bytesDecommissioned || record.BytesDecommissioned || record.bytes || record.Bytes),
bytesFailed: asNumber(record.bytesDecommissionedFailed || record.BytesDecommissionedFailed || record.bytesFailed),
}
}
function hasProgress(progress: PoolUsageProgress): boolean {
return Boolean(progress.bytes || progress.objects || progress.versions || progress.eta || progress.elapsed)
}
@@ -150,26 +187,22 @@ function pickStatus(record: JsonRecord): string {
function normalizePool(value: unknown, index: number): PoolSummary {
const record = asRecord(value)
const decommissionInfo = asRecord(record.decommissionInfo || record.DecommissionInfo)
const decommission = normalizeDecommissionSummary(decommissionInfo)
const total = asNumber(
record.total ||
record.capacity ||
record.totalSize ||
record.Total ||
record.TotalSize ||
decommissionInfo.totalSize ||
decommissionInfo.TotalSize,
)
const used = asNumber(
record.used ||
record.usedCapacity ||
record.Used ||
record.UsedCapacity ||
decommissionInfo.currentSize ||
decommissionInfo.CurrentSize,
record.total || record.capacity || record.totalSize || record.Total || record.TotalSize || decommission.totalSize,
)
const explicitUsed = asNumber(record.usedSize || record.UsedSize || record.usedCapacity || record.UsedCapacity)
const rawUsed = asNumber(record.used || record.Used)
const used =
explicitUsed || (rawUsed > 0 && rawUsed <= 1 && total > 0 ? total * rawUsed : rawUsed) || decommission.currentSize
const currentSize = asNumber(record.currentSize || record.CurrentSize || decommission.currentSize)
const available =
asNumber(record.available || record.availableCapacity || record.Available || record.AvailableCapacity) ||
Math.max(total - used, 0)
const rawUsagePercent = asNumber(record.used || record.Used || record.usagePercent || record.UsagePercent)
const usagePercent = clampPercent(
rawUsagePercent > 0 && rawUsagePercent <= 1 ? rawUsagePercent * 100 : rawUsagePercent,
)
const progress = normalizeProgress(record.progress || record.Progress)
const rawId =
record.id ?? record.poolId ?? record.pool ?? record.index ?? record.ID ?? record.PoolID ?? record.poolIndex
@@ -180,8 +213,12 @@ function normalizePool(value: unknown, index: number): PoolSummary {
total,
used,
available,
currentSize,
usagePercent: usagePercent || (total > 0 ? clampPercent((used / total) * 100) : 0),
lastUpdate: asString(record.lastUpdate || record.LastUpdate || record.updatedAt || record.UpdatedAt) || undefined,
status: pickStatus(record),
progress,
decommission,
}
}
+41
View File
@@ -69,6 +69,47 @@ test("normalizePoolsOverview reads pool list decommissionInfo capacities", () =>
assert.equal(overview.pools[0]?.used, 55435714560)
})
test("normalizePoolsOverview preserves detailed pool list fields", () => {
const overview = normalizePoolsOverview([
{
id: 0,
cmdline: "http://rustfs-node{1...32}:9000/data/rustfs{1...4}/mnmd",
lastUpdate: "2026-05-09T04:29:55.213860104Z",
totalSize: 5147518304256,
currentSize: 5014093479936,
usedSize: 133424824320,
used: 0.02592022338408851,
status: "active",
decommissionInfo: {
startTime: null,
startSize: 0,
totalSize: 5147518304256,
currentSize: 5014093479936,
complete: false,
failed: false,
canceled: false,
objectsDecommissioned: 3,
objectsDecommissionedFailed: 1,
bytesDecommissioned: 2048,
bytesDecommissionedFailed: 512,
},
},
])
const pool = overview.pools[0]
assert.equal(overview.totalUsedCapacity, 133424824320)
assert.equal(pool?.used, 133424824320)
assert.equal(pool?.currentSize, 5014093479936)
assert.equal(pool?.usagePercent, 2.592022338408851)
assert.equal(pool?.lastUpdate, "2026-05-09T04:29:55.213860104Z")
assert.equal(pool?.decommission.complete, false)
assert.equal(pool?.decommission.objects, 3)
assert.equal(pool?.decommission.objectsFailed, 1)
assert.equal(pool?.decommission.bytes, 2048)
assert.equal(pool?.decommission.bytesFailed, 512)
})
test("normalizeRebalanceStatus reads progress and pool details", () => {
const status = normalizeRebalanceStatus({
id: "reb-1",