mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-05 00:26:24 +00:00
improvement(frontend): update and refactor user project membership page styling and add details section
This commit is contained in:
@@ -32,7 +32,7 @@ export const PageHeader = ({ title, description, children, className, scope }: P
|
|||||||
<div className="mr-4 flex w-full items-center">
|
<div className="mr-4 flex w-full items-center">
|
||||||
<h1
|
<h1
|
||||||
className={twMerge(
|
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 === "org" && "decoration-org/90",
|
||||||
scope === "instance" && "decoration-neutral/90",
|
scope === "instance" && "decoration-neutral/90",
|
||||||
scope === "namespace" && "decoration-sub-org/90",
|
scope === "namespace" && "decoration-sub-org/90",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { subject } from "@casl/ability";
|
import { subject } from "@casl/ability";
|
||||||
import { DropdownMenu } from "@radix-ui/react-dropdown-menu";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Link, useNavigate, useParams } from "@tanstack/react-router";
|
import { Link, useNavigate, useParams } from "@tanstack/react-router";
|
||||||
import { ChevronLeftIcon, EllipsisIcon, InfoIcon } from "lucide-react";
|
import { ChevronLeftIcon, EllipsisIcon, InfoIcon } from "lucide-react";
|
||||||
@@ -26,6 +25,7 @@ import {
|
|||||||
UnstableCardDescription,
|
UnstableCardDescription,
|
||||||
UnstableCardHeader,
|
UnstableCardHeader,
|
||||||
UnstableCardTitle,
|
UnstableCardTitle,
|
||||||
|
UnstableDropdownMenu,
|
||||||
UnstableDropdownMenuContent,
|
UnstableDropdownMenuContent,
|
||||||
UnstableDropdownMenuItem,
|
UnstableDropdownMenuItem,
|
||||||
UnstableDropdownMenuTrigger,
|
UnstableDropdownMenuTrigger,
|
||||||
@@ -187,7 +187,7 @@ const Page = () => {
|
|||||||
description={`Configure and manage${isProjectIdentity ? " machine identity and " : " "}project access control`}
|
description={`Configure and manage${isProjectIdentity ? " machine identity and " : " "}project access control`}
|
||||||
title={identityMembershipDetails.identity.name}
|
title={identityMembershipDetails.identity.name}
|
||||||
>
|
>
|
||||||
<DropdownMenu>
|
<UnstableDropdownMenu>
|
||||||
<UnstableDropdownMenuTrigger asChild>
|
<UnstableDropdownMenuTrigger asChild>
|
||||||
<UnstableButton variant="outline">
|
<UnstableButton variant="outline">
|
||||||
Options
|
Options
|
||||||
@@ -197,7 +197,7 @@ const Page = () => {
|
|||||||
<UnstableDropdownMenuContent align="end">
|
<UnstableDropdownMenuContent align="end">
|
||||||
<UnstableDropdownMenuItem
|
<UnstableDropdownMenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(identityMembershipDetails.id);
|
navigator.clipboard.writeText(identityMembershipDetails.identity.id);
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Machine identity ID copied to clipboard",
|
text: "Machine identity ID copied to clipboard",
|
||||||
type: "info"
|
type: "info"
|
||||||
@@ -211,7 +211,6 @@ const Page = () => {
|
|||||||
a={subject(ProjectPermissionSub.Identity, {
|
a={subject(ProjectPermissionSub.Identity, {
|
||||||
identityId: identityMembershipDetails?.identity.id
|
identityId: identityMembershipDetails?.identity.id
|
||||||
})}
|
})}
|
||||||
passThrough={false}
|
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<UnstableDropdownMenuItem
|
<UnstableDropdownMenuItem
|
||||||
@@ -221,7 +220,7 @@ const Page = () => {
|
|||||||
Assume Privileges
|
Assume Privileges
|
||||||
<Tooltip
|
<Tooltip
|
||||||
side="bottom"
|
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>
|
<div>
|
||||||
<InfoIcon className="text-muted" />
|
<InfoIcon className="text-muted" />
|
||||||
@@ -251,7 +250,7 @@ const Page = () => {
|
|||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
</UnstableDropdownMenuContent>
|
</UnstableDropdownMenuContent>
|
||||||
</DropdownMenu>
|
</UnstableDropdownMenu>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<div className="flex flex-col gap-5 lg:flex-row">
|
<div className="flex flex-col gap-5 lg:flex-row">
|
||||||
<ProjectIdentityDetailsSection
|
<ProjectIdentityDetailsSection
|
||||||
@@ -283,7 +282,7 @@ const Page = () => {
|
|||||||
<UnstableAlertDescription>
|
<UnstableAlertDescription>
|
||||||
<p>
|
<p>
|
||||||
This machine identity's authentication methods are controlled by your
|
This machine identity's authentication methods are controlled by your
|
||||||
organization. To make changes,{" "}
|
organization. <br /> To make changes,{" "}
|
||||||
<OrgPermissionCan
|
<OrgPermissionCan
|
||||||
I={OrgPermissionIdentityActions.Read}
|
I={OrgPermissionIdentityActions.Read}
|
||||||
an={OrgPermissionSubjects.Identity}
|
an={OrgPermissionSubjects.Identity}
|
||||||
|
|||||||
+2
-1
@@ -114,6 +114,7 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
reset,
|
||||||
formState: { isDirty, isSubmitting }
|
formState: { isDirty, isSubmitting }
|
||||||
} = form;
|
} = form;
|
||||||
|
|
||||||
@@ -310,7 +311,7 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
|
|||||||
variant="link"
|
variant="link"
|
||||||
isDisabled={isSubmitting}
|
isDisabled={isSubmitting}
|
||||||
isLoading={isSubmitting}
|
isLoading={isSubmitting}
|
||||||
onClick={onGoBack}
|
onClick={() => reset()}
|
||||||
>
|
>
|
||||||
Discard Changes
|
Discard Changes
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
+1
-3
@@ -81,9 +81,7 @@ export const IdentityProjectAdditionalPrivilegeSection = ({ identityMembershipDe
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<UnstableCard>
|
<UnstableCard>
|
||||||
<UnstableCardHeader
|
<UnstableCardHeader>
|
||||||
// className="border-b"
|
|
||||||
>
|
|
||||||
<UnstableCardTitle>Project Additional Privileges</UnstableCardTitle>
|
<UnstableCardTitle>Project Additional Privileges</UnstableCardTitle>
|
||||||
<UnstableCardDescription>
|
<UnstableCardDescription>
|
||||||
Assign one-off policies to this machine identity
|
Assign one-off policies to this machine identity
|
||||||
|
|||||||
+1
-6
@@ -96,9 +96,7 @@ export const IdentityRoleDetailsSection = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<UnstableCard>
|
<UnstableCard>
|
||||||
<UnstableCardHeader
|
<UnstableCardHeader>
|
||||||
// className="border-b"
|
|
||||||
>
|
|
||||||
<UnstableCardTitle>Project Roles</UnstableCardTitle>
|
<UnstableCardTitle>Project Roles</UnstableCardTitle>
|
||||||
<UnstableCardDescription>
|
<UnstableCardDescription>
|
||||||
Manage roles assigned to this machine identity
|
Manage roles assigned to this machine identity
|
||||||
@@ -208,8 +206,6 @@ export const IdentityRoleDetailsSection = ({
|
|||||||
a={subject(ProjectPermissionSub.Identity, {
|
a={subject(ProjectPermissionSub.Identity, {
|
||||||
identityId: identityMembershipDetails.identity.id
|
identityId: identityMembershipDetails.identity.id
|
||||||
})}
|
})}
|
||||||
renderTooltip
|
|
||||||
allowedLabel="Remove Role"
|
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<UnstableDropdownMenuItem
|
<UnstableDropdownMenuItem
|
||||||
@@ -223,7 +219,6 @@ export const IdentityRoleDetailsSection = ({
|
|||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
variant="danger"
|
variant="danger"
|
||||||
>
|
>
|
||||||
{/* <TrashIcon /> */}
|
|
||||||
Remove Role
|
Remove Role
|
||||||
</UnstableDropdownMenuItem>
|
</UnstableDropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
|
|||||||
+2
-4
@@ -43,10 +43,8 @@ export const ProjectIdentityDetailsSection = ({ identity, isOrgIdentity, members
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<UnstableCard className="w-full max-w-[22rem]">
|
<UnstableCard className="w-full lg:max-w-[24rem]">
|
||||||
<UnstableCardHeader
|
<UnstableCardHeader>
|
||||||
// className="border-b"
|
|
||||||
>
|
|
||||||
<UnstableCardTitle>Details</UnstableCardTitle>
|
<UnstableCardTitle>Details</UnstableCardTitle>
|
||||||
<UnstableCardDescription>Machine identity details</UnstableCardDescription>
|
<UnstableCardDescription>Machine identity details</UnstableCardDescription>
|
||||||
{!isOrgIdentity && (
|
{!isOrgIdentity && (
|
||||||
|
|||||||
@@ -3,25 +3,34 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
|
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { Link, useNavigate, useParams } from "@tanstack/react-router";
|
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 { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import {
|
import {
|
||||||
Button,
|
|
||||||
ConfirmActionModal,
|
ConfirmActionModal,
|
||||||
DeleteActionModal,
|
DeleteActionModal,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
PageHeader,
|
PageHeader,
|
||||||
Spinner
|
Spinner,
|
||||||
|
Tooltip
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
UnstableButton,
|
||||||
|
UnstableDropdownMenu,
|
||||||
|
UnstableDropdownMenuContent,
|
||||||
|
UnstableDropdownMenuItem,
|
||||||
|
UnstableDropdownMenuTrigger
|
||||||
|
} from "@app/components/v3";
|
||||||
import {
|
import {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
ProjectPermissionMemberActions,
|
ProjectPermissionMemberActions,
|
||||||
ProjectPermissionSub,
|
ProjectPermissionSub,
|
||||||
useOrganization,
|
useOrganization,
|
||||||
useProject
|
useProject,
|
||||||
|
useUser
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import { getProjectBaseURL, getProjectHomePage } from "@app/helpers/project";
|
import { getProjectBaseURL, getProjectHomePage } from "@app/helpers/project";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
@@ -35,6 +44,7 @@ import { ProjectAccessControlTabs } from "@app/types/project";
|
|||||||
|
|
||||||
import { MemberProjectAdditionalPrivilegeSection } from "./components/MemberProjectAdditionalPrivilegeSection";
|
import { MemberProjectAdditionalPrivilegeSection } from "./components/MemberProjectAdditionalPrivilegeSection";
|
||||||
import { MemberRoleDetailsSection } from "./components/MemberRoleDetailsSection";
|
import { MemberRoleDetailsSection } from "./components/MemberRoleDetailsSection";
|
||||||
|
import { ProjectMemberDetailsSection } from "./components/ProjectMemberDetailsSection";
|
||||||
|
|
||||||
export const Page = () => {
|
export const Page = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -44,12 +54,14 @@ export const Page = () => {
|
|||||||
});
|
});
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const { currentProject, projectId } = useProject();
|
const { currentProject, projectId } = useProject();
|
||||||
|
const {
|
||||||
|
user: { id: currentUserId }
|
||||||
|
} = useUser();
|
||||||
|
|
||||||
const { data: membershipDetails, isPending: isMembershipDetailsLoading } =
|
const { data: membershipDetails, isPending: isMembershipDetailsLoading } =
|
||||||
useGetWorkspaceUserDetails(projectId, membershipId);
|
useGetWorkspaceUserDetails(projectId, membershipId);
|
||||||
|
|
||||||
const { mutateAsync: removeUserFromWorkspace, isPending: isRemovingUserFromWorkspace } =
|
const { mutateAsync: removeUserFromWorkspace } = useDeleteUserFromWorkspace();
|
||||||
useDeleteUserFromWorkspace();
|
|
||||||
const assumePrivileges = useAssumeProjectPrivileges();
|
const assumePrivileges = useAssumeProjectPrivileges();
|
||||||
|
|
||||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
@@ -112,8 +124,10 @@ export const Page = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isOwnProjectMembershipDetails = currentUserId === membershipDetails?.user?.id;
|
||||||
|
|
||||||
return (
|
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 ? (
|
{membershipDetails ? (
|
||||||
<>
|
<>
|
||||||
<Link
|
<Link
|
||||||
@@ -125,7 +139,7 @@ export const Page = () => {
|
|||||||
search={{
|
search={{
|
||||||
selectedTab: ProjectAccessControlTabs.Member
|
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} />
|
<FontAwesomeIcon icon={faChevronLeft} />
|
||||||
Project Users
|
Project Users
|
||||||
@@ -135,62 +149,104 @@ export const Page = () => {
|
|||||||
title={
|
title={
|
||||||
membershipDetails.user.firstName || membershipDetails.user.lastName
|
membershipDetails.user.firstName || membershipDetails.user.lastName
|
||||||
? `${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
|
{isOwnProjectMembershipDetails ? (
|
||||||
I={ProjectPermissionMemberActions.AssumePrivileges}
|
<Tooltip
|
||||||
a={ProjectPermissionSub.Member}
|
side="right"
|
||||||
renderTooltip
|
content="You cannot modify your own membership. Ask a project admin to make changes to your membership."
|
||||||
allowedLabel="Assume privileges of the user"
|
>
|
||||||
passThrough={false}
|
<Badge variant="info" className="ml-2">
|
||||||
>
|
<InfoIcon /> Your project membership
|
||||||
{(isAllowed) => (
|
</Badge>
|
||||||
<Button
|
</Tooltip>
|
||||||
variant="outline_bg"
|
) : (
|
||||||
size="xs"
|
<UnstableDropdownMenu>
|
||||||
isDisabled={!isAllowed}
|
<UnstableDropdownMenuTrigger asChild>
|
||||||
isLoading={assumePrivileges.isPending}
|
<UnstableButton variant="outline">
|
||||||
onClick={() =>
|
Options
|
||||||
handlePopUpOpen("assumePrivileges", { userId: membershipDetails?.user?.id })
|
<EllipsisIcon />
|
||||||
}
|
</UnstableButton>
|
||||||
>
|
</UnstableDropdownMenuTrigger>
|
||||||
Assume Privileges
|
<UnstableDropdownMenuContent align="end">
|
||||||
</Button>
|
<UnstableDropdownMenuItem
|
||||||
)}
|
onClick={() => {
|
||||||
</ProjectPermissionCan>
|
navigator.clipboard.writeText(membershipDetails.user.id);
|
||||||
|
createNotification({
|
||||||
<ProjectPermissionCan
|
text: "User ID copied to clipboard",
|
||||||
I={ProjectPermissionMemberActions.Delete}
|
type: "info"
|
||||||
a={ProjectPermissionSub.Member}
|
});
|
||||||
renderTooltip
|
}}
|
||||||
allowedLabel="Remove from project"
|
>
|
||||||
>
|
Copy User ID
|
||||||
{(isAllowed) => (
|
</UnstableDropdownMenuItem>
|
||||||
<Button
|
{!isOwnProjectMembershipDetails && (
|
||||||
colorSchema="danger"
|
<>
|
||||||
variant="outline_bg"
|
<ProjectPermissionCan
|
||||||
size="xs"
|
I={ProjectPermissionMemberActions.AssumePrivileges}
|
||||||
isDisabled={!isAllowed}
|
a={ProjectPermissionSub.Member}
|
||||||
isLoading={isRemovingUserFromWorkspace}
|
>
|
||||||
onClick={() => handlePopUpOpen("removeMember")}
|
{(isAllowed) => (
|
||||||
>
|
<UnstableDropdownMenuItem
|
||||||
Remove User
|
isDisabled={!isAllowed}
|
||||||
</Button>
|
onClick={() =>
|
||||||
)}
|
handlePopUpOpen("assumePrivileges", {
|
||||||
</ProjectPermissionCan>
|
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>
|
</PageHeader>
|
||||||
<MemberRoleDetailsSection
|
<div className="flex flex-col gap-5 lg:flex-row">
|
||||||
membershipDetails={membershipDetails}
|
<ProjectMemberDetailsSection membership={membershipDetails} />
|
||||||
isMembershipDetailsLoading={isMembershipDetailsLoading}
|
<div className="flex flex-1 flex-col gap-y-5">
|
||||||
onOpenUpgradeModal={() =>
|
<MemberRoleDetailsSection
|
||||||
handlePopUpOpen("upgradePlan", {
|
membershipDetails={membershipDetails}
|
||||||
text: "Assigning custom roles to members can be unlocked if you upgrade to Infisical Pro plan."
|
isMembershipDetailsLoading={isMembershipDetailsLoading}
|
||||||
})
|
onOpenUpgradeModal={() =>
|
||||||
}
|
handlePopUpOpen("upgradePlan", {
|
||||||
/>
|
text: "Assigning custom roles to members can be unlocked if you upgrade to Infisical Pro plan."
|
||||||
<MemberProjectAdditionalPrivilegeSection membershipDetails={membershipDetails} />
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<MemberProjectAdditionalPrivilegeSection membershipDetails={membershipDetails} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
isOpen={popUp.removeMember.isOpen}
|
isOpen={popUp.removeMember.isOpen}
|
||||||
deleteKey="remove"
|
deleteKey="remove"
|
||||||
|
|||||||
+217
-180
@@ -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 { format, formatDistance } from "date-fns";
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { ClockAlertIcon, ClockIcon, EllipsisIcon, PlusIcon } from "lucide-react";
|
||||||
import { twMerge } from "tailwind-merge";
|
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
|
import { DeleteActionModal, Lottie, Modal, ModalContent, Tooltip } from "@app/components/v2";
|
||||||
import {
|
import {
|
||||||
DeleteActionModal,
|
Badge,
|
||||||
EmptyState,
|
UnstableButton,
|
||||||
IconButton,
|
UnstableCard,
|
||||||
Table,
|
UnstableCardAction,
|
||||||
TableContainer,
|
UnstableCardContent,
|
||||||
TableSkeleton,
|
UnstableCardDescription,
|
||||||
Tag,
|
UnstableCardHeader,
|
||||||
TBody,
|
UnstableCardTitle,
|
||||||
Td,
|
UnstableDropdownMenu,
|
||||||
Th,
|
UnstableDropdownMenuContent,
|
||||||
THead,
|
UnstableDropdownMenuItem,
|
||||||
Tooltip,
|
UnstableDropdownMenuTrigger,
|
||||||
Tr
|
UnstableEmpty,
|
||||||
} from "@app/components/v2";
|
UnstableEmptyContent,
|
||||||
|
UnstableEmptyDescription,
|
||||||
|
UnstableEmptyHeader,
|
||||||
|
UnstableEmptyTitle,
|
||||||
|
UnstableIconButton,
|
||||||
|
UnstableTable,
|
||||||
|
UnstableTableBody,
|
||||||
|
UnstableTableCell,
|
||||||
|
UnstableTableHead,
|
||||||
|
UnstableTableHeader,
|
||||||
|
UnstableTableRow
|
||||||
|
} from "@app/components/v3";
|
||||||
import {
|
import {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
ProjectPermissionMemberActions,
|
ProjectPermissionMemberActions,
|
||||||
@@ -68,188 +77,216 @@ export const MemberProjectAdditionalPrivilegeSection = ({ membershipDetails }: P
|
|||||||
handlePopUpClose("deletePrivilege");
|
handlePopUpClose("deletePrivilege");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const hasAdditionalPrivileges = Boolean(userProjectPrivileges?.length);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<>
|
||||||
<AnimatePresence>
|
<UnstableCard>
|
||||||
{popUp?.modifyPrivilege.isOpen ? (
|
<UnstableCardHeader>
|
||||||
<motion.div
|
<UnstableCardTitle>Project Additional Privileges</UnstableCardTitle>
|
||||||
key="privilege-modify"
|
<UnstableCardDescription>Assign one-off policies to this user</UnstableCardDescription>
|
||||||
transition={{ duration: 0.3 }}
|
{!isOwnProjectMembershipDetails && hasAdditionalPrivileges && (
|
||||||
initial={{ opacity: 0, translateX: 30 }}
|
<UnstableCardAction>
|
||||||
animate={{ opacity: 1, translateX: 0 }}
|
<ProjectPermissionCan
|
||||||
exit={{ opacity: 0, translateX: 30 }}
|
I={ProjectPermissionActions.Edit}
|
||||||
className="absolute min-h-40 w-full"
|
a={ProjectPermissionSub.Member}
|
||||||
>
|
>
|
||||||
<MembershipProjectAdditionalPrivilegeModifySection
|
{(isAllowed) => (
|
||||||
onGoBack={() => handlePopUpClose("modifyPrivilege")}
|
<UnstableButton
|
||||||
projectMembershipId={membershipDetails?.id}
|
variant="outline"
|
||||||
privilegeId={(popUp?.modifyPrivilege?.data as { id: string })?.id}
|
size="xs"
|
||||||
isDisabled={
|
onClick={() => {
|
||||||
isOwnProjectMembershipDetails ||
|
handlePopUpOpen("modifyPrivilege");
|
||||||
permission.cannot(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member)
|
}}
|
||||||
}
|
isDisabled={!isAllowed}
|
||||||
/>
|
|
||||||
</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"
|
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
<PlusIcon />
|
||||||
<IconButton
|
Add Additional Privileges
|
||||||
ariaLabel="copy icon"
|
</UnstableButton>
|
||||||
variant="plain"
|
|
||||||
className="group relative"
|
|
||||||
onClick={() => {
|
|
||||||
handlePopUpOpen("modifyPrivilege");
|
|
||||||
}}
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faPlus} />
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
</ProjectPermissionCan>
|
|
||||||
)}
|
)}
|
||||||
|
</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>
|
||||||
<div className="py-4">
|
) : userProjectPrivileges?.length ? (
|
||||||
<TableContainer>
|
<UnstableTable>
|
||||||
<Table>
|
<UnstableTableHeader>
|
||||||
<THead>
|
<UnstableTableRow>
|
||||||
<Tr>
|
<UnstableTableHead className="w-1/2">Name</UnstableTableHead>
|
||||||
<Th>Name</Th>
|
<UnstableTableHead className="w-1/2">Duration</UnstableTableHead>
|
||||||
<Th>Duration</Th>
|
{!isOwnProjectMembershipDetails && <UnstableTableHead className="w-5" />}
|
||||||
<Th className="w-5" />
|
</UnstableTableRow>
|
||||||
</Tr>
|
</UnstableTableHeader>
|
||||||
</THead>
|
<UnstableTableBody>
|
||||||
<TBody>
|
{!isPending &&
|
||||||
{isPending && <TableSkeleton columns={3} innerKey="user-project-memberships" />}
|
userProjectPrivileges?.map((privilegeDetails) => {
|
||||||
{!isPending &&
|
const isTemporary = privilegeDetails?.isTemporary;
|
||||||
userProjectPrivileges?.map((privilegeDetails) => {
|
const isExpired =
|
||||||
const isTemporary = privilegeDetails?.isTemporary;
|
privilegeDetails.isTemporary &&
|
||||||
const isExpired =
|
new Date() > new Date(privilegeDetails.temporaryAccessEndTime || "");
|
||||||
privilegeDetails.isTemporary &&
|
|
||||||
new Date() > new Date(privilegeDetails.temporaryAccessEndTime || "");
|
|
||||||
|
|
||||||
let text = "Permanent";
|
let text = "Permanent";
|
||||||
let toolTipText = "Non-Expiring Access";
|
let toolTipText = "Non-Expiring Access";
|
||||||
if (privilegeDetails.isTemporary) {
|
if (privilegeDetails.isTemporary) {
|
||||||
if (isExpired) {
|
if (isExpired) {
|
||||||
text = "Access Expired";
|
text = "Access Expired";
|
||||||
toolTipText = "Timed Access Expired";
|
toolTipText = "Timed Access Expired";
|
||||||
} else {
|
} else {
|
||||||
text = formatDistance(
|
text = formatDistance(
|
||||||
new Date(privilegeDetails.temporaryAccessEndTime || ""),
|
new Date(privilegeDetails.temporaryAccessEndTime || ""),
|
||||||
new Date()
|
new Date()
|
||||||
);
|
);
|
||||||
toolTipText = `Until ${format(
|
toolTipText = `Until ${format(
|
||||||
new Date(privilegeDetails.temporaryAccessEndTime || ""),
|
new Date(privilegeDetails.temporaryAccessEndTime || ""),
|
||||||
"yyyy-MM-dd hh:mm:ss aaa"
|
"yyyy-MM-dd hh:mm:ss aaa"
|
||||||
)}`;
|
)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tr
|
<UnstableTableRow key={`user-project-privilege-${privilegeDetails?.id}`}>
|
||||||
key={`user-project-privilege-${privilegeDetails?.id}`}
|
<UnstableTableCell className="max-w-0 truncate">
|
||||||
className="group w-full cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
{privilegeDetails.slug}
|
||||||
role="button"
|
</UnstableTableCell>
|
||||||
tabIndex={0}
|
<UnstableTableCell>
|
||||||
onKeyDown={(evt) => {
|
{isTemporary ? (
|
||||||
if (evt.key === "Enter") {
|
<Tooltip content={toolTipText}>
|
||||||
handlePopUpOpen("modifyPrivilege", privilegeDetails);
|
<Badge
|
||||||
}
|
className="capitalize"
|
||||||
}}
|
variant={isExpired ? "danger" : "warning"}
|
||||||
onClick={() => handlePopUpOpen("modifyPrivilege", privilegeDetails)}
|
>
|
||||||
>
|
{isExpired ? <ClockAlertIcon /> : <ClockIcon />}
|
||||||
<Td>{privilegeDetails.slug}</Td>
|
{text}
|
||||||
<Td>
|
</Badge>
|
||||||
<Tooltip asChild={false} content={toolTipText}>
|
</Tooltip>
|
||||||
<Tag
|
) : (
|
||||||
className={twMerge(
|
text
|
||||||
"capitalize",
|
)}
|
||||||
isTemporary && "text-primary",
|
</UnstableTableCell>
|
||||||
isExpired && "text-red-600"
|
{!isOwnProjectMembershipDetails && (
|
||||||
)}
|
<UnstableTableCell>
|
||||||
>
|
<UnstableDropdownMenu>
|
||||||
{text}
|
<UnstableDropdownMenuTrigger asChild>
|
||||||
</Tag>
|
<UnstableIconButton size="xs" variant="ghost">
|
||||||
</Tooltip>
|
<EllipsisIcon />
|
||||||
</Td>
|
</UnstableIconButton>
|
||||||
<Td>
|
</UnstableDropdownMenuTrigger>
|
||||||
<div className="flex space-x-2 opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
<UnstableDropdownMenuContent align="end">
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Edit}
|
I={ProjectPermissionActions.Edit}
|
||||||
a={ProjectPermissionSub.Member}
|
a={ProjectPermissionSub.Member}
|
||||||
renderTooltip
|
|
||||||
allowedLabel="Remove Role"
|
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<IconButton
|
<UnstableDropdownMenuItem
|
||||||
colorSchema="danger"
|
isDisabled={!isAllowed}
|
||||||
ariaLabel="delete-icon"
|
onClick={(e) => {
|
||||||
variant="plain"
|
e.stopPropagation();
|
||||||
className="group relative"
|
handlePopUpOpen("modifyPrivilege", privilegeDetails);
|
||||||
isDisabled={!isAllowed || isOwnProjectMembershipDetails}
|
}}
|
||||||
|
>
|
||||||
|
Edit Additional Privilege
|
||||||
|
</UnstableDropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Edit}
|
||||||
|
a={ProjectPermissionSub.Member}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<UnstableDropdownMenuItem
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
variant="danger"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
|
||||||
handlePopUpOpen("deletePrivilege", {
|
handlePopUpOpen("deletePrivilege", {
|
||||||
id: privilegeDetails?.id,
|
id: privilegeDetails?.id,
|
||||||
slug: privilegeDetails?.slug
|
slug: privilegeDetails?.slug
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faTrash} />
|
Remove Additional Privilege
|
||||||
</IconButton>
|
</UnstableDropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
<IconButton
|
</UnstableDropdownMenuContent>
|
||||||
ariaLabel="more-icon"
|
</UnstableDropdownMenu>
|
||||||
variant="plain"
|
</UnstableTableCell>
|
||||||
className="group relative"
|
)}
|
||||||
>
|
</UnstableTableRow>
|
||||||
<FontAwesomeIcon icon={faEllipsisV} />
|
);
|
||||||
</IconButton>
|
})}
|
||||||
</div>
|
</UnstableTableBody>
|
||||||
</Td>
|
</UnstableTable>
|
||||||
</Tr>
|
) : (
|
||||||
);
|
<UnstableEmpty className="border">
|
||||||
})}
|
<UnstableEmptyHeader>
|
||||||
</TBody>
|
<UnstableEmptyTitle>This user has no additional privileges</UnstableEmptyTitle>
|
||||||
</Table>
|
<UnstableEmptyDescription>
|
||||||
{!isPending && !userProjectPrivileges?.length && (
|
Add an additional privilege to grant one-off access policies
|
||||||
<EmptyState title="This user has no additional privileges" icon={faFolder} />
|
</UnstableEmptyDescription>
|
||||||
)}
|
</UnstableEmptyHeader>
|
||||||
</TableContainer>
|
{!isOwnProjectMembershipDetails && (
|
||||||
</div>
|
<UnstableEmptyContent>
|
||||||
<DeleteActionModal
|
<ProjectPermissionCan
|
||||||
isOpen={popUp.deletePrivilege.isOpen}
|
I={ProjectPermissionActions.Edit}
|
||||||
deleteKey="remove"
|
a={ProjectPermissionSub.Member}
|
||||||
title={`Do you want to remove role ${
|
>
|
||||||
(popUp?.deletePrivilege?.data as { slug: string; id: string })?.slug
|
{(isAllowed) => (
|
||||||
}?`}
|
<UnstableButton
|
||||||
onChange={(isOpen) => handlePopUpToggle("deletePrivilege", isOpen)}
|
variant="project"
|
||||||
onDeleteApproved={() => handlePrivilegeDelete()}
|
size="xs"
|
||||||
/>
|
onClick={() => {
|
||||||
</motion.div>
|
handlePopUpOpen("modifyPrivilege");
|
||||||
)}
|
}}
|
||||||
</AnimatePresence>
|
isDisabled={!isAllowed || isOwnProjectMembershipDetails}
|
||||||
</div>
|
>
|
||||||
|
<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()}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+59
-68
@@ -1,5 +1,5 @@
|
|||||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { format, formatDistance } from "date-fns";
|
import { format, formatDistance } from "date-fns";
|
||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
Tag,
|
Tag,
|
||||||
Tooltip
|
Tooltip
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
|
import { UnstableSeparator } from "@app/components/v3";
|
||||||
import {
|
import {
|
||||||
ProjectPermissionMemberActions,
|
ProjectPermissionMemberActions,
|
||||||
ProjectPermissionSub,
|
ProjectPermissionSub,
|
||||||
@@ -111,6 +112,7 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
reset,
|
||||||
formState: { isDirty, isSubmitting }
|
formState: { isDirty, isSubmitting }
|
||||||
} = form;
|
} = form;
|
||||||
|
|
||||||
@@ -176,59 +178,9 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form className="flex flex-col gap-y-4" onSubmit={handleSubmit(onSubmit)}>
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
|
||||||
className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
|
|
||||||
>
|
|
||||||
<FormProvider {...form}>
|
<FormProvider {...form}>
|
||||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
|
<div>
|
||||||
<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 className="flex items-end space-x-6">
|
<div className="flex items-end space-x-6">
|
||||||
<div className="w-full max-w-md">
|
<div className="w-full max-w-md">
|
||||||
<Controller
|
<Controller
|
||||||
@@ -344,22 +296,61 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4">
|
<UnstableSeparator />
|
||||||
<div className="mb-2 text-lg">Policies</div>
|
<div>
|
||||||
{(isCreate || !isPending) && <PermissionEmptyState />}
|
<div className="mb-3 flex w-full items-center justify-between">
|
||||||
<div>
|
<div className="text-lg">Policies</div>
|
||||||
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => (
|
<div className="flex items-center space-x-4">
|
||||||
<GeneralPermissionPolicies
|
{isDirty && (
|
||||||
subject={subject}
|
<Button
|
||||||
actions={PROJECT_PERMISSION_OBJECT[subject].actions}
|
className="mr-4 text-mineshaft-300"
|
||||||
title={PROJECT_PERMISSION_OBJECT[subject].title}
|
variant="link"
|
||||||
key={`project-permission-${subject}`}
|
isDisabled={isSubmitting}
|
||||||
isDisabled={isDisabled}
|
isLoading={isSubmitting}
|
||||||
>
|
onClick={() => reset()}
|
||||||
{renderConditionalComponents(subject, isDisabled)}
|
>
|
||||||
</GeneralPermissionPolicies>
|
Discard Changes
|
||||||
))}
|
</Button>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center">
|
||||||
|
<AddPoliciesButton
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
projectType={currentProject.type}
|
||||||
|
projectId={projectId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</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>
|
</div>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
+195
-142
@@ -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 { 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 { createNotification } from "@app/components/notifications";
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
|
import { DeleteActionModal, Lottie, Modal, ModalContent, Tooltip } from "@app/components/v2";
|
||||||
import {
|
import {
|
||||||
DeleteActionModal,
|
Badge,
|
||||||
EmptyState,
|
UnstableButton,
|
||||||
IconButton,
|
UnstableCard,
|
||||||
Modal,
|
UnstableCardAction,
|
||||||
ModalContent,
|
UnstableCardContent,
|
||||||
Table,
|
UnstableCardDescription,
|
||||||
TableContainer,
|
UnstableCardHeader,
|
||||||
TableSkeleton,
|
UnstableCardTitle,
|
||||||
Tag,
|
UnstableDropdownMenu,
|
||||||
TBody,
|
UnstableDropdownMenuContent,
|
||||||
Td,
|
UnstableDropdownMenuItem,
|
||||||
Th,
|
UnstableDropdownMenuTrigger,
|
||||||
THead,
|
UnstableEmpty,
|
||||||
Tooltip,
|
UnstableEmptyContent,
|
||||||
Tr
|
UnstableEmptyDescription,
|
||||||
} from "@app/components/v2";
|
UnstableEmptyHeader,
|
||||||
|
UnstableEmptyTitle,
|
||||||
|
UnstableIconButton,
|
||||||
|
UnstableTable,
|
||||||
|
UnstableTableBody,
|
||||||
|
UnstableTableCell,
|
||||||
|
UnstableTableHead,
|
||||||
|
UnstableTableHeader,
|
||||||
|
UnstableTableRow
|
||||||
|
} from "@app/components/v3/generic";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub, useProject, useUser } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub, useProject, useUser } from "@app/context";
|
||||||
import { formatProjectRoleName } from "@app/helpers/roles";
|
import { formatProjectRoleName } from "@app/helpers/roles";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
@@ -88,131 +96,176 @@ export const MemberRoleDetailsSection = ({
|
|||||||
handlePopUpClose("deleteRole");
|
handlePopUpClose("deleteRole");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const hasRoles = Boolean(membershipDetails?.roles.length);
|
||||||
|
|
||||||
return (
|
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">
|
<UnstableCard>
|
||||||
<h3 className="text-lg font-medium text-mineshaft-100">Project Roles</h3>
|
<UnstableCardHeader>
|
||||||
{!isOwnProjectMembershipDetails && membershipDetails?.status !== "invited" && (
|
<UnstableCardTitle>Project Roles</UnstableCardTitle>
|
||||||
<ProjectPermissionCan
|
<UnstableCardDescription>Manage roles assigned to this user</UnstableCardDescription>
|
||||||
I={ProjectPermissionActions.Edit}
|
{!isOwnProjectMembershipDetails && hasRoles && (
|
||||||
a={ProjectPermissionSub.Member}
|
<UnstableCardAction>
|
||||||
renderTooltip
|
<ProjectPermissionCan
|
||||||
allowedLabel="Edit Role(s)"
|
I={ProjectPermissionActions.Edit}
|
||||||
>
|
a={ProjectPermissionSub.Member}
|
||||||
{(isAllowed) => (
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="copy icon"
|
|
||||||
variant="plain"
|
|
||||||
className="group relative"
|
|
||||||
onClick={() => {
|
|
||||||
handlePopUpOpen("modifyRole");
|
|
||||||
}}
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faPencil} />
|
{(isAllowed) => (
|
||||||
</IconButton>
|
<UnstableButton
|
||||||
)}
|
size="xs"
|
||||||
</ProjectPermissionCan>
|
variant="outline"
|
||||||
)}
|
onClick={() => {
|
||||||
</div>
|
handlePopUpOpen("modifyRole");
|
||||||
<div className="py-4">
|
}}
|
||||||
<TableContainer>
|
isDisabled={!isAllowed}
|
||||||
<Table>
|
>
|
||||||
<THead>
|
<PencilIcon />
|
||||||
<Tr>
|
Edit Roles
|
||||||
<Th>Role</Th>
|
</UnstableButton>
|
||||||
<Th>Duration</Th>
|
)}
|
||||||
<Th className="w-5" />
|
</ProjectPermissionCan>
|
||||||
</Tr>
|
</UnstableCardAction>
|
||||||
</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} />
|
|
||||||
)}
|
)}
|
||||||
</TableContainer>
|
</UnstableCardHeader>
|
||||||
</div>
|
<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
|
<DeleteActionModal
|
||||||
isOpen={popUp.deleteRole.isOpen}
|
isOpen={popUp.deleteRole.isOpen}
|
||||||
deleteKey="remove"
|
deleteKey="remove"
|
||||||
@@ -234,6 +287,6 @@ export const MemberRoleDetailsSection = ({
|
|||||||
/>
|
/>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+107
@@ -0,0 +1,107 @@
|
|||||||
|
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 italic">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 */}
|
||||||
|
{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 */}
|
||||||
|
{isCopyingEmail ? <CheckIcon /> : <ClipboardListIcon className="text-label" />}
|
||||||
|
</UnstableIconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</DetailValue>
|
||||||
|
</Detail>
|
||||||
|
{username !== email && (
|
||||||
|
<Detail>
|
||||||
|
<DetailLabel>Username</DetailLabel>
|
||||||
|
<DetailValue>
|
||||||
|
{email || <span className="text-muted italic">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