Files
MikrotikManager/components/data-grids/uptime-resources-data-grid.tsx
T
Denozordec d3a2d38b37
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
fix(ui): заменить таблицы на карточки данных и улучшить функциональность поиска
2026-06-30 22:22:51 +07:00

386 lines
13 KiB
TypeScript

"use client"
import { useMemo } from "react"
import {
type ColumnDef,
getCoreRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table"
import type { Server } from "@/lib/data"
import { Flag } from "@/components/flag"
import { Sparkline } from "@/components/sparkline"
import { StatusDot } from "@/components/status-dot"
import { cn } from "@/lib/utils"
import { DataGridShell } from "@/components/data-grids/shared/data-grid-shell"
import {
DATA_GRID_CELL_PAD,
DATA_GRID_CELL_PAD_FIRST,
DATA_GRID_CELL_PAD_LAST,
} from "@/components/data-grids/shared/data-grid-layout"
import { DataGridSortHeader } from "@/components/data-grids/shared/data-grid-sort-header"
import { EmptyState } from "@/components/empty-state"
import {
AlertCircleIcon,
ClockIcon,
CpuIcon,
HardDriveIcon,
ServerIcon,
SearchIcon,
ThermometerIcon,
} from "lucide-react"
export interface UptimeResourceRow {
serverId: string
server: Server
hasData: boolean
cpu: number
cpuHistory: number[]
ramUsed: number
ramTotal: number
ramPct: number
hddUsed: number
hddTotal: number
hddPct: number
uptimeSeconds: number
boardName: string
temp?: number
}
function TypeChip({ type }: { type: "jump-host" | "exit-node" | "home-router" }) {
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 fmtMB(mb: number): string {
if (mb >= 1024) return `${(mb / 1024).toFixed(mb >= 10240 ? 0 : 1)} ГБ`
return `${mb.toFixed(1)} МБ`
}
function fmtUptime(sec: number): string {
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 resPctColor(pct: number, warn = 70, crit = 85): string {
if (pct >= crit) return "text-red-600 dark:text-red-400"
if (pct >= warn) return "text-amber-600 dark:text-amber-400"
return "text-emerald-600 dark:text-emerald-400"
}
function resBarColor(pct: number, warn = 70, crit = 85): string {
if (pct >= crit) return "bg-red-500"
if (pct >= warn) return "bg-amber-500"
return "bg-emerald-500"
}
function MiniBar({
pct,
warn = 70,
crit = 85,
className,
}: {
pct: number
warn?: number
crit?: number
className?: string
}) {
return (
<div className={cn("h-1.5 rounded-full bg-muted overflow-hidden", className)}>
<div
className={cn("h-full rounded-full transition-all duration-700", resBarColor(pct, warn, crit))}
style={{ width: `${Math.min(100, Math.max(0, pct))}%` }}
/>
</div>
)
}
interface UptimeResourcesDataGridProps {
rows: UptimeResourceRow[]
}
function UptimeResourcesDataGrid({ rows }: UptimeResourcesDataGridProps) {
const columns = useMemo<ColumnDef<UptimeResourceRow>[]>(
() => [
{
id: "name",
accessorFn: (row) => row.server.name,
header: ({ column }) => (
<DataGridSortHeader column={column} title="Сервер" className="ml-1" />
),
cell: ({ row }) => {
const r = row.original
const srv = r.server
const offline = srv.status !== "online"
const noMetrics = offline || !r.hasData
const isCrit =
!noMetrics &&
(r.cpu >= 85 || r.ramPct >= 85 || r.hddPct >= 85 || (r.temp ?? 0) >= 70)
return (
<div className="flex items-center gap-2 flex-wrap">
{isCrit && <AlertCircleIcon className="size-3.5 text-red-500 shrink-0" />}
{!isCrit && <StatusDot status={srv.status} pulse={!offline} />}
<Flag code={srv.country} size={16} />
<span className="font-mono font-semibold">{srv.name}</span>
<TypeChip type={srv.type} />
<span className="text-xs text-muted-foreground hidden xl:inline">{srv.site}</span>
{!offline && !r.hasData && (
<span className="text-[10px] rounded border border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-400 px-1.5 py-0.5">
нет данных
</span>
)}
</div>
)
},
meta: {
headerTitle: "Сервер",
headerClassName: DATA_GRID_CELL_PAD_FIRST,
cellClassName: DATA_GRID_CELL_PAD_FIRST,
},
},
{
id: "board",
accessorKey: "boardName",
header: () => (
<span className="text-xs font-medium text-muted-foreground hidden md:inline">
Модель · ROS
</span>
),
enableSorting: false,
cell: ({ row }) => {
const r = row.original
return (
<div className="hidden md:flex flex-col leading-tight">
<span className="font-mono text-xs text-muted-foreground">
{r.hasData ? r.boardName : "—"}
</span>
<span className="text-[10px] text-muted-foreground/50">{r.server.os}</span>
</div>
)
},
meta: { headerClassName: "hidden md:table-cell", cellClassName: "hidden md:table-cell px-4 py-3" },
},
{
id: "cpu",
accessorKey: "cpu",
header: ({ column }) => (
<span className="inline-flex items-center gap-1.5">
<CpuIcon className="size-3.5 text-muted-foreground" />
<DataGridSortHeader column={column} title="CPU" />
</span>
),
cell: ({ row }) => {
const r = row.original
const offline = r.server.status !== "online"
const noMetrics = offline || !r.hasData
if (noMetrics) {
return (
<span className="text-xs text-muted-foreground/30">
{offline ? "—" : "нет опроса"}
</span>
)
}
const cpuColor =
r.cpu >= 85 ? "hsl(0 84% 60%)" : r.cpu >= 70 ? "hsl(38 92% 50%)" : "hsl(142 76% 36%)"
return (
<div className="flex flex-col gap-1.5 min-w-[140px]">
<div className="flex items-center gap-2">
<span className={cn("font-mono text-sm font-semibold tabular-nums w-10 shrink-0", resPctColor(r.cpu))}>
{r.cpu}%
</span>
<MiniBar pct={r.cpu} className="flex-1" />
</div>
<Sparkline data={r.cpuHistory} width={120} height={18} color={cpuColor} filled />
</div>
)
},
meta: {
headerTitle: "CPU",
headerClassName: cn(DATA_GRID_CELL_PAD, "min-w-[160px]"),
cellClassName: cn(DATA_GRID_CELL_PAD, "min-w-[160px]"),
},
},
{
id: "ram",
accessorKey: "ramPct",
header: ({ column }) => (
<span className="inline-flex items-center gap-1.5">
<HardDriveIcon className="size-3.5 text-muted-foreground" />
<DataGridSortHeader column={column} title="RAM" />
</span>
),
cell: ({ row }) => {
const r = row.original
const offline = r.server.status !== "online"
const noMetrics = offline || !r.hasData
if (noMetrics) {
return <span className="text-xs text-muted-foreground/30">{offline ? "—" : "нет опроса"}</span>
}
return (
<div className="flex flex-col gap-1.5 min-w-[155px]">
<div className="flex items-center justify-between text-xs">
<span className={cn("font-mono font-semibold", resPctColor(r.ramPct))}>{r.ramPct}%</span>
<span className="text-muted-foreground/60 font-mono text-[10px]">
{fmtMB(r.ramUsed)}/{fmtMB(r.ramTotal)}
</span>
</div>
<MiniBar pct={r.ramPct} />
</div>
)
},
meta: {
headerTitle: "RAM",
headerClassName: cn(DATA_GRID_CELL_PAD, "min-w-[175px]"),
cellClassName: cn(DATA_GRID_CELL_PAD, "min-w-[175px]"),
},
},
{
id: "hdd",
accessorKey: "hddPct",
header: ({ column }) => (
<span className="inline-flex items-center gap-1.5">
<HardDriveIcon className="size-3.5 text-muted-foreground" />
<DataGridSortHeader column={column} title="Диск" />
</span>
),
cell: ({ row }) => {
const r = row.original
const offline = r.server.status !== "online"
const noMetrics = offline || !r.hasData
if (noMetrics) {
return <span className="text-xs text-muted-foreground/30">{offline ? "—" : "нет опроса"}</span>
}
return (
<div className="flex flex-col gap-1.5 min-w-[155px]">
<div className="flex items-center justify-between text-xs">
<span className={cn("font-mono font-semibold", resPctColor(r.hddPct))}>{r.hddPct}%</span>
<span className="text-muted-foreground/60 font-mono text-[10px]">
{fmtMB(r.hddUsed)}/{fmtMB(r.hddTotal)}
</span>
</div>
<MiniBar pct={r.hddPct} />
</div>
)
},
meta: {
headerTitle: "Диск",
headerClassName: cn(DATA_GRID_CELL_PAD, "min-w-[175px]"),
cellClassName: cn(DATA_GRID_CELL_PAD, "min-w-[175px]"),
},
},
{
id: "uptime",
accessorKey: "uptimeSeconds",
header: ({ column }) => (
<span className="inline-flex items-center gap-1.5">
<ClockIcon className="size-3.5 text-muted-foreground" />
<DataGridSortHeader column={column} title="Uptime" />
</span>
),
cell: ({ row }) => {
const r = row.original
const offline = r.server.status !== "online"
const noMetrics = offline || !r.hasData
return (
<span className="font-mono text-xs text-muted-foreground">
{noMetrics ? "—" : fmtUptime(r.uptimeSeconds)}
</span>
)
},
meta: {
headerTitle: "Uptime",
headerClassName: DATA_GRID_CELL_PAD,
cellClassName: DATA_GRID_CELL_PAD,
},
},
{
id: "temp",
accessorKey: "temp",
header: ({ column }) => (
<span className="inline-flex items-center gap-1.5">
<ThermometerIcon className="size-3.5 text-muted-foreground" />
<DataGridSortHeader column={column} title="°C" />
</span>
),
cell: ({ row }) => {
const r = row.original
const offline = r.server.status !== "online"
const noMetrics = offline || !r.hasData
if (r.temp !== undefined && !noMetrics) {
return (
<span
className={cn(
"font-mono text-sm font-semibold tabular-nums",
r.temp >= 70
? "text-red-600 dark:text-red-400"
: r.temp >= 55
? "text-amber-600 dark:text-amber-400"
: "text-muted-foreground",
)}
>
{r.temp}°C
</span>
)
}
return <span className="text-muted-foreground/30 text-xs"></span>
},
meta: {
headerTitle: "°C",
headerClassName: DATA_GRID_CELL_PAD_LAST,
cellClassName: DATA_GRID_CELL_PAD_LAST,
},
},
],
[],
)
const table = useReactTable({
data: rows,
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getRowId: (row) => row.serverId,
})
if (rows.length === 0) {
return (
<EmptyState
icon={<SearchIcon className="size-4" />}
title="Ничего не найдено"
className="border-0 py-12"
/>
)
}
return (
<DataGridShell
table={table}
recordCount={rows.length}
tableClassNames={{
headerRow: "border-b border-border bg-muted/30",
bodyRow: cn(
"group/row hover:bg-muted/30",
"[&:has([data-resource-offline=true])]:opacity-50",
"[&:has([data-resource-crit=true])]:bg-red-500/3",
),
}}
/>
)
}
export { UptimeResourcesDataGrid, type UptimeResourcesDataGridProps }