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:
@@ -690,6 +690,36 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{
|
||||
percent: number | null;
|
||||
usedHuman: string | null;
|
||||
totalHuman: string | null;
|
||||
availableHuman: string | null;
|
||||
};
|
||||
network: {
|
||||
interfaces: Array<{
|
||||
name: string;
|
||||
ip: string;
|
||||
state: string;
|
||||
rxBytes: string | null;
|
||||
txBytes: string | null;
|
||||
}>;
|
||||
};
|
||||
uptime: {
|
||||
seconds: number | null;
|
||||
formatted: string | null;
|
||||
};
|
||||
processes: {
|
||||
total: number | null;
|
||||
running: number | null;
|
||||
top: Array<{
|
||||
pid: string;
|
||||
user: string;
|
||||
cpu: string;
|
||||
mem: string;
|
||||
command: string;
|
||||
}>;
|
||||
};
|
||||
system: {
|
||||
hostname: string | null;
|
||||
kernel: string | null;
|
||||
os: string | null;
|
||||
};
|
||||
}> {
|
||||
const cached = metricsCache.get(host.id);
|
||||
@@ -842,6 +872,159 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{
|
||||
availableHuman = null;
|
||||
}
|
||||
|
||||
// Collect network interfaces
|
||||
let interfaces: Array<{
|
||||
name: string;
|
||||
ip: string;
|
||||
state: string;
|
||||
rxBytes: string | null;
|
||||
txBytes: string | null;
|
||||
}> = [];
|
||||
try {
|
||||
const ifconfigOut = await execCommand(
|
||||
client,
|
||||
"ip -o addr show | awk '{print $2,$4}' | grep -v '^lo'",
|
||||
);
|
||||
const netStatOut = await execCommand(
|
||||
client,
|
||||
"ip -o link show | awk '{print $2,$9}' | sed 's/:$//'",
|
||||
);
|
||||
|
||||
const addrs = ifconfigOut.stdout
|
||||
.split("\n")
|
||||
.map((l) => l.trim())
|
||||
.filter(Boolean);
|
||||
const states = netStatOut.stdout
|
||||
.split("\n")
|
||||
.map((l) => l.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const ifMap = new Map<string, { ip: string; state: string }>();
|
||||
for (const line of addrs) {
|
||||
const parts = line.split(/\s+/);
|
||||
if (parts.length >= 2) {
|
||||
const name = parts[0];
|
||||
const ip = parts[1].split("/")[0];
|
||||
if (!ifMap.has(name)) ifMap.set(name, { ip, state: "UNKNOWN" });
|
||||
}
|
||||
}
|
||||
for (const line of states) {
|
||||
const parts = line.split(/\s+/);
|
||||
if (parts.length >= 2) {
|
||||
const name = parts[0];
|
||||
const state = parts[1];
|
||||
const existing = ifMap.get(name);
|
||||
if (existing) {
|
||||
existing.state = state;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [name, data] of ifMap.entries()) {
|
||||
interfaces.push({
|
||||
name,
|
||||
ip: data.ip,
|
||||
state: data.state,
|
||||
rxBytes: null,
|
||||
txBytes: null,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
statsLogger.warn(
|
||||
`Failed to collect network metrics for host ${host.id}`,
|
||||
e,
|
||||
);
|
||||
}
|
||||
|
||||
// Collect uptime
|
||||
let uptimeSeconds: number | null = null;
|
||||
let uptimeFormatted: string | null = null;
|
||||
try {
|
||||
const uptimeOut = await execCommand(client, "cat /proc/uptime");
|
||||
const uptimeParts = uptimeOut.stdout.trim().split(/\s+/);
|
||||
if (uptimeParts.length >= 1) {
|
||||
uptimeSeconds = Number(uptimeParts[0]);
|
||||
if (Number.isFinite(uptimeSeconds)) {
|
||||
const days = Math.floor(uptimeSeconds / 86400);
|
||||
const hours = Math.floor((uptimeSeconds % 86400) / 3600);
|
||||
const minutes = Math.floor((uptimeSeconds % 3600) / 60);
|
||||
uptimeFormatted = `${days}d ${hours}h ${minutes}m`;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
statsLogger.warn(`Failed to collect uptime for host ${host.id}`, e);
|
||||
}
|
||||
|
||||
// Collect process information
|
||||
let totalProcesses: number | null = null;
|
||||
let runningProcesses: number | null = null;
|
||||
let topProcesses: Array<{
|
||||
pid: string;
|
||||
user: string;
|
||||
cpu: string;
|
||||
mem: string;
|
||||
command: string;
|
||||
}> = [];
|
||||
try {
|
||||
const psOut = await execCommand(
|
||||
client,
|
||||
"ps aux --sort=-%cpu | head -n 11",
|
||||
);
|
||||
const psLines = psOut.stdout
|
||||
.split("\n")
|
||||
.map((l) => l.trim())
|
||||
.filter(Boolean);
|
||||
if (psLines.length > 1) {
|
||||
for (let i = 1; i < Math.min(psLines.length, 11); i++) {
|
||||
const parts = psLines[i].split(/\s+/);
|
||||
if (parts.length >= 11) {
|
||||
topProcesses.push({
|
||||
pid: parts[1],
|
||||
user: parts[0],
|
||||
cpu: parts[2],
|
||||
mem: parts[3],
|
||||
command: parts.slice(10).join(" ").substring(0, 50),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const procCount = await execCommand(client, "ps aux | wc -l");
|
||||
const runningCount = await execCommand(
|
||||
client,
|
||||
"ps aux | grep -c ' R '",
|
||||
);
|
||||
totalProcesses = Number(procCount.stdout.trim()) - 1;
|
||||
runningProcesses = Number(runningCount.stdout.trim());
|
||||
} catch (e) {
|
||||
statsLogger.warn(
|
||||
`Failed to collect process info for host ${host.id}`,
|
||||
e,
|
||||
);
|
||||
}
|
||||
|
||||
// Collect system information
|
||||
let hostname: string | null = null;
|
||||
let kernel: string | null = null;
|
||||
let os: string | null = null;
|
||||
try {
|
||||
const hostnameOut = await execCommand(client, "hostname");
|
||||
const kernelOut = await execCommand(client, "uname -r");
|
||||
const osOut = await execCommand(
|
||||
client,
|
||||
"cat /etc/os-release | grep '^PRETTY_NAME=' | cut -d'\"' -f2",
|
||||
);
|
||||
|
||||
hostname = hostnameOut.stdout.trim() || null;
|
||||
kernel = kernelOut.stdout.trim() || null;
|
||||
os = osOut.stdout.trim() || null;
|
||||
} catch (e) {
|
||||
statsLogger.warn(
|
||||
`Failed to collect system info for host ${host.id}`,
|
||||
e,
|
||||
);
|
||||
}
|
||||
|
||||
const result = {
|
||||
cpu: { percent: toFixedNum(cpuPercent, 0), cores, load: loadTriplet },
|
||||
memory: {
|
||||
@@ -855,6 +1038,23 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{
|
||||
totalHuman,
|
||||
availableHuman,
|
||||
},
|
||||
network: {
|
||||
interfaces,
|
||||
},
|
||||
uptime: {
|
||||
seconds: uptimeSeconds,
|
||||
formatted: uptimeFormatted,
|
||||
},
|
||||
processes: {
|
||||
total: totalProcesses,
|
||||
running: runningProcesses,
|
||||
top: topProcesses,
|
||||
},
|
||||
system: {
|
||||
hostname,
|
||||
kernel,
|
||||
os,
|
||||
},
|
||||
};
|
||||
|
||||
metricsCache.set(host.id, result);
|
||||
@@ -1033,7 +1233,16 @@ app.get("/metrics/:id", validateHostId, async (req, res) => {
|
||||
error: "Host is offline",
|
||||
cpu: { percent: null, cores: null, load: null },
|
||||
memory: { percent: null, usedGiB: null, totalGiB: null },
|
||||
disk: { percent: null, usedHuman: null, totalHuman: null },
|
||||
disk: {
|
||||
percent: null,
|
||||
usedHuman: null,
|
||||
totalHuman: null,
|
||||
availableHuman: null,
|
||||
},
|
||||
network: { interfaces: [] },
|
||||
uptime: { seconds: null, formatted: null },
|
||||
processes: { total: null, running: null, top: [] },
|
||||
system: { hostname: null, kernel: null, os: null },
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
@@ -1050,7 +1259,16 @@ app.get("/metrics/:id", validateHostId, async (req, res) => {
|
||||
message: "Server Stats unavailable for TOTP-enabled servers",
|
||||
cpu: { percent: null, cores: null, load: null },
|
||||
memory: { percent: null, usedGiB: null, totalGiB: null },
|
||||
disk: { percent: null, usedHuman: null, totalHuman: null },
|
||||
disk: {
|
||||
percent: null,
|
||||
usedHuman: null,
|
||||
totalHuman: null,
|
||||
availableHuman: null,
|
||||
},
|
||||
network: { interfaces: [] },
|
||||
uptime: { seconds: null, formatted: null },
|
||||
processes: { total: null, running: null, top: [] },
|
||||
system: { hostname: null, kernel: null, os: null },
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
@@ -1062,7 +1280,16 @@ app.get("/metrics/:id", validateHostId, async (req, res) => {
|
||||
error: "Metrics collection timeout",
|
||||
cpu: { percent: null, cores: null, load: null },
|
||||
memory: { percent: null, usedGiB: null, totalGiB: null },
|
||||
disk: { percent: null, usedHuman: null, totalHuman: null },
|
||||
disk: {
|
||||
percent: null,
|
||||
usedHuman: null,
|
||||
totalHuman: null,
|
||||
availableHuman: null,
|
||||
},
|
||||
network: { interfaces: [] },
|
||||
uptime: { seconds: null, formatted: null },
|
||||
processes: { total: null, running: null, top: [] },
|
||||
system: { hostname: null, kernel: null, os: null },
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
@@ -1071,7 +1298,16 @@ app.get("/metrics/:id", validateHostId, async (req, res) => {
|
||||
error: "Failed to collect metrics",
|
||||
cpu: { percent: null, cores: null, load: null },
|
||||
memory: { percent: null, usedGiB: null, totalGiB: null },
|
||||
disk: { percent: null, usedHuman: null, totalHuman: null },
|
||||
disk: {
|
||||
percent: null,
|
||||
usedHuman: null,
|
||||
totalHuman: null,
|
||||
availableHuman: null,
|
||||
},
|
||||
network: { interfaces: [] },
|
||||
uptime: { seconds: null, formatted: null },
|
||||
processes: { total: null, running: null, top: [] },
|
||||
system: { hostname: null, kernel: null, os: null },
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1169,7 +1169,18 @@
|
||||
"saveLayout": "Save Layout",
|
||||
"unsavedChanges": "Unsaved changes",
|
||||
"layoutSaved": "Layout saved successfully",
|
||||
"failedToSaveLayout": "Failed to save layout"
|
||||
"failedToSaveLayout": "Failed to save layout",
|
||||
"systemInfo": "System Information",
|
||||
"hostname": "Hostname",
|
||||
"operatingSystem": "Operating System",
|
||||
"kernel": "Kernel",
|
||||
"totalUptime": "Total Uptime",
|
||||
"seconds": "seconds",
|
||||
"networkInterfaces": "Network Interfaces",
|
||||
"noInterfacesFound": "No network interfaces found",
|
||||
"totalProcesses": "Total Processes",
|
||||
"running": "Running",
|
||||
"noProcessesFound": "No processes found"
|
||||
},
|
||||
"auth": {
|
||||
"loginTitle": "Login to Termix",
|
||||
|
||||
@@ -1149,7 +1149,18 @@
|
||||
"saveLayout": "保存布局",
|
||||
"unsavedChanges": "有未保存的更改",
|
||||
"layoutSaved": "布局保存成功",
|
||||
"failedToSaveLayout": "保存布局失败"
|
||||
"failedToSaveLayout": "保存布局失败",
|
||||
"systemInfo": "系统信息",
|
||||
"hostname": "主机名",
|
||||
"operatingSystem": "操作系统",
|
||||
"kernel": "内核",
|
||||
"totalUptime": "总运行时间",
|
||||
"seconds": "秒",
|
||||
"networkInterfaces": "网络接口",
|
||||
"noInterfacesFound": "未找到网络接口",
|
||||
"totalProcesses": "总进程数",
|
||||
"running": "运行中",
|
||||
"noProcessesFound": "未找到进程"
|
||||
},
|
||||
"auth": {
|
||||
"loginTitle": "登录 Termix",
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
export type WidgetType = "cpu" | "memory" | "disk";
|
||||
export type WidgetType =
|
||||
| "cpu"
|
||||
| "memory"
|
||||
| "disk"
|
||||
| "network"
|
||||
| "uptime"
|
||||
| "processes"
|
||||
| "system";
|
||||
|
||||
export interface StatsConfig {
|
||||
enabledWidgets: WidgetType[];
|
||||
}
|
||||
|
||||
export const DEFAULT_STATS_CONFIG: StatsConfig = {
|
||||
enabledWidgets: ["cpu", "memory", "disk"],
|
||||
enabledWidgets: ["cpu", "memory", "disk", "network", "uptime", "system"],
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
64
src/ui/Desktop/Apps/Server/widgets/NetworkWidget.tsx
Normal file
64
src/ui/Desktop/Apps/Server/widgets/NetworkWidget.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
76
src/ui/Desktop/Apps/Server/widgets/ProcessesWidget.tsx
Normal file
76
src/ui/Desktop/Apps/Server/widgets/ProcessesWidget.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
64
src/ui/Desktop/Apps/Server/widgets/SystemWidget.tsx
Normal file
64
src/ui/Desktop/Apps/Server/widgets/SystemWidget.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
49
src/ui/Desktop/Apps/Server/widgets/UptimeWidget.tsx
Normal file
49
src/ui/Desktop/Apps/Server/widgets/UptimeWidget.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user