mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge pull request #5020 from Infisical/ENG-4283-1
improvement(frontend): update and refactor user project membership page components and add details section
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -32,7 +32,7 @@ export const PageHeader = ({ title, description, children, className, scope }: P
|
||||
<div className="mr-4 flex w-full items-center">
|
||||
<h1
|
||||
className={twMerge(
|
||||
"text-2xl font-medium text-white capitalize underline decoration-2 underline-offset-4",
|
||||
"text-2xl font-medium text-white underline decoration-2 underline-offset-4",
|
||||
scope === "org" && "decoration-org/90",
|
||||
scope === "instance" && "decoration-neutral/90",
|
||||
scope === "namespace" && "decoration-sub-org/90",
|
||||
|
||||
@@ -11,8 +11,9 @@ const alertVariants = cva(
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-container text-card-foreground",
|
||||
info: "bg-info/10 text-info border-info/20",
|
||||
org: "bg-org/10 text-org border-org/20"
|
||||
info: "bg-info/5 text-info border-info/20",
|
||||
org: "bg-org/5 text-org border-org/20",
|
||||
"sub-org": "bg-sub-org/5 text-sub-org border-sub-org/20"
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
@@ -16,7 +16,7 @@ export const IdentityAuthFieldDisplay = ({ label, children, className }: Props)
|
||||
{children ? (
|
||||
<p className="break-words">{children}</p>
|
||||
) : (
|
||||
<p className="text-muted italic">Not set</p>
|
||||
<p className="text-muted">Not set</p>
|
||||
)}
|
||||
</DetailValue>
|
||||
</Detail>
|
||||
|
||||
@@ -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 <UnstablePageLoader />;
|
||||
}
|
||||
|
||||
const isOrgIdentity = !isProjectIdentity;
|
||||
const isSubOrgIdentity =
|
||||
isOrgIdentity &&
|
||||
isSubOrganization &&
|
||||
currentOrg.rootOrgId !== identityMembershipDetails?.identity.orgId;
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex max-w-8xl flex-col">
|
||||
{identityMembershipDetails ? (
|
||||
@@ -187,7 +194,7 @@ const Page = () => {
|
||||
description={`Configure and manage${isProjectIdentity ? " machine identity and " : " "}project access control`}
|
||||
title={identityMembershipDetails.identity.name}
|
||||
>
|
||||
<DropdownMenu>
|
||||
<UnstableDropdownMenu>
|
||||
<UnstableDropdownMenuTrigger asChild>
|
||||
<UnstableButton variant="outline">
|
||||
Options
|
||||
@@ -197,7 +204,7 @@ const Page = () => {
|
||||
<UnstableDropdownMenuContent align="end">
|
||||
<UnstableDropdownMenuItem
|
||||
onClick={() => {
|
||||
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) => (
|
||||
<UnstableDropdownMenuItem
|
||||
@@ -221,7 +227,7 @@ const Page = () => {
|
||||
Assume Privileges
|
||||
<Tooltip
|
||||
side="bottom"
|
||||
content="Assume the privileges of the machine identity, allowing you to replicate their access behavior."
|
||||
content="Assume the privileges of this machine identity, allowing you to replicate their access behavior."
|
||||
>
|
||||
<div>
|
||||
<InfoIcon className="text-muted" />
|
||||
@@ -251,12 +257,13 @@ const Page = () => {
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</UnstableDropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</UnstableDropdownMenu>
|
||||
</PageHeader>
|
||||
<div className="flex flex-col gap-5 lg:flex-row">
|
||||
<ProjectIdentityDetailsSection
|
||||
identity={identity || { ...identityMembershipDetails?.identity, projectId: "" }}
|
||||
isOrgIdentity={!isProjectIdentity}
|
||||
isOrgIdentity={isOrgIdentity}
|
||||
isSubOrgIdentity={isSubOrgIdentity}
|
||||
membership={identityMembershipDetails!}
|
||||
/>
|
||||
|
||||
@@ -275,15 +282,15 @@ const Page = () => {
|
||||
</UnstableCardDescription>
|
||||
</UnstableCardHeader>
|
||||
<UnstableCardContent>
|
||||
<UnstableAlert variant="org">
|
||||
<OrgIcon />
|
||||
<UnstableAlert variant={isSubOrgIdentity ? "sub-org" : "org"}>
|
||||
{isSubOrgIdentity ? <SubOrgIcon /> : <OrgIcon />}
|
||||
<UnstableAlertTitle>
|
||||
Machine identity managed by organization
|
||||
Machine identity managed by {isSubOrgIdentity ? "sub-" : ""}organization
|
||||
</UnstableAlertTitle>
|
||||
<UnstableAlertDescription>
|
||||
<p>
|
||||
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. <br /> To make changes,{" "}
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionIdentityActions.Read}
|
||||
an={OrgPermissionSubjects.Identity}
|
||||
@@ -295,10 +302,10 @@ const Page = () => {
|
||||
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
|
||||
</Link>
|
||||
) : null
|
||||
}
|
||||
|
||||
@@ -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
|
||||
</Button>
|
||||
|
||||
@@ -81,9 +81,7 @@ export const IdentityProjectAdditionalPrivilegeSection = ({ identityMembershipDe
|
||||
return (
|
||||
<>
|
||||
<UnstableCard>
|
||||
<UnstableCardHeader
|
||||
// className="border-b"
|
||||
>
|
||||
<UnstableCardHeader>
|
||||
<UnstableCardTitle>Project Additional Privileges</UnstableCardTitle>
|
||||
<UnstableCardDescription>
|
||||
Assign one-off policies to this machine identity
|
||||
|
||||
@@ -96,9 +96,7 @@ export const IdentityRoleDetailsSection = ({
|
||||
return (
|
||||
<>
|
||||
<UnstableCard>
|
||||
<UnstableCardHeader
|
||||
// className="border-b"
|
||||
>
|
||||
<UnstableCardHeader>
|
||||
<UnstableCardTitle>Project Roles</UnstableCardTitle>
|
||||
<UnstableCardDescription>
|
||||
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) => (
|
||||
<UnstableDropdownMenuItem
|
||||
@@ -223,7 +219,6 @@ export const IdentityRoleDetailsSection = ({
|
||||
isDisabled={!isAllowed}
|
||||
variant="danger"
|
||||
>
|
||||
{/* <TrashIcon /> */}
|
||||
Remove Role
|
||||
</UnstableDropdownMenuItem>
|
||||
)}
|
||||
|
||||
@@ -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<string>({
|
||||
initialState: "Copy ID to clipboard"
|
||||
@@ -43,10 +50,8 @@ export const ProjectIdentityDetailsSection = ({ identity, isOrgIdentity, members
|
||||
|
||||
return (
|
||||
<>
|
||||
<UnstableCard className="w-full max-w-[22rem]">
|
||||
<UnstableCardHeader
|
||||
// className="border-b"
|
||||
>
|
||||
<UnstableCard className="w-full lg:max-w-[24rem]">
|
||||
<UnstableCardHeader>
|
||||
<UnstableCardTitle>Details</UnstableCardTitle>
|
||||
<UnstableCardDescription>Machine identity details</UnstableCardDescription>
|
||||
{!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 ? <CheckIcon /> : <ClipboardListIcon className="text-label" />}
|
||||
</UnstableIconButton>
|
||||
</Tooltip>
|
||||
@@ -102,9 +107,9 @@ export const ProjectIdentityDetailsSection = ({ identity, isOrgIdentity, members
|
||||
<DetailLabel>Managed by</DetailLabel>
|
||||
<DetailValue>
|
||||
{isOrgIdentity ? (
|
||||
<Badge variant="org">
|
||||
<OrgIcon />
|
||||
Organization
|
||||
<Badge variant={isSubOrgIdentity ? "sub-org" : "org"}>
|
||||
{isSubOrgIdentity ? <SubOrgIcon /> : <OrgIcon />}
|
||||
{isSubOrgIdentity ? "Sub-" : ""}Organization
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="project">
|
||||
@@ -129,7 +134,7 @@ export const ProjectIdentityDetailsSection = ({ identity, isOrgIdentity, members
|
||||
</UnstableButtonGroup>
|
||||
))
|
||||
) : (
|
||||
<span className="text-muted italic">No metadata</span>
|
||||
<span className="text-muted">No metadata</span>
|
||||
)}
|
||||
</DetailValue>
|
||||
</Detail>
|
||||
@@ -145,7 +150,7 @@ export const ProjectIdentityDetailsSection = ({ identity, isOrgIdentity, members
|
||||
{membership.lastLoginAuthMethod ? (
|
||||
identityAuthToNameMap[membership.lastLoginAuthMethod]
|
||||
) : (
|
||||
<span className="text-muted italic">N/A</span>
|
||||
<span className="text-muted">N/A</span>
|
||||
)}
|
||||
</DetailValue>
|
||||
</Detail>
|
||||
@@ -155,7 +160,7 @@ export const ProjectIdentityDetailsSection = ({ identity, isOrgIdentity, members
|
||||
{membership.lastLoginTime ? (
|
||||
format(membership.lastLoginTime, "PPpp")
|
||||
) : (
|
||||
<span className="text-muted italic">N/A</span>
|
||||
<span className="text-muted">N/A</span>
|
||||
)}
|
||||
</DetailValue>
|
||||
</Detail>
|
||||
|
||||
@@ -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 (
|
||||
<div className="mx-auto flex max-w-8xl flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto flex max-w-8xl flex-col">
|
||||
{membershipDetails ? (
|
||||
<>
|
||||
<Link
|
||||
@@ -125,7 +139,7 @@ export const Page = () => {
|
||||
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"
|
||||
>
|
||||
<FontAwesomeIcon icon={faChevronLeft} />
|
||||
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"
|
||||
>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionMemberActions.AssumePrivileges}
|
||||
a={ProjectPermissionSub.Member}
|
||||
renderTooltip
|
||||
allowedLabel="Assume privileges of the user"
|
||||
passThrough={false}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
isDisabled={!isAllowed}
|
||||
isLoading={assumePrivileges.isPending}
|
||||
onClick={() =>
|
||||
handlePopUpOpen("assumePrivileges", { userId: membershipDetails?.user?.id })
|
||||
}
|
||||
>
|
||||
Assume Privileges
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionMemberActions.Delete}
|
||||
a={ProjectPermissionSub.Member}
|
||||
renderTooltip
|
||||
allowedLabel="Remove from project"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="danger"
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
isDisabled={!isAllowed}
|
||||
isLoading={isRemovingUserFromWorkspace}
|
||||
onClick={() => handlePopUpOpen("removeMember")}
|
||||
>
|
||||
Remove User
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
{isOwnProjectMembershipDetails ? (
|
||||
<Tooltip
|
||||
side="right"
|
||||
content="You cannot modify your own membership. Ask a project admin to make changes to your membership."
|
||||
>
|
||||
<Badge variant="info" className="ml-2">
|
||||
<InfoIcon /> Your project membership
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<UnstableDropdownMenu>
|
||||
<UnstableDropdownMenuTrigger asChild>
|
||||
<UnstableButton variant="outline">
|
||||
Options
|
||||
<EllipsisIcon />
|
||||
</UnstableButton>
|
||||
</UnstableDropdownMenuTrigger>
|
||||
<UnstableDropdownMenuContent align="end">
|
||||
<UnstableDropdownMenuItem
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(membershipDetails.user.id);
|
||||
createNotification({
|
||||
text: "User ID copied to clipboard",
|
||||
type: "info"
|
||||
});
|
||||
}}
|
||||
>
|
||||
Copy User ID
|
||||
</UnstableDropdownMenuItem>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionMemberActions.AssumePrivileges}
|
||||
a={ProjectPermissionSub.Member}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<UnstableDropdownMenuItem
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() =>
|
||||
handlePopUpOpen("assumePrivileges", {
|
||||
userId: membershipDetails.user.id
|
||||
})
|
||||
}
|
||||
>
|
||||
Assume Privileges
|
||||
<Tooltip
|
||||
side="bottom"
|
||||
content="Assume the privileges of this user, allowing you to replicate their access behavior."
|
||||
>
|
||||
<div>
|
||||
<InfoIcon className="text-muted" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</UnstableDropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionMemberActions.Delete}
|
||||
a={ProjectPermissionSub.Member}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<UnstableDropdownMenuItem
|
||||
variant="danger"
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() => handlePopUpOpen("removeMember")}
|
||||
>
|
||||
Remove User From Project
|
||||
</UnstableDropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</UnstableDropdownMenuContent>
|
||||
</UnstableDropdownMenu>
|
||||
)}
|
||||
</PageHeader>
|
||||
<MemberRoleDetailsSection
|
||||
membershipDetails={membershipDetails}
|
||||
isMembershipDetailsLoading={isMembershipDetailsLoading}
|
||||
onOpenUpgradeModal={() =>
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
text: "Assigning custom roles to members can be unlocked if you upgrade to Infisical Pro plan."
|
||||
})
|
||||
}
|
||||
/>
|
||||
<MemberProjectAdditionalPrivilegeSection membershipDetails={membershipDetails} />
|
||||
<div className="flex flex-col gap-5 lg:flex-row">
|
||||
<ProjectMemberDetailsSection membership={membershipDetails} />
|
||||
<div className="flex flex-1 flex-col gap-y-5">
|
||||
<MemberRoleDetailsSection
|
||||
membershipDetails={membershipDetails}
|
||||
isMembershipDetailsLoading={isMembershipDetailsLoading}
|
||||
onOpenUpgradeModal={() =>
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
text: "Assigning custom roles to members can be unlocked if you upgrade to Infisical Pro plan."
|
||||
})
|
||||
}
|
||||
/>
|
||||
<MemberProjectAdditionalPrivilegeSection membershipDetails={membershipDetails} />
|
||||
</div>
|
||||
</div>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.removeMember.isOpen}
|
||||
deleteKey="remove"
|
||||
|
||||
@@ -1,26 +1,35 @@
|
||||
import { faEllipsisV, faFolder, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format, formatDistance } from "date-fns";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { ClockAlertIcon, ClockIcon, EllipsisIcon, PlusIcon } 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,
|
||||
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";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionMemberActions,
|
||||
@@ -68,188 +77,216 @@ export const MemberProjectAdditionalPrivilegeSection = ({ membershipDetails }: P
|
||||
handlePopUpClose("deletePrivilege");
|
||||
};
|
||||
|
||||
const hasAdditionalPrivileges = Boolean(userProjectPrivileges?.length);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<AnimatePresence>
|
||||
{popUp?.modifyPrivilege.isOpen ? (
|
||||
<motion.div
|
||||
key="privilege-modify"
|
||||
transition={{ duration: 0.3 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
className="absolute min-h-40 w-full"
|
||||
>
|
||||
<MembershipProjectAdditionalPrivilegeModifySection
|
||||
onGoBack={() => handlePopUpClose("modifyPrivilege")}
|
||||
projectMembershipId={membershipDetails?.id}
|
||||
privilegeId={(popUp?.modifyPrivilege?.data as { id: string })?.id}
|
||||
isDisabled={
|
||||
isOwnProjectMembershipDetails ||
|
||||
permission.cannot(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member)
|
||||
}
|
||||
/>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="privilege-list"
|
||||
transition={{ duration: 0.3 }}
|
||||
initial={{ opacity: 0, translateX: 0 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: -30 }}
|
||||
className="absolute 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">
|
||||
Project Additional Privileges
|
||||
</h3>
|
||||
{userId !== membershipDetails?.user?.id &&
|
||||
membershipDetails?.status !== "invited" && (
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Member}
|
||||
renderTooltip
|
||||
allowedLabel="Add Privilege"
|
||||
<>
|
||||
<UnstableCard>
|
||||
<UnstableCardHeader>
|
||||
<UnstableCardTitle>Project Additional Privileges</UnstableCardTitle>
|
||||
<UnstableCardDescription>Assign one-off policies to this user</UnstableCardDescription>
|
||||
{!isOwnProjectMembershipDetails && hasAdditionalPrivileges && (
|
||||
<UnstableCardAction>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Member}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<UnstableButton
|
||||
variant="outline"
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
handlePopUpOpen("modifyPrivilege");
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
variant="plain"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
handlePopUpOpen("modifyPrivilege");
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<PlusIcon />
|
||||
Add Additional Privileges
|
||||
</UnstableButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</UnstableCardAction>
|
||||
)}
|
||||
</UnstableCardHeader>
|
||||
<UnstableCardContent>
|
||||
{/* eslint-disable-next-line no-nested-ternary */}
|
||||
{isPending ? (
|
||||
// scott: todo proper loader
|
||||
<div className="flex h-40 w-full items-center justify-center">
|
||||
<Lottie icon="infisical_loading_white" isAutoPlay className="w-16" />
|
||||
</div>
|
||||
<div className="py-4">
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Name</Th>
|
||||
<Th>Duration</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isPending && <TableSkeleton columns={3} innerKey="user-project-memberships" />}
|
||||
{!isPending &&
|
||||
userProjectPrivileges?.map((privilegeDetails) => {
|
||||
const isTemporary = privilegeDetails?.isTemporary;
|
||||
const isExpired =
|
||||
privilegeDetails.isTemporary &&
|
||||
new Date() > new Date(privilegeDetails.temporaryAccessEndTime || "");
|
||||
) : userProjectPrivileges?.length ? (
|
||||
<UnstableTable>
|
||||
<UnstableTableHeader>
|
||||
<UnstableTableRow>
|
||||
<UnstableTableHead className="w-1/2">Name</UnstableTableHead>
|
||||
<UnstableTableHead className="w-1/2">Duration</UnstableTableHead>
|
||||
{!isOwnProjectMembershipDetails && <UnstableTableHead className="w-5" />}
|
||||
</UnstableTableRow>
|
||||
</UnstableTableHeader>
|
||||
<UnstableTableBody>
|
||||
{!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 (
|
||||
<Tr
|
||||
key={`user-project-privilege-${privilegeDetails?.id}`}
|
||||
className="group w-full cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(evt) => {
|
||||
if (evt.key === "Enter") {
|
||||
handlePopUpOpen("modifyPrivilege", privilegeDetails);
|
||||
}
|
||||
}}
|
||||
onClick={() => handlePopUpOpen("modifyPrivilege", privilegeDetails)}
|
||||
>
|
||||
<Td>{privilegeDetails.slug}</Td>
|
||||
<Td>
|
||||
<Tooltip asChild={false} content={toolTipText}>
|
||||
<Tag
|
||||
className={twMerge(
|
||||
"capitalize",
|
||||
isTemporary && "text-primary",
|
||||
isExpired && "text-red-600"
|
||||
)}
|
||||
>
|
||||
{text}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex space-x-2 opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
||||
return (
|
||||
<UnstableTableRow key={`user-project-privilege-${privilegeDetails?.id}`}>
|
||||
<UnstableTableCell className="max-w-0 truncate">
|
||||
{privilegeDetails.slug}
|
||||
</UnstableTableCell>
|
||||
<UnstableTableCell>
|
||||
{isTemporary ? (
|
||||
<Tooltip content={toolTipText}>
|
||||
<Badge
|
||||
className="capitalize"
|
||||
variant={isExpired ? "danger" : "warning"}
|
||||
>
|
||||
{isExpired ? <ClockAlertIcon /> : <ClockIcon />}
|
||||
{text}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
) : (
|
||||
text
|
||||
)}
|
||||
</UnstableTableCell>
|
||||
{!isOwnProjectMembershipDetails && (
|
||||
<UnstableTableCell>
|
||||
<UnstableDropdownMenu>
|
||||
<UnstableDropdownMenuTrigger asChild>
|
||||
<UnstableIconButton size="xs" variant="ghost">
|
||||
<EllipsisIcon />
|
||||
</UnstableIconButton>
|
||||
</UnstableDropdownMenuTrigger>
|
||||
<UnstableDropdownMenuContent align="end">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Member}
|
||||
renderTooltip
|
||||
allowedLabel="Remove Role"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
colorSchema="danger"
|
||||
ariaLabel="delete-icon"
|
||||
variant="plain"
|
||||
className="group relative"
|
||||
isDisabled={!isAllowed || isOwnProjectMembershipDetails}
|
||||
<UnstableDropdownMenuItem
|
||||
isDisabled={!isAllowed}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("modifyPrivilege", privilegeDetails);
|
||||
}}
|
||||
>
|
||||
Edit Additional Privilege
|
||||
</UnstableDropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Member}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<UnstableDropdownMenuItem
|
||||
isDisabled={!isAllowed}
|
||||
variant="danger"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
handlePopUpOpen("deletePrivilege", {
|
||||
id: privilegeDetails?.id,
|
||||
slug: privilegeDetails?.slug
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
Remove Additional Privilege
|
||||
</UnstableDropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<IconButton
|
||||
ariaLabel="more-icon"
|
||||
variant="plain"
|
||||
className="group relative"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{!isPending && !userProjectPrivileges?.length && (
|
||||
<EmptyState title="This user has no additional privileges" icon={faFolder} />
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deletePrivilege.isOpen}
|
||||
deleteKey="remove"
|
||||
title={`Do you want to remove role ${
|
||||
(popUp?.deletePrivilege?.data as { slug: string; id: string })?.slug
|
||||
}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deletePrivilege", isOpen)}
|
||||
onDeleteApproved={() => handlePrivilegeDelete()}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</UnstableDropdownMenuContent>
|
||||
</UnstableDropdownMenu>
|
||||
</UnstableTableCell>
|
||||
)}
|
||||
</UnstableTableRow>
|
||||
);
|
||||
})}
|
||||
</UnstableTableBody>
|
||||
</UnstableTable>
|
||||
) : (
|
||||
<UnstableEmpty className="border">
|
||||
<UnstableEmptyHeader>
|
||||
<UnstableEmptyTitle>This user has no additional privileges</UnstableEmptyTitle>
|
||||
<UnstableEmptyDescription>
|
||||
Add an additional privilege to grant one-off access policies
|
||||
</UnstableEmptyDescription>
|
||||
</UnstableEmptyHeader>
|
||||
{!isOwnProjectMembershipDetails && (
|
||||
<UnstableEmptyContent>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Member}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<UnstableButton
|
||||
variant="project"
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
handlePopUpOpen("modifyPrivilege");
|
||||
}}
|
||||
isDisabled={!isAllowed || isOwnProjectMembershipDetails}
|
||||
>
|
||||
<PlusIcon />
|
||||
Add Additional Privileges
|
||||
</UnstableButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</UnstableEmptyContent>
|
||||
)}
|
||||
</UnstableEmpty>
|
||||
)}
|
||||
</UnstableCardContent>
|
||||
</UnstableCard>
|
||||
<Modal
|
||||
isOpen={popUp.modifyPrivilege.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("modifyPrivilege", isOpen)}
|
||||
>
|
||||
<ModalContent
|
||||
className="max-w-6xl"
|
||||
title="Additional Privileges"
|
||||
subTitle="Additional privileges take precedence over roles when permissions conflict"
|
||||
>
|
||||
<MembershipProjectAdditionalPrivilegeModifySection
|
||||
onGoBack={() => handlePopUpClose("modifyPrivilege")}
|
||||
projectMembershipId={membershipDetails?.id}
|
||||
privilegeId={(popUp?.modifyPrivilege?.data as { id: string })?.id}
|
||||
isDisabled={
|
||||
isOwnProjectMembershipDetails ||
|
||||
permission.cannot(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member)
|
||||
}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deletePrivilege.isOpen}
|
||||
deleteKey="remove"
|
||||
title={`Do you want to remove role ${
|
||||
(popUp?.deletePrivilege?.data as { slug: string; id: string })?.slug
|
||||
}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deletePrivilege", isOpen)}
|
||||
onDeleteApproved={() => handlePrivilegeDelete()}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
|
||||
>
|
||||
<form className="flex flex-col gap-y-4" onSubmit={handleSubmit(onSubmit)}>
|
||||
<FormProvider {...form}>
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
|
||||
className="text-lg font-medium text-mineshaft-100"
|
||||
variant="link"
|
||||
onClick={onGoBack}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
<div className="flex items-center space-x-4">
|
||||
{isDirty && (
|
||||
<Button
|
||||
className="mr-4 text-mineshaft-300"
|
||||
variant="link"
|
||||
isDisabled={isSubmitting}
|
||||
isLoading={isSubmitting}
|
||||
onClick={onGoBack}
|
||||
>
|
||||
Discard
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
type="submit"
|
||||
className={twMerge(
|
||||
"mr-4 h-10 border border-primary",
|
||||
isDirty && "bg-primary text-black"
|
||||
)}
|
||||
isDisabled={isSubmitting || !isDirty || isDisabled}
|
||||
isLoading={isSubmitting}
|
||||
leftIcon={<FontAwesomeIcon icon={faSave} />}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<AddPoliciesButton
|
||||
isDisabled={isDisabled}
|
||||
projectType={currentProject.type}
|
||||
projectId={projectId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 border-b border-gray-800 p-4 pt-2 first:rounded-t-md last:rounded-b-md">
|
||||
<div className="text-lg">Overview</div>
|
||||
<p className="mb-4 text-sm text-mineshaft-300">
|
||||
Additional privileges take precedence over roles when permissions conflict
|
||||
</p>
|
||||
<div>
|
||||
<div className="flex items-end space-x-6">
|
||||
<div className="w-full max-w-md">
|
||||
<Controller
|
||||
@@ -344,22 +296,61 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="mb-2 text-lg">Policies</div>
|
||||
{(isCreate || !isPending) && <PermissionEmptyState />}
|
||||
<div>
|
||||
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => (
|
||||
<GeneralPermissionPolicies
|
||||
subject={subject}
|
||||
actions={PROJECT_PERMISSION_OBJECT[subject].actions}
|
||||
title={PROJECT_PERMISSION_OBJECT[subject].title}
|
||||
key={`project-permission-${subject}`}
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
{renderConditionalComponents(subject, isDisabled)}
|
||||
</GeneralPermissionPolicies>
|
||||
))}
|
||||
<UnstableSeparator />
|
||||
<div>
|
||||
<div className="mb-3 flex w-full items-center justify-between">
|
||||
<div className="text-lg">Policies</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
{isDirty && (
|
||||
<Button
|
||||
className="mr-4 text-mineshaft-300"
|
||||
variant="link"
|
||||
isDisabled={isSubmitting}
|
||||
isLoading={isSubmitting}
|
||||
onClick={() => reset()}
|
||||
>
|
||||
Discard Changes
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex items-center">
|
||||
<AddPoliciesButton
|
||||
isDisabled={isDisabled}
|
||||
projectType={currentProject.type}
|
||||
projectId={projectId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{(isCreate || !isPending) && <PermissionEmptyState />}
|
||||
<div className="scrollbar-thin max-h-[50vh] overflow-y-auto">
|
||||
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map(
|
||||
(permissionSubject) => (
|
||||
<GeneralPermissionPolicies
|
||||
subject={permissionSubject}
|
||||
actions={PROJECT_PERMISSION_OBJECT[permissionSubject].actions}
|
||||
title={PROJECT_PERMISSION_OBJECT[permissionSubject].title}
|
||||
key={`project-permission-${permissionSubject}`}
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
{renderConditionalComponents(permissionSubject, isDisabled)}
|
||||
</GeneralPermissionPolicies>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<UnstableSeparator />
|
||||
<div className="flex w-full items-center justify-end gap-x-2">
|
||||
<Button colorSchema="secondary" variant="plain" onClick={onGoBack}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
isDisabled={isSubmitting || !isDirty || isDisabled}
|
||||
isLoading={isSubmitting}
|
||||
leftIcon={<FontAwesomeIcon icon={faSave} />}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</FormProvider>
|
||||
</form>
|
||||
|
||||
@@ -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 (
|
||||
<div className="mb-4 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">Project Roles</h3>
|
||||
{!isOwnProjectMembershipDetails && membershipDetails?.status !== "invited" && (
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Member}
|
||||
renderTooltip
|
||||
allowedLabel="Edit Role(s)"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
variant="plain"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
handlePopUpOpen("modifyRole");
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
<>
|
||||
<UnstableCard>
|
||||
<UnstableCardHeader>
|
||||
<UnstableCardTitle>Project Roles</UnstableCardTitle>
|
||||
<UnstableCardDescription>Manage roles assigned to this user</UnstableCardDescription>
|
||||
{!isOwnProjectMembershipDetails && hasRoles && (
|
||||
<UnstableCardAction>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Member}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPencil} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
)}
|
||||
</div>
|
||||
<div className="py-4">
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Role</Th>
|
||||
<Th>Duration</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isMembershipDetailsLoading && (
|
||||
<TableSkeleton columns={3} innerKey="user-project-memberships" />
|
||||
)}
|
||||
{!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 (
|
||||
<Tr className="group h-10" key={`user-project-membership-${roleDetails?.id}`}>
|
||||
<Td className="capitalize">
|
||||
{roleDetails.role === "custom"
|
||||
? roleDetails.customRoleName
|
||||
: formatProjectRoleName(roleDetails.role)}
|
||||
</Td>
|
||||
<Td>
|
||||
<Tooltip asChild={false} content={toolTipText}>
|
||||
<Tag
|
||||
className={twMerge(
|
||||
"capitalize",
|
||||
isTemporary && "text-primary",
|
||||
isExpired && "text-red-600"
|
||||
)}
|
||||
>
|
||||
{text}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Member}
|
||||
renderTooltip
|
||||
allowedLabel="Remove Role"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
colorSchema="danger"
|
||||
ariaLabel="copy icon"
|
||||
variant="plain"
|
||||
className="group relative"
|
||||
isDisabled={!isAllowed || isOwnProjectMembershipDetails}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("deleteRole", {
|
||||
id: roleDetails?.id,
|
||||
slug: roleDetails?.customRoleName || roleDetails?.role
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{!isMembershipDetailsLoading && !membershipDetails?.roles?.length && (
|
||||
<EmptyState title="This user has no roles" icon={faFolder} />
|
||||
{(isAllowed) => (
|
||||
<UnstableButton
|
||||
size="xs"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
handlePopUpOpen("modifyRole");
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<PencilIcon />
|
||||
Edit Roles
|
||||
</UnstableButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</UnstableCardAction>
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
</UnstableCardHeader>
|
||||
<UnstableCardContent>
|
||||
{
|
||||
/* eslint-disable-next-line no-nested-ternary */
|
||||
isMembershipDetailsLoading ? (
|
||||
// scott: todo proper loader
|
||||
<div className="flex h-40 w-full items-center justify-center">
|
||||
<Lottie icon="infisical_loading_white" isAutoPlay className="w-16" />
|
||||
</div>
|
||||
) : hasRoles ? (
|
||||
<UnstableTable>
|
||||
<UnstableTableHeader>
|
||||
<UnstableTableRow>
|
||||
<UnstableTableHead className="w-1/2">Role</UnstableTableHead>
|
||||
<UnstableTableHead className="w-1/2">Duration</UnstableTableHead>
|
||||
{!isOwnProjectMembershipDetails && <UnstableTableHead className="w-5" />}
|
||||
</UnstableTableRow>
|
||||
</UnstableTableHeader>
|
||||
<UnstableTableBody>
|
||||
{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 (
|
||||
<UnstableTableRow
|
||||
className="group h-10"
|
||||
key={`user-project-identity-${roleDetails?.id}`}
|
||||
>
|
||||
<UnstableTableCell className="max-w-0 truncate">
|
||||
{roleDetails.role === "custom"
|
||||
? roleDetails.customRoleName
|
||||
: formatProjectRoleName(roleDetails.role)}
|
||||
</UnstableTableCell>
|
||||
<UnstableTableCell>
|
||||
{isTemporary ? (
|
||||
<Tooltip content={toolTipText}>
|
||||
<Badge
|
||||
className="capitalize"
|
||||
variant={isExpired ? "danger" : "warning"}
|
||||
>
|
||||
{isExpired ? <ClockAlertIcon /> : <ClockIcon />}
|
||||
{text}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
) : (
|
||||
text
|
||||
)}
|
||||
</UnstableTableCell>
|
||||
{!isOwnProjectMembershipDetails && (
|
||||
<UnstableTableCell>
|
||||
<UnstableDropdownMenu>
|
||||
<UnstableDropdownMenuTrigger asChild>
|
||||
<UnstableIconButton size="xs" variant="ghost">
|
||||
<EllipsisIcon />
|
||||
</UnstableIconButton>
|
||||
</UnstableDropdownMenuTrigger>
|
||||
<UnstableDropdownMenuContent align="end">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Member}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<UnstableDropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("deleteRole", {
|
||||
id: roleDetails?.id,
|
||||
slug: roleDetails?.customRoleName || roleDetails?.role
|
||||
});
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
variant="danger"
|
||||
>
|
||||
Remove Role
|
||||
</UnstableDropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</UnstableDropdownMenuContent>
|
||||
</UnstableDropdownMenu>
|
||||
</UnstableTableCell>
|
||||
)}
|
||||
</UnstableTableRow>
|
||||
);
|
||||
})}
|
||||
</UnstableTableBody>
|
||||
</UnstableTable>
|
||||
) : (
|
||||
<UnstableEmpty className="border">
|
||||
<UnstableEmptyHeader>
|
||||
<UnstableEmptyTitle>This user doesn t have any roles</UnstableEmptyTitle>
|
||||
<UnstableEmptyDescription>
|
||||
Give this user one or more roles
|
||||
</UnstableEmptyDescription>
|
||||
</UnstableEmptyHeader>
|
||||
<UnstableEmptyContent>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Member}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<UnstableButton
|
||||
variant="project"
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
handlePopUpOpen("modifyRole");
|
||||
}}
|
||||
isDisabled={!isAllowed || isOwnProjectMembershipDetails}
|
||||
>
|
||||
<PencilIcon />
|
||||
Edit Roles
|
||||
</UnstableButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</UnstableEmptyContent>
|
||||
</UnstableEmpty>
|
||||
)
|
||||
}
|
||||
</UnstableCardContent>
|
||||
</UnstableCard>
|
||||
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteRole.isOpen}
|
||||
deleteKey="remove"
|
||||
@@ -234,6 +287,6 @@ export const MemberRoleDetailsSection = ({
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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<string>({
|
||||
initialState: "Copy ID to clipboard"
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/naming-convention
|
||||
const [_copyEmail, isCopyingEmail, setCopyEmail] = useTimedReset<string>({
|
||||
initialState: "Copy email to clipboard"
|
||||
});
|
||||
|
||||
const {
|
||||
user: { email, username, firstName, lastName, id: userId }
|
||||
} = membership;
|
||||
|
||||
const name = firstName || lastName ? `${firstName} ${lastName}`.trim() : null;
|
||||
|
||||
return (
|
||||
<UnstableCard className="w-full lg:max-w-[24rem]">
|
||||
<UnstableCardHeader>
|
||||
<UnstableCardTitle>Details</UnstableCardTitle>
|
||||
<UnstableCardDescription>User membership details</UnstableCardDescription>
|
||||
</UnstableCardHeader>
|
||||
<UnstableCardContent>
|
||||
<DetailGroup>
|
||||
<Detail>
|
||||
<DetailLabel>Name</DetailLabel>
|
||||
<DetailValue>{name || <span className="text-muted">Not set</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(userId);
|
||||
setCopyTextId("Copied");
|
||||
}}
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
>
|
||||
{/* TODO(scott): color this should be a button variant and create re-usable copy button */}
|
||||
{isCopyingId ? <CheckIcon /> : <ClipboardListIcon className="text-label" />}
|
||||
</UnstableIconButton>
|
||||
</Tooltip>
|
||||
</DetailValue>
|
||||
</Detail>
|
||||
<Detail>
|
||||
<DetailLabel>Email</DetailLabel>
|
||||
<DetailValue className="flex items-center gap-x-1">
|
||||
{email}
|
||||
<Tooltip content="Copy user email to clipboard">
|
||||
<UnstableIconButton
|
||||
onClick={() => {
|
||||
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 ? <CheckIcon /> : <ClipboardListIcon className="text-label" />}
|
||||
</UnstableIconButton>
|
||||
</Tooltip>
|
||||
</DetailValue>
|
||||
</Detail>
|
||||
{username !== email && (
|
||||
<Detail>
|
||||
<DetailLabel>Username</DetailLabel>
|
||||
<DetailValue>{username || <span className="text-muted">Not set</span>}</DetailValue>
|
||||
</Detail>
|
||||
)}
|
||||
<Detail>
|
||||
<DetailLabel>Joined project</DetailLabel>
|
||||
<DetailValue>{format(membership.createdAt, "PPpp")}</DetailValue>
|
||||
</Detail>
|
||||
</DetailGroup>
|
||||
</UnstableCardContent>
|
||||
</UnstableCard>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user