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

This commit is contained in:
Denozordec
2026-06-30 22:22:51 +07:00
parent a1a9124f3d
commit d3a2d38b37
63 changed files with 8509 additions and 3652 deletions
+28 -123
View File
@@ -4,6 +4,16 @@ import { useEffect, useRef, useState, useMemo, useCallback } from "react"
import { PageHeader } from "@/components/page-header"
import { FormToggle } from "@/components/form-kit"
import { Card, CardContent } from "@/components/ui/card"
import { DataPageCard } from "@/components/data-page-card"
import {
ProbesScheduleDataGrid,
type SchedRule,
type SchedType,
} from "@/components/data-grids/probes-schedule-data-grid"
import {
ProbesSpeedProbesDataGrid,
type SpeedProbeApiRow,
} from "@/components/data-grids/probes-speed-probes-data-grid"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { servers, greTunnels, type GreTunnel, type Server } from "@/lib/data"
@@ -41,13 +51,8 @@ interface DiagTest {
source?: "demo" | "live"
}
type SchedType = "ping" | "bandwidth" | "both"
// SchedRule imported from probes-schedule-data-grid
interface SchedRule {
id: string; srcId: string; tunnelId: string; type: SchedType
intervalMin: number; enabled: boolean
lastRun: string | null; nextRunMin: number | null
}
// ─── tool metadata ────────────────────────────────────────────────────────────
@@ -98,22 +103,7 @@ interface BackendServerRow {
latency?: number | null
}
interface SpeedProbeApiRow {
id: string
srcServerId: string
dstServerId: string
srcInterface: string
dstInterface: string
protocol: string
direction: string
durationSec: string
enabled: boolean
lastRunAt: string | null
lastTxAvgMbps: number | null
lastRxAvgMbps: number | null
lastStatus: string | null
lastError: string | null
}
// SpeedProbeApiRow imported from probes-speed-probes-data-grid
// ─── helpers ──────────────────────────────────────────────────────────────────
@@ -466,53 +456,9 @@ function ScheduleSpeedProbesLive({
return (
<div className="flex flex-col gap-3">
<Card className="overflow-hidden">
{rows.length === 0 ? (
<div className="flex flex-col items-center justify-center py-10 gap-2 text-muted-foreground">
<ClockIcon className="size-7 opacity-20" />
<p className="text-sm">Нет записей speed-test в мониторинге</p>
<p className="text-xs text-muted-foreground/70 max-w-md text-center">
Настраиваются через API <code className="text-[11px]">PUT /api/uptime/speed-probes</code> или связанный UI.
</p>
</div>
) : (
<>
<div className="grid grid-cols-[1fr_1fr_80px_90px_80px_1fr] gap-2 items-center px-4 py-2 bg-muted/30 border-b text-[10px] font-semibold text-muted-foreground uppercase tracking-wide">
<span>Источник</span>
<span>Назначение</span>
<span>Протокол</span>
<span>Сек</span>
<span>Вкл</span>
<span>Последний запуск</span>
</div>
<div className="divide-y divide-border/60">
{rows.map(r => (
<div key={r.id} className={cn(
"grid grid-cols-[1fr_1fr_80px_90px_80px_1fr] gap-2 items-center px-4 py-2.5 text-xs",
!r.enabled && "opacity-50",
)}>
<span className="truncate font-mono">{name(r.srcServerId)}{r.srcInterface ? ` · ${r.srcInterface}` : ""}</span>
<span className="truncate font-mono">{name(r.dstServerId)}{r.dstInterface ? ` · ${r.dstInterface}` : ""}</span>
<span>{r.protocol.toUpperCase()}</span>
<span className="font-mono">{r.durationSec}s</span>
<span>{r.enabled ? "да" : "нет"}</span>
<span className="text-muted-foreground truncate">
{r.lastRunAt ?? "—"}
{r.lastStatus === "done" && r.lastTxAvgMbps != null && (
<span className="text-emerald-600 dark:text-emerald-400 ml-1">
TX≈{r.lastTxAvgMbps.toFixed(1)} RX≈{(r.lastRxAvgMbps ?? 0).toFixed(1)} Mb/s
</span>
)}
{r.lastStatus === "error" && r.lastError && (
<span className="text-destructive ml-1 truncate">{r.lastError}</span>
)}
</span>
</div>
))}
</div>
</>
)}
</Card>
<DataPageCard>
<ProbesSpeedProbesDataGrid rows={rows} serverName={name} />
</DataPageCard>
<p className="text-[11px] text-muted-foreground">
Данные из коллектора uptime (та же БД, что и дашборд). Редактирование — через настройки мониторинга / API.
</p>
@@ -553,63 +499,22 @@ function ScheduleTab({
}
}, [addSrc, addTun, tunnelsForServer])
const typeLabel: Record<SchedType, string> = { ping: "Ping", bandwidth: "BW-тест", both: "Ping + BW" }
const tunnelName = (srcId: string, tunnelId: string) =>
tunnelsForServer(srcId).find((t) => t.id === tunnelId)?.name
return (
<div className="flex flex-col gap-3">
<Card className="overflow-hidden">
{rules.length === 0 ? (
<div className="flex flex-col items-center justify-center py-10 gap-2 text-muted-foreground">
<ClockIcon className="size-7 opacity-20" />
<p className="text-sm">Нет правил расписания</p>
</div>
) : (
<>
<div className="grid grid-cols-[40px_1fr_140px_80px_100px_1fr_auto] gap-2 items-center px-4 py-2 bg-muted/30 border-b text-[10px] font-semibold text-muted-foreground uppercase tracking-wide">
<span />
<span>Туннель</span>
<span>Сервер</span>
<span>Тип</span>
<span>Интервал</span>
<span>Последний / следующий</span>
<span />
</div>
<div className="divide-y divide-border/60">
{rules.map(rule => {
const src = serverOptions.find(s => s.id === rule.srcId)
const tun = tunnelsForServer(rule.srcId).find(t => t.id === rule.tunnelId)
return (
<div key={rule.id} className={cn(
"grid grid-cols-[40px_1fr_140px_80px_100px_1fr_auto] gap-2 items-center px-4 py-2.5 hover:bg-muted/20 transition-colors",
!rule.enabled && "opacity-50",
)}>
<FormToggle checked={rule.enabled}
onChange={v => setRules(p => p.map(r => r.id === rule.id ? { ...r, enabled: v } : r))} />
<code className="font-mono text-xs truncate">{tun?.name ?? rule.tunnelId}</code>
<span className="text-xs text-muted-foreground truncate">{src?.name ?? rule.srcId}</span>
<span className={cn("text-[10px] px-1.5 py-0.5 rounded border font-medium w-fit",
rule.type === "ping" ? "bg-sky-500/10 text-sky-600 dark:text-sky-400 border-sky-500/20"
: rule.type === "bandwidth" ? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20"
: "bg-violet-500/10 text-violet-600 dark:text-violet-400 border-violet-500/20",
)}>{typeLabel[rule.type]}</span>
<span className="text-xs text-muted-foreground">каждые {rule.intervalMin} мин</span>
<div className="text-xs text-muted-foreground flex items-center gap-2 min-w-0">
{rule.lastRun && <span className="truncate">{rule.lastRun}</span>}
{rule.nextRunMin != null && rule.enabled && (
<span className="text-sky-600 dark:text-sky-400 shrink-0">· через {rule.nextRunMin} мин</span>
)}
</div>
<button onClick={() => setRules(p => p.filter(r => r.id !== rule.id))}
className="size-6 flex items-center justify-center rounded text-muted-foreground/40 hover:text-red-500 hover:bg-red-500/10 transition-colors">
<Trash2Icon className="size-3.5" />
</button>
</div>
)
})}
</div>
</>
)}
</Card>
<DataPageCard>
<ProbesScheduleDataGrid
rules={rules}
serverOptions={serverOptions}
tunnelName={tunnelName}
onToggleEnabled={(id, enabled) =>
setRules((p) => p.map((r) => (r.id === id ? { ...r, enabled } : r)))
}
onDelete={(id) => setRules((p) => p.filter((r) => r.id !== id))}
/>
</DataPageCard>
{showAdd ? (
<Card className="overflow-hidden">
<div className="px-4 py-3 border-b flex items-center gap-2 text-sm font-medium">