From b04385a3404267ae6b2cbc30b87623a0a0cb13b8 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 29 Jan 2026 05:48:41 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A7=20search=20on=20table?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 14 ++++++++++++- package.json | 3 ++- server/routers/site/listSites.ts | 36 ++++++++++++++++++++++---------- src/components/SitesTable.tsx | 34 +++++++++++++++++------------- src/components/ui/data-table.tsx | 7 ++++--- 5 files changed, 64 insertions(+), 30 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4a01c8c5..af864a57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -104,6 +104,7 @@ "tailwind-merge": "3.4.0", "topojson-client": "3.1.0", "tw-animate-css": "1.4.0", + "use-debounce": "^10.1.0", "uuid": "13.0.0", "vaul": "1.1.2", "visionscarto-world-atlas": "1.0.0", @@ -13944,7 +13945,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -23240,6 +23240,18 @@ } } }, + "node_modules/use-debounce": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.1.0.tgz", + "integrity": "sha512-lu87Za35V3n/MyMoEpD5zJv0k7hCn0p+V/fK2kWD+3k2u3kOCwO593UArbczg1fhfs2rqPEnHpULJ3KmGdDzvg==", + "license": "MIT", + "engines": { + "node": ">= 16.0.0" + }, + "peerDependencies": { + "react": "*" + } + }, "node_modules/use-intl": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.7.0.tgz", diff --git a/package.json b/package.json index 25d94c4d..5de7629d 100644 --- a/package.json +++ b/package.json @@ -128,6 +128,7 @@ "tailwind-merge": "3.4.0", "topojson-client": "3.1.0", "tw-animate-css": "1.4.0", + "use-debounce": "^10.1.0", "uuid": "13.0.0", "vaul": "1.1.2", "visionscarto-world-atlas": "1.0.0", @@ -152,6 +153,7 @@ "@types/express": "5.0.6", "@types/express-session": "1.18.2", "@types/jmespath": "0.15.2", + "@types/js-yaml": "4.0.9", "@types/jsonwebtoken": "9.0.10", "@types/node": "24.10.2", "@types/nodemailer": "7.0.4", @@ -164,7 +166,6 @@ "@types/topojson-client": "3.1.5", "@types/ws": "8.18.1", "@types/yargs": "17.0.35", - "@types/js-yaml": "4.0.9", "babel-plugin-react-compiler": "1.0.0", "drizzle-kit": "0.31.8", "esbuild": "0.27.2", diff --git a/server/routers/site/listSites.ts b/server/routers/site/listSites.ts index dab79c8d..cefaeaf3 100644 --- a/server/routers/site/listSites.ts +++ b/server/routers/site/listSites.ts @@ -4,7 +4,7 @@ import { remoteExitNodes } from "@server/db"; import logger from "@server/logger"; import HttpCode from "@server/types/HttpCode"; import response from "@server/lib/response"; -import { and, count, eq, inArray, or, sql } from "drizzle-orm"; +import { and, count, eq, ilike, inArray, or, sql } from "drizzle-orm"; import { NextFunction, Request, Response } from "express"; import createHttpError from "http-errors"; import { z } from "zod"; @@ -87,10 +87,29 @@ const listSitesSchema = z.object({ .min(0) .optional() .catch(1) - .default(1) + .default(1), + query: z.string().optional() }); -function querySites(orgId: string, accessibleSiteIds: number[]) { +function querySites( + orgId: string, + accessibleSiteIds: number[], + query: string = "" +) { + let conditions = and( + inArray(sites.siteId, accessibleSiteIds), + eq(sites.orgId, orgId) + ); + + if (query) { + conditions = and( + conditions, + or( + ilike(sites.name, "%" + query + "%"), + ilike(sites.niceId, "%" + query + "%") + ) + ); + } return db .select({ siteId: sites.siteId, @@ -118,12 +137,7 @@ function querySites(orgId: string, accessibleSiteIds: number[]) { remoteExitNodes, eq(remoteExitNodes.exitNodeId, sites.exitNodeId) ) - .where( - and( - inArray(sites.siteId, accessibleSiteIds), - eq(sites.orgId, orgId) - ) - ); + .where(conditions); } type SiteWithUpdateAvailable = Awaited>[0] & { @@ -162,7 +176,7 @@ export async function listSites( ) ); } - const { pageSize, page } = parsedQuery.data; + const { pageSize, page, query } = parsedQuery.data; const parsedParams = listSitesParamsSchema.safeParse(req.params); if (!parsedParams.success) { @@ -206,7 +220,7 @@ export async function listSites( } const accessibleSiteIds = accessibleSites.map((site) => site.siteId); - const baseQuery = querySites(orgId, accessibleSiteIds); + const baseQuery = querySites(orgId, accessibleSiteIds, query); const countQuery = db .select({ count: count() }) diff --git a/src/components/SitesTable.tsx b/src/components/SitesTable.tsx index 77698a2e..5cbc92f6 100644 --- a/src/components/SitesTable.tsx +++ b/src/components/SitesTable.tsx @@ -21,7 +21,7 @@ import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { parseDataSize } from "@app/lib/dataSize"; import { build } from "@server/build"; -import { Column } from "@tanstack/react-table"; +import { Column, type PaginationState } from "@tanstack/react-table"; import { ArrowRight, ArrowUpDown, @@ -31,7 +31,8 @@ import { import { useTranslations } from "next-intl"; import Link from "next/link"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; -import { useEffect, useState, useTransition } from "react"; +import { useState, useTransition } from "react"; +import { useDebouncedCallback } from "use-debounce"; export type SiteRow = { id: number; @@ -419,10 +420,20 @@ export default function SitesTable({ } ]; - console.log({ - sites, - pagination - }); + const handlePaginationChange = (newPage: PaginationState) => { + const sp = new URLSearchParams(searchParams); + sp.set("page", (newPage.pageIndex + 1).toString()); + sp.set("pageSize", newPage.pageSize.toString()); + startTransition(() => router.push(`${pathname}?${sp.toString()}`)); + }; + + // const = useDebouncedCallback() + + const handleSearchChange = useDebouncedCallback((query: string) => { + const sp = new URLSearchParams(searchParams); + sp.set("query", query); + startTransition(() => router.push(`${pathname}?${sp.toString()}`)); + }, 300); return ( <> @@ -456,15 +467,10 @@ export default function SitesTable({ searchPlaceholder={t("searchSitesProgress")} manualFiltering pagination={pagination} - onPaginationChange={(newPage) => { - const sp = new URLSearchParams(searchParams); - sp.set("page", (newPage.pageIndex + 1).toString()); - sp.set("pageSize", newPage.pageSize.toString()); - startTransition(() => - router.push(`${pathname}?${sp.toString()}`) - ); - }} + onPaginationChange={handlePaginationChange} onAdd={() => router.push(`/${orgId}/settings/sites/create`)} + searchQuery={searchParams.get("query")?.toString()} + onSearch={handleSearchChange} addButtonText={t("siteAdd")} onRefresh={() => startTransition(refreshData)} isRefreshing={isRefreshing} diff --git a/src/components/ui/data-table.tsx b/src/components/ui/data-table.tsx index bb350577..63e75f93 100644 --- a/src/components/ui/data-table.tsx +++ b/src/components/ui/data-table.tsx @@ -189,7 +189,7 @@ type DataTableProps = { enableColumnVisibility?: boolean; manualFiltering?: boolean; onSearch?: (input: string) => void; - searchValue?: string; + searchQuery?: string; pagination?: DataTablePaginationState; onPaginationChange?: DataTablePaginationUpdateFn; persistColumnVisibility?: boolean | string; @@ -221,7 +221,7 @@ export function DataTable({ pagination: paginationState, stickyLeftColumn, onSearch, - searchValue, + searchQuery, onPaginationChange, stickyRightColumn }: DataTableProps) { @@ -508,7 +508,8 @@ export function DataTable({
{ onSearch ? onSearch(e.currentTarget.value)