Files
MikrotikManager/components/data-grids/probes-schedule-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

222 lines
6.9 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 { cn } from "@/lib/utils"
import { FormToggle } from "@/components/form-kit"
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 { ClockIcon, Trash2Icon } from "lucide-react"
export type SchedType = "ping" | "bandwidth" | "both"
export interface SchedRule {
id: string
srcId: string
tunnelId: string
type: SchedType
intervalMin: number
enabled: boolean
lastRun: string | null
nextRunMin: number | null
}
const TYPE_LABEL: Record<SchedType, string> = {
ping: "Ping",
bandwidth: "BW-тест",
both: "Ping + BW",
}
interface ProbesScheduleDataGridProps {
rules: SchedRule[]
serverOptions: Server[]
tunnelName: (srcId: string, tunnelId: string) => string | undefined
onToggleEnabled: (id: string, enabled: boolean) => void
onDelete: (id: string) => void
}
function ProbesScheduleDataGrid({
rules,
serverOptions,
tunnelName,
onToggleEnabled,
onDelete,
}: ProbesScheduleDataGridProps) {
const serverMap = useMemo(
() => new Map(serverOptions.map((s) => [s.id, s])),
[serverOptions],
)
const columns = useMemo<ColumnDef<SchedRule>[]>(
() => [
{
id: "enabled",
accessorKey: "enabled",
header: () => <span className="sr-only">Вкл</span>,
enableSorting: false,
cell: ({ row }) => (
<div onClick={(e) => e.stopPropagation()}>
<FormToggle
checked={row.original.enabled}
onChange={(v) => onToggleEnabled(row.original.id, v)}
/>
</div>
),
size: 48,
meta: {
headerClassName: DATA_GRID_CELL_PAD_FIRST,
cellClassName: DATA_GRID_CELL_PAD_FIRST,
},
},
{
id: "tunnel",
accessorKey: "tunnelId",
header: ({ column }) => <DataGridSortHeader column={column} title="Туннель" />,
cell: ({ row }) => (
<code className="font-mono text-xs truncate block">
{tunnelName(row.original.srcId, row.original.tunnelId) ?? row.original.tunnelId}
</code>
),
meta: {
headerTitle: "Туннель",
headerClassName: DATA_GRID_CELL_PAD,
cellClassName: DATA_GRID_CELL_PAD,
},
},
{
id: "server",
accessorKey: "srcId",
header: ({ column }) => <DataGridSortHeader column={column} title="Сервер" />,
cell: ({ row }) => (
<span className="text-xs text-muted-foreground truncate block">
{serverMap.get(row.original.srcId)?.name ?? row.original.srcId}
</span>
),
meta: {
headerTitle: "Сервер",
headerClassName: DATA_GRID_CELL_PAD,
cellClassName: DATA_GRID_CELL_PAD,
},
},
{
id: "type",
accessorKey: "type",
header: ({ column }) => <DataGridSortHeader column={column} title="Тип" />,
cell: ({ row }) => (
<span
className={cn(
"text-[10px] px-1.5 py-0.5 rounded border font-medium w-fit",
row.original.type === "ping"
? "bg-sky-500/10 text-sky-600 dark:text-sky-400 border-sky-500/20"
: row.original.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",
)}
>
{TYPE_LABEL[row.original.type]}
</span>
),
meta: {
headerTitle: "Тип",
headerClassName: DATA_GRID_CELL_PAD,
cellClassName: DATA_GRID_CELL_PAD,
},
},
{
id: "intervalMin",
accessorKey: "intervalMin",
header: ({ column }) => <DataGridSortHeader column={column} title="Интервал" />,
cell: ({ row }) => (
<span className="text-xs text-muted-foreground">каждые {row.original.intervalMin} мин</span>
),
meta: {
headerTitle: "Интервал",
headerClassName: DATA_GRID_CELL_PAD,
cellClassName: DATA_GRID_CELL_PAD,
},
},
{
id: "lastRun",
header: () => (
<span className="text-xs font-medium text-muted-foreground">Последний / следующий</span>
),
enableSorting: false,
cell: ({ row }) => {
const rule = row.original
return (
<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>
)
},
meta: {
headerTitle: "Последний / следующий",
headerClassName: DATA_GRID_CELL_PAD,
cellClassName: DATA_GRID_CELL_PAD,
},
},
{
id: "actions",
header: () => <span className="sr-only">Действия</span>,
enableSorting: false,
cell: ({ row }) => (
<button
type="button"
onClick={() => onDelete(row.original.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 opacity-0 group-hover/row:opacity-100 focus-visible:opacity-100"
aria-label="Удалить правило"
>
<Trash2Icon className="size-3.5" />
</button>
),
size: 48,
meta: {
headerClassName: DATA_GRID_CELL_PAD_LAST,
cellClassName: DATA_GRID_CELL_PAD_LAST,
},
},
],
[onDelete, onToggleEnabled, serverMap, tunnelName],
)
const table = useReactTable({
data: rules,
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getRowId: (row) => row.id,
})
if (rules.length === 0) {
return (
<EmptyState
icon={<ClockIcon className="size-4" />}
title="Нет правил расписания"
description="Добавьте правило ping или bandwidth-теста"
className="border-0 py-10"
/>
)
}
return <DataGridShell table={table} recordCount={rules.length} />
}
export { ProbesScheduleDataGrid, type ProbesScheduleDataGridProps }