Feature/cal 605 add webhook test check during webhook (#1035)
* starting point * lint fix * add mock placeholder * simplified a bit * add some placeholder ui * err handling * multiple fixes * post rebase fixes * removed extra webhook enabled button * finishing touches * added translations * removed debug remnants * requested changes Co-authored-by: KATT <alexander@n1s.se>
This commit is contained in:
parent
9842aaaf6a
commit
baba307a9f
4 changed files with 134 additions and 4 deletions
|
@ -1,9 +1,16 @@
|
||||||
import { PencilAltIcon, TrashIcon } from "@heroicons/react/outline";
|
import {
|
||||||
|
PencilAltIcon,
|
||||||
|
SwitchHorizontalIcon,
|
||||||
|
TrashIcon,
|
||||||
|
ChevronDownIcon,
|
||||||
|
ChevronUpIcon,
|
||||||
|
} from "@heroicons/react/outline";
|
||||||
import { ClipboardIcon } from "@heroicons/react/solid";
|
import { ClipboardIcon } from "@heroicons/react/solid";
|
||||||
import { WebhookTriggerEvents } from "@prisma/client";
|
import { WebhookTriggerEvents } from "@prisma/client";
|
||||||
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||||
import { useMutation } from "react-query";
|
import { useMutation } from "react-query";
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
|
@ -113,6 +120,60 @@ function WebhookListItem(props: { webhook: TWebhook; onEditWebhook: () => void }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function WebhookTestDisclosure() {
|
||||||
|
const subscriberUrl: string = useWatch({ name: "subscriberUrl" });
|
||||||
|
const { t } = useLocale();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const mutation = trpc.useMutation("viewer.webhook.testTrigger", {
|
||||||
|
onError(err) {
|
||||||
|
showToast(err.message, "error");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Collapsible open={open} onOpenChange={() => setOpen(!open)}>
|
||||||
|
<CollapsibleTrigger type="button" className={"cursor-pointer flex w-full text-sm"}>
|
||||||
|
{t("webhook_test")}{" "}
|
||||||
|
{open ? (
|
||||||
|
<ChevronUpIcon className="w-5 h-5 text-gray-700" />
|
||||||
|
) : (
|
||||||
|
<ChevronDownIcon className="w-5 h-5 text-gray-700" />
|
||||||
|
)}
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent>
|
||||||
|
<InputGroupBox className="px-0 space-y-0 border-0">
|
||||||
|
<div className="flex justify-between p-2 bg-gray-50">
|
||||||
|
<h3 className="self-center text-gray-700">{t("webhook_response")}</h3>
|
||||||
|
<Button
|
||||||
|
StartIcon={SwitchHorizontalIcon}
|
||||||
|
type="button"
|
||||||
|
color="minimal"
|
||||||
|
disabled={mutation.isLoading}
|
||||||
|
onClick={() => mutation.mutate({ url: subscriberUrl, type: "PING" })}>
|
||||||
|
{t("ping_test")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="p-2 text-gray-500 border-8 border-gray-50">
|
||||||
|
{!mutation.data && <em>{t("no_data_yet")}</em>}
|
||||||
|
{mutation.status === "success" && (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"px-2 py-1 w-max text-xs ml-auto",
|
||||||
|
mutation.data.status === 200 ? "text-green-500 bg-green-50" : "text-red-500 bg-red-50"
|
||||||
|
)}>
|
||||||
|
{mutation.data.status === 200 ? t("success") : t("failed")}
|
||||||
|
</div>
|
||||||
|
<pre className="overflow-x-auto">{JSON.stringify(mutation.data, null, 4)}</pre>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</InputGroupBox>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function WebhookDialogForm(props: {
|
function WebhookDialogForm(props: {
|
||||||
//
|
//
|
||||||
defaultValues?: TWebhook;
|
defaultValues?: TWebhook;
|
||||||
|
@ -133,6 +194,7 @@ function WebhookDialogForm(props: {
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
defaultValues,
|
defaultValues,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form
|
<Form
|
||||||
data-testid="WebhookDialogForm"
|
data-testid="WebhookDialogForm"
|
||||||
|
@ -209,6 +271,7 @@ function WebhookDialogForm(props: {
|
||||||
))}
|
))}
|
||||||
</InputGroupBox>
|
</InputGroupBox>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
<WebhookTestDisclosure />
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button type="button" color="secondary" onClick={props.handleClose} tabIndex={-1}>
|
<Button type="button" color="secondary" onClick={props.handleClose} tabIndex={-1}>
|
||||||
{t("cancel")}
|
{t("cancel")}
|
||||||
|
|
|
@ -45,11 +45,15 @@
|
||||||
"webhook_status": "Webhook Status",
|
"webhook_status": "Webhook Status",
|
||||||
"webhook_enabled": "Webhook Enabled",
|
"webhook_enabled": "Webhook Enabled",
|
||||||
"webhook_disabled": "Webhook Disabled",
|
"webhook_disabled": "Webhook Disabled",
|
||||||
|
"webhook_response": "Webhook response",
|
||||||
|
"webhook_test": "Webhook test",
|
||||||
"manage_your_webhook": "Manage your webhook",
|
"manage_your_webhook": "Manage your webhook",
|
||||||
"webhook_created_successfully": "Webhook created successfully!",
|
"webhook_created_successfully": "Webhook created successfully!",
|
||||||
"webhook_updated_successfully": "Webhook updated successfully!",
|
"webhook_updated_successfully": "Webhook updated successfully!",
|
||||||
"webhook_removed_successfully": "Webhook removed successfully!",
|
"webhook_removed_successfully": "Webhook removed successfully!",
|
||||||
"dismiss": "Dismiss",
|
"dismiss": "Dismiss",
|
||||||
|
"no_data_yet": "No data yet",
|
||||||
|
"ping_test": "Ping test",
|
||||||
"add_to_homescreen": "Add this app to your home screen for faster access and improved experience.",
|
"add_to_homescreen": "Add this app to your home screen for faster access and improved experience.",
|
||||||
"upcoming": "Upcoming",
|
"upcoming": "Upcoming",
|
||||||
"past": "Past",
|
"past": "Past",
|
||||||
|
@ -169,6 +173,7 @@
|
||||||
"whoops": "Whoops",
|
"whoops": "Whoops",
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
"success": "Success",
|
"success": "Success",
|
||||||
|
"failed": "Failed",
|
||||||
"password_has_been_reset_login": "Your password has been reset. You can now login with your newly created password.",
|
"password_has_been_reset_login": "Your password has been reset. You can now login with your newly created password.",
|
||||||
"unexpected_error_try_again": "An unexpected error occurred. Try again.",
|
"unexpected_error_try_again": "An unexpected error occurred. Try again.",
|
||||||
"back_to_bookings": "Back to bookings",
|
"back_to_bookings": "Back to bookings",
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { TRPCError } from "@trpc/server";
|
||||||
|
|
||||||
import { createProtectedRouter, createRouter } from "../createRouter";
|
import { createProtectedRouter, createRouter } from "../createRouter";
|
||||||
import { resizeBase64Image } from "../lib/resizeBase64Image";
|
import { resizeBase64Image } from "../lib/resizeBase64Image";
|
||||||
|
import { webhookRouter } from "./viewer/webhook";
|
||||||
|
|
||||||
const checkUsername =
|
const checkUsername =
|
||||||
process.env.NEXT_PUBLIC_APP_URL === "https://cal.com" ? checkPremiumUsername : checkRegularUsername;
|
process.env.NEXT_PUBLIC_APP_URL === "https://cal.com" ? checkPremiumUsername : checkRegularUsername;
|
||||||
|
@ -383,4 +384,7 @@ const loggedInViewerRouter = createProtectedRouter()
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const viewerRouter = createRouter().merge(publicViewerRouter).merge(loggedInViewerRouter);
|
export const viewerRouter = createRouter()
|
||||||
|
.merge(publicViewerRouter)
|
||||||
|
.merge(loggedInViewerRouter)
|
||||||
|
.merge("webhook.", webhookRouter);
|
||||||
|
|
58
server/routers/viewer/webhook.tsx
Normal file
58
server/routers/viewer/webhook.tsx
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { getErrorFromUnknown } from "@lib/errors";
|
||||||
|
|
||||||
|
import { createProtectedRouter } from "@server/createRouter";
|
||||||
|
|
||||||
|
export const webhookRouter = createProtectedRouter().mutation("testTrigger", {
|
||||||
|
input: z.object({
|
||||||
|
url: z.string().url(),
|
||||||
|
type: z.string(),
|
||||||
|
}),
|
||||||
|
async resolve({ input }) {
|
||||||
|
const { url, type } = input;
|
||||||
|
|
||||||
|
const responseBodyMocks: Record<"PING", unknown> = {
|
||||||
|
PING: {
|
||||||
|
triggerEvent: "PING",
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
payload: {
|
||||||
|
type: "Test",
|
||||||
|
title: "Test trigger event",
|
||||||
|
description: "",
|
||||||
|
startTime: new Date().toISOString(),
|
||||||
|
endTime: new Date().toISOString(),
|
||||||
|
organizer: {
|
||||||
|
name: "Cal",
|
||||||
|
email: "",
|
||||||
|
timeZone: "Europe/London",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const body = responseBodyMocks[type as "PING"];
|
||||||
|
if (!body) {
|
||||||
|
throw new Error(`Unknown type '${type}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
// [...]
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
const text = await res.text();
|
||||||
|
return {
|
||||||
|
status: res.status,
|
||||||
|
message: text,
|
||||||
|
};
|
||||||
|
} catch (_err) {
|
||||||
|
const err = getErrorFromUnknown(_err);
|
||||||
|
return {
|
||||||
|
status: 500,
|
||||||
|
message: err.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
Loading…
Reference in a new issue