UpdateRolesConfirmationModal.tsx 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. import { useQueryClient } from '@tanstack/react-query'
  2. import { useParams } from 'common'
  3. import { useState } from 'react'
  4. import { toast } from 'sonner'
  5. import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
  6. import {
  7. deriveChanges,
  8. deriveRoleChangeActions,
  9. formatMemberRoleToProjectRoleConfiguration,
  10. ProjectRoleConfiguration,
  11. } from './UpdateRolesPanel.utils'
  12. import { organizationKeys } from '@/data/organization-members/keys'
  13. import { useOrganizationMemberAssignRoleMutation } from '@/data/organization-members/organization-member-role-assign-mutation'
  14. import { useOrganizationMemberUnassignRoleMutation } from '@/data/organization-members/organization-member-role-unassign-mutation'
  15. import { useOrganizationMemberUpdateRoleMutation } from '@/data/organization-members/organization-member-role-update-mutation'
  16. import {
  17. OrganizationRole,
  18. useOrganizationRolesV2Query,
  19. } from '@/data/organization-members/organization-roles-query'
  20. import { organizationKeys as organizationKeysV1 } from '@/data/organizations/keys'
  21. import { OrganizationMember } from '@/data/organizations/organization-members-query'
  22. import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
  23. interface UpdateRolesConfirmationModal {
  24. visible: boolean
  25. member: OrganizationMember
  26. projectsRoleConfiguration: ProjectRoleConfiguration[]
  27. onClose: (success?: boolean) => void
  28. }
  29. export const UpdateRolesConfirmationModal = ({
  30. visible,
  31. member,
  32. projectsRoleConfiguration,
  33. onClose,
  34. }: UpdateRolesConfirmationModal) => {
  35. const { slug } = useParams()
  36. const queryClient = useQueryClient()
  37. const { data: organization } = useSelectedOrganizationQuery()
  38. const { data: allRoles } = useOrganizationRolesV2Query({ slug: organization?.slug })
  39. // [Joshen] Separate saving state instead of using RQ due to several successive steps
  40. const [saving, setSaving] = useState(false)
  41. const { mutateAsync: assignRole } = useOrganizationMemberAssignRoleMutation()
  42. const { mutateAsync: removeRole } = useOrganizationMemberUnassignRoleMutation({
  43. onError: () => {},
  44. })
  45. const { mutateAsync: updateRole } = useOrganizationMemberUpdateRoleMutation()
  46. const availableRoles = allRoles?.org_scoped_roles ?? []
  47. const { org_scoped_roles, project_scoped_roles } = allRoles ?? {
  48. org_scoped_roles: [],
  49. project_scoped_roles: [],
  50. }
  51. const originalConfiguration =
  52. allRoles !== undefined ? formatMemberRoleToProjectRoleConfiguration(member, allRoles) : []
  53. const changesToRoles = deriveChanges(originalConfiguration, projectsRoleConfiguration)
  54. const onConfirmUpdateMemberRoles = async () => {
  55. if (slug === undefined) return console.error('Slug is required')
  56. setSaving(true)
  57. const gotrueId = member.gotrue_id
  58. const existingRoles = member.role_ids
  59. .map((id) => {
  60. return [...org_scoped_roles, ...project_scoped_roles].find((r) => r.id === id)
  61. })
  62. .filter(Boolean) as OrganizationRole[]
  63. const isChangeWithinOrgScope =
  64. projectsRoleConfiguration.length === 1 && projectsRoleConfiguration[0].ref === undefined
  65. // Early return if we're just updating org level roles
  66. // Everything else below is just project level role changes then
  67. if (isChangeWithinOrgScope) {
  68. try {
  69. await assignRole({
  70. slug,
  71. gotrueId,
  72. roleId: projectsRoleConfiguration[0].roleId,
  73. })
  74. toast.success(`Successfully updated role for ${member.username}`)
  75. onClose(true)
  76. } catch (error: any) {
  77. toast.error(`Failed to update role: ${error.message}`)
  78. } finally {
  79. setSaving(false)
  80. return
  81. }
  82. }
  83. const { toRemove, toAssign, toUpdate } = deriveRoleChangeActions(existingRoles, changesToRoles)
  84. try {
  85. for (const { roleId, refs } of toAssign) {
  86. await assignRole({
  87. slug,
  88. gotrueId,
  89. roleId,
  90. projects: refs,
  91. skipInvalidation: true,
  92. })
  93. }
  94. for (const roleId of toRemove) {
  95. await removeRole({ slug, gotrueId, roleId, skipInvalidation: true })
  96. }
  97. for (const { roleId, refs } of toUpdate) {
  98. await updateRole({
  99. slug,
  100. gotrueId,
  101. roleId,
  102. roleName: project_scoped_roles.find((r) => r.id === roleId)?.name as string,
  103. projects: refs,
  104. skipInvalidation: true,
  105. })
  106. }
  107. await Promise.all([
  108. queryClient.invalidateQueries({ queryKey: organizationKeys.rolesV2(slug) }),
  109. queryClient.invalidateQueries({ queryKey: organizationKeysV1.members(slug) }),
  110. ])
  111. toast.success(`Successfully updated role for ${member.username}`)
  112. onClose(true)
  113. } catch (error: any) {
  114. toast.error(`Failed to update role: ${error.message}`)
  115. } finally {
  116. setSaving(false)
  117. return
  118. }
  119. }
  120. return (
  121. <ConfirmationModal
  122. size="medium"
  123. visible={visible}
  124. loading={saving}
  125. title="Confirm to change roles of member"
  126. confirmLabel="Update roles"
  127. confirmLabelLoading="Updating"
  128. onCancel={() => onClose()}
  129. onConfirm={onConfirmUpdateMemberRoles}
  130. >
  131. <div className="flex flex-col gap-y-3">
  132. <p className="text-sm text-foreground-light">
  133. You are making the following changes to the role of{' '}
  134. <span className="text-foreground">{member.username}</span> in the organization{' '}
  135. <span className="text-foreground">{organization?.name}</span>:
  136. </p>
  137. <div className="flex flex-col gap-y-2">
  138. {changesToRoles.removed.length !== 0 && (
  139. <div>
  140. <p className="text-sm">
  141. Removing {changesToRoles.removed.length} role
  142. {changesToRoles.removed.length > 1 ? 's' : ''} for user:
  143. </p>
  144. <ul className="list-disc pl-6">
  145. {changesToRoles.removed.map((x, i) => {
  146. const role =
  147. org_scoped_roles.find((y) => y.id === x.roleId) ??
  148. project_scoped_roles.find((y) => y.id === x.roleId)
  149. const roleName = (role?.name ?? 'Unknown').split('_')[0]
  150. return (
  151. <li key={`update-${i}`} className="text-sm text-foreground-light">
  152. <span className="text-foreground">{roleName}</span> on{' '}
  153. <span className="text-foreground">{x?.name ?? 'organization'}</span>
  154. </li>
  155. )
  156. })}
  157. </ul>
  158. </div>
  159. )}
  160. {changesToRoles.added.length !== 0 && (
  161. <div>
  162. <p className="text-sm">
  163. Adding {changesToRoles.added.length} role
  164. {changesToRoles.added.length > 1 ? 's' : ''} for user:
  165. </p>
  166. <ul className="list-disc pl-6">
  167. {changesToRoles.added.map((x, i) => {
  168. const role = availableRoles.find((y) => y.id === x.roleId)
  169. return (
  170. <li key={`update-${i}`} className="text-sm text-foreground-light">
  171. <span className="text-foreground">{role?.name}</span> on{' '}
  172. <span className="text-foreground">{x?.name ?? 'organization'}</span>
  173. </li>
  174. )
  175. })}
  176. </ul>
  177. </div>
  178. )}
  179. {changesToRoles.updated.length !== 0 && (
  180. <div>
  181. <p className="text-sm">
  182. Updating {changesToRoles.updated.length} role
  183. {changesToRoles.updated.length > 1 ? 's' : ''} for user:
  184. </p>
  185. <ul className="list-disc pl-6">
  186. {changesToRoles.updated.map((x, i) => {
  187. const originalRole =
  188. org_scoped_roles.find((y) => y.id === x.originalRole) ??
  189. project_scoped_roles.find((y) => y.id === x.originalRole)
  190. const updatedRole = org_scoped_roles.find((y) => y.id === x.updatedRole)
  191. const originalRoleName = (originalRole?.name ?? 'Unknown').split('_')[0]
  192. return (
  193. <li key={`update-${i}`} className="text-sm text-foreground-light">
  194. From <span className="text-foreground">{originalRoleName}</span> to{' '}
  195. <span className="text-foreground">{updatedRole?.name ?? 'Unknown'}</span> on{' '}
  196. <span className="text-foreground">{x?.name ?? 'organization'}</span>
  197. </li>
  198. )
  199. })}
  200. </ul>
  201. </div>
  202. )}
  203. </div>
  204. <p className="text-sm text-foreground">
  205. By changing the role of this member their permissions will change.
  206. </p>
  207. </div>
  208. </ConfirmationModal>
  209. )
  210. }