fix(gre): сохранять меню действий при обновлении таблицы

This commit is contained in:
Denozordec committed 2026-10-08 16:27:17 +07:00
1 parent 26e8225815
commit 731803bade
7 files changed
+170 -85

No files matched your search

+92 -80
View File
@@ -2,7 +2,7 @@
import { DataGridContainer, DataGridScrollArea, DataGridTable, dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid"
import { useEffect, useMemo } from "react"
import { createContext, useContext, useEffect, useMemo } from "react"
import {
type ColumnDef,
useTable,
@@ -111,6 +111,82 @@ interface GreTunnelsDataGridProps {
emptyDescription?: string
}
type GreActions = Pick<GreTunnelsDataGridProps, "onCodePreview" | "onEdit" | "onToggle" | "onDelete" | "mutationsLocked" | "canManage">
const GreActionsContext = createContext<GreActions | null>(null)
// Keep the cell component identity stable while permissions and callbacks update.
function GreActionsCell({ row }: { row: { original: GreTunnel } }) {
const actions = useContext(GreActionsContext)
if (!actions) throw new Error("GRE actions context is missing")
const { onCodePreview, onEdit, onToggle, onDelete, mutationsLocked, canManage } = actions
const t = row.original
return (
<div className="flex items-center gap-1 justify-end">
<Button
type="button"
variant="ghost"
size="icon"
className={cn(
"size-11 shrink-0",
)}
title="Предпросмотр кода RouterOS"
aria-label={`Просмотр кода: ${t.name}`}
onClick={(e) => {
e.stopPropagation()
onCodePreview(t)
}}
>
<CodeXmlIcon className="size-3.5" />
</Button>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
type="button"
variant="outline"
size="icon"
className={cn(
"size-11 shrink-0 border-border/60 bg-background/80 text-muted-foreground shadow-none",
"opacity-100",
"data-popup-open:opacity-100",
)}
aria-label={`Действия: ${t.name}`}
>
<MoreHorizontalIcon className="size-4" />
</Button>
}
/>
<DropdownMenuContent side="bottom" align="end">
<DropdownMenuGroup>
<DropdownMenuLabel>{t.name}</DropdownMenuLabel>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => onCodePreview(t)}>
<CodeXmlIcon className="size-4" /> Просмотр кода
</DropdownMenuItem>
<DropdownMenuItem disabled={mutationsLocked || (canManage && !canManage(t.serverId)) || !onEdit} onClick={() => onEdit?.(t)}>
<PencilIcon className="size-4" /> Редактировать
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem disabled={mutationsLocked || (canManage && !canManage(t.serverId)) || !onToggle} onClick={() => onToggle?.(t)}>
<PowerIcon className="size-4" />
{t.enabled ? "Выключить" : "Включить"}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
variant="destructive"
disabled={mutationsLocked || (canManage && !canManage(t.serverId)) || !onDelete}
onClick={() => onDelete?.(t)}
>
<Trash2Icon className="size-4" /> Удалить туннель
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)
}
function GreTunnelsDataGrid({
tunnels,
servers,
@@ -281,78 +357,12 @@ function GreTunnelsDataGrid({
id: "actions",
header: () => <span className="sr-only">Действия</span>,
enableSorting: false,
cell: ({ row }) => {
const t = row.original
return (
<div className="flex items-center gap-1 justify-end">
<Button
type="button"
variant="ghost"
size="icon"
className={cn(
"size-11 shrink-0",
)}
title="Предпросмотр кода RouterOS"
aria-label={`Просмотр кода: ${t.name}`}
onClick={(e) => {
e.stopPropagation()
onCodePreview(t)
}}
>
<CodeXmlIcon className="size-3.5" />
</Button>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
type="button"
variant="outline"
size="icon"
className={cn(
"size-11 shrink-0 border-border/60 bg-background/80 text-muted-foreground shadow-none",
"opacity-100",
"data-popup-open:opacity-100",
)}
aria-label={`Действия: ${t.name}`}
>
<MoreHorizontalIcon className="size-4" />
</Button>
}
/>
<DropdownMenuContent side="bottom" align="end">
<DropdownMenuGroup>
<DropdownMenuLabel>{t.name}</DropdownMenuLabel>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => onCodePreview(t)}>
<CodeXmlIcon className="size-4" /> Просмотр кода
</DropdownMenuItem>
<DropdownMenuItem disabled={mutationsLocked || (canManage && !canManage(t.serverId)) || !onEdit} onClick={() => onEdit?.(t)}>
<PencilIcon className="size-4" /> Редактировать
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem disabled={mutationsLocked || (canManage && !canManage(t.serverId)) || !onToggle} onClick={() => onToggle?.(t)}>
<PowerIcon className="size-4" />
{t.enabled ? "Выключить" : "Включить"}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
variant="destructive"
disabled={mutationsLocked || (canManage && !canManage(t.serverId)) || !onDelete}
onClick={() => onDelete?.(t)}
>
<Trash2Icon className="size-4" /> Удалить туннель
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)
},
cell: GreActionsCell,
size: 112,
meta: { headerClassName: DATA_GRID_CELL_PAD_LAST, cellClassName: DATA_GRID_CELL_PAD_LAST },
},
],
[onCodePreview, onEdit, onToggle, onDelete, mutationsLocked, canManage, unknownServerIds, poolMap, serverMap],
[unknownServerIds, poolMap, serverMap],
)
const table = useTable({
@@ -383,17 +393,19 @@ function GreTunnelsDataGrid({
}
return (
<DataGridShell
table={table}
recordCount={tunnels.length}
pagination={tunnels.length > 50 || pageSize !== 50}
tableLayout={{ dense: true, columnsPinnable: true }}
tableClassNames={{ headerRow: "border-b border-border", bodyRow: "group/row" }}
>
<DataGridContainer border={false} className="w-full rounded-none border-0">
<DataGridScrollArea orientation="horizontal"><DataGridTable /></DataGridScrollArea>
</DataGridContainer>
</DataGridShell>
<GreActionsContext.Provider value={{ onCodePreview, onEdit, onToggle, onDelete, mutationsLocked, canManage }}>
<DataGridShell
table={table}
recordCount={tunnels.length}
pagination={tunnels.length > 50 || pageSize !== 50}
tableLayout={{ dense: true, columnsPinnable: true }}
tableClassNames={{ headerRow: "border-b border-border", bodyRow: "group/row" }}
>
<DataGridContainer border={false} className="w-full rounded-none border-0">
<DataGridScrollArea orientation="horizontal"><DataGridTable /></DataGridScrollArea>
</DataGridContainer>
</DataGridShell>
</GreActionsContext.Provider>
)
}