diff --git a/app/(dashboard)/_components/performance-infrastructure-card.tsx b/app/(dashboard)/_components/performance-infrastructure-card.tsx index 61434bc..950c666 100644 --- a/app/(dashboard)/_components/performance-infrastructure-card.tsx +++ b/app/(dashboard)/_components/performance-infrastructure-card.tsx @@ -5,14 +5,20 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com export function PerformanceInfrastructureCard({ onlineServers, offlineServers, + unknownServers, + degradedServers, onlineDisks, offlineDisks, + unknownDisks, t, }: { onlineServers: number offlineServers: number + unknownServers: number + degradedServers: number onlineDisks: number offlineDisks: number + unknownDisks: number t: (key: string) => string }) { return ( @@ -25,7 +31,7 @@ export function PerformanceInfrastructureCard({

{t("Servers")}

-
+

{t("Online")}

{onlineServers}

@@ -34,11 +40,19 @@ export function PerformanceInfrastructureCard({

{t("Offline")}

{offlineServers}

+
+

{t("Unknown")}

+

{unknownServers}

+
+
+

{t("Degraded")}

+

{degradedServers}

+

{t("Disks")}

-
+

{t("Online")}

{onlineDisks}

@@ -47,6 +61,10 @@ export function PerformanceInfrastructureCard({

{t("Offline")}

{offlineDisks}

+
+

{t("Unknown")}

+

{unknownDisks}

+
diff --git a/app/(dashboard)/_components/performance-server-list.tsx b/app/(dashboard)/_components/performance-server-list.tsx index cbcca25..33a45b6 100644 --- a/app/(dashboard)/_components/performance-server-list.tsx +++ b/app/(dashboard)/_components/performance-server-list.tsx @@ -8,6 +8,7 @@ import { Progress } from "@/components/ui/progress" import { ScrollArea } from "@/components/ui/scroll-area" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { niceBytes } from "@/lib/functions" +import { normalizeServerHealthState, type ServerHealthState } from "@/lib/performance-data" import { cn } from "@/lib/utils" import dayjs from "dayjs" import relativeTime from "dayjs/plugin/relativeTime" @@ -29,13 +30,39 @@ type PerformanceServerSort = | "endpoint-desc" | "status-online" | "status-offline" + | "status-unknown" + | "status-degraded" | "uptime-desc" | "uptime-asc" -type PerformanceServerFilter = "all" | "online" | "offline" +type PerformanceServerFilter = "all" | ServerHealthState -function isOnlineServer(server: ServerInfo) { - return (server.state ?? "").toLowerCase() === "online" +const serverStates: ServerHealthState[] = ["online", "offline", "unknown", "degraded"] + +function getServerState(server: ServerInfo) { + return normalizeServerHealthState(server.state) +} + +function getServerStateLabel(state: ServerHealthState, t: (key: string) => string) { + if (state === "online") return t("Online") + if (state === "offline") return t("Offline") + if (state === "degraded") return t("Degraded") + return t("Unknown") +} + +function getServerStateTone(state: ServerHealthState) { + if (state === "online") return "bg-primary" + if (state === "offline") return "bg-destructive" + if (state === "degraded") return "bg-amber-500" + return "bg-muted-foreground" +} + +function getServerStateButtonClass(state: ServerHealthState, active: boolean) { + if (!active) return "shadow-none" + if (state === "offline") return "border-destructive/40 text-destructive shadow-none" + if (state === "degraded") return "border-amber-500/50 text-amber-700 shadow-none dark:text-amber-300" + if (state === "unknown") return "border-muted-foreground/40 text-muted-foreground shadow-none" + return "shadow-none" } function compareEndpoint(left: ServerInfo, right: ServerInfo, direction: "asc" | "desc") { @@ -68,7 +95,17 @@ function compareUptime(left: ServerInfo, right: ServerInfo, direction: "asc" | " export function PerformanceServerList({ servers, t }: { servers: ServerInfo[]; t: (key: string) => string }) { const [sortBy, setSortBy] = React.useState("default") const [filterBy, setFilterBy] = React.useState("all") - const onlineCount = servers.filter((server) => isOnlineServer(server)).length + const stateCounts = React.useMemo( + () => + servers.reduce( + (counts, server) => { + counts[getServerState(server)] += 1 + return counts + }, + { online: 0, offline: 0, unknown: 0, degraded: 0 } satisfies Record, + ), + [servers], + ) const visibleServers = React.useMemo(() => { const rows = servers @@ -77,8 +114,7 @@ export function PerformanceServerList({ servers, t }: { servers: ServerInfo[]; t originalIndex, })) .filter(({ server }) => { - if (filterBy === "online") return isOnlineServer(server) - if (filterBy === "offline") return !isOnlineServer(server) + if (filterBy !== "all") return getServerState(server) === filterBy return true }) @@ -90,13 +126,25 @@ export function PerformanceServerList({ servers, t }: { servers: ServerInfo[]; t return compareEndpoint(left.server, right.server, "desc") || left.originalIndex - right.originalIndex case "status-online": return ( - Number(isOnlineServer(right.server)) - Number(isOnlineServer(left.server)) || + Number(getServerState(right.server) === "online") - Number(getServerState(left.server) === "online") || compareEndpoint(left.server, right.server, "asc") || left.originalIndex - right.originalIndex ) case "status-offline": return ( - Number(isOnlineServer(left.server)) - Number(isOnlineServer(right.server)) || + Number(getServerState(right.server) === "offline") - Number(getServerState(left.server) === "offline") || + compareEndpoint(left.server, right.server, "asc") || + left.originalIndex - right.originalIndex + ) + case "status-unknown": + return ( + Number(getServerState(right.server) === "unknown") - Number(getServerState(left.server) === "unknown") || + compareEndpoint(left.server, right.server, "asc") || + left.originalIndex - right.originalIndex + ) + case "status-degraded": + return ( + Number(getServerState(right.server) === "degraded") - Number(getServerState(left.server) === "degraded") || compareEndpoint(left.server, right.server, "asc") || left.originalIndex - right.originalIndex ) @@ -125,24 +173,18 @@ export function PerformanceServerList({ servers, t }: { servers: ServerInfo[]; t {t("Total")}: {visibleServers.length}/{servers.length}
- - + {serverStates.map((state) => ( + + ))}
{t("Sort by")} @@ -156,6 +198,8 @@ export function PerformanceServerList({ servers, t }: { servers: ServerInfo[]; t {`${t("Endpoint")} ↓`} {`${t("Status")} · ${t("Online")}`} {`${t("Status")} · ${t("Offline")}`} + {`${t("Status")} · ${t("Unknown")}`} + {`${t("Status")} · ${t("Degraded")}`} {`${t("Uptime")} ↓`} {`${t("Uptime")} ↑`} @@ -174,12 +218,12 @@ export function PerformanceServerList({ servers, t }: { servers: ServerInfo[]; t
{server.endpoint ?? "--"} + + {getServerStateLabel(getServerState(server), t)} +
diff --git a/app/(dashboard)/status/page.tsx b/app/(dashboard)/status/page.tsx index 8e11c02..fb0ba9c 100644 --- a/app/(dashboard)/status/page.tsx +++ b/app/(dashboard)/status/page.tsx @@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button" import { Spinner } from "@/components/ui/spinner" import { usePerformanceData } from "@/hooks/use-performance-data" import { niceBytes } from "@/lib/functions" +import { normalizeServerHealthState, type ServerHealthState } from "@/lib/performance-data" import { RiArchiveDrawerFill, RiArchiveLine, @@ -115,15 +116,20 @@ export default function PerformancePage() { [t, fromLastStartTime, fromLastScanTime, lastScanTime], ) - const onlineServers = useMemo( - () => (systemInfo?.servers || []).filter((s) => s.state === "online").length, - [systemInfo], - ) + const serverCounts = useMemo(() => { + const counts: Record = { + online: 0, + offline: 0, + unknown: 0, + degraded: 0, + } - const offlineServers = useMemo( - () => (systemInfo?.servers || []).filter((s) => s.state === "offline").length, - [systemInfo], - ) + for (const server of systemInfo?.servers ?? []) { + counts[normalizeServerHealthState(server.state)] += 1 + } + + return counts + }, [systemInfo]) const backendInfo = useMemo( () => [ @@ -204,10 +210,13 @@ export default function PerformancePage() { /> diff --git a/i18n/locales/ar-MA.json b/i18n/locales/ar-MA.json index 2c00835..4a15128 100644 --- a/i18n/locales/ar-MA.json +++ b/i18n/locales/ar-MA.json @@ -1182,6 +1182,7 @@ "Understanding Key Types": "فهم أنواع المفاتيح", "Unhealthy": "غير سليم", "Unknown": "غير معروف", + "Degraded": "متدهور", "Unknown Folder": "مجلد غير معروف", "Unlimited": "غير محدود", "Unnamed Site": "موقع بدون اسم", diff --git a/i18n/locales/de-DE.json b/i18n/locales/de-DE.json index 39dbed0..2a8db0d 100644 --- a/i18n/locales/de-DE.json +++ b/i18n/locales/de-DE.json @@ -1182,6 +1182,7 @@ "Understanding Key Types": "Schlüsseltypen verstehen", "Unhealthy": "Fehlerhaft", "Unknown": "Unbekannt", + "Degraded": "Beeinträchtigt", "Unknown Folder": "Unbekannter Ordner", "Unlimited": "Unbegrenzt", "Unnamed Site": "Unbenannte Site", diff --git a/i18n/locales/en-US.json b/i18n/locales/en-US.json index 0aee5fb..1131372 100644 --- a/i18n/locales/en-US.json +++ b/i18n/locales/en-US.json @@ -1182,6 +1182,7 @@ "Understanding Key Types": "Understanding Key Types", "Unhealthy": "Unhealthy", "Unknown": "Unknown", + "Degraded": "Degraded", "Unknown Folder": "Unknown Folder", "Unlimited": "Unlimited", "Unnamed Site": "Unnamed Site", diff --git a/i18n/locales/es-ES.json b/i18n/locales/es-ES.json index 8e7468f..8845c12 100644 --- a/i18n/locales/es-ES.json +++ b/i18n/locales/es-ES.json @@ -1182,6 +1182,7 @@ "Understanding Key Types": "Comprensión de Tipos de Clave", "Unhealthy": "Con problemas", "Unknown": "Desconocido", + "Degraded": "Degradado", "Unknown Folder": "Carpeta Desconocida", "Unlimited": "Ilimitado", "Unnamed Site": "Sitio sin nombre", diff --git a/i18n/locales/fr-FR.json b/i18n/locales/fr-FR.json index e02eaaa..e188b46 100644 --- a/i18n/locales/fr-FR.json +++ b/i18n/locales/fr-FR.json @@ -1182,6 +1182,7 @@ "Understanding Key Types": "Comprendre les types de clés", "Unhealthy": "Défaillant", "Unknown": "Inconnu", + "Degraded": "Dégradé", "Unknown Folder": "Dossier inconnu", "Unlimited": "Illimité", "Unnamed Site": "Site sans nom", diff --git a/i18n/locales/id-ID.json b/i18n/locales/id-ID.json index dcd2345..82fa907 100644 --- a/i18n/locales/id-ID.json +++ b/i18n/locales/id-ID.json @@ -1182,6 +1182,7 @@ "Understanding Key Types": "Memahami Tipe Kunci", "Unhealthy": "Tidak sehat", "Unknown": "Tidak Diketahui", + "Degraded": "Terdegradasi", "Unknown Folder": "Folder Tidak Diketahui", "Unlimited": "Tidak Terbatas", "Unnamed Site": "Situs tanpa nama", diff --git a/i18n/locales/it-IT.json b/i18n/locales/it-IT.json index 91a6d2e..c2342f3 100644 --- a/i18n/locales/it-IT.json +++ b/i18n/locales/it-IT.json @@ -1182,6 +1182,7 @@ "Understanding Key Types": "Comprensione tipi chiave", "Unhealthy": "Non integro", "Unknown": "Sconosciuto", + "Degraded": "Degradato", "Unknown Folder": "Cartella sconosciuta", "Unlimited": "Illimitato", "Unnamed Site": "Sito senza nome", diff --git a/i18n/locales/ja-JP.json b/i18n/locales/ja-JP.json index bb5e7af..a51f369 100644 --- a/i18n/locales/ja-JP.json +++ b/i18n/locales/ja-JP.json @@ -1182,6 +1182,7 @@ "Understanding Key Types": "キータイプの理解", "Unhealthy": "異常", "Unknown": "不明", + "Degraded": "劣化", "Unknown Folder": "不明なフォルダ", "Unlimited": "無制限", "Unnamed Site": "無名のサイト", diff --git a/i18n/locales/ko-KR.json b/i18n/locales/ko-KR.json index 76c4a19..53cadf6 100644 --- a/i18n/locales/ko-KR.json +++ b/i18n/locales/ko-KR.json @@ -1182,6 +1182,7 @@ "Understanding Key Types": "키 유형 이해", "Unhealthy": "비정상", "Unknown": "알 수 없음", + "Degraded": "성능 저하", "Unknown Folder": "알 수 없는 폴더", "Unlimited": "무제한", "Unnamed Site": "이름 없는 사이트", diff --git a/i18n/locales/pt-BR.json b/i18n/locales/pt-BR.json index f13a1ff..16e7db2 100644 --- a/i18n/locales/pt-BR.json +++ b/i18n/locales/pt-BR.json @@ -1182,6 +1182,7 @@ "Understanding Key Types": "Entendendo Tipos de Chave", "Unhealthy": "Com problemas", "Unknown": "Desconhecido", + "Degraded": "Degradado", "Unknown Folder": "Pasta Desconhecida", "Unlimited": "Ilimitado", "Unnamed Site": "Site sem nome", diff --git a/i18n/locales/ru-RU.json b/i18n/locales/ru-RU.json index 30c370c..cb66dd9 100644 --- a/i18n/locales/ru-RU.json +++ b/i18n/locales/ru-RU.json @@ -1182,6 +1182,7 @@ "Understanding Key Types": "Понимание типов ключей", "Unhealthy": "Неисправен", "Unknown": "Неизвестно", + "Degraded": "Деградировано", "Unknown Folder": "Неизвестная папка", "Unlimited": "Неограниченно", "Unnamed Site": "Сайт без названия", diff --git a/i18n/locales/tr-TR.json b/i18n/locales/tr-TR.json index 7d8ef16..a416f04 100644 --- a/i18n/locales/tr-TR.json +++ b/i18n/locales/tr-TR.json @@ -1182,6 +1182,7 @@ "Understanding Key Types": "Anahtar Türlerini Anlamak", "Unhealthy": "Sağlıksız", "Unknown": "Bilinmeyen", + "Degraded": "Bozulmuş", "Unknown Folder": "Bilinmeyen Klasör", "Unlimited": "Sınırsız", "Unnamed Site": "Adsız Site", diff --git a/i18n/locales/vi-VN.json b/i18n/locales/vi-VN.json index af5cb30..3e1715d 100644 --- a/i18n/locales/vi-VN.json +++ b/i18n/locales/vi-VN.json @@ -1182,6 +1182,7 @@ "Understanding Key Types": "Tìm hiểu các loại khóa", "Unhealthy": "Không khỏe mạnh", "Unknown": "Không xác định", + "Degraded": "Suy giảm", "Unknown Folder": "Thư mục không xác định", "Unlimited": "Không giới hạn", "Unnamed Site": "Site chưa đặt tên", diff --git a/i18n/locales/zh-CN.json b/i18n/locales/zh-CN.json index 24f53c9..84dae13 100644 --- a/i18n/locales/zh-CN.json +++ b/i18n/locales/zh-CN.json @@ -1182,6 +1182,7 @@ "Understanding Key Types": "了解密钥类型", "Unhealthy": "异常", "Unknown": "未知", + "Degraded": "降级", "Unknown Folder": "未知文件夹", "Unlimited": "无限制", "Unnamed Site": "未命名站点", diff --git a/lib/performance-data.ts b/lib/performance-data.ts index 55696e4..b0d54d5 100644 --- a/lib/performance-data.ts +++ b/lib/performance-data.ts @@ -23,6 +23,7 @@ export interface SystemInfo { backendType?: string onlineDisks?: number offlineDisks?: number + unknownDisks?: number } } @@ -50,6 +51,8 @@ export interface MetricsInfo { type JsonRecord = Record +export type ServerHealthState = "online" | "offline" | "unknown" | "degraded" + function asRecord(value: unknown): JsonRecord { return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {} } @@ -71,6 +74,14 @@ function asString(value: unknown): string | undefined { return typeof value === "string" && value.trim() !== "" ? value : undefined } +export function normalizeServerHealthState(state?: string | null): ServerHealthState { + const normalized = state?.trim().toLowerCase() + if (normalized === "online" || normalized === "offline" || normalized === "unknown" || normalized === "degraded") { + return normalized + } + return "unknown" +} + function unwrapInfoRecord(value: unknown): JsonRecord { const record = asRecord(value) const wrapped = record.info ?? record.Info @@ -95,6 +106,7 @@ export function normalizeSystemInfo(value: unknown): SystemInfo { backendType: asString(backend.backendType ?? backend.BackendType), onlineDisks: asNumber(backend.onlineDisks ?? backend.OnlineDisks), offlineDisks: asNumber(backend.offlineDisks ?? backend.OfflineDisks), + unknownDisks: asNumber(backend.unknownDisks ?? backend.UnknownDisks), }, } } diff --git a/tests/lib/performance-data.test.js b/tests/lib/performance-data.test.js index 26ef160..ac92013 100644 --- a/tests/lib/performance-data.test.js +++ b/tests/lib/performance-data.test.js @@ -1,7 +1,17 @@ import test from "node:test" import assert from "node:assert/strict" -import { normalizeStorageInfo, normalizeSystemInfo } from "../../lib/performance-data.ts" +import { normalizeServerHealthState, normalizeStorageInfo, normalizeSystemInfo } from "../../lib/performance-data.ts" + +test("normalizeServerHealthState preserves backend admin info states", () => { + assert.equal(normalizeServerHealthState("online"), "online") + assert.equal(normalizeServerHealthState("offline"), "offline") + assert.equal(normalizeServerHealthState("unknown"), "unknown") + assert.equal(normalizeServerHealthState("degraded"), "degraded") + assert.equal(normalizeServerHealthState(" Degraded "), "degraded") + assert.equal(normalizeServerHealthState("initializing"), "unknown") + assert.equal(normalizeServerHealthState(undefined), "unknown") +}) test("normalizeSystemInfo preserves legacy unwrapped info responses", () => { const info = normalizeSystemInfo({ @@ -11,8 +21,13 @@ test("normalizeSystemInfo preserves legacy unwrapped info responses", () => { backendType: "Erasure", onlineDisks: 8, offlineDisks: 1, + unknownDisks: 1, }, - servers: [{ endpoint: "node-a", state: "online" }], + servers: [ + { endpoint: "node-a", state: "online" }, + { endpoint: "node-b", state: "unknown" }, + { endpoint: "node-c", state: "degraded" }, + ], }) assert.equal(info.buckets?.count, 2) @@ -20,7 +35,10 @@ test("normalizeSystemInfo preserves legacy unwrapped info responses", () => { assert.equal(info.backend?.backendType, "Erasure") assert.equal(info.backend?.onlineDisks, 8) assert.equal(info.backend?.offlineDisks, 1) + assert.equal(info.backend?.unknownDisks, 1) assert.equal(info.servers?.[0]?.endpoint, "node-a") + assert.equal(info.servers?.[1]?.state, "unknown") + assert.equal(info.servers?.[2]?.state, "degraded") }) test("normalizeSystemInfo unwraps RustFS admin discovery info responses", () => { @@ -32,10 +50,12 @@ test("normalizeSystemInfo unwraps RustFS admin discovery info responses", () => backendType: "Erasure", onlineDisks: 12, offlineDisks: 0, + unknownDisks: 1, }, servers: [ { endpoint: "rustfs-node7", state: "online", drives: [{ state: "ok" }] }, { endpoint: "10.0.0.9:19000", state: "offline", drives: [] }, + { endpoint: "rustfs-node8", state: "degraded", drives: [{ state: "ok" }] }, ], }, admin_discovery: { @@ -49,7 +69,9 @@ test("normalizeSystemInfo unwraps RustFS admin discovery info responses", () => assert.equal(info.objects?.count, 2560) assert.equal(info.backend?.onlineDisks, 12) assert.equal(info.backend?.offlineDisks, 0) - assert.equal(info.servers?.length, 2) + assert.equal(info.backend?.unknownDisks, 1) + assert.equal(info.servers?.length, 3) + assert.equal(info.servers?.[2]?.state, "degraded") }) test("normalizeStorageInfo unwraps RustFS admin discovery storage responses", () => { diff --git a/tests/lib/performance-status-source.test.js b/tests/lib/performance-status-source.test.js new file mode 100644 index 0000000..8ebb8b4 --- /dev/null +++ b/tests/lib/performance-status-source.test.js @@ -0,0 +1,33 @@ +import test from "node:test" +import assert from "node:assert/strict" +import fs from "node:fs" + +test("status page passes unknown and degraded admin info states into infrastructure health", () => { + const source = fs.readFileSync("app/(dashboard)/status/page.tsx", "utf8") + + assert.match(source, /normalizeServerHealthState\(server\.state\)/) + assert.match(source, /unknownServers=\{serverCounts\.unknown\}/) + assert.match(source, /degradedServers=\{serverCounts\.degraded\}/) + assert.match(source, /unknownDisks=\{systemInfo\?\.backend\?\.unknownDisks \?\? 0\}/) +}) + +test("performance infrastructure card renders unknown and degraded buckets", () => { + const source = fs.readFileSync("app/(dashboard)/_components/performance-infrastructure-card.tsx", "utf8") + + assert.match(source, /unknownServers: number/) + assert.match(source, /degradedServers: number/) + assert.match(source, /unknownDisks: number/) + assert.match(source, /\{t\("Unknown"\)\}/) + assert.match(source, /\{t\("Degraded"\)\}/) +}) + +test("performance server list treats unknown and degraded as first-class filters", () => { + const source = fs.readFileSync("app/(dashboard)/_components/performance-server-list.tsx", "utf8") + + assert.match(source, /const serverStates: ServerHealthState\[\] = \["online", "offline", "unknown", "degraded"\]/) + assert.match(source, /getServerState\(server\) === filterBy/) + assert.match(source, /status-unknown/) + assert.match(source, /status-degraded/) + assert.match(source, /getServerStateTone\(getServerState\(server\)\)/) + assert.doesNotMatch(source, /!isOnlineServer\(server\)/) +})