diff --git a/server/routers/client/listClients.ts b/server/routers/client/listClients.ts index 220f845f4..84abb95f4 100644 --- a/server/routers/client/listClients.ts +++ b/server/routers/client/listClients.ts @@ -118,7 +118,27 @@ const listClientsSchema = z.object({ description: "Filter by client status. Can be a comma-separated list of values. Defaults to 'active'." }) - ) + ), + labels: z + .preprocess((val) => { + if (val === undefined || val === null || val === "") { + return undefined; + } + if (Array.isArray(val)) { + return val; + } + // the array is returned as this + if (typeof val === "string") { + return val.split(","); + } + return undefined; + }, z.array(z.string())) + .optional() + .catch([]) + .openapi({ + type: "array", + description: "Filter by client labels" + }) }); function queryClientsBase() { @@ -210,8 +230,16 @@ export async function listClients( ) ); } - const { page, pageSize, online, query, status, sort_by, order } = - parsedQuery.data; + const { + page, + pageSize, + online, + query, + status, + sort_by, + order, + labels: labelFilter + } = parsedQuery.data; const parsedParams = listClientsParamsSchema.safeParse(req.params); if (!parsedParams.success) { @@ -298,6 +326,22 @@ export async function listClients( conditions.push(or(...filterAggregates)); } + if (isLabelFeatureEnabled && labelFilter && labelFilter.length > 0) { + conditions.push( + inArray( + clients.clientId, + db + .select({ id: clientLabels.clientId }) + .from(clientLabels) + .innerJoin( + labels, + eq(labels.labelId, clientLabels.labelId) + ) + .where(inArray(labels.name, labelFilter)) + ) + ); + } + if (query) { const q = "%" + query.toLowerCase() + "%"; const queryList = [ diff --git a/src/components/LabelColumnFilterButton.tsx b/src/components/LabelColumnFilterButton.tsx index 91b8faa92..ff55e3ebd 100644 --- a/src/components/LabelColumnFilterButton.tsx +++ b/src/components/LabelColumnFilterButton.tsx @@ -94,91 +94,94 @@ export function LabelColumnFilterButton({ } return ( - - - - - - - - - {t("labelsNotFound")} - - {selectedValues.length > 0 && ( - { - onSelectedValuesChange([]); - setOpen(false); - }} - className="text-muted-foreground" + > +
+ {label} + + {summary && ( + - {t("accessLabelFilterClear")} - + {summary} + )} - {labels.map((label) => ( - { - toggle(label.name); - }} - className="flex items-center gap-2" - > - -
+ + + + + + + {t("labelsNotFound")} + + {selectedValues.length > 0 && ( + { + onSelectedValuesChange([]); + setOpen(false); }} - /> - {label.name} - - ))} - - - - - + className="text-muted-foreground" + > + {t("accessLabelFilterClear")} + + )} + {labels.map((label) => ( + { + toggle(label.name); + }} + className="flex items-center gap-2" + > + +
+ {label.name} + + ))} + + + + + +
); } diff --git a/src/components/MachineClientsTable.tsx b/src/components/MachineClientsTable.tsx index 8e113f340..17e6a2e3d 100644 --- a/src/components/MachineClientsTable.tsx +++ b/src/components/MachineClientsTable.tsx @@ -2,7 +2,7 @@ import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import { Button } from "@app/components/ui/button"; -import { DataTable, ExtendedColumnDef } from "@app/components/ui/data-table"; +import { ExtendedColumnDef } from "@app/components/ui/data-table"; import { DropdownMenu, DropdownMenuContent, @@ -10,19 +10,21 @@ import { DropdownMenuTrigger } from "@app/components/ui/dropdown-menu"; import { useEnvContext } from "@app/hooks/useEnvContext"; +import { useNavigationContext } from "@app/hooks/useNavigationContext"; import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { cn } from "@app/lib/cn"; +import { getNextSortOrder, getSortDirection } from "@app/lib/sortColumn"; import { tierMatrix } from "@server/lib/billing/tierMatrix"; +import type { PaginationState } from "@tanstack/react-table"; import { - ArrowRight, - ArrowUpDown, - MoreHorizontal, - CircleSlash, ArrowDown01Icon, + ArrowRight, ArrowUp10Icon, ChevronsUpDownIcon, + CircleSlash, + MoreHorizontal, PlusIcon } from "lucide-react"; import { useTranslations } from "next-intl"; @@ -35,21 +37,15 @@ import { useState, useTransition } from "react"; -import { LabelBadge } from "./label-badge"; -import { LabelsSelector, type SelectedLabel } from "./labels-selector"; -import { - Popover, - PopoverContent, - PopoverTrigger -} from "./ui/popover"; -import { Badge } from "./ui/badge"; -import type { PaginationState } from "@tanstack/react-table"; -import { ControlledDataTable } from "./ui/controlled-data-table"; -import { useNavigationContext } from "@app/hooks/useNavigationContext"; import { useDebouncedCallback } from "use-debounce"; import z from "zod"; -import { getNextSortOrder, getSortDirection } from "@app/lib/sortColumn"; import { ColumnFilterButton } from "./ColumnFilterButton"; +import { LabelBadge } from "./label-badge"; +import { LabelsSelector, type SelectedLabel } from "./labels-selector"; +import { Badge } from "./ui/badge"; +import { ControlledDataTable } from "./ui/controlled-data-table"; +import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; +import { LabelColumnFilterButton } from "./LabelColumnFilterButton"; export type ClientRow = { id: number; @@ -415,9 +411,15 @@ export default function MachineClientsTable({ id: "labels", accessorKey: "labels", header: () => ( - - {t("labels")} - + + handleFilterChange("labels", value) + } + label={t("labels")} + className="p-3" + /> ), cell: ({ row }: { row: { original: ClientRow } }) => ( { try { if (action === "attach") {