mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 14:45:24 +00:00
103 lines
3.8 KiB
TypeScript
103 lines
3.8 KiB
TypeScript
import { useMemo, useState } from "react";
|
|
import { Unplug, Search } from "lucide-react";
|
|
import { useTranslation } from "react-i18next";
|
|
import type { ServerMetrics, ListeningPort } from "@/main-axios";
|
|
import { MetricCard } from "@/components/metric-card";
|
|
import { Select2 } from "@/components/select2";
|
|
|
|
function formatAddress(addr: string) {
|
|
return addr === "0.0.0.0" || addr === "*" || addr === "::" ? "*" : addr;
|
|
}
|
|
|
|
function PortRow({ port }: { port: ListeningPort }) {
|
|
return (
|
|
<div className="grid grid-cols-[3.5rem_3rem_1fr_4rem] gap-2 overflow-hidden border-b border-border/50 py-1 font-mono text-xs last:border-0">
|
|
<span className="truncate font-bold text-accent-brand">
|
|
{port.localPort}
|
|
</span>
|
|
<span className="truncate text-muted-foreground">
|
|
{port.protocol.toUpperCase()}
|
|
</span>
|
|
<span className="truncate font-semibold">
|
|
{port.process ?? (port.pid ? `PID:${port.pid}` : "—")}
|
|
</span>
|
|
<span className="truncate text-right text-muted-foreground">
|
|
{formatAddress(port.localAddress)}
|
|
</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function PortsCard({ metrics }: { metrics: ServerMetrics | null }) {
|
|
const { t } = useTranslation();
|
|
const [query, setQuery] = useState("");
|
|
const [proto, setProto] = useState<"all" | "tcp" | "udp">("all");
|
|
const ports = useMemo(() => metrics?.ports?.ports ?? [], [metrics]);
|
|
|
|
const filtered = useMemo(() => {
|
|
const q = query.toLowerCase();
|
|
return ports.filter((p) => {
|
|
if (proto !== "all" && p.protocol.toLowerCase() !== proto) return false;
|
|
if (!q) return true;
|
|
return (
|
|
String(p.localPort).includes(q) ||
|
|
(p.process ?? "").toLowerCase().includes(q) ||
|
|
String(p.pid ?? "").includes(q) ||
|
|
p.localAddress.toLowerCase().includes(q)
|
|
);
|
|
});
|
|
}, [ports, query, proto]);
|
|
|
|
return (
|
|
<MetricCard
|
|
title={t("hostMetrics.ports.title")}
|
|
icon={<Unplug className="size-3.5" />}
|
|
scroll
|
|
>
|
|
<div className="mb-2 flex items-center gap-1.5">
|
|
<div className="relative flex-1">
|
|
<Search className="pointer-events-none absolute left-2 top-1/2 size-3 -translate-y-1/2 text-muted-foreground/60" />
|
|
<input
|
|
value={query}
|
|
onChange={(e) => setQuery(e.target.value)}
|
|
placeholder={t("hostMetrics.ports.search")}
|
|
className="h-7 w-full border border-border bg-background pl-7 pr-2 text-xs outline-none focus:ring-1 focus:ring-ring"
|
|
/>
|
|
</div>
|
|
<Select2
|
|
value={proto}
|
|
onChange={(e) => setProto(e.target.value as "all" | "tcp" | "udp")}
|
|
className="h-7 border border-border bg-background px-1 text-xs"
|
|
>
|
|
<option value="all">{t("hostMetrics.ports.allProtocols")}</option>
|
|
<option value="tcp">TCP</option>
|
|
<option value="udp">UDP</option>
|
|
</Select2>
|
|
<span className="shrink-0 text-[10px] tabular-nums text-muted-foreground">
|
|
{filtered.length}
|
|
</span>
|
|
</div>
|
|
<div className="flex flex-col">
|
|
<div className="grid grid-cols-[3.5rem_3rem_1fr_4rem] gap-2 border-b border-border pb-1 text-[10px] font-bold uppercase text-muted-foreground">
|
|
<span>{t("hostMetrics.ports.port")}</span>
|
|
<span>{t("hostMetrics.ports.protocol")}</span>
|
|
<span>{t("hostMetrics.ports.process")}</span>
|
|
<span className="text-right">{t("hostMetrics.ports.address")}</span>
|
|
</div>
|
|
{filtered.length === 0 ? (
|
|
<span className="py-2 text-xs italic text-muted-foreground">
|
|
{t("hostMetrics.ports.noData")}
|
|
</span>
|
|
) : (
|
|
filtered.map((port, i) => (
|
|
<PortRow
|
|
key={`${port.protocol}-${port.localPort}-${i}`}
|
|
port={port}
|
|
/>
|
|
))
|
|
)}
|
|
</div>
|
|
</MetricCard>
|
|
);
|
|
}
|