From ab334764cf050ef58b57be9673c480603984f6db Mon Sep 17 00:00:00 2001 From: Denozordec Date: Sun, 4 Oct 2026 03:00:32 +0700 Subject: [PATCH] =?UTF-8?q?fix(ui):=20=D1=80=D0=B0=D1=81=D0=BA=D1=80=D1=8B?= =?UTF-8?q?=D0=B2=D0=B0=D1=82=D1=8C=20=D0=BF=D1=80=D0=B5=D0=B4=D1=83=D0=BF?= =?UTF-8?q?=D1=80=D0=B5=D0=B6=D0=B4=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=80=D0=B5?= =?UTF-8?q?=D1=81=D1=83=D1=80=D1=81=D0=BE=D0=B2=20=D0=BF=D0=BE=D1=80=D1=86?= =?UTF-8?q?=D0=B8=D1=8F=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(main)/uptime/page.tsx | 55 +++++++++++++----------- docs/ux-ui-performance.md | 2 + docs/ux-ui-plan.md | 2 +- docs/ux-ui-progress.md | 10 ++++- docs/ux-ui-stage-12.md | 8 ++++ tests/e2e/large-uptime-resources.spec.ts | 28 ++++++++++++ 6 files changed, 77 insertions(+), 28 deletions(-) diff --git a/app/(main)/uptime/page.tsx b/app/(main)/uptime/page.tsx index 2eb4572d..d2dc398b 100644 --- a/app/(main)/uptime/page.tsx +++ b/app/(main)/uptime/page.tsx @@ -870,6 +870,34 @@ function SortIcon({ k, sortKey, sortAsc }: { k: ResSortKey; sortKey: ResSortKey; type ResTypeFilter = "all" | "jump-host" | "exit-node" | "home-router" +function ResourceWarnings({ rows }: { rows: UptimeResourceRow[] }) { + const [expanded, setExpanded] = useState(false) + const [visible, setVisible] = useState(55) + if (!rows.length) return null + function renderRow(row: UptimeResourceRow) { + const issues: string[] = [] + if (row.cpu >= 85) issues.push(`CPU ${row.cpu}%`) + if (row.ramPct >= 85) issues.push(`RAM ${row.ramPct}%`) + if (row.hddPct >= 85) issues.push(`HDD ${row.hddPct}%`) + if ((row.temp ?? 0) >= 70) issues.push(`${row.temp}°C`) + return + {row.server.name} — {issues.join(", ")} + + } + return + + Серверов с предупреждениями: {rows.length} + +
{rows.slice(0, 5).map(renderRow)}
+ {rows.length > 5 &&
{ setExpanded(event.currentTarget.open); if (!event.currentTarget.open) setVisible(55) }}> + Ещё {rows.length - 5} + {expanded &&
{rows.slice(5, visible).map(renderRow)}
} + {expanded && rows.length > visible && } +
} +
+
+} + function ResourcesTab({ resources, serversList, liveApi }: { resources: ServerResource[]; serversList: Server[]; liveApi?: boolean }) { const { resource, setResourceSearch: setResSearch, setResourceType: setTypeFilter, resetResources } = useUptimeUrlState() const { search: resSearch, type: typeFilter, error: resourceError } = resource @@ -944,32 +972,7 @@ function ResourcesTab({ resources, serversList, liveApi }: { resources: ServerRe
{/* ── Alert banner ──────────────────────────────────────────────────── */} - {alerts.length > 0 && ( - - - - {alerts.length} {alerts.length === 1 ? "сервер требует внимания" : "сервера требуют внимания"} - - -
- {alerts.map(r => { - const s = r.server - const issues: string[] = [] - if (r.cpu >= 85) issues.push(`CPU ${r.cpu}%`) - if (r.ramPct >= 85) issues.push(`RAM ${r.ramPct}%`) - if (r.hddPct >= 85) issues.push(`HDD ${r.hddPct}%`) - if ((r.temp ?? 0) >= 70) issues.push(`${r.temp}°C`) - return ( - - - {s.name} — {issues.join(", ")} - - ) - })} -
-
-
- )} + { test.setTimeout(90000) @@ -44,8 +45,35 @@ test("1000 ресурсов: страницы, поиск, обновление for (let i = 0; i < 3; i++) await page.getByRole("button", { name: "…", exact: true }).last().click() await page.getByRole("button", { name: "Перейти на страницу 20", exact: true }).click() await expect(page.getByText("951–1000 из 1000", { exact: true })).toBeVisible() + resources = resources.map(resource => ({ ...resource, cpu: 90 })) + await page.getByRole("button", { name: "Обновить данные", exact: true }).click() + const warnings = page.getByRole("alert").filter({ hasText: "Серверов с предупреждениями: 1000" }) + await expect(warnings).toBeVisible() + await expect(warnings.locator("[data-resource-warning]")).toHaveCount(5) + console.log(JSON.stringify({ warningRows: 1000, warningNodes: await warnings.locator("*").count(), domNodes: await page.locator("*").count() })) + expect(await page.locator("*").count()).toBeLessThan(3500) + const summary = warnings.locator("summary") + await summary.focus() + await page.keyboard.press("Enter") + await expect(warnings.locator("[data-resource-warning]")).toHaveCount(55) + await warnings.getByRole("button", { name: "Показать ещё 50", exact: true }).click() + await expect(warnings.locator("[data-resource-warning]")).toHaveCount(105) + await summary.click() + await expect(warnings.locator("[data-resource-warning]")).toHaveCount(5) + await summary.click() + await expect(warnings.locator("[data-resource-warning]")).toHaveCount(55) + await summary.click() + await page.setViewportSize({ width: 320, height: 900 }) expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true) + await page.addScriptTag({ content: axe.source }) + const audit = await page.evaluate(async () => { + const engine = (window as unknown as { axe: typeof axe }).axe + const root = document.querySelector("[data-resource-warning]")!.closest("[role=alert]")! + return engine.run(root, { runOnly: { type: "tag", values: ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa"] } }) + }) + await info.attach("resource-warning-axe", { body: JSON.stringify(audit), contentType: "application/json" }) + expect(audit.violations.map(violation => ({ id: violation.id, nodes: violation.nodes.map(node => node.target) }))).toEqual([]) resources = resources.slice(0, 1) await page.getByRole("button", { name: "Обновить данные", exact: true }).click() await expect(rows).toHaveCount(1)