From 094c812c37e92a9d2d6938ea11aed118a7843ebe Mon Sep 17 00:00:00 2001 From: Denozordec Date: Thu, 24 Sep 2026 09:53:10 +0700 Subject: [PATCH] =?UTF-8?q?fix(ui):=20=D0=B0=D0=B4=D0=B0=D0=BF=D1=82=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20=D0=B4=D0=BB=D0=B8=D0=BD?= =?UTF-8?q?=D0=BD=D1=8B=D0=B5=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D0=B5=20?= =?UTF-8?q?=D0=B8=20=D0=BF=D1=83=D1=81=D1=82=D1=8B=D0=B5=20=D1=81=D0=BF?= =?UTF-8?q?=D0=B8=D1=81=D0=BA=D0=B8=20=D1=82=D1=80=D0=B0=D1=84=D0=B8=D0=BA?= =?UTF-8?q?=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(main)/traffic/page.tsx | 29 ++++++++------- docs/ux-ui-progress.md | 8 +++++ tests/e2e/traffic-mobile.spec.ts | 61 ++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 12 deletions(-) create mode 100644 tests/e2e/traffic-mobile.spec.ts diff --git a/app/(main)/traffic/page.tsx b/app/(main)/traffic/page.tsx index d9277118..a12551b2 100644 --- a/app/(main)/traffic/page.tsx +++ b/app/(main)/traffic/page.tsx @@ -323,7 +323,7 @@ function StatChip({ icon, label, value }: { icon: ReactNode; label: string; valu return (
{icon}
-
+

{label}

{value}

@@ -475,13 +475,13 @@ function IfaceCard({ c, selected, onClick }: { c: BoundIfaceTraffic; selected: b function IfaceRow({ c, showServer = false }: { c: BoundIfaceTraffic; showServer?: boolean }) { return (
-
+
- {boundIfaceLabel(c)} + {boundIfaceLabel(c)} {IFACE_TYPE_LABEL[c.interfaceType]} {showServer && ( @@ -558,7 +558,7 @@ function TotalsRow({ rxTotal, txTotal, rxSeries, txSeries }: { rxTotal: number; txTotal: number; rxSeries: number[]; txSeries: number[] }) { return ( -
+

Получено за период

@@ -595,7 +595,7 @@ function ServerDetail({ <> -

{sel.name}

+

{sel.name}

{sel.site} @@ -662,8 +662,8 @@ function UserDetail({ sel, range, setRange }: { sel: UserTraffic; range: Range;
-

{sel.login}

-

{sel.displayName} · {sel.role}

+

{sel.login}

+

{sel.displayName} · {sel.role}

{!hasClients ? ( @@ -698,7 +698,7 @@ function IfaceDetail({ sel, range, setRange }: { sel: BoundIfaceTraffic; range:
-

{boundIfaceLabel(sel)}

+

{boundIfaceLabel(sel)}

{sel.comment || sel.userLogin}

{IFACE_TYPE_LABEL[sel.interfaceType]} @@ -717,7 +717,7 @@ function IfaceDetail({ sel, range, setRange }: { sel: BoundIfaceTraffic; range:

Сервер

-

{sel.serverName}

+

{sel.serverName}

Тип

@@ -725,7 +725,7 @@ function IfaceDetail({ sel, range, setRange }: { sel: BoundIfaceTraffic; range:

Пользователь

-

{sel.userLogin}

+

{sel.userLogin}

@@ -1178,7 +1178,7 @@ export default function TrafficPage() { {/* ── mode tab bar ── */}
-
+
{GROUP_MODES.map(({ mode, icon, label }) => ( } + {(!isLive || trafficReady) && (effectiveMode === "servers" ? sortedServers.length : effectiveMode === "users" ? sortedUsers.length : sortedIfaces.length) === 0 && ( +

+ {search.trim() ? "По вашему запросу ничего не найдено" : effectiveMode === "servers" ? "Нет серверов с данными трафика" : effectiveMode === "users" ? "Нет клиентов с данными трафика" : "Нет привязанных интерфейсов с данными трафика"} +

+ )} {/* entity list */}
{effectiveMode === "servers" && sortedServers.map(s => ( diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index ba3c68eb..367f275c 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -29,6 +29,14 @@ | 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат | | 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат | +## Блок 8.4Б — клиенты, интерфейсы и пустые списки (2026-09-24) + +- Длинные идентификаторы переносятся в деталях и строках привязанных интерфейсов; строки адаптированы под телефон. Мини-графики итогов ограничены шириной ячейки: устранено переполнение на 320 px. +- Пустой успешный список показывает объяснение для серверов/клиентов/интерфейсов; поиск без совпадений имеет отдельную подпись. Сообщения не показываются вместо загрузки или ошибки API. Выбранные детали при поиске сохраняются. +- 10 E2E прошли: четыре сценария деталей плюс клиенты/интерфейсы с длинными значениями на 320/390 px, пустой список клиентов и очистка поиска. Тесты проверяют выход элементов за край экрана, исключая намеренно прокручиваемую навигацию. Скриншот клиентов 320 px просмотрен; снимки сохранены в игнорируемом test-results. +- Production build/TypeScript прошли (34 страницы). Lint нового теста чистый; на странице остаётся прежняя ошибка синхронного сброса flowIface. diff-check чистый. Использовано 25% пятичасового и 66% недельного лимита. +- Далее: единицы/неизвестные значения, пустые состояния деталей и flow; оставшиеся проверки backend/сервера/SSE и строгая валидация sample. Этап 8 остаётся в работе. API-тесты с моками не подтверждают реальную телеметрию RouterOS/IPFIX. + ## Блок 8.4А2 — мобильный период серверного трафика (2026-09-24) - Добавлены E2E ширин 320/390 px: кнопка 24ч помещается в экран, меняет период, показывает подтверждённые детали; документ не переполняется по горизонтали. Всего 4 теста деталей прошли, lint теста чистый. Изменений production-кода в этом блоке нет. diff --git a/tests/e2e/traffic-mobile.spec.ts b/tests/e2e/traffic-mobile.spec.ts new file mode 100644 index 00000000..0fae27e6 --- /dev/null +++ b/tests/e2e/traffic-mobile.spec.ts @@ -0,0 +1,61 @@ +import { test, expect, type Page } from '@playwright/test' + +const longName = 'customer-with-a-very-long-unbroken-identifier-12345678901234567890' +async function setup(page: Page, empty = false) { + const metrics = { rxNow: 2, txNow: 3, rxPeak: 4, txPeak: 5, rxTotal: 100, txTotal: 200, rxSeries: [1, 2], txSeries: [2, 3] } + const iface = { ...metrics, id: 'i1', bindingId: 'b1', userId: 'u1', userLogin: longName, userName: longName, interfaceName: longName, interfaceType: 'gre', comment: longName, serverId: '1', serverName: longName, serverSite: 'TEST', serverCountry: 'RU', status: 'online' } + const user = { ...metrics, id: 'u1', login: longName, displayName: longName, role: 'viewer', interfaces: [iface] } + await page.route('**/health', r => r.fulfill({ json: { ok: true } })) + await page.route('**/api/**', route => { + const path = new URL(route.request().url()).pathname + const json = path === '/api/auth/config' ? { required: false } + : path === '/api/traffic/servers' ? { servers: [] } + : path === '/api/traffic/users' ? { users: empty ? [] : [user] } + : path === '/api/traffic/bound-interfaces' ? { interfaces: empty ? [] : [iface] } + : path.endsWith('/interfaces') ? { interfaces: [] } + : path === '/api/servers' ? [] : {} + return route.fulfill({ json }) + }) + await page.goto('/traffic') +} + +for (const width of [320, 390]) { + for (const mode of ['Клиенты', 'Интерфейсы']) { + test(`Трафик: ${mode}, длинные значения, ${width}px`, async ({ page }) => { + await page.setViewportSize({ width, height: 900 }) + await setup(page) + await page.getByRole('button', { name: mode, exact: true }).click() + const heading = page.getByRole('heading', { name: longName, exact: true }) + await expect(heading).toBeVisible() + const box = await heading.boundingBox() + expect(box!.x + box!.width).toBeLessThanOrEqual(width + 1) + await page.getByRole('button', { name: '24ч', exact: true }).click() + await expect(heading).toBeVisible() + const overflow = await page.evaluate(() => [...document.querySelectorAll('main *')].filter(el => { + const r = el.getBoundingClientRect() + return r.width > 0 && r.right > innerWidth + 1 && getComputedStyle(el).position !== 'fixed' && !el.closest('[aria-label="Разделы трафика"]') + }).map(el => el.tagName + '.' + el.className)) + expect(overflow).toEqual([]) + await page.screenshot({ path: `test-results/traffic-${mode}-${width}.png`, fullPage: true }) + }) + } +} + +test('Трафик: пустой список клиентов имеет объяснение', async ({ page }) => { + await setup(page, true) + await page.getByRole('button', { name: 'Клиенты', exact: true }).click() + await expect(page.getByText('Нет клиентов с данными трафика', { exact: true })).toBeVisible() +}) + + +test('Трафик: поиск без совпадений сохраняет данные при очистке', async ({ page }) => { + await setup(page) + await page.getByRole('button', { name: 'Клиенты', exact: true }).click() + await expect(page.getByRole('heading', { name: longName, exact: true })).toBeVisible() + await page.getByPlaceholder('Поиск…').fill('отсутствующий-клиент') + await expect(page.getByText('По вашему запросу ничего не найдено', { exact: true })).toBeVisible() + await expect(page.getByText('Нет клиентов с данными трафика', { exact: true })).toHaveCount(0) + await page.getByPlaceholder('Поиск…').fill('') + await expect(page.getByText('По вашему запросу ничего не найдено', { exact: true })).toHaveCount(0) + await expect(page.getByRole('button').filter({ hasText: longName })).toHaveCount(1) +})