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;
|
percent: number | null;
|
||||||
usedHuman: string | null;
|
usedHuman: string | null;
|
||||||
totalHuman: 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);
|
const cached = metricsCache.get(host.id);
|
||||||
@@ -842,6 +872,159 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{
|
|||||||
availableHuman = null;
|
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 = {
|
const result = {
|
||||||
cpu: { percent: toFixedNum(cpuPercent, 0), cores, load: loadTriplet },
|
cpu: { percent: toFixedNum(cpuPercent, 0), cores, load: loadTriplet },
|
||||||
memory: {
|
memory: {
|
||||||
@@ -855,6 +1038,23 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{
|
|||||||
totalHuman,
|
totalHuman,
|
||||||
availableHuman,
|
availableHuman,
|
||||||
},
|
},
|
||||||
|
network: {
|
||||||
|
interfaces,
|
||||||
|
},
|
||||||
|
uptime: {
|
||||||
|
seconds: uptimeSeconds,
|
||||||
|
formatted: uptimeFormatted,
|
||||||
|
},
|
||||||
|
processes: {
|
||||||
|
total: totalProcesses,
|
||||||
|
running: runningProcesses,
|
||||||
|
top: topProcesses,
|
||||||
|
},
|
||||||
|
system: {
|
||||||
|
hostname,
|
||||||
|
kernel,
|
||||||
|
os,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
metricsCache.set(host.id, result);
|
metricsCache.set(host.id, result);
|
||||||
@@ -1033,7 +1233,16 @@ app.get("/metrics/:id", validateHostId, async (req, res) => {
|
|||||||
error: "Host is offline",
|
error: "Host is offline",
|
||||||
cpu: { percent: null, cores: null, load: null },
|
cpu: { percent: null, cores: null, load: null },
|
||||||
memory: { percent: null, usedGiB: null, totalGiB: 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(),
|
lastChecked: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1050,7 +1259,16 @@ app.get("/metrics/:id", validateHostId, async (req, res) => {
|
|||||||
message: "Server Stats unavailable for TOTP-enabled servers",
|
message: "Server Stats unavailable for TOTP-enabled servers",
|
||||||
cpu: { percent: null, cores: null, load: null },
|
cpu: { percent: null, cores: null, load: null },
|
||||||
memory: { percent: null, usedGiB: null, totalGiB: 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(),
|
lastChecked: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1062,7 +1280,16 @@ app.get("/metrics/:id", validateHostId, async (req, res) => {
|
|||||||
error: "Metrics collection timeout",
|
error: "Metrics collection timeout",
|
||||||
cpu: { percent: null, cores: null, load: null },
|
cpu: { percent: null, cores: null, load: null },
|
||||||
memory: { percent: null, usedGiB: null, totalGiB: 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(),
|
lastChecked: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1071,7 +1298,16 @@ app.get("/metrics/:id", validateHostId, async (req, res) => {
|
|||||||
error: "Failed to collect metrics",
|
error: "Failed to collect metrics",
|
||||||
cpu: { percent: null, cores: null, load: null },
|
cpu: { percent: null, cores: null, load: null },
|
||||||
memory: { percent: null, usedGiB: null, totalGiB: 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(),
|
lastChecked: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1169,7 +1169,18 @@
|
|||||||
"saveLayout": "Save Layout",
|
"saveLayout": "Save Layout",
|
||||||
"unsavedChanges": "Unsaved changes",
|
"unsavedChanges": "Unsaved changes",
|
||||||
"layoutSaved": "Layout saved successfully",
|
"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": {
|
"auth": {
|
||||||
"loginTitle": "Login to Termix",
|
"loginTitle": "Login to Termix",
|
||||||
|
|||||||
@@ -1149,7 +1149,18 @@
|
|||||||
"saveLayout": "保存布局",
|
"saveLayout": "保存布局",
|
||||||
"unsavedChanges": "有未保存的更改",
|
"unsavedChanges": "有未保存的更改",
|
||||||
"layoutSaved": "布局保存成功",
|
"layoutSaved": "布局保存成功",
|
||||||
"failedToSaveLayout": "保存布局失败"
|
"failedToSaveLayout": "保存布局失败",
|
||||||
|
"systemInfo": "系统信息",
|
||||||
|
"hostname": "主机名",
|
||||||
|
"operatingSystem": "操作系统",
|
||||||
|
"kernel": "内核",
|
||||||
|
"totalUptime": "总运行时间",
|
||||||
|
"seconds": "秒",
|
||||||
|
"networkInterfaces": "网络接口",
|
||||||
|
"noInterfacesFound": "未找到网络接口",
|
||||||
|
"totalProcesses": "总进程数",
|
||||||
|
"running": "运行中",
|
||||||
|
"noProcessesFound": "未找到进程"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"loginTitle": "登录 Termix",
|
"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 {
|
export interface StatsConfig {
|
||||||
enabledWidgets: WidgetType[];
|
enabledWidgets: WidgetType[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_STATS_CONFIG: StatsConfig = {
|
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
|
statsConfig: z
|
||||||
.object({
|
.object({
|
||||||
enabledWidgets: z
|
enabledWidgets: z
|
||||||
.array(z.enum(["cpu", "memory", "disk"]))
|
.array(
|
||||||
.default(["cpu", "memory", "disk"]),
|
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) => {
|
.superRefine((data, ctx) => {
|
||||||
if (data.authType === "password") {
|
if (data.authType === "password") {
|
||||||
@@ -1562,7 +1581,17 @@ export function HostManagerEditor({
|
|||||||
{t("hosts.enabledWidgetsDesc")}
|
{t("hosts.enabledWidgetsDesc")}
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<div className="space-y-3 mt-3">
|
<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
|
<div
|
||||||
key={widget}
|
key={widget}
|
||||||
className="flex items-center space-x-2"
|
className="flex items-center space-x-2"
|
||||||
@@ -1585,6 +1614,13 @@ export function HostManagerEditor({
|
|||||||
{widget === "memory" &&
|
{widget === "memory" &&
|
||||||
t("serverStats.memoryUsage")}
|
t("serverStats.memoryUsage")}
|
||||||
{widget === "disk" && t("serverStats.diskUsage")}
|
{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>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -17,7 +17,15 @@ import {
|
|||||||
type StatsConfig,
|
type StatsConfig,
|
||||||
DEFAULT_STATS_CONFIG,
|
DEFAULT_STATS_CONFIG,
|
||||||
} from "@/types/stats-widgets";
|
} from "@/types/stats-widgets";
|
||||||
import { CpuWidget, MemoryWidget, DiskWidget } from "./widgets";
|
import {
|
||||||
|
CpuWidget,
|
||||||
|
MemoryWidget,
|
||||||
|
DiskWidget,
|
||||||
|
NetworkWidget,
|
||||||
|
UptimeWidget,
|
||||||
|
ProcessesWidget,
|
||||||
|
SystemWidget,
|
||||||
|
} from "./widgets";
|
||||||
|
|
||||||
interface ServerProps {
|
interface ServerProps {
|
||||||
hostConfig?: any;
|
hostConfig?: any;
|
||||||
@@ -82,6 +90,26 @@ export function Server({
|
|||||||
case "disk":
|
case "disk":
|
||||||
return <DiskWidget metrics={metrics} metricsHistory={metricsHistory} />;
|
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:
|
default:
|
||||||
return null;
|
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">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{enabledWidgets.map((widgetType) => (
|
{enabledWidgets.map((widgetType) => (
|
||||||
<div key={widgetType} className="min-h-[200px]">
|
<div key={widgetType} className="h-[280px]">
|
||||||
{renderWidget(widgetType)}
|
{renderWidget(widgetType)}
|
||||||
</div>
|
</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 { CpuWidget } from "./CpuWidget";
|
||||||
export { MemoryWidget } from "./MemoryWidget";
|
export { MemoryWidget } from "./MemoryWidget";
|
||||||
export { DiskWidget } from "./DiskWidget";
|
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