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:
Denozordec
2026-05-07 20:49:35 +07:00
parent 84ecd4f061
commit 11ad94f67d
33 changed files with 12350 additions and 252 deletions
+10 -15
View File
@@ -7,6 +7,7 @@ import type { GrePool, GreTunnel, GreStatus, IpsecEncAlg, IpsecAuthAlg, IpsecDhG
import { useDataSource } from "@/lib/data-source"
import { requestJson } from "@/shared/api/http-client"
import { cn } from "@/lib/utils"
import { toast } from "sonner"
import { Card, CardContent } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
@@ -25,7 +26,6 @@ import {
EyeIcon, EyeOffIcon, ChevronDownIcon, ChevronRightIcon,
CodeXmlIcon, PencilIcon, PowerIcon, Trash2Icon, CopyIcon, CheckIcon,
DatabaseIcon,
AlertCircleIcon,
} from "lucide-react"
// ─── label maps ─────────────────────────────────────────────────────────────
@@ -281,7 +281,6 @@ export default function GrePage() {
const [dataLoading, setDataLoading] = useState(false)
const [dataError, setDataError] = useState<string | null>(null)
const [syncJhBusy, setSyncJhBusy] = useState(false)
const [syncJhMessage, setSyncJhMessage] = useState<string | null>(null)
const [pageTab, setPageTab] = useState<PageTab>("tunnels")
const [tabFilter, setTabFilter] = useState<TabFilter>("all")
@@ -302,7 +301,6 @@ export default function GrePage() {
if (!isLive) return
setDataLoading(true)
setDataError(null)
setSyncJhMessage(null)
try {
const [backendServers, greRes] = await Promise.all([
apiFetch<BackendServer[]>("/api/servers"),
@@ -353,11 +351,10 @@ export default function GrePage() {
if (!isLive || syncJhBusy) return
const jh = displayServers.filter((s) => s.type === "jump-host" && s.enabled)
if (jh.length === 0) {
setSyncJhMessage("Нет включённых Jump Host в списке серверов")
toast.info("Нет включённых Jump Host в списке серверов")
return
}
setSyncJhBusy(true)
setSyncJhMessage(null)
const errors: string[] = []
try {
for (const s of jh) {
@@ -373,17 +370,21 @@ export default function GrePage() {
const fresh = await apiFetch<GreTunnelsApiResponse>("/api/filters/gre-tunnels")
setLiveTunnels(fresh.tunnels)
if (errors.length) {
setSyncJhMessage(`Синхронизировано JH: ${jh.length - errors.length}/${jh.length}. Ошибки: ${errors.join("; ")}`)
toast.warning(`Синхронизировано JH: ${jh.length - errors.length}/${jh.length}. Ошибки: ${errors.join("; ")}`)
} else {
setSyncJhMessage(`Правила с ${jh.length} JH записаны в БД, список GRE обновлён.`)
toast.success(`Правила с ${jh.length} JH записаны в БД, список GRE обновлён.`)
}
} catch (e) {
setSyncJhMessage(e instanceof Error ? e.message : "Ошибка после синхронизации")
toast.error(e instanceof Error ? e.message : "Ошибка после синхронизации")
} finally {
setSyncJhBusy(false)
}
}, [isLive, syncJhBusy, apiFetch, displayServers])
useEffect(() => {
if (dataError) toast.error(dataError)
}, [dataError])
const filtered = useMemo(() => {
return displayTunnels.filter((t) => {
if (tabFilter === "up" && t.status !== "up") return false
@@ -413,6 +414,7 @@ export default function GrePage() {
function handleCopy(code: string) {
navigator.clipboard.writeText(code).then(() => {
setCopied(true)
toast.success("Команды скопированы")
setTimeout(() => setCopied(false), 2000)
})
}
@@ -453,13 +455,6 @@ export default function GrePage() {
<div className="flex-1 overflow-y-auto p-6">
<div className="flex flex-col gap-5">
{(dataError || syncJhMessage) && (
<div className={`flex items-start gap-3 rounded-lg border px-4 py-3 text-sm ${dataError ? "bg-destructive/5 border-destructive/30 text-destructive" : "bg-muted/40 border-border text-muted-foreground"}`}>
<AlertCircleIcon className="size-5 shrink-0 mt-0.5" />
<div>{dataError ?? syncJhMessage}</div>
</div>
)}
{/* Legacy banner */}
<div className="flex items-start gap-3 rounded-lg bg-amber-500/5 border border-amber-500/20 px-4 py-3 text-sm">
<ShieldCheckIcon className="size-5 text-amber-500 shrink-0 mt-0.5" />