Skip to content

Commit e722fd5

Browse files
committed
Change error status from the main Errors page table
1 parent ec9f6df commit e722fd5

File tree

4 files changed

+314
-206
lines changed
  • apps/webapp/app
    • components
    • routes
      • _app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint
      • _app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors._index

4 files changed

+314
-206
lines changed
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
import { CheckIcon } from "@heroicons/react/20/solid";
2+
import {
3+
IconAlarmSnooze as IconAlarmSnoozeBase,
4+
IconArrowBackUp as IconArrowBackUpBase,
5+
IconBugOff as IconBugOffBase,
6+
} from "@tabler/icons-react";
7+
import { useState } from "react";
8+
import { type ErrorGroupStatus } from "@trigger.dev/database";
9+
import { Form, useNavigation, useSubmit } from "@remix-run/react";
10+
import { Button } from "~/components/primitives/Buttons";
11+
import { FormError } from "~/components/primitives/FormError";
12+
import { Input } from "~/components/primitives/Input";
13+
import { InputGroup } from "~/components/primitives/InputGroup";
14+
import { Label } from "~/components/primitives/Label";
15+
import { PopoverMenuItem } from "~/components/primitives/Popover";
16+
import {
17+
Dialog,
18+
DialogContent,
19+
DialogFooter,
20+
DialogHeader,
21+
DialogTitle,
22+
} from "~/components/primitives/Dialog";
23+
24+
const AlarmSnoozeIcon = ({ className }: { className?: string }) => (
25+
<IconAlarmSnoozeBase className={className} size={18} />
26+
);
27+
const ArrowBackUpIcon = ({ className }: { className?: string }) => (
28+
<IconArrowBackUpBase className={className} size={18} />
29+
);
30+
const BugOffIcon = ({ className }: { className?: string }) => (
31+
<IconBugOffBase className={className} size={18} />
32+
);
33+
34+
export function ErrorStatusMenuItems({
35+
status,
36+
taskIdentifier,
37+
onAction,
38+
onCustomIgnore,
39+
}: {
40+
status: ErrorGroupStatus;
41+
taskIdentifier: string;
42+
onAction: (data: Record<string, string>) => void;
43+
onCustomIgnore: () => void;
44+
}) {
45+
return (
46+
<>
47+
{status === "UNRESOLVED" && (
48+
<>
49+
<PopoverMenuItem
50+
icon={CheckIcon}
51+
leadingIconClassName="text-success"
52+
title="Resolved"
53+
onClick={() => onAction({ taskIdentifier, action: "resolve" })}
54+
/>
55+
<PopoverMenuItem
56+
icon={AlarmSnoozeIcon}
57+
leadingIconClassName="text-blue-500"
58+
title="Ignored for 1 hour"
59+
onClick={() =>
60+
onAction({
61+
taskIdentifier,
62+
action: "ignore",
63+
duration: String(60 * 60 * 1000),
64+
})
65+
}
66+
/>
67+
<PopoverMenuItem
68+
icon={AlarmSnoozeIcon}
69+
leadingIconClassName="text-blue-500"
70+
title="Ignored for 24 hours"
71+
onClick={() =>
72+
onAction({
73+
taskIdentifier,
74+
action: "ignore",
75+
duration: String(24 * 60 * 60 * 1000),
76+
})
77+
}
78+
/>
79+
<PopoverMenuItem
80+
icon={BugOffIcon}
81+
leadingIconClassName="text-blue-500"
82+
title="Ignored forever"
83+
onClick={() => onAction({ taskIdentifier, action: "ignore" })}
84+
/>
85+
<PopoverMenuItem
86+
icon={AlarmSnoozeIcon}
87+
leadingIconClassName="text-blue-500"
88+
title="Ignored with custom condition…"
89+
onClick={onCustomIgnore}
90+
/>
91+
</>
92+
)}
93+
94+
{status === "IGNORED" && (
95+
<>
96+
<PopoverMenuItem
97+
icon={CheckIcon}
98+
leadingIconClassName="text-success"
99+
title="Resolved"
100+
onClick={() => onAction({ taskIdentifier, action: "resolve" })}
101+
/>
102+
<PopoverMenuItem
103+
icon={ArrowBackUpIcon}
104+
leadingIconClassName="text-error"
105+
title="Unresolved"
106+
onClick={() => onAction({ taskIdentifier, action: "unresolve" })}
107+
/>
108+
</>
109+
)}
110+
111+
{status === "RESOLVED" && (
112+
<PopoverMenuItem
113+
icon={ArrowBackUpIcon}
114+
leadingIconClassName="text-error"
115+
title="Unresolved"
116+
onClick={() => onAction({ taskIdentifier, action: "unresolve" })}
117+
/>
118+
)}
119+
</>
120+
);
121+
}
122+
123+
export function CustomIgnoreDialog({
124+
open,
125+
onOpenChange,
126+
taskIdentifier,
127+
formAction,
128+
}: {
129+
open: boolean;
130+
onOpenChange: (open: boolean) => void;
131+
taskIdentifier: string;
132+
formAction?: string;
133+
}) {
134+
const submit = useSubmit();
135+
const navigation = useNavigation();
136+
const isSubmitting = navigation.state !== "idle";
137+
const [conditionError, setConditionError] = useState<string | null>(null);
138+
139+
return (
140+
<Dialog open={open} onOpenChange={onOpenChange}>
141+
<DialogContent className="sm:max-w-md">
142+
<DialogHeader>
143+
<DialogTitle className="flex items-center gap-1.5">
144+
<IconAlarmSnoozeBase className="-ml-1.5 size-6 text-blue-500" />
145+
Custom ignore condition
146+
</DialogTitle>
147+
</DialogHeader>
148+
<Form
149+
method="post"
150+
action={formAction}
151+
onSubmit={(e) => {
152+
e.preventDefault();
153+
const formData = new FormData(e.currentTarget);
154+
const rate = formData.get("occurrenceRate")?.toString().trim();
155+
const total = formData.get("totalOccurrences")?.toString().trim();
156+
157+
if (!rate && !total) {
158+
setConditionError("At least one unignore condition is required");
159+
return;
160+
}
161+
162+
setConditionError(null);
163+
submit(e.currentTarget, { method: "post", action: formAction });
164+
setTimeout(() => onOpenChange(false), 100);
165+
}}
166+
>
167+
<input type="hidden" name="action" value="ignore" />
168+
<input type="hidden" name="taskIdentifier" value={taskIdentifier} />
169+
170+
<div className="flex flex-col gap-4 py-4">
171+
<InputGroup fullWidth>
172+
<Label htmlFor="occurrenceRate" variant="small">
173+
Unignore when occurrence rate exceeds (per minute)
174+
</Label>
175+
<Input
176+
id="occurrenceRate"
177+
name="occurrenceRate"
178+
type="number"
179+
min={1}
180+
placeholder="e.g. 10"
181+
onChange={() => conditionError && setConditionError(null)}
182+
/>
183+
</InputGroup>
184+
185+
<InputGroup fullWidth>
186+
<Label htmlFor="totalOccurrences" variant="small">
187+
Unignore when total occurrences exceed
188+
</Label>
189+
<Input
190+
id="totalOccurrences"
191+
name="totalOccurrences"
192+
type="number"
193+
min={1}
194+
placeholder="e.g. 100"
195+
onChange={() => conditionError && setConditionError(null)}
196+
/>
197+
</InputGroup>
198+
199+
{conditionError && <FormError>{conditionError}</FormError>}
200+
201+
<InputGroup fullWidth>
202+
<Label htmlFor="reason" variant="small" required={false}>
203+
Reason
204+
</Label>
205+
<Input id="reason" name="reason" type="text" placeholder="e.g. Known flaky test" />
206+
</InputGroup>
207+
</div>
208+
209+
<DialogFooter>
210+
<Button variant="tertiary/medium" type="button" onClick={() => onOpenChange(false)}>
211+
Cancel
212+
</Button>
213+
<Button variant="primary/medium" type="submit" disabled={isSubmitting}>
214+
{isSubmitting ? "Ignoring…" : "Ignore error"}
215+
</Button>
216+
</DialogFooter>
217+
</Form>
218+
</DialogContent>
219+
</Dialog>
220+
);
221+
}

apps/webapp/app/components/primitives/Table.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,7 @@ export const TableCellMenu = forwardRef<
431431
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
432432
visibleButtons?: ReactNode;
433433
hiddenButtons?: ReactNode;
434-
popoverContent?: ReactNode;
434+
popoverContent?: ReactNode | ((close: () => void) => ReactNode);
435435
children?: ReactNode;
436436
isSelected?: boolean;
437437
}
@@ -451,6 +451,8 @@ export const TableCellMenu = forwardRef<
451451
) => {
452452
const [isOpen, setIsOpen] = useState(false);
453453
const { variant } = useContext(TableContext);
454+
const resolvedContent =
455+
typeof popoverContent === "function" ? popoverContent(() => setIsOpen(false)) : popoverContent;
454456

455457
return (
456458
<TableCell
@@ -486,8 +488,8 @@ export const TableCellMenu = forwardRef<
486488
{/* Always visible buttons */}
487489
{visibleButtons}
488490
{/* Always visible popover with ellipsis trigger */}
489-
{popoverContent && (
490-
<Popover onOpenChange={(open) => setIsOpen(open)}>
491+
{resolvedContent && (
492+
<Popover open={isOpen} onOpenChange={(open) => setIsOpen(open)}>
491493
<PopoverVerticalEllipseTrigger
492494
isOpen={isOpen}
493495
className="duration-0 group-hover/table-row:text-text-bright"
@@ -496,7 +498,11 @@ export const TableCellMenu = forwardRef<
496498
className="min-w-[10rem] max-w-[20rem] overflow-y-auto p-0 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
497499
align="end"
498500
>
499-
<div className="flex flex-col gap-1 p-1">{popoverContent}</div>
501+
{typeof popoverContent === "function" ? (
502+
resolvedContent
503+
) : (
504+
<div className="flex flex-col gap-1 p-1">{resolvedContent}</div>
505+
)}
500506
</PopoverContent>
501507
</Popover>
502508
)}

0 commit comments

Comments
 (0)