fix(web): привести таблицы к виду MikrotikManager — тулбар, сегменты, шапка и ячейки
quality / changes (push) Successful in 8s
quality / api (push) Skipped
quality / commitlint (push) Skipped
quality / docker-check (push) Skipped
quality / web (push) Successful in 1m2s
CD / quality (push) Successful in 1m12s
CD / publish (push) Successful in 1m31s

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
DenozordecandClaude Sonnet 5.5 committed 2026-10-23 14:43:46 +07:00
1 parent e7b34ce6ae
commit 0948455d15
4 files changed
+253 -161

No files matched your search

@@ -0,0 +1,26 @@
import type { DataGridFeatures, DataGridProps } from '@/components/reui/data-grid/data-grid'
export const DATA_GRID_CELL_PAD = 'py-3'
export const DATA_GRID_CELL_PAD_FIRST = 'pl-5 py-3'
export const DATA_GRID_CELL_PAD_LAST = 'pr-4 py-3'
export const DATA_GRID_LAYOUT: NonNullable<DataGridProps<DataGridFeatures, object>['tableLayout']> = {
rowBorder: true,
headerBackground: true,
headerBorder: true,
columnsResizable: false,
}
export const DATA_GRID_CLASS_NAMES: NonNullable<DataGridProps<DataGridFeatures, object>['tableClassNames']> = {
headerRow: 'border-b border-border',
bodyRow: 'group/row',
}
export const DATA_GRID_CONTAINER_CLASS = 'w-full rounded-none border-0'
/** Отступы ячейки по положению колонки: первая — pl-5, последняя — pr-4. */
export function cellPad(index: number, count: number) {
if (index === 0) return DATA_GRID_CELL_PAD_FIRST
if (index === count - 1) return DATA_GRID_CELL_PAD_LAST
return DATA_GRID_CELL_PAD
}
@@ -0,0 +1,123 @@
/**
* Каркас списочной страницы как в MikrotikManager (/servers): карточка Frame без полей, тулбар
* «сегменты · фильтры · поиск · счётчик», таблица с подложкой шапки и ячейками py-3.
*/
import type { ReactNode } from 'react'
import type { Column } from '@tanstack/react-table'
import { ArrowDownIcon, ArrowUpDownIcon, ArrowUpIcon, SearchIcon } from 'lucide-react'
import { Frame, FramePanel } from '@/components/reui/frame'
import { InputGroup, InputGroupAddon, InputGroupInput } from '@authportal/ui/components/input-group'
import { cn } from '@authportal/ui/lib/utils'
import type { DataGridFeatures } from '@/components/reui/data-grid/data-grid'
export function DataPageCard({ children, className }: { children: ReactNode; className?: string }) {
return (
<Frame dense className={cn('w-full min-w-0', className)}>
<FramePanel className="flex w-full min-w-0 flex-col gap-0 overflow-hidden p-0 py-0 shadow-none">{children}</FramePanel>
</Frame>
)
}
export function SegmentedControl<T extends string>({
value,
onChange,
options,
label,
}: {
value: T
onChange: (value: T) => void
options: { value: T; label: string; count?: number }[]
label?: string
}) {
return (
<div role="group" aria-label={label} className="border-border bg-muted/40 flex w-fit items-center gap-1 rounded-md border p-0.5">
{options.map((o) => (
<button
key={o.value}
type="button"
aria-pressed={value === o.value}
onClick={() => onChange(o.value)}
className={cn(
'flex items-center gap-1.5 rounded px-3 py-1 text-sm transition-colors',
value === o.value ? 'bg-background text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground',
)}
>
{o.label}
{o.count != null ? <span className="text-xs tabular-nums">{o.count}</span> : null}
</button>
))}
</div>
)
}
export function DataPageToolbar({
leading,
segmented,
filterControl,
search,
onSearchChange,
searchPlaceholder = 'Поиск…',
countLabel,
actions,
}: {
leading?: ReactNode
segmented?: { value: string; onChange: (value: string) => void; options: { value: string; label: string; count?: number }[]; label?: string }
filterControl?: ReactNode
search?: string
onSearchChange?: (value: string) => void
searchPlaceholder?: string
countLabel?: string
actions?: ReactNode
}) {
return (
<div className="flex flex-wrap items-center gap-3 border-b px-5 py-3">
{leading}
{segmented ? <SegmentedControl {...segmented} /> : null}
{filterControl}
{onSearchChange ? (
<InputGroup className="max-w-sm min-w-[220px]">
<InputGroupAddon>
<SearchIcon className="size-3.5" aria-hidden />
</InputGroupAddon>
<InputGroupInput aria-label={searchPlaceholder} placeholder={searchPlaceholder} value={search ?? ''} onChange={(e) => onSearchChange(e.target.value)} />
</InputGroup>
) : null}
{countLabel ? <span className="text-muted-foreground ml-auto text-sm">{countLabel}</span> : null}
{actions}
</div>
)
}
export function DataGridSortHeader<TData extends object>({
column,
title,
className,
}: {
column: Column<DataGridFeatures, TData, unknown>
title: string
className?: string
}) {
const sorted = column.getIsSorted()
if (!column.getCanSort()) {
return <span className={cn('text-muted-foreground text-xs font-medium', className)}>{title}</span>
}
return (
<button
type="button"
className={cn(
'text-muted-foreground hover:text-foreground focus-visible:outline-ring -ml-1 inline-flex items-center gap-1.5 rounded-md px-1 py-0.5 text-xs font-medium transition-colors focus-visible:outline-2 focus-visible:outline-offset-2',
className,
)}
onClick={column.getToggleSortingHandler()}
>
{title}
{sorted === 'asc' ? (
<ArrowUpIcon aria-hidden className="text-foreground size-3" />
) : sorted === 'desc' ? (
<ArrowDownIcon aria-hidden className="text-foreground size-3" />
) : (
<ArrowUpDownIcon aria-hidden className="size-3 opacity-35" />
)}
</button>
)
}
+66 -75
View File
@@ -1,10 +1,9 @@
import { useMemo, useState, type ReactNode } from 'react' import { useMemo, useState, type ReactNode } from 'react'
import { Columns3Icon, MoreHorizontalIcon, SearchIcon } from 'lucide-react' import { Columns3Icon, MoreHorizontalIcon } from 'lucide-react'
import { toast } from 'sonner' import { toast } from 'sonner'
import { useTable, type ColumnDef, type RowSelectionState, type SortingState } from '@tanstack/react-table' import { useTable, type ColumnDef, type RowSelectionState, type SortingState } from '@tanstack/react-table'
import { DATA_GRID_RU } from '@/lib/reui-ru' import { DATA_GRID_RU } from '@/lib/reui-ru'
import { DataGrid, dataGridFeatures, type DataGridFeatures } from '@/components/reui/data-grid/data-grid' import { DataGrid, dataGridFeatures, type DataGridFeatures } from '@/components/reui/data-grid/data-grid'
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
import { DataGridPagination } from '@/components/reui/data-grid/data-grid-pagination' import { DataGridPagination } from '@/components/reui/data-grid/data-grid-pagination'
import { DataGridScrollArea } from '@/components/reui/data-grid/data-grid-scroll-area' import { DataGridScrollArea } from '@/components/reui/data-grid/data-grid-scroll-area'
import { import {
@@ -17,7 +16,6 @@ import {
DataGridTableRowSelect, DataGridTableRowSelect,
DataGridTableRowSelectAll, DataGridTableRowSelectAll,
} from '@/components/reui/data-grid/data-grid-table' } from '@/components/reui/data-grid/data-grid-table'
import { Frame, FrameDescription, FrameFooter, FrameHeader, FramePanel, FrameTitle } from '@/components/reui/frame'
import { Button } from '@authportal/ui/components/button' import { Button } from '@authportal/ui/components/button'
import { import {
DropdownMenu, DropdownMenu,
@@ -26,12 +24,22 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@authportal/ui/components/dropdown-menu' } from '@authportal/ui/components/dropdown-menu'
import { InputGroup, InputGroupAddon, InputGroupInput } from '@authportal/ui/components/input-group'
import { Separator } from '@authportal/ui/components/separator'
import { Switch } from '@authportal/ui/components/switch' import { Switch } from '@authportal/ui/components/switch'
import { Tabs, TabsList, TabsTrigger } from '@authportal/ui/components/tabs' import { Tabs, TabsList, TabsTrigger } from '@authportal/ui/components/tabs'
import { cn } from '@authportal/ui/lib/utils' import { cn } from '@authportal/ui/lib/utils'
import { StatusBadge } from './bits' import { StatusBadge } from './bits'
import {
DATA_GRID_CELL_PAD,
DATA_GRID_CELL_PAD_FIRST,
DATA_GRID_CELL_PAD_LAST,
DATA_GRID_CLASS_NAMES,
DATA_GRID_LAYOUT,
} from './data-page-layout'
import {
DataGridSortHeader,
DataPageCard,
DataPageToolbar,
} from './data-page'
import type { Tone } from './tone' import type { Tone } from './tone'
import type { RowMark } from './tone' import type { RowMark } from './tone'
import { TONE_DOT } from './tone' import { TONE_DOT } from './tone'
@@ -101,8 +109,7 @@ export function SectionGrid<T extends object>({
rowMenu, rowMenu,
onReorder, onReorder,
rowStatus, rowStatus,
pageSize = 8, pageSize = 50,
footer,
emptyMessage = 'Нет записей по текущим фильтрам.', emptyMessage = 'Нет записей по текущим фильтрам.',
className, className,
}: { }: {
@@ -194,20 +201,22 @@ export function SectionGrid<T extends object>({
enableSorting: false, enableSorting: false,
enableResizing: false, enableResizing: false,
size: SELECT_COLUMN_SIZE, size: SELECT_COLUMN_SIZE,
meta: { headerClassName: 'pl-5 py-3', cellClassName: 'pl-5 py-3' },
}) })
} }
for (const col of visibleColumns) { visibleColumns.forEach((col, i) => {
const pad = i === 0 && !selectable && !onReorder ? DATA_GRID_CELL_PAD_FIRST : DATA_GRID_CELL_PAD
list.push({ list.push({
id: col.id, id: col.id,
accessorFn: col.sortValue ?? (() => ''), accessorFn: col.sortValue ?? (() => ''),
header: ({ column }) => <DataGridColumnHeader title={col.title} column={column} />, header: ({ column }) => <DataGridSortHeader column={column} title={col.title} />,
cell: ({ row }) => col.cell(row.original), cell: ({ row }) => col.cell(row.original),
enableSorting: Boolean(col.sortValue), enableSorting: Boolean(col.sortValue),
enableResizing: false, enableResizing: false,
size: col.size, size: col.size,
meta: { headerTitle: col.title }, meta: { headerTitle: col.title, headerClassName: pad, cellClassName: pad },
})
}) })
}
list.push({ list.push({
id: '__actions', id: '__actions',
header: () => <span className="sr-only">Действия</span>, header: () => <span className="sr-only">Действия</span>,
@@ -242,6 +251,7 @@ export function SectionGrid<T extends object>({
enableSorting: false, enableSorting: false,
enableResizing: false, enableResizing: false,
size: ACTIONS_COLUMN_SIZE, size: ACTIONS_COLUMN_SIZE,
meta: { headerClassName: DATA_GRID_CELL_PAD_LAST, cellClassName: DATA_GRID_CELL_PAD_LAST },
}) })
return list return list
}, [visibleColumns, selectable, onReorder, rowSwitch, rowMenu, onRowClick]) }, [visibleColumns, selectable, onReorder, rowSwitch, rowMenu, onRowClick])
@@ -277,6 +287,8 @@ export function SectionGrid<T extends object>({
const paged = data.length > pageSize const paged = data.length > pageSize
const showCards = Boolean(card) && cards const showCards = Boolean(card) && cards
const countLabel = `${data.length} ${data.length === rows.length ? 'записей' : `из ${rows.length}`}`
return ( return (
<DataGrid <DataGrid
table={table} table={table}
@@ -285,16 +297,27 @@ export function SectionGrid<T extends object>({
onRowClick={onRowClick} onRowClick={onRowClick}
getRowStatus={rowStatus} getRowStatus={rowStatus}
emptyMessage={emptyMessage} emptyMessage={emptyMessage}
tableLayout={{ headerSticky: false, dense: false, width: 'auto', rowsDraggable: Boolean(onReorder) }} tableLayout={{ ...DATA_GRID_LAYOUT, rowsDraggable: Boolean(onReorder) }}
tableClassNames={DATA_GRID_CLASS_NAMES}
> >
<Frame className={cn('[--frame-panel-header-py-adjust:4px] w-full min-w-0', className)}> <DataPageCard className={className}>
<FrameHeader className="flex-row flex-wrap items-center gap-x-3 gap-y-2"> <h2 className="sr-only">{title}</h2>
<div className="flex min-w-0 flex-1 basis-52 flex-col gap-0.5"> <DataPageToolbar
<FrameTitle>{title}</FrameTitle> segmented={
<FrameDescription className="text-xs text-pretty">{description}</FrameDescription> views
</div> ? {
{typeof tools === 'function' ? tools(selectedIds) : tools} value: String(view),
{card ? ( label: description,
onChange: (v) => {
setView(Number(v))
setPagination((p) => ({ ...p, pageIndex: 0 }))
},
options: views.map((v, i) => ({ value: String(i), label: v.label, count: rows.filter(v.match).length })),
}
: undefined
}
leading={
card ? (
<Tabs value={cards ? 'cards' : 'table'} onValueChange={(v) => setCards(v === 'cards')} aria-label="Вид списка"> <Tabs value={cards ? 'cards' : 'table'} onValueChange={(v) => setCards(v === 'cards')} aria-label="Вид списка">
<TabsList className="h-9! gap-0.5 p-[3px]"> <TabsList className="h-9! gap-0.5 p-[3px]">
<TabsTrigger value="table" className="h-7! px-2.5 text-[12.5px]"> <TabsTrigger value="table" className="h-7! px-2.5 text-[12.5px]">
@@ -305,23 +328,22 @@ export function SectionGrid<T extends object>({
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
</Tabs> </Tabs>
) : null} ) : undefined
{searchText ? ( }
<InputGroup className="w-[220px] max-w-full"> search={query}
<InputGroupAddon> onSearchChange={
<SearchIcon aria-hidden /> searchText
</InputGroupAddon> ? (v) => {
<InputGroupInput setQuery(v)
value={query}
placeholder="Поиск"
aria-label={searchPlaceholder}
onChange={(e) => {
setQuery(e.target.value)
setPagination((p) => ({ ...p, pageIndex: 0 })) setPagination((p) => ({ ...p, pageIndex: 0 }))
}} }
/> : undefined
</InputGroup> }
) : null} searchPlaceholder={searchPlaceholder}
countLabel={countLabel}
actions={
<>
{typeof tools === 'function' ? tools(selectedIds) : tools}
{columns.length > 1 ? ( {columns.length > 1 ? (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger <DropdownMenuTrigger
@@ -331,36 +353,16 @@ export function SectionGrid<T extends object>({
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="min-w-52"> <DropdownMenuContent align="end" className="min-w-52">
{columns.slice(1).map((c) => ( {columns.slice(1).map((c) => (
<DropdownMenuCheckboxItem <DropdownMenuCheckboxItem key={c.id} checked={!hidden[c.id]} onCheckedChange={(v) => setHidden((h) => ({ ...h, [c.id]: !v }))}>
key={c.id}
checked={!hidden[c.id]}
onCheckedChange={(v) => setHidden((h) => ({ ...h, [c.id]: !v }))}
>
{c.title} {c.title}
</DropdownMenuCheckboxItem> </DropdownMenuCheckboxItem>
))} ))}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
) : null} ) : null}
</FrameHeader> </>
<FramePanel className="min-w-0 p-0 shadow-none"> }
{views ? ( />
<Tabs
value={String(view)}
onValueChange={(v) => {
setView(Number(v))
setPagination((p) => ({ ...p, pageIndex: 0 }))
}}
>
<TabsList variant="line" aria-label="Сохранённые виды" className="h-10! w-full justify-start gap-4 overflow-x-auto px-4">
{views.map((v, i) => (
<TabsTrigger key={v.label} value={String(i)} className="px-1 text-[13px]">
{v.label} · {rows.filter(v.match).length}
</TabsTrigger>
))}
</TabsList>
</Tabs>
) : null}
{selectable && selectedIds.length > 0 ? ( {selectable && selectedIds.length > 0 ? (
<div <div
role="toolbar" role="toolbar"
@@ -461,24 +463,13 @@ export function SectionGrid<T extends object>({
</ul> </ul>
) : ( ) : (
<DataGridScrollArea> <DataGridScrollArea>
<div className="pb-3" style={{ minWidth }}> <div style={{ minWidth }}>
{onReorder ? ( {onReorder ? <DataGridTableDndRows dataIds={dataIds} handleDragEnd={handleDragEnd} /> : <DataGridTable />}
<DataGridTableDndRows dataIds={dataIds} handleDragEnd={handleDragEnd} />
) : (
<DataGridTable />
)}
</div> </div>
</DataGridScrollArea> </DataGridScrollArea>
)} )}
<Separator /> {paged && !showCards ? <DataGridPagination className="px-5 py-3" sizes={[8, 16, 32]} /> : null}
<FrameFooter className="flex-wrap justify-between gap-2"> </DataPageCard>
<span className="text-muted-foreground text-xs">
{footer ?? `Показано ${showCards ? data.length : table.getRowModel().rows.length} из ${data.length}`}
</span>
{paged && !showCards ? <DataGridPagination sizes={[8, 16, 32]} /> : null}
</FrameFooter>
</FramePanel>
</Frame>
</DataGrid> </DataGrid>
) )
} }
@@ -44,6 +44,13 @@ import {
DataGridTableRowSelectAll, DataGridTableRowSelectAll,
} from '@/components/reui/data-grid/data-grid-table' } from '@/components/reui/data-grid/data-grid-table'
import { DATA_GRID_RU, FILTERS_RU, FILTER_OPERATORS_RU } from '@/lib/reui-ru' import { DATA_GRID_RU, FILTERS_RU, FILTER_OPERATORS_RU } from '@/lib/reui-ru'
import { cellPad } from '@/components/portal/data-page-layout'
import {
DATA_GRID_CLASS_NAMES,
DATA_GRID_LAYOUT,
} from '@/components/portal/data-page-layout'
import { DataPageCard, DataPageToolbar } from '@/components/portal/data-page'
import { cn } from '@authportal/ui/lib/utils'
import { Filters } from '@/components/reui/filters/filters' import { Filters } from '@/components/reui/filters/filters'
import { import {
createFilterQuery, createFilterQuery,
@@ -54,14 +61,6 @@ import type {
FilterField, FilterField,
FilterQuery, FilterQuery,
} from '@/components/reui/filters/filters-types' } from '@/components/reui/filters/filters-types'
import {
Frame,
FrameDescription,
FrameFooter,
FrameHeader,
FramePanel,
FrameTitle,
} from '@/components/reui/frame'
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
@@ -93,7 +92,6 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '@authportal/ui/components/select' } from '@authportal/ui/components/select'
import { Separator } from '@authportal/ui/components/separator'
import { Skeleton } from '@authportal/ui/components/skeleton' import { Skeleton } from '@authportal/ui/components/skeleton'
import { TooltipProvider } from '@authportal/ui/components/tooltip' import { TooltipProvider } from '@authportal/ui/components/tooltip'
@@ -201,15 +199,6 @@ function formatLastActive(iso: string | null) {
}).format(date) }).format(date)
} }
function DotSeparator() {
return (
<span
className="bg-muted-foreground/40 size-1 shrink-0 rounded-full"
aria-hidden="true"
/>
)
}
function RoleBadge({ isAdmin }: { isAdmin: boolean }) { function RoleBadge({ isAdmin }: { isAdmin: boolean }) {
return isAdmin ? ( return isAdmin ? (
<Badge variant="info-outline">Админ</Badge> <Badge variant="info-outline">Админ</Badge>
@@ -710,15 +699,6 @@ export function AdminUsersGrid({
const [bulkRole, setBulkRole] = useState<BulkRole>('user') const [bulkRole, setBulkRole] = useState<BulkRole>('user')
const lastAppliedKey = useRef<string>(serializeConditionsKey([])) const lastAppliedKey = useRef<string>(serializeConditionsKey([]))
const stats = useMemo(
() => ({
total: users.length,
admins: users.filter((u) => u.is_admin).length,
disabled: users.filter((u) => u.disabled).length,
}),
[users],
)
const filterFields: FilterField[] = useMemo( const filterFields: FilterField[] = useMemo(
() => [ () => [
{ {
@@ -778,6 +758,16 @@ export function AdminUsersGrid({
onDeactivate, onDeactivate,
onDelete, onDelete,
onResetPasskeys, onResetPasskeys,
}).map((col, i, all) => {
const pad = cellPad(i, all.length)
return {
...col,
meta: {
...col.meta,
headerClassName: cn(col.meta?.headerClassName, pad),
cellClassName: cn(col.meta?.cellClassName, pad),
},
}
}), }),
[onOpenAudit, onOpenAccess, onDeactivate, onDelete, onResetPasskeys], [onOpenAudit, onOpenAccess, onDeactivate, onDelete, onResetPasskeys],
) )
@@ -850,38 +840,17 @@ export function AdminUsersGrid({
: undefined : undefined
} }
tableLayout={{ tableLayout={{
...DATA_GRID_LAYOUT,
columnsResizable: true, columnsResizable: true,
columnsMovable: true, columnsMovable: true,
columnsVisibility: true, columnsVisibility: true,
headerSticky: true, headerSticky: true,
dense: true,
}} }}
tableClassNames={DATA_GRID_CLASS_NAMES}
> >
<Frame spacing="sm" className="w-full"> <DataPageCard>
<FrameHeader className="flex-row items-center justify-between gap-3"> <DataPageToolbar
<div className="flex flex-col gap-0.5"> filterControl={
<FrameTitle id="page-heading" className="text-balance">
Пользователи
</FrameTitle>
<FrameDescription className="flex items-center gap-1.5 text-xs text-pretty">
<span>{stats.total} учётных записей</span>
<DotSeparator />
<span>{stats.admins} админов</span>
{stats.disabled > 0 ? (
<>
<DotSeparator />
<span>{stats.disabled} отключено</span>
</>
) : null}
</FrameDescription>
</div>
<Button type="button" size="default" onClick={onCreateClick}>
<UserPlusIcon aria-hidden="true" />
Создать
</Button>
</FrameHeader>
<FramePanel className="p-0 shadow-none">
<div className="flex flex-wrap items-center justify-between gap-2 px-(--frame-panel-header-px) py-(--frame-panel-header-py)">
<Filters <Filters
query={query} query={query}
fields={filterFields} fields={filterFields}
@@ -890,27 +859,18 @@ export function AdminUsersGrid({
operatorLabels={FILTER_OPERATORS_RU} operatorLabels={FILTER_OPERATORS_RU}
size="default" size="default"
trigger={ trigger={
<Button <Button type="button" size="default" variant="outline" aria-label="Фильтры">
type="button"
size="default"
variant="outline"
aria-label="Фильтры"
>
<FilterIcon aria-hidden /> <FilterIcon aria-hidden />
Фильтры Фильтры
</Button> </Button>
} }
/> />
<div className="flex flex-wrap items-center gap-2"> }
countLabel={`${filteredData.length} пользователей`}
actions={
<>
{showClearButton ? ( {showClearButton ? (
<Button <Button type="button" size="default" variant="outline" className="shrink-0" onClick={handleClearFilters} disabled={gridLoading}>
type="button"
size="default"
variant="outline"
className="shrink-0"
onClick={handleClearFilters}
disabled={gridLoading}
>
<FunnelXIcon aria-hidden /> <FunnelXIcon aria-hidden />
Сбросить Сбросить
</Button> </Button>
@@ -918,22 +878,20 @@ export function AdminUsersGrid({
<DataGridColumnVisibility <DataGridColumnVisibility
table={table} table={table}
trigger={ trigger={
<Button <Button type="button" size="default" variant="outline" aria-label="Настройки таблицы">
type="button"
size="default"
variant="outline"
aria-label="Настройки таблицы"
>
<Settings2Icon aria-hidden="true" /> <Settings2Icon aria-hidden="true" />
Колонки Колонки
</Button> </Button>
} }
/> />
</div> <Button type="button" size="default" onClick={onCreateClick}>
</div> <UserPlusIcon aria-hidden="true" />
<Separator /> Создать
</Button>
</>
}
/>
{selectedCount > 0 ? ( {selectedCount > 0 ? (
<>
<AdminUsersBulkActionBar <AdminUsersBulkActionBar
selectedCount={selectedCount} selectedCount={selectedCount}
roleValue={bulkRole} roleValue={bulkRole}
@@ -942,18 +900,12 @@ export function AdminUsersGrid({
onDeactivate={handleBulkDeactivate} onDeactivate={handleBulkDeactivate}
onClear={handleClearSelection} onClear={handleClearSelection}
/> />
<Separator />
</>
) : null} ) : null}
<DataGridScrollArea> <DataGridScrollArea>
<DataGridTable /> <DataGridTable />
</DataGridScrollArea> </DataGridScrollArea>
<Separator /> <DataGridPagination className="px-5 py-3" sizes={[10, 20, 30]} />
<FrameFooter> </DataPageCard>
<DataGridPagination sizes={[10, 20, 30]} />
</FrameFooter>
</FramePanel>
</Frame>
</DataGrid> </DataGrid>
</TooltipProvider> </TooltipProvider>
) )