diff --git a/apps/web/src/components/reui-kit/resource-page.tsx b/apps/web/src/components/reui-kit/resource-page.tsx index 2771484..fa255c0 100644 --- a/apps/web/src/components/reui-kit/resource-page.tsx +++ b/apps/web/src/components/reui-kit/resource-page.tsx @@ -1,10 +1,11 @@ -import { useCallback, useMemo, useState, type ReactNode } from 'react' +import { useCallback, useMemo, useState, type ComponentProps, type ReactNode } from 'react' import { getCoreRowModel, getPaginationRowModel, getSortedRowModel, useReactTable, type ColumnDef, + type ColumnPinningState, type PaginationState, type RowSelectionState, type SortingState, @@ -46,6 +47,10 @@ import { import { EmptyState } from '@/components/empty-state' import { applyFiltersToData } from './filter-utils' +type DataGridTableLayout = NonNullable< + ComponentProps['tableLayout'] +> + export interface ResourcePageTab { id: string label: string @@ -88,6 +93,10 @@ export interface ResourcePageProps { onSearchChange?: (query: string) => void searchPlaceholder?: string getSearchText?: (item: T) => string + /** Merge over defaults: dense + headerSticky + width fixed */ + tableLayout?: Partial + /** Pin columns (e.g. `{ right: ['actions'] }`) — enables column pinning */ + columnPinning?: ColumnPinningState } function ResourcePageSkeleton() { @@ -140,12 +149,17 @@ export function ResourcePage({ onSearchChange, searchPlaceholder = 'Поиск…', getSearchText, + tableLayout: tableLayoutProp, + columnPinning: columnPinningProp, }: ResourcePageProps) { const [internalTab, setInternalTab] = useState(tabs?.[0]?.id ?? 'all') const activeTab = controlledTab ?? internalTab const [sorting, setSorting] = useState([]) const [rowSelection, setRowSelection] = useState({}) + const [columnPinning, setColumnPinning] = useState( + () => columnPinningProp ?? {}, + ) const [pagination, setPagination] = useState({ pageIndex: 0, pageSize, @@ -213,15 +227,32 @@ export function ResourcePage({ setRowSelection({}) }, []) + const tableLayout = useMemo( + () => ({ + dense: true, + headerSticky: true, + width: 'fixed' as const, + ...tableLayoutProp, + ...(columnPinningProp || Object.keys(columnPinning).length > 0 + ? { columnsPinnable: true } + : {}), + }), + [tableLayoutProp, columnPinningProp, columnPinning], + ) + const table = useReactTable({ data: filteredData, columns, getRowId, - state: { sorting, rowSelection, pagination }, + state: { sorting, rowSelection, pagination, columnPinning }, enableRowSelection, + enableColumnPinning: Boolean( + tableLayout.columnsPinnable || columnPinningProp, + ), onSortingChange: setSorting, onRowSelectionChange: setRowSelection, onPaginationChange: setPagination, + onColumnPinningChange: setColumnPinning, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), getPaginationRowModel: getPaginationRowModel(), @@ -338,7 +369,7 @@ export function ResourcePage({ table={table} recordCount={filteredData.length} emptyMessage={emptyMessage} - tableLayout={{ dense: true }} + tableLayout={tableLayout} onRowClick={onRowClick} > diff --git a/apps/web/src/routes/_auth/agents/index.tsx b/apps/web/src/routes/_auth/agents/index.tsx index 245d67b..f410187 100644 --- a/apps/web/src/routes/_auth/agents/index.tsx +++ b/apps/web/src/routes/_auth/agents/index.tsx @@ -76,6 +76,20 @@ function formatPackets(n: number | undefined, hasApply: boolean): string { return packetFmt.format(n) } +const seenFmt = new Intl.DateTimeFormat('ru-RU', { + day: '2-digit', + month: '2-digit', + hour: '2-digit', + minute: '2-digit', +}) + +function formatAgentSeen(iso: string | null | undefined): string { + if (!iso) return '—' + const t = Date.parse(iso) + if (Number.isNaN(t)) return '—' + return seenFmt.format(t) +} + /** * Agents ops console — Solutions Agents DNA. * Preview: https://reui.io/preview/base/solution-agents-1 · stats-12 · data-grid-filtering-2 @@ -265,6 +279,9 @@ function AgentsPage() { () => [ { accessorKey: 'name', + size: 220, + minSize: 160, + maxSize: 320, header: ({ column }) => ( ), @@ -284,6 +301,9 @@ function AgentsPage() { }, { accessorKey: 'status', + size: 110, + minSize: 100, + maxSize: 130, header: ({ column }) => ( ), @@ -291,6 +311,9 @@ function AgentsPage() { }, { id: 'apply', + size: 90, + minSize: 80, + maxSize: 110, accessorFn: (row) => row.last_apply_status ?? '', header: ({ column }) => ( @@ -316,6 +339,9 @@ function AgentsPage() { }, { id: 'dropped', + size: 90, + minSize: 80, + maxSize: 110, accessorFn: (row) => row.last_apply_packets_dropped ?? -1, header: ({ column }) => ( @@ -346,6 +372,9 @@ function AgentsPage() { }, { id: 'accepted', + size: 90, + minSize: 80, + maxSize: 110, accessorFn: (row) => row.last_apply_packets_accepted ?? -1, header: ({ column }) => ( @@ -376,6 +405,9 @@ function AgentsPage() { }, { id: 'install', + size: 100, + minSize: 90, + maxSize: 120, enableSorting: false, header: ({ column }) => ( @@ -392,13 +424,13 @@ function AgentsPage() {