feat: add listening ports widget for server stats (#483)

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
This commit was merged in pull request #483.
This commit is contained in:
ZacharyZcR
2026-01-12 15:46:05 +08:00
committed by GitHub
parent d821373b15
commit 2e3f7e10c7
9 changed files with 308 additions and 0 deletions

View File

@@ -33,6 +33,7 @@ import {
ProcessesWidget,
SystemWidget,
LoginStatsWidget,
PortsWidget,
FirewallWidget,
} from "./widgets";
import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader.tsx";
@@ -266,6 +267,10 @@ export function ServerStats({
<LoginStatsWidget metrics={metrics} metricsHistory={metricsHistory} />
);
case "ports":
return (
<PortsWidget metrics={metrics} metricsHistory={metricsHistory} />
case "firewall":
return (
<FirewallWidget metrics={metrics} metricsHistory={metricsHistory} />

View File

@@ -0,0 +1,98 @@
import React from "react";
import { Network } from "lucide-react";
import { useTranslation } from "react-i18next";
import type { ServerMetrics } from "@/ui/main-axios.ts";
import type { PortsMetrics, ListeningPort } from "@/types/stats-widgets";
interface PortsWidgetProps {
metrics: ServerMetrics | null;
metricsHistory: ServerMetrics[];
}
function PortRow({ port }: { port: ListeningPort }) {
const formatAddress = (addr: string) => {
if (addr === "0.0.0.0" || addr === "*" || addr === "::") {
return "*";
}
return addr;
};
return (
<div className="grid grid-cols-5 gap-2 text-xs py-1.5 border-b border-edge/30 last:border-0">
<div className="font-mono text-foreground-subtle">
{port.protocol.toUpperCase()}
</div>
<div className="font-mono text-foreground">
{port.localPort}
</div>
<div className="font-mono text-foreground-subtle truncate" title={formatAddress(port.localAddress)}>
{formatAddress(port.localAddress)}
</div>
<div className="text-foreground-subtle">
{port.state || "-"}
</div>
<div className="text-foreground-subtle truncate" title={port.process || "-"}>
{port.process || (port.pid ? `PID:${port.pid}` : "-")}
</div>
</div>
);
}
export function PortsWidget({ metrics }: PortsWidgetProps) {
const { t } = useTranslation();
const portsData = (
metrics as ServerMetrics & { ports?: PortsMetrics }
)?.ports;
const tcpPorts = portsData?.ports.filter(p => p.protocol === "tcp") || [];
const udpPorts = portsData?.ports.filter(p => p.protocol === "udp") || [];
return (
<div className="h-full w-full p-4 rounded-lg bg-canvas/50 border border-edge/50 hover:bg-canvas/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-cyan-400" />
<h3 className="font-semibold text-lg text-foreground">
{t("serverStats.ports.title")}
</h3>
{portsData && portsData.source !== "none" && (
<span className="text-xs text-muted-foreground ml-auto bg-canvas/50 px-2 py-0.5 rounded">
{portsData.source}
</span>
)}
</div>
<div className="flex items-center gap-4 mb-3 flex-shrink-0 text-sm">
<span className="text-foreground-subtle">
TCP: <span className="text-cyan-400 font-medium">{tcpPorts.length}</span>
</span>
<span className="text-foreground-subtle">
UDP: <span className="text-cyan-400 font-medium">{udpPorts.length}</span>
</span>
</div>
{portsData && portsData.ports.length > 0 ? (
<div className="flex-1 overflow-hidden flex flex-col">
<div className="grid grid-cols-5 gap-2 text-xs text-muted-foreground border-b border-edge/50 pb-1 mb-1 flex-shrink-0">
<div>{t("serverStats.ports.protocol")}</div>
<div>{t("serverStats.ports.port")}</div>
<div>{t("serverStats.ports.address")}</div>
<div>{t("serverStats.ports.state")}</div>
<div>{t("serverStats.ports.process")}</div>
</div>
<div className="flex-1 overflow-y-auto thin-scrollbar">
{portsData.ports.map((port, idx) => (
<PortRow key={`${port.protocol}-${port.localPort}-${idx}`} port={port} />
))}
</div>
</div>
) : (
<div className="flex-1 flex items-center justify-center">
<p className="text-sm text-muted-foreground">
{t("serverStats.ports.noData")}
</p>
</div>
)}
</div>
);
}

View File

@@ -6,4 +6,5 @@ export { UptimeWidget } from "./UptimeWidget.tsx";
export { ProcessesWidget } from "./ProcessesWidget.tsx";
export { SystemWidget } from "./SystemWidget.tsx";
export { LoginStatsWidget } from "./LoginStatsWidget.tsx";
export { PortsWidget } from "./PortsWidget.tsx";
export { FirewallWidget } from "./FirewallWidget.tsx";

View File

@@ -317,6 +317,7 @@ export function HostManagerEditor({
"processes",
"system",
"login_stats",
"ports",
"firewall",
]),
)
@@ -328,6 +329,7 @@ export function HostManagerEditor({
"uptime",
"system",
"login_stats",
"ports",
"firewall",
]),
statusCheckEnabled: z.boolean().default(true),
@@ -344,6 +346,7 @@ export function HostManagerEditor({
"uptime",
"system",
"login_stats",
"ports",
"firewall",
],
statusCheckEnabled: true,

View File

@@ -239,6 +239,7 @@ export function HostStatisticsTab({
"processes",
"system",
"login_stats",
"ports",
"firewall",
] as const
).map((widget) => (
@@ -267,6 +268,8 @@ export function HostStatisticsTab({
{widget === "system" && t("serverStats.systemInfo")}
{widget === "login_stats" &&
t("serverStats.loginStats")}
{widget === "ports" &&
t("serverStats.ports.title")}
{widget === "firewall" &&
t("serverStats.firewall.title")}
</label>