Files
MikrotikManager/components/data-grids/dashboard-active-probes-data-grid.tsx
T
Denozordec d2de8d3188
Docker images / prepare-release (push) Successful in 7s
Docker images / backend-image (push) Successful in 2m1s
Docker images / frontend-image (push) Successful in 2m7s
Docker images / updater-image (push) Successful in 41s
Docker images / notify-webhook (push) Has been skipped
Docker images / publish-release (push) Successful in 8s
fix(ui): улучшить функциональность панели данных и заменить устаревшие компоненты
2026-06-30 22:36:00 +07:00

191 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client"
import { useMemo } from "react"
import type { PingProbe, Server, ServerType } from "@/lib/data"
import { Flag } from "@/components/flag"
import { StatusBadge } from "@/components/status-badge"
import { StatusDot } from "@/components/status-dot"
import { Sparkline } from "@/components/sparkline"
import { CompactDataGrid, type CompactDataGridColumn } from "@/components/data-grids/compact-data-grid"
import { cn } from "@/lib/utils"
import { FilterIcon } from "lucide-react"
function formatLossPct(loss: number): string {
if (!Number.isFinite(loss)) return "—"
return Number.isInteger(loss) ? `${loss}%` : `${loss.toFixed(1)}%`
}
function TypeChip({ type }: { type: ServerType }) {
return (
<span
className={cn(
"inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-bold border shrink-0",
type === "home-router"
? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20"
: type === "jump-host"
? "bg-violet-500/10 text-violet-600 dark:text-violet-400 border-violet-500/20"
: "bg-sky-500/10 text-sky-600 dark:text-sky-400 border-sky-500/20",
)}
>
{type === "jump-host" ? "JH" : type === "home-router" ? "HR" : "EN"}
</span>
)
}
function ProbeSourceCell({ probe, catalog }: { probe: PingProbe; catalog: Server[] }) {
const srv = catalog.find((s) => s.id === probe.srcServerId)
const iface = (probe.srcInterface ?? "").trim() || "auto"
if (!srv) {
return (
<div className="flex items-start gap-2 min-w-0 max-w-[280px]">
<span className="mt-1 shrink-0 inline-flex">
<StatusDot status="offline" />
</span>
<div className="min-w-0">
<p className="text-[13px] font-medium text-muted-foreground truncate">
Сервер <span className="font-mono tabular-nums">{probe.srcServerId}</span>
</p>
<p className="text-[11px] font-mono text-muted-foreground truncate mt-0.5" title={iface}>
{iface}
</p>
</div>
</div>
)
}
return (
<div className="flex items-start gap-2 min-w-0 max-w-[280px]">
<span className="mt-1 shrink-0 inline-flex">
<StatusDot status={srv.status} pulse={srv.status === "online"} />
</span>
<div className="flex gap-2 min-w-0 flex-1">
<Flag code={srv.country} size={16} className="shrink-0 mt-0.5" />
<div className="min-w-0 flex-1">
<div className="flex items-center gap-1.5 flex-wrap">
<span className="text-[13px] font-medium leading-tight truncate">{srv.name}</span>
<TypeChip type={srv.type} />
</div>
<p className="text-[11px] text-muted-foreground mt-0.5 truncate" title={`Интерфейс: ${iface}`}>
<span className="font-mono tabular-nums">{iface}</span>
{srv.site && srv.site !== "—" && (
<span className="text-muted-foreground/90"> · {srv.site}</span>
)}
</p>
</div>
</div>
</div>
)
}
interface DashboardActiveProbesDataGridProps {
probes: PingProbe[]
catalog: Server[]
isLoading?: boolean
emptyTitle?: string
emptyDescription?: string
}
function DashboardActiveProbesDataGrid({
probes,
catalog,
isLoading,
emptyTitle = "Нет активных проб",
emptyDescription,
}: DashboardActiveProbesDataGridProps) {
const columns = useMemo<CompactDataGridColumn<PingProbe>[]>(
() => [
{
id: "source",
header: "Источник",
enableSorting: false,
cell: (p) => <ProbeSourceCell probe={p} catalog={catalog} />,
},
{
id: "name",
header: "Проба",
accessorKey: "name",
cell: (p) => <span className="font-medium">{p.name}</span>,
},
{
id: "target",
header: "Цель",
accessorKey: "target",
cell: (p) => <span className="font-mono text-xs text-muted-foreground">{p.target}</span>,
},
{
id: "filter",
header: "Фильтр",
accessorKey: "filter",
enableSorting: false,
cell: (p) => (
<span className="inline-flex items-center gap-1 text-xs border border-border rounded px-2 py-0.5">
<FilterIcon className="size-3 text-muted-foreground" />
{p.filter}
</span>
),
},
{
id: "rtt",
header: "RTT",
headerClassName: "text-right",
cellClassName: "font-mono text-right",
cell: (p) => (p.rtt == null ? "—" : `${p.rtt} мс`),
},
{
id: "loss",
header: "Потери",
headerClassName: "text-right",
cellClassName: "font-mono text-right",
cell: (p) => (
<span
className={cn(
p.loss > 5 ? "text-red-500" : p.loss > 0 ? "text-amber-500" : "text-muted-foreground",
)}
>
{formatLossPct(p.loss)}
</span>
),
},
{
id: "series",
header: "60с",
enableSorting: false,
headerClassName: "w-36",
cell: (p) => {
const sparkColor =
p.status === "down"
? "hsl(0 84% 60%)"
: p.status === "warn"
? "hsl(32 94% 44%)"
: "hsl(142 76% 36%)"
return <Sparkline data={p.series} width={120} height={24} color={sparkColor} />
},
},
{
id: "status",
header: "Статус",
enableSorting: false,
cell: (p) => (
<StatusBadge
status={p.status === "up" ? "online" : p.status === "warn" ? "degraded" : "offline"}
/>
),
},
],
[catalog],
)
return (
<CompactDataGrid
data={probes}
columns={columns}
isLoading={isLoading}
emptyTitle={emptyTitle}
emptyDescription={emptyDescription}
/>
)
}
export { DashboardActiveProbesDataGrid, type DashboardActiveProbesDataGridProps }