fix(ui): сохранять явный выбор туннеля диагностики

This commit is contained in:
Denozordec committed 2026-10-01 09:21:05 +07:00
1 parent d28d7a2cdb
commit 3c41de136e
11 files changed
+41 -9

No files matched your search

+6 -8
View File
@@ -590,7 +590,7 @@ function ProbesContent() {
const [traceUseDns, setTraceUseDns] = useState(false)
const [traceHops] = useState(30)
const [dnsType, setDnsType] = useState<DnsType>("A")
const [bwTunId, setBwTunId] = useState("")
const [bwChoice, setBwChoice] = useState<{ serverId: string; id: string } | null>(null)
const [bwProto, setBwProto] = useState<"tcp" | "udp">("tcp")
const [bwDuration, setBwDuration] = useState(10)
const [runBusy, setRunBusy] = useState(false)
@@ -671,11 +671,6 @@ function ProbesContent() {
}, [isLive, greByServer, srcId])
useEffect(() => {
const list = isLive ? (greByServer[srcId] ?? []) : greTunnels.filter(t => t.serverId === srcId)
if (list.length && !list.some(t => t.id === bwTunId)) setBwTunId(list[0]!.id)
}, [isLive, greByServer, srcId, bwTunId])
// ── run state ──
const [tests, setTests] = useState<DiagTest[]>([])
const [tab, setTab] = useState<"history" | "schedule">("history")
@@ -694,6 +689,8 @@ function ProbesContent() {
}, [])
const bwTunnels = tunnelsForSrc
const bwTunId = bwChoice?.serverId === srcId ? bwChoice.id : (bwTunnels[0]?.id ?? "")
const setBwTunId = (id: string) => setBwChoice({ serverId: srcId, id })
const srcServer = useMemo(() => allServers.find(s => s.id === srcId), [allServers, srcId])
const sourceKey = JSON.stringify([srcId, srcServer?.host])
const sourceRequired = tool === "ping" || tool === "traceroute" || tool === "mtu"
@@ -744,7 +741,7 @@ function ProbesContent() {
target: tool === "bandwidth" ? (bwTun?.name ?? target) : target,
command: cmdPreview,
startedAt: Date.now(),
lines: [{ text: "Выполняется запрос к MikroTik через API…", kind: "dim" }],
lines: [{ text: tool === "dns" ? "Выполняется DNS-запрос приложения…" : "Выполняется запрос к MikroTik через API…", kind: "dim" }],
totalLines: 1,
source: "live",
}, ...p.slice(0, 9)])
@@ -1014,7 +1011,8 @@ function ProbesContent() {
{tool === "bandwidth" && (
<div>
<OptionLabel>GRE-туннель (цель)</OptionLabel>
<NativeSelect ariaLabel="GRE-туннель для BW-теста" value={bwTunId} onChange={setBwTunId} className="min-w-[180px]">
<NativeSelect ariaLabel="GRE-туннель для BW-теста" value={bwTunnels.some(t => t.id === bwTunId) ? bwTunId : ""} onChange={setBwTunId} className="min-w-[180px]">
{!bwTunnels.some(t => t.id === bwTunId) && <option value="" disabled>Выберите GRE-туннель</option>}
{bwTunnels.map(t => <option key={t.id} value={t.id}>{t.name}</option>)}
</NativeSelect>
</div>
+8 -1
View File
@@ -26,7 +26,7 @@
| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Завершён в границах локальной приёмки 2026-09-23; внешние ограничения ниже |
| 8 | Трафик и статистика | Завершён 2026-09-28 в границах локальной приёмки; ограничения в блоке 8.13 |
| 9 | VPN, Firewall и Filters | Завершён в границах локальной приёмки 2026-09-30; блоки 9.1–9.64, ограничения в 9.64 |
| 10 | Маршрутизация, диагностика, карта и терминал | В работе: 10.1–10.69; оптимизатор, карта и терминал приняты локально; диагностика /probes в работе |
| 10 | Маршрутизация, диагностика, карта и терминал | В работе: 10.1–10.70; оптимизатор, карта и терминал приняты локально; диагностика /probes в работе |
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Не начат |
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат |
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
@@ -36,6 +36,13 @@
## Блок 10.70 — контекст BW и клавиатурные действия (2026-10-01)
- Выбор GRE для BW вычисляется из текущего каталога без setState в effect. Первоначальный выбор сохраняет прежнее поведение; ручной выбор привязан к устройству. Если выбранный GRE исчез, запуск блокируется до нового явного выбора, другой туннель не подставляется.
- Ожидание DNS теперь подписано как DNS-запрос приложения. Клавиатурой проверены DNS/CNAME, Enter из поля цели, копирование результата с подтверждением и удаление с возвратом фокуса.
- TypeScript и lint тестов прошли. Профильный прогон: 42 сценария passed; один новый тест сначала упал из-за неполной GRE HTTP-фикстуры, после исправления контракта и aria селектора прошёл отдельным повтором. Производственный код не обходил проблему теста. Прежняя диагностическая lint ошибка устранена; три оставшиеся относятся к расписанию 11Б.
- Далее 10.71: итоговая профильная регрессия этапа 10, production build, перечень внешних ограничений и локальная приёмка. Затем 11А: пользователи. Моки не доказывают реальную сеть/БД/портал.
## Блок 10.69 — доступность и адаптивность диагностики (2026-10-01)
- Сохранена композиция Frame/FramePanel, OpsPanel и ServerRail; API Frame и ReUI checklist сверены. Vendor/theme не изменены. Цвета диагностики используют семантические токены, анимация учитывает reduced motion, кнопки/переключатели и область вывода имеют доступные имена и состояния.
+8
View File
@@ -442,3 +442,11 @@ Shared контракт и backend observed исходные значения/д
- На 1440/390/320 px нет горизонтального переполнения; команда на телефоне использует всю доступную ширину, адрес цели читается, DNS подписан как тест приложения. Сохранены и визуально проверены шесть снимков ready/output.
- TypeScript, 37 data/source E2E, 4 layout/clipboard E2E и 4 context E2E прошли. Production frontend build прошёл. Lint изменённого теста чистый; в странице остаются четыре прежние set-state-in-effect ошибки (три в расписании, одна в выборе BW). Новых ошибок/предупреждений нет.
- Далее 10.70: убрать эффект выбора BW в диагностике, дополнить проверку клавиатуры/успешного clipboard и локальную приёмку. Расписание и три его lint ошибки перенесены в 11Б; реальные RouterOS/БД/портал не приняты по мокам.
## Блок 10.70 — контекст BW и клавиатурные действия (2026-10-01)
- Выбор GRE для BW вычисляется из текущего каталога без setState в effect. Первоначальный выбор сохраняет прежнее поведение; ручной выбор привязан к устройству. Если выбранный GRE исчез, запуск блокируется до нового явного выбора, другой туннель не подставляется.
- Ожидание DNS теперь подписано как DNS-запрос приложения. Клавиатурой проверены DNS/CNAME, Enter из поля цели, копирование результата с подтверждением и удаление с возвратом фокуса.
- TypeScript и lint тестов прошли. Профильный прогон: 42 сценария passed; один новый тест сначала упал из-за неполной GRE HTTP-фикстуры, после исправления контракта и aria селектора прошёл отдельным повтором. Производственный код не обходил проблему теста. Прежняя диагностическая lint ошибка устранена; три оставшиеся относятся к расписанию 11Б.
- Далее 10.71: итоговая профильная регрессия этапа 10, production build, перечень внешних ограничений и локальная приёмка. Затем 11А: пользователи. Моки не доказывают реальную сеть/БД/портал.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

+9
View File
@@ -112,3 +112,12 @@ test("Диагностика: URL выбор, назад и reload сохран
await page.goto("/probes?server=2&keep=1#result");await expect(page.getByRole("option",{name:/Второй/})).toHaveAttribute("aria-selected","true")
await page.getByRole("option",{name:/Первый/}).click();await expect(page).toHaveURL(/keep=1&server=1#result$/);await page.goBack();await expect(page.getByRole("option",{name:/Второй/})).toHaveAttribute("aria-selected","true");await page.reload();await expect(page.getByRole("option",{name:/Второй/})).toHaveAttribute("aria-selected","true")
})
test("Диагностика: исчезнувший выбранный GRE не подменяется первым",async({page})=>{
let tunnels=[{...tunnel,id:"gre-a",name:"Первый GRE"},{...tunnel,id:"gre-b",name:"Выбранный GRE",remoteAddress:"192.0.2.3"}]
await page.route("**/api/filters/gre-tunnels?*",r=>r.fulfill({json:{tunnels}}))
await page.goto("/probes?server=1");await page.getByRole("button",{name:/BW-тест/}).click();const choice=page.getByRole("combobox",{name:"GRE-туннель для BW-теста"});await expect(run(page)).toBeEnabled();await choice.selectOption("gre-b")
tunnels=tunnels.slice(0,1);await page.getByRole("button",{name:"Обновить устройства диагностики",exact:true}).click()
await expect(choice.getByRole("option",{name:"Выбранный GRE"})).toHaveCount(0);await expect(choice).toHaveValue("");await expect(run(page)).toBeDisabled()
await choice.selectOption("gre-a");await expect(run(page)).toBeEnabled()
})
+10
View File
@@ -18,3 +18,13 @@ test("Диагностика: недоступный clipboard сохраняе
await page.addInitScript(()=>Object.defineProperty(navigator,"clipboard",{value:{writeText:()=>Promise.reject(new Error("permission denied"))},configurable:true}))
await page.goto("/probes?server=1");await page.getByRole("button",{name:"Скопировать параметры диагностики"}).click();await expect(page.getByText(/Не удалось скопировать/)).toBeVisible();await expect(page.getByRole("textbox",{name:"Цель диагностики"})).toHaveValue("8.8.8.8")
})
test("Диагностика: клавиатура, подтверждённый clipboard и место DNS",async({page})=>{
await page.route("**/health",r=>r.fulfill({json:{ok:true}}));await page.route("**/api/**",r=>r.fulfill({json:new URL(r.request().url()).pathname==="/api/servers"?[{id:1,name:"Устройство",host:"192.0.2.1",enabled:true,status:null,type:"exit-node"}]:new URL(r.request().url()).pathname==="/api/auth/config"?{required:false}:{tunnels:[]}}))
await page.addInitScript(()=>Object.defineProperty(navigator,"clipboard",{value:{writeText:async(text:string)=>{sessionStorage.setItem("test-clipboard",text)}},configurable:true}))
await page.route("**/api/servers/1/probes/run",r=>r.fulfill({json:{output:"example.test. IN CNAME target.test."}}))
await page.goto("/probes?server=1");const dns=page.getByRole("button",{name:"DNS",exact:true});await dns.focus();await page.keyboard.press("Enter");const cname=page.getByRole("button",{name:"CNAME",exact:true});await cname.focus();await page.keyboard.press("Space");await expect(cname).toHaveAttribute("aria-pressed","true")
const target=page.getByRole("textbox",{name:"Домен"});await target.fill("example.test");await target.press("Enter");const region=page.getByRole("region",{name:/^Вывод DNS/});await expect(region).toContainText("example.test. IN CNAME target.test.");await expect(target).toBeFocused();await expect(page.getByText("DNS приложения",{exact:true})).toBeVisible()
const copy=page.getByRole("button",{name:/^Скопировать показанный вывод/});await copy.focus();await page.keyboard.press("Enter");await expect(page.getByText("Скопировано.",{exact:true})).toBeVisible();expect(await page.evaluate(()=>sessionStorage.getItem("test-clipboard"))).toBe("example.test. IN CNAME target.test.")
const remove=page.getByRole("button",{name:/^Удалить результат/});await remove.focus();await page.keyboard.press("Enter");await expect(region).toHaveCount(0);await expect(page.getByRole("button",{name:"Запустить",exact:true})).toBeFocused()
})