diff --git a/backend/src/ee/routes/v1/user-additional-privilege-router.ts b/backend/src/ee/routes/v1/user-additional-privilege-router.ts index 926b222319..debaa12cfe 100644 --- a/backend/src/ee/routes/v1/user-additional-privilege-router.ts +++ b/backend/src/ee/routes/v1/user-additional-privilege-router.ts @@ -142,6 +142,7 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr data: { ...req.body, ...req.body.type, + name: req.body.slug, permissions: req.body.permissions ? // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore-error this is valid ts diff --git a/frontend/src/components/v2/PageHeader/PageHeader.tsx b/frontend/src/components/v2/PageHeader/PageHeader.tsx index 05e0c131bf..ffe4c9daee 100644 --- a/frontend/src/components/v2/PageHeader/PageHeader.tsx +++ b/frontend/src/components/v2/PageHeader/PageHeader.tsx @@ -32,7 +32,7 @@ export const PageHeader = ({ title, description, children, className, scope }: P

{children}

) : ( -

Not set

+

Not set

)} diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx index 486bcdae1a..5b7a9b7dcf 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx @@ -1,7 +1,6 @@ import { Helmet } from "react-helmet"; import { useTranslation } from "react-i18next"; import { subject } from "@casl/ability"; -import { DropdownMenu } from "@radix-ui/react-dropdown-menu"; import { useQuery } from "@tanstack/react-query"; import { Link, useNavigate, useParams } from "@tanstack/react-router"; import { ChevronLeftIcon, EllipsisIcon, InfoIcon } from "lucide-react"; @@ -17,6 +16,7 @@ import { } from "@app/components/v2"; import { OrgIcon, + SubOrgIcon, UnstableAlert, UnstableAlertDescription, UnstableAlertTitle, @@ -26,6 +26,7 @@ import { UnstableCardDescription, UnstableCardHeader, UnstableCardTitle, + UnstableDropdownMenu, UnstableDropdownMenuContent, UnstableDropdownMenuItem, UnstableDropdownMenuTrigger, @@ -63,7 +64,7 @@ const Page = () => { select: (el) => el.identityId as string }); const { currentProject, projectId } = useProject(); - const { currentOrg } = useOrganization(); + const { currentOrg, isSubOrganization } = useOrganization(); const { data: identityMembershipDetails, isPending: isMembershipDetailsLoading } = useGetProjectIdentityMembershipV2(projectId, identityId); @@ -164,6 +165,12 @@ const Page = () => { return ; } + const isOrgIdentity = !isProjectIdentity; + const isSubOrgIdentity = + isOrgIdentity && + isSubOrganization && + currentOrg.rootOrgId !== identityMembershipDetails?.identity.orgId; + return (
{identityMembershipDetails ? ( @@ -187,7 +194,7 @@ const Page = () => { description={`Configure and manage${isProjectIdentity ? " machine identity and " : " "}project access control`} title={identityMembershipDetails.identity.name} > - + Options @@ -197,7 +204,7 @@ const Page = () => { { - navigator.clipboard.writeText(identityMembershipDetails.id); + navigator.clipboard.writeText(identityMembershipDetails.identity.id); createNotification({ text: "Machine identity ID copied to clipboard", type: "info" @@ -211,7 +218,6 @@ const Page = () => { a={subject(ProjectPermissionSub.Identity, { identityId: identityMembershipDetails?.identity.id })} - passThrough={false} > {(isAllowed) => ( { Assume Privileges
@@ -251,12 +257,13 @@ const Page = () => { )} - +
@@ -275,15 +282,15 @@ const Page = () => { - - + + {isSubOrgIdentity ? : } - Machine identity managed by organization + Machine identity managed by {isSubOrgIdentity ? "sub-" : ""}organization

- This machine identity's authentication methods are controlled by your - organization. To make changes,{" "} + This machine identity's authentication methods are managed by your + {isSubOrgIdentity ? "sub-" : ""}organization.
To make changes,{" "} { className="inline-block cursor-pointer text-foreground underline underline-offset-2" params={{ identityId, - orgId: currentOrg.id + orgId: identityMembershipDetails.identity.orgId }} > - go to organization access control + go to {isSubOrgIdentity ? "sub-" : ""}organization access control ) : null } diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeModifySection.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeModifySection.tsx index b51501d481..9aa864b593 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeModifySection.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeModifySection.tsx @@ -114,6 +114,7 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({ const { handleSubmit, + reset, formState: { isDirty, isSubmitting } } = form; @@ -310,7 +311,7 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({ variant="link" isDisabled={isSubmitting} isLoading={isSubmitting} - onClick={onGoBack} + onClick={() => reset()} > Discard Changes diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeSection.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeSection.tsx index cc686a2457..9d37c50d13 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeSection.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeSection.tsx @@ -81,9 +81,7 @@ export const IdentityProjectAdditionalPrivilegeSection = ({ identityMembershipDe return ( <> - + Project Additional Privileges Assign one-off policies to this machine identity diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityRoleDetailsSection/IdentityRoleDetailsSection.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityRoleDetailsSection/IdentityRoleDetailsSection.tsx index 9d88924b31..b16b74690f 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityRoleDetailsSection/IdentityRoleDetailsSection.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityRoleDetailsSection/IdentityRoleDetailsSection.tsx @@ -96,9 +96,7 @@ export const IdentityRoleDetailsSection = ({ return ( <> - + Project Roles Manage roles assigned to this machine identity @@ -208,8 +206,6 @@ export const IdentityRoleDetailsSection = ({ a={subject(ProjectPermissionSub.Identity, { identityId: identityMembershipDetails.identity.id })} - renderTooltip - allowedLabel="Remove Role" > {(isAllowed) => ( - {/* */} Remove Role )} diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/components/ProjectIdentityDetailsSection.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/components/ProjectIdentityDetailsSection.tsx index f26dd60dae..34df3888b1 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/components/ProjectIdentityDetailsSection.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/components/ProjectIdentityDetailsSection.tsx @@ -12,6 +12,7 @@ import { DetailValue, OrgIcon, ProjectIcon, + SubOrgIcon, UnstableButtonGroup, UnstableCard, UnstableCardAction, @@ -30,10 +31,16 @@ import { ProjectIdentityModal } from "@app/pages/project/AccessControlPage/compo type Props = { identity: TProjectIdentity; isOrgIdentity?: boolean; + isSubOrgIdentity?: boolean; membership: IdentityProjectMembershipV1; }; -export const ProjectIdentityDetailsSection = ({ identity, isOrgIdentity, membership }: Props) => { +export const ProjectIdentityDetailsSection = ({ + identity, + isOrgIdentity, + isSubOrgIdentity, + membership +}: Props) => { // eslint-disable-next-line @typescript-eslint/naming-convention,@typescript-eslint/no-unused-vars const [_, isCopyingId, setCopyTextId] = useTimedReset({ initialState: "Copy ID to clipboard" @@ -43,10 +50,8 @@ export const ProjectIdentityDetailsSection = ({ identity, isOrgIdentity, members return ( <> - - + + Details Machine identity details {!isOrgIdentity && ( @@ -92,7 +97,7 @@ export const ProjectIdentityDetailsSection = ({ identity, isOrgIdentity, members variant="ghost" size="xs" > - {/* TODO(scott): color this should be a button variant */} + {/* TODO(scott): color this should be a button variant and create re-usable copy button */} {isCopyingId ? : } @@ -102,9 +107,9 @@ export const ProjectIdentityDetailsSection = ({ identity, isOrgIdentity, members Managed by {isOrgIdentity ? ( - - - Organization + + {isSubOrgIdentity ? : } + {isSubOrgIdentity ? "Sub-" : ""}Organization ) : ( @@ -129,7 +134,7 @@ export const ProjectIdentityDetailsSection = ({ identity, isOrgIdentity, members )) ) : ( - No metadata + No metadata )} @@ -145,7 +150,7 @@ export const ProjectIdentityDetailsSection = ({ identity, isOrgIdentity, members {membership.lastLoginAuthMethod ? ( identityAuthToNameMap[membership.lastLoginAuthMethod] ) : ( - N/A + N/A )} @@ -155,7 +160,7 @@ export const ProjectIdentityDetailsSection = ({ identity, isOrgIdentity, members {membership.lastLoginTime ? ( format(membership.lastLoginTime, "PPpp") ) : ( - N/A + N/A )} diff --git a/frontend/src/pages/project/MemberDetailsByIDPage/MemberDetailsByIDPage.tsx b/frontend/src/pages/project/MemberDetailsByIDPage/MemberDetailsByIDPage.tsx index 46883733d7..095faed42f 100644 --- a/frontend/src/pages/project/MemberDetailsByIDPage/MemberDetailsByIDPage.tsx +++ b/frontend/src/pages/project/MemberDetailsByIDPage/MemberDetailsByIDPage.tsx @@ -3,25 +3,34 @@ 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 { formatRelative } from "date-fns"; +import { EllipsisIcon, InfoIcon } from "lucide-react"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { createNotification } from "@app/components/notifications"; import { ProjectPermissionCan } from "@app/components/permissions"; import { - Button, ConfirmActionModal, DeleteActionModal, EmptyState, PageHeader, - Spinner + Spinner, + Tooltip } from "@app/components/v2"; +import { + Badge, + UnstableButton, + UnstableDropdownMenu, + UnstableDropdownMenuContent, + UnstableDropdownMenuItem, + UnstableDropdownMenuTrigger +} from "@app/components/v3"; import { ProjectPermissionActions, ProjectPermissionMemberActions, ProjectPermissionSub, useOrganization, - useProject + useProject, + useUser } from "@app/context"; import { getProjectBaseURL, getProjectHomePage } from "@app/helpers/project"; import { usePopUp } from "@app/hooks"; @@ -35,6 +44,7 @@ import { ProjectAccessControlTabs } from "@app/types/project"; import { MemberProjectAdditionalPrivilegeSection } from "./components/MemberProjectAdditionalPrivilegeSection"; import { MemberRoleDetailsSection } from "./components/MemberRoleDetailsSection"; +import { ProjectMemberDetailsSection } from "./components/ProjectMemberDetailsSection"; export const Page = () => { const navigate = useNavigate(); @@ -44,12 +54,14 @@ export const Page = () => { }); const { currentOrg } = useOrganization(); const { currentProject, projectId } = useProject(); + const { + user: { id: currentUserId } + } = useUser(); const { data: membershipDetails, isPending: isMembershipDetailsLoading } = useGetWorkspaceUserDetails(projectId, membershipId); - const { mutateAsync: removeUserFromWorkspace, isPending: isRemovingUserFromWorkspace } = - useDeleteUserFromWorkspace(); + const { mutateAsync: removeUserFromWorkspace } = useDeleteUserFromWorkspace(); const assumePrivileges = useAssumeProjectPrivileges(); const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ @@ -112,8 +124,10 @@ export const Page = () => { ); } + const isOwnProjectMembershipDetails = currentUserId === membershipDetails?.user?.id; + return ( -

+
{membershipDetails ? ( <> { search={{ selectedTab: ProjectAccessControlTabs.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" > Project Users @@ -135,62 +149,100 @@ export const Page = () => { title={ membershipDetails.user.firstName || membershipDetails.user.lastName ? `${membershipDetails.user.firstName} ${membershipDetails.user.lastName}` - : "-" + : membershipDetails.user.email || + membershipDetails.user.username || + membershipDetails.inviteEmail || + "Unnamed User" } - description={`User joined on ${membershipDetails?.createdAt && formatRelative(new Date(membershipDetails?.createdAt || ""), new Date())}`} + description="Configure and manage project access control" > - - {(isAllowed) => ( - - )} - - - - {(isAllowed) => ( - - )} - + {isOwnProjectMembershipDetails ? ( + + + Your project membership + + + ) : ( + + + + Options + + + + + { + navigator.clipboard.writeText(membershipDetails.user.id); + createNotification({ + text: "User ID copied to clipboard", + type: "info" + }); + }} + > + Copy User ID + + + {(isAllowed) => ( + + handlePopUpOpen("assumePrivileges", { + userId: membershipDetails.user.id + }) + } + > + Assume Privileges + +
+ +
+
+
+ )} +
+ + {(isAllowed) => ( + handlePopUpOpen("removeMember")} + > + Remove User From Project + + )} + +
+
+ )} - - handlePopUpOpen("upgradePlan", { - text: "Assigning custom roles to members can be unlocked if you upgrade to Infisical Pro plan." - }) - } - /> - +
+ +
+ + handlePopUpOpen("upgradePlan", { + text: "Assigning custom roles to members can be unlocked if you upgrade to Infisical Pro plan." + }) + } + /> + +
+
- - {popUp?.modifyPrivilege.isOpen ? ( - - handlePopUpClose("modifyPrivilege")} - projectMembershipId={membershipDetails?.id} - privilegeId={(popUp?.modifyPrivilege?.data as { id: string })?.id} - isDisabled={ - isOwnProjectMembershipDetails || - permission.cannot(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member) - } - /> - - ) : ( - -
-

- Project Additional Privileges -

- {userId !== membershipDetails?.user?.id && - membershipDetails?.status !== "invited" && ( - + + + Project Additional Privileges + Assign one-off policies to this user + {!isOwnProjectMembershipDetails && hasAdditionalPrivileges && ( + + + {(isAllowed) => ( + { + handlePopUpOpen("modifyPrivilege"); + }} + isDisabled={!isAllowed} > - {(isAllowed) => ( - { - handlePopUpOpen("modifyPrivilege"); - }} - isDisabled={!isAllowed} - > - - - )} - + + Add Additional Privileges + )} + + + )} + + + {/* eslint-disable-next-line no-nested-ternary */} + {isPending ? ( + // scott: todo proper loader +
+
-
- - - - - - - - - - {isPending && } - {!isPending && - userProjectPrivileges?.map((privilegeDetails) => { - const isTemporary = privilegeDetails?.isTemporary; - const isExpired = - privilegeDetails.isTemporary && - new Date() > new Date(privilegeDetails.temporaryAccessEndTime || ""); + ) : userProjectPrivileges?.length ? ( + + + + Name + Duration + {!isOwnProjectMembershipDetails && } + + + + {!isPending && + userProjectPrivileges?.map((privilegeDetails) => { + const isTemporary = privilegeDetails?.isTemporary; + const isExpired = + privilegeDetails.isTemporary && + new Date() > new Date(privilegeDetails.temporaryAccessEndTime || ""); - let text = "Permanent"; - let toolTipText = "Non-Expiring Access"; - if (privilegeDetails.isTemporary) { - if (isExpired) { - text = "Access Expired"; - toolTipText = "Timed Access Expired"; - } else { - text = formatDistance( - new Date(privilegeDetails.temporaryAccessEndTime || ""), - new Date() - ); - toolTipText = `Until ${format( - new Date(privilegeDetails.temporaryAccessEndTime || ""), - "yyyy-MM-dd hh:mm:ss aaa" - )}`; - } - } + let text = "Permanent"; + let toolTipText = "Non-Expiring Access"; + if (privilegeDetails.isTemporary) { + if (isExpired) { + text = "Access Expired"; + toolTipText = "Timed Access Expired"; + } else { + text = formatDistance( + new Date(privilegeDetails.temporaryAccessEndTime || ""), + new Date() + ); + toolTipText = `Until ${format( + new Date(privilegeDetails.temporaryAccessEndTime || ""), + "yyyy-MM-dd hh:mm:ss aaa" + )}`; + } + } - return ( - { - if (evt.key === "Enter") { - handlePopUpOpen("modifyPrivilege", privilegeDetails); - } - }} - onClick={() => handlePopUpOpen("modifyPrivilege", privilegeDetails)} - > - - - - - ); - })} - -
NameDuration -
{privilegeDetails.slug} - - - {text} - - - -
+ return ( + + + {privilegeDetails.slug} + + + {isTemporary ? ( + + + {isExpired ? : } + {text} + + + ) : ( + text + )} + + {!isOwnProjectMembershipDetails && ( + + + + + + + + {(isAllowed) => ( - { + e.stopPropagation(); + handlePopUpOpen("modifyPrivilege", privilegeDetails); + }} + > + Edit Additional Privilege + + )} + + + {(isAllowed) => ( + { e.stopPropagation(); - e.preventDefault(); handlePopUpOpen("deletePrivilege", { id: privilegeDetails?.id, slug: privilegeDetails?.slug }); }} > - - + Remove Additional Privilege + )} - - - -
-
- {!isPending && !userProjectPrivileges?.length && ( - - )} -
-
- handlePopUpToggle("deletePrivilege", isOpen)} - onDeleteApproved={() => handlePrivilegeDelete()} - /> - - )} - -
+ + + + )} + + ); + })} + + + ) : ( + + + This user has no additional privileges + + Add an additional privilege to grant one-off access policies + + + {!isOwnProjectMembershipDetails && ( + + + {(isAllowed) => ( + { + handlePopUpOpen("modifyPrivilege"); + }} + isDisabled={!isAllowed || isOwnProjectMembershipDetails} + > + + Add Additional Privileges + + )} + + + )} + + )} + + + handlePopUpToggle("modifyPrivilege", isOpen)} + > + + handlePopUpClose("modifyPrivilege")} + projectMembershipId={membershipDetails?.id} + privilegeId={(popUp?.modifyPrivilege?.data as { id: string })?.id} + isDisabled={ + isOwnProjectMembershipDetails || + permission.cannot(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member) + } + /> + + + handlePopUpToggle("deletePrivilege", isOpen)} + onDeleteApproved={() => handlePrivilegeDelete()} + /> + ); }; diff --git a/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx b/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx index 6719d3785b..7185f5a2f0 100644 --- a/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx +++ b/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx @@ -1,5 +1,5 @@ import { Controller, FormProvider, useForm } from "react-hook-form"; -import { faCaretDown, faChevronLeft, faClock, faSave } from "@fortawesome/free-solid-svg-icons"; +import { faCaretDown, faClock, faSave } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; import { format, formatDistance } from "date-fns"; @@ -20,6 +20,7 @@ import { Tag, Tooltip } from "@app/components/v2"; +import { UnstableSeparator } from "@app/components/v3"; import { ProjectPermissionMemberActions, ProjectPermissionSub, @@ -111,6 +112,7 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({ const { handleSubmit, + reset, formState: { isDirty, isSubmitting } } = form; @@ -176,59 +178,9 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({ } return ( -
+ -
- -
- {isDirty && ( - - )} -
- - -
-
-
-
-
Overview
-

- Additional privileges take precedence over roles when permissions conflict -

+
-
-
Policies
- {(isCreate || !isPending) && } -
- {(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => ( - - {renderConditionalComponents(subject, isDisabled)} - - ))} + +
+
+
Policies
+
+ {isDirty && ( + + )} +
+ +
+
+ {(isCreate || !isPending) && } +
+ {(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map( + (permissionSubject) => ( + + {renderConditionalComponents(permissionSubject, isDisabled)} + + ) + )} +
+
+ +
+ +
diff --git a/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberRoleDetailsSection/MemberRoleDetailsSection.tsx b/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberRoleDetailsSection/MemberRoleDetailsSection.tsx index 2654e237e2..f935eb67b7 100644 --- a/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberRoleDetailsSection/MemberRoleDetailsSection.tsx +++ b/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberRoleDetailsSection/MemberRoleDetailsSection.tsx @@ -1,27 +1,35 @@ -import { faFolder, faPencil, faTrash } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { format, formatDistance } from "date-fns"; -import { twMerge } from "tailwind-merge"; +import { ClockAlertIcon, ClockIcon, EllipsisIcon, PencilIcon } from "lucide-react"; import { createNotification } from "@app/components/notifications"; import { ProjectPermissionCan } from "@app/components/permissions"; +import { DeleteActionModal, Lottie, Modal, ModalContent, Tooltip } from "@app/components/v2"; import { - DeleteActionModal, - EmptyState, - IconButton, - Modal, - ModalContent, - Table, - TableContainer, - TableSkeleton, - Tag, - TBody, - Td, - Th, - THead, - Tooltip, - Tr -} from "@app/components/v2"; + Badge, + UnstableButton, + UnstableCard, + UnstableCardAction, + UnstableCardContent, + UnstableCardDescription, + UnstableCardHeader, + UnstableCardTitle, + UnstableDropdownMenu, + UnstableDropdownMenuContent, + UnstableDropdownMenuItem, + UnstableDropdownMenuTrigger, + UnstableEmpty, + UnstableEmptyContent, + UnstableEmptyDescription, + UnstableEmptyHeader, + UnstableEmptyTitle, + UnstableIconButton, + UnstableTable, + UnstableTableBody, + UnstableTableCell, + UnstableTableHead, + UnstableTableHeader, + UnstableTableRow +} from "@app/components/v3/generic"; import { ProjectPermissionActions, ProjectPermissionSub, useProject, useUser } from "@app/context"; import { formatProjectRoleName } from "@app/helpers/roles"; import { usePopUp } from "@app/hooks"; @@ -88,131 +96,176 @@ export const MemberRoleDetailsSection = ({ handlePopUpClose("deleteRole"); }; + const hasRoles = Boolean(membershipDetails?.roles.length); + return ( -
-
-

Project Roles

- {!isOwnProjectMembershipDetails && membershipDetails?.status !== "invited" && ( - - {(isAllowed) => ( - { - handlePopUpOpen("modifyRole"); - }} - isDisabled={!isAllowed} + <> + + + Project Roles + Manage roles assigned to this user + {!isOwnProjectMembershipDetails && hasRoles && ( + + - - - )} - - )} -
-
- - - - - - - - - - {isMembershipDetailsLoading && ( - - )} - {!isMembershipDetailsLoading && - membershipDetails?.roles?.map((roleDetails) => { - const isTemporary = roleDetails?.isTemporary; - const isExpired = - roleDetails.isTemporary && - new Date() > new Date(roleDetails.temporaryAccessEndTime || ""); - - let text = "Permanent"; - let toolTipText = "Non-Expiring Access"; - if (roleDetails.isTemporary) { - if (isExpired) { - text = "Access Expired"; - toolTipText = "Timed Access Expired"; - } else { - text = formatDistance( - new Date(roleDetails.temporaryAccessEndTime || ""), - new Date() - ); - toolTipText = `Until ${format( - new Date(roleDetails.temporaryAccessEndTime || ""), - "yyyy-MM-dd hh:mm:ss aaa" - )}`; - } - } - - return ( - - - - - - ); - })} - -
RoleDuration -
- {roleDetails.role === "custom" - ? roleDetails.customRoleName - : formatProjectRoleName(roleDetails.role)} - - - - {text} - - - -
- - {(isAllowed) => ( - { - e.stopPropagation(); - handlePopUpOpen("deleteRole", { - id: roleDetails?.id, - slug: roleDetails?.customRoleName || roleDetails?.role - }); - }} - > - - - )} - -
-
- {!isMembershipDetailsLoading && !membershipDetails?.roles?.length && ( - + {(isAllowed) => ( + { + handlePopUpOpen("modifyRole"); + }} + isDisabled={!isAllowed} + > + + Edit Roles + + )} + + )} -
-
+ + + { + /* eslint-disable-next-line no-nested-ternary */ + isMembershipDetailsLoading ? ( + // scott: todo proper loader +
+ +
+ ) : hasRoles ? ( + + + + Role + Duration + {!isOwnProjectMembershipDetails && } + + + + {membershipDetails?.roles?.map((roleDetails) => { + const isTemporary = roleDetails?.isTemporary; + const isExpired = + roleDetails.isTemporary && + new Date() > new Date(roleDetails.temporaryAccessEndTime || ""); + + let text = "Permanent"; + let toolTipText = "Non-Expiring Access"; + if (roleDetails.isTemporary) { + if (isExpired) { + text = "Access Expired"; + toolTipText = "Timed Access Expired"; + } else { + text = formatDistance( + new Date(roleDetails.temporaryAccessEndTime || ""), + new Date() + ); + toolTipText = `Until ${format( + new Date(roleDetails.temporaryAccessEndTime || ""), + "yyyy-MM-dd hh:mm:ss aaa" + )}`; + } + } + + return ( + + + {roleDetails.role === "custom" + ? roleDetails.customRoleName + : formatProjectRoleName(roleDetails.role)} + + + {isTemporary ? ( + + + {isExpired ? : } + {text} + + + ) : ( + text + )} + + {!isOwnProjectMembershipDetails && ( + + + + + + + + + + {(isAllowed) => ( + { + e.stopPropagation(); + handlePopUpOpen("deleteRole", { + id: roleDetails?.id, + slug: roleDetails?.customRoleName || roleDetails?.role + }); + }} + isDisabled={!isAllowed} + variant="danger" + > + Remove Role + + )} + + + + + )} + + ); + })} + + + ) : ( + + + This user doesn t have any roles + + Give this user one or more roles + + + + + {(isAllowed) => ( + { + handlePopUpOpen("modifyRole"); + }} + isDisabled={!isAllowed || isOwnProjectMembershipDetails} + > + + Edit Roles + + )} + + + + ) + } +
+ + -
+ ); }; diff --git a/frontend/src/pages/project/MemberDetailsByIDPage/components/ProjectMemberDetailsSection.tsx b/frontend/src/pages/project/MemberDetailsByIDPage/components/ProjectMemberDetailsSection.tsx new file mode 100644 index 0000000000..09cd3593de --- /dev/null +++ b/frontend/src/pages/project/MemberDetailsByIDPage/components/ProjectMemberDetailsSection.tsx @@ -0,0 +1,105 @@ +import { format } from "date-fns"; +import { CheckIcon, ClipboardListIcon } from "lucide-react"; + +import { Tooltip } from "@app/components/v2"; +import { + Detail, + DetailGroup, + DetailLabel, + DetailValue, + UnstableCard, + UnstableCardContent, + UnstableCardDescription, + UnstableCardHeader, + UnstableCardTitle, + UnstableIconButton +} from "@app/components/v3"; +import { useTimedReset } from "@app/hooks"; +import { TWorkspaceUser } from "@app/hooks/api/types"; + +type Props = { + membership: TWorkspaceUser; +}; + +export const ProjectMemberDetailsSection = ({ membership }: Props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/naming-convention + const [_copyId, isCopyingId, setCopyTextId] = useTimedReset({ + initialState: "Copy ID to clipboard" + }); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/naming-convention + const [_copyEmail, isCopyingEmail, setCopyEmail] = useTimedReset({ + initialState: "Copy email to clipboard" + }); + + const { + user: { email, username, firstName, lastName, id: userId } + } = membership; + + const name = firstName || lastName ? `${firstName} ${lastName}`.trim() : null; + + return ( + + + Details + User membership details + + + + + Name + {name || Not set} + + + ID + + {membership.user.id} + + { + navigator.clipboard.writeText(userId); + setCopyTextId("Copied"); + }} + variant="ghost" + size="xs" + > + {/* TODO(scott): color this should be a button variant and create re-usable copy button */} + {isCopyingId ? : } + + + + + + Email + + {email} + + { + navigator.clipboard.writeText(email); + setCopyEmail("Copied"); + }} + variant="ghost" + size="xs" + > + {/* TODO(scott): color this should be a button variant and create re-usable copy button */} + {isCopyingEmail ? : } + + + + + {username !== email && ( + + Username + {username || Not set} + + )} + + Joined project + {format(membership.createdAt, "PPpp")} + + + + + ); +};