feat(webhook): implemented ui for webhooks

This commit is contained in:
akhilmhdh
2023-07-13 23:22:20 +05:30
parent 0f81c78639
commit daf2e2036e
19 changed files with 890 additions and 245 deletions
+11
View File
@@ -44,6 +44,7 @@
"classnames": "^2.3.1", "classnames": "^2.3.1",
"cookies": "^0.8.0", "cookies": "^0.8.0",
"cva": "npm:class-variance-authority@^0.4.0", "cva": "npm:class-variance-authority@^0.4.0",
"dayjs": "^1.11.9",
"framer-motion": "^6.2.3", "framer-motion": "^6.2.3",
"fs": "^0.0.2", "fs": "^0.0.2",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
@@ -10571,6 +10572,11 @@
"integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
"dev": true "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": { "node_modules/debug": {
"version": "4.3.4", "version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@@ -30438,6 +30444,11 @@
"integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
"dev": true "dev": true
}, },
"dayjs": {
"version": "1.11.9",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.9.tgz",
"integrity": "sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA=="
},
"debug": { "debug": {
"version": "4.3.4", "version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+1
View File
@@ -52,6 +52,7 @@
"classnames": "^2.3.1", "classnames": "^2.3.1",
"cookies": "^0.8.0", "cookies": "^0.8.0",
"cva": "npm:class-variance-authority@^0.4.0", "cva": "npm:class-variance-authority@^0.4.0",
"dayjs": "^1.11.9",
"framer-motion": "^6.2.3", "framer-motion": "^6.2.3",
"fs": "^0.0.2", "fs": "^0.0.2",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
@@ -251,6 +251,10 @@
} }
}, },
"settings": { "settings": {
"webhooks": {
"title": "Webhooks",
"description": "Manage webhooks to setup deployment hooks for your various integrations."
},
"members": { "members": {
"title": "Project Members", "title": "Project Members",
"description": "This page shows the members of the selected project, and allows you to modify their permissions." "description": "This page shows the members of the selected project, and allows you to modify their permissions."
+17 -6
View File
@@ -61,7 +61,8 @@ const buttonVariants = cva(
{ {
colorSchema: "primary", colorSchema: "primary",
variant: "star", 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", colorSchema: "primary",
@@ -76,12 +77,14 @@ const buttonVariants = cva(
{ {
colorSchema: "primary", colorSchema: "primary",
variant: "outline_bg", 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", colorSchema: "secondary",
variant: "star", 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", colorSchema: "danger",
@@ -163,13 +166,13 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
type="button" type="button"
className={twMerge( className={twMerge(
buttonVariants({ buttonVariants({
className,
colorSchema, colorSchema,
size, size,
variant, variant,
isRounded, isRounded,
isDisabled, isDisabled,
isFullWidth isFullWidth,
className
}) })
)} )}
disabled={isDisabled} disabled={isDisabled}
@@ -193,7 +196,15 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
> >
{leftIcon} {leftIcon}
</div> </div>
<span className={twMerge("transition-all", isFullWidth ? "w-full" : "w-min", loadingToggleClass)}>{children}</span> <span
className={twMerge(
"transition-all",
isFullWidth ? "w-full" : "w-min",
loadingToggleClass
)}
>
{children}
</span>
<div <div
className={twMerge( className={twMerge(
"inline-flex shrink-0 cursor-pointer items-center justify-center transition-all", "inline-flex shrink-0 cursor-pointer items-center justify-center transition-all",
@@ -2,7 +2,7 @@ import { ReactNode } from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip"; import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
export type TooltipProps = { export type TooltipProps = Omit<TooltipPrimitive.TooltipContentProps, "open" | "content"> & {
children: ReactNode; children: ReactNode;
content?: ReactNode; content?: ReactNode;
isOpen?: boolean; isOpen?: boolean;
@@ -10,7 +10,7 @@ export type TooltipProps = {
asChild?: boolean; asChild?: boolean;
onOpenChange?: (isOpen: boolean) => void; onOpenChange?: (isOpen: boolean) => void;
defaultOpen?: boolean; defaultOpen?: boolean;
} & Omit<TooltipPrimitive.TooltipContentProps, "open">; };
export const Tooltip = ({ export const Tooltip = ({
children, children,
+1
View File
@@ -13,4 +13,5 @@ export * from "./serviceTokens";
export * from "./subscriptions"; export * from "./subscriptions";
export * from "./tags"; export * from "./tags";
export * from "./users"; export * from "./users";
export * from "./webhooks";
export * from "./workspace"; export * from "./workspace";
+1
View File
@@ -8,6 +8,7 @@ export type { CreateServiceTokenDTO, ServiceToken } from "./serviceTokens/types"
export type { SubscriptionPlan } from "./subscriptions/types"; export type { SubscriptionPlan } from "./subscriptions/types";
export type { WsTag } from "./tags/types"; export type { WsTag } from "./tags/types";
export type { AddUserToWsDTO, AddUserToWsRes, OrgUser, User } from "./users/types"; export type { AddUserToWsDTO, AddUserToWsRes, OrgUser, User } from "./users/types";
export type { TWebhook } from "./webhooks/types";
export type { export type {
CreateEnvironmentDTO, CreateEnvironmentDTO,
CreateWorkspaceDTO, CreateWorkspaceDTO,
@@ -0,0 +1,2 @@
export { useCreateWebhook, useDeleteWebhook, useTestWebhook, useUpdateWebhook } from "./mutation";
export { useGetWebhooks } from "./query";
@@ -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));
}
});
};
+26
View File
@@ -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)
});
+36
View File
@@ -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;
};
+260 -188
View File
@@ -13,7 +13,18 @@ import { useTranslation } from "react-i18next";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons"; 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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"; import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
@@ -41,7 +52,14 @@ import {
} from "@app/components/v2"; } from "@app/components/v2";
import { useOrganization, useSubscription, useUser, useWorkspace } from "@app/context"; import { useOrganization, useSubscription, useUser, useWorkspace } from "@app/context";
import { usePopUp } from "@app/hooks"; 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 { interface LayoutProps {
children: React.ReactNode; children: React.ReactNode;
@@ -89,7 +107,9 @@ export const AppLayout = ({ children }: LayoutProps) => {
const { subscription } = useSubscription(); const { subscription } = useSubscription();
// const [ isLearningNoteOpen, setIsLearningNoteOpen ] = useState(true); // 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 createWs = useCreateWorkspace();
const uploadWsKey = useUploadWsKey(); const uploadWsKey = useUploadWsKey();
@@ -110,22 +130,22 @@ export const AppLayout = ({ children }: LayoutProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
useEffect(() => { useEffect(() => {
const handleRouteChange = () => { const handleRouteChange = () => {
(window).Intercom("update"); window.Intercom("update");
}; };
router.events.on("routeChangeComplete", handleRouteChange); router.events.on("routeChangeComplete", handleRouteChange);
return () => { return () => {
router.events.off("routeChangeComplete", handleRouteChange); router.events.off("routeChangeComplete", handleRouteChange);
}; };
}, []); }, []);
const logout = useLogoutUser(); const logout = useLogoutUser();
const logOutUser = async () => { const logOutUser = async () => {
try { try {
console.log("Logging out...") console.log("Logging out...");
await logout.mutateAsync(); await logout.mutateAsync();
localStorage.removeItem("protectedKey"); localStorage.removeItem("protectedKey");
localStorage.removeItem("protectedKeyIV"); localStorage.removeItem("protectedKeyIV");
@@ -145,27 +165,30 @@ export const AppLayout = ({ children }: LayoutProps) => {
const changeOrg = async (orgId) => { const changeOrg = async (orgId) => {
localStorage.setItem("orgData.id", 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 // TODO(akhilmhdh): This entire logic will be rechecked and will try to avoid
// Placing the localstorage as much as possible // Placing the localstorage as much as possible
// Wait till tony integrates the azure and its launched // Wait till tony integrates the azure and its launched
useEffect(() => { useEffect(() => {
// Put a user in an org if they're not in one yet // Put a user in an org if they're not in one yet
const putUserInOrg = async () => { const putUserInOrg = async () => {
if (tempLocalStorage("orgData.id") === "") { if (tempLocalStorage("orgData.id") === "") {
localStorage.setItem("orgData.id", orgs[0]?._id); localStorage.setItem("orgData.id", orgs[0]?._id);
} }
if (currentOrg && ( if (
(workspaces?.length === 0 && router.asPath.includes("project")) currentOrg &&
|| router.asPath.includes("/project/undefined") ((workspaces?.length === 0 && router.asPath.includes("project")) ||
|| (!orgs?.map(org => org._id)?.includes(router.query.id) && !router.asPath.includes("project") && !router.asPath.includes("personal") && !router.asPath.includes("integration")) 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`); 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")) { // 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); // const pathSegments = router.asPath.split("/").filter((segment) => segment.length > 0);
@@ -244,134 +267,171 @@ export const AppLayout = ({ children }: LayoutProps) => {
<> <>
<div className="dark hidden h-screen w-full flex-col overflow-x-hidden md:flex"> <div className="dark hidden h-screen w-full flex-col overflow-x-hidden md:flex">
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row"> <div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
<aside className="w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60 dark"> <aside className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60">
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]"> <nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]">
<div> <div>
{!router.asPath.includes("personal") && <div className="h-12 px-3 flex items-center pt-6 cursor-default"> {!router.asPath.includes("personal") && (
{(router.asPath.includes("project") || router.asPath.includes("integrations")) && <Link href={`/org/${currentOrg?._id}/overview`}><div className="pl-1 pr-2 text-mineshaft-400 hover:text-mineshaft-100 duration-200"> <div className="flex h-12 cursor-default items-center px-3 pt-6">
<FontAwesomeIcon icon={faArrowLeft} /> {(router.asPath.includes("project") ||
</div></Link>} router.asPath.includes("integrations")) && (
<DropdownMenu> <Link href={`/org/${currentOrg?._id}/overview`}>
<DropdownMenuTrigger asChild className="data-[state=open]:bg-mineshaft-600"> <div className="pl-1 pr-2 text-mineshaft-400 duration-200 hover:text-mineshaft-100">
<div className="mr-auto flex items-center hover:bg-mineshaft-600 py-1.5 pl-1.5 pr-2 rounded-md"> <FontAwesomeIcon icon={faArrowLeft} />
<div className="w-5 h-5 rounded-md bg-primary flex justify-center items-center text-sm">{currentOrg?.name.charAt(0)}</div> </div>
<div className="pl-3 text-mineshaft-100 text-sm">{currentOrg?.name} <FontAwesomeIcon icon={faAngleDown} className="text-xs pl-1 pt-1 text-mineshaft-300" /></div> </Link>
</div> )}
</DropdownMenuTrigger> <DropdownMenu>
<DropdownMenuContent align="start" className="p-1"> <DropdownMenuTrigger asChild className="data-[state=open]:bg-mineshaft-600">
<div className="text-xs text-mineshaft-400 px-2 py-1">{user?.email}</div> <div className="mr-auto flex items-center rounded-md py-1.5 pl-1.5 pr-2 hover:bg-mineshaft-600">
{orgs?.map(org => <DropdownMenuItem key={org._id}> <div className="flex h-5 w-5 items-center justify-center rounded-md bg-primary text-sm">
<Button {currentOrg?.name.charAt(0)}
onClick={() => changeOrg(org?._id)} </div>
variant="plain" <div className="pl-3 text-sm text-mineshaft-100">
colorSchema="secondary" {currentOrg?.name}{" "}
size="xs" <FontAwesomeIcon
className="w-full flex items-center justify-start p-0 font-normal" icon={faAngleDown}
leftIcon={currentOrg._id === org._id && <FontAwesomeIcon icon={faCheck} className="mr-3 text-primary"/>} className="pl-1 pt-1 text-xs text-mineshaft-300"
> />
<div className="w-full flex justify-between items-center">{org.name}</div> </div>
</Button> </div>
</DropdownMenuItem> </DropdownMenuTrigger>
)} <DropdownMenuContent align="start" className="p-1">
<div className="h-1 mt-1 border-t border-mineshaft-600"/> <div className="px-2 py-1 text-xs text-mineshaft-400">{user?.email}</div>
<button {orgs?.map((org) => (
type="button" <DropdownMenuItem key={org._id}>
onClick={logOutUser} <Button
className="w-full" onClick={() => changeOrg(org?._id)}
> variant="plain"
<DropdownMenuItem>Log Out</DropdownMenuItem> colorSchema="secondary"
</button> size="xs"
</DropdownMenuContent> className="flex w-full items-center justify-start p-0 font-normal"
</DropdownMenu> leftIcon={
<DropdownMenu> currentOrg._id === org._id && (
<DropdownMenuTrigger asChild className="hover:bg-primary-400 hover:text-black data-[state=open]:text-black data-[state=open]:bg-primary-400 p-1"> <FontAwesomeIcon icon={faCheck} className="mr-3 text-primary" />
<div className="child w-6 h-6 rounded-full bg-mineshaft hover:bg-mineshaft-500 pr-1 text-xs text-mineshaft-300 flex justify-center items-center"> )
{user?.firstName?.charAt(0)}{user?.lastName && user?.lastName?.charAt(0)} }
</div> >
</DropdownMenuTrigger> <div className="flex w-full items-center justify-between">
<DropdownMenuContent align="start" className="p-1"> {org.name}
<div className="text-xs text-mineshaft-400 px-2 py-1">{user?.email}</div> </div>
<Link href="/personal-settings"><DropdownMenuItem>Personal Settings</DropdownMenuItem></Link> </Button>
<a </DropdownMenuItem>
href="https://infisical.com/docs/documentation/getting-started/introduction"
target="_blank"
rel="noopener noreferrer"
className="w-full mt-3 text-sm text-mineshaft-300 font-normal leading-[1.2rem] hover:text-mineshaft-100"
>
<DropdownMenuItem>Documentation<FontAwesomeIcon icon={faArrowUpRightFromSquare} className="pl-1.5 text-xxs mb-[0.06rem]" /></DropdownMenuItem>
</a>
<a
href="https://join.slack.com/t/infisical-users/shared_invite/zt-1ye0tm8ab-899qZ6ZbpfESuo6TEikyOQ"
target="_blank"
rel="noopener noreferrer"
className="w-full mt-3 text-sm text-mineshaft-300 font-normal leading-[1.2rem] hover:text-mineshaft-100"
>
<DropdownMenuItem>Join Slack Community<FontAwesomeIcon icon={faArrowUpRightFromSquare} className="pl-1.5 text-xxs mb-[0.06rem]" /></DropdownMenuItem>
</a>
<div className="h-1 mt-1 border-t border-mineshaft-600"/>
<button
type="button"
onClick={logOutUser}
className="w-full"
>
<DropdownMenuItem>Log Out</DropdownMenuItem>
</button>
</DropdownMenuContent>
</DropdownMenu>
</div>}
{!router.asPath.includes("org") && (!router.asPath.includes("personal") && currentWorkspace ? (
<div className="mt-5 mb-4 w-full p-3">
<p className="ml-1.5 mb-1 text-xs font-semibold uppercase text-gray-400">
Project
</p>
<Select
defaultValue={currentWorkspace?._id}
value={currentWorkspace?._id}
className="w-full truncate bg-mineshaft-600 py-2.5 font-medium"
onValueChange={(value) => {
router.push(`/project/${value}/secrets`);
}}
position="popper"
dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 z-50 max-h-96 border-gray-700"
>
<div className='h-full no-scrollbar no-scrollbar::-webkit-scrollbar'>
{workspaces
.filter((ws) => ws.organization === currentOrg?._id)
.map(({ _id, name }) => (
<SelectItem
key={`ws-layout-list-${_id}`}
value={_id}
className={`${currentWorkspace?._id === _id && "bg-mineshaft-600"}`}
>
{name}
</SelectItem>
))} ))}
</div> <div className="mt-1 h-1 border-t border-mineshaft-600" />
<hr className="mt-1 mb-1 h-px border-0 bg-gray-700" /> <button type="button" onClick={logOutUser} className="w-full">
<div className="w-full"> <DropdownMenuItem>Log Out</DropdownMenuItem>
<Button </button>
className="w-full bg-mineshaft-700 py-2 text-bunker-200" </DropdownMenuContent>
colorSchema="primary" </DropdownMenu>
variant="outline_bg" <DropdownMenu>
size="sm" <DropdownMenuTrigger
onClick={() => { asChild
if (isAddingProjectsAllowed) { className="p-1 hover:bg-primary-400 hover:text-black data-[state=open]:bg-primary-400 data-[state=open]:text-black"
handlePopUpOpen("addNewWs") >
} else { <div className="child flex h-6 w-6 items-center justify-center rounded-full bg-mineshaft pr-1 text-xs text-mineshaft-300 hover:bg-mineshaft-500">
handlePopUpOpen("upgradePlan"); {user?.firstName?.charAt(0)}
} {user?.lastName && user?.lastName?.charAt(0)}
}} </div>
leftIcon={<FontAwesomeIcon icon={faPlus} />} </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<div className="px-2 py-1 text-xs text-mineshaft-400">{user?.email}</div>
<Link href="/personal-settings">
<DropdownMenuItem>Personal Settings</DropdownMenuItem>
</Link>
<a
href="https://infisical.com/docs/documentation/getting-started/introduction"
target="_blank"
rel="noopener noreferrer"
className="mt-3 w-full text-sm font-normal leading-[1.2rem] text-mineshaft-300 hover:text-mineshaft-100"
> >
Add Project <DropdownMenuItem>
</Button> Documentation
</div> <FontAwesomeIcon
</Select> icon={faArrowUpRightFromSquare}
className="mb-[0.06rem] pl-1.5 text-xxs"
/>
</DropdownMenuItem>
</a>
<a
href="https://join.slack.com/t/infisical-users/shared_invite/zt-1ye0tm8ab-899qZ6ZbpfESuo6TEikyOQ"
target="_blank"
rel="noopener noreferrer"
className="mt-3 w-full text-sm font-normal leading-[1.2rem] text-mineshaft-300 hover:text-mineshaft-100"
>
<DropdownMenuItem>
Join Slack Community
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.06rem] pl-1.5 text-xxs"
/>
</DropdownMenuItem>
</a>
<div className="mt-1 h-1 border-t border-mineshaft-600" />
<button type="button" onClick={logOutUser} className="w-full">
<DropdownMenuItem>Log Out</DropdownMenuItem>
</button>
</DropdownMenuContent>
</DropdownMenu>
</div> </div>
) : <Link href={`/org/${currentOrg?._id}/overview`}><div className="pr-2 my-6 flex justify-center items-center text-mineshaft-300 hover:text-mineshaft-100 cursor-default text-sm"> )}
<FontAwesomeIcon icon={faArrowLeft} className="pr-3"/> {!router.asPath.includes("org") &&
Back to organization (!router.asPath.includes("personal") && currentWorkspace ? (
</div></Link>)} <div className="mt-5 mb-4 w-full p-3">
<p className="ml-1.5 mb-1 text-xs font-semibold uppercase text-gray-400">
Project
</p>
<Select
defaultValue={currentWorkspace?._id}
value={currentWorkspace?._id}
className="w-full truncate bg-mineshaft-600 py-2.5 font-medium"
onValueChange={(value) => {
router.push(`/project/${value}/secrets`);
}}
position="popper"
dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 z-50 max-h-96 border-gray-700"
>
<div className="no-scrollbar::-webkit-scrollbar h-full no-scrollbar">
{workspaces
.filter((ws) => ws.organization === currentOrg?._id)
.map(({ _id, name }) => (
<SelectItem
key={`ws-layout-list-${_id}`}
value={_id}
className={`${currentWorkspace?._id === _id && "bg-mineshaft-600"}`}
>
{name}
</SelectItem>
))}
</div>
<hr className="mt-1 mb-1 h-px border-0 bg-gray-700" />
<div className="w-full">
<Button
className="w-full bg-mineshaft-700 py-2 text-bunker-200"
colorSchema="primary"
variant="outline_bg"
size="sm"
onClick={() => {
if (isAddingProjectsAllowed) {
handlePopUpOpen("addNewWs");
} else {
handlePopUpOpen("upgradePlan");
}
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
Add Project
</Button>
</div>
</Select>
</div>
) : (
<Link href={`/org/${currentOrg?._id}/overview`}>
<div className="my-6 flex cursor-default items-center justify-center pr-2 text-sm text-mineshaft-300 hover:text-mineshaft-100">
<FontAwesomeIcon icon={faArrowLeft} className="pr-3" />
Back to organization
</div>
</Link>
))}
<div className={`px-1 ${!router.asPath.includes("personal") ? "block" : "hidden"}`}> <div className={`px-1 ${!router.asPath.includes("personal") ? "block" : "hidden"}`}>
{((router.asPath.includes("project") || router.asPath.includes("integrations")) && currentWorkspace) ? <Menu> {((router.asPath.includes("project") || router.asPath.includes("integrations")) && currentWorkspace) ? <Menu>
<Link href={`/project/${currentWorkspace?._id}/secrets`} passHref> <Link href={`/project/${currentWorkspace?._id}/secrets`} passHref>
@@ -486,20 +546,25 @@ export const AppLayout = ({ children }: LayoutProps) => {
<Link href={`/org/${currentOrg?._id}/settings`} passHref> <Link href={`/org/${currentOrg?._id}/settings`} passHref>
<a> <a>
<MenuItem <MenuItem
isSelected={ isSelected={router.asPath === `/org/${currentOrg?._id}/settings`}
router.asPath === `/org/${currentOrg?._id}/settings`
}
icon="system-outline-109-slider-toggle-settings" icon="system-outline-109-slider-toggle-settings"
> >
Organization Settings Organization Settings
</MenuItem> </MenuItem>
</a> </a>
</Link> </Link>
</Menu>} </Menu>
)}
</div> </div>
</div> </div>
<div className={`relative mt-10 ${subscription && subscription.slug === "starter" && !subscription.has_used_trial ? "mb-2" : "mb-4"} w-full px-3 text-mineshaft-400 cursor-default text-sm flex flex-col items-center`}> <div
{/* <div className={`${isLearningNoteOpen ? "block" : "hidden"} z-0 absolute h-60 w-[9.9rem] ${router.asPath.includes("org") ? "bottom-[8.4rem]" : "bottom-[5.4rem]"} bg-mineshaft-900 border border-mineshaft-600 mb-4 rounded-md opacity-30`}/> className={`relative mt-10 ${
subscription && subscription.slug === "starter" && !subscription.has_used_trial
? "mb-2"
: "mb-4"
} flex w-full cursor-default flex-col items-center px-3 text-sm text-mineshaft-400`}
>
{/* <div className={`${isLearningNoteOpen ? "block" : "hidden"} z-0 absolute h-60 w-[9.9rem] ${router.asPath.includes("org") ? "bottom-[8.4rem]" : "bottom-[5.4rem]"} bg-mineshaft-900 border border-mineshaft-600 mb-4 rounded-md opacity-30`}/>
<div className={`${isLearningNoteOpen ? "block" : "hidden"} z-0 absolute h-60 w-[10.7rem] ${router.asPath.includes("org") ? "bottom-[8.15rem]" : "bottom-[5.15rem]"} bg-mineshaft-900 border border-mineshaft-600 mb-4 rounded-md opacity-50`}/> <div className={`${isLearningNoteOpen ? "block" : "hidden"} z-0 absolute h-60 w-[10.7rem] ${router.asPath.includes("org") ? "bottom-[8.15rem]" : "bottom-[5.15rem]"} bg-mineshaft-900 border border-mineshaft-600 mb-4 rounded-md opacity-50`}/>
<div className={`${isLearningNoteOpen ? "block" : "hidden"} z-0 absolute h-60 w-[11.5rem] ${router.asPath.includes("org") ? "bottom-[7.9rem]" : "bottom-[4.9rem]"} bg-mineshaft-900 border border-mineshaft-600 mb-4 rounded-md opacity-70`}/> <div className={`${isLearningNoteOpen ? "block" : "hidden"} z-0 absolute h-60 w-[11.5rem] ${router.asPath.includes("org") ? "bottom-[7.9rem]" : "bottom-[4.9rem]"} bg-mineshaft-900 border border-mineshaft-600 mb-4 rounded-md opacity-70`}/>
<div className={`${isLearningNoteOpen ? "block" : "hidden"} z-0 absolute h-60 w-[12.3rem] ${router.asPath.includes("org") ? "bottom-[7.65rem]" : "bottom-[4.65rem]"} bg-mineshaft-900 border border-mineshaft-600 mb-4 rounded-md opacity-90`}/> <div className={`${isLearningNoteOpen ? "block" : "hidden"} z-0 absolute h-60 w-[12.3rem] ${router.asPath.includes("org") ? "bottom-[7.65rem]" : "bottom-[4.65rem]"} bg-mineshaft-900 border border-mineshaft-600 mb-4 rounded-md opacity-90`}/>
@@ -527,22 +592,24 @@ export const AppLayout = ({ children }: LayoutProps) => {
</a> </a>
</div> </div>
</div> */} </div> */}
{router.asPath.includes("org") && <div {router.asPath.includes("org") && (
onKeyDown={() => null} <div
role="button" onKeyDown={() => null}
tabIndex={0} role="button"
onClick={() => router.push(`/org/${router.query.id}/members?action=invite`)} tabIndex={0}
className="w-full" onClick={() => router.push(`/org/${router.query.id}/members?action=invite`)}
> className="w-full"
<div className="hover:text-mineshaft-200 duration-200 mb-3 pl-5 w-full"> >
<FontAwesomeIcon icon={faPlus} className="mr-3"/> <div className="mb-3 w-full pl-5 duration-200 hover:text-mineshaft-200">
Invite people <FontAwesomeIcon icon={faPlus} className="mr-3" />
Invite people
</div>
</div> </div>
</div>} )}
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<div className="hover:text-mineshaft-200 duration-200 mb-2 pl-5 w-full"> <div className="mb-2 w-full pl-5 duration-200 hover:text-mineshaft-200">
<FontAwesomeIcon icon={faQuestion} className="px-[0.1rem] mr-3"/> <FontAwesomeIcon icon={faQuestion} className="mr-3 px-[0.1rem]" />
Help & Support Help & Support
</div> </div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
@@ -564,28 +631,33 @@ export const AppLayout = ({ children }: LayoutProps) => {
))} ))}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
{subscription && subscription.slug === "starter" && !subscription.has_used_trial && ( {subscription &&
<button subscription.slug === "starter" &&
type="button" !subscription.has_used_trial && (
onClick={async () => { <button
if (!subscription || !currentOrg) return; type="button"
onClick={async () => {
// direct user to start pro trial if (!subscription || !currentOrg) return;
const url = await mutateAsync({
orgId: currentOrg._id, // direct user to start pro trial
success_url: window.location.href const url = await mutateAsync({
}); orgId: currentOrg._id,
success_url: window.location.href
window.location.href = url; });
}}
className="w-full mt-1.5" window.location.href = url;
> }}
<div className="hover:text-primary-400 text-mineshaft-300 duration-200 flex justify-left items-center py-1 bg-mineshaft-600 rounded-md hover:bg-mineshaft-500 mb-1.5 mt-1.5 pl-4 w-full"> className="mt-1.5 w-full"
<FontAwesomeIcon icon={faInfinity} className="mr-3 ml-0.5 py-2 text-primary"/> >
Start Free Pro Trial <div className="justify-left mb-1.5 mt-1.5 flex w-full items-center rounded-md bg-mineshaft-600 py-1 pl-4 text-mineshaft-300 duration-200 hover:bg-mineshaft-500 hover:text-primary-400">
</div> <FontAwesomeIcon
</button> icon={faInfinity}
)} className="mr-3 ml-0.5 py-2 text-primary"
/>
Start Free Pro Trial
</div>
</button>
)}
</div> </div>
</nav> </nav>
</aside> </aside>
@@ -1,18 +1,59 @@
import { Fragment } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Tab } from "@headlessui/react";
import { ProjectTabGroup } from "./components"; import NavHeader from "@app/components/navigation/NavHeader";
import { ProjectGeneralTab } from "./components/ProjectGeneralTab";
import { ProjectServiceTokensTab } from "./components/ProjectServiceTokensTab";
import { WebhooksTab } from "./components/WebhooksTab";
const tabs = [
{ name: "General", key: "tab-project-general" },
{ name: "Service Tokens", key: "tab-project-service-tokens" },
{ name: "Webhooks", key: "tab-project-webhooks" }
];
export const ProjectSettingsPage = () => { export const ProjectSettingsPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div className="flex justify-center bg-bunker-800 text-white w-full"> <div className="flex h-full w-full justify-center bg-bunker-800 px-6 text-white">
<div className="max-w-7xl w-full px-6"> <div className="w-full max-w-screen-lg">
<div className="mt-6 mb-6"> <div className="relative right-5 ml-4">
<p className="text-3xl font-semibold text-gray-200"> <NavHeader pageName={t("settings.project.title")} isProjectRelated />
{t("settings.project.title")}
</p>
</div> </div>
<ProjectTabGroup /> <div className="my-8">
<p className="text-3xl font-semibold text-gray-200">{t("settings.project.title")}</p>
</div>
<Tab.Group>
<Tab.List className="mb-6 w-full border-b-2 border-mineshaft-800">
{tabs.map((tab) => (
<Tab as={Fragment} key={tab.key}>
{({ selected }) => (
<button
type="button"
className={`w-30 p-4 font-semibold outline-none ${
selected ? "border-b-2 border-white text-white" : "text-mineshaft-400"
}`}
>
{tab.name}
</button>
)}
</Tab>
))}
</Tab.List>
<Tab.Panels>
<Tab.Panel>
<ProjectGeneralTab />
</Tab.Panel>
<Tab.Panel>
<ProjectServiceTokensTab />
</Tab.Panel>
<Tab.Panel>
<WebhooksTab />
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</div> </div>
</div> </div>
); );
@@ -1,39 +0,0 @@
import { Fragment } from "react"
import { Tab } from "@headlessui/react"
import { ProjectGeneralTab } from "../ProjectGeneralTab";
import { ProjectServiceTokensTab } from "../ProjectServiceTokensTab";
const tabs = [
{ name: "General", key: "tab-project-general" },
{ name: "Service Tokens", key: "tab-project-service-tokens" }
];
export const ProjectTabGroup = () => {
return (
<Tab.Group>
<Tab.List className="mb-6 border-b-2 border-mineshaft-800 w-full">
{tabs.map((tab) => (
<Tab as={Fragment} key={tab.key}>
{({ selected }) => (
<button
type="button"
className={`w-30 py-2 mx-2 mr-4 font-medium text-sm outline-none ${selected ? "border-b border-white text-white" : "text-mineshaft-400"}`}
>
{tab.name}
</button>
)}
</Tab>
))}
</Tab.List>
<Tab.Panels>
<Tab.Panel>
<ProjectGeneralTab />
</Tab.Panel>
<Tab.Panel>
<ProjectServiceTokensTab />
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
);
}
@@ -1 +0,0 @@
export { ProjectTabGroup } from "./ProjectTabGroup";
@@ -0,0 +1,133 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import {
Button,
FormControl,
Input,
Modal,
ModalClose,
ModalContent,
Select,
SelectItem
} from "@app/components/v2";
const formSchema = yup.object({
environment: yup.string().required().trim().label("Environment"),
webhookUrl: yup.string().url().required().trim().label("Webhook URL"),
webhookSecretKey: yup.string().trim().label("Secret Key"),
secretPath: yup.string().required().trim().label("Secret Path")
});
export type TFormSchema = yup.InferType<typeof formSchema>;
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
onCreateWebhook: (data: TFormSchema) => void;
environments?: Array<{ slug: string; name: string }>;
};
export const AddWebhookForm = ({
isOpen,
onOpenChange,
onCreateWebhook,
environments = []
}: Props) => {
const {
control,
handleSubmit,
register,
reset,
formState: { errors, isSubmitting }
} = useForm<TFormSchema>({
resolver: yupResolver(formSchema)
});
useEffect(() => {
if (!isOpen) {
reset();
}
}, [isOpen]);
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent title="Create a new webhook">
<form onSubmit={handleSubmit(onCreateWebhook)}>
<div>
<Controller
control={control}
name="environment"
defaultValue={environments?.[0]?.slug}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Environment"
isRequired
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{environments.map(({ name, slug }) => (
<SelectItem value={slug} key={slug}>
{name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<FormControl
label="Secret Path"
isRequired
isError={Boolean(errors?.secretPath)}
errorText={errors?.secretPath?.message}
>
<Input placeholder="/, /**/*" {...register("secretPath")} />
</FormControl>
<FormControl
label="Secret Key"
isError={Boolean(errors?.webhookSecretKey)}
errorText={errors?.webhookSecretKey?.message}
helperText="To generate webhook signature for verification"
>
<Input
placeholder="Provided during webhook setup"
{...register("webhookSecretKey")}
/>
</FormControl>
<FormControl
label="Webhook URL"
isRequired
isError={Boolean(errors?.webhookUrl)}
errorText={errors?.webhookUrl?.message}
>
<Input {...register("webhookUrl")} />
</FormControl>
</div>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
type="submit"
isDisabled={isSubmitting}
isLoading={isSubmitting}
>
Create
</Button>
<ModalClose asChild>
<Button variant="plain" colorSchema="secondary">
Cancel
</Button>
</ModalClose>
</div>
</form>
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,279 @@
import { useTranslation } from "react-i18next";
import { faInfoCircle, faPlug, faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import dayjs from "dayjs";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
Button,
DeleteActionModal,
EmptyState,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
THead,
Tooltip,
Tr
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { usePopUp } from "@app/hooks";
import {
useCreateWebhook,
useDeleteWebhook,
useGetWebhooks,
useTestWebhook,
useUpdateWebhook
} from "@app/hooks/api";
import { AddWebhookForm, TFormSchema } from "./AddWebhookForm";
export const WebhooksTab = () => {
const { t } = useTranslation();
const { createNotification } = useNotificationContext();
const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?._id || "";
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
"addWebhook",
"deleteWebhook"
] as const);
const { data: webhooks, isLoading: isWebhooksLoading } = useGetWebhooks(workspaceId);
// mutation
const { mutateAsync: createWebhook } = useCreateWebhook();
const {
mutateAsync: testWebhook,
variables: testWebhookVars,
isLoading: isTestWebhookSubmitting
} = useTestWebhook();
const {
mutateAsync: updateWebhook,
variables: updateWebhookVars,
isLoading: isUpdateWebhookSubmitting
} = useUpdateWebhook();
const { mutateAsync: deleteWebhook } = useDeleteWebhook();
const handleWebhookCreate = async (data: TFormSchema) => {
try {
await createWebhook({
...data,
workspaceId
});
handlePopUpClose("addWebhook");
createNotification({
type: "success",
text: "Successfully created webhook"
});
} catch (err) {
console.log(err);
createNotification({
type: "error",
text: "Failed to create webhook"
});
}
};
const handleWebhookDisable = async (webhookId: string, isDisabled: boolean) => {
try {
await updateWebhook({
webhookId,
workspaceId,
isDisabled
});
createNotification({
type: "success",
text: "Successfully updated webhook"
});
} catch (err) {
console.log(err);
createNotification({
type: "error",
text: "Failed to update webhook"
});
}
};
const handleWebhookDelete = async () => {
try {
const webhookId = popUp?.deleteWebhook?.data as string;
await deleteWebhook({
webhookId,
workspaceId
});
handlePopUpClose("deleteWebhook");
createNotification({
type: "success",
text: "Successfully deleted webhook"
});
} catch (err) {
console.log(err);
createNotification({
type: "error",
text: "Failed to delete webhook"
});
}
};
const handleWebhookTest = async (webhookId: string) => {
try {
await testWebhook({
webhookId,
workspaceId
});
createNotification({
type: "success",
text: "Successfully triggered webhook"
});
} catch (err) {
console.log(err);
createNotification({
type: "error",
text: "Failed to trigger webhook"
});
}
};
return (
<div className="mb-6 max-w-screen-lg rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">{t("settings.webhooks.title")}</p>
<Button
onClick={() => handlePopUpOpen("addWebhook")}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
Create
</Button>
</div>
<p className="mb-8 text-gray-400">{t("settings.webhooks.description")}</p>
<div>
<TableContainer>
<Table>
<THead>
<Tr>
<Td>URL</Td>
<Td>Environment</Td>
<Td>Secret Path</Td>
<Td>Status</Td>
<Td className="text-right">Action</Td>
</Tr>
</THead>
<TBody>
{isWebhooksLoading && <TableSkeleton columns={5} key="webhooks-loading" />}
{!isWebhooksLoading && webhooks && webhooks?.length === 0 && (
<Tr>
<Td colSpan={5}>
<EmptyState title="No webhooks found" icon={faPlug} />
</Td>
</Tr>
)}
{!isWebhooksLoading &&
webhooks?.map(
({
_id: id,
url,
environment,
secretPath,
lastStatus,
isDisabled,
updatedAt,
lastRunErrorMessage
}) => (
<Tr key={id}>
<Td className="max-w-xs overflow-hidden text-ellipsis hover:overflow-auto hover:break-all">
{url}
</Td>
<Td>{environment}</Td>
<Td>{secretPath}</Td>
<Td>
{!lastStatus ? (
"-"
) : (
<div className="inline-flex w-min items-center rounded bg-mineshaft-600 px-2 py-0.5 text-sm">
{lastStatus}{" "}
<Tooltip
content={
<div className="text-xs">
<div>
Updated At: {dayjs(updatedAt).format("YYYY-MM-DD, hh:mm A")}
</div>
{lastRunErrorMessage && (
<div className="mt-2 text-red">
Error: {lastRunErrorMessage}
</div>
)}
</div>
}
>
<FontAwesomeIcon
className={`ml-1 ${
lastStatus === "failed" ? "text-red" : "text-green"
}`}
icon={faInfoCircle}
/>
</Tooltip>
</div>
)}
</Td>
<Td>
<div className="flex items-center justify-end space-x-2">
<Button
variant="star"
size="xs"
onClick={() => handleWebhookTest(id)}
isDisabled={
isTestWebhookSubmitting && testWebhookVars?.webhookId === id
}
isLoading={isTestWebhookSubmitting && testWebhookVars?.webhookId === id}
>
Test
</Button>
<Button
variant="outline_bg"
size="xs"
onClick={() => handleWebhookDisable(id, !isDisabled)}
isDisabled={
isUpdateWebhookSubmitting && updateWebhookVars?.webhookId === id
}
isLoading={
isUpdateWebhookSubmitting && updateWebhookVars?.webhookId === id
}
>
{isDisabled ? "Enable" : "Disable"}
</Button>
<Button
variant="outline_bg"
className="border-red-800 bg-red-800 hover:border-red-700 hover:bg-red-700"
colorSchema="danger"
size="xs"
onClick={() => handlePopUpOpen("deleteWebhook", id)}
>
Delete
</Button>
</div>
</Td>
</Tr>
)
)}
</TBody>
</Table>
</TableContainer>
</div>
<AddWebhookForm
environments={currentWorkspace?.environments}
isOpen={popUp?.addWebhook?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addWebhook", isOpen)}
onCreateWebhook={handleWebhookCreate}
/>
<DeleteActionModal
isOpen={popUp.deleteWebhook.isOpen}
deleteKey="remove"
title="Are you sure you want to delete this webhook?"
onChange={(isOpen) => handlePopUpToggle("deleteWebhook", isOpen)}
onClose={() => handlePopUpClose("deleteWebhook")}
onDeleteApproved={handleWebhookDelete}
/>
</div>
);
};
@@ -0,0 +1 @@
export { WebhooksTab } from "./WebhooksTab";
@@ -4,6 +4,5 @@ export { E2EESection } from "./E2EESection";
export { EnvironmentSection } from "./EnvironmentSection"; export { EnvironmentSection } from "./EnvironmentSection";
export { ProjectIndexSecretsSection } from "./ProjectIndexSecretsSection"; export { ProjectIndexSecretsSection } from "./ProjectIndexSecretsSection";
export { ProjectNameChangeSection } from "./ProjectNameChangeSection"; export { ProjectNameChangeSection } from "./ProjectNameChangeSection";
export { ProjectTabGroup } from "./ProjectTabGroup";
export { SecretTagsSection } from "./SecretTagsSection"; export { SecretTagsSection } from "./SecretTagsSection";
export { ServiceTokenSection } from "./ServiceTokenSection"; export { ServiceTokenSection } from "./ServiceTokenSection";