import { useParams } from 'common' import { isEqual } from 'lodash' import { ChevronDown, X } from 'lucide-react' import { useEffect, useMemo, useState } from 'react' import { Alert, AlertDescription, AlertTitle, Button, cn, Collapsible, CollapsibleContent, CollapsibleTrigger, Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, Sheet, SheetContent, SheetFooter, SheetHeader, SheetSection, Switch, Tooltip, TooltipContent, TooltipTrigger, WarningIcon, } from 'ui' import { useGetRolesManagementPermissions } from '../TeamSettings.utils' import { UpdateRolesConfirmationModal } from './UpdateRolesConfirmationModal' import { formatMemberRoleToProjectRoleConfiguration, ProjectRoleConfiguration, } from './UpdateRolesPanel.utils' import { ButtonTooltip } from '@/components/ui/ButtonTooltip' import { DocsButton } from '@/components/ui/DocsButton' import { OrganizationProjectSelector } from '@/components/ui/OrganizationProjectSelector' import { useOrganizationRolesV2Query } from '@/data/organization-members/organization-roles-query' import { OrganizationMember } from '@/data/organizations/organization-members-query' import { usePermissionsQuery } from '@/data/permissions/permissions-query' import { OrgProject, useOrgProjectsInfiniteQuery, } from '@/data/projects/org-projects-infinite-query' import { useHasAccessToProjectLevelPermissions } from '@/data/subscriptions/org-subscription-query' import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' import { DOCS_URL } from '@/lib/constants' import { MANAGED_BY } from '@/lib/constants/infrastructure' interface UpdateRolesPanelProps { visible: boolean member: OrganizationMember onClose: () => void } export const UpdateRolesPanel = ({ visible, member, onClose }: UpdateRolesPanelProps) => { const { slug } = useParams() const { data: organization } = useSelectedOrganizationQuery() const isOptedIntoProjectLevelPermissions = useHasAccessToProjectLevelPermissions(slug as string) const { data: permissions } = usePermissionsQuery() const { data: allRoles, isSuccess: isSuccessRoles } = useOrganizationRolesV2Query({ slug }) const { data: projectsData } = useOrgProjectsInfiniteQuery({ slug }) const totalNumOrgProjects = projectsData?.pages[0].pagination.count ?? 0 const orgProjects = useMemo(() => projectsData?.pages.flatMap((page) => page.projects), [projectsData?.pages]) || [] // [Joshen] We use the org scoped roles as the source for available roles const orgScopedRoles = allRoles?.org_scoped_roles ?? [] const projectScopedRoles = allRoles?.project_scoped_roles ?? [] const { rolesAddable, rolesRemovable } = useGetRolesManagementPermissions( organization?.slug, orgScopedRoles.concat(projectScopedRoles), permissions ?? [] ) const cannotAddAnyRoles = orgScopedRoles.every((r) => !rolesAddable.includes(r.id)) const isStripeProjectsOrg = organization?.managed_by === MANAGED_BY.STRIPE_PROJECTS const [showConfirmation, setShowConfirmation] = useState(false) const [showProjectDropdown, setShowProjectDropdown] = useState(false) const [projectsRoleConfiguration, setProjectsRoleConfiguration] = useState< ProjectRoleConfiguration[] >([]) const originalConfiguration = allRoles !== undefined ? formatMemberRoleToProjectRoleConfiguration(member, allRoles) : [] const originalConfigurationType = originalConfiguration.length === 1 && !!orgScopedRoles.find((r) => r.id === originalConfiguration[0].roleId) ? 'org-scope' : 'project-scope' const isApplyingRoleToAllProjects = projectsRoleConfiguration.length === 1 && projectsRoleConfiguration[0]?.ref === undefined const canSaveRoles = projectsRoleConfiguration.length > 0 const lowerPermissionsRole = orgScopedRoles.find((r) => r.name === 'Developer')?.id const noAccessProjects = orgProjects.filter((project) => { return !projectsRoleConfiguration.some((p) => p.ref === project.ref) }) const numberOfProjectsWithAccess = projectsRoleConfiguration.filter( (p) => p.ref !== undefined ).length const hasNoChanges = isEqual(projectsRoleConfiguration, originalConfiguration) const onSelectProject = (project: OrgProject) => { setProjectsRoleConfiguration( projectsRoleConfiguration.concat({ ref: project.ref, name: project.name, roleId: lowerPermissionsRole ?? orgScopedRoles[0].id, }) ) setShowProjectDropdown(false) } const onRemoveProject = (ref?: string) => { if (ref === undefined) return setProjectsRoleConfiguration(projectsRoleConfiguration.filter((p) => p.ref !== ref)) } const onSelectRole = (value: string, project: ProjectRoleConfiguration) => { if (project.ref !== undefined) { setProjectsRoleConfiguration( projectsRoleConfiguration.map((p) => { if (p.ref === project.ref) { return { ref: p.ref, name: p.name, roleId: Number(value) } } else { return p } }) ) } else { setProjectsRoleConfiguration([{ ref: undefined, roleId: Number(value) }]) } } const onToggleApplyToAllProjects = (isApplyAllProjects: boolean) => { const roleIdToApply = lowerPermissionsRole ?? orgScopedRoles[0].id if (isApplyAllProjects) { if (originalConfigurationType === 'org-scope') { setProjectsRoleConfiguration(originalConfiguration) } else { setProjectsRoleConfiguration([{ ref: undefined, name: undefined, roleId: roleIdToApply }]) } } else { if (originalConfigurationType === 'project-scope') { setProjectsRoleConfiguration(originalConfiguration) } else { setProjectsRoleConfiguration( orgProjects.map((p) => { return { ref: p.ref, name: p.name, roleId: roleIdToApply } }) ) } } } useEffect(() => { if (visible && isSuccessRoles) { const roleConfiguration = formatMemberRoleToProjectRoleConfiguration(member, allRoles) setProjectsRoleConfiguration(roleConfiguration) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [visible, isSuccessRoles]) return ( <> onClose()}>

Manage access for {member.username}

{isOptedIntoProjectLevelPermissions && (

Apply roles to all projects in the organization

)} {projectsRoleConfiguration.length === 0 && ( Team members need to be assigned at least one role You may not remove all roles from a team member )} {!isApplyingRoleToAllProjects && projectsRoleConfiguration.length > 0 && projectsRoleConfiguration.length < totalNumOrgProjects && ( {hasNoChanges ? `This member only has access to ${numberOfProjectsWithAccess} project${numberOfProjectsWithAccess > 1 ? 's' : ''}` : `This member will only have access to ${numberOfProjectsWithAccess} project${numberOfProjectsWithAccess > 1 ? 's' : ''}`}

{member.username} {hasNoChanges ? 'does' : 'will'} not have access to the following {noAccessProjects.length} project {noAccessProjects.length > 1 ? 's' : ''}:

    {noAccessProjects.map((project) => { return
  • {project.name}
  • })}
)}
{projectsRoleConfiguration.map((project) => { const name = project.ref === undefined ? 'All projects' : project.name const role = orgScopedRoles.find((r) => { if (project.baseRoleId !== undefined) return r.id === project.baseRoleId else return r.id === project.roleId }) const canRemoveRole = rolesRemovable.includes(role?.id ?? 0) return (

{name}

{cannotAddAnyRoles ? (
{role?.name ?? 'Unknown'}
Additional permissions required to update role
) : ( )} {!isApplyingRoleToAllProjects && ( } onClick={() => onRemoveProject(project?.ref)} tooltip={{ content: { side: 'bottom', text: !canRemoveRole ? 'Additional permission required to remove role from member' : 'Remove access to project', }, }} /> )}
) })}
{!isApplyingRoleToAllProjects && ( ( )} renderRow={(project) => { const hasRoleAssigned = projectsRoleConfiguration.some( (p) => p.ref === project.ref ) return (
{project.name} {hasRoleAssigned &&

Already assigned

}
) }} isOptionDisabled={(project) => projectsRoleConfiguration.some((p) => p.ref === project.ref) } /> )}
{ setShowConfirmation(false) if (success) onClose() }} /> ) }