Files
MikrotikManager/components/data-grids/wireguard-peers-detail.tsx
T

141 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client"
import { useState } from "react"
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 === undefined || !Number.isFinite(n) || n < 0) 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,
interfaceName,
onDeletePeer,
onExportPeer,
}: {
peers: WireGuardPeer[]
interfaceName: string
onDeletePeer?: (peerId: string) => void
onExportPeer?: (peerId: string) => void
}) {
const [page, setPage] = useState(0)
const lastPage = Math.max(0, Math.ceil(peers.length / 50) - 1)
const currentPage = Math.min(page, lastPage)
const offset = currentPage * 50
if (page > lastPage) setPage(lastPage)
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 (
<section aria-label={"Пиры: " + interfaceName} className="sticky left-0 w-full max-w-[calc(100vw-2rem)] border-t border-border/50 md:max-w-none">
<div className="flex flex-wrap items-center justify-between gap-2 px-4 py-2 bg-muted/10">
<p className="text-xs text-muted-foreground" aria-live="polite">Пиры {offset + 1}–{Math.min(offset + 50, peers.length)} из {peers.length}</p>
{peers.length > 50 && <div className="flex gap-1">
<Button type="button" variant="outline" className="min-h-11" disabled={currentPage === 0} aria-label="Предыдущая страница пиров" onClick={() => setPage(currentPage - 1)}>Назад</Button>
<Button type="button" variant="outline" className="min-h-11" disabled={currentPage >= lastPage} aria-label="Следующая страница пиров" onClick={() => setPage(currentPage + 1)}>Далее</Button>
</div>}
</div>
{peers.slice(offset, offset + 50).map((peer, index) => {
const id = peerKey(peer, offset + index)
return (
<div
key={id}
data-wireguard-peer={id}
className="grid grid-cols-2 gap-3 px-4 py-3 items-center text-xs border-t border-border/50 bg-muted/20 md:grid-cols-[1fr_1fr_auto_auto_auto_auto]"
>
<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 break-all" title={peer.publicKey}>
{peer.name || truncKey(peer.publicKey)}
</span>
</div>
<div className="min-w-0 break-all font-mono text-muted-foreground">
<span className="sr-only">Allowed IPs: </span><span>{peer.allowedIps.join(", ")}</span>
</div>
<span
className={cn(
"font-mono text-[11px] whitespace-nowrap",
"text-muted-foreground",
)}
>
<span className="sr-only">Последнее рукопожатие: </span><span>{peer.latestHandshake?.trim() || "Нет данных"}</span>
</span>
<div className="flex items-center gap-2 text-muted-foreground whitespace-nowrap">
<span aria-label="RX" className="flex items-center gap-0.5">
<ArrowDownIcon aria-hidden="true" className="size-3 text-success" />
{fmtBytes(peer.transferRx)}
</span>
<span aria-label="TX" className="flex items-center gap-0.5">
<ArrowUpIcon aria-hidden="true" className="size-3 text-info" />
{fmtBytes(peer.transferTx)}
</span>
</div>
<span className="min-w-0 break-all font-mono text-muted-foreground/60 text-[11px]"><span>Endpoint: </span>{peer.endpoint ?? "—"}</span>
<div className="flex items-center gap-1 justify-end">
{onExportPeer && (
<Button
type="button"
variant="ghost"
size="icon"
className="size-11"
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-11 text-destructive"
aria-label="Удалить пира"
onClick={(e) => {
e.stopPropagation()
onDeletePeer(id)
}}
>
<Trash2Icon className="size-3.5" />
</Button>
)}
</div>
</div>
)
})}
</section>
)
}
export { WireGuardPeersDetail, fmtBytes, truncKey }