diff --git a/server/routers/client/listClients.ts b/server/routers/client/listClients.ts index c6bc87aa..058080dc 100644 --- a/server/routers/client/listClients.ts +++ b/server/routers/client/listClients.ts @@ -119,12 +119,12 @@ const listClientsSchema = z.object({ }), query: z.string().optional(), sort_by: z - .enum(["megabytesIn", "megabytesOut"]) + .enum(["name", "megabytesIn", "megabytesOut"]) .optional() .catch(undefined) .openapi({ type: "string", - enum: ["megabytesIn", "megabytesOut"], + enum: ["name", "megabytesIn", "megabytesOut"], description: "Field to sort by" }), order: z diff --git a/server/routers/resource/listResources.ts b/server/routers/resource/listResources.ts index a26a5df5..305ba00f 100644 --- a/server/routers/resource/listResources.ts +++ b/server/routers/resource/listResources.ts @@ -19,6 +19,7 @@ import { and, asc, count, + desc, eq, inArray, isNull, @@ -63,6 +64,26 @@ const listResourcesSchema = z.object({ description: "Page number to retrieve" }), query: z.string().optional(), + sort_by: z + .enum(["name"]) + .optional() + .catch(undefined) + .openapi({ + type: "string", + enum: ["name"], + description: "Field to sort by" + }), + order: z + .enum(["asc", "desc"]) + .optional() + .default("asc") + .catch("asc") + .openapi({ + type: "string", + enum: ["asc", "desc"], + default: "asc", + description: "Sort order" + }), enabled: z .enum(["true", "false"]) .transform((v) => v === "true") @@ -229,8 +250,16 @@ export async function listResources( ) ); } - const { page, pageSize, authState, enabled, query, healthStatus } = - parsedQuery.data; + const { + page, + pageSize, + authState, + enabled, + query, + healthStatus, + sort_by, + order + } = parsedQuery.data; const parsedParams = listResourcesParamsSchema.safeParse(req.params); if (!parsedParams.success) { @@ -395,7 +424,13 @@ export async function listResources( baseQuery .limit(pageSize) .offset(pageSize * (page - 1)) - .orderBy(asc(resources.resourceId)), + .orderBy( + sort_by + ? order === "asc" + ? asc(resources[sort_by]) + : desc(resources[sort_by]) + : asc(resources.resourceId) + ), countQuery ]); diff --git a/server/routers/site/listSites.ts b/server/routers/site/listSites.ts index 14f3024d..b69f597e 100644 --- a/server/routers/site/listSites.ts +++ b/server/routers/site/listSites.ts @@ -108,12 +108,12 @@ const listSitesSchema = z.object({ }), query: z.string().optional(), sort_by: z - .enum(["megabytesIn", "megabytesOut"]) + .enum(["name", "megabytesIn", "megabytesOut"]) .optional() .catch(undefined) .openapi({ type: "string", - enum: ["megabytesIn", "megabytesOut"], + enum: ["name", "megabytesIn", "megabytesOut"], description: "Field to sort by" }), order: z diff --git a/server/routers/siteResource/listAllSiteResourcesByOrg.ts b/server/routers/siteResource/listAllSiteResourcesByOrg.ts index 5aec53c7..6693d35e 100644 --- a/server/routers/siteResource/listAllSiteResourcesByOrg.ts +++ b/server/routers/siteResource/listAllSiteResourcesByOrg.ts @@ -4,7 +4,7 @@ import logger from "@server/logger"; import { OpenAPITags, registry } from "@server/openApi"; import HttpCode from "@server/types/HttpCode"; import type { PaginatedResponse } from "@server/types/Pagination"; -import { and, asc, eq, like, or, sql } from "drizzle-orm"; +import { and, asc, desc, eq, like, or, sql } from "drizzle-orm"; import { NextFunction, Request, Response } from "express"; import createHttpError from "http-errors"; import { z } from "zod"; @@ -48,6 +48,26 @@ const listAllSiteResourcesByOrgQuerySchema = z.object({ type: "string", enum: ["host", "cidr"], description: "Filter site resources by mode" + }), + sort_by: z + .enum(["name"]) + .optional() + .catch(undefined) + .openapi({ + type: "string", + enum: ["name"], + description: "Field to sort by" + }), + order: z + .enum(["asc", "desc"]) + .optional() + .default("asc") + .catch("asc") + .openapi({ + type: "string", + enum: ["asc", "desc"], + default: "asc", + description: "Sort order" }) }); @@ -131,7 +151,8 @@ export async function listAllSiteResourcesByOrg( } const { orgId } = parsedParams.data; - const { page, pageSize, query, mode } = parsedQuery.data; + const { page, pageSize, query, mode, sort_by, order } = + parsedQuery.data; const conditions = [and(eq(siteResources.orgId, orgId))]; if (query) { @@ -179,7 +200,13 @@ export async function listAllSiteResourcesByOrg( baseQuery .limit(pageSize) .offset(pageSize * (page - 1)) - .orderBy(asc(siteResources.siteResourceId)), + .orderBy( + sort_by + ? order === "asc" + ? asc(siteResources[sort_by]) + : desc(siteResources[sort_by]) + : asc(siteResources.siteResourceId) + ), countQuery ]); diff --git a/server/routers/siteResource/listSiteResources.ts b/server/routers/siteResource/listSiteResources.ts index 6ecda7c4..5bdf6709 100644 --- a/server/routers/siteResource/listSiteResources.ts +++ b/server/routers/siteResource/listSiteResources.ts @@ -5,7 +5,7 @@ import { siteResources, sites, SiteResource } from "@server/db"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; -import { eq, and } from "drizzle-orm"; +import { and, asc, desc, eq } from "drizzle-orm"; import { fromError } from "zod-validation-error"; import logger from "@server/logger"; import { OpenAPITags, registry } from "@server/openApi"; @@ -27,7 +27,16 @@ const listSiteResourcesQuerySchema = z.object({ .optional() .default("0") .transform(Number) - .pipe(z.int().nonnegative()) + .pipe(z.int().nonnegative()), + sort_by: z + .enum(["name"]) + .optional() + .catch(undefined), + order: z + .enum(["asc", "desc"]) + .optional() + .default("asc") + .catch("asc") }); export type ListSiteResourcesResponse = { @@ -75,7 +84,7 @@ export async function listSiteResources( } const { siteId, orgId } = parsedParams.data; - const { limit, offset } = parsedQuery.data; + const { limit, offset, sort_by, order } = parsedQuery.data; // Verify the site exists and belongs to the org const site = await db @@ -98,6 +107,13 @@ export async function listSiteResources( eq(siteResources.orgId, orgId) ) ) + .orderBy( + sort_by + ? order === "asc" + ? asc(siteResources[sort_by]) + : desc(siteResources[sort_by]) + : asc(siteResources.siteResourceId) + ) .limit(limit) .offset(offset); diff --git a/src/components/ClientResourcesTable.tsx b/src/components/ClientResourcesTable.tsx index 68c72b9e..5066f273 100644 --- a/src/components/ClientResourcesTable.tsx +++ b/src/components/ClientResourcesTable.tsx @@ -15,7 +15,15 @@ import { InfoPopup } from "@app/components/ui/info-popup"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; -import { ArrowUpDown, ArrowUpRight, MoreHorizontal } from "lucide-react"; +import { getNextSortOrder, getSortDirection } from "@app/lib/sortColumn"; +import { + ArrowDown01Icon, + ArrowUp10Icon, + ArrowUpDown, + ArrowUpRight, + ChevronsUpDownIcon, + MoreHorizontal +} from "lucide-react"; import { useTranslations } from "next-intl"; import Link from "next/link"; import { useRouter } from "next/navigation"; @@ -133,7 +141,26 @@ export default function ClientResourcesTable({ accessorKey: "name", enableHiding: false, friendlyName: t("name"), - header: () => {t("name")} + header: () => { + const nameOrder = getSortDirection("name", searchParams); + const Icon = + nameOrder === "asc" + ? ArrowDown01Icon + : nameOrder === "desc" + ? ArrowUp10Icon + : ChevronsUpDownIcon; + + return ( + + ); + } }, { id: "niceId", @@ -329,6 +356,14 @@ export default function ClientResourcesTable({ }); } + function toggleSort(column: string) { + const newSearch = getNextSortOrder(column, searchParams); + + filter({ + searchParams: newSearch + }); + } + const handlePaginationChange = (newPage: PaginationState) => { searchParams.set("page", (newPage.pageIndex + 1).toString()); searchParams.set("pageSize", newPage.pageSize.toString()); diff --git a/src/components/MachineClientsTable.tsx b/src/components/MachineClientsTable.tsx index 97de4113..bd5a8e00 100644 --- a/src/components/MachineClientsTable.tsx +++ b/src/components/MachineClientsTable.tsx @@ -204,7 +204,26 @@ export default function MachineClientsTable({ accessorKey: "name", enableHiding: false, friendlyName: t("name"), - header: () => {t("name")}, + header: () => { + const nameOrder = getSortDirection("name", searchParams); + const Icon = + nameOrder === "asc" + ? ArrowDown01Icon + : nameOrder === "desc" + ? ArrowUp10Icon + : ChevronsUpDownIcon; + + return ( + + ); + }, cell: ({ row }) => { const r = row.original; return ( diff --git a/src/components/ProxyResourcesTable.tsx b/src/components/ProxyResourcesTable.tsx index 490904c7..353eddb5 100644 --- a/src/components/ProxyResourcesTable.tsx +++ b/src/components/ProxyResourcesTable.tsx @@ -14,15 +14,19 @@ import { InfoPopup } from "@app/components/ui/info-popup"; import { Switch } from "@app/components/ui/switch"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { useNavigationContext } from "@app/hooks/useNavigationContext"; +import { getNextSortOrder, getSortDirection } from "@app/lib/sortColumn"; import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { UpdateResourceResponse } from "@server/routers/resource"; import type { PaginationState } from "@tanstack/react-table"; import { AxiosResponse } from "axios"; import { + ArrowDown01Icon, ArrowRight, + ArrowUp10Icon, CheckCircle2, ChevronDown, + ChevronsUpDownIcon, Clock, MoreHorizontal, ShieldCheck, @@ -318,7 +322,26 @@ export default function ProxyResourcesTable({ accessorKey: "name", enableHiding: false, friendlyName: t("name"), - header: () => {t("name")} + header: () => { + const nameOrder = getSortDirection("name", searchParams); + const Icon = + nameOrder === "asc" + ? ArrowDown01Icon + : nameOrder === "desc" + ? ArrowUp10Icon + : ChevronsUpDownIcon; + + return ( + + ); + } }, { id: "niceId", @@ -563,6 +586,14 @@ export default function ProxyResourcesTable({ }); } + function toggleSort(column: string) { + const newSearch = getNextSortOrder(column, searchParams); + + filter({ + searchParams: newSearch + }); + } + const handlePaginationChange = (newPage: PaginationState) => { searchParams.set("page", (newPage.pageIndex + 1).toString()); searchParams.set("pageSize", newPage.pageSize.toString()); diff --git a/src/components/SitesTable.tsx b/src/components/SitesTable.tsx index c7857773..cc02e5d3 100644 --- a/src/components/SitesTable.tsx +++ b/src/components/SitesTable.tsx @@ -141,7 +141,24 @@ export default function SitesTable({ accessorKey: "name", enableHiding: false, header: () => { - return {t("name")}; + const nameOrder = getSortDirection("name", searchParams); + const Icon = + nameOrder === "asc" + ? ArrowDown01Icon + : nameOrder === "desc" + ? ArrowUp10Icon + : ChevronsUpDownIcon; + + return ( + + ); } }, {