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");
+ }}
+ >
+
+
+
+
+
+
+
+
+
+
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 (
+
+ );
+};
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";