fix(ui): ограничить раскрытие большого списка пиров WireGuard
This commit is contained in:
1 parent
747f544462
commit
837298a171
7 files changed
+74
-21
No files matched your search
@@ -122,6 +122,7 @@ function WireguardDataGrid({
|
||||
expandedContent: (row: WgIfaceWithServer) => (
|
||||
<WireGuardPeersDetail
|
||||
peers={row.peers}
|
||||
interfaceName={row.name}
|
||||
onDeletePeer={onDeletePeer ? (peerId) => onDeletePeer(row, peerId) : undefined}
|
||||
onExportPeer={onExportPeer ? (peerId) => onExportPeer(row, peerId) : undefined}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import type { WireGuardPeer } from "@/lib/data"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
@@ -30,13 +31,20 @@ function peerKey(peer: WireGuardPeer, index: number): string {
|
||||
|
||||
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">
|
||||
@@ -46,30 +54,30 @@ function WireGuardPeersDetail({
|
||||
}
|
||||
|
||||
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>
|
||||
<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.map((peer, index) => {
|
||||
const id = peerKey(peer, index)
|
||||
{peers.slice(offset, offset + 50).map((peer, index) => {
|
||||
const id = peerKey(peer, offset + 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"
|
||||
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 truncate" title={peer.publicKey}>
|
||||
{truncKey(peer.publicKey)}
|
||||
<span className="font-mono text-muted-foreground break-all" title={peer.publicKey}>
|
||||
{peer.name || truncKey(peer.publicKey)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="font-mono text-muted-foreground truncate">
|
||||
{peer.allowedIps.join(", ")}
|
||||
<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(
|
||||
@@ -77,7 +85,7 @@ function WireGuardPeersDetail({
|
||||
peer.latestHandshake ? "text-success" : "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{peer.latestHandshake ?? "нет рукопожатия"}
|
||||
<span className="sr-only">Последнее рукопожатие: </span>{peer.latestHandshake ?? "нет рукопожатия"}
|
||||
</span>
|
||||
<div className="flex items-center gap-2 text-muted-foreground whitespace-nowrap">
|
||||
<span aria-label="RX" className="flex items-center gap-0.5">
|
||||
@@ -89,14 +97,14 @@ function WireGuardPeersDetail({
|
||||
{fmtBytes(peer.transferTx)}
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-mono text-muted-foreground/60 text-[11px]">{peer.endpoint ?? "—"}</span>
|
||||
<span className="min-w-0 break-all font-mono text-muted-foreground/60 text-[11px]"><span className="sr-only">Endpoint: </span>{peer.endpoint ?? "—"}</span>
|
||||
<div className="flex items-center gap-1 justify-end">
|
||||
{onExportPeer && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7"
|
||||
className="size-11"
|
||||
aria-label="Экспорт peer .conf"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
@@ -111,7 +119,7 @@ function WireGuardPeersDetail({
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7 text-destructive"
|
||||
className="size-11 text-destructive"
|
||||
aria-label="Удалить пира"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
@@ -125,7 +133,7 @@ function WireGuardPeersDetail({
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user