feat(ui): completed ui for user additional privilege

This commit is contained in:
Akhil Mohan
2024-03-15 15:56:16 +05:30
parent edbe1c8eae
commit 81cf19cb4a
9 changed files with 794 additions and 61 deletions

View File

@@ -20,6 +20,7 @@ const tagVariants = cva(
green: "bg-primary-800 text-white"
},
size: {
xs: "text-xs px-1 py-0.5",
sm: "px-2 py-0.5"
}
}

View File

@@ -32,15 +32,7 @@ export const MembersPage = withProjectPermission(
<Tab value={TabSections.Roles}>Project Roles</Tab>
</TabList>
<TabPanel value={TabSections.Member}>
<motion.div
key="panel-1"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<MemberListTab />
</motion.div>
<MemberListTab />
</TabPanel>
<TabPanel value={TabSections.Identities}>
<IdentityTab />

View File

@@ -0,0 +1,283 @@
import { useForm } from "react-hook-form";
import { faElementor } from "@fortawesome/free-brands-svg-icons";
import {
faAnchorLock,
faArrowLeft,
faBook,
faCog,
faKey,
faLock,
faNetworkWired,
faPuzzlePiece,
faServer,
faShield,
faTags,
faUser,
faUsers
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { Button, FormControl, Input } from "@app/components/v2";
import { ProjectPermissionSub } from "@app/context";
import {
useCreateProjectUserAdditionalPrivilege,
useGetProjectUserPrivilegeDetails,
useUpdateProjectUserAdditionalPrivilege
} from "@app/hooks/api";
import { MultiEnvProjectPermission } from "../ProjectRoleListTab/components/ProjectRoleModifySection/MultiEnvProjectPermission";
import {
formRolePermission2API,
formSchema,
rolePermission2Form,
// rolePermission2Form,
TFormSchema
} from "../ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils";
import { SecretRollbackPermission } from "../ProjectRoleListTab/components/ProjectRoleModifySection/SecretRollbackPermission";
import { SingleProjectPermission } from "../ProjectRoleListTab/components/ProjectRoleModifySection/SingleProjectPermission";
import { WsProjectPermission } from "../ProjectRoleListTab/components/ProjectRoleModifySection/WsProjectPermission";
const SINGLE_PERMISSION_LIST = [
{
title: "Integrations",
subtitle: "Integration management control",
icon: faPuzzlePiece,
formName: "integrations"
},
{
title: "Secret Protect policy",
subtitle: "Manage policies for secret protection for unauthorized secret changes",
icon: faShield,
formName: ProjectPermissionSub.SecretApproval
},
{
title: "Roles",
subtitle: "Role management control",
icon: faUsers,
formName: "role"
},
{
title: "Project Members",
subtitle: "Project members management control",
icon: faUser,
formName: "member"
},
{
title: "Machine identity management",
subtitle: "Add, view, update and remove (machine) identities from the project",
icon: faServer,
formName: "identity"
},
{
title: "Webhooks",
subtitle: "Webhook management control",
icon: faAnchorLock,
formName: "webhooks"
},
{
title: "Service Tokens",
subtitle: "Token management control",
icon: faKey,
formName: "service-tokens"
},
{
title: "Settings",
subtitle: "Settings control",
icon: faCog,
formName: "settings"
},
{
title: "Environments",
subtitle: "Environment management control",
icon: faElementor,
formName: "environments"
},
{
title: "Tags",
subtitle: "Tag management control",
icon: faTags,
formName: "tags"
},
{
title: "Audit Logs",
subtitle: "Audit log management control",
icon: faBook,
formName: "audit-logs"
},
{
title: "IP Allowlist",
subtitle: "IP allowlist management control",
icon: faNetworkWired,
formName: "ip-allowlist"
}
] as const;
type Props = {
onGoBack: VoidFunction;
isIdentity?: boolean;
privilegeId?: string;
workspaceId: string;
// isIdentity true -> actorId is identity Id
// isIdentity false -> actorId is projectMembershipId
actorId: string;
};
export const AdditionalPrivilegeForm = ({ onGoBack, privilegeId, actorId, workspaceId }: Props) => {
const { createNotification } = useNotificationContext();
const isNewRole = !privilegeId;
const { data: projectUserPrivilegeDetails } = useGetProjectUserPrivilegeDetails(
privilegeId || ""
);
const {
handleSubmit,
register,
formState: { isSubmitting, isDirty, errors },
setValue,
getValues,
control
} = useForm<TFormSchema>({
resolver: zodResolver(formSchema),
values: projectUserPrivilegeDetails && {
...projectUserPrivilegeDetails,
description: projectUserPrivilegeDetails.description || "",
permissions: rolePermission2Form(projectUserPrivilegeDetails.permissions)
}
});
const createProjectUserAdditionalPrivilege = useCreateProjectUserAdditionalPrivilege();
const updateProjectUserAdditionalPrivilege = useUpdateProjectUserAdditionalPrivilege();
const handleRoleUpdate = async (el: TFormSchema) => {
try {
await updateProjectUserAdditionalPrivilege.mutateAsync({
...el,
permissions: formRolePermission2API(el.permissions),
privilegeId: privilegeId as string,
workspaceId
});
createNotification({ type: "success", text: "Successfully update privilege" });
onGoBack();
} catch (err) {
console.log(err);
createNotification({ type: "error", text: "Failed to update privilege" });
}
};
const handleFormSubmit = async (el: TFormSchema) => {
if (!isNewRole) {
await handleRoleUpdate(el);
return;
}
try {
await createProjectUserAdditionalPrivilege.mutateAsync({
...el,
permissions: formRolePermission2API(el.permissions),
projectMembershipId: actorId,
workspaceId
});
createNotification({ type: "success", text: "Created new privilege" });
onGoBack();
} catch (err) {
console.log(err);
createNotification({ type: "error", text: "Failed to create privilege" });
}
};
return (
<div>
<form onSubmit={handleSubmit(handleFormSubmit)}>
<div className="mb-2 flex items-center justify-between">
<h1 className="text-xl font-semibold text-mineshaft-100">
{isNewRole ? "New" : "Edit"} user additional privilege
</h1>
<Button
onClick={onGoBack}
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faArrowLeft} />}
>
Go back
</Button>
</div>
<p className="mb-8 text-gray-400">
Select multiple privilege that can be granted to the user
</p>
<div className="flex flex-col space-y-6">
<FormControl
label="Name"
helperText="Use descriptive names to clearly identify permissions"
isRequired
className="mb-0"
isError={Boolean(errors?.name)}
errorText={errors?.name?.message}
>
<Input {...register("name")} />
</FormControl>
<FormControl
label="Slug"
helperText="Slugs are used for API access"
isRequired
isError={Boolean(errors?.slug)}
errorText={errors?.slug?.message}
>
<Input {...register("slug")} placeholder="biller" />
</FormControl>
<FormControl
label="Description"
helperText="A short description about this privilege"
isError={Boolean(errors?.description)}
errorText={errors?.description?.message}
>
<Input {...register("description")} />
</FormControl>
<div className="flex items-center justify-between border-t border-t-mineshaft-800 pt-6">
<div>
<h2 className="text-xl font-medium">Add Privilege</h2>
</div>
</div>
<div>
<MultiEnvProjectPermission
getValue={getValues}
control={control}
setValue={setValue}
icon={faLock}
title="Secrets"
subtitle="Create, modify and remove secrets, folders and secret imports"
formName="secrets"
/>
</div>
<div key="permission-ws">
<WsProjectPermission control={control} setValue={setValue} />
</div>
{SINGLE_PERMISSION_LIST.map(({ title, subtitle, icon, formName }) => (
<div key={`permission-${title}`}>
<SingleProjectPermission
control={control}
setValue={setValue}
icon={icon}
title={title}
subtitle={subtitle}
formName={formName}
/>
</div>
))}
<div key="permission-secret-rollback">
<SecretRollbackPermission control={control} setValue={setValue} />
</div>
</div>
<div className="mt-12 flex items-center space-x-4">
<Button type="submit" isDisabled={isSubmitting || !isDirty} isLoading={isSubmitting}>
{isNewRole ? "Grant Privilege" : "Save Changes"}
</Button>
<Button onClick={onGoBack} variant="outline_bg">
Cancel
</Button>
</div>
</form>
</div>
);
};

View File

@@ -0,0 +1,181 @@
import {
faArrowLeft,
faPencil,
faPlus,
faTrash,
faUserShield
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { motion } from "framer-motion";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
Button,
DeleteActionModal,
EmptyState,
IconButton,
Tag,
Tooltip
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { usePopUp } from "@app/hooks";
import { useDeleteProjectUserAdditionalPrivilege } from "@app/hooks/api";
import { TWorkspaceUser } from "@app/hooks/api/types";
import { AdditionalPrivilegeForm } from "./AdditionalPrivilegeForm";
import { AdditionalPrivilegeTemporaryAccess } from "./AdditionalPrivilegeTemporaryAccess";
type Props = {
onGoBack: VoidFunction;
name: string;
projectMembershipId: string;
privileges: TWorkspaceUser["additionalPrivileges"];
};
export const AdditionalPrivilegeSection = ({
onGoBack,
privileges = [],
projectMembershipId,
name
}: Props) => {
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
"modifyPrivilege",
"deletePrivilege"
] as const);
const { createNotification } = useNotificationContext();
const deleteProjectUserAdditionalPrivilege = useDeleteProjectUserAdditionalPrivilege();
const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?.id || "";
const onPrivilegeDelete = async (privilegeId: string) => {
try {
await deleteProjectUserAdditionalPrivilege.mutateAsync({
privilegeId,
workspaceId
});
handlePopUpClose("deletePrivilege");
createNotification({
type: "success",
text: "Successfully removed user privilege"
});
} catch (err) {
createNotification({
type: "error",
text: "Failed to delete user privilege"
});
}
};
if (popUp.modifyPrivilege.isOpen) {
const privilegeDetails = popUp?.modifyPrivilege?.data as {
id: string;
};
return (
<motion.div
key="panel-additional-permission"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<AdditionalPrivilegeForm
onGoBack={() => handlePopUpClose("modifyPrivilege")}
privilegeId={privilegeDetails?.id}
workspaceId={workspaceId}
actorId={projectMembershipId}
/>
</motion.div>
);
}
return (
<motion.div
key="panel-privileges-list"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<div className="mb-8 flex items-center justify-between rounded-lg">
<h1 className="text-xl font-semibold capitalize text-mineshaft-100">
Additional Privileges - {name}
</h1>
<div className="flex items-center space-x-4">
<Button
onClick={onGoBack}
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faArrowLeft} />}
>
Go back
</Button>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("modifyPrivilege")}
>
New Privilege
</Button>
</div>
</div>
<div className="mt-6 flex flex-col space-y-4">
{privileges.length === 0 && (
<EmptyState title="User has no additional privileges" iconSize="3x" icon={faUserShield} />
)}
{privileges.map(({ id, name: privilegeName, description, slug, ...dto }) => (
<div
className="flex items-center space-x-4 rounded-md bg-mineshaft-800 p-4 px-6"
key={id}
>
<div className="flex flex-grow flex-col">
<div className="mb-1 flex items-center text-lg font-medium capitalize">
{privilegeName}
<Tag size="xs" className="ml-2">
{slug}
</Tag>
</div>
<div className="text-xs font-light capitalize">{description}</div>
</div>
<div className="flex items-center space-x-4">
<AdditionalPrivilegeTemporaryAccess
privilegeId={id}
workspaceId={workspaceId}
temporaryConfig={!dto.isTemporary ? { isTemporary: false } : { ...dto }}
/>
<IconButton
size="sm"
variant="outline_bg"
ariaLabel="update"
onClick={() => handlePopUpOpen("modifyPrivilege", { id })}
>
<Tooltip content="Edit">
<FontAwesomeIcon icon={faPencil} />
</Tooltip>
</IconButton>
<IconButton
size="sm"
colorSchema="danger"
variant="outline_bg"
ariaLabel="delete-privilege"
onClick={() => handlePopUpOpen("deletePrivilege", { name: privilegeName, id })}
>
<Tooltip content="Delete">
<FontAwesomeIcon icon={faTrash} />
</Tooltip>
</IconButton>
</div>
</div>
))}
</div>
<DeleteActionModal
isOpen={popUp.deletePrivilege.isOpen}
title={`Are you sure want to remove privilege ${(popUp?.deletePrivilege.data as { name: string })?.name || " "
} for user ${name}?`}
onChange={(isOpen) => handlePopUpToggle("deletePrivilege", isOpen)}
deleteKey="delete"
onDeleteApproved={async () =>
onPrivilegeDelete((popUp?.deletePrivilege.data as { id: string }).id)
}
/>
</motion.div>
);
};

View File

@@ -0,0 +1,193 @@
import { Controller, useForm } from "react-hook-form";
import { faClock } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { twMerge } from "tailwind-merge";
import { z } from "zod";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
Button,
FormControl,
IconButton,
Input,
Popover,
PopoverContent,
PopoverTrigger,
Tag,
Tooltip
} from "@app/components/v2";
import { usePopUp } from "@app/hooks";
import { useUpdateProjectUserAdditionalPrivilege } from "@app/hooks/api";
import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/hooks/api/projectUserAdditionalPrivilege/types";
const temporaryRoleFormSchema = z.object({
temporaryRange: z.string().min(1, "Required")
});
type TTemporaryRoleFormSchema = z.infer<typeof temporaryRoleFormSchema>;
type TTemporaryRoleFormProps = {
privilegeId: string;
workspaceId: string;
temporaryConfig?: {
isTemporary?: boolean;
temporaryAccessEndTime?: string | null;
temporaryAccessStartTime?: string | null;
temporaryRange?: string | null;
};
};
export const AdditionalPrivilegeTemporaryAccess = ({
temporaryConfig: defaultValues = {},
workspaceId,
privilegeId
}: TTemporaryRoleFormProps) => {
const { popUp, handlePopUpToggle } = usePopUp(["setTempRole"] as const);
const { createNotification } = useNotificationContext();
const { control, handleSubmit } = useForm<TTemporaryRoleFormSchema>({
resolver: zodResolver(temporaryRoleFormSchema),
values: {
temporaryRange: defaultValues.temporaryRange || "1h"
}
});
const isTemporaryFieldValue = defaultValues.isTemporary;
const isExpired =
isTemporaryFieldValue && new Date() > new Date(defaultValues.temporaryAccessEndTime || "");
const updateProjectUserAdditionalPrivilege = useUpdateProjectUserAdditionalPrivilege();
const handleGrantTemporaryAccess = async (el: TTemporaryRoleFormSchema) => {
try {
await updateProjectUserAdditionalPrivilege.mutateAsync({
privilegeId: privilegeId as string,
workspaceId,
isTemporary: true,
temporaryRange: el.temporaryRange,
temporaryAccessStartTime: new Date().toISOString(),
temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative
});
createNotification({ type: "success", text: "Successfully updated access" });
handlePopUpToggle("setTempRole");
} catch (err) {
console.log(err);
createNotification({ type: "error", text: "Failed to update access" });
}
};
const handleRevokeTemporaryAccess = async () => {
try {
await updateProjectUserAdditionalPrivilege.mutateAsync({
privilegeId: privilegeId as string,
workspaceId,
isTemporary: false
});
createNotification({ type: "success", text: "Successfully updated access" });
handlePopUpToggle("setTempRole");
} catch (err) {
console.log(err);
createNotification({ type: "error", text: "Failed to update access" });
}
};
return (
<Popover
open={popUp.setTempRole.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("setTempRole", isOpen);
}}
>
<PopoverTrigger>
<IconButton ariaLabel="role-temp" size="sm" variant="outline_bg">
<Tooltip content={isExpired ? "Timed access expired" : "Grant timed access"}>
<FontAwesomeIcon
icon={faClock}
className={twMerge(
isTemporaryFieldValue && "text-primary",
isExpired && "text-red-600"
)}
/>
</Tooltip>
</IconButton>
</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={control}
name="temporaryRange"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Validity"
isError={Boolean(error?.message)}
errorText={error?.message}
helperText={
<span>
1m, 2h, 3d.{" "}
<a
href="https://github.com/vercel/ms?tab=readme-ov-file#examples"
target="_blank"
rel="noopener noreferrer"
className="text-primary-700"
>
More
</a>
</span>
}
>
<Input {...field} />
</FormControl>
)}
/>
<div className="flex items-center space-x-2">
<Button
size="xs"
isLoading={
updateProjectUserAdditionalPrivilege.isLoading &&
updateProjectUserAdditionalPrivilege.variables?.isTemporary
}
isDisabled={
updateProjectUserAdditionalPrivilege.isLoading &&
updateProjectUserAdditionalPrivilege.variables?.isTemporary
}
onClick={() => {
handleSubmit(({ temporaryRange }) => {
handleGrantTemporaryAccess({ temporaryRange });
})();
}}
>
{isTemporaryFieldValue ? "Restart" : "Grant access"}
</Button>
{isTemporaryFieldValue && (
<Button
size="xs"
variant="outline_bg"
colorSchema="danger"
onClick={handleRevokeTemporaryAccess}
isLoading={
updateProjectUserAdditionalPrivilege.isLoading &&
!updateProjectUserAdditionalPrivilege.variables?.isTemporary
}
isDisabled={
updateProjectUserAdditionalPrivilege.isLoading &&
!updateProjectUserAdditionalPrivilege.variables?.isTemporary
}
>
Revoke Access
</Button>
)}
</div>
</div>
</PopoverContent>
</Popover>
);
};

View File

@@ -0,0 +1 @@
export { AdditionalPrivilegeSection } from "./AdditionalPrivilegeSection";

View File

@@ -2,9 +2,17 @@ import { useMemo, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import Link from "next/link";
import { faMagnifyingGlass, faPlus, faUsers, faXmark } from "@fortawesome/free-solid-svg-icons";
import {
faMagnifyingGlass,
faPlus,
faUsers,
faUserShield,
faXmark
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
@@ -48,6 +56,7 @@ import {
} from "@app/hooks/api";
import { ProjectVersion } from "@app/hooks/api/workspace/types";
import { AdditionalPrivilegeSection } from "../AdditionalPrivilegeSection";
import { MemberRoles } from "./MemberRoles";
const addMemberFormSchema = z.object({
@@ -77,7 +86,8 @@ export const MemberListTab = () => {
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"addMember",
"removeMember",
"upgradePlan"
"upgradePlan",
"additionalPrivilege"
] as const);
const {
@@ -185,8 +195,41 @@ export const MemberListTab = () => {
);
}, [orgUsers, members]);
if (popUp.additionalPrivilege.isOpen) {
const privilegeDetails = popUp?.additionalPrivilege?.data as {
name: string;
index: number;
projectMembershipId: string;
};
return (
<motion.div
key="panel-additional-permission"
className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<AdditionalPrivilegeSection
onGoBack={() => handlePopUpClose("additionalPrivilege")}
privileges={members?.[privilegeDetails.index]?.additionalPrivileges || []}
name={privilegeDetails.name}
projectMembershipId={privilegeDetails.projectMembershipId}
/>
</motion.div>
);
}
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<motion.div
key="panel-1"
className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<div className="mb-4 flex items-center justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Members</p>
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Member}>
@@ -223,58 +266,90 @@ export const MemberListTab = () => {
<TBody>
{isMembersLoading && <TableSkeleton columns={4} innerKey="project-members" />}
{!isMembersLoading &&
filterdUsers?.map(({ user: u, inviteEmail, id: membershipId, roles }) => {
const name = u ? `${u.firstName} ${u.lastName}` : "-";
const email = u?.email || inviteEmail;
filterdUsers?.map(
(
{ user: u, inviteEmail, id: membershipId, roles, additionalPrivileges },
index
) => {
const name = u ? `${u.firstName} ${u.lastName}` : "-";
const email = u?.email || inviteEmail;
const hasAdditionalPrivilege = Boolean(additionalPrivileges.length);
return (
<Tr key={`membership-${membershipId}`} className="w-full">
<Td>{name}</Td>
<Td>{email}</Td>
<Td>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Member}
>
{(isAllowed) => (
<MemberRoles
roles={roles}
disableEdit={u.id === user?.id || !isAllowed}
onOpenUpgradeModal={(description) =>
handlePopUpOpen("upgradePlan", { description })
}
membershipId={membershipId}
/>
)}
</ProjectPermissionCan>
</Td>
<Td>
{userId !== u?.id && (
return (
<Tr key={`membership-${membershipId}`} className="w-full">
<Td>{name}</Td>
<Td>{email}</Td>
<Td>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Member}
>
{(isAllowed) => (
<IconButton
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="ml-4"
isDisabled={userId === u?.id || !isAllowed}
onClick={() =>
handlePopUpOpen("removeMember", { username: u.username })
<MemberRoles
roles={roles}
disableEdit={u.id === user?.id || !isAllowed}
onOpenUpgradeModal={(description) =>
handlePopUpOpen("upgradePlan", { description })
}
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
membershipId={membershipId}
/>
)}
</ProjectPermissionCan>
)}
</Td>
</Tr>
);
})}
</Td>
<Td>
{userId !== u?.id && (
<div className="flex items-center space-x-2">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Member}
allowedLabel="Additional Privilege"
>
{(isAllowed) => (
<IconButton
size="lg"
variant="plain"
ariaLabel="update"
className={twMerge(hasAdditionalPrivilege && "text-primary")}
isDisabled={userId === u?.id || !isAllowed}
onClick={() =>
handlePopUpOpen("additionalPrivilege", {
name: `${user.firstName} ${user.lastName || ""}`,
index,
projectMembershipId: membershipId
})
}
>
<FontAwesomeIcon icon={faUserShield} />
</IconButton>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Member}
>
{(isAllowed) => (
<IconButton
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="ml-4"
isDisabled={userId === u?.id || !isAllowed}
onClick={() =>
handlePopUpOpen("removeMember", { username: u.username })
}
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
)}
</ProjectPermissionCan>
</div>
)}
</Td>
</Tr>
);
}
)}
</TBody>
</Table>
{!isMembersLoading && filterdUsers?.length === 0 && (
@@ -355,6 +430,6 @@ export const MemberListTab = () => {
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={(popUp.upgradePlan?.data as { description: string })?.description}
/>
</div>
</motion.div>
);
};

View File

@@ -1,5 +1,5 @@
import { useMemo } from "react";
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
import { Control, Controller, UseFormGetValues, UseFormSetValue, useWatch } from "react-hook-form";
import { IconProp } from "@fortawesome/fontawesome-svg-core";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { motion } from "framer-motion";
@@ -28,6 +28,7 @@ type Props = {
formName: "secrets";
isNonEditable?: boolean;
setValue: UseFormSetValue<TFormSchema>;
getValue: UseFormGetValues<TFormSchema>;
control: Control<TFormSchema>;
title: string;
subtitle: string;
@@ -44,6 +45,7 @@ enum Permission {
export const MultiEnvProjectPermission = ({
isNonEditable,
setValue,
getValue,
control,
formName,
title,
@@ -69,9 +71,12 @@ export const MultiEnvProjectPermission = ({
const handlePermissionChange = (val: Permission) => {
switch (val) {
case Permission.NoAccess:
setValue(`permissions.${formName}`, undefined, { shouldDirty: true });
case Permission.NoAccess: {
const permissions = getValue("permissions");
if (permissions) delete permissions[formName];
setValue("permissions", permissions, { shouldDirty: true });
break;
}
case Permission.FullAccess:
setValue(
`permissions.${formName}`,
@@ -101,7 +106,7 @@ export const MultiEnvProjectPermission = ({
className={twMerge(
"rounded-md bg-mineshaft-800 px-10 py-6",
(selectedPermissionCategory !== Permission.NoAccess || isCustom) &&
"border-l-2 border-primary-600"
"border-l-2 border-primary-600"
)}
>
<div className="flex items-center space-x-4">

View File

@@ -128,6 +128,7 @@ export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
register,
formState: { isSubmitting, isDirty, errors },
setValue,
getValues,
control
} = useForm<TFormSchema>({
defaultValues: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : {},
@@ -226,6 +227,7 @@ export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
</div>
<div>
<MultiEnvProjectPermission
getValue={getValues}
isNonEditable={isNonEditable}
control={control}
setValue={setValue}