Docker images / prepare-release (push) Successful in 9s
Docker images / backend-image (push) Successful in 1m43s
Docker images / frontend-image (push) Successful in 2m58s
Docker images / notify-webhook (push) Skipped
Docker images / updater-image (push) Successful in 39s
Docker images / publish-release (push) Successful in 8s
Replaced existing KPI display implementations across multiple pages with the new KpiStatGrid component for a more consistent and visually appealing presentation of statistics. Updated the Backups, BGP, Certificates, Communities, Containers, Dashboard, and Data Collection pages to utilize the KpiStatGrid, improving the overall user experience and maintainability of the codebase. Additionally, added new dependencies in package.json for required libraries.
245 lines
9.1 KiB
TypeScript
245 lines
9.1 KiB
TypeScript
"use client"
|
||
|
||
import { useMemo, useState } from "react"
|
||
import { PageHeader } from "@/components/page-header"
|
||
import { vxlanTunnels, servers } from "@/lib/data"
|
||
import type { VxlanTunnel } from "@/lib/data"
|
||
import { KpiStatGrid } from "@/components/reui-kit/kpi-stat-grid"
|
||
import { OpsPanel } from "@/components/ops-panel"
|
||
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 {
|
||
NetworkIcon, PlusIcon, CodeXmlIcon, LayersIcon,
|
||
} from "lucide-react"
|
||
import { CodeExportSheet } from "@/components/reui-kit/code-export-sheet"
|
||
|
||
// ─── helpers ──────────────────────────────────────────────────────────────────
|
||
|
||
function serverFor(id: string) {
|
||
return servers.find((s) => s.id === id)
|
||
}
|
||
|
||
// ─── RSC generator ───────────────────────────────────────────────────────────
|
||
|
||
function generateVxlanRsc(t: VxlanTunnel): string {
|
||
const srv = serverFor(t.serverId)
|
||
const lines: string[] = []
|
||
lines.push(`# VXLAN — ${t.name} · VNI ${t.vni}`)
|
||
if (srv) lines.push(`# Сервер: ${srv.name} (${srv.host})`)
|
||
lines.push(`# RouterOS 7.x · /interface/vxlan`)
|
||
lines.push(``)
|
||
lines.push(`/interface/vxlan/add \\`)
|
||
lines.push(` name=${t.name} \\`)
|
||
lines.push(` vni=${t.vni} \\`)
|
||
lines.push(` port=${t.dstPort} \\`)
|
||
lines.push(` vtep-mac-address=auto \\`)
|
||
lines.push(` arp-proxy=${t.arpProxy ? "yes" : "no"} \\`)
|
||
lines.push(` mac-learning=${t.macLearning ? "yes" : "no"} \\`)
|
||
lines.push(` l2mtu=${t.l2mtu} \\`)
|
||
if (t.comment) lines.push(` comment="${t.comment}" \\`)
|
||
if (!t.enabled) lines.push(` disabled=yes \\`)
|
||
lines.push(``)
|
||
|
||
// FDB entries for remote VTEPs
|
||
for (const vtep of t.remoteVteps) {
|
||
lines.push(`/interface/vxlan/vteps/add \\`)
|
||
lines.push(` interface=${t.name} \\`)
|
||
lines.push(` remote-ip=${vtep}`)
|
||
lines.push(``)
|
||
}
|
||
|
||
// Bridge
|
||
lines.push(`# Добавить в bridge:`)
|
||
lines.push(`/interface/bridge/port/add \\`)
|
||
lines.push(` bridge=bridge-overlay \\`)
|
||
lines.push(` interface=${t.name}`)
|
||
|
||
return lines.join("\n")
|
||
}
|
||
|
||
// ─── Export Sheet ─────────────────────────────────────────────────────────────
|
||
|
||
function ExportSheet({ open, tunnel, onClose }: {
|
||
open: boolean; tunnel: VxlanTunnel | null; onClose: () => void
|
||
}) {
|
||
const code = useMemo(() => tunnel ? generateVxlanRsc(tunnel) : "", [tunnel])
|
||
|
||
return (
|
||
<CodeExportSheet
|
||
open={open}
|
||
onClose={onClose}
|
||
title="Экспорт VXLAN"
|
||
description="RouterOS 7.x · /interface/vxlan + vteps"
|
||
formats={[
|
||
{
|
||
id: "rsc",
|
||
label: "MikroTik .rsc",
|
||
filename: `${tunnel?.name ?? "vxlan"}.rsc`,
|
||
code,
|
||
},
|
||
]}
|
||
/>
|
||
)
|
||
}
|
||
|
||
// ─── Export Sheet ─────────────────────────────────────────────────────────────
|
||
export default function VxlanPage() {
|
||
const [search, setSearch] = useState("")
|
||
const [exportTunnel, setExportTunnel] = useState<VxlanTunnel | null>(null)
|
||
|
||
const filtered = useMemo(() => {
|
||
if (!search) return vxlanTunnels
|
||
const q = search.toLowerCase()
|
||
return vxlanTunnels.filter((t) =>
|
||
t.name.includes(q) ||
|
||
String(t.vni).includes(q) ||
|
||
t.vtepIp.includes(q) ||
|
||
(serverFor(t.serverId)?.name.toLowerCase().includes(q) ?? false)
|
||
)
|
||
}, [search])
|
||
|
||
const upCount = vxlanTunnels.filter((t) => t.status === "up").length
|
||
const vnis = new Set(vxlanTunnels.map((t) => t.vni)).size
|
||
|
||
return (
|
||
<div className="flex flex-col h-full">
|
||
<PageHeader
|
||
crumbs={[{ label: "Управление" }, { label: "VXLAN" }]}
|
||
actions={
|
||
<Button size="sm">
|
||
<PlusIcon className="size-4" />Новый VXLAN
|
||
</Button>
|
||
}
|
||
/>
|
||
|
||
<div className="flex-1 overflow-y-auto p-6">
|
||
<div className="flex flex-col gap-5">
|
||
|
||
<KpiStatGrid
|
||
aria-label="Сводка VXLAN"
|
||
items={[
|
||
{
|
||
id: "tunnels",
|
||
label: "Туннелей",
|
||
value: vxlanTunnels.length,
|
||
icon: <NetworkIcon className="size-4" />,
|
||
iconClassName: "text-muted-foreground",
|
||
},
|
||
{
|
||
id: "up",
|
||
label: "Активных",
|
||
value: upCount,
|
||
icon: <LayersIcon className="size-4" />,
|
||
iconClassName: "text-success",
|
||
},
|
||
{
|
||
id: "vni",
|
||
label: "Уникальных VNI",
|
||
value: vnis,
|
||
icon: <LayersIcon className="size-4" />,
|
||
iconClassName: "text-info",
|
||
},
|
||
{
|
||
id: "servers",
|
||
label: "Серверов",
|
||
value: new Set(vxlanTunnels.map((t) => t.serverId)).size,
|
||
icon: <NetworkIcon className="size-4" />,
|
||
iconClassName: "text-primary",
|
||
},
|
||
]}
|
||
/>
|
||
|
||
{/* Info banner */}
|
||
<div className="flex items-start gap-3 rounded-lg bg-sky-500/5 border border-sky-500/20 px-4 py-3 text-sm">
|
||
<NetworkIcon className="size-5 text-sky-500 shrink-0 mt-0.5" />
|
||
<div>
|
||
<p className="font-medium text-sky-600 dark:text-sky-400">VXLAN — L2-over-L3 оверлей для RouterOS 7.x</p>
|
||
<p className="text-muted-foreground text-xs mt-0.5">
|
||
Доступен с RouterOS 7.1+. VNI (Virtual Network Identifier) — уникальный идентификатор сегмента (0–16777215).
|
||
Рекомендуется использовать совместно с WireGuard или GRE туннелями для шифрования.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Table */}
|
||
<DataPageCard>
|
||
<DataPageToolbar
|
||
search={search}
|
||
onSearchChange={setSearch}
|
||
searchPlaceholder="Поиск по имени, VNI, серверу…"
|
||
countLabel={`${filtered.length} туннелей`}
|
||
/>
|
||
<VxlanDataGrid
|
||
tunnels={filtered}
|
||
servers={servers}
|
||
onExport={setExportTunnel}
|
||
/>
|
||
</DataPageCard>
|
||
|
||
{/* Reference */}
|
||
<OpsPanel title="RouterOS 7 · /interface/vxlan — быстрые команды" contentClassName="px-5 py-4">
|
||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 text-xs font-mono">
|
||
{[
|
||
{
|
||
title: "Создать VXLAN",
|
||
lines: [
|
||
"/interface/vxlan/add \\",
|
||
" name=vxlan-10 \\",
|
||
" vni=10010 \\",
|
||
" port=8472 \\",
|
||
" arp-proxy=yes \\",
|
||
" mac-learning=yes",
|
||
],
|
||
},
|
||
{
|
||
title: "Добавить VTEP",
|
||
lines: [
|
||
"/interface/vxlan/vteps/add \\",
|
||
" interface=vxlan-10 \\",
|
||
" remote-ip=10.0.1.1",
|
||
"",
|
||
"/interface/vxlan/vteps/add \\",
|
||
" interface=vxlan-10 \\",
|
||
" remote-ip=10.0.2.1",
|
||
],
|
||
},
|
||
{
|
||
title: "Bridge + IP",
|
||
lines: [
|
||
"/interface/bridge/add \\",
|
||
" name=br-overlay",
|
||
"",
|
||
"/interface/bridge/port/add \\",
|
||
" bridge=br-overlay \\",
|
||
" interface=vxlan-10",
|
||
"",
|
||
"/ip/address/add \\",
|
||
" address=10.100.0.1/24 \\",
|
||
" interface=br-overlay",
|
||
],
|
||
},
|
||
].map((b) => (
|
||
<div key={b.title}>
|
||
<p className="font-sans font-semibold text-foreground/80 mb-1.5 text-[11px] uppercase tracking-wide">{b.title}</p>
|
||
<pre className="bg-zinc-950 rounded-md p-2.5 text-zinc-300 text-[11px] leading-relaxed overflow-x-auto whitespace-pre">
|
||
{b.lines.join("\n")}
|
||
</pre>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</OpsPanel>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<ExportSheet
|
||
open={!!exportTunnel}
|
||
tunnel={exportTunnel}
|
||
onClose={() => setExportTunnel(null)}
|
||
/>
|
||
</div>
|
||
)
|
||
}
|