fix(ui): адаптировать формы серверов и прокрутку ресурсов

This commit is contained in:
Denozordec committed 2026-09-23 14:48:13 +07:00
1 parent 9cd15528c9
commit ca9474ac83
7 files changed
+74 -11

No files matched your search

+4 -3
View File
@@ -720,7 +720,7 @@ function ServersPageContent() {
{/* ══ Sheet: Add / Edit Server ══════════════════════════════════════════ */} {/* ══ Sheet: Add / Edit Server ══════════════════════════════════════════ */}
<Sheet open={open} onOpenChange={closeForm}> <Sheet open={open} onOpenChange={closeForm}>
<SheetContent side="right" showCloseButton={!saving} className="w-full sm:max-w-lg flex flex-col gap-0 p-0"> <SheetContent side="right" showCloseButton={!saving} className="data-[side=right]:w-full sm:max-w-lg flex flex-col gap-0 p-0">
<SheetHeader className="px-6 pt-6 pb-4 border-b shrink-0"> <SheetHeader className="px-6 pt-6 pb-4 border-b shrink-0">
<SheetTitle>{sheetMode === "edit" ? "Редактировать сервер" : "Новый сервер"}</SheetTitle> <SheetTitle>{sheetMode === "edit" ? "Редактировать сервер" : "Новый сервер"}</SheetTitle>
<SheetDescription>MikroTik RouterOS · Web API (REST)</SheetDescription> <SheetDescription>MikroTik RouterOS · Web API (REST)</SheetDescription>
@@ -731,7 +731,7 @@ function ServersPageContent() {
{!detailsReady && !detailsLoading && editingId && <Button type="button" variant="outline" onClick={() => { const server = serverList.find(s => s.id === editingId); if (server) void openEdit(server) }}>Повторить загрузку</Button>} {!detailsReady && !detailsLoading && editingId && <Button type="button" variant="outline" onClick={() => { const server = serverList.find(s => s.id === editingId); if (server) void openEdit(server) }}>Повторить загрузку</Button>}
</div>} </div>}
<fieldset disabled={!canWrite || saving || !detailsReady} className="flex min-h-0 flex-1 flex-col" aria-busy={saving || detailsLoading}> <fieldset disabled={!canWrite || saving || !detailsReady} className="flex min-h-0 min-w-0 flex-1 flex-col" aria-busy={saving || detailsLoading}>
<Stepper value={sheetStep} onValueChange={setSheetStep} className="flex-1 flex flex-col min-h-0 px-6 py-5"> <Stepper value={sheetStep} onValueChange={setSheetStep} className="flex-1 flex flex-col min-h-0 px-6 py-5">
<StepperNav className="mb-5"> <StepperNav className="mb-5">
<StepperItem step={1}> <StepperItem step={1}>
@@ -772,6 +772,7 @@ function ServersPageContent() {
<FormField label="Тип узла" required> <FormField label="Тип узла" required>
<SegmentedControl <SegmentedControl
className="max-w-full flex-wrap"
value={form.type} value={form.type}
onChange={v => set("type", v)} onChange={v => set("type", v)}
options={[ options={[
@@ -945,7 +946,7 @@ function ServersPageContent() {
</Stepper> </Stepper>
</fieldset> </fieldset>
<SheetFooter className="px-6 py-4 border-t shrink-0 flex-row gap-2"> <SheetFooter className="px-6 py-4 border-t shrink-0 flex-row flex-wrap gap-2">
<SheetClose render={<Button variant="outline" disabled={saving} />}>Отмена</SheetClose> <SheetClose render={<Button variant="outline" disabled={saving} />}>Отмена</SheetClose>
{sheetStep > 1 && ( {sheetStep > 1 && (
<Button variant="outline" disabled={saving || !detailsReady} onClick={() => setSheetStep((s) => s - 1)}>Назад</Button> <Button variant="outline" disabled={saving || !detailsReady} onClick={() => setSheetStep((s) => s - 1)}>Назад</Button>
+1 -5
View File
@@ -1030,11 +1030,7 @@ function ResourcesTab({ resources, serversList, liveApi }: { resources: ServerRe
{/* ── Table ─────────────────────────────────────────────────────────── */} {/* ── Table ─────────────────────────────────────────────────────────── */}
<DataPageCard> <DataPageCard>
<DataPageToolbar <DataPageToolbar
segmented={{ leading={<SegmentedControl className="max-w-full flex-wrap" value={typeFilter} onChange={setTypeFilter} options={typeOpts} />}
value: typeFilter,
onChange: setTypeFilter,
options: typeOpts,
}}
search={resSearch} search={resSearch}
onSearchChange={setResSearch} onSearchChange={setResSearch}
searchPlaceholder="Поиск по имени, площадке…" searchPlaceholder="Поиск по имени, площадке…"
@@ -1,6 +1,6 @@
"use client" "use client"
import { dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid" import { DataGridContainer, DataGridScrollArea, DataGridTable, dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid"
import { useMemo } from "react" import { useMemo } from "react"
import { import {
@@ -377,7 +377,13 @@ function UptimeResourcesDataGrid({ rows }: UptimeResourcesDataGridProps) {
"[&:has([data-resource-crit=true])]:bg-red-500/3", "[&:has([data-resource-crit=true])]:bg-red-500/3",
), ),
}} }}
/> >
<DataGridContainer border={false} className="w-full rounded-none border-0">
<DataGridScrollArea orientation="horizontal">
<DataGridTable />
</DataGridScrollArea>
</DataGridContainer>
</DataGridShell>
) )
} }
+7
View File
@@ -29,6 +29,13 @@
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат | | 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат |
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат | | 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
## Приёмка 7.5В — формы серверов и таблица Resources (2026-09-23)
- Форма сервера на телефоне занимает полную ширину; fieldset получил min-w-0, выбор типа узла и кнопки подвала допускают перенос. На 320 px прежде выходили за экран поля, страны и последний шаг. Проверены четыре шага редактирования, выбор Home Router, добавленный WAN, раскрытые дополнительные поля и Escape на 320/390/1440 px. Существующие проверки создания и ошибок сохранения также прошли.
- Resources: фильтры типов переносятся внутри панели. Таблица обёрнута установленным ReUI DataGridScrollArea с orientation=horizontal внутри DataGridContainer: правые колонки доступны при прокрутке, общая страница не расширяется. Общая оболочка DataGridShell, vendored ReUI и тема не изменены.
- Проверки: 55 профильных E2E серверов/мониторинга прошли; затем ещё 2 уточнённые проверки Resources на 320/390 px подтвердили доступ к значению последней колонки, поиск, пустой результат и восстановление строки. Скриншоты WAN, дополнительных полей и прокрученной таблицы просмотрены. Production build и TypeScript прошли: 34 статические страницы. Lint изменённых файлов чистый, кроме исходных 1 ошибки и 6 предупреждений uptime. ReUI API и audit checklist проверены.
- Границы: HTTP-моки, без реального RouterOS/JWT; сортировка в адаптивном тесте проверена на одной строке, порядок нескольких строк этим тестом не доказывается. Сквозной сценарий и смена backend/mode при незавершённой операции остаются следующими пунктами 7.5. Этап 7 целиком ещё не завершён. На контрольной точке осталось 23% пятичасовой и 72% недельной квоты. Локальный коммит без push.
## Приёмка 7.5Б — адаптивность мониторинга и форм (2026-09-23) ## Приёмка 7.5Б — адаптивность мониторинга и форм (2026-09-23)
- Формы Ping/Speed используют всю ширину телефона; min-w-0 у fieldset устраняет выход полей за границы панели. Параметры Speed идут вертикально также на ПК: ширины боковой панели недостаточно для трёх колонок без перекрытия Direction и длительности. - Формы Ping/Speed используют всю ширину телефона; min-w-0 у fieldset устраняет выход полей за границы панели. Параметры Speed идут вертикально также на ПК: ширины боковой панели недостаточно для трёх колонок без перекрытия Direction и длительности.
+1 -1
View File
@@ -61,7 +61,7 @@ URL — источник сохранённого состояния. Back/Forwa
3. **7.4 — формы, детали и полный контекст мониторинга.** Восстановление вкладки/фильтров из URL, понятные единицы и подписи, возврат к списку после действия. Не дублировать сохранённые исправления этапа 1; проверить загрузку, отмену, 400/403/500, timeout и повторное нажатие. 3. **7.4 — формы, детали и полный контекст мониторинга.** Восстановление вкладки/фильтров из URL, понятные единицы и подписи, возврат к списку после действия. Не дублировать сохранённые исправления этапа 1; проверить загрузку, отмену, 400/403/500, timeout и повторное нажатие.
4. **7.5 — приёмка этапа.** Сценарий «проблема → сервер/проверка → действие → возврат» с сохранением контекста; профильные E2E, права в HTTP-моках, TypeScript, lint и build. Реальные JWT/RouterOS отдельно в тестовом окружении. 4. **7.5 — приёмка этапа.** Сценарий «проблема → сервер/проверка → действие → возврат» с сохранением контекста; профильные E2E, права в HTTP-моках, TypeScript, lint и build. Реальные JWT/RouterOS отдельно в тестовом окружении.
Следующий исполняемый шаг: 7.5 — адаптивность форм серверов и таблицы Resources после прокрутки, сквозные переходы, смена backend/mode при незавершённой операции и фиксация ограничений. Формы Ping/Speed и вкладки проверены на 320/390/1440 px; устранены переполнение форм, перекрытие параметров Speed и сдвиг страницы при выборе вкладки. URL-контекст вкладки, Ping и Resources выполнен. Учесть границы HTTP-моков, экспорт серверов и ограничения сетевых операций из журнала; этап 7 целиком пока не завершён. Следующий исполняемый шаг: 7.5 — сквозные переходы дашборд → объект → действие → возврат, смена backend/mode при незавершённой операции и фиксация ограничений. Формы серверов проверены на 320/390/1440 px, Resources после прокрутки — на 320/390 px; исправлены переполнение формы и обрезка фильтров/таблицы. Формы Ping/Speed и вкладки проверены на 320/390/1440 px; устранены переполнение форм, перекрытие параметров Speed и сдвиг страницы при выборе вкладки. URL-контекст вкладки, Ping и Resources выполнен. Учесть границы HTTP-моков, экспорт серверов и ограничения сетевых операций из журнала; этап 7 целиком пока не завершён.
## Решение по синхронизации URL (7.1) ## Решение по синхронизации URL (7.1)
+24
View File
@@ -509,3 +509,27 @@ test("права серверов: отзыв перед сохранением
await expect(page.getByRole("button", { name: "Сохранить", exact: true })).toBeDisabled() await expect(page.getByRole("button", { name: "Сохранить", exact: true })).toBeDisabled()
expect(writes).toBe(0) expect(writes).toBe(0)
}) })
for (const width of [320, 390, 1440]) {
test(`приёмка формы сервера ${width}px: все шаги и отмена`, async ({ page }) => {
await page.setViewportSize({ width, height: 844 })
await setup(page)
await edit(page)
const dialog = page.getByRole('dialog')
await dialog.getByRole("button", { name: "Home Router", exact: true }).click()
for (let step = 1; step <= 4; step++) {
if (step === 2) await dialog.getByRole("button", { name: "Добавить WAN-аплинк", exact: true }).click()
if (step === 4) await dialog.getByRole("button", { name: "Дополнительно", exact: true }).last().click()
await expect(dialog.getByRole('button', { name: 'Отмена', exact: true })).toBeInViewport()
await expect.poll(() => dialog.locator('button, input').evaluateAll(elements => elements.filter(el => {
const rect = el.getBoundingClientRect()
return rect.width > 0 && (rect.left < -1 || rect.right > innerWidth + 1)
}).map(el => el.textContent || el.tagName))).toEqual([])
await page.screenshot({ path: `test-results/server-form-${width}-${step}.png`, animations: 'disabled' })
if (step < 4) await dialog.getByRole('button', { name: 'Далее', exact: true }).click()
}
await expect(dialog.getByRole('button', { name: 'Сохранить', exact: true })).toBeInViewport()
await page.keyboard.press('Escape')
await expect(dialog).toBeHidden()
})
}
+29
View File
@@ -612,3 +612,32 @@ for (const width of [320, 390, 1440]) {
await page.screenshot({ path: `test-results/speed-${width}.png`, animations: "disabled" }) await page.screenshot({ path: `test-results/speed-${width}.png`, animations: "disabled" })
}) })
} }
for (const width of [320, 390]) {
test(`Resources ${width}px: таблица после прокрутки и поиск`, async ({ page }) => {
await page.setViewportSize({ width, height: 844 })
await resourceSetup(page)
const search = page.getByPlaceholder('Поиск по имени, площадке…')
await search.scrollIntoViewIfNeeded()
await search.fill('Источник')
await expect(page.locator('tr[data-row-id]')).toHaveCount(1)
const cpu = page.getByRole('button', { name: 'CPU', exact: true })
await cpu.click()
await expect(cpu).toBeInViewport()
const viewport = page.locator('[data-slot="data-grid"] [data-slot="scroll-area-viewport"]')
await viewport.evaluate(el => { el.scrollLeft = el.scrollWidth })
await expect(page.getByRole("columnheader").last()).toBeInViewport()
const lastValue = page.locator("tr[data-row-id] td").last().locator("span").last()
await lastValue.scrollIntoViewIfNeeded()
await expect(lastValue).toBeInViewport()
const home = page.getByRole("button", { name: "Home Router", exact: true })
const bounds = await home.boundingBox()
expect(bounds!.x + bounds!.width).toBeLessThanOrEqual(width)
await page.screenshot({ path: `test-results/resources-table-${width}.png`, animations: 'disabled' })
await search.fill('нет-совпадений')
await expect(page.getByText('Ничего не найдено', { exact: true })).toBeVisible()
await search.clear()
await expect(page.locator('tr[data-row-id]')).toHaveCount(1)
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true)
})
}