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,221 @@
|
||||
"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 }
|
||||
Reference in New Issue
Block a user