import { type ActionFunctionArgs, type LoaderFunctionArgs, json, redirect } from "@remix-run/node"; import { fromPromise } from "neverthrow"; import { Form, useActionData, useNavigation } from "@remix-run/react"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; import { DialogClose } from "@radix-ui/react-dialog"; import { TrashIcon } from "@heroicons/react/20/solid"; import { BugIcon } from "~/assets/icons/BugIcon"; import { SlackMonoIcon } from "~/assets/icons/SlackMonoIcon"; import { Button } from "~/components/primitives/Buttons"; import { DateTime } from "~/components/primitives/DateTime"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from "~/components/primitives/Dialog"; import { FormButtons } from "~/components/primitives/FormButtons"; import { Header2, Header3 } from "~/components/primitives/Headers"; import { Hint } from "~/components/primitives/Hint"; import { MainHorizontallyCenteredContainer, PageBody, PageContainer, } from "~/components/layout/AppLayout"; import { NavBar, PageTitle } from "~/components/primitives/PageHeader"; import { Paragraph } from "~/components/primitives/Paragraph"; import { Table, TableBody, TableCell, TableHeader, TableHeaderCell, TableRow, } from "~/components/primitives/Table"; import { EnabledStatus } from "~/components/runs/v3/EnabledStatus"; import { $transaction, prisma } from "~/db.server"; import { requireOrganization } from "~/services/org.server"; import { OrganizationParamsSchema, organizationSlackIntegrationPath } from "~/utils/pathBuilder"; import { logger } from "~/services/logger.server"; export const loader = async ({ request, params }: LoaderFunctionArgs) => { const { organizationSlug } = OrganizationParamsSchema.parse(params); const { organization } = await requireOrganization(request, organizationSlug); const slackIntegration = await prisma.organizationIntegration.findFirst({ where: { organizationId: organization.id, service: "SLACK", deletedAt: null, }, }); if (!slackIntegration) { return typedjson({ organization, slackIntegration: null, alertChannels: [], teamName: null, }); } const integrationData = slackIntegration.integrationData as any; const teamName = integrationData?.team?.name ?? null; const alertChannels = await prisma.projectAlertChannel.findMany({ where: { type: "SLACK", project: { organizationId: organization.id }, OR: [ { integrationId: slackIntegration.id }, { properties: { path: ["integrationId"], equals: slackIntegration.id, }, }, ], }, include: { project: { select: { id: true, slug: true, name: true, }, }, }, orderBy: { createdAt: "desc", }, }); return typedjson({ organization, slackIntegration, alertChannels, teamName, }); }; const ActionSchema = z.object({ intent: z.literal("uninstall"), }); export const action = async ({ request, params }: ActionFunctionArgs) => { const { organizationSlug } = OrganizationParamsSchema.parse(params); const { organization, userId } = await requireOrganization(request, organizationSlug); const formData = await request.formData(); const result = ActionSchema.safeParse({ intent: formData.get("intent") }); if (!result.success) { return json({ error: "Invalid action" }, { status: 400 }); } const slackIntegration = await prisma.organizationIntegration.findFirst({ where: { organizationId: organization.id, service: "SLACK", deletedAt: null, }, }); if (!slackIntegration) { return json({ error: "Slack integration not found" }, { status: 404 }); } const txResult = await fromPromise( $transaction(prisma, async (tx) => { await tx.projectAlertChannel.updateMany({ where: { type: "SLACK", OR: [ { integrationId: slackIntegration.id }, { properties: { path: ["integrationId"], equals: slackIntegration.id, }, }, ], }, data: { enabled: false, integrationId: null, }, }); await tx.organizationIntegration.update({ where: { id: slackIntegration.id }, data: { deletedAt: new Date() }, }); }), (error) => error ); if (txResult.isErr()) { logger.error("Failed to remove Slack integration", { organizationId: organization.id, organizationSlug, userId, integrationId: slackIntegration.id, error: txResult.error instanceof Error ? txResult.error.message : String(txResult.error), }); return json( { error: "Failed to remove Slack integration. Please try again." }, { status: 500 } ); } logger.info("Slack integration removed successfully", { organizationId: organization.id, organizationSlug, userId, integrationId: slackIntegration.id, }); return redirect(organizationSlackIntegrationPath({ slug: organizationSlug })); }; export default function SlackIntegrationPage() { const { slackIntegration, alertChannels, teamName } = useTypedLoaderData(); const actionData = useActionData(); const navigation = useNavigation(); const isUninstalling = navigation.state === "submitting" && navigation.formData?.get("intent") === "uninstall"; if (!slackIntegration) { return (
No Slack integration found Your organization doesn't have a Slack integration configured. You can connect Slack when setting up alerts from the{" "} Errors page.
); } return (
Integration details
{teamName && ( Workspace:{" "} {teamName} )} Installed:{" "}
Connected alert channels ({alertChannels.length}) {alertChannels.length === 0 ? ( No alert channels are currently connected to this Slack integration. ) : ( Channel Project Status Created {alertChannels.map((channel) => ( {channel.name} {channel.project.name} ))}
)}
Danger zone
Remove integration This will remove the Slack integration and disable all connected alert channels. This action cannot be undone. {actionData?.error && ( {actionData.error} )} Remove Slack integration This will remove the Slack integration and disable all connected alert channels. This action cannot be undone. } cancelButton={ } /> } />
); }