fix(ui): восстанавливать фильтры WireGuard из ссылки
This commit is contained in:
1 parent
c6702efcc4
commit
e51964b4d2
8 files changed
+103
-13
No files matched your search
@@ -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
@@ -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.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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
@@ -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}),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
})
|
||||||
@@ -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() : "")
|
||||||
|
}
|
||||||
@@ -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([])
|
||||||
|
})
|
||||||
Reference in new issue
Block a user