Docker images / prepare-release (push) Successful in 5s
Docker images / backend-image (push) Successful in 2m37s
Docker images / frontend-image (push) Successful in 2m22s
Docker images / updater-image (push) Successful in 38s
Docker images / notify-webhook (push) Has been skipped
Docker images / publish-release (push) Successful in 8s
124 lines
5.7 KiB
TypeScript
124 lines
5.7 KiB
TypeScript
"use client"
|
|
|
|
import type { Server } from "@/lib/data"
|
|
import { cn } from "@/lib/utils"
|
|
import { Button } from "@/components/ui/button"
|
|
import { RosBadge, rosVer } from "@/components/data-grids/servers-data-grid"
|
|
import { CheckCircleIcon, XCircleIcon, RefreshCwIcon } from "lucide-react"
|
|
|
|
const ROS_FEATURES = [
|
|
{ name: "WireGuard", minVer: 701, label: "7.1+", desc: "WireGuard VPN туннели" },
|
|
{ name: "Container", minVer: 704, label: "7.4+", desc: "Docker-совместимые контейнеры" },
|
|
{ name: "BFD", minVer: 705, label: "7.5+", desc: "Bidirectional Forwarding Detection" },
|
|
{ name: "Large Communities", minVer: 707, label: "7.7+", desc: "BGP Large Communities (RFC 8092)" },
|
|
{ name: "VXLAN", minVer: 710, label: "7.10+", desc: "VXLAN overlay туннели" },
|
|
{ name: "RPKI", minVer: 713, label: "7.13+", desc: "Route Origin Validation" },
|
|
{ name: "BGP Flowspec", minVer: 714, label: "7.14+", desc: "BGP Flow Spec (RFC 8955)" },
|
|
{ name: "IPv6 Firewall", minVer: 715, label: "7.15+", desc: "Расширенный IPv6 Firewall" },
|
|
{ name: "REST API v2", minVer: 716, label: "7.16+", desc: "Обновлённый REST API" },
|
|
{ name: "VRF Enhanced", minVer: 717, label: "7.17+", desc: "Расширенная поддержка VRF" },
|
|
]
|
|
|
|
interface ServerExpandedDetailProps {
|
|
server: Server
|
|
isLive: boolean
|
|
isPolling: boolean
|
|
onPoll: () => void
|
|
}
|
|
|
|
function ServerExpandedDetail({ server: s, isLive, isPolling, onPoll }: ServerExpandedDetailProps) {
|
|
const ver = rosVer(s.os)
|
|
|
|
return (
|
|
<div className="flex flex-col gap-5 px-5 py-5 bg-muted/20">
|
|
<div className="flex items-start justify-between gap-4 flex-wrap">
|
|
<div className="flex flex-wrap gap-x-6 gap-y-2 text-xs">
|
|
{s.model && s.model !== "—" && (
|
|
<span className="text-muted-foreground">Модель: <span className="font-mono text-foreground">{s.model}</span></span>
|
|
)}
|
|
{s.uptime && (
|
|
<span className="text-muted-foreground">Uptime: <span className="font-mono text-foreground">{s.uptime}</span></span>
|
|
)}
|
|
{s.cpuLoad != null && (
|
|
<span className="text-muted-foreground">CPU: <span className={cn("font-mono font-semibold", s.cpuLoad > 80 ? "text-red-400" : s.cpuLoad > 50 ? "text-amber-400" : "text-emerald-400")}>{s.cpuLoad}%</span></span>
|
|
)}
|
|
{s.asn && (
|
|
<span className="text-muted-foreground">ASN: <span className="font-mono text-foreground">{s.asn}</span></span>
|
|
)}
|
|
{s.ipv6Address && (
|
|
<span className="text-muted-foreground">IPv6: <span className="font-mono text-sky-400">{s.ipv6Address}</span></span>
|
|
)}
|
|
{s.vrfNames?.map((v) => (
|
|
<span key={v} className="text-muted-foreground">VRF: <span className="font-mono text-foreground">{v}</span></span>
|
|
))}
|
|
{s.comment && <span className="text-muted-foreground italic">{s.comment}</span>}
|
|
{s.polledAt ? (
|
|
<span className="text-muted-foreground/50 text-[11px]">
|
|
Опрошен: {new Date(s.polledAt).toLocaleString("ru")}
|
|
</span>
|
|
) : (
|
|
<span className="text-amber-500/70 text-[11px]">⚠ Ещё не опрашивался</span>
|
|
)}
|
|
</div>
|
|
{isLive && (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="h-7 gap-1.5 text-xs shrink-0"
|
|
disabled={isPolling}
|
|
onClick={(e) => { e.stopPropagation(); onPoll() }}
|
|
>
|
|
<RefreshCwIcon className={cn("size-3.5", isPolling && "animate-spin")} />
|
|
{isPolling ? "Опрос…" : "Опросить сейчас"}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
<div>
|
|
<div className="flex items-center gap-3 mb-3">
|
|
<p className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider">
|
|
Возможности RouterOS
|
|
</p>
|
|
<RosBadge os={s.os} />
|
|
<span className="text-[11px] text-muted-foreground">
|
|
{ver >= 715
|
|
? "✓ Актуальная версия — все ключевые фичи доступны"
|
|
: ver >= 710
|
|
? "⚠ Рекомендуется обновление до 7.15+"
|
|
: s.os !== "—"
|
|
? "✗ Устаревшая версия — требуется обновление"
|
|
: "Нет данных — нажмите «Опросить сейчас»"}
|
|
</span>
|
|
</div>
|
|
<div className="grid grid-cols-2 sm:grid-cols-3 xl:grid-cols-5 gap-2">
|
|
{ROS_FEATURES.map((f) => {
|
|
const ok = ver >= f.minVer
|
|
return (
|
|
<div
|
|
key={f.name}
|
|
className={cn(
|
|
"flex items-start gap-2 rounded-md border px-3 py-2.5 transition-colors",
|
|
ok ? "border-emerald-500/25 bg-emerald-500/5" : "border-border/40 bg-background/40 opacity-60",
|
|
)}
|
|
>
|
|
{ok
|
|
? <CheckCircleIcon className="size-3.5 text-emerald-500 shrink-0 mt-0.5" />
|
|
: <XCircleIcon className="size-3.5 text-muted-foreground/40 shrink-0 mt-0.5" />}
|
|
<div className="min-w-0">
|
|
<p className={cn("text-xs font-medium leading-tight truncate", ok ? "text-foreground" : "text-muted-foreground")}>
|
|
{f.name}
|
|
</p>
|
|
<p className="text-[10px] text-muted-foreground leading-tight mt-0.5">
|
|
{f.label} · {f.desc}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export { ServerExpandedDetail }
|