fix: migrate tables to TanStack React Table v9

This commit is contained in:
naiba
2026-08-15 05:00:47 +00:00
parent 76d95ebbd4
commit c7368ae7dc
19 changed files with 223 additions and 94 deletions
+45 -1
View File
@@ -21,6 +21,8 @@ jobs:
timeout-minutes: 25 timeout-minutes: 25
env: env:
AGENT_REPO: nezhahq/agent
AGENT_REF: main
NEZHA_REPO: nezhahq/nezha NEZHA_REPO: nezhahq/nezha
NEZHA_REF: master NEZHA_REF: master
NZ_LISTENHOST: 127.0.0.1 NZ_LISTENHOST: 127.0.0.1
@@ -31,6 +33,7 @@ jobs:
NZ_DEBUG: "true" NZ_DEBUG: "true"
E2E_ADMIN_USER: admin E2E_ADMIN_USER: admin
E2E_ADMIN_PASS: admin E2E_ADMIN_PASS: admin
E2E_AGENT_UUID: 11111111-2222-4333-8444-555555555555
E2E_BASE_URL: http://127.0.0.1:5173 E2E_BASE_URL: http://127.0.0.1:5173
steps: steps:
@@ -46,6 +49,13 @@ jobs:
ref: ${{ env.NEZHA_REF }} ref: ${{ env.NEZHA_REF }}
path: nezha path: nezha
- name: Checkout Nezha Agent
uses: actions/checkout@v7
with:
repository: ${{ env.AGENT_REPO }}
ref: ${{ env.AGENT_REF }}
path: agent
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v7 uses: actions/setup-node@v7
with: with:
@@ -57,7 +67,9 @@ jobs:
uses: actions/setup-go@v7 uses: actions/setup-go@v7
with: with:
go-version: "1.26.x" go-version: "1.26.x"
cache-dependency-path: nezha/go.sum cache-dependency-path: |
agent/go.sum
nezha/go.sum
- name: Install frontend dependencies - name: Install frontend dependencies
working-directory: admin-frontend 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 env GOPATH)/bin/swag" init --pd -d cmd/dashboard -g main.go -o cmd/dashboard/docs
go build -o /tmp/nezha-dashboard ./cmd/dashboard 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 - name: Start backend
working-directory: nezha working-directory: nezha
env: env:
@@ -101,6 +117,19 @@ jobs:
cat /tmp/dashboard.log || true cat /tmp/dashboard.log || true
exit 1 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 - name: Run Playwright tests
working-directory: admin-frontend working-directory: admin-frontend
env: env:
@@ -123,6 +152,21 @@ jobs:
path: /tmp/dashboard.log path: /tmp/dashboard.log
retention-days: 7 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 - name: Stop backend
if: always() if: always()
run: | run: |
+3 -2
View File
@@ -25,6 +25,7 @@ import {
} from "@/components/ui/dropdown-menu" } from "@/components/ui/dropdown-menu"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { useMediaQuery } from "@/hooks/useMediaQuery" import { useMediaQuery } from "@/hooks/useMediaQuery"
import { virtualizedTableFeatures } from "@/lib/table"
import { copyToClipboard, fm, formatPath, fmWorker as worker } from "@/lib/utils" import { copyToClipboard, fm, formatPath, fmWorker as worker } from "@/lib/utils"
import { import {
FMEntry, FMEntry,
@@ -84,7 +85,7 @@ export const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({
const [dOpen, setdOpen] = useState(false) const [dOpen, setdOpen] = useState(false)
const [uOpen, setuOpen] = useState(false) const [uOpen, setuOpen] = useState(false)
const columns: ColumnDef<FMEntry>[] = [ const columns: ColumnDef<typeof virtualizedTableFeatures, FMEntry>[] = [
{ {
id: "type", id: "type",
header: () => <span>{t("Type")}</span>, header: () => <span>{t("Type")}</span>,
@@ -122,7 +123,7 @@ export const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({
}, },
] ]
const tableRowComponent = (rows: Row<FMEntry>[]) => const tableRowComponent = (rows: Row<typeof virtualizedTableFeatures, FMEntry>[]) =>
function getTableRow(props: VirtualizedTableRowProps) { function getTableRow(props: VirtualizedTableRowProps) {
const index = Number(props["data-index"]) const index = Number(props["data-index"])
const row = rows[index] const row = rows[index]
+13 -12
View File
@@ -3,16 +3,16 @@
import { ScrollArea } from "@/components/ui/scroll-area" import { ScrollArea } from "@/components/ui/scroll-area"
import { TableCell, TableHead, TableRow } from "@/components/ui/table" import { TableCell, TableHead, TableRow } from "@/components/ui/table"
import { useMediaQuery } from "@/hooks/useMediaQuery" import { useMediaQuery } from "@/hooks/useMediaQuery"
import { virtualizedTableFeatures } from "@/lib/table"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { import {
ColumnDef, ColumnDef,
Row, Row,
RowData,
SortDirection, SortDirection,
SortingState, SortingState,
flexRender, flexRender,
getCoreRowModel, useTable,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table" } from "@tanstack/react-table"
import { HTMLAttributes, JSX, forwardRef, useEffect, useRef, useState } from "react" import { HTMLAttributes, JSX, forwardRef, useEffect, useRef, useState } from "react"
import { TableVirtuoso } from "react-virtuoso" import { TableVirtuoso } from "react-virtuoso"
@@ -26,7 +26,9 @@ const TableComponent = forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTab
) )
TableComponent.displayName = "TableComponent" TableComponent.displayName = "TableComponent"
const TableRowComponent = <TData,>(rows: Row<TData>[]) => const TableRowComponent = <TData extends RowData>(
rows: Row<typeof virtualizedTableFeatures, TData>[],
) =>
function getTableRow(props: HTMLAttributes<HTMLTableRowElement>) { function getTableRow(props: HTMLAttributes<HTMLTableRowElement>) {
// @ts-expect-error data-index is a valid attribute // @ts-expect-error data-index is a valid attribute
const index = props["data-index"] const index = props["data-index"]
@@ -64,34 +66,33 @@ function SortingIndicator({ isSorted }: { isSorted: SortDirection | false }) {
) )
} }
interface DataTableProps<TData, TValue> { interface DataTableProps<TData extends RowData> {
columns: ColumnDef<TData, TValue>[] columns: ColumnDef<typeof virtualizedTableFeatures, TData>[]
data: TData[] data: TData[]
rowComponent?: ( rowComponent?: (
rows: Row<TData>[], rows: Row<typeof virtualizedTableFeatures, TData>[],
) => (props: HTMLAttributes<HTMLTableRowElement>) => JSX.Element | null ) => (props: HTMLAttributes<HTMLTableRowElement>) => JSX.Element | null
} }
export function DataTable<TData, TValue>({ export function DataTable<TData extends RowData>({
columns, columns,
data, data,
rowComponent, rowComponent,
}: DataTableProps<TData, TValue>) { }: DataTableProps<TData>) {
const [sorting, setSorting] = useState<SortingState>([ const [sorting, setSorting] = useState<SortingState>([
{ {
id: "type", id: "type",
desc: true, desc: true,
}, },
]) ])
const table = useReactTable({ const table = useTable({
features: virtualizedTableFeatures,
data, data,
columns, columns,
state: { state: {
sorting, sorting,
}, },
onSortingChange: setSorting, onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
}) })
const { rows } = table.getRowModel() const { rows } = table.getRowModel()
+29
View File
@@ -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,
},
})
+5 -5
View File
@@ -14,8 +14,9 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table" } from "@/components/ui/table"
import { selectableTableFeatures } from "@/lib/table"
import { ModelAlertRule, triggerModes } from "@/types" 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 { useEffect, useMemo } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { toast } from "sonner" import { toast } from "sonner"
@@ -37,7 +38,7 @@ export default function AlertRulePage() {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [error]) }, [error])
const columns: ColumnDef<ModelAlertRule>[] = [ const columns: ColumnDef<typeof selectableTableFeatures, ModelAlertRule>[] = [
{ {
id: "select", id: "select",
header: ({ table }) => ( header: ({ table }) => (
@@ -57,7 +58,6 @@ export default function AlertRulePage() {
aria-label="Select row" aria-label="Select row"
/> />
), ),
enableSorting: false,
enableHiding: false, enableHiding: false,
}, },
{ {
@@ -131,10 +131,10 @@ export default function AlertRulePage() {
return data ?? [] return data ?? []
}, [data]) }, [data])
const table = useReactTable({ const table = useTable({
features: selectableTableFeatures,
data: dataCache, data: dataCache,
columns, columns,
getCoreRowModel: getCoreRowModel(),
}) })
const selectedRows = table.getSelectedRowModel().rows const selectedRows = table.getSelectedRowModel().rows
+5 -5
View File
@@ -14,9 +14,10 @@ import {
TableRow, TableRow,
} from "@/components/ui/table" } from "@/components/ui/table"
import { IconButton } from "@/components/xui/icon-button" import { IconButton } from "@/components/xui/icon-button"
import { selectableTableFeatures } from "@/lib/table"
import { ModelCron } from "@/types" import { ModelCron } from "@/types"
import { cronTypes } 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 { useEffect, useMemo } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { toast } from "sonner" import { toast } from "sonner"
@@ -36,7 +37,7 @@ export default function CronPage() {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [error]) }, [error])
const columns: ColumnDef<ModelCron>[] = [ const columns: ColumnDef<typeof selectableTableFeatures, ModelCron>[] = [
{ {
id: "select", id: "select",
header: ({ table }) => ( header: ({ table }) => (
@@ -56,7 +57,6 @@ export default function CronPage() {
aria-label="Select row" aria-label="Select row"
/> />
), ),
enableSorting: false,
enableHiding: false, enableHiding: false,
}, },
{ {
@@ -199,10 +199,10 @@ export default function CronPage() {
return data ?? [] return data ?? []
}, [data]) }, [data])
const table = useReactTable({ const table = useTable({
features: selectableTableFeatures,
data: dataCache, data: dataCache,
columns, columns,
getCoreRowModel: getCoreRowModel(),
}) })
const selectedRows = table.getSelectedRowModel().rows const selectedRows = table.getSelectedRowModel().rows
+5 -5
View File
@@ -12,8 +12,9 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table" } from "@/components/ui/table"
import { selectableTableFeatures } from "@/lib/table"
import { ModelDDNSProfile } from "@/types" 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 { useEffect, useMemo, useState } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { toast } from "sonner" import { toast } from "sonner"
@@ -45,7 +46,7 @@ export default function DDNSPage() {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [error]) }, [error])
const columns: ColumnDef<ModelDDNSProfile>[] = [ const columns: ColumnDef<typeof selectableTableFeatures, ModelDDNSProfile>[] = [
{ {
id: "select", id: "select",
header: ({ table }) => ( header: ({ table }) => (
@@ -65,7 +66,6 @@ export default function DDNSPage() {
aria-label="Select row" aria-label="Select row"
/> />
), ),
enableSorting: false,
enableHiding: false, enableHiding: false,
}, },
{ {
@@ -136,10 +136,10 @@ export default function DDNSPage() {
return data ?? [] return data ?? []
}, [data]) }, [data])
const table = useReactTable({ const table = useTable({
features: selectableTableFeatures,
data: dataCache, data: dataCache,
columns, columns,
getCoreRowModel: getCoreRowModel(),
}) })
const selectedRows = table.getSelectedRowModel().rows const selectedRows = table.getSelectedRowModel().rows
+5 -5
View File
@@ -12,8 +12,9 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table" } from "@/components/ui/table"
import { selectableTableFeatures } from "@/lib/table"
import { ModelNAT } from "@/types" 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 { useEffect, useMemo } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { toast } from "sonner" import { toast } from "sonner"
@@ -33,7 +34,7 @@ export default function NATPage() {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [error]) }, [error])
const columns: ColumnDef<ModelNAT>[] = [ const columns: ColumnDef<typeof selectableTableFeatures, ModelNAT>[] = [
{ {
id: "select", id: "select",
header: ({ table }) => ( header: ({ table }) => (
@@ -53,7 +54,6 @@ export default function NATPage() {
aria-label="Select row" aria-label="Select row"
/> />
), ),
enableSorting: false,
enableHiding: false, enableHiding: false,
}, },
{ {
@@ -119,10 +119,10 @@ export default function NATPage() {
return data ?? [] return data ?? []
}, [data]) }, [data])
const table = useReactTable({ const table = useTable({
features: selectableTableFeatures,
data: dataCache, data: dataCache,
columns, columns,
getCoreRowModel: getCoreRowModel(),
}) })
const selectedRows = table.getSelectedRowModel().rows const selectedRows = table.getSelectedRowModel().rows
+10 -5
View File
@@ -13,13 +13,19 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table" } from "@/components/ui/table"
import { selectableTableFeatures } from "@/lib/table"
import { ModelNotificationGroupResponseItem } from "@/types" 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 { useEffect, useMemo } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { toast } from "sonner" import { toast } from "sonner"
import useSWR from "swr" import useSWR from "swr"
type NotificationGroupColumn = ColumnDef<
typeof selectableTableFeatures,
ModelNotificationGroupResponseItem
>
export default function NotificationGroupPage() { export default function NotificationGroupPage() {
const { t } = useTranslation() const { t } = useTranslation()
const { data, mutate, error, isLoading } = useSWR<ModelNotificationGroupResponseItem[]>( const { data, mutate, error, isLoading } = useSWR<ModelNotificationGroupResponseItem[]>(
@@ -37,7 +43,7 @@ export default function NotificationGroupPage() {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [error]) }, [error])
const columns: ColumnDef<ModelNotificationGroupResponseItem>[] = [ const columns: NotificationGroupColumn[] = [
{ {
id: "select", id: "select",
header: ({ table }) => ( header: ({ table }) => (
@@ -57,7 +63,6 @@ export default function NotificationGroupPage() {
aria-label="Select row" aria-label="Select row"
/> />
), ),
enableSorting: false,
enableHiding: false, enableHiding: false,
}, },
{ {
@@ -112,10 +117,10 @@ export default function NotificationGroupPage() {
return data ?? [] return data ?? []
}, [data]) }, [data])
const table = useReactTable({ const table = useTable({
features: selectableTableFeatures,
data: dataCache, data: dataCache,
columns, columns,
getCoreRowModel: getCoreRowModel(),
}) })
const selectedRows = table.getSelectedRowModel().rows const selectedRows = table.getSelectedRowModel().rows
+5 -5
View File
@@ -15,8 +15,9 @@ import {
TableRow, TableRow,
} from "@/components/ui/table" } from "@/components/ui/table"
import { useNotification } from "@/hooks/useNotfication" import { useNotification } from "@/hooks/useNotfication"
import { selectableTableFeatures } from "@/lib/table"
import { ModelNotification } from "@/types" 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 { useEffect, useMemo } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { toast } from "sonner" import { toast } from "sonner"
@@ -40,7 +41,7 @@ export default function NotificationPage() {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [error]) }, [error])
const columns: ColumnDef<ModelNotification>[] = [ const columns: ColumnDef<typeof selectableTableFeatures, ModelNotification>[] = [
{ {
id: "select", id: "select",
header: ({ table }) => ( header: ({ table }) => (
@@ -60,7 +61,6 @@ export default function NotificationPage() {
aria-label="Select row" aria-label="Select row"
/> />
), ),
enableSorting: false,
enableHiding: false, enableHiding: false,
}, },
{ {
@@ -127,10 +127,10 @@ export default function NotificationPage() {
return data ?? [] return data ?? []
}, [data]) }, [data])
const table = useReactTable({ const table = useTable({
features: selectableTableFeatures,
data: dataCache, data: dataCache,
columns, columns,
getCoreRowModel: getCoreRowModel(),
}) })
const selectedRows = table.getSelectedRowModel().rows const selectedRows = table.getSelectedRowModel().rows
+5 -5
View File
@@ -22,8 +22,9 @@ import {
TableRow, TableRow,
} from "@/components/ui/table" } from "@/components/ui/table"
import { useAuth } from "@/hooks/useAuth" import { useAuth } from "@/hooks/useAuth"
import { selectableTableFeatures } from "@/lib/table"
import { ModelOnlineUser, ModelOnlineUserApi } from "@/types" 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 { useEffect, useMemo } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { useSearchParams } from "react-router-dom" import { useSearchParams } from "react-router-dom"
@@ -55,7 +56,7 @@ export default function OnlineUserPage() {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [error]) }, [error])
let columns: ColumnDef<ModelOnlineUser>[] = [ let columns: ColumnDef<typeof selectableTableFeatures, ModelOnlineUser>[] = [
{ {
id: "select", id: "select",
header: ({ table }) => ( header: ({ table }) => (
@@ -75,7 +76,6 @@ export default function OnlineUserPage() {
aria-label="Select row" aria-label="Select row"
/> />
), ),
enableSorting: false,
enableHiding: false, enableHiding: false,
}, },
{ {
@@ -128,10 +128,10 @@ export default function OnlineUserPage() {
return data?.value ?? [] return data?.value ?? []
}, [data]) }, [data])
const table = useReactTable<ModelOnlineUser>({ const table = useTable({
features: selectableTableFeatures,
data: dataCache, data: dataCache,
columns, columns,
getCoreRowModel: getCoreRowModel(),
}) })
const selectedRows = table.getSelectedRowModel().rows const selectedRows = table.getSelectedRowModel().rows
+5 -5
View File
@@ -13,8 +13,9 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table" } from "@/components/ui/table"
import { selectableTableFeatures } from "@/lib/table"
import { ModelServerGroupResponseItem } from "@/types" 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 { useEffect, useMemo } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { toast } from "sonner" import { toast } from "sonner"
@@ -37,7 +38,7 @@ export default function ServerGroupPage() {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [error]) }, [error])
const columns: ColumnDef<ModelServerGroupResponseItem>[] = [ const columns: ColumnDef<typeof selectableTableFeatures, ModelServerGroupResponseItem>[] = [
{ {
id: "select", id: "select",
header: ({ table }) => ( header: ({ table }) => (
@@ -57,7 +58,6 @@ export default function ServerGroupPage() {
aria-label="Select row" aria-label="Select row"
/> />
), ),
enableSorting: false,
enableHiding: false, enableHiding: false,
}, },
{ {
@@ -112,10 +112,10 @@ export default function ServerGroupPage() {
return data ?? [] return data ?? []
}, [data]) }, [data])
const table = useReactTable({ const table = useTable({
features: selectableTableFeatures,
data: dataCache, data: dataCache,
columns, columns,
getCoreRowModel: getCoreRowModel(),
}) })
const selectedRows = table.getSelectedRowModel().rows const selectedRows = table.getSelectedRowModel().rows
+5 -5
View File
@@ -27,9 +27,10 @@ import {
} from "@/components/ui/table" } from "@/components/ui/table"
import { IconButton } from "@/components/xui/icon-button" import { IconButton } from "@/components/xui/icon-button"
import { useServer } from "@/hooks/useServer" import { useServer } from "@/hooks/useServer"
import { selectableTableFeatures } from "@/lib/table"
import { joinIP } from "@/lib/utils" import { joinIP } from "@/lib/utils"
import { ModelServerTaskResponse, ModelServer as Server } from "@/types" 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 { useEffect, useMemo } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { toast } from "sonner" import { toast } from "sonner"
@@ -51,7 +52,7 @@ export default function ServerPage() {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [error]) }, [error])
const columns: ColumnDef<Server>[] = [ const columns: ColumnDef<typeof selectableTableFeatures, Server>[] = [
{ {
id: "select", id: "select",
header: ({ table }) => ( header: ({ table }) => (
@@ -71,7 +72,6 @@ export default function ServerPage() {
aria-label="Select row" aria-label="Select row"
/> />
), ),
enableSorting: false,
enableHiding: false, enableHiding: false,
}, },
{ {
@@ -217,10 +217,10 @@ export default function ServerPage() {
return data ?? [] return data ?? []
}, [data]) }, [data])
const table = useReactTable({ const table = useTable({
features: selectableTableFeatures,
data: dataCache, data: dataCache,
columns, columns,
getCoreRowModel: getCoreRowModel(),
}) })
const selectedRows = table.getSelectedRowModel().rows const selectedRows = table.getSelectedRowModel().rows
+5 -5
View File
@@ -12,9 +12,10 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table" } from "@/components/ui/table"
import { selectableTableFeatures } from "@/lib/table"
import { ModelService as Service } from "@/types" import { ModelService as Service } from "@/types"
import { serviceTypes } 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 { useEffect, useMemo } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { toast } from "sonner" import { toast } from "sonner"
@@ -34,7 +35,7 @@ export default function ServicePage() {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [error]) }, [error])
const columns: ColumnDef<Service>[] = [ const columns: ColumnDef<typeof selectableTableFeatures, Service>[] = [
{ {
id: "select", id: "select",
header: ({ table }) => ( header: ({ table }) => (
@@ -54,7 +55,6 @@ export default function ServicePage() {
aria-label="Select row" aria-label="Select row"
/> />
), ),
enableSorting: false,
enableHiding: false, enableHiding: false,
}, },
{ {
@@ -165,10 +165,10 @@ export default function ServicePage() {
return data ?? [] return data ?? []
}, [data]) }, [data])
const table = useReactTable({ const table = useTable({
features: selectableTableFeatures,
data: dataCache, data: dataCache,
columns, columns,
getCoreRowModel: getCoreRowModel(),
}) })
const selectedRows = table.getSelectedRowModel().rows const selectedRows = table.getSelectedRowModel().rows
+5 -5
View File
@@ -13,8 +13,9 @@ import {
TableRow, TableRow,
} from "@/components/ui/table" } from "@/components/ui/table"
import { UserCard } from "@/components/user" import { UserCard } from "@/components/user"
import { selectableTableFeatures } from "@/lib/table"
import { ModelUser } from "@/types" 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 { useEffect, useMemo } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { toast } from "sonner" import { toast } from "sonner"
@@ -34,7 +35,7 @@ export default function UserPage() {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [error]) }, [error])
const columns: ColumnDef<ModelUser>[] = [ const columns: ColumnDef<typeof selectableTableFeatures, ModelUser>[] = [
{ {
id: "select", id: "select",
header: ({ table }) => ( header: ({ table }) => (
@@ -54,7 +55,6 @@ export default function UserPage() {
aria-label="Select row" aria-label="Select row"
/> />
), ),
enableSorting: false,
enableHiding: false, enableHiding: false,
}, },
{ {
@@ -105,10 +105,10 @@ export default function UserPage() {
return data ?? [] return data ?? []
}, [data]) }, [data])
const table = useReactTable({ const table = useTable({
features: selectableTableFeatures,
data: dataCache, data: dataCache,
columns, columns,
getCoreRowModel: getCoreRowModel(),
}) })
const selectedRows = table.getSelectedRowModel().rows const selectedRows = table.getSelectedRowModel().rows
+5 -5
View File
@@ -22,13 +22,14 @@ import {
TableRow, TableRow,
} from "@/components/ui/table" } from "@/components/ui/table"
import { useAuth } from "@/hooks/useAuth" import { useAuth } from "@/hooks/useAuth"
import { selectableTableFeatures } from "@/lib/table"
import { import {
GithubComNezhahqNezhaModelValueArrayModelWAFApiMock, GithubComNezhahqNezhaModelValueArrayModelWAFApiMock,
ModelWAFApiMock, ModelWAFApiMock,
wafBlockIdentifiers, wafBlockIdentifiers,
wafBlockReasons, wafBlockReasons,
} from "@/types" } 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 { useEffect, useMemo } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { useSearchParams } from "react-router-dom" import { useSearchParams } from "react-router-dom"
@@ -61,7 +62,7 @@ export default function WAFPage() {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [error]) }, [error])
let columns: ColumnDef<ModelWAFApiMock>[] = [ let columns: ColumnDef<typeof selectableTableFeatures, ModelWAFApiMock>[] = [
{ {
id: "select", id: "select",
header: ({ table }) => ( header: ({ table }) => (
@@ -81,7 +82,6 @@ export default function WAFPage() {
aria-label="Select row" aria-label="Select row"
/> />
), ),
enableSorting: false,
enableHiding: false, enableHiding: false,
}, },
{ {
@@ -147,10 +147,10 @@ export default function WAFPage() {
return data?.value ?? [] return data?.value ?? []
}, [data]) }, [data])
const table = useReactTable({ const table = useTable({
features: selectableTableFeatures,
data: dataCache, data: dataCache,
columns, columns,
getCoreRowModel: getCoreRowModel(),
}) })
const selectedRows = table.getSelectedRowModel().rows const selectedRows = table.getSelectedRowModel().rows
+50
View File
@@ -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()
})
+5 -9
View File
@@ -1,12 +1,11 @@
import { expect } from "@playwright/test" 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 }) => { test("manual cron trigger goes through POST, not GET", async ({ adminPage: page }) => {
const created = await page.request.post("/api/v1/cron", { const created = await csrfRequest(page, "post", "/api/v1/cron", {
headers: await csrfHeaders(page),
data: { data: {
name: "e2e-cron-csrf", name: `e2e-cron-csrf-${Date.now().toString(36)}`,
task_type: 0, task_type: 0,
scheduler: "@every 1h", scheduler: "@every 1h",
command: "true", 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()})`, `GET must no longer be routable (got ${getResp.status()})`,
).toBeTruthy() ).toBeTruthy()
const postResp = await page.request.post(`/api/v1/cron/${cronID}/manual`, { const postResp = await csrfRequest(page, "post", `/api/v1/cron/${cronID}/manual`)
headers: await csrfHeaders(page),
})
expect(postResp.ok(), `POST must succeed (got ${postResp.status()})`).toBeTruthy() expect(postResp.ok(), `POST must succeed (got ${postResp.status()})`).toBeTruthy()
const body = await postResp.json() const body = await postResp.json()
expect(body.success).toBe(true) expect(body.success).toBe(true)
} finally { } finally {
await page.request.post("/api/v1/batch-delete/cron", { await csrfRequest(page, "post", "/api/v1/batch-delete/cron", {
headers: await csrfHeaders(page),
data: [cronID], data: [cronID],
}) })
} }
+13 -10
View File
@@ -1,8 +1,10 @@
import { expect } from "@playwright/test" 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", { const getResp = await page.request.get("/api/v1/file?id=1", {
failOnStatusCode: false, 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()})`, `GET /api/v1/file must no longer be routable (got ${getResp.status()})`,
).toBeTruthy() ).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, failOnStatusCode: false,
headers: await csrfHeaders(page),
}) })
expect(postResp.status()).toBe(200) expect(postResp.status()).toBe(200)
const body = await postResp.json() const body = (await postResp.json()) as {
expect( data?: { session_id?: string }
body.success, success?: boolean
"without a connected agent server the POST surfaces a Service error, but the route is reachable", }
).not.toBe(true) expect(body.success, "POST must create a file-manager session through the real Agent").toBe(
expect(body.error).toBeTruthy() true,
)
expect(body.data?.session_id).toBeTruthy()
}) })