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:
@@ -0,0 +1,191 @@
|
||||
"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}
|
||||
compact
|
||||
isLoading={isLoading}
|
||||
emptyTitle={emptyTitle}
|
||||
emptyDescription={emptyDescription}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { DashboardActiveProbesDataGrid, type DashboardActiveProbesDataGridProps }
|
||||
Reference in New Issue
Block a user