feat: add system, uptime, network and processes widgets

Add four new server statistics widgets:
- SystemWidget: displays hostname, OS, and kernel information
- UptimeWidget: shows server total uptime with formatted display
- NetworkWidget: lists network interfaces with IP and status
- ProcessesWidget: displays top processes by CPU usage

Backend changes:
- Extended SSH metrics collection to gather network, uptime, process, and system data
- Added commands to parse /proc/uptime, ip addr, ps aux output

Frontend changes:
- Created 4 new widget components with consistent styling
- Updated widget type definitions and HostManagerEditor
- Unified all widget heights to 280px for consistent layout
- Added translations for all new widgets (EN/ZH)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
ZacharyZcR
2025-10-09 13:21:09 +08:00
parent d87cabb708
commit d740abd0e8
11 changed files with 600 additions and 14 deletions

View File

@@ -217,10 +217,29 @@ export function HostManagerEditor({
statsConfig: z
.object({
enabledWidgets: z
.array(z.enum(["cpu", "memory", "disk"]))
.default(["cpu", "memory", "disk"]),
.array(
z.enum([
"cpu",
"memory",
"disk",
"network",
"uptime",
"processes",
"system",
]),
)
.default(["cpu", "memory", "disk", "network", "uptime", "system"]),
})
.default({ enabledWidgets: ["cpu", "memory", "disk"] }),
.default({
enabledWidgets: [
"cpu",
"memory",
"disk",
"network",
"uptime",
"system",
],
}),
})
.superRefine((data, ctx) => {
if (data.authType === "password") {
@@ -1562,7 +1581,17 @@ export function HostManagerEditor({
{t("hosts.enabledWidgetsDesc")}
</FormDescription>
<div className="space-y-3 mt-3">
{(["cpu", "memory", "disk"] as const).map((widget) => (
{(
[
"cpu",
"memory",
"disk",
"network",
"uptime",
"processes",
"system",
] as const
).map((widget) => (
<div
key={widget}
className="flex items-center space-x-2"
@@ -1585,6 +1614,13 @@ export function HostManagerEditor({
{widget === "memory" &&
t("serverStats.memoryUsage")}
{widget === "disk" && t("serverStats.diskUsage")}
{widget === "network" &&
t("serverStats.networkInterfaces")}
{widget === "uptime" && t("serverStats.uptime")}
{widget === "processes" &&
t("serverStats.processes")}
{widget === "system" &&
t("serverStats.systemInfo")}
</label>
</div>
))}

View File

@@ -17,7 +17,15 @@ import {
type StatsConfig,
DEFAULT_STATS_CONFIG,
} from "@/types/stats-widgets";
import { CpuWidget, MemoryWidget, DiskWidget } from "./widgets";
import {
CpuWidget,
MemoryWidget,
DiskWidget,
NetworkWidget,
UptimeWidget,
ProcessesWidget,
SystemWidget,
} from "./widgets";
interface ServerProps {
hostConfig?: any;
@@ -82,6 +90,26 @@ export function Server({
case "disk":
return <DiskWidget metrics={metrics} metricsHistory={metricsHistory} />;
case "network":
return (
<NetworkWidget metrics={metrics} metricsHistory={metricsHistory} />
);
case "uptime":
return (
<UptimeWidget metrics={metrics} metricsHistory={metricsHistory} />
);
case "processes":
return (
<ProcessesWidget metrics={metrics} metricsHistory={metricsHistory} />
);
case "system":
return (
<SystemWidget metrics={metrics} metricsHistory={metricsHistory} />
);
default:
return null;
}
@@ -373,7 +401,7 @@ export function Server({
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{enabledWidgets.map((widgetType) => (
<div key={widgetType} className="min-h-[200px]">
<div key={widgetType} className="h-[280px]">
{renderWidget(widgetType)}
</div>
))}

View File

@@ -0,0 +1,64 @@
import React from "react";
import { Network, Wifi, WifiOff } from "lucide-react";
import { useTranslation } from "react-i18next";
import type { ServerMetrics } from "@/ui/main-axios.ts";
interface NetworkWidgetProps {
metrics: ServerMetrics | null;
metricsHistory: ServerMetrics[];
}
export function NetworkWidget({ metrics }: NetworkWidgetProps) {
const { t } = useTranslation();
const network = (metrics as any)?.network;
const interfaces = network?.interfaces || [];
return (
<div className="h-full w-full p-4 rounded-lg bg-dark-bg/50 border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden">
<div className="flex items-center gap-2 flex-shrink-0 mb-3">
<Network className="h-5 w-5 text-indigo-400" />
<h3 className="font-semibold text-lg text-white">
{t("serverStats.networkInterfaces")}
</h3>
</div>
<div className="space-y-2 overflow-auto flex-1">
{interfaces.length === 0 ? (
<div className="flex flex-col items-center justify-center py-8 text-gray-400">
<WifiOff className="h-8 w-8 mb-2" />
<p className="text-sm">{t("serverStats.noInterfacesFound")}</p>
</div>
) : (
interfaces.map((iface: any, index: number) => (
<div
key={index}
className="p-3 rounded-md bg-dark-bg/50 border border-dark-border/30"
>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<Wifi
className={`h-4 w-4 ${iface.state === "UP" ? "text-green-400" : "text-gray-400"}`}
/>
<span className="text-sm font-semibold text-white font-mono">
{iface.name}
</span>
</div>
<span
className={`text-xs px-2 py-0.5 rounded-full ${
iface.state === "UP"
? "bg-green-500/20 text-green-400"
: "bg-gray-500/20 text-gray-400"
}`}
>
{iface.state}
</span>
</div>
<div className="text-xs text-gray-400 font-mono">{iface.ip}</div>
</div>
))
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,76 @@
import React from "react";
import { List, Activity } from "lucide-react";
import { useTranslation } from "react-i18next";
import type { ServerMetrics } from "@/ui/main-axios.ts";
interface ProcessesWidgetProps {
metrics: ServerMetrics | null;
metricsHistory: ServerMetrics[];
}
export function ProcessesWidget({ metrics }: ProcessesWidgetProps) {
const { t } = useTranslation();
const processes = (metrics as any)?.processes;
const topProcesses = processes?.top || [];
return (
<div className="h-full w-full p-4 rounded-lg bg-dark-bg/50 border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden">
<div className="flex items-center gap-2 flex-shrink-0 mb-3">
<List className="h-5 w-5 text-yellow-400" />
<h3 className="font-semibold text-lg text-white">
{t("serverStats.processes")}
</h3>
</div>
<div className="flex items-center justify-between mb-3 pb-2 border-b border-dark-border/30">
<div className="text-sm text-gray-400">
{t("serverStats.totalProcesses")}:{" "}
<span className="text-white font-semibold">
{processes?.total ?? "N/A"}
</span>
</div>
<div className="text-sm text-gray-400">
{t("serverStats.running")}:{" "}
<span className="text-green-400 font-semibold">
{processes?.running ?? "N/A"}
</span>
</div>
</div>
<div className="overflow-auto flex-1">
{topProcesses.length === 0 ? (
<div className="flex flex-col items-center justify-center py-8 text-gray-400">
<Activity className="h-8 w-8 mb-2" />
<p className="text-sm">{t("serverStats.noProcessesFound")}</p>
</div>
) : (
<div className="space-y-1">
{topProcesses.map((proc: any, index: number) => (
<div
key={index}
className="p-2 rounded-md bg-dark-bg/30 hover:bg-dark-bg/50 transition-colors"
>
<div className="flex items-center justify-between mb-1">
<span className="text-xs font-mono text-gray-400">
PID: {proc.pid}
</span>
<div className="flex gap-3 text-xs">
<span className="text-blue-400">CPU: {proc.cpu}%</span>
<span className="text-green-400">MEM: {proc.mem}%</span>
</div>
</div>
<div className="text-xs text-white font-mono truncate">
{proc.command}
</div>
<div className="text-xs text-gray-500 mt-1">
User: {proc.user}
</div>
</div>
))}
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,64 @@
import React from "react";
import { Server, Info } from "lucide-react";
import { useTranslation } from "react-i18next";
import type { ServerMetrics } from "@/ui/main-axios.ts";
interface SystemWidgetProps {
metrics: ServerMetrics | null;
metricsHistory: ServerMetrics[];
}
export function SystemWidget({ metrics }: SystemWidgetProps) {
const { t } = useTranslation();
const system = (metrics as any)?.system;
return (
<div className="h-full w-full p-4 rounded-lg bg-dark-bg/50 border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden">
<div className="flex items-center gap-2 flex-shrink-0 mb-3">
<Server className="h-5 w-5 text-purple-400" />
<h3 className="font-semibold text-lg text-white">
{t("serverStats.systemInfo")}
</h3>
</div>
<div className="space-y-3">
<div className="flex items-start gap-3">
<Info className="h-4 w-4 text-gray-400 mt-0.5 flex-shrink-0" />
<div className="min-w-0 flex-1">
<p className="text-xs text-gray-400 mb-1">
{t("serverStats.hostname")}
</p>
<p className="text-sm text-white font-mono truncate">
{system?.hostname || "N/A"}
</p>
</div>
</div>
<div className="flex items-start gap-3">
<Info className="h-4 w-4 text-gray-400 mt-0.5 flex-shrink-0" />
<div className="min-w-0 flex-1">
<p className="text-xs text-gray-400 mb-1">
{t("serverStats.operatingSystem")}
</p>
<p className="text-sm text-white font-mono truncate">
{system?.os || "N/A"}
</p>
</div>
</div>
<div className="flex items-start gap-3">
<Info className="h-4 w-4 text-gray-400 mt-0.5 flex-shrink-0" />
<div className="min-w-0 flex-1">
<p className="text-xs text-gray-400 mb-1">
{t("serverStats.kernel")}
</p>
<p className="text-sm text-white font-mono truncate">
{system?.kernel || "N/A"}
</p>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,49 @@
import React from "react";
import { Clock, Activity } from "lucide-react";
import { useTranslation } from "react-i18next";
import type { ServerMetrics } from "@/ui/main-axios.ts";
interface UptimeWidgetProps {
metrics: ServerMetrics | null;
metricsHistory: ServerMetrics[];
}
export function UptimeWidget({ metrics }: UptimeWidgetProps) {
const { t } = useTranslation();
const uptime = (metrics as any)?.uptime;
return (
<div className="h-full w-full p-4 rounded-lg bg-dark-bg/50 border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden">
<div className="flex items-center gap-2 flex-shrink-0 mb-3">
<Clock className="h-5 w-5 text-cyan-400" />
<h3 className="font-semibold text-lg text-white">
{t("serverStats.uptime")}
</h3>
</div>
<div className="flex flex-col items-center justify-center flex-1">
<div className="relative mb-4">
<div className="w-24 h-24 rounded-full bg-cyan-500/10 flex items-center justify-center">
<Activity className="h-12 w-12 text-cyan-400" />
</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-cyan-400 mb-2">
{uptime?.formatted || "N/A"}
</div>
<div className="text-sm text-gray-400">
{t("serverStats.totalUptime")}
</div>
{uptime?.seconds && (
<div className="text-xs text-gray-500 mt-2">
{Math.floor(uptime.seconds).toLocaleString()}{" "}
{t("serverStats.seconds")}
</div>
)}
</div>
</div>
</div>
);
}

View File

@@ -1,3 +1,7 @@
export { CpuWidget } from "./CpuWidget";
export { MemoryWidget } from "./MemoryWidget";
export { DiskWidget } from "./DiskWidget";
export { NetworkWidget } from "./NetworkWidget";
export { UptimeWidget } from "./UptimeWidget";
export { ProcessesWidget } from "./ProcessesWidget";
export { SystemWidget } from "./SystemWidget";