- 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) => (
-
- handlePopUpOpen("assumePrivileges", { userId: membershipDetails?.user?.id })
- }
- >
- Assume Privileges
-
- )}
-
-
-
- {(isAllowed) => (
- handlePopUpOpen("removeMember")}
- >
- Remove User
-
- )}
-
+ {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
+
+
-
-
-
-
-
- Name
- Duration
-
-
-
-
- {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)}
- >
- {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 (
-