feat: integrate sonner for toast notifications and enhance UI feedback
Added the sonner library for toast notifications across various components, improving user feedback for actions such as saving settings, syncing rules, and handling errors. Updated the layout to include a Toaster component for consistent notification display. Refactored alert messages in the backups, gre, and filters pages to utilize the new notification system, enhancing overall user experience.
This commit is contained in:
@@ -56,12 +56,6 @@ import { Flag } from "@/components/flag"
|
||||
|
||||
// ─── Resource metrics (для мини-блока справа; числа детерминированы по id узла) ─
|
||||
|
||||
const BOARD_MAP: Record<ServerType, string> = {
|
||||
"jump-host": "RB5009UG+S+IN",
|
||||
"exit-node": "RB4011iGS+RM",
|
||||
"home-router": "hAP ax²",
|
||||
}
|
||||
|
||||
// ─── Backend → frontend (как /servers) ───────────────────────────────────────
|
||||
|
||||
interface BackendServerRow {
|
||||
@@ -707,11 +701,12 @@ function Minimap({ pan, zoom, nodes, greEdges, satPos, wanJhEdges, homeRouters,
|
||||
stroke={TUNNEL_STYLE[e.tunnel.status].stroke} strokeWidth="5" opacity="0.25" />
|
||||
))}
|
||||
{/* WAN edges */}
|
||||
{wanJhEdges.map((edge, i) => {
|
||||
{wanJhEdges.map((edge) => {
|
||||
const sat = satPos[edge.homeId]?.[edge.wanIdx]
|
||||
const jh = nodes.find(n => n.id === edge.jhId)
|
||||
if (!sat || !jh) return null
|
||||
return <line key={i} x1={sat.x} y1={sat.y} x2={jh.x} y2={jh.y}
|
||||
const edgeKey = `${edge.homeId}-${edge.wanIdx}-${edge.jhId}`
|
||||
return <line key={edgeKey} x1={sat.x} y1={sat.y} x2={jh.x} y2={jh.y}
|
||||
stroke={WAN_COLORS[edge.wanIdx]} strokeWidth="3" opacity="0.25" />
|
||||
})}
|
||||
{/* nodes */}
|
||||
@@ -1333,11 +1328,52 @@ export default function NetworkMapPage() {
|
||||
|
||||
const connectedTunnels = selected
|
||||
? mapGreTunnels.filter((t) => {
|
||||
if (t.serverId === selected.id) return true
|
||||
const peer = findServerByGreRemote(mapServers, t.remoteAddress, greResolvedMap)
|
||||
if (!peer) return false
|
||||
return t.serverId === selected.id || peer.id === selected.id
|
||||
if (peer?.id === selected.id) return true
|
||||
const selectedHost = normalizeGreEndpointAddr(selected.host)
|
||||
const selectedWanIps = (selected.wanUplinks ?? [])
|
||||
.map((w) => normalizeGreEndpointAddr(w.ip))
|
||||
.filter(Boolean)
|
||||
const remote = normalizeGreEndpointAddr(t.remoteAddress)
|
||||
const local = normalizeGreEndpointAddr(t.localAddress)
|
||||
return (
|
||||
(selectedHost.length > 0 && (remote === selectedHost || local === selectedHost)) ||
|
||||
selectedWanIps.includes(remote) ||
|
||||
selectedWanIps.includes(local)
|
||||
)
|
||||
})
|
||||
: []
|
||||
const grePrimaryByGroupKey = useMemo(() => {
|
||||
if (!selected) return new Map<string, string>()
|
||||
const group = new Map<string, GreTunnel[]>()
|
||||
const score = (t: GreTunnel): number => {
|
||||
const statusScore = t.status === "up" ? 3 : t.status === "degraded" ? 2 : 1
|
||||
const enabledScore = t.enabled ? 10 : 0
|
||||
return enabledScore + statusScore
|
||||
}
|
||||
for (const t of connectedTunnels) {
|
||||
const peer =
|
||||
t.serverId === selected.id
|
||||
? findServerByGreRemote(mapServers, t.remoteAddress, greResolvedMap)
|
||||
: mapServers.find((s) => s.id === t.serverId)
|
||||
const groupKey = peer?.id ?? (normalizeGreEndpointAddr(t.remoteAddress) || t.remoteAddress)
|
||||
const arr = group.get(groupKey) ?? []
|
||||
arr.push(t)
|
||||
group.set(groupKey, arr)
|
||||
}
|
||||
const primaryByKey = new Map<string, string>()
|
||||
for (const [groupKey, tunnels] of group.entries()) {
|
||||
const sorted = [...tunnels].sort((a, b) => {
|
||||
const d = score(b) - score(a)
|
||||
if (d !== 0) return d
|
||||
return a.id.localeCompare(b.id)
|
||||
})
|
||||
const first = sorted[0]
|
||||
if (first) primaryByKey.set(groupKey, first.id)
|
||||
}
|
||||
return primaryByKey
|
||||
}, [connectedTunnels, selected, mapServers, greResolvedMap])
|
||||
|
||||
const hoveredNode = hoveredId ? nodes.find(n => n.id === hoveredId) : null
|
||||
|
||||
@@ -1619,16 +1655,17 @@ export default function NetworkMapPage() {
|
||||
})}
|
||||
|
||||
{/* ── WAN→JH edges ── */}
|
||||
{visibleWanJhEdges.map((edge, i) => {
|
||||
{visibleWanJhEdges.map((edge) => {
|
||||
const jh = nodeById[edge.jhId]
|
||||
const satPos = effectiveSatPos[edge.homeId]?.[edge.wanIdx]
|
||||
if (!jh || !satPos) return null
|
||||
const edgeKey = `${edge.homeId}-${edge.wanIdx}-${edge.jhId}`
|
||||
const color = WAN_COLORS[edge.wanIdx] ?? "#888"
|
||||
const vis = filter === "all" || filter === "home-router" || filter === "jump-host" || filter === "online"
|
||||
const { mx, my } = edgeBadgePosition(satPos.x, satPos.y, jh.x, jh.y, 0.62, -17)
|
||||
const isHL = selected?.id === edge.homeId && (selWanIdx === null || selWanIdx === edge.wanIdx)
|
||||
return (
|
||||
<g key={`wan-jh-${i}`} opacity={vis ? (isHL ? 1 : 0.45) : 0.05}
|
||||
<g key={edgeKey} opacity={vis ? (isHL ? 1 : 0.45) : 0.05}
|
||||
style={{ transition: "opacity 0.3s" }}>
|
||||
<line
|
||||
x1={satPos.x} y1={satPos.y} x2={jh.x} y2={jh.y}
|
||||
@@ -1640,7 +1677,7 @@ export default function NetworkMapPage() {
|
||||
/>
|
||||
{showAnimDots && edge.active && (
|
||||
<circle r="3.5" fill={color} opacity="0.85">
|
||||
<animateMotion dur={`${1.8 + i * 0.3}s`} repeatCount="indefinite"
|
||||
<animateMotion dur={`${1.8 + (edge.wanIdx % 5) * 0.3}s`} repeatCount="indefinite"
|
||||
path={`M ${satPos.x} ${satPos.y} L ${jh.x} ${jh.y}`} />
|
||||
</circle>
|
||||
)}
|
||||
@@ -2143,10 +2180,11 @@ export default function NetworkMapPage() {
|
||||
<p className="text-[9px] uppercase text-muted-foreground tracking-wider mb-1.5">
|
||||
Подключения к JH
|
||||
</p>
|
||||
{myEdges.map((e, ei) => {
|
||||
{myEdges.map((e) => {
|
||||
const jh = mapServers.find(s => s.id === e.jhId)
|
||||
const edgeKey = `${e.homeId}-${e.wanIdx}-${e.jhId}`
|
||||
return (
|
||||
<div key={ei} className="flex items-center justify-between py-0.5">
|
||||
<div key={edgeKey} className="flex items-center justify-between py-0.5">
|
||||
<span className="text-[10px] font-mono text-muted-foreground truncate">
|
||||
→ {jh?.name ?? e.jhId}
|
||||
</span>
|
||||
@@ -2186,6 +2224,8 @@ export default function NetworkMapPage() {
|
||||
? `${fromServer.id}|${toServer.id}|${t.id}|${t.localAddress}|${t.remoteAddress}`
|
||||
: `${t.serverId}|${t.id}|${t.localAddress}|${t.remoteAddress}`
|
||||
const baseProbe = greTunnelProbe(t)
|
||||
const groupKey = peer?.id ?? (normalizeGreEndpointAddr(t.remoteAddress) || t.remoteAddress)
|
||||
const isPrimary = grePrimaryByGroupKey.get(groupKey) === t.id
|
||||
const spGre =
|
||||
fromServer && toServer ? speedProbeByTunnelId.get(tunnelPanelKey) : undefined
|
||||
const merged = mergeGreMetricsWithSpeedProbe(spGre, baseProbe)
|
||||
@@ -2197,9 +2237,17 @@ export default function NetworkMapPage() {
|
||||
<div key={tunnelPanelKey} className="rounded-md border border-border/60 px-3 py-2 bg-muted/20">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-xs font-mono font-medium">{t.name}</span>
|
||||
<span className="text-[10px] font-medium" style={{ color: ts.stroke }}>
|
||||
{t.status === "up" ? "Up" : t.status === "degraded" ? "Degraded" : "Down"}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={cn(
|
||||
"text-[10px] font-medium px-1.5 py-0.5 rounded-full",
|
||||
isPrimary ? "bg-emerald-500/10 text-emerald-400" : "bg-muted text-muted-foreground",
|
||||
)}>
|
||||
{isPrimary ? "primary" : "backup"}
|
||||
</span>
|
||||
<span className="text-[10px] font-medium" style={{ color: ts.stroke }}>
|
||||
{t.status === "up" ? "Up" : t.status === "degraded" ? "Degraded" : "Down"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-[10px] text-muted-foreground font-mono truncate max-w-[200px]">
|
||||
@@ -2317,7 +2365,7 @@ export default function NetworkMapPage() {
|
||||
)}
|
||||
<div className="flex items-center justify-between py-1.5">
|
||||
<span className="text-[10px] text-muted-foreground">Плата</span>
|
||||
<span className="text-[10px] font-mono">{BOARD_MAP[selected.type]}</span>
|
||||
<span className="text-[10px] font-mono">{selected.model || "—"}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user