diff --git a/backend/src/ee/services/permission/default-roles.ts b/backend/src/ee/services/permission/default-roles.ts index a3c9c2c11..a018ffe81 100644 --- a/backend/src/ee/services/permission/default-roles.ts +++ b/backend/src/ee/services/permission/default-roles.ts @@ -126,7 +126,6 @@ const buildAdminPermissionRules = () => { can( [ - ProjectPermissionSecretActions.DescribeAndReadValue, ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSecretActions.ReadValue, ProjectPermissionSecretActions.Create, @@ -207,7 +206,6 @@ const buildMemberPermissionRules = () => { can( [ - ProjectPermissionSecretActions.DescribeAndReadValue, ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSecretActions.ReadValue, ProjectPermissionSecretActions.Edit, @@ -386,9 +384,10 @@ const buildMemberPermissionRules = () => { const buildViewerPermissionRules = () => { const { can, rules } = new AbilityBuilder>(createMongoAbility); - can(ProjectPermissionSecretActions.DescribeAndReadValue, ProjectPermissionSub.Secrets); - can(ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSub.Secrets); - can(ProjectPermissionSecretActions.ReadValue, ProjectPermissionSub.Secrets); + can( + [ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSecretActions.ReadValue], + ProjectPermissionSub.Secrets + ); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders); can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports); diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTable.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTable.tsx index e4c7aca98..508e385df 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTable.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTable.tsx @@ -34,6 +34,7 @@ import { isCustomOrgRole } from "@app/helpers/roles"; import { usePopUp } from "@app/hooks"; import { useDeleteOrgRole, useGetOrgRoles, useUpdateOrg } from "@app/hooks/api"; import { TOrgRole } from "@app/hooks/api/roles/types"; +import { DuplicateOrgRoleModal } from "@app/pages/organization/RoleByIDPage/components/DuplicateOrgRoleModal"; import { RoleModal } from "@app/pages/organization/RoleByIDPage/components/RoleModal"; export const OrgRoleTable = () => { @@ -44,6 +45,7 @@ export const OrgRoleTable = () => { const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "role", "deleteRole", + "duplicateRole", "upgradePlan" ] as const); @@ -192,6 +194,25 @@ export const OrgRoleTable = () => { )} + + {(isAllowed) => ( + { + e.stopPropagation(); + handlePopUpOpen("duplicateRole", role); + }} + disabled={!isAllowed} + > + Duplicate Role + + )} + {!isDefaultOrgRole && ( { onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)} text={(popUp.upgradePlan?.data as { description: string })?.description} /> + handlePopUpToggle("duplicateRole", isOpen)} + roleId={(popUp?.duplicateRole?.data as TOrgRole)?.id} + /> ); }; diff --git a/frontend/src/pages/organization/RoleByIDPage/RoleByIDPage.tsx b/frontend/src/pages/organization/RoleByIDPage/RoleByIDPage.tsx index a3165709a..7acc468f8 100644 --- a/frontend/src/pages/organization/RoleByIDPage/RoleByIDPage.tsx +++ b/frontend/src/pages/organization/RoleByIDPage/RoleByIDPage.tsx @@ -19,6 +19,7 @@ import { ROUTE_PATHS } from "@app/const/routes"; import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context"; import { useDeleteOrgRole, useGetOrgRole } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; +import { DuplicateOrgRoleModal } from "@app/pages/organization/RoleByIDPage/components/DuplicateOrgRoleModal"; import { OrgAccessControlTabSections } from "@app/types/org"; import { RoleDetailsSection, RoleModal, RolePermissionsSection } from "./components"; @@ -36,7 +37,8 @@ export const Page = () => { const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "role", - "deleteOrgRole" + "deleteOrgRole", + "duplicateRole" ] as const); const onDeleteOrgRoleSubmit = async () => { @@ -106,6 +108,21 @@ export const Page = () => { )} + + {(isAllowed) => ( + { + handlePopUpOpen("duplicateRole"); + }} + disabled={!isAllowed} + > + Duplicate Role + + )} + {(isAllowed) => ( { deleteKey="confirm" onDeleteApproved={() => onDeleteOrgRoleSubmit()} /> + handlePopUpToggle("duplicateRole", isOpen)} + roleId={data?.id} + /> ); }; diff --git a/frontend/src/pages/organization/RoleByIDPage/components/DuplicateOrgRoleModal.tsx b/frontend/src/pages/organization/RoleByIDPage/components/DuplicateOrgRoleModal.tsx new file mode 100644 index 000000000..8995c03b3 --- /dev/null +++ b/frontend/src/pages/organization/RoleByIDPage/components/DuplicateOrgRoleModal.tsx @@ -0,0 +1,150 @@ +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useNavigate } from "@tanstack/react-router"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { Button, FormControl, Input, Modal, ModalContent, Spinner } from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { useCreateOrgRole, useGetOrgRole } from "@app/hooks/api"; +import { TOrgRole } from "@app/hooks/api/roles/types"; +import { slugSchema } from "@app/lib/schemas"; + +type Props = { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; + roleId?: string; +}; + +const schema = z + .object({ + name: z.string().min(1, "Name required"), + description: z.string(), + slug: slugSchema({ min: 1 }) + }) + .required(); + +export type FormData = z.infer; + +type ContentProps = { + role: TOrgRole; + onClose: () => void; +}; + +const Content = ({ role, onClose }: ContentProps) => { + const { + control, + handleSubmit, + formState: { isSubmitting } + } = useForm({ + defaultValues: { + name: `${role.name} Duplicate` + }, + resolver: zodResolver(schema) + }); + + const createRole = useCreateOrgRole(); + const navigate = useNavigate(); + + const handleDuplicateRole = async (form: FormData) => { + const newRole = await createRole.mutateAsync({ + orgId: role.orgId, + permissions: role.permissions, + ...form + }); + + createNotification({ + type: "success", + text: "Role duplicated successfully" + }); + + navigate({ + to: "/organization/roles/$roleId", + params: { + roleId: newRole.id + } + }); + + onClose(); + }; + + return ( +
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> +
+ + +
+ + ); +}; + +export const DuplicateOrgRoleModal = ({ isOpen, onOpenChange, roleId }: Props) => { + const { currentOrg } = useOrganization(); + + const { data: role, isPending } = useGetOrgRole(currentOrg.id, roleId ?? ""); + + if (!roleId) return null; + + return ( + + + {/* eslint-disable-next-line no-nested-ternary */} + {isPending ? ( +
+ +

Loading Role...

+
+ ) : role ? ( + onOpenChange(false)} /> + ) : ( +

+ Error: could not find role with slug "{roleId}" +

+ )} +
+
+ ); +}; diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RoleModal.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RoleModal.tsx index da93d3cc0..88f568c4a 100644 --- a/frontend/src/pages/organization/RoleByIDPage/components/RoleModal.tsx +++ b/frontend/src/pages/organization/RoleByIDPage/components/RoleModal.tsx @@ -13,7 +13,7 @@ import { slugSchema } from "@app/lib/schemas"; const schema = z .object({ - name: z.string(), + name: z.string().min(1, "Name required"), description: z.string(), slug: slugSchema({ min: 1 }) }) @@ -71,12 +71,6 @@ export const RoleModal = ({ popUp, handlePopUpToggle }: Props) => { const onFormSubmit = async ({ name, description, slug }: FormData) => { try { - console.log("onFormSubmit args: ", { - name, - description, - slug - }); - if (!orgId) return; if (role) { diff --git a/frontend/src/pages/project/AccessControlPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx b/frontend/src/pages/project/AccessControlPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx index 550e0e1a1..9f04ff722 100644 --- a/frontend/src/pages/project/AccessControlPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx @@ -25,13 +25,15 @@ import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@a import { usePopUp } from "@app/hooks"; import { useDeleteProjectRole, useGetProjectRoles } from "@app/hooks/api"; import { ProjectMembershipRole, TProjectRole } from "@app/hooks/api/roles/types"; +import { DuplicateProjectRoleModal } from "@app/pages/project/RoleDetailsBySlugPage/components/DuplicateProjectRoleModal"; import { RoleModal } from "@app/pages/project/RoleDetailsBySlugPage/components/RoleModal"; export const ProjectRoleList = () => { const navigate = useNavigate(); const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "role", - "deleteRole" + "deleteRole", + "duplicateRole" ] as const); const { currentWorkspace } = useWorkspace(); const projectId = currentWorkspace?.id || ""; @@ -139,6 +141,25 @@ export const ProjectRoleList = () => {
)} + + {(isAllowed) => ( + { + e.stopPropagation(); + handlePopUpOpen("duplicateRole", role); + }} + disabled={!isAllowed} + > + Duplicate Role + + )} + {!isNonMutatable && ( { onClose={() => handlePopUpClose("deleteRole")} onDeleteApproved={handleRoleDelete} /> + handlePopUpToggle("duplicateRole", isOpen)} + roleSlug={(popUp?.duplicateRole?.data as TProjectRole)?.slug} + /> ); }; diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/RoleDetailsBySlugPage.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/RoleDetailsBySlugPage.tsx index 0431726da..3f5de1ca7 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/RoleDetailsBySlugPage.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/RoleDetailsBySlugPage.tsx @@ -19,6 +19,7 @@ import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@a import { useDeleteProjectRole, useGetProjectRoleBySlug } from "@app/hooks/api"; import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; import { usePopUp } from "@app/hooks/usePopUp"; +import { DuplicateProjectRoleModal } from "@app/pages/project/RoleDetailsBySlugPage/components/DuplicateProjectRoleModal"; import { ProjectAccessControlTabs } from "@app/types/project"; import { RoleDetailsSection } from "./components/RoleDetailsSection"; @@ -40,7 +41,8 @@ const Page = () => { const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "role", - "deleteRole" + "deleteRole", + "duplicateRole" ] as const); const onDeleteRoleSubmit = async () => { @@ -117,6 +119,24 @@ const Page = () => { )} + + {(isAllowed) => ( + { + handlePopUpOpen("duplicateRole"); + }} + disabled={!isAllowed} + > + Duplicate Role + + )} + { deleteKey="confirm" onDeleteApproved={() => onDeleteRoleSubmit()} /> + handlePopUpToggle("duplicateRole", isOpen)} + roleSlug={roleSlug} + /> ); }; diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/DuplicateProjectRoleModal.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/DuplicateProjectRoleModal.tsx new file mode 100644 index 000000000..4ac54e973 --- /dev/null +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/DuplicateProjectRoleModal.tsx @@ -0,0 +1,153 @@ +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useNavigate } from "@tanstack/react-router"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { Button, FormControl, Input, Modal, ModalContent, Spinner } from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { useCreateProjectRole, useGetProjectRoleBySlug } from "@app/hooks/api"; +import { TProjectRole } from "@app/hooks/api/roles/types"; +import { slugSchema } from "@app/lib/schemas"; + +type Props = { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; + roleSlug?: string; +}; + +const schema = z + .object({ + name: z.string().min(1, "Name required"), + description: z.string(), + slug: slugSchema({ min: 1 }) + }) + .required(); + +export type FormData = z.infer; + +type ContentProps = { + role: TProjectRole; + onClose: () => void; +}; + +const Content = ({ role, onClose }: ContentProps) => { + const { + control, + handleSubmit, + formState: { isSubmitting } + } = useForm({ + defaultValues: { + name: `${role.name} Duplicate` + }, + resolver: zodResolver(schema) + }); + + const { currentWorkspace } = useWorkspace(); + + const createRole = useCreateProjectRole(); + const navigate = useNavigate(); + + const handleDuplicateRole = async (form: FormData) => { + const newRole = await createRole.mutateAsync({ + projectId: currentWorkspace.id, + permissions: role.permissions, + ...form + }); + + createNotification({ + type: "success", + text: "Role duplicated successfully" + }); + + navigate({ + to: `/${currentWorkspace.type}/$projectId/roles/$roleSlug` as const, + params: { + roleSlug: newRole.slug, + projectId: currentWorkspace.id + } + }); + + onClose(); + }; + + return ( +
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> +
+ + +
+ + ); +}; + +export const DuplicateProjectRoleModal = ({ isOpen, onOpenChange, roleSlug }: Props) => { + const { currentWorkspace } = useWorkspace(); + + const { data: role, isPending } = useGetProjectRoleBySlug(currentWorkspace.id, roleSlug ?? ""); + + if (!roleSlug) return null; + + return ( + + + {/* eslint-disable-next-line no-nested-ternary */} + {isPending ? ( +
+ +

Loading Role...

+
+ ) : role ? ( + onOpenChange(false)} /> + ) : ( +

+ Error: could not find role with slug "{roleSlug}" +

+ )} +
+
+ ); +}; diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RoleModal.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RoleModal.tsx index 7a799bc84..2e51de9b7 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RoleModal.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RoleModal.tsx @@ -17,7 +17,7 @@ import { slugSchema } from "@app/lib/schemas"; const schema = z .object({ - name: z.string(), + name: z.string().min(1, "Name required"), description: z.string(), slug: slugSchema({ min: 1 }) })