chore: import upstream snapshot with attribution
This commit is contained in:
+216
@@ -0,0 +1,216 @@
|
||||
import { parseWithZod } from "@conform-to/zod";
|
||||
import { Outlet, useNavigate } from "@remix-run/react";
|
||||
import { type ActionFunctionArgs, type LoaderFunctionArgs, json } from "@remix-run/server-runtime";
|
||||
import { useCallback } from "react";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { PageContainer } from "~/components/layout/AppLayout";
|
||||
import {
|
||||
ConfigureErrorAlerts,
|
||||
ErrorAlertsFormSchema,
|
||||
} from "~/components/errors/ConfigureErrorAlerts";
|
||||
import { Sheet, SheetContent } from "~/components/primitives/SheetV3";
|
||||
import { prisma } from "~/db.server";
|
||||
import { ErrorAlertChannelPresenter } from "~/presenters/v3/ErrorAlertChannelPresenter.server";
|
||||
import { findProjectBySlug } from "~/models/project.server";
|
||||
import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { env } from "~/env.server";
|
||||
import {
|
||||
EnvironmentParamSchema,
|
||||
v3ErrorsConnectToSlackPath,
|
||||
v3ErrorsPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import {
|
||||
type CreateAlertChannelOptions,
|
||||
CreateAlertChannelService,
|
||||
} from "~/v3/services/alerts/createAlertChannel.server";
|
||||
import { ServiceValidationError } from "~/v3/services/baseService.server";
|
||||
import { useOptimisticLocation } from "~/hooks/useOptimisticLocation";
|
||||
import { useSearchParams } from "~/hooks/useSearchParam";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
const { projectParam, organizationSlug, envParam } = EnvironmentParamSchema.parse(params);
|
||||
|
||||
const project = await findProjectBySlug(organizationSlug, projectParam, userId);
|
||||
if (!project) {
|
||||
throw new Response("Project not found", { status: 404 });
|
||||
}
|
||||
|
||||
const environment = await findEnvironmentBySlug(project.id, envParam, userId);
|
||||
if (!environment) {
|
||||
throw new Response("Environment not found", { status: 404 });
|
||||
}
|
||||
|
||||
const presenter = new ErrorAlertChannelPresenter();
|
||||
const alertData = await presenter.call(project.id, environment.type);
|
||||
|
||||
const connectToSlackHref = v3ErrorsConnectToSlackPath({ slug: organizationSlug }, project, {
|
||||
slug: envParam,
|
||||
});
|
||||
|
||||
const errorsPath = v3ErrorsPath({ slug: organizationSlug }, project, { slug: envParam });
|
||||
|
||||
return typedjson({
|
||||
alertData,
|
||||
projectRef: project.externalRef,
|
||||
projectId: project.id,
|
||||
environmentType: environment.type,
|
||||
connectToSlackHref,
|
||||
errorsPath,
|
||||
});
|
||||
};
|
||||
|
||||
export const action = async ({ request, params }: ActionFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
const { projectParam, organizationSlug, envParam } = EnvironmentParamSchema.parse(params);
|
||||
|
||||
if (request.method.toUpperCase() !== "POST") {
|
||||
return json({ status: 405, error: "Method Not Allowed" }, { status: 405 });
|
||||
}
|
||||
|
||||
const project = await findProjectBySlug(organizationSlug, projectParam, userId);
|
||||
if (!project) {
|
||||
return json({ error: "Project not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
const environment = await findEnvironmentBySlug(project.id, envParam, userId);
|
||||
if (!environment) {
|
||||
return json({ error: "Environment not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
const formData = await request.formData();
|
||||
const submission = parseWithZod(formData, { schema: ErrorAlertsFormSchema });
|
||||
|
||||
if (submission.status !== "success") {
|
||||
return json(submission.reply());
|
||||
}
|
||||
|
||||
const { emails, webhooks, slackChannel, slackIntegrationId } = submission.value;
|
||||
|
||||
const emailEnabled = env.ALERT_FROM_EMAIL !== undefined && env.ALERT_RESEND_API_KEY !== undefined;
|
||||
const slackEnabled = !!slackIntegrationId;
|
||||
|
||||
const existingChannels = await prisma.projectAlertChannel.findMany({
|
||||
where: {
|
||||
projectId: project.id,
|
||||
alertTypes: { has: "ERROR_GROUP" },
|
||||
environmentTypes: { has: environment.type },
|
||||
},
|
||||
});
|
||||
|
||||
const service = new CreateAlertChannelService();
|
||||
const environmentTypes = [environment.type];
|
||||
const processedChannelIds = new Set<string>();
|
||||
|
||||
if (emailEnabled) {
|
||||
for (const email of emails) {
|
||||
const options: CreateAlertChannelOptions = {
|
||||
name: `Error alert to ${email}`,
|
||||
alertTypes: ["ERROR_GROUP"],
|
||||
environmentTypes,
|
||||
deduplicationKey: `error-email:${email}:${environment.type}`,
|
||||
channel: { type: "EMAIL", email },
|
||||
};
|
||||
const channel = await service.call(project.externalRef, userId, options);
|
||||
processedChannelIds.add(channel.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (slackEnabled && slackChannel) {
|
||||
const [channelId, channelName] = slackChannel.split("/");
|
||||
if (channelId && channelName) {
|
||||
const options: CreateAlertChannelOptions = {
|
||||
name: `Error alert to #${channelName}`,
|
||||
alertTypes: ["ERROR_GROUP"],
|
||||
environmentTypes,
|
||||
deduplicationKey: `error-slack:${environment.type}`,
|
||||
channel: {
|
||||
type: "SLACK",
|
||||
channelId,
|
||||
channelName,
|
||||
integrationId: slackIntegrationId,
|
||||
},
|
||||
};
|
||||
const channel = await service.call(project.externalRef, userId, options);
|
||||
processedChannelIds.add(channel.id);
|
||||
}
|
||||
}
|
||||
|
||||
for (const url of webhooks) {
|
||||
const options: CreateAlertChannelOptions = {
|
||||
name: `Error alert to ${new URL(url).hostname}`,
|
||||
alertTypes: ["ERROR_GROUP"],
|
||||
environmentTypes,
|
||||
deduplicationKey: `error-webhook:${url}:${environment.type}`,
|
||||
channel: { type: "WEBHOOK", url },
|
||||
};
|
||||
try {
|
||||
const channel = await service.call(project.externalRef, userId, options);
|
||||
processedChannelIds.add(channel.id);
|
||||
} catch (error) {
|
||||
// CreateAlertChannelService rejects unsafe webhook URLs.
|
||||
if (error instanceof ServiceValidationError) {
|
||||
return json(submission.reply({ fieldErrors: { webhooks: [error.message] } }));
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const editableTypes = new Set<string>(["WEBHOOK"]);
|
||||
if (emailEnabled) {
|
||||
editableTypes.add("EMAIL");
|
||||
}
|
||||
if (slackEnabled) {
|
||||
editableTypes.add("SLACK");
|
||||
}
|
||||
|
||||
const channelsToDelete = existingChannels.filter(
|
||||
(ch) =>
|
||||
!processedChannelIds.has(ch.id) &&
|
||||
editableTypes.has(ch.type) &&
|
||||
ch.alertTypes.length === 1 &&
|
||||
ch.alertTypes[0] === "ERROR_GROUP"
|
||||
);
|
||||
|
||||
for (const ch of channelsToDelete) {
|
||||
await prisma.projectAlertChannel.delete({ where: { id: ch.id } });
|
||||
}
|
||||
|
||||
return json({ ok: true });
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const { alertData, connectToSlackHref, errorsPath } = useTypedLoaderData<typeof loader>();
|
||||
const { has } = useSearchParams();
|
||||
const showAlerts = has("alerts") ?? false;
|
||||
const navigate = useNavigate();
|
||||
const location = useOptimisticLocation();
|
||||
|
||||
const closeAlerts = useCallback(() => {
|
||||
const params = new URLSearchParams(location.search);
|
||||
params.delete("alerts");
|
||||
const qs = params.toString();
|
||||
navigate(qs ? `?${qs}` : location.pathname, { replace: true });
|
||||
}, [location.search, location.pathname, navigate]);
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<Outlet />
|
||||
|
||||
<Sheet open={showAlerts} onOpenChange={(open) => !open && closeAlerts()}>
|
||||
<SheetContent
|
||||
side="right"
|
||||
className="w-[420px] min-w-[320px] max-w-[560px] p-0 sm:max-w-[560px]"
|
||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
<ConfigureErrorAlerts
|
||||
{...alertData}
|
||||
connectToSlackHref={connectToSlackHref}
|
||||
formAction={errorsPath}
|
||||
/>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user