-
{cfg.icon}
+
{cfg.icon}
{cert.name}
@@ -196,6 +198,7 @@ function CertificatesDataGrid({ certificates, serverMap, isLoading, emptyTitle =
},
{
id: "status",
+ size: 120,
accessorKey: "status",
header: ({ column }) =>
,
cell: ({ row }) => {
@@ -226,7 +229,7 @@ function CertificatesDataGrid({ certificates, serverMap, isLoading, emptyTitle =
manualPagination: false,
autoResetPageIndex: false,
autoResetExpanded: false,
- initialState: { pagination: { pageIndex: 0, pageSize: 50 } },
+ initialState: { pagination: { pageIndex: 0, pageSize: 50 }, columnPinning: { start: ["name"], end: [] } },
data: certificates,
columns,
getRowId: (row) => row.id,
@@ -254,6 +257,7 @@ function CertificatesDataGrid({ certificates, serverMap, isLoading, emptyTitle =
50 || pageSize !== 50}
isLoading={isLoading}
loadingMode="skeleton"
diff --git a/components/data-grids/compact-data-grid.tsx b/components/data-grids/compact-data-grid.tsx
index 27241f0d..e3ebc7cd 100644
--- a/components/data-grids/compact-data-grid.tsx
+++ b/components/data-grids/compact-data-grid.tsx
@@ -2,7 +2,7 @@
import { DataGridContainer, DataGridScrollArea, DataGridTable, dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid"
-import { useMemo, useEffect, type ReactNode } from "react"
+import { useMemo, useEffect, type ReactNode, type CSSProperties } from "react"
import {
type ColumnDef,
useTable,
@@ -12,6 +12,7 @@ import {
DATA_GRID_CELL_PAD,
DATA_GRID_CELL_PAD_FIRST,
DATA_GRID_CELL_PAD_LAST,
+ DEFAULT_DATA_GRID_LAYOUT,
} from "@/components/data-grids/shared/data-grid-layout"
import { DataGridSortHeader } from "@/components/data-grids/shared/data-grid-sort-header"
import { EmptyState } from "@/components/empty-state"
@@ -23,6 +24,7 @@ export interface CompactDataGridColumn {
accessorKey?: keyof T & string
cell?: (row: T) => ReactNode
enableSorting?: boolean
+ size?: number
headerClassName?: string
cellClassName?: string
}
@@ -32,6 +34,7 @@ interface CompactDataGridProps {
columns: CompactDataGridColumn[]
isLoading?: boolean
horizontalScroll?: boolean
+ pinnedColumnId?: string
emptyTitle?: string
emptyDescription?: string
onRowClick?: (row: T) => void
@@ -43,6 +46,7 @@ function CompactDataGrid({
columns,
isLoading,
horizontalScroll = false,
+ pinnedColumnId,
emptyTitle = "Нет записей",
emptyDescription,
onRowClick,
@@ -52,6 +56,7 @@ function CompactDataGrid({
() =>
columns.map((col, index) => ({
id: col.id,
+ size: col.size,
accessorKey: col.accessorKey ?? col.id,
header: ({ column }) => (
@@ -86,7 +91,7 @@ function CompactDataGrid({
features: dataGridFeatures,
manualPagination: false,
autoResetPageIndex: false,
- initialState: { pagination: { pageIndex: 0, pageSize: 50 } },
+ initialState: { pagination: { pageIndex: 0, pageSize: 50 }, columnPinning: { start: pinnedColumnId ? [pinnedColumnId] : [], end: [] } },
data,
columns: columnDefs,
getRowId: (row) => row.id,
@@ -103,6 +108,7 @@ function CompactDataGrid({
50 || table.state.pagination.pageSize !== 50}
isLoading={isLoading}
loadingMode="skeleton"
@@ -123,7 +129,11 @@ function CompactDataGrid({
>
{horizontalScroll ? (
-
+
diff --git a/components/statistics/statistics-pivot-grid.tsx b/components/statistics/statistics-pivot-grid.tsx
index 3049ed6d..5b4c079b 100644
--- a/components/statistics/statistics-pivot-grid.tsx
+++ b/components/statistics/statistics-pivot-grid.tsx
@@ -37,12 +37,14 @@ export function StatisticsPivotGrid({
const columns: CompactDataGridColumn[] = [
{
id: "label",
+ size: 140,
header: "Измерение",
accessorKey: "label",
cell: (row) => {row.label},
},
...data.columns.map((col) => ({
id: `c:${col.id}`,
+ size: 110,
header: col.label,
accessorKey: `c:${col.id}` as const,
cell: (row: PivotGridRow) => {
@@ -71,6 +73,7 @@ export function StatisticsPivotGrid({
})),
{
id: "total",
+ size: 110,
header: "Итого",
accessorKey: "total",
cell: (row) => (
@@ -85,6 +88,7 @@ export function StatisticsPivotGrid({
{
+for (const [name, width] of [["desktop", 1440], ["mobile", 390], ["narrow", 320]] as const) test("Сертификаты: клавиатура и адаптивность " + name, async ({ page }, info) => {
await page.setViewportSize({ width, height: 900 }); await certificateRoutes(page)
const long = { ...certificate, name: "certificate-with-a-very-long-name-for-narrow-screens", commonName: "very-long-subdomain-for-responsive-layout.example.test", sans: ["very-long-alternative-domain-for-responsive-layout.example.test"] }
await page.route("**/api/certificates/state", r => r.fulfill({ json: certificateState([long]) }))
@@ -10,15 +10,15 @@ for (const [name, width] of [["desktop", 1440], ["mobile", 390], ["narrow", 320]
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth + 1)).toBe(true)
const detail = page.getByRole("button", { name: "Детали сертификата " + long.name, exact: true }); await detail.focus(); await page.keyboard.press("Enter")
await expect(detail).toHaveAttribute("aria-expanded", "true"); await expect(page.getByText("Доверие", { exact: true })).toBeVisible()
- await page.screenshot({ path: "docs/ux-ui/screens/certificates-details-" + name + ".png", fullPage: true })
+ await page.screenshot({ path: info.outputPath("certificates-details-" + name + ".png"), fullPage: true })
await page.evaluate(() => { for (const element of document.querySelectorAll("*")) if (element.scrollTop > 0) element.scrollTop = 0 })
- await page.screenshot({ path: "docs/ux-ui/screens/certificates-overview-" + name + ".png", fullPage: true })
+ await page.screenshot({ path: info.outputPath("certificates-overview-" + name + ".png"), fullPage: true })
await page.keyboard.press("Enter"); await expect(detail).toHaveAttribute("aria-expanded", "false")
const search = page.getByRole("textbox", { name: "Поиск по имени, CN, эмитенту…" }); await search.fill("нет-совпадений"); await expect(page.getByText("Нет совпадений", { exact: true })).toBeVisible(); await search.fill("")
const dialog = await openIssue(page)
await expect.poll(async () => { const box = await dialog.boundingBox(); return !!box && box.x >= 0 && box.x + box.width <= width + 1 && (width >= 640 || box.width >= width - 2) }).toBe(true)
expect(await dialog.evaluate(element => element.scrollWidth <= element.clientWidth + 1)).toBe(true)
- await page.screenshot({ path: "docs/ux-ui/screens/certificates-issue-" + name + ".png", fullPage: true })
+ await page.screenshot({ path: info.outputPath("certificates-issue-" + name + ".png"), fullPage: true })
await page.keyboard.press("Escape"); await expect(dialog).toHaveCount(0)
await expect(page.getByRole("button", { name: "Выпустить сертификат", exact: true })).toBeFocused()
})
diff --git a/tests/e2e/large-certificates.spec.ts b/tests/e2e/large-certificates.spec.ts
index bc1d4152..1c983aa9 100644
--- a/tests/e2e/large-certificates.spec.ts
+++ b/tests/e2e/large-certificates.spec.ts
@@ -44,12 +44,17 @@ test("Certificates1000: DOM, страницы, детали, поиск, refresh
await expect(scroll).toHaveCount(1)
expect(await scroll.evaluate(el => el.scrollWidth > el.clientWidth)).toBe(true)
await scroll.evaluate(el => { el.scrollLeft = el.scrollWidth })
+ await expect.poll(() => scroll.evaluate(el => el.scrollLeft)).toBeGreaterThan(0)
+ const pinned = rows.first().getByRole("cell").first()
+ expect(await pinned.evaluate(el => { const cell = el.getBoundingClientRect(), viewport = el.closest('[data-slot="scroll-area-viewport"]')!.getBoundingClientRect(); return cell.left >= viewport.left - 1 && cell.right <= viewport.right + 1 })).toBe(true)
+ const statusValue = rows.first().getByText("Действителен", { exact: true })
+ expect(await statusValue.evaluate(el => { const value = el.getBoundingClientRect(), viewport = el.closest('[data-slot="scroll-area-viewport"]')!.getBoundingClientRect(), pinned = el.closest("tr")!.querySelector("td")!.getBoundingClientRect(); return value.left >= pinned.right - 1 && value.right <= viewport.right + 1 })).toBe(true)
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true)
await page.addScriptTag({ content: axe.source })
const audit = await page.evaluate(async () => (window as unknown as { axe: typeof axe }).axe.run(document.querySelector('[data-slot="data-grid"]')!, { runOnly: { type: "tag", values: ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"] } }))
expect(audit.violations).toEqual([])
await page.addStyleTag({ content: "nextjs-portal { display: none !important; }" }); mkdirSync("docs/ux-ui/screens", { recursive: true })
- await page.getByRole("table").scrollIntoViewIfNeeded(); await page.screenshot({ path: "docs/ux-ui/screens/large-certificates-320.png", animations: "disabled" })
+ await page.getByRole("table").getByRole("button", { name: "Сертификат", exact: true }).scrollIntoViewIfNeeded(); await page.screenshot({ path: "docs/ux-ui/screens/large-certificates-320.png", animations: "disabled" })
await page.getByRole("button", { name: "Следующая страница", exact: true }).click()
certificates = [certificates[999]]; const previous = calls; await page.getByRole("button", { name: "Обновить", exact: true }).click()
await expect.poll(() => calls).toBeGreaterThan(previous); await expect(rows).toHaveCount(1)
diff --git a/tests/e2e/large-speed-history.spec.ts b/tests/e2e/large-speed-history.spec.ts
index 98fe2981..f53c6c14 100644
--- a/tests/e2e/large-speed-history.spec.ts
+++ b/tests/e2e/large-speed-history.spec.ts
@@ -42,6 +42,7 @@ test("Speed history200: DOM, сортировка, refresh, уменьшение
const audit = await page.evaluate(async () => (window as unknown as { axe: typeof axe }).axe.run(document.querySelector('[data-slot="data-grid"]')!, { runOnly: { type: "tag", values: ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"] } }))
expect(audit.violations).toEqual([])
await page.addStyleTag({ content: "nextjs-portal { display: none !important; }" }); mkdirSync("docs/ux-ui/screens", { recursive: true })
+ await page.getByText("История тестов", { exact: true }).scrollIntoViewIfNeeded()
await page.screenshot({ path: "docs/ux-ui/screens/large-speed-history-320.png", animations: "disabled" })
runs = [runs[199]]; const previous = calls; await page.getByRole("button", { name: "Обновить", exact: true }).click()
await expect.poll(() => calls).toBeGreaterThan(previous)
diff --git a/tests/e2e/large-statistics-pivot.spec.ts b/tests/e2e/large-statistics-pivot.spec.ts
index c3ef29c8..00baa667 100644
--- a/tests/e2e/large-statistics-pivot.spec.ts
+++ b/tests/e2e/large-statistics-pivot.spec.ts
@@ -36,6 +36,9 @@ for (const width of [1440, 320]) test("Pivot51×16: границы, сортир
expect(await scroll.evaluate(el => el.scrollWidth > el.clientWidth)).toBe(true)
await scroll.evaluate(el => { el.scrollLeft = el.scrollWidth })
await expect.poll(() => scroll.evaluate(el => el.scrollLeft)).toBeGreaterThan(0)
+ const pinned = rendered.first().getByRole("cell").first()
+ expect(await pinned.evaluate(el => { const cell = el.getBoundingClientRect(), viewport = el.closest('[data-slot="scroll-area-viewport"]')!.getBoundingClientRect(); return cell.left >= viewport.left - 1 && cell.right <= viewport.right + 1 })).toBe(true)
+ expect(await rendered.first().getByRole("cell").last().evaluate(el => { const total = el.getBoundingClientRect(), viewport = el.closest('[data-slot="scroll-area-viewport"]')!.getBoundingClientRect(), pinned = el.closest("tr")!.querySelector("td")!.getBoundingClientRect(); return total.left >= pinned.right - 1 && total.right <= viewport.right + 1 })).toBe(true)
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true)
await expect(rendered.first().getByRole("button")).toHaveCount(0)
await expect(rendered.nth(1).getByRole("cell").nth(16).getByRole("button")).toHaveCount(0)
@@ -46,7 +49,9 @@ for (const width of [1440, 320]) test("Pivot51×16: границы, сортир
mkdirSync("docs/ux-ui/screens", { recursive: true })
await page.screenshot({ path: "docs/ux-ui/screens/wide-statistics-pivot-" + width + ".png", animations: "disabled" })
const lastCell = rendered.nth(1).getByRole("cell").nth(15).getByRole("button")
- await lastCell.focus(); await page.keyboard.press("Enter")
+ await lastCell.focus()
+ expect(await lastCell.evaluate(el => { const value = el.getBoundingClientRect(), viewport = el.closest('[data-slot="scroll-area-viewport"]')!.getBoundingClientRect(), pinned = el.closest("tr")!.querySelector("td")!.getBoundingClientRect(); return value.left >= pinned.right - 1 && value.right <= viewport.right + 1 })).toBe(true)
+ await page.keyboard.press("Enter")
await expect(page).toHaveURL(/view=explore/)
const selected = new URL(page.url()); expect(selected.searchParams.get("userId")).toBe("50"); expect(selected.searchParams.get("service")).toBe("service-15")
expect(reads.some(url => url.pathname === "/api/statistics" && url.searchParams.get("userId") === "50" && url.searchParams.get("service") === "service-15")).toBe(true)