diff --git a/app/(main)/statistics/page.tsx b/app/(main)/statistics/page.tsx index 36ba0e15..bcbf394e 100644 --- a/app/(main)/statistics/page.tsx +++ b/app/(main)/statistics/page.tsx @@ -26,8 +26,7 @@ import { type StatisticsSliceKind, } from "@/components/data-grids/statistics-breakdown-data-grid" import { Alert, AlertDescription, AlertTitle } from "@/components/reui/alert" -import type { Filter } from "@/components/reui/filters" -import { STATISTICS_FILTER_FIELDS } from "@/lib/data-filters/statistics-filter-fields" +import { StatisticsFilters } from "@/components/statistics/statistics-filters" import { isStatisticsPivotDim, isStatisticsSliceKind, @@ -129,29 +128,6 @@ function readSlices(sp: URLSearchParams): CubeSlices { return next } -function slicesToFilters(slices: CubeSlices): Filter[] { - return SLICE_KEYS.flatMap((key) => { - const val = slices[key] - if (!val) return [] - return [{ id: key, field: key, operator: "is", values: [val] }] - }) -} - -function filtersToSlices(filters: Filter[]): CubeSlices { - const next: CubeSlices = {} - for (const f of filters) { - const raw = String(f.values[0] ?? "").trim() - if (!raw) continue - if (f.field === "country") next.country = raw.toUpperCase().slice(0, 2) - else if (f.field === "service") next.service = raw - else if (f.field === "asn") next.asn = raw.replace(/[^\d]/g, "") - else if (f.field === "serverId") next.serverId = raw - else if (f.field === "userId") next.userId = raw - else if (f.field === "iface") next.iface = raw - } - return next -} - function toQuery(range: DateRangeYmd, slices: CubeSlices, planes: "unique" | "all"): StatisticsQuery { const serverId = slices.serverId ? Number(slices.serverId) : undefined const asn = slices.asn != null && slices.asn !== "" ? Number(slices.asn) : undefined @@ -261,7 +237,6 @@ function StatisticsPageInner() { const range = useMemo(() => readRange(searchParams), [searchParams]) const slices = useMemo(() => readSlices(searchParams), [searchParams]) - const filters = useMemo(() => slicesToFilters(slices), [slices]) const dim = useMemo(() => readDim(searchParams), [searchParams]) const view = useMemo(() => readView(searchParams), [searchParams]) const planes = useMemo(() => readPlanes(searchParams), [searchParams]) @@ -506,9 +481,7 @@ function StatisticsPageInner() { ) : null} } - filters={filters} - onFiltersChange={(next) => setSlices(filtersToSlices(next))} - filterFields={STATISTICS_FILTER_FIELDS} + filterControl={} countLabel={countLabel} /> void +}) { + const [query, setQuery] = useState(() => slicesToFilterQuery(slices)) + const result = filterQueryToSlices(query) + return
+
+ + +
+ {result.status === "invalid" &&

{result.error}

} + {result.status === "draft" &&

Завершите условие. Текущий срез сохранён.

} +
+} diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index f27eabe9..08def05d 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -338,6 +338,14 @@ BGP, backups и BackupsHistory используют FilterQuery и соврем Квота после подготовки: 49% пятичасовой и 97% недельной использовано. Сохранить подготовку локальным коммитом без push. Чужие изменения skill/AGENTS/.codex не включать. +## Этап 4 — блок 13: подключение Filters статистики (2026-09-14) + +StatisticsFilters подключён вместо старого массива Filter. Поля предлагают только is, адаптер дополнительно отклоняет неподдерживаемые выражения. Локальный черновик не изменяет URL до явного «Применить срез»; invalid/draft блокируют применение и сохраняют действующие срезы. Ключ редактора зависит от URL-срезов, поэтому внешний переход сбрасывает черновик. Старые функции slicesToFilters/filtersToSlices страницы удалены. + +TypeScript до и после первого браузерного прогона и scoped ESLint прошли. Расширенный браузерный сценарий статистики прошёл: сортировка/переход в срез, восстановление фильтра из URL, редактирование без изменения URL, применение, сброс, отклонение RUS без обрезания до RU и внешняя смена страны на DE. В первом расширенном прогоне исправлен неоднозначный локатор alert; повтор — 1 passed. Проверки через HTTP-моки. + +На последнем чтении использовано 62% пятичасовой и 99% недельной квоты. Старые Filters/fallback/helper ещё физически остаются, поэтому этап 4 НЕ завершён. Следующий шаг — удалить неиспользуемый fallback DataPageToolbar и старые Filters/evaluator после проверки импортов, обновить тест совместимости, затем выполнить полную регрессию и production build. Сохранить этот блок локальным коммитом без push. + ## Карта миграции (исходная инвентаризация этапа 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. diff --git a/lib/data-filters/statistics-filter-fields.ts b/lib/data-filters/statistics-filter-fields.ts index 2506d20e..14751d85 100644 --- a/lib/data-filters/statistics-filter-fields.ts +++ b/lib/data-filters/statistics-filter-fields.ts @@ -1,46 +1,52 @@ -import type { FilterFieldConfig } from "@/components/reui/filters" +import type { FilterField } from "@/components/reui/filters/filters-types" -export const STATISTICS_FILTER_FIELDS: FilterFieldConfig[] = [ +export const STATISTICS_FILTER_FIELDS: FilterField[] = [ { - key: "country", + id: "country", label: "Страна", type: "text", placeholder: "ISO, напр. US", defaultOperator: "is", + operators: [{ value: "is", label: "равно" }], }, { - key: "service", + id: "service", label: "Сервис", type: "text", placeholder: "https, dns…", defaultOperator: "is", + operators: [{ value: "is", label: "равно" }], }, { - key: "asn", + id: "asn", label: "ASN", type: "text", placeholder: "номер ASN", defaultOperator: "is", + operators: [{ value: "is", label: "равно" }], }, { - key: "serverId", + id: "serverId", label: "Сервер", type: "text", placeholder: "id сервера", defaultOperator: "is", + operators: [{ value: "is", label: "равно" }], }, { - key: "userId", + id: "userId", label: "Пользователь", type: "text", placeholder: "id пользователя", defaultOperator: "is", + operators: [{ value: "is", label: "равно" }], }, { - key: "iface", + id: "iface", label: "Интерфейс", type: "text", placeholder: "ether1", defaultOperator: "is", + operators: [{ value: "is", label: "равно" }], }, ] diff --git a/tests/e2e/migrated-tables.spec.ts b/tests/e2e/migrated-tables.spec.ts index 96219c12..a64e6e1e 100644 --- a/tests/e2e/migrated-tables.spec.ts +++ b/tests/e2e/migrated-tables.spec.ts @@ -107,4 +107,25 @@ test("Статистика: CompactDataGrid сохраняет 12 строк, с await expect(rows.first()).toContainText("Пользователь 12") await rows.first().click() await expect(page).toHaveURL(/userId=user-12/) + await expect(page.getByRole("button", { name: "user-12", exact: true })).toBeVisible() + await page.getByRole("button", { name: "user-12", exact: true }).click() + await page.getByPlaceholder("id пользователя").fill("user-2") + await page.getByPlaceholder("id пользователя").press("Enter") + await expect(page).toHaveURL(/userId=user-12/) + await page.getByRole("button", { name: "Применить срез", exact: true }).click() + await expect(page).toHaveURL(/userId=user-2/) + await page.getByRole("button", { name: "Очистить", exact: true }).click() + await expect(page).toHaveURL(/userId=user-2/) + await page.getByRole("button", { name: "Применить срез", exact: true }).click() + await expect(page).not.toHaveURL(/userId=/) + await page.goto("/statistics?from=2026-09-01&to=2026-09-13&country=RU") + await page.getByRole("button", { name: "RU", exact: true }).click() + await page.getByPlaceholder("ISO, напр. US").fill("RUS") + await page.getByPlaceholder("ISO, напр. US").press("Enter") + await expect(page.getByRole("alert").filter({ hasText: "Укажите двухбуквенный код страны." })).toBeVisible() + await expect(page.getByRole("button", { name: "Применить срез", exact: true })).toBeDisabled() + await expect(page).toHaveURL(/country=RU(?:&|$)/) + await page.evaluate(() => window.history.pushState(null, "", "/statistics?country=DE")) + await expect(page.getByRole("button", { name: "DE", exact: true })).toBeVisible() + await expect(page.getByText("Укажите двухбуквенный код страны.", { exact: true })).toHaveCount(0) })