Init 2
This commit is contained in:
+3
-141
@@ -116,16 +116,6 @@ interface LiveTrafficServer {
|
||||
txSeries: number[]
|
||||
}
|
||||
|
||||
interface TrafficSettingsDto {
|
||||
enabled: boolean
|
||||
intervalSec: number
|
||||
retentionDays: number
|
||||
lastCollectedAt: string | null
|
||||
lastDurationMs: number | null
|
||||
lastError: string | null
|
||||
collectorRunning: boolean
|
||||
}
|
||||
|
||||
interface LiveTrafficInterface {
|
||||
name: string
|
||||
running: boolean
|
||||
@@ -787,10 +777,6 @@ export default function TrafficPage() {
|
||||
const [liveServers, setLiveServers] = useState<ServerTraffic[]>([])
|
||||
const [liveBusy, setLiveBusy] = useState(false)
|
||||
const [liveError, setLiveError] = useState<string | null>(null)
|
||||
const [settingsBusy, setSettingsBusy] = useState(false)
|
||||
const [trafficSettings, setTrafficSettings] = useState<TrafficSettingsDto | null>(null)
|
||||
const [intervalDraft, setIntervalDraft] = useState("30")
|
||||
const [retentionDraft, setRetentionDraft] = useState("14")
|
||||
const [serverIfaces, setServerIfaces] = useState<LiveTrafficInterface[]>([])
|
||||
const [selectedIface, setSelectedIface] = useState("__all__")
|
||||
const [hideDisabledIfaces, setHideDisabledIfaces] = useState(true)
|
||||
@@ -834,30 +820,16 @@ export default function TrafficPage() {
|
||||
}
|
||||
}, [apiFetch, isLive])
|
||||
|
||||
const loadTrafficSettings = useCallback(async () => {
|
||||
if (!isLive) return
|
||||
try {
|
||||
const s = await apiFetch<TrafficSettingsDto>("/api/traffic/settings")
|
||||
setTrafficSettings(s)
|
||||
setIntervalDraft(String(s.intervalSec))
|
||||
setRetentionDraft(String(s.retentionDays))
|
||||
} catch (e) {
|
||||
setLiveError(e instanceof Error ? e.message : "Не удалось загрузить настройки сбора")
|
||||
}
|
||||
}, [apiFetch, isLive])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLive) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setLiveServers([])
|
||||
setLiveError(null)
|
||||
setTrafficSettings(null)
|
||||
return
|
||||
}
|
||||
if (groupMode !== "servers") setGroupMode("servers")
|
||||
void loadLiveTraffic(range)
|
||||
void loadTrafficSettings()
|
||||
}, [isLive, groupMode, range, loadLiveTraffic, loadTrafficSettings])
|
||||
}, [isLive, groupMode, range, loadLiveTraffic])
|
||||
|
||||
const activeServerTraffic = isLive ? liveServers : serverTraffic
|
||||
const visibleIfaces = useMemo(
|
||||
@@ -974,28 +946,6 @@ export default function TrafficPage() {
|
||||
>
|
||||
<RefreshCwIcon className={cn("size-4", isLive && liveBusy && "animate-spin")} />Обновить
|
||||
</Button>
|
||||
{isLive && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
setSettingsBusy(true)
|
||||
setLiveError(null)
|
||||
try {
|
||||
await apiFetch("/api/traffic/collect-now", { method: "POST" })
|
||||
await loadLiveTraffic(range)
|
||||
await loadTrafficSettings()
|
||||
} catch (e) {
|
||||
setLiveError(e instanceof Error ? e.message : "Не удалось выполнить сбор")
|
||||
} finally {
|
||||
setSettingsBusy(false)
|
||||
}
|
||||
}}
|
||||
disabled={settingsBusy}
|
||||
>
|
||||
<ActivityIcon className={cn("size-4", settingsBusy && "animate-pulse")} />Собрать сейчас
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="outline" size="sm"><DownloadIcon className="size-4" />Экспорт</Button>
|
||||
</>
|
||||
}
|
||||
@@ -1020,94 +970,6 @@ export default function TrafficPage() {
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
<div className="flex flex-col gap-5">
|
||||
{isLive && trafficSettings && (
|
||||
<Card>
|
||||
<CardContent className="pt-4 pb-4 px-4">
|
||||
<div className="flex flex-wrap items-end gap-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-[11px] text-muted-foreground">Сбор статистики</span>
|
||||
<div className="flex rounded-md border border-input overflow-hidden h-8">
|
||||
<button
|
||||
className={cn("px-3 text-xs", trafficSettings.enabled ? "bg-emerald-600 text-white" : "text-muted-foreground hover:bg-muted")}
|
||||
onClick={async () => {
|
||||
setSettingsBusy(true)
|
||||
try {
|
||||
await apiFetch("/api/traffic/settings", { method: "PUT", body: JSON.stringify({ enabled: true }) })
|
||||
await loadTrafficSettings()
|
||||
} finally { setSettingsBusy(false) }
|
||||
}}
|
||||
disabled={settingsBusy}
|
||||
>Вкл</button>
|
||||
<button
|
||||
className={cn("px-3 text-xs border-l border-input", !trafficSettings.enabled ? "bg-muted-foreground text-white" : "text-muted-foreground hover:bg-muted")}
|
||||
onClick={async () => {
|
||||
setSettingsBusy(true)
|
||||
try {
|
||||
await apiFetch("/api/traffic/settings", { method: "PUT", body: JSON.stringify({ enabled: false }) })
|
||||
await loadTrafficSettings()
|
||||
} finally { setSettingsBusy(false) }
|
||||
}}
|
||||
disabled={settingsBusy}
|
||||
>Выкл</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-[11px] text-muted-foreground">Интервал (сек)</span>
|
||||
<input
|
||||
value={intervalDraft}
|
||||
onChange={(e) => setIntervalDraft(e.target.value)}
|
||||
className="h-8 w-24 text-xs bg-muted/50 border border-border rounded-md px-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-[11px] text-muted-foreground">Хранение (дней)</span>
|
||||
<input
|
||||
value={retentionDraft}
|
||||
onChange={(e) => setRetentionDraft(e.target.value)}
|
||||
className="h-8 w-24 text-xs bg-muted/50 border border-border rounded-md px-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-8"
|
||||
disabled={settingsBusy}
|
||||
onClick={async () => {
|
||||
setSettingsBusy(true)
|
||||
try {
|
||||
await apiFetch("/api/traffic/settings", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
intervalSec: Number.parseInt(intervalDraft, 10) || 30,
|
||||
retentionDays: Number.parseInt(retentionDraft, 10) || 14,
|
||||
}),
|
||||
})
|
||||
await loadTrafficSettings()
|
||||
} catch (e) {
|
||||
setLiveError(e instanceof Error ? e.message : "Не удалось сохранить настройки")
|
||||
} finally {
|
||||
setSettingsBusy(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
Сохранить сбор
|
||||
</Button>
|
||||
|
||||
<div className="ml-auto text-xs text-muted-foreground">
|
||||
<p>Последний сбор: {trafficSettings.lastCollectedAt ? new Date(trafficSettings.lastCollectedAt).toLocaleString("ru-RU") : "—"}</p>
|
||||
<p>Длительность: {trafficSettings.lastDurationMs != null ? `${trafficSettings.lastDurationMs} мс` : "—"}</p>
|
||||
<p className={cn(trafficSettings.lastError ? "text-destructive" : "")}>
|
||||
{trafficSettings.lastError ? `Ошибка: ${trafficSettings.lastError}` : "Ошибок нет"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* ── summary stat cards ── */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||||
{[
|
||||
@@ -1206,11 +1068,11 @@ export default function TrafficPage() {
|
||||
>
|
||||
Все
|
||||
</button>
|
||||
{visibleIfaces.map((iface) => {
|
||||
{visibleIfaces.map((iface, index) => {
|
||||
const active = selectedIface === iface.name
|
||||
return (
|
||||
<button
|
||||
key={iface.name}
|
||||
key={`${iface.name}:${index}`}
|
||||
onClick={() => setSelectedIface(iface.name)}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-[10px] font-medium transition-all",
|
||||
|
||||
Reference in New Issue
Block a user