diff --git a/frontend/src/components/v3/platform/PermissionActionSelect.tsx b/frontend/src/components/v3/platform/PermissionActionSelect.tsx new file mode 100644 index 00000000000..7a7a9a3df1b --- /dev/null +++ b/frontend/src/components/v3/platform/PermissionActionSelect.tsx @@ -0,0 +1,93 @@ +import { + components, + GroupBase, + MultiValueProps, + MultiValueRemoveProps, + OptionProps, + Props +} from "react-select"; +import { CheckIcon } from "lucide-react"; + +import { FilterableSelect } from "../generic/ReactSelect"; +import { Tooltip, TooltipContent, TooltipTrigger } from "../generic/Tooltip"; + +export type PermissionActionOption = { + label: string; + value: string; + description?: string; +}; + +const OptionWithDescription = (props: OptionProps) => { + const { data, children, isSelected } = props; + return ( + +
+
+

{children}

+ {data.description && ( +

{data.description}

+ )} +
+ {isSelected && } +
+
+ ); +}; + +const PermissionActionMultiValueRemove = ({ selectProps, ...props }: MultiValueRemoveProps) => { + if (selectProps?.isDisabled) return null; + return ; +}; + +const MultiValueWithTooltip = (props: MultiValueProps) => { + const { data } = props; + if (!data.description) return ; + return ( + + +
+ +
+
+ {data.description} +
+ ); +}; + +type PermissionActionSelectProps = Omit< + Props>, + "isMulti" +> & { + groupBy?: string | null; + getGroupHeaderLabel?: ((groupValue: unknown) => string) | null; + isError?: boolean; +}; + +export const PermissionActionSelect = ({ + components: customComponents, + ...props +}: PermissionActionSelectProps) => { + return ( + + isMulti + filterOption={(option, inputValue) => { + if (!inputValue) return true; + const lowerInput = inputValue.toLowerCase(); + const data = option.data as T; + return ( + option.label.toLowerCase().includes(lowerInput) || + Boolean(data.description?.toLowerCase().includes(lowerInput)) + ); + }} + components={ + { + Option: OptionWithDescription, + MultiValueRemove: PermissionActionMultiValueRemove, + MultiValue: MultiValueWithTooltip, + ...customComponents + } as any + } + {...props} + /> + ); +}; diff --git a/frontend/src/components/v3/platform/index.ts b/frontend/src/components/v3/platform/index.ts index ab583961838..d265d95ca74 100644 --- a/frontend/src/components/v3/platform/index.ts +++ b/frontend/src/components/v3/platform/index.ts @@ -1,2 +1,3 @@ export * from "./DocumentationLinkBadge"; +export * from "./PermissionActionSelect"; export * from "./ScopeIcons"; diff --git a/frontend/src/pages/organization/RoleByIDPage/RoleByIDPage.tsx b/frontend/src/pages/organization/RoleByIDPage/RoleByIDPage.tsx index ff3594ef36e..62107b16f2b 100644 --- a/frontend/src/pages/organization/RoleByIDPage/RoleByIDPage.tsx +++ b/frontend/src/pages/organization/RoleByIDPage/RoleByIDPage.tsx @@ -1,20 +1,19 @@ import { Helmet } from "react-helmet"; import { useTranslation } from "react-i18next"; -import { faChevronLeft, faCopy, faEllipsisV } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Link, useNavigate, useParams } from "@tanstack/react-router"; +import { ChevronLeftIcon, CopyIcon, EllipsisIcon, PencilIcon, TrashIcon } from "lucide-react"; +import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; +import { DeleteActionModal, PageHeader } from "@app/components/v2"; import { Button, - DeleteActionModal, DropdownMenu, DropdownMenuContent, DropdownMenuItem, - DropdownMenuTrigger, - PageHeader -} from "@app/components/v2"; + DropdownMenuTrigger +} from "@app/components/v3"; import { ROUTE_PATHS } from "@app/const/routes"; import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context"; import { useDeleteOrgRole, useGetOrgRole } from "@app/hooks/api"; @@ -76,9 +75,9 @@ export const Page = () => { search={{ selectedTab: OrgAccessControlTabSections.Roles }} - className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400" + className="mb-4 flex items-center gap-x-2 text-sm text-muted" > - + Roles { {isCustomRole && ( - - + { navigator.clipboard.writeText(data.id); @@ -110,8 +107,8 @@ export const Page = () => { type: "info" }); }} - icon={} > + Copy ID { type: "info" }); }} - icon={} > + Copy Slug {(isAllowed) => ( { handlePopUpOpen("role", { roleId @@ -137,6 +137,7 @@ export const Page = () => { }} isDisabled={!isAllowed} > + Edit Role )} @@ -144,11 +145,15 @@ export const Page = () => { {(isAllowed) => ( { handlePopUpOpen("duplicateRole"); }} isDisabled={!isAllowed} > + Duplicate Role )} @@ -156,11 +161,13 @@ export const Page = () => { {(isAllowed) => ( { handlePopUpOpen("deleteOrgRole"); }} isDisabled={!isAllowed} > + Delete Role )} diff --git a/frontend/src/pages/organization/RoleByIDPage/components/OrgRoleModifySection.utils.ts b/frontend/src/pages/organization/RoleByIDPage/components/OrgRoleModifySection.utils.ts index 566a02e45e9..f8b1acde96f 100644 --- a/frontend/src/pages/organization/RoleByIDPage/components/OrgRoleModifySection.utils.ts +++ b/frontend/src/pages/organization/RoleByIDPage/components/OrgRoleModifySection.utils.ts @@ -4,6 +4,8 @@ import { z } from "zod"; import { OrgPermissionSubjects } from "@app/context"; import { OrgGatewayPermissionActions, + OrgPermissionActions, + OrgPermissionAdminConsoleAction, OrgPermissionAppConnectionActions, OrgPermissionAuditLogsActions, OrgPermissionBillingActions, @@ -20,139 +22,153 @@ import { import { TPermission } from "@app/hooks/api/roles/types"; const generalPermissionSchema = z - .object({ - read: z.boolean().optional(), - edit: z.boolean().optional(), - delete: z.boolean().optional(), - create: z.boolean().optional() - }) + .array( + z.object({ + read: z.boolean().optional(), + edit: z.boolean().optional(), + delete: z.boolean().optional(), + create: z.boolean().optional() + }) + ) .optional(); const auditLogsPermissionSchema = z - .object({ - [OrgPermissionAuditLogsActions.Read]: z.boolean().optional() - }) + .array(z.object({ [OrgPermissionAuditLogsActions.Read]: z.boolean().optional() })) .optional(); const billingPermissionSchema = z - .object({ - [OrgPermissionBillingActions.Read]: z.boolean().optional(), - [OrgPermissionBillingActions.ManageBilling]: z.boolean().optional() - }) + .array( + z.object({ + [OrgPermissionBillingActions.Read]: z.boolean().optional(), + [OrgPermissionBillingActions.ManageBilling]: z.boolean().optional() + }) + ) .optional(); const emailDomainPermissionSchema = z - .object({ - [OrgPermissionEmailDomainActions.Read]: z.boolean().optional(), - [OrgPermissionEmailDomainActions.Create]: z.boolean().optional(), - [OrgPermissionEmailDomainActions.VerifyDomain]: z.boolean().optional(), - [OrgPermissionEmailDomainActions.Delete]: z.boolean().optional() - }) + .array( + z.object({ + [OrgPermissionEmailDomainActions.Read]: z.boolean().optional(), + [OrgPermissionEmailDomainActions.Create]: z.boolean().optional(), + [OrgPermissionEmailDomainActions.VerifyDomain]: z.boolean().optional(), + [OrgPermissionEmailDomainActions.Delete]: z.boolean().optional() + }) + ) .optional(); const appConnectionsPermissionSchema = z - .object({ - [OrgPermissionAppConnectionActions.Read]: z.boolean().optional(), - [OrgPermissionAppConnectionActions.Edit]: z.boolean().optional(), - [OrgPermissionAppConnectionActions.Create]: z.boolean().optional(), - [OrgPermissionAppConnectionActions.Delete]: z.boolean().optional(), - [OrgPermissionAppConnectionActions.Connect]: z.boolean().optional(), - [OrgPermissionAppConnectionActions.RotateCredentials]: z.boolean().optional() - }) + .array( + z.object({ + [OrgPermissionAppConnectionActions.Read]: z.boolean().optional(), + [OrgPermissionAppConnectionActions.Edit]: z.boolean().optional(), + [OrgPermissionAppConnectionActions.Create]: z.boolean().optional(), + [OrgPermissionAppConnectionActions.Delete]: z.boolean().optional(), + [OrgPermissionAppConnectionActions.Connect]: z.boolean().optional(), + [OrgPermissionAppConnectionActions.RotateCredentials]: z.boolean().optional() + }) + ) .optional(); const kmipPermissionSchema = z - .object({ - [OrgPermissionKmipActions.Proxy]: z.boolean().optional() - }) + .array(z.object({ [OrgPermissionKmipActions.Proxy]: z.boolean().optional() })) .optional(); const identityPermissionSchema = z - .object({ - [OrgPermissionIdentityActions.Read]: z.boolean().optional(), - [OrgPermissionIdentityActions.Edit]: z.boolean().optional(), - [OrgPermissionIdentityActions.Delete]: z.boolean().optional(), - [OrgPermissionIdentityActions.Create]: z.boolean().optional(), - [OrgPermissionIdentityActions.GrantPrivileges]: z.boolean().optional(), - [OrgPermissionIdentityActions.RevokeAuth]: z.boolean().optional(), - [OrgPermissionIdentityActions.CreateToken]: z.boolean().optional(), - [OrgPermissionIdentityActions.GetToken]: z.boolean().optional(), - [OrgPermissionIdentityActions.DeleteToken]: z.boolean().optional() - }) + .array( + z.object({ + [OrgPermissionIdentityActions.Read]: z.boolean().optional(), + [OrgPermissionIdentityActions.Edit]: z.boolean().optional(), + [OrgPermissionIdentityActions.Delete]: z.boolean().optional(), + [OrgPermissionIdentityActions.Create]: z.boolean().optional(), + [OrgPermissionIdentityActions.GrantPrivileges]: z.boolean().optional(), + [OrgPermissionIdentityActions.RevokeAuth]: z.boolean().optional(), + [OrgPermissionIdentityActions.CreateToken]: z.boolean().optional(), + [OrgPermissionIdentityActions.GetToken]: z.boolean().optional(), + [OrgPermissionIdentityActions.DeleteToken]: z.boolean().optional() + }) + ) .optional(); const groupPermissionSchema = z - .object({ - [OrgPermissionGroupActions.Read]: z.boolean().optional(), - [OrgPermissionGroupActions.Create]: z.boolean().optional(), - [OrgPermissionGroupActions.Edit]: z.boolean().optional(), - [OrgPermissionGroupActions.Delete]: z.boolean().optional(), - [OrgPermissionGroupActions.GrantPrivileges]: z.boolean().optional(), - [OrgPermissionGroupActions.AddMembers]: z.boolean().optional(), - [OrgPermissionGroupActions.RemoveMembers]: z.boolean().optional() - }) + .array( + z.object({ + [OrgPermissionGroupActions.Read]: z.boolean().optional(), + [OrgPermissionGroupActions.Create]: z.boolean().optional(), + [OrgPermissionGroupActions.Edit]: z.boolean().optional(), + [OrgPermissionGroupActions.Delete]: z.boolean().optional(), + [OrgPermissionGroupActions.GrantPrivileges]: z.boolean().optional(), + [OrgPermissionGroupActions.AddMembers]: z.boolean().optional(), + [OrgPermissionGroupActions.RemoveMembers]: z.boolean().optional() + }) + ) .optional(); const orgGatewayPermissionSchema = z - .object({ - [OrgGatewayPermissionActions.ListGateways]: z.boolean().optional(), - [OrgGatewayPermissionActions.EditGateways]: z.boolean().optional(), - [OrgGatewayPermissionActions.DeleteGateways]: z.boolean().optional(), - [OrgGatewayPermissionActions.CreateGateways]: z.boolean().optional(), - [OrgGatewayPermissionActions.AttachGateways]: z.boolean().optional() - }) + .array( + z.object({ + [OrgGatewayPermissionActions.ListGateways]: z.boolean().optional(), + [OrgGatewayPermissionActions.EditGateways]: z.boolean().optional(), + [OrgGatewayPermissionActions.DeleteGateways]: z.boolean().optional(), + [OrgGatewayPermissionActions.CreateGateways]: z.boolean().optional(), + [OrgGatewayPermissionActions.AttachGateways]: z.boolean().optional() + }) + ) .optional(); const orgRelayPermissionSchema = z - .object({ - [OrgRelayPermissionActions.ListRelays]: z.boolean().optional(), - [OrgRelayPermissionActions.EditRelays]: z.boolean().optional(), - [OrgRelayPermissionActions.DeleteRelays]: z.boolean().optional(), - [OrgRelayPermissionActions.CreateRelays]: z.boolean().optional() - }) + .array( + z.object({ + [OrgRelayPermissionActions.ListRelays]: z.boolean().optional(), + [OrgRelayPermissionActions.EditRelays]: z.boolean().optional(), + [OrgRelayPermissionActions.DeleteRelays]: z.boolean().optional(), + [OrgRelayPermissionActions.CreateRelays]: z.boolean().optional() + }) + ) .optional(); const machineIdentityAuthTemplatePermissionSchema = z - .object({ - [OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates]: z.boolean().optional(), - [OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates]: z.boolean().optional(), - [OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates]: z.boolean().optional(), - [OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates]: z.boolean().optional(), - [OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: z.boolean().optional(), - [OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates]: z.boolean().optional() - }) + .array( + z.object({ + [OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates]: z.boolean().optional(), + [OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates]: z.boolean().optional(), + [OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates]: z.boolean().optional(), + [OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates]: z.boolean().optional(), + [OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: z.boolean().optional(), + [OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates]: z.boolean().optional() + }) + ) .optional(); const adminConsolePermissionSchmea = z - .object({ - "access-all-projects": z.boolean().optional() - }) + .array(z.object({ "access-all-projects": z.boolean().optional() })) .optional(); const secretSharingPermissionSchema = z - .object({ - [OrgPermissionSecretShareAction.ManageSettings]: z.boolean().optional() - }) + .array(z.object({ [OrgPermissionSecretShareAction.ManageSettings]: z.boolean().optional() })) .optional(); const subOrganizationPermissionSchema = z - .object({ - [OrgPermissionSubOrgActions.Create]: z.boolean().optional(), - [OrgPermissionSubOrgActions.Edit]: z.boolean().optional(), - [OrgPermissionSubOrgActions.Delete]: z.boolean().optional(), - [OrgPermissionSubOrgActions.DirectAccess]: z.boolean().optional(), - [OrgPermissionSubOrgActions.LinkGroup]: z.boolean().optional() - }) + .array( + z.object({ + [OrgPermissionSubOrgActions.Create]: z.boolean().optional(), + [OrgPermissionSubOrgActions.Edit]: z.boolean().optional(), + [OrgPermissionSubOrgActions.Delete]: z.boolean().optional(), + [OrgPermissionSubOrgActions.DirectAccess]: z.boolean().optional(), + [OrgPermissionSubOrgActions.LinkGroup]: z.boolean().optional() + }) + ) .optional(); const ssoPermissionSchema = z - .object({ - [OrgPermissionSsoActions.Read]: z.boolean().optional(), - [OrgPermissionSsoActions.Create]: z.boolean().optional(), - [OrgPermissionSsoActions.Edit]: z.boolean().optional(), - [OrgPermissionSsoActions.Delete]: z.boolean().optional(), - [OrgPermissionSsoActions.BypassSsoEnforcement]: z.boolean().optional() - }) + .array( + z.object({ + [OrgPermissionSsoActions.Read]: z.boolean().optional(), + [OrgPermissionSsoActions.Create]: z.boolean().optional(), + [OrgPermissionSsoActions.Edit]: z.boolean().optional(), + [OrgPermissionSsoActions.Delete]: z.boolean().optional(), + [OrgPermissionSsoActions.BypassSsoEnforcement]: z.boolean().optional() + }) + ) .optional(); export const formSchema = z.object({ @@ -164,11 +180,7 @@ export const formSchema = z.object({ .refine((val) => val !== "custom", { message: "Cannot use custom as its a keyword" }), permissions: z .object({ - project: z - .object({ - create: z.boolean().optional() - }) - .optional(), + project: z.array(z.object({ create: z.boolean().optional() })).optional(), "audit-logs": auditLogsPermissionSchema, member: generalPermissionSchema, groups: groupPermissionSchema, @@ -179,7 +191,8 @@ export const formSchema = z.object({ "secret-scanning": generalPermissionSchema, sso: ssoPermissionSchema, scim: generalPermissionSchema, - [OrgPermissionSubjects.GithubOrgSync]: generalPermissionSchema, + "github-org-sync": generalPermissionSchema, + "github-org-sync-manual": z.array(z.object({ edit: z.boolean().optional() })).optional(), ldap: generalPermissionSchema, billing: billingPermissionSchema, identity: identityPermissionSchema, @@ -196,14 +209,31 @@ export const formSchema = z.object({ "email-domains": emailDomainPermissionSchema }) .optional() + .superRefine((permissions, ctx) => { + if (!permissions) return; + + Object.entries(permissions).forEach(([subject, rules]) => { + if (!Array.isArray(rules)) return; + rules.forEach((rule, ruleIndex) => { + if (!rule || typeof rule !== "object") return; + const hasAction = Object.values(rule).some((value) => value === true); + if (!hasAction) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "At least one action is required", + path: [subject, ruleIndex, "actionRequired"] + }); + } + }); + }); + }) }); export type TFormSchema = z.infer; +export type TPermissionsKey = keyof NonNullable; -// convert role permission to form compatiable data structure export const rolePermission2Form = (permissions: TPermission[] = []) => { - // any because if it set it as form type due to the discriminated union type of ts - // i would have to write a if loop with both conditions same + // eslint-disable-next-line @typescript-eslint/no-explicit-any const formVal: Record = {}; permissions.forEach((permission) => { const { action } = permission; @@ -211,17 +241,660 @@ export const rolePermission2Form = (permissions: TPermission[] = []) => { if (subject === OrgPermissionSubjects.Workspace) { subject = OrgPermissionSubjects.Project; } - if (!formVal?.[subject]) formVal[subject] = {}; - formVal[subject][action] = true; + if (!formVal?.[subject]) formVal[subject] = [{}]; + formVal[subject][0][action] = true; }); return formVal; }; +export type TOrgPermissionAction = { + value: string; + label: string; + description?: string; +}; + +export type TOrgPermissionConfig = { + title: string; + description: string; + actions: readonly TOrgPermissionAction[]; +}; + +export const ORG_PERMISSION_OBJECT: Record = { + [OrgPermissionSubjects.Member]: { + title: "User Management", + description: "Manage organization member access and role assignments", + actions: [ + { + value: OrgPermissionActions.Read, + label: "View all members", + description: "View organization members and their roles" + }, + { + value: OrgPermissionActions.Create, + label: "Invite members", + description: "Invite new users to join the organization" + }, + { + value: OrgPermissionActions.Edit, + label: "Edit members", + description: "Modify member roles and access settings" + }, + { + value: OrgPermissionActions.Delete, + label: "Remove members", + description: "Remove members from the organization" + } + ] + }, + [OrgPermissionSubjects.Role]: { + title: "Role Management", + description: "Define and configure custom organization-level permission roles", + actions: [ + { + value: OrgPermissionActions.Read, + label: "View", + description: "View organization roles and their permissions" + }, + { + value: OrgPermissionActions.Create, + label: "Create", + description: "Create new custom organization roles" + }, + { + value: OrgPermissionActions.Edit, + label: "Modify", + description: "Update role permissions and settings" + }, + { + value: OrgPermissionActions.Delete, + label: "Remove", + description: "Delete organization roles" + } + ] + }, + [OrgPermissionSubjects.IncidentAccount]: { + title: "Incident Contacts", + description: "Manage contacts notified during security incidents", + actions: [ + { + value: OrgPermissionActions.Read, + label: "View contacts", + description: "View contacts notified during security incidents" + }, + { + value: OrgPermissionActions.Create, + label: "Add new contacts", + description: "Add new contacts for incident notifications" + }, + { + value: OrgPermissionActions.Edit, + label: "Edit contacts", + description: "Update existing contact information" + }, + { + value: OrgPermissionActions.Delete, + label: "Remove contacts", + description: "Remove contacts from incident notifications" + } + ] + }, + [OrgPermissionSubjects.Settings]: { + title: "Organization Profile", + description: "Configure organization-wide settings and preferences", + actions: [ + { + value: OrgPermissionActions.Read, + label: "View", + description: "View organization settings and configuration" + }, + { + value: OrgPermissionActions.Create, + label: "Create", + description: "Configure new organization settings" + }, + { + value: OrgPermissionActions.Edit, + label: "Modify", + description: "Update organization profile and settings" + }, + { + value: OrgPermissionActions.Delete, + label: "Remove", + description: "Remove organization configuration entries" + } + ] + }, + [OrgPermissionSubjects.SecretScanning]: { + title: "Secret Scanning", + description: "Configure automated scanning for leaked secrets", + actions: [ + { + value: OrgPermissionActions.Read, + label: "View risks", + description: "View detected leaked secret risks" + }, + { + value: OrgPermissionActions.Create, + label: "Add integrations", + description: "Connect new secret scanning integrations" + }, + { + value: OrgPermissionActions.Edit, + label: "Edit risk status", + description: "Update the status of detected risks" + }, + { + value: OrgPermissionActions.Delete, + label: "Remove integrations", + description: "Disconnect secret scanning integrations" + } + ] + }, + [OrgPermissionSubjects.Ldap]: { + title: "LDAP", + description: "Configure LDAP directory integration for authentication", + actions: [ + { + value: OrgPermissionActions.Read, + label: "View", + description: "View LDAP directory configuration" + }, + { + value: OrgPermissionActions.Create, + label: "Create", + description: "Configure LDAP integration" + }, + { value: OrgPermissionActions.Edit, label: "Modify", description: "Update LDAP settings" }, + { + value: OrgPermissionActions.Delete, + label: "Remove", + description: "Remove LDAP configuration" + } + ] + }, + [OrgPermissionSubjects.Scim]: { + title: "SCIM", + description: "Manage SCIM provisioning for automated user lifecycle management", + actions: [ + { + value: OrgPermissionActions.Read, + label: "View", + description: "View SCIM provisioning configuration" + }, + { + value: OrgPermissionActions.Create, + label: "Create", + description: "Set up SCIM provisioning" + }, + { value: OrgPermissionActions.Edit, label: "Modify", description: "Update SCIM settings" }, + { + value: OrgPermissionActions.Delete, + label: "Remove", + description: "Remove SCIM configuration" + } + ] + }, + [OrgPermissionSubjects.GithubOrgSync]: { + title: "GitHub Organization Sync", + description: "Sync GitHub organization teams with Infisical groups", + actions: [ + { + value: OrgPermissionActions.Read, + label: "View", + description: "View GitHub organization sync configuration" + }, + { + value: OrgPermissionActions.Create, + label: "Create", + description: "Set up GitHub organization team sync" + }, + { + value: OrgPermissionActions.Edit, + label: "Modify", + description: "Update sync configuration" + }, + { + value: OrgPermissionActions.Delete, + label: "Remove", + description: "Remove GitHub organization sync" + } + ] + }, + [OrgPermissionSubjects.GithubOrgSyncManual]: { + title: "GitHub Organization Sync (Manual)", + description: "Manually trigger GitHub organization sync", + actions: [ + { + value: OrgPermissionActions.Edit, + label: "Trigger", + description: "Manually trigger a GitHub organization sync" + } + ] + }, + [OrgPermissionSubjects.Kms]: { + title: "External KMS", + description: "Configure external key management systems for encryption", + actions: [ + { + value: OrgPermissionActions.Read, + label: "View", + description: "View external KMS configuration" + }, + { + value: OrgPermissionActions.Create, + label: "Create", + description: "Configure external key management systems" + }, + { value: OrgPermissionActions.Edit, label: "Modify", description: "Update KMS settings" }, + { + value: OrgPermissionActions.Delete, + label: "Remove", + description: "Remove external KMS configuration" + } + ] + }, + [OrgPermissionSubjects.ProjectTemplates]: { + title: "Project Templates", + description: "Manage reusable templates applied when creating new projects", + actions: [ + { + value: OrgPermissionActions.Read, + label: "View & Apply", + description: "View and apply templates when creating projects" + }, + { + value: OrgPermissionActions.Create, + label: "Create", + description: "Create new project templates" + }, + { + value: OrgPermissionActions.Edit, + label: "Modify", + description: "Update existing project templates" + }, + { + value: OrgPermissionActions.Delete, + label: "Remove", + description: "Delete project templates" + } + ] + }, + [OrgPermissionSubjects.Sso]: { + title: "SSO", + description: "Configure and enforce single sign-on authentication for the organization", + actions: [ + { value: OrgPermissionSsoActions.Read, label: "View", description: "View SSO configuration" }, + { + value: OrgPermissionSsoActions.Create, + label: "Create", + description: "Set up new SSO providers" + }, + { + value: OrgPermissionSsoActions.Edit, + label: "Modify", + description: "Update SSO configuration" + }, + { + value: OrgPermissionSsoActions.Delete, + label: "Remove", + description: "Remove SSO providers" + }, + { + value: OrgPermissionSsoActions.BypassSsoEnforcement, + label: "Bypass SSO Enforcement", + description: "Allow login without SSO when enforcement is enabled" + } + ] + }, + [OrgPermissionSubjects.AuditLogs]: { + title: "Audit Logs", + description: "View organization activity and audit trail", + actions: [ + { + value: OrgPermissionAuditLogsActions.Read, + label: "Read", + description: "View organization activity and audit events" + } + ] + }, + [OrgPermissionSubjects.Identity]: { + title: "Machine Identity Management", + description: "Manage machine identities and their access within the organization", + actions: [ + { + value: OrgPermissionIdentityActions.Read, + label: "Read Identities", + description: "View machine identities and their configuration" + }, + { + value: OrgPermissionIdentityActions.Create, + label: "Create Identities", + description: "Create new machine identities" + }, + { + value: OrgPermissionIdentityActions.Edit, + label: "Edit Identities", + description: "Update machine identity settings" + }, + { + value: OrgPermissionIdentityActions.Delete, + label: "Delete Identities", + description: "Delete machine identities" + }, + { value: OrgPermissionIdentityActions.GrantPrivileges, label: "Grant Privileges" }, + { + value: OrgPermissionIdentityActions.RevokeAuth, + label: "Revoke Auth", + description: "Revoke authentication for a machine identity" + }, + { + value: OrgPermissionIdentityActions.CreateToken, + label: "Create Token", + description: "Generate access tokens for machine identities" + }, + { + value: OrgPermissionIdentityActions.GetToken, + label: "Get Token", + description: "View existing access tokens" + }, + { + value: OrgPermissionIdentityActions.DeleteToken, + label: "Delete Token", + description: "Revoke access tokens" + } + ] + }, + [OrgPermissionSubjects.Groups]: { + title: "Group Management", + description: "Organize users into groups for bulk permission management", + actions: [ + { + value: OrgPermissionGroupActions.Read, + label: "Read Groups", + description: "View groups and their members" + }, + { + value: OrgPermissionGroupActions.Create, + label: "Create Groups", + description: "Create new user groups" + }, + { + value: OrgPermissionGroupActions.Edit, + label: "Edit Groups", + description: "Update group membership and settings" + }, + { + value: OrgPermissionGroupActions.Delete, + label: "Delete Groups", + description: "Delete groups" + }, + { value: OrgPermissionGroupActions.GrantPrivileges, label: "Grant Privileges" }, + { + value: OrgPermissionGroupActions.AddMembers, + label: "Add Members", + description: "Add users to a group" + }, + { + value: OrgPermissionGroupActions.RemoveMembers, + label: "Remove Members", + description: "Remove users from a group" + } + ] + }, + [OrgPermissionSubjects.AppConnections]: { + title: "App Connections", + description: "Manage connections to external platforms and services", + actions: [ + { + value: OrgPermissionAppConnectionActions.Read, + label: "Read", + description: "View configured app connections" + }, + { + value: OrgPermissionAppConnectionActions.Create, + label: "Create", + description: "Create new connections to external platforms and services" + }, + { + value: OrgPermissionAppConnectionActions.Edit, + label: "Modify", + description: "Modify app connection settings" + }, + { + value: OrgPermissionAppConnectionActions.Delete, + label: "Remove", + description: "Remove app connections" + }, + { + value: OrgPermissionAppConnectionActions.Connect, + label: "Connect", + description: "Use this connection when configuring syncs and integrations" + }, + { + value: OrgPermissionAppConnectionActions.RotateCredentials, + label: "Rotate Credentials", + description: "Rotate credentials for app connections" + } + ] + }, + [OrgPermissionSubjects.Gateway]: { + title: "Gateways", + description: "Manage gateways used for private network access", + actions: [ + { + value: OrgGatewayPermissionActions.ListGateways, + label: "List Gateways", + description: "View available gateways" + }, + { + value: OrgGatewayPermissionActions.CreateGateways, + label: "Create Gateways", + description: "Register new gateways for private network access" + }, + { + value: OrgGatewayPermissionActions.EditGateways, + label: "Edit Gateways", + description: "Update gateway configuration" + }, + { + value: OrgGatewayPermissionActions.DeleteGateways, + label: "Delete Gateways", + description: "Remove gateways" + }, + { + value: OrgGatewayPermissionActions.AttachGateways, + label: "Attach Gateways", + description: "Attach gateways to organization resources" + } + ] + }, + [OrgPermissionSubjects.Relay]: { + title: "Relays", + description: "Manage relay servers used for secure network tunneling", + actions: [ + { + value: OrgRelayPermissionActions.ListRelays, + label: "List Relays", + description: "View available relay servers" + }, + { + value: OrgRelayPermissionActions.CreateRelays, + label: "Create Relays", + description: "Add new relay servers for network tunneling" + }, + { + value: OrgRelayPermissionActions.EditRelays, + label: "Edit Relays", + description: "Update relay server configuration" + }, + { + value: OrgRelayPermissionActions.DeleteRelays, + label: "Delete Relays", + description: "Remove relay servers" + } + ] + }, + [OrgPermissionSubjects.Billing]: { + title: "Billing", + description: "View and manage billing details, invoices, and payment methods", + actions: [ + { + value: OrgPermissionBillingActions.Read, + label: "View bills", + description: "View invoices and billing history" + }, + { + value: OrgPermissionBillingActions.ManageBilling, + label: "Manage billing", + description: "Update payment methods and billing settings" + } + ] + }, + [OrgPermissionSubjects.EmailDomains]: { + title: "Email Domains", + description: "Manage organization email domain verification and configuration", + actions: [ + { + value: OrgPermissionEmailDomainActions.Read, + label: "View domains", + description: "View organization email domains" + }, + { + value: OrgPermissionEmailDomainActions.Create, + label: "Add domains", + description: "Add new email domains to the organization" + }, + { + value: OrgPermissionEmailDomainActions.VerifyDomain, + label: "Verify domains", + description: "Verify ownership of email domains" + }, + { + value: OrgPermissionEmailDomainActions.Delete, + label: "Delete domains", + description: "Remove email domains from the organization" + } + ] + }, + [OrgPermissionSubjects.SecretShare]: { + title: "Secret Share", + description: "Configure settings for sharing secrets externally", + actions: [ + { + value: OrgPermissionSecretShareAction.ManageSettings, + label: "Manage settings", + description: "Configure settings for sharing secrets externally" + } + ] + }, + [OrgPermissionSubjects.Project]: { + title: "Project", + description: "Create new projects within the organization", + actions: [ + { + value: OrgPermissionActions.Create, + label: "Create projects", + description: "Create new projects within the organization" + } + ] + }, + [OrgPermissionSubjects.AdminConsole]: { + title: "Organization Admin Console", + description: "Bypass project membership to access all projects in the organization", + actions: [ + { + value: OrgPermissionAdminConsoleAction.AccessAllProjects, + label: "Access all organization projects", + description: "Bypass project membership to access all projects in the organization" + } + ] + }, + [OrgPermissionSubjects.MachineIdentityAuthTemplate]: { + title: "Machine Identity Auth Templates", + description: "Manage reusable authentication configuration templates for machine identities", + actions: [ + { + value: OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates, + label: "List Templates", + description: "View available authentication templates" + }, + { + value: OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates, + label: "Create Templates", + description: "Create reusable authentication configuration templates" + }, + { + value: OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates, + label: "Edit Templates", + description: "Update authentication template settings" + }, + { + value: OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates, + label: "Delete Templates", + description: "Remove authentication templates" + }, + { + value: OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates, + label: "Unlink Templates", + description: "Detach authentication templates from machine identities" + }, + { + value: OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates, + label: "Attach Templates", + description: "Apply authentication templates to machine identities" + } + ] + }, + [OrgPermissionSubjects.Kmip]: { + title: "KMIP", + description: "Proxy KMIP requests to organization key management infrastructure", + actions: [ + { + value: OrgPermissionKmipActions.Proxy, + label: "Proxy KMIP requests", + description: "Route KMIP requests to organization key management infrastructure" + } + ] + }, + [OrgPermissionSubjects.SubOrganization]: { + title: "Sub-Organizations", + description: "Create and manage namespaces within the organization", + actions: [ + { + value: OrgPermissionSubOrgActions.Create, + label: "Create", + description: "Create new sub-organizations" + }, + { + value: OrgPermissionSubOrgActions.Edit, + label: "Edit", + description: "Update sub-organization settings" + }, + { + value: OrgPermissionSubOrgActions.Delete, + label: "Delete", + description: "Remove sub-organizations" + }, + { + value: OrgPermissionSubOrgActions.DirectAccess, + label: "Direct Access", + description: "Access sub-organizations directly without membership" + }, + { + value: OrgPermissionSubOrgActions.LinkGroup, + label: "Link Group", + description: "Link organization groups to sub-organizations" + } + ] + } +}; + export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => { const permissions: TPermission[] = []; - Object.entries(formVal || {}).forEach(([rule, actions]) => { - Object.entries(actions).forEach(([action, isAllowed]) => { + Object.entries(formVal || {}).forEach(([rule, ruleArr]) => { + if (!ruleArr?.[0]) return; + Object.entries(ruleArr[0]).forEach(([action, isAllowed]) => { if (isAllowed) { permissions.push({ subject: rule, action }); } diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgAddPoliciesButton.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgAddPoliciesButton.tsx new file mode 100644 index 00000000000..01e6d141df0 --- /dev/null +++ b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgAddPoliciesButton.tsx @@ -0,0 +1,28 @@ +import { PlusIcon } from "lucide-react"; + +import { Button } from "@app/components/v3"; +import { usePopUp } from "@app/hooks"; + +import { OrgPolicySelectionPopover } from "./OrgPolicySelectionModal"; + +type Props = { + isDisabled?: boolean; + invalidSubjects?: string[]; +}; + +export const OrgAddPoliciesButton = ({ isDisabled, invalidSubjects }: Props) => { + const { popUp, handlePopUpToggle } = usePopUp(["addPolicy"] as const); + + return ( + handlePopUpToggle("addPolicy", isOpen)} + invalidSubjects={invalidSubjects} + > + + + ); +}; diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionAdminConsoleRow.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionAdminConsoleRow.tsx deleted file mode 100644 index 48a0de62e29..00000000000 --- a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionAdminConsoleRow.tsx +++ /dev/null @@ -1,134 +0,0 @@ -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 "../OrgRoleModifySection.utils"; - -type Props = { - isEditable: boolean; - setValue: UseFormSetValue; - control: Control; -}; - -enum Permission { - NoAccess = "no-access", - Custom = "custom" -} - -const PERMISSION_ACTIONS = [ - { action: "access-all-projects", label: "Access all organization projects" } -] as const; - -export const OrgPermissionAdminConsoleRow = ({ isEditable, control, setValue }: Props) => { - const [isRowExpanded, setIsRowExpanded] = useToggle(); - const [isCustom, setIsCustom] = useToggle(); - - const rule = useWatch({ - control, - name: "permissions.organization-admin-console" - }); - - const selectedPermissionCategory = useMemo(() => { - if (rule?.["access-all-projects"]) { - return Permission.Custom; - } - return Permission.NoAccess; - }, [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) return; - if (val === Permission.Custom) { - setIsRowExpanded.on(); - setIsCustom.on(); - return; - } - setIsCustom.off(); - - if (val === Permission.NoAccess) { - setValue( - "permissions.organization-admin-console", - { "access-all-projects": false }, - { shouldDirty: true } - ); - } - }; - - return ( - <> - setIsRowExpanded.toggle()} - > - - - - Organization Admin Console - - - - - {isRowExpanded && ( - - -
- {PERMISSION_ACTIONS.map(({ action, label }) => { - return ( - ( - { - if (!isEditable) { - createNotification({ - type: "error", - text: "Failed to update default role" - }); - return; - } - field.onChange(e); - }} - id={`permissions.organization-admin-console.${action}`} - > - {label} - - )} - /> - ); - })} -
- - - )} - - ); -}; diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionAppConnectionRow.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionAppConnectionRow.tsx deleted file mode 100644 index ead37e75f2d..00000000000 --- a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionAppConnectionRow.tsx +++ /dev/null @@ -1,188 +0,0 @@ -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 { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types"; -import { useToggle } from "@app/hooks"; - -import { TFormSchema } from "../OrgRoleModifySection.utils"; - -type Props = { - isEditable: boolean; - setValue: UseFormSetValue; - control: Control; -}; - -enum Permission { - NoAccess = "no-access", - ReadOnly = "read-only", - FullAccess = "full-access", - Custom = "custom" -} - -const PERMISSION_ACTIONS = [ - { action: OrgPermissionAppConnectionActions.Read, label: "Read" }, - { action: OrgPermissionAppConnectionActions.Create, label: "Create" }, - { action: OrgPermissionAppConnectionActions.Edit, label: "Modify" }, - { action: OrgPermissionAppConnectionActions.Delete, label: "Remove" }, - { action: OrgPermissionAppConnectionActions.Connect, label: "Connect" }, - { action: OrgPermissionAppConnectionActions.RotateCredentials, label: "Rotate Credentials" } -] as const; - -export const OrgPermissionAppConnectionRow = ({ isEditable, control, setValue }: Props) => { - const [isRowExpanded, setIsRowExpanded] = useToggle(); - const [isCustom, setIsCustom] = useToggle(); - - const rule = useWatch({ - control, - name: "permissions.app-connections" - }); - - const selectedPermissionCategory = useMemo(() => { - const actions = Object.keys(rule || {}) as Array; - const totalActions = PERMISSION_ACTIONS.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 (score === 1 && rule?.[OrgPermissionAppConnectionActions.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) return; - if (val === Permission.Custom) { - setIsRowExpanded.on(); - setIsCustom.on(); - return; - } - setIsCustom.off(); - - switch (val) { - case Permission.FullAccess: - setValue( - "permissions.app-connections", - { - [OrgPermissionAppConnectionActions.Read]: true, - [OrgPermissionAppConnectionActions.Edit]: true, - [OrgPermissionAppConnectionActions.Create]: true, - [OrgPermissionAppConnectionActions.Delete]: true, - [OrgPermissionAppConnectionActions.Connect]: true, - [OrgPermissionAppConnectionActions.RotateCredentials]: true - }, - { shouldDirty: true } - ); - break; - case Permission.ReadOnly: - setValue( - "permissions.app-connections", - { - [OrgPermissionAppConnectionActions.Read]: true, - [OrgPermissionAppConnectionActions.Edit]: false, - [OrgPermissionAppConnectionActions.Create]: false, - [OrgPermissionAppConnectionActions.Delete]: false, - [OrgPermissionAppConnectionActions.Connect]: false, - [OrgPermissionAppConnectionActions.RotateCredentials]: false - }, - { shouldDirty: true } - ); - break; - - case Permission.NoAccess: - default: - setValue( - "permissions.app-connections", - { - [OrgPermissionAppConnectionActions.Read]: false, - [OrgPermissionAppConnectionActions.Edit]: false, - [OrgPermissionAppConnectionActions.Create]: false, - [OrgPermissionAppConnectionActions.Delete]: false, - [OrgPermissionAppConnectionActions.Connect]: false, - [OrgPermissionAppConnectionActions.RotateCredentials]: false - }, - { shouldDirty: true } - ); - } - }; - - return ( - <> - setIsRowExpanded.toggle()} - > - - - - App Connections - - - - - {isRowExpanded && ( - - -
- {PERMISSION_ACTIONS.map(({ action, label }) => { - return ( - ( - { - if (!isEditable) { - createNotification({ - type: "error", - text: "Failed to update default role" - }); - return; - } - field.onChange(e); - }} - id={`permissions.app-connections.${action}`} - > - {label} - - )} - /> - ); - })} -
- - - )} - - ); -}; diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionAuditLogsRow.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionAuditLogsRow.tsx deleted file mode 100644 index fec6da56ac3..00000000000 --- a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionAuditLogsRow.tsx +++ /dev/null @@ -1,145 +0,0 @@ -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 { OrgPermissionAuditLogsActions } from "@app/context/OrgPermissionContext/types"; -import { useToggle } from "@app/hooks"; - -import { TFormSchema } from "../OrgRoleModifySection.utils"; - -type Props = { - isEditable: boolean; - setValue: UseFormSetValue; - control: Control; -}; - -enum Permission { - NoAccess = "no-access", - Custom = "custom" -} - -const PERMISSION_ACTIONS = [ - { - action: OrgPermissionAuditLogsActions.Read, - label: "Read" - } -] as const; - -export const OrgPermissionAuditLogsRow = ({ isEditable, control, setValue }: Props) => { - const [isRowExpanded, setIsRowExpanded] = useToggle(); - const [isCustom, setIsCustom] = useToggle(); - - const rule = useWatch({ - control, - name: "permissions.audit-logs" - }); - - const selectedPermissionCategory = useMemo(() => { - const actions = Object.keys(rule || {}) as Array; - 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; - - 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) return; - if (val === Permission.Custom) { - setIsRowExpanded.on(); - setIsCustom.on(); - return; - } - setIsCustom.off(); - - switch (val) { - case Permission.NoAccess: - default: - setValue( - "permissions.audit-logs", - { - [OrgPermissionAuditLogsActions.Read]: false - }, - { shouldDirty: true } - ); - } - }; - - return ( - <> - setIsRowExpanded.toggle()} - > - - - - Audit Logs - - - - - {isRowExpanded && ( - - -
- {PERMISSION_ACTIONS.map(({ action, label }) => { - return ( - ( - { - if (!isEditable) { - createNotification({ - type: "error", - text: "Failed to update default role" - }); - return; - } - field.onChange(e); - }} - id={`permissions.audit-logs.${action}`} - > - {label} - - )} - /> - ); - })} -
- - - )} - - ); -}; diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionBillingRow.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionBillingRow.tsx deleted file mode 100644 index de81119a1e9..00000000000 --- a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionBillingRow.tsx +++ /dev/null @@ -1,172 +0,0 @@ -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 { OrgPermissionBillingActions } from "@app/context/OrgPermissionContext/types"; -import { useToggle } from "@app/hooks"; - -import { TFormSchema } from "../OrgRoleModifySection.utils"; - -const PERMISSION_ACTIONS = [ - { action: OrgPermissionBillingActions.Read, label: "View bills" }, - { action: OrgPermissionBillingActions.ManageBilling, label: "Manage billing" } -] as const; - -type Props = { - isEditable: boolean; - setValue: UseFormSetValue; - control: Control; -}; - -enum Permission { - NoAccess = "no-access", - ReadOnly = "read-only", - FullAccess = "full-access", - Custom = "custom" -} - -export const OrgPermissionBillingRow = ({ isEditable, control, setValue }: Props) => { - const [isRowExpanded, setIsRowExpanded] = useToggle(); - const [isCustom, setIsCustom] = useToggle(); - - const rule = useWatch({ - control, - name: "permissions.billing" - }); - - const selectedPermissionCategory = useMemo(() => { - const actions = Object.keys(rule || {}) as Array; - const totalActions = PERMISSION_ACTIONS.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 (score === 1 && rule?.[OrgPermissionBillingActions.Read]) return Permission.ReadOnly; - return Permission.Custom; - }, [rule, isCustom]); - - useEffect(() => { - if (selectedPermissionCategory === Permission.Custom) setIsCustom.on(); - else setIsCustom.off(); - }, [selectedPermissionCategory]); - - const handlePermissionChange = (val: Permission) => { - if (val === Permission.Custom) { - setIsRowExpanded.on(); - setIsCustom.on(); - return; - } - setIsCustom.off(); - - switch (val) { - case Permission.NoAccess: - setValue( - "permissions.billing", - { - [OrgPermissionBillingActions.Read]: false, - [OrgPermissionBillingActions.ManageBilling]: false - }, - { shouldDirty: true } - ); - break; - case Permission.ReadOnly: - setValue( - "permissions.billing", - { - [OrgPermissionBillingActions.Read]: true, - [OrgPermissionBillingActions.ManageBilling]: false - }, - { shouldDirty: true } - ); - break; - case Permission.FullAccess: - setValue( - "permissions.billing", - { - [OrgPermissionBillingActions.Read]: true, - [OrgPermissionBillingActions.ManageBilling]: true - }, - { shouldDirty: true } - ); - break; - default: - setValue( - "permissions.billing", - { - [OrgPermissionBillingActions.Read]: false, - [OrgPermissionBillingActions.ManageBilling]: false - }, - { shouldDirty: true } - ); - break; - } - }; - - return ( - <> - setIsRowExpanded.toggle()} - > - - - - Billing - - - - - {isRowExpanded && ( - - -
- {PERMISSION_ACTIONS.map(({ action, label }) => { - return ( - ( - { - if (!isEditable) { - createNotification({ - type: "error", - text: "Failed to update default role" - }); - return; - } - field.onChange(e); - }} - id={`permissions.billing.${action}`} - > - {label} - - )} - /> - ); - })} -
- - - )} - - ); -}; diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionEmailDomainRow.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionEmailDomainRow.tsx deleted file mode 100644 index 5cc4d5100e8..00000000000 --- a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionEmailDomainRow.tsx +++ /dev/null @@ -1,166 +0,0 @@ -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 { OrgPermissionEmailDomainActions } from "@app/context/OrgPermissionContext/types"; -import { useToggle } from "@app/hooks"; - -import { TFormSchema } from "../OrgRoleModifySection.utils"; - -const PERMISSION_ACTIONS = [ - { action: OrgPermissionEmailDomainActions.Read, label: "View domains" }, - { action: OrgPermissionEmailDomainActions.Create, label: "Add domains" }, - { action: OrgPermissionEmailDomainActions.VerifyDomain, label: "Verify domains" }, - { action: OrgPermissionEmailDomainActions.Delete, label: "Delete domains" } -] as const; - -type Props = { - isEditable: boolean; - setValue: UseFormSetValue; - control: Control; -}; - -enum Permission { - NoAccess = "no-access", - ReadOnly = "read-only", - FullAccess = "full-access", - Custom = "custom" -} - -export const OrgPermissionEmailDomainRow = ({ isEditable, control, setValue }: Props) => { - const [isRowExpanded, setIsRowExpanded] = useToggle(); - const [isCustom, setIsCustom] = useToggle(); - - const rule = useWatch({ - control, - name: "permissions.email-domains" - }); - - const selectedPermissionCategory = useMemo(() => { - const actions = Object.keys(rule || {}) as Array; - const totalActions = PERMISSION_ACTIONS.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 (score === 1 && rule?.[OrgPermissionEmailDomainActions.Read]) return Permission.ReadOnly; - return Permission.Custom; - }, [rule, isCustom]); - - useEffect(() => { - if (selectedPermissionCategory === Permission.Custom) setIsCustom.on(); - else setIsCustom.off(); - }, [selectedPermissionCategory]); - - const handlePermissionChange = (val: Permission) => { - if (val === Permission.Custom) { - setIsRowExpanded.on(); - setIsCustom.on(); - return; - } - setIsCustom.off(); - - const allFalse = { - [OrgPermissionEmailDomainActions.Read]: false, - [OrgPermissionEmailDomainActions.Create]: false, - [OrgPermissionEmailDomainActions.VerifyDomain]: false, - [OrgPermissionEmailDomainActions.Delete]: false - }; - - switch (val) { - case Permission.NoAccess: - setValue("permissions.email-domains", allFalse, { shouldDirty: true }); - break; - case Permission.ReadOnly: - setValue( - "permissions.email-domains", - { ...allFalse, [OrgPermissionEmailDomainActions.Read]: true }, - { shouldDirty: true } - ); - break; - case Permission.FullAccess: - setValue( - "permissions.email-domains", - { - [OrgPermissionEmailDomainActions.Read]: true, - [OrgPermissionEmailDomainActions.Create]: true, - [OrgPermissionEmailDomainActions.VerifyDomain]: true, - [OrgPermissionEmailDomainActions.Delete]: true - }, - { shouldDirty: true } - ); - break; - default: - setValue("permissions.email-domains", allFalse, { shouldDirty: true }); - break; - } - }; - - return ( - <> - setIsRowExpanded.toggle()} - > - - - - Email Domains - - - - - {isRowExpanded && ( - - -
- {PERMISSION_ACTIONS.map(({ action, label }) => { - return ( - ( - { - if (!isEditable) { - createNotification({ - type: "error", - text: "Failed to update default role" - }); - return; - } - field.onChange(e); - }} - id={`permissions.email-domains.${action}`} - > - {label} - - )} - /> - ); - })} -
- - - )} - - ); -}; diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionGatewayRow.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionGatewayRow.tsx deleted file mode 100644 index 468b2f5f25c..00000000000 --- a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionGatewayRow.tsx +++ /dev/null @@ -1,181 +0,0 @@ -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 { OrgGatewayPermissionActions } from "@app/context/OrgPermissionContext/types"; -import { useToggle } from "@app/hooks"; - -import { TFormSchema } from "../OrgRoleModifySection.utils"; - -type Props = { - isEditable: boolean; - setValue: UseFormSetValue; - control: Control; -}; - -enum Permission { - NoAccess = "no-access", - ReadOnly = "read-only", - FullAccess = "full-access", - Custom = "custom" -} - -const PERMISSION_ACTIONS = [ - { action: OrgGatewayPermissionActions.ListGateways, label: "List Gateways" }, - { action: OrgGatewayPermissionActions.CreateGateways, label: "Create Gateways" }, - { action: OrgGatewayPermissionActions.EditGateways, label: "Edit Gateways" }, - { action: OrgGatewayPermissionActions.DeleteGateways, label: "Delete Gateways" }, - { action: OrgGatewayPermissionActions.AttachGateways, label: "Attach Gateways" } -] as const; - -export const OrgGatewayPermissionRow = ({ isEditable, control, setValue }: Props) => { - const [isRowExpanded, setIsRowExpanded] = useToggle(); - const [isCustom, setIsCustom] = useToggle(); - - const rule = useWatch({ - control, - name: "permissions.gateway" - }); - - const selectedPermissionCategory = useMemo(() => { - const actions = Object.keys(rule || {}) as Array; - const totalActions = PERMISSION_ACTIONS.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 (score === 1 && rule?.[OrgGatewayPermissionActions.ListGateways]) 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) return; - if (val === Permission.Custom) { - setIsRowExpanded.on(); - setIsCustom.on(); - return; - } - setIsCustom.off(); - - switch (val) { - case Permission.FullAccess: - setValue( - "permissions.gateway", - { - [OrgGatewayPermissionActions.ListGateways]: true, - [OrgGatewayPermissionActions.EditGateways]: true, - [OrgGatewayPermissionActions.CreateGateways]: true, - [OrgGatewayPermissionActions.DeleteGateways]: true - }, - { shouldDirty: true } - ); - break; - case Permission.ReadOnly: - setValue( - "permissions.gateway", - { - [OrgGatewayPermissionActions.ListGateways]: true, - [OrgGatewayPermissionActions.EditGateways]: false, - [OrgGatewayPermissionActions.CreateGateways]: false, - [OrgGatewayPermissionActions.DeleteGateways]: false - }, - { shouldDirty: true } - ); - break; - - case Permission.NoAccess: - default: - setValue( - "permissions.gateway", - { - [OrgGatewayPermissionActions.ListGateways]: false, - [OrgGatewayPermissionActions.EditGateways]: false, - [OrgGatewayPermissionActions.CreateGateways]: false, - [OrgGatewayPermissionActions.DeleteGateways]: false - }, - { shouldDirty: true } - ); - } - }; - - return ( - <> - setIsRowExpanded.toggle()} - > - - - - Gateways - - - - - {isRowExpanded && ( - - -
- {PERMISSION_ACTIONS.map(({ action, label }) => { - return ( - ( - { - if (!isEditable) { - createNotification({ - type: "error", - text: "Failed to update default role" - }); - return; - } - field.onChange(e); - }} - id={`permissions.gateways.${action}`} - > - {label} - - )} - /> - ); - })} -
- - - )} - - ); -}; diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionGroupRow.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionGroupRow.tsx deleted file mode 100644 index 04e38a53329..00000000000 --- a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionGroupRow.tsx +++ /dev/null @@ -1,205 +0,0 @@ -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 { OrgPermissionGroupActions } from "@app/context/OrgPermissionContext/types"; -import { useToggle } from "@app/hooks"; - -import { TFormSchema } from "../OrgRoleModifySection.utils"; - -const PERMISSION_ACTIONS = [ - { action: OrgPermissionGroupActions.Read, label: "Read Groups" }, - { action: OrgPermissionGroupActions.Create, label: "Create Groups" }, - { action: OrgPermissionGroupActions.Edit, label: "Edit Groups" }, - { action: OrgPermissionGroupActions.Delete, label: "Delete Groups" }, - { action: OrgPermissionGroupActions.GrantPrivileges, label: "Grant Privileges" }, - { action: OrgPermissionGroupActions.AddMembers, label: "Add Members" }, - { action: OrgPermissionGroupActions.RemoveMembers, label: "Remove Members" } -] as const; - -type Props = { - isEditable: boolean; - setValue: UseFormSetValue; - control: Control; -}; - -enum Permission { - NoAccess = "no-access", - ReadOnly = "read-only", - FullAccess = "full-access", - Custom = "custom" -} - -export const OrgPermissionGroupRow = ({ isEditable, control, setValue }: Props) => { - const [isRowExpanded, setIsRowExpanded] = useToggle(); - const [isCustom, setIsCustom] = useToggle(); - - const rule = useWatch({ - control, - name: "permissions.groups" - }); - - const selectedPermissionCategory = useMemo(() => { - const actions = Object.keys(rule || {}) as Array; - const totalActions = PERMISSION_ACTIONS.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 (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.groups", - { - [OrgPermissionGroupActions.Read]: false, - [OrgPermissionGroupActions.Create]: false, - [OrgPermissionGroupActions.Edit]: false, - [OrgPermissionGroupActions.Delete]: false, - [OrgPermissionGroupActions.GrantPrivileges]: false, - [OrgPermissionGroupActions.AddMembers]: false, - [OrgPermissionGroupActions.RemoveMembers]: false - }, - { shouldDirty: true } - ); - break; - case Permission.FullAccess: - setValue( - "permissions.groups", - { - [OrgPermissionGroupActions.Read]: true, - [OrgPermissionGroupActions.Create]: true, - [OrgPermissionGroupActions.Edit]: true, - [OrgPermissionGroupActions.Delete]: true, - [OrgPermissionGroupActions.GrantPrivileges]: true, - [OrgPermissionGroupActions.AddMembers]: true, - [OrgPermissionGroupActions.RemoveMembers]: true - }, - { shouldDirty: true } - ); - break; - case Permission.ReadOnly: - setValue( - "permissions.groups", - { - [OrgPermissionGroupActions.Read]: true, - [OrgPermissionGroupActions.Edit]: false, - [OrgPermissionGroupActions.Create]: false, - [OrgPermissionGroupActions.Delete]: false, - [OrgPermissionGroupActions.GrantPrivileges]: false, - [OrgPermissionGroupActions.AddMembers]: false, - [OrgPermissionGroupActions.RemoveMembers]: false - }, - { shouldDirty: true } - ); - break; - default: - setValue( - "permissions.groups", - { - [OrgPermissionGroupActions.Read]: false, - [OrgPermissionGroupActions.Edit]: false, - [OrgPermissionGroupActions.Create]: false, - [OrgPermissionGroupActions.Delete]: false, - [OrgPermissionGroupActions.GrantPrivileges]: false, - [OrgPermissionGroupActions.AddMembers]: false, - [OrgPermissionGroupActions.RemoveMembers]: false - }, - { shouldDirty: true } - ); - break; - } - }; - - return ( - <> - setIsRowExpanded.toggle()} - > - - - - Group Management - - - - - {isRowExpanded && ( - - -
- {PERMISSION_ACTIONS.map(({ action, label }) => { - return ( - ( - { - if (!isEditable) { - createNotification({ - type: "error", - text: "Failed to update default role" - }); - return; - } - field.onChange(e); - }} - id={`permissions.groups.${action}`} - > - {label} - - )} - /> - ); - })} -
- - - )} - - ); -}; diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionIdentityRow.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionIdentityRow.tsx deleted file mode 100644 index 0dbe4d30c7b..00000000000 --- a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionIdentityRow.tsx +++ /dev/null @@ -1,215 +0,0 @@ -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 { OrgPermissionIdentityActions } from "@app/context/OrgPermissionContext/types"; -import { useToggle } from "@app/hooks"; - -import { TFormSchema } from "../OrgRoleModifySection.utils"; - -const PERMISSION_ACTIONS = [ - { action: OrgPermissionIdentityActions.Read, label: "Read Identities" }, - { action: OrgPermissionIdentityActions.Create, label: "Create Identities" }, - { action: OrgPermissionIdentityActions.Edit, label: "Edit Identities" }, - { action: OrgPermissionIdentityActions.Delete, label: "Delete Identities" }, - { action: OrgPermissionIdentityActions.GrantPrivileges, label: "Grant Privileges" }, - { action: OrgPermissionIdentityActions.RevokeAuth, label: "Revoke Auth" }, - { action: OrgPermissionIdentityActions.CreateToken, label: "Create Token" }, - { action: OrgPermissionIdentityActions.GetToken, label: "Get Token" }, - { action: OrgPermissionIdentityActions.DeleteToken, label: "Delete Token" } -] as const; - -type Props = { - isEditable: boolean; - setValue: UseFormSetValue; - control: Control; -}; - -enum Permission { - NoAccess = "no-access", - ReadOnly = "read-only", - FullAccess = "full-access", - Custom = "custom" -} - -export const OrgPermissionIdentityRow = ({ isEditable, control, setValue }: Props) => { - const [isRowExpanded, setIsRowExpanded] = useToggle(); - const [isCustom, setIsCustom] = useToggle(); - - const rule = useWatch({ - control, - name: "permissions.identity" - }); - - const selectedPermissionCategory = useMemo(() => { - const actions = Object.keys(rule || {}) as Array; - const totalActions = PERMISSION_ACTIONS.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 (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.identity", - { - [OrgPermissionIdentityActions.Read]: false, - [OrgPermissionIdentityActions.Edit]: false, - [OrgPermissionIdentityActions.Create]: false, - [OrgPermissionIdentityActions.Delete]: false, - [OrgPermissionIdentityActions.GrantPrivileges]: false, - [OrgPermissionIdentityActions.RevokeAuth]: false, - [OrgPermissionIdentityActions.CreateToken]: false, - [OrgPermissionIdentityActions.GetToken]: false, - [OrgPermissionIdentityActions.DeleteToken]: false - }, - { shouldDirty: true } - ); - break; - case Permission.FullAccess: - setValue( - "permissions.identity", - { - [OrgPermissionIdentityActions.Read]: true, - [OrgPermissionIdentityActions.Edit]: true, - [OrgPermissionIdentityActions.Create]: true, - [OrgPermissionIdentityActions.Delete]: true, - [OrgPermissionIdentityActions.GrantPrivileges]: true, - [OrgPermissionIdentityActions.RevokeAuth]: true, - [OrgPermissionIdentityActions.CreateToken]: true, - [OrgPermissionIdentityActions.GetToken]: true, - [OrgPermissionIdentityActions.DeleteToken]: true - }, - { shouldDirty: true } - ); - break; - case Permission.ReadOnly: - setValue( - "permissions.identity", - { - [OrgPermissionIdentityActions.Read]: true, - [OrgPermissionIdentityActions.Edit]: false, - [OrgPermissionIdentityActions.Create]: false, - [OrgPermissionIdentityActions.Delete]: false, - [OrgPermissionIdentityActions.GrantPrivileges]: false, - [OrgPermissionIdentityActions.RevokeAuth]: false, - [OrgPermissionIdentityActions.CreateToken]: false, - [OrgPermissionIdentityActions.GetToken]: false, - [OrgPermissionIdentityActions.DeleteToken]: false - }, - { shouldDirty: true } - ); - break; - default: - setValue( - "permissions.identity", - { - [OrgPermissionIdentityActions.Read]: false, - [OrgPermissionIdentityActions.Edit]: false, - [OrgPermissionIdentityActions.Create]: false, - [OrgPermissionIdentityActions.Delete]: false, - [OrgPermissionIdentityActions.GrantPrivileges]: false, - [OrgPermissionIdentityActions.RevokeAuth]: false, - [OrgPermissionIdentityActions.CreateToken]: false, - [OrgPermissionIdentityActions.GetToken]: false, - [OrgPermissionIdentityActions.DeleteToken]: false - }, - { shouldDirty: true } - ); - break; - } - }; - - return ( - <> - setIsRowExpanded.toggle()} - > - - - - Machine Identity Management - - - - - {isRowExpanded && ( - - -
- {PERMISSION_ACTIONS.map(({ action, label }) => { - return ( - ( - { - if (!isEditable) { - createNotification({ - type: "error", - text: "Failed to update default role" - }); - return; - } - field.onChange(e); - }} - id={`permissions.identity.${action}`} - > - {label} - - )} - /> - ); - })} -
- - - )} - - ); -}; diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionKmipRow.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionKmipRow.tsx deleted file mode 100644 index 22aa6f965f4..00000000000 --- a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionKmipRow.tsx +++ /dev/null @@ -1,128 +0,0 @@ -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 "../OrgRoleModifySection.utils"; - -type Props = { - isEditable: boolean; - setValue: UseFormSetValue; - control: Control; -}; - -enum Permission { - NoAccess = "no-access", - Custom = "custom" -} - -const PERMISSION_ACTIONS = [{ action: "proxy", label: "Proxy KMIP requests" }] as const; - -export const OrgPermissionKmipRow = ({ isEditable, control, setValue }: Props) => { - const [isRowExpanded, setIsRowExpanded] = useToggle(); - const [isCustom, setIsCustom] = useToggle(); - - const rule = useWatch({ - control, - name: "permissions.kmip" - }); - - const selectedPermissionCategory = useMemo(() => { - if (rule?.proxy) { - return Permission.Custom; - } - return Permission.NoAccess; - }, [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) return; - if (val === Permission.Custom) { - setIsRowExpanded.on(); - setIsCustom.on(); - return; - } - setIsCustom.off(); - - if (val === Permission.NoAccess) { - setValue("permissions.kmip", { proxy: false }, { shouldDirty: true }); - } - }; - - return ( - <> - setIsRowExpanded.toggle()} - > - - - - KMIP - - - - - {isRowExpanded && ( - - -
- {PERMISSION_ACTIONS.map(({ action, label }) => { - return ( - ( - { - if (!isEditable) { - createNotification({ - type: "error", - text: "Failed to update default role" - }); - return; - } - field.onChange(e); - }} - id={`permissions.kmip.${action}`} - > - {label} - - )} - /> - ); - })} -
- - - )} - - ); -}; diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionMachineIdentityAuthTemplateRow.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionMachineIdentityAuthTemplateRow.tsx deleted file mode 100644 index 46908889aad..00000000000 --- a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionMachineIdentityAuthTemplateRow.tsx +++ /dev/null @@ -1,211 +0,0 @@ -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 { OrgPermissionMachineIdentityAuthTemplateActions } from "@app/context/OrgPermissionContext/types"; -import { useToggle } from "@app/hooks"; - -import { TFormSchema } from "../OrgRoleModifySection.utils"; - -type Props = { - isEditable: boolean; - setValue: UseFormSetValue; - control: Control; -}; - -enum Permission { - NoAccess = "no-access", - ReadOnly = "read-only", - FullAccess = "full-access", - Custom = "custom" -} - -const PERMISSION_ACTIONS = [ - { - action: OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates, - label: "List Templates" - }, - { - action: OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates, - label: "Create Templates" - }, - { - action: OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates, - label: "Edit Templates" - }, - { - action: OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates, - label: "Delete Templates" - }, - { - action: OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates, - label: "Unlink Templates" - }, - { - action: OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates, - label: "Attach Templates" - } -] as const; - -export const OrgPermissionMachineIdentityAuthTemplateRow = ({ - isEditable, - control, - setValue -}: Props) => { - const [isRowExpanded, setIsRowExpanded] = useToggle(); - const [isCustom, setIsCustom] = useToggle(); - - const rule = useWatch({ - control, - name: "permissions.machine-identity-auth-template" - }); - - const selectedPermissionCategory = useMemo(() => { - const actions = Object.keys(rule || {}) as Array; - const totalActions = PERMISSION_ACTIONS.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 (score === 1 && rule?.[OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates]) - 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) return; - if (val === Permission.Custom) { - setIsRowExpanded.on(); - setIsCustom.on(); - return; - } - setIsCustom.off(); - - switch (val) { - case Permission.FullAccess: - setValue( - "permissions.machine-identity-auth-template", - { - [OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates]: true, - [OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates]: true, - [OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates]: true, - [OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates]: true, - [OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: true, - [OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates]: true - }, - { shouldDirty: true } - ); - break; - case Permission.ReadOnly: - setValue( - "permissions.machine-identity-auth-template", - { - [OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates]: true, - [OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates]: false, - [OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates]: false, - [OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates]: false, - [OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: false, - [OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates]: true - }, - { shouldDirty: true } - ); - break; - - case Permission.NoAccess: - default: - setValue( - "permissions.machine-identity-auth-template", - { - [OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates]: false, - [OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates]: false, - [OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates]: false, - [OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates]: false, - [OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: false, - [OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates]: false - }, - { shouldDirty: true } - ); - } - }; - - return ( - <> - setIsRowExpanded.toggle()} - > - - - - Machine Identity Auth Templates - - - - - {isRowExpanded && ( - - -
- {PERMISSION_ACTIONS.map(({ action, label }) => { - return ( - ( - { - if (!isEditable) { - createNotification({ - type: "error", - text: "Failed to update default role" - }); - return; - } - field.onChange(e); - }} - id={`permissions.machine-identity-auth-template.${action}`} - > - {label} - - )} - /> - ); - })} -
- - - )} - - ); -}; diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionQuickSelect.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionQuickSelect.tsx new file mode 100644 index 00000000000..a6fa09d545f --- /dev/null +++ b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionQuickSelect.tsx @@ -0,0 +1,97 @@ +import { useMemo } from "react"; +import { useFormContext, useWatch } from "react-hook-form"; + +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@app/components/v3"; +import { OrgPermissionActions } from "@app/context/OrgPermissionContext/types"; + +import { TFormSchema, TOrgPermissionAction, TPermissionsKey } from "../OrgRoleModifySection.utils"; + +enum Permission { + NoAccess = "no-access", + ReadOnly = "read-only", + FullAccess = "full-access", + Custom = "custom" +} + +type Props = { + subject: TPermissionsKey; + actions: readonly TOrgPermissionAction[]; + isDisabled?: boolean; +}; + +export const OrgPermissionQuickSelect = ({ subject, actions, isDisabled }: Props) => { + const { setValue, trigger, getValues } = useFormContext(); + const permissions = useWatch({ name: "permissions" }); + const rule = permissions?.[subject]?.[0] as Record | undefined; + + const selectedPermissionCategory = useMemo(() => { + if (!rule) return Permission.NoAccess; + const score = actions.filter(({ value }) => rule[value]).length; + if (score === 0) return Permission.NoAccess; + if (score === actions.length) return Permission.FullAccess; + if (score === 1 && rule[OrgPermissionActions.Read]) return Permission.ReadOnly; + return Permission.Custom; + }, [rule, actions]); + + const selectedCount = actions.filter(({ value }) => rule?.[value]).length; + + const setSubjectPermission = ( + value: NonNullable[TPermissionsKey] + ) => { + const current = getValues("permissions") ?? {}; + setValue( + "permissions", + { ...current, [subject]: value } as NonNullable, + { + shouldDirty: true + } + ); + trigger("permissions"); + }; + + const handlePermissionChange = (val: Permission) => { + if (val === Permission.Custom) return; + + if (val === Permission.NoAccess) { + setSubjectPermission(undefined); + return; + } + + const allTrue = Object.fromEntries(actions.map(({ value }) => [value, true])); + + const next = + val === Permission.FullAccess + ? allTrue + : { + ...Object.fromEntries(actions.map(({ value }) => [value, false])), + [OrgPermissionActions.Read]: true + }; + + setSubjectPermission([next] as NonNullable[TPermissionsKey]); + }; + + return ( + + ); +}; diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionRelayRow.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionRelayRow.tsx deleted file mode 100644 index 238a03ff5d1..00000000000 --- a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionRelayRow.tsx +++ /dev/null @@ -1,180 +0,0 @@ -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 { OrgRelayPermissionActions } from "@app/context/OrgPermissionContext/types"; -import { useToggle } from "@app/hooks"; - -import { TFormSchema } from "../OrgRoleModifySection.utils"; - -type Props = { - isEditable: boolean; - setValue: UseFormSetValue; - control: Control; -}; - -enum Permission { - NoAccess = "no-access", - ReadOnly = "read-only", - FullAccess = "full-access", - Custom = "custom" -} - -const PERMISSION_ACTIONS = [ - { action: OrgRelayPermissionActions.ListRelays, label: "List Relays" }, - { action: OrgRelayPermissionActions.CreateRelays, label: "Create Relays" }, - { action: OrgRelayPermissionActions.EditRelays, label: "Edit Relays" }, - { action: OrgRelayPermissionActions.DeleteRelays, label: "Delete Relays" } -] as const; - -export const OrgRelayPermissionRow = ({ isEditable, control, setValue }: Props) => { - const [isRowExpanded, setIsRowExpanded] = useToggle(); - const [isCustom, setIsCustom] = useToggle(); - - const rule = useWatch({ - control, - name: "permissions.relay" - }); - - const selectedPermissionCategory = useMemo(() => { - const actions = Object.keys(rule || {}) as Array; - const totalActions = PERMISSION_ACTIONS.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 (score === 1 && rule?.[OrgRelayPermissionActions.ListRelays]) 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) return; - if (val === Permission.Custom) { - setIsRowExpanded.on(); - setIsCustom.on(); - return; - } - setIsCustom.off(); - - switch (val) { - case Permission.FullAccess: - setValue( - "permissions.relay", - { - [OrgRelayPermissionActions.ListRelays]: true, - [OrgRelayPermissionActions.EditRelays]: true, - [OrgRelayPermissionActions.CreateRelays]: true, - [OrgRelayPermissionActions.DeleteRelays]: true - }, - { shouldDirty: true } - ); - break; - case Permission.ReadOnly: - setValue( - "permissions.relay", - { - [OrgRelayPermissionActions.ListRelays]: true, - [OrgRelayPermissionActions.EditRelays]: false, - [OrgRelayPermissionActions.CreateRelays]: false, - [OrgRelayPermissionActions.DeleteRelays]: false - }, - { shouldDirty: true } - ); - break; - - case Permission.NoAccess: - default: - setValue( - "permissions.relay", - { - [OrgRelayPermissionActions.ListRelays]: false, - [OrgRelayPermissionActions.EditRelays]: false, - [OrgRelayPermissionActions.CreateRelays]: false, - [OrgRelayPermissionActions.DeleteRelays]: false - }, - { shouldDirty: true } - ); - } - }; - - return ( - <> - setIsRowExpanded.toggle()} - > - - - - Relays - - - - - {isRowExpanded && ( - - -
- {PERMISSION_ACTIONS.map(({ action, label }) => { - return ( - ( - { - if (!isEditable) { - createNotification({ - type: "error", - text: "Failed to update default role" - }); - return; - } - field.onChange(e); - }} - id={`permissions.relay.${action}`} - > - {label} - - )} - /> - ); - })} -
- - - )} - - ); -}; diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionSecretShareRow.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionSecretShareRow.tsx deleted file mode 100644 index 084687a0899..00000000000 --- a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionSecretShareRow.tsx +++ /dev/null @@ -1,128 +0,0 @@ -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 "../OrgRoleModifySection.utils"; - -type Props = { - isEditable: boolean; - setValue: UseFormSetValue; - control: Control; -}; - -enum Permission { - NoAccess = "no-access", - Custom = "custom" -} - -const PERMISSION_ACTIONS = [{ action: "manage-settings", label: "Manage settings" }] as const; - -export const OrgPermissionSecretShareRow = ({ isEditable, control, setValue }: Props) => { - const [isRowExpanded, setIsRowExpanded] = useToggle(); - const [isCustom, setIsCustom] = useToggle(); - - const rule = useWatch({ - control, - name: "permissions.secret-share" - }); - - const selectedPermissionCategory = useMemo(() => { - if (rule?.["manage-settings"]) { - return Permission.Custom; - } - return Permission.NoAccess; - }, [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) return; - if (val === Permission.Custom) { - setIsRowExpanded.on(); - setIsCustom.on(); - return; - } - setIsCustom.off(); - - if (val === Permission.NoAccess) { - setValue("permissions.secret-share", { "manage-settings": false }, { shouldDirty: true }); - } - }; - - return ( - <> - setIsRowExpanded.toggle()} - > - - - - Secret Share - - - - - {isRowExpanded && ( - - -
- {PERMISSION_ACTIONS.map(({ action, label }) => { - return ( - ( - { - if (!isEditable) { - createNotification({ - type: "error", - text: "Failed to update default role" - }); - return; - } - field.onChange(e); - }} - id={`permissions.secret-share.${action}`} - > - {label} - - )} - /> - ); - })} -
- - - )} - - ); -}; diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionSsoRow.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionSsoRow.tsx deleted file mode 100644 index 5db78ae6bbe..00000000000 --- a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionSsoRow.tsx +++ /dev/null @@ -1,195 +0,0 @@ -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 { OrgPermissionSsoActions } from "@app/context/OrgPermissionContext/types"; -import { useToggle } from "@app/hooks"; - -import { TFormSchema } from "../OrgRoleModifySection.utils"; - -const PERMISSION_ACTIONS = [ - { action: OrgPermissionSsoActions.Read, label: "View" }, - { action: OrgPermissionSsoActions.Create, label: "Create" }, - { action: OrgPermissionSsoActions.Edit, label: "Modify" }, - { action: OrgPermissionSsoActions.Delete, label: "Remove" }, - { action: OrgPermissionSsoActions.BypassSsoEnforcement, label: "Bypass SSO Enforcement" } -] as const; - -type Props = { - isEditable: boolean; - setValue: UseFormSetValue; - control: Control; -}; - -enum Permission { - NoAccess = "no-access", - ReadOnly = "read-only", - FullAccess = "full-access", - Custom = "custom" -} - -export const OrgPermissionSsoRow = ({ isEditable, control, setValue }: Props) => { - const [isRowExpanded, setIsRowExpanded] = useToggle(); - const [isCustom, setIsCustom] = useToggle(); - - const rule = useWatch({ - control, - name: "permissions.sso" - }); - - const selectedPermissionCategory = useMemo(() => { - const actions = Object.keys(rule || {}) as Array; - const totalActions = PERMISSION_ACTIONS.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 (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.sso", - { - [OrgPermissionSsoActions.Read]: false, - [OrgPermissionSsoActions.Create]: false, - [OrgPermissionSsoActions.Edit]: false, - [OrgPermissionSsoActions.Delete]: false, - [OrgPermissionSsoActions.BypassSsoEnforcement]: false - }, - { shouldDirty: true } - ); - break; - case Permission.FullAccess: - setValue( - "permissions.sso", - { - [OrgPermissionSsoActions.Read]: true, - [OrgPermissionSsoActions.Create]: true, - [OrgPermissionSsoActions.Edit]: true, - [OrgPermissionSsoActions.Delete]: true, - [OrgPermissionSsoActions.BypassSsoEnforcement]: true - }, - { shouldDirty: true } - ); - break; - case Permission.ReadOnly: - setValue( - "permissions.sso", - { - [OrgPermissionSsoActions.Read]: true, - [OrgPermissionSsoActions.Create]: false, - [OrgPermissionSsoActions.Edit]: false, - [OrgPermissionSsoActions.Delete]: false, - [OrgPermissionSsoActions.BypassSsoEnforcement]: false - }, - { shouldDirty: true } - ); - break; - default: - setValue( - "permissions.sso", - { - [OrgPermissionSsoActions.Read]: false, - [OrgPermissionSsoActions.Create]: false, - [OrgPermissionSsoActions.Edit]: false, - [OrgPermissionSsoActions.Delete]: false, - [OrgPermissionSsoActions.BypassSsoEnforcement]: false - }, - { shouldDirty: true } - ); - break; - } - }; - - return ( - <> - setIsRowExpanded.toggle()} - > - - - - SSO - - - - - {isRowExpanded && ( - - -
- {PERMISSION_ACTIONS.map(({ action, label }) => { - return ( - ( - { - if (!isEditable) { - createNotification({ - type: "error", - text: "Failed to update default role" - }); - return; - } - field.onChange(e); - }} - id={`permissions.sso.${action}`} - > - {label} - - )} - /> - ); - })} -
- - - )} - - ); -}; diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionSubOrgRow.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionSubOrgRow.tsx deleted file mode 100644 index 7cad2eb1bf5..00000000000 --- a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionSubOrgRow.tsx +++ /dev/null @@ -1,171 +0,0 @@ -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 { OrgPermissionSubOrgActions } from "@app/context/OrgPermissionContext/types"; -import { useToggle } from "@app/hooks"; - -import { TFormSchema } from "../OrgRoleModifySection.utils"; - -const PERMISSION_ACTIONS = [ - { action: OrgPermissionSubOrgActions.Create, label: "Create" }, - { action: OrgPermissionSubOrgActions.Edit, label: "Edit" }, - { action: OrgPermissionSubOrgActions.Delete, label: "Delete" }, - { action: OrgPermissionSubOrgActions.DirectAccess, label: "Direct Access" }, - { action: OrgPermissionSubOrgActions.LinkGroup, label: "Link Group" } -] as const; - -type Props = { - isEditable: boolean; - setValue: UseFormSetValue; - control: Control; -}; - -enum Permission { - NoAccess = "no-access", - FullAccess = "full-access", - Custom = "custom" -} - -export const OrgPermissionSubOrgRow = ({ isEditable, control, setValue }: Props) => { - const [isRowExpanded, setIsRowExpanded] = useToggle(); - const [isCustom, setIsCustom] = useToggle(); - - const rule = useWatch({ - control, - name: "permissions.sub-organization" - }); - - const selectedPermissionCategory = useMemo(() => { - const actions = Object.keys(rule || {}) as Array; - const totalActions = PERMISSION_ACTIONS.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; - }, [rule, isCustom]); - - useEffect(() => { - if (selectedPermissionCategory === Permission.Custom) setIsCustom.on(); - else setIsCustom.off(); - }, [selectedPermissionCategory]); - - const handlePermissionChange = (val: Permission) => { - if (val === Permission.Custom) { - setIsRowExpanded.on(); - setIsCustom.on(); - return; - } - setIsCustom.off(); - - switch (val) { - case Permission.NoAccess: - setValue( - "permissions.sub-organization", - { - [OrgPermissionSubOrgActions.Create]: false, - [OrgPermissionSubOrgActions.Edit]: false, - [OrgPermissionSubOrgActions.Delete]: false, - [OrgPermissionSubOrgActions.DirectAccess]: false, - [OrgPermissionSubOrgActions.LinkGroup]: false - }, - { shouldDirty: true } - ); - break; - case Permission.FullAccess: - setValue( - "permissions.sub-organization", - { - [OrgPermissionSubOrgActions.Create]: true, - [OrgPermissionSubOrgActions.Edit]: true, - [OrgPermissionSubOrgActions.Delete]: true, - [OrgPermissionSubOrgActions.DirectAccess]: true, - [OrgPermissionSubOrgActions.LinkGroup]: true - }, - { shouldDirty: true } - ); - break; - default: - setValue( - "permissions.sub-organization", - { - [OrgPermissionSubOrgActions.Create]: true, - [OrgPermissionSubOrgActions.Edit]: true, - [OrgPermissionSubOrgActions.Delete]: true, - [OrgPermissionSubOrgActions.DirectAccess]: true, - [OrgPermissionSubOrgActions.LinkGroup]: true - }, - { shouldDirty: true } - ); - break; - } - }; - - return ( - <> - setIsRowExpanded.toggle()} - > - - - - Sub-Organizations - - - - - {isRowExpanded && ( - - -
- {PERMISSION_ACTIONS.map(({ action, label }) => { - return ( - ( - { - if (!isEditable) { - createNotification({ - type: "error", - text: "Failed to update default role" - }); - return; - } - field.onChange(e); - }} - id={`permissions.sub-organization.${action}`} - > - {label} - - )} - /> - ); - })} -
- - - )} - - ); -}; diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPolicySelectionModal.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPolicySelectionModal.tsx new file mode 100644 index 00000000000..2b5da3cae52 --- /dev/null +++ b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPolicySelectionModal.tsx @@ -0,0 +1,94 @@ +import { useFormContext, useWatch } from "react-hook-form"; +import { CheckIcon } from "lucide-react"; + +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + Popover, + PopoverContent, + PopoverTrigger +} from "@app/components/v3"; +import { cn } from "@app/components/v3/utils"; + +import { ORG_PERMISSION_OBJECT, TFormSchema } from "../OrgRoleModifySection.utils"; + +type Props = { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; + children: React.ReactNode; + invalidSubjects?: string[]; +}; + +const Content = ({ invalidSubjects }: { invalidSubjects?: string[] }) => { + const form = useFormContext(); + const currentPermissions = useWatch({ control: form.control, name: "permissions" }); + + const handleSelectPolicy = (subject: string) => { + const existingValue = form.getValues(`permissions.${subject}` as never) as unknown[]; + if (existingValue?.length > 0) { + form.setValue(`permissions.${subject}` as never, undefined as never, { shouldDirty: true }); + } else { + form.setValue(`permissions.${subject}` as never, [{}] as never, { shouldDirty: true }); + } + }; + + const filteredSubjects = Object.entries(ORG_PERMISSION_OBJECT) + .filter(([subject]) => !invalidSubjects?.includes(subject)) + .sort((a, b) => a[1].title.localeCompare(b[1].title)) + .map(([subject]) => subject); + + return ( + { + if (keywords?.some((k) => k.toLowerCase().includes(search.toLowerCase()))) return 1; + return 0; + }} + > + + + No policies found. + + {filteredSubjects.map((subject) => { + const hasPolicy = + (currentPermissions?.[subject as keyof typeof currentPermissions] as unknown[]) + ?.length > 0; + + return ( + + + {ORG_PERMISSION_OBJECT[subject].title} + + ); + })} + + + + ); +}; + +export const OrgPolicySelectionPopover = ({ + isOpen, + onOpenChange, + children, + invalidSubjects +}: Props) => { + return ( + + {children} + e.stopPropagation()}> + + + + ); +}; diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgRoleWorkspaceRow.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgRoleWorkspaceRow.tsx deleted file mode 100644 index aa70a2e0832..00000000000 --- a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgRoleWorkspaceRow.tsx +++ /dev/null @@ -1,128 +0,0 @@ -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 "../OrgRoleModifySection.utils"; - -type Props = { - isEditable: boolean; - setValue: UseFormSetValue; - control: Control; -}; - -enum Permission { - NoAccess = "no-access", - Custom = "custom" -} - -const PERMISSION_ACTIONS = [{ action: "create", label: "Create projects" }] as const; - -export const OrgRoleWorkspaceRow = ({ isEditable, control, setValue }: Props) => { - const [isRowExpanded, setIsRowExpanded] = useToggle(); - const [isCustom, setIsCustom] = useToggle(); - - const rule = useWatch({ - control, - name: "permissions.project" - }); - - const selectedPermissionCategory = useMemo(() => { - if (rule?.create) { - return Permission.Custom; - } - return Permission.NoAccess; - }, [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) return; - if (val === Permission.Custom) { - setIsRowExpanded.on(); - setIsCustom.on(); - return; - } - setIsCustom.off(); - - if (val === Permission.NoAccess) { - setValue("permissions.project", { create: false }, { shouldDirty: true }); - } - }; - - return ( - <> - setIsRowExpanded.toggle()} - > - - - - Project - - - - - {isRowExpanded && ( - - -
- {PERMISSION_ACTIONS.map(({ action, label }) => { - return ( - ( - { - if (!isEditable) { - createNotification({ - type: "error", - text: "Failed to update default role" - }); - return; - } - field.onChange(e); - }} - id={`permissions.organization-admin-console.${action}`} - > - {label} - - )} - /> - ); - })} -
- - - )} - - ); -}; diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionRow.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionRow.tsx deleted file mode 100644 index d48ee46fe09..00000000000 --- a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionRow.tsx +++ /dev/null @@ -1,229 +0,0 @@ -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 { OrgPermissionSubjects } from "@app/context"; -import { useToggle } from "@app/hooks"; - -import { TFormSchema } from "../OrgRoleModifySection.utils"; - -const PERMISSIONS = [ - { action: "read", label: "View" }, - { action: "create", label: "Create" }, - { action: "edit", label: "Modify" }, - { action: "delete", label: "Remove" } -] as const; - -const SECRET_SCANNING_PERMISSIONS = [ - { action: "read", label: "View risks" }, - { action: "create", label: "Add integrations" }, - { action: "edit", label: "Edit risk status" }, - { action: "delete", label: "Remove integrations" } -] as const; - -const INCIDENT_CONTACTS_PERMISSIONS = [ - { action: "read", label: "View contacts" }, - { action: "create", label: "Add new contacts" }, - { action: "edit", label: "Edit contacts" }, - { action: "delete", label: "Remove contacts" } -] 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 PROJECT_TEMPLATES_PERMISSIONS = [ - { action: "read", label: "View & Apply" }, - { action: "create", label: "Create" }, - { action: "edit", label: "Modify" }, - { action: "delete", label: "Remove" } -] as const; - -const getPermissionList = (formName: Props["formName"]) => { - switch (formName) { - case "member": - return MEMBERS_PERMISSIONS; - case OrgPermissionSubjects.ProjectTemplates: - return PROJECT_TEMPLATES_PERMISSIONS; - case "secret-scanning": - return SECRET_SCANNING_PERMISSIONS; - case "incident-contact": - return INCIDENT_CONTACTS_PERMISSIONS; - default: - return PERMISSIONS; - } -}; - -type Props = { - isEditable: boolean; - title: string; - formName: keyof Omit< - Exclude, - | "project" - | "organization-admin-console" - | "kmip" - | "gateway" - | "relay" - | "secret-share" - | "billing" - | "audit-logs" - | "machine-identity-auth-template" - | "sub-organization" - | "sso" - | "email-domains" - >; - 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(() => { - const actions = Object.keys(rule || {}) as Array; - const totalActions = 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 (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 }) => { - return ( - ( - { - if (!isEditable) { - createNotification({ - type: "error", - text: "Failed to update default role" - }); - return; - } - field.onChange(e); - }} - id={`permissions.${formName}.${action}`} - > - {label} - - )} - /> - ); - })} -
- - - )} - - ); -}; diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionsSection.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionsSection.tsx index e30080763c3..2997fcc4d79 100644 --- a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionsSection.tsx +++ b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionsSection.tsx @@ -1,79 +1,31 @@ -import { useForm } from "react-hook-form"; -import { faSave } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useState } from "react"; +import { FormProvider, useForm, useWatch } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; +import { SaveIcon } from "lucide-react"; import { createNotification } from "@app/components/notifications"; -import { Button, Table, TableContainer, TBody } from "@app/components/v2"; +import { + Accordion, + Button, + Empty, + EmptyDescription, + EmptyHeader, + EmptyTitle +} from "@app/components/v3"; import { OrgPermissionSubjects, useOrganization } from "@app/context"; import { useGetOrgRole, useUpdateOrgRole } from "@app/hooks/api"; -import { OrgPermissionAppConnectionRow } from "@app/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionAppConnectionRow"; +import { GeneralPermissionPolicies } from "@app/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies"; import { formRolePermission2API, formSchema, + ORG_PERMISSION_OBJECT, rolePermission2Form, - TFormSchema + TFormSchema, + TPermissionsKey } from "../OrgRoleModifySection.utils"; -import { OrgPermissionAdminConsoleRow } from "./OrgPermissionAdminConsoleRow"; -import { OrgPermissionAuditLogsRow } from "./OrgPermissionAuditLogsRow"; -import { OrgPermissionBillingRow } from "./OrgPermissionBillingRow"; -import { OrgPermissionEmailDomainRow } from "./OrgPermissionEmailDomainRow"; -import { OrgGatewayPermissionRow } from "./OrgPermissionGatewayRow"; -import { OrgPermissionGroupRow } from "./OrgPermissionGroupRow"; -import { OrgPermissionIdentityRow } from "./OrgPermissionIdentityRow"; -import { OrgPermissionKmipRow } from "./OrgPermissionKmipRow"; -import { OrgPermissionMachineIdentityAuthTemplateRow } from "./OrgPermissionMachineIdentityAuthTemplateRow"; -import { OrgRelayPermissionRow } from "./OrgPermissionRelayRow"; -import { OrgPermissionSecretShareRow } from "./OrgPermissionSecretShareRow"; -import { OrgPermissionSsoRow } from "./OrgPermissionSsoRow"; -import { OrgPermissionSubOrgRow } from "./OrgPermissionSubOrgRow"; -import { OrgRoleWorkspaceRow } from "./OrgRoleWorkspaceRow"; -import { RolePermissionRow } from "./RolePermissionRow"; - -const SIMPLE_PERMISSION_OPTIONS = [ - { - title: "User Management", - formName: "member" - }, - { - title: "Role Management", - formName: "role" - }, - { - title: "Incident Contacts", - formName: "incident-contact" - }, - { - title: "Organization Profile", - formName: "settings" - }, - { - title: "Secret Scanning", - formName: "secret-scanning" - }, - { - title: "LDAP", - formName: "ldap" - }, - { - title: "SCIM", - formName: "scim" - }, - { - title: "GitHub Organization Sync", - formName: OrgPermissionSubjects.GithubOrgSync - }, - { - title: "External KMS", - formName: OrgPermissionSubjects.Kms - }, - { title: "Project Templates", formName: OrgPermissionSubjects.ProjectTemplates } -] as const; - -type Props = { - roleId: string; -}; +import { OrgAddPoliciesButton } from "./OrgAddPoliciesButton"; +import { OrgPermissionQuickSelect } from "./OrgPermissionQuickSelect"; const INVALID_SUBORG_PERMISSIONS = [ OrgPermissionSubjects.Sso, @@ -85,22 +37,28 @@ const INVALID_SUBORG_PERMISSIONS = [ OrgPermissionSubjects.SubOrganization ]; +type Props = { + roleId: string; +}; + export const RolePermissionsSection = ({ roleId }: Props) => { const { currentOrg, isRootOrganization } = useOrganization(); const orgId = currentOrg?.id || ""; const { data: role } = useGetOrgRole(orgId, roleId); + const form = useForm({ + defaultValues: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : {}, + resolver: zodResolver(formSchema) + }); + const { - setValue, - control, handleSubmit, formState: { isDirty, isSubmitting }, reset - } = useForm({ - defaultValues: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : {}, - resolver: zodResolver(formSchema) - }); + } = form; + + const [openPolicies, setOpenPolicies] = useState([]); const { mutateAsync: updateRole } = useUpdateOrgRole(); @@ -111,152 +69,128 @@ export const RolePermissionsSection = ({ roleId }: Props) => { ...el, permissions: formRolePermission2API(el.permissions) }); + reset(el); createNotification({ type: "success", text: "Successfully updated role" }); }; + const handleFormSubmit = handleSubmit(onSubmit, (formErrors) => { + if (formErrors.permissions) { + const subjectsWithErrors = Object.keys(formErrors.permissions) as OrgPermissionSubjects[]; + setOpenPolicies((prev) => { + const next = new Set(prev); + subjectsWithErrors.forEach((subject) => next.add(subject)); + return Array.from(next); + }); + } + }); + const isCustomRole = !["admin", "member", "no-access"].includes(role?.slug ?? ""); + const permissions = useWatch({ control: form.control, name: "permissions" }); + + const hasPermissions = Object.values(permissions || {}).some( + (v) => Array.isArray(v) && v.length > 0 + ); + + const invalidSubjectsForAddPolicy = isRootOrganization ? [] : INVALID_SUBORG_PERMISSIONS; + return ( -
-
-
-

Policies

-

Configure granular access policies

-
- {isCustomRole && ( -
- {isDirty && ( - - )} - + + +
+
+

Policies

+

Configure granular access policies

- )} -
-
- - - - {SIMPLE_PERMISSION_OPTIONS.filter((el) => - isRootOrganization - ? true - : !INVALID_SUBORG_PERMISSIONS.includes(el.formName as OrgPermissionSubjects) - ).map((permission) => { - return ( - - ); - })} - {isRootOrganization && ( - - )} - - - - - - - {isRootOrganization && ( - + {isCustomRole && ( +
+ {isDirty && ( + )} - - - - - - - {isRootOrganization && ( - + + Save + +
+ - )} -
-
-
-
- +
+
+ )} + +
+ {!hasPermissions && ( + + + No policies applied + + Add policies to configure permissions for this role. + + + + )} + {hasPermissions && ( + + {Object.entries(ORG_PERMISSION_OBJECT) + .filter( + ([subject]) => + isRootOrganization || + !INVALID_SUBORG_PERMISSIONS.includes(subject as OrgPermissionSubjects) + ) + .filter( + ([subject]) => + (permissions?.[subject as keyof typeof permissions] as unknown[])?.length > 0 + ) + .map(([subject, config]) => ( + + } + onRemoveLastRule={ + isCustomRole + ? () => { + const current = form.getValues("permissions") ?? {}; + form.setValue( + "permissions", + { ...current, [subject]: undefined } as NonNullable< + TFormSchema["permissions"] + >, + { shouldDirty: true } + ); + } + : undefined + } + /> + ))} + + )} +
+ + ); }; diff --git a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateEditRoleForm.tsx b/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateEditRoleForm.tsx index 8241851e54d..928e581e5db 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateEditRoleForm.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateEditRoleForm.tsx @@ -12,10 +12,14 @@ import { isCustomProjectRole } from "@app/helpers/roles"; import { TProjectTemplate, useUpdateProjectTemplate } from "@app/hooks/api/projectTemplates"; import { slugSchema } from "@app/lib/schemas"; import { AddPoliciesButton } from "@app/pages/project/RoleDetailsBySlugPage/components/AddPoliciesButton"; -import { GeneralPermissionPolicies } from "@app/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies"; +import { + GeneralPermissionPolicies, + TPermissionAction +} from "@app/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies"; import { PermissionEmptyState } from "@app/pages/project/RoleDetailsBySlugPage/components/PermissionEmptyState"; import { formRolePermission2API, + isConditionalSubjects, PROJECT_PERMISSION_OBJECT, projectRoleFormSchema, rolePermission2Form @@ -200,12 +204,13 @@ export const ProjectTemplateEditRoleForm = ({ {(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => ( {renderConditionalComponents(subject, isDisabled)} diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeModifySection.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeModifySection.tsx index ee3221695dc..d087938e29e 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeModifySection.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeModifySection.tsx @@ -47,6 +47,7 @@ import { GeneralPermissionPolicies } from "@app/pages/project/RoleDetailsBySlugP import { PermissionEmptyState } from "@app/pages/project/RoleDetailsBySlugPage/components/PermissionEmptyState"; import { formRolePermission2API, + isConditionalSubjects, PROJECT_PERMISSION_OBJECT, projectRoleFormSchema, rolePermission2Form @@ -448,6 +449,7 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({ isDisabled={isDisabled} isOpen={openPolicies.includes(permissionSubject)} menuPortalContainerRef={menuPortalContainerRef} + isConditional={isConditionalSubjects(permissionSubject)} > {renderConditionalComponents(permissionSubject, isDisabled)} diff --git a/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx b/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx index d982732fa74..00142f73cbb 100644 --- a/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx +++ b/frontend/src/pages/project/MemberDetailsByIDPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx @@ -46,6 +46,7 @@ import { GeneralPermissionPolicies } from "@app/pages/project/RoleDetailsBySlugP import { PermissionEmptyState } from "@app/pages/project/RoleDetailsBySlugPage/components/PermissionEmptyState"; import { formRolePermission2API, + isConditionalSubjects, PROJECT_PERMISSION_OBJECT, projectRoleFormSchema, rolePermission2Form @@ -444,6 +445,7 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({ isDisabled={isDisabled} isOpen={openPolicies.includes(permissionSubject)} menuPortalContainerRef={menuPortalContainerRef} + isConditional={isConditionalSubjects(permissionSubject)} > {renderConditionalComponents(permissionSubject, isDisabled)} diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx index c6b7c15711d..47c018056e2 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies.tsx @@ -1,4 +1,4 @@ -import { cloneElement, Fragment, RefObject, useMemo } from "react"; +import { cloneElement, Fragment, ReactNode, RefObject, useEffect, useMemo } from "react"; import { Control, Controller, @@ -7,8 +7,7 @@ import { useFormState, useWatch } from "react-hook-form"; -import { components, MultiValueProps, MultiValueRemoveProps, OptionProps } from "react-select"; -import { CheckIcon, NetworkIcon, PlusIcon, TrashIcon } from "lucide-react"; +import { NetworkIcon, PlusIcon, TrashIcon } from "lucide-react"; import { twMerge } from "tailwind-merge"; import { @@ -17,8 +16,8 @@ import { AccordionTrigger, Badge, Button, - FilterableSelect, IconButton, + PermissionActionSelect, Select, SelectContent, SelectItem, @@ -29,98 +28,55 @@ import { TooltipTrigger } from "@app/components/v3"; import { + OrgPermissionSubjects, ProjectPermissionGroupActions, ProjectPermissionIdentityActions, ProjectPermissionMemberActions, ProjectPermissionSub } from "@app/context"; -import { - isConditionalSubjects, - TFormSchema, - TProjectPermissionObject -} from "./ProjectRoleModifySection.utils"; +export type TPermissionAction = { + value: string | number; + label: string; + description?: string; +}; + +type AnyPermissionSubject = ProjectPermissionSub | OrgPermissionSubjects; -type Props = { +type Props = { title: string; description: string; subject: T; - actions: TProjectPermissionObject[T]["actions"]; + actions: readonly TPermissionAction[]; + isConditional?: boolean; + triggerSuffix?: ReactNode; + onRemoveLastRule?: () => void; children?: JSX.Element; isDisabled?: boolean; isOpen?: boolean; - onShowAccessTree?: (subject: ProjectPermissionSub) => void; + onShowAccessTree?: (subject: string) => void; menuPortalContainerRef?: RefObject; }; -type ActionOption = { - label: string; - value: string; - description?: string; -}; - -const OptionWithDescription = (props: OptionProps) => { - const { data, children, isSelected } = props; - - return ( - -
-
-

{children}

- {data.description && ( -

{data.description}

- )} -
- {isSelected && } -
-
- ); -}; - -const MultiValueRemove = ({ selectProps, ...props }: MultiValueRemoveProps) => { - if (selectProps?.isDisabled) { - return null; - } - return ; -}; - -const MultiValueWithTooltip = (props: MultiValueProps) => { - const { data } = props; - - if (!data.description) { - return ; - } - - return ( - - -
- -
-
- {data.description} -
- ); -}; - -type ActionsMultiSelectProps = { - subject: T; +type ActionsMultiSelectProps = { + subject: string; rootIndex: number; - actions: TProjectPermissionObject[T]["actions"]; + actions: readonly TPermissionAction[]; isDisabled?: boolean; - control: Control; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + control: Control; menuPortalContainerRef?: RefObject; }; -const ActionsMultiSelect = ({ +const ActionsMultiSelect = ({ subject, rootIndex, actions, isDisabled, control, menuPortalContainerRef -}: ActionsMultiSelectProps) => { - const { setValue, trigger } = useFormContext(); +}: ActionsMultiSelectProps) => { + const { setValue, trigger } = useFormContext(); const { errors } = useFormState({ control, @@ -134,16 +90,11 @@ const ActionsMultiSelect = ({ defaultValue: {} }); - const secretsRead = Boolean(permissionRule?.read); - const memberGrantPrivileges = Boolean( - permissionRule?.[ProjectPermissionMemberActions.GrantPrivileges] - ); - const identityGrantPrivileges = Boolean( - permissionRule?.[ProjectPermissionIdentityActions.GrantPrivileges] - ); - const groupsGrantPrivileges = Boolean( - permissionRule?.[ProjectPermissionGroupActions.GrantPrivileges] - ); + const rule = permissionRule as Record | undefined; + const secretsRead = Boolean(rule?.read); + const memberGrantPrivileges = Boolean(rule?.[ProjectPermissionMemberActions.GrantPrivileges]); + const identityGrantPrivileges = Boolean(rule?.[ProjectPermissionIdentityActions.GrantPrivileges]); + const groupsGrantPrivileges = Boolean(rule?.[ProjectPermissionGroupActions.GrantPrivileges]); const legacyActionsState = useMemo( () => ({ @@ -201,8 +152,8 @@ const ActionsMultiSelect = ({ ); const selectedActions = useMemo( - () => actionOptions.filter((opt) => Boolean(permissionRule?.[opt.value])), - [actionOptions, permissionRule] + () => actionOptions.filter((opt) => Boolean(rule?.[opt.value])), + [actionOptions, rule] ); const handleChange = (newValue: unknown) => { @@ -221,8 +172,7 @@ const ActionsMultiSelect = ({ return (
- ({ {...(menuPortalContainerRef?.current ? { menuPortalTarget: menuPortalContainerRef.current } : {})} - components={{ - Option: OptionWithDescription, - MultiValueRemove, - MultiValue: MultiValueWithTooltip - }} isError={actionsError} /> {actionsError && ( @@ -248,28 +193,35 @@ const ActionsMultiSelect = ({ ); }; -export const GeneralPermissionPolicies = >({ +export const GeneralPermissionPolicies = ({ subject, actions, children, title, description, + isConditional, + triggerSuffix, + onRemoveLastRule, isDisabled, isOpen = false, onShowAccessTree, menuPortalContainerRef }: Props) => { - const { control, watch } = useFormContext(); - const { fields, remove, insert } = useFieldArray({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { control, watch, trigger } = useFormContext(); + + useEffect(() => { + trigger("permissions"); + }, []); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { fields, remove, insert } = useFieldArray({ control, name: `permissions.${subject}` }); // scott: this is a hacky work-around to resolve bug of fields not updating UI when removed - const watchFields = useWatch({ - control, - name: `permissions.${subject}` - }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const watchFields = useWatch({ control, name: `permissions.${subject}` as any }) as unknown[]; if (!watchFields || !Array.isArray(watchFields) || watchFields.length === 0) return null; @@ -281,6 +233,11 @@ export const GeneralPermissionPolicies = {title} {description}
+ {triggerSuffix && ( +
e.stopPropagation()}> + {triggerSuffix} +
+ )} {fields.length > 1 && ( {fields.length} Rules @@ -302,7 +259,7 @@ export const GeneralPermissionPolicies = )} - {!isDisabled && isConditionalSubjects(subject) && ( + {!isDisabled && isConditional && (