fix(ui): заменить таблицы на карточки данных и улучшить функциональность поиска
Docker images / prepare-release (push) Successful in 6s
Docker images / backend-image (push) Successful in 1m37s
Docker images / frontend-image (push) Successful in 1m50s
Docker images / updater-image (push) Successful in 44s
Docker images / notify-webhook (push) Has been skipped
Docker images / publish-release (push) Successful in 7s
Docker images / prepare-release (push) Successful in 6s
Docker images / backend-image (push) Successful in 1m37s
Docker images / frontend-image (push) Successful in 1m50s
Docker images / updater-image (push) Successful in 44s
Docker images / notify-webhook (push) Has been skipped
Docker images / publish-release (push) Successful in 7s
This commit is contained in:
+156
-460
@@ -28,7 +28,6 @@ import {
|
||||
import { requestJson, ApiClientError } from "@/shared/api/http-client"
|
||||
import {
|
||||
parseSchedulerRunSnapshot,
|
||||
type AlertEngineRuleDiagSnapshot,
|
||||
type AlertEngineRunSnapshot,
|
||||
type GreBgpSnapshotRunSnapshot,
|
||||
type InternetPathRunSnapshot,
|
||||
@@ -41,6 +40,19 @@ import {
|
||||
type SpeedScheduledRunSnapshot,
|
||||
type TrafficRunSnapshot,
|
||||
} from "@/lib/scheduler-run-snapshot"
|
||||
import {
|
||||
AlertEngineRuleDiagGrid,
|
||||
PingSnapshotGrid,
|
||||
ResourcesSnapshotGrid,
|
||||
ServersRestPingSnapshotGrid,
|
||||
SpeedSnapshotGrid,
|
||||
TrafficSnapshotGrid,
|
||||
} from "@/components/data-grids/snapshot-data-grid"
|
||||
import {
|
||||
DataCollectionSchedulerDataGrid,
|
||||
type SchedulerJobGridRow,
|
||||
} from "@/components/data-grids/data-collection-scheduler-data-grid"
|
||||
import { DataPageCard } from "@/components/data-page-card"
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
AlertCircleIcon,
|
||||
@@ -86,16 +98,6 @@ function fmtMs(ms: number): string {
|
||||
return s < 60 ? `${s.toFixed(1)} с` : `${Math.floor(s / 60)} м ${Math.round(s % 60)} с`
|
||||
}
|
||||
|
||||
function fmtUptimeSec(sec: number): string {
|
||||
if (sec <= 0) return "—"
|
||||
const d = Math.floor(sec / 86400)
|
||||
const h = Math.floor((sec % 86400) / 3600)
|
||||
const m = Math.floor((sec % 3600) / 60)
|
||||
if (d > 0) return `${d}д ${h}ч`
|
||||
if (h > 0) return `${h}ч ${m}м`
|
||||
return `${m}м`
|
||||
}
|
||||
|
||||
function SnapshotTables({ snap }: { snap: SchedulerRunSnapshot }) {
|
||||
if (snap.job === "traffic") {
|
||||
const t = snap as TrafficRunSnapshot
|
||||
@@ -113,44 +115,9 @@ function SnapshotTables({ snap }: { snap: SchedulerRunSnapshot }) {
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Сэмплы на момент <span className="font-mono tabular-nums">{new Date(t.sampledAt).toLocaleString("ru-RU")}</span>
|
||||
</p>
|
||||
<div className="overflow-x-auto rounded-md border border-border">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="border-b border-border bg-muted/40 text-muted-foreground text-left">
|
||||
<th className="px-3 py-2 font-medium">Сервер</th>
|
||||
<th className="px-3 py-2 font-medium">Хост</th>
|
||||
<th className="px-3 py-2 font-medium">Результат</th>
|
||||
<th className="px-3 py-2 font-medium text-right">IF</th>
|
||||
<th className="px-3 py-2 font-medium text-right">Σ RX</th>
|
||||
<th className="px-3 py-2 font-medium text-right">Σ TX</th>
|
||||
<th className="px-3 py-2 font-medium">Ошибка</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
{t.servers.map((s) => (
|
||||
<tr key={s.serverId} className="hover:bg-muted/30">
|
||||
<td className="px-3 py-2 font-medium">{s.name}</td>
|
||||
<td className="px-3 py-2 font-mono text-muted-foreground">{s.host}</td>
|
||||
<td className="px-3 py-2">
|
||||
{s.ok ? (
|
||||
<Badge variant="outline" className="text-[10px] border-emerald-500/40 text-emerald-700 dark:text-emerald-400">
|
||||
ok
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="outline" className="text-[10px] border-destructive/50 text-destructive">
|
||||
ошибка
|
||||
</Badge>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right tabular-nums">{s.interfaces ?? "—"}</td>
|
||||
<td className="px-3 py-2 text-right tabular-nums">{s.sumRxMbps != null ? `${s.sumRxMbps} Мбит/с` : "—"}</td>
|
||||
<td className="px-3 py-2 text-right tabular-nums">{s.sumTxMbps != null ? `${s.sumTxMbps} Мбит/с` : "—"}</td>
|
||||
<td className="px-3 py-2 text-destructive max-w-[220px] truncate" title={s.error}>{s.error ?? "—"}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<DataPageCard>
|
||||
<TrafficSnapshotGrid servers={t.servers} />
|
||||
</DataPageCard>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -170,59 +137,9 @@ function SnapshotTables({ snap }: { snap: SchedulerRunSnapshot }) {
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Сэмплы на <span className="font-mono tabular-nums">{new Date(u.sampledAt).toLocaleString("ru-RU")}</span>
|
||||
</p>
|
||||
<div className="overflow-x-auto rounded-md border border-border">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="border-b border-border bg-muted/40 text-muted-foreground text-left">
|
||||
<th className="px-3 py-2 font-medium">Сервер</th>
|
||||
<th className="px-3 py-2 font-medium">Статус</th>
|
||||
<th className="px-3 py-2 font-medium text-right">CPU %</th>
|
||||
<th className="px-3 py-2 font-medium text-right">Память</th>
|
||||
<th className="px-3 py-2 font-medium text-right">% RAM</th>
|
||||
<th className="px-3 py-2 font-medium text-right">Диск своб.</th>
|
||||
<th className="px-3 py-2 font-medium">Uptime</th>
|
||||
<th className="px-3 py-2 font-medium">Плата / ROS</th>
|
||||
<th className="px-3 py-2 font-medium">Ошибка</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
{u.servers.map((s) => (
|
||||
<tr key={s.serverId} className="hover:bg-muted/30">
|
||||
<td className="px-3 py-2">
|
||||
<span className="font-medium">{s.name}</span>
|
||||
<span className="block font-mono text-[10px] text-muted-foreground">{s.host}</span>
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"text-[10px]",
|
||||
s.status === "online" && "border-emerald-500/40 text-emerald-700 dark:text-emerald-400",
|
||||
s.status === "offline" && "border-destructive/50 text-destructive",
|
||||
)}
|
||||
>
|
||||
{s.status}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right tabular-nums">{s.cpuLoadPct ?? "—"}</td>
|
||||
<td className="px-3 py-2 text-right tabular-nums whitespace-nowrap">
|
||||
{s.memUsedMb != null && s.memTotalMb != null ? `${s.memUsedMb} / ${s.memTotalMb} МБ` : "—"}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right tabular-nums">{s.memUsedPct != null ? `${s.memUsedPct}%` : "—"}</td>
|
||||
<td className="px-3 py-2 text-right tabular-nums whitespace-nowrap">
|
||||
{s.diskFreeMb != null && s.diskTotalMb != null ? `${s.diskFreeMb} / ${s.diskTotalMb} МБ` : "—"}
|
||||
</td>
|
||||
<td className="px-3 py-2 tabular-nums">{s.uptimeSeconds != null ? fmtUptimeSec(s.uptimeSeconds) : "—"}</td>
|
||||
<td className="px-3 py-2 max-w-[140px]">
|
||||
<span className="block truncate" title={s.boardName}>{s.boardName || "—"}</span>
|
||||
<span className="block truncate text-muted-foreground font-mono text-[10px]" title={s.rosVersion}>{s.rosVersion || ""}</span>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-destructive max-w-[160px] truncate" title={s.error}>{s.error ?? "—"}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<DataPageCard>
|
||||
<ResourcesSnapshotGrid servers={u.servers} />
|
||||
</DataPageCard>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -243,42 +160,9 @@ function SnapshotTables({ snap }: { snap: SchedulerRunSnapshot }) {
|
||||
GET <span className="font-mono">/system/identity</span> на{" "}
|
||||
<span className="font-mono tabular-nums">{new Date(s.sampledAt).toLocaleString("ru-RU")}</span>
|
||||
</p>
|
||||
<div className="overflow-x-auto rounded-md border border-border">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="border-b border-border bg-muted/40 text-muted-foreground text-left">
|
||||
<th className="px-3 py-2 font-medium">Сервер</th>
|
||||
<th className="px-3 py-2 font-medium">Хост</th>
|
||||
<th className="px-3 py-2 font-medium">Результат</th>
|
||||
<th className="px-3 py-2 font-medium text-right">RTT REST</th>
|
||||
<th className="px-3 py-2 font-medium">Ошибка</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
{s.servers.map((row) => (
|
||||
<tr key={row.serverId} className="hover:bg-muted/30">
|
||||
<td className="px-3 py-2 font-medium">{row.name}</td>
|
||||
<td className="px-3 py-2 font-mono text-muted-foreground">{row.host}</td>
|
||||
<td className="px-3 py-2">
|
||||
{row.ok ? (
|
||||
<Badge variant="outline" className="text-[10px] border-emerald-500/40 text-emerald-700 dark:text-emerald-400">
|
||||
ok
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="outline" className="text-[10px] border-destructive/50 text-destructive">
|
||||
недоступен
|
||||
</Badge>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right tabular-nums">
|
||||
{row.latencyMs != null ? `${row.latencyMs} мс` : "—"}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-destructive max-w-[220px] truncate" title={row.error}>{row.error ?? "—"}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<DataPageCard>
|
||||
<ServersRestPingSnapshotGrid servers={s.servers} />
|
||||
</DataPageCard>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -301,41 +185,9 @@ function SnapshotTables({ snap }: { snap: SchedulerRunSnapshot }) {
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Сэмплы на <span className="font-mono tabular-nums">{new Date(p.sampledAt).toLocaleString("ru-RU")}</span> — только пробы, для которых записан замер в этом тике
|
||||
</p>
|
||||
<div className="overflow-x-auto rounded-md border border-border">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="border-b border-border bg-muted/40 text-muted-foreground text-left">
|
||||
<th className="px-3 py-2 font-medium">Проба</th>
|
||||
<th className="px-3 py-2 font-medium">Цель</th>
|
||||
<th className="px-3 py-2 font-medium">Источник</th>
|
||||
<th className="px-3 py-2 font-medium">IF</th>
|
||||
<th className="px-3 py-2 font-medium text-right">RTT</th>
|
||||
<th className="px-3 py-2 font-medium text-right">Loss</th>
|
||||
<th className="px-3 py-2 font-medium">Статус</th>
|
||||
<th className="px-3 py-2 font-medium">Ошибка</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
{p.probes.map((x) => (
|
||||
<tr key={`${x.probeId}-${x.target}`} className="hover:bg-muted/30">
|
||||
<td className="px-3 py-2">
|
||||
<span className="font-medium">{x.name}</span>
|
||||
<span className="block font-mono text-[10px] text-muted-foreground">{x.probeId}</span>
|
||||
</td>
|
||||
<td className="px-3 py-2 font-mono">{x.target}</td>
|
||||
<td className="px-3 py-2">{x.srcServerName}</td>
|
||||
<td className="px-3 py-2 font-mono text-muted-foreground">{x.srcInterface || "—"}</td>
|
||||
<td className="px-3 py-2 text-right tabular-nums">{x.rttMs != null ? `${x.rttMs} мс` : "—"}</td>
|
||||
<td className="px-3 py-2 text-right tabular-nums">{x.lossPct}%</td>
|
||||
<td className="px-3 py-2">
|
||||
<Badge variant="outline" className="text-[10px]">{x.status}</Badge>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-destructive max-w-[180px] truncate" title={x.error}>{x.error ?? "—"}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<DataPageCard>
|
||||
<PingSnapshotGrid probes={p.probes} />
|
||||
</DataPageCard>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -346,56 +198,9 @@ function SnapshotTables({ snap }: { snap: SchedulerRunSnapshot }) {
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Прогоны speed на <span className="font-mono tabular-nums">{new Date(s.sampledAt).toLocaleString("ru-RU")}</span> — по очереди для каждой включённой пробы
|
||||
</p>
|
||||
<div className="overflow-x-auto rounded-md border border-border">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="border-b border-border bg-muted/40 text-muted-foreground text-left">
|
||||
<th className="px-3 py-2 font-medium">Проба</th>
|
||||
<th className="px-3 py-2 font-medium">Маршрут</th>
|
||||
<th className="px-3 py-2 font-medium">Интерфейсы</th>
|
||||
<th className="px-3 py-2 font-medium">Протокол</th>
|
||||
<th className="px-3 py-2 font-medium text-right">TX</th>
|
||||
<th className="px-3 py-2 font-medium text-right">RX</th>
|
||||
<th className="px-3 py-2 font-medium text-right">Ping RTT</th>
|
||||
<th className="px-3 py-2 font-medium text-right">Loss</th>
|
||||
<th className="px-3 py-2 font-medium">Результат</th>
|
||||
<th className="px-3 py-2 font-medium">Ошибка</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
{s.runs.map((x) => (
|
||||
<tr key={x.probeId} className="hover:bg-muted/30">
|
||||
<td className="px-3 py-2 font-mono">{x.probeId}</td>
|
||||
<td className="px-3 py-2 whitespace-nowrap">
|
||||
{x.srcServerName} <span className="text-muted-foreground">→</span> {x.dstServerName}
|
||||
</td>
|
||||
<td className="px-3 py-2 font-mono text-[10px]">
|
||||
<span className="block">{x.srcInterface || "—"}</span>
|
||||
<span className="block text-muted-foreground">{x.dstInterface || "—"}</span>
|
||||
</td>
|
||||
<td className="px-3 py-2">{x.protocol} / {x.direction} / {x.durationSec}s</td>
|
||||
<td className="px-3 py-2 text-right tabular-nums">{x.txAvgMbps != null ? `${Number(x.txAvgMbps).toFixed(1)}` : "—"}</td>
|
||||
<td className="px-3 py-2 text-right tabular-nums">{x.rxAvgMbps != null ? `${Number(x.rxAvgMbps).toFixed(1)}` : "—"}</td>
|
||||
<td className="px-3 py-2 text-right tabular-nums">{x.pingRttMs != null ? `${x.pingRttMs} мс` : "—"}</td>
|
||||
<td className="px-3 py-2 text-right tabular-nums">{x.pingLossPct != null ? `${x.pingLossPct}%` : "—"}</td>
|
||||
<td className="px-3 py-2">
|
||||
{x.ok ? (
|
||||
<Badge variant="outline" className="text-[10px] border-emerald-500/40 text-emerald-700 dark:text-emerald-400">ok</Badge>
|
||||
) : (
|
||||
<Badge variant="outline" className="text-[10px] border-destructive/50 text-destructive">ошибка</Badge>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-3 py-2 max-w-[200px]">
|
||||
<span className="text-destructive block truncate" title={x.error}>{x.error ?? ""}</span>
|
||||
{x.pingError ? (
|
||||
<span className="text-[10px] text-amber-600 dark:text-amber-400 block truncate" title={x.pingError ?? ""}>ping: {x.pingError}</span>
|
||||
) : null}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<DataPageCard>
|
||||
<SpeedSnapshotGrid runs={s.runs} />
|
||||
</DataPageCard>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -535,36 +340,6 @@ function SnapshotTables({ snap }: { snap: SchedulerRunSnapshot }) {
|
||||
}
|
||||
if (snap.job === "alert_engine") {
|
||||
const a = snap as AlertEngineRunSnapshot
|
||||
const transitionRu = (t: AlertEngineRuleDiagSnapshot["hitTransition"]) => {
|
||||
switch (t) {
|
||||
case "problem":
|
||||
return "проблема"
|
||||
case "recovery":
|
||||
return "восстановление"
|
||||
case "neutral":
|
||||
return "нейтрально"
|
||||
default:
|
||||
return "—"
|
||||
}
|
||||
}
|
||||
const blockedRu = (b: AlertEngineRuleDiagSnapshot["blocked"]) => {
|
||||
switch (b) {
|
||||
case "no_hit":
|
||||
return "условие не выполнено"
|
||||
case "stability":
|
||||
return "стабильность (confirmStabilitySec)"
|
||||
case "cooldown":
|
||||
return "cooldown"
|
||||
case "no_telegram":
|
||||
return "нет Telegram"
|
||||
case "dedupe_positive":
|
||||
return "дедуп восстановления"
|
||||
case "in_group":
|
||||
return "в группе (отдельно не шлём)"
|
||||
default:
|
||||
return "—"
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
@@ -605,40 +380,9 @@ function SnapshotTables({ snap }: { snap: SchedulerRunSnapshot }) {
|
||||
{a.ruleDiag && a.ruleDiag.length > 0 ? (
|
||||
<div className="rounded-md border border-border bg-muted/20 px-3 py-2 space-y-2">
|
||||
<p className="text-[11px] font-medium text-muted-foreground">По правилам (почему не ушло в Telegram)</p>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-[11px] border-collapse">
|
||||
<thead>
|
||||
<tr className="text-left text-muted-foreground border-b border-border">
|
||||
<th className="py-1 pr-2 font-medium">ID правила</th>
|
||||
<th className="py-1 pr-2 font-medium">Сработало</th>
|
||||
<th className="py-1 pr-2 font-medium">Тип срабатывания</th>
|
||||
<th className="py-1 pr-2 font-medium">Стабильность</th>
|
||||
<th className="py-1 pr-2 font-medium">Кулдаун</th>
|
||||
<th className="py-1 pr-2 font-medium">Telegram</th>
|
||||
<th className="py-1 pr-2 font-medium">Сообщение</th>
|
||||
<th className="py-1 font-medium">Причина блока</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{a.ruleDiag.map((d) => (
|
||||
<tr key={d.ruleId} className="border-b border-border/60 font-mono">
|
||||
<td className="py-1 pr-2 max-w-[140px] truncate" title={d.ruleId}>
|
||||
{d.ruleId}
|
||||
</td>
|
||||
<td className="py-1 pr-2">{d.evalHit ? "Да" : "Нет"}</td>
|
||||
<td className="py-1 pr-2">{transitionRu(d.hitTransition)}</td>
|
||||
<td className="py-1 pr-2">{d.stabilityOk ? "Да" : "Нет"}</td>
|
||||
<td className="py-1 pr-2">{d.cooldownOk ? "Да" : "Нет"}</td>
|
||||
<td className="py-1 pr-2">{d.telegramOk ? "Да" : "Нет"}</td>
|
||||
<td className="py-1 pr-2 max-w-[280px] truncate text-muted-foreground" title={d.hitMessage ?? ""}>
|
||||
{d.hitMessage ?? "—"}
|
||||
</td>
|
||||
<td className="py-1 text-muted-foreground">{blockedRu(d.blocked)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<DataPageCard className="border-0 shadow-none bg-transparent">
|
||||
<AlertEngineRuleDiagGrid ruleDiag={a.ruleDiag} />
|
||||
</DataPageCard>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -1057,6 +801,130 @@ export default function DataCollectionPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const schedulerGridRows = useMemo<SchedulerJobGridRow[]>(() => {
|
||||
return SCHEDULER_JOB_KEYS.map((jobKey) => {
|
||||
const j = schedulerJobsByKey[jobKey]
|
||||
const fixedSchedule = jobKey === "gre_bgp" || jobKey === "alert_engine" || jobKey === "backups"
|
||||
const enabled = fixedSchedule
|
||||
? Boolean(j?.enabled ?? true)
|
||||
: jobKey === "traffic"
|
||||
? draftTrafficEnabled
|
||||
: jobKey === "servers_rest_ping"
|
||||
? draftServersApiEnabled
|
||||
: jobKey === "uptime_resources"
|
||||
? draftResourcesEnabled
|
||||
: jobKey === "uptime_ping"
|
||||
? draftPingEnabled
|
||||
: jobKey === "uptime_speed"
|
||||
? draftSpeedEnabled
|
||||
: jobKey === "certificates_renew"
|
||||
? draftCertRenewEnabled
|
||||
: draftInternetPathEnabled
|
||||
const intervalValue = fixedSchedule
|
||||
? String(j?.intervalSec ?? (jobKey === "gre_bgp" ? 30 : 20))
|
||||
: jobKey === "traffic"
|
||||
? trafficIntervalDraft
|
||||
: jobKey === "servers_rest_ping"
|
||||
? serversApiIntervalDraft
|
||||
: jobKey === "uptime_resources"
|
||||
? uptimeResourceIntervalDraft
|
||||
: jobKey === "uptime_ping"
|
||||
? uptimeIntervalDraft
|
||||
: jobKey === "uptime_speed"
|
||||
? uptimeSpeedIntervalDraft
|
||||
: jobKey === "certificates_renew"
|
||||
? certRenewIntervalDraft
|
||||
: internetPathIntervalDraft
|
||||
const onIntervalChange = fixedSchedule
|
||||
? () => {}
|
||||
: jobKey === "traffic"
|
||||
? setTrafficIntervalDraft
|
||||
: jobKey === "servers_rest_ping"
|
||||
? setServersApiIntervalDraft
|
||||
: jobKey === "uptime_resources"
|
||||
? setUptimeResourceIntervalDraft
|
||||
: jobKey === "uptime_ping"
|
||||
? setUptimeIntervalDraft
|
||||
: jobKey === "uptime_speed"
|
||||
? setUptimeSpeedIntervalDraft
|
||||
: jobKey === "certificates_renew"
|
||||
? setCertRenewIntervalDraft
|
||||
: setInternetPathIntervalDraft
|
||||
const defaultInterval = fixedSchedule
|
||||
? Number(j?.intervalSec ?? (jobKey === "gre_bgp" ? 30 : 20))
|
||||
: jobKey === "traffic"
|
||||
? 30
|
||||
: jobKey === "servers_rest_ping"
|
||||
? 120
|
||||
: jobKey === "uptime_resources"
|
||||
? 300
|
||||
: jobKey === "uptime_ping"
|
||||
? 15
|
||||
: jobKey === "uptime_speed"
|
||||
? 60
|
||||
: jobKey === "certificates_renew"
|
||||
? 21600
|
||||
: 300
|
||||
|
||||
return {
|
||||
id: jobKey,
|
||||
jobKey,
|
||||
label: SCHEDULER_JOB_LABELS[jobKey] ?? jobKey,
|
||||
description: SCHEDULER_JOB_DESCRIPTIONS[jobKey],
|
||||
fixedSchedule,
|
||||
enabled,
|
||||
intervalValue,
|
||||
intervalReadOnly: fixedSchedule,
|
||||
intervalDisabled: !enabled && !fixedSchedule,
|
||||
defaultInterval,
|
||||
job: j,
|
||||
onEnabledChange: fixedSchedule
|
||||
? undefined
|
||||
: (nextEnabled) => {
|
||||
void handleJobEnabledChange(jobKey, nextEnabled)
|
||||
},
|
||||
onIntervalChange,
|
||||
onRunNow: async () => {
|
||||
setRunNowJobKey(jobKey)
|
||||
setCollectorError(null)
|
||||
try {
|
||||
await apiFetch(`/api/scheduler/jobs/${encodeURIComponent(jobKey)}/run-now`, {
|
||||
method: "POST",
|
||||
})
|
||||
await loadCollectors()
|
||||
} catch (e) {
|
||||
setCollectorError(e instanceof Error ? e.message : "Ошибка запуска")
|
||||
} finally {
|
||||
setRunNowJobKey(null)
|
||||
}
|
||||
},
|
||||
runNowLoading: runNowJobKey === jobKey,
|
||||
saveBusy: schedulerSaveBusy,
|
||||
}
|
||||
})
|
||||
}, [
|
||||
apiFetch,
|
||||
certRenewIntervalDraft,
|
||||
draftCertRenewEnabled,
|
||||
draftInternetPathEnabled,
|
||||
draftPingEnabled,
|
||||
draftResourcesEnabled,
|
||||
draftServersApiEnabled,
|
||||
draftSpeedEnabled,
|
||||
draftTrafficEnabled,
|
||||
handleJobEnabledChange,
|
||||
internetPathIntervalDraft,
|
||||
loadCollectors,
|
||||
runNowJobKey,
|
||||
schedulerJobsByKey,
|
||||
schedulerSaveBusy,
|
||||
serversApiIntervalDraft,
|
||||
trafficIntervalDraft,
|
||||
uptimeIntervalDraft,
|
||||
uptimeResourceIntervalDraft,
|
||||
uptimeSpeedIntervalDraft,
|
||||
])
|
||||
|
||||
const enabledJobsCount = useMemo(() => {
|
||||
const jobs = uptimeCollector?.scheduler?.jobs
|
||||
if (jobs?.length) return jobs.filter((job) => job.enabled).length
|
||||
@@ -1223,179 +1091,7 @@ export default function DataCollectionPage() {
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="px-0 pb-0">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-border text-xs text-muted-foreground">
|
||||
<th className="text-left font-medium px-5 py-3">Задача</th>
|
||||
<th className="text-center font-medium px-3 py-3 w-[1%]">Вкл</th>
|
||||
<th className="text-left font-medium px-4 py-3">Интервал (с)</th>
|
||||
<th className="text-left font-medium px-4 py-3">Последний прогон</th>
|
||||
<th className="text-left font-medium px-4 py-3">Статус</th>
|
||||
<th className="text-right font-medium px-4 py-3 w-[1%]">Сейчас</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
{SCHEDULER_JOB_KEYS.map((jobKey) => {
|
||||
const j = schedulerJobsByKey[jobKey]
|
||||
const fixedSchedule = jobKey === "gre_bgp" || jobKey === "alert_engine" || jobKey === "backups"
|
||||
const en = fixedSchedule
|
||||
? Boolean(j?.enabled ?? true)
|
||||
: jobKey === "traffic"
|
||||
? draftTrafficEnabled
|
||||
: jobKey === "servers_rest_ping"
|
||||
? draftServersApiEnabled
|
||||
: jobKey === "uptime_resources"
|
||||
? draftResourcesEnabled
|
||||
: jobKey === "uptime_ping"
|
||||
? draftPingEnabled
|
||||
: jobKey === "uptime_speed"
|
||||
? draftSpeedEnabled
|
||||
: jobKey === "certificates_renew"
|
||||
? draftCertRenewEnabled
|
||||
: draftInternetPathEnabled
|
||||
const iv = fixedSchedule
|
||||
? String(j?.intervalSec ?? (jobKey === "gre_bgp" ? 30 : 20))
|
||||
: jobKey === "traffic"
|
||||
? trafficIntervalDraft
|
||||
: jobKey === "servers_rest_ping"
|
||||
? serversApiIntervalDraft
|
||||
: jobKey === "uptime_resources"
|
||||
? uptimeResourceIntervalDraft
|
||||
: jobKey === "uptime_ping"
|
||||
? uptimeIntervalDraft
|
||||
: jobKey === "uptime_speed"
|
||||
? uptimeSpeedIntervalDraft
|
||||
: jobKey === "certificates_renew"
|
||||
? certRenewIntervalDraft
|
||||
: internetPathIntervalDraft
|
||||
const setIv = fixedSchedule
|
||||
? () => {}
|
||||
: jobKey === "traffic"
|
||||
? setTrafficIntervalDraft
|
||||
: jobKey === "servers_rest_ping"
|
||||
? setServersApiIntervalDraft
|
||||
: jobKey === "uptime_resources"
|
||||
? setUptimeResourceIntervalDraft
|
||||
: jobKey === "uptime_ping"
|
||||
? setUptimeIntervalDraft
|
||||
: jobKey === "uptime_speed"
|
||||
? setUptimeSpeedIntervalDraft
|
||||
: jobKey === "certificates_renew"
|
||||
? setCertRenewIntervalDraft
|
||||
: setInternetPathIntervalDraft
|
||||
const defSec = fixedSchedule
|
||||
? Number(j?.intervalSec ?? (jobKey === "gre_bgp" ? 30 : 20))
|
||||
: jobKey === "traffic"
|
||||
? 30
|
||||
: jobKey === "servers_rest_ping"
|
||||
? 120
|
||||
: jobKey === "uptime_resources"
|
||||
? 300
|
||||
: jobKey === "uptime_ping"
|
||||
? 15
|
||||
: jobKey === "uptime_speed"
|
||||
? 60
|
||||
: jobKey === "certificates_renew"
|
||||
? 21600
|
||||
: 300
|
||||
return (
|
||||
<tr key={jobKey} className="hover:bg-muted/40">
|
||||
<td className="px-5 py-3 align-top">
|
||||
<span className="font-medium">{SCHEDULER_JOB_LABELS[jobKey] ?? jobKey}</span>
|
||||
<p className="text-[11px] text-muted-foreground mt-0.5 leading-snug">
|
||||
{SCHEDULER_JOB_DESCRIPTIONS[jobKey]}
|
||||
</p>
|
||||
<p className="text-[11px] text-muted-foreground font-mono mt-1">{jobKey}</p>
|
||||
</td>
|
||||
<td className="px-3 py-3 text-center align-top">
|
||||
<span className={fixedSchedule ? "inline-flex pointer-events-none opacity-50" : "inline-flex"}>
|
||||
<FormToggle
|
||||
checked={en}
|
||||
disabled={fixedSchedule || schedulerSaveBusy}
|
||||
onChange={(v) => {
|
||||
if (fixedSchedule || schedulerSaveBusy) return
|
||||
void handleJobEnabledChange(jobKey, v)
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 w-28 align-top">
|
||||
<Input
|
||||
value={iv}
|
||||
onChange={(e) => setIv(e.target.value)}
|
||||
className="h-8 text-sm tabular-nums"
|
||||
inputMode="numeric"
|
||||
readOnly={fixedSchedule}
|
||||
disabled={!en && !fixedSchedule}
|
||||
placeholder={String(defSec)}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-xs text-muted-foreground align-top">
|
||||
{j?.lastFinishedAt ? new Date(j.lastFinishedAt).toLocaleString("ru-RU") : "—"}
|
||||
{j?.lastDurationMs != null && (
|
||||
<span className="block text-[11px]">{j.lastDurationMs} мс</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 align-top">
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
{j?.running ? (
|
||||
<Badge variant="secondary" className="text-[10px]">
|
||||
выполняется
|
||||
</Badge>
|
||||
) : null}
|
||||
{j?.lastStatus ? (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"text-[10px]",
|
||||
j.lastStatus === "ok" && "border-emerald-500/40 text-emerald-700 dark:text-emerald-400",
|
||||
j.lastStatus === "error" && "border-destructive/50 text-destructive",
|
||||
)}
|
||||
>
|
||||
{j.lastStatus}
|
||||
</Badge>
|
||||
) : null}
|
||||
{j?.lastError ? (
|
||||
<span
|
||||
className="text-[10px] text-destructive max-w-[200px] truncate block"
|
||||
title={j.lastError}
|
||||
>
|
||||
{j.lastError}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right align-top">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-8"
|
||||
disabled={j?.running || runNowJobKey !== null}
|
||||
onClick={async () => {
|
||||
setRunNowJobKey(jobKey)
|
||||
setCollectorError(null)
|
||||
try {
|
||||
await apiFetch(`/api/scheduler/jobs/${encodeURIComponent(jobKey)}/run-now`, {
|
||||
method: "POST",
|
||||
})
|
||||
await loadCollectors()
|
||||
} catch (e) {
|
||||
setCollectorError(e instanceof Error ? e.message : "Ошибка запуска")
|
||||
} finally {
|
||||
setRunNowJobKey(null)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<RefreshCwIcon className={cn("size-3.5", runNowJobKey === jobKey && "animate-spin")} />
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<DataCollectionSchedulerDataGrid rows={schedulerGridRows} />
|
||||
<Separator />
|
||||
<div className="space-y-3 px-5 py-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
|
||||
Reference in New Issue
Block a user