import { useMemo, useState } from "react"; import { faArrowDown, faArrowUp, faBuilding, faCircleQuestion, faEllipsisV, faEnvelope, faEye, faMagnifyingGlass, faPlus, faTrash, faUserCheck, faUserMinus, faUserPlus, faUsers, faUserXmark, faWarning } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useNavigate } from "@tanstack/react-router"; import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; import { Badge, Button, DeleteActionModal, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, EmptyState, IconButton, Input, Modal, ModalContent, Pagination, Table, TableContainer, TableSkeleton, TBody, Td, Th, THead, Tooltip, Tr } from "@app/components/v2"; import { useUser } from "@app/context"; import { OrgMembershipRole } from "@app/helpers/roles"; import { getUserTablePreference, PreferenceKey, setUserTablePreference } from "@app/helpers/userTablePreferences"; import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks"; import { useAdminDeleteOrganization, useAdminDeleteOrganizationMembership, useAdminDeleteUser, useAdminGetOrganizations, useServerAdminAccessOrg, useServerAdminResendOrgInvite } from "@app/hooks/api"; import { OrganizationWithProjects } from "@app/hooks/api/admin/types"; import { OrderByDirection } from "@app/hooks/api/generic/types"; import { OrgMembershipStatus } from "@app/hooks/api/organization/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; import { AddOrganizationModal } from "@app/pages/admin/ResourceOverviewPage/components/AddOrganizationModal"; enum MembersOrderBy { Name = "firstName", Email = "email" } const ORG_MEMBERS_TABLE_LIMIT = 15; const ViewMembersModalContent = ({ popUp, handlePopUpOpen }: { popUp: UsePopUpState<["viewMembers"]>; handlePopUpOpen: ( popUpName: keyof UsePopUpState<["deleteOrganizationMembership", "deleteUser"]>, data?: { username?: string; membershipId?: string; userId?: string; orgName?: string; orgId?: string; organization?: OrganizationWithProjects; } ) => void; }) => { const organization = popUp.viewMembers?.data?.organization as OrganizationWithProjects; const [resendInviteId, setResendInviteId] = useState(null); const members = organization?.members ?? []; const { search, setSearch, setPage, page, perPage, setPerPage, offset, orderDirection, orderBy, setOrderBy, setOrderDirection, toggleOrderDirection } = usePagination(MembersOrderBy.Name, { initPerPage: ORG_MEMBERS_TABLE_LIMIT }); const filteredMembers = useMemo( () => members ?.filter( ({ user: u }) => u?.firstName?.toLowerCase().includes(search.toLowerCase()) || u?.lastName?.toLowerCase().includes(search.toLowerCase()) || u?.username?.toLowerCase().includes(search.toLowerCase()) || u?.email?.toLowerCase().includes(search.toLowerCase()) ) .sort((a, b) => { const [memberOne, memberTwo] = orderDirection === OrderByDirection.ASC ? [a, b] : [b, a]; let valueOne: string | null; let valueTwo: string | null; switch (orderBy) { case MembersOrderBy.Email: valueOne = memberOne.user.email || memberOne.user.username; valueTwo = memberTwo.user.email || memberTwo.user.username; break; case MembersOrderBy.Name: default: valueOne = memberOne.user.firstName ?? memberOne.user.lastName; valueTwo = memberTwo.user.firstName ?? memberTwo.user.lastName; } if (!valueOne) return 1; if (!valueTwo) return -1; return valueOne.toLowerCase().localeCompare(valueTwo.toLowerCase()); }), [members, search, orderBy, orderDirection] ); const handleSort = (column: MembersOrderBy) => { if (column === orderBy) { toggleOrderDirection(); return; } setOrderBy(column); setOrderDirection(OrderByDirection.ASC); }; useResetPageHelper({ totalCount: filteredMembers.length, offset, setPage }); const resendOrgInvite = useServerAdminResendOrgInvite(); const onResendInvite = async (membershipId: string) => { setResendInviteId(membershipId); try { await resendOrgInvite.mutateAsync({ membershipId, organizationId: organization.id }); createNotification({ text: "Successfully resent org invitation", type: "success" }); } catch (err) { console.error(err); createNotification({ text: "Failed to resend org invitation", type: "error" }); } finally { setResendInviteId(null); } }; return ( <> setSearch(e.target.value)} leftIcon={} placeholder="Search members..." /> {filteredMembers.slice(offset, perPage * page).map((member) => { const { username, email, firstName, lastName, id } = member.user; const { role, status } = member; const name = firstName || lastName ? `${firstName} ${lastName}` : null; return ( ); })}
Name handleSort(MembersOrderBy.Name)} >
Email handleSort(MembersOrderBy.Email)} >
Role

{name ?? Not Set}

{username || email}

{role === OrgMembershipRole.Admin && status !== OrgMembershipStatus.Accepted && ( )}

{member.role.replace("-", " ")}

{Boolean(member.roleId) && ( )}
} onClick={() => handlePopUpOpen("deleteOrganizationMembership", { membershipId: member.membershipId, orgId: organization.id, username: member.user.username, orgName: organization.name }) } > Remove From Organization } onClick={() => handlePopUpOpen("deleteUser", { userId: member.user.id }) } > Delete User
{!filteredMembers.length && ( )}
{Boolean(filteredMembers.length) && ( )} ); }; const ViewMembersModal = ({ isOpen, onOpenChange, popUp, handlePopUpOpen }: { isOpen: boolean; onOpenChange: (isOpen: boolean) => void; popUp: UsePopUpState<["viewMembers", "deleteOrganizationMembership", "deleteUser"]>; handlePopUpOpen: ( popUpName: keyof UsePopUpState<["deleteOrganizationMembership", "deleteUser"]> ) => void; }) => { return ( { event.preventDefault(); }} title="Organization Members" subTitle="View the members of the organization." className="h-full max-w-4xl" bodyClassName="flex flex-col h-full" > ); }; const OrganizationsPanelTable = ({ popUp, handlePopUpOpen, handlePopUpToggle }: { popUp: UsePopUpState< [ "deleteOrganization", "viewMembers", "deleteOrganizationMembership", "deleteUser", "createOrganization" ] >; handlePopUpOpen: ( popUpName: keyof UsePopUpState< [ "deleteOrganization", "viewMembers", "deleteOrganizationMembership", "deleteUser", "createOrganization" ] >, data?: { orgName?: string; orgId?: string; message?: string; organization?: OrganizationWithProjects; } ) => void; handlePopUpToggle: ( popUpName: keyof UsePopUpState<["deleteOrganization", "viewMembers"]>, isOpen?: boolean ) => void; }) => { const [searchOrganizationsFilter, setSearchOrganizationsFilter] = useState(""); const [debouncedSearchTerm] = useDebounce(searchOrganizationsFilter, 500); const { user } = useUser(); const navigate = useNavigate(); const { offset, limit, setPage, perPage, page, setPerPage } = usePagination("", { initPerPage: getUserTablePreference("ResourceOverviewOrgsTable", PreferenceKey.PerPage, 10) }); const handlePerPageChange = (newPerPage: number) => { setPerPage(newPerPage); setUserTablePreference("ResourceOverviewOrgsTable", PreferenceKey.PerPage, newPerPage); }; const { data, isPending } = useAdminGetOrganizations({ limit, offset, searchTerm: debouncedSearchTerm }); const { organizations, totalCount = 0 } = data ?? {}; const isEmpty = !isPending && !totalCount; useResetPageHelper({ totalCount, offset, setPage }); const { mutateAsync: accessOrganization } = useServerAdminAccessOrg(); const handleAccessOrg = async (orgId: string) => { try { await accessOrganization(orgId); navigate({ to: "/login/select-organization", search: { org_id: orgId } }); createNotification({ text: "Successfully joined organization", type: "success" }); } catch { createNotification({ text: "Failed to join organization", type: "error" }); } }; return ( <> setSearchOrganizationsFilter(e.target.value)} leftIcon={} placeholder="Search organizations..." className="flex-1" />
{isPending && } {!isPending && organizations?.map((org) => { const isMember = org.members.find((member) => member.user.id === user.id); return ( ); })}
Name Members Projects
{org.name ? (

{org.name}

) : ( Not Set )}
{org.projects.length} {org.projects.length === 1 ? "Project" : "Projects"}
{isMember && (
)} {!isMember && ( { e.stopPropagation(); handleAccessOrg(org.id); }} icon={} > Join Organization )} { e.stopPropagation(); handlePopUpOpen("deleteOrganization", { orgId: org.id, orgName: org.name }); }} icon={} > Delete Organization
{!isPending && isEmpty && }
{!isPending && totalCount > 0 && ( setPage(newPage)} onChangePerPage={handlePerPageChange} /> )}
handlePopUpToggle("viewMembers", isOpen)} /> ); }; export const OrganizationsTable = () => { const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ "deleteOrganization", "deleteOrganizationMembership", "deleteUser", "viewMembers", "createOrganization" ] as const); const { mutateAsync: deleteOrganization } = useAdminDeleteOrganization(); const { mutateAsync: deleteOrganizationMembership } = useAdminDeleteOrganizationMembership(); const { mutateAsync: deleteUser } = useAdminDeleteUser(); const handleDeleteOrganization = async () => { const { orgId } = popUp?.deleteOrganization?.data as { orgId: string }; await deleteOrganization(orgId); createNotification({ type: "success", text: "Successfully deleted organization" }); handlePopUpClose("deleteOrganization"); }; const handleDeleteOrganizationMembership = async () => { const { orgId, membershipId } = popUp?.deleteOrganizationMembership?.data as { orgId: string; membershipId: string; }; if (!orgId || !membershipId) { return; } await deleteOrganizationMembership({ organizationId: orgId, membershipId }); createNotification({ type: "success", text: "Successfully removed user from organization" }); handlePopUpClose("viewMembers"); handlePopUpClose("deleteOrganizationMembership"); }; const handleDeleteUser = async () => { const { userId } = popUp?.deleteUser?.data as { userId: string }; if (!userId) { return; } await deleteUser(userId); createNotification({ type: "success", text: "Successfully deleted user" }); handlePopUpClose("viewMembers"); handlePopUpClose("deleteUser"); }; return (

Organizations

Manage, join and view organizations across your instance.

handlePopUpToggle("deleteOrganization", isOpen)} onDeleteApproved={handleDeleteOrganization} /> handlePopUpToggle("deleteOrganizationMembership", isOpen)} onDeleteApproved={handleDeleteOrganizationMembership} /> handlePopUpToggle("deleteUser", isOpen)} onDeleteApproved={handleDeleteUser} /> handlePopUpToggle("createOrganization", isOpen)} />
); };