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
222 lines
6.9 KiB
TypeScript
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 }
|