+ {controls}
+ {toggles}
{search ?
: null}
- {controls || toggles ? (
-
- {controls}
- {toggles}
- {onReset ? : null}
-
- ) : null}
- {hasMeta ? (
-
- {chips?.length ?
: null}
- {shown != null && total != null ? (
-
- ) : resultsSuffix ? (
-
{resultsSuffix}
- ) : null}
+ {onReset ?
: null}
+ {filtered ?
: null}
+ {chips?.length ? (
+
+
) : null}
diff --git a/apps/web/src/components/reui-kit/data-grid-kit-defaults.test.ts b/apps/web/src/components/reui-kit/data-grid-kit-defaults.test.ts
index 4a63e69..9a077ec 100644
--- a/apps/web/src/components/reui-kit/data-grid-kit-defaults.test.ts
+++ b/apps/web/src/components/reui-kit/data-grid-kit-defaults.test.ts
@@ -5,9 +5,10 @@ import { BLOCKING_MATRIX_GRID } from '../censorcheck/blocking-grid'
import { GEO_MATRIX_GRID } from '../ipregion/geo-grid'
describe('kitDataGridTableLayout', () => {
- it('CRUD defaults: без bg-muted header и width fixed', () => {
+ it('CRUD defaults: подложка шапки, без зебры, width fixed', () => {
const layout = kitDataGridTableLayout()
- expect(layout.headerBackground).toBe(false)
+ expect(layout.headerBackground).toBe(true)
+ expect(layout.stripped).toBe(false)
expect(layout.width).toBe('fixed')
expect(layout.headerSticky).toBe(true)
expect(layout.columnsPinnable).toBe(false)
@@ -20,7 +21,8 @@ describe('kitDataGridTableLayout', () => {
})
expect(layout.width).toBe('auto')
expect(layout.columnsPinnable).toBe(true)
- expect(layout.headerBackground).toBe(false)
+ expect(layout.headerBackground).toBe(true)
+ expect(layout.stripped).toBe(false)
})
})
diff --git a/apps/web/src/components/reui-kit/frame-data-grid.tsx b/apps/web/src/components/reui-kit/frame-data-grid.tsx
index 82986ec..9945d5d 100644
--- a/apps/web/src/components/reui-kit/frame-data-grid.tsx
+++ b/apps/web/src/components/reui-kit/frame-data-grid.tsx
@@ -105,10 +105,11 @@ export function kitDataGridTableLayout(opts: {
} = {}) {
return {
dense: opts.dense ?? true,
- stripped: true,
+ // MikrotikManager v2 / макет `gh` + `rowv`: подложка у шапки, строки без зебры, разделитель.
+ stripped: false,
rowBorder: true,
headerSticky: true,
- headerBackground: false,
+ headerBackground: true,
headerBorder: true,
width: opts.width ?? ('fixed' as const),
columnsVisibility: opts.columnsVisibility ?? false,
@@ -146,6 +147,8 @@ export interface FrameDataGridProps
{
title?: ReactNode
description?: ReactNode
actions?: ReactNode
+ /** Поиск / фильтры страницы — в шапке рамки рядом с заголовком (раскладка MikrotikManager v2). */
+ toolbar?: ReactNode
columns: DataGridColumnDef[]
data: TData[]
/** Ключ строки — функция, возвращающая уникальный id. */
@@ -199,25 +202,29 @@ export interface FrameDataGridProps {
columnPinControls?: boolean
}
-function DataGridSectionHeader({
+/** Шапка рамки: заголовок слева, справа тулбар (фильтры, поиск) и действия — переносятся строкой ниже на узком экране. */
+export function DataGridSectionHeader({
title,
description,
actions,
+ toolbar,
}: {
title?: ReactNode
description?: ReactNode
actions?: ReactNode
+ toolbar?: ReactNode
}) {
- if (!title && !description && !actions) return null
+ if (!title && !description && !actions && !toolbar) return null
return (
-
-
+
+
{title ? {title} : null}
{description ? (
{description}
) : null}
+ {toolbar ? {toolbar}
: null}
{actions ? (
{actions}
) : null}
@@ -315,6 +322,7 @@ export function FrameDataGrid({
title,
description,
actions,
+ toolbar,
columns,
data,
rowId,
@@ -480,13 +488,13 @@ export function FrameDataGrid({
columnVisibilityAction
)
- const hasHeader = Boolean(title || description || actions || showColumnVisibilityTrigger)
+ const hasHeader = Boolean(title || description || actions || toolbar || showColumnVisibilityTrigger)
if (data.length === 0) {
return (
{hasHeader ? (
-
+
) : null}
@@ -516,7 +524,7 @@ export function FrameDataGrid({
return (
{hasHeader ? (
-
+
) : null}
{gridBody}
diff --git a/apps/web/src/components/reui-kit/resource-page.tsx b/apps/web/src/components/reui-kit/resource-page.tsx
index c8ea051..7bb86bc 100644
--- a/apps/web/src/components/reui-kit/resource-page.tsx
+++ b/apps/web/src/components/reui-kit/resource-page.tsx
@@ -210,13 +210,11 @@ function ResourcePageSimple({
return (
- {toolbarExtra ? (
-
{toolbarExtra}
- ) : null}
({
})}
>
- {!hideHeader ? (
-
-
+ {/* Раскладка MikrotikManager v2: заголовок, затем фильтры/поиск/сброс и действия в одной шапке. */}
+
+ {!hideHeader ? (
+
{title}
{description ? (
-
- {description}
-
-
- {filteredData.length} записей
-
- {selectedCount > 0 ? (
- <>
-
- {selectedCount} выбрано
- >
- ) : null}
-
+ {description}
) : null}
- {headerActions ? (
-
- {headerActions}
-
- ) : null}
-
- ) : null}
+ ) : (
+
+ )}
+ {showFilters ? (
+
+
+ Фильтры
+ {filters.length > 0 ? (
+
+ {filters.length}
+
+ ) : null}
+
+ }
+ />
+ ) : null}
+ {toolbarExtra}
+ {showFilters && onClearFilters && filters.length > 0 ? (
+
+ ) : null}
+ {headerActions && !hideHeader ? (
+ {headerActions}
+ ) : null}
+
{countedTabs.length > 0 ? (
@@ -477,41 +484,6 @@ function ResourcePageFiltered({
>
) : null}
-
- {showFilters ? (
-
-
- Фильтры
-
- }
- />
- ) : (
-
- )}
-
- {toolbarExtra}
- {selectedCount > 0 ? (
-
- {selectedCount} выбрано
-
- ) : null}
- {showFilters && onClearFilters ? (
-
- ) : null}
-
-
-
- {(showFilters || toolbarExtra || selectedCount > 0) ? : null}
-
diff --git a/apps/web/src/routes/_auth/accounts.tsx b/apps/web/src/routes/_auth/accounts.tsx
index 5ff5707..4daaaad 100644
--- a/apps/web/src/routes/_auth/accounts.tsx
+++ b/apps/web/src/routes/_auth/accounts.tsx
@@ -444,17 +444,21 @@ function AccountsPage() {
{() => (
{snapshot ?
: null}
- {snapshot ? (
-
- ) : null}
{health ?
: null}
+ ) : undefined
+ }
columns={columnDefFromDataGrid(columns)}
data={filteredAccounts}
getRowId={(a) => a.id}
diff --git a/apps/web/src/routes/_auth/balance.tsx b/apps/web/src/routes/_auth/balance.tsx
index 14ec304..96a9c2b 100644
--- a/apps/web/src/routes/_auth/balance.tsx
+++ b/apps/web/src/routes/_auth/balance.tsx
@@ -237,6 +237,8 @@ function BalancePage() {
]}
/>
r.id}
diff --git a/apps/web/src/routes/_auth/payments.tsx b/apps/web/src/routes/_auth/payments.tsx
index 7a7a477..e824b6d 100644
--- a/apps/web/src/routes/_auth/payments.tsx
+++ b/apps/web/src/routes/_auth/payments.tsx
@@ -247,6 +247,8 @@ function PaymentsPage() {
]}
/>
p.id}
diff --git a/apps/web/src/routes/_auth/projects.tsx b/apps/web/src/routes/_auth/projects.tsx
index 849d55a..098994f 100644
--- a/apps/web/src/routes/_auth/projects.tsx
+++ b/apps/web/src/routes/_auth/projects.tsx
@@ -26,7 +26,6 @@ import {
} from '@/components/project-filters'
import { Button } from '@cfdm/ui/components/button'
import { Badge } from '@/components/reui/badge'
-import { EmptyState } from '@/components/empty-state'
import { ProjectColorDot } from '@/components/project-color-dot'
import { ProjectEditSheet, projectFormDefaults } from '@/components/domain/project-edit-sheet'
import type { ProjectFormValues } from '@/lib/schemas'
@@ -277,35 +276,37 @@ function ProjectsPage() {
]}
/>
) : null}
-
- {rows.length === 0 && hasActiveProjectFilters(filters) ? (
-
+ }
+ columns={columnDefFromDataGrid(columns)}
+ data={rows}
+ getRowId={(r) => r.id}
+ emptyTitle={hasActiveProjectFilters(filters) ? 'Ничего не найдено' : 'Нет проектов'}
+ emptyDescription={hasActiveProjectFilters(filters) ? 'Измените фильтры или сбросьте их' : undefined}
+ emptyAction={
+ hasActiveProjectFilters(filters) ? (
- }
- />
- ) : (
- r.id}
- onRowClick={(row) =>
- void navigate({
- to: '/projects/$projectId',
- params: { projectId: row.id },
- })
- }
- />
- )}
+ ) : undefined
+ }
+ onRowClick={(row) =>
+ void navigate({
+ to: '/projects/$projectId',
+ params: { projectId: row.id },
+ })
+ }
+ />
)}
diff --git a/apps/web/src/routes/_auth/providers.tsx b/apps/web/src/routes/_auth/providers.tsx
index 8b51238..8ab5a97 100644
--- a/apps/web/src/routes/_auth/providers.tsx
+++ b/apps/web/src/routes/_auth/providers.tsx
@@ -150,6 +150,8 @@ function ProvidersPage() {
>
{(snap) => (
p.id}
diff --git a/apps/web/src/routes/_auth/sync-journal.tsx b/apps/web/src/routes/_auth/sync-journal.tsx
index 01d0a0a..7f42fe0 100644
--- a/apps/web/src/routes/_auth/sync-journal.tsx
+++ b/apps/web/src/routes/_auth/sync-journal.tsx
@@ -99,6 +99,8 @@ function SyncJournalPage() {
>
{(snap) => (
r.id}
diff --git a/apps/web/src/routes/_auth/tariffs.tsx b/apps/web/src/routes/_auth/tariffs.tsx
index bc89acf..ed2e223 100644
--- a/apps/web/src/routes/_auth/tariffs.tsx
+++ b/apps/web/src/routes/_auth/tariffs.tsx
@@ -385,8 +385,10 @@ function TariffsPage() {
) : null}
- {toolbar}
t.id}
diff --git a/apps/web/src/routes/_auth/vps.tsx b/apps/web/src/routes/_auth/vps.tsx
index b7f9857..3cd4fe3 100644
--- a/apps/web/src/routes/_auth/vps.tsx
+++ b/apps/web/src/routes/_auth/vps.tsx
@@ -344,6 +344,9 @@ function VpsPage() {
key: 'ip',
header: 'IP / DNS',
icon: GlobeIcon,
+ // Полный IPv4 + иконка не помещались в равную долю fixed-таблицы и обрезались.
+ size: 240,
+ minSize: 220,
sortValue: (v) => v.ip || v.dns || '',
cell: (v) => (
void copyText(v.ip, 'IP скопирован')}
>
{v.ip}
@@ -654,17 +657,8 @@ function VpsPage() {
)
}
- return (
-
- {health ?
: null}
-
bulkMutation.mutate({ ids: selectedIds, action: 'status', value: status })}
- onSetProject={(project) => bulkMutation.mutate({ ids: selectedIds, action: 'project', value: project })}
- onDelete={() => bulkMutation.mutate({ ids: selectedIds, action: 'delete' })}
- />
+ // Один набор фильтров: в шапке таблицы (MikrotikManager v2), при группировке — над секциями.
+ const filtersToolbar = (
+ )
+
+ return (
+
+ {health ? : null}
+ bulkMutation.mutate({ ids: selectedIds, action: 'status', value: status })}
+ onSetProject={(project) => bulkMutation.mutate({ ids: selectedIds, action: 'project', value: project })}
+ onDelete={() => bulkMutation.mutate({ ids: selectedIds, action: 'delete' })}
+ />
+ {tableSections.length > 1 ? filtersToolbar : null}
{tableSections.map((section) => (