diff --git a/server/routers/site/listSites.ts b/server/routers/site/listSites.ts index e77f6333..9c25897e 100644 --- a/server/routers/site/listSites.ts +++ b/server/routers/site/listSites.ts @@ -4,7 +4,17 @@ 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, ilike, inArray, or, sql } from "drizzle-orm"; +import { + and, + asc, + count, + desc, + eq, + ilike, + inArray, + or, + sql +} from "drizzle-orm"; import { NextFunction, Request, Response } from "express"; import createHttpError from "http-errors"; import { z } from "zod"; @@ -88,28 +98,15 @@ const listSitesSchema = z.object({ .optional() .catch(1) .default(1), - query: z.string().optional() + query: z.string().optional(), + sort_by: z + .enum(["megabytesIn", "megabytesOut"]) + .optional() + .catch(undefined), + order: z.enum(["asc", "desc"]).optional().default("asc").catch("asc") }); -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 + "%") - ) - ); - } +function querySitesBase() { return db .select({ siteId: sites.siteId, @@ -136,11 +133,10 @@ function querySites( .leftJoin( remoteExitNodes, eq(remoteExitNodes.exitNodeId, sites.exitNodeId) - ) - .where(conditions); + ); } -type SiteWithUpdateAvailable = Awaited>[0] & { +type SiteWithUpdateAvailable = Awaited>[0] & { newtUpdateAvailable?: boolean; }; @@ -176,7 +172,7 @@ export async function listSites( ) ); } - const { pageSize, page, query } = parsedQuery.data; + const { pageSize, page, query, sort_by, order } = parsedQuery.data; const parsedParams = listSitesParamsSchema.safeParse(req.params); if (!parsedParams.success) { @@ -220,7 +216,7 @@ export async function listSites( } const accessibleSiteIds = accessibleSites.map((site) => site.siteId); - const baseQuery = querySites(orgId, accessibleSiteIds, query); + const baseQuery = querySitesBase(); let conditions = and( inArray(sites.siteId, accessibleSiteIds), @@ -241,23 +237,30 @@ export async function listSites( .from(sites) .where(conditions); - const sitesList = await baseQuery + const siteListQuery = baseQuery + .where(conditions) .limit(pageSize) .offset(pageSize * (page - 1)); + + if (sort_by) { + siteListQuery.orderBy( + order === "asc" ? asc(sites[sort_by]) : desc(sites[sort_by]) + ); + } const totalCountResult = await countQuery; const totalCount = totalCountResult[0].count; // Get latest version asynchronously without blocking the response const latestNewtVersionPromise = getLatestNewtVersion(); - const sitesWithUpdates: SiteWithUpdateAvailable[] = sitesList.map( - (site) => { - const siteWithUpdate: SiteWithUpdateAvailable = { ...site }; - // Initially set to false, will be updated if version check succeeds - siteWithUpdate.newtUpdateAvailable = false; - return siteWithUpdate; - } - ); + const sitesWithUpdates: SiteWithUpdateAvailable[] = ( + await siteListQuery + ).map((site) => { + const siteWithUpdate: SiteWithUpdateAvailable = { ...site }; + // Initially set to false, will be updated if version check succeeds + siteWithUpdate.newtUpdateAvailable = false; + return siteWithUpdate; + }); // Try to get the latest version, but don't block if it fails try { diff --git a/src/components/SitesTable.tsx b/src/components/SitesTable.tsx index 68a7fc37..f99da889 100644 --- a/src/components/SitesTable.tsx +++ b/src/components/SitesTable.tsx @@ -12,15 +12,18 @@ import { } from "@app/components/ui/dropdown-menu"; import { InfoPopup } from "@app/components/ui/info-popup"; import { useEnvContext } from "@app/hooks/useEnvContext"; +import { useSortColumn } from "@app/hooks/useSortColumn"; 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, type PaginationState } from "@tanstack/react-table"; +import { type PaginationState } from "@tanstack/react-table"; import { + ArrowDown01Icon, ArrowRight, - ArrowUpDown, + ArrowUp10Icon, ArrowUpRight, + ChevronsUpDownIcon, MoreHorizontal } from "lucide-react"; import { useTranslations } from "next-intl"; @@ -71,6 +74,8 @@ export default function SitesTable({ const [selectedSite, setSelectedSite] = useState(null); const [isRefreshing, startTransition] = useTransition(); + const [getSortDirection, toggleSorting] = useSortColumn(); + const api = createApiClient(useEnvContext()); const t = useTranslations(); @@ -102,22 +107,15 @@ export default function SitesTable({ }); }; + const dataInOrder = getSortDirection("megabytesIn"); + const dataOutOrder = getSortDirection("megabytesOut"); + const columns: ExtendedColumnDef[] = [ { accessorKey: "name", enableHiding: false, - header: ({ column }) => { - return ( - - ); + header: () => { + return {t("name")}; } }, { @@ -125,18 +123,8 @@ export default function SitesTable({ accessorKey: "nice", friendlyName: t("identifier"), enableHiding: true, - header: ({ column }) => { - return ( - - ); + header: () => { + return {t("identifier")}; }, cell: ({ row }) => { return {row.original.nice || "-"}; @@ -145,18 +133,8 @@ export default function SitesTable({ { accessorKey: "online", friendlyName: t("online"), - header: ({ column }) => { - return ( - - ); + header: () => { + return {t("online")}; }, cell: ({ row }) => { const originalRow = row.original; @@ -187,16 +165,20 @@ export default function SitesTable({ { accessorKey: "mbIn", friendlyName: t("dataIn"), - header: ({ column }) => { + header: () => { + const Icon = + dataInOrder === "asc" + ? ArrowDown01Icon + : dataInOrder === "desc" + ? ArrowUp10Icon + : ChevronsUpDownIcon; return ( ); }, @@ -207,16 +189,20 @@ export default function SitesTable({ { accessorKey: "mbOut", friendlyName: t("dataOut"), - header: ({ column }) => { + header: () => { + const Icon = + dataOutOrder === "asc" + ? ArrowDown01Icon + : dataOutOrder === "desc" + ? ArrowUp10Icon + : ChevronsUpDownIcon; return ( ); }, @@ -227,18 +213,8 @@ export default function SitesTable({ { accessorKey: "type", friendlyName: t("type"), - header: ({ column }) => { - return ( - - ); + header: () => { + return {t("type")}; }, cell: ({ row }) => { const originalRow = row.original; @@ -283,18 +259,8 @@ export default function SitesTable({ { accessorKey: "exitNode", friendlyName: t("exitNode"), - header: ({ column }) => { - return ( - - ); + header: () => { + return {t("exitNode")}; }, cell: ({ row }) => { const originalRow = row.original; @@ -347,18 +313,8 @@ export default function SitesTable({ }, { accessorKey: "address", - header: ({ column }: { column: Column }) => { - return ( - - ); + header: () => { + return {t("address")}; }, cell: ({ row }: { row: any }) => { const originalRow = row.original; @@ -435,11 +391,6 @@ export default function SitesTable({ startTransition(() => router.push(`${pathname}?${sp.toString()}`)); }, 300); - console.log({ - pagination, - rowCount - }); - return ( <> {selectedSite && ( diff --git a/src/hooks/useSortColumn.ts b/src/hooks/useSortColumn.ts new file mode 100644 index 00000000..95fb673e --- /dev/null +++ b/src/hooks/useSortColumn.ts @@ -0,0 +1,56 @@ +import type { SortOrder } from "@app/lib/types/sort"; +import { useSearchParams, useRouter, usePathname } from "next/navigation"; +import { startTransition } from "react"; + +export function useSortColumn() { + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + + const toggleSorting = (column: string) => { + const sp = new URLSearchParams(searchParams); + + let nextDirection: SortOrder = "indeterminate"; + + if (sp.get("sort_by") === column) { + nextDirection = (sp.get("order") as SortOrder) ?? "indeterminate"; + } + + switch (nextDirection) { + case "indeterminate": { + nextDirection = "asc"; + break; + } + case "asc": { + nextDirection = "desc"; + break; + } + default: { + nextDirection = "indeterminate"; + break; + } + } + + sp.delete("sort_by"); + sp.delete("order"); + + if (nextDirection !== "indeterminate") { + sp.set("sort_by", column); + sp.set("order", nextDirection); + } + + startTransition(() => router.push(`${pathname}?${sp.toString()}`)); + }; + + function getSortDirection(column: string) { + let currentDirection: SortOrder = "indeterminate"; + + if (searchParams.get("sort_by") === column) { + currentDirection = + (searchParams.get("order") as SortOrder) ?? "indeterminate"; + } + return currentDirection; + } + + return [getSortDirection, toggleSorting] as const; +} diff --git a/src/lib/types/sort.ts b/src/lib/types/sort.ts new file mode 100644 index 00000000..69161f5a --- /dev/null +++ b/src/lib/types/sort.ts @@ -0,0 +1 @@ +export type SortOrder = "asc" | "desc" | "indeterminate";