fix(ui): ограничить отображение каталога интерфейсов WireGuard
This commit is contained in:
1 parent
7693b32372
commit
a6be18c1d6
6 files changed
+112
-14
No files matched your search
@@ -1,8 +1,8 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid"
|
import { DataGridContainer, DataGridScrollArea, DataGridTable, dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid"
|
||||||
|
|
||||||
import { useMemo, type ReactNode } from "react"
|
import { useEffect, useMemo, type ReactNode } from "react"
|
||||||
import {
|
import {
|
||||||
type ColumnDef,
|
type ColumnDef,
|
||||||
useTable,
|
useTable,
|
||||||
@@ -74,6 +74,7 @@ function WireguardDataGrid({
|
|||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
id: "name",
|
id: "name",
|
||||||
|
size: 176,
|
||||||
accessorKey: "name",
|
accessorKey: "name",
|
||||||
header: ({ column }) => (
|
header: ({ column }) => (
|
||||||
<DataGridSortHeader
|
<DataGridSortHeader
|
||||||
@@ -88,11 +89,9 @@ function WireguardDataGrid({
|
|||||||
const onlinePeers = iface.peers.filter((p) => !!p.latestHandshake).length
|
const onlinePeers = iface.peers.filter((p) => !!p.latestHandshake).length
|
||||||
return (
|
return (
|
||||||
<div className="flex items-start gap-2 min-w-0">
|
<div className="flex items-start gap-2 min-w-0">
|
||||||
{expanded ? (
|
<Button type="button" variant="ghost" size="icon" className="size-11 shrink-0" aria-label={"Пиры: " + iface.name} aria-expanded={expanded} onClick={event => { event.stopPropagation(); row.toggleExpanded() }}>
|
||||||
<ChevronDownIcon className="size-3.5 mt-0.5 shrink-0 text-muted-foreground" />
|
{expanded ? <ChevronDownIcon aria-hidden="true" className="size-3.5" /> : <ChevronRightIcon aria-hidden="true" className="size-3.5" />}
|
||||||
) : (
|
</Button>
|
||||||
<ChevronRightIcon className="size-3.5 mt-0.5 shrink-0 text-muted-foreground/40" />
|
|
||||||
)}
|
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span
|
<span
|
||||||
@@ -101,7 +100,7 @@ function WireguardDataGrid({
|
|||||||
iface.status === "up" ? "bg-success animate-pulse" : "bg-destructive",
|
iface.status === "up" ? "bg-success animate-pulse" : "bg-destructive",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<span className="font-mono font-semibold text-sm">{iface.name}</span>
|
<span className="min-w-0 break-all font-mono font-semibold text-sm">{iface.name}</span>
|
||||||
</div>
|
</div>
|
||||||
{!compactServer ? (
|
{!compactServer ? (
|
||||||
<div className="flex items-center gap-1.5 mt-0.5 text-[11px] text-muted-foreground font-mono">
|
<div className="flex items-center gap-1.5 mt-0.5 text-[11px] text-muted-foreground font-mono">
|
||||||
@@ -131,6 +130,7 @@ function WireguardDataGrid({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "listenPort",
|
id: "listenPort",
|
||||||
|
size: 100,
|
||||||
accessorKey: "listenPort",
|
accessorKey: "listenPort",
|
||||||
header: ({ column }) => <DataGridSortHeader column={column} title="Порт" />,
|
header: ({ column }) => <DataGridSortHeader column={column} title="Порт" />,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
@@ -144,6 +144,7 @@ function WireguardDataGrid({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "mtu",
|
id: "mtu",
|
||||||
|
size: 80,
|
||||||
accessorKey: "mtu",
|
accessorKey: "mtu",
|
||||||
header: ({ column }) => <DataGridSortHeader column={column} title="MTU" />,
|
header: ({ column }) => <DataGridSortHeader column={column} title="MTU" />,
|
||||||
cell: ({ row }) => <span className="font-mono text-sm">{row.original.mtu}</span>,
|
cell: ({ row }) => <span className="font-mono text-sm">{row.original.mtu}</span>,
|
||||||
@@ -155,6 +156,7 @@ function WireguardDataGrid({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "peers",
|
id: "peers",
|
||||||
|
size: 80,
|
||||||
header: () => (
|
header: () => (
|
||||||
<span className="text-xs font-medium text-muted-foreground">Пиры</span>
|
<span className="text-xs font-medium text-muted-foreground">Пиры</span>
|
||||||
),
|
),
|
||||||
@@ -177,6 +179,7 @@ function WireguardDataGrid({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "status",
|
id: "status",
|
||||||
|
size: 104,
|
||||||
accessorKey: "status",
|
accessorKey: "status",
|
||||||
header: ({ column }) => <DataGridSortHeader column={column} title="Статус" />,
|
header: ({ column }) => <DataGridSortHeader column={column} title="Статус" />,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
@@ -213,8 +216,8 @@ function WireguardDataGrid({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
className={cn(
|
className={cn(
|
||||||
"size-8 shrink-0 border-border/60 bg-background/80 text-muted-foreground shadow-none",
|
"size-11 shrink-0 border-border/60 bg-background/80 text-muted-foreground shadow-none",
|
||||||
"opacity-0 transition-[opacity,background-color,color,border-color]",
|
"transition-[background-color,color,border-color]",
|
||||||
"group-hover/row:opacity-100 focus-visible:opacity-100",
|
"group-hover/row:opacity-100 focus-visible:opacity-100",
|
||||||
"data-popup-open:opacity-100 data-popup-open:bg-muted",
|
"data-popup-open:opacity-100 data-popup-open:bg-muted",
|
||||||
)}
|
)}
|
||||||
@@ -260,7 +263,7 @@ function WireguardDataGrid({
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
size: 56,
|
size: 72,
|
||||||
meta: {
|
meta: {
|
||||||
headerClassName: DATA_GRID_CELL_PAD_LAST,
|
headerClassName: DATA_GRID_CELL_PAD_LAST,
|
||||||
cellClassName: DATA_GRID_CELL_PAD_LAST,
|
cellClassName: DATA_GRID_CELL_PAD_LAST,
|
||||||
@@ -272,13 +275,22 @@ function WireguardDataGrid({
|
|||||||
|
|
||||||
const table = useTable({
|
const table = useTable({
|
||||||
features: dataGridFeatures,
|
features: dataGridFeatures,
|
||||||
manualPagination: true,
|
manualPagination: false,
|
||||||
|
autoResetPageIndex: false,
|
||||||
|
autoResetExpanded: false,
|
||||||
|
initialState: { pagination: { pageIndex: 0, pageSize: 50 }, columnPinning: { start: ["name"], end: [] } },
|
||||||
data: interfaces,
|
data: interfaces,
|
||||||
columns,
|
columns,
|
||||||
getRowId: (row) => JSON.stringify([row.serverId, row.id]),
|
getRowId: (row) => JSON.stringify([row.serverId, row.id]),
|
||||||
getRowCanExpand: () => true,
|
getRowCanExpand: () => true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const { pageIndex, pageSize } = table.state.pagination
|
||||||
|
useEffect(() => {
|
||||||
|
const lastPage = Math.max(0, Math.ceil(interfaces.length / pageSize) - 1)
|
||||||
|
if (pageIndex > lastPage) table.setPageIndex(lastPage)
|
||||||
|
}, [interfaces.length, pageIndex, pageSize, table])
|
||||||
|
|
||||||
if (interfaces.length === 0) {
|
if (interfaces.length === 0) {
|
||||||
return (
|
return (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
@@ -295,12 +307,18 @@ function WireguardDataGrid({
|
|||||||
<DataGridShell
|
<DataGridShell
|
||||||
table={table}
|
table={table}
|
||||||
recordCount={interfaces.length}
|
recordCount={interfaces.length}
|
||||||
|
pagination={interfaces.length > 50 || pageSize !== 50}
|
||||||
|
tableLayout={{ dense: true, columnsPinnable: true }}
|
||||||
onRowClick={(row) => table.getRow(JSON.stringify([row.serverId, row.id])).toggleExpanded()}
|
onRowClick={(row) => table.getRow(JSON.stringify([row.serverId, row.id])).toggleExpanded()}
|
||||||
tableClassNames={{
|
tableClassNames={{
|
||||||
headerRow: "border-b border-border",
|
headerRow: "border-b border-border",
|
||||||
bodyRow: cn("group/row", "[&[data-disabled=true]]:opacity-50"),
|
bodyRow: cn("group/row", "[&[data-disabled=true]]:opacity-50"),
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<DataGridContainer border={false} className="w-full rounded-none border-0">
|
||||||
|
<DataGridScrollArea orientation="horizontal"><DataGridTable /></DataGridScrollArea>
|
||||||
|
</DataGridContainer>
|
||||||
|
</DataGridShell>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
## Актуальная точка продолжения — 2026-10-04
|
## Актуальная точка продолжения — 2026-10-04
|
||||||
|
|
||||||
Этап 11Б завершён в границах локальной приёмки (11Б.7); внешний выпуск не проверен. 12.1 принят локально: доступность оболочки/клавиатура/health в описанных сценариях. 12.2.1 принят локально: strict BGP catalog/partial/nullable counters. 12.2.2 принят локально: strict traffic DTO и агрегация без выдуманных точек. 12.2.3 принят локально: observations для server traffic от backend до dashboard. 12.2.4 принят локально: strict servers/probes и unknown server status. 12.2.5 принят локально: реальные ping samples/unknown/ось RTT/Recharts. 12.2.6 принят локально: strict GRE/WireGuard/VXLAN и device failures. 12.2.7 принят локально: strict Internet path snapshot/fallback. 12.2.8 принят локально: подтверждение полного backend сбора. 12.2.9 принят локально: метрики участков и наблюдаемый WAN без эвристической подмены. 12.3.1 принят локально:1000 серверов/штатная пагинация. 12.3.2 принят локально: пагинация CompactDataGrid/1000 строк статистики. 12.3.3 принят локально: порционный DOM очереди проблем. 12.3.4 принят локально: большой BGP каталог/страница при обновлении. Production build и123 профильных browser приняты локально на3978976. 12.2.10 принят локально: обязательные RouterOS поля. 12.3.5 принят локально: страница CompactDataGrid сохраняется при опросе,89 browser и повторный production build прошли. 12.3.6 сохранён: baseline1000 speed probes (ограничение DOM пока не выполнено). 12.3.7 принят локально:50 строк speed-проб/сохранение страницы/глобальная сортировка/1000→1; каталог1001 устройства остаётся в DOM. 12.3.8 принят локально:50 строк ресурсов, URL/поиск/сохранение страницы/1000→1; исправлена pagination оболочки с children, production build и8 больших browser прошли. 12.3.9 принят локально:порционное раскрытие1000 предупреждений ресурсов/клавиатура/axe; исходная ошибка ESLint speedDraft подтверждена. 12.3.10 принят локально:недоступные интерфейсы очищаются в обработчиках,54 browser/полный ESLint/production build прошли. 12.3.11 принят локально:ServerTileRail50 устройств/URL/поиск/клавиатура/две площадки/mobile Sheet/axe обе темы;47 browser и production build прошли. 12.3.12 принят локально:BGP ручная страница при выбранной URL сессии/refresh/reload,16 browser/build прошли. 12.4.1 записан:main1091 сценарий/1088 passed/3 устаревших fixtures; оба исправленных файла6 passed. 12.4.2 принят локально:context205/24 файла и unit28 прошли. 12.3.13 принят локально:wide pivot51×16/ScrollArea/axe/24+2 browser/builds. 12.3.14 принят локально:history200/50 DOM/54+1 browser/build. 12.3.15 принят локально:live расписание1000/page20 error cache/15 browser. 12.3.16 принят локально:certificates1000/50 DOM/refresh деталей/19+3 browser/build. 12.3.17 принят локально:pinning идентификатора/видимость значений/29 browser/build. 12.3.18 принят локально:users1000/поиск всего списка/детали и ошибка refresh/32 browser/build. 12.3.19 принят локально:backups1000/все мобильные действия/возврат focus/39+4 browser/build. 12.3.20 принят локально:независимые speed GET/cache/freshness/58+4 browser/build. 12.3.21 принят локально:strict history DTO/null rates от DB до UI/60+5 browser/unit4/builds. 12.4.3 принят локально:users/backups delta context36 на a558412. 12.3.22 принят локально:shared speed-probes DTO/context8/60+6+47 browser/unit5/builds. 12.3.23 принят локально:strict write DTO/atomic replacement/confirmed scheduler warning/65 browser/HTTP DI JWT/builds. 12.3.24 принят локально:OSPF routes1000/50 DOM/page20/cache/320px/22+1 browser/build. 12.3.25 принLine truncated
|
Этап 11Б завершён в границах локальной приёмки (11Б.7); внешний выпуск не проверен. 12.1 принят локально: доступность оболочки/клавиатура/health в описанных сценариях. 12.2.1 принят локально: strict BGP catalog/partial/nullable counters. 12.2.2 принят локально: strict traffic DTO и агрегация без выдуманных точек. 12.2.3 принят локально: observations для server traffic от backend до dashboard. 12.2.4 принят локально: strict servers/probes и unknown server status. 12.2.5 принят локально: реальные ping samples/unknown/ось RTT/Recharts. 12.2.6 принят локально: strict GRE/WireGuard/VXLAN и device failures. 12.2.7 принят локально: strict Internet path snapshot/fallback. 12.2.8 принят локально: подтверждение полного backend сбора. 12.2.9 принят локально: метрики участков и наблюдаемый WAN без эвристической подмены. 12.3.1 принят локально:1000 серверов/штатная пагинация. 12.3.2 принят локально: пагинация CompactDataGrid/1000 строк статистики. 12.3.3 принят локально: порционный DOM очереди проблем. 12.3.4 принят локально: большой BGP каталог/страница при обновлении. Production build и123 профильных browser приняты локально на3978976. 12.2.10 принят локально: обязательные RouterOS поля. 12.3.5 принят локально: страница CompactDataGrid сохраняется при опросе,89 browser и повторный production build прошли. 12.3.6 сохранён: baseline1000 speed probes (ограничение DOM пока не выполнено). 12.3.7 принят локально:50 строк speed-проб/сохранение страницы/глобальная сортировка/1000→1; каталог1001 устройства остаётся в DOM. 12.3.8 принят локально:50 строк ресурсов, URL/поиск/сохранение страницы/1000→1; исправлена pagination оболочки с children, production build и8 больших browser прошли. 12.3.9 принят локально:порционное раскрытие1000 предупреждений ресурсов/клавиатура/axe; исходная ошибка ESLint speedDraft подтверждена. 12.3.10 принят локально:недоступные интерфейсы очищаются в обработчиках,54 browser/полный ESLint/production build прошли. 12.3.11 принят локально:ServerTileRail50 устройств/URL/поиск/клавиатура/две площадки/mobile Sheet/axe обе темы;47 browser и production build прошли. 12.3.12 принят локально:BGP ручная страница при выбранной URL сессии/refresh/reload,16 browser/build прошли. 12.4.1 записан:main1091 сценарий/1088 passed/3 устаревших fixtures; оба исправленных файла6 passed. 12.4.2 принят локально:context205/24 файла и unit28 прошли. 12.3.13 принят локально:wide pivot51×16/ScrollArea/axe/24+2 browser/builds. 12.3.14 принят локально:history200/50 DOM/54+1 browser/build. 12.3.15 принят локально:live расписание1000/page20 error cache/15 browser. 12.3.16 принят локально:certificates1000/50 DOM/refresh деталей/19+3 browser/build. 12.3.17 принят локально:pinning идентификатора/видимость значений/29 browser/build. 12.3.18 принят локально:users1000/поиск всего списка/детали и ошибка refresh/32 browser/build. 12.3.19 принят локально:backups1000/все мобильные действия/возврат focus/39+4 browser/build. 12.3.20 принят локально:независимые speed GET/cache/freshness/58+4 browser/build. 12.3.21 принят локально:strict history DTO/null rates от DB до UI/60+5 browser/unit4/builds. 12.4.3 принят локально:users/backups delta context36 на a558412. 12.3.22 принят локально:shared speed-probes DTO/context8/60+6+47 browser/unit5/builds. 12.3.23 принят локально:strict write DTO/atomic replacement/confirmed scheduler warning/65 browser/HTTP DI JWT/builds. 12.3.24 принят локально:OSPF routes1000/50 DOM/page20/cache/320px/22+1 browser/build. 12.3.25 принLine truncated
|
||||||
|
|
||||||
Реальные pg_dump/pg_restore, PG rollback и остановка UDP worker требуют внешней приёмки. Датированные продолжения ниже — история; точный следующий шаг также указан в начале [журнала](ux-ui-progress.md).
|
Реальные pg_dump/pg_restore, PG rollback и остановка UDP worker требуют внешней приёмки. Датированные продолжения ниже — история; точный следующий шаг также указан в начале [журнала](ux-ui-progress.md).
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,14 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 12.3.28 — большой каталог интерфейсов WireGuard (2026-10-04)
|
||||||
|
|
||||||
|
Полный API /wireguard не ограничивает каталог страницей. Таблица теперь использует client pagination50, глобальную сортировку, сохранение page/expanded при обновлении и clamp после сокращения списка. Composite ID server/interface сохранён. ReUI DataGridScrollArea и штатное start pinning удерживают имя на320px; длинные имена переносятся. Кнопка раскрытия доступна с клавиатуры, меню действий видно без hover и имеет44px цель.
|
||||||
|
|
||||||
|
Профиль1000 интерфейсов/316026 JSON bytes:50 строк/2136 DOM nodes/1573ms до ready в локальном dev с HTTP-моками (не оценка RouterOS или production latency). Проверены page20, Enter/раскрытие и refresh, поиск записи1000, глобальная числовая сортировка порта,503/cache/page, горизонтальная прокрутка с закреплением имени/меню, axe grid и1000→1 с правильным объектом в диалоге удаления.29 прежних browser прошли; большой профиль после уточнения локального поиска и вертикального scroll1/1. TypeScript без incremental/ESLint и frontend production build прошли. [Снимок320px](ux-ui/screens/large-wireguard-interfaces-320.png) просмотрен.
|
||||||
|
|
||||||
|
Поиск/вкладка/статус WireGuard пока локальные, сервер/interface контекст в URL сохраняется; восстановление остальных фильтров записано отдельным остатком. Осталось18 manualPagination:true specialized grids, включая bounded/demo источники; это не18 доказанных дефектов. Следующий блок12.3.29: полный peer grid1000/50 DOM/действия/320px. Unbounded раскрытие peers одного интерфейса и внешняя12.5 остаются. Коммит локальный без push.
|
||||||
|
|
||||||
## 12.3.27 — достоверные счётчики WireGuard (2026-10-04)
|
## 12.3.27 — достоверные счётчики WireGuard (2026-10-04)
|
||||||
|
|
||||||
Нулевые RX/TX отображаются как 0 Б, отсутствующие значения — как — в таблице пиров и раскрытии интерфейса. Направления подписаны для accessibility. Backend принимает только целые неотрицательные десятичные счётчики в safe integer диапазоне: строки вроде12junk, дроби/минус/unsafe больше не превращаются в показания. Существующие правила сетевых операций не изменены.
|
Нулевые RX/TX отображаются как 0 Б, отсутствующие значения — как — в таблице пиров и раскрытии интерфейса. Направления подписаны для accessibility. Backend принимает только целые неотрицательные десятичные счётчики в safe integer диапазоне: строки вроде12junk, дроби/минус/unsafe больше не превращаются в показания. Существующие правила сетевых операций не изменены.
|
||||||
|
|||||||
@@ -398,3 +398,12 @@ UI проверяет Ping-каталог до публикации, сохра
|
|||||||
|
|
||||||
Проверки: wireguard-links/tables29/29, включая обе композиции zero/missing и старые сценарии операций/ссылок; pure mapper1/1; TypeScript без incremental, ESLint изменённых файлов и backend/frontend production build прошли. Реальные RouterOS ответы не проверены. Следующий блок12.3.28: большой каталог интерфейсов WireGuard, пагинация50/refresh/search/320px; полный peer grid и раскрытие большого списка пиров остаются отдельным объёмом. Коммит локальный без push.
|
Проверки: wireguard-links/tables29/29, включая обе композиции zero/missing и старые сценарии операций/ссылок; pure mapper1/1; TypeScript без incremental, ESLint изменённых файлов и backend/frontend production build прошли. Реальные RouterOS ответы не проверены. Следующий блок12.3.28: большой каталог интерфейсов WireGuard, пагинация50/refresh/search/320px; полный peer grid и раскрытие большого списка пиров остаются отдельным объёмом. Коммит локальный без push.
|
||||||
|
|
||||||
|
|
||||||
|
## 12.3.28 — большой каталог интерфейсов WireGuard (2026-10-04)
|
||||||
|
|
||||||
|
Полный API /wireguard не ограничивает каталог страницей. Таблица теперь использует client pagination50, глобальную сортировку, сохранение page/expanded при обновлении и clamp после сокращения списка. Composite ID server/interface сохранён. ReUI DataGridScrollArea и штатное start pinning удерживают имя на320px; длинные имена переносятся. Кнопка раскрытия доступна с клавиатуры, меню действий видно без hover и имеет44px цель.
|
||||||
|
|
||||||
|
Профиль1000 интерфейсов/316026 JSON bytes:50 строк/2136 DOM nodes/1573ms до ready в локальном dev с HTTP-моками (не оценка RouterOS или production latency). Проверены page20, Enter/раскрытие и refresh, поиск записи1000, глобальная числовая сортировка порта,503/cache/page, горизонтальная прокрутка с закреплением имени/меню, axe grid и1000→1 с правильным объектом в диалоге удаления.29 прежних browser прошли; большой профиль после уточнения локального поиска и вертикального scroll1/1. TypeScript без incremental/ESLint и frontend production build прошли. [Снимок320px](ux-ui/screens/large-wireguard-interfaces-320.png) просмотрен.
|
||||||
|
|
||||||
|
Поиск/вкладка/статус WireGuard пока локальные, сервер/interface контекст в URL сохраняется; восстановление остальных фильтров записано отдельным остатком. Осталось18 manualPagination:true specialized grids, включая bounded/demo источники; это не18 доказанных дефектов. Следующий блок12.3.29: полный peer grid1000/50 DOM/действия/320px. Unbounded раскрытие peers одного интерфейса и внешняя12.5 остаются. Коммит локальный без push.
|
||||||
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
@@ -0,0 +1,63 @@
|
|||||||
|
import { test, expect } from "@playwright/test"
|
||||||
|
import { wgListResponseSchema } from "../../packages/contracts/src/wireguard"
|
||||||
|
import axe from "axe-core"
|
||||||
|
|
||||||
|
test("WireGuard1000 interfaces: page/refresh/expansion/search/sort/cache/actions/320px", async ({ page }, info) => {
|
||||||
|
test.setTimeout(90000)
|
||||||
|
let interfaces = Array.from({ length: 1000 }, (_, i) => ({
|
||||||
|
id: "*" + (i + 1), rosId: "*" + (i + 1), name: "wg-test-" + String(i + 1).padStart(4, "0"),
|
||||||
|
serverId: "1", serverName: "Тестовый роутер", serverCountry: "RU", listenPort: 13231 + i, mtu: 1420,
|
||||||
|
comment: "", enabled: true, status: "up", peers: [{ id: "p" + (i + 1), rosId: "*P" + (i + 1),
|
||||||
|
publicKey: "test-key-" + (i + 1), allowedIps: ["10." + Math.floor(i / 256) + "." + (i % 256) + ".1/32"], transferRx: 0 }],
|
||||||
|
}))
|
||||||
|
let failed = false, calls = 0
|
||||||
|
const writes: string[] = []
|
||||||
|
await page.route("**/health", r => r.fulfill({ json: { ok: true } }))
|
||||||
|
await page.route("**/api/**", r => {
|
||||||
|
const path = new URL(r.request().url()).pathname
|
||||||
|
if (r.request().method() !== "GET") writes.push(path)
|
||||||
|
if (path === "/api/wireguard") { calls++; return r.fulfill(failed ? { status: 503, json: { error: "Unavailable" } } : { json: wgListResponseSchema.parse({ interfaces }) }) }
|
||||||
|
return r.fulfill({ json: path === "/api/auth/config" ? { required: false } : path === "/api/servers" ? [{ id: 1, name: "Тестовый роутер", host: "192.0.2.1", country: "RU", enabled: true, status: null }] : {} })
|
||||||
|
})
|
||||||
|
const started = Date.now(); await page.goto("/wireguard?keep=1")
|
||||||
|
const rows = page.locator("tr[data-row-id]")
|
||||||
|
await expect(rows).toHaveCount(50)
|
||||||
|
const profile = { count: interfaces.length, jsonBytes: Buffer.byteLength(JSON.stringify(interfaces)), rows: await rows.count(), nodes: await page.locator("*").count(), readyMs: Date.now() - started }
|
||||||
|
console.log("WG interface profile", JSON.stringify(profile)); await info.attach("profile", { body: JSON.stringify(profile), contentType: "application/json" })
|
||||||
|
const next = page.getByRole("button", { name: "Следующая страница", exact: true })
|
||||||
|
for (let i=0;i<19;i++) await next.click()
|
||||||
|
await expect(next).toBeDisabled(); await expect(rows.first()).toContainText("wg-test-0951")
|
||||||
|
const expand = rows.first().getByRole("button", { name: "Пиры: wg-test-0951", exact: true })
|
||||||
|
await expand.focus(); await page.keyboard.press("Enter"); await expect(expand).toHaveAttribute("aria-expanded", "true")
|
||||||
|
await expect(page.getByText(interfaces[950].peers[0].allowedIps[0], { exact: true })).toBeVisible()
|
||||||
|
const before = calls; await page.getByRole("button", { name: "Обновить", exact: true }).click()
|
||||||
|
await expect.poll(() => calls).toBeGreaterThan(before); await expect(rows.first()).toContainText("wg-test-0951"); await expect(expand).toHaveAttribute("aria-expanded", "true")
|
||||||
|
await expand.click()
|
||||||
|
const search = page.getByPlaceholder("Поиск по имени, серверу, IP…")
|
||||||
|
await search.fill("wg-test-1000"); await expect(rows).toHaveCount(1); await expect(rows.first()).toContainText("wg-test-1000")
|
||||||
|
await expect(page).toHaveURL(url => url.searchParams.get("keep") === "1")
|
||||||
|
await search.fill(""); await expect(rows).toHaveCount(50)
|
||||||
|
await page.getByRole("table").getByRole("button", { name: "Порт", exact: true }).click()
|
||||||
|
await expect(rows.first()).toContainText("wg-test-1000")
|
||||||
|
await next.click(); const previous = await rows.first().getAttribute("data-row-id")
|
||||||
|
failed = true; await page.getByRole("button", { name: "Обновить", exact: true }).click()
|
||||||
|
await expect(page.getByText(/Показаны последние полученные данные/)).toBeVisible()
|
||||||
|
await expect(rows).toHaveCount(50); await expect(rows.first()).toHaveAttribute("data-row-id", previous!)
|
||||||
|
await page.setViewportSize({ width: 320, height: 1000 })
|
||||||
|
const scroll = page.locator('[data-slot="data-grid"] [data-slot="scroll-area-viewport"]')
|
||||||
|
await scroll.evaluate(el => { el.scrollLeft = el.scrollWidth })
|
||||||
|
await expect.poll(() => scroll.evaluate(el => el.scrollLeft)).toBeGreaterThan(0)
|
||||||
|
expect(await rows.first().getByRole("cell").first().evaluate(el => {const a=el.getBoundingClientRect(),v=el.closest('[data-slot="scroll-area-viewport"]')!.getBoundingClientRect(); return a.left>=v.left-1&&a.right<=v.right+1})).toBe(true)
|
||||||
|
const action = rows.first().getByRole("button", { name: /Действия:/ })
|
||||||
|
await action.scrollIntoViewIfNeeded(); await scroll.evaluate(el => { el.scrollLeft = el.scrollWidth }); await expect(action).toBeInViewport(); await action.click(); await expect(page.getByRole("menuitem", { name: "Экспорт", exact: true })).toBeVisible(); await page.keyboard.press("Escape")
|
||||||
|
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true)
|
||||||
|
await page.addScriptTag({ content: axe.source })
|
||||||
|
expect((await page.evaluate(async () => (window as unknown as { axe: typeof axe }).axe.run(document.querySelector('[data-slot="data-grid"]')!, { runOnly: { type: "tag", values: ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"] } }))).violations).toEqual([])
|
||||||
|
await page.addStyleTag({ content: "nextjs-portal { display: none !important; }" }); await page.getByRole("table").scrollIntoViewIfNeeded()
|
||||||
|
await page.screenshot({ path: info.outputPath("wireguard-interfaces-320.png"), animations: "disabled" })
|
||||||
|
failed = false; interfaces = [interfaces[999]]; await page.getByRole("button", { name: "Обновить", exact: true }).click()
|
||||||
|
await expect(rows).toHaveCount(1); await expect(rows.first()).toContainText("wg-test-1000")
|
||||||
|
await action.click(); await page.getByRole("menuitem", { name: "Удалить", exact: true }).click()
|
||||||
|
await expect(page.getByRole("alertdialog")).toContainText("wg-test-1000"); await page.getByRole("alertdialog").getByRole("button", { name: "Отмена", exact: true }).click()
|
||||||
|
expect(writes).toEqual([])
|
||||||
|
})
|
||||||
Reference in new issue
Block a user