mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge pull request #4291 from Infisical/server-admin-bulk-delete
improvement(server-admin): add bulk delete users support, bulk actions server admin table support, overflow/truncation and dropdown improvements
This commit is contained in:
@@ -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({
|
||||
method: "PATCH",
|
||||
url: "/user-management/users/:userId/admin-access",
|
||||
|
||||
@@ -704,10 +704,39 @@ export const superAdminServiceFactory = ({
|
||||
};
|
||||
|
||||
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);
|
||||
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 identity = await identityDAL.findById(identityId);
|
||||
if (!identity) {
|
||||
@@ -730,6 +759,17 @@ export const superAdminServiceFactory = ({
|
||||
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 });
|
||||
|
||||
return updatedUser;
|
||||
@@ -913,6 +953,7 @@ export const superAdminServiceFactory = ({
|
||||
initializeAdminIntegrationConfigSync,
|
||||
initializeEnvConfigSync,
|
||||
getEnvOverrides,
|
||||
getEnvOverridesOrganized
|
||||
getEnvOverridesOrganized,
|
||||
deleteUsers
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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 = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<object, object, { organizationId: string; membershipId: string }>({
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
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 { createNotification } from "@app/components/notifications";
|
||||
@@ -12,6 +18,7 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Input,
|
||||
Table,
|
||||
TableContainer,
|
||||
@@ -88,18 +95,33 @@ const IdentityPanelTable = ({
|
||||
{isInstanceAdmin && (
|
||||
<div className="flex justify-end">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
||||
</div>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Options"
|
||||
colorSchema="secondary"
|
||||
className="w-6"
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
<DropdownMenuContent sideOffset={2} align="end">
|
||||
{isInstanceAdmin && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
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
|
||||
</DropdownMenuItem>
|
||||
@@ -125,7 +147,7 @@ const IdentityPanelTable = ({
|
||||
isDisabled={isFetchingNextPage || !hasNextPage}
|
||||
onClick={() => fetchNextPage()}
|
||||
>
|
||||
{hasNextPage ? "Load More" : "End of list"}
|
||||
{hasNextPage ? "Load More" : "End of List"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { useState } from "react";
|
||||
import { Dispatch, SetStateAction, useState } from "react";
|
||||
import {
|
||||
faCheckCircle,
|
||||
faEllipsis,
|
||||
faEllipsisV,
|
||||
faFilter,
|
||||
faMagnifyingGlass,
|
||||
faShieldHalved,
|
||||
faTrash,
|
||||
faUsers,
|
||||
faUserShield
|
||||
faUserShield,
|
||||
faUserXmark,
|
||||
faWarning,
|
||||
faXmark
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { InfiniteData } from "@tanstack/react-query";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
@@ -15,6 +21,7 @@ import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Checkbox,
|
||||
DeleteActionModal,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -31,23 +38,37 @@ import {
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useSubscription } from "@app/context";
|
||||
import { useSubscription, useUser } from "@app/context";
|
||||
import { useDebounce, usePopUp } from "@app/hooks";
|
||||
import {
|
||||
useAdminBulkDeleteUsers,
|
||||
useAdminDeleteUser,
|
||||
useAdminGetUsers,
|
||||
useAdminGrantServerAdminAccess,
|
||||
useRemoveUserServerAdminAccess
|
||||
} from "@app/hooks/api";
|
||||
import { User } from "@app/hooks/api/users/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const addServerAdminUpgradePlanMessage = "Granting another user Server Admin permissions";
|
||||
const removeServerAdminUpgradePlanMessage = "Removing Server Admin permissions from user";
|
||||
|
||||
const UserPanelTable = ({
|
||||
handlePopUpOpen
|
||||
handlePopUpOpen,
|
||||
users: usersPages,
|
||||
isPending,
|
||||
adminsOnly,
|
||||
searchUserFilter,
|
||||
setSearchUserFilter,
|
||||
setAdminsOnly,
|
||||
isFetchingNextPage,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
selectedUsers,
|
||||
setSelectedUsers
|
||||
}: {
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<
|
||||
@@ -59,21 +80,38 @@ const UserPanelTable = ({
|
||||
message?: string;
|
||||
}
|
||||
) => 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 { data, isPending, isFetchingNextPage, hasNextPage, fetchNextPage } = useAdminGetUsers({
|
||||
limit: 20,
|
||||
searchTerm: debouncedSearchTerm,
|
||||
adminsOnly
|
||||
});
|
||||
const users = usersPages?.pages.flat();
|
||||
|
||||
const isEmpty = !isPending && !data?.pages?.[0].length;
|
||||
const isEmpty = !isPending && !users?.length;
|
||||
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 (
|
||||
<>
|
||||
<div className="flex gap-2">
|
||||
@@ -121,90 +159,144 @@ const UserPanelTable = ({
|
||||
<Table>
|
||||
<THead>
|
||||
<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">Username</Th>
|
||||
<Th className="w-5" />
|
||||
<Th className="w-1/2">Username</Th>
|
||||
<Th className="w-2/12" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isPending && <TableSkeleton columns={4} innerKey="users" />}
|
||||
{!isPending &&
|
||||
data?.pages?.map((users) =>
|
||||
users.map(({ username, email, firstName, lastName, id, superAdmin }) => {
|
||||
const name = firstName || lastName ? `${firstName} ${lastName}` : "-";
|
||||
users?.map((user) => {
|
||||
const { username, email, firstName, lastName, id, superAdmin } = user;
|
||||
const name = firstName || lastName ? `${firstName} ${lastName}` : null;
|
||||
|
||||
return (
|
||||
<Tr key={`user-${id}`} className="w-full">
|
||||
<Td className="w-5/12">
|
||||
{name}
|
||||
const isSelected = selectedUserIds.includes(id);
|
||||
return (
|
||||
<Tr key={`user-${id}`} className="w-full">
|
||||
<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 && (
|
||||
<Badge variant="primary" className="ml-2">
|
||||
<Badge variant="primary" className="ml-2 whitespace-nowrap">
|
||||
Server Admin
|
||||
</Badge>
|
||||
)}
|
||||
</Td>
|
||||
<Td className="w-5/12">{email}</Td>
|
||||
<Td>
|
||||
<div className="flex justify-end">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
</div>
|
||||
</Td>
|
||||
<Td className="w-5/12 max-w-0">
|
||||
<p className="truncate">{email}</p>
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex justify-end">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
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
|
||||
icon={<FontAwesomeIcon icon={faUserShield} />}
|
||||
onClick={(e) => {
|
||||
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>
|
||||
{!superAdmin && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!subscription?.instanceUserManagement) {
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
username,
|
||||
id,
|
||||
message: addServerAdminUpgradePlanMessage
|
||||
});
|
||||
return;
|
||||
}
|
||||
handlePopUpOpen("upgradeToServerAdmin", { username, id });
|
||||
}}
|
||||
>
|
||||
Make User Server Admin
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{superAdmin && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!subscription?.instanceUserManagement) {
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
username,
|
||||
id,
|
||||
message: removeServerAdminUpgradePlanMessage
|
||||
});
|
||||
return;
|
||||
}
|
||||
handlePopUpOpen("removeServerAdmin", { username, id });
|
||||
}}
|
||||
>
|
||||
Remove Server Admin
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
)}
|
||||
{superAdmin && (
|
||||
<DropdownMenuItem
|
||||
icon={
|
||||
<div className="relative">
|
||||
<FontAwesomeIcon icon={faShieldHalved} />
|
||||
<FontAwesomeIcon
|
||||
className="absolute -bottom-[0.01rem] -right-1"
|
||||
size="2xs"
|
||||
icon={faXmark}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!subscription?.instanceUserManagement) {
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
username,
|
||||
id,
|
||||
message: removeServerAdminUpgradePlanMessage
|
||||
});
|
||||
return;
|
||||
}
|
||||
handlePopUpOpen("removeServerAdmin", { username, id });
|
||||
}}
|
||||
>
|
||||
Remove Server Admin
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{!isPending && isEmpty && <EmptyState title="No users found" icon={faUsers} />}
|
||||
@@ -218,7 +310,7 @@ const UserPanelTable = ({
|
||||
isDisabled={isFetchingNextPage || !hasNextPage}
|
||||
onClick={() => fetchNextPage()}
|
||||
>
|
||||
{hasNextPage ? "Load More" : "End of list"}
|
||||
{hasNextPage ? "Load More" : "End of List"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -231,13 +323,36 @@ export const UserIdentitiesTable = () => {
|
||||
"removeUser",
|
||||
"upgradePlan",
|
||||
"upgradeToServerAdmin",
|
||||
"removeServerAdmin"
|
||||
"removeServerAdmin",
|
||||
"removeUsers"
|
||||
] as const);
|
||||
|
||||
const {
|
||||
user: { id: userId }
|
||||
} = useUser();
|
||||
|
||||
const { mutateAsync: deleteUser } = useAdminDeleteUser();
|
||||
const { mutateAsync: deleteUsers } = useAdminBulkDeleteUsers();
|
||||
const { mutateAsync: grantAdminAccess } = useAdminGrantServerAdminAccess();
|
||||
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 { id } = popUp?.removeUser?.data as { id: string; username: string };
|
||||
|
||||
@@ -295,45 +410,158 @@ export const UserIdentitiesTable = () => {
|
||||
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 (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<UserPanelTable handlePopUpOpen={handlePopUpOpen} />
|
||||
<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.`}
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
<div
|
||||
className={twMerge(
|
||||
"h-0 flex-shrink-0 overflow-hidden transition-all",
|
||||
selectedUsers.length > 0 && "h-16"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center rounded-md border border-mineshaft-600 bg-mineshaft-800 px-4 py-2 text-bunker-300">
|
||||
<div className="mr-2 text-sm">{selectedUsers.length} Selected</div>
|
||||
<button
|
||||
type="button"
|
||||
className="mr-auto text-xs text-mineshaft-400 underline-offset-2 hover:text-mineshaft-200 hover:underline"
|
||||
onClick={() => setSelectedUsers([])}
|
||||
>
|
||||
Unselect All
|
||||
</button>
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
colorSchema="danger"
|
||||
leftIcon={<FontAwesomeIcon icon={faTrash} />}
|
||||
className="ml-2"
|
||||
onClick={() => {
|
||||
if (!selectedUsers?.length) return;
|
||||
|
||||
handlePopUpOpen("removeUsers");
|
||||
}}
|
||||
size="xs"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<UserPanelTable
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
users={users}
|
||||
selectedUsers={selectedUsers}
|
||||
setSelectedUsers={setSelectedUsers}
|
||||
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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -309,13 +309,16 @@ export const OrgMembersTable = ({
|
||||
|
||||
const filteredMembersPage = filteredMembers.slice(offset, perPage * page);
|
||||
|
||||
const isPageSelected = filteredMembersPage.every((member) =>
|
||||
selectedMemberIds.includes(member.id)
|
||||
);
|
||||
const isPageSelected = filteredMembersPage.length
|
||||
? filteredMembersPage.every((member) => selectedMemberIds.includes(member.id))
|
||||
: false;
|
||||
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
const isPageIndeterminate = isPageSelected
|
||||
? false
|
||||
: filteredMembersPage.some((member) => selectedMemberIds.includes(member.id));
|
||||
: filteredMembersPage.length
|
||||
? filteredMembersPage.some((member) => selectedMemberIds.includes(member.id))
|
||||
: false;
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user