UpdateRolesPanel.utils.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. import { groupBy, isEqual } from 'lodash'
  2. import {
  3. OrganizationRole,
  4. OrganizationRolesResponse,
  5. } from '@/data/organization-members/organization-roles-query'
  6. import { OrganizationMember } from '@/data/organizations/organization-members-query'
  7. export interface ProjectRoleConfiguration {
  8. ref?: string
  9. name?: string
  10. roleId: number
  11. baseRoleId?: number
  12. }
  13. export const formatMemberRoleToProjectRoleConfiguration = (
  14. member: OrganizationMember,
  15. allRoles: OrganizationRolesResponse
  16. ) => {
  17. const { org_scoped_roles, project_scoped_roles } = allRoles
  18. const roleConfiguration = member.role_ids
  19. .map((id) => {
  20. const orgRole = org_scoped_roles.find((role) => role.id === id)
  21. if (orgRole !== undefined) {
  22. return { ref: undefined, name: undefined, roleId: orgRole.id }
  23. }
  24. const projectRole = project_scoped_roles.find((role) => role.id === id)
  25. if (projectRole !== undefined) {
  26. return projectRole.projects.map((x) => ({
  27. ref: x.ref,
  28. name: x.name,
  29. roleId: projectRole.id,
  30. baseRoleId: projectRole.base_role_id,
  31. }))
  32. }
  33. return undefined
  34. })
  35. .filter(Boolean)
  36. .flat()
  37. .filter((p) => {
  38. // [Joshen] Validate only for project scoped roles
  39. // This filters out project scoped roles for projects that the user doesn't have access to
  40. // (e.g if the project was deleted)
  41. if ('baseRoleId' in p!) {
  42. return p.ref !== undefined
  43. } else {
  44. return p
  45. }
  46. })
  47. .sort((a, b) => (a?.name ?? '').localeCompare(b?.name ?? '')) as ProjectRoleConfiguration[]
  48. return roleConfiguration
  49. }
  50. export const deriveChanges = (
  51. original: ProjectRoleConfiguration[] = [],
  52. final: ProjectRoleConfiguration[] = []
  53. ) => {
  54. const removed: ProjectRoleConfiguration[] = []
  55. const added: ProjectRoleConfiguration[] = []
  56. const updated: {
  57. ref?: string
  58. name?: string
  59. originalRole: number
  60. originalBaseRole?: number
  61. updatedRole: number
  62. }[] = []
  63. original.forEach((x) => {
  64. const updatedRoleForProject = final.find((y) => x.ref === y.ref)
  65. if (updatedRoleForProject === undefined) {
  66. removed.push(x)
  67. } else if (updatedRoleForProject.roleId !== x.roleId) {
  68. updated.push({
  69. ref: updatedRoleForProject.ref,
  70. name: updatedRoleForProject.name,
  71. originalRole: x.roleId,
  72. originalBaseRole: x.baseRoleId,
  73. updatedRole: updatedRoleForProject.roleId,
  74. })
  75. }
  76. })
  77. final.forEach((x) => {
  78. const newRoleForProject = original.find((y) => x.ref === y.ref)
  79. if (newRoleForProject === undefined) {
  80. added.push(x)
  81. }
  82. })
  83. return { removed, added, updated }
  84. }
  85. // [Joshen] Essentially spit out 3 arrays
  86. // What to (1) Remove (2) Assign (3) Update
  87. // And so that we can unit test this
  88. export const deriveRoleChangeActions = (
  89. existingRoles: OrganizationRole[],
  90. changesToRoles: {
  91. removed: ProjectRoleConfiguration[]
  92. added: ProjectRoleConfiguration[]
  93. updated: {
  94. ref?: string
  95. name?: string
  96. originalRole: number
  97. originalBaseRole?: number
  98. updatedRole: number
  99. }[]
  100. }
  101. ) => {
  102. const { removed, added, updated } = changesToRoles
  103. const toRemove: number[] = []
  104. const toAssign: { roleId: number; refs: string[] }[] = []
  105. const toUpdate: { roleId: number; refs: string[] }[] = []
  106. const groupByAddedRoles = groupBy(added, 'roleId')
  107. const groupByRemovedRoles = groupBy(removed, 'roleId')
  108. const groupByUpdatingFromRoles = groupBy(updated, 'originalRole')
  109. const groupByUpdatingToRoles = groupBy(updated, 'updatedRole')
  110. const existingProjectRolesByBaseIds = existingRoles
  111. .filter((r) => (r?.projects ?? []).length > 0)
  112. .map((r) => r.base_role_id)
  113. existingRoles.forEach((role) => {
  114. const projectRefsApplied = role.projects.map((x) => x.ref)
  115. if (projectRefsApplied.length === 0) {
  116. // [Joshen] In this case we're removing an org scope role, skip all the chekcs
  117. return toRemove.push(role.id)
  118. }
  119. const toRemoveRole = projectRefsApplied.every((ref) => {
  120. const isRemoved = removed.map((r) => r.ref).some((x) => x === ref)
  121. const isUpdated = updated.map((r) => r.ref).some((x) => x === ref)
  122. return isRemoved || isUpdated
  123. })
  124. const isRoleGettingAdded = added.some((r) => r.roleId === role.base_role_id)
  125. const isRoleGettingUpdatedTo = updated.some((r) => r.updatedRole === role.base_role_id)
  126. if (toRemoveRole && !isRoleGettingAdded && !isRoleGettingUpdatedTo) {
  127. return toRemove.push(role.id)
  128. }
  129. const projectsToAddToRole = (groupByAddedRoles[role.base_role_id]?.map((r) => r.ref) ??
  130. []) as string[]
  131. const projectsToRemoveFromRole = (groupByRemovedRoles[role.id]?.map((r) => r.ref) ??
  132. []) as string[]
  133. const projectsUpdatingFromRole = (groupByUpdatingFromRoles[role.id]?.map((r) => r.ref) ??
  134. []) as string[]
  135. const projectsUpdatingToRole = (groupByUpdatingToRoles[role.base_role_id]?.map((r) => r.ref) ??
  136. []) as string[]
  137. const projectRefsAppliedUpdated = projectRefsApplied
  138. .filter((x) => !projectsToRemoveFromRole.includes(x))
  139. .filter((x) => !projectsUpdatingFromRole.includes(x))
  140. .concat(projectsToAddToRole)
  141. .concat(projectsUpdatingToRole)
  142. if (!isEqual(projectRefsApplied, projectRefsAppliedUpdated)) {
  143. toUpdate.push({ roleId: role.id, refs: projectRefsAppliedUpdated })
  144. }
  145. })
  146. Object.keys(groupByAddedRoles).forEach((roleId) => {
  147. if (!existingProjectRolesByBaseIds.includes(Number(roleId))) {
  148. toAssign.push({
  149. roleId: Number(roleId),
  150. refs: groupByAddedRoles[roleId].map((x) => x.ref).filter((x) => x !== undefined),
  151. })
  152. }
  153. })
  154. Object.keys(groupByUpdatingToRoles).forEach((roleId) => {
  155. if (!existingProjectRolesByBaseIds.includes(Number(roleId))) {
  156. toAssign.push({
  157. roleId: Number(roleId),
  158. refs: groupByUpdatingToRoles[roleId].map((x) => x.ref).filter((x) => x !== undefined),
  159. })
  160. }
  161. })
  162. // [Joshen] Am sorting the results just so its more deterministic when writing tests
  163. return {
  164. toRemove: toRemove.sort((a, b) => a - b),
  165. toAssign: toAssign.map((x) => ({ ...x, refs: x.refs.sort((a, b) => a.localeCompare(b)) })),
  166. toUpdate: toUpdate.map((x) => ({ ...x, refs: x.refs.sort((a, b) => a.localeCompare(b)) })),
  167. }
  168. }