fix(ui): сохранять фильтры ресурсов в URL
This commit is contained in:
1 parent
fd7a08d5dc
commit
0734679fd6
7 files changed
+85
-6
No files matched your search
@@ -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
|
||||
|
||||
@@ -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 и посторонние параметры сохраняются.
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user