chore(ui): завершить миграцию Filters и удалить старый API
This commit is contained in:
1 parent
4fd844f977
commit
0d418e2e39
6 files changed
+37
-2025
No files matched your search
@@ -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
@@ -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).
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -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)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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 })
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
Reference in new issue
Block a user