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

235 lines
7.5 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 { 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 { CableIcon, CopyIcon, EyeIcon, EyeOffIcon, Trash2Icon } from "lucide-react"
export interface SubUserRow {
id: string
login: string
password: string
description: string
jhServerIds: string[]
clientIp: string
active: boolean
lastSeen: string | null
}
interface SubusersDataGridProps {
subUsers: SubUserRow[]
servers: Server[]
revealedIds: Set<string>
onToggleReveal: (id: string) => void
onToggleActive: (id: string) => void
onRemove: (id: string) => void
}
function SubusersDataGrid({
subUsers,
servers,
revealedIds,
onToggleReveal,
onToggleActive,
onRemove,
}: SubusersDataGridProps) {
const columns = useMemo<ColumnDef<SubUserRow>[]>(
() => [
{
id: "login",
accessorKey: "login",
header: ({ column }) => (
<DataGridSortHeader column={column} title="Логин / описание" className="ml-1" />
),
cell: ({ row }) => {
const su = row.original
return (
<div className="min-w-0">
<p className="text-xs font-mono font-medium truncate">{su.login}</p>
{su.description && (
<p className="text-[11px] text-muted-foreground truncate">{su.description}</p>
)}
{su.lastSeen && (
<p className="text-[10px] text-muted-foreground/50">{su.lastSeen}</p>
)}
</div>
)
},
meta: {
headerTitle: "Логин / описание",
headerClassName: DATA_GRID_CELL_PAD_FIRST,
cellClassName: DATA_GRID_CELL_PAD_FIRST,
},
},
{
id: "password",
header: () => <span className="text-xs font-medium text-muted-foreground">Пароль</span>,
enableSorting: false,
cell: ({ row }) => {
const su = row.original
const revealed = revealedIds.has(su.id)
return (
<div className="flex items-center gap-1 min-w-0" onClick={(e) => e.stopPropagation()}>
<span className="font-mono text-[11px] truncate flex-1">
{revealed ? su.password : "••••••••••••"}
</span>
<button
type="button"
onClick={() => onToggleReveal(su.id)}
className="text-muted-foreground/50 hover:text-muted-foreground shrink-0 transition-colors"
>
{revealed ? <EyeOffIcon className="size-3" /> : <EyeIcon className="size-3" />}
</button>
<button
type="button"
onClick={() => navigator.clipboard.writeText(su.password).catch(() => {})}
className="text-muted-foreground/50 hover:text-muted-foreground shrink-0 transition-colors"
>
<CopyIcon className="size-3" />
</button>
</div>
)
},
meta: {
headerTitle: "Пароль",
headerClassName: DATA_GRID_CELL_PAD,
cellClassName: DATA_GRID_CELL_PAD,
},
},
{
id: "jhServers",
header: () => <span className="text-xs font-medium text-muted-foreground">JH-серверы</span>,
enableSorting: false,
cell: ({ row }) => {
const jhs = servers.filter((s) => row.original.jhServerIds.includes(s.id))
return (
<div className="flex flex-wrap gap-1 min-w-0">
{jhs.length === 0 ? (
<span className="text-[11px] text-muted-foreground/40"></span>
) : (
jhs.map((jh) => (
<span
key={jh.id}
className="inline-flex items-center gap-1 text-[10px] font-medium bg-violet-500/10 text-violet-600 dark:text-violet-400 border border-violet-500/20 rounded px-1 py-0.5"
>
<Flag code={jh.country} size={10} />
{jh.name.split("-").slice(-1)[0]}
</span>
))
)}
</div>
)
},
meta: {
headerTitle: "JH-серверы",
headerClassName: DATA_GRID_CELL_PAD,
cellClassName: DATA_GRID_CELL_PAD,
},
},
{
id: "clientIp",
accessorKey: "clientIp",
header: ({ column }) => <DataGridSortHeader column={column} title="IP-клиента" />,
cell: ({ row }) => (
<span className="font-mono text-[11px] text-muted-foreground truncate">
{row.original.clientIp || "—"}
</span>
),
meta: {
headerTitle: "IP-клиента",
headerClassName: DATA_GRID_CELL_PAD,
cellClassName: DATA_GRID_CELL_PAD,
},
},
{
id: "active",
accessorKey: "active",
header: () => <span className="sr-only">Активен</span>,
enableSorting: false,
cell: ({ row }) => (
<div onClick={(e) => e.stopPropagation()}>
<FormToggle
checked={row.original.active}
onChange={() => onToggleActive(row.original.id)}
/>
</div>
),
size: 48,
meta: {
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={() => onRemove(row.original.id)}
className="text-muted-foreground/50 hover:text-destructive transition-colors opacity-0 group-hover/row:opacity-100 focus-visible:opacity-100"
aria-label="Удалить GRE-клиента"
>
<Trash2Icon className="size-3.5" />
</button>
),
size: 40,
meta: {
headerClassName: DATA_GRID_CELL_PAD_LAST,
cellClassName: DATA_GRID_CELL_PAD_LAST,
},
},
],
[onRemove, onToggleActive, onToggleReveal, revealedIds, servers],
)
const table = useReactTable({
data: subUsers,
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getRowId: (row) => row.id,
})
if (subUsers.length === 0) {
return (
<EmptyState
icon={<CableIcon className="size-4" />}
title="Нет GRE-клиентов"
description="Добавьте учётки для подключения устройств"
className="border-0 py-12"
/>
)
}
return (
<DataGridShell
table={table}
recordCount={subUsers.length}
tableClassNames={{
headerRow: "border-b border-border",
bodyRow: cn("group/row", "[&[data-disabled=true]]:opacity-50"),
}}
/>
)
}
export { SubusersDataGrid, type SubusersDataGridProps }