mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 13:28:27 +00:00
feat: added role modify popup
This commit is contained in:
@@ -4,5 +4,17 @@ enum OrgMembershipRole {
|
|||||||
NoAccess = "no-access"
|
NoAccess = "no-access"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ProjectMemberRole {
|
||||||
|
Admin = "admin",
|
||||||
|
Member = "member",
|
||||||
|
Viewer = "viewer",
|
||||||
|
NoAccess = "no-access"
|
||||||
|
}
|
||||||
|
|
||||||
export const isCustomOrgRole = (slug: string) =>
|
export const isCustomOrgRole = (slug: string) =>
|
||||||
!Object.values(OrgMembershipRole).includes(slug as OrgMembershipRole);
|
!Object.values(OrgMembershipRole).includes(slug as OrgMembershipRole);
|
||||||
|
|
||||||
|
export const formatProjectRoleName = (name: string) => {
|
||||||
|
if (name === ProjectMemberRole.Member) return "developer";
|
||||||
|
return name;
|
||||||
|
};
|
||||||
|
|||||||
@@ -420,8 +420,11 @@ export const useUpdateUserWorkspaceRole = () => {
|
|||||||
);
|
);
|
||||||
return membership;
|
return membership;
|
||||||
},
|
},
|
||||||
onSuccess: (_, { workspaceId }) => {
|
onSuccess: (_, { workspaceId, membershipId }) => {
|
||||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceUsers(workspaceId));
|
queryClient.invalidateQueries(workspaceKeys.getWorkspaceUsers(workspaceId));
|
||||||
|
queryClient.invalidateQueries(
|
||||||
|
workspaceKeys.getWorkspaceUserDetails(workspaceId, membershipId)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export const MemberDetailsPage = withProjectPermission(
|
|||||||
<h3 className="text-lg font-semibold text-mineshaft-100">Project User Management</h3>
|
<h3 className="text-lg font-semibold text-mineshaft-100">Project User Management</h3>
|
||||||
<div>
|
<div>
|
||||||
<Button colorSchema="danger" variant="outline_bg" size="xs">
|
<Button colorSchema="danger" variant="outline_bg" size="xs">
|
||||||
Remove
|
Remove User
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+109
-38
@@ -1,4 +1,5 @@
|
|||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import {
|
import {
|
||||||
DeleteActionModal,
|
DeleteActionModal,
|
||||||
IconButton,
|
IconButton,
|
||||||
@@ -11,20 +12,27 @@ import {
|
|||||||
TableSkeleton,
|
TableSkeleton,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
TBody,
|
TBody,
|
||||||
Tooltip
|
Tooltip,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
Tag
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useUser, useWorkspace } from "@app/context";
|
|
||||||
import { usePopUp } from "@app/hooks";
|
|
||||||
import {
|
import {
|
||||||
useDeleteProjectRole,
|
ProjectPermissionActions,
|
||||||
useGetWorkspaceUserDetails,
|
ProjectPermissionSub,
|
||||||
useUpdateUserWorkspaceRole
|
useUser,
|
||||||
} from "@app/hooks/api";
|
useWorkspace
|
||||||
|
} from "@app/context";
|
||||||
|
import { formatProjectRoleName } from "@app/helpers/roles";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
|
import { useUpdateUserWorkspaceRole } from "@app/hooks/api";
|
||||||
import { TProjectRole } from "@app/hooks/api/roles/types";
|
import { TProjectRole } from "@app/hooks/api/roles/types";
|
||||||
import { TWorkspaceUser } from "@app/hooks/api/types";
|
import { TWorkspaceUser } from "@app/hooks/api/types";
|
||||||
import { faFolder, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
import { faFolder, faPencil, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { useRouter } from "next/router";
|
import { format, formatDistance } from "date-fns";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import { MemberRoleModify } from "./MemberRoleModify";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
membershipDetails: TWorkspaceUser;
|
membershipDetails: TWorkspaceUser;
|
||||||
@@ -35,11 +43,11 @@ export const MemberRoleDetailsSection = ({
|
|||||||
membershipDetails,
|
membershipDetails,
|
||||||
isMembershipDetailsLoading
|
isMembershipDetailsLoading
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const router = useRouter();
|
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
|
||||||
"deleteRole"
|
"deleteRole",
|
||||||
|
"modifyRole"
|
||||||
] as const);
|
] as const);
|
||||||
const { mutateAsync: updateUserWorkspaceRole } = useUpdateUserWorkspaceRole();
|
const { mutateAsync: updateUserWorkspaceRole } = useUpdateUserWorkspaceRole();
|
||||||
|
|
||||||
@@ -67,14 +75,26 @@ export const MemberRoleDetailsSection = ({
|
|||||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||||
<h3 className="text-lg font-semibold text-mineshaft-100">Project Roles</h3>
|
<h3 className="text-lg font-semibold text-mineshaft-100">Project Roles</h3>
|
||||||
{userId !== membershipDetails?.user?.id && membershipDetails?.status !== "invited" && (
|
{userId !== membershipDetails?.user?.id && membershipDetails?.status !== "invited" && (
|
||||||
<IconButton
|
<ProjectPermissionCan
|
||||||
ariaLabel="copy icon"
|
I={ProjectPermissionActions.Edit}
|
||||||
variant="plain"
|
a={ProjectPermissionSub.Member}
|
||||||
className="group relative"
|
renderTooltip
|
||||||
onClick={() => {}}
|
allowedLabel="Edit role"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faPlus} />
|
{(isAllowed) => (
|
||||||
</IconButton>
|
<IconButton
|
||||||
|
ariaLabel="copy icon"
|
||||||
|
variant="plain"
|
||||||
|
className="group relative"
|
||||||
|
onClick={() => {
|
||||||
|
handlePopUpOpen("modifyRole");
|
||||||
|
}}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faPencil} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="py-4">
|
<div className="py-4">
|
||||||
@@ -93,33 +113,76 @@ export const MemberRoleDetailsSection = ({
|
|||||||
)}
|
)}
|
||||||
{!isMembershipDetailsLoading &&
|
{!isMembershipDetailsLoading &&
|
||||||
membershipDetails?.roles?.map((roleDetails) => {
|
membershipDetails?.roles?.map((roleDetails) => {
|
||||||
|
const isTemporary = roleDetails?.isTemporary;
|
||||||
|
const isExpired =
|
||||||
|
roleDetails.isTemporary &&
|
||||||
|
new Date() > new Date(roleDetails.temporaryAccessEndTime || "");
|
||||||
|
|
||||||
|
let text = "Permanent";
|
||||||
|
let toolTipText = "Non expiry 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 (
|
return (
|
||||||
<Tr className="group h-10" key={`user-project-membership-${roleDetails?.id}`}>
|
<Tr className="group h-10" key={`user-project-membership-${roleDetails?.id}`}>
|
||||||
<Td>
|
<Td className="capitalize">
|
||||||
{roleDetails.role === "custom"
|
{roleDetails.role === "custom"
|
||||||
? roleDetails.customRoleName
|
? roleDetails.customRoleName
|
||||||
: roleDetails.role}
|
: 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>
|
||||||
<Td>{!roleDetails.isTemporary ? "permanent" : "temporary"}</Td>
|
|
||||||
<Td>
|
<Td>
|
||||||
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
||||||
<Tooltip content="Remove">
|
<ProjectPermissionCan
|
||||||
<IconButton
|
I={ProjectPermissionActions.Edit}
|
||||||
colorSchema="danger"
|
a={ProjectPermissionSub.Member}
|
||||||
ariaLabel="copy icon"
|
renderTooltip
|
||||||
variant="plain"
|
allowedLabel="Remove role"
|
||||||
className="group relative"
|
>
|
||||||
onClick={(e) => {
|
{(isAllowed) => (
|
||||||
e.stopPropagation();
|
<IconButton
|
||||||
handlePopUpOpen("deleteRole", {
|
colorSchema="danger"
|
||||||
id: roleDetails?.id,
|
ariaLabel="copy icon"
|
||||||
slug: roleDetails?.customRoleSlug || roleDetails?.role
|
variant="plain"
|
||||||
});
|
className="group relative"
|
||||||
}}
|
isDisabled={!isAllowed}
|
||||||
>
|
onClick={(e) => {
|
||||||
<FontAwesomeIcon icon={faTrash} />
|
e.stopPropagation();
|
||||||
</IconButton>
|
handlePopUpOpen("deleteRole", {
|
||||||
</Tooltip>
|
id: roleDetails?.id,
|
||||||
|
slug: roleDetails?.customRoleName || roleDetails?.role
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
@@ -139,6 +202,14 @@ export const MemberRoleDetailsSection = ({
|
|||||||
onChange={(isOpen) => handlePopUpToggle("deleteRole", isOpen)}
|
onChange={(isOpen) => handlePopUpToggle("deleteRole", isOpen)}
|
||||||
onDeleteApproved={() => handleRoleDelete()}
|
onDeleteApproved={() => handleRoleDelete()}
|
||||||
/>
|
/>
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp.modifyRole.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("modifyRole", isOpen)}
|
||||||
|
>
|
||||||
|
<ModalContent title="Roles" subTitle="Select one of the pre-defined or custom roles.">
|
||||||
|
<MemberRoleModify projectMember={membershipDetails} onOpenUpgradeModal={() => {}} />
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+346
@@ -0,0 +1,346 @@
|
|||||||
|
/* eslint-disable no-nested-ternary */
|
||||||
|
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||||
|
import { faCaretDown, faClock, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { format, formatDistance } from "date-fns";
|
||||||
|
import ms from "ms";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TtlFormLabel } from "@app/components/features";
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
Spinner,
|
||||||
|
Tag,
|
||||||
|
Tooltip
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import {
|
||||||
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionSub,
|
||||||
|
useProjectPermission,
|
||||||
|
useSubscription,
|
||||||
|
useWorkspace
|
||||||
|
} from "@app/context";
|
||||||
|
import { useGetProjectRoles, useUpdateUserWorkspaceRole } from "@app/hooks/api";
|
||||||
|
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
||||||
|
import { TWorkspaceUser } from "@app/hooks/api/types";
|
||||||
|
import { ProjectUserMembershipTemporaryMode } from "@app/hooks/api/workspace/types";
|
||||||
|
|
||||||
|
const roleFormSchema = z.object({
|
||||||
|
roles: z
|
||||||
|
.object({
|
||||||
|
slug: z.string(),
|
||||||
|
temporaryAccess: z.discriminatedUnion("isTemporary", [
|
||||||
|
z.object({
|
||||||
|
isTemporary: z.literal(true),
|
||||||
|
temporaryRange: z.string().min(1),
|
||||||
|
temporaryAccessStartTime: z.string().datetime(),
|
||||||
|
temporaryAccessEndTime: z.string().datetime().nullable().optional()
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
isTemporary: z.literal(false)
|
||||||
|
})
|
||||||
|
])
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
});
|
||||||
|
type TRoleForm = z.infer<typeof roleFormSchema>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
projectMember: TWorkspaceUser;
|
||||||
|
onOpenUpgradeModal: (title: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MemberRoleModify = ({ projectMember, onOpenUpgradeModal }: Props) => {
|
||||||
|
const { subscription } = useSubscription();
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const workspaceId = currentWorkspace?.id || "";
|
||||||
|
const { data: projectRoles, isLoading: isRolesLoading } = useGetProjectRoles(workspaceId);
|
||||||
|
const { permission } = useProjectPermission();
|
||||||
|
const isMemberEditDisabled = permission.cannot(
|
||||||
|
ProjectPermissionActions.Edit,
|
||||||
|
ProjectPermissionSub.Member
|
||||||
|
);
|
||||||
|
|
||||||
|
const roleForm = useForm<TRoleForm>({
|
||||||
|
resolver: zodResolver(roleFormSchema),
|
||||||
|
values: {
|
||||||
|
roles: projectMember?.roles?.map(({ customRoleSlug, role, ...dto }) => ({
|
||||||
|
slug: customRoleSlug || role,
|
||||||
|
temporaryAccess: dto.isTemporary
|
||||||
|
? {
|
||||||
|
isTemporary: true,
|
||||||
|
temporaryRange: dto.temporaryRange,
|
||||||
|
temporaryAccessEndTime: dto.temporaryAccessEndTime,
|
||||||
|
temporaryAccessStartTime: dto.temporaryAccessStartTime
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
isTemporary: dto.isTemporary
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const selectedRoleList = useFieldArray({
|
||||||
|
name: "roles",
|
||||||
|
control: roleForm.control
|
||||||
|
});
|
||||||
|
|
||||||
|
const formRoleField = roleForm.watch("roles");
|
||||||
|
|
||||||
|
const updateMembershipRole = useUpdateUserWorkspaceRole();
|
||||||
|
|
||||||
|
const handleRoleUpdate = async (data: TRoleForm) => {
|
||||||
|
if (updateMembershipRole.isLoading) return;
|
||||||
|
|
||||||
|
const sanitizedRoles = data.roles.map((el) => {
|
||||||
|
const { isTemporary } = el.temporaryAccess;
|
||||||
|
if (!isTemporary) {
|
||||||
|
return { role: el.slug, isTemporary: false as const };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
role: el.slug,
|
||||||
|
isTemporary: true as const,
|
||||||
|
temporaryMode: ProjectUserMembershipTemporaryMode.Relative,
|
||||||
|
temporaryRange: el.temporaryAccess.temporaryRange,
|
||||||
|
temporaryAccessStartTime: el.temporaryAccess.temporaryAccessStartTime
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasCustomRoleSelected = sanitizedRoles.some(
|
||||||
|
(el) => !Object.values(ProjectMembershipRole).includes(el.role as ProjectMembershipRole)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasCustomRoleSelected && subscription && !subscription?.rbac) {
|
||||||
|
onOpenUpgradeModal(
|
||||||
|
"You can assign custom roles to members if you upgrade your Infisical plan."
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateMembershipRole.mutateAsync({
|
||||||
|
workspaceId,
|
||||||
|
membershipId: projectMember.id,
|
||||||
|
roles: sanitizedRoles
|
||||||
|
});
|
||||||
|
createNotification({ text: "Successfully updated roles", type: "success" });
|
||||||
|
} catch (err) {
|
||||||
|
createNotification({ text: "Failed to update role", type: "error" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isRolesLoading)
|
||||||
|
return (
|
||||||
|
<div className="flex w-full items-center justify-center p-8">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={roleForm.handleSubmit(handleRoleUpdate)}>
|
||||||
|
<div className="mt-2 flex flex-col space-y-2">
|
||||||
|
{selectedRoleList.fields.map(({ id }, index) => {
|
||||||
|
const { temporaryAccess } = formRoleField[index];
|
||||||
|
const isTemporary = temporaryAccess?.isTemporary;
|
||||||
|
const isExpired =
|
||||||
|
temporaryAccess.isTemporary &&
|
||||||
|
new Date() > new Date(temporaryAccess.temporaryAccessEndTime || "");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={id} className="flex items-center space-x-2">
|
||||||
|
<Controller
|
||||||
|
control={roleForm.control}
|
||||||
|
name={`roles.${index}.slug`}
|
||||||
|
render={({ field: { onChange, ...field } }) => (
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
isDisabled={isMemberEditDisabled}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full bg-mineshaft-600 duration-200 hover:bg-mineshaft-500"
|
||||||
|
containerClassName="w-1/2"
|
||||||
|
>
|
||||||
|
{projectRoles?.map(({ name, slug, id: projectRoleId }) => (
|
||||||
|
<SelectItem value={slug} key={projectRoleId}>
|
||||||
|
{name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger disabled={isMemberEditDisabled} asChild>
|
||||||
|
<div className="flex-grow">
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
temporaryAccess?.isTemporary
|
||||||
|
? isExpired
|
||||||
|
? "Timed Access Expired"
|
||||||
|
: `Until ${format(
|
||||||
|
new Date(temporaryAccess.temporaryAccessEndTime || ""),
|
||||||
|
"yyyy-MM-dd HH:mm:ss"
|
||||||
|
)}`
|
||||||
|
: "Non expiry access"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="outline_bg"
|
||||||
|
leftIcon={isTemporary ? <FontAwesomeIcon icon={faClock} /> : undefined}
|
||||||
|
rightIcon={<FontAwesomeIcon icon={faCaretDown} className="ml-2" />}
|
||||||
|
isDisabled={isMemberEditDisabled}
|
||||||
|
className={twMerge(
|
||||||
|
"w-full border-none bg-mineshaft-600 py-2.5 text-xs capitalize hover:bg-mineshaft-500",
|
||||||
|
isTemporary && "text-primary",
|
||||||
|
isExpired && "text-red-600"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{temporaryAccess?.isTemporary
|
||||||
|
? isExpired
|
||||||
|
? "Access Expired"
|
||||||
|
: formatDistance(
|
||||||
|
new Date(temporaryAccess.temporaryAccessEndTime || ""),
|
||||||
|
new Date()
|
||||||
|
)
|
||||||
|
: "Permanent"}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
arrowClassName="fill-gray-600"
|
||||||
|
side="right"
|
||||||
|
sideOffset={12}
|
||||||
|
hideCloseBtn
|
||||||
|
className="border border-gray-600 pt-4"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col space-y-4">
|
||||||
|
<div className="border-b border-b-gray-700 pb-2 text-sm text-mineshaft-300">
|
||||||
|
Configure timed access
|
||||||
|
</div>
|
||||||
|
{isExpired && <Tag colorSchema="red">Expired</Tag>}
|
||||||
|
<Controller
|
||||||
|
control={roleForm.control}
|
||||||
|
defaultValue="1h"
|
||||||
|
name={`roles.${index}.temporaryAccess.temporaryRange`}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label={<TtlFormLabel label="Validity" />}
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
onClick={() => {
|
||||||
|
const temporaryRange = roleForm.getValues(
|
||||||
|
`roles.${index}.temporaryAccess.temporaryRange`
|
||||||
|
);
|
||||||
|
if (!temporaryRange) {
|
||||||
|
roleForm.setError(
|
||||||
|
`roles.${index}.temporaryAccess.temporaryRange`,
|
||||||
|
{ type: "required", message: "Required" },
|
||||||
|
{ shouldFocus: true }
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
roleForm.clearErrors(`roles.${index}.temporaryAccess.temporaryRange`);
|
||||||
|
roleForm.setValue(
|
||||||
|
`roles.${index}.temporaryAccess`,
|
||||||
|
{
|
||||||
|
isTemporary: true,
|
||||||
|
temporaryAccessStartTime: new Date().toISOString(),
|
||||||
|
temporaryRange,
|
||||||
|
temporaryAccessEndTime: new Date(
|
||||||
|
new Date().getTime() + ms(temporaryRange)
|
||||||
|
).toISOString()
|
||||||
|
},
|
||||||
|
{ shouldDirty: true }
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{temporaryAccess.isTemporary ? "Restart" : "Grant"}
|
||||||
|
</Button>
|
||||||
|
{temporaryAccess.isTemporary && (
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="outline_bg"
|
||||||
|
colorSchema="danger"
|
||||||
|
onClick={() => {
|
||||||
|
roleForm.setValue(`roles.${index}.temporaryAccess`, {
|
||||||
|
isTemporary: false
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Revoke Access
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<IconButton
|
||||||
|
variant="outline_bg"
|
||||||
|
className="border border-mineshaft-500 bg-mineshaft-600 py-3 hover:border-red/70 hover:bg-red/20"
|
||||||
|
ariaLabel="delete-role"
|
||||||
|
isDisabled={isMemberEditDisabled}
|
||||||
|
onClick={() => {
|
||||||
|
if (selectedRoleList.fields.length > 1) {
|
||||||
|
selectedRoleList.remove(index);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex justify-between space-x-2">
|
||||||
|
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Member}>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button
|
||||||
|
variant="outline_bg"
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
onClick={() =>
|
||||||
|
selectedRoleList.append({
|
||||||
|
slug: ProjectMembershipRole.Member,
|
||||||
|
temporaryAccess: { isTemporary: false }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Add Role
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className={twMerge(
|
||||||
|
"transition-all",
|
||||||
|
"cursor-default opacity-0",
|
||||||
|
roleForm.formState.isDirty && "cursor-pointer opacity-100"
|
||||||
|
)}
|
||||||
|
isDisabled={!roleForm.formState.isDirty}
|
||||||
|
isLoading={roleForm.formState.isSubmitting}
|
||||||
|
>
|
||||||
|
Save Roles
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user