mirror of
https://github.com/tale/headplane.git
synced 2026-08-21 02:06:37 +00:00
feat: implement custom hook for machine filter parameters management
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
import { ChevronDown, X } from "lucide-react";
|
import { ChevronDown, X } from "lucide-react";
|
||||||
import type { JSX } from "react";
|
import type { JSX } from "react";
|
||||||
import { useSearchParams } from "react-router";
|
|
||||||
|
|
||||||
import { Menu, MenuContent, MenuItem, MenuSeparator, MenuTrigger } from "~/components/menu";
|
import { Menu, MenuContent, MenuItem, MenuSeparator, MenuTrigger } from "~/components/menu";
|
||||||
import type { User } from "~/types/User";
|
import type { User } from "~/types/User";
|
||||||
@@ -8,6 +7,8 @@ import cn from "~/utils/cn";
|
|||||||
import type { PopulatedNode } from "~/utils/node-info";
|
import type { PopulatedNode } from "~/utils/node-info";
|
||||||
import { getUserDisplayName } from "~/utils/user";
|
import { getUserDisplayName } from "~/utils/user";
|
||||||
|
|
||||||
|
import { useMachineFilterParams } from "../hooks/use-machine-filter-params";
|
||||||
|
|
||||||
const STATUS_OPTIONS = [
|
const STATUS_OPTIONS = [
|
||||||
{ value: "online", label: "Online" },
|
{ value: "online", label: "Online" },
|
||||||
{ value: "offline", label: "Offline" },
|
{ value: "offline", label: "Offline" },
|
||||||
@@ -30,7 +31,7 @@ function FilterDropdown({
|
|||||||
options: readonly { value: string; label: string }[];
|
options: readonly { value: string; label: string }[];
|
||||||
onChange: (value: string | null) => void;
|
onChange: (value: string | null) => void;
|
||||||
}): JSX.Element {
|
}): 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;
|
const isActive = activeOption !== null;
|
||||||
|
|
||||||
return (
|
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",
|
: "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}
|
||||||
<ChevronDown className="h-3.5 w-3.5" />
|
<ChevronDown className="h-3.5 w-3.5" />
|
||||||
</MenuTrigger>
|
</MenuTrigger>
|
||||||
<MenuContent>
|
<MenuContent>
|
||||||
@@ -79,42 +80,16 @@ interface MachineFiltersProps {
|
|||||||
populatedNodes: PopulatedNode[];
|
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 {
|
export function MachineFilters({ users, populatedNodes }: MachineFiltersProps): JSX.Element {
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const {
|
||||||
|
filterUser,
|
||||||
const filterUser = searchParams.get("user");
|
filterTag,
|
||||||
const filterTag = searchParams.get("tag");
|
filterStatus,
|
||||||
const filterStatus = searchParams.get("status");
|
filterRoute,
|
||||||
const filterRoute = searchParams.get("route");
|
hasActiveFilters,
|
||||||
|
setParam,
|
||||||
const hasActiveFilters =
|
clearFilters,
|
||||||
filterUser !== null || filterTag !== null || filterStatus !== null || filterRoute !== null;
|
} = useMachineFilterParams();
|
||||||
|
|
||||||
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 tagOwnedExists = populatedNodes.some((n) => !n.user);
|
const tagOwnedExists = populatedNodes.some((n) => !n.user);
|
||||||
const userOptions = [
|
const userOptions = [
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -10,13 +10,13 @@ import Tooltip from "~/components/tooltip";
|
|||||||
import { nodesResource, usersResource } from "~/server/headscale/live-store";
|
import { nodesResource, usersResource } from "~/server/headscale/live-store";
|
||||||
import { Capabilities } from "~/server/web/roles";
|
import { Capabilities } from "~/server/web/roles";
|
||||||
import cn from "~/utils/cn";
|
import cn from "~/utils/cn";
|
||||||
import { mapNodes, sortNodeTags } from "~/utils/node-info";
|
import { mapNodes, sortNodeTags, type PopulatedNode } from "~/utils/node-info";
|
||||||
import type { PopulatedNode } from "~/utils/node-info";
|
|
||||||
|
|
||||||
import type { Route } from "./+types/overview";
|
import type { Route } from "./+types/overview";
|
||||||
import { MachineFilters } from "./components/machine-filters";
|
import { MachineFilters } from "./components/machine-filters";
|
||||||
import MachineRow from "./components/machine-row";
|
import MachineRow from "./components/machine-row";
|
||||||
import NewMachine from "./dialogs/new";
|
import NewMachine from "./dialogs/new";
|
||||||
|
import { useMachineFilterParams } from "./hooks/use-machine-filter-params";
|
||||||
import { machineAction } from "./machine-actions";
|
import { machineAction } from "./machine-actions";
|
||||||
|
|
||||||
export async function loader({ request, context }: Route.LoaderArgs) {
|
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 [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc");
|
||||||
|
|
||||||
const searchQuery = searchParams.get("q") ?? "";
|
const searchQuery = searchParams.get("q") ?? "";
|
||||||
const filterUser = searchParams.get("user");
|
const { filterUser, filterTag, filterStatus, filterRoute, hasActiveFilters } =
|
||||||
const filterTag = searchParams.get("tag");
|
useMachineFilterParams();
|
||||||
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 setSearchQuery = (value: string) => {
|
const setSearchQuery = (value: string) => {
|
||||||
setSearchParams((prev) => {
|
setSearchParams((prev) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user