From 1cb4dc9e844e6f919eeed34da182687322bb0673 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Thu, 25 Jul 2024 06:47:18 -0700 Subject: [PATCH 1/5] Start project role concept --- .../project/[id]/roles/[roleId]/index.tsx | 20 ++ .../ProjectRoleList/ProjectRoleList.tsx | 153 +++++++++++----- .../src/views/Project/RolePage/RolePage.tsx | 173 ++++++++++++++++++ frontend/src/views/Project/RolePage/index.tsx | 1 + 4 files changed, 301 insertions(+), 46 deletions(-) create mode 100644 frontend/src/pages/project/[id]/roles/[roleId]/index.tsx create mode 100644 frontend/src/views/Project/RolePage/RolePage.tsx create mode 100644 frontend/src/views/Project/RolePage/index.tsx diff --git a/frontend/src/pages/project/[id]/roles/[roleId]/index.tsx b/frontend/src/pages/project/[id]/roles/[roleId]/index.tsx new file mode 100644 index 000000000..13a6278bc --- /dev/null +++ b/frontend/src/pages/project/[id]/roles/[roleId]/index.tsx @@ -0,0 +1,20 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { useTranslation } from "react-i18next"; +import Head from "next/head"; + +import { RolePage } from "@app/views/Project/RolePage"; + +export default function Role() { + const { t } = useTranslation(); + return ( + <> + + {t("common.head-title", { title: t("settings.org.title") })} + + + + + ); +} + +Role.requireAuth = true; diff --git a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx index e19e61aec..4ea04c8f6 100644 --- a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx +++ b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx @@ -1,14 +1,17 @@ -import { useState } from "react"; -import { faEdit, faMagnifyingGlass, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { useRouter } from "next/router"; +import { faEllipsis, faPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; import { ProjectPermissionCan } from "@app/components/permissions"; import { Button, DeleteActionModal, - IconButton, - Input, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, Table, TableContainer, TableSkeleton, @@ -28,11 +31,11 @@ type Props = { }; export const ProjectRoleList = ({ onSelectRole }: Props) => { - const [searchRoles, setSearchRoles] = useState(""); - + const router = useRouter(); const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["deleteRole"] as const); const { currentWorkspace } = useWorkspace(); const projectSlug = currentWorkspace?.slug || ""; + const projectId = currentWorkspace?.id || ""; const { data: roles, isLoading: isRolesLoading } = useGetProjectRoles(projectSlug); @@ -54,21 +57,20 @@ export const ProjectRoleList = ({ onSelectRole }: Props) => { }; return ( -
-
-
- setSearchRoles(e.target.value)} - leftIcon={} - placeholder="Search roles..." - /> -
+
+
+

Project Roles

{(isAllowed) => (
-
- - - - - - - - - - {isRolesLoading && } - {roles?.map((role) => { - const { id, name, slug } = role; - const isNonMutatable = ["admin", "member", "viewer", "no-access"].includes(slug); + +
NameSlug -
+ + + + + + + + {isRolesLoading && } + {roles?.map((role) => { + const { id, name, slug } = role; + const isNonMutatable = ["admin", "member", "viewer", "no-access"].includes(slug); - return ( - - - - router.push(`/project/${projectId}/roles/${slug}`)} + > + + + + {/* - - ); - })} - -
NameSlug +
{name}{slug} + return ( +
{name}{slug} + + +
+ +
+
+ + + {(isAllowed) => ( + { + e.stopPropagation(); + + // TODO: remove/replace + onSelectRole(role.slug); + // router.push(`/project/${projectId}/roles/${id}`); + }} + disabled={!isAllowed} + > + {`${isNonMutatable ? "View" : "Edit"} Role`} + + )} + + {!isNonMutatable && ( + + {(isAllowed) => ( + { + e.stopPropagation(); + handlePopUpOpen("deleteRole", role); + }} + disabled={!isAllowed} + > + Delete Role + + )} + + )} + +
+
{ )}
-
-
-
+ */} + + ); + })} + + + handlePopUpClose("deleteRole")} onDeleteApproved={handleRoleDelete} diff --git a/frontend/src/views/Project/RolePage/RolePage.tsx b/frontend/src/views/Project/RolePage/RolePage.tsx new file mode 100644 index 000000000..4294a018b --- /dev/null +++ b/frontend/src/views/Project/RolePage/RolePage.tsx @@ -0,0 +1,173 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { useRouter } from "next/router"; +import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { twMerge } from "tailwind-merge"; + +import { createNotification } from "@app/components/notifications"; +// import { OrgPermissionCan } from "@app/components/permissions"; +// import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context"; +import { ProjectPermissionCan } from "@app/components/permissions"; +import { + Button, + DeleteActionModal, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + Tooltip +} from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +// import { withPermission } from "@app/hoc"; +import { withProjectPermission } from "@app/hoc"; +import { useDeleteOrgRole, useGetOrgRole, useGetProjectRoleBySlug } from "@app/hooks/api"; +import { usePopUp } from "@app/hooks/usePopUp"; + +// import { RoleDetailsSection, RoleModal, RolePermissionsSection } from "./components"; + +export const RolePage = withProjectPermission( + () => { + const router = useRouter(); + const roleSlug = router.query.roleSlug as string; + const { currentWorkspace } = useWorkspace(); + const projectId = currentWorkspace?.id || ""; + + console.log("RolePage currentWorkspace: ", currentWorkspace); + + // const { currentOrg } = useOrganization(); + // const orgId = currentOrg?.id || ""; + + const { data } = useGetProjectRoleBySlug(currentWorkspace?.slug ?? "", roleSlug as string); + + console.log("useGetProjectRoleBySlug data: ", data); + + // const { data } = useGetOrgRole(orgId, roleId); // TODO: get project role + const { mutateAsync: deleteOrgRole } = useDeleteOrgRole(); + + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + "role", + "deleteOrgRole" + ] as const); + + const onDeleteOrgRoleSubmit = async () => { + try { + // if (!orgId || !roleId) return; + // await deleteOrgRole({ + // orgId, + // id: roleId + // }); + // createNotification({ + // text: "Successfully deleted organization role", + // type: "success" + // }); + // handlePopUpClose("deleteOrgRole"); + // router.push(`/org/${orgId}/members`); + } catch (err) { + console.error(err); + const error = err as any; + const text = error?.response?.data?.message ?? "Failed to delete organization role"; + + createNotification({ + text, + type: "error" + }); + } + }; + + const isCustomRole = !["admin", "member", "no-access"].includes(data?.slug ?? ""); + + return ( +
+ {data && ( +
+ +
+

{data.name}

+ {isCustomRole && ( + + +
+ + + +
+
+ + + {(isAllowed) => ( + { + // TODO + // handlePopUpOpen("role", { + // roleId + // }); + }} + disabled={!isAllowed} + > + Edit Role + + )} + + + {(isAllowed) => ( + { + handlePopUpOpen("deleteOrgRole"); + }} + disabled={!isAllowed} + > + Delete Role + + )} + + +
+ )} +
+
+
+ RoleDetailsSection here + {/* */} +
+ RolePermissionsSection here + {/* */} +
+
+ )} + {/* */} + handlePopUpToggle("deleteOrgRole", isOpen)} + deleteKey="confirm" + onDeleteApproved={() => onDeleteOrgRoleSubmit()} + /> +
+ ); + }, + { action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Role } +); diff --git a/frontend/src/views/Project/RolePage/index.tsx b/frontend/src/views/Project/RolePage/index.tsx new file mode 100644 index 000000000..71e7114fc --- /dev/null +++ b/frontend/src/views/Project/RolePage/index.tsx @@ -0,0 +1 @@ +export { RolePage } from "./RolePage"; From bf00d16c800bef84b37392a8a2cad3ecfee91514 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Thu, 25 Jul 2024 14:45:02 -0700 Subject: [PATCH 2/5] Continue progress on project role page --- .../roles/{[roleId] => [roleSlug]}/index.tsx | 2 +- .../src/views/Project/RolePage/RolePage.tsx | 36 +-- .../components/RoleDetailsSection.tsx | 99 ++++++++ .../RolePermissionRow.tsx | 232 ++++++++++++++++++ .../RolePermissionSecretsRow.tsx | 110 +++++++++ .../RolePermissionsSection.tsx | 199 +++++++++++++++ .../RolePermissionsSection/index.tsx | 1 + .../Project/RolePage/components/index.tsx | 2 + 8 files changed, 657 insertions(+), 24 deletions(-) rename frontend/src/pages/project/[id]/roles/{[roleId] => [roleSlug]}/index.tsx (83%) create mode 100644 frontend/src/views/Project/RolePage/components/RoleDetailsSection.tsx create mode 100644 frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionRow.tsx create mode 100644 frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionSecretsRow.tsx create mode 100644 frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx create mode 100644 frontend/src/views/Project/RolePage/components/RolePermissionsSection/index.tsx create mode 100644 frontend/src/views/Project/RolePage/components/index.tsx diff --git a/frontend/src/pages/project/[id]/roles/[roleId]/index.tsx b/frontend/src/pages/project/[id]/roles/[roleSlug]/index.tsx similarity index 83% rename from frontend/src/pages/project/[id]/roles/[roleId]/index.tsx rename to frontend/src/pages/project/[id]/roles/[roleSlug]/index.tsx index 13a6278bc..17c854cee 100644 --- a/frontend/src/pages/project/[id]/roles/[roleId]/index.tsx +++ b/frontend/src/pages/project/[id]/roles/[roleSlug]/index.tsx @@ -9,7 +9,7 @@ export default function Role() { return ( <> - {t("common.head-title", { title: t("settings.org.title") })} + {t("common.head-title", { title: "Project Settings" })} diff --git a/frontend/src/views/Project/RolePage/RolePage.tsx b/frontend/src/views/Project/RolePage/RolePage.tsx index 4294a018b..235c28974 100644 --- a/frontend/src/views/Project/RolePage/RolePage.tsx +++ b/frontend/src/views/Project/RolePage/RolePage.tsx @@ -5,8 +5,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; -// import { OrgPermissionCan } from "@app/components/permissions"; -// import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context"; import { ProjectPermissionCan } from "@app/components/permissions"; import { Button, @@ -18,11 +16,12 @@ import { Tooltip } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; -// import { withPermission } from "@app/hoc"; import { withProjectPermission } from "@app/hoc"; -import { useDeleteOrgRole, useGetOrgRole, useGetProjectRoleBySlug } from "@app/hooks/api"; +import { useGetProjectRoleBySlug } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; +import { RoleDetailsSection, RolePermissionsSection } from "./components"; + // import { RoleDetailsSection, RoleModal, RolePermissionsSection } from "./components"; export const RolePage = withProjectPermission( @@ -32,24 +31,17 @@ export const RolePage = withProjectPermission( const { currentWorkspace } = useWorkspace(); const projectId = currentWorkspace?.id || ""; - console.log("RolePage currentWorkspace: ", currentWorkspace); - - // const { currentOrg } = useOrganization(); - // const orgId = currentOrg?.id || ""; - const { data } = useGetProjectRoleBySlug(currentWorkspace?.slug ?? "", roleSlug as string); - console.log("useGetProjectRoleBySlug data: ", data); - // const { data } = useGetOrgRole(orgId, roleId); // TODO: get project role - const { mutateAsync: deleteOrgRole } = useDeleteOrgRole(); + // const { mutateAsync: deleteOrgRole } = useDeleteOrgRole(); const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "role", - "deleteOrgRole" + "deleteRole" ] as const); - const onDeleteOrgRoleSubmit = async () => { + const onDeleteRoleSubmit = async () => { try { // if (!orgId || !roleId) return; // await deleteOrgRole({ @@ -65,7 +57,7 @@ export const RolePage = withProjectPermission( } catch (err) { console.error(err); const error = err as any; - const text = error?.response?.data?.message ?? "Failed to delete organization role"; + const text = error?.response?.data?.message ?? "Failed to delete project role"; createNotification({ text, @@ -136,7 +128,7 @@ export const RolePage = withProjectPermission( : "pointer-events-none cursor-not-allowed opacity-50" )} onClick={async () => { - handlePopUpOpen("deleteOrgRole"); + handlePopUpOpen("deleteRole"); }} disabled={!isAllowed} > @@ -150,21 +142,19 @@ export const RolePage = withProjectPermission(
- RoleDetailsSection here - {/* */} +
- RolePermissionsSection here - {/* */} +
)} {/* */} handlePopUpToggle("deleteOrgRole", isOpen)} + onChange={(isOpen) => handlePopUpToggle("deleteRole", isOpen)} deleteKey="confirm" - onDeleteApproved={() => onDeleteOrgRoleSubmit()} + onDeleteApproved={() => onDeleteRoleSubmit()} />
); diff --git a/frontend/src/views/Project/RolePage/components/RoleDetailsSection.tsx b/frontend/src/views/Project/RolePage/components/RoleDetailsSection.tsx new file mode 100644 index 000000000..1fb9e7767 --- /dev/null +++ b/frontend/src/views/Project/RolePage/components/RoleDetailsSection.tsx @@ -0,0 +1,99 @@ +import { faCheck, faCopy, faPencil } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { ProjectPermissionCan } from "@app/components/permissions"; +import { IconButton, Tooltip } from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { useTimedReset } from "@app/hooks"; +import { useGetProjectRoleBySlug } from "@app/hooks/api"; +// import { UsePopUpState } from "@app/hooks/usePopUp"; + +type Props = { + roleSlug: string; + // handlePopUpOpen: (popUpName: keyof UsePopUpState<["role"]>, data?: {}) => void; +}; + +export const RoleDetailsSection = ({ + roleSlug +}: // handlePopUpOpen +Props) => { + const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset({ + initialState: "Copy ID to clipboard" + }); + + const { currentWorkspace } = useWorkspace(); + const { data } = useGetProjectRoleBySlug(currentWorkspace?.slug ?? "", roleSlug as string); + + const isCustomRole = !["admin", "member", "viewer", "no-access"].includes(data?.slug ?? ""); + + return data ? ( +
+
+

Details

+ {isCustomRole && ( + + {(isAllowed) => { + return ( + + { + // TODO + // handlePopUpOpen("role", { + // roleId + // }) + }} + > + + + + ); + }} + + )} +
+
+
+

Role ID

+
+

{data.id}

+
+ + { + navigator.clipboard.writeText(data.id); + setCopyTextId("Copied"); + }} + > + + + +
+
+
+
+

Name

+

{data.name}

+
+
+

Slug

+

{data.slug}

+
+
+

Description

+

+ {data.description?.length ? data.description : "-"} +

+
+
+
+ ) : ( +
+ ); +}; diff --git a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionRow.tsx b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionRow.tsx new file mode 100644 index 000000000..163f3a839 --- /dev/null +++ b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionRow.tsx @@ -0,0 +1,232 @@ +import { useEffect, useMemo } from "react"; +import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form"; +import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { createNotification } from "@app/components/notifications"; +import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2"; +import { useToggle } from "@app/hooks"; +import { TFormSchema } from "@app/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils"; + +const GENERAL_PERMISSIONS = [ + { action: "read", label: "View" }, + { action: "create", label: "Create" }, + { action: "edit", label: "Modify" }, + { action: "delete", label: "Remove" } +] as const; + +const WORKSPACE_PERMISSIONS = [ + { action: "edit", label: "Update project details" }, + { action: "delete", label: "Delete projects" } +] as const; + +const MEMBERS_PERMISSIONS = [ + { action: "read", label: "View all members" }, + { action: "create", label: "Invite members" }, + { action: "edit", label: "Edit members" }, + { action: "delete", label: "Remove members" } +] as const; + +const SECRET_ROLLBACK_PERMISSIONS = [ + { action: "create", label: "Perform Rollback" }, + { action: "read", label: "View" } +] as const; + +const getPermissionList = (option: Props["formName"]) => { + switch (option) { + case "workspace": + return WORKSPACE_PERMISSIONS; + case "member": + return MEMBERS_PERMISSIONS; + case "secret-rollback": + return SECRET_ROLLBACK_PERMISSIONS; + default: + return GENERAL_PERMISSIONS; + } +}; + +type PermissionName = + | `permissions.workspace.${"edit" | "delete"}` + | `permissions.secret-rollback.${"create" | "read"}` + | `permissions.${Exclude< + keyof NonNullable, + "workspace" | "secret-rollback" | "secrets" + >}.${"read" | "create" | "edit" | "delete"}`; + +type Props = { + isEditable: boolean; + title: string; + formName: keyof Omit, "secrets">; + setValue: UseFormSetValue; + control: Control; +}; + +enum Permission { + NoAccess = "no-access", + ReadOnly = "read-only", + FullAccess = "full-acess", + Custom = "custom" +} + +export const RolePermissionRow = ({ isEditable, title, formName, control, setValue }: Props) => { + const [isRowExpanded, setIsRowExpanded] = useToggle(); + const [isCustom, setIsCustom] = useToggle(); + + const rule = useWatch({ + control, + name: `permissions.${formName}` + }); + + const selectedPermissionCategory = useMemo(() => { + // TODO: re-check + const actions = Object.keys(rule || {}) as Array; + + switch (formName) { + case "secret-rollback": + case "workspace": { + const totalActions = WORKSPACE_PERMISSIONS.length; + const score = actions + .map((key) => (rule?.[key] ? 1 : 0)) + .reduce((a, b) => a + b, 0 as number); + if (isCustom) return Permission.Custom; + if (score === 0) return Permission.NoAccess; + if (score === totalActions) return Permission.FullAccess; + + return Permission.Custom; // TODO: update + } + default: { + const totalActions = GENERAL_PERMISSIONS.length; + const score = actions + .map((key) => (rule?.[key] ? 1 : 0)) + .reduce((a, b) => a + b, 0 as number); + if (isCustom) return Permission.Custom; + if (score === 0) return Permission.NoAccess; + if (score === totalActions) return Permission.FullAccess; + if (rule && "read" in rule) { + if (score === 1 && rule?.read) return Permission.ReadOnly; + } + + return Permission.Custom; + } + } + }, [rule, isCustom]); + + useEffect(() => { + if (selectedPermissionCategory === Permission.Custom) setIsCustom.on(); + else setIsCustom.off(); + }, [selectedPermissionCategory]); + + useEffect(() => { + const isRowCustom = selectedPermissionCategory === Permission.Custom; + if (isRowCustom) { + setIsRowExpanded.on(); + } + }, []); + + const handlePermissionChange = (val: Permission) => { + if (val === Permission.Custom) { + setIsRowExpanded.on(); + setIsCustom.on(); + return; + } + setIsCustom.off(); + + switch (val) { + case Permission.NoAccess: + setValue( + `permissions.${formName}`, + { read: false, edit: false, create: false, delete: false }, + { shouldDirty: true } + ); + break; + case Permission.FullAccess: + setValue( + `permissions.${formName}`, + { read: true, edit: true, create: true, delete: true }, + { shouldDirty: true } + ); + break; + case Permission.ReadOnly: + setValue( + `permissions.${formName}`, + { read: true, edit: false, create: false, delete: false }, + { shouldDirty: true } + ); + break; + default: + setValue( + `permissions.${formName}`, + { read: false, edit: false, create: false, delete: false }, + { shouldDirty: true } + ); + break; + } + }; + + return ( + <> + setIsRowExpanded.toggle()} + > + + + + {title} + + + + + {isRowExpanded && ( + + +
+ {getPermissionList(formName).map(({ action, label }) => { + const permissionName = `permissions.${formName}.${action}` as PermissionName; + return ( + ( + { + if (!isEditable) { + createNotification({ + type: "error", + text: "Failed to update default role" + }); + return; + } + field.onChange(e); + }} + id={permissionName} + > + {label} + + )} + /> + ); + })} +
+ + + )} + + ); +}; diff --git a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionSecretsRow.tsx b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionSecretsRow.tsx new file mode 100644 index 000000000..d36be71ae --- /dev/null +++ b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionSecretsRow.tsx @@ -0,0 +1,110 @@ +import { useMemo } from "react"; +import { + Control, + // Controller, + UseFormGetValues, + UseFormSetValue, + useWatch +} from "react-hook-form"; +import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { Select, SelectItem,Td, Tr } from "@app/components/v2"; +import { useToggle } from "@app/hooks"; +import { TFormSchema } from "@app/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils"; + +type Props = { + title: string; + formName: "secrets"; + isEditable: boolean; + setValue: UseFormSetValue; + getValue: UseFormGetValues; + control: Control; +}; + +enum Permission { + NoAccess = "no-access", + ReadOnly = "read-only", + FullAccess = "full-acess", + Custom = "custom" +} + +export const RowPermissionSecretsRow = ({ + title, + formName, + isEditable, + setValue, + getValue, + control +}: Props) => { + const [isRowExpanded, setIsRowExpanded] = useToggle(); + // const [isCustom, setIsCustom] = useToggle(); + + const allRule = useWatch({ control, name: `permissions.${formName}.all` }); + + const selectedPermissionCategory = useMemo(() => { + const { read, delete: del, edit, create } = allRule || {}; + if (read && del && edit && create) return Permission.FullAccess; + if (read) return Permission.ReadOnly; + return Permission.NoAccess; + }, [allRule]); + + const handlePermissionChange = (val: Permission) => { + if (!val) return; + switch (val) { + case Permission.NoAccess: { + const permissions = getValue("permissions"); + if (permissions) delete permissions[formName]; + setValue("permissions", permissions, { shouldDirty: true }); + break; + } + case Permission.FullAccess: + setValue( + `permissions.${formName}`, + { all: { read: true, edit: true, create: true, delete: true } }, + { shouldDirty: true } + ); + break; + case Permission.ReadOnly: + setValue( + `permissions.${formName}`, + { all: { read: true, edit: false, create: false, delete: false } }, + { shouldDirty: true } + ); + break; + default: + setValue( + `permissions.${formName}`, + { custom: { read: false, edit: false, create: false, delete: false } }, + { shouldDirty: true } + ); + break; + } + }; + + return ( + setIsRowExpanded.toggle()} + > + + + + {title} + + + + + ); +}; diff --git a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx new file mode 100644 index 000000000..3097520bc --- /dev/null +++ b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx @@ -0,0 +1,199 @@ +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; + +import { createNotification } from "@app/components/notifications"; +import { + Button, + Table, + TableContainer, + TBody, + Th, + THead, + Tr + // Td +} from "@app/components/v2"; +import { ProjectPermissionSub,useWorkspace } from "@app/context"; +import { + // useUpdateOrgRole, + useGetProjectRoleBySlug +} from "@app/hooks/api"; +import { + formSchema, + rolePermission2Form, + TFormSchema} from "@app/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils"; + +import { RolePermissionRow } from "./RolePermissionRow"; + +const SINGLE_PERMISSION_LIST = [ + { + title: "Project", + formName: "workspace" + }, + { + title: "Integrations", + formName: "integrations" + }, + { + title: "Secret Protect policy", + formName: ProjectPermissionSub.SecretApproval + }, + { + title: "Roles", + formName: "role" + }, + { + title: "User management", + formName: "member" + }, + { + title: "Group management", + formName: "groups" + }, + { + title: "Machine identity management", + formName: "identity" + }, + { + title: "Webhooks", + formName: "webhooks" + }, + { + title: "Service Tokens", + formName: "service-tokens" + }, + { + title: "Settings", + formName: "settings" + }, + { + title: "Environments", + formName: "environments" + }, + { + title: "Tags", + formName: "tags" + }, + { + title: "Audit Logs", + formName: "audit-logs" + }, + { + title: "IP Allowlist", + formName: "ip-allowlist" + }, + { + title: "Certificate Authorities", + formName: "certificate-authorities" + }, + { + title: "Certificates", + formName: "certificates" + }, + { + title: "Secret Rollback", + formName: "secret-rollback" + } +] as const; + +type Props = { + roleSlug: string; +}; + +// TODO: fill table + +export const RolePermissionsSection = ({ roleSlug }: Props) => { + const { currentWorkspace } = useWorkspace(); + const { data: role } = useGetProjectRoleBySlug(currentWorkspace?.slug ?? "", roleSlug as string); + + console.log("RolePermissionsSection role data: ", role); + + const { + setValue, + control, + handleSubmit, + formState: { isDirty, isSubmitting }, + reset + } = useForm({ + defaultValues: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : {}, + resolver: zodResolver(formSchema) + }); + + // const { mutateAsync: updateRole } = useUpdateOrgRole(); + + const onSubmit = async () => { + try { + // await updateRole({ + // orgId, + // id: roleId, + // ...el, + // permissions: formRolePermission2API(el.permissions) + // }); + createNotification({ type: "success", text: "Successfully updated role" }); + } catch (err) { + console.log(err); + createNotification({ type: "error", text: "Failed to update role" }); + } + }; + + const isCustomRole = !["admin", "member", "viewer", "no-access"].includes(role?.slug ?? ""); + + return ( +
+
+

Permissions

+ {isCustomRole && ( +
+ + +
+ )} +
+
+ + + + + + + + + + {SINGLE_PERMISSION_LIST.map((permission) => { + console.log("permission: ", permission); + return ( + + ); + })} + +
+ ResourcePermission
+
+
+
+ ); +}; diff --git a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/index.tsx b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/index.tsx new file mode 100644 index 000000000..104e2144e --- /dev/null +++ b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/index.tsx @@ -0,0 +1 @@ +export { RolePermissionsSection } from "./RolePermissionsSection"; diff --git a/frontend/src/views/Project/RolePage/components/index.tsx b/frontend/src/views/Project/RolePage/components/index.tsx new file mode 100644 index 000000000..dadfb93af --- /dev/null +++ b/frontend/src/views/Project/RolePage/components/index.tsx @@ -0,0 +1,2 @@ +export { RoleDetailsSection } from "./RoleDetailsSection"; +export { RolePermissionsSection } from "./RolePermissionsSection"; From 5ad406188171b364a7f778f56e361d6b9baafa9c Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Fri, 26 Jul 2024 06:43:09 -0700 Subject: [PATCH 3/5] Continue project role page --- .../ProjectRoleModifySection.tsx | 3 +- .../src/views/Project/RolePage/RolePage.tsx | 8 +- .../components/RoleDetailsSection.tsx | 16 +- .../Project/RolePage/components/RoleModal.tsx | 231 ++++++++++++++++++ .../RolePermissionSecretsRow.tsx | 177 ++++++++++++-- .../RolePermissionsSection.tsx | 59 +++-- .../Project/RolePage/components/index.tsx | 1 + 7 files changed, 429 insertions(+), 66 deletions(-) create mode 100644 frontend/src/views/Project/RolePage/components/RoleModal.tsx diff --git a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.tsx b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.tsx index 2140887de..c386ebc11 100644 --- a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.tsx +++ b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.tsx @@ -14,7 +14,8 @@ import { faShield, faTags, faUser, - faUsers} from "@fortawesome/free-solid-svg-icons"; + faUsers +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; diff --git a/frontend/src/views/Project/RolePage/RolePage.tsx b/frontend/src/views/Project/RolePage/RolePage.tsx index 235c28974..19ec657ec 100644 --- a/frontend/src/views/Project/RolePage/RolePage.tsx +++ b/frontend/src/views/Project/RolePage/RolePage.tsx @@ -20,9 +20,7 @@ import { withProjectPermission } from "@app/hoc"; import { useGetProjectRoleBySlug } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; -import { RoleDetailsSection, RolePermissionsSection } from "./components"; - -// import { RoleDetailsSection, RoleModal, RolePermissionsSection } from "./components"; +import { RoleDetailsSection, RoleModal,RolePermissionsSection } from "./components"; export const RolePage = withProjectPermission( () => { @@ -66,7 +64,7 @@ export const RolePage = withProjectPermission( } }; - const isCustomRole = !["admin", "member", "no-access"].includes(data?.slug ?? ""); + const isCustomRole = !["admin", "member", "viewer", "no-access"].includes(data?.slug ?? ""); return (
@@ -148,7 +146,7 @@ export const RolePage = withProjectPermission(
)} - {/* */} + , data?: {}) => void; + handlePopUpOpen: (popUpName: keyof UsePopUpState<["role"]>, data?: {}) => void; }; -export const RoleDetailsSection = ({ - roleSlug -}: // handlePopUpOpen -Props) => { +export const RoleDetailsSection = ({ roleSlug, handlePopUpOpen }: Props) => { const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset({ initialState: "Copy ID to clipboard" }); @@ -41,10 +38,9 @@ Props) => { variant="plain" className="group relative" onClick={() => { - // TODO - // handlePopUpOpen("role", { - // roleId - // }) + handlePopUpOpen("role", { + roleSlug + }); }} > diff --git a/frontend/src/views/Project/RolePage/components/RoleModal.tsx b/frontend/src/views/Project/RolePage/components/RoleModal.tsx new file mode 100644 index 000000000..463f36a78 --- /dev/null +++ b/frontend/src/views/Project/RolePage/components/RoleModal.tsx @@ -0,0 +1,231 @@ +import { useEffect } from "react"; +import { Controller, useForm } from "react-hook-form"; +// import { useRouter } from "next/router"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2"; +import { + // useOrganization, + useWorkspace +} from "@app/context"; +import { + // useCreateProjectRole, + // useUpdateProjectRole, + useGetProjectRoleBySlug + // useCreateOrgRole, + // useGetOrgRole, + // useUpdateOrgRole +} from "@app/hooks/api"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +const schema = z + .object({ + name: z.string(), + description: z.string(), + slug: z.string() + }) + .required(); + +export type FormData = z.infer; + +type Props = { + popUp: UsePopUpState<["role"]>; + handlePopUpToggle: (popUpName: keyof UsePopUpState<["role"]>, state?: boolean) => void; +}; + +export const RoleModal = ({ popUp, handlePopUpToggle }: Props) => { + // const router = useRouter(); + + const popupData = popUp?.role?.data as { + roleSlug: string; + }; + + const { currentWorkspace } = useWorkspace(); + const projectSlug = currentWorkspace?.slug || ""; + + const { data: role } = useGetProjectRoleBySlug(projectSlug, popupData?.roleSlug ?? ""); + + console.log("Project RoleModal role: ", role); + + // const { mutateAsync: createProjectRole } = useCreateProjectRole(); + // const { mutateAsync: updateProjectRole } = useUpdateProjectRole(); + + const { + control, + handleSubmit, + reset, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { + name: "", + description: "" + } + }); + + useEffect(() => { + if (role) { + reset({ + name: role.name, + description: role.description, + slug: role.slug + }); + } else { + reset({ + name: "", + description: "", + slug: "" + }); + } + }, [role]); + + const onFormSubmit = async ({ name, description, slug }: FormData) => { + try { + console.log("onFormSubmit args: ", { + name, + description, + slug + }); + + if (!projectSlug || !role?.id) return; + + if (role) { + // update + console.log("update"); + // const u1 = await updateProjectRole({ + // id: role.id, + // projectSlug, + // name, + // description, + // slug + // }); + // console.log("u1: ", u1); + } else { + // create + console.log("create"); + } + + // if (!orgId) return; + + // if (role) { + // // update + + // await updateOrgRole({ + // orgId, + // id: role.id, + // name, + // description, + // slug + // }); + + // handlePopUpToggle("role", false); + // } else { + // // create + + // const newRole = await createOrgRole({ + // orgId, + // name, + // description, + // slug, + // permissions: [] + // }); + + // handlePopUpToggle("role", false); + // router.push(`/org/${orgId}/roles/${newRole.id}`); + // } + + createNotification({ + text: `Successfully ${popUp?.role?.data ? "updated" : "created"} role`, + type: "success" + }); + + reset(); + } catch (err) { + console.error(err); + const error = err as any; + const text = + error?.response?.data?.message ?? + `Failed to ${popUp?.role?.data ? "update" : "create"} role`; + + createNotification({ + text, + type: "error" + }); + } + }; + + return ( + { + handlePopUpToggle("role", isOpen); + reset(); + }} + > + +
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> +
+ + +
+ +
+
+ ); +}; diff --git a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionSecretsRow.tsx b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionSecretsRow.tsx index d36be71ae..786f58cf1 100644 --- a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionSecretsRow.tsx +++ b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionSecretsRow.tsx @@ -1,16 +1,24 @@ import { useMemo } from "react"; -import { - Control, - // Controller, - UseFormGetValues, - UseFormSetValue, - useWatch -} from "react-hook-form"; -import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons"; +import { Control, Controller, UseFormGetValues, UseFormSetValue, useWatch } from "react-hook-form"; +import { faChevronDown } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Select, SelectItem,Td, Tr } from "@app/components/v2"; -import { useToggle } from "@app/hooks"; +import GlobPatternExamples from "@app/components/basic/popups/GlobPatternExamples"; +import { + Checkbox, + FormControl, + Input, + Select, + SelectItem, + Table, + TableContainer, + TBody, + Td, + Th, + THead, + Tr +} from "@app/components/v2"; +import { useWorkspace } from "@app/context"; import { TFormSchema } from "@app/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils"; type Props = { @@ -37,8 +45,14 @@ export const RowPermissionSecretsRow = ({ getValue, control }: Props) => { - const [isRowExpanded, setIsRowExpanded] = useToggle(); - // const [isCustom, setIsCustom] = useToggle(); + const { currentWorkspace } = useWorkspace(); + const environments = currentWorkspace?.environments || []; + + const customRule = useWatch({ + control, + name: `permissions.${formName}.custom` + }); + const isCustom = Boolean(customRule); const allRule = useWatch({ control, name: `permissions.${formName}.all` }); @@ -83,17 +97,13 @@ export const RowPermissionSecretsRow = ({ }; return ( - setIsRowExpanded.toggle()} - > - - - + <> + + {isCustom && } {title} + + )} + /> + + + ( +
+ +
+ )} + /> + + + ( +
+ +
+ )} + /> + + + ( +
+ +
+ )} + /> + + + ( +
+ +
+ )} + /> + + + ))} + + + + + + + )} + ); }; diff --git a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx index 3097520bc..6a03503ea 100644 --- a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx +++ b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx @@ -2,27 +2,17 @@ import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { createNotification } from "@app/components/notifications"; +import { Button, Table, TableContainer, TBody, Th, THead, Tr } from "@app/components/v2"; +import { ProjectPermissionSub, useWorkspace } from "@app/context"; +import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api"; import { - Button, - Table, - TableContainer, - TBody, - Th, - THead, - Tr - // Td -} from "@app/components/v2"; -import { ProjectPermissionSub,useWorkspace } from "@app/context"; -import { - // useUpdateOrgRole, - useGetProjectRoleBySlug -} from "@app/hooks/api"; -import { - formSchema, +formRolePermission2API, formSchema, rolePermission2Form, - TFormSchema} from "@app/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils"; + TFormSchema + } from "@app/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils"; import { RolePermissionRow } from "./RolePermissionRow"; +import { RowPermissionSecretsRow } from "./RolePermissionSecretsRow"; const SINGLE_PERMISSION_LIST = [ { @@ -99,16 +89,15 @@ type Props = { roleSlug: string; }; -// TODO: fill table - +// note: isEditable should also depend on if user has edit role permission export const RolePermissionsSection = ({ roleSlug }: Props) => { const { currentWorkspace } = useWorkspace(); + const projectSlug = currentWorkspace?.slug || ""; const { data: role } = useGetProjectRoleBySlug(currentWorkspace?.slug ?? "", roleSlug as string); - console.log("RolePermissionsSection role data: ", role); - const { setValue, + getValues, control, handleSubmit, formState: { isDirty, isSubmitting }, @@ -118,16 +107,19 @@ export const RolePermissionsSection = ({ roleSlug }: Props) => { resolver: zodResolver(formSchema) }); - // const { mutateAsync: updateRole } = useUpdateOrgRole(); + const { mutateAsync: updateRole } = useUpdateProjectRole(); - const onSubmit = async () => { + const onSubmit = async (el: TFormSchema) => { try { - // await updateRole({ - // orgId, - // id: roleId, - // ...el, - // permissions: formRolePermission2API(el.permissions) - // }); + if (!projectSlug || !role?.id) return; + + await updateRole({ + id: role?.id as string, + projectSlug, + ...el, + permissions: formRolePermission2API(el.permissions) + }); + createNotification({ type: "success", text: "Successfully updated role" }); } catch (err) { console.log(err); @@ -177,8 +169,15 @@ export const RolePermissionsSection = ({ roleSlug }: Props) => { + {SINGLE_PERMISSION_LIST.map((permission) => { - console.log("permission: ", permission); return ( Date: Fri, 26 Jul 2024 11:00:47 -0700 Subject: [PATCH 4/5] Finish project role page --- .../src/ee/routes/v1/project-role-router.ts | 4 +- .../project-role/project-role-service.ts | 9 +- frontend/src/hooks/api/roles/mutation.tsx | 32 +++++-- .../ProjectRoleListTab/ProjectRoleListTab.tsx | 21 +---- .../ProjectRoleList/ProjectRoleList.tsx | 64 ++----------- .../src/views/Project/RolePage/RolePage.tsx | 50 +++++----- .../Project/RolePage/components/RoleModal.tsx | 91 ++++++------------- .../RolePermissionRow.tsx | 13 --- .../RolePermissionsSection.tsx | 6 +- 9 files changed, 97 insertions(+), 193 deletions(-) diff --git a/backend/src/ee/routes/v1/project-role-router.ts b/backend/src/ee/routes/v1/project-role-router.ts index 69038a057..3ada2e50c 100644 --- a/backend/src/ee/routes/v1/project-role-router.ts +++ b/backend/src/ee/routes/v1/project-role-router.ts @@ -101,7 +101,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { message: "Slug must be a valid" }), name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name), - permissions: ProjectPermissionSchema.array().describe(PROJECT_ROLE.UPDATE.permissions) + permissions: ProjectPermissionSchema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional() }), response: { 200: z.object({ @@ -120,7 +120,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { roleId: req.params.roleId, data: { ...req.body, - permissions: JSON.stringify(packRules(req.body.permissions)) + permissions: req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined } }); return { role }; diff --git a/backend/src/services/project-role/project-role-service.ts b/backend/src/services/project-role/project-role-service.ts index ffd446fad..8c71a6e02 100644 --- a/backend/src/services/project-role/project-role-service.ts +++ b/backend/src/services/project-role/project-role-service.ts @@ -162,12 +162,19 @@ export const projectRoleServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Role); + if (data?.slug) { const existingRole = await projectRoleDAL.findOne({ slug: data.slug, projectId }); if (existingRole && existingRole.id !== roleId) throw new BadRequestError({ name: "Update Role", message: "Duplicate role" }); } - const [updatedRole] = await projectRoleDAL.update({ id: roleId, projectId }, data); + const [updatedRole] = await projectRoleDAL.update( + { id: roleId, projectId }, + { + ...data, + permissions: data.permissions ? data.permissions : undefined + } + ); if (!updatedRole) throw new BadRequestError({ message: "Role not found", name: "Update role" }); return { ...updatedRole, permissions: unpackPermissions(updatedRole.permissions) }; }; diff --git a/frontend/src/hooks/api/roles/mutation.tsx b/frontend/src/hooks/api/roles/mutation.tsx index 7f831cd4c..3f782171e 100644 --- a/frontend/src/hooks/api/roles/mutation.tsx +++ b/frontend/src/hooks/api/roles/mutation.tsx @@ -10,6 +10,7 @@ import { TDeleteOrgRoleDTO, TDeleteProjectRoleDTO, TOrgRole, + TProjectRole, TUpdateOrgRoleDTO, TUpdateProjectRoleDTO } from "./types"; @@ -17,9 +18,13 @@ import { export const useCreateProjectRole = () => { const queryClient = useQueryClient(); - return useMutation({ - mutationFn: ({ projectSlug, ...dto }: TCreateProjectRoleDTO) => - apiRequest.post(`/api/v1/workspace/${projectSlug}/roles`, dto), + return useMutation({ + mutationFn: async ({ projectSlug, ...dto }: TCreateProjectRoleDTO) => { + const { + data: { role } + } = await apiRequest.post(`/api/v1/workspace/${projectSlug}/roles`, dto); + return role; + }, onSuccess: (_, { projectSlug }) => { queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectSlug)); } @@ -29,9 +34,13 @@ export const useCreateProjectRole = () => { export const useUpdateProjectRole = () => { const queryClient = useQueryClient(); - return useMutation({ - mutationFn: ({ id, projectSlug, ...dto }: TUpdateProjectRoleDTO) => - apiRequest.patch(`/api/v1/workspace/${projectSlug}/roles/${id}`, dto), + return useMutation({ + mutationFn: async ({ id, projectSlug, ...dto }: TUpdateProjectRoleDTO) => { + const { + data: { role } + } = await apiRequest.patch(`/api/v1/workspace/${projectSlug}/roles/${id}`, dto); + return role; + }, onSuccess: (_, { projectSlug }) => { queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectSlug)); } @@ -40,10 +49,13 @@ export const useUpdateProjectRole = () => { export const useDeleteProjectRole = () => { const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: ({ projectSlug, id }: TDeleteProjectRoleDTO) => - apiRequest.delete(`/api/v1/workspace/${projectSlug}/roles/${id}`), + return useMutation({ + mutationFn: async ({ projectSlug, id }: TDeleteProjectRoleDTO) => { + const { + data: { role } + } = await apiRequest.delete(`/api/v1/workspace/${projectSlug}/roles/${id}`); + return role; + }, onSuccess: (_, { projectSlug }) => { queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectSlug)); } diff --git a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/ProjectRoleListTab.tsx b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/ProjectRoleListTab.tsx index 5eb443fb6..4a5fc37ac 100644 --- a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/ProjectRoleListTab.tsx +++ b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/ProjectRoleListTab.tsx @@ -2,29 +2,12 @@ import { motion } from "framer-motion"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { withProjectPermission } from "@app/hoc"; -import { usePopUp } from "@app/hooks"; import { ProjectRoleList } from "./components/ProjectRoleList"; -import { ProjectRoleModifySection } from "./components/ProjectRoleModifySection"; export const ProjectRoleListTab = withProjectPermission( () => { - const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["editRole"] as const); - - return popUp.editRole.isOpen ? ( - - handlePopUpClose("editRole")} - /> - - ) : ( + return ( - handlePopUpOpen("editRole", slug)} /> + ); }, diff --git a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx index 4ea04c8f6..59a13c67f 100644 --- a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx +++ b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx @@ -25,14 +25,14 @@ import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@a import { usePopUp } from "@app/hooks"; import { useDeleteProjectRole, useGetProjectRoles } from "@app/hooks/api"; import { TProjectRole } from "@app/hooks/api/roles/types"; +import { RoleModal } from "@app/views/Project/RolePage/components"; -type Props = { - onSelectRole: (slug?: string) => void; -}; - -export const ProjectRoleList = ({ onSelectRole }: Props) => { +export const ProjectRoleList = () => { const router = useRouter(); - const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["deleteRole"] as const); + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + "role", + "deleteRole" + ] as const); const { currentWorkspace } = useWorkspace(); const projectSlug = currentWorkspace?.slug || ""; const projectId = currentWorkspace?.id || ""; @@ -66,11 +66,7 @@ export const ProjectRoleList = ({ onSelectRole }: Props) => { colorSchema="primary" type="submit" leftIcon={} - onClick={() => onSelectRole()} - // onClick={() => { - // TODO - // handlePopUpOpen("role"); - // }} + onClick={() => handlePopUpOpen("role")} isDisabled={!isAllowed} > Add Role @@ -120,10 +116,7 @@ export const ProjectRoleList = ({ onSelectRole }: Props) => { )} onClick={(e) => { e.stopPropagation(); - - // TODO: remove/replace - onSelectRole(role.slug); - // router.push(`/project/${projectId}/roles/${id}`); + router.push(`/project/${projectId}/roles/${slug}`); }} disabled={!isAllowed} > @@ -157,52 +150,13 @@ export const ProjectRoleList = ({ onSelectRole }: Props) => { - {/* -
- - {(isAllowed) => ( - onSelectRole(role.slug)} - variant="plain" - > - - - )} - - - {(isAllowed) => ( - handlePopUpOpen("deleteRole", role)} - variant="plain" - isDisabled={isNonMutatable || !isAllowed} - > - - - )} - -
- */} ); })} + { @@ -31,8 +31,7 @@ export const RolePage = withProjectPermission( const { data } = useGetProjectRoleBySlug(currentWorkspace?.slug ?? "", roleSlug as string); - // const { data } = useGetOrgRole(orgId, roleId); // TODO: get project role - // const { mutateAsync: deleteOrgRole } = useDeleteOrgRole(); + const { mutateAsync: deleteProjectRole } = useDeleteProjectRole(); const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "role", @@ -41,17 +40,19 @@ export const RolePage = withProjectPermission( const onDeleteRoleSubmit = async () => { try { - // if (!orgId || !roleId) return; - // await deleteOrgRole({ - // orgId, - // id: roleId - // }); - // createNotification({ - // text: "Successfully deleted organization role", - // type: "success" - // }); - // handlePopUpClose("deleteOrgRole"); - // router.push(`/org/${orgId}/members`); + if (!currentWorkspace?.slug || !data?.id) return; + + await deleteProjectRole({ + projectSlug: currentWorkspace.slug, + id: data.id + }); + + createNotification({ + text: "Successfully deleted project role", + type: "success" + }); + handlePopUpClose("deleteRole"); + router.push(`/project/${projectId}/members`); } catch (err) { console.error(err); const error = err as any; @@ -74,9 +75,7 @@ export const RolePage = withProjectPermission( variant="link" type="submit" leftIcon={} - onClick={() => { - router.push(`/project/${projectId}/members`); - }} + onClick={() => router.push(`/project/${projectId}/members`)} className="mb-4" > Roles @@ -102,12 +101,11 @@ export const RolePage = withProjectPermission( className={twMerge( !isAllowed && "pointer-events-none cursor-not-allowed opacity-50" )} - onClick={async () => { - // TODO - // handlePopUpOpen("role", { - // roleId - // }); - }} + onClick={() => + handlePopUpOpen("role", { + roleSlug + }) + } disabled={!isAllowed} > Edit Role @@ -125,9 +123,7 @@ export const RolePage = withProjectPermission( ? "hover:!bg-red-500 hover:!text-white" : "pointer-events-none cursor-not-allowed opacity-50" )} - onClick={async () => { - handlePopUpOpen("deleteRole"); - }} + onClick={() => handlePopUpOpen("deleteRole")} disabled={!isAllowed} > Delete Role diff --git a/frontend/src/views/Project/RolePage/components/RoleModal.tsx b/frontend/src/views/Project/RolePage/components/RoleModal.tsx index 463f36a78..435378a02 100644 --- a/frontend/src/views/Project/RolePage/components/RoleModal.tsx +++ b/frontend/src/views/Project/RolePage/components/RoleModal.tsx @@ -1,23 +1,16 @@ import { useEffect } from "react"; import { Controller, useForm } from "react-hook-form"; -// import { useRouter } from "next/router"; +import { useRouter } from "next/router"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2"; +import { useWorkspace } from "@app/context"; import { - // useOrganization, - useWorkspace -} from "@app/context"; -import { - // useCreateProjectRole, - // useUpdateProjectRole, - useGetProjectRoleBySlug - // useCreateOrgRole, - // useGetOrgRole, - // useUpdateOrgRole -} from "@app/hooks/api"; + useCreateProjectRole, + useGetProjectRoleBySlug, + useUpdateProjectRole} from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; const schema = z @@ -36,7 +29,7 @@ type Props = { }; export const RoleModal = ({ popUp, handlePopUpToggle }: Props) => { - // const router = useRouter(); + const router = useRouter(); const popupData = popUp?.role?.data as { roleSlug: string; @@ -47,10 +40,8 @@ export const RoleModal = ({ popUp, handlePopUpToggle }: Props) => { const { data: role } = useGetProjectRoleBySlug(projectSlug, popupData?.roleSlug ?? ""); - console.log("Project RoleModal role: ", role); - - // const { mutateAsync: createProjectRole } = useCreateProjectRole(); - // const { mutateAsync: updateProjectRole } = useUpdateProjectRole(); + const { mutateAsync: createProjectRole } = useCreateProjectRole(); + const { mutateAsync: updateProjectRole } = useUpdateProjectRole(); const { control, @@ -83,59 +74,33 @@ export const RoleModal = ({ popUp, handlePopUpToggle }: Props) => { const onFormSubmit = async ({ name, description, slug }: FormData) => { try { - console.log("onFormSubmit args: ", { - name, - description, - slug - }); - - if (!projectSlug || !role?.id) return; + if (!projectSlug) return; if (role) { // update - console.log("update"); - // const u1 = await updateProjectRole({ - // id: role.id, - // projectSlug, - // name, - // description, - // slug - // }); - // console.log("u1: ", u1); + await updateProjectRole({ + id: role.id, + projectSlug, + name, + description, + slug + }); + + handlePopUpToggle("role", false); } else { // create - console.log("create"); + const newRole = await createProjectRole({ + projectSlug, + name, + description, + slug, + permissions: [] + }); + + router.push(`/project/${currentWorkspace?.id}/roles/${newRole.slug}`); + handlePopUpToggle("role", false); } - // if (!orgId) return; - - // if (role) { - // // update - - // await updateOrgRole({ - // orgId, - // id: role.id, - // name, - // description, - // slug - // }); - - // handlePopUpToggle("role", false); - // } else { - // // create - - // const newRole = await createOrgRole({ - // orgId, - // name, - // description, - // slug, - // permissions: [] - // }); - - // handlePopUpToggle("role", false); - // router.push(`/org/${orgId}/roles/${newRole.id}`); - // } - createNotification({ text: `Successfully ${popUp?.role?.data ? "updated" : "created"} role`, type: "success" diff --git a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionRow.tsx b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionRow.tsx index 163f3a839..a98bf0d93 100644 --- a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionRow.tsx +++ b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionRow.tsx @@ -78,22 +78,9 @@ export const RolePermissionRow = ({ isEditable, title, formName, control, setVal }); const selectedPermissionCategory = useMemo(() => { - // TODO: re-check const actions = Object.keys(rule || {}) as Array; switch (formName) { - case "secret-rollback": - case "workspace": { - const totalActions = WORKSPACE_PERMISSIONS.length; - const score = actions - .map((key) => (rule?.[key] ? 1 : 0)) - .reduce((a, b) => a + b, 0 as number); - if (isCustom) return Permission.Custom; - if (score === 0) return Permission.NoAccess; - if (score === totalActions) return Permission.FullAccess; - - return Permission.Custom; // TODO: update - } default: { const totalActions = GENERAL_PERMISSIONS.length; const score = actions diff --git a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx index 6a03503ea..5010842ba 100644 --- a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx +++ b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx @@ -6,10 +6,11 @@ import { Button, Table, TableContainer, TBody, Th, THead, Tr } from "@app/compon import { ProjectPermissionSub, useWorkspace } from "@app/context"; import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api"; import { -formRolePermission2API, formSchema, + formRolePermission2API, + formSchema, rolePermission2Form, TFormSchema - } from "@app/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils"; +} from "@app/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils"; import { RolePermissionRow } from "./RolePermissionRow"; import { RowPermissionSecretsRow } from "./RolePermissionSecretsRow"; @@ -89,7 +90,6 @@ type Props = { roleSlug: string; }; -// note: isEditable should also depend on if user has edit role permission export const RolePermissionsSection = ({ roleSlug }: Props) => { const { currentWorkspace } = useWorkspace(); const projectSlug = currentWorkspace?.slug || ""; From 29c5bf5491127642cd9d75e7c99bc3688f563538 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Fri, 26 Jul 2024 11:22:15 -0700 Subject: [PATCH 5/5] Remove top margin from RolePermissionSecretsRow --- .../RolePermissionsSection/RolePermissionSecretsRow.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionSecretsRow.tsx b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionSecretsRow.tsx index 786f58cf1..5833fee13 100644 --- a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionSecretsRow.tsx +++ b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionSecretsRow.tsx @@ -123,7 +123,7 @@ export const RowPermissionSecretsRow = ({ className={`bg-bunker-600 px-0 py-0 ${isCustom && " border-mineshaft-500 p-8"}`} >
- +