From 0593cc659c44063d70fc2e26ca3028c184d262ca Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Tue, 14 Jul 2026 00:14:59 -0700 Subject: [PATCH] UI: useDebouncedTermSearch hook --- ui/src/lib/hooks.ts | 25 ++++++++++++++++++++++++- ui/src/pages/containers.tsx | 21 +++++++-------------- ui/src/pages/dashboard/tables.tsx | 19 ++++++++----------- ui/src/pages/resources.tsx | 18 ++++++------------ ui/src/pages/schedules.tsx | 26 +++++++++++--------------- ui/src/pages/terminals/index.tsx | 27 ++++++++------------------- ui/src/resources/multi-selector.tsx | 17 +++-------------- 7 files changed, 67 insertions(+), 86 deletions(-) diff --git a/ui/src/lib/hooks.ts b/ui/src/lib/hooks.ts index ba44aea81..68ef41ecc 100644 --- a/ui/src/lib/hooks.ts +++ b/ui/src/lib/hooks.ts @@ -13,7 +13,7 @@ import { useQuery, useQueryClient, } from "@tanstack/react-query"; -import { useEffect, useMemo } from "react"; +import { useEffect, useMemo, useState } from "react"; import { atom, useAtom } from "jotai"; import { atomFamily } from "jotai-family"; import { atomWithHash } from "jotai-location"; @@ -25,6 +25,7 @@ import { termMatchesTypeKeyword, } from "@/lib/utils"; import { notifications } from "@mantine/notifications"; +import { useDebounce } from "mogh_ui"; export function komodo_client() { return KomodoClient(KOMODO_BASE_URL, { @@ -182,6 +183,28 @@ export function atomWithStorage(key: string, init: T) { ); } +export function useDebouncedTermSearch(props?: { + debounce?: number; + onUpdate?: () => void; +}) { + const { debounce = 200, onUpdate } = props ?? {}; + const [search, setSearch] = useState(""); + const debouncedSearch = useDebounce(search, debounce); + const terms = useMemo(() => { + onUpdate?.(); + return debouncedSearch + .toLowerCase() + .split(" ") + .map((term) => term.trim()) + .filter((term) => term); + }, [debouncedSearch]); + return { + search, + setSearch, + terms, + }; +} + export function useResourceParamType() { const type = useParams().type; if (!type) return undefined; diff --git a/ui/src/pages/containers.tsx b/ui/src/pages/containers.tsx index 13482a13d..34a4928d8 100644 --- a/ui/src/pages/containers.tsx +++ b/ui/src/pages/containers.tsx @@ -1,11 +1,11 @@ import ContainerPorts from "@/components/docker/container-ports"; import DockerResourceLink from "@/components/docker/link"; import { containerStateIntention } from "@/lib/color"; -import { useRead, useTagsFilter } from "@/lib/hooks"; +import { useDebouncedTermSearch, useRead, useTagsFilter } from "@/lib/hooks"; import { keepPreviousData } from "@tanstack/react-query"; import { Types } from "komodo_client"; import { ICONS } from "@/lib/icons"; -import { DataTable, SortableHeader, useDebounce } from "mogh_ui"; +import { DataTable, SortableHeader } from "mogh_ui"; import { Page } from "mogh_ui"; import { StatusBadge } from "mogh_ui"; import { Group, Stack } from "@mantine/core"; @@ -23,17 +23,10 @@ export default function Containers() { const [selectedServers, setSelectedServers] = useState([]); const [page, setPage] = useState(0); - const [search, setSearch] = useState(""); - const debouncedSearch = useDebounce(search, 200); - const searchTerms = useMemo(() => { - // back to first page if search changes - setPage(0); - return debouncedSearch - .toLowerCase() - .split(" ") - .map((term) => term.trim()) - .filter((term) => term); - }, [debouncedSearch]); + + const { search, setSearch, terms } = useDebouncedTermSearch({ + onUpdate: () => setPage(0), + }); const tags = useTagsFilter(); @@ -53,7 +46,7 @@ export default function Containers() { useRead( "ListAllContainers", { - terms: searchTerms, + terms, servers: selectedServers, tags, page, diff --git a/ui/src/pages/dashboard/tables.tsx b/ui/src/pages/dashboard/tables.tsx index 9db5335ac..0bb5ae378 100644 --- a/ui/src/pages/dashboard/tables.tsx +++ b/ui/src/pages/dashboard/tables.tsx @@ -1,6 +1,11 @@ import TagsFilter from "@/components/tags/filter"; import { keepPreviousData } from "@tanstack/react-query"; -import { useRead, useTagsFilter, useTemplatesQueryBehavior } from "@/lib/hooks"; +import { + useDebouncedTermSearch, + useRead, + useTagsFilter, + useTemplatesQueryBehavior, +} from "@/lib/hooks"; import { usableResourcePath } from "@/lib/utils"; import { RequiredResourceComponents, @@ -8,7 +13,7 @@ import { UsableResource, } from "@/resources"; import { ICONS } from "@/lib/icons"; -import { Section, useDebounce } from "mogh_ui"; +import { Section } from "mogh_ui"; import { Group, Stack, Text } from "@mantine/core"; import { useEffect, useMemo, useState } from "react"; import ListPagination from "@/components/list-pagination"; @@ -18,15 +23,7 @@ import { ShowHideButton } from "mogh_ui"; import { SearchInput } from "mogh_ui"; export default function DashboardTables() { - const [search, setSearch] = useState(""); - const debouncedSearch = useDebounce(search, 200); - const terms = useMemo(() => { - return debouncedSearch - .toLowerCase() - .split(" ") - .map((term) => term.trim()) - .filter((term) => term); - }, [debouncedSearch]); + const { search, setSearch, terms } = useDebouncedTermSearch(); const Tables = useMemo( () => diff --git a/ui/src/pages/resources.tsx b/ui/src/pages/resources.tsx index 47e362d41..45e51678d 100644 --- a/ui/src/pages/resources.tsx +++ b/ui/src/pages/resources.tsx @@ -1,5 +1,6 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { + useDebouncedTermSearch, useFilterByUpdateAvailable, useRead, useResourceParamType, @@ -10,7 +11,7 @@ import { } from "@/lib/hooks"; import { ResourceComponents, UsableResource } from "@/resources"; import { Types } from "komodo_client"; -import { Page, useDebounce } from "mogh_ui"; +import { Page } from "mogh_ui"; import { Group, Stack } from "@mantine/core"; import ListPagination from "@/components/list-pagination"; import { TableSkeleton } from "mogh_ui"; @@ -34,17 +35,10 @@ export default function Resources({ _type }: { _type?: UsableResource }) { useSetTitle(name + "s"); const [page, setPage] = useState(0); - const [search, setSearch] = useState(""); - const debouncedSearch = useDebounce(search, 200); - const terms = useMemo(() => { - // Set to page 0 whenever search changes - setPage(0); - return debouncedSearch - .toLowerCase() - .split(" ") - .map((term) => term.trim()) - .filter((term) => term); - }, [debouncedSearch]); + + const { search, setSearch, terms } = useDebouncedTermSearch({ + onUpdate: () => setPage(0), + }); const [filterUpdateAvailable, toggleFilterUpdateAvailable] = useFilterByUpdateAvailable(); diff --git a/ui/src/pages/schedules.tsx b/ui/src/pages/schedules.tsx index 4ded5e1c3..abc05fe87 100644 --- a/ui/src/pages/schedules.tsx +++ b/ui/src/pages/schedules.tsx @@ -2,42 +2,38 @@ import TagsFilter from "@/components/tags/filter"; import TableTags from "@/components/tags/table"; import ListPagination from "@/components/list-pagination"; import { + useDebouncedTermSearch, usePermissions, useRead, useSetTitle, - useTags, + useTagsFilter, useWrite, } from "@/lib/hooks"; import { UsableResource } from "@/resources"; import ResourceLink from "@/resources/link"; import { ICONS } from "@/lib/icons"; -import { DataTable, SortableHeader, useDebounce } from "mogh_ui"; +import { DataTable, SortableHeader } from "mogh_ui"; import { Page } from "mogh_ui"; import { SearchInput } from "mogh_ui"; import { Group, Stack, Switch } from "@mantine/core"; import { notifications } from "@mantine/notifications"; import { keepPreviousData } from "@tanstack/react-query"; import { Types } from "komodo_client"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useState } from "react"; const SCHEDULE_SORT_KEYS = Object.values(Types.ScheduleSortBy); export default function Schedules() { useSetTitle("Schedules"); - const [search, setSearch] = useState(""); - const debouncedSearch = useDebounce(search, 200); - const terms = useMemo( - () => - debouncedSearch - .toLowerCase() - .split(" ") - .map((term) => term.trim()) - .filter((term) => term), - [debouncedSearch], - ); - const { tags } = useTags(); const [page, setPage] = useState(0); + + const { search, setSearch, terms } = useDebouncedTermSearch({ + onUpdate: () => setPage(0), + }); + + const tags = useTagsFilter(); + // Server side sort, passed up from the table. const [sort, setSort] = useState<{ sort_by?: Types.ScheduleSortBy; diff --git a/ui/src/pages/terminals/index.tsx b/ui/src/pages/terminals/index.tsx index 487cf5e8c..fab32f949 100644 --- a/ui/src/pages/terminals/index.tsx +++ b/ui/src/pages/terminals/index.tsx @@ -1,19 +1,14 @@ import TerminalTargetLink from "@/pages/terminals/target-link"; import ListPagination from "@/components/list-pagination"; -import { useRead, useSetTitle } from "@/lib/hooks"; +import { useDebouncedTermSearch, useRead, useSetTitle } from "@/lib/hooks"; import { ICONS } from "@/lib/icons"; import { terminalLink } from "@/lib/utils"; -import { - DataTable, - fmtDateWithMinutes, - SortableHeader, - useDebounce, -} from "mogh_ui"; +import { DataTable, fmtDateWithMinutes, SortableHeader } from "mogh_ui"; import { Page } from "mogh_ui"; import { Group, Stack, Text } from "@mantine/core"; import { keepPreviousData } from "@tanstack/react-query"; import { Types } from "komodo_client"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useState } from "react"; import { Link } from "react-router-dom"; import DeleteTerminal from "./delete"; import BatchDeleteAllTerminals from "./batch-delete"; @@ -24,19 +19,13 @@ const TERMINAL_SORT_KEYS = Object.values(Types.TerminalSortBy); export default function Terminals() { useSetTitle("Terminals"); - const [search, setSearch] = useState(""); - const debouncedSearch = useDebounce(search, 200); - const terms = useMemo( - () => - debouncedSearch - .toLowerCase() - .split(" ") - .map((term) => term.trim()) - .filter((term) => term), - [debouncedSearch], - ); const [page, setPage] = useState(0); + + const { search, setSearch, terms } = useDebouncedTermSearch({ + onUpdate: () => setPage(0), + }); + // Server side sort, passed up from the table. const [sort, setSort] = useState<{ sort_by?: Types.TerminalSortBy; diff --git a/ui/src/resources/multi-selector.tsx b/ui/src/resources/multi-selector.tsx index bad14a4ae..7e815be63 100644 --- a/ui/src/resources/multi-selector.tsx +++ b/ui/src/resources/multi-selector.tsx @@ -1,9 +1,8 @@ -import { useRead } from "@/lib/hooks"; +import { useDebouncedTermSearch, useRead } from "@/lib/hooks"; import { keepPreviousData } from "@tanstack/react-query"; import { MultiSelect } from "@mantine/core"; import { Types } from "komodo_client"; -import { useDebounce } from "mogh_ui"; -import { useMemo, useState } from "react"; +import { useMemo } from "react"; import { UsableResource } from "."; import { fmtResourceType } from "@/lib/formatting"; @@ -25,17 +24,7 @@ export default function ResourceMultiSelector({ templates?: Types.TemplatesQueryBehavior; placeholder?: string; }) { - const [search, setSearch] = useState(""); - const debouncedSearch = useDebounce(search, 200); - const terms = useMemo( - () => - debouncedSearch - .toLowerCase() - .split(" ") - .map((term) => term.trim()) - .filter((term) => term), - [debouncedSearch], - ); + const { search, setSearch, terms } = useDebouncedTermSearch(); const resources = useRead(