Docker images / prepare-release (push) Successful in 7s
Docker images / backend-image (push) Successful in 1m36s
Docker images / frontend-image (push) Successful in 2m45s
Docker images / notify-webhook (push) Skipped
Docker images / updater-image (push) Successful in 43s
Docker images / publish-release (push) Successful in 10s
Updated the TerminalPage to include a new ServerTileRail for server selection and added QuickCmds for quick command execution. Enhanced the WireGuardPage with new tabs for interface and peer management, improved server selection handling, and added support for displaying peer details. Refactored data grids to support compact server views and improved empty state handling. Introduced new hooks for managing server states and improved user experience across components.
133 lines
4.4 KiB
TypeScript
133 lines
4.4 KiB
TypeScript
"use client"
|
|
|
|
import type { WireGuardPeer } from "@/lib/data"
|
|
import { cn } from "@/lib/utils"
|
|
import { Button } from "@/components/ui/button"
|
|
import {
|
|
ArrowDownIcon,
|
|
ArrowUpIcon,
|
|
CodeXmlIcon,
|
|
KeyRoundIcon,
|
|
Trash2Icon,
|
|
} from "lucide-react"
|
|
|
|
function fmtBytes(n: number | undefined): string {
|
|
if (!n) return "—"
|
|
if (n >= 1_000_000_000) return `${(n / 1_000_000_000).toFixed(1)} ГБ`
|
|
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)} МБ`
|
|
if (n >= 1_000) return `${(n / 1_000).toFixed(0)} КБ`
|
|
return `${n} Б`
|
|
}
|
|
|
|
function truncKey(key: string): string {
|
|
if (key.length <= 20) return key
|
|
return `${key.slice(0, 8)}…${key.slice(-8)}`
|
|
}
|
|
|
|
function peerKey(peer: WireGuardPeer, index: number): string {
|
|
return peer.id ?? peer.rosId ?? peer.publicKey ?? String(index)
|
|
}
|
|
|
|
function WireGuardPeersDetail({
|
|
peers,
|
|
onDeletePeer,
|
|
onExportPeer,
|
|
}: {
|
|
peers: WireGuardPeer[]
|
|
onDeletePeer?: (peerId: string) => void
|
|
onExportPeer?: (peerId: string) => void
|
|
}) {
|
|
if (peers.length === 0) {
|
|
return (
|
|
<div className="px-5 py-4 text-xs text-muted-foreground text-center border-t border-border/50">
|
|
Нет пиров
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="border-t border-border/50">
|
|
<div className="grid grid-cols-[1fr_1fr_auto_auto_auto_auto] gap-3 px-5 py-1.5 bg-muted/10 text-[10px] font-semibold uppercase tracking-widest text-muted-foreground">
|
|
<span>Public Key</span>
|
|
<span>Allowed IPs</span>
|
|
<span>Последнее рукопожатие</span>
|
|
<span>RX / TX</span>
|
|
<span>Endpoint</span>
|
|
<span className="sr-only">Действия</span>
|
|
</div>
|
|
{peers.map((peer, index) => {
|
|
const id = peerKey(peer, index)
|
|
return (
|
|
<div
|
|
key={id}
|
|
className="grid grid-cols-[1fr_1fr_auto_auto_auto_auto] gap-3 px-5 py-2.5 items-center text-xs border-t border-border/50 bg-muted/20"
|
|
>
|
|
<div className="flex items-center gap-1.5 min-w-0">
|
|
<KeyRoundIcon className="size-3 text-muted-foreground shrink-0" />
|
|
<span className="font-mono text-muted-foreground truncate" title={peer.publicKey}>
|
|
{truncKey(peer.publicKey)}
|
|
</span>
|
|
</div>
|
|
<div className="font-mono text-muted-foreground truncate">
|
|
{peer.allowedIps.join(", ")}
|
|
</div>
|
|
<span
|
|
className={cn(
|
|
"font-mono text-[11px] whitespace-nowrap",
|
|
peer.latestHandshake ? "text-success" : "text-muted-foreground",
|
|
)}
|
|
>
|
|
{peer.latestHandshake ?? "нет рукопожатия"}
|
|
</span>
|
|
<div className="flex items-center gap-2 text-muted-foreground whitespace-nowrap">
|
|
<span className="flex items-center gap-0.5">
|
|
<ArrowDownIcon className="size-3 text-success" />
|
|
{fmtBytes(peer.transferRx)}
|
|
</span>
|
|
<span className="flex items-center gap-0.5">
|
|
<ArrowUpIcon className="size-3 text-info" />
|
|
{fmtBytes(peer.transferTx)}
|
|
</span>
|
|
</div>
|
|
<span className="font-mono text-muted-foreground/60 text-[11px]">{peer.endpoint ?? "—"}</span>
|
|
<div className="flex items-center gap-1 justify-end">
|
|
{onExportPeer && (
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon"
|
|
className="size-7"
|
|
aria-label="Экспорт peer .conf"
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
onExportPeer(id)
|
|
}}
|
|
>
|
|
<CodeXmlIcon className="size-3.5" />
|
|
</Button>
|
|
)}
|
|
{onDeletePeer && (
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon"
|
|
className="size-7 text-destructive"
|
|
aria-label="Удалить пира"
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
onDeletePeer(id)
|
|
}}
|
|
>
|
|
<Trash2Icon className="size-3.5" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export { WireGuardPeersDetail, fmtBytes, truncKey }
|