Docker images / prepare-release (push) Successful in 9s
Docker images / backend-test (push) Successful in 4m35s
Docker images / frontend-image (push) Successful in 3m29s
Docker images / updater-image (push) Successful in 41s
Docker images / netflow-agent-image (push) Successful in 34s
Docker images / backend-image (push) Successful in 1m44s
Docker images / publish-release (push) Successful in 10s
Docker images / notify-webhook (push) Successful in 2s
55 lines
6.4 KiB
TypeScript
55 lines
6.4 KiB
TypeScript
"use client"
|
||
import { useEffect, useRef, useState } from "react"
|
||
import Link from "next/link"
|
||
import { type LogSettings, type LogSummary } from "@mmapp/contracts"
|
||
import { Frame, FrameHeader, FrameTitle, FrameDescription, FramePanel } from "@/components/reui/frame"
|
||
import { Alert, AlertTitle, AlertDescription } from "@/components/reui/alert"
|
||
import { Badge } from "@/components/reui/badge"
|
||
import { Input } from "@/components/ui/input"
|
||
import { Button } from "@/components/ui/button"
|
||
import { Switch } from "@/components/ui/switch"
|
||
import { Skeleton } from "@/components/ui/skeleton"
|
||
import { getLogSettings, getLogSummary, putLogSettings } from "@/shared/api/logs"
|
||
import { can, ensureAuthConfig } from "@/lib/auth"
|
||
import { formatLogTime } from "./log-presentation"
|
||
|
||
export function LogsCollectorSettings({ base, onSaved }: { base: string; onSaved?: (settings: LogSettings) => void }) {
|
||
const [settings, setSettings] = useState<LogSettings | null>(null), [summary, setSummary] = useState<LogSummary | null>(null)
|
||
const [interval, setIntervalDraft] = useState("10"), [retention, setRetention] = useState("14"), [enabled, setEnabled] = useState(false)
|
||
const [error, setError] = useState<string | null>(null), [saving, setSaving] = useState(false), [revision, setRevision] = useState(0), [notice, setNotice] = useState("")
|
||
const lifetime = useRef<AbortController | null>(null), busy = useRef(false)
|
||
useEffect(() => {
|
||
const ac = new AbortController(); lifetime.current = ac
|
||
void ensureAuthConfig().then(async () => {
|
||
if (!can("mm:settings:admin")) throw new Error("Настройки логов доступны с mm:settings:admin")
|
||
const data = await getLogSettings(base, ac.signal)
|
||
if (ac.signal.aborted) return
|
||
setSettings(data); setEnabled(data.enabled); setIntervalDraft(String(data.intervalSec)); setRetention(String(data.retentionDays)); setError(null)
|
||
const stats = await getLogSummary(base, new URLSearchParams({ from: new Date(Date.now() - 900000).toISOString() }), ac.signal)
|
||
if (!ac.signal.aborted) setSummary(stats)
|
||
}).catch(e => { if (!ac.signal.aborted) setError(e instanceof Error ? e.message : String(e)) })
|
||
return () => ac.abort()
|
||
}, [base, revision])
|
||
const save = async () => {
|
||
if (busy.current || !settings || !can("mm:settings:admin")) return
|
||
if (!/^\d+$/.test(interval) || !/^\d+$/.test(retention)) { setError("Укажите целые значения интервала и срока хранения"); return }
|
||
if (Number(interval) < 5 || Number(interval) > 3600 || Number(retention) < 1 || Number(retention) > 365) { setError("Интервал должен быть от 5 до 3600 секунд, срок хранения — от 1 до 365 дней"); return }
|
||
busy.current = true; setSaving(true); setNotice("")
|
||
const signal = lifetime.current?.signal
|
||
try {
|
||
const confirmed = await putLogSettings(base, { enabled, intervalSec: Number(interval), retentionDays: Number(retention) }, signal)
|
||
if (signal?.aborted) return
|
||
setSettings(confirmed); setError(null); setNotice("Настройки сборщика логов сохранены"); onSaved?.(confirmed)
|
||
} catch (e) { if (!signal?.aborted) setError(e instanceof Error ? e.message : String(e)) }
|
||
finally { busy.current = false; if (!signal?.aborted) setSaving(false) }
|
||
}
|
||
return <Frame id="router-logs" dense><FrameHeader><FrameTitle>Логи MikroTik</FrameTitle><FrameDescription>REST-сбор буфера memory, сохранение в PostgreSQL. При открытом live опрос выбранного устройства ускоряется до 2 секунд.</FrameDescription></FrameHeader><FramePanel className="flex flex-col gap-4">
|
||
{error && <Alert variant="destructive"><AlertTitle>Данные сборщика не подтверждены</AlertTitle><AlertDescription>{error}<Button size="sm" variant="outline" disabled={saving} onClick={() => setRevision(n => n + 1)}>Повторить загрузку</Button></AlertDescription></Alert>}
|
||
{!settings && !error && <Skeleton className="h-24 w-full" />}
|
||
<div className="flex flex-wrap items-end gap-4"><label className="flex items-center gap-2 text-sm"><Switch checked={enabled} onCheckedChange={setEnabled} disabled={!settings || saving} aria-label="Включить сбор логов" />Собирать логи</label><label className="flex flex-col gap-1 text-xs">Фоновый интервал, секунд<Input aria-label="Интервал сбора логов" className="w-40" inputMode="numeric" value={interval} onChange={e => setIntervalDraft(e.target.value)} disabled={!settings || saving} /></label><label className="flex flex-col gap-1 text-xs">Хранить, дней<Input aria-label="Срок хранения логов" className="w-40" inputMode="numeric" value={retention} onChange={e => setRetention(e.target.value)} disabled={!settings || saving} /></label><Button size="sm" disabled={!settings || saving} onClick={() => void save()}>{saving ? "Сохранение…" : "Сохранить настройки логов"}</Button></div>
|
||
{notice && <p role="status" className="text-sm">{notice}</p>}
|
||
<p className="text-xs text-muted-foreground">Интервал: 5–3600 секунд, хранение: 1–365 дней. Буфер MikroTik может перезаписаться между опросами. Новые правила логирования на устройствах не создаются.</p>
|
||
{summary?.states.length ? <div className="flex flex-col gap-2">{summary.states.map(state => <div key={state.serverId} className="flex flex-wrap items-center gap-2 border-t py-2 text-xs"><Link href={`/logs?server=${state.serverId}`} className="font-medium underline">{state.serverName}</Link><Badge size="sm" variant={state.lastError ? "warning-light" : state.lastCollectedAt ? "success-light" : "secondary"}>{state.lastError ? "Ошибка" : state.lastCollectedAt ? "Получено" : "Ожидание"}</Badge><span className="text-muted-foreground">{formatLogTime(state.lastCollectedAt)}</span>{state.gapAt && <span>Возможный разрыв: {formatLogTime(state.gapAt)}</span>}{state.lastError && <span role="status" className="break-all">{state.lastError}</span>}</div>)}</div> : <p className="text-xs text-muted-foreground">Результатов сбора по устройствам пока нет.</p>}
|
||
</FramePanel></Frame>
|
||
}
|