mirror of
https://github.com/YuzuZensai/Termix.git
synced 2026-09-13 18:58:52 +00:00
v2.3.0
This commit is contained in:
@@ -21,36 +21,40 @@ function Sparkline({
|
||||
current ?? 0,
|
||||
].slice(-20);
|
||||
|
||||
if (points.length < 2) return null;
|
||||
|
||||
const w = 300;
|
||||
const h = 48;
|
||||
const max = Math.max(...points, 1);
|
||||
const coords = points.map((v, i) => {
|
||||
const x = (i / (points.length - 1)) * w;
|
||||
const y = h - (v / max) * h;
|
||||
return `${x},${y}`;
|
||||
});
|
||||
|
||||
const d = `M ${coords.join(" L ")}`;
|
||||
const fill = `M 0,${h} L ${coords.join(" L ")} L ${w},${h} Z`;
|
||||
const hasData = points.length >= 2;
|
||||
const max = hasData ? Math.max(...points, 1) : 1;
|
||||
const coords = hasData
|
||||
? points.map((v, i) => {
|
||||
const x = (i / (points.length - 1)) * w;
|
||||
const y = h - (v / max) * h;
|
||||
return `${x},${y}`;
|
||||
})
|
||||
: [];
|
||||
|
||||
const d = hasData ? `M ${coords.join(" L ")}` : "";
|
||||
const fill = hasData ? `M 0,${h} L ${coords.join(" L ")} L ${w},${h} Z` : "";
|
||||
|
||||
return (
|
||||
<div className="h-12 md:h-16 w-full mt-2 bg-muted/20 border border-border/50 relative overflow-hidden">
|
||||
<svg
|
||||
className="absolute inset-0 h-full w-full"
|
||||
viewBox={`0 0 ${w} ${h}`}
|
||||
preserveAspectRatio="none"
|
||||
>
|
||||
<path d={fill} fill="currentColor" className="text-accent-brand/10" />
|
||||
<path
|
||||
d={d}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
className="text-accent-brand/60"
|
||||
/>
|
||||
</svg>
|
||||
{hasData && (
|
||||
<svg
|
||||
className="absolute inset-0 h-full w-full"
|
||||
viewBox={`0 0 ${w} ${h}`}
|
||||
preserveAspectRatio="none"
|
||||
>
|
||||
<path d={fill} fill="currentColor" className="text-accent-brand/10" />
|
||||
<path
|
||||
d={d}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
className="text-accent-brand/60"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,36 +20,40 @@ function Sparkline({
|
||||
current ?? 0,
|
||||
].slice(-20);
|
||||
|
||||
if (points.length < 2) return null;
|
||||
|
||||
const w = 300;
|
||||
const h = 48;
|
||||
const max = Math.max(...points, 1);
|
||||
const coords = points.map((v, i) => {
|
||||
const x = (i / (points.length - 1)) * w;
|
||||
const y = h - (v / max) * h;
|
||||
return `${x},${y}`;
|
||||
});
|
||||
|
||||
const d = `M ${coords.join(" L ")}`;
|
||||
const fill = `M 0,${h} L ${coords.join(" L ")} L ${w},${h} Z`;
|
||||
const hasData = points.length >= 2;
|
||||
const max = hasData ? Math.max(...points, 1) : 1;
|
||||
const coords = hasData
|
||||
? points.map((v, i) => {
|
||||
const x = (i / (points.length - 1)) * w;
|
||||
const y = h - (v / max) * h;
|
||||
return `${x},${y}`;
|
||||
})
|
||||
: [];
|
||||
|
||||
const d = hasData ? `M ${coords.join(" L ")}` : "";
|
||||
const fill = hasData ? `M 0,${h} L ${coords.join(" L ")} L ${w},${h} Z` : "";
|
||||
|
||||
return (
|
||||
<div className="h-12 md:h-16 w-full mt-2 bg-muted/20 border border-border/50 relative overflow-hidden">
|
||||
<svg
|
||||
className="absolute inset-0 h-full w-full"
|
||||
viewBox={`0 0 ${w} ${h}`}
|
||||
preserveAspectRatio="none"
|
||||
>
|
||||
<path d={fill} fill="currentColor" className="text-accent-brand/10" />
|
||||
<path
|
||||
d={d}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
className="text-accent-brand/60"
|
||||
/>
|
||||
</svg>
|
||||
{hasData && (
|
||||
<svg
|
||||
className="absolute inset-0 h-full w-full"
|
||||
viewBox={`0 0 ${w} ${h}`}
|
||||
preserveAspectRatio="none"
|
||||
>
|
||||
<path d={fill} fill="currentColor" className="text-accent-brand/10" />
|
||||
<path
|
||||
d={d}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
className="text-accent-brand/60"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ export function FirewallWidget({ metrics }: FirewallWidgetProps) {
|
||||
</span>
|
||||
)}
|
||||
{firewall && firewall.chains.length > 0 ? (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex flex-col gap-1 overflow-y-auto max-h-[320px]">
|
||||
{firewall.chains.map((chain) => (
|
||||
<ChainSection key={chain.name} chain={chain} />
|
||||
))}
|
||||
|
||||
@@ -46,40 +46,42 @@ export function LoginStatsWidget({ metrics }: LoginStatsWidgetProps) {
|
||||
{t("serverStats.noRecentLoginData")}
|
||||
</span>
|
||||
) : (
|
||||
allLogins.map((login, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`flex items-center justify-between p-2 border ${login.status === "success" ? "border-border bg-muted/30" : "border-destructive/30 bg-destructive/5"}`}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center gap-1.5">
|
||||
{login.status === "failed" ? (
|
||||
<UserX className="size-3 text-destructive" />
|
||||
) : (
|
||||
<UserCheck className="size-3 text-accent-brand" />
|
||||
)}
|
||||
<div className="flex flex-col gap-2 overflow-y-auto max-h-[300px]">
|
||||
{allLogins.map((login, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`flex items-center justify-between p-2 border ${login.status === "success" ? "border-border bg-muted/30" : "border-destructive/30 bg-destructive/5"}`}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center gap-1.5">
|
||||
{login.status === "failed" ? (
|
||||
<UserX className="size-3 text-destructive" />
|
||||
) : (
|
||||
<UserCheck className="size-3 text-accent-brand" />
|
||||
)}
|
||||
<span
|
||||
className={`text-xs font-bold ${login.status === "failed" ? "text-destructive" : ""}`}
|
||||
>
|
||||
{login.user}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[10px] text-muted-foreground font-mono">
|
||||
{login.ip}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-1">
|
||||
<span
|
||||
className={`text-xs font-bold ${login.status === "failed" ? "text-destructive" : ""}`}
|
||||
className={`text-[9px] font-bold uppercase px-1.5 py-px border ${login.status === "success" ? "border-accent-brand/40 text-accent-brand bg-accent-brand/10" : "border-destructive/40 text-destructive"}`}
|
||||
>
|
||||
{login.user}
|
||||
{login.status}
|
||||
</span>
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
{new Date(login.time).toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[10px] text-muted-foreground font-mono">
|
||||
{login.ip}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-1">
|
||||
<span
|
||||
className={`text-[9px] font-bold uppercase px-1.5 py-px border ${login.status === "success" ? "border-accent-brand/40 text-accent-brand bg-accent-brand/10" : "border-destructive/40 text-destructive"}`}
|
||||
>
|
||||
{login.status}
|
||||
</span>
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
{new Date(login.time).toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SectionCard>
|
||||
|
||||
@@ -20,36 +20,40 @@ function Sparkline({
|
||||
current ?? 0,
|
||||
].slice(-20);
|
||||
|
||||
if (points.length < 2) return null;
|
||||
|
||||
const w = 300;
|
||||
const h = 48;
|
||||
const max = Math.max(...points, 1);
|
||||
const coords = points.map((v, i) => {
|
||||
const x = (i / (points.length - 1)) * w;
|
||||
const y = h - (v / max) * h;
|
||||
return `${x},${y}`;
|
||||
});
|
||||
|
||||
const d = `M ${coords.join(" L ")}`;
|
||||
const fill = `M 0,${h} L ${coords.join(" L ")} L ${w},${h} Z`;
|
||||
const hasData = points.length >= 2;
|
||||
const max = hasData ? Math.max(...points, 1) : 1;
|
||||
const coords = hasData
|
||||
? points.map((v, i) => {
|
||||
const x = (i / (points.length - 1)) * w;
|
||||
const y = h - (v / max) * h;
|
||||
return `${x},${y}`;
|
||||
})
|
||||
: [];
|
||||
|
||||
const d = hasData ? `M ${coords.join(" L ")}` : "";
|
||||
const fill = hasData ? `M 0,${h} L ${coords.join(" L ")} L ${w},${h} Z` : "";
|
||||
|
||||
return (
|
||||
<div className="h-12 md:h-16 w-full mt-2 bg-muted/20 border border-border/50 relative overflow-hidden">
|
||||
<svg
|
||||
className="absolute inset-0 h-full w-full"
|
||||
viewBox={`0 0 ${w} ${h}`}
|
||||
preserveAspectRatio="none"
|
||||
>
|
||||
<path d={fill} fill="currentColor" className="text-accent-brand/10" />
|
||||
<path
|
||||
d={d}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
className="text-accent-brand/60"
|
||||
/>
|
||||
</svg>
|
||||
{hasData && (
|
||||
<svg
|
||||
className="absolute inset-0 h-full w-full"
|
||||
viewBox={`0 0 ${w} ${h}`}
|
||||
preserveAspectRatio="none"
|
||||
>
|
||||
<path d={fill} fill="currentColor" className="text-accent-brand/10" />
|
||||
<path
|
||||
d={d}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
className="text-accent-brand/60"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,34 +38,36 @@ export function NetworkWidget({ metrics }: NetworkWidgetProps) {
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
interfaces.map((iface, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex flex-col p-2 border border-border bg-muted/30 gap-1"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={`size-1.5 rounded-full ${iface.state === "UP" ? "bg-accent-brand" : "bg-muted-foreground"}`}
|
||||
/>
|
||||
<span className="text-sm font-bold font-mono">
|
||||
{iface.name}
|
||||
<div className="flex flex-col gap-2 overflow-y-auto max-h-[260px]">
|
||||
{interfaces.map((iface, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex flex-col p-2 border border-border bg-muted/30 gap-1"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={`size-1.5 rounded-full ${iface.state === "UP" ? "bg-accent-brand" : "bg-muted-foreground"}`}
|
||||
/>
|
||||
<span className="text-sm font-bold font-mono">
|
||||
{iface.name}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[10px] font-semibold px-1.5 py-px border border-border text-muted-foreground uppercase">
|
||||
{iface.state}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[10px] font-semibold px-1.5 py-px border border-border text-muted-foreground uppercase">
|
||||
{iface.state}
|
||||
</span>
|
||||
<div className="flex justify-between text-[10px] font-mono text-muted-foreground">
|
||||
<span>{iface.ip}</span>
|
||||
{(iface.rx || iface.tx) && (
|
||||
<span>
|
||||
↓ {iface.rx ?? "—"} / ↑ {iface.tx ?? "—"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between text-[10px] font-mono text-muted-foreground">
|
||||
<span>{iface.ip}</span>
|
||||
{(iface.rx || iface.tx) && (
|
||||
<span>
|
||||
↓ {iface.rx ?? "—"} / ↑ {iface.tx ?? "—"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SectionCard>
|
||||
|
||||
@@ -14,15 +14,17 @@ function PortRow({ port }: { port: ListeningPort }) {
|
||||
addr === "0.0.0.0" || addr === "*" || addr === "::" ? "*" : addr;
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-4 text-xs font-mono py-1 border-b border-border/50 last:border-0 min-w-0">
|
||||
<span className="text-accent-brand font-bold">{port.localPort}</span>
|
||||
<span className="text-muted-foreground">
|
||||
<div className="grid grid-cols-4 text-xs font-mono py-1 border-b border-border/50 last:border-0 min-w-0 overflow-hidden">
|
||||
<span className="text-accent-brand font-bold truncate">
|
||||
{port.localPort}
|
||||
</span>
|
||||
<span className="text-muted-foreground truncate">
|
||||
{port.protocol.toUpperCase()}
|
||||
</span>
|
||||
<span className="font-semibold truncate">
|
||||
{port.process ?? (port.pid ? `PID:${port.pid}` : "—")}
|
||||
</span>
|
||||
<span className="text-right text-muted-foreground">
|
||||
<span className="text-right text-muted-foreground truncate">
|
||||
{formatAddress(port.localAddress)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -41,7 +43,7 @@ export function PortsWidget({ metrics }: PortsWidgetProps) {
|
||||
title={t("serverStats.ports.title")}
|
||||
icon={<Unplug className="size-3.5" />}
|
||||
>
|
||||
<div className="flex flex-col gap-1.5 py-1">
|
||||
<div className="flex flex-col gap-1.5 py-1 overflow-x-hidden">
|
||||
<div className="grid grid-cols-4 text-[10px] text-muted-foreground font-bold uppercase pb-1 border-b border-border min-w-0">
|
||||
<span>{t("serverStats.ports.port")}</span>
|
||||
<span>{t("serverStats.ports.protocol")}</span>
|
||||
@@ -53,12 +55,14 @@ export function PortsWidget({ metrics }: PortsWidgetProps) {
|
||||
{t("serverStats.ports.noData")}
|
||||
</span>
|
||||
) : (
|
||||
ports.map((port, i) => (
|
||||
<PortRow
|
||||
key={`${port.protocol}-${port.localPort}-${i}`}
|
||||
port={port}
|
||||
/>
|
||||
))
|
||||
<div className="overflow-y-auto max-h-[300px]">
|
||||
{ports.map((port, i) => (
|
||||
<PortRow
|
||||
key={`${port.protocol}-${port.localPort}-${i}`}
|
||||
port={port}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SectionCard>
|
||||
|
||||
@@ -45,19 +45,21 @@ export function ProcessesWidget({ metrics }: ProcessesWidgetProps) {
|
||||
<span className="text-xs">{t("serverStats.noProcessesFound")}</span>
|
||||
</div>
|
||||
) : (
|
||||
topProcesses.map((proc, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="grid grid-cols-4 text-xs font-mono py-1 border-b border-border/50 last:border-0 min-w-0"
|
||||
>
|
||||
<span className="text-muted-foreground">{proc.pid}</span>
|
||||
<span className="text-accent-brand font-bold">{proc.cpu}%</span>
|
||||
<span>{proc.mem}%</span>
|
||||
<span className="truncate font-semibold" title={proc.command}>
|
||||
{proc.command.split("/").pop()}
|
||||
</span>
|
||||
</div>
|
||||
))
|
||||
<div className="overflow-y-auto max-h-[280px]">
|
||||
{topProcesses.map((proc, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="grid grid-cols-4 text-xs font-mono py-1 border-b border-border/50 last:border-0 min-w-0"
|
||||
>
|
||||
<span className="text-muted-foreground">{proc.pid}</span>
|
||||
<span className="text-accent-brand font-bold">{proc.cpu}%</span>
|
||||
<span>{proc.mem}%</span>
|
||||
<span className="truncate font-semibold" title={proc.command}>
|
||||
{proc.command.split("/").pop()}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SectionCard>
|
||||
|
||||
Reference in New Issue
Block a user