Skip to content

Commit cf81919

Browse files
Merge branch 'main' into main
2 parents e5cbf03 + 14cffd4 commit cf81919

File tree

82 files changed

+729
-295
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

82 files changed

+729
-295
lines changed

.changeset/pre.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,10 @@
118118
"tender-moose-tell",
119119
"tender-oranges-rhyme",
120120
"thin-parents-heal",
121+
"thirty-islands-kiss",
121122
"tidy-balloons-suffer",
122123
"tidy-dryers-sleep",
124+
"tidy-tomatoes-explain",
123125
"tiny-doors-type",
124126
"tiny-elephants-scream",
125127
"tricky-bulldogs-heal",

apps/webapp/app/components/environments/EnvironmentLabel.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,30 @@ const variants = {
99
large: "h-6 text-xs px-1.5 rounded",
1010
};
1111

12+
export function EnvironmentTypeLabel({
13+
environment,
14+
size = "small",
15+
className,
16+
}: {
17+
environment: Environment;
18+
size?: keyof typeof variants;
19+
className?: string;
20+
}) {
21+
return (
22+
<span
23+
className={cn(
24+
"text-midnight-900 inline-flex items-center justify-center whitespace-nowrap border font-medium uppercase tracking-wider",
25+
environmentBorderClassName(environment),
26+
environmentTextClassName(environment),
27+
variants[size],
28+
className
29+
)}
30+
>
31+
{environmentTypeTitle(environment)}
32+
</span>
33+
);
34+
}
35+
1236
export function EnvironmentLabel({
1337
environment,
1438
size = "small",
@@ -116,6 +140,19 @@ export function environmentTitle(environment: Environment, username?: string) {
116140
}
117141
}
118142

143+
export function environmentTypeTitle(environment: Environment) {
144+
switch (environment.type) {
145+
case "PRODUCTION":
146+
return "Prod";
147+
case "STAGING":
148+
return "Staging";
149+
case "DEVELOPMENT":
150+
return "Dev";
151+
case "PREVIEW":
152+
return "Preview";
153+
}
154+
}
155+
119156
export function environmentColorClassName(environment: Environment) {
120157
switch (environment.type) {
121158
case "PRODUCTION":

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ const DialogContent = React.forwardRef<
4343
<DialogPrimitive.Content
4444
ref={ref}
4545
className={cn(
46-
"fixed z-50 grid w-full gap-4 rounded-b-lg border bg-background-dimmed px-4 pb-4 pt-3.5 shadow-lg animate-in data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-bottom-10 sm:max-w-lg sm:rounded-lg sm:zoom-in-90 data-[state=open]:sm:slide-in-from-bottom-0",
46+
"fixed z-50 grid w-full gap-4 rounded-b-lg border bg-background-dimmed px-4 pb-4 pt-2.5 shadow-lg animate-in data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-bottom-10 sm:max-w-lg sm:rounded-lg sm:zoom-in-90 data-[state=open]:sm:slide-in-from-bottom-0",
4747
className
4848
)}
4949
{...props}
@@ -74,7 +74,7 @@ DialogContent.displayName = DialogPrimitive.Content.displayName;
7474

7575
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
7676
<div
77-
className={cn("flex flex-col text-left font-medium text-text-dimmed", className)}
77+
className={cn("flex flex-col text-left font-medium text-text-bright", className)}
7878
{...props}
7979
/>
8080
);

apps/webapp/app/components/runs/v3/EnabledStatus.tsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,31 @@
11
import { BoltSlashIcon, CheckCircleIcon } from "@heroicons/react/20/solid";
22

3-
export function EnabledStatus({ enabled }: { enabled: boolean }) {
3+
type EnabledStatusProps = {
4+
enabled: boolean;
5+
enabledIcon?: React.ComponentType<any>;
6+
disabledIcon?: React.ComponentType<any>;
7+
};
8+
9+
export function EnabledStatus({
10+
enabled,
11+
enabledIcon = CheckCircleIcon,
12+
disabledIcon = BoltSlashIcon,
13+
}: EnabledStatusProps) {
14+
const EnabledIcon = enabledIcon;
15+
const DisabledIcon = disabledIcon;
16+
417
switch (enabled) {
518
case true:
619
return (
720
<div className="flex items-center gap-1 text-xs text-success">
8-
<CheckCircleIcon className="h-4 w-4" />
21+
<EnabledIcon className="size-4" />
922
Enabled
1023
</div>
1124
);
1225
case false:
1326
return (
1427
<div className="text-dimmed flex items-center gap-1 text-xs">
15-
<BoltSlashIcon className="h-4 w-4" />
28+
<DisabledIcon className="size-4" />
1629
Disabled
1730
</div>
1831
);

apps/webapp/app/models/orgIntegration.server.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ type SlackSecret = z.infer<typeof SlackSecretSchema>;
2727

2828
const REDIRECT_AFTER_AUTH_KEY = "redirect-back-after-auth";
2929

30-
type OrganizationIntegrationForService<TService extends IntegrationService> = Omit<
30+
export type OrganizationIntegrationForService<TService extends IntegrationService> = Omit<
3131
AuthenticatableIntegration,
3232
"service"
3333
> & {
@@ -83,7 +83,14 @@ export class OrgIntegrationRepository {
8383

8484
static slackAuthorizationUrl(
8585
state: string,
86-
scopes: string[] = ["channels:read", "groups:read", "im:read", "mpim:read", "chat:write"],
86+
scopes: string[] = [
87+
"channels:read",
88+
"groups:read",
89+
"im:read",
90+
"mpim:read",
91+
"chat:write",
92+
"chat:write.public",
93+
],
8794
userScopes: string[] = ["channels:read", "groups:read", "im:read", "mpim:read", "chat:write"]
8895
) {
8996
return `https://slack.com/oauth/v2/authorize?client_id=${

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ export const ApiAlertType = z.enum(["attempt_failure", "deployment_failure", "de
1616

1717
export type ApiAlertType = z.infer<typeof ApiAlertType>;
1818

19+
export const ApiAlertEnvironmentType = z.enum(["STAGING", "PRODUCTION"]);
20+
21+
export type ApiAlertEnvironmentType = z.infer<typeof ApiAlertEnvironmentType>;
22+
1923
export const ApiAlertChannel = z.enum(["email", "webhook"]);
2024

2125
export type ApiAlertChannel = z.infer<typeof ApiAlertChannel>;
@@ -34,6 +38,7 @@ export const ApiCreateAlertChannel = z.object({
3438
channel: ApiAlertChannel,
3539
channelData: ApiAlertChannelData,
3640
deduplicationKey: z.string().optional(),
41+
environmentTypes: ApiAlertEnvironmentType.array().default(["STAGING", "PRODUCTION"]),
3742
});
3843

3944
export type ApiCreateAlertChannel = z.infer<typeof ApiCreateAlertChannel>;

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.alerts.new/route.tsx

Lines changed: 86 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { typedjson, useTypedLoaderData } from "remix-typedjson";
1010
import { z } from "zod";
1111
import { InlineCode } from "~/components/code/InlineCode";
1212
import { Button, LinkButton } from "~/components/primitives/Buttons";
13-
import { Callout } from "~/components/primitives/Callout";
13+
import { Callout, variantClasses } from "~/components/primitives/Callout";
1414
import { Checkbox } from "~/components/primitives/Checkbox";
1515
import { Dialog, DialogContent, DialogHeader } from "~/components/primitives/Dialog";
1616
import { Fieldset } from "~/components/primitives/Fieldset";
@@ -20,14 +20,17 @@ import { Hint } from "~/components/primitives/Hint";
2020
import { Input } from "~/components/primitives/Input";
2121
import { InputGroup } from "~/components/primitives/InputGroup";
2222
import { Label } from "~/components/primitives/Label";
23+
import { Paragraph } from "~/components/primitives/Paragraph";
2324
import SegmentedControl from "~/components/primitives/SegmentedControl";
2425
import { Select, SelectItem } from "~/components/primitives/Select";
26+
import { InfoIconTooltip } from "~/components/primitives/Tooltip";
2527
import { useOrganization } from "~/hooks/useOrganizations";
2628
import { useProject } from "~/hooks/useProject";
2729
import { redirectWithSuccessMessage } from "~/models/message.server";
2830
import { findProjectBySlug } from "~/models/project.server";
2931
import { NewAlertChannelPresenter } from "~/presenters/v3/NewAlertChannelPresenter.server";
3032
import { requireUserId } from "~/services/session.server";
33+
import { cn } from "~/utils/cn";
3134
import { ProjectParamSchema, v3ProjectAlertsPath } from "~/utils/pathBuilder";
3235
import {
3336
CreateAlertChannelOptions,
@@ -40,6 +43,10 @@ const FormSchema = z
4043
.array(z.enum(["TASK_RUN_ATTEMPT", "DEPLOYMENT_FAILURE", "DEPLOYMENT_SUCCESS"]))
4144
.min(1)
4245
.or(z.enum(["TASK_RUN_ATTEMPT", "DEPLOYMENT_FAILURE", "DEPLOYMENT_SUCCESS"])),
46+
environmentTypes: z
47+
.array(z.enum(["STAGING", "PRODUCTION"]))
48+
.min(1)
49+
.or(z.enum(["STAGING", "PRODUCTION"])),
4350
type: z.enum(["WEBHOOK", "SLACK", "EMAIL"]).default("EMAIL"),
4451
channelValue: z.string().nonempty(),
4552
integrationId: z.string().optional(),
@@ -81,6 +88,9 @@ function formDataToCreateAlertChannelOptions(
8188
alertTypes: Array.isArray(formData.alertTypes)
8289
? formData.alertTypes
8390
: [formData.alertTypes],
91+
environmentTypes: Array.isArray(formData.environmentTypes)
92+
? formData.environmentTypes
93+
: [formData.environmentTypes],
8494
channel: {
8595
type: "WEBHOOK",
8696
url: formData.channelValue,
@@ -93,6 +103,9 @@ function formDataToCreateAlertChannelOptions(
93103
alertTypes: Array.isArray(formData.alertTypes)
94104
? formData.alertTypes
95105
: [formData.alertTypes],
106+
environmentTypes: Array.isArray(formData.environmentTypes)
107+
? formData.environmentTypes
108+
: [formData.environmentTypes],
96109
channel: {
97110
type: "EMAIL",
98111
email: formData.channelValue,
@@ -107,6 +120,9 @@ function formDataToCreateAlertChannelOptions(
107120
alertTypes: Array.isArray(formData.alertTypes)
108121
? formData.alertTypes
109122
: [formData.alertTypes],
123+
environmentTypes: Array.isArray(formData.environmentTypes)
124+
? formData.environmentTypes
125+
: [formData.environmentTypes],
110126
channel: {
111127
type: "SLACK",
112128
channelId,
@@ -193,20 +209,27 @@ export default function Page() {
193209
const project = useProject();
194210
const [currentAlertChannel, setCurrentAlertChannel] = useState<string | null>(option ?? "EMAIL");
195211

212+
const [selectedSlackChannelValue, setSelectedSlackChannelValue] = useState<string | undefined>();
213+
214+
const selectedSlackChannel = slack.channels?.find(
215+
(s) => selectedSlackChannelValue === `${s.id}/${s.name}`
216+
);
217+
196218
const isLoading =
197219
navigation.state !== "idle" &&
198220
navigation.formMethod === "post" &&
199221
navigation.formData?.get("action") === "create";
200222

201-
const [form, { channelValue: channelValue, alertTypes, type, integrationId }] = useForm({
202-
id: "create-alert",
203-
// TODO: type this
204-
lastSubmission: lastSubmission as any,
205-
onValidate({ formData }) {
206-
return parse(formData, { schema: FormSchema });
207-
},
208-
shouldRevalidate: "onSubmit",
209-
});
223+
const [form, { channelValue: channelValue, alertTypes, environmentTypes, type, integrationId }] =
224+
useForm({
225+
id: "create-alert",
226+
// TODO: type this
227+
lastSubmission: lastSubmission as any,
228+
onValidate({ formData }) {
229+
return parse(formData, { schema: FormSchema });
230+
},
231+
shouldRevalidate: "onSubmit",
232+
});
210233

211234
useEffect(() => {
212235
setIsOpen(true);
@@ -271,6 +294,9 @@ export default function Page() {
271294
dropdownIcon
272295
variant="tertiary/medium"
273296
items={slack.channels}
297+
setValue={(value) => {
298+
typeof value === "string" && setSelectedSlackChannelValue(value);
299+
}}
274300
filter={(channel, search) =>
275301
channel.name?.toLowerCase().includes(search.toLowerCase()) ?? false
276302
}
@@ -290,10 +316,19 @@ export default function Page() {
290316
</>
291317
)}
292318
</Select>
293-
<Hint className="leading-relaxed">
294-
If selecting a private channel, you will need to invite the bot to the channel
295-
using <InlineCode variant="extra-small">/invite @Trigger.dev</InlineCode>
296-
</Hint>
319+
{selectedSlackChannel && selectedSlackChannel.is_private && (
320+
<Callout
321+
variant="warning"
322+
className={cn("text-sm", variantClasses.warning.textColor)}
323+
>
324+
To receive alerts in the{" "}
325+
<InlineCode variant="extra-small">{selectedSlackChannel.name}</InlineCode>{" "}
326+
channel, you need to invite the @Trigger.dev Slack Bot. Go to the channel in
327+
Slack and type:{" "}
328+
<InlineCode variant="extra-small">/invite @Trigger.dev</InlineCode>.
329+
</Callout>
330+
)}
331+
297332
<FormError id={channelValue.errorId}>{channelValue.error}</FormError>
298333
<input type="hidden" name="integrationId" value={slack.integrationId} />
299334
</>
@@ -324,24 +359,28 @@ export default function Page() {
324359
</InputGroup>
325360
)}
326361

327-
<InputGroup fullWidth>
328-
<Label>Events</Label>
329-
330-
<Checkbox
331-
name={alertTypes.name}
332-
id="TASK_RUN_ATTEMPT"
333-
value="TASK_RUN_ATTEMPT"
334-
variant="simple/small"
335-
label="Task run failure"
336-
defaultChecked
337-
/>
362+
<InputGroup>
363+
<Label>Alert me when</Label>
364+
365+
<div className="flex items-center gap-1">
366+
<Checkbox
367+
name={alertTypes.name}
368+
id="TASK_RUN_ATTEMPT"
369+
value="TASK_RUN_ATTEMPT"
370+
variant="simple/small"
371+
label="Task run attempts fail"
372+
defaultChecked
373+
className="pr-0"
374+
/>
375+
<InfoIconTooltip content="You'll receive an alert every time an attempt fails on a run." />
376+
</div>
338377

339378
<Checkbox
340379
name={alertTypes.name}
341380
id="DEPLOYMENT_FAILURE"
342381
value="DEPLOYMENT_FAILURE"
343382
variant="simple/small"
344-
label="Deployment failure"
383+
label="Deployments fail"
345384
defaultChecked
346385
/>
347386

@@ -350,13 +389,33 @@ export default function Page() {
350389
id="DEPLOYMENT_SUCCESS"
351390
value="DEPLOYMENT_SUCCESS"
352391
variant="simple/small"
353-
label="Deployment success"
392+
label="Deployments succeed"
354393
defaultChecked
355394
/>
356395

357396
<FormError id={alertTypes.errorId}>{alertTypes.error}</FormError>
358397
</InputGroup>
398+
<InputGroup>
399+
<Label>Environments</Label>
400+
<Checkbox
401+
name={environmentTypes.name}
402+
id="PRODUCTION"
403+
value="PRODUCTION"
404+
variant="simple/small"
405+
label="PROD"
406+
defaultChecked
407+
/>
408+
<Checkbox
409+
name={environmentTypes.name}
410+
id="STAGING"
411+
value="STAGING"
412+
variant="simple/small"
413+
label="STAGING"
414+
defaultChecked
415+
/>
359416

417+
<FormError id={environmentTypes.errorId}>{environmentTypes.error}</FormError>
418+
</InputGroup>
360419
<FormError>{form.error}</FormError>
361420
<div className="border-t border-grid-bright pt-3">
362421
<FormButtons

0 commit comments

Comments
 (0)