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