fix(ui): сохранять фильтры ресурсов в URL

This commit is contained in:
Denozordec committed 2026-09-23 09:28:03 +07:00
1 parent fd7a08d5dc
commit 0734679fd6
7 files changed
+85 -6

No files matched your search

+7 -3
View File
@@ -871,8 +871,8 @@ function SortIcon({ k, sortKey, sortAsc }: { k: ResSortKey; sortKey: ResSortKey;
type ResTypeFilter = "all" | "jump-host" | "exit-node" | "home-router"
function ResourcesTab({ resources, serversList, liveApi }: { resources: ServerResource[]; serversList: Server[]; liveApi?: boolean }) {
const [resSearch, setResSearch] = useState("")
const [typeFilter, setTypeFilter] = useState<ResTypeFilter>("all")
const { resource, setResourceSearch: setResSearch, setResourceType: setTypeFilter, resetResources } = useUptimeUrlState()
const { search: resSearch, type: typeFilter, error: resourceError } = resource
const rows = useMemo((): UptimeResourceRow[] => resources.map((r) => {
const hasData = r.hasData !== false
@@ -903,6 +903,7 @@ function ResourcesTab({ resources, serversList, liveApi }: { resources: ServerRe
// Filtered + sorted
const visible = useMemo(() => {
if (resourceError) return []
let list = rows
if (typeFilter !== "all") list = list.filter(r => r.server.type === typeFilter)
if (resSearch.trim()) {
@@ -914,7 +915,7 @@ function ResourcesTab({ resources, serversList, liveApi }: { resources: ServerRe
)
}
return list
}, [rows, typeFilter, resSearch])
}, [rows, typeFilter, resSearch, resourceError])
function exportCsv() {
const header = ["Сервер", "Тип", "Площадка", "CPU %", "RAM %", "RAM использ.", "RAM всего", "HDD %", "HDD использ.", "HDD всего", "Uptime", "Температура °C", "RouterOS"]
@@ -1023,6 +1024,9 @@ function ResourcesTab({ resources, serversList, liveApi }: { resources: ServerRe
]}
/>
{resourceError && <ContextAlert variant="warning" role="alert"><ContextDescription>{resourceError}</ContextDescription></ContextAlert>}
{(resSearch || typeFilter !== "all" || resourceError) && <Button type="button" variant="outline" className="self-start" onClick={resetResources}>Сбросить фильтры ресурсов</Button>}
{/* ── Table ─────────────────────────────────────────────────────────── */}
<DataPageCard>
<DataPageToolbar
+9 -1
View File
@@ -21,7 +21,7 @@
| 4 | Остальные таблицы, удаление старых API, общая регрессия | Завершён 2026-09-20; границы приёмки ниже |
| 5 | Состояния данных, свежесть, частичные сбои, гонки polling | Завершён 2026-09-20 для дашборда; границы проверок ниже |
| 6 | Навигация и дашборд | Завершён 2026-09-20 в границах локальной приёмки; ограничения ниже |
| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | В работе: 7.1–7.2, права серверов и ограничения действий мониторинга выполнены; формы проб сохраняются после API; переключатели подтверждаются API; удаление и звезда подтверждаются API; Speed-test блокирует запись; Ping сериализован с записью; вкладка и Ping-фильтры в URL; далее Resources |
| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | В работе: 7.1–7.2, права серверов и ограничения действий мониторинга выполнены; формы проб сохраняются после API; переключатели подтверждаются API; удаление и звезда подтверждаются API; Speed-test блокирует запись; Ping сериализован с записью; вкладка и Ping-фильтры в URL; Resources-фильтры в URL; далее приёмка 7.5 |
| 8 | Трафик и статистика | Не начат |
| 9 | VPN, Firewall и Filters | Не начат |
| 10 | Маршрутизация, диагностика, карта и терминал | Не начат |
@@ -29,6 +29,14 @@
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат |
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
## Блок 7.4В2 — URL-фильтры Resources (2026-09-23)
- Resources использует rq (поиск: replaceState) и rtype (jump-host/exit-node/home-router: pushState; отсутствие означает все). Фильтры сохраняются при смене вкладки, reload и Back/Forward. Значения Ping q/status/servers, probe, tab и чужие параметры не изменяются при сбросе Resources.
- Неизвестный rtype и дубли rq/rtype дают локальное предупреждение Resources; таблица не расширяется до всех строк. Кнопка сброса удаляет только rq/rtype. Использованы прежние DataPageToolbar, Grid, Alert и Button, без изменения общей темы.
- Проверки: 4 codec unit-теста и 9 URL E2E прошли (3 новых Resources, с реальными строками HTTP-мока). Проверены фильтрация, reload, история, независимость Ping и отсутствие запросов записи. Lint новых файлов/тестов чистый; uptime — прежние 1 ошибка/6 предупреждений. Production build и TypeScript прошли: 34 статические страницы.
- Следующий шаг: 7.5 — профильная приёмка серверов и мониторинга, сквозной сценарий из дашборда, адаптивность форм/Resources/Speed, список оставшихся ограничений (экспорт серверов без обработчика, длительный Speed, смена backend/mode во время операции). Этап 7 целиком ещё не объявлять завершённым. Реальные JWT/RouterOS доступны только во внешнем тестовом окружении.
- Квота после проверок: 81% пятичасовой / 13% недельной израсходовано. Локальный коммит без push, сторонние изменения не включать.
## Блок 7.4В1 — URL вкладки и фильтров Ping (2026-09-23)
- Новый codec lib/data-filters/uptime-url-state.ts и hook useUptimeUrlState делают URL источником вкладки и Ping-фильтров. tab=probes/resources/speed (по умолчанию probes), q=поиск, status=up/warn/down (отсутствует для всех), servers=JSON массива строковых ID. probe и посторонние параметры сохраняются.
+1 -1
View File
@@ -61,7 +61,7 @@ URL — источник сохранённого состояния. Back/Forwa
3. **7.4 — формы, детали и полный контекст мониторинга.** Восстановление вкладки/фильтров из URL, понятные единицы и подписи, возврат к списку после действия. Не дублировать сохранённые исправления этапа 1; проверить загрузку, отмену, 400/403/500, timeout и повторное нажатие.
4. **7.5 — приёмка этапа.** Сценарий «проблема → сервер/проверка → действие → возврат» с сохранением контекста; профильные E2E, права в HTTP-моках, TypeScript, lint и build. Реальные JWT/RouterOS отдельно в тестовом окружении.
Следующий исполняемый шаг: 7.4В2 — URL поиска и типа серверов Resources. Вкладка и Ping-фильтры (q/status/servers) уже сохраняются в URL; переходы сохраняют probe и чужие параметры. Затем приёмка 7.5 с учётом границ HTTP-моков и сетевых операций из журнала.
Следующий исполняемый шаг: 7.5 — профильная приёмка серверов и мониторинга, сквозные переходы, адаптивность форм/Resources/Speed и фиксация ограничений. URL-контекст вкладки, Ping и Resources выполнен. Учесть границы HTTP-моков, экспорт серверов и ограничения сетевых операций из журнала; этап 7 целиком пока не завершён.
## Решение по синхронизации URL (7.1)
+6 -1
View File
@@ -2,11 +2,12 @@
import { useMemo } from "react"
import { useSearchParams } from "next/navigation"
import { readUptimeUrl, patchUptimeUrl, type UptimeTab, type ProbeStatusFilter, type UrlSetter } from "@/lib/data-filters/uptime-url-state"
import { readUptimeUrl, readResourceUrl, patchUptimeUrl, type ResourceTypeFilter, type UptimeTab, type ProbeStatusFilter, type UrlSetter } from "@/lib/data-filters/uptime-url-state"
export function useUptimeUrlState() {
const raw = useSearchParams().toString()
const state = useMemo(() => readUptimeUrl(new URLSearchParams(raw)), [raw])
const resource = useMemo(() => readResourceUrl(new URLSearchParams(raw)), [raw])
function write(changes: Record<string, string | null>, replace = false) {
const href = patchUptimeUrl(new URLSearchParams(window.location.search), changes) + window.location.hash
if (href === window.location.pathname + window.location.search + window.location.hash) return
@@ -24,6 +25,10 @@ export function useUptimeUrlState() {
write({ servers: next.length ? JSON.stringify(next) : null })
}
return {
resource,
setResourceSearch: (rq: string) => write({ rq }, true),
setResourceType: (type: ResourceTypeFilter) => write({ rtype: type === "all" ? null : type }),
resetResources: () => write({ rq: null, rtype: null }),
tab: state.tab, search: state.search, statusFilter: state.status, serverFilter: state.servers, error: state.error,
setTab: (tab: UptimeTab) => write({ tab: tab === "probes" ? null : tab }),
setSearch: (q: string) => write({ q }, true), setStatusFilter, setServerFilter,
@@ -15,3 +15,12 @@ test("сброс оставляет пробу, вкладку и чужие п
const result = patchUptimeUrl(new URLSearchParams('probe=p%2F2&keep=1&tab=speed&q=a&status=down&servers=%5B%221%22%5D'), { q: null, status: null, servers: null })
assert.equal(result, '/uptime?probe=p%2F2&keep=1&tab=speed')
})
test("фильтры Resources независимы от Ping и валидируются", async () => {
const { readResourceUrl } = await import("./uptime-url-state")
const params = new URLSearchParams('tab=resources&q=ping&status=down&rq=CHR&rtype=exit-node')
assert.deepEqual(readResourceUrl(params), { search: 'CHR', type: 'exit-node', error: null })
assert.equal(readUptimeUrl(params).search, 'ping')
for (const raw of ['rtype=unknown', 'rtype=exit-node&rtype=jump-host', 'rq=a&rq=b']) assert.ok(readResourceUrl(new URLSearchParams(raw)).error)
assert.equal(patchUptimeUrl(params, { rq: null, rtype: null }), '/uptime?tab=resources&q=ping&status=down')
})
+9
View File
@@ -35,3 +35,12 @@ export function patchUptimeUrl(params: URLSearchParams, changes: Record<string,
}
export type UrlSetter<T> = Dispatch<SetStateAction<T>>
export type ResourceTypeFilter = "all" | "jump-host" | "exit-node" | "home-router"
export function readResourceUrl(params: URLSearchParams) {
const raw = params.get("rtype")
const valid = raw === null || ["jump-host", "exit-node", "home-router"].includes(raw)
const error = !valid || ["rq", "rtype"].some(key => params.getAll(key).length > 1)
? "В ссылке указаны неверные или повторяющиеся фильтры ресурсов." : null
return { search: params.get("rq") ?? "", type: (valid && raw !== null ? raw : "all") as ResourceTypeFilter, error }
}
+44
View File
@@ -523,3 +523,47 @@ for (const invalid of ["tab=unknown", "status=bad", "servers=%7B%7D", "q=a&q=b"]
expect(control.writes).toEqual([])
})
}
async function resourceSetup(page: Page, extra = "") {
const control = await setup(page, '/uptime?tab=resources&probe=p1&keep=1&q=ping' + extra, 0, false, "mm:uptime:read")
await page.route("**/api/uptime/overview?*", route => route.fulfill({ json: { probes: [], resources: [{ serverId: "1", hasData: true, cpu: 10, cpuHistory: [10], ramUsed: 100, ramTotal: 1000, hddUsed: 10, hddTotal: 100, uptimeSeconds: 60, boardName: "CHR" }] } }))
await page.reload()
return control
}
test("Resources URL: фильтры, смена вкладки, история и сброс сохраняют Ping-контекст", async ({ page }) => {
const control = await resourceSetup(page)
const rows = page.locator("tr[data-row-id]")
await expect(rows).toHaveCount(1)
const search = page.getByPlaceholder("Поиск по имени, площадке…")
await search.fill("Источник")
await page.getByRole("button", { name: "JumpHost", exact: true }).click()
await expect(rows).toHaveCount(0)
await expect(page).toHaveURL(url => url.searchParams.get("rtype") === "jump-host" && url.searchParams.get("rq") === "Источник")
await page.goBack()
await expect(rows).toHaveCount(1)
await page.goForward()
await expect(rows).toHaveCount(0)
await page.reload()
await expect(search).toHaveValue("Источник")
await expect(rows).toHaveCount(0)
await page.getByRole("button", { name: /Пинг-пробы/ }).click()
await expect(page.getByPlaceholder("Имя, IP-адрес, фильтр…")).toHaveValue("ping")
await page.getByRole("button", { name: /Ресурсы/ }).click()
await expect(search).toHaveValue("Источник")
await expect(rows).toHaveCount(0)
await page.getByRole("button", { name: "Сбросить фильтры ресурсов", exact: true }).click()
await expect(rows).toHaveCount(1)
await expect(page).toHaveURL(url => !url.searchParams.has("rq") && !url.searchParams.has("rtype") && url.searchParams.get("q") === "ping" && url.searchParams.get("probe") === "p1" && url.searchParams.get("keep") === "1")
expect(control.writes).toEqual([])
})
for (const extra of ["&rtype=unknown", "&rq=a&rq=b"]) {
test(`Resources URL: неверные фильтры ${extra}`, async ({ page }) => {
await resourceSetup(page, extra)
await expect(page.getByRole("alert").filter({ hasText: "фильтры ресурсов" })).toBeVisible()
await expect(page.locator("tr[data-row-id]")).toHaveCount(0)
await page.getByRole("button", { name: "Сбросить фильтры ресурсов", exact: true }).click()
await expect(page.locator("tr[data-row-id]")).toHaveCount(1)
})
}