mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
improvement: 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({
|
server.route({
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
url: "/user-management/users/:userId/admin-access",
|
url: "/user-management/users/:userId/admin-access",
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 }>({
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user