mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 19:44:52 +00:00
Merge pull request #4269 from Infisical/org-bulk-user-deletion
improvement(org-memberships): add bulk delete org memberships endpoint and table support
This commit is contained in:
@@ -17,7 +17,8 @@ import {
|
||||
APIKeyData,
|
||||
AuthMethod,
|
||||
CreateAPIKeyRes,
|
||||
DeletOrgMembershipDTO,
|
||||
DeleteOrgMembershipBatchDTO,
|
||||
DeleteOrgMembershipDTO,
|
||||
OrgUser,
|
||||
RenameUserDTO,
|
||||
TokenVersion,
|
||||
@@ -243,7 +244,7 @@ export const useGetOrgMembershipProjectMemberships = (
|
||||
export const useDeleteOrgMembership = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<object, object, DeletOrgMembershipDTO>({
|
||||
return useMutation<object, object, DeleteOrgMembershipDTO>({
|
||||
mutationFn: ({ membershipId, orgId }) => {
|
||||
return apiRequest.delete(`/api/v2/organizations/${orgId}/memberships/${membershipId}`);
|
||||
},
|
||||
@@ -253,10 +254,27 @@ export const useDeleteOrgMembership = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteOrgMembershipBatch = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<object, object, DeleteOrgMembershipBatchDTO>({
|
||||
mutationFn: ({ membershipIds, orgId }) => {
|
||||
return apiRequest.delete(`/api/v2/organizations/${orgId}/memberships`, {
|
||||
data: {
|
||||
membershipIds
|
||||
}
|
||||
});
|
||||
},
|
||||
onSuccess: (_, { orgId }) => {
|
||||
queryClient.invalidateQueries({ queryKey: userKeys.getOrgUsers(orgId) });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeactivateOrgMembership = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<object, object, DeletOrgMembershipDTO>({
|
||||
return useMutation<object, object, DeleteOrgMembershipDTO>({
|
||||
mutationFn: ({ membershipId, orgId }) => {
|
||||
return apiRequest.post(
|
||||
`/api/v2/organizations/${orgId}/memberships/${membershipId}/deactivate`
|
||||
|
||||
@@ -143,11 +143,16 @@ export type UpdateOrgMembershipDTO = {
|
||||
metadata?: { key: string; value: string }[];
|
||||
};
|
||||
|
||||
export type DeletOrgMembershipDTO = {
|
||||
export type DeleteOrgMembershipDTO = {
|
||||
membershipId: string;
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type DeleteOrgMembershipBatchDTO = {
|
||||
membershipIds: string[];
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type AddUserToOrgDTO = {
|
||||
inviteeEmails: string[];
|
||||
organizationRoleSlug: string;
|
||||
|
||||
+213
-69
@@ -1,18 +1,28 @@
|
||||
import { useState } from "react";
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faBan, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button, DeleteActionModal, EmailServiceSetupModal } from "@app/components/v2";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
EmailServiceSetupModal,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
useOrganization,
|
||||
useSubscription
|
||||
useSubscription,
|
||||
useUser
|
||||
} from "@app/context";
|
||||
import { useDeleteOrgMembership, useUpdateOrgMembership } from "@app/hooks/api";
|
||||
import { useDeleteOrgMembership, useGetOrgUsers, useUpdateOrgMembership } from "@app/hooks/api";
|
||||
import { useDeleteOrgMembershipBatch } from "@app/hooks/api/users/queries";
|
||||
import { OrgUser } from "@app/hooks/api/users/types";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { AddOrgMemberModal } from "./AddOrgMemberModal";
|
||||
@@ -22,7 +32,8 @@ export const OrgMembersSection = () => {
|
||||
const { subscription } = useSubscription();
|
||||
const { currentOrg } = useOrganization();
|
||||
const orgId = currentOrg?.id ?? "";
|
||||
|
||||
const { user } = useUser();
|
||||
const userId = user?.id || "";
|
||||
const [completeInviteLinks, setCompleteInviteLinks] = useState<Array<{
|
||||
email: string;
|
||||
link: string;
|
||||
@@ -33,10 +44,14 @@ export const OrgMembersSection = () => {
|
||||
"removeMember",
|
||||
"deactivateMember",
|
||||
"upgradePlan",
|
||||
"setUpEmail"
|
||||
"setUpEmail",
|
||||
"removeMembers"
|
||||
] as const);
|
||||
|
||||
const [selectedMemberIds, setSelectedMemberIds] = useState<string[]>([]);
|
||||
|
||||
const { mutateAsync: deleteMutateAsync } = useDeleteOrgMembership();
|
||||
const { mutateAsync: deleteBatchMutateAsync } = useDeleteOrgMembershipBatch();
|
||||
const { mutateAsync: updateOrgMembership } = useUpdateOrgMembership();
|
||||
|
||||
const isMoreIdentitiesAllowed = subscription?.identityLimit
|
||||
@@ -109,70 +124,199 @@ export const OrgMembersSection = () => {
|
||||
handlePopUpClose("removeMember");
|
||||
};
|
||||
|
||||
const { data: members = [] } = useGetOrgUsers(orgId);
|
||||
|
||||
const handleRemoveMembers = async (selectedMembers: OrgUser[]) => {
|
||||
try {
|
||||
await deleteBatchMutateAsync({
|
||||
orgId,
|
||||
membershipIds: selectedMembers
|
||||
.filter((member) => member.user.id !== userId)
|
||||
.map((member) => member.id)
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully removed users from organization",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
setSelectedMemberIds([]);
|
||||
handlePopUpClose("removeMembers");
|
||||
} catch {
|
||||
createNotification({
|
||||
text: "Failed to remove users from the organization",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Users</p>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Member}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handleAddMemberModal()}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Add Member
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
<>
|
||||
<div
|
||||
className={twMerge(
|
||||
"h-0 flex-shrink-0 overflow-hidden transition-all",
|
||||
selectedMemberIds.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">{selectedMemberIds.length} Selected</div>
|
||||
<button
|
||||
type="button"
|
||||
className="mr-auto text-xs text-mineshaft-400 underline-offset-2 hover:text-mineshaft-200 hover:underline"
|
||||
onClick={() => setSelectedMemberIds([])}
|
||||
>
|
||||
Unselect All
|
||||
</button>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Delete}
|
||||
a={OrgPermissionSubjects.Member}
|
||||
renderTooltip
|
||||
allowedLabel={
|
||||
currentOrg?.scimEnabled
|
||||
? "You cannot manage users from Infisical when org-level auth is enforced for your organization"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
colorSchema="danger"
|
||||
leftIcon={<FontAwesomeIcon icon={faTrash} />}
|
||||
className="ml-2"
|
||||
onClick={() => {
|
||||
const selectedOrgMemberships = members.filter((member) =>
|
||||
selectedMemberIds.includes(member.id)
|
||||
);
|
||||
|
||||
if (!selectedOrgMemberships.length) return;
|
||||
|
||||
handlePopUpOpen("removeMembers", { selectedOrgMemberships });
|
||||
}}
|
||||
isDisabled={!isAllowed || currentOrg?.scimEnabled}
|
||||
size="xs"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
<OrgMembersTable
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
setCompleteInviteLinks={setCompleteInviteLinks}
|
||||
/>
|
||||
<AddOrgMemberModal
|
||||
popUp={popUp}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
completeInviteLinks={completeInviteLinks}
|
||||
setCompleteInviteLinks={setCompleteInviteLinks}
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.removeMember.isOpen}
|
||||
title={`Are you sure you want to remove member with username ${
|
||||
(popUp?.removeMember?.data as { username: string })?.username || ""
|
||||
}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("removeMember", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() =>
|
||||
onRemoveMemberSubmit(
|
||||
(popUp?.removeMember?.data as { orgMembershipId: string })?.orgMembershipId
|
||||
)
|
||||
}
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deactivateMember.isOpen}
|
||||
title={`Are you sure you want to deactivate member with username ${
|
||||
(popUp?.deactivateMember?.data as { username: string })?.username || ""
|
||||
}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deactivateMember", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() =>
|
||||
onDeactivateMemberSubmit(
|
||||
(popUp?.deactivateMember?.data as { orgMembershipId: string })?.orgMembershipId
|
||||
)
|
||||
}
|
||||
buttonText="Deactivate"
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text={(popUp.upgradePlan?.data as { description: string })?.description}
|
||||
/>
|
||||
<EmailServiceSetupModal
|
||||
isOpen={popUp.setUpEmail?.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("setUpEmail", isOpen)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Users</p>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Member}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handleAddMemberModal()}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Add Member
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<OrgMembersTable
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
setCompleteInviteLinks={setCompleteInviteLinks}
|
||||
selectedMemberIds={selectedMemberIds}
|
||||
setSelectedMemberIds={setSelectedMemberIds}
|
||||
/>
|
||||
<AddOrgMemberModal
|
||||
popUp={popUp}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
completeInviteLinks={completeInviteLinks}
|
||||
setCompleteInviteLinks={setCompleteInviteLinks}
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.removeMember.isOpen}
|
||||
title={`Are you sure you want to remove member with username ${
|
||||
(popUp?.removeMember?.data as { username: string })?.username || ""
|
||||
}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("removeMember", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() =>
|
||||
onRemoveMemberSubmit(
|
||||
(popUp?.removeMember?.data as { orgMembershipId: string })?.orgMembershipId
|
||||
)
|
||||
}
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deactivateMember.isOpen}
|
||||
title={`Are you sure you want to deactivate member with username ${
|
||||
(popUp?.deactivateMember?.data as { username: string })?.username || ""
|
||||
}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deactivateMember", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() =>
|
||||
onDeactivateMemberSubmit(
|
||||
(popUp?.deactivateMember?.data as { orgMembershipId: string })?.orgMembershipId
|
||||
)
|
||||
}
|
||||
buttonText="Deactivate"
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.removeMembers.isOpen}
|
||||
title="Are you sure you want to remove the following members?"
|
||||
onChange={(isOpen) => handlePopUpToggle("removeMembers", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() =>
|
||||
handleRemoveMembers(popUp.removeMembers.data.selectedOrgMemberships as OrgUser[])
|
||||
}
|
||||
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">
|
||||
{(popUp.removeMembers.data?.selectedOrgMemberships as OrgUser[])?.map((member) => {
|
||||
const email = member.user.email ?? member.user.username ?? member.inviteEmail;
|
||||
return (
|
||||
<li key={member.id}>
|
||||
<div className="flex items-center">
|
||||
<p className={userId === member.user.id ? "line-through" : ""}>
|
||||
{member.user.firstName || member.user.lastName ? (
|
||||
<>
|
||||
{`${`${member.user.firstName} ${member.user.lastName}`.trim()} `}(
|
||||
<span className="break-all">{email}</span>)
|
||||
</>
|
||||
) : (
|
||||
<span className="break-all">{email}</span>
|
||||
)}{" "}
|
||||
</p>
|
||||
{userId === member.user.id && (
|
||||
<Tooltip content="You cannot remove yourself from this organization">
|
||||
<div className="inline-block">
|
||||
<Badge
|
||||
variant="danger"
|
||||
className="ml-1 mt-[0.05rem] inline-flex w-min items-center gap-1.5 whitespace-nowrap"
|
||||
>
|
||||
<FontAwesomeIcon icon={faBan} />
|
||||
<span>Ignored</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</DeleteActionModal>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text={(popUp.upgradePlan?.data as { description: string })?.description}
|
||||
/>
|
||||
<EmailServiceSetupModal
|
||||
isOpen={popUp.setUpEmail?.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("setUpEmail", isOpen)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+263
-205
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { Dispatch, SetStateAction, useCallback, useMemo, useState } from "react";
|
||||
import {
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
faMagnifyingGlass,
|
||||
faSearch,
|
||||
faUsers,
|
||||
faUserShield,
|
||||
faUserSlash,
|
||||
faUserXmark
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
@@ -22,6 +23,7 @@ import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Checkbox,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
@@ -43,6 +45,7 @@ import {
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
@@ -66,18 +69,24 @@ import {
|
||||
} from "@app/hooks/api";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import { useResendOrgMemberInvitation } from "@app/hooks/api/users/mutation";
|
||||
import { OrgUser } from "@app/hooks/api/users/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["removeMember", "deactivateMember", "upgradePlan"]>,
|
||||
popUpName: keyof UsePopUpState<
|
||||
["removeMember", "deactivateMember", "upgradePlan", "removeMembers"]
|
||||
>,
|
||||
data?: {
|
||||
orgMembershipId?: string;
|
||||
username?: string;
|
||||
description?: string;
|
||||
selectedOrgMemberships?: OrgUser[];
|
||||
}
|
||||
) => void;
|
||||
setCompleteInviteLinks: (links: Array<{ email: string; link: string }> | null) => void;
|
||||
selectedMemberIds: string[];
|
||||
setSelectedMemberIds: Dispatch<SetStateAction<string[]>>;
|
||||
};
|
||||
|
||||
enum OrgMembersOrderBy {
|
||||
@@ -90,7 +99,12 @@ type Filter = {
|
||||
roles: string[];
|
||||
};
|
||||
|
||||
export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Props) => {
|
||||
export const OrgMembersTable = ({
|
||||
handlePopUpOpen,
|
||||
setCompleteInviteLinks,
|
||||
selectedMemberIds,
|
||||
setSelectedMemberIds
|
||||
}: Props) => {
|
||||
const navigate = useNavigate();
|
||||
const { subscription } = useSubscription();
|
||||
const { currentOrg } = useOrganization();
|
||||
@@ -208,7 +222,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
|
||||
roles: []
|
||||
});
|
||||
|
||||
const filteredUsers = useMemo(
|
||||
const filteredMembers = useMemo(
|
||||
() =>
|
||||
members
|
||||
?.filter(({ user: u, inviteEmail, role, roleId }) => {
|
||||
@@ -273,7 +287,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
|
||||
};
|
||||
|
||||
useResetPageHelper({
|
||||
totalCount: filteredUsers.length,
|
||||
totalCount: filteredMembers.length,
|
||||
offset,
|
||||
setPage
|
||||
});
|
||||
@@ -293,6 +307,16 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
|
||||
|
||||
const isTableFiltered = Boolean(filter.roles.length);
|
||||
|
||||
const filteredMembersPage = filteredMembers.slice(offset, perPage * page);
|
||||
|
||||
const isPageSelected = filteredMembersPage.every((member) =>
|
||||
selectedMemberIds.includes(member.id)
|
||||
);
|
||||
|
||||
const isPageIndeterminate = isPageSelected
|
||||
? false
|
||||
: filteredMembersPage.some((member) => selectedMemberIds.includes(member.id));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex gap-2">
|
||||
@@ -357,6 +381,26 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-5">
|
||||
<Checkbox
|
||||
id="member-page-select"
|
||||
isChecked={isPageSelected || isPageIndeterminate}
|
||||
isIndeterminate={isPageIndeterminate}
|
||||
onCheckedChange={() => {
|
||||
if (isPageSelected) {
|
||||
setSelectedMemberIds((prev) =>
|
||||
prev.filter(
|
||||
(memberId) => !filteredMembersPage.find((m) => m.id === memberId)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
setSelectedMemberIds((prev) => [
|
||||
...new Set([...prev, ...filteredMembersPage.map((member) => member.id)])
|
||||
]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Th>
|
||||
<Th className="w-1/3">
|
||||
<div className="flex items-center">
|
||||
Name
|
||||
@@ -423,227 +467,241 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
|
||||
<TBody>
|
||||
{isLoading && <TableSkeleton columns={5} innerKey="org-members" />}
|
||||
{!isLoading &&
|
||||
filteredUsers
|
||||
.slice(offset, perPage * page)
|
||||
.map(
|
||||
({
|
||||
user: u,
|
||||
inviteEmail,
|
||||
role,
|
||||
roleId,
|
||||
id: orgMembershipId,
|
||||
status,
|
||||
isActive
|
||||
}) => {
|
||||
const name =
|
||||
u && u.firstName ? `${u.firstName} ${u.lastName ?? ""}`.trim() : null;
|
||||
const email = u?.email || inviteEmail;
|
||||
const username = u?.username ?? inviteEmail ?? "-";
|
||||
return (
|
||||
<Tr
|
||||
key={`org-membership-${orgMembershipId}`}
|
||||
className="h-10 w-full cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||
onClick={() =>
|
||||
navigate({
|
||||
to: "/organization/members/$membershipId" as const,
|
||||
params: {
|
||||
membershipId: orgMembershipId
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
<Td className={isActive ? "" : "text-mineshaft-400"}>
|
||||
{name ?? <span className="text-mineshaft-400">Not Set</span>}
|
||||
filteredMembersPage.map(
|
||||
({ user: u, inviteEmail, role, roleId, id: orgMembershipId, status, isActive }) => {
|
||||
const name =
|
||||
u && u.firstName ? `${u.firstName} ${u.lastName ?? ""}`.trim() : null;
|
||||
const email = u?.email || inviteEmail;
|
||||
const username = u?.username ?? inviteEmail ?? "-";
|
||||
const isSelected = selectedMemberIds.includes(orgMembershipId);
|
||||
return (
|
||||
<Tr
|
||||
key={`org-membership-${orgMembershipId}`}
|
||||
className="h-10 w-full cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||
onClick={() =>
|
||||
navigate({
|
||||
to: "/organization/members/$membershipId" as const,
|
||||
params: {
|
||||
membershipId: orgMembershipId
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
<Td>
|
||||
<Checkbox
|
||||
id={`select-member-${orgMembershipId}`}
|
||||
isChecked={isSelected}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedMemberIds((prev) =>
|
||||
isSelected
|
||||
? prev.filter((id) => id !== orgMembershipId)
|
||||
: [...prev, orgMembershipId]
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Td>
|
||||
<Td className={twMerge("max-w-0", isActive ? "" : "text-mineshaft-400")}>
|
||||
<div className="flex items-center">
|
||||
<p className="truncate">
|
||||
{name ?? <span className="text-mineshaft-400">Not Set</span>}
|
||||
</p>
|
||||
{u.superAdmin && (
|
||||
<Badge variant="primary" className="ml-2">
|
||||
Server Admin
|
||||
<Badge variant="primary" className="ml-2 w-min whitespace-nowrap">
|
||||
<span className="hidden xl:inline">Server Admin</span>
|
||||
<Tooltip content="Server Admin">
|
||||
<FontAwesomeIcon className="xl:hidden" icon={faUserShield} />
|
||||
</Tooltip>
|
||||
</Badge>
|
||||
)}
|
||||
</Td>
|
||||
<Td className={isActive ? "" : "text-mineshaft-400"}>{username}</Td>
|
||||
<Td>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Edit}
|
||||
a={OrgPermissionSubjects.Member}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Select
|
||||
value={role === "custom" ? findRoleFromId(roleId)?.slug : role}
|
||||
isDisabled={userId === u?.id || !isAllowed}
|
||||
className="h-8 w-48 bg-mineshaft-700"
|
||||
position="popper"
|
||||
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
||||
onValueChange={(selectedRole) =>
|
||||
onRoleChange(orgMembershipId, selectedRole)
|
||||
}
|
||||
</div>
|
||||
</Td>
|
||||
<Td className={twMerge("max-w-0", isActive ? "" : "text-mineshaft-400")}>
|
||||
<p className="truncate">{username}</p>
|
||||
</Td>
|
||||
<Td>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Edit}
|
||||
a={OrgPermissionSubjects.Member}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Select
|
||||
value={role === "custom" ? findRoleFromId(roleId)?.slug : role}
|
||||
isDisabled={userId === u?.id || !isAllowed}
|
||||
className="h-8 w-48 bg-mineshaft-700"
|
||||
position="popper"
|
||||
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
||||
onValueChange={(selectedRole) =>
|
||||
onRoleChange(orgMembershipId, selectedRole)
|
||||
}
|
||||
>
|
||||
{(roles || [])
|
||||
.filter(({ slug }) =>
|
||||
slug === "owner" ? isIamOwner || role === "owner" : true
|
||||
)
|
||||
.map(({ slug, name: roleName }) => (
|
||||
<SelectItem value={slug} key={`owner-option-${slug}`}>
|
||||
{roleName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex items-center justify-end gap-6">
|
||||
{isActive &&
|
||||
(status === "invited" || status === "verified") &&
|
||||
email &&
|
||||
serverDetails?.emailConfigured && (
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Edit}
|
||||
a={OrgPermissionSubjects.Member}
|
||||
>
|
||||
{(roles || [])
|
||||
.filter(({ slug }) =>
|
||||
slug === "owner" ? isIamOwner || role === "owner" : true
|
||||
)
|
||||
.map(({ slug, name: roleName }) => (
|
||||
<SelectItem value={slug} key={`owner-option-${slug}`}>
|
||||
{roleName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
isDisabled={!isAllowed || isResendInvitePending}
|
||||
className="h-8 border-mineshaft-600 bg-mineshaft-700 font-normal"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
isLoading={
|
||||
isResendInvitePending && resendInviteId === orgMembershipId
|
||||
}
|
||||
onClick={(e) => {
|
||||
onResendInvite(orgMembershipId);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
Resend Invite
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex items-center justify-end gap-6">
|
||||
{isActive &&
|
||||
(status === "invited" || status === "verified") &&
|
||||
email &&
|
||||
serverDetails?.emailConfigured && (
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Edit}
|
||||
a={OrgPermissionSubjects.Member}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
isDisabled={!isAllowed || isResendInvitePending}
|
||||
className="h-8 border-mineshaft-600 bg-mineshaft-700 font-normal"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
isLoading={
|
||||
isResendInvitePending && resendInviteId === orgMembershipId
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger disabled={userId === u?.id} asChild>
|
||||
<IconButton
|
||||
ariaLabel="Options"
|
||||
colorSchema="secondary"
|
||||
className={twMerge("w-6", userId === u?.id && "opacity-50")}
|
||||
variant="plain"
|
||||
isDisabled={userId === u?.id}
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent sideOffset={2} align="end">
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Edit}
|
||||
a={OrgPermissionSubjects.Member}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate({
|
||||
to: "/organization/members/$membershipId" as const,
|
||||
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;
|
||||
}
|
||||
onClick={(e) => {
|
||||
onResendInvite(orgMembershipId);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
Resend Invite
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
)}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Options"
|
||||
colorSchema="secondary"
|
||||
className={twMerge("w-6", userId === u?.id && "opacity-50")}
|
||||
variant="plain"
|
||||
isDisabled={userId === u?.id}
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent sideOffset={2} align="end">
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Edit}
|
||||
a={OrgPermissionSubjects.Member}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate({
|
||||
to: "/organization/members/$membershipId" as const,
|
||||
params: {
|
||||
membershipId: orgMembershipId
|
||||
}
|
||||
|
||||
if (!isActive) {
|
||||
// activate user
|
||||
await updateOrgMembership({
|
||||
organizationId: orgId,
|
||||
membershipId: orgMembershipId,
|
||||
isActive: true
|
||||
});
|
||||
}}
|
||||
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;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isActive) {
|
||||
// activate user
|
||||
await updateOrgMembership({
|
||||
organizationId: orgId,
|
||||
membershipId: orgMembershipId,
|
||||
isActive: true
|
||||
});
|
||||
// deactivate user
|
||||
handlePopUpOpen("deactivateMember", {
|
||||
orgMembershipId,
|
||||
username
|
||||
});
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
{`${isActive ? "Deactivate" : "Activate"} User`}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Delete}
|
||||
a={OrgPermissionSubjects.Member}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// deactivate user
|
||||
handlePopUpOpen("deactivateMember", {
|
||||
orgMembershipId,
|
||||
username
|
||||
if (currentOrg?.scimEnabled && isActive) {
|
||||
createNotification({
|
||||
text: "You cannot manage users from Infisical when org-level auth is enforced for your organization",
|
||||
type: "error"
|
||||
});
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
{`${isActive ? "Deactivate" : "Activate"} User`}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Delete}
|
||||
a={OrgPermissionSubjects.Member}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentOrg?.scimEnabled && isActive) {
|
||||
createNotification({
|
||||
text: "You cannot manage users from Infisical when org-level auth is enforced for your organization",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("removeMember", {
|
||||
orgMembershipId,
|
||||
username
|
||||
});
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faUserXmark} />}
|
||||
>
|
||||
Remove User
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
}
|
||||
)}
|
||||
handlePopUpOpen("removeMember", {
|
||||
orgMembershipId,
|
||||
username
|
||||
});
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faUserXmark} />}
|
||||
>
|
||||
Remove User
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
{Boolean(filteredUsers.length) && (
|
||||
{Boolean(filteredMembers.length) && (
|
||||
<Pagination
|
||||
count={filteredUsers.length}
|
||||
count={filteredMembers.length}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={setPage}
|
||||
onChangePerPage={handlePerPageChange}
|
||||
/>
|
||||
)}
|
||||
{!isMembersLoading && !filteredUsers?.length && (
|
||||
{!isMembersLoading && !filteredMembers?.length && (
|
||||
<EmptyState
|
||||
title={
|
||||
members.length
|
||||
|
||||
+17
-14
@@ -120,7 +120,7 @@ export const UserDetailsSection = ({ membershipId, handlePopUpOpen }: Props) =>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-semibold text-mineshaft-300">Username</p>
|
||||
<div className="group flex align-top">
|
||||
<p className="text-sm text-mineshaft-300">{membership.user.username}</p>
|
||||
<p className="break-all text-sm text-mineshaft-300">{membership.user.username}</p>
|
||||
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
||||
<Tooltip content={copyTextUsername}>
|
||||
<IconButton
|
||||
@@ -141,19 +141,22 @@ export const UserDetailsSection = ({ membershipId, handlePopUpOpen }: Props) =>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-semibold text-mineshaft-300">Email</p>
|
||||
<div className="flex items-center">
|
||||
<p className="mr-2 text-sm text-mineshaft-300">{membership.user.email ?? "-"}</p>
|
||||
<Tooltip
|
||||
content={
|
||||
membership.user.isEmailVerified
|
||||
? "Email has been verified"
|
||||
: "Email has not been verified"
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
size="sm"
|
||||
icon={membership.user.isEmailVerified ? faCheckCircle : faCircleXmark}
|
||||
/>
|
||||
</Tooltip>
|
||||
<p className="mr-2 break-all text-sm text-mineshaft-300">
|
||||
{membership.user.email ?? "-"}{" "}
|
||||
<Tooltip
|
||||
content={
|
||||
membership.user.isEmailVerified
|
||||
? "Email has been verified"
|
||||
: "Email has not been verified"
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
size="sm"
|
||||
icon={membership.user.isEmailVerified ? faCheckCircle : faCircleXmark}
|
||||
className={membership.user.isEmailVerified ? "text-green" : "text-red"}
|
||||
/>
|
||||
</Tooltip>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
|
||||
+10
-8
@@ -1,11 +1,11 @@
|
||||
import { useMemo } from "react";
|
||||
import { subject } from "@casl/ability";
|
||||
import { faAnglesRight, faMinusSquare, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faAnglesRight, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, DeleteActionModal, IconButton, Tooltip } from "@app/components/v2";
|
||||
import { Button, DeleteActionModal, Tooltip } from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
@@ -221,12 +221,14 @@ export const SelectionPanel = ({
|
||||
)}
|
||||
>
|
||||
<div className="mt-3.5 flex items-center rounded-md border border-mineshaft-600 bg-mineshaft-800 px-4 py-2 text-bunker-300">
|
||||
<Tooltip content="Clear">
|
||||
<IconButton variant="plain" ariaLabel="clear-selection" onClick={resetSelectedEntries}>
|
||||
<FontAwesomeIcon icon={faMinusSquare} size="lg" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<div className="ml-1 flex-grow px-2 text-sm">{selectedCount} Selected</div>
|
||||
<div className="mr-2 text-sm">{selectedCount} Selected</div>
|
||||
<button
|
||||
type="button"
|
||||
className="mr-auto text-xs text-mineshaft-400 underline-offset-2 hover:text-mineshaft-200 hover:underline"
|
||||
onClick={resetSelectedEntries}
|
||||
>
|
||||
Unselect All
|
||||
</button>
|
||||
{isRotatedSecretSelected && (
|
||||
<span className="text-sm text-mineshaft-400">
|
||||
Rotated Secrets will not be affected by action.
|
||||
|
||||
+8
-9
@@ -16,7 +16,6 @@ import {
|
||||
faFolderPlus,
|
||||
faKey,
|
||||
faLock,
|
||||
faMinusSquare,
|
||||
faPaste,
|
||||
faPlus,
|
||||
faRotate,
|
||||
@@ -1061,14 +1060,14 @@ export const ActionBar = ({
|
||||
)}
|
||||
>
|
||||
<div className="mt-3.5 flex items-center rounded-md border border-mineshaft-600 bg-mineshaft-800 px-4 py-2 text-bunker-300">
|
||||
<Tooltip content="Clear">
|
||||
<IconButton variant="plain" ariaLabel="clear-selection" onClick={resetSelectedSecret}>
|
||||
<FontAwesomeIcon icon={faMinusSquare} size="lg" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<div className="ml-2 flex-grow px-2 text-sm">
|
||||
{Object.keys(selectedSecrets).length} Selected
|
||||
</div>
|
||||
<div className="mr-2 text-sm">{Object.keys(selectedSecrets).length} Selected</div>
|
||||
<button
|
||||
type="button"
|
||||
className="mr-auto text-xs text-mineshaft-400 underline-offset-2 hover:text-mineshaft-200 hover:underline"
|
||||
onClick={resetSelectedSecret}
|
||||
>
|
||||
Unselect All
|
||||
</button>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={subject(ProjectPermissionSub.Secrets, {
|
||||
|
||||
Reference in New Issue
Block a user