chore(ui): завершить миграцию Filters и удалить старый API

This commit is contained in:
Denozordec committed 2026-09-20 00:47:33 +07:00
1 parent 4fd844f977
commit 0d418e2e39
6 files changed
+37 -2025

No files matched your search

+2 -27
View File
@@ -1,19 +1,13 @@
"use client" "use client"
import { ReactNode } from "react" import { ReactNode } from "react"
import { ListFilterIcon, SearchIcon } from "lucide-react" import { SearchIcon } from "lucide-react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { import {
InputGroup, InputGroup,
InputGroupAddon, InputGroupAddon,
InputGroupInput, InputGroupInput,
} from "@/components/ui/input-group" } from "@/components/ui/input-group"
import { Button } from "@/components/ui/button"
import {
Filters,
type Filter,
type FilterFieldConfig,
} from "@/components/reui/filters"
import { SegmentedControl } from "@/components/form-kit" import { SegmentedControl } from "@/components/form-kit"
import { DATA_PAGE_TOOLBAR_CLASS } from "@/components/data-grids/shared/data-grid-layout" import { DATA_PAGE_TOOLBAR_CLASS } from "@/components/data-grids/shared/data-grid-layout"
@@ -41,9 +35,6 @@ interface DataPageToolbarProps<T extends string = string> {
onChange: (value: T) => void onChange: (value: T) => void
options: { value: T; label: string; count?: number }[] options: { value: T; label: string; count?: number }[]
} }
filters?: Filter[]
onFiltersChange?: (filters: Filter[]) => void
filterFields?: FilterFieldConfig[]
filterControl?: ReactNode filterControl?: ReactNode
countLabel?: string countLabel?: string
actions?: ReactNode actions?: ReactNode
@@ -56,9 +47,6 @@ function DataPageToolbar<T extends string = string>({
onSearchChange, onSearchChange,
searchPlaceholder = "Поиск…", searchPlaceholder = "Поиск…",
segmented, segmented,
filters,
onFiltersChange,
filterFields,
filterControl, filterControl,
countLabel, countLabel,
actions, actions,
@@ -74,20 +62,7 @@ function DataPageToolbar<T extends string = string>({
options={segmented.options} options={segmented.options}
/> />
)} )}
{filterControl ?? (filterFields && filters && onFiltersChange && ( {filterControl}
<Filters
filters={filters}
fields={filterFields}
onChange={onFiltersChange}
size="sm"
trigger={
<Button type="button" variant="outline" size="sm">
<ListFilterIcon className="size-3.5" />
Фильтры
</Button>
}
/>
))}
{onSearchChange != null && ( {onSearchChange != null && (
<InputGroup className="min-w-[220px] max-w-sm"> <InputGroup className="min-w-[220px] max-w-sm">
<InputGroupAddon> <InputGroupAddon>
File diff suppressed because it is too large. Load diff
+23 -2
View File
@@ -18,7 +18,7 @@
| 1 | Достоверность сохранения, удаления, включения и опроса серверов | Завершён 2026-09-13 | | 1 | Достоверность сохранения, удаления, включения и опроса серверов | Завершён 2026-09-13 |
| 2 | Общая оболочка Data Grid/TanStack v9, Filters и эталон серверов | Завершён 2026-09-13, с перечисленными ограничениями приёмки | | 2 | Общая оболочка Data Grid/TanStack v9, Filters и эталон серверов | Завершён 2026-09-13, с перечисленными ограничениями приёмки |
| 3 | Основные таблицы и справочники | Завершён 2026-09-13, границы проверок ниже | | 3 | Основные таблицы и справочники | Завершён 2026-09-13, границы проверок ниже |
| 4 | Остальные таблицы, удаление старых API, общая регрессия | В работе: BGP/OSPF, VPN, пользователи, сертификаты и бэкапы перенесены | | 4 | Остальные таблицы, удаление старых API, общая регрессия | Завершён 2026-09-20; границы приёмки ниже |
| 5 | Состояния данных, свежесть, частичные сбои, гонки polling | Не начат | | 5 | Состояния данных, свежесть, частичные сбои, гонки polling | Не начат |
| 6 | Навигация и дашборд | Не начат | | 6 | Навигация и дашборд | Не начат |
| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Не начат | | 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Не начат |
@@ -346,9 +346,30 @@ TypeScript до и после первого браузерного прогон
На последнем чтении использовано 62% пятичасовой и 99% недельной квоты. Старые Filters/fallback/helper ещё физически остаются, поэтому этап 4 НЕ завершён. Следующий шаг — удалить неиспользуемый fallback DataPageToolbar и старые Filters/evaluator после проверки импортов, обновить тест совместимости, затем выполнить полную регрессию и production build. Сохранить этот блок локальным коммитом без push. На последнем чтении использовано 62% пятичасовой и 99% недельной квоты. Старые Filters/fallback/helper ещё физически остаются, поэтому этап 4 НЕ завершён. Следующий шаг — удалить неиспользуемый fallback DataPageToolbar и старые Filters/evaluator после проверки импортов, обновить тест совместимости, затем выполнить полную регрессию и production build. Сохранить этот блок локальным коммитом без push.
## Этап 4 — завершение: удаление старого Filters (2026-09-20)
Удалены `components/reui/filters.tsx`, `apply-reui-filters.ts` и временный `migrate-filter-query.ts`. У DataPageToolbar удалены устаревшие props filters/onFiltersChange/filterFields и их fallback; все четыре действующих потребителя используют filterControl и FilterQuery. Props filters/onFiltersChange у BackupsHistory остаются внутренним контрактом нового FilterQuery, а не старого Filters. Unit-тест сравнения двух реализаций заменён независимыми ожидаемыми результатами для шести операторов с AND.
| Финальная проверка миграции | Результат |
| --- | --- |
| Поиск старых FilterFieldConfig/applyReuiFilters/migrateFilterQuery, legacy-data-grid и react-table-v8 в app/components/lib/tests/package.json/package-lock.json | Совпадений нет |
| Unit-тесты дерева FilterQuery | 7 passed |
| Unit-тесты адаптера статистики | 5 passed |
| TypeScript без incremental | Успешно |
| ESLint изменённых файлов | Успешно |
| Полная браузерная регрессия | 51 passed, 2.4 минуты |
| Production build | Успешно, 34 статические страницы |
| git diff --check изменённых файлов | Успешно |
Получен актуальный ReUI MCP `get_audit_checklist`: в этом блоке сохранены установленная композиция, публичный API и тема, новых данных/интерактивных элементов/секретов не добавлено. Миграция Data Grid/TanStack v9 и Filters завершена; старые временные API больше не нужны. Это завершение технической миграции, а не полной UX/UI-приёмки: остаются ранее записанные ограничения адаптивности/доступности, исходные lint-долги страниц, имитация Community apply и оценки в оптимизаторе. Реальные RouterOS, права на сетевые операции и все состояния backend по мокам не подтверждены. Полный repository lint не заявляется чистым; профильные проверки изменённых участков и production build прошли.
Квота: 2% пятичасовой/0% недельной использовано на старте; после регрессии — 32%/5%. Этап 4 сохранить локальным коммитом без push. Изменения skills/AGENTS/.codex другой работы не включать. Общую регрессию этапа 4 считать выполненной; автоматический merge, push или релиз не выполнялись.
**Следующий этап 5:** `hooks/use-dashboard-live.ts` и `app/(main)/dashboard/page.tsx`. Подтверждено чтением: fetchSnapshot уже использует Promise.allSettled, но при ошибках очищает probes/servers/BGP/traffic/overlay, а fetchRecentEvents очищает события; обновления запускаются из нескольких effects и таймеров без защиты результатов запросов. Ввести состояния/свежесть по источникам и сохранение последних успешных данных; отличать начальную ошибку от частичного сбоя и фонового обновления. Защитить смену backend/mode и перекрывающиеся запросы от поздних результатов, включая дополнительные запросы Internet Path. Проверить HTTP-моками первоначальную ошибку, частичный сбой, повторное восстановление и гонки; профильный UX показывать без подмены неизвестных данных нулями. Зафиксировать отдельным коммитом этапа 5.
## Карта миграции (исходная инвентаризация этапа 0) ## Карта миграции (исходная инвентаризация этапа 0)
Текущая связка: @tanstack/react-table ^8.21.3, @tanstack/react-virtual ^3.14.4, base-nova. Актуальный ReUI MCP ранее подтвердил useTable/dataGridFeatures для Data Grid и дерево FilterQuery вместо filters/onChange для Filters. Перед установкой перепроверить совместимые версии, сохранить семантику существующих фильтров (простые условия → AND) и закрепить lockfile. Исходная связка на этапе 0: @tanstack/react-table ^8.21.3, @tanstack/react-virtual ^3.14.4, base-nova. По итогам этапа 4 используется @tanstack/react-table 9.2.4 с useTable/dataGridFeatures и дерево FilterQuery вместо старого filters/onChange. Список ниже сохранён как историческая инвентаризация.
Источники: [контракт UI](ui-design-contract.md), [таблицы PRO](https://reui.io/preview/base/data-grid-filtering-2?ref=mcp), [API Data Grid](https://reui.io/docs/components/base/data-grid?ref=mcp), [API Filters](https://reui.io/docs/components/base/filters?ref=mcp). Источники: [контракт UI](ui-design-contract.md), [таблицы PRO](https://reui.io/preview/base/data-grid-filtering-2?ref=mcp), [API Data Grid](https://reui.io/docs/components/base/data-grid?ref=mcp), [API Filters](https://reui.io/docs/components/base/filters?ref=mcp).
+12 -7
View File
@@ -1,8 +1,6 @@
import assert from "node:assert/strict" import assert from "node:assert/strict"
import { test } from "node:test" import { test } from "node:test"
import { applyFilterQuery } from "./apply-filter-query" import { applyFilterQuery } from "./apply-filter-query"
import { migrateFilterQuery } from "./migrate-filter-query"
import { applyReuiFilters } from "./apply-reui-filters"
import { createFilterQuery, createFilterRule } from "../../components/reui/filters/filters-query" import { createFilterQuery, createFilterRule } from "../../components/reui/filters/filters-query"
const rows = [{ country: "RU", status: "online" }, { country: "DE", status: "offline" }, { country: "NL", status: "offline" }] const rows = [{ country: "RU", status: "online" }, { country: "DE", status: "offline" }, { country: "NL", status: "offline" }]
@@ -36,10 +34,17 @@ test("text prefix and suffix use case-insensitive matching and support negation"
assert.deepEqual(applyFilterQuery(rows, createFilterQuery([{ ...rule("country", "starts_with", "r"), negated: true }]), accessors), rows.slice(1)) assert.deepEqual(applyFilterQuery(rows, createFilterQuery([{ ...rule("country", "starts_with", "r"), negated: true }]), accessors), rows.slice(1))
}) })
test("migration preserves the existing operators and implicit AND", () => { test("scalar operators keep their expected results when combined with AND", () => {
for (const operator of ["is", "isNot", "isAnyOf", "isNotAnyOf", "contains", "doesNotContain"]) { const cases: [string, unknown, typeof rows][] = [
const filters = [{ id: "country", field: "country", operator, values: ["RU", "DE"] }, ["is", "RU", []],
{ id: "status", field: "status", operator: "isNot", values: ["online"] }] ["is_not", "RU", [rows[1], rows[2]]],
assert.deepEqual(applyFilterQuery(rows, migrateFilterQuery(filters), accessors), applyReuiFilters(rows, filters, accessors)) ["is_any_of", ["RU", "DE"], [rows[1]]],
["is_none_of", ["RU", "DE"], [rows[2]]],
["contains", "e", [rows[1]]],
["not_contains", "e", [rows[2]]],
]
for (const [operator, value, expected] of cases) {
const query = createFilterQuery([rule("country", operator, value), rule("status", "is_not", "online")])
assert.deepEqual(applyFilterQuery(rows, query, accessors), expected)
} }
}) })
-40
View File
@@ -1,40 +0,0 @@
import type { Filter } from "@/components/reui/filters"
type FieldAccessor<T> = Record<string, (row: T) => unknown>
function matchFilter<T>(row: T, filter: Filter, accessors: FieldAccessor<T>): boolean {
const getValue = accessors[filter.field]
if (!getValue) return true
const value = getValue(row)
const vals = filter.values
const str = (v: unknown) => String(v ?? "").toLowerCase()
switch (filter.operator) {
case "is":
return vals.length > 0 && String(value) === String(vals[0])
case "isNot":
return vals.length === 0 || String(value) !== String(vals[0])
case "isAnyOf":
return vals.some((v) => String(value) === String(v))
case "isNotAnyOf":
return !vals.some((v) => String(value) === String(v))
case "contains":
return vals.some((v) => str(value).includes(str(v)))
case "doesNotContain":
return !vals.some((v) => str(value).includes(str(v)))
default:
return true
}
}
export function applyReuiFilters<T>(
rows: T[],
filters: Filter[],
accessors: FieldAccessor<T>,
): T[] {
if (filters.length === 0) return rows
return rows.filter((row) =>
filters.every((f) => matchFilter(row, f, accessors)),
)
}
-15
View File
@@ -1,15 +0,0 @@
import { createFilterQuery, createFilterRule } from "@/components/reui/filters/filters-query"
const OPERATORS: Record<string, string> = {
is: "is", isNot: "is_not", isAnyOf: "is_any_of", isNotAnyOf: "is_none_of",
contains: "contains", doesNotContain: "not_contains",
}
/** Temporary input contract, independent of the old UI component. */
export function migrateFilterQuery(filters: { id: string; field: string; operator: string; values: unknown[] }[]) {
return createFilterQuery(filters.map(filter => {
const operator = OPERATORS[filter.operator]
if (!operator) throw new Error(`Неподдерживаемое условие: ${filter.operator}`)
return createFilterRule({ id: filter.id, path: [filter.field], operator, value: filter.values })
}))
}