diff --git a/app/routes/machines/components/machine-filters.tsx b/app/routes/machines/components/machine-filters.tsx index b008a93..f88b84a 100644 --- a/app/routes/machines/components/machine-filters.tsx +++ b/app/routes/machines/components/machine-filters.tsx @@ -1,6 +1,5 @@ import { ChevronDown, X } from "lucide-react"; import type { JSX } from "react"; -import { useSearchParams } from "react-router"; import { Menu, MenuContent, MenuItem, MenuSeparator, MenuTrigger } from "~/components/menu"; import type { User } from "~/types/User"; @@ -8,6 +7,8 @@ import cn from "~/utils/cn"; import type { PopulatedNode } from "~/utils/node-info"; import { getUserDisplayName } from "~/utils/user"; +import { useMachineFilterParams } from "../hooks/use-machine-filter-params"; + const STATUS_OPTIONS = [ { value: "online", label: "Online" }, { value: "offline", label: "Offline" }, @@ -30,7 +31,7 @@ function FilterDropdown({ options: readonly { value: string; label: string }[]; onChange: (value: string | null) => void; }): JSX.Element { - const activeOption = value !== null ? options.find((o) => o.value === value) : null; + const activeOption = options.find((o) => o.value === value) ?? null; const isActive = activeOption !== null; return ( @@ -45,7 +46,7 @@ function FilterDropdown({ : "border-mist-200 dark:border-mist-700 text-mist-700 dark:text-mist-300 hover:border-mist-300 dark:hover:border-mist-600", )} > - {isActive ? activeOption!.label : label} + {activeOption?.label ?? label} @@ -79,42 +80,16 @@ interface MachineFiltersProps { populatedNodes: PopulatedNode[]; } -/** - * Renders the filter dropdowns (User, Tag, Status, Route) and a "Clear filters" - * button as a React Fragment so they slot directly into the parent flex row. - * Filter state is stored in URL search params for bookmarkability. - */ export function MachineFilters({ users, populatedNodes }: MachineFiltersProps): JSX.Element { - const [searchParams, setSearchParams] = useSearchParams(); - - const filterUser = searchParams.get("user"); - const filterTag = searchParams.get("tag"); - const filterStatus = searchParams.get("status"); - const filterRoute = searchParams.get("route"); - - const hasActiveFilters = - filterUser !== null || filterTag !== null || filterStatus !== null || filterRoute !== null; - - const setParam = (key: string, value: string | null) => { - setSearchParams((prev) => { - const next = new URLSearchParams(prev); - if (value === null) { - next.delete(key); - } else { - next.set(key, value); - } - return next; - }); - }; - - const clearFilters = () => { - setSearchParams((prev) => { - const next = new URLSearchParams(); - const q = prev.get("q"); - if (q) next.set("q", q); - return next; - }); - }; + const { + filterUser, + filterTag, + filterStatus, + filterRoute, + hasActiveFilters, + setParam, + clearFilters, + } = useMachineFilterParams(); const tagOwnedExists = populatedNodes.some((n) => !n.user); const userOptions = [ diff --git a/app/routes/machines/hooks/use-machine-filter-params.ts b/app/routes/machines/hooks/use-machine-filter-params.ts new file mode 100644 index 0000000..0f16a6b --- /dev/null +++ b/app/routes/machines/hooks/use-machine-filter-params.ts @@ -0,0 +1,51 @@ +import { useSearchParams } from "react-router"; + +export interface MachineFilterParams { + filterUser: string | null; + filterTag: string | null; + filterStatus: "online" | "offline" | "expired" | null; + filterRoute: "exit-node" | "subnet" | null; + hasActiveFilters: boolean; + setParam: (key: string, value: string | null) => void; + clearFilters: () => void; +} + +export function useMachineFilterParams(): MachineFilterParams { + const [searchParams, setSearchParams] = useSearchParams(); + + const filterUser = searchParams.get("user"); + const filterTag = searchParams.get("tag"); + const filterStatus = searchParams.get("status") as MachineFilterParams["filterStatus"]; + const filterRoute = searchParams.get("route") as MachineFilterParams["filterRoute"]; + + const hasActiveFilters = + filterUser !== null || filterTag !== null || filterStatus !== null || filterRoute !== null; + + const setParam = (key: string, value: string | null) => { + setSearchParams((prev) => { + const next = new URLSearchParams(prev); + if (value === null) next.delete(key); + else next.set(key, value); + return next; + }); + }; + + const clearFilters = () => { + setSearchParams((prev) => { + const next = new URLSearchParams(); + const q = prev.get("q"); + if (q) next.set("q", q); + return next; + }); + }; + + return { + filterUser, + filterTag, + filterStatus, + filterRoute, + hasActiveFilters, + setParam, + clearFilters, + }; +} diff --git a/app/routes/machines/overview.tsx b/app/routes/machines/overview.tsx index 35378ac..adb92bb 100644 --- a/app/routes/machines/overview.tsx +++ b/app/routes/machines/overview.tsx @@ -10,13 +10,13 @@ import Tooltip from "~/components/tooltip"; import { nodesResource, usersResource } from "~/server/headscale/live-store"; import { Capabilities } from "~/server/web/roles"; import cn from "~/utils/cn"; -import { mapNodes, sortNodeTags } from "~/utils/node-info"; -import type { PopulatedNode } from "~/utils/node-info"; +import { mapNodes, sortNodeTags, type PopulatedNode } from "~/utils/node-info"; import type { Route } from "./+types/overview"; import { MachineFilters } from "./components/machine-filters"; import MachineRow from "./components/machine-row"; import NewMachine from "./dialogs/new"; +import { useMachineFilterParams } from "./hooks/use-machine-filter-params"; import { machineAction } from "./machine-actions"; export async function loader({ request, context }: Route.LoaderArgs) { @@ -89,13 +89,8 @@ export default function Page({ loaderData }: Route.ComponentProps) { const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc"); const searchQuery = searchParams.get("q") ?? ""; - const filterUser = searchParams.get("user"); - const filterTag = searchParams.get("tag"); - const filterStatus = searchParams.get("status") as "online" | "offline" | "expired" | null; - const filterRoute = searchParams.get("route") as "exit-node" | "subnet" | null; - - const hasActiveFilters = - filterUser !== null || filterTag !== null || filterStatus !== null || filterRoute !== null; + const { filterUser, filterTag, filterStatus, filterRoute, hasActiveFilters } = + useMachineFilterParams(); const setSearchQuery = (value: string) => { setSearchParams((prev) => {