fix(ui): перенести таблицы администрирования и сортировать бэкапы по байтам
This commit is contained in:
1 parent
583b23ded1
commit
d47f7ab162
8 files changed
+135
-50
No files matched your search
@@ -90,6 +90,7 @@ export default function BackupsPage() {
|
|||||||
serverId: b.serverId == null ? null : String(b.serverId),
|
serverId: b.serverId == null ? null : String(b.serverId),
|
||||||
filename: b.filename,
|
filename: b.filename,
|
||||||
size: `${kb} КБ`,
|
size: `${kb} КБ`,
|
||||||
|
sizeBytes: b.sizeBytes,
|
||||||
created: new Date(b.createdAt).toLocaleString("ru-RU"),
|
created: new Date(b.createdAt).toLocaleString("ru-RU"),
|
||||||
kind: b.kind,
|
kind: b.kind,
|
||||||
notes: b.notes ?? "",
|
notes: b.notes ?? "",
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
|
import { dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid"
|
||||||
|
|
||||||
import { useMemo, type ReactNode } from "react"
|
import { useMemo, type ReactNode } from "react"
|
||||||
import {
|
import {
|
||||||
type ColumnDef,
|
type ColumnDef,
|
||||||
getCoreRowModel,
|
useTable,
|
||||||
getSortedRowModel,
|
} from "@tanstack/react-table"
|
||||||
useReactTable,
|
|
||||||
} from "@tanstack/react-table-v8"
|
|
||||||
import type { Backup } from "@/lib/data"
|
import type { Backup } from "@/lib/data"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Badge } from "@/components/reui/badge"
|
import { Badge } from "@/components/reui/badge"
|
||||||
import { IconTile } from "@/components/reui/icon-tile"
|
import { IconTile } from "@/components/reui/icon-tile"
|
||||||
import { DataGridShell } from "@/components/data-grids/shared/legacy-data-grid-shell"
|
import { DataGridShell } from "@/components/data-grids/shared/data-grid-shell"
|
||||||
import {
|
import {
|
||||||
DATA_GRID_CELL_PAD,
|
DATA_GRID_CELL_PAD,
|
||||||
DATA_GRID_CELL_PAD_FIRST,
|
DATA_GRID_CELL_PAD_FIRST,
|
||||||
DATA_GRID_CELL_PAD_LAST,
|
DATA_GRID_CELL_PAD_LAST,
|
||||||
} from "@/components/data-grids/shared/legacy-data-grid-layout"
|
} from "@/components/data-grids/shared/data-grid-layout"
|
||||||
import { DataGridSortHeader } from "@/components/data-grids/shared/legacy-data-grid-sort-header"
|
import { DataGridSortHeader } from "@/components/data-grids/shared/data-grid-sort-header"
|
||||||
import { EmptyState } from "@/components/empty-state"
|
import { EmptyState } from "@/components/empty-state"
|
||||||
import {
|
import {
|
||||||
CloudIcon,
|
CloudIcon,
|
||||||
@@ -52,7 +52,7 @@ function BackupsDataGrid({
|
|||||||
emptyTitle = "Нет бэкапов",
|
emptyTitle = "Нет бэкапов",
|
||||||
emptyDescription = "Создайте первый бэкап вручную или настройте расписание",
|
emptyDescription = "Создайте первый бэкап вручную или настройте расписание",
|
||||||
}: BackupsDataGridProps) {
|
}: BackupsDataGridProps) {
|
||||||
const columns = useMemo<ColumnDef<Backup>[]>(
|
const columns = useMemo<ColumnDef<DataGridFeatures, Backup>[]>(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
id: "filename",
|
id: "filename",
|
||||||
@@ -100,7 +100,8 @@ function BackupsDataGrid({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "size",
|
id: "size",
|
||||||
accessorKey: "size",
|
accessorFn: (row) => row.sizeBytes,
|
||||||
|
sortUndefined: "last",
|
||||||
header: ({ column }) => <DataGridSortHeader column={column} title="Размер" />,
|
header: ({ column }) => <DataGridSortHeader column={column} title="Размер" />,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<span className="font-mono text-xs text-muted-foreground">{row.original.size}</span>
|
<span className="font-mono text-xs text-muted-foreground">{row.original.size}</span>
|
||||||
@@ -230,11 +231,11 @@ function BackupsDataGrid({
|
|||||||
[onDelete, onDownload, onRestore],
|
[onDelete, onDownload, onRestore],
|
||||||
)
|
)
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useTable({
|
||||||
|
features: dataGridFeatures,
|
||||||
|
manualPagination: true,
|
||||||
data: backups,
|
data: backups,
|
||||||
columns,
|
columns,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
getSortedRowModel: getSortedRowModel(),
|
|
||||||
getRowId: (row) => row.id,
|
getRowId: (row) => row.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,23 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
|
import { dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid"
|
||||||
|
|
||||||
import { useMemo, type ReactNode } from "react"
|
import { useMemo, type ReactNode } from "react"
|
||||||
import {
|
import {
|
||||||
type ColumnDef,
|
type ColumnDef,
|
||||||
getCoreRowModel,
|
useTable,
|
||||||
getExpandedRowModel,
|
} from "@tanstack/react-table"
|
||||||
getSortedRowModel,
|
|
||||||
useReactTable,
|
|
||||||
} from "@tanstack/react-table-v8"
|
|
||||||
import type { CertStatus, Server } from "@/lib/data"
|
import type { CertStatus, Server } from "@/lib/data"
|
||||||
import type { CertificateDto } from "@mmapp/contracts/certificates"
|
import type { CertificateDto } from "@mmapp/contracts/certificates"
|
||||||
import { Flag } from "@/components/flag"
|
import { Flag } from "@/components/flag"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { DataGridShell } from "@/components/data-grids/shared/legacy-data-grid-shell"
|
import { DataGridShell } from "@/components/data-grids/shared/data-grid-shell"
|
||||||
import {
|
import {
|
||||||
DATA_GRID_CELL_PAD,
|
DATA_GRID_CELL_PAD,
|
||||||
DATA_GRID_CELL_PAD_FIRST,
|
DATA_GRID_CELL_PAD_FIRST,
|
||||||
DATA_GRID_CELL_PAD_LAST,
|
DATA_GRID_CELL_PAD_LAST,
|
||||||
} from "@/components/data-grids/shared/legacy-data-grid-layout"
|
} from "@/components/data-grids/shared/data-grid-layout"
|
||||||
import { DataGridSortHeader } from "@/components/data-grids/shared/legacy-data-grid-sort-header"
|
import { DataGridSortHeader } from "@/components/data-grids/shared/data-grid-sort-header"
|
||||||
import {
|
import {
|
||||||
CertificateExpandedDetail,
|
CertificateExpandedDetail,
|
||||||
daysLeftBar,
|
daysLeftBar,
|
||||||
@@ -99,7 +98,7 @@ interface CertificatesDataGridProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function CertificatesDataGrid({ certificates, serverMap, isLoading }: CertificatesDataGridProps) {
|
function CertificatesDataGrid({ certificates, serverMap, isLoading }: CertificatesDataGridProps) {
|
||||||
const columns = useMemo<ColumnDef<CertificateDto>[]>(
|
const columns = useMemo<ColumnDef<DataGridFeatures, CertificateDto>[]>(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
id: "name",
|
id: "name",
|
||||||
@@ -244,12 +243,11 @@ function CertificatesDataGrid({ certificates, serverMap, isLoading }: Certificat
|
|||||||
[serverMap],
|
[serverMap],
|
||||||
)
|
)
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useTable({
|
||||||
|
features: dataGridFeatures,
|
||||||
|
manualPagination: true,
|
||||||
data: certificates,
|
data: certificates,
|
||||||
columns,
|
columns,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
getSortedRowModel: getSortedRowModel(),
|
|
||||||
getExpandedRowModel: getExpandedRowModel(),
|
|
||||||
getRowId: (row) => row.id,
|
getRowId: (row) => row.id,
|
||||||
getRowCanExpand: () => true,
|
getRowCanExpand: () => true,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
|
import { dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid"
|
||||||
|
|
||||||
import { useMemo } from "react"
|
import { useMemo } from "react"
|
||||||
import {
|
import {
|
||||||
type ColumnDef,
|
type ColumnDef,
|
||||||
getCoreRowModel,
|
useTable,
|
||||||
getSortedRowModel,
|
} from "@tanstack/react-table"
|
||||||
useReactTable,
|
|
||||||
} from "@tanstack/react-table-v8"
|
|
||||||
import type { Server } from "@/lib/data"
|
import type { Server } from "@/lib/data"
|
||||||
import { Flag } from "@/components/flag"
|
import { Flag } from "@/components/flag"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { FormToggle } from "@/components/form-kit"
|
import { FormToggle } from "@/components/form-kit"
|
||||||
import { DataGridShell } from "@/components/data-grids/shared/legacy-data-grid-shell"
|
import { DataGridShell } from "@/components/data-grids/shared/data-grid-shell"
|
||||||
import {
|
import {
|
||||||
DATA_GRID_CELL_PAD,
|
DATA_GRID_CELL_PAD,
|
||||||
DATA_GRID_CELL_PAD_FIRST,
|
DATA_GRID_CELL_PAD_FIRST,
|
||||||
DATA_GRID_CELL_PAD_LAST,
|
DATA_GRID_CELL_PAD_LAST,
|
||||||
} from "@/components/data-grids/shared/legacy-data-grid-layout"
|
} from "@/components/data-grids/shared/data-grid-layout"
|
||||||
import { DataGridSortHeader } from "@/components/data-grids/shared/legacy-data-grid-sort-header"
|
import { DataGridSortHeader } from "@/components/data-grids/shared/data-grid-sort-header"
|
||||||
import { EmptyState } from "@/components/empty-state"
|
import { EmptyState } from "@/components/empty-state"
|
||||||
import { CableIcon, CopyIcon, EyeIcon, EyeOffIcon, Trash2Icon } from "lucide-react"
|
import { CableIcon, CopyIcon, EyeIcon, EyeOffIcon, Trash2Icon } from "lucide-react"
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ function SubusersDataGrid({
|
|||||||
onToggleActive,
|
onToggleActive,
|
||||||
onRemove,
|
onRemove,
|
||||||
}: SubusersDataGridProps) {
|
}: SubusersDataGridProps) {
|
||||||
const columns = useMemo<ColumnDef<SubUserRow>[]>(
|
const columns = useMemo<ColumnDef<DataGridFeatures, SubUserRow>[]>(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
id: "login",
|
id: "login",
|
||||||
@@ -200,11 +200,11 @@ function SubusersDataGrid({
|
|||||||
[onRemove, onToggleActive, onToggleReveal, revealedIds, servers],
|
[onRemove, onToggleActive, onToggleReveal, revealedIds, servers],
|
||||||
)
|
)
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useTable({
|
||||||
|
features: dataGridFeatures,
|
||||||
|
manualPagination: true,
|
||||||
data: subUsers,
|
data: subUsers,
|
||||||
columns,
|
columns,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
getSortedRowModel: getSortedRowModel(),
|
|
||||||
getRowId: (row) => row.id,
|
getRowId: (row) => row.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,22 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
|
import { dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid"
|
||||||
|
|
||||||
import { useMemo } from "react"
|
import { useMemo } from "react"
|
||||||
import {
|
import {
|
||||||
type ColumnDef,
|
type ColumnDef,
|
||||||
getCoreRowModel,
|
useTable,
|
||||||
getExpandedRowModel,
|
} from "@tanstack/react-table"
|
||||||
getSortedRowModel,
|
|
||||||
useReactTable,
|
|
||||||
} from "@tanstack/react-table-v8"
|
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Badge } from "@/components/reui/badge"
|
import { Badge } from "@/components/reui/badge"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { DataGridShell } from "@/components/data-grids/shared/legacy-data-grid-shell"
|
import { DataGridShell } from "@/components/data-grids/shared/data-grid-shell"
|
||||||
import {
|
import {
|
||||||
DATA_GRID_CELL_PAD,
|
DATA_GRID_CELL_PAD,
|
||||||
DATA_GRID_CELL_PAD_FIRST,
|
DATA_GRID_CELL_PAD_FIRST,
|
||||||
DATA_GRID_CELL_PAD_LAST,
|
DATA_GRID_CELL_PAD_LAST,
|
||||||
} from "@/components/data-grids/shared/legacy-data-grid-layout"
|
} from "@/components/data-grids/shared/data-grid-layout"
|
||||||
import { DataGridSortHeader } from "@/components/data-grids/shared/legacy-data-grid-sort-header"
|
import { DataGridSortHeader } from "@/components/data-grids/shared/data-grid-sort-header"
|
||||||
import { UsersExpandedDetail } from "@/components/data-grids/users-expanded-detail"
|
import { UsersExpandedDetail } from "@/components/data-grids/users-expanded-detail"
|
||||||
import { EmptyState } from "@/components/empty-state"
|
import { EmptyState } from "@/components/empty-state"
|
||||||
import { ALL_SECTIONS, ROLE_LABEL, type AppUser } from "@/lib/users"
|
import { ALL_SECTIONS, ROLE_LABEL, type AppUser } from "@/lib/users"
|
||||||
@@ -60,7 +59,7 @@ function UsersDataGrid({
|
|||||||
onEdit,
|
onEdit,
|
||||||
onDelete,
|
onDelete,
|
||||||
}: UsersDataGridProps) {
|
}: UsersDataGridProps) {
|
||||||
const columns = useMemo<ColumnDef<AppUser>[]>(
|
const columns = useMemo<ColumnDef<DataGridFeatures, AppUser>[]>(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
id: "name",
|
id: "name",
|
||||||
@@ -209,12 +208,11 @@ function UsersDataGrid({
|
|||||||
[allSectionsCount, onDelete, onEdit, serversCount],
|
[allSectionsCount, onDelete, onEdit, serversCount],
|
||||||
)
|
)
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useTable({
|
||||||
|
features: dataGridFeatures,
|
||||||
|
manualPagination: true,
|
||||||
data: users,
|
data: users,
|
||||||
columns,
|
columns,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
getSortedRowModel: getSortedRowModel(),
|
|
||||||
getExpandedRowModel: getExpandedRowModel(),
|
|
||||||
getRowId: (row) => row.id,
|
getRowId: (row) => row.id,
|
||||||
getRowCanExpand: () => true,
|
getRowCanExpand: () => true,
|
||||||
})
|
})
|
||||||
|
|||||||
+15
-1
@@ -18,7 +18,7 @@
|
|||||||
| 1 | Достоверность сохранения, удаления, включения и опроса серверов | Завершён 2026-09-13 |
|
| 1 | Достоверность сохранения, удаления, включения и опроса серверов | Завершён 2026-09-13 |
|
||||||
| 2 | Общая оболочка Data Grid/TanStack v9, Filters и эталон серверов | Завершён 2026-09-13, с перечисленными ограничениями приёмки |
|
| 2 | Общая оболочка Data Grid/TanStack v9, Filters и эталон серверов | Завершён 2026-09-13, с перечисленными ограничениями приёмки |
|
||||||
| 3 | Основные таблицы и справочники | Завершён 2026-09-13, границы проверок ниже |
|
| 3 | Основные таблицы и справочники | Завершён 2026-09-13, границы проверок ниже |
|
||||||
| 4 | Остальные таблицы, удаление старых API, общая регрессия | В работе: BGP/OSPF и VPN-группа перенесены |
|
| 4 | Остальные таблицы, удаление старых API, общая регрессия | В работе: BGP/OSPF, VPN, пользователи, сертификаты и бэкапы перенесены |
|
||||||
| 5 | Состояния данных, свежесть, частичные сбои, гонки polling | Не начат |
|
| 5 | Состояния данных, свежесть, частичные сбои, гонки polling | Не начат |
|
||||||
| 6 | Навигация и дашборд | Не начат |
|
| 6 | Навигация и дашборд | Не начат |
|
||||||
| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Не начат |
|
| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Не начат |
|
||||||
@@ -246,6 +246,20 @@ ReUI checklist получен; сохранены публичный API, ком
|
|||||||
|
|
||||||
Квота: в начале использовано 1%, после проверок 18% пятичасового окна (общий расход аккаунта). VPN-группа миграции завершена, этап 4 в целом ещё открыт. Следующий конкретный блок — `users-data-grid`, `subusers-data-grid`, `certificates-data-grid`, `backups-data-grid`; затем оставшиеся таблицы диагностики/управления и Filters. Только после последнего потребителя удалить legacy-основу и v8 и выполнить общую регрессию. Блок IPsec/GRE сохранить локальным коммитом без push; ветку до завершения этапа 4 не выпускать.
|
Квота: в начале использовано 1%, после проверок 18% пятичасового окна (общий расход аккаунта). VPN-группа миграции завершена, этап 4 в целом ещё открыт. Следующий конкретный блок — `users-data-grid`, `subusers-data-grid`, `certificates-data-grid`, `backups-data-grid`; затем оставшиеся таблицы диагностики/управления и Filters. Только после последнего потребителя удалить legacy-основу и v8 и выполнить общую регрессию. Блок IPsec/GRE сохранить локальным коммитом без push; ветку до завершения этапа 4 не выпускать.
|
||||||
|
|
||||||
|
## Этап 4 — блок 5: таблицы администрирования (2026-09-14)
|
||||||
|
|
||||||
|
Перенесены `users-data-grid`, `subusers-data-grid`, `certificates-data-grid`, `backups-data-grid` на установленный API ReUI v9 с явным `manualPagination: true`. Сохранены идентификаторы, обработчики и раскрытие. `SubusersDataGrid` сейчас не имеет потребителей в app/components; для него выполнена статическая проверка, браузерная приёмка не заявляется.
|
||||||
|
|
||||||
|
Браузерная проверка выявила сортировку размера бэкапа по форматированной строке. В `Backup` добавлено необязательное `sizeBytes`; страница сохраняет число байтов из API, таблица сортирует по нему. Отображение размера не менялось. При отсутствии исходного числа запись считается неизвестной и располагается в конце (`sortUndefined: "last"`), строка не превращается в выдуманное число. Старые демонстрационные объекты без sizeBytes не дают числовой сортировки; текущая страница загружает бэкапы через API.
|
||||||
|
|
||||||
|
Три сценария `tests/e2e/admin-tables.spec.ts` прошли: 12 пользователей и сертификатов с сортировкой/раскрытием, SAN выбранного сертификата, 12 бэкапов с числовой сортировкой и правильным файлом в подтверждении удаления с отменой. Исправлены неполные HTTP-фикстуры настроек сертификатов, вызывавшие предупреждения controlled/uncontrolled. Реальные права, удаление, восстановление и операции сертификатов этим прогоном не проверены. Общая сборка/регрессия остаётся завершению этапа 4.
|
||||||
|
|
||||||
|
Применён полученный ReUI checklist: сохранены композиция и установленный API; полная доступность и адаптивность остаются профильной приёмке. Квота: использовано 26% на старте, 50% после браузерных проверок (общий расход аккаунта). Блок сохранить локальным коммитом без push.
|
||||||
|
|
||||||
|
Следующий блок: `filters-data-grid`, `firewall-rules-data-grid`, `firewall-scenario-rules-data-grid`. Затем `data-collection-scheduler-data-grid`, `probes-schedule-data-grid`, `probes-speed-probes-data-grid`, `recursive-routes-data-grid` и четыре `route-optimizer-*-data-grid`. Также остаются общая legacy-основа, `components/data-table.tsx`, старые Filters и зависимость v8. Этап 4 не завершён, ветку не выпускать.
|
||||||
|
|
||||||
|
Статические проверки блока 5: TypeScript прошёл; lint четырёх таблиц, фикстур и доменной модели — без замечаний. Расширенный lint `app/(main)/backups/page.tsx` обнаружил исходную ошибку `react-hooks/set-state-in-effect` на `void loadLive()` в эффекте (подтверждено в HEAD до правок). Эта ошибка не исправлялась в миграции; устранить при работе над состояниями данных на этапе 5. Не считать lint всей страницы успешным.
|
||||||
|
|
||||||
## Карта миграции (исходная инвентаризация этапа 0)
|
## Карта миграции (исходная инвентаризация этапа 0)
|
||||||
|
|
||||||
Текущая связка: @tanstack/react-table ^8.21.3, @tanstack/react-virtual ^3.14.4, base-nova. Актуальный ReUI MCP ранее подтвердил useTable/dataGridFeatures для Data Grid и дерево FilterQuery вместо filters/onChange для Filters. Перед установкой перепроверить совместимые версии, сохранить семантику существующих фильтров (простые условия → AND) и закрепить lockfile.
|
Текущая связка: @tanstack/react-table ^8.21.3, @tanstack/react-virtual ^3.14.4, base-nova. Актуальный ReUI MCP ранее подтвердил useTable/dataGridFeatures для Data Grid и дерево FilterQuery вместо filters/onChange для Filters. Перед установкой перепроверить совместимые версии, сохранить семантику существующих фильтров (простые условия → AND) и закрепить lockfile.
|
||||||
|
|||||||
@@ -192,6 +192,7 @@ export interface Backup {
|
|||||||
serverId?: string | null
|
serverId?: string | null
|
||||||
filename: string
|
filename: string
|
||||||
size: string
|
size: string
|
||||||
|
sizeBytes?: number
|
||||||
created: string
|
created: string
|
||||||
kind: "auto" | "manual"
|
kind: "auto" | "manual"
|
||||||
notes: string
|
notes: string
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { test, expect, type Page } from "@playwright/test"
|
||||||
|
import { appUserReadSchema } from "../../packages/contracts/src/users"
|
||||||
|
import { certificatesListResponseSchema } from "../../packages/contracts/src/certificates"
|
||||||
|
import type { BackupItem } from "../../shared/api/backups"
|
||||||
|
|
||||||
|
const ids = Array.from({ length: 12 }, (_, i) => i + 1)
|
||||||
|
const users = ids.map(i => appUserReadSchema.parse({
|
||||||
|
id: String(i), name: `Пользователь ${i}`, login: `user-${i}`, email: `user${i}@example.test`,
|
||||||
|
role: "viewer", active: true, avatar: "U", lastSeen: null, sections: [], servers: [], bindings: [],
|
||||||
|
createdAt: "2026-09-14T00:00:00Z", updatedAt: "2026-09-14T00:00:00Z",
|
||||||
|
}))
|
||||||
|
const certificates = certificatesListResponseSchema.parse({ failures: [], certificates: ids.map(i => ({
|
||||||
|
id: String(i), name: `cert-${i}`, serverId: "1", commonName: `host${i}.example.test`,
|
||||||
|
sans: [`alt${i}.example.test`], issuedBy: "Test CA", validFrom: "2026-09-01", validUntil: "2027-09-01",
|
||||||
|
daysLeft: 300 + i, keySize: 2048, usage: ["server"], trusted: true, status: "valid",
|
||||||
|
})) })
|
||||||
|
const backups: BackupItem[] = ids.map(i => ({ id: String(i), serverId: 1, serverName: "Тестовый роутер",
|
||||||
|
filename: `backup-${i}.backup`, sizeBytes: i === 12 ? 1048576 : i * 1024, createdAt: "2026-09-14T00:00:00Z", kind: "manual" }))
|
||||||
|
|
||||||
|
async function setup(page: Page, url: string) {
|
||||||
|
await page.route("**/health", route => route.fulfill({ json: { ok: true } }))
|
||||||
|
await page.route("**/api/**", route => {
|
||||||
|
const path = new URL(route.request().url()).pathname
|
||||||
|
return route.fulfill({ json: path === "/api/users" ? { users }
|
||||||
|
: path === "/api/certificates" ? certificates : path === "/api/backups" ? backups
|
||||||
|
: path === "/api/certificates/acme-settings" ? { directoryUrl: "https://acme.example.test/directory", defaultZoneId: "", tokenConfigured: false }
|
||||||
|
: path === "/api/certificates/renew-settings" ? { enabled: false, intervalSec: 3600, renewBeforeDays: 30 }
|
||||||
|
: path === "/api/backups/schedule" ? { enabled: false, frequency: "daily", hour: 3, minute: 0, weekDay: 0, monthDay: 1, keepCount: 7, format: "backup", serverIds: [] }
|
||||||
|
: path === "/api/backups/storage" ? { provider: "local", s3Endpoint: "", s3Region: "", s3Bucket: "", s3Prefix: "", s3AccessKeyId: "", secretConfigured: false, s3ForcePathStyle: false, keepLocalCopy: true }
|
||||||
|
: path === "/api/auth/config" ? { required: false } : path === "/api/servers" ? [] : {} })
|
||||||
|
})
|
||||||
|
await page.goto(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
test("Пользователи: 12 строк, сортировка и раскрытие", async ({ page }) => {
|
||||||
|
await setup(page, "/users")
|
||||||
|
const rows = page.locator("tr[data-row-id]")
|
||||||
|
await expect(rows).toHaveCount(12)
|
||||||
|
await page.getByRole("table").getByRole("button", { name: "Пользователь", exact: true }).click()
|
||||||
|
await expect(rows.first()).toContainText("Пользователь 1")
|
||||||
|
await page.getByRole("table").getByRole("button", { name: "Пользователь", exact: true }).click()
|
||||||
|
await expect(rows.first()).toContainText("Пользователь 12")
|
||||||
|
await rows.first().click()
|
||||||
|
await expect(page.getByText("Разделы:", { exact: false }).last()).toBeVisible()
|
||||||
|
await rows.first().click()
|
||||||
|
await expect(rows).toHaveCount(12)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Сертификаты: 12 строк, сортировка и SAN в деталях", async ({ page }) => {
|
||||||
|
await setup(page, "/certificates")
|
||||||
|
const rows = page.locator("tr[data-row-id]")
|
||||||
|
await expect(rows).toHaveCount(12)
|
||||||
|
await page.getByRole("button", { name: "Сертификат", exact: true }).click()
|
||||||
|
await page.getByRole("button", { name: "Сертификат", exact: true }).click()
|
||||||
|
await expect(rows.first()).toContainText("cert-12")
|
||||||
|
await rows.first().click()
|
||||||
|
await expect(page.getByText("alt12.example.test", { exact: true })).toBeVisible()
|
||||||
|
await rows.first().click()
|
||||||
|
await expect(page.getByText("alt12.example.test", { exact: true })).toBeHidden()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Бэкапы: 12 строк, числовая сортировка и выбранный файл в подтверждении", async ({ page }) => {
|
||||||
|
await setup(page, "/backups")
|
||||||
|
const rows = page.locator("tr[data-row-id]")
|
||||||
|
await expect(rows).toHaveCount(12)
|
||||||
|
await page.getByRole("button", { name: "Размер", exact: true }).click()
|
||||||
|
await expect(rows.first()).toContainText("backup-12.backup")
|
||||||
|
await rows.first().getByRole("button", { name: "Удалить backup-12.backup", exact: true }).click()
|
||||||
|
await expect(page.getByRole("alertdialog")).toContainText("backup-12.backup")
|
||||||
|
await page.getByRole("button", { name: "Отмена", exact: true }).click()
|
||||||
|
await expect(rows).toHaveCount(12)
|
||||||
|
})
|
||||||
Reference in new issue
Block a user