fix(ui): выровнять экспорт WireGuard под ReUI Frame
Убрать дубль копирования, вынести CodeExportSheet и семантические токены. Открывать вкладку Peer при экспорте пира. Мигрировать VXLAN/Firewall/Containers/GRE. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -17,13 +17,14 @@ import {
|
||||
import {
|
||||
BoxIcon, PlayIcon, StopCircleIcon, SearchIcon,
|
||||
MoreHorizontalIcon, Trash2Icon, PencilIcon, PowerIcon,
|
||||
CodeXmlIcon, CopyIcon, CheckIcon, ActivityIcon, ServerIcon,
|
||||
CodeXmlIcon, ActivityIcon, ServerIcon,
|
||||
TerminalIcon, AlertCircleIcon,
|
||||
} from "lucide-react"
|
||||
import {
|
||||
Sheet, SheetContent, SheetHeader, SheetTitle,
|
||||
SheetDescription, SheetFooter, SheetClose,
|
||||
} from "@/components/ui/sheet"
|
||||
import { CodeExportSheet } from "@/components/reui-kit/code-export-sheet"
|
||||
|
||||
// ─── helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -104,57 +105,23 @@ function generateContainerRsc(c: RouterContainer): string {
|
||||
function ExportSheet({ open, container, onClose }: {
|
||||
open: boolean; container: RouterContainer | null; onClose: () => void
|
||||
}) {
|
||||
const [copied, setCopied] = useState(false)
|
||||
const code = useMemo(() => container ? generateContainerRsc(container) : "", [container])
|
||||
|
||||
function handleCopy() {
|
||||
navigator.clipboard.writeText(code).then(() => {
|
||||
setCopied(true); setTimeout(() => setCopied(false), 2000)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={(v) => { if (!v) onClose() }}>
|
||||
<SheetContent className="flex flex-col overflow-hidden p-0 gap-0 sm:max-w-2xl">
|
||||
<SheetHeader className="shrink-0 px-6 pt-5 pb-4 border-b">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<SheetTitle>Экспорт Container</SheetTitle>
|
||||
<SheetDescription>RouterOS 7.4+ · /container · /interface/veth</SheetDescription>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" className="shrink-0" onClick={handleCopy}>
|
||||
{copied ? <><CheckIcon className="size-3.5 text-emerald-500" />Скопировано</> : <><CopyIcon className="size-3.5" />Копировать</>}
|
||||
</Button>
|
||||
</div>
|
||||
</SheetHeader>
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<pre className="px-6 py-5 text-[12px] font-mono leading-relaxed text-foreground/85 whitespace-pre select-all">
|
||||
{code.split("\n").map((line, i) => {
|
||||
const isComment = line.startsWith("#")
|
||||
const isCmd = /^\//.test(line.trimStart())
|
||||
const isParam = /^\s+[a-z]/.test(line)
|
||||
return (
|
||||
<span key={i} className={
|
||||
isComment ? "text-muted-foreground"
|
||||
: isCmd ? "text-sky-400"
|
||||
: isParam ? "text-violet-300"
|
||||
: "text-foreground"
|
||||
}>
|
||||
{line}{"\n"}
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</pre>
|
||||
</div>
|
||||
<SheetFooter className="shrink-0 px-6 py-4 border-t flex-row gap-2">
|
||||
<SheetClose render={<Button variant="outline" className="flex-1" />}>Закрыть</SheetClose>
|
||||
<Button className="flex-1" onClick={handleCopy}>
|
||||
{copied ? <CheckIcon className="size-4" /> : <CopyIcon className="size-4" />}
|
||||
{copied ? "Скопировано" : "Копировать .rsc"}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
<CodeExportSheet
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
title="Экспорт Container"
|
||||
description="RouterOS 7.4+ · /container · /interface/veth"
|
||||
formats={[
|
||||
{
|
||||
id: "rsc",
|
||||
label: "MikroTik .rsc",
|
||||
filename: `${container?.name ?? "container"}.rsc`,
|
||||
code,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -34,12 +34,13 @@ import { cn } from "@/lib/utils"
|
||||
import {
|
||||
PlusIcon, SearchIcon, ShieldIcon, ShieldOffIcon,
|
||||
ListFilterIcon, ArrowRightLeftIcon, WrenchIcon, LayersIcon,
|
||||
MoreHorizontalIcon, PencilIcon, Trash2Icon, CopyIcon, CodeXmlIcon,
|
||||
CheckIcon, PowerIcon, CheckCircleIcon,
|
||||
MoreHorizontalIcon, PencilIcon, Trash2Icon, CodeXmlIcon,
|
||||
PowerIcon, CheckCircleIcon,
|
||||
PlayIcon, SquareIcon, RotateCcwIcon, ZapIcon,
|
||||
CheckCircle2Icon, XCircleIcon, MinusCircleIcon, SkipForwardIcon,
|
||||
SlidersHorizontalIcon,
|
||||
} from "lucide-react"
|
||||
import { CodeExportSheet } from "@/components/reui-kit/code-export-sheet"
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -602,68 +603,28 @@ function RuleSheet({ open, onClose, initialRule, chainGroup }: {
|
||||
function ExportSheet({ open, onClose, rules }: {
|
||||
open: boolean; onClose: () => void; rules: FirewallRule[]
|
||||
}) {
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
// Derived from open — new timestamp each time modal opens, undefined when closed
|
||||
const timestamp = useMemo(
|
||||
() => open ? new Date().toLocaleString("ru") : undefined,
|
||||
[open],
|
||||
[open],
|
||||
)
|
||||
|
||||
const code = useMemo(() => generateRsc(rules, timestamp), [rules, timestamp])
|
||||
|
||||
function handleCopy() {
|
||||
const full = generateRsc(rules, new Date().toLocaleString("ru"))
|
||||
navigator.clipboard.writeText(full).then(() => {
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={(v) => { if (!v) onClose() }}>
|
||||
<SheetContent className="flex flex-col overflow-hidden p-0 gap-0 sm:max-w-2xl">
|
||||
<SheetHeader className="shrink-0 px-6 pt-5 pb-4 border-b">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<SheetTitle>Экспорт Firewall</SheetTitle>
|
||||
<SheetDescription>RouterOS .rsc · /ip firewall filter, nat, mangle</SheetDescription>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" className="shrink-0" onClick={handleCopy}>
|
||||
{copied
|
||||
? <><CheckIcon className="size-3.5 text-emerald-500" />Скопировано</>
|
||||
: <><CopyIcon className="size-3.5" />Копировать</>}
|
||||
</Button>
|
||||
</div>
|
||||
</SheetHeader>
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<pre className="px-6 py-5 text-[12px] font-mono leading-relaxed text-foreground/85 whitespace-pre select-all">
|
||||
{code.split("\n").map((line, i) => {
|
||||
const isComment = line.startsWith("#")
|
||||
const isSection = isComment && line.includes("──")
|
||||
const isKey = /^\s+[a-z]/.test(line)
|
||||
return (
|
||||
<span key={i} className={
|
||||
isSection ? "text-muted-foreground/50"
|
||||
: isComment ? "text-muted-foreground"
|
||||
: isKey ? "text-sky-400/90"
|
||||
: "text-foreground"
|
||||
}>
|
||||
{line}{"\n"}
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</pre>
|
||||
</div>
|
||||
<SheetFooter className="shrink-0 px-6 py-4 border-t flex-row gap-2">
|
||||
<SheetClose render={<Button variant="outline" className="flex-1" />}>Закрыть</SheetClose>
|
||||
<Button className="flex-1" onClick={handleCopy}>
|
||||
{copied ? <CheckIcon className="size-4" /> : <CopyIcon className="size-4" />}
|
||||
{copied ? "Скопировано" : "Копировать .rsc"}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
<CodeExportSheet
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
title="Экспорт Firewall"
|
||||
description="RouterOS .rsc · /ip firewall filter, nat, mangle"
|
||||
formats={[
|
||||
{
|
||||
id: "rsc",
|
||||
label: "MikroTik .rsc",
|
||||
filename: `firewall-${new Date().toISOString().slice(0, 10)}.rsc`,
|
||||
code,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+47
-85
@@ -31,9 +31,10 @@ import {
|
||||
PlusIcon, RefreshCwIcon, MoreHorizontalIcon,
|
||||
LockIcon, LockOpenIcon, ShieldCheckIcon, NetworkIcon,
|
||||
EyeIcon, EyeOffIcon, ChevronDownIcon, ChevronRightIcon,
|
||||
CodeXmlIcon, PencilIcon, PowerIcon, Trash2Icon, CopyIcon, CheckIcon,
|
||||
CodeXmlIcon, PencilIcon, PowerIcon, Trash2Icon,
|
||||
DatabaseIcon,
|
||||
} from "lucide-react"
|
||||
import { CodeExportSheet } from "@/components/reui-kit/code-export-sheet"
|
||||
|
||||
// ─── label maps ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -244,7 +245,6 @@ export default function GrePage() {
|
||||
const [tunnelOpen, setTunnelOpen] = useState(false)
|
||||
const [poolOpen, setPoolOpen] = useState(false)
|
||||
const [codePreviewTunnel, setCodePreviewTunnel] = useState<GreTunnel | null>(null)
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const [tForm, setTForm] = useState(defaultTunnelForm)
|
||||
const [pForm, setPForm] = useState(defaultPoolForm)
|
||||
@@ -366,13 +366,10 @@ export default function GrePage() {
|
||||
{ value: "plain", label: "Без IPsec", count: displayTunnels.length - ipsecCount },
|
||||
]
|
||||
|
||||
function handleCopy(code: string) {
|
||||
navigator.clipboard.writeText(code).then(() => {
|
||||
setCopied(true)
|
||||
toast.success("Команды скопированы")
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
})
|
||||
}
|
||||
const greExportCode = useMemo(
|
||||
() => (codePreviewTunnel ? generateRosCommands(codePreviewTunnel, serverById) : ""),
|
||||
[codePreviewTunnel, serverById],
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
@@ -551,82 +548,47 @@ export default function GrePage() {
|
||||
</div>
|
||||
|
||||
{/* ══ Sheet: Code Preview ════════════════════════════════════════════════ */}
|
||||
<Sheet open={!!codePreviewTunnel} onOpenChange={(open) => { if (!open) setCodePreviewTunnel(null) }}>
|
||||
<SheetContent side="right" className="w-full sm:max-w-2xl flex flex-col gap-0 p-0">
|
||||
{codePreviewTunnel && (() => {
|
||||
const code = generateRosCommands(codePreviewTunnel, serverById)
|
||||
return (
|
||||
<>
|
||||
<SheetHeader className="px-6 pt-6 pb-4 border-b shrink-0">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<SheetTitle className="font-mono">{codePreviewTunnel.name}</SheetTitle>
|
||||
<SheetDescription>Команды RouterOS 7.20+ для создания туннеля</SheetDescription>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline" size="sm"
|
||||
className="shrink-0 gap-1.5"
|
||||
onClick={() => handleCopy(code)}
|
||||
>
|
||||
{copied
|
||||
? <><CheckIcon className="size-3.5 text-emerald-500" /> Скопировано</>
|
||||
: <><CopyIcon className="size-3.5" /> Копировать</>}
|
||||
</Button>
|
||||
</div>
|
||||
</SheetHeader>
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{/* meta strip */}
|
||||
<div className="flex flex-wrap gap-3 px-6 py-3 border-b bg-muted/30 text-xs">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className={`size-1.5 rounded-full ${STATUS_MAP[codePreviewTunnel.status].dot}`} />
|
||||
{STATUS_MAP[codePreviewTunnel.status].label}
|
||||
</span>
|
||||
<span className="text-muted-foreground">·</span>
|
||||
<span>{serverById[codePreviewTunnel.serverId]?.name}</span>
|
||||
<span className="text-muted-foreground">·</span>
|
||||
<span className="font-mono">{codePreviewTunnel.localAddress === "0.0.0.0" ? "авто" : codePreviewTunnel.localAddress} → {codePreviewTunnel.remoteAddress}</span>
|
||||
{codePreviewTunnel.ipsec && (
|
||||
<>
|
||||
<span className="text-muted-foreground">·</span>
|
||||
<span className="flex items-center gap-1 text-emerald-400"><LockIcon className="size-3" /> IPsec {IKE_LABELS[codePreviewTunnel.ipsec.ikeVersion]}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* code block */}
|
||||
<pre className="px-6 py-5 text-xs font-mono leading-relaxed text-foreground/90 whitespace-pre overflow-x-auto select-all">
|
||||
{code.split("\n").map((line, i) => {
|
||||
const isComment = line.startsWith("#")
|
||||
const isSection = isComment && line.includes("──")
|
||||
const isKey = /^\s+[a-z]/.test(line)
|
||||
return (
|
||||
<span key={i} className={
|
||||
isSection ? "text-muted-foreground/60"
|
||||
: isComment ? "text-muted-foreground"
|
||||
: isKey ? "text-sky-400/90"
|
||||
: "text-foreground"
|
||||
}>
|
||||
{line}
|
||||
{"\n"}
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<SheetFooter className="px-6 py-4 border-t shrink-0 flex-row gap-2">
|
||||
<SheetClose render={<Button variant="outline" className="flex-1" />}>Закрыть</SheetClose>
|
||||
<Button className="flex-1 gap-1.5" onClick={() => handleCopy(code)}>
|
||||
{copied ? <CheckIcon className="size-4" /> : <CopyIcon className="size-4" />}
|
||||
{copied ? "Скопировано" : "Копировать команды"}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
<CodeExportSheet
|
||||
open={!!codePreviewTunnel}
|
||||
onClose={() => setCodePreviewTunnel(null)}
|
||||
title={codePreviewTunnel?.name ?? "GRE"}
|
||||
description="Команды RouterOS 7.20+ для создания туннеля"
|
||||
formats={[
|
||||
{
|
||||
id: "rsc",
|
||||
label: "RouterOS",
|
||||
filename: `${codePreviewTunnel?.name ?? "gre"}.rsc`,
|
||||
code: greExportCode,
|
||||
},
|
||||
]}
|
||||
beforeCode={
|
||||
codePreviewTunnel ? (
|
||||
<div className="flex flex-wrap gap-3 text-xs shrink-0">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className={`size-1.5 rounded-full ${STATUS_MAP[codePreviewTunnel.status].dot}`} />
|
||||
{STATUS_MAP[codePreviewTunnel.status].label}
|
||||
</span>
|
||||
<span className="text-muted-foreground">·</span>
|
||||
<span>{serverById[codePreviewTunnel.serverId]?.name}</span>
|
||||
<span className="text-muted-foreground">·</span>
|
||||
<span className="font-mono">
|
||||
{codePreviewTunnel.localAddress === "0.0.0.0" ? "авто" : codePreviewTunnel.localAddress}
|
||||
{" → "}
|
||||
{codePreviewTunnel.remoteAddress}
|
||||
</span>
|
||||
{codePreviewTunnel.ipsec ? (
|
||||
<>
|
||||
<span className="text-muted-foreground">·</span>
|
||||
<span className="flex items-center gap-1 text-success">
|
||||
<LockIcon className="size-3" />
|
||||
IPsec {IKE_LABELS[codePreviewTunnel.ipsec.ikeVersion]}
|
||||
</span>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
|
||||
{/* ══ Sheet: Add Tunnel ══════════════════════════════════════════════════ */}
|
||||
<Sheet open={tunnelOpen} onOpenChange={setTunnelOpen}>
|
||||
|
||||
+16
-54
@@ -13,13 +13,9 @@ import { VxlanDataGrid } from "@/components/data-grids/vxlan-data-grid"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
NetworkIcon, PlusIcon, CopyIcon, CheckIcon,
|
||||
CodeXmlIcon, LayersIcon,
|
||||
NetworkIcon, PlusIcon, CodeXmlIcon, LayersIcon,
|
||||
} from "lucide-react"
|
||||
import {
|
||||
Sheet, SheetContent, SheetHeader, SheetTitle,
|
||||
SheetDescription, SheetFooter, SheetClose,
|
||||
} from "@/components/ui/sheet"
|
||||
import { CodeExportSheet } from "@/components/reui-kit/code-export-sheet"
|
||||
|
||||
// ─── helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -70,57 +66,23 @@ function generateVxlanRsc(t: VxlanTunnel): string {
|
||||
function ExportSheet({ open, tunnel, onClose }: {
|
||||
open: boolean; tunnel: VxlanTunnel | null; onClose: () => void
|
||||
}) {
|
||||
const [copied, setCopied] = useState(false)
|
||||
const code = useMemo(() => tunnel ? generateVxlanRsc(tunnel) : "", [tunnel])
|
||||
|
||||
function handleCopy() {
|
||||
navigator.clipboard.writeText(code).then(() => {
|
||||
setCopied(true); setTimeout(() => setCopied(false), 2000)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={(v) => { if (!v) onClose() }}>
|
||||
<SheetContent className="flex flex-col overflow-hidden p-0 gap-0 sm:max-w-2xl">
|
||||
<SheetHeader className="shrink-0 px-6 pt-5 pb-4 border-b">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<SheetTitle>Экспорт VXLAN</SheetTitle>
|
||||
<SheetDescription>RouterOS 7.x · /interface/vxlan + vteps</SheetDescription>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" className="shrink-0" onClick={handleCopy}>
|
||||
{copied ? <><CheckIcon className="size-3.5 text-emerald-500" />Скопировано</> : <><CopyIcon className="size-3.5" />Копировать</>}
|
||||
</Button>
|
||||
</div>
|
||||
</SheetHeader>
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<pre className="px-6 py-5 text-[12px] font-mono leading-relaxed text-foreground/85 whitespace-pre select-all">
|
||||
{code.split("\n").map((line, i) => {
|
||||
const isComment = line.startsWith("#")
|
||||
const isCmd = /^\//.test(line.trimStart())
|
||||
const isParam = /^\s+[a-z]/.test(line)
|
||||
return (
|
||||
<span key={i} className={
|
||||
isComment ? "text-muted-foreground"
|
||||
: isCmd ? "text-sky-400"
|
||||
: isParam ? "text-violet-300"
|
||||
: "text-foreground"
|
||||
}>
|
||||
{line}{"\n"}
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</pre>
|
||||
</div>
|
||||
<SheetFooter className="shrink-0 px-6 py-4 border-t flex-row gap-2">
|
||||
<SheetClose render={<Button variant="outline" className="flex-1" />}>Закрыть</SheetClose>
|
||||
<Button className="flex-1" onClick={handleCopy}>
|
||||
{copied ? <CheckIcon className="size-4" /> : <CopyIcon className="size-4" />}
|
||||
{copied ? "Скопировано" : "Копировать .rsc"}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
<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,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,12 @@ import {
|
||||
type WgIfaceWithServer,
|
||||
} from "@/components/data-grids/wireguard-data-grid"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Frame, FramePanel } from "@/components/reui/frame"
|
||||
import { IconTile } from "@/components/reui/icon-tile"
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
} from "@/components/reui/alert"
|
||||
import { KpiStatGrid } from "@/components/reui-kit/kpi-stat-grid"
|
||||
import { OpsPanel } from "@/components/ops-panel"
|
||||
import { useDataSource } from "@/lib/data-source"
|
||||
import { requestJson } from "@/shared/api/http-client"
|
||||
@@ -34,7 +38,7 @@ import { WgPeerSheet, type WgPeerFormState } from "@/components/wireguard/wg-pee
|
||||
import { toast } from "sonner"
|
||||
import {
|
||||
ShieldCheckIcon, PlusIcon, KeyRoundIcon,
|
||||
UsersIcon, ActivityIcon, RefreshCwIcon, UploadIcon,
|
||||
UsersIcon, ActivityIcon, RefreshCwIcon, UploadIcon, InfoIcon,
|
||||
} from "lucide-react"
|
||||
|
||||
function collectMockInterfaces(): WgIfaceWithServer[] {
|
||||
@@ -139,6 +143,8 @@ export default function WireGuardPage() {
|
||||
const [createOpen, setCreateOpen] = useState(false)
|
||||
const [importOpen, setImportOpen] = useState(false)
|
||||
const [exportIface, setExportIface] = useState<WgIfaceWithServer | null>(null)
|
||||
const [exportInitialTab, setExportInitialTab] = useState<"rsc" | "conf" | "peer">("rsc")
|
||||
const [exportPeerId, setExportPeerId] = useState<string | null>(null)
|
||||
const [peerIface, setPeerIface] = useState<WgIfaceWithServer | null>(null)
|
||||
const [liveExport, setLiveExport] = useState<{
|
||||
rsc?: string
|
||||
@@ -372,6 +378,7 @@ export default function WireGuardPage() {
|
||||
interfaceName: exportIface.name,
|
||||
format,
|
||||
includePrivateKey: format !== "peer-conf",
|
||||
peerId: format === "peer-conf" ? (exportPeerId ?? undefined) : undefined,
|
||||
})
|
||||
setLiveExport((prev) => ({
|
||||
...prev,
|
||||
@@ -389,6 +396,13 @@ export default function WireGuardPage() {
|
||||
}
|
||||
}
|
||||
|
||||
function openExport(iface: WgIfaceWithServer, tab: "rsc" | "conf" | "peer" = "rsc", peerId?: string) {
|
||||
setLiveExport(null)
|
||||
setExportInitialTab(tab)
|
||||
setExportPeerId(peerId ?? null)
|
||||
setExportIface(iface)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<PageHeader
|
||||
@@ -420,40 +434,49 @@ export default function WireGuardPage() {
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{[
|
||||
{ label: "Интерфейсов", value: displayIfaces.length, icon: <ShieldCheckIcon className="size-4 text-muted-foreground" /> },
|
||||
{ label: "Активных (UP)", value: upIfaces, icon: <ActivityIcon className="size-4 text-emerald-500" /> },
|
||||
{ label: "Всего пиров", value: totalPeers, icon: <UsersIcon className="size-4 text-sky-400" /> },
|
||||
{ label: "Пиров онлайн", value: `${onlinePeers}/${totalPeers}`, icon: <KeyRoundIcon className="size-4 text-violet-400" /> },
|
||||
].map((s) => (
|
||||
<Frame key={s.label} className="h-full">
|
||||
<FramePanel className="relative isolate flex h-full items-start gap-3">
|
||||
<IconTile variant="elevated" aria-hidden="true" className="size-10.5 text-muted-foreground">
|
||||
{s.icon}
|
||||
</IconTile>
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
||||
<p className="text-muted-foreground text-sm font-medium">{s.label}</p>
|
||||
<p className="text-2xl leading-none font-bold tabular-nums">{s.value}</p>
|
||||
</div>
|
||||
</FramePanel>
|
||||
</Frame>
|
||||
))}
|
||||
</div>
|
||||
<KpiStatGrid
|
||||
aria-label="Сводка WireGuard"
|
||||
items={[
|
||||
{
|
||||
id: "ifaces",
|
||||
label: "Интерфейсов",
|
||||
value: displayIfaces.length,
|
||||
icon: <ShieldCheckIcon className="size-4" />,
|
||||
iconClassName: "text-muted-foreground",
|
||||
},
|
||||
{
|
||||
id: "up",
|
||||
label: "Активных (UP)",
|
||||
value: upIfaces,
|
||||
icon: <ActivityIcon className="size-4" />,
|
||||
iconClassName: "text-success",
|
||||
},
|
||||
{
|
||||
id: "peers",
|
||||
label: "Всего пиров",
|
||||
value: totalPeers,
|
||||
icon: <UsersIcon className="size-4" />,
|
||||
iconClassName: "text-info",
|
||||
},
|
||||
{
|
||||
id: "online",
|
||||
label: "Пиров онлайн",
|
||||
value: `${onlinePeers}/${totalPeers}`,
|
||||
icon: <KeyRoundIcon className="size-4" />,
|
||||
iconClassName: "text-primary",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className="flex items-start gap-3 rounded-lg bg-sky-500/5 border border-sky-500/20 px-4 py-3 text-sm">
|
||||
<ShieldCheckIcon className="size-5 text-sky-500 shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p className="font-medium text-sky-600 dark:text-sky-400">
|
||||
WireGuard — live-интеграция RouterOS 7.x
|
||||
</p>
|
||||
<p className="text-muted-foreground text-xs mt-0.5">
|
||||
{isLive
|
||||
? "Опрос /interface/wireguard на включённых серверах. Создание, импорт .rsc/.conf и экспорт с роутера."
|
||||
: "Сейчас mock-режим. Переключитесь в live в настройках, чтобы применять изменения на MikroTik."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Alert variant="info">
|
||||
<InfoIcon />
|
||||
<AlertTitle>WireGuard — live-интеграция RouterOS 7.x</AlertTitle>
|
||||
<AlertDescription>
|
||||
{isLive
|
||||
? "Опрос /interface/wireguard на включённых серверах. Создание, импорт .rsc/.conf и экспорт с роутера."
|
||||
: "Сейчас mock-режим. Переключитесь в live в настройках, чтобы применять изменения на MikroTik."}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<DataPageCard>
|
||||
<DataPageToolbar
|
||||
@@ -464,18 +487,12 @@ export default function WireGuardPage() {
|
||||
/>
|
||||
<WireguardDataGrid
|
||||
interfaces={filtered}
|
||||
onExport={(iface) => {
|
||||
setLiveExport(null)
|
||||
setExportIface(iface)
|
||||
}}
|
||||
onExport={(iface) => openExport(iface, "rsc")}
|
||||
onAddPeer={setPeerIface}
|
||||
onToggle={handleToggle}
|
||||
onDelete={handleDelete}
|
||||
onDeletePeer={handleDeletePeer}
|
||||
onExportPeer={(iface) => {
|
||||
setLiveExport(null)
|
||||
setExportIface(iface)
|
||||
}}
|
||||
onExportPeer={(iface, peerId) => openExport(iface, "peer", peerId)}
|
||||
/>
|
||||
</DataPageCard>
|
||||
|
||||
@@ -518,7 +535,7 @@ export default function WireGuardPage() {
|
||||
<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">
|
||||
<pre className="bg-muted rounded-md p-2.5 text-muted-foreground text-[11px] leading-relaxed overflow-x-auto">
|
||||
{b.lines.join("\n")}
|
||||
</pre>
|
||||
</div>
|
||||
@@ -552,8 +569,12 @@ export default function WireGuardPage() {
|
||||
<WgExportSheet
|
||||
open={!!exportIface}
|
||||
iface={exportIface}
|
||||
initialTab={exportInitialTab}
|
||||
peerId={exportPeerId}
|
||||
onClose={() => {
|
||||
setExportIface(null)
|
||||
setExportPeerId(null)
|
||||
setExportInitialTab("rsc")
|
||||
setLiveExport(null)
|
||||
}}
|
||||
liveContent={liveExport}
|
||||
|
||||
Reference in New Issue
Block a user