-
handleSearchChange(e.target.value)}
- />
-
-
-
-
-
-
-
-
- {hasFilters && (
-
+
+ ))}
+
+
+
)}
+
+ );
+}
+
+function ClearFiltersButton() {
+ const navigate = useNavigate();
+ const location = useOptimisticLocation();
+
+ const clearFilters = useCallback(() => {
+ const params = new URLSearchParams(location.search);
+ params.delete("page");
+ params.delete("tasks");
+ params.delete("search");
+ params.delete("type");
+ navigate(`${location.pathname}?${params.toString()}`);
+ }, [location, navigate]);
+
+ return (
+
+
);
}
diff --git a/apps/webapp/app/components/runs/v3/SharedFilters.tsx b/apps/webapp/app/components/runs/v3/SharedFilters.tsx
index 3e24f601f2a..0bdd7c4ac5f 100644
--- a/apps/webapp/app/components/runs/v3/SharedFilters.tsx
+++ b/apps/webapp/app/components/runs/v3/SharedFilters.tsx
@@ -1,5 +1,4 @@
import * as Ariakit from "@ariakit/react";
-import type { RuntimeEnvironment } from "@trigger.dev/database";
import {
endOfDay,
endOfMonth,
@@ -11,20 +10,23 @@ import {
subWeeks,
} from "date-fns";
import parse from "parse-duration";
-import { startTransition, useCallback, useEffect, useRef, useState, type ReactNode } from "react";
+import { type ReactNode, startTransition, useCallback, useEffect, useRef, useState } from "react";
import simplur from "simplur";
import { AppliedFilter } from "~/components/primitives/AppliedFilter";
import { Callout } from "~/components/primitives/Callout";
import { DateTime } from "~/components/primitives/DateTime";
import { DateTimePicker } from "~/components/primitives/DateTimePicker";
+import { FormError } from "~/components/primitives/FormError";
+import { Header3 } from "~/components/primitives/Headers";
+import { Input } from "~/components/primitives/Input";
import { Label } from "~/components/primitives/Label";
import { Paragraph } from "~/components/primitives/Paragraph";
import { RadioButtonCircle } from "~/components/primitives/RadioButton";
import { ComboboxProvider, SelectPopover, SelectProvider } from "~/components/primitives/Select";
+import { ShortcutKey } from "~/components/primitives/ShortcutKey";
import { useOptionalOrganization } from "~/hooks/useOrganizations";
import { useSearchParams } from "~/hooks/useSearchParam";
import { type ShortcutDefinition, useShortcutKeys } from "~/hooks/useShortcutKeys";
-import { ShortcutKey } from "~/components/primitives/ShortcutKey";
import { cn } from "~/utils/cn";
import { organizationBillingPath } from "~/utils/pathBuilder";
import { Button, LinkButton } from "../../primitives/Buttons";
@@ -422,11 +424,7 @@ export function TimeFilter({
Filter by time period
-
+
)}
@@ -1005,3 +1003,102 @@ function QuickDateButton({
);
}
+
+export type IdFilterDropdownProps = {
+ trigger: ReactNode;
+ clearSearchValue: () => void;
+ searchValue: string;
+ onClose?: () => void;
+ label: string;
+ placeholder: string;
+ paramKey: string;
+ validate?: (value: string) => string | undefined;
+ inputWidth?: string;
+};
+
+export function IdFilterDropdown({
+ trigger,
+ clearSearchValue,
+ onClose,
+ label,
+ placeholder,
+ paramKey,
+ validate,
+ inputWidth = "w-[29ch]",
+}: IdFilterDropdownProps) {
+ const [open, setOpen] = useState
();
+ const { value, replace } = useSearchParams();
+ const currentValue = value(paramKey);
+
+ const [inputValue, setInputValue] = useState(currentValue);
+ const [prevOpen, setPrevOpen] = useState(open);
+ if (open !== prevOpen) {
+ setPrevOpen(open);
+ if (open) setInputValue(currentValue);
+ }
+
+ const apply = () => {
+ clearSearchValue();
+ replace({
+ cursor: undefined,
+ direction: undefined,
+ [paramKey]: inputValue === "" ? undefined : inputValue?.toString(),
+ });
+
+ setOpen(false);
+ };
+
+ const error = inputValue ? validate?.(inputValue) : undefined;
+
+ return (
+
+ {trigger}
+ {
+ if (onClose) {
+ onClose();
+ return false;
+ }
+
+ return true;
+ }}
+ className="max-w-[min(32ch,var(--popover-available-width))]"
+ >
+
+
+
+ {label}
+
+
setInputValue(e.target.value)}
+ variant="small"
+ className={cn(inputWidth, "font-mono")}
+ spellCheck={false}
+ />
+ {error ?
{error} : null}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx b/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx
index fbede0e7cec..346fd25eee2 100644
--- a/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx
+++ b/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx
@@ -31,7 +31,7 @@ import {
type NextRunListItem,
} from "~/presenters/v3/NextRunListPresenter.server";
import { formatCurrencyAccurate } from "~/utils/numberFormatter";
-import { docsPath, v3RunSpanPath, v3TestPath,v3TestTaskPath } from "~/utils/pathBuilder";
+import { docsPath, v3RunSpanPath, v3TestPath, v3TestTaskPath } from "~/utils/pathBuilder";
import { DateTime } from "../../primitives/DateTime";
import { Paragraph } from "../../primitives/Paragraph";
import { Spinner } from "../../primitives/Spinner";
@@ -102,7 +102,7 @@ export function TaskRunsTable({
}
const search = params.toString();
/** TableState has to be encoded as a separate URI component, so it's merged under one, 'tableState' param */
- const tableStateParam = disableAdjacentRows ? '' : encodeURIComponent(search);
+ const tableStateParam = disableAdjacentRows ? "" : encodeURIComponent(search);
const showCompute = isManagedCloud;
@@ -162,6 +162,7 @@ export function TaskRunsTable({
Task
Version
{filterableTaskRunStatuses.map((status) => (
@@ -185,6 +186,7 @@ export function TaskRunsTable({
Started
@@ -319,9 +321,16 @@ export function TaskRunsTable({
if (tableStateParam) {
searchParams.set("tableState", tableStateParam);
}
- const path = v3RunSpanPath(organization, project, run.environment, run, {
- spanId: run.spanId,
- }, searchParams);
+ const path = v3RunSpanPath(
+ organization,
+ project,
+ run.environment,
+ run,
+ {
+ spanId: run.spanId,
+ },
+ searchParams
+ );
return (
{allowSelection && (
@@ -427,7 +436,11 @@ export function TaskRunsTable({
- {run.isTest ? : "–"}
+ {run.isTest ? (
+
+ ) : (
+ "–"
+ )}
{run.createdAt ? : "–"}
@@ -449,7 +462,7 @@ export function TaskRunsTable({
{isLoading && (
Loading…
@@ -592,7 +605,9 @@ function BlankState({ isLoading, filters }: Pick
or
-
+
Run a test
diff --git a/apps/webapp/app/components/runs/v3/WaitpointTokenFilters.tsx b/apps/webapp/app/components/runs/v3/WaitpointTokenFilters.tsx
index ae416394147..3868a496d79 100644
--- a/apps/webapp/app/components/runs/v3/WaitpointTokenFilters.tsx
+++ b/apps/webapp/app/components/runs/v3/WaitpointTokenFilters.tsx
@@ -1,28 +1,24 @@
import * as Ariakit from "@ariakit/react";
-import { CalendarIcon, FingerPrintIcon, TagIcon, TrashIcon } from "@heroicons/react/20/solid";
+import { FingerPrintIcon, TagIcon, XMarkIcon } from "@heroicons/react/20/solid";
import { Form, useFetcher } from "@remix-run/react";
import { WaitpointTokenStatus, waitpointTokenStatuses } from "@trigger.dev/core/v3";
-import { ListChecks, ListFilterIcon } from "lucide-react";
+import { ListChecks } from "lucide-react";
import { matchSorter } from "match-sorter";
-import { type ReactNode, useCallback, useEffect, useMemo, useState } from "react";
+import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { z } from "zod";
import { StatusIcon } from "~/assets/icons/StatusIcon";
import { AppliedFilter } from "~/components/primitives/AppliedFilter";
import { Button } from "~/components/primitives/Buttons";
-import { FormError } from "~/components/primitives/FormError";
-import { Input } from "~/components/primitives/Input";
-import { Label } from "~/components/primitives/Label";
import { Paragraph } from "~/components/primitives/Paragraph";
import {
ComboBox,
- SelectButtonItem,
SelectItem,
SelectList,
SelectPopover,
SelectProvider,
- SelectTrigger,
shortcutFromIndex,
} from "~/components/primitives/Select";
+import { ShortcutKey } from "~/components/primitives/ShortcutKey";
import { Spinner } from "~/components/primitives/Spinner";
import {
Tooltip,
@@ -35,8 +31,15 @@ import { useOptimisticLocation } from "~/hooks/useOptimisticLocation";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { useSearchParams } from "~/hooks/useSearchParam";
+import { useShortcutKeys } from "~/hooks/useShortcutKeys";
import { type loader as tagsLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tags";
-import { TimeFilter, appliedSummary, FilterMenuProvider } from "./SharedFilters";
+import {
+ IdFilterDropdown,
+ type IdFilterDropdownProps,
+ appliedSummary,
+ FilterMenuProvider,
+ TimeFilter,
+} from "./SharedFilters";
import { WaitpointStatusCombo, waitpointStatusTitle } from "./WaitpointStatus";
export const WaitpointSearchParamsSchema = z.object({
@@ -66,136 +69,33 @@ export function WaitpointTokenFilters(props: WaitpointTokenFiltersProps) {
searchParams.has("statuses") ||
searchParams.has("tags") ||
searchParams.has("id") ||
- searchParams.has("idempotencyKey");
+ searchParams.has("idempotencyKey") ||
+ searchParams.has("period") ||
+ searchParams.has("from") ||
+ searchParams.has("to");
return (
-
-
-
-
+
+
+
+
+
+
{hasFilters && (
-
)}
);
}
-const filterTypes = [
- {
- name: "statuses",
- title: "Status",
- icon:
,
- },
- { name: "tags", title: "Tags", icon:
},
- { name: "id", title: "Waitpoint ID", icon:
},
- { name: "idempotencyKey", title: "Idempotency key", icon:
},
-] as const;
-
-type FilterType = (typeof filterTypes)[number]["name"];
-
-const shortcut = { key: "f" };
-
-function FilterMenu() {
- const [filterType, setFilterType] = useState
();
-
- const filterTrigger = (
-
-
-
- }
- variant={"secondary/small"}
- shortcut={shortcut}
- tooltipTitle={"Filter runs"}
- >
- Filter
-
- );
-
- return (
- setFilterType(undefined)}>
- {(search, setSearch) => (
-
- );
-}
-
-function AppliedFilters() {
- return (
- <>
-
-
-
-
- >
- );
-}
-
-type MenuProps = {
- searchValue: string;
- clearSearchValue: () => void;
- trigger: React.ReactNode;
- filterType: FilterType | undefined;
- setFilterType: (filterType: FilterType | undefined) => void;
-};
-
-function Menu(props: MenuProps) {
- switch (props.filterType) {
- case undefined:
- return ;
- case "statuses":
- return props.setFilterType(undefined)} {...props} />;
- case "tags":
- return props.setFilterType(undefined)} {...props} />;
- case "id":
- return props.setFilterType(undefined)} {...props} />;
- case "idempotencyKey":
- return props.setFilterType(undefined)} {...props} />;
- }
-}
-
-function MainMenu({ searchValue, trigger, clearSearchValue, setFilterType }: MenuProps) {
- const filtered = useMemo(() => {
- return filterTypes.filter((item) => {
- return item.title.toLowerCase().includes(searchValue.toLowerCase());
- });
- }, [searchValue]);
-
- return (
-
- {trigger}
-
-
-
- {filtered.map((type, index) => (
- {
- clearSearchValue();
- setFilterType(type.name);
- }}
- icon={type.icon}
- shortcut={shortcutFromIndex(index, { shortcutsEnabled: true })}
- >
- {type.title}
-
- ))}
-
-
-
- );
-}
-
const statuses = waitpointTokenStatuses.map((status) => ({
title: waitpointStatusTitle(status),
value: status,
@@ -237,7 +137,6 @@ function StatusDropdown({
return true;
}}
>
-
{filtered.map((item, index) => {
return (
@@ -249,7 +148,7 @@ function StatusDropdown({
-
+
@@ -267,30 +166,68 @@ function StatusDropdown({
);
}
-function AppliedStatusFilter() {
- const { values, del } = useSearchParams();
- const statuses = values("statuses");
+const statusShortcut = { key: "s" };
- if (statuses.length === 0) {
- return null;
- }
+function PermanentStatusFilter() {
+ const { values, del } = useSearchParams();
+ const selectedStatuses = values("statuses");
+ const hasStatuses = selectedStatuses.length > 0 && !selectedStatuses.every((v) => v === "");
+ const triggerRef = useRef(null);
+
+ useShortcutKeys({
+ shortcut: statusShortcut,
+ action: (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ triggerRef.current?.click();
+ },
+ });
return (
{(search, setSearch) => (
}>
- }
- value={appliedSummary(
- statuses.map((v) => waitpointStatusTitle(v as WaitpointTokenStatus))
+
+ }
+ />
+ }
+ >
+ {hasStatuses ? (
+ }
+ value={appliedSummary(
+ selectedStatuses.map((v) => waitpointStatusTitle(v as WaitpointTokenStatus))
+ )}
+ onRemove={() => del(["statuses", "cursor", "direction"])}
+ variant="secondary/small"
+ className="pl-1"
+ />
+ ) : (
+
)}
- onRemove={() => del(["statuses", "cursor", "direction"])}
- variant="secondary/small"
- />
-
+
+
+
+ Filter by status
+
+
+
+
}
searchValue={search}
clearSearchValue={() => setSearch("")}
@@ -366,19 +303,21 @@ function TagsDropdown({
return true;
}}
>
- (
-
-
- {fetcher.state === "loading" && }
-
- )}
- />
+ {!(filtered.length === 0 && fetcher.state !== "loading" && searchValue === "") && (
+ (
+
+
+ {fetcher.state === "loading" && }
+
+ )}
+ />
+ )}
{filtered.length > 0
- ? filtered.map((tag, index) => (
-
+ ? filtered.map((tag) => (
+
{tag}
))
@@ -392,29 +331,64 @@ function TagsDropdown({
);
}
-function AppliedTagsFilter() {
- const { values, del } = useSearchParams();
+const tagsShortcut = { key: "g" };
+function PermanentTagsFilter() {
+ const { values, del } = useSearchParams();
const tags = values("tags");
-
- if (tags.length === 0) {
- return null;
- }
+ const hasTags = tags.length > 0 && !tags.every((v) => v === "");
+ const triggerRef = useRef(null);
+
+ useShortcutKeys({
+ shortcut: tagsShortcut,
+ action: (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ triggerRef.current?.click();
+ },
+ });
return (
{(search, setSearch) => (
}>
- }
- value={appliedSummary(values("tags"))}
- onRemove={() => del(["tags", "cursor", "direction"])}
- variant="secondary/small"
- />
-
+
+ }
+ />
+ }
+ >
+ {hasTags ? (
+ }
+ value={appliedSummary(tags)}
+ onRemove={() => del(["tags", "cursor", "direction"])}
+ variant="secondary/small"
+ className="pl-1"
+ />
+ ) : (
+
+
+ Tags
+
+ )}
+
+
+
+ Filter by tags
+
+
+
+
}
searchValue={search}
clearSearchValue={() => setSearch("")}
@@ -424,117 +398,82 @@ function AppliedTagsFilter() {
);
}
-function WaitpointIdDropdown({
- trigger,
- clearSearchValue,
- searchValue,
- onClose,
-}: {
- trigger: ReactNode;
- clearSearchValue: () => void;
- searchValue: string;
- onClose?: () => void;
-}) {
- const [open, setOpen] = useState();
- const { value, replace } = useSearchParams();
- const idValue = value("id");
-
- const [id, setId] = useState(idValue);
-
- const apply = useCallback(() => {
- clearSearchValue();
- replace({
- cursor: undefined,
- direction: undefined,
- id: id === "" ? undefined : id?.toString(),
- });
-
- setOpen(false);
- }, [id, replace]);
-
- let error: string | undefined = undefined;
- if (id) {
- if (!id.startsWith("waitpoint_")) {
- error = "Waitpoint IDs start with 'waitpoint_'";
- } else if (id.length !== 35) {
- error = "Waitpoint IDs are 35 characters long";
- }
- }
-
+function WaitpointIdDropdown(
+ props: Omit
+) {
return (
-
- {trigger}
- {
- if (onClose) {
- onClose();
- return false;
- }
-
- return true;
- }}
- className="max-w-[min(32ch,var(--popover-available-width))]"
- >
-
-
-
- setId(e.target.value)}
- variant="small"
- className="w-[27ch] font-mono"
- spellCheck={false}
- />
- {error ? {error} : null}
-
-
-
-
-
-
-
-
+ {
+ if (!v.startsWith("waitpoint_")) return "Waitpoint IDs start with 'waitpoint_'";
+ if (v.length !== 35) return "Waitpoint IDs are 35 characters long";
+ return undefined;
+ }}
+ />
);
}
-function AppliedWaitpointIdFilter() {
- const { value, del } = useSearchParams();
-
- if (value("id") === undefined) {
- return null;
- }
+const waitpointIdShortcut = { key: "w" };
+function PermanentWaitpointIdFilter() {
+ const { value, del } = useSearchParams();
const id = value("id");
+ const hasId = id !== undefined;
+ const triggerRef = useRef(null);
+
+ useShortcutKeys({
+ shortcut: waitpointIdShortcut,
+ action: (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ triggerRef.current?.click();
+ },
+ });
return (
{(search, setSearch) => (
}>
- }
- value={id}
- onRemove={() => del(["id", "cursor", "direction"])}
- variant="secondary/small"
- />
-
+
+ }
+ />
+ }
+ >
+ {hasId ? (
+ }
+ value={id}
+ onRemove={() => del(["id", "cursor", "direction"])}
+ variant="secondary/small"
+ className="pl-1"
+ />
+ ) : (
+
+
+ Waitpoint ID
+
+ )}
+
+
+
+ Filter by waitpoint ID
+
+
+
+
}
searchValue={search}
clearSearchValue={() => setSearch("")}
@@ -544,115 +483,81 @@ function AppliedWaitpointIdFilter() {
);
}
-function IdempotencyKeyDropdown({
- trigger,
- clearSearchValue,
- searchValue,
- onClose,
-}: {
- trigger: ReactNode;
- clearSearchValue: () => void;
- searchValue: string;
- onClose?: () => void;
-}) {
- const [open, setOpen] = useState();
- const { value, replace } = useSearchParams();
- const idValue = value("idempotencyKey");
-
- const [idempotencyKey, setIdempotencyKey] = useState(idValue);
-
- const apply = useCallback(() => {
- clearSearchValue();
- replace({
- cursor: undefined,
- direction: undefined,
- idempotencyKey: idempotencyKey === "" ? undefined : idempotencyKey?.toString(),
- });
-
- setOpen(false);
- }, [idempotencyKey, replace]);
-
- let error: string | undefined = undefined;
- if (idempotencyKey) {
- if (idempotencyKey.length === 0) {
- error = "Idempotency keys need to be at least 1 character in length";
- }
- }
-
+function IdempotencyKeyDropdown(
+ props: Omit
+) {
return (
-
- {trigger}
- {
- if (onClose) {
- onClose();
- return false;
- }
-
- return true;
- }}
- className="max-w-[min(32ch,var(--popover-available-width))]"
- >
-
-
-
- setIdempotencyKey(e.target.value)}
- variant="small"
- className="w-[27ch] font-mono"
- spellCheck={false}
- />
- {error ? {error} : null}
-
-
-
-
-
-
-
-
+ {
+ if (v.length === 0) return "Idempotency keys need to be at least 1 character in length";
+ return undefined;
+ }}
+ />
);
}
-function AppliedIdempotencyKeyFilter() {
- const { value, del } = useSearchParams();
-
- if (value("idempotencyKey") === undefined) {
- return null;
- }
+const idempotencyKeyShortcut = { key: "i" };
+function PermanentIdempotencyKeyFilter() {
+ const { value, del } = useSearchParams();
const idempotencyKey = value("idempotencyKey");
+ const hasKey = idempotencyKey !== undefined;
+ const triggerRef = useRef(null);
+
+ useShortcutKeys({
+ shortcut: idempotencyKeyShortcut,
+ action: (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ triggerRef.current?.click();
+ },
+ });
return (
{(search, setSearch) => (
}>
- }
- value={idempotencyKey}
- onRemove={() => del(["idempotencyKey", "cursor", "direction"])}
- variant="secondary/small"
- />
-
+
+ }
+ />
+ }
+ >
+ {hasKey ? (
+ }
+ value={idempotencyKey}
+ onRemove={() => del(["idempotencyKey", "cursor", "direction"])}
+ variant="secondary/small"
+ className="pl-1"
+ />
+ ) : (
+
+
+ Idempotency key
+
+ )}
+
+
+
+ Filter by idempotency key
+
+
+
+
}
searchValue={search}
clearSearchValue={() => setSearch("")}
diff --git a/apps/webapp/app/hooks/useFuzzyFilter.ts b/apps/webapp/app/hooks/useFuzzyFilter.ts
index 3f0797179f2..ff4504ce8c2 100644
--- a/apps/webapp/app/hooks/useFuzzyFilter.ts
+++ b/apps/webapp/app/hooks/useFuzzyFilter.ts
@@ -10,8 +10,9 @@ import { matchSorter } from "match-sorter";
* @param params.items - Array of objects to filter
* @param params.keys - Array of object keys to perform the fuzzy search on (supports dot-notation for nested properties)
* @returns An object containing:
- * - filterText: The current filter text
- * - setFilterText: Function to update the filter text
+ * - filterText: The current filter text (the controlled value if provided, otherwise the internal state)
+ * - setFilterText: Updates the internal filter text. No-op when `filterText` is provided
+ * (controlled mode) — the parent owns the value in that case.
* - filteredItems: The filtered array of items based on the current filter text
*
* @example
@@ -26,11 +27,15 @@ import { matchSorter } from "match-sorter";
export function useFuzzyFilter({
items,
keys,
+ filterText: controlledFilterText,
}: {
items: T[];
keys: (Extract | (string & {}))[];
+ /** Optional controlled filter text. If provided, internal state is ignored. */
+ filterText?: string;
}) {
- const [filterText, setFilterText] = useState("");
+ const [internalFilterText, setInternalFilterText] = useState("");
+ const filterText = controlledFilterText ?? internalFilterText;
const filteredItems = useMemo(() => {
const filterTerms = filterText
@@ -43,7 +48,6 @@ export function useFuzzyFilter({
return items;
}
- // sort by the score of the first term
return filterTerms.reduceRight(
(results, term) =>
matchSorter(results, term, {
@@ -55,7 +59,7 @@ export function useFuzzyFilter({
return {
filterText,
- setFilterText,
+ setFilterText: setInternalFilterText,
filteredItems,
};
}
diff --git a/apps/webapp/app/presenters/v3/BuiltInDashboards.server.ts b/apps/webapp/app/presenters/v3/BuiltInDashboards.server.ts
index 2ec34614533..a6374c60be2 100644
--- a/apps/webapp/app/presenters/v3/BuiltInDashboards.server.ts
+++ b/apps/webapp/app/presenters/v3/BuiltInDashboards.server.ts
@@ -216,7 +216,7 @@ const overviewDashboard: BuiltInDashboard = {
const llmDashboard: BuiltInDashboard = {
key: "llm",
- title: "AI Metrics",
+ title: "AI metrics",
filters: ["tasks", "models", "prompts", "operations", "providers"],
layout: {
version: "1",
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 2cf8b844a9e..d61c357e02e 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
@@ -5,8 +5,6 @@ import {
ChevronUpIcon,
ExclamationTriangleIcon,
LightBulbIcon,
- MagnifyingGlassIcon,
- XMarkIcon,
UserPlusIcon,
VideoCameraIcon,
} from "@heroicons/react/20/solid";
@@ -38,7 +36,6 @@ import { Callout } from "~/components/primitives/Callout";
import { formatDateTime } from "~/components/primitives/DateTime";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "~/components/primitives/Dialog";
import { Header2, Header3 } from "~/components/primitives/Headers";
-import { Input } from "~/components/primitives/Input";
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
import { Paragraph } from "~/components/primitives/Paragraph";
import { PopoverMenuItem } from "~/components/primitives/Popover";
@@ -50,6 +47,7 @@ import {
ResizablePanelGroup,
collapsibleHandleClassName,
} from "~/components/primitives/Resizable";
+import { SearchInput } from "~/components/primitives/SearchInput";
import { Spinner } from "~/components/primitives/Spinner";
import { StepNumber } from "~/components/primitives/StepNumber";
import {
@@ -75,6 +73,7 @@ import { useEventSource } from "~/hooks/useEventSource";
import { useFuzzyFilter } from "~/hooks/useFuzzyFilter";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
+import { useSearchParams } from "~/hooks/useSearchParam";
import { findProjectBySlug } from "~/models/project.server";
import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server";
import {
@@ -88,7 +87,6 @@ import {
uiPreferencesStorage,
} from "~/services/preferences/uiPreferences.server";
import { requireUserId } from "~/services/session.server";
-import { motion } from "framer-motion";
import { cn } from "~/utils/cn";
import {
docsPath,
@@ -176,9 +174,11 @@ export default function Page() {
const environment = useEnvironment();
const { tasks, activity, runningStats, durations, usefulLinksPreference } =
useTypedLoaderData();
- const { filterText, setFilterText, filteredItems } = useFuzzyFilter({
+ const { value } = useSearchParams();
+ const { filteredItems } = useFuzzyFilter({
items: tasks,
keys: ["slug", "filePath", "triggerSource"],
+ filterText: value("search") ?? "",
});
const hasTasks = tasks.length > 0;
@@ -244,16 +244,12 @@ export default function Page() {
{tasks.length === 0 ? : null}
-
- {!showUsefulLinks && (
+
+ {!showUsefulLinks && (