fix(ui): сохранять явный выбор туннеля диагностики
No files matched your search
@@ -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>
|
||||
|
||||
@@ -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, кнопки/переключатели и область вывода имеют доступные имена и состояния.
|
||||
|
||||
@@ -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А: пользователи. Моки не доказывают реальную сеть/БД/портал.
|
||||
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
@@ -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()
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||