From 2d5cdadcd5f9333415ac1411e1cf6c5df2046ec5 Mon Sep 17 00:00:00 2001 From: saadi Date: Mon, 21 Apr 2025 09:58:35 -0700 Subject: [PATCH 1/4] Add a custom react hook to enable fuzzy searching in item lists --- apps/webapp/app/hooks/useFuzzyFilter.ts | 61 +++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 apps/webapp/app/hooks/useFuzzyFilter.ts diff --git a/apps/webapp/app/hooks/useFuzzyFilter.ts b/apps/webapp/app/hooks/useFuzzyFilter.ts new file mode 100644 index 00000000000..1c0f6048268 --- /dev/null +++ b/apps/webapp/app/hooks/useFuzzyFilter.ts @@ -0,0 +1,61 @@ +import { useMemo, useState } from "react"; +import { matchSorter } from "match-sorter"; + +/** + * A hook that provides fuzzy filtering functionality for a list of objects. + * Uses match-sorter to perform the filtering across multiple object properties and + * consistently order the results by score. + * + * @param params - The parameters object + * @param params.items - Array of objects to filter + * @param params.keys - Array of object keys to perform the fuzzy search on + * @returns An object containing: + * - filterText: The current filter text + * - setFilterText: Function to update the filter text + * - filteredItems: The filtered array of items based on the current filter text + * + * @example + * ```tsx + * const users = [{ name: "John", email: "john@example.com" }]; + * const { filterText, setFilterText, filteredItems } = useFuzzyFilter({ + * items: users, + * keys: ["name", "email"] + * }); + * ``` + */ +export function useFuzzyFilter({ + items, + keys, +}: { + items: T[]; + keys: Extract[]; +}) { + const [filterText, setFilterText] = useState(""); + + const filteredItems = useMemo(() => { + const filterTerms = filterText + .trim() + .split(" ") + .map((term) => term.trim()) + .filter((term) => term !== ""); + + if (filterTerms.length === 0) { + return items; + } + + // sort by the score of the first term + return filterTerms.reduceRight( + (results, term) => + matchSorter(results, term, { + keys, + }), + items + ); + }, [items, filterText]); + + return { + filterText, + setFilterText, + filteredItems, + }; +} From e2ddbe0a830617b193008540842bdb9b855108ca Mon Sep 17 00:00:00 2001 From: saadi Date: Mon, 21 Apr 2025 10:01:37 -0700 Subject: [PATCH 2/4] Use fuzzy filtering in the tasks view list --- .../route.tsx | 20 +++---------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsx index e9204a7d31f..73c0dbbdddc 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsx @@ -67,9 +67,9 @@ import { } from "~/components/runs/v3/TaskTriggerSource"; import { useEnvironment } from "~/hooks/useEnvironment"; import { useEventSource } from "~/hooks/useEventSource"; +import { useFuzzyFilter } from "~/hooks/useFuzzyFilter"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; -import { useTextFilter } from "~/hooks/useTextFilter"; import { findProjectBySlug } from "~/models/project.server"; import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; import { @@ -169,23 +169,9 @@ export default function Page() { const environment = useEnvironment(); const { tasks, activity, runningStats, durations, usefulLinksPreference } = useTypedLoaderData(); - const { filterText, setFilterText, filteredItems } = useTextFilter({ + const { filterText, setFilterText, filteredItems } = useFuzzyFilter({ items: tasks, - filter: (task, text) => { - if (task.slug.toLowerCase().includes(text.toLowerCase())) { - return true; - } - - if (task.filePath.toLowerCase().includes(text.toLowerCase())) { - return true; - } - - if (task.triggerSource === "SCHEDULED" && "scheduled".includes(text.toLowerCase())) { - return true; - } - - return false; - }, + keys: ["slug", "filePath", "triggerSource"], }); const hasTasks = tasks.length > 0; From 21283989d2be00d5559ea2f60c1d3e62a1a2daf3 Mon Sep 17 00:00:00 2001 From: saadi Date: Mon, 21 Apr 2025 10:02:03 -0700 Subject: [PATCH 3/4] Use fuzzy filtering in the test tasks list --- .../route.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test/route.tsx index ce901332534..4d33289493c 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test/route.tsx @@ -26,7 +26,7 @@ import { } from "~/components/primitives/Table"; import { TaskTriggerSourceIcon } from "~/components/runs/v3/TaskTriggerSource"; import { useEnvironment } from "~/hooks/useEnvironment"; -import { useFilterTasks } from "~/hooks/useFilterTasks"; +import { useFuzzyFilter } from "~/hooks/useFuzzyFilter"; import { useLinkStatus } from "~/hooks/useLinkStatus"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; @@ -120,7 +120,10 @@ function TaskSelector({ tasks: TaskListItem[]; activeTaskIdentifier?: string; }) { - const { filterText, setFilterText, filteredItems } = useFilterTasks({ tasks }); + const { filterText, setFilterText, filteredItems } = useFuzzyFilter({ + items: tasks, + keys: ["taskIdentifier", "friendlyId", "id", "filePath", "triggerSource"], + }); const hasTaskInEnvironment = activeTaskIdentifier ? tasks.some((t) => t.taskIdentifier === activeTaskIdentifier) : undefined; From 5530f693d3b82f91d795a390431e78d14ad67528 Mon Sep 17 00:00:00 2001 From: saadi Date: Mon, 21 Apr 2025 10:03:05 -0700 Subject: [PATCH 4/4] Remove the old tasks filtering react hook --- apps/webapp/app/hooks/useFilterTasks.ts | 38 ------------------------- 1 file changed, 38 deletions(-) delete mode 100644 apps/webapp/app/hooks/useFilterTasks.ts diff --git a/apps/webapp/app/hooks/useFilterTasks.ts b/apps/webapp/app/hooks/useFilterTasks.ts deleted file mode 100644 index 7b95cf81287..00000000000 --- a/apps/webapp/app/hooks/useFilterTasks.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { useTextFilter } from "./useTextFilter"; - -type Task = { - id: string; - friendlyId: string; - taskIdentifier: string; - filePath: string; - triggerSource: string; -}; - -export function useFilterTasks({ tasks }: { tasks: T[] }) { - return useTextFilter({ - items: tasks, - filter: (task, text) => { - if (task.taskIdentifier.toLowerCase().includes(text.toLowerCase())) { - return true; - } - - if (task.filePath.toLowerCase().includes(text.toLowerCase())) { - return true; - } - - if (task.id.toLowerCase().includes(text.toLowerCase())) { - return true; - } - - if (task.friendlyId.toLowerCase().includes(text.toLowerCase())) { - return true; - } - - if (task.triggerSource === "SCHEDULED" && "scheduled".includes(text.toLowerCase())) { - return true; - } - - return false; - }, - }); -}