Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: workspace webhooks #2748

Merged
merged 6 commits into from
Nov 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions web/components/web-hooks/delete-webhook-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { Dialog, Transition } from "@headlessui/react";
import { Button, Input } from "@plane/ui";
import { error } from "console";
import useToast from "hooks/use-toast";
import { useMobxStore } from "lib/mobx/store-provider";
import { AlertTriangle } from "lucide-react";
import { useRouter } from "next/router";
import React, { useState } from "react";
import { FC } from "react";
import { Controller, useForm } from "react-hook-form";
import { WebhookStore } from "store/webhook.store";

interface IDeleteWebhook {
isOpen: boolean;
webhook_url: string;
onClose: () => void;
}

export const DeleteWebhookModal: FC<IDeleteWebhook> = (props) => {
const { isOpen, onClose } = props;

const router = useRouter();

const { webhook: webhookStore } = useMobxStore();

const { setToastAlert } = useToast();

const [deleting, setDelete] = useState(false);

const {
control,
formState: { errors, isSubmitting },
handleSubmit,
reset,
watch,
} = useForm({});

const { workspaceSlug, webhookId } = router.query;

const handleClose = () => {
onClose();
};

const handleDelete = async () => {
setDelete(true);
if (!workspaceSlug || !webhookId) return;
webhookStore
.remove(workspaceSlug.toString(), webhookId.toString())
.then(() => {
setToastAlert({
title: "Success",
type: "success",
message: "Successfully deleted",
});
router.replace(`/${workspaceSlug}/settings/webhooks/`);
})
.catch((error) => {
console.log(error);
setToastAlert({
title: "Oops!",
type: "error",
message: error?.error,
});
})
.finally(() => {
setDelete(false);
});
};

return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child>

<div className="fixed inset-0 z-20 overflow-y-auto">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg border border-custom-border-200 bg-custom-background-100 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl p-6">
<div className="flex w-full items-center justify-start gap-6">
<span className="place-items-center rounded-full bg-red-500/20 p-4">
<AlertTriangle className="h-6 w-6 text-red-600" aria-hidden="true" />
</span>
<span className="flex items-center justify-start">
<h3 className="text-xl font-medium 2xl:text-2xl">Delete Webhook</h3>
</span>
</div>

<span>
<p className="text-sm leading-7 text-custom-text-200">
Are you sure you want to delete workspace <span className="break-words font-semibold"></span>? All
of the data related to the workspace will be permanently removed. This action cannot be undone.
</p>
</span>

<div className="flex justify-end gap-2">
<Button variant="neutral-primary" onClick={onClose}>
Cancel
</Button>
<Button variant="danger" type="submit" onClick={handleDelete}>
{deleting ? "Deleting..." : "Delete Webhook"}
</Button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
};
31 changes: 31 additions & 0 deletions web/components/web-hooks/empty-webhooks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { FC } from "react";
import Link from "next/link";
import { Button } from "@plane/ui";
import Image from "next/image";
import EmptyWebhookLogo from "public/empty-state/issue.svg";

interface IWebHookLists {
workspaceSlug: string;
}


export const EmptyWebhooks:FC<IWebHookLists> = (props) => {

const {workspaceSlug} = props;

return (
<div className="flex items-start justify-center">
<div className="flex p-10 flex-col items-center justify-center rounded-[4px] border border-custom-border-200 bg-custom-color-background-90">
<Image width="178" height="116" src={EmptyWebhookLogo} alt="empty-webhook image" />

<div className="mt-4 text-base font-semibold">No Webhooks</div>
<p className="text-sm text-neutral-600">Create webhooks to receive real-time updates and automate actions</p>
<Link href={`/${workspaceSlug}/settings/webhooks/create`}>
<Button variant="primary" className="mt-2">
Add Webhook
</Button>
</Link>
</div>
</div>
);
};
122 changes: 122 additions & 0 deletions web/components/web-hooks/generate-key.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { useState, FC } from "react";
import { Button, Input } from "@plane/ui";
import { Copy, Eye, EyeOff, RefreshCw } from "lucide-react";
import { generateRandomString } from "helpers/generate-random-string";
import { observer } from "mobx-react-lite";
import { RootStore } from "store/root";
import { useMobxStore } from "lib/mobx/store-provider";
import { copyTextToClipboard, copyUrlToClipboard } from "helpers/string.helper";
import { useRouter } from "next/router";
import useToast from "hooks/use-toast";
import { csvDownload } from "helpers/download.helper";
import { stringify } from "querystring";
import { renderDateFormat } from "helpers/date-time.helper";

interface IGenerateKey {
type: "create" | "edit";
}

export const GenerateKey: FC<IGenerateKey> = observer((props) => {
const { type } = props;
const [regenerating, setRegenerate] = useState(false);
const router = useRouter();
const { workspaceSlug, webhookId } = router.query as { workspaceSlug: string, webhookId: string };
const { webhook: webhookStore, workspace: workspaceStore }: RootStore = useMobxStore();
const { setToastAlert } = useToast();
// const [showgenerateKey, setShowGenarateKey] = useState(false);

function handleRegenerate() {
setRegenerate(true);
webhookStore.regenerate(workspaceSlug, webhookId).then(
() => {
// setShowGenarateKey(true);
setToastAlert({
title: "Success",
type: "success",
message: "Successfully regenerated",
});
csvDownload([[
"id",
"url",
"created_at",
"updated_at",
"is_active",
"secret_key",
"project",
"issue",
"module",
"cycle",
"issue_comment",
"workspace"
], [
webhookStore.currentWebhook?.id!,
webhookStore.currentWebhook?.url!,
renderDateFormat(webhookStore.currentWebhook?.created_at!),
renderDateFormat(webhookStore.currentWebhook?.updated_at!),
String(webhookStore.currentWebhook?.is_active!),
webhookStore.currentWebhook?.secret_key!,
String(webhookStore.currentWebhook?.project!),
String(webhookStore.currentWebhook?.issue!),
String(webhookStore.currentWebhook?.module!),
String(webhookStore.currentWebhook?.cycle!),
String(webhookStore.currentWebhook?.issue_comment!),
workspaceStore.currentWorkspace?.name!,
]
], "Secret-key")
}
).catch((err)=>{
setToastAlert({
title: "Oops!",
type: "error",
message: err?.error,
});
}).finally(()=>{
setRegenerate(false);
})
}
console.log(webhookStore?.webhookSecretKey);
return (
<>
{(type === 'edit' || (type === 'create' && webhookStore?.webhookSecretKey)) && (
<div className="space-y-2">
<div className="text-sm font-medium">Secret Key</div>
<div className="text-sm text-neutral-400">Genarate a token to sign-in the webhook payload</div>

<div className="flex gap-5">
<div className="relative flex items-center p-2 rounded w-full border border-custom-border-200">
<div className="w-full overflow-hidden px-2 font-medium select-none">
{(webhookStore?.webhookSecretKey) ? <div>{webhookStore?.webhookSecretKey}</div> :
<div className="flex items-center gap-1.5">
{[...Array(41)].map((_, index) => <div key={index} className="w-[4px] h-[4px] bg-gray-300 rounded-full"></div>)}
</div>
}
</div>
{webhookStore?.webhookSecretKey && (
<div className="w-7 h-7 flex-shrink-0 flex justify-center items-center cursor-pointer hover:bg-custom-background-80 rounded" onClick={() => copyTextToClipboard(webhookStore?.webhookSecretKey || '')}>
<Copy onClick={() => {
navigator.clipboard.writeText(webhookStore.webhookSecretKey!);
setToastAlert({
title: "Success",
type: "success",
message: "Secret key copied",
});
}} className="text-custom-text-400 w-4 h-4" />
</div>
)}
</div>
<div>
{type != 'create' && (
<Button disabled={regenerating} onClick={
handleRegenerate
} className="">
<RefreshCw className={`h-3 w-3`} />
{regenerating ? "Re-generating..." : "Re-genarate Key"}
</Button>
)}
</div>
</div>
</div >
)}
</>
);
});
4 changes: 4 additions & 0 deletions web/components/web-hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from "./empty-webhooks";
export * from "./webhooks-list";
export * from "./webhooks-list-item";
export * from "./webhook-form";
Loading
Loading