-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Errors (versions) #3187
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
matt-aitken
wants to merge
80
commits into
main
Choose a base branch
from
errors-add-version-filter
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Errors (versions) #3187
Changes from all commits
Commits
Show all changes
80 commits
Select commit
Hold shift + click to select a range
ec56354
Add versions filtering to the Errors list page
matt-aitken d68e3c7
Added versions filter to the fingerprint page
matt-aitken dd80e64
Stacked bars for the versions
matt-aitken ae03edb
Removed legend
matt-aitken b3a865d
Error alerting
matt-aitken 7aba9ea
Use SegmentedControl for error filtering
matt-aitken 9644270
Switch to a status dropdown
matt-aitken 06bc436
Configure alerts WIP
matt-aitken 61721b0
Nicer Slack flow. Improvement to evaluation scheduling
matt-aitken a6841d4
Missed a conflict marker
matt-aitken 16b9089
Unordered list component
matt-aitken d2eafe0
Layout improvements
matt-aitken 0771d37
Show ignore reasons
matt-aitken 84fdcf1
Fix for ignore layout
matt-aitken acd3af3
Switch to not use a fetcher so the page reloads
matt-aitken 34eaee2
Ignore tertiary style
matt-aitken 937d623
Stop the legend key colliding with the value
matt-aitken 4be6ed9
Better Slack error alerts, added webhook error alerts
matt-aitken dec1d1c
Handle ERROR_GROUP in alertTypeTitle to prevent alerts page crash
matt-aitken d6b8782
Query with taskIdentifier on the group page
matt-aitken 1f93caf
Fix for deletion logic and tighter schema
matt-aitken 910b77b
Rework ErrorGroupState queries/table for indexes
matt-aitken c4ec6ac
Fix for webhook dates
matt-aitken 513ac64
Configuring alerts better loading state and close panel when done
matt-aitken 743c48d
useToast hook for success/error messages
matt-aitken 95c5d95
Fix for Configure alerts form action path
matt-aitken dcd9b4c
Fixed TS errors by adding ERROR_GROUP to cases where it was missing
matt-aitken dc24731
UI improvements to the Configure alerts feature
samejr 7b33e28
use tabular nums to avoid layout shift
samejr 174cf8d
Unify the error statuses into a reusable badge
samejr 8306b0e
Adds more tabular-nums
samejr 16cbf6b
Improvements to the errors details panel
samejr e5d21c4
fixed width of popover
samejr b3a8b11
Improvements to the Ignored panel
samejr c6f1f4f
WIP new UI skills file
samejr cb70b5f
layout and chart behaviour improvements
samejr f221c5e
no animation on chart tooltips
samejr 80588be
Revert to otlpExporter to main
matt-aitken e5ef568
Revert without formatting
matt-aitken 4465c2f
small table improvements
samejr 0cb2a31
chart ux improvments
samejr 8c285cd
Removed search filter from the error status
samejr 4dd5eb1
Filter button alignment
samejr 8a9c53d
Page layout fix
samejr 2ede323
Nicer ignored panel layout
samejr b8a9e21
Change ignored permanently icon
samejr dcd79d0
Adds unresolved icon
samejr c731751
use proper input form components
samejr 7f3a3e6
Fix for submitting custom error form
samejr 598d235
Nicer chart padding
samejr 832789b
filter button alignment update
samejr 664311d
Use CodeBlock instead to show the full error
samejr 0c431f5
2 new variants for the popover button
samejr c59f016
Make the ignored badge blue
samejr 4b044f7
Improvments to the ignored status
samejr 9ef3a59
Adds link to manage your Slack connection
samejr aa073c3
Adds new prop to control icon size
samejr 9c264b5
Updates org side menu
samejr 5922a2a
Updates slack integration page layout
samejr 3252cb0
custom variable for Errors
samejr 767d263
Mono slack icon
samejr 9903301
Better blank state
samejr 20271df
Fixes for Slack channel disconnecting and reconnecting flow
samejr eb4c5fb
Adds a cancel button
samejr 853d5c9
text improvements to the error config sheet
samejr c6cbd1a
Nicer icons
samejr b292d28
If you disconnect the slack integration and reconnect it, don’t retai…
samejr 70b737e
Redirect after disconnecting the slack channel to the slack integrati…
samejr ec9f6df
Make sure the toast displays when saving form
samejr e722fd5
Change error status from the main Errors page table
samejr 298829a
increase activity chart size
samejr c23133d
clearer activity chart
samejr 93a3f37
Merge branch 'main' into errors-add-version-filter
samejr 8feff7d
Remove UI skill from this PR
samejr b2608f1
Coderabbit fixes
samejr 8974b3f
Devin fix
samejr b667160
Devin suggested fixes
samejr 8140d3a
Another devin fix
samejr 418029b
Fix for chidren with the same key
samejr 6eda63a
Better error colors
samejr File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| export function SlackMonoIcon({ className }: { className?: string }) { | ||
| return ( | ||
| <svg className={className} viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> | ||
| <path d="M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zm1.271 0a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313z" /> | ||
| <path d="M8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zm0 1.271a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312z" /> | ||
| <path d="M18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zm-1.27 0a2.528 2.528 0 0 1-2.522 2.521 2.527 2.527 0 0 1-2.521-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.522 2.522v6.312z" /> | ||
| <path d="M15.165 18.956a2.528 2.528 0 0 1 2.522 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.521-2.522v-2.522h2.521zm0-1.27a2.527 2.527 0 0 1-2.521-2.522 2.528 2.528 0 0 1 2.521-2.521h6.313A2.528 2.528 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.522h-6.313z" /> | ||
| </svg> | ||
| ); | ||
| } |
365 changes: 365 additions & 0 deletions
365
apps/webapp/app/components/errors/ConfigureErrorAlerts.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,365 @@ | ||
| import { conform, list, requestIntent, useFieldList, useForm } from "@conform-to/react"; | ||
| import { parse } from "@conform-to/zod"; | ||
| import { | ||
| EnvelopeIcon, | ||
| GlobeAltIcon, | ||
| HashtagIcon, | ||
| LockClosedIcon, | ||
| XMarkIcon, | ||
| } from "@heroicons/react/20/solid"; | ||
| import { useFetcher, useNavigate } from "@remix-run/react"; | ||
| import { SlackIcon } from "@trigger.dev/companyicons"; | ||
| import { Fragment, useEffect, useRef, useState } from "react"; | ||
| import { z } from "zod"; | ||
| import { Button, LinkButton } from "~/components/primitives/Buttons"; | ||
| import { Callout, variantClasses } from "~/components/primitives/Callout"; | ||
| import { useToast } from "~/components/primitives/Toast"; | ||
| import { Fieldset } from "~/components/primitives/Fieldset"; | ||
| import { FormError } from "~/components/primitives/FormError"; | ||
| import { Header2, Header3 } from "~/components/primitives/Headers"; | ||
| import { Hint } from "~/components/primitives/Hint"; | ||
| import { InlineCode } from "~/components/code/InlineCode"; | ||
| import { Input } from "~/components/primitives/Input"; | ||
| import { InputGroup } from "~/components/primitives/InputGroup"; | ||
| import { Paragraph } from "~/components/primitives/Paragraph"; | ||
| import { Select, SelectItem } from "~/components/primitives/Select"; | ||
| import { UnorderedList } from "~/components/primitives/UnorderedList"; | ||
| import type { ErrorAlertChannelData } from "~/presenters/v3/ErrorAlertChannelPresenter.server"; | ||
| import { useOptimisticLocation } from "~/hooks/useOptimisticLocation"; | ||
| import { useOrganization } from "~/hooks/useOrganizations"; | ||
| import { cn } from "~/utils/cn"; | ||
| import { organizationSlackIntegrationPath } from "~/utils/pathBuilder"; | ||
| import { ExitIcon } from "~/assets/icons/ExitIcon"; | ||
| import { TextLink } from "~/components/primitives/TextLink"; | ||
| import { BellAlertIcon } from "@heroicons/react/24/solid"; | ||
|
|
||
| export const ErrorAlertsFormSchema = z.object({ | ||
| emails: z.preprocess((i) => { | ||
| if (typeof i === "string") return i === "" ? [] : [i]; | ||
| if (Array.isArray(i)) return i.filter((v) => typeof v === "string" && v !== ""); | ||
| return []; | ||
| }, z.string().email().array()), | ||
| slackChannel: z.string().optional(), | ||
| slackIntegrationId: z.string().optional(), | ||
| webhooks: z.preprocess((i) => { | ||
| if (typeof i === "string") return i === "" ? [] : [i]; | ||
| if (Array.isArray(i)) return i.filter((v) => typeof v === "string" && v !== ""); | ||
| return []; | ||
| }, z.string().url().array()), | ||
| }); | ||
|
|
||
| type ConfigureErrorAlertsProps = ErrorAlertChannelData & { | ||
| connectToSlackHref?: string; | ||
| formAction: string; | ||
| }; | ||
|
|
||
| export function ConfigureErrorAlerts({ | ||
| emails: existingEmails, | ||
| webhooks: existingWebhooks, | ||
| slackChannel: existingSlackChannel, | ||
| slack, | ||
| emailAlertsEnabled, | ||
| connectToSlackHref, | ||
| formAction, | ||
| }: ConfigureErrorAlertsProps) { | ||
| const organization = useOrganization(); | ||
| const fetcher = useFetcher<{ ok?: boolean }>(); | ||
| const navigate = useNavigate(); | ||
| const toast = useToast(); | ||
| const location = useOptimisticLocation(); | ||
| const isSubmitting = fetcher.state !== "idle"; | ||
|
|
||
| const [selectedSlackChannelValue, setSelectedSlackChannelValue] = useState<string | undefined>( | ||
| existingSlackChannel | ||
| ? `${existingSlackChannel.channelId}/${existingSlackChannel.channelName}` | ||
| : undefined | ||
| ); | ||
|
|
||
| const selectedSlackChannel = | ||
| slack.status === "READY" | ||
| ? slack.channels?.find((s) => selectedSlackChannelValue === `${s.id}/${s.name}`) | ||
| : undefined; | ||
|
|
||
| const closeHref = (() => { | ||
| const params = new URLSearchParams(location.search); | ||
| params.delete("alerts"); | ||
| const qs = params.toString(); | ||
| return qs ? `?${qs}` : location.pathname; | ||
| })(); | ||
|
|
||
| const hasHandledSuccess = useRef(false); | ||
| useEffect(() => { | ||
| if (fetcher.state === "idle" && fetcher.data?.ok && !hasHandledSuccess.current) { | ||
| hasHandledSuccess.current = true; | ||
| toast.success("Alert settings saved"); | ||
| navigate(closeHref, { replace: true }); | ||
| } | ||
| }, [fetcher.state, fetcher.data, closeHref, navigate, toast]); | ||
|
|
||
| const emailFieldValues = useRef<string[]>( | ||
| existingEmails.length > 0 ? [...existingEmails.map((e) => e.email), ""] : [""] | ||
| ); | ||
|
|
||
| const webhookFieldValues = useRef<string[]>( | ||
| existingWebhooks.length > 0 ? [...existingWebhooks.map((w) => w.url), ""] : [""] | ||
| ); | ||
|
|
||
| const [form, { emails, webhooks, slackChannel, slackIntegrationId }] = useForm({ | ||
| id: "configure-error-alerts", | ||
| onValidate({ formData }) { | ||
| return parse(formData, { schema: ErrorAlertsFormSchema }); | ||
| }, | ||
| shouldRevalidate: "onSubmit", | ||
| defaultValue: { | ||
| emails: emailFieldValues.current, | ||
| webhooks: webhookFieldValues.current, | ||
| }, | ||
| }); | ||
|
|
||
| const emailFields = useFieldList(form.ref, emails); | ||
| const webhookFields = useFieldList(form.ref, webhooks); | ||
|
|
||
| return ( | ||
| <div className="grid h-full grid-rows-[auto_1fr_auto] overflow-hidden"> | ||
| <div className="flex items-center justify-between border-b border-grid-bright px-3 py-2"> | ||
| <Header2 className="flex items-center gap-2"> | ||
| <BellAlertIcon className="size-5 text-alerts" /> Configure alerts | ||
| </Header2> | ||
| <LinkButton | ||
| to={closeHref} | ||
| variant="minimal/small" | ||
| TrailingIcon={ExitIcon} | ||
| shortcut={{ key: "esc" }} | ||
| shortcutPosition="before-trailing-icon" | ||
| className="pl-1" | ||
| /> | ||
| </div> | ||
|
|
||
| <fetcher.Form | ||
| method="post" | ||
| action={formAction} | ||
| {...form.props} | ||
| className="contents" | ||
| > | ||
| <div className="flex-1 overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"> | ||
| <Fieldset className="flex flex-col gap-4 p-4"> | ||
| <div className="flex flex-col"> | ||
| <Header3>Receive alerts when</Header3> | ||
| <UnorderedList variant="small/dimmed" className="mt-1"> | ||
| <li>An error is seen for the first time</li> | ||
| <li>A resolved error re-occurs</li> | ||
| <li>An ignored error re-occurs based on settings you configured</li> | ||
| </UnorderedList> | ||
| </div> | ||
|
|
||
| {/* Email section */} | ||
| <div> | ||
| <Header3 className="mb-1">Email</Header3> | ||
| {emailAlertsEnabled ? ( | ||
| <InputGroup> | ||
| {emailFields.map((emailField, index) => ( | ||
| <Fragment key={emailField.key}> | ||
| <Input | ||
| {...conform.input(emailField, { type: "email" })} | ||
| placeholder={index === 0 ? "Enter an email address" : "Add another email"} | ||
| icon={EnvelopeIcon} | ||
| onChange={(e) => { | ||
| emailFieldValues.current[index] = e.target.value; | ||
| if ( | ||
| emailFields.length === emailFieldValues.current.length && | ||
| emailFieldValues.current.every((v) => v !== "") | ||
| ) { | ||
| requestIntent(form.ref.current ?? undefined, list.append(emails.name)); | ||
| } | ||
| }} | ||
| /> | ||
| <FormError id={emailField.errorId}>{emailField.error}</FormError> | ||
| </Fragment> | ||
| ))} | ||
| </InputGroup> | ||
| ) : ( | ||
| <Callout variant="warning"> | ||
| Email integration is not available. Please contact your organization | ||
| administrator. | ||
| </Callout> | ||
| )} | ||
| </div> | ||
|
|
||
| {/* Slack section */} | ||
| <div> | ||
| <Header3 className="mb-1">Slack</Header3> | ||
|
|
||
| <InputGroup fullWidth> | ||
| {slack.status === "READY" ? ( | ||
| <> | ||
| <Select | ||
| name={slackChannel.name} | ||
| placeholder={<span className="text-text-dimmed">Select a Slack channel</span>} | ||
| heading="Filter channels…" | ||
| defaultValue={selectedSlackChannelValue} | ||
| dropdownIcon | ||
| variant="tertiary/medium" | ||
| items={slack.channels} | ||
| setValue={(value) => { | ||
| typeof value === "string" && setSelectedSlackChannelValue(value); | ||
| }} | ||
| filter={(channel, search) => | ||
| channel.name?.toLowerCase().includes(search.toLowerCase()) ?? false | ||
| } | ||
| text={(value) => { | ||
| const channel = slack.channels.find((s) => value === `${s.id}/${s.name}`); | ||
| if (!channel) return; | ||
| return ( | ||
| <span className="text-text-bright"> | ||
| <SlackChannelTitle {...channel} /> | ||
| </span> | ||
| ); | ||
| }} | ||
| > | ||
| {(matches) => ( | ||
| <> | ||
| {matches?.map((channel) => ( | ||
| <SelectItem | ||
| key={channel.id} | ||
| value={`${channel.id}/${channel.name}`} | ||
| className="text-text-bright" | ||
| > | ||
| <SlackChannelTitle {...channel} /> | ||
| </SelectItem> | ||
| ))} | ||
| </> | ||
| )} | ||
| </Select> | ||
| {selectedSlackChannel && selectedSlackChannel.is_private && ( | ||
| <Callout | ||
| variant="warning" | ||
| className={cn("text-sm", variantClasses.warning.textColor)} | ||
| > | ||
| To receive alerts in the{" "} | ||
| <InlineCode variant="extra-small">{selectedSlackChannel.name}</InlineCode>{" "} | ||
| channel, you need to invite the @Trigger.dev Slack Bot. Go to the channel in | ||
| Slack and type:{" "} | ||
| <InlineCode variant="extra-small">/invite @Trigger.dev</InlineCode>. | ||
| </Callout> | ||
| )} | ||
| <Hint> | ||
| <TextLink to={organizationSlackIntegrationPath(organization)}> | ||
| Manage Slack connection | ||
| </TextLink> | ||
| </Hint> | ||
| <input | ||
| type="hidden" | ||
| name={slackIntegrationId.name} | ||
| value={slack.integrationId} | ||
| /> | ||
| </> | ||
| ) : slack.status === "NOT_CONFIGURED" ? ( | ||
| connectToSlackHref ? ( | ||
| <LinkButton variant="tertiary/medium" to={connectToSlackHref} fullWidth> | ||
| <span className="flex items-center gap-2 text-text-bright"> | ||
| <SlackIcon className="size-5" /> Connect to Slack | ||
| </span> | ||
| </LinkButton> | ||
| ) : ( | ||
| <Callout variant="info"> | ||
| Slack is not connected. Connect Slack from the{" "} | ||
| <span className="font-medium text-text-bright">Alerts</span> page to enable | ||
| Slack notifications. | ||
| </Callout> | ||
| ) | ||
| ) : slack.status === "TOKEN_REVOKED" || slack.status === "TOKEN_EXPIRED" ? ( | ||
| connectToSlackHref ? ( | ||
| <div className="flex flex-col gap-4"> | ||
| <Callout variant="info"> | ||
| The Slack integration in your workspace has been revoked or has expired. | ||
| Please re-connect your Slack workspace. | ||
| </Callout> | ||
| <LinkButton | ||
| variant="tertiary/large" | ||
| to={`${connectToSlackHref}?reinstall=true`} | ||
| fullWidth | ||
| > | ||
| <span className="flex items-center gap-2 text-text-bright"> | ||
| <SlackIcon className="size-5" /> Connect to Slack | ||
| </span> | ||
| </LinkButton> | ||
| </div> | ||
| ) : ( | ||
| <Callout variant="info"> | ||
| The Slack integration in your workspace has been revoked or expired. Please | ||
| re-connect from the{" "} | ||
| <span className="font-medium text-text-bright">Alerts</span> page. | ||
| </Callout> | ||
| ) | ||
| ) : slack.status === "FAILED_FETCHING_CHANNELS" ? ( | ||
| <Callout variant="warning"> | ||
| Failed loading channels from Slack. Please try again later. | ||
| </Callout> | ||
| ) : ( | ||
| <Callout variant="warning"> | ||
| Slack integration is not available. Please contact your organization | ||
| administrator. | ||
| </Callout> | ||
| )} | ||
| </InputGroup> | ||
| </div> | ||
|
|
||
| {/* Webhook section */} | ||
| <div> | ||
| <Header3 className="mb-1">Webhook</Header3> | ||
| <InputGroup> | ||
| {webhookFields.map((webhookField, index) => ( | ||
| <Fragment key={webhookField.key}> | ||
| <Input | ||
| {...conform.input(webhookField, { type: "url" })} | ||
| placeholder={ | ||
| index === 0 ? "https://example.com/webhook" : "Add another webhook URL" | ||
| } | ||
| icon={GlobeAltIcon} | ||
| onChange={(e) => { | ||
| webhookFieldValues.current[index] = e.target.value; | ||
| if ( | ||
| webhookFields.length === webhookFieldValues.current.length && | ||
| webhookFieldValues.current.every((v) => v !== "") | ||
| ) { | ||
| requestIntent(form.ref.current ?? undefined, list.append(webhooks.name)); | ||
| } | ||
| }} | ||
| /> | ||
| <FormError id={webhookField.errorId}>{webhookField.error}</FormError> | ||
| </Fragment> | ||
| ))} | ||
| <Hint>We'll issue POST requests to these URLs with a JSON payload.</Hint> | ||
| </InputGroup> | ||
| </div> | ||
|
|
||
| <FormError>{form.error}</FormError> | ||
| </Fieldset> | ||
| </div> | ||
|
|
||
| <div className="flex items-center justify-between border-t border-grid-bright px-3 py-3"> | ||
| <LinkButton variant="secondary/medium" to={closeHref}> | ||
| Cancel | ||
| </LinkButton> | ||
| <Button | ||
| variant="primary/medium" | ||
| type="submit" | ||
| disabled={isSubmitting} | ||
| isLoading={isSubmitting} | ||
| > | ||
| {isSubmitting ? "Saving…" : "Save"} | ||
| </Button> | ||
| </div> | ||
| </fetcher.Form> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| function SlackChannelTitle({ name, is_private }: { name?: string; is_private?: boolean }) { | ||
| return ( | ||
| <div className="flex items-center gap-1.5"> | ||
| {is_private ? <LockClosedIcon className="size-4" /> : <HashtagIcon className="size-4" />} | ||
| <span>{name}</span> | ||
| </div> | ||
| ); | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.