Files
Denozordec fe32c9313a
Docker images / prepare-release (push) Successful in 9s
Docker images / backend-image (push) Successful in 1m43s
Docker images / frontend-image (push) Successful in 2m58s
Docker images / notify-webhook (push) Skipped
Docker images / updater-image (push) Successful in 39s
Docker images / publish-release (push) Successful in 8s
feat(ui): integrate KpiStatGrid for enhanced statistics display
Replaced existing KPI display implementations across multiple pages with the new KpiStatGrid component for a more consistent and visually appealing presentation of statistics. Updated the Backups, BGP, Certificates, Communities, Containers, Dashboard, and Data Collection pages to utilize the KpiStatGrid, improving the overall user experience and maintainability of the codebase. Additionally, added new dependencies in package.json for required libraries.
2026-09-06 17:58:05 +07:00

94 lines
2.8 KiB
TypeScript

"use client"
import { useEffect, useState } from "react"
import { resolveApiUrl, withAuthHeaders } from "@/shared/api/http-client"
export interface TrafficLiveSample {
rxMbps: number
txMbps: number
at: string
}
function parseSseBlock(block: string): { event: string; data: string } {
let event = "message"
const dataLines: string[] = []
for (const line of block.split("\n")) {
if (line.startsWith("event:")) event = line.slice(6).trim()
else if (line.startsWith("data:")) dataLines.push(line.slice(5).trim())
}
return { event, data: dataLines.join("\n") }
}
export function useTrafficLive(opts: {
enabled: boolean
backendUrl: string
serverId: string
iface: string
}): { sample: TrafficLiveSample | null; error: string | null } {
const [sample, setSample] = useState<TrafficLiveSample | null>(null)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
if (!opts.enabled || !opts.serverId) {
setSample(null)
setError(null)
return
}
const ac = new AbortController()
setSample(null)
setError(null)
const ifaceQ =
opts.iface && opts.iface !== "__all__"
? `?iface=${encodeURIComponent(opts.iface)}`
: ""
const path = `/api/traffic/servers/${encodeURIComponent(opts.serverId)}/live${ifaceQ}`
const url = resolveApiUrl(opts.backendUrl, path)
let buf = ""
setError(null)
void (async () => {
try {
const res = await fetch(url, {
headers: withAuthHeaders({ Accept: "text/event-stream" }),
signal: ac.signal,
credentials: "include",
})
if (!res.ok || !res.body) {
setError(`live HTTP ${res.status}`)
return
}
const reader = res.body.getReader()
const decoder = new TextDecoder()
while (!ac.signal.aborted) {
const { done, value } = await reader.read()
if (done) break
buf += decoder.decode(value, { stream: true })
const parts = buf.split("\n\n")
buf = parts.pop() ?? ""
for (const raw of parts) {
if (!raw.trim() || raw.trim().startsWith(":")) continue
const ev = parseSseBlock(raw)
if (ev.event === "sample" && ev.data) {
const parsed = JSON.parse(ev.data) as TrafficLiveSample
setSample(parsed)
setError(null)
} else if (ev.event === "error" && ev.data) {
const parsed = JSON.parse(ev.data) as { error?: string }
setError(parsed.error ?? "live error")
}
}
}
} catch (e) {
if (ac.signal.aborted) return
setError(e instanceof Error ? e.message : "live error")
}
})()
return () => ac.abort()
}, [opts.enabled, opts.backendUrl, opts.serverId, opts.iface])
return { sample, error }
}