Skip to content

Commit e609e8b

Browse files
committed
Switch to a status dropdown
1 parent de3a49f commit e609e8b

File tree

2 files changed

+141
-48
lines changed

2 files changed

+141
-48
lines changed

apps/webapp/app/presenters/v3/ErrorsListPresenter.server.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export type ErrorsListOptions = {
2323
// filters
2424
tasks?: string[];
2525
versions?: string[];
26-
status?: ErrorGroupStatus;
26+
statuses?: ErrorGroupStatus[];
2727
period?: string;
2828
from?: number;
2929
to?: number;
@@ -42,7 +42,7 @@ export const ErrorsListOptionsSchema = z.object({
4242
projectId: z.string(),
4343
tasks: z.array(z.string()).optional(),
4444
versions: z.array(z.string()).optional(),
45-
status: z.enum(["UNRESOLVED", "RESOLVED", "IGNORED"]).optional(),
45+
statuses: z.array(z.enum(["UNRESOLVED", "RESOLVED", "IGNORED"])).optional(),
4646
period: z.string().optional(),
4747
from: z.number().int().nonnegative().optional(),
4848
to: z.number().int().nonnegative().optional(),
@@ -132,7 +132,7 @@ export class ErrorsListPresenter extends BasePresenter {
132132
projectId,
133133
tasks,
134134
versions,
135-
status,
135+
statuses,
136136
period,
137137
search,
138138
from,
@@ -168,7 +168,7 @@ export class ErrorsListPresenter extends BasePresenter {
168168
(tasks !== undefined && tasks.length > 0) ||
169169
(versions !== undefined && versions.length > 0) ||
170170
(search !== undefined && search !== "") ||
171-
status !== undefined ||
171+
(statuses !== undefined && statuses.length > 0) ||
172172
!time.isDefault;
173173

174174
const possibleTasksAsync = getAllTaskIdentifiers(this.replica, environmentId);
@@ -292,8 +292,10 @@ export class ErrorsListPresenter extends BasePresenter {
292292
};
293293
});
294294

295-
if (status) {
296-
transformedErrorGroups = transformedErrorGroups.filter((g) => g.status === status);
295+
if (statuses && statuses.length > 0) {
296+
transformedErrorGroups = transformedErrorGroups.filter((g) =>
297+
statuses.includes(g.status as ErrorGroupStatus)
298+
);
297299
}
298300

299301
return {
@@ -305,7 +307,7 @@ export class ErrorsListPresenter extends BasePresenter {
305307
filters: {
306308
tasks,
307309
versions,
308-
status,
310+
statuses,
309311
search,
310312
period: time,
311313
from: effectiveFrom,

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors._index/route.tsx

Lines changed: 132 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import * as Ariakit from "@ariakit/react";
12
import { XMarkIcon } from "@heroicons/react/20/solid";
2-
import { Form, type MetaFunction, useSearchParams as useRemixSearchParams } from "@remix-run/react";
3+
import { Form, type MetaFunction } from "@remix-run/react";
34
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
45
import { type ErrorGroupStatus } from "@trigger.dev/database";
6+
import { IconBugFilled } from "@tabler/icons-react";
57
import { ErrorId } from "@trigger.dev/core/v3/isomorphic";
6-
import { Suspense, useMemo } from "react";
8+
import { Suspense, useMemo, type ReactNode } from "react";
79
import {
810
Bar,
911
BarChart,
@@ -18,12 +20,21 @@ import { PageBody } from "~/components/layout/AppLayout";
1820
import { SearchInput } from "~/components/primitives/SearchInput";
1921
import { LogsTaskFilter } from "~/components/logs/LogsTaskFilter";
2022
import { LogsVersionFilter } from "~/components/logs/LogsVersionFilter";
23+
import { AppliedFilter } from "~/components/primitives/AppliedFilter";
2124
import { Button } from "~/components/primitives/Buttons";
2225
import { Callout } from "~/components/primitives/Callout";
2326
import { formatDateTime, RelativeDateTime } from "~/components/primitives/DateTime";
2427
import { Header3 } from "~/components/primitives/Headers";
2528
import { NavBar, PageTitle } from "~/components/primitives/PageHeader";
2629
import { Paragraph } from "~/components/primitives/Paragraph";
30+
import {
31+
ComboBox,
32+
SelectItem,
33+
SelectList,
34+
SelectPopover,
35+
SelectProvider,
36+
SelectTrigger,
37+
} from "~/components/primitives/Select";
2738
import { Spinner } from "~/components/primitives/Spinner";
2839
import {
2940
CopyableTableCell,
@@ -36,9 +47,10 @@ import {
3647
TableRow,
3748
} from "~/components/primitives/Table";
3849
import TooltipPortal from "~/components/primitives/TooltipPortal";
39-
import { TimeFilter } from "~/components/runs/v3/SharedFilters";
50+
import { appliedSummary, FilterMenuProvider, TimeFilter } from "~/components/runs/v3/SharedFilters";
4051
import { $replica } from "~/db.server";
4152
import { useOptimisticLocation } from "~/hooks/useOptimisticLocation";
53+
import { useSearchParams } from "~/hooks/useSearchParam";
4254
import { findProjectBySlug } from "~/models/project.server";
4355
import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server";
4456
import {
@@ -55,7 +67,6 @@ import { ListPagination } from "~/components/ListPagination";
5567
import { formatNumberCompact } from "~/utils/numberFormatter";
5668
import { EnvironmentParamSchema, v3ErrorPath } from "~/utils/pathBuilder";
5769
import { ServiceValidationError } from "~/v3/services/baseService.server";
58-
import SegmentedControl from "~/components/primitives/SegmentedControl";
5970

6071
export const meta: MetaFunction = () => {
6172
return [
@@ -84,11 +95,11 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
8495
const url = new URL(request.url);
8596
const tasks = url.searchParams.getAll("tasks").filter((t) => t.length > 0);
8697
const versions = url.searchParams.getAll("versions").filter((v) => v.length > 0);
87-
const statusParam = url.searchParams.get("status") ?? undefined;
88-
const status =
89-
statusParam === "UNRESOLVED" || statusParam === "RESOLVED" || statusParam === "IGNORED"
90-
? (statusParam as ErrorGroupStatus)
91-
: undefined;
98+
const statuses = url.searchParams
99+
.getAll("status")
100+
.filter(
101+
(s): s is ErrorGroupStatus => s === "UNRESOLVED" || s === "RESOLVED" || s === "IGNORED"
102+
);
92103
const search = url.searchParams.get("search") ?? undefined;
93104
const period = url.searchParams.get("period") ?? undefined;
94105
const fromStr = url.searchParams.get("from");
@@ -111,7 +122,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
111122
projectId: project.id,
112123
tasks: tasks.length > 0 ? tasks : undefined,
113124
versions: versions.length > 0 ? versions : undefined,
114-
status,
125+
statuses: statuses.length > 0 ? statuses : undefined,
115126
search,
116127
period,
117128
from,
@@ -237,38 +248,117 @@ export default function Page() {
237248
);
238249
}
239250

240-
function StatusFilterButton() {
241-
const [searchParams, setSearchParams] = useRemixSearchParams();
242-
const currentStatus = searchParams.get("status") ?? "all";
251+
const errorStatusOptions = [
252+
{ value: "UNRESOLVED", label: "Unresolved" },
253+
{ value: "RESOLVED", label: "Resolved" },
254+
{ value: "IGNORED", label: "Ignored" },
255+
] as const;
243256

244-
const options: Array<{ value: string; label: string }> = [
245-
{ value: "all", label: "All" },
246-
{ value: "UNRESOLVED", label: "Unresolved" },
247-
{ value: "RESOLVED", label: "Resolved" },
248-
{ value: "IGNORED", label: "Ignored" },
249-
];
257+
const statusIcon = <IconBugFilled className="size-4" />;
258+
const statusShortcut = { key: "s" };
259+
260+
function StatusFilter() {
261+
const { values, del } = useSearchParams();
262+
const selectedStatuses = values("status");
263+
264+
if (selectedStatuses.length === 0 || selectedStatuses.every((v) => v === "")) {
265+
return (
266+
<FilterMenuProvider>
267+
{(search, setSearch) => (
268+
<ErrorStatusDropdown
269+
trigger={
270+
<SelectTrigger
271+
icon={statusIcon}
272+
variant="secondary/small"
273+
shortcut={statusShortcut}
274+
tooltipTitle="Filter by status"
275+
>
276+
<span className="ml-0.5">Status</span>
277+
</SelectTrigger>
278+
}
279+
searchValue={search}
280+
clearSearchValue={() => setSearch("")}
281+
/>
282+
)}
283+
</FilterMenuProvider>
284+
);
285+
}
250286

251287
return (
252-
<SegmentedControl
253-
name="status"
254-
value={currentStatus}
255-
variant="secondary/small"
256-
options={options.map((opt) => ({
257-
label: opt.label,
258-
value: opt.value,
259-
}))}
260-
onChange={(value) => {
261-
const next = new URLSearchParams(searchParams);
262-
next.delete("cursor");
263-
next.delete("direction");
264-
if (value === "all") {
265-
next.delete("status");
266-
} else {
267-
next.set("status", value);
268-
}
269-
setSearchParams(next);
270-
}}
271-
/>
288+
<FilterMenuProvider>
289+
{(search, setSearch) => (
290+
<ErrorStatusDropdown
291+
trigger={
292+
<Ariakit.Select render={<div className="group cursor-pointer focus-custom" />}>
293+
<AppliedFilter
294+
label="Status"
295+
icon={statusIcon}
296+
value={appliedSummary(
297+
selectedStatuses.map((s) => {
298+
const opt = errorStatusOptions.find((o) => o.value === s);
299+
return opt ? opt.label : s;
300+
})
301+
)}
302+
onRemove={() => del(["status", "cursor", "direction"])}
303+
variant="secondary/small"
304+
/>
305+
</Ariakit.Select>
306+
}
307+
searchValue={search}
308+
clearSearchValue={() => setSearch("")}
309+
/>
310+
)}
311+
</FilterMenuProvider>
312+
);
313+
}
314+
315+
function ErrorStatusDropdown({
316+
trigger,
317+
clearSearchValue,
318+
searchValue,
319+
onClose,
320+
}: {
321+
trigger: ReactNode;
322+
clearSearchValue: () => void;
323+
searchValue: string;
324+
onClose?: () => void;
325+
}) {
326+
const { values, replace } = useSearchParams();
327+
328+
const handleChange = (values: string[]) => {
329+
clearSearchValue();
330+
replace({ status: values.length > 0 ? values : undefined, cursor: undefined, direction: undefined });
331+
};
332+
333+
const filtered = useMemo(() => {
334+
return errorStatusOptions.filter((item) =>
335+
item.label.toLowerCase().includes(searchValue.toLowerCase())
336+
);
337+
}, [searchValue]);
338+
339+
return (
340+
<SelectProvider value={values("status")} setValue={handleChange} virtualFocus={true}>
341+
{trigger}
342+
<SelectPopover
343+
className="min-w-0 max-w-[min(240px,var(--popover-available-width))]"
344+
hideOnEscape={() => {
345+
if (onClose) {
346+
onClose();
347+
return false;
348+
}
349+
return true;
350+
}}
351+
>
352+
<ComboBox placeholder="Filter by status..." value={searchValue} />
353+
<SelectList>
354+
{filtered.map((item) => (
355+
<SelectItem key={item.value} value={item.value}>
356+
{item.label}
357+
</SelectItem>
358+
))}
359+
</SelectList>
360+
</SelectPopover>
361+
</SelectProvider>
272362
);
273363
}
274364

@@ -284,6 +374,7 @@ function FiltersBar({
284374
const location = useOptimisticLocation();
285375
const searchParams = new URLSearchParams(location.search);
286376
const hasFilters =
377+
searchParams.has("status") ||
287378
searchParams.has("tasks") ||
288379
searchParams.has("versions") ||
289380
searchParams.has("search") ||
@@ -296,7 +387,7 @@ function FiltersBar({
296387
<div className="flex flex-row flex-wrap items-center gap-1">
297388
{list ? (
298389
<>
299-
<StatusFilterButton />
390+
<StatusFilter />
300391
<LogsTaskFilter possibleTasks={list.filters.possibleTasks} />
301392
<LogsVersionFilter />
302393
<TimeFilter
@@ -317,7 +408,7 @@ function FiltersBar({
317408
</>
318409
) : (
319410
<>
320-
<StatusFilterButton />
411+
<StatusFilter />
321412
<LogsTaskFilter possibleTasks={[]} />
322413
<LogsVersionFilter />
323414
<TimeFilter defaultPeriod={defaultPeriod} maxPeriodDays={retentionLimitDays} />

0 commit comments

Comments
 (0)