diff --git a/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateEditRoleForm.tsx b/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateEditRoleForm.tsx index b1a639e89..7431b41fa 100644 --- a/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateEditRoleForm.tsx +++ b/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateEditRoleForm.tsx @@ -6,15 +6,15 @@ import { twMerge } from "tailwind-merge"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; -import { Button, FormControl, Input, Modal, ModalContent, ModalTrigger } from "@app/components/v2"; +import { Button, FormControl, Input } from "@app/components/v2"; import { ProjectPermissionSub } from "@app/context"; import { isCustomProjectRole } from "@app/helpers/roles"; import { usePopUp } from "@app/hooks"; import { TProjectTemplate, useUpdateProjectTemplate } from "@app/hooks/api/projectTemplates"; import { slugSchema } from "@app/lib/schemas"; import { GeneralPermissionPolicies } from "@app/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies"; -import { NewPermissionRule } from "@app/pages/project/RoleDetailsBySlugPage/components/NewPermissionRule"; import { PermissionEmptyState } from "@app/pages/project/RoleDetailsBySlugPage/components/PermissionEmptyState"; +import { PolicySelectionModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal"; import { formRolePermission2API, PROJECT_PERMISSION_OBJECT, @@ -44,7 +44,7 @@ export const ProjectTemplateEditRoleForm = ({ role, isDisabled }: Props) => { - const { popUp, handlePopUpToggle } = usePopUp(["createPolicy"] as const); + const { popUp, handlePopUpToggle } = usePopUp(["addPolicy"] as const); const formMethods = useForm({ values: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : undefined, @@ -119,34 +119,29 @@ export const ProjectTemplateEditRoleForm = ({ - handlePopUpToggle("createPolicy", isOpen)} + - - - handlePopUpToggle("createPolicy")} /> - - + Add Policies + + handlePopUpToggle("addPolicy", isOpen)} + /> )} diff --git a/frontend/src/hooks/useGetProjectTypeFromRoute.tsx b/frontend/src/hooks/useGetProjectTypeFromRoute.tsx index 95215eff5..b3b8f3d5e 100644 --- a/frontend/src/hooks/useGetProjectTypeFromRoute.tsx +++ b/frontend/src/hooks/useGetProjectTypeFromRoute.tsx @@ -1,3 +1,4 @@ +import { useMemo } from "react"; import { useRouterState } from "@tanstack/react-router"; import { ProjectType } from "@app/hooks/api/workspace/types"; @@ -5,9 +6,17 @@ import { ProjectType } from "@app/hooks/api/workspace/types"; export const useGetProjectTypeFromRoute = () => { const { location } = useRouterState(); - const segment = location.pathname.split("/")[2]; + return useMemo(() => { + const segments = location.pathname.split("/"); - if (!Object.values(ProjectType).includes(segment as ProjectType)) return undefined; + let type: ProjectType | undefined; - return segment as ProjectType; + // location of project type can vary in router path, so we need to check all possible values + segments.forEach((segment) => { + if (Object.values(ProjectType).includes(segment as ProjectType)) + type = segment as ProjectType; + }); + + return type; + }, [location]); }; diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeModifySection.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeModifySection.tsx index ac761e129..50d392748 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeModifySection.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeModifySection.tsx @@ -18,10 +18,6 @@ import { TtlFormLabel } from "@app/components/features"; import { createNotification } from "@app/components/notifications"; import { Button, - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, FormControl, FormLabel, Input, @@ -37,6 +33,7 @@ import { useProjectPermission, useWorkspace } from "@app/context"; +import { usePopUp } from "@app/hooks"; import { useCreateIdentityProjectAdditionalPrivilege, useGetIdentityProjectPrivilegeDetails, @@ -45,12 +42,11 @@ import { import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/hooks/api/identityProjectAdditionalPrivilege/types"; import { GeneralPermissionPolicies } from "@app/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies"; import { PermissionEmptyState } from "@app/pages/project/RoleDetailsBySlugPage/components/PermissionEmptyState"; +import { PolicySelectionModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal"; import { formRolePermission2API, - isConditionalSubjects, PROJECT_PERMISSION_OBJECT, projectRoleFormSchema, - ProjectTypePermissionSubjects, rolePermission2Form } from "@app/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils"; import { renderConditionalComponents } from "@app/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection"; @@ -101,6 +97,7 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({ ProjectPermissionIdentityActions.Edit, subject(ProjectPermissionSub.Identity, { identityId }) ); + const { popUp, handlePopUpToggle } = usePopUp(["addPolicy"] as const); const form = useForm({ values: privilegeDetails @@ -195,30 +192,6 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({ } } - const onNewPolicy = (selectedSubject: ProjectPermissionSub) => { - const rootPolicyValue = form.getValues(`permissions.${selectedSubject}`); - if (rootPolicyValue && isConditionalSubjects(selectedSubject)) { - form.setValue( - `permissions.${selectedSubject}`, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore-error akhilmhdh: this is because of ts collision with both - [...rootPolicyValue, {}], - { shouldDirty: true, shouldTouch: true } - ); - } else { - form.setValue( - `permissions.${selectedSubject}`, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore-error akhilmhdh: this is because of ts collision with both - [{}], - { - shouldDirty: true, - shouldTouch: true - } - ); - } - }; - return (
} > Save - - - - - - {Object.keys(PROJECT_PERMISSION_OBJECT) - .filter( - (sub) => - ProjectTypePermissionSubjects[currentWorkspace.type][ - sub as ProjectPermissionSub - ] - ) - .sort((a, b) => - PROJECT_PERMISSION_OBJECT[a as keyof typeof PROJECT_PERMISSION_OBJECT].title - .toLowerCase() - .localeCompare( - PROJECT_PERMISSION_OBJECT[ - b as keyof typeof PROJECT_PERMISSION_OBJECT - ].title.toLowerCase() - ) - ) - .map((permissionSubject) => ( - onNewPolicy(permissionSubject as ProjectPermissionSub)} - > - {PROJECT_PERMISSION_OBJECT[permissionSubject as ProjectPermissionSub].title} - - ))} - - + @@ -436,6 +382,10 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({ ) )} + handlePopUpToggle("addPolicy", isOpen)} + /> ); diff --git a/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx b/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx index 6b345250c..5fee21f9c 100644 --- a/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx +++ b/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx @@ -17,10 +17,6 @@ import { TtlFormLabel } from "@app/components/features"; import { createNotification } from "@app/components/notifications"; import { Button, - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, FormControl, FormLabel, Input, @@ -36,6 +32,7 @@ import { useProjectPermission, useWorkspace } from "@app/context"; +import { usePopUp } from "@app/hooks"; import { useCreateProjectUserAdditionalPrivilege, useGetProjectUserPrivilegeDetails, @@ -44,12 +41,11 @@ import { import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/hooks/api/projectUserAdditionalPrivilege/types"; import { GeneralPermissionPolicies } from "@app/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies"; import { PermissionEmptyState } from "@app/pages/project/RoleDetailsBySlugPage/components/PermissionEmptyState"; +import { PolicySelectionModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal"; import { formRolePermission2API, - isConditionalSubjects, PROJECT_PERMISSION_OBJECT, projectRoleFormSchema, - ProjectTypePermissionSubjects, rolePermission2Form } from "@app/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils"; import { renderConditionalComponents } from "@app/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection"; @@ -87,6 +83,8 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({ projectMembershipId, isDisabled }: Props) => { + const { popUp, handlePopUpToggle } = usePopUp(["addPolicy"] as const); + const isCreate = !privilegeId; const { currentWorkspace } = useWorkspace(); const projectId = currentWorkspace?.id || ""; @@ -167,30 +165,6 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({ } }; - const onNewPolicy = (selectedSubject: ProjectPermissionSub) => { - const rootPolicyValue = form.getValues(`permissions.${selectedSubject}`); - if (rootPolicyValue && isConditionalSubjects(selectedSubject)) { - form.setValue( - `permissions.${selectedSubject}`, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore-error akhilmhdh: this is because of ts collision with both - [...rootPolicyValue, {}], - { shouldDirty: true, shouldTouch: true } - ); - } else { - form.setValue( - `permissions.${selectedSubject}`, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore-error akhilmhdh: this is because of ts collision with both - [{}], - { - shouldDirty: true, - shouldTouch: true - } - ); - } - }; - const privilegeTemporaryAccess = form.watch("temporaryAccess"); const isTemporary = privilegeTemporaryAccess?.isTemporary; const isExpired = @@ -246,52 +220,25 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({ - - - - - - {Object.keys(PROJECT_PERMISSION_OBJECT) - .filter( - (subject) => - ProjectTypePermissionSubjects[currentWorkspace.type][ - subject as ProjectPermissionSub - ] - ) - .sort((a, b) => - PROJECT_PERMISSION_OBJECT[a as keyof typeof PROJECT_PERMISSION_OBJECT].title - .toLowerCase() - .localeCompare( - PROJECT_PERMISSION_OBJECT[ - b as keyof typeof PROJECT_PERMISSION_OBJECT - ].title.toLowerCase() - ) - ) - .map((subject) => ( - onNewPolicy(subject as ProjectPermissionSub)} - > - {PROJECT_PERMISSION_OBJECT[subject as ProjectPermissionSub].title} - - ))} - - + @@ -430,6 +377,10 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({ ))} + handlePopUpToggle("addPolicy", isOpen)} + /> ); diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/NewPermissionRule.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/NewPermissionRule.tsx deleted file mode 100644 index cb3ac2f85..000000000 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/NewPermissionRule.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { Controller, useForm, useFormContext } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { z } from "zod"; - -import { - Button, - Checkbox, - FormControl, - FormLabel, - ModalClose, - Select, - SelectItem -} from "@app/components/v2"; -import { ProjectPermissionSub } from "@app/context"; -import { useGetProjectTypeFromRoute } from "@app/hooks"; -import { ProjectType } from "@app/hooks/api/workspace/types"; - -import { - isConditionalSubjects, - PROJECT_PERMISSION_OBJECT, - projectRoleFormSchema, - ProjectTypePermissionSubjects, - TFormSchema -} from "./ProjectRoleModifySection.utils"; - -type Props = { - onClose: () => void; -}; - -export const NewPermissionRule = ({ onClose }: Props) => { - const rootForm = useFormContext(); - - const form = useForm<{ - type: ProjectPermissionSub; - permissions: NonNullable; - }>({ - resolver: zodResolver( - projectRoleFormSchema - .pick({ permissions: true }) - .extend({ type: z.nativeEnum(ProjectPermissionSub) }) - ), - defaultValues: { - type: ProjectPermissionSub.Project - } - }); - - const selectedSubject = form.watch("type"); - - const projectType = useGetProjectTypeFromRoute(); - - return ( -
- ( - - - - )} - /> - -
- {PROJECT_PERMISSION_OBJECT?.[selectedSubject]?.actions?.map(({ label, value }) => ( - ( -
- - {label} - -
- )} - /> - ))} -
-
- - - - -
-
- ); -}; diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal.tsx new file mode 100644 index 000000000..441ab1939 --- /dev/null +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal.tsx @@ -0,0 +1,214 @@ +import { useState } from "react"; +import { Controller, useForm, useFormContext } from "react-hook-form"; +import { faCheck, faSearch, faXmark, faXmarkCircle } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { + Button, + EmptyState, + IconButton, + Input, + Modal, + ModalClose, + ModalContent, + Table, + TableContainer, + TBody, + Td, + Tooltip, + Tr +} from "@app/components/v2"; +import { ProjectPermissionSub } from "@app/context"; +import { useGetProjectTypeFromRoute } from "@app/hooks"; +import { ProjectType } from "@app/hooks/api/workspace/types"; + +import { + isConditionalSubjects, + PROJECT_PERMISSION_OBJECT, + ProjectTypePermissionSubjects, + TFormSchema +} from "./ProjectRoleModifySection.utils"; + +type Props = { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; +}; + +type ContentProps = { + onClose: () => void; +}; + +type TForm = { permissions: Record }; + +const Content = ({ onClose }: ContentProps) => { + const rootForm = useFormContext(); + const [search, setSearch] = useState(""); + const { + control, + handleSubmit, + formState: { isDirty }, + setValue, + reset + } = useForm({ + defaultValues: { + permissions: Object.fromEntries( + Object.values(ProjectPermissionSub).map((subject) => [subject, false]) + ) + } + }); + + const projectType = useGetProjectTypeFromRoute(); + + const filteredPolicies = Object.entries(PROJECT_PERMISSION_OBJECT) + .filter( + ([subject, { title }]) => + ProjectTypePermissionSubjects[projectType ?? ProjectType.SecretManager][ + subject as ProjectPermissionSub + ] && (search ? title.toLowerCase().includes(search.toLowerCase()) : true) + ) + .sort((a, b) => a[1].title.localeCompare(b[1].title)) + .map(([subject]) => subject); + + const onSubmit = () => + handleSubmit((form) => { + Object.entries(form.permissions).forEach(([subject, add]) => { + if (!add) return; + + const type = subject as ProjectPermissionSub; + + const rootPolicyValue = rootForm.getValues("permissions")?.[type]; + + if (rootPolicyValue && isConditionalSubjects(subject as ProjectPermissionSub)) { + rootForm.setValue( + `permissions.${type}`, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore-error akhilmhdh: this is because of ts collision with both + [...rootPolicyValue, {}], + { shouldDirty: true, shouldTouch: true } + ); + } else { + rootForm.setValue( + `permissions.${type}`, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore-error akhilmhdh: this is because of ts collision with both + [{}], + { + shouldDirty: true, + shouldTouch: true + } + ); + } + }); + onClose(); + })(); + + return ( + <> + setSearch(e.target.value)} + leftIcon={} + rightIcon={ + search ? ( + setSearch("")}> + + + ) : null + } + /> + +
+ Resource +
+ + + reset()} + variant="plain" + size="xs" + className={`text-mineshaft-400 ${!isDirty ? "pointer-events-none opacity-50" : ""} hover:text-red`} + isDisabled={!isDirty} + > + + + +
+
+ + + {filteredPolicies.map((subject) => ( + ( + onChange(!value)} + > + + + + )} + name={`permissions.${subject as ProjectPermissionSub}`} + /> + ))} + +
+ {PROJECT_PERMISSION_OBJECT[subject as ProjectPermissionSub].title} + + {value ? : null} +
+ {!filteredPolicies.length && ( + + )} +
+
+ + + + +
+ + ); +}; + +export const PolicySelectionModal = ({ isOpen, onOpenChange }: Props) => { + return ( + + + onOpenChange(false)} /> + + + ); +}; diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx index a43674d5d..bd5cdaa4e 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx @@ -913,7 +913,7 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { actions: [{ label: "Modify", value: "edit" }] }, [ProjectPermissionSub.Integrations]: { - title: "Integrations", + title: "Native Integrations", actions: [ { label: "Read", value: "read" }, { label: "Create", value: "create" }, diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx index 0331b87c3..8dc9a2fa1 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx @@ -8,19 +8,15 @@ import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; import { AccessTree } from "@app/components/permissions"; -import { - Button, - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger -} from "@app/components/v2"; +import { Button } from "@app/components/v2"; import { ProjectPermissionSub, useWorkspace } from "@app/context"; import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext"; import { evaluatePermissionsAbility } from "@app/helpers/permissions"; +import { usePopUp } from "@app/hooks"; import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api"; import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; import { ProjectType } from "@app/hooks/api/workspace/types"; +import { PolicySelectionModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal"; import { DynamicSecretPermissionConditions } from "./DynamicSecretPermissionConditions"; import { GeneralPermissionConditions } from "./GeneralPermissionConditions"; @@ -90,6 +86,8 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => { const { mutateAsync: updateRole } = useUpdateProjectRole(); + const { popUp, handlePopUpToggle } = usePopUp(["addPolicy"] as const); + const onSubmit = async (el: TFormSchema) => { try { if (!projectId || !role?.id) return; @@ -110,30 +108,6 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => { (role?.slug ?? "") as ProjectMembershipRole ); - const onNewPolicy = (selectedSubject: ProjectPermissionSub) => { - const rootPolicyValue = form.getValues(`permissions.${selectedSubject}`); - if (rootPolicyValue && isConditionalSubjects(selectedSubject)) { - form.setValue( - `permissions.${selectedSubject}`, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore-error akhilmhdh: this is because of ts collision with both - [...rootPolicyValue, {}], - { shouldDirty: true, shouldTouch: true } - ); - } else { - form.setValue( - `permissions.${selectedSubject}`, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore-error akhilmhdh: this is because of ts collision with both - [{}], - { - shouldDirty: true, - shouldTouch: true - } - ); - } - }; - const permissions = form.watch("permissions"); const formattedPermissions = useMemo( @@ -176,54 +150,25 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => { - - - - - - {Object.keys(PROJECT_PERMISSION_OBJECT) - .filter( - (subject) => - ProjectTypePermissionSubjects[currentWorkspace.type][ - subject as ProjectPermissionSub - ] - ) - .sort((a, b) => - PROJECT_PERMISSION_OBJECT[ - a as keyof typeof PROJECT_PERMISSION_OBJECT - ].title - .toLowerCase() - .localeCompare( - PROJECT_PERMISSION_OBJECT[ - b as keyof typeof PROJECT_PERMISSION_OBJECT - ].title.toLowerCase() - ) - ) - .map((subject) => ( - onNewPolicy(subject as ProjectPermissionSub)} - > - {PROJECT_PERMISSION_OBJECT[subject as ProjectPermissionSub].title} - - ))} - - + )} @@ -245,6 +190,10 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => { ))} + handlePopUpToggle("addPolicy", isOpen)} + />