fix(frontend): починить сортировку DataGrid и стиль колонки actions
Build and Push Telemt Panel Docker Image / build-and-push (push) Successful in 1m57s
Build and Push Telemt Panel Docker Image / create-release (push) Skipped

Клик по заголовку снова сортирует напрямую; убран pin последней колонки, из-за которого actions выглядел отрезанным от строки.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-08-04 22:22:29 +07:00
co-authored by Cursor
parent ee3e96647e
commit 14911cd067
6 changed files with 75 additions and 19 deletions
@@ -238,7 +238,12 @@ export function createErrorsColumns(opts: {
enableSorting: false, enableSorting: false,
enableHiding: false, enableHiding: false,
enableResizing: false, enableResizing: false,
meta: { skeleton: <Skeleton className="size-7 rounded-md" /> }, enablePinning: false,
meta: {
skeleton: <Skeleton className="size-7 rounded-md" />,
cellClassName: 'bg-transparent!',
headerClassName: 'bg-transparent!',
},
}, },
] ]
} }
@@ -157,7 +157,6 @@ export function ErrorsGridView() {
} }
getFilterFieldValue={getFilterFieldValue} getFilterFieldValue={getFilterFieldValue}
onRowClick={handleOpen} onRowClick={handleOpen}
pinLastColumn
pageSize={10} pageSize={10}
initialSorting={[{ id: 'total', desc: true }]} initialSorting={[{ id: 'total', desc: true }]}
enableColumnVisibility enableColumnVisibility
@@ -177,8 +176,10 @@ export function ErrorsGridView() {
columnsResizable: true, columnsResizable: true,
columnsMovable: true, columnsMovable: true,
columnsVisibility: true, columnsVisibility: true,
columnsPinnable: false,
rowsPinnable: false, rowsPinnable: false,
}} }}
pinLastColumn={false}
/> />
<ResourcePage <ResourcePage
@@ -6,6 +6,7 @@ import {
useReactTable, useReactTable,
type ColumnDef, type ColumnDef,
type ColumnOrderState, type ColumnOrderState,
type ColumnPinningState,
type OnChangeFn, type OnChangeFn,
type PaginationState, type PaginationState,
type RowPinningState, type RowPinningState,
@@ -172,7 +173,7 @@ const DEFAULT_FILTERED_LAYOUT: TableLayoutConfig = {
dense: true, dense: true,
columnsVisibility: true, columnsVisibility: true,
columnsResizable: true, columnsResizable: true,
columnsPinnable: true, columnsPinnable: false,
columnsMovable: true, columnsMovable: true,
rowsPinnable: false, rowsPinnable: false,
} }
@@ -404,6 +405,20 @@ function ResourcePageFiltered<T extends object>({
? (columns[columns.length - 1]?.id ?? '') ? (columns[columns.length - 1]?.id ?? '')
: '' : ''
const [columnPinning, setColumnPinning] = useState<ColumnPinningState>(() =>
pinLastColumn && lastColId
? { left: [], right: [lastColId] }
: { left: [], right: [] },
)
const handleSortingChange = useCallback<OnChangeFn<SortingState>>(
(updater) => {
setSorting(updater)
resetPagination()
},
[resetPagination],
)
const table = useReactTable({ const table = useReactTable({
data: filteredData, data: filteredData,
columns, columns,
@@ -414,6 +429,7 @@ function ResourcePageFiltered<T extends object>({
pagination, pagination,
columnVisibility, columnVisibility,
columnOrder, columnOrder,
columnPinning,
...(enableRowPinning ? { rowPinning: resolvedRowPinning } : {}), ...(enableRowPinning ? { rowPinning: resolvedRowPinning } : {}),
}, },
enableRowSelection, enableRowSelection,
@@ -421,20 +437,18 @@ function ResourcePageFiltered<T extends object>({
keepPinnedRows: enableRowPinning, keepPinnedRows: enableRowPinning,
enableColumnPinning: Boolean(pinLastColumn || layout.columnsPinnable), enableColumnPinning: Boolean(pinLastColumn || layout.columnsPinnable),
enableHiding: enableColumnVisibility, enableHiding: enableColumnVisibility,
onSortingChange: setSorting, enableSorting: true,
manualSorting: false,
onSortingChange: handleSortingChange,
onRowSelectionChange: setRowSelection, onRowSelectionChange: setRowSelection,
onPaginationChange: setPagination, onPaginationChange: setPagination,
onColumnVisibilityChange: setColumnVisibility, onColumnVisibilityChange: setColumnVisibility,
onColumnOrderChange: setColumnOrder, onColumnOrderChange: setColumnOrder,
onColumnPinningChange: setColumnPinning,
onRowPinningChange: enableRowPinning ? setRowPinning : undefined, onRowPinningChange: enableRowPinning ? setRowPinning : undefined,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(), getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(), getPaginationRowModel: getPaginationRowModel(),
initialState: {
...(pinLastColumn && lastColId
? { columnPinning: { right: [lastColId] } }
: {}),
},
}) })
const handleTabChange = useCallback( const handleTabChange = useCallback(
@@ -289,18 +289,36 @@ function DataGridColumnHeaderInner<TData, TValue>({
if (hasControls) { if (hasControls) {
return ( return (
<div className="-ms-2 flex h-full items-center justify-between gap-1.5"> <div className="-ms-2 flex h-full items-center gap-0.5">
{canSort ? (
<Button
variant="ghost"
className={headerButtonClassName}
disabled={isLoading}
onClick={handleSort}
aria-label={`Сортировать: ${resolvedTitle}`}
>
{icon && icon}
{resolvedTitle}
{sortIcon}
</Button>
) : (
<span className={cn(headerLabelClassName, "px-2")}>
{icon && icon}
{resolvedTitle}
</span>
)}
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger <DropdownMenuTrigger
render={ render={
<Button <Button
size="icon-sm"
variant="ghost" variant="ghost"
className={headerButtonClassName} className="rounded-lg size-7"
disabled={isLoading} disabled={isLoading}
aria-label={`Параметры колонки: ${resolvedTitle}`}
> >
{icon && icon} <Settings2Icon className="size-3.5 opacity-60" aria-hidden="true" />
{resolvedTitle}
{sortIcon}
</Button> </Button>
} }
/> />
@@ -308,7 +326,7 @@ function DataGridColumnHeaderInner<TData, TValue>({
{menuItems} {menuItems}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
{props.tableLayout?.columnsPinnable && canPin && isPinned && ( {props.tableLayout?.columnsPinnable && canPin && isPinned ? (
<Button <Button
size="icon-sm" size="icon-sm"
variant="ghost" variant="ghost"
@@ -319,7 +337,7 @@ function DataGridColumnHeaderInner<TData, TValue>({
> >
<PinOffIcon className="size-3.5! opacity-50!" aria-hidden="true" /> <PinOffIcon className="size-3.5! opacity-50!" aria-hidden="true" />
</Button> </Button>
)} ) : null}
</div> </div>
) )
} }
@@ -349,7 +349,7 @@ export function createUsersColumns(opts: {
}, },
{ {
id: 'current_connections', id: 'current_connections',
accessorKey: 'current_connections', accessorFn: (row) => Number(row.current_connections ?? 0),
header: ({ column }) => ( header: ({ column }) => (
<DataGridColumnHeader <DataGridColumnHeader
title="Соединения" title="Соединения"
@@ -362,6 +362,7 @@ export function createUsersColumns(opts: {
enableSorting: true, enableSorting: true,
enableHiding: true, enableHiding: true,
enableResizing: true, enableResizing: true,
sortingFn: 'basic',
meta: { meta: {
skeleton: ( skeleton: (
<div className="flex min-w-0 items-center gap-2"> <div className="flex min-w-0 items-center gap-2">
@@ -442,7 +443,12 @@ export function createUsersColumns(opts: {
}, },
{ {
id: 'data_quota_bytes', id: 'data_quota_bytes',
accessorKey: 'data_quota_bytes', accessorFn: (row) => {
const quota = row.data_quota_bytes
if (typeof quota === 'number' && quota > 0) return quota
// «Без квоты» в UI показывает usage — сортируем по тому же смыслу
return Number(row.total_octets ?? 0)
},
header: ({ column }) => ( header: ({ column }) => (
<DataGridColumnHeader title="Квота" visibility={true} column={column} /> <DataGridColumnHeader title="Квота" visibility={true} column={column} />
), ),
@@ -451,6 +457,7 @@ export function createUsersColumns(opts: {
enableSorting: true, enableSorting: true,
enableHiding: true, enableHiding: true,
enableResizing: true, enableResizing: true,
sortingFn: 'basic',
meta: { meta: {
skeleton: ( skeleton: (
<div className="flex min-w-0 flex-col gap-1"> <div className="flex min-w-0 flex-col gap-1">
@@ -534,9 +541,12 @@ export function createUsersColumns(opts: {
enableSorting: false, enableSorting: false,
enableHiding: false, enableHiding: false,
enableResizing: false, enableResizing: false,
enablePinning: false,
size: 52, size: 52,
meta: { meta: {
skeleton: <Skeleton className="size-7 rounded-md" />, skeleton: <Skeleton className="size-7 rounded-md" />,
cellClassName: 'bg-transparent!',
headerClassName: 'bg-transparent!',
}, },
}, },
] ]
@@ -276,9 +276,17 @@ export function UsersGridView() {
enableColumnVisibility enableColumnVisibility
columnVisibility={columnVisibility} columnVisibility={columnVisibility}
onColumnVisibilityChange={setColumnVisibility} onColumnVisibilityChange={setColumnVisibility}
pinLastColumn
pageSize={5} pageSize={5}
initialSorting={[{ id: 'username', desc: false }]} initialSorting={[{ id: 'username', desc: false }]}
tableLayout={{
dense: true,
columnsResizable: true,
columnsMovable: true,
columnsVisibility: true,
columnsPinnable: false,
rowsPinnable: true,
}}
pinLastColumn={false}
emptyState={{ emptyState={{
title: 'Нет пользователей', title: 'Нет пользователей',
description: 'Создайте первый аккаунт Telemt.', description: 'Создайте первый аккаунт Telemt.',