diff --git a/backend/src/services/identity/identity-org-dal.ts b/backend/src/services/identity/identity-org-dal.ts index 5ca6cbbc1..3c5ca8ffe 100644 --- a/backend/src/services/identity/identity-org-dal.ts +++ b/backend/src/services/identity/identity-org-dal.ts @@ -392,7 +392,12 @@ export const identityOrgDALFactory = (db: TDbClient) => { .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityOrgMembership}.identityId`) .where(`${TableName.IdentityOrgMembership}.orgId`, orgId) .leftJoin(TableName.OrgRoles, `${TableName.IdentityOrgMembership}.roleId`, `${TableName.OrgRoles}.id`) - .orderBy(`${TableName.Identity}.${orderBy}`, orderDirection) + .orderBy( + orderBy === OrgIdentityOrderBy.Role + ? `${TableName.IdentityOrgMembership}.${orderBy}` + : `${TableName.Identity}.${orderBy}`, + orderDirection + ) .select(`${TableName.IdentityOrgMembership}.id`) .select<{ id: string; total_count: string }>( db.raw( @@ -523,6 +528,23 @@ export const identityOrgDALFactory = (db: TDbClient) => { if (orderBy === OrgIdentityOrderBy.Name) { void query.orderBy("identityName", orderDirection); + } else if (orderBy === OrgIdentityOrderBy.Role) { + void query.orderByRaw( + ` + CASE + WHEN ??.role = ? + THEN ??.slug + ELSE ??.role + END ? + `, + [ + TableName.IdentityOrgMembership, + "custom", + TableName.OrgRoles, + TableName.IdentityOrgMembership, + db.raw(orderDirection) + ] + ); } const docs = await query; diff --git a/backend/src/services/identity/identity-types.ts b/backend/src/services/identity/identity-types.ts index 8d23f34fe..4380f6f41 100644 --- a/backend/src/services/identity/identity-types.ts +++ b/backend/src/services/identity/identity-types.ts @@ -46,8 +46,8 @@ export type TListOrgIdentitiesByOrgIdDTO = { } & TOrgPermission; export enum OrgIdentityOrderBy { - Name = "name" - // Role = "role" + Name = "name", + Role = "role" } export type TSearchOrgIdentitiesByOrgIdDAL = { diff --git a/frontend/src/components/v2/Pagination/Pagination.tsx b/frontend/src/components/v2/Pagination/Pagination.tsx index c1f9306fa..d1f2e28aa 100644 --- a/frontend/src/components/v2/Pagination/Pagination.tsx +++ b/frontend/src/components/v2/Pagination/Pagination.tsx @@ -64,7 +64,7 @@ export const Pagination = ({ - + {perPageList.map((perPageOption) => ( { search }); return data; - } + }, + placeholderData: (previousData) => previousData }); }; diff --git a/frontend/src/hooks/api/organization/types.ts b/frontend/src/hooks/api/organization/types.ts index 068cfad6d..73267f0f6 100644 --- a/frontend/src/hooks/api/organization/types.ts +++ b/frontend/src/hooks/api/organization/types.ts @@ -154,6 +154,6 @@ export type TOrgIdentitiesList = { }; export enum OrgIdentityOrderBy { - Name = "name" - // Role = "role" + Name = "name", + Role = "role" } diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsSection.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsSection.tsx index fbe2d88f0..93af6583a 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsSection.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsSection.tsx @@ -56,12 +56,12 @@ export const OrgGroupsSection = () => { return ( - + Groups {(isAllowed) => ( } onClick={() => handleAddGroupModal()} diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsTable.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsTable.tsx index 14de28fd6..61119fce1 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsTable.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsTable.tsx @@ -2,14 +2,17 @@ import { useMemo } from "react"; import { faArrowDown, faArrowUp, - faEllipsis, + faCopy, + faEdit, + faEllipsisV, faMagnifyingGlass, faSearch, + faTrash, + faUserGroup, faUsers } 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 { OrgPermissionCan } from "@app/components/permissions"; @@ -261,7 +264,8 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => { handleChangeRole({ @@ -282,13 +286,19 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => { - - - - + + + + - + } onClick={(e) => { e.stopPropagation(); createNotification({ @@ -306,10 +316,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => { > {(isAllowed) => ( } onClick={(e) => { e.stopPropagation(); handlePopUpOpen("group", { @@ -320,7 +327,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => { customRole }); }} - disabled={!isAllowed} + isDisabled={!isAllowed} > Edit Group @@ -332,10 +339,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => { > {(isAllowed) => ( } onClick={() => navigate({ to: "/organization/groups/$groupId", @@ -344,7 +348,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => { } }) } - disabled={!isAllowed} + isDisabled={!isAllowed} > Manage Members @@ -356,11 +360,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => { > {(isAllowed) => ( } onClick={(e) => { e.stopPropagation(); handlePopUpOpen("deleteGroup", { @@ -368,7 +368,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => { name }); }} - disabled={!isAllowed} + isDisabled={!isAllowed} > Delete Group diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx index 19680ef24..cb9715b53 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx @@ -1,4 +1,4 @@ -import { faArrowUpRightFromSquare, faPlus } from "@fortawesome/free-solid-svg-icons"; +import { faArrowUpRightFromSquare, faBookOpen, faPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; @@ -71,20 +71,22 @@ export const IdentitySection = withPermission( return ( - - Identities - + + + Identities - Documentation{" "} - + + + Docs + + {(isAllowed) => ( } onClick={() => { diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx index f445505e0..407ee3a55 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx @@ -1,12 +1,15 @@ -import { useState } from "react"; -import { Controller, useForm } from "react-hook-form"; +import { useCallback, useState } from "react"; import { faArrowDown, faArrowUp, - faEllipsis, + faCheckCircle, + faChevronRight, + faEdit, + faEllipsisV, faFilter, faMagnifyingGlass, - faServer + faServer, + faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useNavigate } from "@tanstack/react-router"; @@ -15,19 +18,18 @@ import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; import { - Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, + DropdownMenuLabel, DropdownMenuTrigger, + DropdownSubMenu, + DropdownSubMenuContent, + DropdownSubMenuTrigger, EmptyState, - FormControl, IconButton, Input, Pagination, - Popover, - PopoverContent, - PopoverTrigger, Select, SelectItem, Spinner, @@ -38,7 +40,6 @@ import { Td, Th, THead, - Tooltip, Tr } from "@app/components/v2"; import { OrgPermissionIdentityActions, OrgPermissionSubjects, useOrganization } from "@app/context"; @@ -63,6 +64,10 @@ type Props = { ) => void; }; +type Filter = { + roles: string[]; +}; + export const IdentityTable = ({ handlePopUpOpen }: Props) => { const navigate = useNavigate(); const { currentOrg } = useOrganization(); @@ -90,7 +95,9 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => { setUserTablePreference("identityTable", PreferenceKey.PerPage, newPerPage); }; - const [filteredRoles, setFilteredRoles] = useState([]); + const [filter, setFilter] = useState({ + roles: [] + }); const organizationId = currentOrg?.id || ""; @@ -103,7 +110,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => { orderBy, search: { name: debouncedSearch ? { $contains: debouncedSearch } : undefined, - role: filteredRoles?.length ? { $in: filteredRoles } : undefined + role: filter.roles?.length ? { $in: filter.roles } : undefined } }); @@ -113,7 +120,6 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => { offset, setPage }); - const filterForm = useForm<{ roles: string }>(); const { data: roles } = useGetOrgRoles(organizationId); @@ -153,79 +159,80 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => { } }; + const handleRoleToggle = useCallback( + (roleSlug: string) => + setFilter((state) => { + const currentRoles = state.roles || []; + + if (currentRoles.includes(roleSlug)) { + return { ...state, roles: currentRoles.filter((role) => role !== roleSlug) }; + } + return { ...state, roles: [...currentRoles, roleSlug] }; + }), + [] + ); + + const isTableFiltered = Boolean(filter.roles.length); + return ( + + + + + + + + Filter By + + } + > + Roles + + + + Apply Roles to Filter Identities + + {roles?.map(({ id, slug, name }) => ( + { + evt.preventDefault(); + handleRoleToggle(slug); + }} + key={id} + icon={filter.roles.includes(slug) && } + iconPos="right" + > + + + {name} + + + ))} + + + + setSearch(e.target.value)} leftIcon={} placeholder="Search identities by name..." /> - - - - - - - - - - - - Advance Filter - - { - setFilteredRoles(el.roles?.split(",")?.filter(Boolean) || []); - })} - > - ( - - - - )} - /> - - - Apply Filter - - {Boolean(filteredRoles.length) && ( - { - filterForm.reset({ roles: "" }); - setFilteredRoles([]); - }} - > - Clear - - )} - - - - - @@ -251,8 +258,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => { - Role - {/* + Role { /> - */} + {isFetching ? : null} @@ -303,7 +309,8 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => { handleChangeRole({ @@ -324,21 +331,24 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => { - - - - + + + + - + {(isAllowed) => ( } onClick={(e) => { e.stopPropagation(); navigate({ @@ -348,7 +358,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => { } }); }} - disabled={!isAllowed} + isDisabled={!isAllowed} > Edit Identity @@ -360,11 +370,6 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => { > {(isAllowed) => ( { e.stopPropagation(); handlePopUpOpen("deleteIdentity", { @@ -372,7 +377,8 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => { name }); }} - disabled={!isAllowed} + isDisabled={!isAllowed} + icon={} > Delete Identity @@ -398,7 +404,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => { {!isPending && data && data?.identities.length === 0 && ( 0 || filteredRoles?.length > 0 + debouncedSearch.trim().length > 0 || filter.roles?.length > 0 ? "No identities match search filter" : "No identities have been created in this organization" } diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx index f88ef5131..87e55c145 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx @@ -115,12 +115,12 @@ export const OrgMembersSection = () => { return ( - + Users {(isAllowed) => ( } onClick={() => handleAddMemberModal()} diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx index 1ad9cfdd1..c323c4057 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx @@ -4,11 +4,14 @@ import { faArrowUp, faCheckCircle, faChevronRight, - faEllipsis, + faEdit, + faEllipsisV, faFilter, faMagnifyingGlass, faSearch, - faUsers + faUsers, + faUserSlash, + faUserXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useNavigate } from "@tanstack/react-router"; @@ -79,7 +82,8 @@ type Props = { enum OrgMembersOrderBy { Name = "firstName", - Email = "email" + Email = "email", + Role = "role" } type Filter = { @@ -99,8 +103,10 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro const { data: serverDetails } = useFetchServerStatus(); const { data: members = [], isPending: isMembersLoading } = useGetOrgUsers(orgId); - const { mutateAsync: resendOrgMemberInvitation } = useResendOrgMemberInvitation(); + const { mutateAsync: resendOrgMemberInvitation, isPending: isResendInvitePending } = + useResendOrgMemberInvitation(); const { mutateAsync: updateOrgMembership } = useUpdateOrgMembership(); + const [resendInviteId, setResendInviteId] = useState(null); const onRoleChange = async (membershipId: string, role: string) => { if (!currentOrg?.id) return; @@ -136,6 +142,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro }; const onResendInvite = async (membershipId: string) => { + setResendInviteId(membershipId); try { const signupToken = await resendOrgMemberInvitation({ membershipId @@ -156,6 +163,8 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro text: "Failed to resend org invitation", type: "error" }); + } finally { + setResendInviteId(null); } }; @@ -229,6 +238,16 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro valueOne = memberOne.user.email || memberOne.inviteEmail; valueTwo = memberTwo.user.email || memberTwo.inviteEmail; break; + case OrgMembersOrderBy.Role: + valueOne = + memberOne.role === "custom" + ? findRoleFromId(memberOne.roleId)!.slug + : memberOne.role; + valueTwo = + memberTwo.role === "custom" + ? findRoleFromId(memberTwo.roleId)!.slug + : memberTwo.role; + break; case OrgMembersOrderBy.Name: default: valueOne = memberOne.user.firstName; @@ -284,7 +303,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro variant="plain" size="sm" className={twMerge( - "flex h-10 w-11 items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10", + "flex h-[2.375rem] w-[2.6rem] items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10", isTableFiltered && "border-primary/50 text-primary" )} > @@ -378,7 +397,26 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro - Role + + + Role + handleSort(OrgMembersOrderBy.Role)} + > + + + + @@ -398,7 +436,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro isActive }) => { const name = - u && u.firstName ? `${u.firstName} ${u.lastName ?? ""}`.trim() : "-"; + u && u.firstName ? `${u.firstName} ${u.lastName ?? ""}`.trim() : null; const email = u?.email || inviteEmail; const username = u?.username ?? inviteEmail ?? "-"; return ( @@ -415,7 +453,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro } > - {name} + {name ?? Not Set} {u.superAdmin && ( Server Admin @@ -429,79 +467,77 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro a={OrgPermissionSubjects.Member} > {(isAllowed) => ( - <> - {!isActive && ( - {}} - > - Suspended - - )} - {isActive && status === "accepted" && ( - - onRoleChange(orgMembershipId, selectedRole) - } - > - {(roles || []) - .filter(({ slug }) => - slug === "owner" ? isIamOwner || role === "owner" : true - ) - .map(({ slug, name: roleName }) => ( - - {roleName} - - ))} - - )} - {isActive && - (status === "invited" || status === "verified") && - email && - serverDetails?.emailConfigured && ( + + onRoleChange(orgMembershipId, selectedRole) + } + > + {(roles || []) + .filter(({ slug }) => + slug === "owner" ? isIamOwner || role === "owner" : true + ) + .map(({ slug, name: roleName }) => ( + + {roleName} + + ))} + + )} + + + + + {isActive && + (status === "invited" || status === "verified") && + email && + serverDetails?.emailConfigured && ( + + {(isAllowed) => ( { onResendInvite(orgMembershipId); e.stopPropagation(); }} > - Resend invite + Resend Invite )} - > - )} - - - - {userId !== u?.id && ( + + )} - - - - + + + + - + {(isAllowed) => ( { e.stopPropagation(); navigate({ @@ -511,7 +547,8 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro } }); }} - disabled={!isAllowed} + isDisabled={!isAllowed} + icon={} > Edit User @@ -523,15 +560,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro > {(isAllowed) => ( } onClick={async (e) => { e.stopPropagation(); @@ -560,7 +589,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro username }); }} - disabled={!isAllowed} + isDisabled={!isAllowed} > {`${isActive ? "Deactivate" : "Activate"} User`} @@ -572,11 +601,6 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro > {(isAllowed) => ( { e.stopPropagation(); @@ -593,7 +617,8 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro username }); }} - disabled={!isAllowed} + isDisabled={!isAllowed} + icon={} > Remove User @@ -601,7 +626,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro - )} + ); diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTabSection.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTabSection.tsx index 8e6c5d591..57353e8d9 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTabSection.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTabSection.tsx @@ -6,10 +6,10 @@ export const OrgRoleTabSection = () => { return ( diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTable.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTable.tsx index 54b257a72..e7426cdc9 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTable.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTable.tsx @@ -1,4 +1,17 @@ -import { faEllipsis, faPlus } from "@fortawesome/free-solid-svg-icons"; +import { useMemo } from "react"; +import { + faArrowDown, + faArrowUp, + faCopy, + faEdit, + faEllipsisV, + faEye, + faIdBadge, + faMagnifyingGlass, + faPlus, + faSearch, + faTrash +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useNavigate } from "@tanstack/react-router"; import { twMerge } from "tailwind-merge"; @@ -14,6 +27,10 @@ import { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, + EmptyState, + IconButton, + Input, + Pagination, Table, TableContainer, TableSkeleton, @@ -30,13 +47,25 @@ import { useOrganization, useSubscription } from "@app/context"; -import { isCustomOrgRole } from "@app/helpers/roles"; -import { usePopUp } from "@app/hooks"; +import { isCustomOrgRole, isCustomProjectRole } from "@app/helpers/roles"; +import { + getUserTablePreference, + PreferenceKey, + setUserTablePreference +} from "@app/helpers/userTablePreferences"; +import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks"; import { useDeleteOrgRole, useGetOrgRoles, useUpdateOrg } from "@app/hooks/api"; +import { OrderByDirection } from "@app/hooks/api/generic/types"; import { TOrgRole } from "@app/hooks/api/roles/types"; import { DuplicateOrgRoleModal } from "@app/pages/organization/RoleByIDPage/components/DuplicateOrgRoleModal"; import { RoleModal } from "@app/pages/organization/RoleByIDPage/components/RoleModal"; +enum RolesOrderBy { + Name = "name", + Slug = "slug", + Type = "type" +} + export const OrgRoleTable = () => { const navigate = useNavigate(); const { currentOrg } = useOrganization(); @@ -93,14 +122,89 @@ export const OrgRoleTable = () => { } }; + const { + orderDirection, + toggleOrderDirection, + orderBy, + setOrderDirection, + setOrderBy, + search, + setSearch, + page, + perPage, + setPerPage, + setPage, + offset + } = usePagination(RolesOrderBy.Type, { + initPerPage: getUserTablePreference("orgRolesTable", PreferenceKey.PerPage, 20) + }); + + const handlePerPageChange = (newPerPage: number) => { + setPerPage(newPerPage); + setUserTablePreference("orgRolesTable", PreferenceKey.PerPage, newPerPage); + }; + + const filteredRoles = useMemo( + () => + roles + ?.filter((role) => { + const { slug, name } = role; + + const searchValue = search.trim().toLowerCase(); + + return ( + name.toLowerCase().includes(searchValue) || slug.toLowerCase().includes(searchValue) + ); + }) + .sort((a, b) => { + const [roleOne, roleTwo] = orderDirection === OrderByDirection.ASC ? [a, b] : [b, a]; + + switch (orderBy) { + case RolesOrderBy.Slug: + return roleOne.slug.toLowerCase().localeCompare(roleTwo.slug.toLowerCase()); + case RolesOrderBy.Type: { + const roleOneValue = isCustomOrgRole(roleOne.slug) ? -1 : 1; + const roleTwoValue = isCustomOrgRole(roleTwo.slug) ? -1 : 1; + + return roleTwoValue - roleOneValue; + } + case RolesOrderBy.Name: + default: + return roleOne.name.toLowerCase().localeCompare(roleTwo.name.toLowerCase()); + } + }) ?? [], + [roles, orderDirection, search, orderBy] + ); + + useResetPageHelper({ + totalCount: filteredRoles.length, + offset, + setPage + }); + + const handleSort = (column: RolesOrderBy) => { + if (column === orderBy) { + toggleOrderDirection(); + return; + } + + setOrderBy(column); + setOrderDirection(OrderByDirection.ASC); + }; + + const getClassName = (col: RolesOrderBy) => twMerge("ml-2", orderBy === col ? "" : "opacity-30"); + + const getColSortIcon = (col: RolesOrderBy) => + orderDirection === OrderByDirection.DESC && orderBy === col ? faArrowUp : faArrowDown; + return ( - + Organization Roles {(isAllowed) => ( } onClick={() => { @@ -113,18 +217,63 @@ export const OrgRoleTable = () => { )} + setSearch(e.target.value)} + leftIcon={} + placeholder="Search roles..." + className="flex-1" + containerClassName="mb-4" + /> - Name - Slug + + + Name + handleSort(RolesOrderBy.Name)} + > + + + + + + + Slug + handleSort(RolesOrderBy.Slug)} + > + + + + + + + Type + handleSort(RolesOrderBy.Type)} + > + + + + - {isRolesLoading && } - {roles?.map((role) => { + {isRolesLoading && } + {filteredRoles?.slice(offset, perPage * page).map((role) => { const { id, name, slug } = role; const isNonMutatable = ["owner", "admin", "member", "no-access"].includes(slug); const isDefaultOrgRole = isCustomOrgRole(slug) @@ -162,23 +311,30 @@ export const OrgRoleTable = () => { {slug} + + + {isCustomProjectRole(slug) ? "Custom" : "Default"} + + - - - - + + + + - + {(isAllowed) => ( { e.stopPropagation(); navigate({ @@ -188,7 +344,8 @@ export const OrgRoleTable = () => { } }); }} - disabled={!isAllowed} + isDisabled={!isAllowed} + icon={} > {`${isNonMutatable ? "View" : "Edit"} Role`} @@ -200,14 +357,12 @@ export const OrgRoleTable = () => { > {(isAllowed) => ( { e.stopPropagation(); handlePopUpOpen("duplicateRole", role); }} - disabled={!isAllowed} + isDisabled={!isAllowed} + icon={} > Duplicate Role @@ -220,14 +375,12 @@ export const OrgRoleTable = () => { > {(isAllowed) => ( { e.stopPropagation(); handleSetRoleAsDefault(slug); }} + icon={} > Set as Default Role @@ -250,16 +403,12 @@ export const OrgRoleTable = () => { > {(isAllowed) => ( { e.stopPropagation(); handlePopUpOpen("deleteRole", role); }} - disabled={!isAllowed || isDefaultOrgRole} + icon={} + isDisabled={!isAllowed || isDefaultOrgRole} > Delete Role @@ -276,6 +425,25 @@ export const OrgRoleTable = () => { })} + {Boolean(filteredRoles?.length) && ( + + )} + {!filteredRoles?.length && !isRolesLoading && ( + + )} { variant="plain" size="sm" className={twMerge( - "flex h-10 w-11 items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10", + "flex h-[2.375rem] w-[2.6rem] items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10", isTableFiltered && "border-primary/50 text-primary" )} > @@ -298,7 +298,8 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => { {!isMembersLoading && filteredUsers.slice(offset, perPage * page).map((projectMember) => { const { user: u, inviteEmail, id: membershipId, roles } = projectMember; - const name = u.firstName || u.lastName ? `${u.firstName} ${u.lastName || ""}` : "-"; + const name = + u.firstName || u.lastName ? `${u.firstName} ${u.lastName || ""}` : null; const email = u?.email || inviteEmail; return ( @@ -328,7 +329,7 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => { }) } > - {name} + {name ?? Not Set} {email} diff --git a/frontend/src/pages/project/AccessControlPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx b/frontend/src/pages/project/AccessControlPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx index 01c97060f..b29bf469b 100644 --- a/frontend/src/pages/project/AccessControlPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx @@ -236,7 +236,7 @@ export const ProjectRoleList = () => { - {isRolesLoading && } + {isRolesLoading && } {filteredRoles?.slice(offset, perPage * page).map((role) => { const { id, name, slug } = role; const isNonMutatable = Object.values(ProjectMembershipRole).includes( diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx index c05f9fe6c..c657ffd40 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx @@ -12,7 +12,7 @@ import { import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; -import { Button, Checkbox, Select, SelectItem, Tag, Tooltip } from "@app/components/v2"; +import { Button, Checkbox, IconButton, Select, SelectItem, Tag, Tooltip } from "@app/components/v2"; import { ProjectPermissionSub } from "@app/context"; import { useToggle } from "@app/hooks"; @@ -241,16 +241,19 @@ export const GeneralPermissionPolicies = {!isDisabled && ( - } - variant="outline_bg" - size="xs" - className="ml-auto mr-3" - onClick={() => remove(rootIndex)} - isDisabled={isDisabled} - > - Remove Rule - + + remove(rootIndex)} + isDisabled={isDisabled} + > + + + )} {!isDisabled && ( @@ -271,16 +274,19 @@ export const GeneralPermissionPolicies = Actions {!isDisabled && !isConditionalSubjects(subject) && ( - } - variant="outline_bg" - size="xs" - className="ml-auto" - onClick={() => remove(rootIndex)} - isDisabled={isDisabled} - > - Remove Rule - + + remove(rootIndex)} + isDisabled={isDisabled} + > + + + )}
Groups
Identities
Users
Organization Roles