improvement: standardize and improve org access control

This commit is contained in:
Scott Wilson
2025-06-27 15:15:12 -07:00
parent e8e90585ca
commit ed7d709a70
15 changed files with 527 additions and 304 deletions
@@ -392,7 +392,12 @@ export const identityOrgDALFactory = (db: TDbClient) => {
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityOrgMembership}.identityId`) .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityOrgMembership}.identityId`)
.where(`${TableName.IdentityOrgMembership}.orgId`, orgId) .where(`${TableName.IdentityOrgMembership}.orgId`, orgId)
.leftJoin(TableName.OrgRoles, `${TableName.IdentityOrgMembership}.roleId`, `${TableName.OrgRoles}.id`) .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(`${TableName.IdentityOrgMembership}.id`)
.select<{ id: string; total_count: string }>( .select<{ id: string; total_count: string }>(
db.raw( db.raw(
@@ -523,6 +528,23 @@ export const identityOrgDALFactory = (db: TDbClient) => {
if (orderBy === OrgIdentityOrderBy.Name) { if (orderBy === OrgIdentityOrderBy.Name) {
void query.orderBy("identityName", orderDirection); 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; const docs = await query;
@@ -46,8 +46,8 @@ export type TListOrgIdentitiesByOrgIdDTO = {
} & TOrgPermission; } & TOrgPermission;
export enum OrgIdentityOrderBy { export enum OrgIdentityOrderBy {
Name = "name" Name = "name",
// Role = "role" Role = "role"
} }
export type TSearchOrgIdentitiesByOrgIdDAL = { export type TSearchOrgIdentitiesByOrgIdDAL = {
@@ -64,7 +64,7 @@ export const Pagination = ({
<FontAwesomeIcon className="text-xs" icon={faCaretDown} /> <FontAwesomeIcon className="text-xs" icon={faCaretDown} />
</IconButton> </IconButton>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="min-w-fit"> <DropdownMenuContent sideOffset={2} className="min-w-fit">
{perPageList.map((perPageOption) => ( {perPageList.map((perPageOption) => (
<DropdownMenuItem <DropdownMenuItem
key={`pagination-per-page-options-${perPageOption}`} key={`pagination-per-page-options-${perPageOption}`}
@@ -81,7 +81,8 @@ export const useSearchIdentities = (dto: TSearchIdentitiesDTO) => {
search search
}); });
return data; return data;
} },
placeholderData: (previousData) => previousData
}); });
}; };
+2 -2
View File
@@ -154,6 +154,6 @@ export type TOrgIdentitiesList = {
}; };
export enum OrgIdentityOrderBy { export enum OrgIdentityOrderBy {
Name = "name" Name = "name",
// Role = "role" Role = "role"
} }
@@ -56,12 +56,12 @@ export const OrgGroupsSection = () => {
return ( return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between"> <div className="mb-4 flex items-center justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Groups</p> <p className="text-xl font-semibold text-mineshaft-100">Groups</p>
<OrgPermissionCan I={OrgPermissionGroupActions.Create} a={OrgPermissionSubjects.Groups}> <OrgPermissionCan I={OrgPermissionGroupActions.Create} a={OrgPermissionSubjects.Groups}>
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
colorSchema="primary" colorSchema="secondary"
type="submit" type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handleAddGroupModal()} onClick={() => handleAddGroupModal()}
@@ -2,14 +2,17 @@ import { useMemo } from "react";
import { import {
faArrowDown, faArrowDown,
faArrowUp, faArrowUp,
faEllipsis, faCopy,
faEdit,
faEllipsisV,
faMagnifyingGlass, faMagnifyingGlass,
faSearch, faSearch,
faTrash,
faUserGroup,
faUsers faUsers
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router"; import { useNavigate } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
@@ -261,7 +264,8 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
<Select <Select
value={role === "custom" ? (customRole?.slug as string) : role} value={role === "custom" ? (customRole?.slug as string) : role}
isDisabled={!isAllowed} isDisabled={!isAllowed}
className="w-48 bg-mineshaft-600" className="h-8 w-48 bg-mineshaft-700"
position="popper"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800" dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
onValueChange={(selectedRole) => onValueChange={(selectedRole) =>
handleChangeRole({ handleChangeRole({
@@ -282,13 +286,19 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
</Td> </Td>
<Td> <Td>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg"> <DropdownMenuTrigger asChild>
<div className="hover:text-primary-400 data-[state=open]:text-primary-400"> <IconButton
<FontAwesomeIcon size="sm" icon={faEllipsis} /> ariaLabel="Options"
</div> className="w-6"
colorSchema="secondary"
variant="plain"
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1"> <DropdownMenuContent sideOffset={2} align="end">
<DropdownMenuItem <DropdownMenuItem
icon={<FontAwesomeIcon icon={faCopy} />}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
createNotification({ createNotification({
@@ -306,10 +316,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
> >
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
className={twMerge( icon={<FontAwesomeIcon icon={faEdit} />}
!isAllowed &&
"pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
handlePopUpOpen("group", { handlePopUpOpen("group", {
@@ -320,7 +327,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
customRole customRole
}); });
}} }}
disabled={!isAllowed} isDisabled={!isAllowed}
> >
Edit Group Edit Group
</DropdownMenuItem> </DropdownMenuItem>
@@ -332,10 +339,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
> >
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
className={twMerge( icon={<FontAwesomeIcon icon={faUserGroup} />}
!isAllowed &&
"pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={() => onClick={() =>
navigate({ navigate({
to: "/organization/groups/$groupId", to: "/organization/groups/$groupId",
@@ -344,7 +348,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
} }
}) })
} }
disabled={!isAllowed} isDisabled={!isAllowed}
> >
Manage Members Manage Members
</DropdownMenuItem> </DropdownMenuItem>
@@ -356,11 +360,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
> >
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
className={twMerge( icon={<FontAwesomeIcon icon={faTrash} />}
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
handlePopUpOpen("deleteGroup", { handlePopUpOpen("deleteGroup", {
@@ -368,7 +368,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
name name
}); });
}} }}
disabled={!isAllowed} isDisabled={!isAllowed}
> >
Delete Group Delete Group
</DropdownMenuItem> </DropdownMenuItem>
@@ -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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
@@ -71,20 +71,22 @@ export const IdentitySection = withPermission(
return ( return (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between"> <div className="mb-4 flex items-center justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Identities</p> <div className="flex items-center gap-1">
<div className="flex w-full justify-end pr-4"> <p className="text-xl font-semibold text-mineshaft-100">Identities</p>
<a <a
href="https://infisical.com/docs/documentation/platform/identities/overview"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
href="https://infisical.com/docs/documentation/platform/identities/overview"
className="flex w-max cursor-pointer items-center rounded-md border border-mineshaft-500 bg-mineshaft-600 px-4 py-2 text-mineshaft-200 duration-200 hover:border-primary/40 hover:bg-primary/10 hover:text-white"
> >
Documentation{" "} <div className="ml-1 mt-[0.16rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon <FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
icon={faArrowUpRightFromSquare} <span>Docs</span>
className="mb-[0.06rem] ml-1 text-xs" <FontAwesomeIcon
/> icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-[10px]"
/>
</div>
</a> </a>
</div> </div>
<OrgPermissionCan <OrgPermissionCan
@@ -93,7 +95,7 @@ export const IdentitySection = withPermission(
> >
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
colorSchema="primary" colorSchema="secondary"
type="submit" type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => { onClick={() => {
@@ -1,12 +1,15 @@
import { useState } from "react"; import { useCallback, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { import {
faArrowDown, faArrowDown,
faArrowUp, faArrowUp,
faEllipsis, faCheckCircle,
faChevronRight,
faEdit,
faEllipsisV,
faFilter, faFilter,
faMagnifyingGlass, faMagnifyingGlass,
faServer faServer,
faTrash
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router"; import { useNavigate } from "@tanstack/react-router";
@@ -15,19 +18,18 @@ import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import { import {
Button,
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger, DropdownMenuTrigger,
DropdownSubMenu,
DropdownSubMenuContent,
DropdownSubMenuTrigger,
EmptyState, EmptyState,
FormControl,
IconButton, IconButton,
Input, Input,
Pagination, Pagination,
Popover,
PopoverContent,
PopoverTrigger,
Select, Select,
SelectItem, SelectItem,
Spinner, Spinner,
@@ -38,7 +40,6 @@ import {
Td, Td,
Th, Th,
THead, THead,
Tooltip,
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { OrgPermissionIdentityActions, OrgPermissionSubjects, useOrganization } from "@app/context"; import { OrgPermissionIdentityActions, OrgPermissionSubjects, useOrganization } from "@app/context";
@@ -63,6 +64,10 @@ type Props = {
) => void; ) => void;
}; };
type Filter = {
roles: string[];
};
export const IdentityTable = ({ handlePopUpOpen }: Props) => { export const IdentityTable = ({ handlePopUpOpen }: Props) => {
const navigate = useNavigate(); const navigate = useNavigate();
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
@@ -90,7 +95,9 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
setUserTablePreference("identityTable", PreferenceKey.PerPage, newPerPage); setUserTablePreference("identityTable", PreferenceKey.PerPage, newPerPage);
}; };
const [filteredRoles, setFilteredRoles] = useState<string[]>([]); const [filter, setFilter] = useState<Filter>({
roles: []
});
const organizationId = currentOrg?.id || ""; const organizationId = currentOrg?.id || "";
@@ -103,7 +110,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
orderBy, orderBy,
search: { search: {
name: debouncedSearch ? { $contains: debouncedSearch } : undefined, 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, offset,
setPage setPage
}); });
const filterForm = useForm<{ roles: string }>();
const { data: roles } = useGetOrgRoles(organizationId); 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 ( return (
<div> <div>
<div className="mb-4 flex items-center space-x-2"> <div className="mb-4 flex items-center space-x-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Filter Identities"
variant="plain"
size="sm"
className={twMerge(
"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"
)}
>
<FontAwesomeIcon icon={faFilter} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-0">
<DropdownMenuLabel>Filter By</DropdownMenuLabel>
<DropdownSubMenu>
<DropdownSubMenuTrigger
iconPos="right"
icon={<FontAwesomeIcon icon={faChevronRight} size="sm" />}
>
Roles
</DropdownSubMenuTrigger>
<DropdownSubMenuContent className="thin-scrollbar max-h-[20rem] overflow-y-auto rounded-l-none">
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
Apply Roles to Filter Identities
</DropdownMenuLabel>
{roles?.map(({ id, slug, name }) => (
<DropdownMenuItem
onClick={(evt) => {
evt.preventDefault();
handleRoleToggle(slug);
}}
key={id}
icon={filter.roles.includes(slug) && <FontAwesomeIcon icon={faCheckCircle} />}
iconPos="right"
>
<div className="flex items-center">
<div
className="mr-2 h-2 w-2 rounded-full"
style={{ background: "#bec2c8" }}
/>
{name}
</div>
</DropdownMenuItem>
))}
</DropdownSubMenuContent>
</DropdownSubMenu>
</DropdownMenuContent>
</DropdownMenu>
<Input <Input
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />} leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search identities by name..." placeholder="Search identities by name..."
/> />
<div>
<Popover>
<PopoverTrigger>
<IconButton
ariaLabel="filter"
variant="outline_bg"
className={filteredRoles?.length ? "border-primary" : ""}
>
<Tooltip content="Advance Filter">
<FontAwesomeIcon icon={faFilter} />
</Tooltip>
</IconButton>
</PopoverTrigger>
<PopoverContent className="w-auto border border-mineshaft-600 bg-mineshaft-800 p-2 drop-shadow-2xl">
<div className="mb-4 border-b border-b-gray-700 pb-2 text-sm text-mineshaft-300">
Advance Filter
</div>
<form
onSubmit={filterForm.handleSubmit((el) => {
setFilteredRoles(el.roles?.split(",")?.filter(Boolean) || []);
})}
>
<Controller
control={filterForm.control}
name="roles"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Roles"
helperText="Eg: admin,viewer"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
<div className="flex items-center space-x-2">
<Button
type="submit"
size="xs"
colorSchema="primary"
variant="outline_bg"
className="mt-4"
>
Apply Filter
</Button>
{Boolean(filteredRoles.length) && (
<Button
size="xs"
variant="link"
className="ml-4 mt-4"
onClick={() => {
filterForm.reset({ roles: "" });
setFilteredRoles([]);
}}
>
Clear
</Button>
)}
</div>
</form>
</PopoverContent>
</Popover>
</div>
</div> </div>
<TableContainer> <TableContainer>
<Table> <Table>
@@ -251,8 +258,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
</IconButton> </IconButton>
</div> </div>
</Th> </Th>
<Th>Role</Th> <Th>
{/* <Th>
<div className="flex items-center"> <div className="flex items-center">
Role Role
<IconButton <IconButton
@@ -271,7 +277,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
/> />
</IconButton> </IconButton>
</div> </div>
</Th> */} </Th>
<Th className="w-16">{isFetching ? <Spinner size="xs" /> : null}</Th> <Th className="w-16">{isFetching ? <Spinner size="xs" /> : null}</Th>
</Tr> </Tr>
</THead> </THead>
@@ -303,7 +309,8 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
<Select <Select
value={role === "custom" ? (customRole?.slug as string) : role} value={role === "custom" ? (customRole?.slug as string) : role}
isDisabled={!isAllowed} isDisabled={!isAllowed}
className="w-48 bg-mineshaft-600" className="h-8 w-48 bg-mineshaft-700"
position="popper"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800" dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
onValueChange={(selectedRole) => onValueChange={(selectedRole) =>
handleChangeRole({ handleChangeRole({
@@ -324,21 +331,24 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
</Td> </Td>
<Td> <Td>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg"> <DropdownMenuTrigger asChild>
<div className="flex justify-center hover:text-primary-400 data-[state=open]:text-primary-400"> <IconButton
<FontAwesomeIcon size="sm" icon={faEllipsis} /> ariaLabel="Options"
</div> className="w-6"
colorSchema="secondary"
variant="plain"
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="mt-3 p-1"> <DropdownMenuContent sideOffset={2} align="end">
<OrgPermissionCan <OrgPermissionCan
I={OrgPermissionIdentityActions.Edit} I={OrgPermissionIdentityActions.Edit}
a={OrgPermissionSubjects.Identity} a={OrgPermissionSubjects.Identity}
> >
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
className={twMerge( icon={<FontAwesomeIcon icon={faEdit} />}
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
navigate({ navigate({
@@ -348,7 +358,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
} }
}); });
}} }}
disabled={!isAllowed} isDisabled={!isAllowed}
> >
Edit Identity Edit Identity
</DropdownMenuItem> </DropdownMenuItem>
@@ -360,11 +370,6 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
> >
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
className={twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
handlePopUpOpen("deleteIdentity", { handlePopUpOpen("deleteIdentity", {
@@ -372,7 +377,8 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
name name
}); });
}} }}
disabled={!isAllowed} isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faTrash} />}
> >
Delete Identity Delete Identity
</DropdownMenuItem> </DropdownMenuItem>
@@ -398,7 +404,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
{!isPending && data && data?.identities.length === 0 && ( {!isPending && data && data?.identities.length === 0 && (
<EmptyState <EmptyState
title={ title={
debouncedSearch.trim().length > 0 || filteredRoles?.length > 0 debouncedSearch.trim().length > 0 || filter.roles?.length > 0
? "No identities match search filter" ? "No identities match search filter"
: "No identities have been created in this organization" : "No identities have been created in this organization"
} }
@@ -115,12 +115,12 @@ export const OrgMembersSection = () => {
return ( return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between"> <div className="mb-4 flex items-center justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Users</p> <p className="text-xl font-semibold text-mineshaft-100">Users</p>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Member}> <OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Member}>
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
colorSchema="primary" colorSchema="secondary"
type="submit" type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handleAddMemberModal()} onClick={() => handleAddMemberModal()}
@@ -4,11 +4,14 @@ import {
faArrowUp, faArrowUp,
faCheckCircle, faCheckCircle,
faChevronRight, faChevronRight,
faEllipsis, faEdit,
faEllipsisV,
faFilter, faFilter,
faMagnifyingGlass, faMagnifyingGlass,
faSearch, faSearch,
faUsers faUsers,
faUserSlash,
faUserXmark
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router"; import { useNavigate } from "@tanstack/react-router";
@@ -79,7 +82,8 @@ type Props = {
enum OrgMembersOrderBy { enum OrgMembersOrderBy {
Name = "firstName", Name = "firstName",
Email = "email" Email = "email",
Role = "role"
} }
type Filter = { type Filter = {
@@ -229,6 +233,16 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
valueOne = memberOne.user.email || memberOne.inviteEmail; valueOne = memberOne.user.email || memberOne.inviteEmail;
valueTwo = memberTwo.user.email || memberTwo.inviteEmail; valueTwo = memberTwo.user.email || memberTwo.inviteEmail;
break; 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: case OrgMembersOrderBy.Name:
default: default:
valueOne = memberOne.user.firstName; valueOne = memberOne.user.firstName;
@@ -284,7 +298,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
variant="plain" variant="plain"
size="sm" size="sm"
className={twMerge( 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" isTableFiltered && "border-primary/50 text-primary"
)} )}
> >
@@ -378,7 +392,26 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
</IconButton> </IconButton>
</div> </div>
</Th> </Th>
<Th>Role</Th> <Th>
<div className="flex items-center">
Role
<IconButton
variant="plain"
className={`ml-2 ${orderBy === OrgMembersOrderBy.Role ? "" : "opacity-30"}`}
ariaLabel="sort"
onClick={() => handleSort(OrgMembersOrderBy.Role)}
>
<FontAwesomeIcon
icon={
orderDirection === OrderByDirection.DESC &&
orderBy === OrgMembersOrderBy.Role
? faArrowUp
: faArrowDown
}
/>
</IconButton>
</div>
</Th>
<Th className="w-5" /> <Th className="w-5" />
</Tr> </Tr>
</THead> </THead>
@@ -398,7 +431,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
isActive isActive
}) => { }) => {
const name = 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 email = u?.email || inviteEmail;
const username = u?.username ?? inviteEmail ?? "-"; const username = u?.username ?? inviteEmail ?? "-";
return ( return (
@@ -415,7 +448,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
} }
> >
<Td className={isActive ? "" : "text-mineshaft-400"}> <Td className={isActive ? "" : "text-mineshaft-400"}>
{name} {name ?? <span className="text-mineshaft-400">Not Set</span>}
{u.superAdmin && ( {u.superAdmin && (
<Badge variant="primary" className="ml-2"> <Badge variant="primary" className="ml-2">
Server Admin Server Admin
@@ -445,7 +478,8 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
<Select <Select
value={role === "custom" ? findRoleFromId(roleId)?.slug : role} value={role === "custom" ? findRoleFromId(roleId)?.slug : role}
isDisabled={userId === u?.id || !isAllowed} isDisabled={userId === u?.id || !isAllowed}
className="w-48 bg-mineshaft-600" className="h-8 w-48 bg-mineshaft-700"
position="popper"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800" dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
onValueChange={(selectedRole) => onValueChange={(selectedRole) =>
onRoleChange(orgMembershipId, selectedRole) onRoleChange(orgMembershipId, selectedRole)
@@ -468,7 +502,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
serverDetails?.emailConfigured && ( serverDetails?.emailConfigured && (
<Button <Button
isDisabled={!isAllowed} isDisabled={!isAllowed}
className="w-48" className="h-8 w-48 border-mineshaft-600 bg-mineshaft-700 font-normal"
colorSchema="primary" colorSchema="primary"
variant="outline_bg" variant="outline_bg"
onClick={(e) => { onClick={(e) => {
@@ -476,7 +510,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
e.stopPropagation(); e.stopPropagation();
}} }}
> >
Resend invite Resend Invite
</Button> </Button>
)} )}
</> </>
@@ -484,124 +518,113 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
</OrgPermissionCan> </OrgPermissionCan>
</Td> </Td>
<Td> <Td>
{userId !== u?.id && ( <DropdownMenu>
<DropdownMenu> <DropdownMenuTrigger asChild>
<DropdownMenuTrigger asChild className="rounded-lg"> <IconButton
<div className="hover:text-primary-400 data-[state=open]:text-primary-400"> ariaLabel="Options"
<FontAwesomeIcon size="sm" icon={faEllipsis} /> colorSchema="secondary"
</div> className={twMerge("w-6", userId === u?.id && "opacity-50")}
</DropdownMenuTrigger> variant="plain"
<DropdownMenuContent align="start" className="p-1"> isDisabled={userId === u?.id}
<OrgPermissionCan >
I={OrgPermissionActions.Edit} <FontAwesomeIcon icon={faEllipsisV} />
a={OrgPermissionSubjects.Member} </IconButton>
> </DropdownMenuTrigger>
{(isAllowed) => ( <DropdownMenuContent sideOffset={2} align="end">
<DropdownMenuItem <OrgPermissionCan
className={twMerge( I={OrgPermissionActions.Edit}
!isAllowed && a={OrgPermissionSubjects.Member}
"pointer-events-none cursor-not-allowed opacity-50" >
)} {(isAllowed) => (
onClick={(e) => { <DropdownMenuItem
e.stopPropagation(); onClick={(e) => {
navigate({ e.stopPropagation();
to: "/organization/members/$membershipId" as const, navigate({
params: { to: "/organization/members/$membershipId" as const,
membershipId: orgMembershipId params: {
} membershipId: orgMembershipId
}
});
}}
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faEdit} />}
>
Edit User
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Member}
>
{(isAllowed) => (
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faUserSlash} />}
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;
disabled={!isAllowed}
>
Edit User
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Member}
>
{(isAllowed) => (
<DropdownMenuItem
className={
isActive
? twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)
: ""
} }
onClick={async (e) => {
e.stopPropagation();
if (currentOrg?.scimEnabled) { if (!isActive) {
createNotification({ // activate user
text: "You cannot manage users from Infisical when org-level auth is enforced for your organization", await updateOrgMembership({
type: "error" organizationId: orgId,
}); membershipId: orgMembershipId,
return; isActive: true
}
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`}
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Member}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
if (currentOrg?.scimEnabled && isActive) { return;
createNotification({ }
text: "You cannot manage users from Infisical when org-level auth is enforced for your organization",
type: "error"
});
return;
}
handlePopUpOpen("removeMember", { // deactivate user
orgMembershipId, handlePopUpOpen("deactivateMember", {
username orgMembershipId,
username
});
}}
isDisabled={!isAllowed}
>
{`${isActive ? "Deactivate" : "Activate"} User`}
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Member}
>
{(isAllowed) => (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
if (currentOrg?.scimEnabled && isActive) {
createNotification({
text: "You cannot manage users from Infisical when org-level auth is enforced for your organization",
type: "error"
}); });
}} return;
disabled={!isAllowed} }
>
Remove User handlePopUpOpen("removeMember", {
</DropdownMenuItem> orgMembershipId,
)} username
</OrgPermissionCan> });
</DropdownMenuContent> }}
</DropdownMenu> isDisabled={!isAllowed}
)} icon={<FontAwesomeIcon icon={faUserXmark} />}
>
Remove User
</DropdownMenuItem>
)}
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Td> </Td>
</Tr> </Tr>
); );
@@ -6,10 +6,10 @@ export const OrgRoleTabSection = () => {
return ( return (
<motion.div <motion.div
key="role-list" key="role-list"
transition={{ duration: 0.1 }} transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: -30 }} initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }} animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -30 }} exit={{ opacity: 0, translateX: 30 }}
> >
<OrgRoleTable /> <OrgRoleTable />
</motion.div> </motion.div>
@@ -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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router"; import { useNavigate } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
@@ -14,6 +27,10 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
EmptyState,
IconButton,
Input,
Pagination,
Table, Table,
TableContainer, TableContainer,
TableSkeleton, TableSkeleton,
@@ -30,13 +47,25 @@ import {
useOrganization, useOrganization,
useSubscription useSubscription
} from "@app/context"; } from "@app/context";
import { isCustomOrgRole } from "@app/helpers/roles"; import { isCustomOrgRole, isCustomProjectRole } from "@app/helpers/roles";
import { usePopUp } from "@app/hooks"; import {
getUserTablePreference,
PreferenceKey,
setUserTablePreference
} from "@app/helpers/userTablePreferences";
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
import { useDeleteOrgRole, useGetOrgRoles, useUpdateOrg } from "@app/hooks/api"; 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 { TOrgRole } from "@app/hooks/api/roles/types";
import { DuplicateOrgRoleModal } from "@app/pages/organization/RoleByIDPage/components/DuplicateOrgRoleModal"; import { DuplicateOrgRoleModal } from "@app/pages/organization/RoleByIDPage/components/DuplicateOrgRoleModal";
import { RoleModal } from "@app/pages/organization/RoleByIDPage/components/RoleModal"; import { RoleModal } from "@app/pages/organization/RoleByIDPage/components/RoleModal";
enum RolesOrderBy {
Name = "name",
Slug = "slug",
Type = "type"
}
export const OrgRoleTable = () => { export const OrgRoleTable = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { currentOrg } = useOrganization(); 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>(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 ( return (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between"> <div className="mb-4 flex items-center justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Organization Roles</p> <p className="text-xl font-semibold text-mineshaft-100">Organization Roles</p>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Role}> <OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Role}>
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
colorSchema="primary" colorSchema="secondary"
type="submit" type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => { onClick={() => {
@@ -113,18 +217,63 @@ export const OrgRoleTable = () => {
)} )}
</OrgPermissionCan> </OrgPermissionCan>
</div> </div>
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search roles..."
className="flex-1"
containerClassName="mb-4"
/>
<TableContainer> <TableContainer>
<Table> <Table>
<THead> <THead>
<Tr> <Tr>
<Th>Name</Th> <Th>
<Th>Slug</Th> <div className="flex items-center">
Name
<IconButton
variant="plain"
className={getClassName(RolesOrderBy.Name)}
ariaLabel="sort"
onClick={() => handleSort(RolesOrderBy.Name)}
>
<FontAwesomeIcon icon={getColSortIcon(RolesOrderBy.Name)} />
</IconButton>
</div>
</Th>
<Th>
<div className="flex items-center">
Slug
<IconButton
variant="plain"
className={getClassName(RolesOrderBy.Slug)}
ariaLabel="sort"
onClick={() => handleSort(RolesOrderBy.Slug)}
>
<FontAwesomeIcon icon={getColSortIcon(RolesOrderBy.Slug)} />
</IconButton>
</div>
</Th>
<Th>
<div className="flex items-center">
Type
<IconButton
variant="plain"
className={getClassName(RolesOrderBy.Type)}
ariaLabel="sort"
onClick={() => handleSort(RolesOrderBy.Type)}
>
<FontAwesomeIcon icon={getColSortIcon(RolesOrderBy.Type)} />
</IconButton>
</div>
</Th>
<Th aria-label="actions" className="w-5" /> <Th aria-label="actions" className="w-5" />
</Tr> </Tr>
</THead> </THead>
<TBody> <TBody>
{isRolesLoading && <TableSkeleton columns={3} innerKey="org-roles" />} {isRolesLoading && <TableSkeleton columns={4} innerKey="org-roles" />}
{roles?.map((role) => { {filteredRoles?.slice(offset, perPage * page).map((role) => {
const { id, name, slug } = role; const { id, name, slug } = role;
const isNonMutatable = ["owner", "admin", "member", "no-access"].includes(slug); const isNonMutatable = ["owner", "admin", "member", "no-access"].includes(slug);
const isDefaultOrgRole = isCustomOrgRole(slug) const isDefaultOrgRole = isCustomOrgRole(slug)
@@ -162,23 +311,30 @@ export const OrgRoleTable = () => {
<Td className="max-w-md overflow-hidden text-ellipsis whitespace-nowrap"> <Td className="max-w-md overflow-hidden text-ellipsis whitespace-nowrap">
{slug} {slug}
</Td> </Td>
<Td>
<Badge className="w-min whitespace-nowrap bg-mineshaft-400/50 text-bunker-200">
{isCustomProjectRole(slug) ? "Custom" : "Default"}
</Badge>
</Td>
<Td> <Td>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg"> <DropdownMenuTrigger asChild>
<div className="hover:text-primary-400 data-[state=open]:text-primary-400"> <IconButton
<FontAwesomeIcon size="sm" icon={faEllipsis} /> ariaLabel="Options"
</div> colorSchema="secondary"
className="w-6"
variant="plain"
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1"> <DropdownMenuContent className="min-w-[12rem]" sideOffset={2} align="end">
<OrgPermissionCan <OrgPermissionCan
I={OrgPermissionActions.Edit} I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Role} a={OrgPermissionSubjects.Role}
> >
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
navigate({ navigate({
@@ -188,7 +344,8 @@ export const OrgRoleTable = () => {
} }
}); });
}} }}
disabled={!isAllowed} isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={isNonMutatable ? faEye : faEdit} />}
> >
{`${isNonMutatable ? "View" : "Edit"} Role`} {`${isNonMutatable ? "View" : "Edit"} Role`}
</DropdownMenuItem> </DropdownMenuItem>
@@ -200,14 +357,12 @@ export const OrgRoleTable = () => {
> >
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
handlePopUpOpen("duplicateRole", role); handlePopUpOpen("duplicateRole", role);
}} }}
disabled={!isAllowed} isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faCopy} />}
> >
Duplicate Role Duplicate Role
</DropdownMenuItem> </DropdownMenuItem>
@@ -220,14 +375,12 @@ export const OrgRoleTable = () => {
> >
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
className={twMerge( isDisabled={!isAllowed}
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
disabled={!isAllowed}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
handleSetRoleAsDefault(slug); handleSetRoleAsDefault(slug);
}} }}
icon={<FontAwesomeIcon icon={faIdBadge} />}
> >
Set as Default Role Set as Default Role
</DropdownMenuItem> </DropdownMenuItem>
@@ -250,16 +403,12 @@ export const OrgRoleTable = () => {
> >
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
className={twMerge(
isAllowed && !isDefaultOrgRole
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
handlePopUpOpen("deleteRole", role); handlePopUpOpen("deleteRole", role);
}} }}
disabled={!isAllowed || isDefaultOrgRole} icon={<FontAwesomeIcon icon={faTrash} />}
isDisabled={!isAllowed || isDefaultOrgRole}
> >
Delete Role Delete Role
</DropdownMenuItem> </DropdownMenuItem>
@@ -276,6 +425,25 @@ export const OrgRoleTable = () => {
})} })}
</TBody> </TBody>
</Table> </Table>
{Boolean(filteredRoles?.length) && (
<Pagination
count={filteredRoles!.length}
page={page}
perPage={perPage}
onChangePage={setPage}
onChangePerPage={handlePerPageChange}
/>
)}
{!filteredRoles?.length && !isRolesLoading && (
<EmptyState
title={
roles?.length
? "No roles match search..."
: "This organization does not have any roles"
}
icon={roles?.length ? faSearch : undefined}
/>
)}
</TableContainer> </TableContainer>
<RoleModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} /> <RoleModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal <DeleteActionModal
@@ -197,7 +197,7 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
variant="plain" variant="plain"
size="sm" size="sm"
className={twMerge( 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" isTableFiltered && "border-primary/50 text-primary"
)} )}
> >
@@ -298,7 +298,8 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
{!isMembersLoading && {!isMembersLoading &&
filteredUsers.slice(offset, perPage * page).map((projectMember) => { filteredUsers.slice(offset, perPage * page).map((projectMember) => {
const { user: u, inviteEmail, id: membershipId, roles } = 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; const email = u?.email || inviteEmail;
return ( return (
@@ -328,7 +329,7 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
}) })
} }
> >
<Td>{name}</Td> <Td>{name ?? <span className="text-mineshaft-400">Not Set</span>}</Td>
<Td>{email}</Td> <Td>{email}</Td>
<Td> <Td>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
@@ -236,7 +236,7 @@ export const ProjectRoleList = () => {
</Tr> </Tr>
</THead> </THead>
<TBody> <TBody>
{isRolesLoading && <TableSkeleton columns={3} innerKey="project-roles" />} {isRolesLoading && <TableSkeleton columns={4} innerKey="project-roles" />}
{filteredRoles?.slice(offset, perPage * page).map((role) => { {filteredRoles?.slice(offset, perPage * page).map((role) => {
const { id, name, slug } = role; const { id, name, slug } = role;
const isNonMutatable = Object.values(ProjectMembershipRole).includes( const isNonMutatable = Object.values(ProjectMembershipRole).includes(