fix(ui): восстанавливать фильтры WireGuard из ссылки

This commit is contained in:
Denozordec committed 2026-10-04 12:30:43 +07:00
1 parent c6702efcc4
commit e51964b4d2
8 files changed
+103 -13

No files matched your search

+12 -12
View File
@@ -3,6 +3,8 @@
import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react" import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react"
import Link from "next/link" import Link from "next/link"
import { useRouter, useSearchParams } from "next/navigation" import { useRouter, useSearchParams } from "next/navigation"
import { useWireGuardUrlState } from "@/hooks/use-wireguard-url-state"
import type { WireGuardTab as WgWorkspaceTab } from "@/lib/wireguard-url-state"
import { PageHeader } from "@/components/page-header" import { PageHeader } from "@/components/page-header"
import { servers as mockServers } from "@/lib/data" import { servers as mockServers } from "@/lib/data"
import type { Server } from "@/lib/data" import type { Server } from "@/lib/data"
@@ -69,8 +71,6 @@ import {
Trash2Icon, CodeXmlIcon, AlertCircleIcon, HistoryIcon, Trash2Icon, CodeXmlIcon, AlertCircleIcon, HistoryIcon,
} from "lucide-react" } from "lucide-react"
type WgWorkspaceTab = "interfaces" | "peers" | "cli"
type WgStatusFilter = "all" | "up" | "down"
type WgFailure = { serverId: string; serverName?: string; error: string } type WgFailure = { serverId: string; serverName?: string; error: string }
type PendingDelete = type PendingDelete =
| { kind: "iface"; iface: WgIfaceWithServer } | { kind: "iface"; iface: WgIfaceWithServer }
@@ -192,9 +192,7 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf
const [togglePending, setTogglePending] = useState<string | null>(null) const [togglePending, setTogglePending] = useState<string | null>(null)
const [toggleError, setToggleError] = useState<string | null>(null) const [toggleError, setToggleError] = useState<string | null>(null)
const [workspaceTab, setWorkspaceTab] = useState<WgWorkspaceTab>("interfaces") const { tab: workspaceTab, setTab: setWorkspaceTab, status: statusFilter, setStatus: setStatusFilter, search, setSearch, error: urlError, resetFilters } = useWireGuardUrlState()
const [statusFilter, setStatusFilter] = useState<WgStatusFilter>("all")
const [search, setSearch] = useState("")
const [createOpen, setCreateOpen] = useState(false) const [createOpen, setCreateOpen] = useState(false)
const [importOpen, setImportOpen] = useState(false) const [importOpen, setImportOpen] = useState(false)
const formLock = useRef(false) const formLock = useRef(false)
@@ -271,7 +269,7 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf
const displayServers = isLive ? liveServers : mockServers.filter((s) => s.enabled) const displayServers = isLive ? liveServers : mockServers.filter((s) => s.enabled)
const effectiveServerId = selectedServerId const effectiveServerId = selectedServerId
const selectedSourceError = isLive && (loadError || failures.some(f => f.serverId === selectedServerId)) const selectedSourceError = !!urlError || isLive && (loadError || failures.some(f => f.serverId === selectedServerId))
const historyServerId = effectiveServerId === ALL_SERVERS_ID ? null : effectiveServerId const historyServerId = effectiveServerId === ALL_SERVERS_ID ? null : effectiveServerId
@@ -315,10 +313,11 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf
}, [isLive, historyServerId, backendUrl, loadLive, loadRevisions]) }, [isLive, historyServerId, backendUrl, loadLive, loadRevisions])
const scopedIfaces = useMemo(() => { const scopedIfaces = useMemo(() => {
if (urlError) return []
if (selectedInterfaceId !== null) return displayIfaces.filter(i => i.serverId === effectiveServerId && i.id === selectedInterfaceId) if (selectedInterfaceId !== null) return displayIfaces.filter(i => i.serverId === effectiveServerId && i.id === selectedInterfaceId)
if (effectiveServerId === ALL_SERVERS_ID) return displayIfaces if (effectiveServerId === ALL_SERVERS_ID) return displayIfaces
return displayIfaces.filter((i) => i.serverId === effectiveServerId) return displayIfaces.filter((i) => i.serverId === effectiveServerId)
}, [displayIfaces, effectiveServerId, selectedInterfaceId]) }, [displayIfaces, effectiveServerId, selectedInterfaceId, urlError])
const selectedInterface = selectedInterfaceId === null ? undefined : scopedIfaces[0] const selectedInterface = selectedInterfaceId === null ? undefined : scopedIfaces[0]
@@ -418,7 +417,7 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf
}, [displayServers, displayIfaces]) }, [displayServers, displayIfaces])
async function handleCreate(form: WgCreateFormState) { async function handleCreate(form: WgCreateFormState) {
if (formLock.current) return if (urlError || formLock.current) return
if (!isLive) { if (!isLive) {
toast.info("Создание на роутер доступно только в live-режиме") toast.info("Создание на роутер доступно только в live-режиме")
return return
@@ -502,7 +501,7 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf
} }
function sourceUnavailable(serverId: string | undefined) { function sourceUnavailable(serverId: string | undefined) {
const unavailable = isLive && (loadError || failures.some(f => f.serverId === serverId)) const unavailable = !!urlError || isLive && (loadError || failures.some(f => f.serverId === serverId))
if (unavailable) toast.error("Обновите данные сервера перед изменением объекта") if (unavailable) toast.error("Обновите данные сервера перед изменением объекта")
return unavailable return unavailable
} }
@@ -652,7 +651,7 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf
} }
const emptyCreateAction = ( const emptyCreateAction = (
<Button size="sm" onClick={() => { setCreateError(null); setCreateOpen(true) }}> <Button size="sm" disabled={!!urlError} onClick={() => { setCreateError(null); setCreateOpen(true) }}>
<PlusIcon className="size-4" /> <PlusIcon className="size-4" />
Новый интерфейс Новый интерфейс
</Button> </Button>
@@ -700,11 +699,11 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf
</Button> </Button>
</> </>
)} )}
<Button size="sm" variant="outline" onClick={() => { setImportError(null); setImportOpen(true) }}> <Button size="sm" variant="outline" disabled={!!urlError} onClick={() => { setImportError(null); setImportOpen(true) }}>
<UploadIcon className="size-4" /> <UploadIcon className="size-4" />
Импорт Импорт
</Button> </Button>
<Button size="sm" onClick={() => { setCreateError(null); setCreateOpen(true) }}> <Button size="sm" disabled={!!urlError} onClick={() => { setCreateError(null); setCreateOpen(true) }}>
<PlusIcon className="size-4" /> <PlusIcon className="size-4" />
Новый интерфейс Новый интерфейс
</Button> </Button>
@@ -714,6 +713,7 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf
} }
> >
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{urlError && <Alert variant="warning" role="alert" aria-label="Фильтры WireGuard"><AlertTitle>Проверьте ссылку</AlertTitle><AlertDescription><p>{urlError}</p><Button type="button" variant="outline" onClick={resetFilters}>Сбросить фильтры</Button></AlertDescription></Alert>}
{isLive && loadError && selectedInterfaceId === null && <Alert variant="warning"><AlertTitle>Не удалось обновить WireGuard</AlertTitle><AlertDescription>Показаны последние полученные данные. Повторите обновление перед изменением объектов.</AlertDescription></Alert>} {isLive && loadError && selectedInterfaceId === null && <Alert variant="warning"><AlertTitle>Не удалось обновить WireGuard</AlertTitle><AlertDescription>Показаны последние полученные данные. Повторите обновление перед изменением объектов.</AlertDescription></Alert>}
{selectedInterfaceId !== null && ( {selectedInterfaceId !== null && (
<Alert variant={selectedSourceError || ((!isLive || !loading) && !selectedInterface) ? "warning" : "info"} role="status" aria-label="WireGuard по ссылке"> <Alert variant={selectedSourceError || ((!isLive || !loading) && !selectedInterface) ? "warning" : "info"} role="status" aria-label="WireGuard по ссылке">
+1 -1
View File
@@ -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).
+8
View File
@@ -36,6 +36,14 @@
## 12.3.32 — контекст фильтров WireGuard в URL (2026-10-04)
Поиск q, вкладка tab и статус интерфейса status сохраняются в URL; reload/назад/вперёд восстанавливают срез. Native history updates сохраняют server/interface, посторонние параметры и hash; ввод поиска использует replace, смена вкладки/статуса — push. Статус относится к интерфейсам, peer view сохраняет его для возврата. Неверные/повторяющиеся фильтры и поиск длиннее200 не расширяют список: показываются предупреждение и явный сброс только фильтров с сохранением устройства. Создание/импорт заблокированы до исправления ссылки.
Проверки:34 browser профильных/large/URL прошли; четыре новых invalid-filter сценария выявили пропущенную блокировку header-кнопки, после исправления повтор4/4. Pure URL1/1, TypeScript без incremental/ESLint и production frontend build прошли. Профили1000 интерфейсов/пиров/деталей включены. DTO server/interface context не расширен и его legacy parse не заявлен новым strict контрактом. Общие правила ReUI/base-nova сохранены.
Следующий блок12.3.33: WireGuard чтение при смене backend/mode и поздних ответах, первоначально неизвестный каталог/KPI, cache/freshness и запрет изменения неизвестного источника. Затем оставшиеся17 specialized grids. Общая main/context после delta и внешняя12.5 остаются. Коммит локальный без push.
## 12.3.31 — рукопожатие не подменяет доступность WireGuard (2026-10-04) ## 12.3.31 — рукопожатие не подменяет доступность WireGuard (2026-10-04)
Убрана оценка «пиров онлайн» по presence старого last-handshake. KPI «Данные рукопожатия» считает непустые сведения в ответе и явно сообщает, что доступность пиров не проверена. Колонка «Пиры» показывает число записей. Старое время рукопожатия отображается без зелёного вывода о доступности; отсутствующее/пустое значение — «Нет данных». В мобильных деталях видна подпись Endpoint. Убрана оценка «пиров онлайн» по presence старого last-handshake. KPI «Данные рукопожатия» считает непустые сведения в ответе и явно сообщает, что доступность пиров не проверена. Колонка «Пиры» показывает число записей. Старое время рукопожатия отображается без зелёного вывода о доступности; отсутствующее/пустое значение — «Нет данных». В мобильных деталях видна подпись Endpoint.
+9
View File
@@ -434,3 +434,12 @@ Peer grid теперь использует client pagination50, clamp и сох
Следующий блок12.3.32: WireGuard URL search/tab/status — reload/история/сохранение server/interface/неверные параметры без расширения среза. Первоначально неизвестные общие KPI WireGuard и freshness/source switching требуют отдельного аудита; этот блок исправляет именно смысл handshake.17 остальных specialized grids/общая регрессия после delta/внешняя12.5 остаются. Коммит локальный без push. Следующий блок12.3.32: WireGuard URL search/tab/status — reload/история/сохранение server/interface/неверные параметры без расширения среза. Первоначально неизвестные общие KPI WireGuard и freshness/source switching требуют отдельного аудита; этот блок исправляет именно смысл handshake.17 остальных specialized grids/общая регрессия после delta/внешняя12.5 остаются. Коммит локальный без push.
## 12.3.32 — контекст фильтров WireGuard в URL (2026-10-04)
Поиск q, вкладка tab и статус интерфейса status сохраняются в URL; reload/назад/вперёд восстанавливают срез. Native history updates сохраняют server/interface, посторонние параметры и hash; ввод поиска использует replace, смена вкладки/статуса — push. Статус относится к интерфейсам, peer view сохраняет его для возврата. Неверные/повторяющиеся фильтры и поиск длиннее200 не расширяют список: показываются предупреждение и явный сброс только фильтров с сохранением устройства. Создание/импорт заблокированы до исправления ссылки.
Проверки:34 browser профильных/large/URL прошли; четыре новых invalid-filter сценария выявили пропущенную блокировку header-кнопки, после исправления повтор4/4. Pure URL1/1, TypeScript без incremental/ESLint и production frontend build прошли. Профили1000 интерфейсов/пиров/деталей включены. DTO server/interface context не расширен и его legacy parse не заявлен новым strict контрактом. Общие правила ReUI/base-nova сохранены.
Следующий блок12.3.33: WireGuard чтение при смене backend/mode и поздних ответах, первоначально неизвестный каталог/KPI, cache/freshness и запрет изменения неизвестного источника. Затем оставшиеся17 specialized grids. Общая main/context после delta и внешняя12.5 остаются. Коммит локальный без push.
+18
View File
@@ -0,0 +1,18 @@
"use client"
import { useMemo } from "react"
import { useSearchParams } from "next/navigation"
import { readWireGuardUrl, patchWireGuardUrl, type WireGuardTab, type WireGuardStatus } from "@/lib/wireguard-url-state"
export function useWireGuardUrlState() {
const raw = useSearchParams().toString()
const state = useMemo(() => readWireGuardUrl(new URLSearchParams(raw)), [raw])
function write(changes: Record<string, string | null>, replace = false) {
const href = patchWireGuardUrl(new URLSearchParams(window.location.search), changes) + window.location.hash
if (href === window.location.pathname + window.location.search + window.location.hash) return
if (replace) window.history.replaceState(null, "", href)
else window.history.pushState(null, "", href)
}
return { ...state, setTab: (tab: WireGuardTab) => write({tab:tab === "interfaces" ? null : tab}),
setSearch: (q: string) => write({q}, true), setStatus: (status: WireGuardStatus) => write({status:status === "all" ? null : status}),
resetFilters: () => write({q:null,status:null,tab:null}),
}
}
+11
View File
@@ -0,0 +1,11 @@
import { test } from "node:test"
import assert from "node:assert/strict"
import { readWireGuardUrl, patchWireGuardUrl } from "./wireguard-url-state"
test("WireGuard URL keeps context and validates all owned filters", () => {
assert.deepEqual(readWireGuardUrl(new URLSearchParams()), {tab:"interfaces",status:"all",search:"",error:null})
const params = new URLSearchParams("server=2&interface=*%2F2&keep=1&tab=peers&status=down&q=key%2B1")
assert.deepEqual(readWireGuardUrl(params), {tab:"peers",status:"down",search:"key+1",error:null})
for (const raw of ["tab=bad", "status=bad", "q="+"a".repeat(201), "q=a&q=b", "tab=peers&tab=cli", "status=up&status=down"]) assert.ok(readWireGuardUrl(new URLSearchParams(raw)).error)
const next=new URL(patchWireGuardUrl(params,{q:null,status:null,tab:null}),"https://test.local")
assert.equal(next.searchParams.get("server"),"2");assert.equal(next.searchParams.get("interface"),"*/2");assert.equal(next.searchParams.get("keep"),"1");assert.equal(next.searchParams.has("q"),false)
})
+18
View File
@@ -0,0 +1,18 @@
export type WireGuardTab = "interfaces" | "peers" | "cli"
export type WireGuardStatus = "all" | "up" | "down"
const tabs = ["interfaces", "peers", "cli"]
const statuses = ["all", "up", "down"]
export function readWireGuardUrl(params: URLSearchParams) {
const tab = params.get("tab") ?? "interfaces", status = params.get("status") ?? "all", search = params.get("q") ?? ""
let error: string | null = null
if (["tab", "status", "q"].some(key => params.getAll(key).length > 1)) error = "В ссылке повторяются фильтры WireGuard."
else if (!tabs.includes(tab)) error = "В ссылке указана неизвестная вкладка WireGuard."
else if (!statuses.includes(status)) error = "В ссылке указан неизвестный статус WireGuard."
else if (search.length > 200) error = "Поиск WireGuard в ссылке слишком длинный."
return { tab: (tabs.includes(tab) ? tab : "interfaces") as WireGuardTab, status: (statuses.includes(status) ? status : "all") as WireGuardStatus, search, error }
}
export function patchWireGuardUrl(params: URLSearchParams, changes: Record<string, string | null>) {
const next = new URLSearchParams(params)
for (const [key, value] of Object.entries(changes)) if (!value) next.delete(key); else next.set(key, value)
return "/wireguard" + (next.size ? "?" + next.toString() : "")
}
+26
View File
@@ -527,3 +527,29 @@ for (const oldFails of [false, true]) {
await expect(sheet.getByRole("button", { name: "Откатить", exact: true })).toBeVisible() await expect(sheet.getByRole("button", { name: "Откатить", exact: true })).toBeVisible()
}) })
} }
test("WireGuard URL filters: search/tab/status reload and history preserve selected device", async ({ page }) => {
const control=await setup(page,"/wireguard?server=2&interface=*%2F2&keep=1&status=down&q=wg-same#context")
const rows=page.locator("tr[data-row-id]")
await expect(rows).toHaveCount(1);await expect(page.getByPlaceholder("Поиск по имени, серверу, IP…")).toHaveValue("wg-same")
await page.getByRole("tab",{name:/Пиры/}).click()
await expect(page).toHaveURL(url=>url.searchParams.get("tab")==="peers"&&url.searchParams.get("status")==="down"&&url.hash==="#context")
const search=page.getByPlaceholder("Поиск по ключу, IP, endpoint…");await search.fill("10.0.2.1")
await expect(page).toHaveURL(url=>url.searchParams.get("q")==="10.0.2.1"&&url.searchParams.get("server")==="2"&&url.searchParams.get("interface")==="*/2")
await page.reload();await expect(search).toHaveValue("10.0.2.1");await expect(rows).toHaveCount(1)
await page.getByRole("tab",{name:/Интерфейсы/}).click();await expect(rows).toHaveCount(1)
await page.goBack();await expect(page.getByRole("tab",{name:/Пиры/})).toHaveAttribute("aria-selected","true")
await page.goForward();await page.getByRole("button",{name:/^UP/}).click();await expect(rows).toHaveCount(0)
await expect(page).toHaveURL(url=>url.searchParams.get("status")==="up"&&url.searchParams.get("keep")==="1")
await page.reload();await expect(rows).toHaveCount(0);expect(control.posts).toEqual([])
})
for(const invalid of ["tab=bad","status=bad","q=a&q=b","tab=peers&tab=cli"])test("WireGuard URL rejects invalid filters without widening/"+invalid,async({page})=>{
const control=await setup(page,"/wireguard?server=2&interface=*%2F2&keep=1&"+invalid)
const warning=page.getByRole("alert",{name:"Фильтры WireGuard",exact:true})
await expect(warning).toBeVisible();await expect(page.locator("tr[data-row-id]")).toHaveCount(0)
await expect(page.getByRole("button",{name:"Новый интерфейс",exact:true}).first()).toBeDisabled()
await warning.getByRole("button",{name:"Сбросить фильтры",exact:true}).click()
await expect(warning).toHaveCount(0);await expect(page.locator("tr[data-row-id]")).toHaveCount(1)
await expect(page).toHaveURL(url=>url.searchParams.get("server")==="2"&&url.searchParams.get("interface")==="*/2"&&url.searchParams.get("keep")==="1")
expect(control.posts).toEqual([])
})