From c7368ae7dc4950b6398f92fe3ad4f2e63fcbe105 Mon Sep 17 00:00:00 2001 From: naiba Date: Sat, 15 Aug 2026 05:00:47 +0000 Subject: [PATCH] fix: migrate tables to TanStack React Table v9 --- .github/workflows/e2e.yml | 46 +++++++++++++++++- src/components/fm.tsx | 5 +- src/components/xui/virtulized-data-table.tsx | 25 +++++----- src/lib/table.ts | 29 ++++++++++++ src/routes/alert-rule.tsx | 10 ++-- src/routes/cron.tsx | 10 ++-- src/routes/ddns.tsx | 10 ++-- src/routes/nat.tsx | 10 ++-- src/routes/notification-group.tsx | 15 ++++-- src/routes/notification.tsx | 10 ++-- src/routes/online-user.tsx | 10 ++-- src/routes/server-group.tsx | 10 ++-- src/routes/server.tsx | 10 ++-- src/routes/service.tsx | 10 ++-- src/routes/user.tsx | 10 ++-- src/routes/waf.tsx | 10 ++-- tests/e2e/agent-dashboard.spec.ts | 50 ++++++++++++++++++++ tests/e2e/cron-csrf.spec.ts | 14 ++---- tests/e2e/fm-csrf.spec.ts | 23 +++++---- 19 files changed, 223 insertions(+), 94 deletions(-) create mode 100644 src/lib/table.ts create mode 100644 tests/e2e/agent-dashboard.spec.ts diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index fb55e2c..c3cc600 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -21,6 +21,8 @@ jobs: timeout-minutes: 25 env: + AGENT_REPO: nezhahq/agent + AGENT_REF: main NEZHA_REPO: nezhahq/nezha NEZHA_REF: master NZ_LISTENHOST: 127.0.0.1 @@ -31,6 +33,7 @@ jobs: NZ_DEBUG: "true" E2E_ADMIN_USER: admin E2E_ADMIN_PASS: admin + E2E_AGENT_UUID: 11111111-2222-4333-8444-555555555555 E2E_BASE_URL: http://127.0.0.1:5173 steps: @@ -46,6 +49,13 @@ jobs: ref: ${{ env.NEZHA_REF }} path: nezha + - name: Checkout Nezha Agent + uses: actions/checkout@v7 + with: + repository: ${{ env.AGENT_REPO }} + ref: ${{ env.AGENT_REF }} + path: agent + - name: Setup Node uses: actions/setup-node@v7 with: @@ -57,7 +67,9 @@ jobs: uses: actions/setup-go@v7 with: go-version: "1.26.x" - cache-dependency-path: nezha/go.sum + cache-dependency-path: | + agent/go.sum + nezha/go.sum - name: Install frontend dependencies working-directory: admin-frontend @@ -77,6 +89,10 @@ jobs: "$(go env GOPATH)/bin/swag" init --pd -d cmd/dashboard -g main.go -o cmd/dashboard/docs go build -o /tmp/nezha-dashboard ./cmd/dashboard + - name: Build Agent + working-directory: agent + run: go build -o /tmp/nezha-agent ./cmd/agent + - name: Start backend working-directory: nezha env: @@ -101,6 +117,19 @@ jobs: cat /tmp/dashboard.log || true exit 1 + - name: Start Agent + working-directory: agent + env: + NZ_SERVER: 127.0.0.1:8008 + NZ_CLIENT_SECRET: ${{ env.NZ_AGENTSECRETKEY }} + NZ_UUID: ${{ env.E2E_AGENT_UUID }} + NZ_TLS: "false" + NZ_DISABLE_AUTO_UPDATE: "true" + run: | + nohup /tmp/nezha-agent -c /tmp/nezha-agent-config.yml \ + > /tmp/agent.log 2>&1 & + echo $! > /tmp/agent.pid + - name: Run Playwright tests working-directory: admin-frontend env: @@ -123,6 +152,21 @@ jobs: path: /tmp/dashboard.log retention-days: 7 + - name: Upload Agent log + if: always() + uses: actions/upload-artifact@v7 + with: + name: agent-log + path: /tmp/agent.log + retention-days: 7 + + - name: Stop Agent + if: always() + run: | + if [ -f /tmp/agent.pid ]; then + kill "$(cat /tmp/agent.pid)" || true + fi + - name: Stop backend if: always() run: | diff --git a/src/components/fm.tsx b/src/components/fm.tsx index 9b87147..e96cbe6 100644 --- a/src/components/fm.tsx +++ b/src/components/fm.tsx @@ -25,6 +25,7 @@ import { } from "@/components/ui/dropdown-menu" import { Input } from "@/components/ui/input" import { useMediaQuery } from "@/hooks/useMediaQuery" +import { virtualizedTableFeatures } from "@/lib/table" import { copyToClipboard, fm, formatPath, fmWorker as worker } from "@/lib/utils" import { FMEntry, @@ -84,7 +85,7 @@ export const FMComponent: React.FC = ({ const [dOpen, setdOpen] = useState(false) const [uOpen, setuOpen] = useState(false) - const columns: ColumnDef[] = [ + const columns: ColumnDef[] = [ { id: "type", header: () => {t("Type")}, @@ -122,7 +123,7 @@ export const FMComponent: React.FC = ({ }, ] - const tableRowComponent = (rows: Row[]) => + const tableRowComponent = (rows: Row[]) => function getTableRow(props: VirtualizedTableRowProps) { const index = Number(props["data-index"]) const row = rows[index] diff --git a/src/components/xui/virtulized-data-table.tsx b/src/components/xui/virtulized-data-table.tsx index ab1f719..5166f8f 100644 --- a/src/components/xui/virtulized-data-table.tsx +++ b/src/components/xui/virtulized-data-table.tsx @@ -3,16 +3,16 @@ import { ScrollArea } from "@/components/ui/scroll-area" import { TableCell, TableHead, TableRow } from "@/components/ui/table" import { useMediaQuery } from "@/hooks/useMediaQuery" +import { virtualizedTableFeatures } from "@/lib/table" import { cn } from "@/lib/utils" import { ColumnDef, Row, + RowData, SortDirection, SortingState, flexRender, - getCoreRowModel, - getSortedRowModel, - useReactTable, + useTable, } from "@tanstack/react-table" import { HTMLAttributes, JSX, forwardRef, useEffect, useRef, useState } from "react" import { TableVirtuoso } from "react-virtuoso" @@ -26,7 +26,9 @@ const TableComponent = forwardRef(rows: Row[]) => +const TableRowComponent = ( + rows: Row[], +) => function getTableRow(props: HTMLAttributes) { // @ts-expect-error data-index is a valid attribute const index = props["data-index"] @@ -64,34 +66,33 @@ function SortingIndicator({ isSorted }: { isSorted: SortDirection | false }) { ) } -interface DataTableProps { - columns: ColumnDef[] +interface DataTableProps { + columns: ColumnDef[] data: TData[] rowComponent?: ( - rows: Row[], + rows: Row[], ) => (props: HTMLAttributes) => JSX.Element | null } -export function DataTable({ +export function DataTable({ columns, data, rowComponent, -}: DataTableProps) { +}: DataTableProps) { const [sorting, setSorting] = useState([ { id: "type", desc: true, }, ]) - const table = useReactTable({ + const table = useTable({ + features: virtualizedTableFeatures, data, columns, state: { sorting, }, onSortingChange: setSorting, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), }) const { rows } = table.getRowModel() diff --git a/src/lib/table.ts b/src/lib/table.ts new file mode 100644 index 0000000..329137d --- /dev/null +++ b/src/lib/table.ts @@ -0,0 +1,29 @@ +import { + columnSizingFeature, + columnVisibilityFeature, + createSortedRowModel, + rowSelectionFeature, + rowSortingFeature, + sortFn_alphanumeric, + sortFn_datetime, + sortFn_text, + tableFeatures, +} from "@tanstack/react-table" + +export const selectableTableFeatures = tableFeatures({ + columnVisibilityFeature, + rowSelectionFeature, +}) + +export const virtualizedTableFeatures = tableFeatures({ + columnSizingFeature, + columnVisibilityFeature, + rowSelectionFeature, + rowSortingFeature, + sortedRowModel: createSortedRowModel(), + sortFns: { + alphanumeric: sortFn_alphanumeric, + datetime: sortFn_datetime, + text: sortFn_text, + }, +}) diff --git a/src/routes/alert-rule.tsx b/src/routes/alert-rule.tsx index 11ac8f4..da6d346 100644 --- a/src/routes/alert-rule.tsx +++ b/src/routes/alert-rule.tsx @@ -14,8 +14,9 @@ import { TableHeader, TableRow, } from "@/components/ui/table" +import { selectableTableFeatures } from "@/lib/table" import { ModelAlertRule, triggerModes } from "@/types" -import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table" +import { ColumnDef, flexRender, useTable } from "@tanstack/react-table" import { useEffect, useMemo } from "react" import { useTranslation } from "react-i18next" import { toast } from "sonner" @@ -37,7 +38,7 @@ export default function AlertRulePage() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [error]) - const columns: ColumnDef[] = [ + const columns: ColumnDef[] = [ { id: "select", header: ({ table }) => ( @@ -57,7 +58,6 @@ export default function AlertRulePage() { aria-label="Select row" /> ), - enableSorting: false, enableHiding: false, }, { @@ -131,10 +131,10 @@ export default function AlertRulePage() { return data ?? [] }, [data]) - const table = useReactTable({ + const table = useTable({ + features: selectableTableFeatures, data: dataCache, columns, - getCoreRowModel: getCoreRowModel(), }) const selectedRows = table.getSelectedRowModel().rows diff --git a/src/routes/cron.tsx b/src/routes/cron.tsx index e37a4e7..4af519b 100644 --- a/src/routes/cron.tsx +++ b/src/routes/cron.tsx @@ -14,9 +14,10 @@ import { TableRow, } from "@/components/ui/table" import { IconButton } from "@/components/xui/icon-button" +import { selectableTableFeatures } from "@/lib/table" import { ModelCron } from "@/types" import { cronTypes } from "@/types" -import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table" +import { ColumnDef, flexRender, useTable } from "@tanstack/react-table" import { useEffect, useMemo } from "react" import { useTranslation } from "react-i18next" import { toast } from "sonner" @@ -36,7 +37,7 @@ export default function CronPage() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [error]) - const columns: ColumnDef[] = [ + const columns: ColumnDef[] = [ { id: "select", header: ({ table }) => ( @@ -56,7 +57,6 @@ export default function CronPage() { aria-label="Select row" /> ), - enableSorting: false, enableHiding: false, }, { @@ -199,10 +199,10 @@ export default function CronPage() { return data ?? [] }, [data]) - const table = useReactTable({ + const table = useTable({ + features: selectableTableFeatures, data: dataCache, columns, - getCoreRowModel: getCoreRowModel(), }) const selectedRows = table.getSelectedRowModel().rows diff --git a/src/routes/ddns.tsx b/src/routes/ddns.tsx index 313f35c..160bc29 100644 --- a/src/routes/ddns.tsx +++ b/src/routes/ddns.tsx @@ -12,8 +12,9 @@ import { TableHeader, TableRow, } from "@/components/ui/table" +import { selectableTableFeatures } from "@/lib/table" import { ModelDDNSProfile } from "@/types" -import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table" +import { ColumnDef, flexRender, useTable } from "@tanstack/react-table" import { useEffect, useMemo, useState } from "react" import { useTranslation } from "react-i18next" import { toast } from "sonner" @@ -45,7 +46,7 @@ export default function DDNSPage() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [error]) - const columns: ColumnDef[] = [ + const columns: ColumnDef[] = [ { id: "select", header: ({ table }) => ( @@ -65,7 +66,6 @@ export default function DDNSPage() { aria-label="Select row" /> ), - enableSorting: false, enableHiding: false, }, { @@ -136,10 +136,10 @@ export default function DDNSPage() { return data ?? [] }, [data]) - const table = useReactTable({ + const table = useTable({ + features: selectableTableFeatures, data: dataCache, columns, - getCoreRowModel: getCoreRowModel(), }) const selectedRows = table.getSelectedRowModel().rows diff --git a/src/routes/nat.tsx b/src/routes/nat.tsx index 9d35dfa..861aab8 100644 --- a/src/routes/nat.tsx +++ b/src/routes/nat.tsx @@ -12,8 +12,9 @@ import { TableHeader, TableRow, } from "@/components/ui/table" +import { selectableTableFeatures } from "@/lib/table" import { ModelNAT } from "@/types" -import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table" +import { ColumnDef, flexRender, useTable } from "@tanstack/react-table" import { useEffect, useMemo } from "react" import { useTranslation } from "react-i18next" import { toast } from "sonner" @@ -33,7 +34,7 @@ export default function NATPage() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [error]) - const columns: ColumnDef[] = [ + const columns: ColumnDef[] = [ { id: "select", header: ({ table }) => ( @@ -53,7 +54,6 @@ export default function NATPage() { aria-label="Select row" /> ), - enableSorting: false, enableHiding: false, }, { @@ -119,10 +119,10 @@ export default function NATPage() { return data ?? [] }, [data]) - const table = useReactTable({ + const table = useTable({ + features: selectableTableFeatures, data: dataCache, columns, - getCoreRowModel: getCoreRowModel(), }) const selectedRows = table.getSelectedRowModel().rows diff --git a/src/routes/notification-group.tsx b/src/routes/notification-group.tsx index 523e0ac..f7837ee 100644 --- a/src/routes/notification-group.tsx +++ b/src/routes/notification-group.tsx @@ -13,13 +13,19 @@ import { TableHeader, TableRow, } from "@/components/ui/table" +import { selectableTableFeatures } from "@/lib/table" import { ModelNotificationGroupResponseItem } from "@/types" -import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table" +import { ColumnDef, flexRender, useTable } from "@tanstack/react-table" import { useEffect, useMemo } from "react" import { useTranslation } from "react-i18next" import { toast } from "sonner" import useSWR from "swr" +type NotificationGroupColumn = ColumnDef< + typeof selectableTableFeatures, + ModelNotificationGroupResponseItem +> + export default function NotificationGroupPage() { const { t } = useTranslation() const { data, mutate, error, isLoading } = useSWR( @@ -37,7 +43,7 @@ export default function NotificationGroupPage() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [error]) - const columns: ColumnDef[] = [ + const columns: NotificationGroupColumn[] = [ { id: "select", header: ({ table }) => ( @@ -57,7 +63,6 @@ export default function NotificationGroupPage() { aria-label="Select row" /> ), - enableSorting: false, enableHiding: false, }, { @@ -112,10 +117,10 @@ export default function NotificationGroupPage() { return data ?? [] }, [data]) - const table = useReactTable({ + const table = useTable({ + features: selectableTableFeatures, data: dataCache, columns, - getCoreRowModel: getCoreRowModel(), }) const selectedRows = table.getSelectedRowModel().rows diff --git a/src/routes/notification.tsx b/src/routes/notification.tsx index d4bf20a..a779afa 100644 --- a/src/routes/notification.tsx +++ b/src/routes/notification.tsx @@ -15,8 +15,9 @@ import { TableRow, } from "@/components/ui/table" import { useNotification } from "@/hooks/useNotfication" +import { selectableTableFeatures } from "@/lib/table" import { ModelNotification } from "@/types" -import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table" +import { ColumnDef, flexRender, useTable } from "@tanstack/react-table" import { useEffect, useMemo } from "react" import { useTranslation } from "react-i18next" import { toast } from "sonner" @@ -40,7 +41,7 @@ export default function NotificationPage() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [error]) - const columns: ColumnDef[] = [ + const columns: ColumnDef[] = [ { id: "select", header: ({ table }) => ( @@ -60,7 +61,6 @@ export default function NotificationPage() { aria-label="Select row" /> ), - enableSorting: false, enableHiding: false, }, { @@ -127,10 +127,10 @@ export default function NotificationPage() { return data ?? [] }, [data]) - const table = useReactTable({ + const table = useTable({ + features: selectableTableFeatures, data: dataCache, columns, - getCoreRowModel: getCoreRowModel(), }) const selectedRows = table.getSelectedRowModel().rows diff --git a/src/routes/online-user.tsx b/src/routes/online-user.tsx index 850f3ae..de3aa85 100644 --- a/src/routes/online-user.tsx +++ b/src/routes/online-user.tsx @@ -22,8 +22,9 @@ import { TableRow, } from "@/components/ui/table" import { useAuth } from "@/hooks/useAuth" +import { selectableTableFeatures } from "@/lib/table" import { ModelOnlineUser, ModelOnlineUserApi } from "@/types" -import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table" +import { ColumnDef, flexRender, useTable } from "@tanstack/react-table" import { useEffect, useMemo } from "react" import { useTranslation } from "react-i18next" import { useSearchParams } from "react-router-dom" @@ -55,7 +56,7 @@ export default function OnlineUserPage() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [error]) - let columns: ColumnDef[] = [ + let columns: ColumnDef[] = [ { id: "select", header: ({ table }) => ( @@ -75,7 +76,6 @@ export default function OnlineUserPage() { aria-label="Select row" /> ), - enableSorting: false, enableHiding: false, }, { @@ -128,10 +128,10 @@ export default function OnlineUserPage() { return data?.value ?? [] }, [data]) - const table = useReactTable({ + const table = useTable({ + features: selectableTableFeatures, data: dataCache, columns, - getCoreRowModel: getCoreRowModel(), }) const selectedRows = table.getSelectedRowModel().rows diff --git a/src/routes/server-group.tsx b/src/routes/server-group.tsx index 98fec61..127e206 100644 --- a/src/routes/server-group.tsx +++ b/src/routes/server-group.tsx @@ -13,8 +13,9 @@ import { TableHeader, TableRow, } from "@/components/ui/table" +import { selectableTableFeatures } from "@/lib/table" import { ModelServerGroupResponseItem } from "@/types" -import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table" +import { ColumnDef, flexRender, useTable } from "@tanstack/react-table" import { useEffect, useMemo } from "react" import { useTranslation } from "react-i18next" import { toast } from "sonner" @@ -37,7 +38,7 @@ export default function ServerGroupPage() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [error]) - const columns: ColumnDef[] = [ + const columns: ColumnDef[] = [ { id: "select", header: ({ table }) => ( @@ -57,7 +58,6 @@ export default function ServerGroupPage() { aria-label="Select row" /> ), - enableSorting: false, enableHiding: false, }, { @@ -112,10 +112,10 @@ export default function ServerGroupPage() { return data ?? [] }, [data]) - const table = useReactTable({ + const table = useTable({ + features: selectableTableFeatures, data: dataCache, columns, - getCoreRowModel: getCoreRowModel(), }) const selectedRows = table.getSelectedRowModel().rows diff --git a/src/routes/server.tsx b/src/routes/server.tsx index 3f3aa20..ff501b0 100644 --- a/src/routes/server.tsx +++ b/src/routes/server.tsx @@ -27,9 +27,10 @@ import { } from "@/components/ui/table" import { IconButton } from "@/components/xui/icon-button" import { useServer } from "@/hooks/useServer" +import { selectableTableFeatures } from "@/lib/table" import { joinIP } from "@/lib/utils" import { ModelServerTaskResponse, ModelServer as Server } from "@/types" -import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table" +import { ColumnDef, flexRender, useTable } from "@tanstack/react-table" import { useEffect, useMemo } from "react" import { useTranslation } from "react-i18next" import { toast } from "sonner" @@ -51,7 +52,7 @@ export default function ServerPage() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [error]) - const columns: ColumnDef[] = [ + const columns: ColumnDef[] = [ { id: "select", header: ({ table }) => ( @@ -71,7 +72,6 @@ export default function ServerPage() { aria-label="Select row" /> ), - enableSorting: false, enableHiding: false, }, { @@ -217,10 +217,10 @@ export default function ServerPage() { return data ?? [] }, [data]) - const table = useReactTable({ + const table = useTable({ + features: selectableTableFeatures, data: dataCache, columns, - getCoreRowModel: getCoreRowModel(), }) const selectedRows = table.getSelectedRowModel().rows diff --git a/src/routes/service.tsx b/src/routes/service.tsx index 27d711a..dac66c9 100644 --- a/src/routes/service.tsx +++ b/src/routes/service.tsx @@ -12,9 +12,10 @@ import { TableHeader, TableRow, } from "@/components/ui/table" +import { selectableTableFeatures } from "@/lib/table" import { ModelService as Service } from "@/types" import { serviceTypes } from "@/types" -import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table" +import { ColumnDef, flexRender, useTable } from "@tanstack/react-table" import { useEffect, useMemo } from "react" import { useTranslation } from "react-i18next" import { toast } from "sonner" @@ -34,7 +35,7 @@ export default function ServicePage() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [error]) - const columns: ColumnDef[] = [ + const columns: ColumnDef[] = [ { id: "select", header: ({ table }) => ( @@ -54,7 +55,6 @@ export default function ServicePage() { aria-label="Select row" /> ), - enableSorting: false, enableHiding: false, }, { @@ -165,10 +165,10 @@ export default function ServicePage() { return data ?? [] }, [data]) - const table = useReactTable({ + const table = useTable({ + features: selectableTableFeatures, data: dataCache, columns, - getCoreRowModel: getCoreRowModel(), }) const selectedRows = table.getSelectedRowModel().rows diff --git a/src/routes/user.tsx b/src/routes/user.tsx index e05c274..1c0e4c5 100644 --- a/src/routes/user.tsx +++ b/src/routes/user.tsx @@ -13,8 +13,9 @@ import { TableRow, } from "@/components/ui/table" import { UserCard } from "@/components/user" +import { selectableTableFeatures } from "@/lib/table" import { ModelUser } from "@/types" -import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table" +import { ColumnDef, flexRender, useTable } from "@tanstack/react-table" import { useEffect, useMemo } from "react" import { useTranslation } from "react-i18next" import { toast } from "sonner" @@ -34,7 +35,7 @@ export default function UserPage() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [error]) - const columns: ColumnDef[] = [ + const columns: ColumnDef[] = [ { id: "select", header: ({ table }) => ( @@ -54,7 +55,6 @@ export default function UserPage() { aria-label="Select row" /> ), - enableSorting: false, enableHiding: false, }, { @@ -105,10 +105,10 @@ export default function UserPage() { return data ?? [] }, [data]) - const table = useReactTable({ + const table = useTable({ + features: selectableTableFeatures, data: dataCache, columns, - getCoreRowModel: getCoreRowModel(), }) const selectedRows = table.getSelectedRowModel().rows diff --git a/src/routes/waf.tsx b/src/routes/waf.tsx index c547ded..7dee61b 100644 --- a/src/routes/waf.tsx +++ b/src/routes/waf.tsx @@ -22,13 +22,14 @@ import { TableRow, } from "@/components/ui/table" import { useAuth } from "@/hooks/useAuth" +import { selectableTableFeatures } from "@/lib/table" import { GithubComNezhahqNezhaModelValueArrayModelWAFApiMock, ModelWAFApiMock, wafBlockIdentifiers, wafBlockReasons, } from "@/types" -import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table" +import { ColumnDef, flexRender, useTable } from "@tanstack/react-table" import { useEffect, useMemo } from "react" import { useTranslation } from "react-i18next" import { useSearchParams } from "react-router-dom" @@ -61,7 +62,7 @@ export default function WAFPage() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [error]) - let columns: ColumnDef[] = [ + let columns: ColumnDef[] = [ { id: "select", header: ({ table }) => ( @@ -81,7 +82,6 @@ export default function WAFPage() { aria-label="Select row" /> ), - enableSorting: false, enableHiding: false, }, { @@ -147,10 +147,10 @@ export default function WAFPage() { return data?.value ?? [] }, [data]) - const table = useReactTable({ + const table = useTable({ + features: selectableTableFeatures, data: dataCache, columns, - getCoreRowModel: getCoreRowModel(), }) const selectedRows = table.getSelectedRowModel().rows diff --git a/tests/e2e/agent-dashboard.spec.ts b/tests/e2e/agent-dashboard.spec.ts new file mode 100644 index 0000000..8437879 --- /dev/null +++ b/tests/e2e/agent-dashboard.spec.ts @@ -0,0 +1,50 @@ +import { expect } from "@playwright/test" + +import { test } from "./fixtures" + +type AgentServer = { + host?: { platform?: string } + id: number + last_active?: string + name: string + uuid: string +} + +test("a real agent reports to the dashboard and is selectable in the server table", async ({ + adminPage: page, +}) => { + const agentUUID = process.env.E2E_AGENT_UUID + expect(agentUUID, "E2E_AGENT_UUID must identify the Agent started by CI").toBeTruthy() + + let agentServer: AgentServer | undefined + await expect + .poll( + async () => { + const response = await page.request.get("/api/v1/server") + if (!response.ok()) return false + + const body = (await response.json()) as { data?: AgentServer[] } + agentServer = body.data?.find((server) => server.uuid === agentUUID) + if (!agentServer?.last_active || !agentServer.host?.platform) return false + + return new Date(agentServer.last_active).getTime() > 0 + }, + { + message: "the real Agent must register and report live host state", + timeout: 30_000, + }, + ) + .toBe(true) + + await page.goto("/dashboard") + + const row = page.getByRole("row").filter({ + has: page.getByText(agentServer!.name, { exact: true }), + }) + await expect(row).toHaveCount(1) + + const selection = row.getByRole("checkbox", { name: "Select row" }) + await expect(selection).not.toBeChecked() + await selection.click() + await expect(selection).toBeChecked() +}) diff --git a/tests/e2e/cron-csrf.spec.ts b/tests/e2e/cron-csrf.spec.ts index 3d7dc3a..7c5f746 100644 --- a/tests/e2e/cron-csrf.spec.ts +++ b/tests/e2e/cron-csrf.spec.ts @@ -1,12 +1,11 @@ import { expect } from "@playwright/test" -import { csrfHeaders, test } from "./fixtures" +import { csrfRequest, test } from "./fixtures" test("manual cron trigger goes through POST, not GET", async ({ adminPage: page }) => { - const created = await page.request.post("/api/v1/cron", { - headers: await csrfHeaders(page), + const created = await csrfRequest(page, "post", "/api/v1/cron", { data: { - name: "e2e-cron-csrf", + name: `e2e-cron-csrf-${Date.now().toString(36)}`, task_type: 0, scheduler: "@every 1h", command: "true", @@ -29,15 +28,12 @@ test("manual cron trigger goes through POST, not GET", async ({ adminPage: page `GET must no longer be routable (got ${getResp.status()})`, ).toBeTruthy() - const postResp = await page.request.post(`/api/v1/cron/${cronID}/manual`, { - headers: await csrfHeaders(page), - }) + const postResp = await csrfRequest(page, "post", `/api/v1/cron/${cronID}/manual`) expect(postResp.ok(), `POST must succeed (got ${postResp.status()})`).toBeTruthy() const body = await postResp.json() expect(body.success).toBe(true) } finally { - await page.request.post("/api/v1/batch-delete/cron", { - headers: await csrfHeaders(page), + await csrfRequest(page, "post", "/api/v1/batch-delete/cron", { data: [cronID], }) } diff --git a/tests/e2e/fm-csrf.spec.ts b/tests/e2e/fm-csrf.spec.ts index 2762223..c894eb8 100644 --- a/tests/e2e/fm-csrf.spec.ts +++ b/tests/e2e/fm-csrf.spec.ts @@ -1,8 +1,10 @@ import { expect } from "@playwright/test" -import { csrfHeaders, test } from "./fixtures" +import { csrfRequest, test } from "./fixtures" -test("file manager creation only accepts POST", async ({ adminPage: page }) => { +test("file manager creation only accepts POST and reaches the connected Agent", async ({ + adminPage: page, +}) => { const getResp = await page.request.get("/api/v1/file?id=1", { failOnStatusCode: false, }) @@ -11,15 +13,16 @@ test("file manager creation only accepts POST", async ({ adminPage: page }) => { `GET /api/v1/file must no longer be routable (got ${getResp.status()})`, ).toBeTruthy() - const postResp = await page.request.post("/api/v1/file?id=1", { + const postResp = await csrfRequest(page, "post", "/api/v1/file?id=1", { failOnStatusCode: false, - headers: await csrfHeaders(page), }) expect(postResp.status()).toBe(200) - const body = await postResp.json() - expect( - body.success, - "without a connected agent server the POST surfaces a Service error, but the route is reachable", - ).not.toBe(true) - expect(body.error).toBeTruthy() + const body = (await postResp.json()) as { + data?: { session_id?: string } + success?: boolean + } + expect(body.success, "POST must create a file-manager session through the real Agent").toBe( + true, + ) + expect(body.data?.session_id).toBeTruthy() })