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) => {