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:
Syed Ali Shahbaz 2021-10-25 21:45:52 +05:30 committed by GitHub
parent 9842aaaf6a
commit baba307a9f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 134 additions and 4 deletions

View file

@ -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 { WebhookTriggerEvents } from "@prisma/client";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
import Image from "next/image";
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import React, { useState } from "react";
import { Controller, useForm, useWatch } from "react-hook-form";
import { useMutation } from "react-query";
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: {
//
defaultValues?: TWebhook;
@ -133,6 +194,7 @@ function WebhookDialogForm(props: {
const form = useForm({
defaultValues,
});
return (
<Form
data-testid="WebhookDialogForm"
@ -209,6 +271,7 @@ function WebhookDialogForm(props: {
))}
</InputGroupBox>
</fieldset>
<WebhookTestDisclosure />
<DialogFooter>
<Button type="button" color="secondary" onClick={props.handleClose} tabIndex={-1}>
{t("cancel")}

View file

@ -45,11 +45,15 @@
"webhook_status": "Webhook Status",
"webhook_enabled": "Webhook Enabled",
"webhook_disabled": "Webhook Disabled",
"webhook_response": "Webhook response",
"webhook_test": "Webhook test",
"manage_your_webhook": "Manage your webhook",
"webhook_created_successfully": "Webhook created successfully!",
"webhook_updated_successfully": "Webhook updated successfully!",
"webhook_removed_successfully": "Webhook removed successfully!",
"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.",
"upcoming": "Upcoming",
"past": "Past",
@ -169,6 +173,7 @@
"whoops": "Whoops",
"login": "Login",
"success": "Success",
"failed": "Failed",
"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.",
"back_to_bookings": "Back to bookings",

View file

@ -13,6 +13,7 @@ import { TRPCError } from "@trpc/server";
import { createProtectedRouter, createRouter } from "../createRouter";
import { resizeBase64Image } from "../lib/resizeBase64Image";
import { webhookRouter } from "./viewer/webhook";
const checkUsername =
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);

View 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,
};
}
},
});