mirror of
https://github.com/Infisical/infisical.git
synced 2026-01-08 23:18:05 -05:00
feat(frontend): update org/sub-org user membership page UI
This commit is contained in:
@@ -1,23 +1,19 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, useNavigate, useParams } from "@tanstack/react-router";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { ChevronLeftIcon, EllipsisIcon } from "lucide-react";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { DeleteActionModal, PageHeader } from "@app/components/v2";
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
PageHeader,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
UnstableButton,
|
||||
UnstableDropdownMenu,
|
||||
UnstableDropdownMenuContent,
|
||||
UnstableDropdownMenuItem,
|
||||
UnstableDropdownMenuTrigger
|
||||
} from "@app/components/v3";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
@@ -100,18 +96,18 @@ const Page = withPermission(
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto flex max-w-8xl flex-col">
|
||||
{membership && (
|
||||
<div className="mx-auto w-full max-w-8xl">
|
||||
<>
|
||||
<Link
|
||||
to="/organizations/$orgId/access-management"
|
||||
params={{ orgId }}
|
||||
search={{
|
||||
selectedTab: OrgAccessControlTabSections.Member
|
||||
}}
|
||||
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
|
||||
className="mb-4 flex w-fit items-center gap-x-1 text-sm text-mineshaft-400 transition duration-100 hover:text-mineshaft-400/80"
|
||||
>
|
||||
<FontAwesomeIcon icon={faChevronLeft} />
|
||||
<ChevronLeftIcon size={16} />
|
||||
{isSubOrganization ? "Sub-" : ""}Organization Users
|
||||
</Link>
|
||||
<PageHeader
|
||||
@@ -124,139 +120,126 @@ const Page = withPermission(
|
||||
membership.inviteEmail ??
|
||||
"Unknown User")
|
||||
}
|
||||
description={`${isSubOrganization ? "Sub-" : ""}Organization User Membership`}
|
||||
description={`Configure and manage${isSubOrganization ? " sub-" : " "}organization user membership`}
|
||||
>
|
||||
<div>
|
||||
{userId !== membership.user.id && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
<Tooltip content="More options">
|
||||
<Button variant="outline_bg" size="sm">
|
||||
More
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="p-1">
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Edit}
|
||||
a={OrgPermissionSubjects.Member}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={() =>
|
||||
handlePopUpOpen("orgMembership", {
|
||||
membershipId: membership.id,
|
||||
role: membership.role,
|
||||
roleId: membership.roleId
|
||||
})
|
||||
}
|
||||
disabled={!isAllowed}
|
||||
>
|
||||
Edit User
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Delete}
|
||||
a={OrgPermissionSubjects.Member}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={
|
||||
membership.isActive
|
||||
? twMerge(
|
||||
isAllowed
|
||||
? "hover:bg-red-500! hover:text-white!"
|
||||
: "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)
|
||||
: ""
|
||||
}
|
||||
onClick={async () => {
|
||||
if (currentOrg?.scimEnabled) {
|
||||
createNotification({
|
||||
text: "You cannot manage users from Infisical when SCIM is enabled for your organization",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!membership.isActive) {
|
||||
// activate user
|
||||
await updateOrgMembership({
|
||||
organizationId: orgId,
|
||||
membershipId,
|
||||
isActive: true
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// deactivate user
|
||||
handlePopUpOpen("deactivateMember", {
|
||||
orgMembershipId: membershipId,
|
||||
username: membership.user.username
|
||||
{userId !== membership.user.id && (
|
||||
<UnstableDropdownMenu>
|
||||
<UnstableDropdownMenuTrigger asChild>
|
||||
<UnstableButton variant="outline">
|
||||
Options
|
||||
<EllipsisIcon />
|
||||
</UnstableButton>
|
||||
</UnstableDropdownMenuTrigger>
|
||||
<UnstableDropdownMenuContent align="end">
|
||||
<UnstableDropdownMenuItem
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(membership.user.id);
|
||||
createNotification({
|
||||
text: "User ID copied to clipboard",
|
||||
type: "info"
|
||||
});
|
||||
}}
|
||||
>
|
||||
Copy User ID
|
||||
</UnstableDropdownMenuItem>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Edit}
|
||||
a={OrgPermissionSubjects.Member}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<UnstableDropdownMenuItem
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() =>
|
||||
handlePopUpOpen("orgMembership", {
|
||||
membershipId: membership.id,
|
||||
role: membership.role,
|
||||
roleId: membership.roleId,
|
||||
metadata: membership.metadata
|
||||
})
|
||||
}
|
||||
>
|
||||
Edit User
|
||||
</UnstableDropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Delete}
|
||||
a={OrgPermissionSubjects.Member}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<UnstableDropdownMenuItem
|
||||
isDisabled={!isAllowed}
|
||||
onClick={async () => {
|
||||
if (currentOrg?.scimEnabled) {
|
||||
createNotification({
|
||||
text: "You cannot manage users from Infisical when SCIM is enabled for your organization",
|
||||
type: "error"
|
||||
});
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
>
|
||||
{`${membership.isActive ? "Deactivate" : "Activate"} User`}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Delete}
|
||||
a={OrgPermissionSubjects.Member}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
isAllowed
|
||||
? "hover:bg-red-500! hover:text-white!"
|
||||
: "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={() => {
|
||||
if (currentOrg?.scimEnabled) {
|
||||
createNotification({
|
||||
text: "You cannot manage users from Infisical when SCIM is enabled for your organization",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("removeMember", {
|
||||
orgMembershipId: membershipId,
|
||||
username: membership.user.username
|
||||
if (!membership.isActive) {
|
||||
// activate user
|
||||
await updateOrgMembership({
|
||||
organizationId: orgId,
|
||||
membershipId,
|
||||
isActive: true
|
||||
});
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
>
|
||||
Remove User
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// deactivate user
|
||||
handlePopUpOpen("deactivateMember", {
|
||||
orgMembershipId: membershipId,
|
||||
username: membership.user.username
|
||||
});
|
||||
}}
|
||||
>
|
||||
{`${membership.isActive ? "Deactivate" : "Activate"} User`}
|
||||
</UnstableDropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Delete}
|
||||
a={OrgPermissionSubjects.Member}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<UnstableDropdownMenuItem
|
||||
variant="danger"
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() => {
|
||||
if (currentOrg?.scimEnabled) {
|
||||
createNotification({
|
||||
text: "You cannot manage users from Infisical when SCIM is enabled for your organization",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("removeMember", {
|
||||
orgMembershipId: membershipId,
|
||||
username: membership.user.username
|
||||
});
|
||||
}}
|
||||
>
|
||||
Remove User
|
||||
</UnstableDropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</UnstableDropdownMenuContent>
|
||||
</UnstableDropdownMenu>
|
||||
)}
|
||||
</PageHeader>
|
||||
<div className="flex flex-col gap-4 md:flex-row">
|
||||
<div className="w-full md:w-96">
|
||||
<UserDetailsSection membershipId={membershipId} handlePopUpOpen={handlePopUpOpen} />
|
||||
</div>
|
||||
<div className="w-full space-y-2">
|
||||
<div className="w-full space-y-4">
|
||||
<UserProjectsSection membershipId={membershipId} />
|
||||
<UserGroupsSection orgMembership={membership} />
|
||||
<UserAuditLogsSection orgMembership={membership} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-5 lg:flex-row">
|
||||
<UserDetailsSection membershipId={membershipId} handlePopUpOpen={handlePopUpOpen} />
|
||||
<div className="flex flex-1 flex-col gap-y-5">
|
||||
<UserProjectsSection membershipId={membershipId} />
|
||||
<UserGroupsSection orgMembership={membership} />
|
||||
<UserAuditLogsSection orgMembership={membership} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.removeMember.isOpen}
|
||||
|
||||
@@ -1,17 +1,33 @@
|
||||
import {
|
||||
faCheck,
|
||||
faCheckCircle,
|
||||
faCircleXmark,
|
||||
faCopy,
|
||||
faKey,
|
||||
faPencil
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format } from "date-fns";
|
||||
import {
|
||||
BanIcon,
|
||||
CheckIcon,
|
||||
ClipboardListIcon,
|
||||
MailIcon,
|
||||
PencilIcon,
|
||||
UserCheckIcon,
|
||||
XIcon
|
||||
} from "lucide-react";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button, IconButton, Tag, Tooltip } from "@app/components/v2";
|
||||
import { Tooltip } from "@app/components/v2";
|
||||
import {
|
||||
Badge,
|
||||
Detail,
|
||||
DetailGroup,
|
||||
DetailLabel,
|
||||
DetailValue,
|
||||
UnstableButton,
|
||||
UnstableButtonGroup,
|
||||
UnstableCard,
|
||||
UnstableCardAction,
|
||||
UnstableCardContent,
|
||||
UnstableCardDescription,
|
||||
UnstableCardHeader,
|
||||
UnstableCardTitle,
|
||||
UnstableIconButton
|
||||
} from "@app/components/v3";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
@@ -30,8 +46,12 @@ type Props = {
|
||||
};
|
||||
|
||||
export const UserDetailsSection = ({ membershipId, handlePopUpOpen }: Props) => {
|
||||
const [copyTextUsername, isCopyingUsername, setCopyTextUsername] = useTimedReset<string>({
|
||||
initialState: "Copy username to clipboard"
|
||||
const [, isCopyingId, setCopyTextId] = useTimedReset<string>({
|
||||
initialState: "Copy ID to clipboard"
|
||||
});
|
||||
|
||||
const [, isCopyingEmail, setCopyEmail] = useTimedReset<string>({
|
||||
initialState: "Copy email to clipboard"
|
||||
});
|
||||
|
||||
const { user } = useUser();
|
||||
@@ -62,174 +82,199 @@ export const UserDetailsSection = ({ membershipId, handlePopUpOpen }: Props) =>
|
||||
|
||||
const getStatus = (m: OrgUser) => {
|
||||
if (!m.isActive) {
|
||||
return "Deactivated";
|
||||
return { label: "Deactivated", variant: "neutral" as const, Icon: <BanIcon /> };
|
||||
}
|
||||
|
||||
return m.status === "invited" ? "Invited" : "Active";
|
||||
return m.status === "invited"
|
||||
? { label: "Invited", variant: "info" as const, Icon: <MailIcon /> }
|
||||
: { label: "Active", variant: "success" as const, Icon: <UserCheckIcon /> };
|
||||
};
|
||||
|
||||
const roleName = roles?.find(
|
||||
(r) => r.slug === membership?.role || r.slug === membership?.customRoleSlug
|
||||
)?.name;
|
||||
|
||||
const name =
|
||||
membership?.user.firstName || membership?.user.lastName
|
||||
? `${membership.user.firstName} ${membership.user.lastName ?? ""}`.trim()
|
||||
: null;
|
||||
|
||||
const status = membership ? getStatus(membership) : null;
|
||||
|
||||
return membership ? (
|
||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||
<h3 className="text-lg font-medium text-mineshaft-100">User Details</h3>
|
||||
<UnstableCard className="w-full lg:max-w-[24rem]">
|
||||
<UnstableCardHeader className="border-b">
|
||||
<UnstableCardTitle>Details</UnstableCardTitle>
|
||||
<UnstableCardDescription>User membership details</UnstableCardDescription>
|
||||
{userId !== membership.user.id && (
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Member}>
|
||||
{(isAllowed) => {
|
||||
return (
|
||||
<Tooltip content="Edit Membership">
|
||||
<IconButton
|
||||
isDisabled={!isAllowed}
|
||||
ariaLabel="copy icon"
|
||||
variant="plain"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
handlePopUpOpen("orgMembership", {
|
||||
membershipId: membership.id,
|
||||
role: membership.role,
|
||||
roleId: membership.roleId,
|
||||
metadata: membership.metadata
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPencil} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
}}
|
||||
</OrgPermissionCan>
|
||||
)}
|
||||
</div>
|
||||
<div className="pt-4">
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-medium text-mineshaft-300">Name</p>
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
{membership.user.firstName || membership.user.lastName
|
||||
? `${membership.user.firstName} ${membership.user.lastName ?? ""}`.trim()
|
||||
: "-"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-medium text-mineshaft-300">Username</p>
|
||||
<div className="group flex align-top">
|
||||
<p className="text-sm break-all text-mineshaft-300">{membership.user.username}</p>
|
||||
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
||||
<Tooltip content={copyTextUsername}>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
variant="plain"
|
||||
className="group relative ml-2"
|
||||
<UnstableCardAction>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Member}>
|
||||
{(isAllowed) => (
|
||||
<UnstableIconButton
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(membership.user.username);
|
||||
setCopyTextUsername("Copied");
|
||||
handlePopUpOpen("orgMembership", {
|
||||
membershipId: membership.id,
|
||||
role: membership.role,
|
||||
roleId: membership.roleId,
|
||||
metadata: membership.metadata
|
||||
});
|
||||
}}
|
||||
size="xs"
|
||||
variant="outline"
|
||||
>
|
||||
<FontAwesomeIcon icon={isCopyingUsername ? faCheck : faCopy} />
|
||||
</IconButton>
|
||||
<PencilIcon />
|
||||
</UnstableIconButton>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</UnstableCardAction>
|
||||
)}
|
||||
</UnstableCardHeader>
|
||||
<UnstableCardContent>
|
||||
<DetailGroup>
|
||||
<Detail>
|
||||
<DetailLabel>Name</DetailLabel>
|
||||
<DetailValue>{name || <span className="text-muted">—</span>}</DetailValue>
|
||||
</Detail>
|
||||
<Detail>
|
||||
<DetailLabel>ID</DetailLabel>
|
||||
<DetailValue className="flex items-center gap-x-1">
|
||||
{membership.user.id}
|
||||
<Tooltip content="Copy user ID to clipboard">
|
||||
<UnstableIconButton
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(membership.user.id);
|
||||
setCopyTextId("Copied");
|
||||
}}
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
>
|
||||
{isCopyingId ? <CheckIcon /> : <ClipboardListIcon className="text-label" />}
|
||||
</UnstableIconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-medium text-mineshaft-300">Email</p>
|
||||
<div className="flex items-center">
|
||||
<p className="mr-2 text-sm break-all 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">
|
||||
<p className="text-sm font-medium text-mineshaft-300">Last Login Auth Method</p>
|
||||
<div className="group flex align-top">
|
||||
<p className="text-sm break-all text-mineshaft-300">
|
||||
{membership.lastLoginAuthMethod || "-"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-medium text-mineshaft-300">Last Login Time</p>
|
||||
<div className="group flex align-top">
|
||||
<p className="text-sm break-all text-mineshaft-300">
|
||||
{membership.lastLoginTime ? format(membership.lastLoginTime, "PPpp") : "-"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-medium text-mineshaft-300">Organization Role</p>
|
||||
<p className="text-sm text-mineshaft-300">{roleName ?? "-"}</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-medium text-mineshaft-300">Status</p>
|
||||
<p className="text-sm text-mineshaft-300">{getStatus(membership)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-mineshaft-300">Metadata</p>
|
||||
{membership?.metadata?.length ? (
|
||||
<div className="mt-1 flex flex-wrap gap-2 text-sm text-mineshaft-300">
|
||||
{membership.metadata?.map((el) => (
|
||||
<div key={el.id} className="flex items-center">
|
||||
<Tag
|
||||
size="xs"
|
||||
className="mr-0 flex items-center rounded-r-none border border-mineshaft-500"
|
||||
</DetailValue>
|
||||
</Detail>
|
||||
<Detail>
|
||||
<DetailLabel>Email</DetailLabel>
|
||||
<DetailValue className="flex items-center gap-x-1">
|
||||
{membership.user.email ? (
|
||||
<>
|
||||
{membership.user.email}
|
||||
<Tooltip
|
||||
content={
|
||||
membership.user.isEmailVerified
|
||||
? "Email has been verified"
|
||||
: "Email has not been verified"
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faKey} size="xs" className="mr-1" />
|
||||
<div>{el.key}</div>
|
||||
</Tag>
|
||||
<Tag
|
||||
size="xs"
|
||||
className="flex items-center rounded-l-none border border-mineshaft-500 bg-mineshaft-900 pl-1"
|
||||
>
|
||||
<div className="max-w-[150px] overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{el.value}
|
||||
</div>
|
||||
</Tag>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-mineshaft-300">-</p>
|
||||
{membership.user.isEmailVerified ? (
|
||||
<CheckIcon size={14} className="text-success" />
|
||||
) : (
|
||||
<XIcon size={14} className="text-danger" />
|
||||
)}
|
||||
</Tooltip>
|
||||
<Tooltip content="Copy user email to clipboard">
|
||||
<UnstableIconButton
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(membership.user.email!);
|
||||
setCopyEmail("Copied");
|
||||
}}
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
>
|
||||
{isCopyingEmail ? (
|
||||
<CheckIcon />
|
||||
) : (
|
||||
<ClipboardListIcon className="text-label" />
|
||||
)}
|
||||
</UnstableIconButton>
|
||||
</Tooltip>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-muted">—</span>
|
||||
)}
|
||||
</DetailValue>
|
||||
</Detail>
|
||||
{membership.user.username !== membership.user.email && (
|
||||
<Detail>
|
||||
<DetailLabel>Username</DetailLabel>
|
||||
<DetailValue>
|
||||
{membership.user.username || <span className="text-muted">—</span>}
|
||||
</DetailValue>
|
||||
</Detail>
|
||||
)}
|
||||
</div>
|
||||
<Detail>
|
||||
<DetailLabel>Organization Role</DetailLabel>
|
||||
<DetailValue>{roleName ?? <span className="text-muted">—</span>}</DetailValue>
|
||||
</Detail>
|
||||
<Detail>
|
||||
<DetailLabel>Status</DetailLabel>
|
||||
<DetailValue>
|
||||
{status && (
|
||||
<Badge variant={status.variant}>
|
||||
{status.Icon}
|
||||
{status.label}
|
||||
</Badge>
|
||||
)}
|
||||
</DetailValue>
|
||||
</Detail>
|
||||
<Detail>
|
||||
<DetailLabel>Last Login Method</DetailLabel>
|
||||
<DetailValue>
|
||||
{membership.lastLoginAuthMethod || <span className="text-muted">—</span>}
|
||||
</DetailValue>
|
||||
</Detail>
|
||||
<Detail>
|
||||
<DetailLabel>Last Logged In</DetailLabel>
|
||||
<DetailValue>
|
||||
{membership.lastLoginTime ? (
|
||||
format(membership.lastLoginTime, "PPpp")
|
||||
) : (
|
||||
<span className="text-muted">—</span>
|
||||
)}
|
||||
</DetailValue>
|
||||
</Detail>
|
||||
<Detail>
|
||||
<DetailLabel>Metadata</DetailLabel>
|
||||
<DetailValue className="flex flex-wrap gap-2">
|
||||
{membership?.metadata?.length ? (
|
||||
membership.metadata?.map((el) => (
|
||||
<UnstableButtonGroup className="min-w-0" key={el.id}>
|
||||
<Badge isTruncatable>
|
||||
<span>{el.key}</span>
|
||||
</Badge>
|
||||
<Badge variant="outline" isTruncatable>
|
||||
<span>{el.value}</span>
|
||||
</Badge>
|
||||
</UnstableButtonGroup>
|
||||
))
|
||||
) : (
|
||||
<span className="text-muted">—</span>
|
||||
)}
|
||||
</DetailValue>
|
||||
</Detail>
|
||||
</DetailGroup>
|
||||
{!isSubOrganization &&
|
||||
membership.isActive &&
|
||||
(membership.status === "invited" || membership.status === "verified") &&
|
||||
membership.user.email &&
|
||||
serverDetails?.emailConfigured && (
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Member}>
|
||||
{(isAllowed) => {
|
||||
return (
|
||||
<Button
|
||||
isDisabled={!isAllowed}
|
||||
className="mt-4 w-full"
|
||||
colorSchema="primary"
|
||||
type="submit"
|
||||
isLoading={isPending}
|
||||
onClick={onResendInvite}
|
||||
>
|
||||
Resend Invite
|
||||
</Button>
|
||||
);
|
||||
}}
|
||||
{(isAllowed) => (
|
||||
<UnstableButton
|
||||
isDisabled={!isAllowed}
|
||||
className="mt-4 w-full"
|
||||
variant="org"
|
||||
isPending={isPending}
|
||||
onClick={onResendInvite}
|
||||
>
|
||||
Resend Invite
|
||||
</UnstableButton>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</UnstableCardContent>
|
||||
</UnstableCard>
|
||||
) : (
|
||||
<div />
|
||||
);
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
/* eslint-disable react/jsx-no-useless-fragment */
|
||||
import { faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { MoreHorizontalIcon } from "lucide-react";
|
||||
|
||||
import { IconButton, Td, Tooltip, Tr } from "@app/components/v2";
|
||||
import {
|
||||
UnstableDropdownMenu,
|
||||
UnstableDropdownMenuContent,
|
||||
UnstableDropdownMenuItem,
|
||||
UnstableDropdownMenuTrigger,
|
||||
UnstableIconButton,
|
||||
UnstableTableCell,
|
||||
UnstableTableRow
|
||||
} from "@app/components/v3";
|
||||
import { TGroupWithProjectMemberships } from "@app/hooks/api/groups/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
@@ -13,34 +19,31 @@ type Props = {
|
||||
|
||||
export const UserGroupsRow = ({ group, handlePopUpOpen }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<Tr
|
||||
className="group h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||
key={`user-project-membership-${group.id}`}
|
||||
>
|
||||
<Td>{group.name}</Td>
|
||||
<Td>
|
||||
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
||||
<Tooltip content="Unassign user from group">
|
||||
<IconButton
|
||||
colorSchema="danger"
|
||||
ariaLabel="copy icon"
|
||||
variant="plain"
|
||||
className="group relative"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("removeUserFromGroup", {
|
||||
groupId: group.id,
|
||||
groupSlug: group.slug
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
</>
|
||||
<UnstableTableRow key={`user-group-membership-${group.id}`}>
|
||||
<UnstableTableCell>{group.name}</UnstableTableCell>
|
||||
<UnstableTableCell>
|
||||
<UnstableDropdownMenu>
|
||||
<UnstableDropdownMenuTrigger>
|
||||
<UnstableIconButton variant="ghost" size="xs">
|
||||
<MoreHorizontalIcon />
|
||||
</UnstableIconButton>
|
||||
</UnstableDropdownMenuTrigger>
|
||||
<UnstableDropdownMenuContent align="end">
|
||||
<UnstableDropdownMenuItem
|
||||
variant="danger"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("removeUserFromGroup", {
|
||||
groupId: group.id,
|
||||
groupSlug: group.slug
|
||||
});
|
||||
}}
|
||||
>
|
||||
Unassign from Group
|
||||
</UnstableDropdownMenuItem>
|
||||
</UnstableDropdownMenuContent>
|
||||
</UnstableDropdownMenu>
|
||||
</UnstableTableCell>
|
||||
</UnstableTableRow>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,6 +2,13 @@ import { useCallback } from "react";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { DeleteActionModal } from "@app/components/v2";
|
||||
import {
|
||||
UnstableCard,
|
||||
UnstableCardContent,
|
||||
UnstableCardDescription,
|
||||
UnstableCardHeader,
|
||||
UnstableCardTitle
|
||||
} from "@app/components/v3";
|
||||
import { useRemoveUserFromGroup } from "@app/hooks/api";
|
||||
import { OrgUser } from "@app/hooks/api/users/types";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
@@ -36,13 +43,15 @@ export const UserGroupsSection = ({ orgMembership }: Props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||
<h3 className="text-lg font-medium text-mineshaft-100">Groups</h3>
|
||||
</div>
|
||||
|
||||
<UserGroupsTable orgMembership={orgMembership} handlePopUpOpen={handlePopUpOpen} />
|
||||
</div>
|
||||
<UnstableCard>
|
||||
<UnstableCardHeader>
|
||||
<UnstableCardTitle>Groups</UnstableCardTitle>
|
||||
<UnstableCardDescription>Manage user group memberships</UnstableCardDescription>
|
||||
</UnstableCardHeader>
|
||||
<UnstableCardContent>
|
||||
<UserGroupsTable orgMembership={orgMembership} handlePopUpOpen={handlePopUpOpen} />
|
||||
</UnstableCardContent>
|
||||
</UnstableCard>
|
||||
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.removeUserFromGroup.isOpen}
|
||||
|
||||
@@ -1,25 +1,21 @@
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faMagnifyingGlass,
|
||||
faSearch,
|
||||
faUser
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { Lottie } from "@app/components/v2";
|
||||
import {
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Input,
|
||||
Pagination,
|
||||
Table,
|
||||
TableContainer,
|
||||
TBody,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
UnstableEmpty,
|
||||
UnstableEmptyDescription,
|
||||
UnstableEmptyHeader,
|
||||
UnstableEmptyTitle,
|
||||
UnstableInput,
|
||||
UnstablePagination,
|
||||
UnstableTable,
|
||||
UnstableTableBody,
|
||||
UnstableTableHead,
|
||||
UnstableTableHeader,
|
||||
UnstableTableRow
|
||||
} from "@app/components/v3";
|
||||
import {
|
||||
getUserTablePreference,
|
||||
PreferenceKey,
|
||||
@@ -85,37 +81,39 @@ export const UserGroupsTable = ({ handlePopUpOpen, orgMembership }: Props) => {
|
||||
setPage
|
||||
});
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="flex h-40 w-full items-center justify-center">
|
||||
<Lottie icon="infisical_loading_white" isAutoPlay className="w-16" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Input
|
||||
<>
|
||||
<UnstableInput
|
||||
className="mb-4"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search groups..."
|
||||
/>
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-full">
|
||||
<div className="flex items-center">
|
||||
Name
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className="ml-2"
|
||||
ariaLabel="sort"
|
||||
onClick={toggleOrderDirection}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={orderDirection === OrderByDirection.DESC ? faArrowUp : faArrowDown}
|
||||
/>
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{filteredGroupMemberships.length ? (
|
||||
<UnstableTable>
|
||||
<UnstableTableHeader>
|
||||
<UnstableTableRow>
|
||||
<UnstableTableHead onClick={toggleOrderDirection} className="w-full">
|
||||
Name
|
||||
<ChevronDownIcon
|
||||
className={twMerge(
|
||||
orderDirection === OrderByDirection.DESC && "rotate-180",
|
||||
"transition-transform"
|
||||
)}
|
||||
/>
|
||||
</UnstableTableHead>
|
||||
<UnstableTableHead className="w-5" />
|
||||
</UnstableTableRow>
|
||||
</UnstableTableHeader>
|
||||
<UnstableTableBody>
|
||||
{filteredGroupMemberships.slice(offset, perPage * page).map((group) => (
|
||||
<UserGroupsRow
|
||||
key={`user-group-${group.id}`}
|
||||
@@ -123,28 +121,33 @@ export const UserGroupsTable = ({ handlePopUpOpen, orgMembership }: Props) => {
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
/>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
{Boolean(filteredGroupMemberships.length) && (
|
||||
<Pagination
|
||||
count={filteredGroupMemberships.length}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={setPage}
|
||||
onChangePerPage={handlePerPageChange}
|
||||
/>
|
||||
)}
|
||||
{!isPending && !filteredGroupMemberships?.length && (
|
||||
<EmptyState
|
||||
title={
|
||||
groupMemberships.length
|
||||
? "No groups match search..."
|
||||
: "This user has not been assigned to any groups"
|
||||
}
|
||||
icon={groupMemberships.length ? faSearch : faUser}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
</UnstableTableBody>
|
||||
</UnstableTable>
|
||||
) : (
|
||||
<UnstableEmpty className="border">
|
||||
<UnstableEmptyHeader>
|
||||
<UnstableEmptyTitle>
|
||||
{groupMemberships.length
|
||||
? "No groups match this search"
|
||||
: "This user has not been assigned to any groups"}
|
||||
</UnstableEmptyTitle>
|
||||
<UnstableEmptyDescription>
|
||||
{groupMemberships.length
|
||||
? "Adjust search filters to view group memberships."
|
||||
: "Assign this user to a group from the group access control page."}
|
||||
</UnstableEmptyDescription>
|
||||
</UnstableEmptyHeader>
|
||||
</UnstableEmpty>
|
||||
)}
|
||||
{Boolean(filteredGroupMemberships.length) && (
|
||||
<UnstablePagination
|
||||
count={filteredGroupMemberships.length}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={setPage}
|
||||
onChangePerPage={handlePerPageChange}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import { useMemo } from "react";
|
||||
import { faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { MoreHorizontalIcon } from "lucide-react";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { IconButton, Td, Tooltip, Tr } from "@app/components/v2";
|
||||
import {
|
||||
UnstableDropdownMenu,
|
||||
UnstableDropdownMenuContent,
|
||||
UnstableDropdownMenuItem,
|
||||
UnstableDropdownMenuTrigger,
|
||||
UnstableIconButton,
|
||||
UnstableTableCell,
|
||||
UnstableTableRow
|
||||
} from "@app/components/v3";
|
||||
import { useOrganization } from "@app/context";
|
||||
import { getProjectBaseURL } from "@app/helpers/project";
|
||||
import { formatProjectRoleName } from "@app/helpers/roles";
|
||||
@@ -40,8 +47,7 @@ export const UserProjectRow = ({
|
||||
}, [workspaces, project]);
|
||||
|
||||
return (
|
||||
<Tr
|
||||
className="group h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||
<UnstableTableRow
|
||||
key={`user-project-membership-${id}`}
|
||||
onClick={() => {
|
||||
if (isAccessible) {
|
||||
@@ -64,34 +70,53 @@ export const UserProjectRow = ({
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Td className="max-w-0 truncate">{project.name}</Td>
|
||||
<Td>{`${formatProjectRoleName(roles[0].role, roles[0].customRoleName)}${
|
||||
<UnstableTableCell className="max-w-0 truncate">{project.name}</UnstableTableCell>
|
||||
<UnstableTableCell>{`${formatProjectRoleName(roles[0].role, roles[0].customRoleName)}${
|
||||
roles.length > 1 ? ` (+${roles.length - 1})` : ""
|
||||
}`}</Td>
|
||||
<Td>
|
||||
{isAccessible && (
|
||||
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
||||
<Tooltip content="Remove">
|
||||
<IconButton
|
||||
colorSchema="danger"
|
||||
ariaLabel="copy icon"
|
||||
variant="plain"
|
||||
className="group relative"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("removeUserFromProject", {
|
||||
username: user.username,
|
||||
projectId: project.id,
|
||||
projectName: project.name
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</Td>
|
||||
</Tr>
|
||||
}`}</UnstableTableCell>
|
||||
<UnstableTableCell>
|
||||
<UnstableDropdownMenu>
|
||||
<UnstableDropdownMenuTrigger>
|
||||
<UnstableIconButton variant="ghost" size="xs">
|
||||
<MoreHorizontalIcon />
|
||||
</UnstableIconButton>
|
||||
</UnstableDropdownMenuTrigger>
|
||||
<UnstableDropdownMenuContent align="end">
|
||||
<UnstableDropdownMenuItem
|
||||
isDisabled={!isAccessible}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate({
|
||||
to: `${getProjectBaseURL(project.type)}/access-management` as const,
|
||||
params: {
|
||||
orgId: currentOrg?.id || "",
|
||||
projectId: project.id
|
||||
},
|
||||
search: {
|
||||
selectedTab: OrgAccessControlTabSections.Member
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
Access Project
|
||||
</UnstableDropdownMenuItem>
|
||||
<UnstableDropdownMenuItem
|
||||
variant="danger"
|
||||
isDisabled={!isAccessible}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("removeUserFromProject", {
|
||||
username: user.username,
|
||||
projectId: project.id,
|
||||
projectName: project.name
|
||||
});
|
||||
}}
|
||||
>
|
||||
Remove From Project
|
||||
</UnstableDropdownMenuItem>
|
||||
</UnstableDropdownMenuContent>
|
||||
</UnstableDropdownMenu>
|
||||
</UnstableTableCell>
|
||||
</UnstableTableRow>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { DeleteActionModal, IconButton } from "@app/components/v2";
|
||||
import { DeleteActionModal } from "@app/components/v2";
|
||||
import {
|
||||
UnstableButton,
|
||||
UnstableCard,
|
||||
UnstableCardAction,
|
||||
UnstableCardContent,
|
||||
UnstableCardDescription,
|
||||
UnstableCardHeader,
|
||||
UnstableCardTitle
|
||||
} from "@app/components/v3";
|
||||
import { useOrganization, useUser } from "@app/context";
|
||||
import { useDeleteUserFromWorkspace, useGetOrgMembership } from "@app/hooks/api";
|
||||
import {
|
||||
useDeleteUserFromWorkspace,
|
||||
useGetOrgMembership,
|
||||
useGetOrgMembershipProjectMemberships
|
||||
} from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { UserAddToProjectModal } from "./UserAddToProjectModal";
|
||||
@@ -22,6 +34,7 @@ export const UserProjectsSection = ({ membershipId }: Props) => {
|
||||
const orgId = currentOrg?.id || "";
|
||||
|
||||
const { data: membership } = useGetOrgMembership(orgId, membershipId);
|
||||
const { data: projectMemberships } = useGetOrgMembershipProjectMemberships(orgId, membershipId);
|
||||
|
||||
const { mutateAsync: removeUserFromWorkspace } = useDeleteUserFromWorkspace();
|
||||
|
||||
@@ -39,28 +52,44 @@ export const UserProjectsSection = ({ membershipId }: Props) => {
|
||||
handlePopUpClose("removeUserFromProject");
|
||||
};
|
||||
|
||||
return membership ? (
|
||||
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||
<h3 className="text-lg font-medium text-mineshaft-100">Projects</h3>
|
||||
{userId !== membership.user.id && membership.status !== "invited" && (
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
variant="plain"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
handlePopUpOpen("addUserToProject", {
|
||||
username: membership.user.username
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
<div className="py-4">
|
||||
<UserProjectsTable membershipId={membershipId} handlePopUpOpen={handlePopUpOpen} />
|
||||
</div>
|
||||
if (!membership) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const canAddToProject = userId !== membership.user.id;
|
||||
|
||||
return (
|
||||
<>
|
||||
<UnstableCard>
|
||||
<UnstableCardHeader>
|
||||
<UnstableCardTitle>Projects</UnstableCardTitle>
|
||||
<UnstableCardDescription>Manage user project memberships</UnstableCardDescription>
|
||||
{canAddToProject && Boolean(projectMemberships?.length) && (
|
||||
<UnstableCardAction>
|
||||
<UnstableButton
|
||||
onClick={() => {
|
||||
handlePopUpOpen("addUserToProject", {
|
||||
username: membership.user.username
|
||||
});
|
||||
}}
|
||||
size="xs"
|
||||
variant="outline"
|
||||
>
|
||||
<PlusIcon />
|
||||
Add to Project
|
||||
</UnstableButton>
|
||||
</UnstableCardAction>
|
||||
)}
|
||||
</UnstableCardHeader>
|
||||
<UnstableCardContent>
|
||||
<UserProjectsTable
|
||||
membershipId={membershipId}
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
canAddToProject={canAddToProject}
|
||||
username={membership.user.username}
|
||||
/>
|
||||
</UnstableCardContent>
|
||||
</UnstableCard>
|
||||
<UserAddToProjectModal
|
||||
membershipId={membershipId}
|
||||
popUp={popUp}
|
||||
@@ -83,8 +112,6 @@ export const UserProjectsSection = ({ membershipId }: Props) => {
|
||||
return handleRemoveUser(popupData.projectId, popupData.username);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,26 +1,23 @@
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faFolder,
|
||||
faMagnifyingGlass,
|
||||
faSearch
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { ChevronDownIcon, PlusIcon } from "lucide-react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { Lottie } from "@app/components/v2";
|
||||
import {
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Input,
|
||||
Pagination,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
UnstableButton,
|
||||
UnstableEmpty,
|
||||
UnstableEmptyContent,
|
||||
UnstableEmptyDescription,
|
||||
UnstableEmptyHeader,
|
||||
UnstableEmptyTitle,
|
||||
UnstableInput,
|
||||
UnstablePagination,
|
||||
UnstableTable,
|
||||
UnstableTableBody,
|
||||
UnstableTableHead,
|
||||
UnstableTableHeader,
|
||||
UnstableTableRow
|
||||
} from "@app/components/v3";
|
||||
import { useOrganization } from "@app/context";
|
||||
import {
|
||||
getUserTablePreference,
|
||||
@@ -36,18 +33,25 @@ import { UserProjectRow } from "./UserProjectRow";
|
||||
|
||||
type Props = {
|
||||
membershipId: string;
|
||||
username: string;
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["removeUserFromProject"]>,
|
||||
popUpName: keyof UsePopUpState<["removeUserFromProject", "addUserToProject"]>,
|
||||
data?: object
|
||||
) => void;
|
||||
canAddToProject: boolean;
|
||||
};
|
||||
|
||||
enum UserProjectsOrderBy {
|
||||
Name = "Name"
|
||||
}
|
||||
|
||||
export const UserProjectsTable = ({ membershipId, handlePopUpOpen }: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
export const UserProjectsTable = ({
|
||||
membershipId,
|
||||
username,
|
||||
handlePopUpOpen,
|
||||
canAddToProject
|
||||
}: Props) => {
|
||||
const { currentOrg, isSubOrganization } = useOrganization();
|
||||
const orgId = currentOrg?.id || "";
|
||||
const {
|
||||
search,
|
||||
@@ -96,71 +100,92 @@ export const UserProjectsTable = ({ membershipId, handlePopUpOpen }: Props) => {
|
||||
setPage
|
||||
});
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="flex h-40 w-full items-center justify-center">
|
||||
<Lottie icon="infisical_loading_white" isAutoPlay className="w-16" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Input
|
||||
<>
|
||||
<UnstableInput
|
||||
className="mb-4"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search projects..."
|
||||
/>
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-2/3">
|
||||
<div className="flex items-center">
|
||||
Name
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className="ml-2"
|
||||
ariaLabel="sort"
|
||||
onClick={toggleOrderDirection}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={orderDirection === OrderByDirection.DESC ? faArrowUp : faArrowDown}
|
||||
/>
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th>Role</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isPending && <TableSkeleton columns={3} innerKey="user-project-memberships" />}
|
||||
{!isPending &&
|
||||
filteredProjectMemberships.slice(offset, perPage * page).map((membership) => {
|
||||
return (
|
||||
<UserProjectRow
|
||||
key={`user-project-membership-${membership.id}`}
|
||||
membership={membership}
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{Boolean(filteredProjectMemberships.length) && (
|
||||
<Pagination
|
||||
count={filteredProjectMemberships.length}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={setPage}
|
||||
onChangePerPage={handlePerPageChange}
|
||||
/>
|
||||
)}
|
||||
{!isPending && !filteredProjectMemberships?.length && (
|
||||
<EmptyState
|
||||
title={
|
||||
projectMemberships.length
|
||||
? "No projects match search..."
|
||||
: "This user has not been assigned to any projects"
|
||||
}
|
||||
icon={projectMemberships.length ? faSearch : faFolder}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
{filteredProjectMemberships.length ? (
|
||||
<UnstableTable>
|
||||
<UnstableTableHeader>
|
||||
<UnstableTableRow>
|
||||
<UnstableTableHead onClick={toggleOrderDirection} className="w-2/3">
|
||||
Name
|
||||
<ChevronDownIcon
|
||||
className={twMerge(
|
||||
orderDirection === OrderByDirection.DESC && "rotate-180",
|
||||
"transition-transform"
|
||||
)}
|
||||
/>
|
||||
</UnstableTableHead>
|
||||
<UnstableTableHead>Role</UnstableTableHead>
|
||||
<UnstableTableHead className="w-5" />
|
||||
</UnstableTableRow>
|
||||
</UnstableTableHeader>
|
||||
<UnstableTableBody>
|
||||
{filteredProjectMemberships.slice(offset, perPage * page).map((membership) => {
|
||||
return (
|
||||
<UserProjectRow
|
||||
key={`user-project-membership-${membership.id}`}
|
||||
membership={membership}
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</UnstableTableBody>
|
||||
</UnstableTable>
|
||||
) : (
|
||||
<UnstableEmpty className="border">
|
||||
<UnstableEmptyHeader>
|
||||
<UnstableEmptyTitle>
|
||||
{projectMemberships.length
|
||||
? "No projects match this search"
|
||||
: "This user is not a member of any projects"}
|
||||
</UnstableEmptyTitle>
|
||||
<UnstableEmptyDescription>
|
||||
{projectMemberships.length
|
||||
? "Adjust search filters to view project memberships."
|
||||
: "Assign this user to a project."}
|
||||
</UnstableEmptyDescription>
|
||||
{!projectMemberships.length && canAddToProject && (
|
||||
<UnstableEmptyContent>
|
||||
<UnstableButton
|
||||
variant={isSubOrganization ? "sub-org" : "org"}
|
||||
size="xs"
|
||||
onClick={() =>
|
||||
handlePopUpOpen("addUserToProject", {
|
||||
username
|
||||
})
|
||||
}
|
||||
>
|
||||
<PlusIcon />
|
||||
Add to Project
|
||||
</UnstableButton>
|
||||
</UnstableEmptyContent>
|
||||
)}
|
||||
</UnstableEmptyHeader>
|
||||
</UnstableEmpty>
|
||||
)}
|
||||
{Boolean(filteredProjectMemberships.length) && (
|
||||
<UnstablePagination
|
||||
count={filteredProjectMemberships.length}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={setPage}
|
||||
onChangePerPage={handlePerPageChange}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -15,10 +15,10 @@ import {
|
||||
ModalContent,
|
||||
TextArea
|
||||
} from "@app/components/v2";
|
||||
import { UnstableAlert, UnstableAlertDescription, UnstableAlertTitle } from "@app/components/v3";
|
||||
import { useProject } from "@app/context";
|
||||
import { ApprovalPolicyType } from "@app/hooks/api/approvalPolicies";
|
||||
import { useCreateApprovalRequest } from "@app/hooks/api/approvalRequests/mutations";
|
||||
import { UnstableAlert, UnstableAlertDescription, UnstableAlertTitle } from "@app/components/v3";
|
||||
|
||||
type Props = {
|
||||
accountPath?: string;
|
||||
|
||||
Reference in New Issue
Block a user