Skip to content

Commit

Permalink
v3: Fix issues with posting alerts to public slack channels (#1108)
Browse files Browse the repository at this point in the history
* Fix issues with posting alerts to public slack channels

* Use the actual values in the new environmentTypes column to display the environment type labels in the alerts list

* Implement environment alert options
  • Loading branch information
ericallam committed May 17, 2024
1 parent 8a263c8 commit 6810756
Show file tree
Hide file tree
Showing 13 changed files with 351 additions and 172 deletions.
37 changes: 37 additions & 0 deletions apps/webapp/app/components/environments/EnvironmentLabel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,30 @@ const variants = {
large: "h-6 text-xs px-1.5 rounded",
};

export function EnvironmentTypeLabel({
environment,
size = "small",
className,
}: {
environment: Environment;
size?: keyof typeof variants;
className?: string;
}) {
return (
<span
className={cn(
"text-midnight-900 inline-flex items-center justify-center whitespace-nowrap border font-medium uppercase tracking-wider",
environmentBorderClassName(environment),
environmentTextClassName(environment),
variants[size],
className
)}
>
{environmentTypeTitle(environment)}
</span>
);
}

export function EnvironmentLabel({
environment,
size = "small",
Expand Down Expand Up @@ -116,6 +140,19 @@ export function environmentTitle(environment: Environment, username?: string) {
}
}

export function environmentTypeTitle(environment: Environment) {
switch (environment.type) {
case "PRODUCTION":
return "Prod";
case "STAGING":
return "Staging";
case "DEVELOPMENT":
return "Dev";
case "PREVIEW":
return "Preview";
}
}

export function environmentColorClassName(environment: Environment) {
switch (environment.type) {
case "PRODUCTION":
Expand Down
11 changes: 9 additions & 2 deletions apps/webapp/app/models/orgIntegration.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ type SlackSecret = z.infer<typeof SlackSecretSchema>;

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

type OrganizationIntegrationForService<TService extends IntegrationService> = Omit<
export type OrganizationIntegrationForService<TService extends IntegrationService> = Omit<
AuthenticatableIntegration,
"service"
> & {
Expand Down Expand Up @@ -83,7 +83,14 @@ export class OrgIntegrationRepository {

static slackAuthorizationUrl(
state: string,
scopes: string[] = ["channels:read", "groups:read", "im:read", "mpim:read", "chat:write"],
scopes: string[] = [
"channels:read",
"groups:read",
"im:read",
"mpim:read",
"chat:write",
"chat:write.public",
],
userScopes: string[] = ["channels:read", "groups:read", "im:read", "mpim:read", "chat:write"]
) {
return `https://slack.com/oauth/v2/authorize?client_id=${
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export const ApiAlertType = z.enum(["attempt_failure", "deployment_failure", "de

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

export const ApiAlertEnvironmentType = z.enum(["STAGING", "PRODUCTION"]);

export type ApiAlertEnvironmentType = z.infer<typeof ApiAlertEnvironmentType>;

export const ApiAlertChannel = z.enum(["email", "webhook"]);

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

export type ApiCreateAlertChannel = z.infer<typeof ApiCreateAlertChannel>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ const FormSchema = z
.array(z.enum(["TASK_RUN_ATTEMPT", "DEPLOYMENT_FAILURE", "DEPLOYMENT_SUCCESS"]))
.min(1)
.or(z.enum(["TASK_RUN_ATTEMPT", "DEPLOYMENT_FAILURE", "DEPLOYMENT_SUCCESS"])),
environmentTypes: z
.array(z.enum(["STAGING", "PRODUCTION"]))
.min(1)
.or(z.enum(["STAGING", "PRODUCTION"])),
type: z.enum(["WEBHOOK", "SLACK", "EMAIL"]).default("EMAIL"),
channelValue: z.string().nonempty(),
integrationId: z.string().optional(),
Expand Down Expand Up @@ -82,6 +86,9 @@ function formDataToCreateAlertChannelOptions(
alertTypes: Array.isArray(formData.alertTypes)
? formData.alertTypes
: [formData.alertTypes],
environmentTypes: Array.isArray(formData.environmentTypes)
? formData.environmentTypes
: [formData.environmentTypes],
channel: {
type: "WEBHOOK",
url: formData.channelValue,
Expand All @@ -94,6 +101,9 @@ function formDataToCreateAlertChannelOptions(
alertTypes: Array.isArray(formData.alertTypes)
? formData.alertTypes
: [formData.alertTypes],
environmentTypes: Array.isArray(formData.environmentTypes)
? formData.environmentTypes
: [formData.environmentTypes],
channel: {
type: "EMAIL",
email: formData.channelValue,
Expand All @@ -108,6 +118,9 @@ function formDataToCreateAlertChannelOptions(
alertTypes: Array.isArray(formData.alertTypes)
? formData.alertTypes
: [formData.alertTypes],
environmentTypes: Array.isArray(formData.environmentTypes)
? formData.environmentTypes
: [formData.environmentTypes],
channel: {
type: "SLACK",
channelId,
Expand Down Expand Up @@ -194,20 +207,27 @@ export default function Page() {
const project = useProject();
const [currentAlertChannel, setCurrentAlertChannel] = useState<string | null>(option ?? "EMAIL");

const [selectedSlackChannelValue, setSelectedSlackChannelValue] = useState<string | undefined>();

const selectedSlackChannel = slack.channels?.find(
(s) => selectedSlackChannelValue === `${s.id}/${s.name}`
);

const isLoading =
navigation.state !== "idle" &&
navigation.formMethod === "post" &&
navigation.formData?.get("action") === "create";

const [form, { channelValue: channelValue, alertTypes, type, integrationId }] = useForm({
id: "create-alert",
// TODO: type this
lastSubmission: lastSubmission as any,
onValidate({ formData }) {
return parse(formData, { schema: FormSchema });
},
shouldRevalidate: "onSubmit",
});
const [form, { channelValue: channelValue, alertTypes, environmentTypes, type, integrationId }] =
useForm({
id: "create-alert",
// TODO: type this
lastSubmission: lastSubmission as any,
onValidate({ formData }) {
return parse(formData, { schema: FormSchema });
},
shouldRevalidate: "onSubmit",
});

useEffect(() => {
setIsOpen(true);
Expand Down Expand Up @@ -272,6 +292,9 @@ export default function Page() {
dropdownIcon
variant="tertiary/medium"
items={slack.channels}
setValue={(value) => {
typeof value === "string" && setSelectedSlackChannelValue(value);
}}
filter={(channel, search) =>
channel.name?.toLowerCase().includes(search.toLowerCase()) ?? false
}
Expand All @@ -291,10 +314,16 @@ export default function Page() {
</>
)}
</Select>
<Hint className="leading-relaxed">
If selecting a private channel, you will need to invite the bot to the channel
using <InlineCode variant="extra-small">/invite @Trigger.dev</InlineCode>
</Hint>
{selectedSlackChannel && selectedSlackChannel.is_private && (
<Callout variant="warning">
Heads up! To receive alerts in the{" "}
<InlineCode variant="extra-small">{selectedSlackChannel.name}</InlineCode>{" "}
channel, you will need to invite the @Trigger.dev Slack Bot. You can do this
by visiting the channel in your Slack workspace issue the following command:{" "}
<InlineCode variant="extra-small">/invite @Trigger.dev</InlineCode>.
</Callout>
)}

<FormError id={channelValue.errorId}>{channelValue.error}</FormError>
<input type="hidden" name="integrationId" value={slack.integrationId} />
</>
Expand Down Expand Up @@ -364,23 +393,23 @@ export default function Page() {
<InputGroup>
<Label>Environments</Label>
<Checkbox
name="environments"
id="production"
value="production"
name={environmentTypes.name}
id="PRODUCTION"
value="PRODUCTION"
variant="simple/small"
label="PROD"
defaultChecked
disabled
/>
<Checkbox
name="environments"
id="staging"
value="staging"
name={environmentTypes.name}
id="STAGING"
value="STAGING"
variant="simple/small"
label="STAGING"
defaultChecked
disabled
/>

<FormError id={environmentTypes.errorId}>{environmentTypes.error}</FormError>
</InputGroup>
<FormError>{form.error}</FormError>
<div className="border-t border-grid-bright pt-3">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { ProjectAlertChannelType, ProjectAlertType } from "@trigger.dev/database
import assertNever from "assert-never";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { z } from "zod";
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
import { EnvironmentTypeLabel } from "~/components/environments/EnvironmentLabel";
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
import { Button, LinkButton } from "~/components/primitives/Buttons";
import { ClipboardField } from "~/components/primitives/ClipboardField";
Expand Down Expand Up @@ -204,8 +204,12 @@ export default function Page() {
<TableCell
className={cn("space-x-2", alertChannel.enabled ? "" : "opacity-50")}
>
<EnvironmentLabel environment={{ type: "PRODUCTION" }} />
<EnvironmentLabel environment={{ type: "STAGING" }} />
{alertChannel.environmentTypes.map((environmentType) => (
<EnvironmentTypeLabel
key={environmentType}
environment={{ type: environmentType }}
/>
))}
</TableCell>
<TableCell className={alertChannel.enabled ? "" : "opacity-50"}>
<AlertChannelDetails alertChannel={alertChannel} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
email: body.data.channelData.email,
},
deduplicationKey: body.data.deduplicationKey,
environmentTypes: body.data.environmentTypes,
});

return json(await ApiAlertChannelPresenter.alertChannelToApi(alertChannel));
Expand All @@ -75,6 +76,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
secret: body.data.channelData.secret,
},
deduplicationKey: body.data.deduplicationKey,
environmentTypes: body.data.environmentTypes,
});

return json(await ApiAlertChannelPresenter.alertChannelToApi(alertChannel));
Expand Down
14 changes: 13 additions & 1 deletion apps/webapp/app/v3/services/alerts/createAlertChannel.server.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { ProjectAlertChannel, ProjectAlertType } from "@trigger.dev/database";
import {
ProjectAlertChannel,
ProjectAlertType,
RuntimeEnvironmentType,
} from "@trigger.dev/database";
import { nanoid } from "nanoid";
import { env } from "~/env.server";
import { findProjectByRef } from "~/models/project.server";
Expand All @@ -9,6 +13,7 @@ import { BaseService, ServiceValidationError } from "../baseService.server";
export type CreateAlertChannelOptions = {
name: string;
alertTypes: ProjectAlertType[];
environmentTypes: RuntimeEnvironmentType[];
deduplicationKey?: string;
channel:
| {
Expand Down Expand Up @@ -40,6 +45,11 @@ export class CreateAlertChannelService extends BaseService {
throw new ServiceValidationError("Project not found");
}

const environmentTypes =
options.environmentTypes.length === 0
? (["STAGING", "PRODUCTION"] satisfies RuntimeEnvironmentType[])
: options.environmentTypes;

const existingAlertChannel = options.deduplicationKey
? await this._prisma.projectAlertChannel.findUnique({
where: {
Expand All @@ -59,6 +69,7 @@ export class CreateAlertChannelService extends BaseService {
alertTypes: options.alertTypes,
type: options.channel.type,
properties: await this.#createProperties(options.channel),
environmentTypes,
},
});
}
Expand All @@ -74,6 +85,7 @@ export class CreateAlertChannelService extends BaseService {
enabled: true,
deduplicationKey: options.deduplicationKey,
userProvidedDeduplicationKey: options.deduplicationKey ? true : false,
environmentTypes,
},
});

Expand Down

0 comments on commit 6810756

Please sign in to comment.