Files
formbricks/apps/web/modules/integrations/webhooks/components/add-webhook-modal.tsx
T

282 lines
10 KiB
TypeScript

"use client";
import { PipelineTriggers, Webhook } from "@prisma/client";
import clsx from "clsx";
import { Webhook as WebhookIcon } from "lucide-react";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { TSurvey } from "@formbricks/types/surveys/types";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { SurveyCheckboxGroup } from "@/modules/integrations/webhooks/components/survey-checkbox-group";
import { TriggerCheckboxGroup } from "@/modules/integrations/webhooks/components/trigger-checkbox-group";
import { WebhookCreatedModal } from "@/modules/integrations/webhooks/components/webhook-created-modal";
import { isDiscordWebhook, validWebHookURL } from "@/modules/integrations/webhooks/lib/utils";
import { Button } from "@/modules/ui/components/button";
import {
Dialog,
DialogBody,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/modules/ui/components/dialog";
import { Input } from "@/modules/ui/components/input";
import { Label } from "@/modules/ui/components/label";
import { createWebhookAction, testEndpointAction } from "../actions";
import { TWebhookInput } from "../types/webhooks";
interface AddWebhookModalProps {
environmentId: string;
open: boolean;
surveys: TSurvey[];
setOpen: (v: boolean) => void;
}
export const AddWebhookModal = ({ environmentId, surveys, open, setOpen }: AddWebhookModalProps) => {
const router = useRouter();
const {
handleSubmit,
reset,
register,
formState: { isSubmitting },
} = useForm<TWebhookInput>();
const { t } = useTranslation();
const [testEndpointInput, setTestEndpointInput] = useState("");
const [hittingEndpoint, setHittingEndpoint] = useState<boolean>(false);
const [endpointAccessible, setEndpointAccessible] = useState<boolean>();
const [selectedTriggers, setSelectedTriggers] = useState<PipelineTriggers[]>([]);
const [selectedSurveys, setSelectedSurveys] = useState<string[]>([]);
const [selectedAllSurveys, setSelectedAllSurveys] = useState(false);
const [creatingWebhook, setCreatingWebhook] = useState(false);
const [createdWebhook, setCreatedWebhook] = useState<Webhook | null>(null);
const [webhookSecret, setWebhookSecret] = useState<string | undefined>();
const handleTestEndpoint = async (
sendSuccessToast: boolean
): Promise<{ success: boolean; secret?: string }> => {
try {
const { valid, error } = validWebHookURL(testEndpointInput);
if (!valid) {
toast.error(error ?? t("common.something_went_wrong_please_try_again"));
return { success: false };
}
setHittingEndpoint(true);
const testEndpointActionResult = await testEndpointAction({
url: testEndpointInput,
secret: webhookSecret,
});
if (!testEndpointActionResult?.data) {
const errorMessage = getFormattedErrorMessage(testEndpointActionResult);
throw new Error(errorMessage);
}
setHittingEndpoint(false);
if (sendSuccessToast) toast.success(t("environments.integrations.webhooks.endpoint_pinged"));
setEndpointAccessible(true);
if (testEndpointActionResult.data.secret) {
setWebhookSecret(testEndpointActionResult.data.secret);
}
return testEndpointActionResult.data;
} catch (err) {
setHittingEndpoint(false);
const errMessage = err instanceof Error ? err.message : "Unknown error occurred";
toast.error(
`${t("environments.integrations.webhooks.endpoint_pinged_error")} \n ${
errMessage.length < 250 ? errMessage : t("environments.integrations.webhooks.please_check_console")
}`,
{ className: errMessage.length < 250 ? "break-all" : "" }
);
console.error(t("environments.integrations.webhooks.webhook_test_failed_due_to"), errMessage);
setEndpointAccessible(false);
return { success: false };
}
};
const handleSelectAllSurveys = () => {
setSelectedAllSurveys(!selectedAllSurveys);
setSelectedSurveys([]);
};
const handleSelectedSurveyChange = (surveyId: string) => {
setSelectedSurveys((prevSelectedSurveys: string[]) =>
prevSelectedSurveys.includes(surveyId)
? prevSelectedSurveys.filter((id) => id !== surveyId)
: [...prevSelectedSurveys, surveyId]
);
};
const handleCheckboxChange = (selectedValue: PipelineTriggers) => {
setSelectedTriggers((prevValues) =>
prevValues.includes(selectedValue)
? prevValues.filter((value) => value !== selectedValue)
: [...prevValues, selectedValue]
);
};
const submitWebhook = async (data: TWebhookInput): Promise<void> => {
if (!isSubmitting) {
try {
setCreatingWebhook(true);
if (!testEndpointInput || testEndpointInput === "") {
throw new Error(t("environments.integrations.webhooks.please_enter_a_url"));
}
if (selectedTriggers.length === 0) {
throw new Error(t("common.please_select_at_least_one_trigger"));
}
if (!selectedAllSurveys && selectedSurveys.length === 0) {
throw new Error(t("common.please_select_at_least_one_survey"));
}
if (isDiscordWebhook(testEndpointInput)) {
throw new Error(t("environments.integrations.webhooks.discord_webhook_not_supported"));
}
const testResult = await handleTestEndpoint(false);
if (!testResult.success) return;
const updatedData: TWebhookInput = {
name: data.name,
url: testEndpointInput,
source: "user",
triggers: selectedTriggers,
surveyIds: selectedSurveys,
};
const createWebhookActionResult = await createWebhookAction({
environmentId,
webhookInput: updatedData,
webhookSecret: testResult.secret,
});
if (createWebhookActionResult?.data) {
router.refresh();
setCreatedWebhook(createWebhookActionResult.data);
toast.success(t("environments.integrations.webhooks.webhook_added_successfully"));
} else {
const errorMessage = getFormattedErrorMessage(createWebhookActionResult);
toast.error(errorMessage);
}
} catch (e) {
toast.error(e instanceof Error ? e.message : "Unknown error occurred");
} finally {
setCreatingWebhook(false);
}
}
};
const resetAndClose = () => {
setOpen(false);
reset();
setTestEndpointInput("");
setEndpointAccessible(undefined);
setSelectedSurveys([]);
setSelectedTriggers([]);
setSelectedAllSurveys(false);
setCreatedWebhook(null);
setWebhookSecret(undefined);
};
// Show success dialog with secret after webhook creation
if (createdWebhook) {
return <WebhookCreatedModal open={open} webhook={createdWebhook} onClose={resetAndClose} />;
}
return (
<Dialog open={open} onOpenChange={resetAndClose}>
<DialogContent>
<DialogHeader>
<WebhookIcon />
<DialogTitle>{t("environments.integrations.webhooks.add_webhook")}</DialogTitle>
<DialogDescription>
{t("environments.integrations.webhooks.add_webhook_description")}
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit(submitWebhook)}>
<DialogBody className="space-y-4 pb-4">
<div className="col-span-1">
<Label htmlFor="name">{t("common.name")}</Label>
<div className="mt-1 flex">
<Input
type="text"
id="name"
{...register("name")}
placeholder={t("environments.integrations.webhooks.webhook_name_placeholder")}
/>
</div>
</div>
<div className="col-span-1">
<Label htmlFor="URL">{t("common.url")}</Label>
<div className="mt-1 flex">
<Input
type="url"
id="URL"
value={testEndpointInput}
onChange={(e) => {
setTestEndpointInput(e.target.value);
}}
className={clsx(
endpointAccessible === true
? "border-green-500 bg-green-50"
: endpointAccessible === false
? "border-red-200 bg-red-50"
: endpointAccessible === undefined
? "border-slate-200 bg-white"
: null
)}
placeholder={t("environments.integrations.webhooks.webhook_url_placeholder")}
/>
<Button
type="button"
variant="secondary"
loading={hittingEndpoint}
className="ml-2 whitespace-nowrap"
disabled={testEndpointInput.trim() === ""}
onClick={() => {
handleTestEndpoint(true);
}}>
{t("environments.integrations.webhooks.test_endpoint")}
</Button>
</div>
</div>
<div>
<Label htmlFor="Triggers">{t("environments.integrations.webhooks.triggers")}</Label>
<TriggerCheckboxGroup
selectedTriggers={selectedTriggers}
onCheckboxChange={handleCheckboxChange}
allowChanges={true}
/>
</div>
<div>
<Label htmlFor="Surveys">{t("common.surveys")}</Label>
<SurveyCheckboxGroup
surveys={surveys}
selectedSurveys={selectedSurveys}
selectedAllSurveys={selectedAllSurveys}
onSelectAllSurveys={handleSelectAllSurveys}
onSelectedSurveyChange={handleSelectedSurveyChange}
allowChanges={true}
/>
</div>
</DialogBody>
<DialogFooter>
<Button type="button" variant="secondary" onClick={resetAndClose}>
{t("common.cancel")}
</Button>
<Button type="submit" loading={creatingWebhook}>
{t("environments.integrations.webhooks.add_webhook")}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
};