improvement: add bulk delete users support, bulk actions server admin table support, overflow/truncation and dropdown improvements

This commit is contained in:
Scott Wilson
2025-07-31 16:14:13 -07:00
parent 7357d377e1
commit 77a8cd9efc
6 changed files with 483 additions and 134 deletions

View File

@@ -464,6 +464,42 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
} }
}); });
server.route({
method: "DELETE",
url: "/user-management/users",
config: {
rateLimit: writeLimit
},
schema: {
body: z.object({
userIds: z.string().array()
}),
response: {
200: z.object({
users: UsersSchema.pick({
username: true,
firstName: true,
lastName: true,
email: true,
id: true
}).array()
})
}
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
handler: async (req) => {
const users = await server.services.superAdmin.deleteUsers(req.body.userIds);
return {
users
};
}
});
server.route({ server.route({
method: "PATCH", method: "PATCH",
url: "/user-management/users/:userId/admin-access", url: "/user-management/users/:userId/admin-access",

View File

@@ -704,10 +704,39 @@ export const superAdminServiceFactory = ({
}; };
const deleteUser = async (userId: string) => { const deleteUser = async (userId: string) => {
const superAdmins = await userDAL.find({
superAdmin: true
});
if (superAdmins.length === 1 && superAdmins[0].id === userId) {
throw new BadRequestError({
message: "Cannot delete the only server admin on this instance. Add another server admin to delete this user."
});
}
const user = await userDAL.deleteById(userId); const user = await userDAL.deleteById(userId);
return user; return user;
}; };
const deleteUsers = async (userIds: string[]) => {
const superAdmins = await userDAL.find({
superAdmin: true
});
if (superAdmins.every((superAdmin) => userIds.includes(superAdmin.id))) {
throw new BadRequestError({
message: "Instance must have at least one server admin. Add another server admin to delete these users."
});
}
const users = await userDAL.delete({
$in: {
id: userIds
}
});
return users;
};
const deleteIdentitySuperAdminAccess = async (identityId: string, actorId: string) => { const deleteIdentitySuperAdminAccess = async (identityId: string, actorId: string) => {
const identity = await identityDAL.findById(identityId); const identity = await identityDAL.findById(identityId);
if (!identity) { if (!identity) {
@@ -730,6 +759,17 @@ export const superAdminServiceFactory = ({
throw new NotFoundError({ name: "User", message: "User not found" }); throw new NotFoundError({ name: "User", message: "User not found" });
} }
const superAdmins = await userDAL.find({
superAdmin: true
});
if (superAdmins.length === 1 && superAdmins[0].id === userId) {
throw new BadRequestError({
message:
"Cannot remove the only server admin on this instance. Add another server admin to remove status for this user."
});
}
const updatedUser = userDAL.updateById(userId, { superAdmin: false }); const updatedUser = userDAL.updateById(userId, { superAdmin: false });
return updatedUser; return updatedUser;
@@ -913,6 +953,7 @@ export const superAdminServiceFactory = ({
initializeAdminIntegrationConfigSync, initializeAdminIntegrationConfigSync,
initializeEnvConfigSync, initializeEnvConfigSync,
getEnvOverrides, getEnvOverrides,
getEnvOverridesOrganized getEnvOverridesOrganized,
deleteUsers
}; };
}; };

View File

@@ -68,6 +68,25 @@ export const useAdminDeleteUser = () => {
}); });
}; };
export const useAdminBulkDeleteUsers = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (userIds: string[]) => {
await apiRequest.delete("/api/v1/admin/user-management/users", {
data: { userIds }
});
return {};
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [adminStandaloneKeys.getUsers]
});
queryClient.invalidateQueries({ queryKey: adminStandaloneKeys.getOrganizations });
}
});
};
export const useAdminDeleteOrganizationMembership = () => { export const useAdminDeleteOrganizationMembership = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation<object, object, { organizationId: string; membershipId: string }>({ return useMutation<object, object, { organizationId: string; membershipId: string }>({

View File

@@ -1,5 +1,11 @@
import { useState } from "react"; import { useState } from "react";
import { faEllipsis, faMagnifyingGlass, faServer } from "@fortawesome/free-solid-svg-icons"; import {
faEllipsisV,
faMagnifyingGlass,
faServer,
faShieldHalved,
faXmark
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
@@ -12,6 +18,7 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
EmptyState, EmptyState,
IconButton,
Input, Input,
Table, Table,
TableContainer, TableContainer,
@@ -88,18 +95,33 @@ const IdentityPanelTable = ({
{isInstanceAdmin && ( {isInstanceAdmin && (
<div className="flex justify-end"> <div className="flex justify-end">
<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 sideOffset={2} align="end">
{isInstanceAdmin && ( {isInstanceAdmin && (
<DropdownMenuItem <DropdownMenuItem
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
handlePopUpOpen("removeServerAdmin", { name, id }); handlePopUpOpen("removeServerAdmin", { name, id });
}} }}
icon={
<div className="relative">
<FontAwesomeIcon icon={faShieldHalved} />
<FontAwesomeIcon
className="absolute -bottom-[0.01rem] -right-1"
size="2xs"
icon={faXmark}
/>
</div>
}
> >
Remove Server Admin Remove Server Admin
</DropdownMenuItem> </DropdownMenuItem>
@@ -125,7 +147,7 @@ const IdentityPanelTable = ({
isDisabled={isFetchingNextPage || !hasNextPage} isDisabled={isFetchingNextPage || !hasNextPage}
onClick={() => fetchNextPage()} onClick={() => fetchNextPage()}
> >
{hasNextPage ? "Load More" : "End of list"} {hasNextPage ? "Load More" : "End of List"}
</Button> </Button>
)} )}
</div> </div>

View File

@@ -1,13 +1,19 @@
import { useState } from "react"; import { Dispatch, SetStateAction, useState } from "react";
import { import {
faCheckCircle, faCheckCircle,
faEllipsis, faEllipsisV,
faFilter, faFilter,
faMagnifyingGlass, faMagnifyingGlass,
faShieldHalved,
faTrash,
faUsers, faUsers,
faUserShield faUserShield,
faUserXmark,
faWarning,
faXmark
} 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 { InfiniteData } from "@tanstack/react-query";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
@@ -15,6 +21,7 @@ import { createNotification } from "@app/components/notifications";
import { import {
Badge, Badge,
Button, Button,
Checkbox,
DeleteActionModal, DeleteActionModal,
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@@ -31,23 +38,37 @@ import {
Td, Td,
Th, Th,
THead, THead,
Tooltip,
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { useSubscription } from "@app/context"; import { useSubscription, useUser } from "@app/context";
import { useDebounce, usePopUp } from "@app/hooks"; import { useDebounce, usePopUp } from "@app/hooks";
import { import {
useAdminBulkDeleteUsers,
useAdminDeleteUser, useAdminDeleteUser,
useAdminGetUsers, useAdminGetUsers,
useAdminGrantServerAdminAccess, useAdminGrantServerAdminAccess,
useRemoveUserServerAdminAccess useRemoveUserServerAdminAccess
} from "@app/hooks/api"; } from "@app/hooks/api";
import { User } from "@app/hooks/api/users/types";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
const addServerAdminUpgradePlanMessage = "Granting another user Server Admin permissions"; const addServerAdminUpgradePlanMessage = "Granting another user Server Admin permissions";
const removeServerAdminUpgradePlanMessage = "Removing Server Admin permissions from user"; const removeServerAdminUpgradePlanMessage = "Removing Server Admin permissions from user";
const UserPanelTable = ({ const UserPanelTable = ({
handlePopUpOpen handlePopUpOpen,
users: usersPages,
isPending,
adminsOnly,
searchUserFilter,
setSearchUserFilter,
setAdminsOnly,
isFetchingNextPage,
fetchNextPage,
hasNextPage,
selectedUsers,
setSelectedUsers
}: { }: {
handlePopUpOpen: ( handlePopUpOpen: (
popUpName: keyof UsePopUpState< popUpName: keyof UsePopUpState<
@@ -59,21 +80,38 @@ const UserPanelTable = ({
message?: string; message?: string;
} }
) => void; ) => void;
isPending: boolean;
users: InfiniteData<User[], unknown> | undefined;
adminsOnly: boolean;
setAdminsOnly: (adminsOnly: boolean) => void;
searchUserFilter: string;
setSearchUserFilter: (filter: string) => void;
selectedUsers: User[];
setSelectedUsers: Dispatch<SetStateAction<User[]>>;
isFetchingNextPage: boolean;
fetchNextPage: () => void;
hasNextPage: boolean;
}) => { }) => {
const [searchUserFilter, setSearchUserFilter] = useState("");
const [adminsOnly, setAdminsOnly] = useState(false);
const [debouncedSearchTerm] = useDebounce(searchUserFilter, 500);
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const { data, isPending, isFetchingNextPage, hasNextPage, fetchNextPage } = useAdminGetUsers({ const users = usersPages?.pages.flat();
limit: 20,
searchTerm: debouncedSearchTerm,
adminsOnly
});
const isEmpty = !isPending && !data?.pages?.[0].length; const isEmpty = !isPending && !users?.length;
const isTableFiltered = Boolean(adminsOnly); const isTableFiltered = Boolean(adminsOnly);
const selectedUserIds = selectedUsers.map((user) => user.id);
const isPageSelected = users?.length
? users.every((user) => selectedUserIds.includes(user.id))
: false;
// eslint-disable-next-line no-nested-ternary
const isPageIndeterminate = isPageSelected
? false
: users?.length
? users?.some((user) => selectedUserIds.includes(user.id))
: false;
return ( return (
<> <>
<div className="flex gap-2"> <div className="flex gap-2">
@@ -121,90 +159,144 @@ const UserPanelTable = ({
<Table> <Table>
<THead> <THead>
<Tr> <Tr>
<Th className="w-5">
<Checkbox
id="member-page-select"
isChecked={isPageSelected || isPageIndeterminate}
isIndeterminate={isPageIndeterminate}
onCheckedChange={() => {
if (isPageSelected) {
setSelectedUsers((prev) =>
prev.filter((u) => !users?.find((user) => user.id === u.id))
);
} else {
setSelectedUsers((prev) => [
...prev,
...(users?.filter((u) => !prev.find((user) => user.id === u.id)) ?? [])
]);
}
}}
/>
</Th>
<Th className="w-5/12">Name</Th> <Th className="w-5/12">Name</Th>
<Th className="w-5/12">Username</Th> <Th className="w-1/2">Username</Th>
<Th className="w-5" /> <Th className="w-2/12" />
</Tr> </Tr>
</THead> </THead>
<TBody> <TBody>
{isPending && <TableSkeleton columns={4} innerKey="users" />} {isPending && <TableSkeleton columns={4} innerKey="users" />}
{!isPending && {!isPending &&
data?.pages?.map((users) => users?.map((user) => {
users.map(({ username, email, firstName, lastName, id, superAdmin }) => { const { username, email, firstName, lastName, id, superAdmin } = user;
const name = firstName || lastName ? `${firstName} ${lastName}` : "-"; const name = firstName || lastName ? `${firstName} ${lastName}` : null;
return ( const isSelected = selectedUserIds.includes(id);
<Tr key={`user-${id}`} className="w-full"> return (
<Td className="w-5/12"> <Tr key={`user-${id}`} className="w-full">
{name} <Td>
<Checkbox
id={`select-user-${id}`}
isChecked={isSelected}
onClick={(e) => {
e.stopPropagation();
setSelectedUsers((prev) =>
isSelected ? prev.filter((u) => u.id !== id) : [...prev, user]
);
}}
/>
</Td>
<Td className="w-5/12 max-w-0">
<div className="flex items-center">
<p className="truncate">
{name ?? <span className="text-mineshaft-400">Not Set</span>}
</p>
{superAdmin && ( {superAdmin && (
<Badge variant="primary" className="ml-2"> <Badge variant="primary" className="ml-2 whitespace-nowrap">
Server Admin Server Admin
</Badge> </Badge>
)} )}
</Td> </div>
<Td className="w-5/12">{email}</Td> </Td>
<Td> <Td className="w-5/12 max-w-0">
<div className="flex justify-end"> <p className="truncate">{email}</p>
<DropdownMenu> </Td>
<DropdownMenuTrigger asChild className="rounded-lg"> <Td>
<div className="hover:text-primary-400 data-[state=open]:text-primary-400"> <div className="flex justify-end">
<FontAwesomeIcon size="sm" icon={faEllipsis} /> <DropdownMenu>
</div> <DropdownMenuTrigger asChild>
</DropdownMenuTrigger> <IconButton
<DropdownMenuContent align="start" className="p-1"> ariaLabel="Options"
colorSchema="secondary"
className="w-6"
variant="plain"
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={2} align="end">
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("removeUser", { username, id });
}}
icon={<FontAwesomeIcon icon={faUserXmark} />}
>
Remove User
</DropdownMenuItem>
{!superAdmin && (
<DropdownMenuItem <DropdownMenuItem
icon={<FontAwesomeIcon icon={faUserShield} />}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
handlePopUpOpen("removeUser", { username, id }); if (!subscription?.instanceUserManagement) {
handlePopUpOpen("upgradePlan", {
username,
id,
message: addServerAdminUpgradePlanMessage
});
return;
}
handlePopUpOpen("upgradeToServerAdmin", { username, id });
}} }}
> >
Remove User Make User Server Admin
</DropdownMenuItem> </DropdownMenuItem>
{!superAdmin && ( )}
<DropdownMenuItem {superAdmin && (
onClick={(e) => { <DropdownMenuItem
e.stopPropagation(); icon={
if (!subscription?.instanceUserManagement) { <div className="relative">
handlePopUpOpen("upgradePlan", { <FontAwesomeIcon icon={faShieldHalved} />
username, <FontAwesomeIcon
id, className="absolute -bottom-[0.01rem] -right-1"
message: addServerAdminUpgradePlanMessage size="2xs"
}); icon={faXmark}
return; />
} </div>
handlePopUpOpen("upgradeToServerAdmin", { username, id }); }
}} onClick={(e) => {
> e.stopPropagation();
Make User Server Admin if (!subscription?.instanceUserManagement) {
</DropdownMenuItem> handlePopUpOpen("upgradePlan", {
)} username,
{superAdmin && ( id,
<DropdownMenuItem message: removeServerAdminUpgradePlanMessage
onClick={(e) => { });
e.stopPropagation(); return;
if (!subscription?.instanceUserManagement) { }
handlePopUpOpen("upgradePlan", { handlePopUpOpen("removeServerAdmin", { username, id });
username, }}
id, >
message: removeServerAdminUpgradePlanMessage Remove Server Admin
}); </DropdownMenuItem>
return; )}
} </DropdownMenuContent>
handlePopUpOpen("removeServerAdmin", { username, id }); </DropdownMenu>
}} </div>
> </Td>
Remove Server Admin </Tr>
</DropdownMenuItem> );
)} })}
</DropdownMenuContent>
</DropdownMenu>
</div>
</Td>
</Tr>
);
})
)}
</TBody> </TBody>
</Table> </Table>
{!isPending && isEmpty && <EmptyState title="No users found" icon={faUsers} />} {!isPending && isEmpty && <EmptyState title="No users found" icon={faUsers} />}
@@ -218,7 +310,7 @@ const UserPanelTable = ({
isDisabled={isFetchingNextPage || !hasNextPage} isDisabled={isFetchingNextPage || !hasNextPage}
onClick={() => fetchNextPage()} onClick={() => fetchNextPage()}
> >
{hasNextPage ? "Load More" : "End of list"} {hasNextPage ? "Load More" : "End of List"}
</Button> </Button>
)} )}
</div> </div>
@@ -231,13 +323,36 @@ export const UserIdentitiesTable = () => {
"removeUser", "removeUser",
"upgradePlan", "upgradePlan",
"upgradeToServerAdmin", "upgradeToServerAdmin",
"removeServerAdmin" "removeServerAdmin",
"removeUsers"
] as const); ] as const);
const {
user: { id: userId }
} = useUser();
const { mutateAsync: deleteUser } = useAdminDeleteUser(); const { mutateAsync: deleteUser } = useAdminDeleteUser();
const { mutateAsync: deleteUsers } = useAdminBulkDeleteUsers();
const { mutateAsync: grantAdminAccess } = useAdminGrantServerAdminAccess(); const { mutateAsync: grantAdminAccess } = useAdminGrantServerAdminAccess();
const { mutateAsync: removeAdminAccess } = useRemoveUserServerAdminAccess(); const { mutateAsync: removeAdminAccess } = useRemoveUserServerAdminAccess();
const [selectedUsers, setSelectedUsers] = useState<User[]>([]);
const [searchUserFilter, setSearchUserFilter] = useState("");
const [adminsOnly, setAdminsOnly] = useState(false);
const [debouncedSearchTerm] = useDebounce(searchUserFilter, 500);
const {
data: users,
isPending,
isFetchingNextPage,
hasNextPage,
fetchNextPage
} = useAdminGetUsers({
limit: 20,
searchTerm: debouncedSearchTerm,
adminsOnly
});
const handleRemoveUser = async () => { const handleRemoveUser = async () => {
const { id } = popUp?.removeUser?.data as { id: string; username: string }; const { id } = popUp?.removeUser?.data as { id: string; username: string };
@@ -295,45 +410,158 @@ export const UserIdentitiesTable = () => {
handlePopUpClose("removeServerAdmin"); handlePopUpClose("removeServerAdmin");
}; };
const handleRemoveUsers = async () => {
try {
await deleteUsers(selectedUsers.map((user) => user.id));
createNotification({
text: "Successfully removed users",
type: "success"
});
setSelectedUsers([]);
handlePopUpClose("removeUsers");
} catch {
createNotification({
text: "Failed to remove users",
type: "error"
});
}
};
return ( return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <>
<UserPanelTable handlePopUpOpen={handlePopUpOpen} /> <div
<DeleteActionModal className={twMerge(
isOpen={popUp.removeUser.isOpen} "h-0 flex-shrink-0 overflow-hidden transition-all",
deleteKey="remove" selectedUsers.length > 0 && "h-16"
title={`Are you sure you want to delete User with username ${ )}
(popUp?.removeUser?.data as { id: string; username: string })?.username || "" >
}?`} <div className="flex items-center rounded-md border border-mineshaft-600 bg-mineshaft-800 px-4 py-2 text-bunker-300">
onChange={(isOpen) => handlePopUpToggle("removeUser", isOpen)} <div className="mr-2 text-sm">{selectedUsers.length} Selected</div>
onDeleteApproved={handleRemoveUser} <button
/> type="button"
<DeleteActionModal className="mr-auto text-xs text-mineshaft-400 underline-offset-2 hover:text-mineshaft-200 hover:underline"
isOpen={popUp.upgradeToServerAdmin.isOpen} onClick={() => setSelectedUsers([])}
title={`Are you sure you want to grant Server Admin permissions to ${ >
(popUp?.upgradeToServerAdmin?.data as { id: string; username: string })?.username || "" Unselect All
}?`} </button>
subTitle="" <Button
onChange={(isOpen) => handlePopUpToggle("upgradeToServerAdmin", isOpen)} variant="outline_bg"
deleteKey="confirm" colorSchema="danger"
onDeleteApproved={handleGrantServerAdminAccess} leftIcon={<FontAwesomeIcon icon={faTrash} />}
buttonText="Grant Access" className="ml-2"
/> onClick={() => {
<DeleteActionModal if (!selectedUsers?.length) return;
isOpen={popUp.removeServerAdmin.isOpen}
title={`Are you sure you want to remove Server Admin permissions from ${ handlePopUpOpen("removeUsers");
(popUp?.removeServerAdmin?.data as { id: string; username: string })?.username || "" }}
}?`} size="xs"
subTitle="" >
onChange={(isOpen) => handlePopUpToggle("removeServerAdmin", isOpen)} Delete
deleteKey="confirm" </Button>
onDeleteApproved={handleRemoveServerAdminAccess} </div>
buttonText="Remove Access" </div>
/> <div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<UpgradePlanModal <UserPanelTable
isOpen={popUp.upgradePlan.isOpen} handlePopUpOpen={handlePopUpOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)} users={users}
text={`${popUp?.upgradePlan?.data?.message} is only available on Infisical's Pro plan and above.`} selectedUsers={selectedUsers}
/> setSelectedUsers={setSelectedUsers}
</div> searchUserFilter={searchUserFilter}
setSearchUserFilter={setSearchUserFilter}
isPending={isPending}
adminsOnly={adminsOnly}
setAdminsOnly={setAdminsOnly}
isFetchingNextPage={isFetchingNextPage}
hasNextPage={hasNextPage}
fetchNextPage={fetchNextPage}
/>
<DeleteActionModal
isOpen={popUp.removeUser.isOpen}
deleteKey="remove"
title={`Are you sure you want to delete User with username ${
(popUp?.removeUser?.data as { id: string; username: string })?.username || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("removeUser", isOpen)}
onDeleteApproved={handleRemoveUser}
/>
<DeleteActionModal
isOpen={popUp.upgradeToServerAdmin.isOpen}
title={`Are you sure you want to grant Server Admin permissions to ${
(popUp?.upgradeToServerAdmin?.data as { id: string; username: string })?.username || ""
}?`}
subTitle=""
onChange={(isOpen) => handlePopUpToggle("upgradeToServerAdmin", isOpen)}
deleteKey="confirm"
onDeleteApproved={handleGrantServerAdminAccess}
buttonText="Grant Access"
/>
<DeleteActionModal
isOpen={popUp.removeServerAdmin.isOpen}
title={`Are you sure you want to remove Server Admin permissions from ${
(popUp?.removeServerAdmin?.data as { id: string; username: string })?.username || ""
}?`}
subTitle=""
onChange={(isOpen) => handlePopUpToggle("removeServerAdmin", isOpen)}
deleteKey="confirm"
onDeleteApproved={handleRemoveServerAdminAccess}
buttonText="Remove Access"
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={`${popUp?.upgradePlan?.data?.message} is only available on Infisical's Pro plan and above.`}
/>
<DeleteActionModal
isOpen={popUp.removeUsers.isOpen}
title="Are you sure you want to remove the following users?"
onChange={(isOpen) => handlePopUpToggle("removeUsers", isOpen)}
deleteKey="confirm"
onDeleteApproved={() => handleRemoveUsers()}
buttonText="Remove"
>
<div className="mt-4 text-sm text-mineshaft-400">
The following members will be removed:
</div>
<div className="mt-2 max-h-[20rem] overflow-y-auto rounded border border-mineshaft-600 bg-red/10 p-4 pl-8 text-sm text-red-200">
<ul className="list-disc">
{selectedUsers?.map((user) => {
const email = user.email ?? user.username;
return (
<li key={user.id}>
<div className="flex items-center">
<p>
{user.firstName || user.lastName ? (
<>
{`${`${user.firstName} ${user.lastName}`.trim()} `}(
<span className="break-all">{email}</span>)
</>
) : (
<span className="break-all">{email}</span>
)}{" "}
</p>
{userId === user.id && (
<Tooltip content="Are you sure you want to remove yourself from this instance?">
<div className="inline-block">
<Badge
variant="primary"
className="ml-1 mt-[0.05rem] inline-flex w-min items-center gap-1.5 whitespace-nowrap"
>
<FontAwesomeIcon icon={faWarning} />
<span>Removing Yourself</span>
</Badge>
</div>
</Tooltip>
)}
</div>
</li>
);
})}
</ul>
</div>
</DeleteActionModal>
</div>
</>
); );
}; };

View File

@@ -309,13 +309,16 @@ export const OrgMembersTable = ({
const filteredMembersPage = filteredMembers.slice(offset, perPage * page); const filteredMembersPage = filteredMembers.slice(offset, perPage * page);
const isPageSelected = filteredMembersPage.every((member) => const isPageSelected = filteredMembersPage.length
selectedMemberIds.includes(member.id) ? filteredMembersPage.every((member) => selectedMemberIds.includes(member.id))
); : false;
// eslint-disable-next-line no-nested-ternary
const isPageIndeterminate = isPageSelected const isPageIndeterminate = isPageSelected
? false ? false
: filteredMembersPage.some((member) => selectedMemberIds.includes(member.id)); : filteredMembersPage.length
? filteredMembersPage.some((member) => selectedMemberIds.includes(member.id))
: false;
return ( return (
<div> <div>