diff --git a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx index ae879e88c..2533b54d0 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx @@ -1,6 +1,13 @@ -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useMemo } from "react"; import { useRouter } from "next/router"; -import { faEllipsis, faMagnifyingGlass, faUsers } from "@fortawesome/free-solid-svg-icons"; +import { + faArrowDown, + faArrowUp, + faEllipsis, + faMagnifyingGlass, + faSearch, + faUsers +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; @@ -14,7 +21,9 @@ import { DropdownMenuItem, DropdownMenuTrigger, EmptyState, + IconButton, Input, + Pagination, Select, SelectItem, Table, @@ -33,6 +42,7 @@ import { useSubscription, useUser } from "@app/context"; +import { usePagination, useResetPageHelper } from "@app/hooks"; import { useAddUsersToOrg, useFetchServerStatus, @@ -40,6 +50,7 @@ import { useGetOrgUsers, useUpdateOrgMembership } from "@app/hooks/api"; +import { OrderByDirection } from "@app/hooks/api/generic/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; type Props = { @@ -54,6 +65,11 @@ type Props = { setCompleteInviteLinks: (links: Array<{ email: string; link: string }> | null) => void; }; +enum OrgMembersOrderBy { + Name = "firstName", + Email = "email" +} + export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Props) => { const router = useRouter(); const { subscription } = useSubscription(); @@ -64,10 +80,8 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro const { data: roles, isLoading: isRolesLoading } = useGetOrgRoles(orgId); - const [searchMemberFilter, setSearchMemberFilter] = useState(""); - const { data: serverDetails } = useFetchServerStatus(); - const { data: members, isLoading: isMembersLoading } = useGetOrgUsers(orgId); + const { data: members = [], isLoading: isMembersLoading } = useGetOrgUsers(orgId); const { mutateAsync: addUsersMutateAsync } = useAddUsersToOrg(); const { mutateAsync: updateOrgMembership } = useUpdateOrgMembership(); @@ -144,24 +158,75 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro [roles] ); - const filterdUser = useMemo( + const { + search, + setSearch, + setPage, + page, + perPage, + setPerPage, + offset, + orderDirection, + orderBy, + setOrderBy, + setOrderDirection, + toggleOrderDirection + } = usePagination(OrgMembersOrderBy.Name); + + const filteredUsers = 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] + members + ?.filter( + ({ user: u, inviteEmail }) => + u?.firstName?.toLowerCase().includes(search.toLowerCase()) || + u?.lastName?.toLowerCase().includes(search.toLowerCase()) || + u?.username?.toLowerCase().includes(search.toLowerCase()) || + u?.email?.toLowerCase().includes(search.toLowerCase()) || + inviteEmail?.toLowerCase().includes(search.toLowerCase()) + ) + .sort((a, b) => { + const [memberOne, memberTwo] = orderDirection === OrderByDirection.ASC ? [a, b] : [b, a]; + + let valueOne: string; + let valueTwo: string; + + switch (orderBy) { + case OrgMembersOrderBy.Email: + valueOne = memberOne.user.email || memberOne.inviteEmail; + valueTwo = memberTwo.user.email || memberTwo.inviteEmail; + break; + case OrgMembersOrderBy.Name: + default: + valueOne = memberOne.user.firstName; + valueTwo = memberTwo.user.firstName; + } + + return valueOne.toLowerCase().localeCompare(valueTwo.toLowerCase()); + }), + [members, search, orderDirection, orderBy] ); + const handleSort = (column: OrgMembersOrderBy) => { + if (column === orderBy) { + toggleOrderDirection(); + return; + } + + setOrderBy(column); + setOrderDirection(OrderByDirection.ASC); + }; + + useResetPageHelper({ + totalCount: filteredUsers.length, + offset, + setPage + }); + return (
setSearchMemberFilter(e.target.value)} + value={search} + onChange={(e) => setSearch(e.target.value)} leftIcon={} placeholder="Search members..." /> @@ -169,8 +234,46 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro - - + + @@ -178,212 +281,231 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro {isLoading && } {!isLoading && - filterdUser?.map( - ({ user: u, inviteEmail, role, roleId, id: orgMembershipId, status, isActive }) => { - const name = u && u.firstName ? `${u.firstName} ${u.lastName}` : "-"; - const email = u?.email || inviteEmail; - const username = u?.username ?? inviteEmail ?? "-"; - return ( - router.push(`/org/${orgId}/memberships/${orgMembershipId}`)} - > - - - router.push(`/org/${orgId}/memberships/${orgMembershipId}`)} + > + + + - + - - ); - } - )} + if (currentOrg?.scimEnabled) { + createNotification({ + text: "You cannot manage users from Infisical when org-level auth is enforced for your organization", + type: "error" + }); + return; + } + + if (!isActive) { + // activate user + await updateOrgMembership({ + organizationId: orgId, + membershipId: orgMembershipId, + isActive: true + }); + + return; + } + + // deactivate user + handlePopUpOpen("deactivateMember", { + orgMembershipId, + username + }); + }} + disabled={!isAllowed} + > + {`${isActive ? "Deactivate" : "Activate"} User`} + + )} + + + {(isAllowed) => ( + { + e.stopPropagation(); + + if (currentOrg?.scimEnabled) { + createNotification({ + text: "You cannot manage users from Infisical when org-level auth is enforced for your organization", + type: "error" + }); + return; + } + + handlePopUpOpen("removeMember", { + orgMembershipId, + username + }); + }} + disabled={!isAllowed} + > + Remove User + + )} + + + + )} + + + ); + } + )}
NameUsername +
+ Name + handleSort(OrgMembersOrderBy.Name)} + > + + +
+
+
+ Email + handleSort(OrgMembersOrderBy.Email)} + > + + +
+
Role
- {name} - {u.superAdmin && ( - - Server Admin - - )} - {username} - - {(isAllowed) => ( - <> - {!isActive && ( - - )} - {isActive && status === "accepted" && ( - - )} - {isActive && - (status === "invited" || status === "verified") && - email && - serverDetails?.emailConfigured && ( + filteredUsers + .slice(offset, perPage * page) + .map( + ({ + user: u, + inviteEmail, + role, + roleId, + id: orgMembershipId, + status, + isActive + }) => { + const name = u && u.firstName ? `${u.firstName} ${u.lastName}` : "-"; + const email = u?.email || inviteEmail; + const username = u?.username ?? inviteEmail ?? "-"; + return ( +
+ {name} + {u.superAdmin && ( + + Server Admin + + )} + {username} + + {(isAllowed) => ( + <> + {!isActive && ( )} - - )} - - - {userId !== u?.id && ( - - -
- -
-
- - - {(isAllowed) => ( - { - e.stopPropagation(); - router.push(`/org/${orgId}/memberships/${orgMembershipId}`); - }} - disabled={!isAllowed} - > - Edit User - - )} - - - {(isAllowed) => ( - + onRoleChange(orgMembershipId, selectedRole) } - onClick={async (e) => { - e.stopPropagation(); - - if (currentOrg?.scimEnabled) { - createNotification({ - text: "You cannot manage users from Infisical when org-level auth is enforced for your organization", - type: "error" - }); - return; - } - - if (!isActive) { - // activate user - await updateOrgMembership({ - organizationId: orgId, - membershipId: orgMembershipId, - isActive: true - }); - - return; - } - - // deactivate user - handlePopUpOpen("deactivateMember", { - orgMembershipId, - username - }); - }} - disabled={!isAllowed} > - {`${isActive ? "Deactivate" : "Activate"} User`} - + {(roles || []) + .filter(({ slug }) => + slug === "owner" ? isIamOwner || role === "owner" : true + ) + .map(({ slug, name: roleName }) => ( + + {roleName} + + ))} + )} - - - {(isAllowed) => ( - { - e.stopPropagation(); - - if (currentOrg?.scimEnabled) { - createNotification({ - text: "You cannot manage users from Infisical when org-level auth is enforced for your organization", - type: "error" - }); - return; + {isActive && + (status === "invited" || status === "verified") && + email && + serverDetails?.emailConfigured && ( + + )} + + )} + +
+ {userId !== u?.id && ( + + +
+ +
+
+ + + {(isAllowed) => ( + { + e.stopPropagation(); + router.push(`/org/${orgId}/memberships/${orgMembershipId}`); + }} + disabled={!isAllowed} + > + Edit User + + )} + + + {(isAllowed) => ( + { + e.stopPropagation(); - handlePopUpOpen("removeMember", { - orgMembershipId, - username - }); - }} - disabled={!isAllowed} - > - Remove User - - )} - - -
- )} -
- {!isLoading && filterdUser?.length === 0 && ( + {Boolean(filteredUsers.length) && ( + + )} + {!isMembersLoading && !filteredUsers?.length && ( )}