fix(wireguard): сохранять меню действий при перерисовке

This commit is contained in:
Denozordec committed 2026-10-08 22:32:57 +07:00
1 parent 4c4ba02b44
commit 6a46fb1e40
13 files changed
+466 -85

No files matched your search

+90 -79
View File
@@ -2,7 +2,7 @@
import { DataGridContainer, DataGridScrollArea, DataGridTable, dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid"
import { useEffect, useMemo, type ReactNode } from "react"
import { createContext, useContext, useEffect, useMemo, type ReactNode } from "react"
import {
type ColumnDef,
useTable,
@@ -58,6 +58,76 @@ interface WireguardDataGridProps {
onExportPeer?: (iface: WgIfaceWithServer, peerId: string) => void
}
type WireguardActions = Pick<WireguardDataGridProps, "onExport" | "onAddPeer" | "onToggle" | "toggleBusy" | "onDelete">
const WireguardActionsContext = createContext<WireguardActions | null>(null)
// Preserve menu state while callbacks and the current row data update.
function WireguardActionsCell({ row }: { row: { original: WgIfaceWithServer } }) {
const actions = useContext(WireguardActionsContext)
if (!actions) throw new Error("WireGuard actions context is missing")
const { onExport, onAddPeer, onToggle, toggleBusy, onDelete } = actions
const iface = row.original
return (
<div
className="flex justify-end"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
<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",
"transition-[background-color,color,border-color]",
"group-hover/row:opacity-100 focus-visible:opacity-100",
"data-popup-open:opacity-100 data-popup-open:bg-muted",
)}
onClick={(e) => e.stopPropagation()}
aria-label={`Действия: ${iface.name}`}
>
<MoreHorizontalIcon className="size-4" />
</Button>
}
/>
<DropdownMenuContent side="bottom" align="end">
<DropdownMenuItem onClick={() => onExport(iface)}>
<CodeXmlIcon className="size-4" />
Экспорт
</DropdownMenuItem>
{onAddPeer && (
<DropdownMenuItem onClick={() => onAddPeer(iface)}>
<PlusIcon className="size-4" />
Добавить пира
</DropdownMenuItem>
)}
{onToggle && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem disabled={toggleBusy} onClick={() => onToggle(iface)}>
<PowerIcon className="size-4" />
{iface.enabled ? "Отключить" : "Включить"}
</DropdownMenuItem>
</>
)}
{onDelete && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive" onClick={() => onDelete(iface)}>
<Trash2Icon className="size-4" />
Удалить
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
)
}
function WireguardDataGrid({
interfaces,
compactServer = false,
@@ -198,68 +268,7 @@ function WireguardDataGrid({
{
id: "actions",
header: () => <span className="sr-only">Действия</span>,
cell: ({ row }) => {
const iface = row.original
return (
<div
className="flex justify-end"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
<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",
"transition-[background-color,color,border-color]",
"group-hover/row:opacity-100 focus-visible:opacity-100",
"data-popup-open:opacity-100 data-popup-open:bg-muted",
)}
onClick={(e) => e.stopPropagation()}
aria-label={`Действия: ${iface.name}`}
>
<MoreHorizontalIcon className="size-4" />
</Button>
}
/>
<DropdownMenuContent side="bottom" align="end">
<DropdownMenuItem onClick={() => onExport(iface)}>
<CodeXmlIcon className="size-4" />
Экспорт
</DropdownMenuItem>
{onAddPeer && (
<DropdownMenuItem onClick={() => onAddPeer(iface)}>
<PlusIcon className="size-4" />
Добавить пира
</DropdownMenuItem>
)}
{onToggle && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem disabled={toggleBusy} onClick={() => onToggle(iface)}>
<PowerIcon className="size-4" />
{iface.enabled ? "Отключить" : "Включить"}
</DropdownMenuItem>
</>
)}
{onDelete && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive" onClick={() => onDelete(iface)}>
<Trash2Icon className="size-4" />
Удалить
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
)
},
cell: WireguardActionsCell,
enableSorting: false,
size: 72,
meta: {
@@ -268,7 +277,7 @@ function WireguardDataGrid({
},
},
],
[compactServer, onExport, onAddPeer, onToggle, toggleBusy, onDelete, onDeletePeer, onExportPeer],
[compactServer, onDeletePeer, onExportPeer],
)
const table = useTable({
@@ -302,21 +311,23 @@ function WireguardDataGrid({
}
return (
<DataGridShell
table={table}
recordCount={interfaces.length}
pagination={interfaces.length > 50 || pageSize !== 50}
tableLayout={{ dense: true, columnsPinnable: true }}
onRowClick={(row) => table.getRow(JSON.stringify([row.serverId, row.id])).toggleExpanded()}
tableClassNames={{
headerRow: "border-b border-border",
bodyRow: cn("group/row", "[&[data-disabled=true]]:opacity-50"),
}}
>
<DataGridContainer border={false} className="w-full rounded-none border-0">
<DataGridScrollArea orientation="horizontal"><DataGridTable /></DataGridScrollArea>
</DataGridContainer>
</DataGridShell>
<WireguardActionsContext.Provider value={{ onExport, onAddPeer, onToggle, toggleBusy, onDelete }}>
<DataGridShell
table={table}
recordCount={interfaces.length}
pagination={interfaces.length > 50 || pageSize !== 50}
tableLayout={{ dense: true, columnsPinnable: true }}
onRowClick={(row) => table.getRow(JSON.stringify([row.serverId, row.id])).toggleExpanded()}
tableClassNames={{
headerRow: "border-b border-border",
bodyRow: cn("group/row", "[&[data-disabled=true]]:opacity-50"),
}}
>
<DataGridContainer border={false} className="w-full rounded-none border-0">
<DataGridScrollArea orientation="horizontal"><DataGridTable /></DataGridScrollArea>
</DataGridContainer>
</DataGridShell>
</WireguardActionsContext.Provider>
)
}