refactor: replace custom API fetch logic with requestJson utility across multiple pages

Updated the API fetching mechanism in various components to utilize the new requestJson function for improved consistency and error handling. This change affects the alerts, dashboard, data collection, filters, gre, network map, probes, recursive routes, route optimizer, servers, settings, traffic, and uptime pages.
This commit is contained in:
Denozordec
2026-05-07 13:35:41 +07:00
parent 5f31bb47fb
commit 6d8379501c
42 changed files with 1336 additions and 631 deletions
+2 -11
View File
@@ -28,6 +28,7 @@ import {
} from "lucide-react"
import { Flag, countryName } from "@/components/flag"
import { cn } from "@/lib/utils"
import { requestJson } from "@/shared/api/http-client"
// ─── types ────────────────────────────────────────────────────────────────────
@@ -165,17 +166,7 @@ interface BgpSessionListRow {
function makeApiFetch(backendUrl: string) {
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
const headers: HeadersInit = init?.body ? { "Content-Type": "application/json" } : {}
const res = await fetch(backendUrl.replace(/\/$/, "") + path, {
...init,
headers: { ...headers, ...(init?.headers ?? {}) },
})
if (!res.ok) {
const err = await res.json().catch(() => ({ error: res.statusText })) as { error?: string }
throw new Error(err.error ?? res.statusText)
}
if (res.status === 204) return undefined as T
return res.json() as Promise<T>
return requestJson<T>(backendUrl, path, init)
}
}
+2 -17
View File
@@ -24,27 +24,12 @@ import { Flag } from "@/components/flag"
import { AlertCircleIcon, AlertTriangleIcon, InfoIcon, FilterIcon, DownloadIcon } from "lucide-react"
import { Button, buttonVariants } from "@/components/ui/button"
import { cn } from "@/lib/utils"
import { requestJson } from "@/shared/api/http-client"
import { buildLatencySeriesByProbeSource } from "@/lib/dashboard-latency"
function makeApiFetch(backendUrl: string) {
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
const headers: HeadersInit = init?.body ? { "Content-Type": "application/json" } : {}
const res = await fetch(backendUrl.replace(/\/$/, "") + path, {
...init,
headers: { ...headers, ...(init?.headers ?? {}) },
})
if (!res.ok) {
let message = res.statusText
try {
const err = await res.json() as { error?: string }
message = err.error ?? message
} catch {
const text = await res.text().catch(() => "")
if (text) message = text
}
throw new Error(message)
}
return res.json() as Promise<T>
return requestJson<T>(backendUrl, path, init)
}
}
+2 -17
View File
@@ -23,6 +23,7 @@ import {
type SchedulerRunRowDto,
type UptimeSettingsDto,
} from "@/lib/scheduler-settings"
import { requestJson } from "@/shared/api/http-client"
import {
parseSchedulerRunSnapshot,
type AlertEngineRuleDiagSnapshot,
@@ -49,23 +50,7 @@ import {
function makeApiFetch(backendUrl: string) {
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
const headers: HeadersInit = init?.body ? { "Content-Type": "application/json" } : {}
const res = await fetch(backendUrl.replace(/\/$/, "") + path, {
...init,
headers: { ...headers, ...(init?.headers ?? {}) },
})
if (!res.ok) {
let message = res.statusText
try {
const err = (await res.json()) as { error?: string }
message = err.error ?? message
} catch {
const text = await res.text().catch(() => "")
if (text) message = text
}
throw new Error(message || "Ошибка запроса")
}
return res.json() as Promise<T>
return requestJson<T>(backendUrl, path, init)
}
}
+2 -10
View File
@@ -9,6 +9,7 @@ import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Separator } from "@/components/ui/separator"
import { cn } from "@/lib/utils"
import { requestJson } from "@/shared/api/http-client"
import {
servers, greTunnels, serverFilterRulesets, filters as mockFiltersCatalog,
type FilterRule, type ServerFilterRuleset, type Server, type GreTunnel,
@@ -1491,16 +1492,7 @@ function buildRulesets(serverList: Server[], sourceRulesets: ServerFilterRuleset
function makeApiFetch(backendUrl: string) {
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
const headers: HeadersInit = init?.body ? { "Content-Type": "application/json" } : {}
const finalRes = await fetch(backendUrl.replace(/\/$/, "") + path, {
...init,
headers: { ...headers, ...(init?.headers ?? {}) },
})
if (!finalRes.ok) {
const err = await finalRes.json().catch(() => ({ error: finalRes.statusText })) as { error?: string }
throw new Error(err.error ?? finalRes.statusText)
}
return finalRes.json() as Promise<T>
return requestJson<T>(backendUrl, path, init)
}
}
+2 -10
View File
@@ -5,6 +5,7 @@ import { PageHeader } from "@/components/page-header"
import { greTunnels as mockGreTunnels, grePools as mockGrePools, servers as mockServers } from "@/lib/data"
import type { GrePool, GreTunnel, GreStatus, IpsecEncAlg, IpsecAuthAlg, IpsecDhGroup, IkeVersion, Server } from "@/lib/data"
import { useDataSource } from "@/lib/data-source"
import { requestJson } from "@/shared/api/http-client"
import { cn } from "@/lib/utils"
import { Card, CardContent } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
@@ -210,16 +211,7 @@ interface GreTunnelsApiResponse {
function makeApiFetch(backendUrl: string) {
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
const headers: HeadersInit = init?.body ? { "Content-Type": "application/json" } : {}
const finalRes = await fetch(backendUrl.replace(/\/$/, "") + path, {
...init,
headers: { ...headers, ...(init?.headers ?? {}) },
})
if (!finalRes.ok) {
const err = await finalRes.json().catch(() => ({ error: finalRes.statusText })) as { error?: string }
throw new Error(err.error ?? finalRes.statusText)
}
return finalRes.json() as Promise<T>
return requestJson<T>(backendUrl, path, init)
}
}
+2 -10
View File
@@ -11,6 +11,7 @@ import {
type ServerType,
} from "@/lib/data"
import { useDataSource } from "@/lib/data-source"
import { requestJson } from "@/shared/api/http-client"
import {
buildGreMapEdges,
buildServerResourceMap,
@@ -175,16 +176,7 @@ function apiGreToGreTunnel(t: ApiGreTunnelRow): GreTunnel {
function makeApiFetch(backendUrl: string) {
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
const headers: HeadersInit = init?.body ? { "Content-Type": "application/json" } : {}
const res = await fetch(backendUrl.replace(/\/$/, "") + path, {
...init,
headers: { ...headers, ...(init?.headers ?? {}) },
})
if (!res.ok) {
const err = await res.json().catch(() => ({ error: res.statusText })) as { error?: string }
throw new Error(err.error ?? res.statusText)
}
return res.json() as Promise<T>
return requestJson<T>(backendUrl, path, init)
}
}
+2 -10
View File
@@ -14,6 +14,7 @@ import {
LoaderCircleIcon, AlertCircleIcon,
} from "lucide-react"
import { cn } from "@/lib/utils"
import { requestJson } from "@/shared/api/http-client"
// ─── types ────────────────────────────────────────────────────────────────────
@@ -66,16 +67,7 @@ const TOOL_META: Record<DiagTool, {
function makeApiFetch(backendUrl: string) {
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
const headers: HeadersInit = init?.body ? { "Content-Type": "application/json" } : {}
const finalRes = await fetch(backendUrl.replace(/\/$/, "") + path, {
...init,
headers: { ...headers, ...(init?.headers ?? {}) },
})
if (!finalRes.ok) {
const err = await finalRes.json().catch(() => ({ error: finalRes.statusText })) as { error?: string }
throw new Error(err.error ?? finalRes.statusText)
}
return finalRes.json() as Promise<T>
return requestJson<T>(backendUrl, path, init)
}
}
+2 -17
View File
@@ -12,6 +12,7 @@ import { useDataSource } from "@/lib/data-source"
import { cn } from "@/lib/utils"
import { servers as mockServers, type Server } from "@/lib/data"
import { PlusIcon, SaveIcon, TrashIcon, SearchIcon, XIcon, PencilIcon, ChevronDownIcon, ChevronRightIcon, AlertCircleIcon, CheckIcon } from "lucide-react"
import { requestJson } from "@/shared/api/http-client"
interface BackendServer {
id: number
@@ -91,23 +92,7 @@ interface RouteGroup {
function makeApiFetch(backendUrl: string) {
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
const headers: HeadersInit = init?.body ? { "Content-Type": "application/json" } : {}
const res = await fetch(backendUrl.replace(/\/$/, "") + path, {
...init,
headers: { ...headers, ...(init?.headers ?? {}) },
})
if (!res.ok) {
let message = res.statusText
try {
const err = await res.json() as { error?: string }
message = err.error ?? message
} catch {
const text = await res.text().catch(() => "")
if (text) message = text
}
throw new Error(message)
}
return res.json() as Promise<T>
return requestJson<T>(backendUrl, path, init)
}
}
+2 -17
View File
@@ -8,6 +8,7 @@ import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Flag } from "@/components/flag"
import { cn } from "@/lib/utils"
import { requestJson } from "@/shared/api/http-client"
import { servers } from "@/lib/data"
import { useDataSource } from "@/lib/data-source"
import {
@@ -40,23 +41,7 @@ import {
function makeApiFetch(backendUrl: string) {
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
const headers: HeadersInit = init?.body ? { "Content-Type": "application/json" } : {}
const res = await fetch(backendUrl.replace(/\/$/, "") + path, {
...init,
headers: { ...headers, ...(init?.headers ?? {}) },
})
if (!res.ok) {
let message = res.statusText
try {
const err = await res.json() as { error?: string }
message = err.error ?? message
} catch {
const text = await res.text().catch(() => "")
if (text) message = text
}
throw new Error(message)
}
return res.json() as Promise<T>
return requestJson<T>(backendUrl, path, init)
}
}
+35 -96
View File
@@ -5,71 +5,21 @@ import { PageHeader } from "@/components/page-header"
import { StatusBadge } from "@/components/status-badge"
import { servers as initialServers } from "@/lib/data"
import type { ServerType, Server, WanUplink } from "@/lib/data"
import type { ServerCreate, ServerUpdate } from "@/packages/contracts/src/servers"
import { toFrontendServer } from "@/entities/server/model/mappers"
import {
createServer,
deleteServer,
getServer,
listServers,
pollServer,
testServerConnection,
updateServer,
} from "@/shared/api/servers"
// ─── Backend integration ──────────────────────────────────────────────────────
import { useDataSource } from "@/lib/data-source"
interface BackendServer {
id: number; name: string; host: string; port: number
useSsl: boolean; verifySsl: boolean; username: string; password: string
type: ServerType; site: string; country: string; asn: string
comment: string; enabled: boolean
lanSubnet: string
wanUplinks: WanUplink[]
status: "online" | "offline" | null; latency: number | null
os: string | null; model: string | null; uptime: string | null
cpuLoad: number | null; freeMemory: number | null; totalMemory: number | null
identityName: string | null
sessions: number; polledAt: string | null
createdAt: string; updatedAt: string
}
function toFrontend(s: BackendServer): Server {
return {
id: String(s.id),
name: s.name || s.host,
host: s.host,
type: s.type,
site: s.site,
country: s.country,
asn: s.asn,
model: s.model ?? "—",
os: s.os ?? "—",
enabled: s.enabled,
status: s.status ?? "offline",
latency: s.latency != null ? Math.round(s.latency) : null,
sessions: s.sessions ?? 0,
comment: s.comment || undefined,
lanSubnet: s.lanSubnet || undefined,
wanUplinks: Array.isArray(s.wanUplinks) && s.wanUplinks.length ? s.wanUplinks : undefined,
// carry extra fields needed for expanded view
uptime: s.uptime ?? undefined,
cpuLoad: s.cpuLoad ?? undefined,
freeMemory: s.freeMemory ?? undefined,
totalMemory: s.totalMemory ?? undefined,
polledAt: s.polledAt ?? undefined,
}
}
function makeApiFetch(backendUrl: string) {
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
const headers: HeadersInit = init?.body
? { "Content-Type": "application/json" }
: {}
const res = await fetch(backendUrl.replace(/\/$/, "") + path, {
...init,
headers: { ...headers, ...(init?.headers ?? {}) },
})
if (!res.ok) {
const err = await res.json().catch(() => ({ error: res.statusText })) as { error?: string }
throw new Error(err.error ?? res.statusText)
}
// 204 No Content
if (res.status === 204) return undefined as T
return res.json() as Promise<T>
}
}
import { Flag } from "@/components/flag"
import { cn } from "@/lib/utils"
import { Card, CardContent } from "@/components/ui/card"
@@ -373,7 +323,6 @@ type SheetMode = "add" | "edit"
export default function ServersPage() {
const { mode, backendUrl, backendStatus } = useDataSource()
const isLive = mode === "live" && backendStatus === true
const apiFetch = makeApiFetch(backendUrl)
const [serverList, setServerList] = useState<Server[]>(initialServers)
const [_backendOk, setBackendOk] = useState(false)
@@ -402,10 +351,10 @@ export default function ServersPage() {
})
return
}
apiFetch<BackendServer[]>("/api/servers")
listServers(backendUrl)
.then(data => {
setBackendOk(true)
setServerList(data.map(toFrontend))
setServerList(data.map(toFrontendServer))
})
.catch(() => {
setBackendOk(false)
@@ -437,7 +386,7 @@ export default function ServersPage() {
// Fetch full server details (including credentials) from backend
if (isLive) {
try {
const full = await apiFetch<BackendServer>(`/api/servers/${s.id}`)
const full = await getServer(backendUrl, s.id)
setForm(prev => ({
...prev,
username: full.username ?? "",
@@ -458,7 +407,7 @@ export default function ServersPage() {
}
async function handleSave() {
const payload = {
const payload: ServerCreate = {
host: form.host,
port: Number(form.port) || (form.proto === "https" ? 443 : 80),
username: form.username,
@@ -479,17 +428,12 @@ export default function ServersPage() {
if (isLive) {
try {
if (sheetMode === "edit" && editingId) {
const updated = await apiFetch<BackendServer>(
`/api/servers/${editingId}`,
{ method: "PUT", body: JSON.stringify(payload) },
)
setServerList(list => list.map(s => s.id === editingId ? toFrontend(updated) : s))
const updatedPayload: ServerUpdate = payload
const updated = await updateServer(backendUrl, editingId, updatedPayload)
setServerList(list => list.map(s => s.id === editingId ? toFrontendServer(updated) : s))
} else {
const created = await apiFetch<BackendServer>(
"/api/servers",
{ method: "POST", body: JSON.stringify(payload) },
)
setServerList(list => [...list, toFrontend(created)])
const created = await createServer(backendUrl, payload)
setServerList(list => [...list, toFrontendServer(created)])
}
} catch (e) {
console.error("Ошибка сохранения:", e)
@@ -540,7 +484,7 @@ export default function ServersPage() {
async function handleDelete(id: string) {
if (isLive) {
try {
await apiFetch(`/api/servers/${id}`, { method: "DELETE" })
await deleteServer(backendUrl, id)
} catch (e) {
console.error("Ошибка удаления:", e)
}
@@ -552,10 +496,10 @@ export default function ServersPage() {
if (!isLive) return
setPollingIds(p => new Set(p).add(id))
try {
await apiFetch(`/api/servers/${id}/poll`, { method: "POST" })
await pollServer(backendUrl, id)
// Reload full list to get updated status/version
const data = await apiFetch<BackendServer[]>("/api/servers")
setServerList(data.map(toFrontend))
const data = await listServers(backendUrl)
setServerList(data.map(toFrontendServer))
} catch (e) {
console.error("Poll error:", e)
} finally {
@@ -568,9 +512,9 @@ export default function ServersPage() {
setPollAllBusy(true)
try {
const ids = serverList.map(s => s.id)
await Promise.all(ids.map(id => apiFetch(`/api/servers/${id}/poll`, { method: "POST" }).catch(() => {})))
const data = await apiFetch<BackendServer[]>("/api/servers")
setServerList(data.map(toFrontend))
await Promise.all(ids.map(id => pollServer(backendUrl, id).catch(() => {})))
const data = await listServers(backendUrl)
setServerList(data.map(toFrontendServer))
} finally {
setPollAllBusy(false)
}
@@ -582,20 +526,15 @@ export default function ServersPage() {
}
setTestState("testing"); setTestMsg("")
try {
const res = await fetch(backendUrl.replace(/\/$/, "") + "/api/servers/test-connection", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
host: form.host,
port: Number(form.port) || (form.proto === "https" ? 443 : 80),
useSsl: form.proto === "https",
verifySsl: form.verifySsl,
apiPath: form.apiPath || "/rest",
username: form.username,
password: form.password,
}),
const data = await testServerConnection(backendUrl, {
host: form.host,
port: Number(form.port) || (form.proto === "https" ? 443 : 80),
useSsl: form.proto === "https",
verifySsl: form.verifySsl,
apiPath: form.apiPath || "/rest",
username: form.username,
password: form.password,
})
const data = await res.json() as { success: boolean; message: string }
if (data.success) {
setTestState("ok"); setTestMsg(data.message)
} else {
+2 -17
View File
@@ -24,6 +24,7 @@ import {
ServerIcon, LayoutDashboardIcon, RefreshCwIcon, CableIcon, LoaderCircleIcon,
} from "lucide-react"
import { cn } from "@/lib/utils"
import { requestJson } from "@/shared/api/http-client"
// ─── types ────────────────────────────────────────────────────────────────────
@@ -152,23 +153,7 @@ type NavSection = typeof SECTIONS_NAV[number]
function makeApiFetch(backendUrl: string) {
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
const headers: HeadersInit = init?.body ? { "Content-Type": "application/json" } : {}
const res = await fetch(backendUrl.replace(/\/$/, "") + path, {
...init,
headers: { ...headers, ...(init?.headers ?? {}) },
})
if (!res.ok) {
let message = res.statusText
try {
const err = await res.json() as { error?: string }
message = err.error ?? message
} catch {
const text = await res.text().catch(() => "")
if (text) message = text
}
throw new Error(message || "Ошибка запроса")
}
return res.json() as Promise<T>
return requestJson<T>(backendUrl, path, init)
}
}
+2 -17
View File
@@ -13,6 +13,7 @@ import {
} from "lucide-react"
import { cn } from "@/lib/utils"
import { useDataSource } from "@/lib/data-source"
import { requestJson } from "@/shared/api/http-client"
// ─── helpers ──────────────────────────────────────────────────────────────────
@@ -126,23 +127,7 @@ interface LiveTrafficInterface {
function makeApiFetch(backendUrl: string) {
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
const headers: HeadersInit = init?.body ? { "Content-Type": "application/json" } : {}
const res = await fetch(backendUrl.replace(/\/$/, "") + path, {
...init,
headers: { ...headers, ...(init?.headers ?? {}) },
})
if (!res.ok) {
let message = res.statusText
try {
const err = await res.json() as { error?: string }
message = err.error ?? message
} catch {
const text = await res.text().catch(() => "")
if (text) message = text
}
throw new Error(message)
}
return res.json() as Promise<T>
return requestJson<T>(backendUrl, path, init)
}
}
+2 -17
View File
@@ -13,6 +13,7 @@ import { cn } from "@/lib/utils"
import { servers as mockServers, pingProbes as INIT_PROBES, filters, type Server, type Filter } from "@/lib/data"
import type { PingProbe } from "@/lib/data"
import { useDataSource } from "@/lib/data-source"
import { requestJson } from "@/shared/api/http-client"
import {
RefreshCwIcon, PlusIcon, SearchIcon, XIcon,
ChevronDownIcon, ChevronRightIcon,
@@ -63,23 +64,7 @@ function mockProbesWithSavedStars(base: PingProbe[]): PingProbe[] {
function makeApiFetch(backendUrl: string) {
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
const headers: HeadersInit = init?.body ? { "Content-Type": "application/json" } : {}
const res = await fetch(backendUrl.replace(/\/$/, "") + path, {
...init,
headers: { ...headers, ...(init?.headers ?? {}) },
})
if (!res.ok) {
let message = res.statusText
try {
const err = await res.json() as { error?: string }
message = err.error ?? message
} catch {
const text = await res.text().catch(() => "")
if (text) message = text
}
throw new Error(message)
}
return res.json() as Promise<T>
return requestJson<T>(backendUrl, path, init)
}
}