diff --git a/frontend/src/views/Org/IdentityPage/components/IdentityProjectsSection/IdentityProjectRow.tsx b/frontend/src/views/Org/IdentityPage/components/IdentityProjectsSection/IdentityProjectRow.tsx index bf428f453..e5ec47f6c 100644 --- a/frontend/src/views/Org/IdentityPage/components/IdentityProjectsSection/IdentityProjectRow.tsx +++ b/frontend/src/views/Org/IdentityPage/components/IdentityProjectsSection/IdentityProjectRow.tsx @@ -71,6 +71,7 @@ export const IdentityProjectRow = ({
Organization Access Control

- People - Groups + Users

Machine Identities

@@ -37,9 +30,6 @@ export const MembersPage = withPermission( - - - diff --git a/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsSection.tsx b/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsSection.tsx index 320c115fd..2df78fe5f 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsSection.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsSection.tsx @@ -3,16 +3,8 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; -import { - Button, - DeleteActionModal, - UpgradePlanModal -} from "@app/components/v2"; -import { - OrgPermissionActions, - OrgPermissionSubjects, - useSubscription -} from "@app/context"; +import { Button, DeleteActionModal, UpgradePlanModal } from "@app/components/v2"; +import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context"; import { useDeleteGroup } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; @@ -21,100 +13,88 @@ import { OrgGroupModal } from "./OrgGroupModal"; import { OrgGroupsTable } from "./OrgGroupsTable"; export const OrgGroupsSection = () => { - const { subscription } = useSubscription(); - const { mutateAsync: deleteMutateAsync } = useDeleteGroup(); - - const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ - "group", - "groupMembers", - "deleteGroup", - "upgradePlan" - ] as const); - - const handleAddGroupModal = () => { - if (!subscription?.groups) { - handlePopUpOpen("upgradePlan", { - description: "You can manage users more efficiently with groups if you upgrade your Infisical plan." - }); - } else { - handlePopUpOpen("group"); - } + const { subscription } = useSubscription(); + const { mutateAsync: deleteMutateAsync } = useDeleteGroup(); + + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + "group", + "groupMembers", + "deleteGroup", + "upgradePlan" + ] as const); + + const handleAddGroupModal = () => { + if (!subscription?.groups) { + handlePopUpOpen("upgradePlan", { + description: + "You can manage users more efficiently with groups if you upgrade your Infisical plan." + }); + } else { + handlePopUpOpen("group"); } - - const onDeleteGroupSubmit = async ({ - name, + }; + + const onDeleteGroupSubmit = async ({ name, slug }: { name: string; slug: string }) => { + try { + await deleteMutateAsync({ slug - }: { - name: string; - slug: string; - }) => { - try { - await deleteMutateAsync({ - slug - }); - createNotification({ - text: `Successfully deleted the group named ${name}`, - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: `Failed to delete the group named ${name}`, - type: "error" - }); - } - - handlePopUpClose("deleteGroup"); + }); + createNotification({ + text: `Successfully deleted the group named ${name}`, + type: "success" + }); + } catch (err) { + console.error(err); + createNotification({ + text: `Failed to delete the group named ${name}`, + type: "error" + }); } - - return ( -
-
-

Groups

- - {(isAllowed) => ( - - )} - -
- - - - handlePopUpToggle("deleteGroup", isOpen)} - deleteKey="confirm" - onDeleteApproved={() => - onDeleteGroupSubmit( - (popUp?.deleteGroup?.data as { name: string; slug: string }) - ) - } - /> - handlePopUpToggle("upgradePlan", isOpen)} - text={(popUp.upgradePlan?.data as { description: string })?.description} - /> -
- ); -} \ No newline at end of file + + handlePopUpClose("deleteGroup"); + }; + + return ( +
+
+

User Groups

+ + {(isAllowed) => ( + + )} + +
+ + + + handlePopUpToggle("deleteGroup", isOpen)} + deleteKey="confirm" + onDeleteApproved={() => + onDeleteGroupSubmit(popUp?.deleteGroup?.data as { name: string; slug: string }) + } + /> + handlePopUpToggle("upgradePlan", isOpen)} + text={(popUp.upgradePlan?.data as { description: string })?.description} + /> +
+ ); +}; diff --git a/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsTable.tsx b/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsTable.tsx index 79e08bc14..336022189 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsTable.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsTable.tsx @@ -1,12 +1,16 @@ import { useState } from "react"; -import { faMagnifyingGlass, faPencil, faUsers, faXmark } from "@fortawesome/free-solid-svg-icons"; +import { faEllipsis,faMagnifyingGlass, faUsers } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, EmptyState, - IconButton, Input, Select, SelectItem, @@ -17,218 +21,200 @@ import { Td, Th, THead, - Tooltip, Tr } from "@app/components/v2"; -import { - OrgPermissionActions, - OrgPermissionSubjects, - useOrganization} from "@app/context"; -import { - useGetOrganizationGroups, - useGetOrgRoles, - useUpdateGroup -} from "@app/hooks/api"; +import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context"; +import { useGetOrganizationGroups, useGetOrgRoles, useUpdateGroup } from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; type Props = { - handlePopUpOpen: ( - popUpName: keyof UsePopUpState< - ["group", "deleteGroup", "groupMembers"] - >, - data?: { - groupId?: string; - name?: string; - slug?: string; - role?: string; - customRole?: { - name: string; - slug: string; - } - } - ) => void; - }; + handlePopUpOpen: ( + popUpName: keyof UsePopUpState<["group", "deleteGroup", "groupMembers"]>, + data?: { + groupId?: string; + name?: string; + slug?: string; + role?: string; + customRole?: { + name: string; + slug: string; + }; + } + ) => void; +}; -export const OrgGroupsTable = ({ - handlePopUpOpen -}: Props) => { - const [searchGroupsFilter, setSearchGroupsFilter] = useState(""); - const { currentOrg } = useOrganization(); - const orgId = currentOrg?.id || ""; - const { isLoading, data: groups } = useGetOrganizationGroups(orgId); - const { mutateAsync: updateMutateAsync } = useUpdateGroup(); - - const { data: roles } = useGetOrgRoles(orgId); - - const handleChangeRole = async ({ +export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => { + const [searchGroupsFilter, setSearchGroupsFilter] = useState(""); + const { currentOrg } = useOrganization(); + const orgId = currentOrg?.id || ""; + const { isLoading, data: groups } = useGetOrganizationGroups(orgId); + const { mutateAsync: updateMutateAsync } = useUpdateGroup(); + + const { data: roles } = useGetOrgRoles(orgId); + + const handleChangeRole = async ({ currentSlug, role }: { currentSlug: string; role: string }) => { + try { + await updateMutateAsync({ currentSlug, role - }: { - currentSlug: string; - role: string; - }) => { - try { - await updateMutateAsync({ - currentSlug, - role - }); - - createNotification({ - text: "Successfully updated group role", - type: "success" - }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to update group role", - type: "error" - }); - } + }); + + createNotification({ + text: "Successfully updated group role", + type: "success" + }); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to update group role", + type: "error" + }); } - - return ( -
- setSearchGroupsFilter(e.target.value)} - leftIcon={} - placeholder="Search groups..." - /> - - - - - - - - - - - {isLoading && } - {!isLoading && groups?.map(({ id, name, slug, role, customRole }) => { - return ( - - - - - - - ); - })} - -
NameSlugRole -
{name}{slug} - - {(isAllowed) => { - return ( - - ); - }} - - -
- - {(isAllowed) => ( - - { - handlePopUpOpen("groupMembers", { - slug - }); - }} - size="lg" - colorSchema="primary" - variant="plain" - ariaLabel="update" - isDisabled={!isAllowed} - > - - - - )} - - - {(isAllowed) => ( - - { - handlePopUpOpen("group", { - groupId: id, - name, - slug, - role, - customRole - }); - }} - size="lg" - colorSchema="primary" - variant="plain" - ariaLabel="update" - className="ml-4" - isDisabled={!isAllowed} - > - - - - )} - - - {(isAllowed) => ( - - { - handlePopUpOpen("deleteGroup", { - slug, - name - }); - }} - size="lg" - colorSchema="danger" - variant="plain" - ariaLabel="update" - className="ml-4" - isDisabled={!isAllowed} - > - - - - )} - -
-
- {groups?.length === 0 && ( - - )} -
-
- ); -} + }; + + return ( +
+ setSearchGroupsFilter(e.target.value)} + leftIcon={} + placeholder="Search groups..." + /> + + + + + + + + + + + {isLoading && } + {!isLoading && + groups?.map(({ id, name, slug, role, customRole }) => { + return ( + + + + + + + ); + })} + +
NameSlugRole +
{name}{slug} + + {(isAllowed) => { + return ( + + ); + }} + + + + +
+ +
+
+ + + {(isAllowed) => ( + { + e.stopPropagation(); + handlePopUpOpen("groupMembers", { + slug + }); + }} + disabled={!isAllowed} + > + Manage Users + + )} + + + {(isAllowed) => ( + { + e.stopPropagation(); + handlePopUpOpen("group", { + groupId: id, + name, + slug, + role, + customRole + }); + }} + disabled={!isAllowed} + > + Edit Group + + )} + + + {(isAllowed) => ( + { + e.stopPropagation(); + handlePopUpOpen("deleteGroup", { + slug, + name + }); + }} + disabled={!isAllowed} + > + Delete Group + + )} + + +
+
+ {groups?.length === 0 && } +
+
+ ); +}; diff --git a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/OrgMembersTab.tsx b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/OrgMembersTab.tsx index 5d50431b7..36230c971 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/OrgMembersTab.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/OrgMembersTab.tsx @@ -1,17 +1,19 @@ import { motion } from "framer-motion"; +import { OrgGroupsSection } from "../OrgGroupsTab/components"; import { OrgMembersSection } from "./components"; export const OrgMembersTab = () => { return ( - + + ); -} +}; diff --git a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx index 8943f2940..f9dc72f4e 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx @@ -90,7 +90,7 @@ export const OrgMembersSection = () => { return (
-

Members

+

Users

{(isAllowed) => ( + )} + +
+ + + handlePopUpToggle("deleteGroup", isOpen)} + deleteKey="confirm" + onDeleteApproved={() => + onRemoveGroupSubmit((popUp?.deleteGroup?.data as { slug: string })?.slug) } - }; - - return ( -
-
-

Groups

- - {(isAllowed) => ( - - )} - -
- - - handlePopUpToggle("deleteGroup", isOpen)} - deleteKey="confirm" - onDeleteApproved={() => - onRemoveGroupSubmit( - (popUp?.deleteGroup?.data as { slug: string })?.slug - ) - } - /> - handlePopUpToggle("upgradePlan", isOpen)} - text={(popUp.upgradePlan?.data as { description: string })?.description} - /> -
- ); -} \ No newline at end of file + /> + handlePopUpToggle("upgradePlan", isOpen)} + text={(popUp.upgradePlan?.data as { description: string })?.description} + /> +
+ ); +}; diff --git a/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupsTable.tsx b/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupsTable.tsx index 5906accab..6b8e7a91b 100644 --- a/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupsTable.tsx +++ b/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupsTable.tsx @@ -1,4 +1,4 @@ -import { faServer, faXmark } from "@fortawesome/free-solid-svg-icons"; +import { faServer, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { format } from "date-fns"; @@ -13,6 +13,7 @@ import { Td, Th, THead, + Tooltip, Tr } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; @@ -36,68 +37,71 @@ export const GroupTable = ({ handlePopUpOpen }: Props) => { const { data, isLoading } = useListWorkspaceGroups(currentWorkspace?.slug || ""); return ( - - - - - - - - - - {isLoading && } - {!isLoading && - data && - data.length > 0 && - data.map(({ group: { id, name, slug }, roles, createdAt }) => { - return ( - - - + + + + + + + + {isLoading && } + {!isLoading && + data && + data.length > 0 && + data.map(({ group: { id, name, slug }, roles, createdAt }) => { + return ( + + + + + - - - - ); - })} - -
NameRoleAdded on -
{name} - +
NameRoleAdded on +
{name} + + {(isAllowed) => ( + + )} + + {format(new Date(createdAt), "yyyy-MM-dd")} + + {(isAllowed) => ( +
+ + { + handlePopUpOpen("deleteGroup", { + slug, + name + }); + }} + colorSchema="danger" + variant="plain" + ariaLabel="update" + className="ml-4" + isDisabled={!isAllowed} > - {(isAllowed) => ( - - )} - -
{format(new Date(createdAt), "yyyy-MM-dd")} - - {(isAllowed) => ( - { - handlePopUpOpen("deleteGroup", { - slug, - name - }); - }} - size="lg" - colorSchema="danger" - variant="plain" - ariaLabel="update" - className="ml-4" - isDisabled={!isAllowed} - > - - - )} - -
- {!isLoading && data?.length === 0 && ( - - )} + + + +
+ )} + + + + ); + })} + + + {!isLoading && data?.length === 0 && ( + + )} ); }; diff --git a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberListTab.tsx b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberListTab.tsx deleted file mode 100644 index b59286577..000000000 --- a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberListTab.tsx +++ /dev/null @@ -1,505 +0,0 @@ -import { useMemo, useState } from "react"; -import { Controller, useForm } from "react-hook-form"; -import { useTranslation } from "react-i18next"; -import Link from "next/link"; -import { - faClock, - faEdit, - faMagnifyingGlass, - faPlus, - faUsers, - faXmark -} from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { motion } from "framer-motion"; -import { twMerge } from "tailwind-merge"; -import { z } from "zod"; - -import { createNotification } from "@app/components/notifications"; -import { ProjectPermissionCan } from "@app/components/permissions"; -import { - Button, - DeleteActionModal, - EmptyState, - FormControl, - HoverCard, - HoverCardContent, - HoverCardTrigger, - IconButton, - Input, - Modal, - ModalContent, - Select, - SelectItem, - Table, - TableContainer, - TableSkeleton, - Tag, - TBody, - Td, - Th, - THead, - Tooltip, - Tr, - UpgradePlanModal -} from "@app/components/v2"; -import { - ProjectPermissionActions, - ProjectPermissionSub, - useOrganization, - useUser, - useWorkspace -} from "@app/context"; -import { usePopUp } from "@app/hooks"; -import { - useAddUserToWsE2EE, - useAddUserToWsNonE2EE, - useDeleteUserFromWorkspace, - useGetOrgUsers, - useGetUserWsKey, - useGetWorkspaceUsers -} from "@app/hooks/api"; -import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; -import { TWorkspaceUser } from "@app/hooks/api/types"; -import { ProjectVersion } from "@app/hooks/api/workspace/types"; - -import { MemberRoleForm } from "./MemberRoleForm"; - -const addMemberFormSchema = z.object({ - orgMembershipId: z.string().trim() -}); - -type TAddMemberForm = z.infer; - -const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2; -const formatRoleName = (role: string, customRoleName?: string) => { - if (role === ProjectMembershipRole.Custom) return customRoleName; - if (role === ProjectMembershipRole.Member) return "Developer"; - if (role === ProjectMembershipRole.NoAccess) return "No access"; - return role; -}; - -export const MemberListTab = () => { - const { t } = useTranslation(); - - const { currentOrg } = useOrganization(); - const { currentWorkspace } = useWorkspace(); - const { user } = useUser(); - - const userId = user?.id || ""; - const orgId = currentOrg?.id || ""; - const workspaceId = currentWorkspace?.id || ""; - - const { data: wsKey } = useGetUserWsKey(workspaceId); - const { data: members, isLoading: isMembersLoading } = useGetWorkspaceUsers(workspaceId); - const { data: orgUsers } = useGetOrgUsers(orgId); - - const [searchMemberFilter, setSearchMemberFilter] = useState(""); - - const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ - "addMember", - "removeMember", - "upgradePlan", - "updateRole" - ] as const); - - const { - control, - handleSubmit, - reset, - formState: { isSubmitting } - } = useForm({ resolver: zodResolver(addMemberFormSchema) }); - - const { mutateAsync: addUserToWorkspace } = useAddUserToWsE2EE(); - const { mutateAsync: addUserToWorkspaceNonE2EE } = useAddUserToWsNonE2EE(); - const { mutateAsync: removeUserFromWorkspace } = useDeleteUserFromWorkspace(); - - const onAddMember = async ({ orgMembershipId }: TAddMemberForm) => { - if (!currentWorkspace) return; - if (!currentOrg?.id) return; - // TODO(akhilmhdh): Move to memory storage - const userPrivateKey = localStorage.getItem("PRIVATE_KEY"); - if (!userPrivateKey || !wsKey) { - createNotification({ - text: "Failed to find private key. Try re-login" - }); - return; - } - const orgUser = (orgUsers || []).find(({ id }) => id === orgMembershipId); - if (!orgUser) return; - - try { - // TODO: update - if (currentWorkspace.version === ProjectVersion.V1) { - await addUserToWorkspace({ - workspaceId, - userPrivateKey, - decryptKey: wsKey, - members: [{ orgMembershipId, userPublicKey: orgUser.user.publicKey }] - }); - } else if (currentWorkspace.version === ProjectVersion.V2) { - await addUserToWorkspaceNonE2EE({ - projectId: workspaceId, - usernames: [orgUser.user.username] - }); - } else { - createNotification({ - text: "Failed to add user to project, unknown project type", - type: "error" - }); - - return; - } - createNotification({ - text: "Successfully added user to the project", - type: "success" - }); - } catch (error) { - console.error(error); - createNotification({ - text: "Failed to add user to project", - type: "error" - }); - } - handlePopUpClose("addMember"); - reset(); - }; - - const handleRemoveUser = async () => { - const username = (popUp?.removeMember?.data as { username: string })?.username; - if (!currentOrg?.id) return; - - try { - await removeUserFromWorkspace({ workspaceId, usernames: [username] }); - createNotification({ - text: "Successfully removed user from project", - type: "success" - }); - } catch (error) { - console.error(error); - createNotification({ - text: "Failed to remove user from the project", - type: "error" - }); - } - handlePopUpClose("removeMember"); - }; - - const filterdUsers = useMemo( - () => - members?.filter( - ({ user: u, inviteEmail }) => - u?.firstName?.toLowerCase().includes(searchMemberFilter.toLowerCase()) || - u?.lastName?.toLowerCase().includes(searchMemberFilter.toLowerCase()) || - u?.username?.toLowerCase().includes(searchMemberFilter.toLowerCase()) || - u?.email?.toLowerCase().includes(searchMemberFilter.toLowerCase()) || - inviteEmail?.includes(searchMemberFilter.toLowerCase()) - ), - [members, searchMemberFilter] - ); - - const filteredOrgUsers = useMemo(() => { - const wsUserUsernames = new Map(); - members?.forEach((member) => { - wsUserUsernames.set(member.user.username, true); - }); - return (orgUsers || []).filter( - ({ status, user: u }) => status === "accepted" && !wsUserUsernames.has(u.username) - ); - }, [orgUsers, members]); - - return ( - -
-

Members

- - {(isAllowed) => ( - - )} - -
- setSearchMemberFilter(e.target.value)} - leftIcon={} - placeholder="Search members..." - /> -
- - - - - - - - - - - {isMembersLoading && } - {!isMembersLoading && - filterdUsers?.map((projectMember, index) => { - const { user: u, inviteEmail, id: membershipId, roles } = projectMember; - const name = u ? `${u.firstName} ${u.lastName}` : "-"; - const email = u?.email || inviteEmail; - - return ( - - - - - - - ); - })} - -
NameUsernameRole -
{name}{email} -
- {roles - .slice(0, MAX_ROLES_TO_BE_SHOWN_IN_TABLE) - .map( - ({ - role, - customRoleName, - id, - isTemporary, - temporaryAccessEndTime - }) => { - const isExpired = - new Date() > new Date(temporaryAccessEndTime || ("" as string)); - return ( - -
-
- {formatRoleName(role, customRoleName)} -
- {isTemporary && ( -
- - - -
- )} -
-
- ); - } - )} - {roles.length > MAX_ROLES_TO_BE_SHOWN_IN_TABLE && ( - - - +{roles.length - MAX_ROLES_TO_BE_SHOWN_IN_TABLE} - - - {roles - .slice(MAX_ROLES_TO_BE_SHOWN_IN_TABLE) - .map( - ({ - role, - customRoleName, - id, - isTemporary, - temporaryAccessEndTime - }) => { - const isExpired = - new Date() > - new Date(temporaryAccessEndTime || ("" as string)); - return ( - -
-
{formatRoleName(role, customRoleName)}
- {isTemporary && ( -
- - - new Date( - temporaryAccessEndTime as string - ) && "text-red-600" - )} - /> - -
- )} -
-
- ); - } - )} -
-
- )} - {userId !== u?.id && ( - - - handlePopUpOpen("updateRole", { ...projectMember, index }) - } - > - - - - )} -
-
- {userId !== u?.id && ( -
- - {(isAllowed) => ( - - handlePopUpOpen("removeMember", { username: u.username }) - } - > - - - )} - -
- )} -
- {!isMembersLoading && filterdUsers?.length === 0 && ( - - )} -
-
- handlePopUpToggle("addMember", isOpen)} - > - - {filteredOrgUsers.length ? ( -
- ( - - - - )} - /> -
- - -
- - ) : ( -
-
All the users in your organization are already invited.
- - - -
- )} -
-
- handlePopUpToggle("updateRole", state)} - > - - handlePopUpOpen("upgradePlan", { description })} - projectMember={ - filterdUsers?.[ - (popUp.updateRole?.data as TWorkspaceUser & { index: number })?.index - ] as TWorkspaceUser - } - /> - - - handlePopUpToggle("removeMember", isOpen)} - onDeleteApproved={handleRemoveUser} - /> - handlePopUpToggle("upgradePlan", isOpen)} - text={(popUp.upgradePlan?.data as { description: string })?.description} - /> -
- ); -}; diff --git a/frontend/src/views/Project/MembersPage/components/MemberListTab/index.tsx b/frontend/src/views/Project/MembersPage/components/MemberListTab/index.tsx deleted file mode 100644 index 695e806c5..000000000 --- a/frontend/src/views/Project/MembersPage/components/MemberListTab/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { MemberListTab } from "./MemberListTab"; diff --git a/frontend/src/views/Project/MembersPage/components/MembersTab/MembersTab.tsx b/frontend/src/views/Project/MembersPage/components/MembersTab/MembersTab.tsx new file mode 100644 index 000000000..25ffc38da --- /dev/null +++ b/frontend/src/views/Project/MembersPage/components/MembersTab/MembersTab.tsx @@ -0,0 +1,22 @@ +import { motion } from "framer-motion"; + +import { useWorkspace } from "@app/context"; + +import { GroupsSection } from "../GroupsTab/components"; +import { MembersSection } from "./components"; + +export const MembersTab = () => { + const { currentWorkspace } = useWorkspace(); + return ( + + + {currentWorkspace?.version && currentWorkspace.version > 1 && } + + ); +}; diff --git a/frontend/src/views/Project/MembersPage/components/MembersTab/components/AddMemberModal.tsx b/frontend/src/views/Project/MembersPage/components/MembersTab/components/AddMemberModal.tsx new file mode 100644 index 000000000..df76520c2 --- /dev/null +++ b/frontend/src/views/Project/MembersPage/components/MembersTab/components/AddMemberModal.tsx @@ -0,0 +1,177 @@ +import { useMemo } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import Link from "next/link"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { Button,FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2"; +import { useOrganization, useWorkspace } from "@app/context"; +import { + useAddUserToWsE2EE, + useAddUserToWsNonE2EE, + useGetOrgUsers, + useGetUserWsKey, + useGetWorkspaceUsers} from "@app/hooks/api"; +import { ProjectVersion } from "@app/hooks/api/workspace/types"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +const addMemberFormSchema = z.object({ + orgMembershipId: z.string().trim() +}); + +type TAddMemberForm = z.infer; + +type Props = { + popUp: UsePopUpState<["addMember"]>; + handlePopUpToggle: (popUpName: keyof UsePopUpState<["addMember"]>, state?: boolean) => void; +}; + +export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => { + const { t } = useTranslation(); + const { currentOrg } = useOrganization(); + const { currentWorkspace } = useWorkspace(); + + const orgId = currentOrg?.id || ""; + const workspaceId = currentWorkspace?.id || ""; + + const { data: wsKey } = useGetUserWsKey(workspaceId); + const { data: members } = useGetWorkspaceUsers(workspaceId); + const { data: orgUsers } = useGetOrgUsers(orgId); + + const { mutateAsync: addUserToWorkspace } = useAddUserToWsE2EE(); + const { mutateAsync: addUserToWorkspaceNonE2EE } = useAddUserToWsNonE2EE(); + + const { + control, + handleSubmit, + reset, + formState: { isSubmitting } + } = useForm({ resolver: zodResolver(addMemberFormSchema) }); + + const onAddMember = async ({ orgMembershipId }: TAddMemberForm) => { + if (!currentWorkspace) return; + if (!currentOrg?.id) return; + // TODO(akhilmhdh): Move to memory storage + const userPrivateKey = localStorage.getItem("PRIVATE_KEY"); + if (!userPrivateKey || !wsKey) { + createNotification({ + text: "Failed to find private key. Try re-login" + }); + return; + } + const orgUser = (orgUsers || []).find(({ id }) => id === orgMembershipId); + if (!orgUser) return; + + try { + // TODO: update + if (currentWorkspace.version === ProjectVersion.V1) { + await addUserToWorkspace({ + workspaceId, + userPrivateKey, + decryptKey: wsKey, + members: [{ orgMembershipId, userPublicKey: orgUser.user.publicKey }] + }); + } else if (currentWorkspace.version === ProjectVersion.V2) { + await addUserToWorkspaceNonE2EE({ + projectId: workspaceId, + usernames: [orgUser.user.username] + }); + } else { + createNotification({ + text: "Failed to add user to project, unknown project type", + type: "error" + }); + + return; + } + createNotification({ + text: "Successfully added user to the project", + type: "success" + }); + } catch (error) { + console.error(error); + createNotification({ + text: "Failed to add user to project", + type: "error" + }); + } + handlePopUpToggle("addMember", false); + reset(); + }; + + const filteredOrgUsers = useMemo(() => { + const wsUserUsernames = new Map(); + members?.forEach((member) => { + wsUserUsernames.set(member.user.username, true); + }); + return (orgUsers || []).filter( + ({ status, user: u }) => status === "accepted" && !wsUserUsernames.has(u.username) + ); + }, [orgUsers, members]); + + return ( + handlePopUpToggle("addMember", isOpen)} + > + + {filteredOrgUsers.length ? ( +
+ ( + + + + )} + /> +
+ + +
+ + ) : ( +
+
All the users in your organization are already invited.
+ + + +
+ )} +
+
+ ); +}; diff --git a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/MemberRbacSection.tsx b/frontend/src/views/Project/MembersPage/components/MembersTab/components/MemberRoleForm/MemberRbacSection.tsx similarity index 100% rename from frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/MemberRbacSection.tsx rename to frontend/src/views/Project/MembersPage/components/MembersTab/components/MemberRoleForm/MemberRbacSection.tsx diff --git a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/MemberRoleForm.tsx b/frontend/src/views/Project/MembersPage/components/MembersTab/components/MemberRoleForm/MemberRoleForm.tsx similarity index 100% rename from frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/MemberRoleForm.tsx rename to frontend/src/views/Project/MembersPage/components/MembersTab/components/MemberRoleForm/MemberRoleForm.tsx diff --git a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx b/frontend/src/views/Project/MembersPage/components/MembersTab/components/MemberRoleForm/SpecificPrivilegeSection.tsx similarity index 100% rename from frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx rename to frontend/src/views/Project/MembersPage/components/MembersTab/components/MemberRoleForm/SpecificPrivilegeSection.tsx diff --git a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/index.tsx b/frontend/src/views/Project/MembersPage/components/MembersTab/components/MemberRoleForm/index.tsx similarity index 100% rename from frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/index.tsx rename to frontend/src/views/Project/MembersPage/components/MembersTab/components/MemberRoleForm/index.tsx diff --git a/frontend/src/views/Project/MembersPage/components/MembersTab/components/MembersSection.tsx b/frontend/src/views/Project/MembersPage/components/MembersTab/components/MembersSection.tsx new file mode 100644 index 000000000..b4c230e19 --- /dev/null +++ b/frontend/src/views/Project/MembersPage/components/MembersTab/components/MembersSection.tsx @@ -0,0 +1,86 @@ +import { faPlus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { createNotification } from "@app/components/notifications"; +import { ProjectPermissionCan } from "@app/components/permissions"; +import { Button, DeleteActionModal, UpgradePlanModal } from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub , useOrganization, useWorkspace } from "@app/context"; +import { usePopUp } from "@app/hooks"; +import { useDeleteUserFromWorkspace } from "@app/hooks/api"; + +import { AddMemberModal } from "./AddMemberModal"; +import { MembersTable } from "./MembersTable"; + +export const MembersSection = () => { + const { currentOrg } = useOrganization(); + const { currentWorkspace } = useWorkspace(); + + const { mutateAsync: removeUserFromWorkspace } = useDeleteUserFromWorkspace(); + + const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ + "addMember", + "removeMember", + "upgradePlan", + "updateRole" + ] as const); + + const handleRemoveUser = async () => { + const username = (popUp?.removeMember?.data as { username: string })?.username; + if (!currentOrg?.id) return; + if (!currentWorkspace?.id) return; + + try { + await removeUserFromWorkspace({ workspaceId: currentWorkspace.id, usernames: [username] }); + createNotification({ + text: "Successfully removed user from project", + type: "success" + }); + } catch (error) { + console.error(error); + createNotification({ + text: "Failed to remove user from the project", + type: "error" + }); + } + handlePopUpClose("removeMember"); + }; + + return ( +
+
+

Users

+ + {(isAllowed) => ( + + )} + +
+ + + handlePopUpToggle("removeMember", isOpen)} + onDeleteApproved={handleRemoveUser} + /> + handlePopUpToggle("upgradePlan", isOpen)} + text={(popUp.upgradePlan?.data as { description: string })?.description} + /> +
+ ); +}; diff --git a/frontend/src/views/Project/MembersPage/components/MembersTab/components/MembersTable.tsx b/frontend/src/views/Project/MembersPage/components/MembersTab/components/MembersTable.tsx new file mode 100644 index 000000000..5aab83882 --- /dev/null +++ b/frontend/src/views/Project/MembersPage/components/MembersTab/components/MembersTable.tsx @@ -0,0 +1,269 @@ +import { useMemo,useState } from "react"; +import { + faClock, + faEdit, + faMagnifyingGlass, + faTrash, + faUsers} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { twMerge } from "tailwind-merge"; + +import { ProjectPermissionCan } from "@app/components/permissions"; +import { + EmptyState, + HoverCard, + HoverCardContent, + HoverCardTrigger, + IconButton, + Input, + Modal, + ModalContent, + Table, + TableContainer, + TableSkeleton, + Tag, + TBody, + Td, + Th, + THead, + Tooltip, + Tr} from "@app/components/v2"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + useUser, + useWorkspace} from "@app/context"; +import { useGetWorkspaceUsers } from "@app/hooks/api"; +import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; +import { TWorkspaceUser } from "@app/hooks/api/types"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +import { MemberRoleForm } from "./MemberRoleForm"; + +const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2; +const formatRoleName = (role: string, customRoleName?: string) => { + if (role === ProjectMembershipRole.Custom) return customRoleName; + if (role === ProjectMembershipRole.Member) return "Developer"; + if (role === ProjectMembershipRole.NoAccess) return "No access"; + return role; +}; + +type Props = { + popUp: UsePopUpState<["updateRole"]>; + handlePopUpOpen: ( + popUpName: keyof UsePopUpState<["removeMember", "updateRole", "upgradePlan"]>, + data?: {} + ) => void; + handlePopUpToggle: (popUpName: keyof UsePopUpState<["updateRole"]>, state?: boolean) => void; +}; + +export const MembersTable = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Props) => { + const [searchMemberFilter, setSearchMemberFilter] = useState(""); + + const { currentWorkspace } = useWorkspace(); + const { user } = useUser(); + + const userId = user?.id || ""; + const workspaceId = currentWorkspace?.id || ""; + + const { data: members, isLoading: isMembersLoading } = useGetWorkspaceUsers(workspaceId); + + const filterdUsers = useMemo( + () => + members?.filter( + ({ user: u, inviteEmail }) => + u?.firstName?.toLowerCase().includes(searchMemberFilter.toLowerCase()) || + u?.lastName?.toLowerCase().includes(searchMemberFilter.toLowerCase()) || + u?.username?.toLowerCase().includes(searchMemberFilter.toLowerCase()) || + u?.email?.toLowerCase().includes(searchMemberFilter.toLowerCase()) || + inviteEmail?.includes(searchMemberFilter.toLowerCase()) + ), + [members, searchMemberFilter] + ); + + return ( +
+ setSearchMemberFilter(e.target.value)} + leftIcon={} + placeholder="Search members..." + /> + + + + + + + + + + + {isMembersLoading && } + {!isMembersLoading && + filterdUsers?.map((projectMember, index) => { + const { user: u, inviteEmail, id: membershipId, roles } = projectMember; + const name = u ? `${u.firstName} ${u.lastName}` : "-"; + const email = u?.email || inviteEmail; + + return ( + + + + + + + ); + })} + +
NameUsernameRole +
{name}{email} +
+ {roles + .slice(0, MAX_ROLES_TO_BE_SHOWN_IN_TABLE) + .map( + ({ role, customRoleName, id, isTemporary, temporaryAccessEndTime }) => { + const isExpired = + new Date() > new Date(temporaryAccessEndTime || ("" as string)); + return ( + +
+
+ {formatRoleName(role, customRoleName)} +
+ {isTemporary && ( +
+ + + +
+ )} +
+
+ ); + } + )} + {roles.length > MAX_ROLES_TO_BE_SHOWN_IN_TABLE && ( + + + +{roles.length - MAX_ROLES_TO_BE_SHOWN_IN_TABLE} + + + {roles + .slice(MAX_ROLES_TO_BE_SHOWN_IN_TABLE) + .map( + ({ + role, + customRoleName, + id, + isTemporary, + temporaryAccessEndTime + }) => { + const isExpired = + new Date() > + new Date(temporaryAccessEndTime || ("" as string)); + return ( + +
+
{formatRoleName(role, customRoleName)}
+ {isTemporary && ( +
+ + + new Date(temporaryAccessEndTime as string) && + "text-red-600" + )} + /> + +
+ )} +
+
+ ); + } + )} +
+
+ )} + {userId !== u?.id && ( + + + handlePopUpOpen("updateRole", { ...projectMember, index }) + } + > + + + + )} +
+
+ {userId !== u?.id && ( +
+ + {(isAllowed) => ( + + handlePopUpOpen("removeMember", { username: u.username }) + } + > + + + )} + +
+ )} +
+ {!isMembersLoading && filterdUsers?.length === 0 && ( + + )} +
+ handlePopUpToggle("updateRole", state)} + > + + handlePopUpOpen("upgradePlan", { description })} + projectMember={ + filterdUsers?.[ + (popUp.updateRole?.data as TWorkspaceUser & { index: number })?.index + ] as TWorkspaceUser + } + /> + + +
+ ); +}; diff --git a/frontend/src/views/Project/MembersPage/components/MembersTab/components/index.tsx b/frontend/src/views/Project/MembersPage/components/MembersTab/components/index.tsx new file mode 100644 index 000000000..ccb1cc6cf --- /dev/null +++ b/frontend/src/views/Project/MembersPage/components/MembersTab/components/index.tsx @@ -0,0 +1 @@ +export { MembersSection } from "./MembersSection"; diff --git a/frontend/src/views/Project/MembersPage/components/MembersTab/index.tsx b/frontend/src/views/Project/MembersPage/components/MembersTab/index.tsx new file mode 100644 index 000000000..f557950c8 --- /dev/null +++ b/frontend/src/views/Project/MembersPage/components/MembersTab/index.tsx @@ -0,0 +1 @@ +export { MembersTab } from "./MembersTab"; diff --git a/frontend/src/views/Project/MembersPage/components/index.tsx b/frontend/src/views/Project/MembersPage/components/index.tsx index 153e904ac..edb14ec8f 100644 --- a/frontend/src/views/Project/MembersPage/components/index.tsx +++ b/frontend/src/views/Project/MembersPage/components/index.tsx @@ -1,5 +1,5 @@ export { GroupsTab } from "./GroupsTab"; export { IdentityTab } from "./IdentityTab"; -export { MemberListTab } from "./MemberListTab"; +export { MembersTab } from "./MembersTab"; export { ProjectRoleListTab } from "./ProjectRoleListTab"; export { ServiceTokenTab } from "./ServiceTokenTab"; diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/RequestAccessModal.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/RequestAccessModal.tsx index 735ca3b53..47f678478 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/RequestAccessModal.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/RequestAccessModal.tsx @@ -1,6 +1,6 @@ import { Modal, ModalContent } from "@app/components/v2"; import { TAccessApprovalPolicy } from "@app/hooks/api/types"; -import { SpecificPrivilegeSecretForm } from "@app/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection"; +import { SpecificPrivilegeSecretForm } from "@app/views/Project/MembersPage/components/MembersTab/components/MemberRoleForm/SpecificPrivilegeSection"; export const RequestAccessModal = ({ isOpen,