fix(ui): заменить таблицы на карточки данных и улучшить функциональность поиска
Docker images / prepare-release (push) Successful in 6s
Docker images / backend-image (push) Successful in 1m37s
Docker images / frontend-image (push) Successful in 1m50s
Docker images / updater-image (push) Successful in 44s
Docker images / notify-webhook (push) Has been skipped
Docker images / publish-release (push) Successful in 7s
Docker images / prepare-release (push) Successful in 6s
Docker images / backend-image (push) Successful in 1m37s
Docker images / frontend-image (push) Successful in 1m50s
Docker images / updater-image (push) Successful in 44s
Docker images / notify-webhook (push) Has been skipped
Docker images / publish-release (push) Successful in 7s
This commit is contained in:
+18
-140
@@ -4,17 +4,14 @@ import { useMemo, useState } from "react"
|
||||
import { PageHeader } from "@/components/page-header"
|
||||
import { vxlanTunnels, servers } from "@/lib/data"
|
||||
import type { VxlanTunnel } from "@/lib/data"
|
||||
import { Flag } from "@/components/flag"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { DataPageCard } from "@/components/data-page-card"
|
||||
import { DataPageToolbar } from "@/components/data-page-toolbar"
|
||||
import { VxlanDataGrid } from "@/components/data-grids/vxlan-data-grid"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
DropdownMenu, DropdownMenuTrigger, DropdownMenuContent,
|
||||
DropdownMenuItem, DropdownMenuSeparator,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import {
|
||||
SearchIcon, NetworkIcon, PlusIcon, MoreHorizontalIcon,
|
||||
Trash2Icon, PencilIcon, PowerIcon, CopyIcon, CheckIcon,
|
||||
NetworkIcon, PlusIcon, CopyIcon, CheckIcon,
|
||||
CodeXmlIcon, LayersIcon,
|
||||
} from "lucide-react"
|
||||
import {
|
||||
@@ -125,100 +122,7 @@ function ExportSheet({ open, tunnel, onClose }: {
|
||||
)
|
||||
}
|
||||
|
||||
// ─── Tunnel row ───────────────────────────────────────────────────────────────
|
||||
|
||||
function TunnelRow({
|
||||
tunnel,
|
||||
onExport,
|
||||
}: {
|
||||
tunnel: VxlanTunnel
|
||||
onExport: () => void
|
||||
}) {
|
||||
const srv = serverFor(tunnel.serverId)
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
"grid grid-cols-[10px_1fr_1fr_auto_auto_auto_auto_auto_auto_auto_auto] gap-3 px-4 py-3 items-center border-b last:border-b-0 hover:bg-muted/30 transition-colors",
|
||||
!tunnel.enabled && "opacity-50",
|
||||
)}>
|
||||
{/* status dot */}
|
||||
<span className={cn(
|
||||
"size-2 rounded-full shrink-0",
|
||||
tunnel.status === "up" ? "bg-emerald-500" : "bg-red-500",
|
||||
)} />
|
||||
|
||||
{/* name */}
|
||||
<div className="min-w-0">
|
||||
<p className="font-mono font-medium text-sm truncate">{tunnel.name}</p>
|
||||
<p className="text-[11px] text-muted-foreground font-mono">VTEP: {tunnel.vtepIp}</p>
|
||||
</div>
|
||||
|
||||
{/* server */}
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground min-w-0">
|
||||
{srv && <><Flag code={srv.country} size={12} /><span className="font-mono truncate">{srv.name}</span></>}
|
||||
</div>
|
||||
|
||||
{/* VNI */}
|
||||
<div className="text-center">
|
||||
<p className="text-[10px] text-muted-foreground">VNI</p>
|
||||
<p className="font-mono text-sm">{tunnel.vni}</p>
|
||||
</div>
|
||||
|
||||
{/* Port */}
|
||||
<div className="text-center">
|
||||
<p className="text-[10px] text-muted-foreground">Port</p>
|
||||
<p className="font-mono text-sm">{tunnel.dstPort}</p>
|
||||
</div>
|
||||
|
||||
{/* Remote VTEPs */}
|
||||
<div className="text-center">
|
||||
<p className="text-[10px] text-muted-foreground">Remote VTEP</p>
|
||||
<p className="font-mono text-sm">{tunnel.remoteVteps.length}</p>
|
||||
</div>
|
||||
|
||||
{/* ARP Proxy */}
|
||||
<span className={cn("text-[10px] font-mono px-1.5 py-0.5 rounded",
|
||||
tunnel.arpProxy ? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400" : "bg-muted text-muted-foreground")}>
|
||||
ARP {tunnel.arpProxy ? "✓" : "✗"}
|
||||
</span>
|
||||
|
||||
{/* MAC learning */}
|
||||
<span className={cn("text-[10px] font-mono px-1.5 py-0.5 rounded",
|
||||
tunnel.macLearning ? "bg-sky-500/10 text-sky-600 dark:text-sky-400" : "bg-muted text-muted-foreground")}>
|
||||
MAC {tunnel.macLearning ? "✓" : "✗"}
|
||||
</span>
|
||||
|
||||
{/* Status badge */}
|
||||
<span className={cn(
|
||||
"text-[11px] font-mono px-2 py-0.5 rounded border whitespace-nowrap",
|
||||
tunnel.status === "up"
|
||||
? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20"
|
||||
: "bg-red-500/10 text-red-500 border-red-500/20",
|
||||
)}>
|
||||
{tunnel.status === "up" ? "UP" : "DOWN"}
|
||||
</span>
|
||||
|
||||
{/* menu */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger render={
|
||||
<Button variant="ghost" size="icon" className="size-7">
|
||||
<MoreHorizontalIcon className="size-4" />
|
||||
</Button>
|
||||
} />
|
||||
<DropdownMenuContent side="bottom" align="end">
|
||||
<DropdownMenuItem onClick={onExport}><CodeXmlIcon className="size-4" />Экспорт .rsc</DropdownMenuItem>
|
||||
<DropdownMenuItem><PencilIcon className="size-4" />Редактировать</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem><PowerIcon className="size-4" />{tunnel.enabled ? "Отключить" : "Включить"}</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem variant="destructive"><Trash2Icon className="size-4" />Удалить</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// ─── Export Sheet ─────────────────────────────────────────────────────────────
|
||||
export default function VxlanPage() {
|
||||
const [search, setSearch] = useState("")
|
||||
const [exportTunnel, setExportTunnel] = useState<VxlanTunnel | null>(null)
|
||||
@@ -284,45 +188,19 @@ export default function VxlanPage() {
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<Card>
|
||||
<div className="flex items-center gap-3 px-4 py-3 border-b">
|
||||
<div className="flex items-center gap-2 h-8 px-3 border border-input rounded-md bg-background min-w-[240px]">
|
||||
<SearchIcon className="size-3.5 text-muted-foreground shrink-0" />
|
||||
<input
|
||||
className="flex-1 bg-transparent outline-none placeholder:text-muted-foreground text-sm"
|
||||
placeholder="Поиск по имени, VNI, серверу…"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground ml-auto">{filtered.length} туннелей</span>
|
||||
</div>
|
||||
|
||||
{/* header */}
|
||||
<div className="grid grid-cols-[10px_1fr_1fr_auto_auto_auto_auto_auto_auto_auto_auto] gap-3 px-4 py-2 border-b text-[10px] font-semibold uppercase tracking-widest text-muted-foreground bg-muted/20">
|
||||
<span />
|
||||
<span>Имя / VTEP IP</span>
|
||||
<span>Сервер</span>
|
||||
<span>VNI</span>
|
||||
<span>Port</span>
|
||||
<span>Remote</span>
|
||||
<span />
|
||||
<span />
|
||||
<span>Статус</span>
|
||||
<span />
|
||||
</div>
|
||||
|
||||
{filtered.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-16 text-center text-muted-foreground">
|
||||
<NetworkIcon className="size-10 mb-3 opacity-20" />
|
||||
<p className="text-sm font-medium">VXLAN туннели не найдены</p>
|
||||
</div>
|
||||
) : (
|
||||
filtered.map((t) => (
|
||||
<TunnelRow key={t.id} tunnel={t} onExport={() => setExportTunnel(t)} />
|
||||
))
|
||||
)}
|
||||
</Card>
|
||||
<DataPageCard>
|
||||
<DataPageToolbar
|
||||
search={search}
|
||||
onSearchChange={setSearch}
|
||||
searchPlaceholder="Поиск по имени, VNI, серверу…"
|
||||
countLabel={`${filtered.length} туннелей`}
|
||||
/>
|
||||
<VxlanDataGrid
|
||||
tunnels={filtered}
|
||||
servers={servers}
|
||||
onExport={setExportTunnel}
|
||||
/>
|
||||
</DataPageCard>
|
||||
|
||||
{/* Reference */}
|
||||
<Card>
|
||||
|
||||
Reference in New Issue
Block a user