fix(ui): показать измерения рядом с закреплённым WAN на телефоне
This commit is contained in:
1 parent
5919af7f9b
commit
579e816bbb
7 files changed
+152
-9
No files matched your search
@@ -2,6 +2,7 @@
|
||||
|
||||
import { dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid"
|
||||
|
||||
import { useIsMobile } from "@/hooks/use-mobile"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Button } from "@/components/ui/button"
|
||||
@@ -41,6 +42,7 @@ function RouteOptimizerWanMatrixDataGrid({
|
||||
legs,
|
||||
jumpHosts,
|
||||
}: RouteOptimizerWanMatrixDataGridProps) {
|
||||
const isMobile = useIsMobile()
|
||||
const [wanQuery, setWanQuery] = useState("")
|
||||
const [jhQuery, setJhQuery] = useState("")
|
||||
const [jhPage, setJhPage] = useState(0)
|
||||
@@ -69,17 +71,17 @@ function RouteOptimizerWanMatrixDataGrid({
|
||||
const base: ColumnDef<DataGridFeatures, HomeRouter["wans"][number]>[] = [
|
||||
{
|
||||
id: "wan",
|
||||
size: 180,
|
||||
size: isMobile ? 112 : 180,
|
||||
accessorKey: "name",
|
||||
header: () => <span className="text-[11px] font-medium text-muted-foreground">WAN-аплинк</span>,
|
||||
cell: ({ row }) => {
|
||||
const wan = row.original
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<WifiIcon aria-hidden="true" className="size-3.5 text-muted-foreground shrink-0" />
|
||||
<div>
|
||||
<p className="font-mono text-xs font-semibold">{wan.name}</p>
|
||||
<p className="text-[10px] text-muted-foreground">{wan.iface}</p>
|
||||
<div className="min-w-0">
|
||||
<p title={wan.name} className="truncate font-mono text-xs font-semibold">{wan.name}</p>
|
||||
<p title={wan.iface} className="truncate text-[10px] text-muted-foreground">{wan.iface}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -127,7 +129,7 @@ function RouteOptimizerWanMatrixDataGrid({
|
||||
|
||||
const jhCols: ColumnDef<DataGridFeatures, HomeRouter["wans"][number]>[] = visibleHosts.map((jh) => ({
|
||||
id: `jh-${jh.id}`,
|
||||
size: 200,
|
||||
size: isMobile ? 160 : 200,
|
||||
header: () => (
|
||||
<div className="text-center">
|
||||
<div className="text-[11px] font-medium text-muted-foreground">{jh.label}</div>
|
||||
@@ -194,7 +196,7 @@ function RouteOptimizerWanMatrixDataGrid({
|
||||
}
|
||||
|
||||
return [...base, ...jhCols]
|
||||
}, [bestScore, visibleHosts, legMap])
|
||||
}, [bestScore, visibleHosts, legMap, isMobile])
|
||||
|
||||
const table = useTable({
|
||||
features: dataGridFeatures,
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# План реализации UX/UI по пятичасовым окнам
|
||||
|
||||
Актуальное продолжение 2026-10-07: 12.3.75 завершён: OSPF raw/model используют client RX→DL/TX→UL,4backend suites и build. Следующий12.3.76: mobile pinned-column readable geometry. После этого обновить профильную и общую регрессию; команды/права не менять,12.5 отдельно.
|
||||
Актуальное продолжение 2026-10-07: 12.3.76 завершён: мобильная ReUI WAN/JH матрица112/160, геометрия/resize/WCAG,26browser/type/lint/build. Следующий12.4.8: общая main регрессия на новом HEAD; затем12.4.9 context. Реальная12.5 отдельно, stage12 пока не завершён.
|
||||
|
||||
Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-04. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# UX/UI: журнал реализации
|
||||
|
||||
Актуальное продолжение 2026-10-07: 12.3.75 завершён: OSPF raw/model используют client RX→DL/TX→UL,4backend suites и build. Следующий12.3.76: mobile pinned-column readable geometry. После этого обновить профильную и общую регрессию; команды/права не менять,12.5 отдельно.
|
||||
Актуальное продолжение 2026-10-07: 12.3.76 завершён: мобильная ReUI WAN/JH матрица112/160, геометрия/resize/WCAG,26browser/type/lint/build. Следующий12.4.8: общая main регрессия на новом HEAD; затем12.4.9 context. Реальная12.5 отдельно, stage12 пока не завершён.
|
||||
|
||||
Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения.
|
||||
|
||||
|
||||
@@ -835,3 +835,12 @@ CPU final:1000routes13.3ms;100000(10×100×100)49.7ms;100000(100×10×100)44.0ms
|
||||
Четыре backend suites passed: pure metrics (новые asymmetric80.75/30.25,0/null,rounding/floor/fallback), identity, plan и HTTP optimizer с DI/in-memory writes. Backend build passed. Source review подтвердил фильтрацию по srcServerId и вызов helper в builder. Реальный DB builder и RouterOS не исполнялись; HTTP suite подменяет buildOptimizationPlan и отдельно проверяет отсутствие операций до валидного плана. Frontend не менялся, повторный browser/build для него не требовался.
|
||||
|
||||
Следующий76: на320px проверить видимость всей соседней metric cell за pinnedWAN, исправить column sizing при подтверждённой проблеме. Общая main/context регрессия прежних HEAD требует обновления позже;12.5 остаётся внешней.
|
||||
|
||||
|
||||
## 12.3.76 — читаемая мобильная WAN/JH матрица
|
||||
|
||||
2026-10-07. Геометрический baseline320px подтвердил дефект: viewport right303,pinned right197,cell right397 (200px колонка рядом с180px WAN). Пропорции ColumnDef на телефоне теперь112/160, desktop180/200 сохранены (actual table fill может расширять размеры). Hook useIsMobile и installed ReUI DataGridShell/ScrollArea/Table переиспользованы, primitive/hook/theme не менялись. Имя/интерфейс WAN ограничены truncate/min-w-0; полное значение остаётся DOM text/title и доступно поиску.
|
||||
|
||||
6 новых geometry cases320/390/1440×1/7JH проверяют всю metric cell и её speed text относительно pinnedRight/viewportRight, все7JH через переключение колонок, long WAN, отсутствие page overflow/API writes. Resize320→1440→320 прошёл, scoped WCAG axe passed. Related26browser, targeted lint, non-incremental TypeScript и frontend production build passed. Первый baseline намеренно failed на реальном переполнении, финальные assertions не ослаблялись. ReUI API и audit checklist прочитаны: v9useTable/dataGridFeatures/table/recordCount, existing layout/semantic tokens/security/scroll composition сохранены.
|
||||
|
||||
Доказательства: [геометрия до/после](ux-ui/acceptance/matrix-geometry-2026-10-07.json), [экран320px](ux-ui/screens/optimizer-matrix-readable-320.png) просмотрен. Historic9optimizer и2speed PNG восстановлены; предыдущие снимки не переписаны. Следующий12.4.8main/12.4.9context повтор на новом HEAD. Внешняя12.5 не выполнена.
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"date": "2026-10-07",
|
||||
"baseHead": "5919af7",
|
||||
"sourceSha256": "70fb4d950ccef0c4feaec8e613c8c3fdb48f75a0fc862c8d7a06a4af51196b8e",
|
||||
"baseline": {
|
||||
"width": 320,
|
||||
"cellLeft": 197,
|
||||
"cellRight": 397,
|
||||
"pinnedRight": 197,
|
||||
"viewportRight": 303,
|
||||
"pinnedWidth": 180,
|
||||
"cellWidth": 200
|
||||
},
|
||||
"final": {
|
||||
"width": 320,
|
||||
"count": 1,
|
||||
"measurements": [
|
||||
{
|
||||
"width": 320,
|
||||
"count": 1,
|
||||
"cellLeft": 143,
|
||||
"cellRight": 303,
|
||||
"pinnedRight": 129,
|
||||
"viewportRight": 303,
|
||||
"pinnedWidth": 112,
|
||||
"cellWidth": 160
|
||||
},
|
||||
{
|
||||
"width": 1440,
|
||||
"count": 1,
|
||||
"cellLeft": 1062.21875,
|
||||
"cellRight": 1415,
|
||||
"pinnedRight": 582.484375,
|
||||
"viewportRight": 1415,
|
||||
"pinnedWidth": 317.484375,
|
||||
"cellWidth": 352.78125
|
||||
},
|
||||
{
|
||||
"width": 320,
|
||||
"count": 1,
|
||||
"cellLeft": 143,
|
||||
"cellRight": 303,
|
||||
"pinnedRight": 129,
|
||||
"viewportRight": 303,
|
||||
"pinnedWidth": 112,
|
||||
"cellWidth": 160
|
||||
}
|
||||
],
|
||||
"writes": []
|
||||
},
|
||||
"scope": "6 geometry cases and related 26-case profile passed; WCAG scoped; no RouterOS/DB writes"
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
@@ -0,0 +1,80 @@
|
||||
import { test, expect } from "@playwright/test"
|
||||
import axe from "axe-core"
|
||||
import { writeFileSync } from "node:fs"
|
||||
|
||||
for (const width of [320, 390, 1440]) for (const count of [1, 7]) {
|
||||
test(`Матрица: измерение целиком рядом с pinned WAN/${width}/${count}`, async ({ page }, info) => {
|
||||
await page.setViewportSize({ width, height: 1000 })
|
||||
const longName = "WAN-" + "площадка".repeat(15)
|
||||
const common = { country: "RU", enabled: true, status: "online", latency: 10, site: "TEST" }
|
||||
const servers = [
|
||||
{ ...common, id: 1, name: "Home", host: "192.0.2.1", type: "home-router", wanUplinks: [{ id: "wan", name: longName, iface: "ether1", isp: "ISP", ip: "198.51.100.1", maxDl: 100, maxUl: 100 }] },
|
||||
...Array.from({ length: count }, (_, i) => ({ ...common, id: i + 2, name: `Jump ${i + 1}`, host: `192.0.2.${i + 2}`, type: "jump-host" })),
|
||||
]
|
||||
const probes = servers.slice(1).map(s => ({ id: `probe-${s.id}`, srcServerId: "1", dstServerId: String(s.id), srcInterface: "ether1", dstInterface: "", enabled: true, lastPingRttMs: 5, lastPingLossPct: 0, lastTxAvgMbps: 30, lastRxAvgMbps: 80, lastPingAt: "2026-10-07T00:00:00Z" }))
|
||||
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" && !path.endsWith("/preview")) writes.push(path)
|
||||
return r.fulfill({ json: path === "/api/auth/config" ? { required: false } : path === "/api/servers" ? servers : path === "/api/filters/rules" ? { rulesets: [] } : path === "/api/uptime/speed-probes" ? { probes } : path === "/api/ospf/catalog" ? { interfaces: [], instances: [], neighbors: [], bfdSessions: [], routes: [], failures: [] } : path.endsWith("/preview") ? { serverId: 1, serverName: "Home", pingWeight: 60, interfacesTotal: 0, changedCount: 0, unchangedCount: 0, interfaces: [], changes: [] } : {} })
|
||||
})
|
||||
await page.goto("/route-optimizer")
|
||||
const region = page.getByRole("region", { name: "Матрица WAN и Jump Host" })
|
||||
const row = region.locator("tr[data-row-id]")
|
||||
await expect(row).toHaveCount(1)
|
||||
const viewport = region.locator('[data-slot="scroll-area-viewport"]')
|
||||
const measurements: unknown[] = []
|
||||
async function inspectColumns() {
|
||||
const cells = row.locator("td").filter({ hasText: "↓80 ↑30 Мбит/с" })
|
||||
for (let i = 0; i < await cells.count(); i++) {
|
||||
const cell = cells.nth(i)
|
||||
await cell.evaluate(el => {
|
||||
const viewport = el.closest('[data-slot="scroll-area-viewport"]') as HTMLElement
|
||||
const pinned = el.parentElement!.querySelector('td[data-pinned="start"]')!
|
||||
viewport.scrollLeft += el.getBoundingClientRect().left - viewport.getBoundingClientRect().left - pinned.getBoundingClientRect().width
|
||||
})
|
||||
const geometry = await cell.evaluate(el => {
|
||||
const viewport = el.closest('[data-slot="scroll-area-viewport"]')!, pinned = el.parentElement!.querySelector('td[data-pinned="start"]')!
|
||||
const c = el.getBoundingClientRect(), v = viewport.getBoundingClientRect(), p = pinned.getBoundingClientRect()
|
||||
return { cellLeft: c.left, cellRight: c.right, pinnedRight: p.right, viewportRight: v.right, pinnedWidth: p.width, cellWidth: c.width }
|
||||
})
|
||||
measurements.push({ width: page.viewportSize()!.width, count, ...geometry })
|
||||
console.log("Matrix geometry", JSON.stringify({ width: page.viewportSize()!.width, count, ...geometry }))
|
||||
expect(geometry.cellLeft).toBeGreaterThanOrEqual(geometry.pinnedRight - 1)
|
||||
expect(geometry.cellRight).toBeLessThanOrEqual(geometry.viewportRight + 1)
|
||||
const text = await cell.getByText("↓80 ↑30 Мбит/с", { exact: true }).boundingBox()
|
||||
expect(text!.x).toBeGreaterThanOrEqual(geometry.pinnedRight - 1)
|
||||
expect(text!.x + text!.width).toBeLessThanOrEqual(geometry.viewportRight + 1)
|
||||
}
|
||||
}
|
||||
await inspectColumns()
|
||||
if (count > 5) {
|
||||
await region.getByRole("button", { name: "Следующие Jump Host" }).click()
|
||||
await expect(region).toContainText("Столбцы 6–7 из 7")
|
||||
await inspectColumns()
|
||||
}
|
||||
await expect(row.getByText(longName, { exact: true })).toHaveAttribute("title", longName)
|
||||
const bounds = await viewport.boundingBox()
|
||||
expect(bounds!.x + bounds!.width).toBeLessThanOrEqual(width)
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(width)
|
||||
if (width === 320 && count === 1) {
|
||||
await page.setViewportSize({ width: 1440, height: 1000 })
|
||||
await expect.poll(() => row.locator('td[data-pinned="start"]').evaluate(el => el.getBoundingClientRect().width)).toBeGreaterThanOrEqual(180)
|
||||
await inspectColumns()
|
||||
await page.setViewportSize({ width: 320, height: 1000 })
|
||||
await expect.poll(() => row.locator('td[data-pinned="start"]').evaluate(el => el.getBoundingClientRect().width)).toBe(112)
|
||||
await inspectColumns()
|
||||
await region.scrollIntoViewIfNeeded()
|
||||
await page.addStyleTag({ content: "nextjs-portal{display:none!important}" })
|
||||
await page.screenshot({ path: info.outputPath("matrix-readable-320.png"), animations: "disabled" })
|
||||
writeFileSync(info.outputPath("geometry.json"), JSON.stringify({ width, count, measurements, writes }, null, 2))
|
||||
}
|
||||
if (width === 320 && count === 1) {
|
||||
await page.addScriptTag({ content: axe.source })
|
||||
const result = await page.evaluate(async () => (window as unknown as { axe: typeof axe }).axe.run(document.querySelector('[aria-label="Матрица WAN и Jump Host"]')!, { runOnly: { type: "tag", values: ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"] } }))
|
||||
expect(result.violations.map(v => ({ id: v.id, nodes: v.nodes.map(n => n.target) }))).toEqual([])
|
||||
}
|
||||
expect(writes).toEqual([])
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user