mirror of
https://github.com/rustfs/console.git
synced 2026-08-29 03:52:28 +08:00
feat: expand pool details for rebalance and decommission
This commit is contained in:
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user