mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 09:26:47 +00:00
feat(ui): completed ui for user additional privilege
This commit is contained in:
@@ -20,6 +20,7 @@ const tagVariants = cva(
|
|||||||
green: "bg-primary-800 text-white"
|
green: "bg-primary-800 text-white"
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
|
xs: "text-xs px-1 py-0.5",
|
||||||
sm: "px-2 py-0.5"
|
sm: "px-2 py-0.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,15 +32,7 @@ export const MembersPage = withProjectPermission(
|
|||||||
<Tab value={TabSections.Roles}>Project Roles</Tab>
|
<Tab value={TabSections.Roles}>Project Roles</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanel value={TabSections.Member}>
|
<TabPanel value={TabSections.Member}>
|
||||||
<motion.div
|
<MemberListTab />
|
||||||
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>
|
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel value={TabSections.Identities}>
|
<TabPanel value={TabSections.Identities}>
|
||||||
<IdentityTab />
|
<IdentityTab />
|
||||||
|
|||||||
+283
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+181
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+193
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { AdditionalPrivilegeSection } from "./AdditionalPrivilegeSection";
|
||||||
+123
-48
@@ -2,9 +2,17 @@ import { useMemo, useState } from "react";
|
|||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Link from "next/link";
|
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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
@@ -48,6 +56,7 @@ import {
|
|||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
||||||
|
|
||||||
|
import { AdditionalPrivilegeSection } from "../AdditionalPrivilegeSection";
|
||||||
import { MemberRoles } from "./MemberRoles";
|
import { MemberRoles } from "./MemberRoles";
|
||||||
|
|
||||||
const addMemberFormSchema = z.object({
|
const addMemberFormSchema = z.object({
|
||||||
@@ -77,7 +86,8 @@ export const MemberListTab = () => {
|
|||||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
"addMember",
|
"addMember",
|
||||||
"removeMember",
|
"removeMember",
|
||||||
"upgradePlan"
|
"upgradePlan",
|
||||||
|
"additionalPrivilege"
|
||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -185,8 +195,41 @@ export const MemberListTab = () => {
|
|||||||
);
|
);
|
||||||
}, [orgUsers, members]);
|
}, [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 (
|
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">
|
<div className="mb-4 flex items-center justify-between">
|
||||||
<p className="text-xl font-semibold text-mineshaft-100">Members</p>
|
<p className="text-xl font-semibold text-mineshaft-100">Members</p>
|
||||||
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Member}>
|
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Member}>
|
||||||
@@ -223,58 +266,90 @@ export const MemberListTab = () => {
|
|||||||
<TBody>
|
<TBody>
|
||||||
{isMembersLoading && <TableSkeleton columns={4} innerKey="project-members" />}
|
{isMembersLoading && <TableSkeleton columns={4} innerKey="project-members" />}
|
||||||
{!isMembersLoading &&
|
{!isMembersLoading &&
|
||||||
filterdUsers?.map(({ user: u, inviteEmail, id: membershipId, roles }) => {
|
filterdUsers?.map(
|
||||||
const name = u ? `${u.firstName} ${u.lastName}` : "-";
|
(
|
||||||
const email = u?.email || inviteEmail;
|
{ 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 (
|
return (
|
||||||
<Tr key={`membership-${membershipId}`} className="w-full">
|
<Tr key={`membership-${membershipId}`} className="w-full">
|
||||||
<Td>{name}</Td>
|
<Td>{name}</Td>
|
||||||
<Td>{email}</Td>
|
<Td>{email}</Td>
|
||||||
<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 && (
|
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Delete}
|
I={ProjectPermissionActions.Edit}
|
||||||
a={ProjectPermissionSub.Member}
|
a={ProjectPermissionSub.Member}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<IconButton
|
<MemberRoles
|
||||||
size="lg"
|
roles={roles}
|
||||||
colorSchema="danger"
|
disableEdit={u.id === user?.id || !isAllowed}
|
||||||
variant="plain"
|
onOpenUpgradeModal={(description) =>
|
||||||
ariaLabel="update"
|
handlePopUpOpen("upgradePlan", { description })
|
||||||
className="ml-4"
|
|
||||||
isDisabled={userId === u?.id || !isAllowed}
|
|
||||||
onClick={() =>
|
|
||||||
handlePopUpOpen("removeMember", { username: u.username })
|
|
||||||
}
|
}
|
||||||
>
|
membershipId={membershipId}
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
/>
|
||||||
</IconButton>
|
|
||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
)}
|
</Td>
|
||||||
</Td>
|
<Td>
|
||||||
</Tr>
|
{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>
|
</TBody>
|
||||||
</Table>
|
</Table>
|
||||||
{!isMembersLoading && filterdUsers?.length === 0 && (
|
{!isMembersLoading && filterdUsers?.length === 0 && (
|
||||||
@@ -355,6 +430,6 @@ export const MemberListTab = () => {
|
|||||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||||
text={(popUp.upgradePlan?.data as { description: string })?.description}
|
text={(popUp.upgradePlan?.data as { description: string })?.description}
|
||||||
/>
|
/>
|
||||||
</div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+9
-4
@@ -1,5 +1,5 @@
|
|||||||
import { useMemo } from "react";
|
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 { IconProp } from "@fortawesome/fontawesome-svg-core";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
@@ -28,6 +28,7 @@ type Props = {
|
|||||||
formName: "secrets";
|
formName: "secrets";
|
||||||
isNonEditable?: boolean;
|
isNonEditable?: boolean;
|
||||||
setValue: UseFormSetValue<TFormSchema>;
|
setValue: UseFormSetValue<TFormSchema>;
|
||||||
|
getValue: UseFormGetValues<TFormSchema>;
|
||||||
control: Control<TFormSchema>;
|
control: Control<TFormSchema>;
|
||||||
title: string;
|
title: string;
|
||||||
subtitle: string;
|
subtitle: string;
|
||||||
@@ -44,6 +45,7 @@ enum Permission {
|
|||||||
export const MultiEnvProjectPermission = ({
|
export const MultiEnvProjectPermission = ({
|
||||||
isNonEditable,
|
isNonEditable,
|
||||||
setValue,
|
setValue,
|
||||||
|
getValue,
|
||||||
control,
|
control,
|
||||||
formName,
|
formName,
|
||||||
title,
|
title,
|
||||||
@@ -69,9 +71,12 @@ export const MultiEnvProjectPermission = ({
|
|||||||
|
|
||||||
const handlePermissionChange = (val: Permission) => {
|
const handlePermissionChange = (val: Permission) => {
|
||||||
switch (val) {
|
switch (val) {
|
||||||
case Permission.NoAccess:
|
case Permission.NoAccess: {
|
||||||
setValue(`permissions.${formName}`, undefined, { shouldDirty: true });
|
const permissions = getValue("permissions");
|
||||||
|
if (permissions) delete permissions[formName];
|
||||||
|
setValue("permissions", permissions, { shouldDirty: true });
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case Permission.FullAccess:
|
case Permission.FullAccess:
|
||||||
setValue(
|
setValue(
|
||||||
`permissions.${formName}`,
|
`permissions.${formName}`,
|
||||||
@@ -101,7 +106,7 @@ export const MultiEnvProjectPermission = ({
|
|||||||
className={twMerge(
|
className={twMerge(
|
||||||
"rounded-md bg-mineshaft-800 px-10 py-6",
|
"rounded-md bg-mineshaft-800 px-10 py-6",
|
||||||
(selectedPermissionCategory !== Permission.NoAccess || isCustom) &&
|
(selectedPermissionCategory !== Permission.NoAccess || isCustom) &&
|
||||||
"border-l-2 border-primary-600"
|
"border-l-2 border-primary-600"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
|
|||||||
+2
@@ -128,6 +128,7 @@ export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
|
|||||||
register,
|
register,
|
||||||
formState: { isSubmitting, isDirty, errors },
|
formState: { isSubmitting, isDirty, errors },
|
||||||
setValue,
|
setValue,
|
||||||
|
getValues,
|
||||||
control
|
control
|
||||||
} = useForm<TFormSchema>({
|
} = useForm<TFormSchema>({
|
||||||
defaultValues: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : {},
|
defaultValues: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : {},
|
||||||
@@ -226,6 +227,7 @@ export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<MultiEnvProjectPermission
|
<MultiEnvProjectPermission
|
||||||
|
getValue={getValues}
|
||||||
isNonEditable={isNonEditable}
|
isNonEditable={isNonEditable}
|
||||||
control={control}
|
control={control}
|
||||||
setValue={setValue}
|
setValue={setValue}
|
||||||
|
|||||||
Reference in New Issue
Block a user