From ea7fce74ab848133994e0ca3b807837e4564081f Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Fri, 3 Apr 2026 18:09:58 +0100 Subject: [PATCH 01/51] Move important run filters outside the filter menu --- .../app/components/runs/v3/RunFilters.tsx | 105 +++++++++--------- 1 file changed, 52 insertions(+), 53 deletions(-) diff --git a/apps/webapp/app/components/runs/v3/RunFilters.tsx b/apps/webapp/app/components/runs/v3/RunFilters.tsx index dc3657b42a9..a55951b57bc 100644 --- a/apps/webapp/app/components/runs/v3/RunFilters.tsx +++ b/apps/webapp/app/components/runs/v3/RunFilters.tsx @@ -3,6 +3,7 @@ import { CalendarIcon, ClockIcon, FingerPrintIcon, + PlusIcon, RectangleStackIcon, Squares2X2Icon, TagIcon, @@ -12,7 +13,6 @@ import { Form, useFetcher } from "@remix-run/react"; import { IconBugFilled, IconRotateClockwise2, IconToggleLeft } from "@tabler/icons-react"; import { MachinePresetName } from "@trigger.dev/core/v3"; import type { BulkActionType, TaskRunStatus, TaskTriggerSource } from "@trigger.dev/database"; -import { ListFilterIcon } from "lucide-react"; import { matchSorter } from "match-sorter"; import { type ReactNode, useCallback, useEffect, useMemo, useState } from "react"; import { z } from "zod"; @@ -57,13 +57,13 @@ import { useOptimisticLocation } from "~/hooks/useOptimisticLocation"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; import { useSearchParams } from "~/hooks/useSearchParam"; +import { type loader as tagsLoader } from "~/routes/resources.environments.$envId.runs.tags"; import { type loader as queuesLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues"; import { type loader as versionsLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.versions"; -import { type loader as tagsLoader } from "~/routes/resources.environments.$envId.runs.tags"; import { Button } from "../../primitives/Buttons"; +import { AIFilterInput } from "./AIFilterInput"; import { BulkActionTypeCombo } from "./BulkAction"; import { appliedSummary, FilterMenuProvider, TimeFilter, timeFilters } from "./SharedFilters"; -import { AIFilterInput } from "./AIFilterInput"; import { allTaskRunStatuses, descriptionForTaskRunStatus, @@ -354,18 +354,20 @@ export function RunsFilters(props: RunFiltersProps) { searchParams.has("errorId"); return ( -
- +
{!props.hideSearch && } - + + + + {hasFilters && (
{searchParams.has("rootOnly") && ( )} -
@@ -373,12 +375,6 @@ export function RunsFilters(props: RunFiltersProps) { } const filterTypes = [ - { - name: "statuses", - title: "Status", - icon: , - }, - { name: "tasks", title: "Tasks", icon: }, { name: "tags", title: "Tags", icon: }, { name: "versions", title: "Versions", icon: }, { name: "queues", title: "Queues", icon: }, @@ -401,15 +397,15 @@ function FilterMenu(props: RunFiltersProps) { - +
} variant={"secondary/small"} shortcut={shortcut} - tooltipTitle={"Filter runs"} - className="pr-0.5" + tooltipTitle={"More filters"} + className="pl-1 pr-2" > - <> + More filters ); @@ -429,11 +425,9 @@ function FilterMenu(props: RunFiltersProps) { ); } -function AppliedFilters({ possibleTasks, bulkActions }: RunFiltersProps) { +function AppliedFilters({ bulkActions }: RunFiltersProps) { return ( <> - - @@ -459,10 +453,6 @@ function Menu(props: MenuProps) { switch (props.filterType) { case undefined: return ; - case "statuses": - return props.setFilterType(undefined)} {...props} />; - case "tasks": - return props.setFilterType(undefined)} {...props} />; case "bulk": return props.setFilterType(undefined)} {...props} />; case "tags": @@ -585,13 +575,10 @@ function StatusDropdown({ ); } -function AppliedStatusFilter() { +function PermanentStatusFilter() { const { values, del } = useSearchParams(); const statuses = values("statuses"); - - if (statuses.length === 0 || statuses.every((v) => v === "")) { - return null; - } + const hasStatuses = statuses.length > 0 && !statuses.every((v) => v === ""); return ( @@ -599,13 +586,20 @@ function AppliedStatusFilter() { }> - runStatusTitle(v as TaskRunStatus)))} - onRemove={() => del(["statuses", "cursor", "direction"])} - variant="secondary/small" - /> + {hasStatuses ? ( + runStatusTitle(v as TaskRunStatus)))} + onRemove={() => del(["statuses", "cursor", "direction"])} + variant="secondary/small" + /> + ) : ( +
+ {filterIcon("statuses")} + Status +
+ )} } searchValue={search} @@ -675,12 +669,10 @@ function TasksDropdown({ ); } -function AppliedTaskFilter({ possibleTasks }: Pick) { +function PermanentTasksFilter({ possibleTasks }: Pick) { const { values, del } = useSearchParams(); - - if (values("tasks").length === 0 || values("tasks").every((v) => v === "")) { - return null; - } + const tasks = values("tasks"); + const hasTasks = tasks.length > 0 && !tasks.every((v) => v === ""); return ( @@ -688,18 +680,25 @@ function AppliedTaskFilter({ possibleTasks }: Pick}> - { - const task = possibleTasks.find((task) => task.slug === v); - return task ? task.slug : v; - }) - )} - onRemove={() => del(["tasks", "cursor", "direction"])} - variant="secondary/small" - /> + {hasTasks ? ( + { + const task = possibleTasks.find((task) => task.slug === v); + return task ? task.slug : v; + }) + )} + onRemove={() => del(["tasks", "cursor", "direction"])} + variant="secondary/small" + /> + ) : ( +
+ {filterIcon("tasks")} + Tasks +
+ )} } searchValue={search} From ab7ec742e483c515004c9b622375e5cd2dba5341 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sat, 4 Apr 2026 11:46:12 +0100 Subject: [PATCH 02/51] adds disableTooltipHoverableContent to 2 run table tooltips --- .../app/components/runs/v3/TaskRunsTable.tsx | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx b/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx index fbede0e7cec..22f24ca9d97 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 && ( @@ -592,7 +601,9 @@ function BlankState({ isLoading, filters }: Pick or - + Run a test
From dc50e8a5d6fc76873d2062dad2094b69bffe2c2b Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sat, 4 Apr 2026 11:46:39 +0100 Subject: [PATCH 03/51] Uses correct hover state for tables --- apps/webapp/app/components/MachineLabelCombo.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/webapp/app/components/MachineLabelCombo.tsx b/apps/webapp/app/components/MachineLabelCombo.tsx index 3d22ca527d0..485f6094cf0 100644 --- a/apps/webapp/app/components/MachineLabelCombo.tsx +++ b/apps/webapp/app/components/MachineLabelCombo.tsx @@ -31,7 +31,9 @@ export function MachineLabel({ className?: string; }) { return ( - {formatMachinePresetName(preset)} + + {formatMachinePresetName(preset)} + ); } From cf146ab55b2ebae7d97907396a0c4c3d1be58e6a Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sat, 4 Apr 2026 11:49:53 +0100 Subject: [PATCH 04/51] Adds tooltips to filters --- .../app/components/runs/v3/RunFilters.tsx | 175 +++++++++++++----- 1 file changed, 127 insertions(+), 48 deletions(-) diff --git a/apps/webapp/app/components/runs/v3/RunFilters.tsx b/apps/webapp/app/components/runs/v3/RunFilters.tsx index a55951b57bc..2ab45d32eeb 100644 --- a/apps/webapp/app/components/runs/v3/RunFilters.tsx +++ b/apps/webapp/app/components/runs/v3/RunFilters.tsx @@ -14,7 +14,7 @@ import { IconBugFilled, IconRotateClockwise2, IconToggleLeft } from "@tabler/ico import { MachinePresetName } from "@trigger.dev/core/v3"; import type { BulkActionType, TaskRunStatus, TaskTriggerSource } from "@trigger.dev/database"; 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 { ListCheckedIcon } from "~/assets/icons/ListCheckedIcon"; import { MachineDefaultIcon } from "~/assets/icons/MachineIcon"; @@ -57,6 +57,8 @@ 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 { ShortcutKey } from "~/components/primitives/ShortcutKey"; import { type loader as tagsLoader } from "~/routes/resources.environments.$envId.runs.tags"; import { type loader as queuesLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues"; import { type loader as versionsLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.versions"; @@ -358,7 +360,7 @@ export function RunsFilters(props: RunFiltersProps) { {!props.hideSearch && } - + @@ -575,32 +577,63 @@ function StatusDropdown({ ); } +const statusShortcut = { key: "s" }; + function PermanentStatusFilter() { const { values, del } = useSearchParams(); const statuses = values("statuses"); const hasStatuses = statuses.length > 0 && !statuses.every((v) => v === ""); + const triggerRef = useRef(null); + + useShortcutKeys({ + shortcut: statusShortcut, + action: (e) => { + e.preventDefault(); + e.stopPropagation(); + triggerRef.current?.click(); + }, + }); return ( {(search, setSearch) => ( }> - {hasStatuses ? ( - runStatusTitle(v as TaskRunStatus)))} - onRemove={() => del(["statuses", "cursor", "direction"])} - variant="secondary/small" - /> - ) : ( -
- {filterIcon("statuses")} - Status + + } /> + } + > + {hasStatuses ? ( + runStatusTitle(v as TaskRunStatus)) + )} + onRemove={() => del(["statuses", "cursor", "direction"])} + variant="secondary/small" + /> + ) : ( +
+ {filterIcon("statuses")} + Status +
+ )} +
+ +
+ Filter by status +
- )} - +
+
} searchValue={search} clearSearchValue={() => setSearch("")} @@ -669,37 +702,66 @@ function TasksDropdown({ ); } +const tasksShortcut = { key: "t" }; + function PermanentTasksFilter({ possibleTasks }: Pick) { const { values, del } = useSearchParams(); const tasks = values("tasks"); const hasTasks = tasks.length > 0 && !tasks.every((v) => v === ""); + const triggerRef = useRef(null); + + useShortcutKeys({ + shortcut: tasksShortcut, + action: (e) => { + e.preventDefault(); + e.stopPropagation(); + triggerRef.current?.click(); + }, + }); return ( {(search, setSearch) => ( }> - {hasTasks ? ( - { - const task = possibleTasks.find((task) => task.slug === v); - return task ? task.slug : v; - }) - )} - onRemove={() => del(["tasks", "cursor", "direction"])} - variant="secondary/small" - /> - ) : ( -
- {filterIcon("tasks")} - Tasks + + } /> + } + > + {hasTasks ? ( + { + const task = possibleTasks.find((task) => task.slug === v); + return task ? task.slug : v; + }) + )} + onRemove={() => del(["tasks", "cursor", "direction"])} + variant="secondary/small" + /> + ) : ( +
+ {filterIcon("tasks")} + Tasks +
+ )} +
+ +
+ Filter by task +
- )} - +
+
} searchValue={search} clearSearchValue={() => setSearch("")} @@ -1364,6 +1426,8 @@ function AppliedVersionsFilter() { ); } +const rootOnlyShortcut = { key: "o" }; + function RootOnlyToggle({ defaultValue }: { defaultValue: boolean }) { const { value, values, replace } = useSearchParams(); const searchValue = value("rootOnly"); @@ -1377,17 +1441,32 @@ function RootOnlyToggle({ defaultValue }: { defaultValue: boolean }) { const disabled = !!batchId || !!runId || !!scheduleId || tasks.length > 0; return ( - { - replace({ - rootOnly: checked ? "true" : "false", - }); - }} - /> + + }> + { + replace({ + rootOnly: checked ? "true" : "false", + }); + }} + /> + + +
+ Toggle root only + +
+
+
); } From 6278f3f291d9c546f3ce57fd24408ba4886d71bb Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sat, 4 Apr 2026 11:58:39 +0100 Subject: [PATCH 05/51] Updates filters on the Batch page --- .../app/components/runs/v3/BatchFilters.tsx | 254 +++++++----------- 1 file changed, 103 insertions(+), 151 deletions(-) diff --git a/apps/webapp/app/components/runs/v3/BatchFilters.tsx b/apps/webapp/app/components/runs/v3/BatchFilters.tsx index e0d417ddec4..2f76b5c083d 100644 --- a/apps/webapp/app/components/runs/v3/BatchFilters.tsx +++ b/apps/webapp/app/components/runs/v3/BatchFilters.tsx @@ -8,9 +8,7 @@ import { } from "@heroicons/react/20/solid"; import { Form } from "@remix-run/react"; import type { BatchTaskRunStatus, RuntimeEnvironment } from "@trigger.dev/database"; -import { ListFilterIcon } from "lucide-react"; -import type { ReactNode } from "react"; -import { useCallback, useMemo, useState } from "react"; +import { type ReactNode, useCallback, useMemo, useRef, useState } from "react"; import { z } from "zod"; import { AppliedFilter } from "~/components/primitives/AppliedFilter"; import { FormError } from "~/components/primitives/FormError"; @@ -19,14 +17,13 @@ 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 { Tooltip, TooltipContent, @@ -35,6 +32,7 @@ import { } from "~/components/primitives/Tooltip"; import { useOptimisticLocation } from "~/hooks/useOptimisticLocation"; import { useSearchParams } from "~/hooks/useSearchParam"; +import { useShortcutKeys } from "~/hooks/useShortcutKeys"; import { Button } from "../../primitives/Buttons"; import { allBatchStatuses, @@ -72,130 +70,19 @@ export function BatchFilters(props: BatchFiltersProps) { const hasFilters = searchParams.has("statuses") || searchParams.has("id"); return ( -
- - - +
+ + + {hasFilters && (
-
); } -const filterTypes = [ - { - name: "statuses", - title: "Status", - icon: ( -
-
-
- ), - }, - { name: "batch", title: "Batch ID", icon: }, -] as const; - -type FilterType = (typeof filterTypes)[number]["name"]; - -const shortcut = { key: "f" }; - -function FilterMenu(props: BatchFiltersProps) { - const [filterType, setFilterType] = useState(); - - const filterTrigger = ( - - -
- } - variant={"secondary/small"} - shortcut={shortcut} - tooltipTitle={"Filter batches"} - > - Filter - - ); - - return ( - setFilterType(undefined)}> - {(search, setSearch) => ( - setSearch("")} - trigger={filterTrigger} - filterType={filterType} - setFilterType={setFilterType} - {...props} - /> - )} - - ); -} - -function AppliedFilters() { - return ( - <> - - - - ); -} - -type MenuProps = { - searchValue: string; - clearSearchValue: () => void; - trigger: React.ReactNode; - filterType: FilterType | undefined; - setFilterType: (filterType: FilterType | undefined) => void; -} & BatchFiltersProps; - -function Menu(props: MenuProps) { - switch (props.filterType) { - case undefined: - return ; - case "statuses": - return props.setFilterType(undefined)} {...props} />; - case "batch": - 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 = allBatchStatuses.map((status) => ({ title: batchStatusTitle(status), value: status, @@ -265,30 +152,63 @@ function StatusDropdown({ ); } -function AppliedStatusFilter() { +const statusShortcut = { key: "s" }; + +function PermanentStatusFilter() { const { values, del } = useSearchParams(); const statuses = values("statuses"); - - if (statuses.length === 0) { - return null; - } + const hasStatuses = statuses.length > 0; + const triggerRef = useRef(null); + + useShortcutKeys({ + shortcut: statusShortcut, + action: (e) => { + e.preventDefault(); + e.stopPropagation(); + triggerRef.current?.click(); + }, + }); return ( {(search, setSearch) => ( }> - } - value={appliedSummary( - statuses.map((v) => batchStatusTitle(v as BatchTaskRunStatus)) + + } /> + } + > + {hasStatuses ? ( + } + value={appliedSummary( + statuses.map((v) => batchStatusTitle(v as BatchTaskRunStatus)) + )} + onRemove={() => del(["statuses", "cursor", "direction"])} + variant="secondary/small" + /> + ) : ( +
+ + Status +
)} - onRemove={() => del(["statuses", "cursor", "direction"])} - variant="secondary/small" - /> - +
+ +
+ Filter by status + +
+
+
} searchValue={search} clearSearchValue={() => setSearch("")} @@ -386,29 +306,61 @@ function BatchIdDropdown({ ); } -function AppliedBatchIdFilter() { - const { value, del } = useSearchParams(); - - if (value("id") === undefined) { - return null; - } +const batchIdShortcut = { key: "b" }; +function PermanentBatchIdFilter() { + const { value, del } = useSearchParams(); const batchId = value("id"); + const hasBatchId = batchId !== undefined; + const triggerRef = useRef(null); + + useShortcutKeys({ + shortcut: batchIdShortcut, + action: (e) => { + e.preventDefault(); + e.stopPropagation(); + triggerRef.current?.click(); + }, + }); return ( {(search, setSearch) => ( }> - } - value={batchId} - onRemove={() => del(["id", "cursor", "direction"])} - variant="secondary/small" - /> - + + } /> + } + > + {hasBatchId ? ( + } + value={batchId} + onRemove={() => del(["id", "cursor", "direction"])} + variant="secondary/small" + /> + ) : ( +
+ + Batch ID +
+ )} +
+ +
+ Filter by batch ID + +
+
+
} searchValue={search} clearSearchValue={() => setSearch("")} From a01c5a2dbec17619e101916598a2a96edc3fbe02 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sat, 4 Apr 2026 12:25:43 +0100 Subject: [PATCH 06/51] Updates schedules page filters to new pattern --- .../components/runs/v3/ScheduleFilters.tsx | 381 +++++++++++++----- .../route.tsx | 92 +++-- .../route.tsx | 2 +- 3 files changed, 325 insertions(+), 150 deletions(-) diff --git a/apps/webapp/app/components/runs/v3/ScheduleFilters.tsx b/apps/webapp/app/components/runs/v3/ScheduleFilters.tsx index 3a30e0cb37e..c606086ae6a 100644 --- a/apps/webapp/app/components/runs/v3/ScheduleFilters.tsx +++ b/apps/webapp/app/components/runs/v3/ScheduleFilters.tsx @@ -1,21 +1,27 @@ +import * as Ariakit from "@ariakit/react"; import { MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/20/solid"; import { useNavigate } from "@remix-run/react"; -import { useCallback } from "react"; +import { AnimatePresence, motion } from "framer-motion"; +import { useCallback, useRef, useState } from "react"; import { z } from "zod"; +import { TaskIcon } from "~/assets/icons/TaskIcon"; +import { AppliedFilter } from "~/components/primitives/AppliedFilter"; import { Input } from "~/components/primitives/Input"; +import { + SelectItem, + SelectList, + SelectPopover, + SelectProvider, +} from "~/components/primitives/Select"; +import { ShortcutKey } from "~/components/primitives/ShortcutKey"; import { useOptimisticLocation } from "~/hooks/useOptimisticLocation"; +import { useSearchParams } from "~/hooks/useSearchParam"; +import { useShortcutKeys } from "~/hooks/useShortcutKeys"; import { useThrottle } from "~/hooks/useThrottle"; +import { cn } from "~/utils/cn"; import { Button } from "../../primitives/Buttons"; -import { Paragraph } from "../../primitives/Paragraph"; -import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectTrigger, - SelectValue, -} from "../../primitives/SimpleSelect"; -import { ScheduleTypeCombo } from "./ScheduleType"; +import { ScheduleTypeCombo, ScheduleTypeIcon, scheduleTypeName } from "./ScheduleType"; +import { FilterMenuProvider } from "./SharedFilters"; export const ScheduleListFilters = z.object({ page: z.coerce.number().default(1), @@ -29,120 +35,285 @@ export const ScheduleListFilters = z.object({ export type ScheduleListFilters = z.infer; -const All = "ALL"; - type ScheduleFiltersProps = { possibleTasks: string[]; }; export function ScheduleFilters({ possibleTasks }: ScheduleFiltersProps) { - const navigate = useNavigate(); const location = useOptimisticLocation(); const searchParams = new URLSearchParams(location.search); - const { tasks, page, search, type } = ScheduleListFilters.parse( - Object.fromEntries(searchParams.entries()) + const hasFilters = + searchParams.has("tasks") || searchParams.has("search") || searchParams.has("type"); + + return ( +
+ + + + {hasFilters && ( + + )} +
); +} - const hasFilters = searchParams.has("tasks") || searchParams.has("search"); +function ScheduleSearchInput() { + const navigate = useNavigate(); + const location = useOptimisticLocation(); + const searchParams = new URLSearchParams(location.search); + const initialSearch = searchParams.get("search") ?? ""; + const [text, setText] = useState(initialSearch); + const [isFocused, setIsFocused] = useState(false); + const inputRef = useRef(null); - const handleFilterChange = useCallback((filterType: string, value: string | undefined) => { - if (value) { - searchParams.set(filterType, value); + const throttledSearch = useThrottle((value: string) => { + const params = new URLSearchParams(location.search); + if (value.length > 0) { + params.set("search", value); } else { - searchParams.delete(filterType); + params.delete("search"); } - searchParams.delete("page"); - navigate(`${location.pathname}?${searchParams.toString()}`); - }, []); + params.delete("page"); + navigate(`${location.pathname}?${params.toString()}`); + }, 300); - const handleTaskChange = useCallback((value: string | typeof All) => { - handleFilterChange("tasks", value === "ALL" ? undefined : value); - }, []); + return ( + 0 ? "24rem" : "auto" }} + transition={{ + type: "spring", + stiffness: 300, + damping: 30, + }} + className="relative h-6 min-w-44" + > + + {isFocused && ( + + )} + +
+ { + setText(e.target.value); + throttledSearch(e.target.value); + }} + fullWidth + ref={inputRef} + className={cn(isFocused && "placeholder:text-text-dimmed/70")} + onFocus={() => setIsFocused(true)} + onBlur={() => setIsFocused(false)} + icon={} + /> +
+
+ ); +} - const handleTypeChange = useCallback((value: string | typeof All) => { - handleFilterChange("type", value === "ALL" ? undefined : value); - }, []); +const typeShortcut = { key: "t" }; - const handleSearchChange = useThrottle((value: string) => { - handleFilterChange("search", value.length === 0 ? undefined : value); - }, 300); +function PermanentTypeFilter() { + const navigate = useNavigate(); + const location = useOptimisticLocation(); + const searchParams = new URLSearchParams(location.search); + const currentType = searchParams.get("type") ?? undefined; + const triggerRef = useRef(null); - const clearFilters = useCallback(() => { - searchParams.delete("page"); - searchParams.delete("enabled"); - searchParams.delete("tasks"); - searchParams.delete("search"); - navigate(`${location.pathname}?${searchParams.toString()}`); - }, []); + useShortcutKeys({ + shortcut: typeShortcut, + action: (e) => { + e.preventDefault(); + e.stopPropagation(); + triggerRef.current?.click(); + }, + }); + + const handleChange = useCallback( + (value: string | string[]) => { + const selected = Array.isArray(value) ? value[0] : value; + const params = new URLSearchParams(location.search); + if (!selected || selected === "ALL") { + params.delete("type"); + } else { + params.set("type", selected); + } + params.delete("page"); + navigate(`${location.pathname}?${params.toString()}`); + }, + [location, navigate] + ); + + const typeLabel = currentType + ? scheduleTypeName(currentType.toUpperCase() as "IMPERATIVE" | "DECLARATIVE") + : "All types"; return ( -
- handleSearchChange(e.target.value)} - /> - - - - - - - + + + + )} + + ); +} - {hasFilters && ( - +const taskShortcut = { key: "k" }; + +function PermanentTaskFilter({ possibleTasks }: { possibleTasks: string[] }) { + const navigate = useNavigate(); + const location = useOptimisticLocation(); + const searchParams = new URLSearchParams(location.search); + const currentTask = searchParams.get("tasks") ?? undefined; + const triggerRef = useRef(null); + + useShortcutKeys({ + shortcut: taskShortcut, + action: (e) => { + e.preventDefault(); + e.stopPropagation(); + triggerRef.current?.click(); + }, + }); + + const handleChange = useCallback( + (value: string | string[]) => { + const selected = Array.isArray(value) ? value[0] : value; + const params = new URLSearchParams(location.search); + if (!selected || selected === "ALL") { + params.delete("tasks"); + } else { + params.set("tasks", selected); + } + params.delete("page"); + navigate(`${location.pathname}?${params.toString()}`); + }, + [location, navigate] + ); + + const taskLabel = currentTask ?? "All tasks"; + + return ( + + {() => ( + + + } /> + } + > + } + value={taskLabel} + removable={!!currentTask} + onRemove={() => handleChange("ALL")} + variant="secondary/small" + /> + + +
+ Filter by task + +
+
+
+ + + All tasks + {possibleTasks.map((task) => ( + + {task} + + ))} + + +
)} -
+
+ ); +} + +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 ( + - - - You've exceeded your limit - - You've used {limits.used}/{limits.limit} of your schedules. - - - {canUpgrade ? ( - - Upgrade - - ) : ( - Request more} - defaultValue="help" - /> - )} - - - - ) : ( - - New schedule - - )} @@ -219,6 +175,54 @@ export default function Page() { totalPages={totalPages} showPageNumbers={false} /> + {limits.used >= limits.limit ? ( + + + + + + You've exceeded your limit + + You've used {limits.used}/{limits.limit} of your schedules. + + + {canUpgrade ? ( + + Upgrade + + ) : ( + Request more + } + defaultValue="help" + /> + )} + + + + ) : ( + + New schedule + + )}
diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.new/route.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.new/route.tsx index 1e44dbdc00b..90a81a5c2c3 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.new/route.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.new/route.tsx @@ -403,7 +403,7 @@ export function UpsertScheduleForm({
Cancel From 6228ea793faf36826b86c9b0475e5712740fd3cc Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sat, 4 Apr 2026 16:39:03 +0100 Subject: [PATCH 07/51] Improved run filters UI --- .../app/components/runs/v3/BatchFilters.tsx | 108 +---- .../app/components/runs/v3/RunFilters.tsx | 455 ++++-------------- .../app/components/runs/v3/SharedFilters.tsx | 108 ++++- 3 files changed, 216 insertions(+), 455 deletions(-) diff --git a/apps/webapp/app/components/runs/v3/BatchFilters.tsx b/apps/webapp/app/components/runs/v3/BatchFilters.tsx index 2f76b5c083d..d4f0e90c1d8 100644 --- a/apps/webapp/app/components/runs/v3/BatchFilters.tsx +++ b/apps/webapp/app/components/runs/v3/BatchFilters.tsx @@ -11,9 +11,6 @@ import type { BatchTaskRunStatus, RuntimeEnvironment } from "@trigger.dev/databa import { type ReactNode, useCallback, useMemo, useRef, useState } from "react"; import { z } from "zod"; import { AppliedFilter } from "~/components/primitives/AppliedFilter"; -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, @@ -40,7 +37,13 @@ import { batchStatusTitle, descriptionForBatchStatus, } from "./BatchStatus"; -import { TimeFilter, appliedSummary, FilterMenuProvider } from "./SharedFilters"; +import { + TimeFilter, + appliedSummary, + FilterMenuProvider, + IdFilterDropdown, + type IdFilterDropdownProps, +} from "./SharedFilters"; import { StatusIcon } from "~/assets/icons/StatusIcon"; export const BatchStatus = z.enum(allBatchStatuses); @@ -218,91 +221,22 @@ function PermanentStatusFilter() { ); } -function BatchIdDropdown({ - trigger, - clearSearchValue, - searchValue, - onClose, -}: { - trigger: ReactNode; - clearSearchValue: () => void; - searchValue: string; - onClose?: () => void; -}) { - const [open, setOpen] = useState(); - const { value, replace } = useSearchParams(); - const batchIdValue = value("id"); - - const [batchId, setBatchId] = useState(batchIdValue); - - const apply = useCallback(() => { - clearSearchValue(); - replace({ - cursor: undefined, - direction: undefined, - id: batchId === "" ? undefined : batchId?.toString(), - }); - - setOpen(false); - }, [batchId, replace]); - - let error: string | undefined = undefined; - if (batchId) { - if (!batchId.startsWith("batch_")) { - error = "Batch IDs start with 'batch_'"; - } else if (batchId.length !== 27 && batchId.length !== 31) { - error = "Batch IDs are 27/32 characters long"; - } - } +function validateBatchId(value: string): string | undefined { + if (!value.startsWith("batch_")) return "Batch IDs start with 'batch_'"; + if (value.length !== 27 && value.length !== 31) return "Batch IDs are 27/32 characters long"; +} +function BatchIdDropdown( + props: Omit +) { return ( - - {trigger} - { - if (onClose) { - onClose(); - return false; - } - - return true; - }} - className="max-w-[min(32ch,var(--popover-available-width))]" - > -
-
- - setBatchId(e.target.value)} - variant="small" - className="w-[29ch] font-mono" - spellCheck={false} - /> - {error ? {error} : null} -
-
- - -
-
-
-
+ ); } diff --git a/apps/webapp/app/components/runs/v3/RunFilters.tsx b/apps/webapp/app/components/runs/v3/RunFilters.tsx index 2ab45d32eeb..33b4d1fdf35 100644 --- a/apps/webapp/app/components/runs/v3/RunFilters.tsx +++ b/apps/webapp/app/components/runs/v3/RunFilters.tsx @@ -28,9 +28,6 @@ import { import { AppliedFilter } from "~/components/primitives/AppliedFilter"; import { Badge } from "~/components/primitives/Badge"; import { DateTime } from "~/components/primitives/DateTime"; -import { FormError } from "~/components/primitives/FormError"; -import { Input } from "~/components/primitives/Input"; -import { Label } from "~/components/primitives/Label"; import { MiddleTruncate } from "~/components/primitives/MiddleTruncate"; import { Paragraph } from "~/components/primitives/Paragraph"; import { @@ -65,7 +62,14 @@ import { type loader as versionsLoader } from "~/routes/resources.orgs.$organiza import { Button } from "../../primitives/Buttons"; import { AIFilterInput } from "./AIFilterInput"; import { BulkActionTypeCombo } from "./BulkAction"; -import { appliedSummary, FilterMenuProvider, TimeFilter, timeFilters } from "./SharedFilters"; +import { + IdFilterDropdown, + type IdFilterDropdownProps, + appliedSummary, + FilterMenuProvider, + TimeFilter, + timeFilters, +} from "./SharedFilters"; import { allTaskRunStatuses, descriptionForTaskRunStatus, @@ -499,7 +503,7 @@ function MainMenu({ searchValue, trigger, clearSearchValue, setFilterType }: Men icon={type.icon} shortcut={shortcutFromIndex(index, { shortcutsEnabled: true })} > - {type.title} + {type.title} ))} @@ -603,22 +607,26 @@ function PermanentStatusFilter() { } /> + } + /> } > {hasStatuses ? ( runStatusTitle(v as TaskRunStatus)) - )} + value={appliedSummary(statuses.map((v) => runStatusTitle(v as TaskRunStatus)))} onRemove={() => del(["statuses", "cursor", "direction"])} variant="secondary/small" + className="pl-1" /> ) : ( -
- {filterIcon("statuses")} +
+
+
+
Status
)} @@ -692,6 +700,7 @@ function TasksDropdown({ icon={ } + className="text-text-bright" > @@ -728,7 +737,10 @@ function PermanentTasksFilter({ possibleTasks }: Pick} /> + } + /> } > {hasTasks ? ( @@ -743,9 +755,10 @@ function PermanentTasksFilter({ possibleTasks }: Pick del(["tasks", "cursor", "direction"])} variant="secondary/small" + className="pl-1" /> ) : ( -
+
{filterIcon("tasks")} Tasks
@@ -956,15 +969,17 @@ function TagsDropdown({ return true; }} > - ( -
- - {fetcher.state === "loading" && } -
- )} - /> + {(filtered.length > 0 || fetcher.state === "loading" || searchValue.length > 0) && ( + ( +
+ + {fetcher.state === "loading" && } +
+ )} + /> + )} {filtered.length > 0 ? filtered.map((tag, index) => ( @@ -1136,6 +1151,7 @@ function QueuesDropdown({ ) } + className="text-text-bright" > {queue.name} @@ -1221,15 +1237,15 @@ function MachinesDropdown({ return true; }} > - {filtered.map((item, index) => ( - + ))} @@ -1377,7 +1393,12 @@ export function VersionsDropdown({ {filtered.length > 0 ? filtered.map((version) => ( - + } + className="text-text-bright" + > {version.version} {version.isCurrent ? Current : null} @@ -1459,102 +1480,28 @@ function RootOnlyToggle({ defaultValue }: { defaultValue: boolean }) {
Toggle root only - +
); } -function RunIdDropdown({ - trigger, - clearSearchValue, - searchValue, - onClose, -}: { - trigger: ReactNode; - clearSearchValue: () => void; - searchValue: string; - onClose?: () => void; -}) { - const [open, setOpen] = useState(); - const { value, replace } = useSearchParams(); - const runIdValue = value("runId"); - - const [runId, setRunId] = useState(runIdValue); - - const apply = useCallback(() => { - clearSearchValue(); - replace({ - cursor: undefined, - direction: undefined, - runId: runId === "" ? undefined : runId?.toString(), - }); - - setOpen(false); - }, [runId, replace]); - - let error: string | undefined = undefined; - if (runId) { - if (!runId.startsWith("run_")) { - error = "Run IDs start with 'run_'"; - } else if (runId.length !== 25 && runId.length !== 29) { - error = "Run IDs are 25/30 characters long"; - } - } +function validateRunId(value: string): string | undefined { + if (!value.startsWith("run_")) return "Run IDs start with 'run_'"; + if (value.length !== 25 && value.length !== 29) return "Run IDs are 25/30 characters long"; +} +function RunIdDropdown(props: Omit) { return ( - - {trigger} - { - if (onClose) { - onClose(); - return false; - } - - return true; - }} - className="max-w-[min(32ch,var(--popover-available-width))]" - > -
-
- - setRunId(e.target.value)} - variant="small" - className="w-[27ch] font-mono" - spellCheck={false} - /> - {error ? {error} : null} -
-
- - -
-
-
-
+ ); } @@ -1590,91 +1537,20 @@ function AppliedRunIdFilter() { ); } -function BatchIdDropdown({ - trigger, - clearSearchValue, - searchValue, - onClose, -}: { - trigger: ReactNode; - clearSearchValue: () => void; - searchValue: string; - onClose?: () => void; -}) { - const [open, setOpen] = useState(); - const { value, replace } = useSearchParams(); - const batchIdValue = value("batchId"); - - const [batchId, setBatchId] = useState(batchIdValue); - - const apply = useCallback(() => { - clearSearchValue(); - replace({ - cursor: undefined, - direction: undefined, - batchId: batchId === "" ? undefined : batchId?.toString(), - }); - - setOpen(false); - }, [batchId, replace]); - - let error: string | undefined = undefined; - if (batchId) { - if (!batchId.startsWith("batch_")) { - error = "Batch IDs start with 'batch_'"; - } else if (batchId.length !== 27 && batchId.length !== 31) { - error = "Batch IDs are 27 or 31 characters long"; - } - } +function validateBatchId(value: string): string | undefined { + if (!value.startsWith("batch_")) return "Batch IDs start with 'batch_'"; + if (value.length !== 27 && value.length !== 31) return "Batch IDs are 27 or 31 characters long"; +} +function BatchIdDropdown(props: Omit) { return ( - - {trigger} - { - if (onClose) { - onClose(); - return false; - } - - return true; - }} - className="max-w-[min(32ch,var(--popover-available-width))]" - > -
-
- - setBatchId(e.target.value)} - variant="small" - className="w-[29ch] font-mono" - spellCheck={false} - /> - {error ? {error} : null} -
-
- - -
-
-
-
+ ); } @@ -1710,91 +1586,20 @@ function AppliedBatchIdFilter() { ); } -function ScheduleIdDropdown({ - trigger, - clearSearchValue, - searchValue, - onClose, -}: { - trigger: ReactNode; - clearSearchValue: () => void; - searchValue: string; - onClose?: () => void; -}) { - const [open, setOpen] = useState(); - const { value, replace } = useSearchParams(); - const scheduleIdValue = value("scheduleId"); - - const [scheduleId, setScheduleId] = useState(scheduleIdValue); - - const apply = useCallback(() => { - clearSearchValue(); - replace({ - cursor: undefined, - direction: undefined, - scheduleId: scheduleId === "" ? undefined : scheduleId?.toString(), - }); - - setOpen(false); - }, [scheduleId, replace]); - - let error: string | undefined = undefined; - if (scheduleId) { - if (!scheduleId.startsWith("sched")) { - error = "Schedule IDs start with 'sched_'"; - } else if (scheduleId.length !== 27) { - error = "Schedule IDs are 27 characters long"; - } - } +function validateScheduleId(value: string): string | undefined { + if (!value.startsWith("sched")) return "Schedule IDs start with 'sched_'"; + if (value.length !== 27) return "Schedule IDs are 27 characters long"; +} +function ScheduleIdDropdown(props: Omit) { return ( - - {trigger} - { - if (onClose) { - onClose(); - return false; - } - - return true; - }} - className="max-w-[min(32ch,var(--popover-available-width))]" - > -
-
- - setScheduleId(e.target.value)} - variant="small" - className="w-[29ch] font-mono" - spellCheck={false} - /> - {error ? {error} : null} -
-
- - -
-
-
-
+ ); } @@ -1830,89 +1635,19 @@ function AppliedScheduleIdFilter() { ); } -function ErrorIdDropdown({ - trigger, - clearSearchValue, - searchValue, - onClose, -}: { - trigger: ReactNode; - clearSearchValue: () => void; - searchValue: string; - onClose?: () => void; -}) { - const [open, setOpen] = useState(); - const { value, replace } = useSearchParams(); - const errorIdValue = value("errorId"); - - const [errorId, setErrorId] = useState(errorIdValue); - - const apply = useCallback(() => { - clearSearchValue(); - replace({ - cursor: undefined, - direction: undefined, - errorId: errorId === "" ? undefined : errorId?.toString(), - }); - - setOpen(false); - }, [errorId, replace]); - - let error: string | undefined = undefined; - if (errorId) { - if (!errorId.startsWith("error_")) { - error = "Error IDs start with 'error_'"; - } - } +function validateErrorId(value: string): string | undefined { + if (!value.startsWith("error_")) return "Error IDs start with 'error_'"; +} +function ErrorIdDropdown(props: Omit) { return ( - - {trigger} - { - if (onClose) { - onClose(); - return false; - } - - return true; - }} - className="max-w-[min(32ch,var(--popover-available-width))]" - > -
-
- - setErrorId(e.target.value)} - variant="small" - className="w-[29ch] font-mono" - spellCheck={false} - /> - {error ? {error} : null} -
-
- - -
-
-
-
+ ); } diff --git a/apps/webapp/app/components/runs/v3/SharedFilters.tsx b/apps/webapp/app/components/runs/v3/SharedFilters.tsx index 3e24f601f2a..a0160dc3f3c 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,97 @@ 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 apply = useCallback(() => { + clearSearchValue(); + replace({ + cursor: undefined, + direction: undefined, + [paramKey]: inputValue === "" ? undefined : inputValue?.toString(), + }); + + setOpen(false); + }, [inputValue, replace, paramKey]); + + 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} +
+
+ + +
+
+
+
+ ); +} From 0f3159c4a3cd8ea86a656aa1ecba50d471391c0d Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sat, 4 Apr 2026 16:56:09 +0100 Subject: [PATCH 08/51] Run table row highlight fix --- apps/webapp/app/components/runs/v3/TaskRunsTable.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx b/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx index 22f24ca9d97..774ab80c6ed 100644 --- a/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx +++ b/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx @@ -436,7 +436,11 @@ export function TaskRunsTable({
- {run.isTest ? : "–"} + {run.isTest ? ( + + ) : ( + "–" + )} {run.createdAt ? : "–"} From 8e86ab5dbab9118a48d1f59139bddc45f7e15210 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sat, 4 Apr 2026 17:11:52 +0100 Subject: [PATCH 09/51] run table loading state background color fix --- apps/webapp/app/components/runs/v3/TaskRunsTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx b/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx index 774ab80c6ed..346fd25eee2 100644 --- a/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx +++ b/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx @@ -462,7 +462,7 @@ export function TaskRunsTable({ {isLoading && ( Loading… From b5a87e3179e315e299532c46579f38e921a039de Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sat, 4 Apr 2026 17:28:40 +0100 Subject: [PATCH 10/51] Better loading bg color --- .../route.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx index eaa040c4081..1a92691ab08 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx @@ -286,7 +286,7 @@ function BatchesTable({ batches, hasFilters, filters }: BatchList) { {isLoading && ( Loading… From a31c0edba700c5b31fa5777cc4437bdd0a9fbda8 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sat, 4 Apr 2026 17:34:04 +0100 Subject: [PATCH 11/51] Batch filter improvements --- .../app/components/runs/v3/BatchFilters.tsx | 30 ++++++++------- .../app/components/runs/v3/RunFilters.tsx | 19 ++++++++-- .../components/runs/v3/ScheduleFilters.tsx | 38 +++++++------------ 3 files changed, 45 insertions(+), 42 deletions(-) diff --git a/apps/webapp/app/components/runs/v3/BatchFilters.tsx b/apps/webapp/app/components/runs/v3/BatchFilters.tsx index d4f0e90c1d8..e92423bf9b3 100644 --- a/apps/webapp/app/components/runs/v3/BatchFilters.tsx +++ b/apps/webapp/app/components/runs/v3/BatchFilters.tsx @@ -8,12 +8,11 @@ import { } from "@heroicons/react/20/solid"; import { Form } from "@remix-run/react"; import type { BatchTaskRunStatus, RuntimeEnvironment } from "@trigger.dev/database"; -import { type ReactNode, useCallback, useMemo, useRef, useState } from "react"; +import { type ReactNode, useCallback, useRef, useState } from "react"; import { z } from "zod"; import { AppliedFilter } from "~/components/primitives/AppliedFilter"; import { Paragraph } from "~/components/primitives/Paragraph"; import { - ComboBox, SelectItem, SelectList, SelectPopover, @@ -109,10 +108,6 @@ function StatusDropdown({ replace({ statuses: values, cursor: undefined, direction: undefined }); }; - const filtered = useMemo(() => { - return statuses.filter((item) => item.title.toLowerCase().includes(searchValue.toLowerCase())); - }, [searchValue]); - return ( {trigger} @@ -127,9 +122,8 @@ function StatusDropdown({ return true; }} > - - {filtered.map((item, index) => ( + {statuses.map((item, index) => ( } /> + } + /> } > {hasStatuses ? ( @@ -193,10 +190,13 @@ function PermanentStatusFilter() { )} onRemove={() => del(["statuses", "cursor", "direction"])} variant="secondary/small" + className="pl-1" /> ) : ( -
- +
+
+
+
Status
)} @@ -266,7 +266,10 @@ function PermanentBatchIdFilter() { } /> + } + /> } > {hasBatchId ? ( @@ -276,9 +279,10 @@ function PermanentBatchIdFilter() { value={batchId} onRemove={() => del(["id", "cursor", "direction"])} variant="secondary/small" + className="pl-1" /> ) : ( -
+
Batch ID
diff --git a/apps/webapp/app/components/runs/v3/RunFilters.tsx b/apps/webapp/app/components/runs/v3/RunFilters.tsx index 33b4d1fdf35..28bf63238ae 100644 --- a/apps/webapp/app/components/runs/v3/RunFilters.tsx +++ b/apps/webapp/app/components/runs/v3/RunFilters.tsx @@ -1492,7 +1492,12 @@ function validateRunId(value: string): string | undefined { if (value.length !== 25 && value.length !== 29) return "Run IDs are 25/30 characters long"; } -function RunIdDropdown(props: Omit) { +function RunIdDropdown( + props: Omit< + IdFilterDropdownProps, + "label" | "placeholder" | "paramKey" | "validate" | "inputWidth" + > +) { return ( ) { +function BatchIdDropdown( + props: Omit +) { return ( ) { +function ScheduleIdDropdown( + props: Omit +) { return ( ) { +function ErrorIdDropdown( + props: Omit +) { return ( - {hasFilters && ( - - )} + {hasFilters && }
); } @@ -161,16 +159,15 @@ function PermanentTypeFilter() { return ( {() => ( - + } /> + } + /> } >
Filter by type - +
@@ -247,16 +240,15 @@ function PermanentTaskFilter({ possibleTasks }: { possibleTasks: string[] }) { return ( {() => ( - + } /> + } + /> } >
Filter by task - +
From a498fae7bee7f50ae5d3855d49143ea933b7881b Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sat, 4 Apr 2026 17:39:02 +0100 Subject: [PATCH 12/51] Copyable batch ID from the batch table --- .../route.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx index 1a92691ab08..17dcfbc4619 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx @@ -30,6 +30,7 @@ import { TableHeader, TableHeaderCell, TableRow, + CopyableTableCell, } from "~/components/primitives/Table"; import { SimpleTooltip } from "~/components/primitives/Tooltip"; import { BatchFilters, BatchListFilters } from "~/components/runs/v3/BatchFilters"; @@ -234,9 +235,9 @@ function BatchesTable({ batches, hasFilters, filters }: BatchList) { return ( - + {batch.friendlyId} - + {batch.batchVersion === "v1" ? ( From c90757b6761d4516dc5bbe5307629a5bdeeaf55d Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sat, 4 Apr 2026 18:25:24 +0100 Subject: [PATCH 13/51] Scheduled filters improvements --- .../components/runs/v3/ScheduleFilters.tsx | 44 +++++++++++++------ 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/apps/webapp/app/components/runs/v3/ScheduleFilters.tsx b/apps/webapp/app/components/runs/v3/ScheduleFilters.tsx index 2eb07094466..cf1613739a1 100644 --- a/apps/webapp/app/components/runs/v3/ScheduleFilters.tsx +++ b/apps/webapp/app/components/runs/v3/ScheduleFilters.tsx @@ -1,5 +1,5 @@ import * as Ariakit from "@ariakit/react"; -import { MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/20/solid"; +import { ClockIcon, MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/20/solid"; import { useNavigate } from "@remix-run/react"; import { AnimatePresence, motion } from "framer-motion"; import { useCallback, useRef, useState } from "react"; @@ -22,6 +22,7 @@ import { cn } from "~/utils/cn"; import { Button } from "../../primitives/Buttons"; import { ScheduleTypeCombo, ScheduleTypeIcon, scheduleTypeName } from "./ScheduleType"; import { FilterMenuProvider } from "./SharedFilters"; +import { ScheduleIcon } from "~/assets/icons/ScheduleIcon"; export const ScheduleListFilters = z.object({ page: z.coerce.number().default(1), @@ -187,12 +188,20 @@ function PermanentTypeFilter() { - All types + + All types + - +
+ + {scheduleTypeName("DECLARATIVE")} +
- +
+ + {scheduleTypeName("IMPERATIVE")} +
@@ -253,7 +262,7 @@ function PermanentTaskFilter({ possibleTasks }: { possibleTasks: string[] }) { > } + icon={} value={taskLabel} removable={!!currentTask} onRemove={() => handleChange("ALL")} @@ -269,9 +278,16 @@ function PermanentTaskFilter({ possibleTasks }: { possibleTasks: string[] }) { - All tasks + + All tasks + {possibleTasks.map((task) => ( - + } + className="text-text-bright" + > {task} ))} @@ -297,11 +313,13 @@ function ClearFiltersButton() { }, [location, navigate]); return ( -
); } From 33f35a14c9f8a5e8f54518f5ebd32a3d7000caa4 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sat, 4 Apr 2026 20:03:54 +0100 Subject: [PATCH 14/51] Adds correct selected row classes to schedule table --- .../route.tsx | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules/route.tsx index 18e5f4b1702..b3181eefa36 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules/route.tsx @@ -420,58 +420,59 @@ function SchedulesTable({ }`; const isSelected = scheduleParam === schedule.friendlyId; const cellClass = schedule.active ? "" : "opacity-50"; + const selectedActionClass = isSelected ? "text-text-bright" : undefined; return ( - + {schedule.friendlyId} - + {schedule.taskIdentifier} - + - + {schedule.type === "IMPERATIVE" ? schedule.externalId ? schedule.externalId : "–" : "N/A"} - + {schedule.cron} - + {schedule.cronDescription} - + {schedule.timezone} - + - + {schedule.lastRun ? ( ) : ( "–" )} - + {schedule.type === "IMPERATIVE" ? schedule.userProvidedDeduplicationKey ? schedule.deduplicationKey : "–" : "N/A"} - +
{schedule.environments.map((env) => ( ))}
- + {schedule.type === "IMPERATIVE" ? ( ) : ( From e20c30b536f1ead1c191664909cca8484433a141 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sat, 4 Apr 2026 20:26:06 +0100 Subject: [PATCH 15/51] Updates filters on Waitpoints page --- .../runs/v3/WaitpointTokenFilters.tsx | 628 ++++++++---------- .../route.tsx | 5 - 2 files changed, 261 insertions(+), 372 deletions(-) diff --git a/apps/webapp/app/components/runs/v3/WaitpointTokenFilters.tsx b/apps/webapp/app/components/runs/v3/WaitpointTokenFilters.tsx index ae416394147..0a76fb63446 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, TrashIcon } 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({ @@ -69,10 +72,12 @@ export function WaitpointTokenFilters(props: WaitpointTokenFiltersProps) { searchParams.has("idempotencyKey"); return ( -
- - - +
+ + + + + {hasFilters && (
- } - variant={"secondary/small"} - shortcut={shortcut} - tooltipTitle={"Filter runs"} - > - Filter - - ); - - return ( - setFilterType(undefined)}> - {(search, setSearch) => ( - setSearch("")} - trigger={filterTrigger} - filterType={filterType} - setFilterType={setFilterType} - /> - )} - - ); -} - -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 +128,6 @@ function StatusDropdown({ return true; }} > - {filtered.map((item, index) => { return ( @@ -249,7 +139,7 @@ function StatusDropdown({ - + @@ -267,30 +157,70 @@ 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" + /> + ) : ( +
+
+
+
+ Status +
)} - onRemove={() => del(["statuses", "cursor", "direction"])} - variant="secondary/small" - /> - + + +
+ Filter by status + +
+
+ } searchValue={search} clearSearchValue={() => setSearch("")} @@ -366,19 +296,21 @@ function TagsDropdown({ return true; }} > - ( -
- - {fetcher.state === "loading" && } -
- )} - /> + {!(filtered.length === 0 && fetcher.state !== "loading") && ( + ( +
+ + {fetcher.state === "loading" && } +
+ )} + /> + )} {filtered.length > 0 - ? filtered.map((tag, index) => ( - + ? filtered.map((tag) => ( + {tag} )) @@ -392,29 +324,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 +391,80 @@ 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 +474,79 @@ 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/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tokens/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tokens/route.tsx index 782f3b132ff..54102b86b54 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tokens/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tokens/route.tsx @@ -234,11 +234,6 @@ export default function Page() { - {(pagination.next || pagination.previous) && ( -
- -
- )}
From 1f74bd8c2865838745ee5e696d15c797407d33a6 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sat, 4 Apr 2026 20:28:58 +0100 Subject: [PATCH 16/51] Improved shortcuts to scheduled filters --- apps/webapp/app/components/runs/v3/ScheduleFilters.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/webapp/app/components/runs/v3/ScheduleFilters.tsx b/apps/webapp/app/components/runs/v3/ScheduleFilters.tsx index cf1613739a1..f503b1306f2 100644 --- a/apps/webapp/app/components/runs/v3/ScheduleFilters.tsx +++ b/apps/webapp/app/components/runs/v3/ScheduleFilters.tsx @@ -49,8 +49,8 @@ export function ScheduleFilters({ possibleTasks }: ScheduleFiltersProps) { return (
- + {hasFilters && }
); @@ -120,7 +120,7 @@ function ScheduleSearchInput() { ); } -const typeShortcut = { key: "t" }; +const typeShortcut = { key: "y" }; function PermanentTypeFilter() { const navigate = useNavigate(); @@ -211,7 +211,7 @@ function PermanentTypeFilter() { ); } -const taskShortcut = { key: "k" }; +const taskShortcut = { key: "t" }; function PermanentTaskFilter({ possibleTasks }: { possibleTasks: string[] }) { const navigate = useNavigate(); From 3c2df74d46d00a9ee6803c7b2e81f4838d69b5a1 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sat, 4 Apr 2026 20:33:07 +0100 Subject: [PATCH 17/51] Consistent clear filters icon --- apps/webapp/app/components/runs/v3/WaitpointTokenFilters.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/components/runs/v3/WaitpointTokenFilters.tsx b/apps/webapp/app/components/runs/v3/WaitpointTokenFilters.tsx index 0a76fb63446..9c3e157bc66 100644 --- a/apps/webapp/app/components/runs/v3/WaitpointTokenFilters.tsx +++ b/apps/webapp/app/components/runs/v3/WaitpointTokenFilters.tsx @@ -1,5 +1,5 @@ import * as Ariakit from "@ariakit/react"; -import { 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 } from "lucide-react"; @@ -80,7 +80,7 @@ export function WaitpointTokenFilters(props: WaitpointTokenFiltersProps) { {hasFilters && ( -
From 88fd5954a082d4e1f8dc63b2b6ba1ca51f6ed4ed Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Wed, 15 Apr 2026 17:50:00 +0100 Subject: [PATCH 18/51] Task page search bar improvements --- .../route.tsx | 48 +++++++++++++------ 1 file changed, 34 insertions(+), 14 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 2cf8b844a9e..d6716adb5fb 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 @@ -6,9 +6,9 @@ import { ExclamationTriangleIcon, LightBulbIcon, MagnifyingGlassIcon, - XMarkIcon, UserPlusIcon, VideoCameraIcon, + XMarkIcon, } from "@heroicons/react/20/solid"; import { json, type MetaFunction } from "@remix-run/node"; import { Link, useFetcher, useRevalidator } from "@remix-run/react"; @@ -62,6 +62,7 @@ import { TableHeaderCell, TableRow, } from "~/components/primitives/Table"; +import { ShortcutKey } from "~/components/primitives/ShortcutKey"; import { SimpleTooltip } from "~/components/primitives/Tooltip"; import TooltipPortal from "~/components/primitives/TooltipPortal"; import { TaskFileName } from "~/components/runs/v3/TaskPath"; @@ -250,10 +251,11 @@ export default function Page() { placeholder="Search tasks…" autoFocus /> - {!showUsefulLinks && ( + {!showUsefulLinks && ( + { + e.preventDefault(); + onChange(""); + }} + className="-mr-1 flex size-4.5 items-center justify-center rounded-[2px] border border-text-dimmed/40 text-text-dimmed transition hover:bg-charcoal-600 hover:text-text-bright" + > + + + } + content={ +
+ Clear field + +
+ } + className="px-2 py-1.5 text-xs" + disableHoverableContent + /> ) : undefined } /> From d9c730fcf05b6b92dc6c6a4f4d238b32d0e498d6 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Wed, 15 Apr 2026 17:59:56 +0100 Subject: [PATCH 19/51] UX behaviour improvements to the search input component --- .../app/components/primitives/SearchInput.tsx | 49 ++++++++++++------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/apps/webapp/app/components/primitives/SearchInput.tsx b/apps/webapp/app/components/primitives/SearchInput.tsx index 639a4d1a737..eed0a54a4df 100644 --- a/apps/webapp/app/components/primitives/SearchInput.tsx +++ b/apps/webapp/app/components/primitives/SearchInput.tsx @@ -3,6 +3,7 @@ import { motion } from "framer-motion"; import { useCallback, useEffect, useRef, useState } from "react"; import { Input } from "~/components/primitives/Input"; import { ShortcutKey } from "~/components/primitives/ShortcutKey"; +import { SimpleTooltip } from "~/components/primitives/Tooltip"; import { useSearchParams } from "~/hooks/useSearchParam"; import { cn } from "~/utils/cn"; @@ -43,15 +44,10 @@ export function SearchInput({ } }, [text, replace, del, resetParams]); - const handleClear = useCallback( - (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - setText(""); - del(["search", ...resetParams]); - }, - [del, resetParams] - ); + const handleClear = useCallback(() => { + setText(""); + del(["search", ...resetParams]); + }, [del, resetParams]); return (
@@ -81,24 +77,41 @@ export function SearchInput({ handleSubmit(); } if (e.key === "Escape") { + e.stopPropagation(); + handleClear(); e.currentTarget.blur(); } }} onFocus={() => setIsFocused(true)} onBlur={() => setIsFocused(false)} - icon={} + icon={} accessory={ text.length > 0 ? (
- + { + e.preventDefault(); + handleClear(); + }} + className="flex size-4.5 items-center justify-center rounded-[2px] border border-text-dimmed/40 text-text-dimmed transition hover:bg-charcoal-600 hover:text-text-bright" + > + + + } + content={ +
+ Clear field + +
+ } + className="px-2 py-1.5 text-xs" + disableHoverableContent + />
) : undefined } From 98c1d5535c44506512356ca5c66ec5bb7c0a1bd4 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Wed, 15 Apr 2026 18:00:04 +0100 Subject: [PATCH 20/51] Scheduled filters improvements --- .../components/runs/v3/ScheduleFilters.tsx | 76 ++----------------- 1 file changed, 5 insertions(+), 71 deletions(-) diff --git a/apps/webapp/app/components/runs/v3/ScheduleFilters.tsx b/apps/webapp/app/components/runs/v3/ScheduleFilters.tsx index f503b1306f2..33bbd62f2c8 100644 --- a/apps/webapp/app/components/runs/v3/ScheduleFilters.tsx +++ b/apps/webapp/app/components/runs/v3/ScheduleFilters.tsx @@ -1,12 +1,10 @@ import * as Ariakit from "@ariakit/react"; -import { ClockIcon, MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/20/solid"; +import { ClockIcon, XMarkIcon } from "@heroicons/react/20/solid"; import { useNavigate } from "@remix-run/react"; -import { AnimatePresence, motion } from "framer-motion"; -import { useCallback, useRef, useState } from "react"; +import { useCallback, useRef } from "react"; import { z } from "zod"; -import { TaskIcon } from "~/assets/icons/TaskIcon"; import { AppliedFilter } from "~/components/primitives/AppliedFilter"; -import { Input } from "~/components/primitives/Input"; +import { SearchInput } from "~/components/primitives/SearchInput"; import { SelectItem, SelectList, @@ -15,14 +13,10 @@ import { } from "~/components/primitives/Select"; import { ShortcutKey } from "~/components/primitives/ShortcutKey"; import { useOptimisticLocation } from "~/hooks/useOptimisticLocation"; -import { useSearchParams } from "~/hooks/useSearchParam"; import { useShortcutKeys } from "~/hooks/useShortcutKeys"; -import { useThrottle } from "~/hooks/useThrottle"; -import { cn } from "~/utils/cn"; import { Button } from "../../primitives/Buttons"; -import { ScheduleTypeCombo, ScheduleTypeIcon, scheduleTypeName } from "./ScheduleType"; +import { ScheduleTypeIcon, scheduleTypeName } from "./ScheduleType"; import { FilterMenuProvider } from "./SharedFilters"; -import { ScheduleIcon } from "~/assets/icons/ScheduleIcon"; export const ScheduleListFilters = z.object({ page: z.coerce.number().default(1), @@ -57,67 +51,7 @@ export function ScheduleFilters({ possibleTasks }: ScheduleFiltersProps) { } function ScheduleSearchInput() { - const navigate = useNavigate(); - const location = useOptimisticLocation(); - const searchParams = new URLSearchParams(location.search); - const initialSearch = searchParams.get("search") ?? ""; - const [text, setText] = useState(initialSearch); - const [isFocused, setIsFocused] = useState(false); - const inputRef = useRef(null); - - const throttledSearch = useThrottle((value: string) => { - const params = new URLSearchParams(location.search); - if (value.length > 0) { - params.set("search", value); - } else { - params.delete("search"); - } - params.delete("page"); - navigate(`${location.pathname}?${params.toString()}`); - }, 300); - - return ( - 0 ? "24rem" : "auto" }} - transition={{ - type: "spring", - stiffness: 300, - damping: 30, - }} - className="relative h-6 min-w-44" - > - - {isFocused && ( - - )} - -
- { - setText(e.target.value); - throttledSearch(e.target.value); - }} - fullWidth - ref={inputRef} - className={cn(isFocused && "placeholder:text-text-dimmed/70")} - onFocus={() => setIsFocused(true)} - onBlur={() => setIsFocused(false)} - icon={} - /> -
-
- ); + return ; } const typeShortcut = { key: "y" }; From dd5e063cd3e32831fb2d1fdd8bc97e6ff3b82f9b Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Wed, 15 Apr 2026 18:12:36 +0100 Subject: [PATCH 21/51] AI search field can now be cleared with ESC key --- .../app/components/runs/v3/AIFilterInput.tsx | 42 ++++++++++++++++--- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/apps/webapp/app/components/runs/v3/AIFilterInput.tsx b/apps/webapp/app/components/runs/v3/AIFilterInput.tsx index ef8384894da..733c2edfd2f 100644 --- a/apps/webapp/app/components/runs/v3/AIFilterInput.tsx +++ b/apps/webapp/app/components/runs/v3/AIFilterInput.tsx @@ -1,3 +1,4 @@ +import { XMarkIcon } from "@heroicons/react/20/solid"; import { useFetcher, useNavigate } from "@remix-run/react"; import { AnimatePresence, motion } from "framer-motion"; import { useEffect, useRef, useState } from "react"; @@ -6,6 +7,7 @@ import { Input } from "~/components/primitives/Input"; import { Popover, PopoverContent, PopoverTrigger } from "~/components/primitives/Popover"; import { ShortcutKey } from "~/components/primitives/ShortcutKey"; import { Spinner } from "~/components/primitives/Spinner"; +import { SimpleTooltip } from "~/components/primitives/Tooltip"; import { useEnvironment } from "~/hooks/useEnvironment"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; @@ -107,6 +109,11 @@ export function AIFilterInput() { form.requestSubmit(); } } + if (e.key === "Escape") { + e.stopPropagation(); + setText(""); + e.currentTarget.blur(); + } }} onFocus={() => setIsFocused(true)} onBlur={() => { @@ -125,11 +132,36 @@ export function AIFilterInput() { className="size-4 opacity-80" /> ) : text.length > 0 ? ( - +
+ + { + e.preventDefault(); + setText(""); + }} + className="flex size-4.5 items-center justify-center rounded-[2px] border border-text-dimmed/40 text-text-dimmed transition hover:bg-charcoal-600 hover:text-text-bright" + > + + + } + content={ +
+ Clear field + +
+ } + className="px-2 py-1.5 text-xs" + disableHoverableContent + /> +
) : undefined } /> From 47a6952b440329df84269f4c4d0d9d8a32dae60a Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 16 Apr 2026 13:26:10 +0100 Subject: [PATCH 22/51] =?UTF-8?q?Search=20input=20component=20can=20handle?= =?UTF-8?q?=20both=20url=20param=20as=20=E2=80=98search=E2=80=99=20and=20?= =?UTF-8?q?=E2=80=98query=E2=80=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/components/primitives/SearchInput.tsx | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/apps/webapp/app/components/primitives/SearchInput.tsx b/apps/webapp/app/components/primitives/SearchInput.tsx index eed0a54a4df..dadeac42260 100644 --- a/apps/webapp/app/components/primitives/SearchInput.tsx +++ b/apps/webapp/app/components/primitives/SearchInput.tsx @@ -9,6 +9,8 @@ import { cn } from "~/utils/cn"; export type SearchInputProps = { placeholder?: string; + /** The URL search param name to read/write. Defaults to "search". */ + paramName?: string; /** Additional URL params to reset when searching or clearing (e.g. pagination). Defaults to ["cursor", "direction"]. */ resetParams?: string[]; autoFocus?: boolean; @@ -16,6 +18,7 @@ export type SearchInputProps = { export function SearchInput({ placeholder = "Search logs…", + paramName = "search", resetParams = ["cursor", "direction"], autoFocus, }: SearchInputProps) { @@ -23,31 +26,31 @@ export function SearchInput({ const { value, replace, del } = useSearchParams(); - const initialSearch = value("search") ?? ""; + const initialSearch = value(paramName) ?? ""; const [text, setText] = useState(initialSearch); const [isFocused, setIsFocused] = useState(false); useEffect(() => { - const urlSearch = value("search") ?? ""; + const urlSearch = value(paramName) ?? ""; if (urlSearch !== text && !isFocused) { setText(urlSearch); } - }, [value, text, isFocused]); + }, [value, text, isFocused, paramName]); const handleSubmit = useCallback(() => { const resetValues = Object.fromEntries(resetParams.map((p) => [p, undefined])); if (text.trim()) { - replace({ search: text.trim(), ...resetValues }); + replace({ [paramName]: text.trim(), ...resetValues }); } else { - del(["search", ...resetParams]); + del([paramName, ...resetParams]); } - }, [text, replace, del, resetParams]); + }, [text, replace, del, resetParams, paramName]); const handleClear = useCallback(() => { setText(""); - del(["search", ...resetParams]); - }, [del, resetParams]); + del([paramName, ...resetParams]); + }, [del, resetParams, paramName]); return (
From dc35d5a7952862a69353dcf094ebd9bfc7ebb139 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 16 Apr 2026 13:26:22 +0100 Subject: [PATCH 23/51] Updates search field on Query page --- .../route.tsx | 67 ++----------------- 1 file changed, 4 insertions(+), 63 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx index b33fc1e809b..d217f3658bb 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx @@ -3,7 +3,6 @@ import { ArrowUpCircleIcon, BookOpenIcon, ChatBubbleLeftEllipsisIcon, - MagnifyingGlassIcon, PauseIcon, PlayIcon, RectangleStackIcon, @@ -32,6 +31,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "~/components import { FormButtons } from "~/components/primitives/FormButtons"; import { Header3 } from "~/components/primitives/Headers"; import { Input } from "~/components/primitives/Input"; +import { SearchInput } from "~/components/primitives/SearchInput"; import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader"; import { PaginationControls } from "~/components/primitives/Pagination"; import { Paragraph } from "~/components/primitives/Paragraph"; @@ -59,7 +59,6 @@ import { useAutoRevalidate } from "~/hooks/useAutoRevalidate"; import { useEnvironment } from "~/hooks/useEnvironment"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; -import { useThrottle } from "~/hooks/useThrottle"; import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server"; import { findProjectBySlug } from "~/models/project.server"; import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; @@ -438,13 +437,8 @@ export default function Page() {
{success ? ( -
1 && "grid-rows-[auto_1fr_auto]" - )} - > -
+
+
- {pagination.totalPages > 1 && ( -
1 ? "grid-rows-[1fr_auto]" : "grid-rows-[1fr]" - )} - > -
1 && - "justify-end border-t border-grid-dimmed px-2 py-3" - )} - > - -
-
- )}
) : (
@@ -1076,39 +1049,7 @@ export function isEnvironmentPauseResumeFormSubmission( } export function QueueFilters() { - const [searchParams, setSearchParams] = useSearchParams(); - - const handleSearchChange = useThrottle((value: string) => { - if (value) { - setSearchParams((prev) => { - prev.set("query", value); - prev.delete("page"); - return prev; - }); - } else { - setSearchParams((prev) => { - prev.delete("query"); - prev.delete("page"); - return prev; - }); - } - }, 300); - - const search = searchParams.get("query") ?? ""; - - return ( -
- handleSearchChange(e.target.value)} - /> -
- ); + return ; } function BurstFactorTooltip({ From 1b8f26685f4524e7ec3a3bfd734d8a0bc8ac26e1 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 16 Apr 2026 13:41:39 +0100 Subject: [PATCH 24/51] Improved the Compare models filter button --- .../route.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models._index/route.tsx index 7bf257f987b..a5ea79b12e1 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models._index/route.tsx @@ -90,6 +90,7 @@ import { MetricWidget } from "~/routes/resources.metric"; import type { QueryWidgetConfig } from "~/components/metrics/QueryWidget"; import { type loader as compareLoader } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models.compare/route"; +import { IconColumns3 } from "@tabler/icons-react"; export const meta: MetaFunction = () => { return [{ title: "Models | Trigger.dev" }]; @@ -238,23 +239,25 @@ function FiltersBar({ return (
-
+
+ - {hasFilters && (
-
+
- + + }> + + + +
+ Toggle all details + +
+
+
); @@ -588,13 +684,13 @@ function CompareDialog({ return ( - - + + Compare models {rows.length > 0 ? (
- +
Metric @@ -1119,7 +1215,7 @@ export default function ModelsPage() { -
+
Date: Thu, 16 Apr 2026 15:42:34 +0100 Subject: [PATCH 27/51] Make sure tooltip span stays the correct height --- apps/webapp/app/components/primitives/Buttons.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webapp/app/components/primitives/Buttons.tsx b/apps/webapp/app/components/primitives/Buttons.tsx index 86e23801e72..1e26a5d92d6 100644 --- a/apps/webapp/app/components/primitives/Buttons.tsx +++ b/apps/webapp/app/components/primitives/Buttons.tsx @@ -362,7 +362,7 @@ export const Button = forwardRef( - + {buttonElement} From 438687cb5ca09bc83c04bcf4de3a6f7c2144f25a Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 16 Apr 2026 17:45:17 +0100 Subject: [PATCH 28/51] Lowercase m --- apps/webapp/app/components/navigation/SideMenu.tsx | 2 +- apps/webapp/app/presenters/v3/BuiltInDashboards.server.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx index 0693a2418b1..4b498e4b322 100644 --- a/apps/webapp/app/components/navigation/SideMenu.tsx +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -489,7 +489,7 @@ export function SideMenu({ /> )} Date: Thu, 16 Apr 2026 18:42:50 +0100 Subject: [PATCH 29/51] Nicer icons for the scope filter button --- .../app/components/metrics/ScopeFilter.tsx | 98 +++++++++++++++---- 1 file changed, 79 insertions(+), 19 deletions(-) diff --git a/apps/webapp/app/components/metrics/ScopeFilter.tsx b/apps/webapp/app/components/metrics/ScopeFilter.tsx index 1bf6b685676..cf3fd12f5bc 100644 --- a/apps/webapp/app/components/metrics/ScopeFilter.tsx +++ b/apps/webapp/app/components/metrics/ScopeFilter.tsx @@ -1,14 +1,17 @@ import * as Ariakit from "@ariakit/react"; -import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel"; +import { FolderIcon } from "@heroicons/react/20/solid"; +import { useRef } from "react"; +import { EnvironmentIcon, EnvironmentLabel } from "~/components/environments/EnvironmentLabel"; import { AppliedFilter } from "~/components/primitives/AppliedFilter"; +import { Avatar } from "~/components/primitives/Avatar"; import { SelectItem, SelectPopover, SelectProvider } from "~/components/primitives/Select"; +import { ShortcutKey } from "~/components/primitives/ShortcutKey"; import { useEnvironment } from "~/hooks/useEnvironment"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; import { useSearchParams } from "~/hooks/useSearchParam"; +import { type ShortcutDefinition, useShortcutKeys } from "~/hooks/useShortcutKeys"; import type { QueryScope } from "~/services/queryService.server"; -import { CubeTransparentIcon, GlobeAltIcon } from "@heroicons/react/20/solid"; -import { IconListLetters } from "@tabler/icons-react"; const scopeOptions = [ { value: "environment", label: "Environment" }, @@ -16,29 +19,61 @@ const scopeOptions = [ { value: "organization", label: "Organization" }, ] as const; -export function ScopeFilter() { +export function ScopeFilter({ shortcut }: { shortcut?: ShortcutDefinition } = {}) { const { value, replace } = useSearchParams(); const scope = (value("scope") as QueryScope) ?? "environment"; + const triggerRef = useRef(null); const handleChange = (newScope: string) => { replace({ scope: newScope === "environment" ? undefined : newScope }); }; + useShortcutKeys({ + shortcut, + action: (e) => { + e.preventDefault(); + e.stopPropagation(); + triggerRef.current?.click(); + }, + disabled: !shortcut, + }); + return ( - }> - } - value={} - removable={false} - variant="secondary/small" - /> - + + } + /> + } + > + } + removable={false} + variant="secondary/small" + /> + + {shortcut && ( + +
+ Change scope + +
+
+ )} +
{scopeOptions.map((option) => ( - - + } + > + ))} @@ -46,19 +81,44 @@ export function ScopeFilter() { ); } -function ScopeItem({ scope }: { scope: QueryScope }) { +function ScopeIcon({ scope }: { scope: QueryScope }) { + const organization = useOrganization(); + const environment = useEnvironment(); + + switch (scope) { + case "organization": + return ; + case "project": + return ; + case "environment": + return ; + default: + return null; + } +} + +function ScopeLabel({ scope }: { scope: QueryScope }) { const organization = useOrganization(); const project = useProject(); const environment = useEnvironment(); switch (scope) { case "organization": - return `Org: ${organization.title}`; + return {organization.title}; case "project": - return `Project: ${project.name}`; + return {project.name}; case "environment": - return ; + return ; default: return scope; } } + +function ScopeItem({ scope }: { scope: QueryScope }) { + return ( + + + + + ); +} From 8a783dda217de52be500a0f120393088b7f84b39 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 16 Apr 2026 18:43:01 +0100 Subject: [PATCH 30/51] Adds shortcut --- .../webapp/app/components/navigation/DashboardDialogs.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/webapp/app/components/navigation/DashboardDialogs.tsx b/apps/webapp/app/components/navigation/DashboardDialogs.tsx index f0cdd0406e0..5046db5a400 100644 --- a/apps/webapp/app/components/navigation/DashboardDialogs.tsx +++ b/apps/webapp/app/components/navigation/DashboardDialogs.tsx @@ -3,7 +3,8 @@ import { Form, useNavigation } from "@remix-run/react"; import { motion } from "framer-motion"; import { ArrowUpCircleIcon } from "@heroicons/react/24/outline"; import { PlusIcon } from "@heroicons/react/20/solid"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; +import { type ShortcutDefinition, useShortcutKeys } from "~/hooks/useShortcutKeys"; import { type MatchedOrganization, useDashboardLimits } from "~/hooks/useOrganizations"; import { useCurrentPlan } from "~/routes/_app.orgs.$organizationSlug/route"; import { Feedback } from "~/components/Feedback"; @@ -118,17 +119,19 @@ export function CreateDashboardPageButton({ organization, project, environment, + shortcut, }: { organization: { slug: string }; project: { slug: string }; environment: { slug: string }; + shortcut?: ShortcutDefinition; }) { const dashboard = useCreateDashboard({ organization, project, environment }); return ( - @@ -162,7 +165,6 @@ function CreateDashboardUpgradeDialog({ isFreePlan: boolean; organization: { slug: string }; }) { - if (isFreePlan) { return ( From e98384d48fb3f798f1fdb13f9dace0de6b625ab5 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 16 Apr 2026 18:43:09 +0100 Subject: [PATCH 31/51] Bright text --- apps/webapp/app/components/metrics/ProvidersFilter.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webapp/app/components/metrics/ProvidersFilter.tsx b/apps/webapp/app/components/metrics/ProvidersFilter.tsx index fe018eefb98..080cd81eb8e 100644 --- a/apps/webapp/app/components/metrics/ProvidersFilter.tsx +++ b/apps/webapp/app/components/metrics/ProvidersFilter.tsx @@ -111,7 +111,7 @@ function ProvidersDropdown({ {filtered.map((provider) => ( - }> + }> {provider} ))} From 31c2a9ee4ed8071ae619cdb6fce416e9182d7eda Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 16 Apr 2026 18:43:27 +0100 Subject: [PATCH 32/51] Bright text --- apps/webapp/app/components/logs/LogsTaskFilter.tsx | 1 + apps/webapp/app/components/metrics/ModelsFilter.tsx | 10 +++++----- .../webapp/app/components/metrics/OperationsFilter.tsx | 2 +- apps/webapp/app/components/metrics/PromptsFilter.tsx | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/apps/webapp/app/components/logs/LogsTaskFilter.tsx b/apps/webapp/app/components/logs/LogsTaskFilter.tsx index fa64eff7bd3..c7d36079a34 100644 --- a/apps/webapp/app/components/logs/LogsTaskFilter.tsx +++ b/apps/webapp/app/components/logs/LogsTaskFilter.tsx @@ -130,6 +130,7 @@ function TasksDropdown({ } diff --git a/apps/webapp/app/components/metrics/ModelsFilter.tsx b/apps/webapp/app/components/metrics/ModelsFilter.tsx index e641f826ae3..455e89f55af 100644 --- a/apps/webapp/app/components/metrics/ModelsFilter.tsx +++ b/apps/webapp/app/components/metrics/ModelsFilter.tsx @@ -38,19 +38,19 @@ function modelIcon(system: string, model: string): ReactNode { // Special case: Anthropic uses a custom SVG icon if (provider === "anthropic") { - return ; + return ; } const iconName = `tabler-brand-${provider}`; if (tablerIcons.has(iconName)) { return ( - + ); } - return ; + return ; } export function ModelsFilter({ possibleModels }: ModelsFilterProps) { @@ -147,11 +147,11 @@ function ModelsDropdown({ {filtered.map((m) => ( - + {m.model} ))} - {filtered.length === 0 && No models found} + {filtered.length === 0 && No models found}
diff --git a/apps/webapp/app/components/metrics/OperationsFilter.tsx b/apps/webapp/app/components/metrics/OperationsFilter.tsx index 679332fc3c4..df59cb804fd 100644 --- a/apps/webapp/app/components/metrics/OperationsFilter.tsx +++ b/apps/webapp/app/components/metrics/OperationsFilter.tsx @@ -125,7 +125,7 @@ function OperationsDropdown({ {filtered.map((op) => ( - }> + }> {formatOperation(op)} ))} diff --git a/apps/webapp/app/components/metrics/PromptsFilter.tsx b/apps/webapp/app/components/metrics/PromptsFilter.tsx index a4ad8a00045..78bb0c1bec8 100644 --- a/apps/webapp/app/components/metrics/PromptsFilter.tsx +++ b/apps/webapp/app/components/metrics/PromptsFilter.tsx @@ -113,7 +113,7 @@ function PromptsDropdown({ {filtered.map((slug) => ( - }> + }> {slug} ))} From 8ceb8d3b2afbe6b3564dc39b304f45dedc02ed05 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 16 Apr 2026 18:43:46 +0100 Subject: [PATCH 33/51] Adds clear filter button --- .../route.tsx | 90 ++++++++++++------- 1 file changed, 57 insertions(+), 33 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards.$dashboardKey/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards.$dashboardKey/route.tsx index 57b6b71db6f..583eabe1404 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards.$dashboardKey/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards.$dashboardKey/route.tsx @@ -1,6 +1,8 @@ +import { XMarkIcon } from "@heroicons/react/20/solid"; import { type LoaderFunctionArgs } from "@remix-run/node"; +import { Form } from "@remix-run/react"; import type { TaskTriggerSource } from "@trigger.dev/database"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react"; import ReactGridLayout from "react-grid-layout"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; @@ -15,7 +17,8 @@ import { QueuesFilter } from "~/components/metrics/QueuesFilter"; import { ScopeFilter } from "~/components/metrics/ScopeFilter"; import { TitleWidget } from "~/components/metrics/TitleWidget"; import { CreateDashboardPageButton } from "~/components/navigation/DashboardDialogs"; -import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader"; +import { Button } from "~/components/primitives/Buttons"; +import { NavBar, PageTitle } from "~/components/primitives/PageHeader"; import { TimeFilter } from "~/components/runs/v3/SharedFilters"; import { $replica } from "~/db.server"; import { useEnvironment } from "~/hooks/useEnvironment"; @@ -146,13 +149,6 @@ export default function Page() { - - -
@@ -168,6 +164,14 @@ export default function Page() { possiblePrompts={possiblePrompts} possibleOperations={possibleOperations} possibleProviders={possibleProviders} + filterAccessories={ + + } />
@@ -191,6 +195,7 @@ export function MetricDashboard({ onRenameWidget, onDeleteWidget, onDuplicateWidget, + filterAccessories, }: { /** The layout items (positions/sizes) - fully controlled from parent */ layout: LayoutItem[]; @@ -215,6 +220,7 @@ export function MetricDashboard({ onRenameWidget?: (widgetId: string, newTitle: string) => void; onDeleteWidget?: (widgetId: string) => void; onDuplicateWidget?: (widgetId: string, widget: WidgetData) => void; + filterAccessories?: ReactNode; }) { const { value, values } = useSearchParams(); const { width, containerRef, mounted } = useContainerWidth(); @@ -242,6 +248,13 @@ export function MetricDashboard({ const providers = values("providers").filter((v) => v !== ""); const activeFilters = filterConfig ?? ["tasks", "queues"]; + const hasAppliedFilters = + tasks.length > 0 || + queues.length > 0 || + models.length > 0 || + prompts.length > 0 || + operations.length > 0 || + providers.length > 0; const handleLayoutChange = useCallback( (newLayout: readonly LayoutItem[]) => { @@ -266,31 +279,42 @@ export function MetricDashboard({ return (
-
- - {activeFilters.includes("tasks") && ( - - )} - {activeFilters.includes("queues") && } - {activeFilters.includes("models") && ( - - )} - {activeFilters.includes("prompts") && ( - - )} - {activeFilters.includes("operations") && ( - - )} - {activeFilters.includes("providers") && ( - +
+
+ + {activeFilters.includes("tasks") && ( + + )} + {activeFilters.includes("queues") && } + {activeFilters.includes("models") && ( + + )} + {activeFilters.includes("prompts") && ( + + )} + {activeFilters.includes("operations") && ( + + )} + {activeFilters.includes("providers") && ( + + )} + + {hasAppliedFilters && ( +
+
+ {filterAccessories && ( +
{filterAccessories}
)} -
Date: Fri, 17 Apr 2026 11:49:19 +0100 Subject: [PATCH 34/51] Better keyboard shortcut keys --- apps/webapp/app/components/metrics/ModelsFilter.tsx | 2 +- apps/webapp/app/components/metrics/OperationsFilter.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/components/metrics/ModelsFilter.tsx b/apps/webapp/app/components/metrics/ModelsFilter.tsx index 455e89f55af..b2b986c9589 100644 --- a/apps/webapp/app/components/metrics/ModelsFilter.tsx +++ b/apps/webapp/app/components/metrics/ModelsFilter.tsx @@ -16,7 +16,7 @@ import { tablerIcons } from "~/utils/tablerIcons"; import tablerSpritePath from "~/components/primitives/tabler-sprite.svg"; import { AnthropicLogoIcon } from "~/assets/icons/AnthropicLogoIcon"; -const shortcut = { key: "l" }; +const shortcut = { key: "m" }; export type ModelOption = { model: string; diff --git a/apps/webapp/app/components/metrics/OperationsFilter.tsx b/apps/webapp/app/components/metrics/OperationsFilter.tsx index df59cb804fd..e0b3ac3f096 100644 --- a/apps/webapp/app/components/metrics/OperationsFilter.tsx +++ b/apps/webapp/app/components/metrics/OperationsFilter.tsx @@ -13,7 +13,7 @@ import { import { useSearchParams } from "~/hooks/useSearchParam"; import { appliedSummary, FilterMenuProvider } from "~/components/runs/v3/SharedFilters"; -const shortcut = { key: "n" }; +const shortcut = { key: "o" }; interface OperationsFilterProps { possibleOperations: string[]; From 274f77211f9ba2abffb28c045062f551e980afab Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sun, 19 Apr 2026 15:42:23 +0100 Subject: [PATCH 35/51] Reorder filters --- .../route.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs/route.tsx index af3cc30a246..012908bf0cd 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs/route.tsx @@ -260,14 +260,14 @@ function FiltersBar({ return (
-
+
{list ? ( <> + - {hasFilters && (
-
+
Configure alerts From 3ce3c17491c8e012ac17ad7cf0db88d944f6726e Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sun, 19 Apr 2026 16:02:23 +0100 Subject: [PATCH 38/51] Fix padding --- apps/webapp/app/components/logs/LogsVersionFilter.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/webapp/app/components/logs/LogsVersionFilter.tsx b/apps/webapp/app/components/logs/LogsVersionFilter.tsx index 4cc10545060..a5a83f6eda4 100644 --- a/apps/webapp/app/components/logs/LogsVersionFilter.tsx +++ b/apps/webapp/app/components/logs/LogsVersionFilter.tsx @@ -22,8 +22,9 @@ export function LogsVersionFilter() { variant="secondary/small" shortcut={shortcut} tooltipTitle="Filter by version" + className="pl-1.5" > - Versions + Versions } searchValue={search} From 36c32ce750b0d08a8c69f5a38038fad902acb4ac Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sun, 19 Apr 2026 16:18:39 +0100 Subject: [PATCH 39/51] Updated env var page search field --- .../route.tsx | 114 ++++++++++-------- 1 file changed, 61 insertions(+), 53 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx index f7f91f33274..9ab76ed49b7 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx @@ -4,17 +4,20 @@ import { BookOpenIcon, InformationCircleIcon, LockClosedIcon, - MagnifyingGlassIcon, PencilSquareIcon, PlusIcon, TrashIcon, } from "@heroicons/react/20/solid"; -import { Form, type MetaFunction, Outlet, useActionData, useFetcher, useNavigation, useRevalidator } from "@remix-run/react"; import { - type ActionFunctionArgs, - type LoaderFunctionArgs, - json, -} from "@remix-run/server-runtime"; + Form, + type MetaFunction, + Outlet, + useActionData, + useFetcher, + useNavigation, + useRevalidator, +} from "@remix-run/react"; +import { type ActionFunctionArgs, type LoaderFunctionArgs, json } from "@remix-run/server-runtime"; import { useEffect, useMemo, useState } from "react"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; @@ -30,9 +33,9 @@ import { Fieldset } from "~/components/primitives/Fieldset"; import { FormButtons } from "~/components/primitives/FormButtons"; import { FormError } from "~/components/primitives/FormError"; import { Header2 } from "~/components/primitives/Headers"; -import { InfoPanel } from "~/components/primitives/InfoPanel"; import { Input } from "~/components/primitives/Input"; import { InputGroup } from "~/components/primitives/InputGroup"; +import { SearchInput } from "~/components/primitives/SearchInput"; import { Label } from "~/components/primitives/Label"; import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader"; import { Paragraph } from "~/components/primitives/Paragraph"; @@ -50,6 +53,7 @@ import { SimpleTooltip } from "~/components/primitives/Tooltip"; import { prisma } from "~/db.server"; import { useEnvironment } from "~/hooks/useEnvironment"; import { useFuzzyFilter } from "~/hooks/useFuzzyFilter"; +import { useSearchParams } from "~/hooks/useSearchParam"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; import { redirectWithSuccessMessage } from "~/models/message.server"; @@ -76,7 +80,11 @@ import { UserAvatar } from "~/components/UserProfilePhoto"; import { VercelIntegrationService } from "~/services/vercelIntegration.server"; import { fromPromise } from "neverthrow"; import { logger } from "~/services/logger.server"; -import { shouldSyncEnvVar, isPullEnvVarsEnabledForEnvironment, type TriggerEnvironmentType } from "~/v3/vercel/vercelProjectIntegrationSchema"; +import { + shouldSyncEnvVar, + isPullEnvVarsEnabledForEnvironment, + type TriggerEnvironmentType, +} from "~/v3/vercel/vercelProjectIntegrationSchema"; export const meta: MetaFunction = () => { return [ @@ -92,10 +100,11 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { try { const presenter = new EnvironmentVariablesPresenter(); - const { environmentVariables, environments, hasStaging, vercelIntegration } = await presenter.call({ - userId, - projectSlug: projectParam, - }); + const { environmentVariables, environments, hasStaging, vercelIntegration } = + await presenter.call({ + userId, + projectSlug: projectParam, + }); return typedjson({ environmentVariables, @@ -123,7 +132,9 @@ const schema = z.discriminatedUnion("action", [ action: z.literal("update-vercel-sync"), key: z.string(), environmentType: z.enum(["PRODUCTION", "STAGING", "PREVIEW", "DEVELOPMENT"]), - syncEnabled: z.union([z.literal("true"), z.literal("false")]).transform((val) => val === "true"), + syncEnabled: z + .union([z.literal("true"), z.literal("false")]) + .transform((val) => val === "true"), }), ]); @@ -249,15 +260,21 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { export default function Page() { const [revealAll, setRevealAll] = useState(false); - const { environmentVariables, environments, vercelIntegration } = useTypedLoaderData(); + const { environmentVariables, environments, vercelIntegration } = + useTypedLoaderData(); const organization = useOrganization(); const project = useProject(); const environment = useEnvironment(); - const { filterText, setFilterText, filteredItems } = - useFuzzyFilter({ - items: environmentVariables, - keys: ["key", "value", "environment.type", "environment.branchName"], - }); + const { value } = useSearchParams(); + const urlSearch = value("search") ?? ""; + const { setFilterText, filteredItems } = useFuzzyFilter({ + items: environmentVariables, + keys: ["key", "value", "environment.type", "environment.branchName"], + }); + + useEffect(() => { + setFilterText(urlSearch); + }, [urlSearch, setFilterText]); // Add isFirst and isLast to each environment variable // They're set based on if they're the first or last time that `key` has been seen in the list @@ -314,18 +331,10 @@ export default function Page() {
{environmentVariables.length > 0 && (
- setFilterText(e.target.value)} - autoFocus - /> -
+ +
setRevealAll(e.valueOf())} @@ -351,7 +360,16 @@ export default function Page() { Value - Environment + + Environment + + + } + content="Dev environment variables specified here will be overridden by ones in your .env file when running locally." + className="max-w-60" + /> {vercelIntegration?.enabled && ( @@ -458,10 +476,11 @@ export default function Page() { /> {variable.updatedByUser.name}
- ) : (variable.lastUpdatedBy?.type === "integration" && variable.lastUpdatedBy?.integration === 'vercel' ) ? ( + ) : variable.lastUpdatedBy?.type === "integration" && + variable.lastUpdatedBy?.integration === "vercel" ? (
- - + + {variable.lastUpdatedBy.integration}
@@ -475,7 +494,7 @@ export default function Page() {
- -
- - Dev environment variables specified here will be overridden by ones in your .env file - when running locally. - -
@@ -561,9 +573,12 @@ function EditEnvironmentVariablePanel({ return ( - + Edit environment variable @@ -715,14 +730,7 @@ function VercelSyncCheckbox({ if (!pullEnvVarsEnabledForEnv) { return ( {}} - /> - } + button={ {}} />} content="Enable 'Pull env vars before build' for this environment in Vercel settings." /> ); From a5730e41864d58c94cc17a774268e33597f0279e Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sun, 19 Apr 2026 16:41:41 +0100 Subject: [PATCH 40/51] Improves the branches page filters --- .../route.tsx | 46 +++++++------------ 1 file changed, 17 insertions(+), 29 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.branches/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.branches/route.tsx index c7d54d1842e..39db1d96f3a 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.branches/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.branches/route.tsx @@ -132,10 +132,7 @@ const PurchaseSchema = z.discriminatedUnion("action", [ }), z.object({ action: z.literal("quota-increase"), - amount: z.coerce - .number() - .int("Must be a whole number") - .min(1, "Amount must be greater than 0"), + amount: z.coerce.number().int("Must be a whole number").min(1, "Amount must be greater than 0"), }), ]); @@ -329,23 +326,16 @@ export default function Page() { ) : ( <> -
+
-
- -
+
-
1 ? "grid-rows-[1fr_auto]" : "grid-rows-[1fr]" - )} - > +
@@ -438,14 +428,6 @@ export default function Page() { )}
-
1 && "justify-end border-t border-grid-dimmed px-2 py-3" - )} - > - -
@@ -545,12 +527,12 @@ export function BranchFilters() { return (
- +
); @@ -673,7 +655,13 @@ function PurchaseBranchesModal({ const [open, setOpen] = useState(false); useEffect(() => { const data = fetcher.data; - if (fetcher.state === "idle" && data !== null && typeof data === "object" && "ok" in data && data.ok) { + if ( + fetcher.state === "idle" && + data !== null && + typeof data === "object" && + "ok" in data && + data.ok + ) { setOpen(false); } }, [fetcher.state, fetcher.data]); From f782a436df7fb14c1148b44e81fb6dd5c278ca75 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sun, 19 Apr 2026 17:17:57 +0100 Subject: [PATCH 41/51] Unified style for all clear filters buttons --- .../app/components/metrics/QueuesFilter.tsx | 1 + .../app/components/runs/v3/BatchFilters.tsx | 10 +++++++-- .../app/components/runs/v3/RunFilters.tsx | 10 +++++++-- .../components/runs/v3/ScheduleFilters.tsx | 4 +++- .../runs/v3/WaitpointTokenFilters.tsx | 22 +++++++++++++------ .../route.tsx | 8 ++++++- .../route.tsx | 12 ++++++---- .../route.tsx | 12 ++++++---- .../route.tsx | 8 ++++++- 9 files changed, 65 insertions(+), 22 deletions(-) diff --git a/apps/webapp/app/components/metrics/QueuesFilter.tsx b/apps/webapp/app/components/metrics/QueuesFilter.tsx index 87d7a612547..7786b12ffc1 100644 --- a/apps/webapp/app/components/metrics/QueuesFilter.tsx +++ b/apps/webapp/app/components/metrics/QueuesFilter.tsx @@ -39,6 +39,7 @@ export function QueuesFilter() { variant="secondary/small" shortcut={shortcut} tooltipTitle="Filter by queue" + className="pl-1.5" > Queues diff --git a/apps/webapp/app/components/runs/v3/BatchFilters.tsx b/apps/webapp/app/components/runs/v3/BatchFilters.tsx index e92423bf9b3..239960abace 100644 --- a/apps/webapp/app/components/runs/v3/BatchFilters.tsx +++ b/apps/webapp/app/components/runs/v3/BatchFilters.tsx @@ -77,8 +77,14 @@ export function BatchFilters(props: BatchFiltersProps) { {hasFilters && ( - -
diff --git a/apps/webapp/app/components/runs/v3/RunFilters.tsx b/apps/webapp/app/components/runs/v3/RunFilters.tsx index 28bf63238ae..4a34b69968e 100644 --- a/apps/webapp/app/components/runs/v3/RunFilters.tsx +++ b/apps/webapp/app/components/runs/v3/RunFilters.tsx @@ -369,11 +369,17 @@ export function RunsFilters(props: RunFiltersProps) { {hasFilters && ( -
+ {searchParams.has("rootOnly") && ( )} -
diff --git a/apps/webapp/app/components/runs/v3/ScheduleFilters.tsx b/apps/webapp/app/components/runs/v3/ScheduleFilters.tsx index 33bbd62f2c8..e0fb819c8d1 100644 --- a/apps/webapp/app/components/runs/v3/ScheduleFilters.tsx +++ b/apps/webapp/app/components/runs/v3/ScheduleFilters.tsx @@ -247,12 +247,14 @@ function ClearFiltersButton() { }, [location, navigate]); return ( -
+
); diff --git a/apps/webapp/app/components/runs/v3/WaitpointTokenFilters.tsx b/apps/webapp/app/components/runs/v3/WaitpointTokenFilters.tsx index 9c3e157bc66..311911554db 100644 --- a/apps/webapp/app/components/runs/v3/WaitpointTokenFilters.tsx +++ b/apps/webapp/app/components/runs/v3/WaitpointTokenFilters.tsx @@ -79,8 +79,14 @@ export function WaitpointTokenFilters(props: WaitpointTokenFiltersProps) { {hasFilters && ( -
-
@@ -193,9 +199,7 @@ function PermanentStatusFilter() { label="Status" icon={} value={appliedSummary( - selectedStatuses.map((v) => - waitpointStatusTitle(v as WaitpointTokenStatus) - ) + selectedStatuses.map((v) => waitpointStatusTitle(v as WaitpointTokenStatus)) )} onRemove={() => del(["statuses", "cursor", "direction"])} variant="secondary/small" @@ -391,7 +395,9 @@ function PermanentTagsFilter() { ); } -function WaitpointIdDropdown(props: Omit) { +function WaitpointIdDropdown( + props: Omit +) { return ( ) { +function IdempotencyKeyDropdown( + props: Omit +) { return ( {hasAppliedFilters && (
-
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors._index/route.tsx index b570b614971..0cfef79cd2c 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors._index/route.tsx @@ -432,11 +432,13 @@ function FiltersBar({ shortcut={timeShortcut} /> {hasFilters && ( -
+
From 7da933d7dddc5b98bb8dcb1013d43e804a94a520 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sun, 19 Apr 2026 18:37:30 +0100 Subject: [PATCH 42/51] Root only toggle stay active when filtering by task --- .../app/components/runs/v3/RunFilters.tsx | 20 ++++++++++++++----- .../runsRepository/runsRepository.server.ts | 5 +++-- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/apps/webapp/app/components/runs/v3/RunFilters.tsx b/apps/webapp/app/components/runs/v3/RunFilters.tsx index 4a34b69968e..e1c6ba5238c 100644 --- a/apps/webapp/app/components/runs/v3/RunFilters.tsx +++ b/apps/webapp/app/components/runs/v3/RunFilters.tsx @@ -672,9 +672,20 @@ function TasksDropdown({ }) { const { values, replace } = useSearchParams(); - const handleChange = (values: string[]) => { + const handleChange = (newValues: string[]) => { clearSearchValue(); - replace({ tasks: values, cursor: undefined, direction: undefined }); + const previousTasks = values("tasks"); + const wasEmpty = previousTasks.length === 0 || previousTasks.every((v) => v === ""); + const isEmpty = newValues.length === 0 || newValues.every((v) => v === ""); + // When transitioning from no tasks to tasks, force rootOnly off so users + // see the runs of the task they just selected (root or otherwise). + const transitioningToTasks = wasEmpty && !isEmpty; + replace({ + tasks: newValues, + cursor: undefined, + direction: undefined, + ...(transitioningToTasks ? { rootOnly: "false" } : {}), + }); }; const filtered = useMemo(() => { @@ -1456,16 +1467,15 @@ function AppliedVersionsFilter() { const rootOnlyShortcut = { key: "o" }; function RootOnlyToggle({ defaultValue }: { defaultValue: boolean }) { - const { value, values, replace } = useSearchParams(); + const { value, replace } = useSearchParams(); const searchValue = value("rootOnly"); const rootOnly = searchValue !== undefined ? searchValue === "true" : defaultValue; const batchId = value("batchId"); const runId = value("runId"); const scheduleId = value("scheduleId"); - const tasks = values("tasks"); - const disabled = !!batchId || !!runId || !!scheduleId || tasks.length > 0; + const disabled = !!batchId || !!runId || !!scheduleId; return ( diff --git a/apps/webapp/app/services/runsRepository/runsRepository.server.ts b/apps/webapp/app/services/runsRepository/runsRepository.server.ts index 4f097f61ce2..c8bb6264b4e 100644 --- a/apps/webapp/app/services/runsRepository/runsRepository.server.ts +++ b/apps/webapp/app/services/runsRepository/runsRepository.server.ts @@ -295,8 +295,9 @@ export async function convertRunListInputOptionsToFilterRunsOptions( convertedOptions.runId = options.runId.map((r) => RunId.toFriendlyId(r)); } - // Show all runs if we are filtering by batchId or runId - if (options.batchId || options.runId?.length || options.scheduleId || options.tasks?.length) { + // batchId/runId/scheduleId target specific runs, so rootOnly is meaningless and forced off. + // tasks is intentionally excluded so rootOnly can narrow a task filter to root runs only. + if (options.batchId || options.runId?.length || options.scheduleId) { convertedOptions.rootOnly = false; } From 97a29ad9be50e7d86ceed69379b496a40c610f64 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Mon, 20 Apr 2026 17:51:58 +0100 Subject: [PATCH 43/51] make the ESC key less agressive and keep input field focus --- apps/webapp/app/components/primitives/SearchInput.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/webapp/app/components/primitives/SearchInput.tsx b/apps/webapp/app/components/primitives/SearchInput.tsx index dadeac42260..05e6c5398e0 100644 --- a/apps/webapp/app/components/primitives/SearchInput.tsx +++ b/apps/webapp/app/components/primitives/SearchInput.tsx @@ -80,9 +80,12 @@ export function SearchInput({ handleSubmit(); } if (e.key === "Escape") { - e.stopPropagation(); - handleClear(); - e.currentTarget.blur(); + if (text.length > 0) { + e.stopPropagation(); + handleClear(); + } else { + e.currentTarget.blur(); + } } }} onFocus={() => setIsFocused(true)} From 2ff11f15ee81a4d6bf789795584eadf745a0d707 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Mon, 20 Apr 2026 17:56:45 +0100 Subject: [PATCH 44/51] Task page search filter no longer debounces for better performance --- apps/webapp/app/hooks/useFuzzyFilter.ts | 9 +- .../route.tsx | 88 ++----------------- 2 files changed, 12 insertions(+), 85 deletions(-) diff --git a/apps/webapp/app/hooks/useFuzzyFilter.ts b/apps/webapp/app/hooks/useFuzzyFilter.ts index 3f0797179f2..6fdf9825cae 100644 --- a/apps/webapp/app/hooks/useFuzzyFilter.ts +++ b/apps/webapp/app/hooks/useFuzzyFilter.ts @@ -26,11 +26,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 +47,6 @@ export function useFuzzyFilter({ return items; } - // sort by the score of the first term return filterTerms.reduceRight( (results, term) => matchSorter(results, term, { @@ -55,7 +58,7 @@ export function useFuzzyFilter({ return { filterText, - setFilterText, + setFilterText: setInternalFilterText, filteredItems, }; } 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 d6716adb5fb..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,10 +5,8 @@ import { ChevronUpIcon, ExclamationTriangleIcon, LightBulbIcon, - MagnifyingGlassIcon, UserPlusIcon, VideoCameraIcon, - XMarkIcon, } from "@heroicons/react/20/solid"; import { json, type MetaFunction } from "@remix-run/node"; import { Link, useFetcher, useRevalidator } from "@remix-run/react"; @@ -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 { @@ -62,7 +60,6 @@ import { TableHeaderCell, TableRow, } from "~/components/primitives/Table"; -import { ShortcutKey } from "~/components/primitives/ShortcutKey"; import { SimpleTooltip } from "~/components/primitives/Tooltip"; import TooltipPortal from "~/components/primitives/TooltipPortal"; import { TaskFileName } from "~/components/runs/v3/TaskPath"; @@ -76,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 { @@ -89,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, @@ -177,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; @@ -245,12 +244,7 @@ export default function Page() { {tasks.length === 0 ? : null}
- + {!showUsefulLinks && ( - } - content={ -
- Clear field - -
- } - className="px-2 py-1.5 text-xs" - disableHoverableContent - /> - ) : undefined - } - /> - - ); -} From a6a60320e6244c52becf46e90d72b9873656b04f Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Wed, 22 Apr 2026 16:59:45 +0100 Subject: [PATCH 45/51] Only persist rootOnly when no tasks are filtered --- .../app/components/runs/v3/RunFilters.tsx | 12 +++++------ .../route.tsx | 20 ++++++++++++------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/apps/webapp/app/components/runs/v3/RunFilters.tsx b/apps/webapp/app/components/runs/v3/RunFilters.tsx index e1c6ba5238c..76f40d059de 100644 --- a/apps/webapp/app/components/runs/v3/RunFilters.tsx +++ b/apps/webapp/app/components/runs/v3/RunFilters.tsx @@ -370,9 +370,6 @@ export function RunsFilters(props: RunFiltersProps) { {hasFilters && (
- {searchParams.has("rootOnly") && ( - - )} ); From b9eb4d38e0dbb21afb687725fb6c8be37aca83f4 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sun, 26 Apr 2026 15:46:35 +0100 Subject: [PATCH 49/51] improves the filter bar buttons for custom dashboards --- .../route.tsx | 197 ++++++++++-------- 1 file changed, 106 insertions(+), 91 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards.custom.$dashboardId/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards.custom.$dashboardId/route.tsx index 418078760cd..3236f432e53 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards.custom.$dashboardId/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards.custom.$dashboardId/route.tsx @@ -205,19 +205,22 @@ export default function Page() { const toast = useToast(); - const handleSyncError = useCallback((error: Error, action: string) => { - const actionMessages: Record = { - add: "Failed to add widget", - update: "Failed to update widget", - delete: "Failed to delete widget", - duplicate: "Failed to duplicate widget", - layout: "Failed to save layout", - }; - - const message = actionMessages[action] || "Failed to save changes"; - - toast.error(`${message}. Your changes may not be saved.`, { title: "Sync Error" }); - }, [toast]); + const handleSyncError = useCallback( + (error: Error, action: string) => { + const actionMessages: Record = { + add: "Failed to add widget", + update: "Failed to update widget", + delete: "Failed to delete widget", + duplicate: "Failed to duplicate widget", + layout: "Failed to save layout", + }; + + const message = actionMessages[action] || "Failed to save changes"; + + toast.error(`${message}. Your changes may not be saved.`, { title: "Sync Error" }); + }, + [toast] + ); // Add title dialog state const [showAddTitleDialog, setShowAddTitleDialog] = useState(false); @@ -349,88 +352,99 @@ export default function Page() { })() : null; + const dashboardMenu = ( + + + +
+ + +
+
+
+ ); + + const filterAccessories = ( +
+ {widgetIsAtLimit ? ( + <> + + + + + + You've exceeded your widget limit + + You've used {widgetLimits.used}/{widgetLimits.limit} widgets on this dashboard. + + + {widgetCanUpgrade ? ( + + Upgrade + + ) : ( + Request more} + defaultValue="help" + /> + )} + + + + + + ) : ( + <> + + + + )} + {dashboardMenu} +
+ ); + return ( - - {totalWidgetCount > 0 && - (widgetIsAtLimit ? ( - <> - - - - - - You've exceeded your widget limit - - You've used {widgetLimits.used}/{widgetLimits.limit} widgets on this - dashboard. - - - {widgetCanUpgrade ? ( - - Upgrade - - ) : ( - Request more} - defaultValue="help" - /> - )} - - - - - - ) : ( - <> - - - - ))} - - - -
- - -
-
-
-
+ {totalWidgetCount === 0 && dashboardMenu}
@@ -471,6 +485,7 @@ export default function Page() { onRenameWidget={actions.renameWidget} onDeleteWidget={actions.deleteWidget} onDuplicateWidget={actions.duplicateWidget} + filterAccessories={filterAccessories} /> )}
From 522ef149de62ec1d9c28b5fc742f9b216c470191 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sun, 26 Apr 2026 15:46:44 +0100 Subject: [PATCH 50/51] Improved tooltip padding and spacing --- apps/webapp/app/components/primitives/Buttons.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/webapp/app/components/primitives/Buttons.tsx b/apps/webapp/app/components/primitives/Buttons.tsx index fd5c5686350..600ff9da325 100644 --- a/apps/webapp/app/components/primitives/Buttons.tsx +++ b/apps/webapp/app/components/primitives/Buttons.tsx @@ -305,7 +305,7 @@ export function ButtonContent(props: ButtonContentPropsType) { {buttonContent} - + {tooltip} {shortcut && renderShortcutKey()} @@ -370,9 +370,9 @@ export const Button = forwardRef( {buttonElement} - + {props.tooltip} {props.shortcut && !props.hideShortcutKey && ( - + )} From 39b88a85d6d64552119bf0bb67c9fc444d9ee6dc Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Sun, 26 Apr 2026 16:53:42 +0100 Subject: [PATCH 51/51] adds shortcuts to the query page filters --- .../app/components/metrics/ScopeFilter.tsx | 25 +++++++-- .../app/components/primitives/Select.tsx | 3 ++ .../app/components/query/QueryEditor.tsx | 52 +++---------------- .../QueryHistoryPopover.tsx | 4 +- 4 files changed, 32 insertions(+), 52 deletions(-) diff --git a/apps/webapp/app/components/metrics/ScopeFilter.tsx b/apps/webapp/app/components/metrics/ScopeFilter.tsx index cf3fd12f5bc..42f50c804d2 100644 --- a/apps/webapp/app/components/metrics/ScopeFilter.tsx +++ b/apps/webapp/app/components/metrics/ScopeFilter.tsx @@ -19,12 +19,27 @@ const scopeOptions = [ { value: "organization", label: "Organization" }, ] as const; -export function ScopeFilter({ shortcut }: { shortcut?: ShortcutDefinition } = {}) { - const { value, replace } = useSearchParams(); - const scope = (value("scope") as QueryScope) ?? "environment"; +export type ScopeFilterProps = { + shortcut?: ShortcutDefinition; + /** Controlled value. If provided, the filter uses controlled mode and ignores search params. */ + value?: QueryScope; + /** Called when the user selects a new scope. Required when `value` is provided. */ + onValueChange?: (scope: QueryScope) => void; +}; + +export function ScopeFilter({ shortcut, value, onValueChange }: ScopeFilterProps = {}) { + const { value: paramValue, replace } = useSearchParams(); + const isControlled = value !== undefined; + const scope: QueryScope = isControlled + ? value + : ((paramValue("scope") as QueryScope) ?? "environment"); const triggerRef = useRef(null); const handleChange = (newScope: string) => { + if (isControlled) { + onValueChange?.(newScope as QueryScope); + return; + } replace({ scope: newScope === "environment" ? undefined : newScope }); }; @@ -57,8 +72,8 @@ export function ScopeFilter({ shortcut }: { shortcut?: ShortcutDefinition } = {} /> {shortcut && ( - -
+ +
Change scope
diff --git a/apps/webapp/app/components/primitives/Select.tsx b/apps/webapp/app/components/primitives/Select.tsx index d3e4c866891..6c4c5d90316 100644 --- a/apps/webapp/app/components/primitives/Select.tsx +++ b/apps/webapp/app/components/primitives/Select.tsx @@ -104,6 +104,7 @@ export interface SelectProps open?: boolean; setOpen?: (open: boolean) => void; shortcut?: ShortcutDefinition; + tooltipTitle?: string; allowItemShortcuts?: boolean; clearSearchOnSelection?: boolean; dropdownIcon?: boolean | React.ReactNode; @@ -127,6 +128,7 @@ export function Select({ open, setOpen, shortcut, + tooltipTitle, allowItemShortcuts = true, disabled, clearSearchOnSelection = true, @@ -206,6 +208,7 @@ export function Select({ text={text} placeholder={placeholder} shortcut={shortcut} + tooltipTitle={tooltipTitle} disabled={disabled} dropdownIcon={dropdownIcon} {...props} diff --git a/apps/webapp/app/components/query/QueryEditor.tsx b/apps/webapp/app/components/query/QueryEditor.tsx index bb9ed036267..e7098b94c59 100644 --- a/apps/webapp/app/components/query/QueryEditor.tsx +++ b/apps/webapp/app/components/query/QueryEditor.tsx @@ -37,6 +37,7 @@ import { type QueryWidgetData, } from "~/components/metrics/QueryWidget"; import { SaveToDashboardDialog } from "~/components/metrics/SaveToDashboardDialog"; +import { ScopeFilter } from "~/components/metrics/ScopeFilter"; import { Button, LinkButton } from "~/components/primitives/Buttons"; import { Callout } from "~/components/primitives/Callout"; import { @@ -89,12 +90,6 @@ function toISOString(value: Date | string): string { return value.toISOString(); } -const scopeOptions = [ - { value: "environment", label: "Environment" }, - { value: "project", label: "Project" }, - { value: "organization", label: "Organization" }, -] as const; - // Type for the query action response type QueryActionResponse = { error: string | null; @@ -277,7 +272,7 @@ const QueryEditorForm = forwardRef< -
+
{isAdmin && (