From 278b239d8161b11764eca2d14a0e038dd6841265 Mon Sep 17 00:00:00 2001 From: Deepak kudi Date: Thu, 4 Jun 2026 10:26:24 +0530 Subject: [PATCH] Fix access and certificate table sorting --- frontend/src/api/backend/models.ts | 1 + frontend/src/pages/Access/Table.test.tsx | 63 ++++++++++++++++ frontend/src/pages/Access/Table.tsx | 20 +++++- .../src/pages/Certificates/Table.test.tsx | 71 +++++++++++++++++++ frontend/src/pages/Certificates/Table.tsx | 36 +++++++++- 5 files changed, 187 insertions(+), 4 deletions(-) create mode 100644 frontend/src/pages/Access/Table.test.tsx create mode 100644 frontend/src/pages/Certificates/Table.test.tsx diff --git a/frontend/src/api/backend/models.ts b/frontend/src/api/backend/models.ts index 2ae0b08348..3548df2ad1 100644 --- a/frontend/src/api/backend/models.ts +++ b/frontend/src/api/backend/models.ts @@ -95,6 +95,7 @@ export interface Certificate { proxyHosts?: ProxyHost[]; deadHosts?: DeadHost[]; redirectionHosts?: RedirectionHost[]; + streams?: Stream[]; } export interface ProxyLocation { diff --git a/frontend/src/pages/Access/Table.test.tsx b/frontend/src/pages/Access/Table.test.tsx new file mode 100644 index 0000000000..1327935d6a --- /dev/null +++ b/frontend/src/pages/Access/Table.test.tsx @@ -0,0 +1,63 @@ +import { fireEvent, render, screen, within } from "@testing-library/react"; +import { LocaleProvider } from "src/context"; +import { describe, expect, it, vi } from "vitest"; +import Table from "./Table"; + +vi.mock("src/hooks", () => ({ + useUser: () => ({ + data: { + roles: ["admin"], + permissions: { + visibility: "all", + proxyHosts: "manage", + redirectionHosts: "manage", + deadHosts: "manage", + streams: "manage", + accessLists: "manage", + certificates: "manage", + }, + }, + isLoading: false, + }), +})); + +describe("Access table sorting", () => { + it("sorts rows when the name header is clicked", () => { + render( + + + , + ); + + fireEvent.click(screen.getByRole("columnheader", { name: /name/i })); + + const rows = screen.getAllByRole("row").slice(1); + expect(within(rows[0]).getByText("Alpha")).toBeTruthy(); + expect(within(rows[1]).getByText("Zulu")).toBeTruthy(); + }); +}); diff --git a/frontend/src/pages/Access/Table.tsx b/frontend/src/pages/Access/Table.tsx index 433c077b34..70cdbea55d 100644 --- a/frontend/src/pages/Access/Table.tsx +++ b/frontend/src/pages/Access/Table.tsx @@ -1,6 +1,12 @@ import { IconDotsVertical, IconEdit, IconTrash } from "@tabler/icons-react"; -import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table"; -import { useMemo } from "react"; +import { + createColumnHelper, + getCoreRowModel, + getSortedRowModel, + type SortingState, + useReactTable, +} from "@tanstack/react-table"; +import { useMemo, useState } from "react"; import type { AccessList } from "src/api/backend"; import { EmptyData, GravatarFormatter, HasPermission, ValueWithDateFormatter } from "src/components"; import { TableLayout } from "src/components/Table/TableLayout"; @@ -21,6 +27,7 @@ export default function Table({ data, isFetching, isFiltered, onEdit, onDelete, () => [ columnHelper.accessor((row: any) => row.owner, { id: "owner", + enableSorting: false, cell: (info: any) => { const value = info.getValue(); return ; @@ -32,6 +39,7 @@ export default function Table({ data, isFetching, isFiltered, onEdit, onDelete, columnHelper.accessor((row: any) => row, { id: "name", header: intl.formatMessage({ id: "column.name" }), + sortingFn: (a, b) => a.original.name.localeCompare(b.original.name), cell: (info: any) => ( ), @@ -39,11 +47,13 @@ export default function Table({ data, isFetching, isFiltered, onEdit, onDelete, columnHelper.accessor((row: any) => row.items, { id: "items", header: intl.formatMessage({ id: "column.authorization" }), + sortingFn: (a, b) => (a.original.items?.length ?? 0) - (b.original.items?.length ?? 0), cell: (info: any) => , }), columnHelper.accessor((row: any) => row.clients, { id: "clients", header: intl.formatMessage({ id: "column.access" }), + sortingFn: (a, b) => (a.original.clients?.length ?? 0) - (b.original.clients?.length ?? 0), cell: (info: any) => , }), columnHelper.accessor((row: any) => row.satisfyAny, { @@ -114,15 +124,21 @@ export default function Table({ data, isFetching, isFiltered, onEdit, onDelete, [columnHelper, onEdit, onDelete], ); + const [sorting, setSorting] = useState([]); + const tableInstance = useReactTable({ columns, data, + state: { sorting }, + onSortingChange: setSorting, getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), rowCount: data.length, meta: { isFetching, }, enableSortingRemoval: false, + sortDescFirst: false, }); return ( diff --git a/frontend/src/pages/Certificates/Table.test.tsx b/frontend/src/pages/Certificates/Table.test.tsx new file mode 100644 index 0000000000..267fc8ba02 --- /dev/null +++ b/frontend/src/pages/Certificates/Table.test.tsx @@ -0,0 +1,71 @@ +import { fireEvent, render, screen, within } from "@testing-library/react"; +import { LocaleProvider } from "src/context"; +import { describe, expect, it, vi } from "vitest"; +import Table from "./Table"; + +vi.mock("src/hooks", () => ({ + useUser: () => ({ + data: { + roles: ["admin"], + permissions: { + visibility: "all", + proxyHosts: "manage", + redirectionHosts: "manage", + deadHosts: "manage", + streams: "manage", + accessLists: "manage", + certificates: "manage", + }, + }, + isLoading: false, + }), +})); + +describe("Certificates table sorting", () => { + it("sorts rows when the name header is clicked", () => { + render( + +
+ , + ); + + fireEvent.click(screen.getByRole("columnheader", { name: /name/i })); + + const rows = screen.getAllByRole("row").slice(1); + expect(within(rows[0]).getByText("Alpha Cert")).toBeTruthy(); + expect(within(rows[1]).getByText("Zulu Cert")).toBeTruthy(); + }); +}); diff --git a/frontend/src/pages/Certificates/Table.tsx b/frontend/src/pages/Certificates/Table.tsx index 7eaeb77701..e14a8fe43b 100644 --- a/frontend/src/pages/Certificates/Table.tsx +++ b/frontend/src/pages/Certificates/Table.tsx @@ -1,6 +1,12 @@ import { IconDotsVertical, IconDownload, IconRefresh, IconTrash } from "@tabler/icons-react"; -import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table"; -import { useMemo } from "react"; +import { + createColumnHelper, + getCoreRowModel, + getSortedRowModel, + type SortingState, + useReactTable, +} from "@tanstack/react-table"; +import { useMemo, useState } from "react"; import type { Certificate } from "src/api/backend"; import { CertificateInUseFormatter, @@ -29,6 +35,7 @@ export default function Table({ data, isFetching, onDelete, onRenew, onDownload, () => [ columnHelper.accessor((row: any) => row.owner, { id: "owner", + enableSorting: false, cell: (info: any) => { const value = info.getValue(); return ; @@ -40,6 +47,11 @@ export default function Table({ data, isFetching, onDelete, onRenew, onDownload, columnHelper.accessor((row: any) => row, { id: "domainNames", header: intl.formatMessage({ id: "column.name" }), + sortingFn: (a, b) => { + const aVal = a.original.niceName || a.original.domainNames?.[0] || ""; + const bVal = b.original.niceName || b.original.domainNames?.[0] || ""; + return aVal.localeCompare(bVal); + }, cell: (info: any) => { const value = info.getValue(); return ( @@ -55,6 +67,7 @@ export default function Table({ data, isFetching, onDelete, onRenew, onDownload, columnHelper.accessor((row: any) => row, { id: "provider", header: intl.formatMessage({ id: "column.provider" }), + sortingFn: (a, b) => (a.original.provider ?? "").localeCompare(b.original.provider ?? ""), cell: (info: any) => { const r = info.getValue(); if (r.provider === "letsencrypt") { @@ -83,6 +96,19 @@ export default function Table({ data, isFetching, onDelete, onRenew, onDownload, columnHelper.accessor((row: any) => row, { id: "proxyHosts", header: intl.formatMessage({ id: "column.status" }), + sortingFn: (a, b) => { + const aVal = + (a.original.proxyHosts?.length ?? 0) + + (a.original.redirectionHosts?.length ?? 0) + + (a.original.deadHosts?.length ?? 0) + + (a.original.streams?.length ?? 0); + const bVal = + (b.original.proxyHosts?.length ?? 0) + + (b.original.redirectionHosts?.length ?? 0) + + (b.original.deadHosts?.length ?? 0) + + (b.original.streams?.length ?? 0); + return aVal - bVal; + }, cell: (info: any) => { const r = info.getValue(); return ( @@ -164,15 +190,21 @@ export default function Table({ data, isFetching, onDelete, onRenew, onDownload, [columnHelper, onDelete, onRenew, onDownload], ); + const [sorting, setSorting] = useState([]); + const tableInstance = useReactTable({ columns, data, + state: { sorting }, + onSortingChange: setSorting, getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), rowCount: data.length, meta: { isFetching, }, enableSortingRemoval: false, + sortDescFirst: false, }); const customAddBtn = (