From daf2e2036eb3093472d6a118309aa2b736acb241 Mon Sep 17 00:00:00 2001 From: akhilmhdh Date: Tue, 11 Jul 2023 22:53:35 +0530 Subject: [PATCH] feat(webhook): implemented ui for webhooks --- frontend/package-lock.json | 11 + frontend/package.json | 1 + frontend/public/locales/en/translations.json | 4 + frontend/src/components/v2/Button/Button.tsx | 23 +- .../src/components/v2/Tooltip/Tooltip.tsx | 4 +- frontend/src/hooks/api/index.tsx | 1 + frontend/src/hooks/api/types.ts | 1 + frontend/src/hooks/api/webhooks/index.tsx | 2 + frontend/src/hooks/api/webhooks/mutation.tsx | 67 +++ frontend/src/hooks/api/webhooks/query.tsx | 26 + frontend/src/hooks/api/webhooks/types.ts | 36 ++ frontend/src/layouts/AppLayout/AppLayout.tsx | 448 ++++++++++-------- .../ProjectSettingsPage.tsx | 57 ++- .../ProjectTabGroup/ProjectTabGroup.tsx | 39 -- .../components/ProjectTabGroup/index.tsx | 1 - .../components/WebhooksTab/AddWebhookForm.tsx | 133 ++++++ .../components/WebhooksTab/WebhooksTab.tsx | 279 +++++++++++ .../components/WebhooksTab/index.tsx | 1 + .../ProjectSettingsPage/components/index.tsx | 1 - 19 files changed, 890 insertions(+), 245 deletions(-) create mode 100644 frontend/src/hooks/api/webhooks/index.tsx create mode 100644 frontend/src/hooks/api/webhooks/mutation.tsx create mode 100644 frontend/src/hooks/api/webhooks/query.tsx create mode 100644 frontend/src/hooks/api/webhooks/types.ts delete mode 100644 frontend/src/views/Settings/ProjectSettingsPage/components/ProjectTabGroup/ProjectTabGroup.tsx delete mode 100644 frontend/src/views/Settings/ProjectSettingsPage/components/ProjectTabGroup/index.tsx create mode 100644 frontend/src/views/Settings/ProjectSettingsPage/components/WebhooksTab/AddWebhookForm.tsx create mode 100644 frontend/src/views/Settings/ProjectSettingsPage/components/WebhooksTab/WebhooksTab.tsx create mode 100644 frontend/src/views/Settings/ProjectSettingsPage/components/WebhooksTab/index.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 717741bfb..cb3360485 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -44,6 +44,7 @@ "classnames": "^2.3.1", "cookies": "^0.8.0", "cva": "npm:class-variance-authority@^0.4.0", + "dayjs": "^1.11.9", "framer-motion": "^6.2.3", "fs": "^0.0.2", "gray-matter": "^4.0.3", @@ -10571,6 +10572,11 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, + "node_modules/dayjs": { + "version": "1.11.9", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.9.tgz", + "integrity": "sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -30438,6 +30444,11 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, + "dayjs": { + "version": "1.11.9", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.9.tgz", + "integrity": "sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==" + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", diff --git a/frontend/package.json b/frontend/package.json index a678504f7..f20477e6f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -52,6 +52,7 @@ "classnames": "^2.3.1", "cookies": "^0.8.0", "cva": "npm:class-variance-authority@^0.4.0", + "dayjs": "^1.11.9", "framer-motion": "^6.2.3", "fs": "^0.0.2", "gray-matter": "^4.0.3", diff --git a/frontend/public/locales/en/translations.json b/frontend/public/locales/en/translations.json index c387d938b..e4c671ca4 100644 --- a/frontend/public/locales/en/translations.json +++ b/frontend/public/locales/en/translations.json @@ -251,6 +251,10 @@ } }, "settings": { + "webhooks": { + "title": "Webhooks", + "description": "Manage webhooks to setup deployment hooks for your various integrations." + }, "members": { "title": "Project Members", "description": "This page shows the members of the selected project, and allows you to modify their permissions." diff --git a/frontend/src/components/v2/Button/Button.tsx b/frontend/src/components/v2/Button/Button.tsx index 785459c4f..b69545766 100644 --- a/frontend/src/components/v2/Button/Button.tsx +++ b/frontend/src/components/v2/Button/Button.tsx @@ -61,7 +61,8 @@ const buttonVariants = cva( { colorSchema: "primary", variant: "star", - className: "bg-mineshaft-700 border border-mineshaft-600 hover:bg-primary hover:text-black hover:border-primary-400 duration-100" + className: + "bg-mineshaft-700 border border-mineshaft-600 hover:bg-primary hover:text-black hover:border-primary-400 duration-100" }, { colorSchema: "primary", @@ -76,12 +77,14 @@ const buttonVariants = cva( { colorSchema: "primary", variant: "outline_bg", - className: "bg-mineshaft-600 border border-mineshaft-500 hover:bg-primary/[0.1] hover:border-primary/40 text-bunker-200" + className: + "bg-mineshaft-600 border border-mineshaft-500 hover:bg-primary/[0.1] hover:border-primary/40 text-bunker-200" }, { colorSchema: "secondary", variant: "star", - className: "bg-mineshaft-700 border border-mineshaft-600 hover:bg-mineshaft hover:text-white" + className: + "bg-mineshaft-700 border border-mineshaft-600 hover:bg-mineshaft hover:text-white" }, { colorSchema: "danger", @@ -163,13 +166,13 @@ export const Button = forwardRef( type="button" className={twMerge( buttonVariants({ - className, colorSchema, size, variant, isRounded, isDisabled, - isFullWidth + isFullWidth, + className }) )} disabled={isDisabled} @@ -193,7 +196,15 @@ export const Button = forwardRef( > {leftIcon} - {children} + + {children} +
& { children: ReactNode; content?: ReactNode; isOpen?: boolean; @@ -10,7 +10,7 @@ export type TooltipProps = { asChild?: boolean; onOpenChange?: (isOpen: boolean) => void; defaultOpen?: boolean; -} & Omit; +}; export const Tooltip = ({ children, diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx index 075702bc7..299629416 100644 --- a/frontend/src/hooks/api/index.tsx +++ b/frontend/src/hooks/api/index.tsx @@ -13,4 +13,5 @@ export * from "./serviceTokens"; export * from "./subscriptions"; export * from "./tags"; export * from "./users"; +export * from "./webhooks"; export * from "./workspace"; diff --git a/frontend/src/hooks/api/types.ts b/frontend/src/hooks/api/types.ts index 1c4316410..098078090 100644 --- a/frontend/src/hooks/api/types.ts +++ b/frontend/src/hooks/api/types.ts @@ -8,6 +8,7 @@ export type { CreateServiceTokenDTO, ServiceToken } from "./serviceTokens/types" export type { SubscriptionPlan } from "./subscriptions/types"; export type { WsTag } from "./tags/types"; export type { AddUserToWsDTO, AddUserToWsRes, OrgUser, User } from "./users/types"; +export type { TWebhook } from "./webhooks/types"; export type { CreateEnvironmentDTO, CreateWorkspaceDTO, diff --git a/frontend/src/hooks/api/webhooks/index.tsx b/frontend/src/hooks/api/webhooks/index.tsx new file mode 100644 index 000000000..44f1b5cfc --- /dev/null +++ b/frontend/src/hooks/api/webhooks/index.tsx @@ -0,0 +1,2 @@ +export { useCreateWebhook, useDeleteWebhook, useTestWebhook, useUpdateWebhook } from "./mutation"; +export { useGetWebhooks } from "./query"; diff --git a/frontend/src/hooks/api/webhooks/mutation.tsx b/frontend/src/hooks/api/webhooks/mutation.tsx new file mode 100644 index 000000000..786fbb459 --- /dev/null +++ b/frontend/src/hooks/api/webhooks/mutation.tsx @@ -0,0 +1,67 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { queryKeys } from "./query"; +import { TCreateWebhookDto, TDeleteWebhookDto, TTestWebhookDTO, TUpdateWebhookDto } from "./types"; + +export const useCreateWebhook = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, TCreateWebhookDto>({ + mutationFn: async (dto) => { + const { data } = await apiRequest.post("/api/v1/webhooks", dto); + return data; + }, + onSuccess: (_, { workspaceId }) => { + queryClient.invalidateQueries(queryKeys.getWebhooks(workspaceId)); + } + }); +}; + +export const useTestWebhook = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, TTestWebhookDTO>({ + mutationFn: async ({ webhookId }) => { + const { data } = await apiRequest.post(`/api/v1/webhooks/${webhookId}/test`); + return data; + }, + onSuccess: (_, { workspaceId }) => { + queryClient.invalidateQueries(queryKeys.getWebhooks(workspaceId)); + }, + onError: (_, { workspaceId }) => { + queryClient.invalidateQueries(queryKeys.getWebhooks(workspaceId)); + } + }); +}; + +export const useUpdateWebhook = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, TUpdateWebhookDto>({ + mutationFn: async (dto) => { + const { data } = await apiRequest.patch(`/api/v1/webhooks/${dto.webhookId}`, { + isDisabled: dto.isDisabled + }); + return data; + }, + onSuccess: (_, { workspaceId }) => { + queryClient.invalidateQueries(queryKeys.getWebhooks(workspaceId)); + } + }); +}; + +export const useDeleteWebhook = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, TDeleteWebhookDto>({ + mutationFn: async (dto) => { + const { data } = await apiRequest.delete(`/api/v1/webhooks/${dto.webhookId}`); + return data; + }, + onSuccess: (_, { workspaceId }) => { + queryClient.invalidateQueries(queryKeys.getWebhooks(workspaceId)); + } + }); +}; diff --git a/frontend/src/hooks/api/webhooks/query.tsx b/frontend/src/hooks/api/webhooks/query.tsx new file mode 100644 index 000000000..fc1840409 --- /dev/null +++ b/frontend/src/hooks/api/webhooks/query.tsx @@ -0,0 +1,26 @@ +import { useQuery } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { TWebhook } from "./types"; + +export const queryKeys = { + getWebhooks: (workspaceId: string) => ["webhooks", { workspaceId }] +}; + +const fetchWebhooks = async (workspaceId: string) => { + const { data } = await apiRequest.get<{ webhooks: TWebhook[] }>("/api/v1/webhooks", { + params: { + workspaceId + } + }); + + return data.webhooks; +}; + +export const useGetWebhooks = (workspaceId: string) => + useQuery({ + queryKey: queryKeys.getWebhooks(workspaceId), + queryFn: () => fetchWebhooks(workspaceId), + enabled: Boolean(workspaceId) + }); diff --git a/frontend/src/hooks/api/webhooks/types.ts b/frontend/src/hooks/api/webhooks/types.ts new file mode 100644 index 000000000..8fd64bcc2 --- /dev/null +++ b/frontend/src/hooks/api/webhooks/types.ts @@ -0,0 +1,36 @@ +export type TWebhook = { + _id: string; + workspace: string; + environment: string; + secretPath: string; + url: string; + lastStatus: "success" | "failed"; + lastRunErrorMessage?: string; + isDisabled: boolean; + createdAt: string; + updatedAt: string; +}; + +export type TCreateWebhookDto = { + workspaceId: string; + environment: string; + webhookUrl: string; + webhookSecretKey?: string; + secretPath: string; +}; + +export type TUpdateWebhookDto = { + webhookId: string; + workspaceId: string; + isDisabled?: boolean; +}; + +export type TDeleteWebhookDto = { + webhookId: string; + workspaceId: string; +}; + +export type TTestWebhookDTO = { + webhookId: string; + workspaceId: string; +}; diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 1e0c7f224..b8d4ad87e 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -13,7 +13,18 @@ import { useTranslation } from "react-i18next"; import Link from "next/link"; import { useRouter } from "next/router"; import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons"; -import { faAngleDown, faArrowLeft, faArrowUpRightFromSquare, faBook, faCheck, faEnvelope, faInfinity, faMobile, faPlus, faQuestion } from "@fortawesome/free-solid-svg-icons"; +import { + faAngleDown, + faArrowLeft, + faArrowUpRightFromSquare, + faBook, + faCheck, + faEnvelope, + faInfinity, + faMobile, + faPlus, + faQuestion +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { yupResolver } from "@hookform/resolvers/yup"; import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"; @@ -41,7 +52,14 @@ import { } from "@app/components/v2"; import { useOrganization, useSubscription, useUser, useWorkspace } from "@app/context"; import { usePopUp } from "@app/hooks"; -import { fetchOrgUsers, useAddUserToWs, useCreateWorkspace, useGetOrgTrialUrl, useLogoutUser, useUploadWsKey } from "@app/hooks/api"; +import { + fetchOrgUsers, + useAddUserToWs, + useCreateWorkspace, + useGetOrgTrialUrl, + useLogoutUser, + useUploadWsKey +} from "@app/hooks/api"; interface LayoutProps { children: React.ReactNode; @@ -89,7 +107,9 @@ export const AppLayout = ({ children }: LayoutProps) => { const { subscription } = useSubscription(); // const [ isLearningNoteOpen, setIsLearningNoteOpen ] = useState(true); - const isAddingProjectsAllowed = subscription?.workspaceLimit ? (subscription.workspacesUsed < subscription.workspaceLimit) : true; + const isAddingProjectsAllowed = subscription?.workspaceLimit + ? subscription.workspacesUsed < subscription.workspaceLimit + : true; const createWs = useCreateWorkspace(); const uploadWsKey = useUploadWsKey(); @@ -110,22 +130,22 @@ export const AppLayout = ({ children }: LayoutProps) => { const { t } = useTranslation(); - useEffect(() => { - const handleRouteChange = () => { - (window).Intercom("update"); - }; - - router.events.on("routeChangeComplete", handleRouteChange); - - return () => { - router.events.off("routeChangeComplete", handleRouteChange); - }; - }, []); + useEffect(() => { + const handleRouteChange = () => { + window.Intercom("update"); + }; + + router.events.on("routeChangeComplete", handleRouteChange); + + return () => { + router.events.off("routeChangeComplete", handleRouteChange); + }; + }, []); const logout = useLogoutUser(); const logOutUser = async () => { try { - console.log("Logging out...") + console.log("Logging out..."); await logout.mutateAsync(); localStorage.removeItem("protectedKey"); localStorage.removeItem("protectedKeyIV"); @@ -145,27 +165,30 @@ export const AppLayout = ({ children }: LayoutProps) => { const changeOrg = async (orgId) => { localStorage.setItem("orgData.id", orgId); - router.push(`/org/${orgId}/overview`) - } + router.push(`/org/${orgId}/overview`); + }; // TODO(akhilmhdh): This entire logic will be rechecked and will try to avoid // Placing the localstorage as much as possible // Wait till tony integrates the azure and its launched useEffect(() => { - // Put a user in an org if they're not in one yet const putUserInOrg = async () => { if (tempLocalStorage("orgData.id") === "") { localStorage.setItem("orgData.id", orgs[0]?._id); } - if (currentOrg && ( - (workspaces?.length === 0 && router.asPath.includes("project")) - || router.asPath.includes("/project/undefined") - || (!orgs?.map(org => org._id)?.includes(router.query.id) && !router.asPath.includes("project") && !router.asPath.includes("personal") && !router.asPath.includes("integration")) - )) { + if ( + currentOrg && + ((workspaces?.length === 0 && router.asPath.includes("project")) || + router.asPath.includes("/project/undefined") || + (!orgs?.map((org) => org._id)?.includes(router.query.id) && + !router.asPath.includes("project") && + !router.asPath.includes("personal") && + !router.asPath.includes("integration"))) + ) { router.push(`/org/${currentOrg?._id}/overview`); - } + } // else if (!router.asPath.includes("org") && !router.asPath.includes("project") && !router.asPath.includes("integrations") && !router.asPath.includes("personal-settings")) { // const pathSegments = router.asPath.split("/").filter((segment) => segment.length > 0); @@ -244,134 +267,171 @@ export const AppLayout = ({ children }: LayoutProps) => { <>
-