From f9110cedfa18ff1b599d5098d4753c40d6d5a359 Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako <matsiiako@gmail.com> Date: Tue, 11 Jul 2023 22:13:54 -0700 Subject: [PATCH 1/3] fixing the bug with switching orgs --- .../src/components/v2/Dropdown/Dropdown.tsx | 2 +- .../OrganizationContext.tsx | 5 +- frontend/src/layouts/AppLayout/AppLayout.tsx | 143 ++++---- frontend/src/pages/_app.tsx | 8 +- .../src/pages/org/[id]/overview/index.tsx | 19 +- .../OrgMembersTable/OrgMembersTable.tsx | 4 +- .../OrgMembersSection/OrgMembersSection.tsx | 57 ---- .../OrgMembersSection/OrgMembersTable.tsx | 304 ------------------ .../components/OrgMembersSection/index.tsx | 1 - .../OrgSettingsPage/components/index.tsx | 1 - 10 files changed, 90 insertions(+), 454 deletions(-) delete mode 100644 frontend/src/views/Settings/OrgSettingsPage/components/OrgMembersSection/OrgMembersSection.tsx delete mode 100644 frontend/src/views/Settings/OrgSettingsPage/components/OrgMembersSection/OrgMembersTable.tsx diff --git a/frontend/src/components/v2/Dropdown/Dropdown.tsx b/frontend/src/components/v2/Dropdown/Dropdown.tsx index cf53714c3..def928c74 100644 --- a/frontend/src/components/v2/Dropdown/Dropdown.tsx +++ b/frontend/src/components/v2/Dropdown/Dropdown.tsx @@ -66,7 +66,7 @@ export const DropdownMenuItem = <T extends ElementType = "button">({ className )} > - <Item type="button" role="menuitem" class="flex w-full items-center" ref={inputRef}> + <Item type="button" role="menuitem" className="flex w-full items-center" ref={inputRef}> {icon && <span className="flex items-center mr-2">{icon}</span>} <span className="flex-grow text-left">{children}</span> </Item> diff --git a/frontend/src/context/OrganizationContext/OrganizationContext.tsx b/frontend/src/context/OrganizationContext/OrganizationContext.tsx index d05c496f6..69fd7fe38 100644 --- a/frontend/src/context/OrganizationContext/OrganizationContext.tsx +++ b/frontend/src/context/OrganizationContext/OrganizationContext.tsx @@ -3,7 +3,6 @@ import { createContext, ReactNode, useContext, useMemo } from "react"; import { useGetOrganization } from "@app/hooks/api"; import { Organization } from "@app/hooks/api/types"; -import { useWorkspace } from "../WorkspaceContext"; type TOrgContext = { orgs?: Organization[]; @@ -18,10 +17,10 @@ type Props = { }; export const OrgProvider = ({ children }: Props): JSX.Element => { - const { currentWorkspace } = useWorkspace(); const { data: userOrgs, isLoading } = useGetOrganization(); - const currentWsOrgID = currentWorkspace?.organization; + // const currentWsOrgID = currentWorkspace?.organization; + const currentWsOrgID = localStorage.getItem("orgData.id"); // memorize the workspace details for the context const value = useMemo<TOrgContext>( diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 5bc1bb345..586f171f4 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -144,6 +144,11 @@ export const AppLayout = ({ children }: LayoutProps) => { } }; + const changeOrg = async (orgId) => { + localStorage.setItem("orgData.id", orgId); + 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 @@ -158,7 +163,7 @@ export const AppLayout = ({ children }: LayoutProps) => { if (currentOrg && ( (workspaces?.length === 0 && router.asPath.includes("project")) || router.asPath.includes("/project/undefined") - || (!orgs?.includes(router.query.id) && !router.asPath.includes("project") && !router.asPath.includes("integration")) + || (!orgs?.map(org => org._id)?.includes(router.query.id) && !router.asPath.includes("project") && !router.asPath.includes("integration")) )) { router.push(`/org/${currentOrg?._id}/overview`); } @@ -254,11 +259,21 @@ export const AppLayout = ({ children }: LayoutProps) => { <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> </div> </DropdownMenuTrigger> - <DropdownMenuContent align="start" className="p-1"> + <DropdownMenuContent align="start" className=""> <div className="text-xs text-mineshaft-400 px-2 py-1">{user?.email}</div> {orgs?.map(org => <DropdownMenuItem key={org._id}> - <Link href={`/org/${org._id}/overview`}><div className="w-full flex justify-between items-center">{org.name}{currentOrg._id === org._id && <FontAwesomeIcon icon={faCheck} className="ml-auto text-primary"/>}</div></Link> - </DropdownMenuItem>)} + <Button + onClick={() => changeOrg(org?._id)} + variant="plain" + colorSchema="secondary" + size="xs" + className="w-full flex items-center justify-start" + leftIcon={currentOrg._id === org._id && <FontAwesomeIcon icon={faCheck} className="mr-3 text-primary"/>} + > + <div className="w-full flex justify-between items-center">{org.name}</div> + </Button> + </DropdownMenuItem> + )} <div className="h-1 mt-1 border-t border-mineshaft-600"/> <button type="button" @@ -361,34 +376,28 @@ export const AppLayout = ({ children }: LayoutProps) => { <div className={`px-1 ${!router.asPath.includes("personal") ? "block" : "hidden"}`}> {((router.asPath.includes("project") || router.asPath.includes("integrations")) && currentWorkspace) ? <Menu> <Link href={`/project/${currentWorkspace?._id}/secrets`} passHref> - <a> - <MenuItem - isSelected={router.asPath.includes(`/project/${currentWorkspace?._id}/secrets`)} - icon="system-outline-90-lock-closed" - > - {t("nav.menu.secrets")} - </MenuItem> - </a> + <MenuItem + isSelected={router.asPath.includes(`/project/${currentWorkspace?._id}/secrets`)} + icon="system-outline-90-lock-closed" + > + {t("nav.menu.secrets")} + </MenuItem> </Link> <Link href={`/project/${currentWorkspace?._id}/members`} passHref> - <a> - <MenuItem - isSelected={router.asPath === `/project/${currentWorkspace?._id}/members`} - icon="system-outline-96-groups" - > - {t("nav.menu.members")} - </MenuItem> - </a> + <MenuItem + isSelected={router.asPath === `/project/${currentWorkspace?._id}/members`} + icon="system-outline-96-groups" + > + {t("nav.menu.members")} + </MenuItem> </Link> <Link href={`/integrations/${currentWorkspace?._id}`} passHref> - <a> - <MenuItem - isSelected={router.asPath === `/integrations/${currentWorkspace?._id}`} - icon="system-outline-82-extension" - > - {t("nav.menu.integrations")} - </MenuItem> - </a> + <MenuItem + isSelected={router.asPath === `/integrations/${currentWorkspace?._id}`} + icon="system-outline-82-extension" + > + {t("nav.menu.integrations")} + </MenuItem> </Link> <Link href={`/project/${currentWorkspace?._id}/audit-logs`} passHref> <MenuItem @@ -400,28 +409,24 @@ export const AppLayout = ({ children }: LayoutProps) => { </MenuItem> </Link> <Link href={`/project/${currentWorkspace?._id}/settings`} passHref> - <a> - <MenuItem - isSelected={ - router.asPath === `/project/${currentWorkspace?._id}/settings` - } - icon="system-outline-109-slider-toggle-settings" - > - {t("nav.menu.project-settings")} - </MenuItem> - </a> + <MenuItem + isSelected={ + router.asPath === `/project/${currentWorkspace?._id}/settings` + } + icon="system-outline-109-slider-toggle-settings" + > + {t("nav.menu.project-settings")} + </MenuItem> </Link> </Menu> : <Menu className="mt-4"> <Link href={`/org/${currentOrg?._id}/overview`} passHref> - <a> - <MenuItem - isSelected={router.asPath.includes("/overview")} - icon="system-outline-165-view-carousel" - > - Overview - </MenuItem> - </a> + <MenuItem + isSelected={router.asPath.includes("/overview")} + icon="system-outline-165-view-carousel" + > + Overview + </MenuItem> </Link> {/* {workspaces.map(project => <Link key={project._id} href={`/project/${project?._id}/secrets`} passHref> <a> @@ -437,36 +442,30 @@ export const AppLayout = ({ children }: LayoutProps) => { </div> </Link>)} */} <Link href={`/org/${currentOrg?._id}/members`} passHref> - <a> - <MenuItem - isSelected={router.asPath === `/org/${currentOrg?._id}/members`} - icon="system-outline-96-groups" - > - Organization Members - </MenuItem> - </a> + <MenuItem + isSelected={router.asPath === `/org/${currentOrg?._id}/members`} + icon="system-outline-96-groups" + > + Members + </MenuItem> </Link> <Link href={`/org/${currentOrg?._id}/billing`} passHref> - <a> - <MenuItem - isSelected={router.asPath === `/org/${currentOrg?._id}/billing`} - icon="system-outline-103-coin-cash-monetization" - > - Usage & Billing - </MenuItem> - </a> + <MenuItem + isSelected={router.asPath === `/org/${currentOrg?._id}/billing`} + icon="system-outline-103-coin-cash-monetization" + > + Usage & Billing + </MenuItem> </Link> <Link href={`/org/${currentOrg?._id}/settings`} passHref> - <a> - <MenuItem - isSelected={ - router.asPath === `/org/${currentOrg?._id}/settings` - } - icon="system-outline-109-slider-toggle-settings" - > - Organization Settings - </MenuItem> - </a> + <MenuItem + isSelected={ + router.asPath === `/org/${currentOrg?._id}/settings` + } + icon="system-outline-109-slider-toggle-settings" + > + Organization Settings + </MenuItem> </Link> </Menu>} </div> diff --git a/frontend/src/pages/_app.tsx b/frontend/src/pages/_app.tsx index 164ad5270..963a8d655 100644 --- a/frontend/src/pages/_app.tsx +++ b/frontend/src/pages/_app.tsx @@ -77,8 +77,8 @@ const App = ({ Component, pageProps, ...appProps }: NextAppProp): JSX.Element => <QueryClientProvider client={queryClient}> <TooltipProvider> <AuthProvider> - <WorkspaceProvider> - <OrgProvider> + <OrgProvider> + <WorkspaceProvider> <SubscriptionProvider> <UserProvider> <NotificationProvider> @@ -90,8 +90,8 @@ const App = ({ Component, pageProps, ...appProps }: NextAppProp): JSX.Element => </NotificationProvider> </UserProvider> </SubscriptionProvider> - </OrgProvider> - </WorkspaceProvider> + </WorkspaceProvider> + </OrgProvider> </AuthProvider> </TooltipProvider> </QueryClientProvider> diff --git a/frontend/src/pages/org/[id]/overview/index.tsx b/frontend/src/pages/org/[id]/overview/index.tsx index cc086937c..6d4f5341d 100644 --- a/frontend/src/pages/org/[id]/overview/index.tsx +++ b/frontend/src/pages/org/[id]/overview/index.tsx @@ -147,9 +147,8 @@ export default function Organization() { const router = useRouter(); - const { workspaces, - // isLoading: isWorkspaceLoading - } = useWorkspace(); + const { workspaces } = useWorkspace(); + const orgWorkspaces = workspaces?.filter(workspace => workspace.organization === localStorage.getItem("orgData.id")) || [] const currentOrg = String(router.query.id); const { createNotification } = useNotificationContext(); const addWsUser = useAddUserToWs(); @@ -243,7 +242,7 @@ export default function Organization() { <link rel="icon" href="/infisical.ico" /> </Head> <div className="flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl mb-4"> - <p className="mr-4 font-semibold text-white">Projects</p> + <p className="mr-4 font-semibold text-white">Projects {orgWorkspaces.map(ws => ws?.name).join("")}</p> <div className="w-full flex flex-row mt-6"> <Input className="h-[2.3rem] text-sm bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80" @@ -268,7 +267,7 @@ export default function Organization() { </Button> </div> <div className="mt-4 w-full grid gap-4 grid-cols-3 grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4"> - {workspaces.filter(ws => ws.name.toLowerCase().includes(searchFilter.toLowerCase())).map(workspace => <div key={workspace._id} className="h-32 lg:h-40 min-w-72 rounded-md bg-mineshaft-800 border border-mineshaft-600 p-4 flex flex-col justify-between"> + {orgWorkspaces.filter(ws => ws?.name?.toLowerCase().includes(searchFilter.toLowerCase())).map(workspace => <div key={workspace._id} className="h-32 lg:h-40 min-w-72 rounded-md bg-mineshaft-800 border border-mineshaft-600 p-4 flex flex-col justify-between"> <div className="text-lg text-mineshaft-100 mt-0">{workspace.name}</div> <div className="text-sm text-mineshaft-300 mt-0 lg:pb-6">{(workspace.environments?.length || 0)} environments</div> <Link href={`/project/${workspace._id}/secrets`}> @@ -276,7 +275,7 @@ export default function Organization() { </Link> </div>)} </div> - {workspaces.length === 0 && ( + {orgWorkspaces.length === 0 && ( <div className="w-full rounded-md bg-mineshaft-800 border border-mineshaft-700 px-4 py-6 text-mineshaft-300 text-base"> <FontAwesomeIcon icon={faFolderOpen} className="w-full text-center text-5xl mb-4 mt-2 text-mineshaft-400" /> <div className="text-center font-light"> @@ -300,16 +299,16 @@ export default function Organization() { userAction: "intro_cta_clicked", link: "https://www.youtube.com/watch?v=PK23097-25I" })} - {workspaces.length !== 0 && learningItem({ + {orgWorkspaces.length !== 0 && learningItem({ text: "Add your secrets", subText: "Click to see example secrets, and add your own.", complete: hasUserPushedSecrets, icon: faPlus, time: "1 min", userAction: "first_time_secrets_pushed", - link: `/project/${workspaces[0]?._id}/secrets` + link: `/project/${orgWorkspaces[0]?._id}/secrets` })} - {workspaces.length !== 0 && <div className="group text-mineshaft-100 relative mb-3 flex h-full w-full cursor-default flex-col items-center justify-between overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-800 pl-2 pr-2 pt-4 pb-2 shadow-xl duration-200"> + {orgWorkspaces.length !== 0 && <div className="group text-mineshaft-100 relative mb-3 flex h-full w-full cursor-default flex-col items-center justify-between overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-800 pl-2 pr-2 pt-4 pb-2 shadow-xl duration-200"> <div className="mb-4 flex w-full flex-row items-center pr-4"> <div className="mr-4 flex w-full flex-row items-center"> <FontAwesomeIcon icon={faNetworkWired} className="mx-2 w-16 text-4xl" /> @@ -332,7 +331,7 @@ export default function Organization() { <TabsObject /> {false && <div className="absolute bottom-0 left-0 h-1 w-full bg-green" />} </div>} - {workspaces.length !== 0 && learningItem({ + {orgWorkspaces.length !== 0 && learningItem({ text: "Integrate Infisical with your infrastructure", subText: "Connect Infisical to various 3rd party services and platforms.", complete: false, diff --git a/frontend/src/views/Org/MembersPage/components/OrgMembersTable/OrgMembersTable.tsx b/frontend/src/views/Org/MembersPage/components/OrgMembersTable/OrgMembersTable.tsx index c9a2cf36c..a995661c9 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgMembersTable/OrgMembersTable.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgMembersTable/OrgMembersTable.tsx @@ -27,6 +27,7 @@ import { THead, Tr, UpgradePlanModal} from "@app/components/v2"; +import { useWorkspace } from "@app/context"; import { usePopUp, useToggle } from "@app/hooks"; import { useFetchServerStatus } from "@app/hooks/api/serverDetails"; import { OrgUser, Workspace } from "@app/hooks/api/types"; @@ -70,6 +71,7 @@ export const OrgMembersTable = ({ const router = useRouter(); const [searchMemberFilter, setSearchMemberFilter] = useState(""); const {data: serverDetails } = useFetchServerStatus() + const { workspaces } = useWorkspace(); const [isInviteLinkCopied, setInviteLinkCopied] = useToggle(false); const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ "addMember", @@ -230,7 +232,7 @@ export const OrgMembersTable = ({ : <Tag colorSchema="red">This user isn't part of any projects yet</Tag>} {router.query.id !== "undefined" && !((status === "invited" || status === "verified") && serverDetails?.emailConfigured) && <button type="button" - onClick={() => router.push(`/users/${router.query.id}`)} + onClick={() => router.push(`/project/${workspaces[0]?._id}/members`)} className='text-sm bg-mineshaft w-max px-1.5 py-0.5 hover:bg-primary duration-200 hover:text-black cursor-pointer rounded-sm' > <FontAwesomeIcon icon={faPlus} className="mr-1" /> diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgMembersSection/OrgMembersSection.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgMembersSection/OrgMembersSection.tsx deleted file mode 100644 index d334d55e1..000000000 --- a/frontend/src/views/Settings/OrgSettingsPage/components/OrgMembersSection/OrgMembersSection.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { faPlus } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; - -import { - Button, - UpgradePlanModal -} from "@app/components/v2"; -import { useSubscription } from "@app/context"; -import { usePopUp } from "@app/hooks"; - -import { AddOrgMemberModal } from "./AddOrgMemberModal"; -import { OrgMembersTable } from "./OrgMembersTable"; - -export const OrgMembersSection = () => { - const { subscription } = useSubscription(); - const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ - "addMember", - "upgradePlan", - ] as const); - - const isMoreMembersAllowed = subscription?.memberLimit ? (subscription.membersUsed < subscription.memberLimit) : true; - - return ( - <div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600"> - <div className="flex justify-between mb-4"> - <p className="text-xl font-semibold text-mineshaft-100"> - Organization members - </p> - <Button - colorSchema="secondary" - type="submit" - leftIcon={<FontAwesomeIcon icon={faPlus} />} - onClick={() => { - if (isMoreMembersAllowed) { - handlePopUpOpen("addMember"); - return; - } - handlePopUpOpen("upgradePlan"); - }} - > - Add Member - </Button> - </div> - <OrgMembersTable /> - <UpgradePlanModal - isOpen={popUp.upgradePlan.isOpen} - onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)} - text="Add more members by upgrading to a higher Infisical plan." - /> - <AddOrgMemberModal - popUp={popUp} - handlePopUpToggle={handlePopUpToggle} - handlePopUpClose={handlePopUpClose} - /> - </div> - ); -} \ No newline at end of file diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgMembersSection/OrgMembersTable.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgMembersSection/OrgMembersTable.tsx deleted file mode 100644 index a9f54eee0..000000000 --- a/frontend/src/views/Settings/OrgSettingsPage/components/OrgMembersSection/OrgMembersTable.tsx +++ /dev/null @@ -1,304 +0,0 @@ -import { useMemo, useState } from "react"; -import { useRouter } from "next/router"; -import { faMagnifyingGlass, faPlus, faTrash, faUsers } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; - -import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; -import { - decryptAssymmetric, - encryptAssymmetric -} from "@app/components/utilities/cryptography/crypto"; -import { - Button, - DeleteActionModal, - EmptyState, - IconButton, - Input, - Select, - SelectItem, - Table, - TableContainer, - TableSkeleton, - Tag, - TBody, - Td, - Th, - THead, - Tr -} from "@app/components/v2"; -import { useOrganization, useUser, useWorkspace } from "@app/context"; -import { usePopUp } from "@app/hooks"; -import { - useAddUserToOrg, - useDeleteOrgMembership, - useGetOrgUsers, - useGetUserWorkspaceMemberships, - useGetUserWsKey, - useUpdateOrgUserRole, - useUploadWsKey -} from "@app/hooks/api"; -import { useFetchServerStatus } from "@app/hooks/api/serverDetails"; - -export const OrgMembersTable = () => { - const router = useRouter(); - const { user: currentUser } = useUser(); - const { currentWorkspace } = useWorkspace(); - const { currentOrg } = useOrganization(); - const { createNotification } = useNotificationContext(); - const [searchMemberFilter, setSearchMemberFilter] = useState(""); - - const { data: serverDetails } = useFetchServerStatus() - const { data: members, isLoading: isOrgUserLoading } = useGetOrgUsers(currentOrg?._id ?? ""); // members - const { data: workspaceMemberships, isLoading: IsWsMembershipLoading } = useGetUserWorkspaceMemberships(currentOrg?._id ?? ""); - const { data: wsKey } = useGetUserWsKey(currentWorkspace?._id || ""); - - const uploadWsKey = useUploadWsKey(); - const addUserToOrg = useAddUserToOrg(); - const removeUserOrgMembership = useDeleteOrgMembership(); - const updateOrgUserRole = useUpdateOrgUserRole(); - - const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ - "removeMember", - "setUpEmail" - ] as const); - - const isLoading = isOrgUserLoading || IsWsMembershipLoading; - const userId = currentUser?._id || ""; - const onRemoveMember = async (membershipId: string) => { - if (!currentOrg?._id) return; - - try { - if (!currentOrg?._id) return; - await removeUserOrgMembership.mutateAsync({ orgId: currentOrg?._id, membershipId }); - createNotification({ - text: "Successfully removed user from org", - type: "success" - }); - } catch (error) { - console.error(error); - createNotification({ - text: "Failed to remove user from the organization", - type: "error" - }); - } - }; - - const onRemoveOrgMemberApproved = async () => { - const orgMembershipId = (popUp?.removeMember?.data as { id: string })?.id; - await onRemoveMember(orgMembershipId); - handlePopUpClose("removeMember"); - }; - - - const onInviteMember = async (email: string) => { - if (!currentOrg?._id) return; - - try { - const { data } = await addUserToOrg.mutateAsync({ - organizationId: currentOrg?._id, - inviteeEmail: email - }); - - // only show this notification when email is configured. A [completeInviteLink] will not be sent if smtp is configured - if (!data.completeInviteLink) { - createNotification({ - text: "Successfully invited user to the organization.", - type: "success" - }); - } - } catch (error) { - console.error(error); - createNotification({ - text: "Failed to invite user to org", - type: "error" - }); - } - }; - - const onGrantAccess = async (targetUserId: string, publicKey: string) => { - try { - const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string; - if (!PRIVATE_KEY || !wsKey) return; - - // assymmetrically decrypt symmetric key with local private key - const key = decryptAssymmetric({ - ciphertext: wsKey.encryptedKey, - nonce: wsKey.nonce, - publicKey: wsKey.sender.publicKey, - privateKey: PRIVATE_KEY - }); - - const { ciphertext, nonce } = encryptAssymmetric({ - plaintext: key, - publicKey, - privateKey: PRIVATE_KEY - }); - - await uploadWsKey.mutateAsync({ - userId: targetUserId, - nonce, - encryptedKey: ciphertext, - workspaceId: currentWorkspace?._id || "" - }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to grant access to user", - type: "error" - }); - } - }; - - const onRoleChange = async (membershipId: string, role: string) => { - if (!currentOrg?._id) return; - - try { - await updateOrgUserRole.mutateAsync({ organizationId: currentOrg?._id, membershipId, role }); - createNotification({ - text: "Successfully updated user role", - type: "success" - }); - } catch (error) { - console.error(error); - createNotification({ - text: "Failed to update user role", - type: "error" - }); - } - }; - - const isIamOwner = useMemo( - () => members ? members.find(({ user }) => userId === user?._id)?.role === "owner" : [], - [userId, members] - ); - - const filterdUser = useMemo( - () => - members ? members.filter( - ({ user, inviteEmail }) => - user?.firstName?.toLowerCase().includes(searchMemberFilter) || - user?.lastName?.toLowerCase().includes(searchMemberFilter) || - user?.email?.toLowerCase().includes(searchMemberFilter) || - inviteEmail?.includes(searchMemberFilter) - ) : [], - [members, searchMemberFilter] - ); - - - return ( - <div className="w-full"> - <Input - value={searchMemberFilter} - onChange={(e) => setSearchMemberFilter(e.target.value)} - leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />} - placeholder="Search members..." - /> - <TableContainer className="mt-4"> - <Table> - <THead> - <Tr> - <Th>Name</Th> - <Th>Email</Th> - <Th>Role</Th> - <Th>Projects</Th> - <Th aria-label="actions" /> - </Tr> - </THead> - <TBody> - {isLoading && <TableSkeleton columns={5} key="org-memberships" />} - {!isLoading && - filterdUser.map(({ user, inviteEmail, role, _id: orgMembershipId, status }) => { - const name = user ? `${user.firstName} ${user.lastName}` : "-"; - const email = user?.email || inviteEmail; - const userWs = workspaceMemberships?.[user?._id]; - - return ( - <Tr key={`org-membership-${orgMembershipId}`} className="w-full"> - <Td>{name}</Td> - <Td>{email}</Td> - <Td> - {status === "accepted" && ( - <Select - defaultValue={role} - isDisabled={userId === user?._id} - className="w-40 bg-mineshaft-600" - dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800" - onValueChange={(selectedRole) => - onRoleChange(orgMembershipId, selectedRole) - } - > - {(isIamOwner || role === "owner") && ( - <SelectItem value="owner">owner</SelectItem> - )} - <SelectItem value="admin">admin</SelectItem> - <SelectItem value="member">member</SelectItem> - </Select> - )} - {((status === "invited" || status === "verified") && serverDetails?.emailConfigured) && ( - <Button className='w-40' colorSchema="primary" variant="outline_bg" onClick={() => onInviteMember(email)}> - Resend Invite - </Button> - )} - {status === "completed" && ( - <Button - colorSchema="secondary" - onClick={() => onGrantAccess(user?._id, user?.publicKey)} - > - Grant Access - </Button> - )} - </Td> - <Td> - {userWs ? ( - userWs?.map(({ name: wsName, _id }) => ( - <Tag key={`user-${currentUser._id}-workspace-${_id}`} className="my-1"> - {wsName} - </Tag> - )) - ) : ( - <div className='flex flex-row'> - {((status === "invited" || status === "verified") && serverDetails?.emailConfigured) - ? <Tag colorSchema="red">This user hasn't accepted the invite yet</Tag> - : <Tag colorSchema="red">This user isn't part of any projects yet</Tag>} - {router.query.id !== "undefined" && !((status === "invited" || status === "verified") && serverDetails?.emailConfigured) && <button - type="button" - onClick={() => router.push(`/users/${router.query.id}`)} - className='text-sm bg-mineshaft w-max px-1.5 py-0.5 hover:bg-primary duration-200 hover:text-black cursor-pointer rounded-sm' - > - <FontAwesomeIcon icon={faPlus} className="mr-1" /> - Add to projects - </button>} - </div> - )} - </Td> - <Td> - {userId !== user?._id && ( - <IconButton - ariaLabel="delete" - colorSchema="danger" - isDisabled={userId === user?._id} - onClick={() => handlePopUpOpen("removeMember", { id: orgMembershipId })} - > - <FontAwesomeIcon icon={faTrash} /> - </IconButton> - )} - </Td> - </Tr> - ); - })} - </TBody> - </Table> - {!isLoading && filterdUser?.length === 0 && ( - <EmptyState title="No project members found" icon={faUsers} /> - )} - </TableContainer> - <DeleteActionModal - isOpen={popUp.removeMember.isOpen} - deleteKey="remove" - title="Do you want to remove this user from the org?" - onChange={(isOpen) => handlePopUpToggle("removeMember", isOpen)} - onDeleteApproved={onRemoveOrgMemberApproved} - /> - </div> - ); -}; \ No newline at end of file diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgMembersSection/index.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgMembersSection/index.tsx index 973e12e3e..e69de29bb 100644 --- a/frontend/src/views/Settings/OrgSettingsPage/components/OrgMembersSection/index.tsx +++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgMembersSection/index.tsx @@ -1 +0,0 @@ -export { OrgMembersSection } from "./OrgMembersSection"; diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/index.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/index.tsx index e852edb06..9d2819536 100644 --- a/frontend/src/views/Settings/OrgSettingsPage/components/index.tsx +++ b/frontend/src/views/Settings/OrgSettingsPage/components/index.tsx @@ -1,4 +1,3 @@ export { OrgIncidentContactsSection } from "./OrgIncidentContactsSection"; -export { OrgMembersSection } from "./OrgMembersSection"; export { OrgNameChangeSection } from "./OrgNameChangeSection"; export { OrgServiceAccountsTable } from "./OrgServiceAccountsTable"; \ No newline at end of file From d3aeb729e081880b7c114563816dc38901c2292c Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako <matsiiako@gmail.com> Date: Wed, 12 Jul 2023 11:18:42 -0700 Subject: [PATCH 2/3] fixing ui/ux bugs --- frontend/src/layouts/AppLayout/AppLayout.tsx | 152 ++++++++++-------- .../src/pages/org/[id]/overview/index.tsx | 135 +++++++++++----- 2 files changed, 181 insertions(+), 106 deletions(-) diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 586f171f4..0f5050554 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -163,7 +163,7 @@ export const AppLayout = ({ children }: LayoutProps) => { 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("integration")) + || (!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`); } @@ -259,7 +259,7 @@ export const AppLayout = ({ children }: LayoutProps) => { <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> </div> </DropdownMenuTrigger> - <DropdownMenuContent align="start" className=""> + <DropdownMenuContent align="start" className="p-1"> <div className="text-xs text-mineshaft-400 px-2 py-1">{user?.email}</div> {orgs?.map(org => <DropdownMenuItem key={org._id}> <Button @@ -267,7 +267,7 @@ export const AppLayout = ({ children }: LayoutProps) => { variant="plain" colorSchema="secondary" size="xs" - className="w-full flex items-center justify-start" + className="w-full flex items-center justify-start p-0 font-normal" leftIcon={currentOrg._id === org._id && <FontAwesomeIcon icon={faCheck} className="mr-3 text-primary"/>} > <div className="w-full flex justify-between items-center">{org.name}</div> @@ -285,8 +285,8 @@ export const AppLayout = ({ children }: LayoutProps) => { </DropdownMenuContent> </DropdownMenu> <DropdownMenu> - <DropdownMenuTrigger asChild className="data-[state=open]:bg-mineshaft-600 p-1"> - <div className="w-6 h-6 rounded-full bg-mineshaft hover:bg-mineshaft-400 pr-1 text-xs text-mineshaft-300 flex justify-center"> + <DropdownMenuTrigger asChild className="hover:bg-primary-400 hover:text-black data-[state=open]:text-black data-[state=open]:bg-primary-400 p-1"> + <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> @@ -369,64 +369,76 @@ export const AppLayout = ({ children }: LayoutProps) => { </div> </Select> </div> - ) : <div className="pr-2 my-6 flex justify-center items-center text-mineshaft-300 hover:text-mineshaft-100 cursor-default text-sm"> + ) : <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"/> - <Link href={`/org/${currentOrg?._id}/overview`}>Back to organization</Link> - </div>)} + Back to organization + </div></Link>)} <div className={`px-1 ${!router.asPath.includes("personal") ? "block" : "hidden"}`}> {((router.asPath.includes("project") || router.asPath.includes("integrations")) && currentWorkspace) ? <Menu> <Link href={`/project/${currentWorkspace?._id}/secrets`} passHref> - <MenuItem - isSelected={router.asPath.includes(`/project/${currentWorkspace?._id}/secrets`)} - icon="system-outline-90-lock-closed" - > - {t("nav.menu.secrets")} - </MenuItem> + <a> + <MenuItem + isSelected={router.asPath.includes(`/project/${currentWorkspace?._id}/secrets`)} + icon="system-outline-90-lock-closed" + > + {t("nav.menu.secrets")} + </MenuItem> + </a> </Link> <Link href={`/project/${currentWorkspace?._id}/members`} passHref> - <MenuItem - isSelected={router.asPath === `/project/${currentWorkspace?._id}/members`} - icon="system-outline-96-groups" - > - {t("nav.menu.members")} - </MenuItem> + <a> + <MenuItem + isSelected={router.asPath === `/project/${currentWorkspace?._id}/members`} + icon="system-outline-96-groups" + > + {t("nav.menu.members")} + </MenuItem> + </a> </Link> <Link href={`/integrations/${currentWorkspace?._id}`} passHref> - <MenuItem - isSelected={router.asPath === `/integrations/${currentWorkspace?._id}`} - icon="system-outline-82-extension" - > - {t("nav.menu.integrations")} - </MenuItem> + <a> + <MenuItem + isSelected={router.asPath === `/integrations/${currentWorkspace?._id}`} + icon="system-outline-82-extension" + > + {t("nav.menu.integrations")} + </MenuItem> + </a> </Link> <Link href={`/project/${currentWorkspace?._id}/audit-logs`} passHref> - <MenuItem - isSelected={router.asPath === `/project/${currentWorkspace?._id}/audit-logs`} - // icon={<FontAwesomeIcon icon={faFileLines} size="lg" />} - icon="system-outline-168-view-headline" - > - Audit Logs - </MenuItem> + <a> + <MenuItem + isSelected={router.asPath === `/project/${currentWorkspace?._id}/audit-logs`} + // icon={<FontAwesomeIcon icon={faFileLines} size="lg" />} + icon="system-outline-168-view-headline" + > + Audit Logs + </MenuItem> + </a> </Link> <Link href={`/project/${currentWorkspace?._id}/settings`} passHref> - <MenuItem - isSelected={ - router.asPath === `/project/${currentWorkspace?._id}/settings` - } - icon="system-outline-109-slider-toggle-settings" - > - {t("nav.menu.project-settings")} - </MenuItem> + <a> + <MenuItem + isSelected={ + router.asPath === `/project/${currentWorkspace?._id}/settings` + } + icon="system-outline-109-slider-toggle-settings" + > + {t("nav.menu.project-settings")} + </MenuItem> + </a> </Link> </Menu> : <Menu className="mt-4"> <Link href={`/org/${currentOrg?._id}/overview`} passHref> - <MenuItem - isSelected={router.asPath.includes("/overview")} - icon="system-outline-165-view-carousel" - > - Overview - </MenuItem> + <a> + <MenuItem + isSelected={router.asPath.includes("/overview")} + icon="system-outline-165-view-carousel" + > + Overview + </MenuItem> + </a> </Link> {/* {workspaces.map(project => <Link key={project._id} href={`/project/${project?._id}/secrets`} passHref> <a> @@ -442,30 +454,36 @@ export const AppLayout = ({ children }: LayoutProps) => { </div> </Link>)} */} <Link href={`/org/${currentOrg?._id}/members`} passHref> - <MenuItem - isSelected={router.asPath === `/org/${currentOrg?._id}/members`} - icon="system-outline-96-groups" - > - Members - </MenuItem> + <a> + <MenuItem + isSelected={router.asPath === `/org/${currentOrg?._id}/members`} + icon="system-outline-96-groups" + > + Members + </MenuItem> + </a> </Link> <Link href={`/org/${currentOrg?._id}/billing`} passHref> - <MenuItem - isSelected={router.asPath === `/org/${currentOrg?._id}/billing`} - icon="system-outline-103-coin-cash-monetization" - > - Usage & Billing - </MenuItem> + <a> + <MenuItem + isSelected={router.asPath === `/org/${currentOrg?._id}/billing`} + icon="system-outline-103-coin-cash-monetization" + > + Usage & Billing + </MenuItem> + </a> </Link> <Link href={`/org/${currentOrg?._id}/settings`} passHref> - <MenuItem - isSelected={ - router.asPath === `/org/${currentOrg?._id}/settings` - } - icon="system-outline-109-slider-toggle-settings" - > - Organization Settings - </MenuItem> + <a> + <MenuItem + isSelected={ + router.asPath === `/org/${currentOrg?._id}/settings` + } + icon="system-outline-109-slider-toggle-settings" + > + Organization Settings + </MenuItem> + </a> </Link> </Menu>} </div> diff --git a/frontend/src/pages/org/[id]/overview/index.tsx b/frontend/src/pages/org/[id]/overview/index.tsx index 6d4f5341d..22f58cb42 100644 --- a/frontend/src/pages/org/[id]/overview/index.tsx +++ b/frontend/src/pages/org/[id]/overview/index.tsx @@ -133,6 +133,61 @@ const learningItem = ({ ); }; +const learningItemSquare = ({ + text, + subText, + complete, + icon, + time, + userAction, + link +}: ItemProps): JSX.Element => { + return ( + <a + target={`${link?.includes("https") ? "_blank" : "_self"}`} + rel="noopener noreferrer" + className={`w-full ${complete && "opacity-30 duration-200 hover:opacity-100"}`} + href={link} + > + <div className={`${complete ? "bg-gradient-to-r from-primary-500/70 p-[0.07rem]" : ""} rounded-md w-full`}> + <div + onKeyDown={() => null} + role="button" + tabIndex={0} + onClick={async () => { + if (userAction && userAction !== "first_time_secrets_pushed") { + await registerUserAction({ + action: userAction + }); + } + }} + className={`group relative flex w-full items-center justify-between overflow-hidden rounded-md border ${complete? "bg-gradient-to-r from-[#0e1f01] to-mineshaft-700 border-mineshaft-900 cursor-default" : "bg-mineshaft-800 hover:bg-mineshaft-700 border-mineshaft-600 shadow-xl cursor-pointer"} duration-200 text-mineshaft-100`} + > + <div className="flex flex-col items-center w-full px-6 py-4"> + <div className="flex flex-row items-start justify-between w-full"> + <FontAwesomeIcon icon={icon} className="w-16 text-5xl text-mineshaft-200 group-hover:text-mineshaft-100 duration-100 pt-2" /> + {complete && ( + <div className="absolute left-14 top-12 flex h-7 w-7 items-center justify-center rounded-full bg-bunker-500 p-2 group-hover:bg-mineshaft-700"> + <FontAwesomeIcon icon={faCheckCircle} className="h-5 w-5 text-4xl text-primary" /> + </div> + )} + <div + className={`text-right text-sm font-normal text-mineshaft-300 ${complete ? "text-primary font-semibold" : ""}`} + > + {complete ? "Complete!" : `About ${time}`} + </div> + </div> + <div className="flex flex-col items-start justify-start w-full pt-4"> + <div className="mt-0.5 text-lg font-medium">{text}</div> + <div className="text-sm font-normal text-mineshaft-300">{subText}</div> + </div> + </div> + </div> + </div> + </a> + ); +}; + const formSchema = yup.object({ name: yup.string().required().label("Project Name").trim(), addMembers: yup.bool().required().label("Add Members") @@ -242,7 +297,7 @@ export default function Organization() { <link rel="icon" href="/infisical.ico" /> </Head> <div className="flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl mb-4"> - <p className="mr-4 font-semibold text-white">Projects {orgWorkspaces.map(ws => ws?.name).join("")}</p> + <p className="mr-4 font-semibold text-white">Projects</p> <div className="w-full flex flex-row mt-6"> <Input className="h-[2.3rem] text-sm bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80" @@ -266,10 +321,10 @@ export default function Organization() { Add New Project </Button> </div> - <div className="mt-4 w-full grid gap-4 grid-cols-3 grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4"> - {orgWorkspaces.filter(ws => ws?.name?.toLowerCase().includes(searchFilter.toLowerCase())).map(workspace => <div key={workspace._id} className="h-32 lg:h-40 min-w-72 rounded-md bg-mineshaft-800 border border-mineshaft-600 p-4 flex flex-col justify-between"> + <div className="mt-4 w-full grid gap-4 grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4"> + {orgWorkspaces.filter(ws => ws?.name?.toLowerCase().includes(searchFilter.toLowerCase())).map(workspace => <div key={workspace._id} className="h-40 min-w-72 rounded-md bg-mineshaft-800 border border-mineshaft-600 p-4 flex flex-col justify-between"> <div className="text-lg text-mineshaft-100 mt-0">{workspace.name}</div> - <div className="text-sm text-mineshaft-300 mt-0 lg:pb-6">{(workspace.environments?.length || 0)} environments</div> + <div className="text-sm text-mineshaft-300 mt-0 pb-6">{(workspace.environments?.length || 0)} environments</div> <Link href={`/project/${workspace._id}/secrets`}> <div className="group cursor-default ml-auto hover:bg-primary-800/20 text-sm text-mineshaft-300 hover:text-mineshaft-200 bg-mineshaft-900 py-2 px-4 rounded-full w-max border border-mineshaft-600 hover:border-primary-500/80">Explore <FontAwesomeIcon icon={faArrowRight} className="pl-1.5 pr-0.5 group-hover:pl-2 group-hover:pr-0 duration-200" /></div> </Link> @@ -290,24 +345,43 @@ export default function Organization() { </div> {((new Date()).getTime() - (new Date(user?.createdAt)).getTime()) < 30 * 24 * 60 * 60 * 1000 && <div className="flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl mb-4"> <p className="mr-4 font-semibold text-white mb-4">Onboarding Guide</p> - {learningItem({ - text: "Watch a video about Infisical", - subText: "", - complete: hasUserClickedIntro, - icon: faHandPeace, - time: "3 min", - userAction: "intro_cta_clicked", - link: "https://www.youtube.com/watch?v=PK23097-25I" - })} - {orgWorkspaces.length !== 0 && learningItem({ - text: "Add your secrets", - subText: "Click to see example secrets, and add your own.", - complete: hasUserPushedSecrets, - icon: faPlus, - time: "1 min", - userAction: "first_time_secrets_pushed", - link: `/project/${orgWorkspaces[0]?._id}/secrets` - })} + <div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-3 w-full mb-3"> + {learningItemSquare({ + text: "Watch Infisical demo", + subText: "Set up Infisical in 3 min.", + complete: hasUserClickedIntro, + icon: faHandPeace, + time: "3 min", + userAction: "intro_cta_clicked", + link: "https://www.youtube.com/watch?v=PK23097-25I" + })} + {orgWorkspaces.length !== 0 && learningItemSquare({ + text: "Add your secrets", + subText: "Drop a .env file or type your secrets.", + complete: hasUserPushedSecrets, + icon: faPlus, + time: "1 min", + userAction: "first_time_secrets_pushed", + link: `/project/${orgWorkspaces[0]?._id}/secrets` + })} + {learningItemSquare({ + text: "Invite your teammates", + subText: "Infisical is better used as a team.", + complete: usersInOrg, + icon: faUserPlus, + time: "2 min", + link: `/org/${router.query.id}/members?action=invite` + })} + <div className="block xl:hidden 2xl:block">{learningItemSquare({ + text: "Join Infisical Slack", + subText: "Have any questions? Ask us!", + complete: hasUserClickedSlack, + icon: faSlack, + time: "1 min", + userAction: "slack_cta_clicked", + link: "https://join.slack.com/t/infisical-users/shared_invite/zt-1ye0tm8ab-899qZ6ZbpfESuo6TEikyOQ" + })}</div> + </div> {orgWorkspaces.length !== 0 && <div className="group text-mineshaft-100 relative mb-3 flex h-full w-full cursor-default flex-col items-center justify-between overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-800 pl-2 pr-2 pt-4 pb-2 shadow-xl duration-200"> <div className="mb-4 flex w-full flex-row items-center pr-4"> <div className="mr-4 flex w-full flex-row items-center"> @@ -339,23 +413,6 @@ export default function Organization() { time: "15 min", link: "https://infisical.com/docs/integrations/overview" })} - {learningItem({ - text: "Invite your teammates", - subText: "", - complete: usersInOrg, - icon: faUserPlus, - time: "2 min", - link: `/org/${router.query.id}/members?action=invite` - })} - {learningItem({ - text: "Join Infisical Slack", - subText: "Have any questions? Ask us!", - complete: hasUserClickedSlack, - icon: faSlack, - time: "1 min", - userAction: "slack_cta_clicked", - link: "https://join.slack.com/t/infisical-users/shared_invite/zt-1ye0tm8ab-899qZ6ZbpfESuo6TEikyOQ" - })} </div>} <div className="flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl mb-4 pb-6"> <p className="mr-4 font-semibold text-white">Explore More</p> From e4e87163e83bdccff17b6d0116b9784bdcb28dc5 Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako <matsiiako@gmail.com> Date: Wed, 12 Jul 2023 11:19:56 -0700 Subject: [PATCH 3/3] removed org member section --- .../OrgMembersSection/AddOrgMemberModal.tsx | 174 ------------------ .../components/OrgMembersSection/index.tsx | 0 2 files changed, 174 deletions(-) delete mode 100644 frontend/src/views/Settings/OrgSettingsPage/components/OrgMembersSection/AddOrgMemberModal.tsx delete mode 100644 frontend/src/views/Settings/OrgSettingsPage/components/OrgMembersSection/index.tsx diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgMembersSection/AddOrgMemberModal.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgMembersSection/AddOrgMemberModal.tsx deleted file mode 100644 index 4966b19bf..000000000 --- a/frontend/src/views/Settings/OrgSettingsPage/components/OrgMembersSection/AddOrgMemberModal.tsx +++ /dev/null @@ -1,174 +0,0 @@ -import { useEffect, useState } from "react"; -import { Controller, useForm } from "react-hook-form"; -import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { yupResolver } from "@hookform/resolvers/yup"; -import * as yup from "yup"; - -import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; -import { - Button, - FormControl, - IconButton, - Input, - Modal, - ModalContent -} from "@app/components/v2"; -import { useOrganization } from "@app/context"; -import { useToggle } from "@app/hooks"; -import { - useAddUserToOrg -} from "@app/hooks/api"; -import { useFetchServerStatus } from "@app/hooks/api/serverDetails"; -import { UsePopUpState } from "@app/hooks/usePopUp"; - -const addMemberFormSchema = yup.object({ - email: yup.string().email().required().label("Email").trim() -}); - -type TAddMemberForm = yup.InferType<typeof addMemberFormSchema>; // TODO: change to FormData - -type Props = { - popUp: UsePopUpState<["addMember"]>; - handlePopUpClose: (popUpName: keyof UsePopUpState<["addMember"]>) => void; - handlePopUpToggle: (popUpName: keyof UsePopUpState<["addMember"]>, state?: boolean) => void; -}; - -// TODO: test no-SMTP setup case - -export const AddOrgMemberModal = ({ - popUp, - handlePopUpToggle, - handlePopUpClose -}: Props) => { - const { currentOrg } = useOrganization(); - const { data: serverDetails } = useFetchServerStatus(); - const { createNotification } = useNotificationContext(); - - const [isInviteLinkCopied, setInviteLinkCopied] = useToggle(false); - const [completeInviteLink, setCompleteInviteLink] = useState<string | undefined>(""); - const { - control, - handleSubmit, - reset - } = useForm<TAddMemberForm>({ resolver: yupResolver(addMemberFormSchema) }); - - const { mutateAsync, isLoading } = useAddUserToOrg(); - - useEffect(() => { - let timer: NodeJS.Timeout; - if (isInviteLinkCopied) { - timer = setTimeout(() => setInviteLinkCopied.off(), 2000); - } - - return () => clearTimeout(timer); - }, [isInviteLinkCopied]); - - const copyTokenToClipboard = () => { - navigator.clipboard.writeText(completeInviteLink as string); - setInviteLinkCopied.on(); - }; - - const onFormSubmit = async ({ email }: TAddMemberForm) => { - try { - if (!currentOrg?._id) return; - - const { data } = await mutateAsync({ - organizationId: currentOrg?._id, - inviteeEmail: email - }); - - setCompleteInviteLink(data?.completeInviteLink); - - if (!data.completeInviteLink) { - createNotification({ - text: "Successfully sent an invite to the user.", - type: "success" - }); - } - - if (serverDetails?.emailConfigured){ - handlePopUpClose("addMember"); - } - - reset(); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to send an invite to the user", - type: "error" - }); - } - } - - return ( - <Modal - isOpen={popUp?.addMember?.isOpen} - onOpenChange={(isOpen) => { - handlePopUpToggle("addMember", isOpen); - setCompleteInviteLink(undefined); - }} - > - <ModalContent - title={`Invite others to ${currentOrg?.name ?? ""}`} - subTitle={ - <div> - {!completeInviteLink && <div> - An invite is specific to an email address and expires after 1 day. - <br /> - For security reasons, you will need to separately add members to projects. - </div>} - {completeInviteLink && "This Infisical instance does not have a email provider setup. Please share this invite link with the invitee manually"} - </div> - } - > - {!completeInviteLink && ( - <form onSubmit={handleSubmit(onFormSubmit)} > - <Controller - control={control} - defaultValue="" - name="email" - render={({ field, fieldState: { error } }) => ( - <FormControl label="Email" isError={Boolean(error)} errorText={error?.message}> - <Input {...field} /> - </FormControl> - )} - /> - <div className="mt-8 flex items-center"> - <Button - className="mr-4" - size="sm" - type="submit" - isLoading={isLoading} - isDisabled={isLoading} - > - Add Member - </Button> - <Button - colorSchema="secondary" - variant="plain" - onClick={() => handlePopUpClose("addMember")} - > - Cancel - </Button> - </div> - </form> - )} - {completeInviteLink && ( - <div className="mt-2 mb-3 mr-2 flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400"> - <p className="mr-4 break-all">{completeInviteLink}</p> - <IconButton - ariaLabel="copy icon" - colorSchema="secondary" - className="group relative" - onClick={copyTokenToClipboard} - > - <FontAwesomeIcon icon={isInviteLinkCopied ? faCheck : faCopy} /> - <span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">click to copy</span> - </IconButton> - </div> - )} - </ModalContent> - </Modal> - ); -} \ No newline at end of file diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgMembersSection/index.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgMembersSection/index.tsx deleted file mode 100644 index e69de29bb..000000000