orgRole.tsx 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. import {Fragment, useEffect, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import * as Sentry from '@sentry/react';
  4. import ExternalLink from 'sentry/components/links/externalLink';
  5. import Link from 'sentry/components/links/link';
  6. import QuestionTooltip from 'sentry/components/questionTooltip';
  7. import {t, tct} from 'sentry/locale';
  8. import {space} from 'sentry/styles/space';
  9. import {Member, Organization} from 'sentry/types';
  10. import {getEffectiveOrgRole} from 'sentry/utils/orgRole';
  11. export function OrgRoleInfo({
  12. organization,
  13. member,
  14. }: {
  15. member: Member;
  16. organization: Organization;
  17. }) {
  18. const {orgRoleList} = organization;
  19. const {orgRole, orgRolesFromTeams} = member;
  20. const orgRoleFromMember = useMemo(() => {
  21. const role = orgRoleList.find(r => r.id === orgRole);
  22. return role;
  23. }, [orgRole, orgRoleList]);
  24. const effectiveOrgRole = useMemo(() => {
  25. const memberOrgRoles = orgRolesFromTeams.map(r => r.role.id).concat([orgRole]);
  26. return getEffectiveOrgRole(memberOrgRoles, orgRoleList);
  27. }, [orgRole, orgRolesFromTeams, orgRoleList]);
  28. useEffect(() => {
  29. if (!orgRoleFromMember) {
  30. Sentry.withScope(scope => {
  31. scope.setExtra('context', {
  32. memberId: member.id,
  33. orgRole: member.orgRole,
  34. });
  35. Sentry.captureException(new Error('OrgMember has an invalid orgRole.'));
  36. });
  37. }
  38. }, [orgRoleFromMember, member]);
  39. useEffect(() => {
  40. if (!effectiveOrgRole) {
  41. Sentry.withScope(scope => {
  42. scope.setExtra('context', {
  43. memberId: member.id,
  44. orgRoleFromMember,
  45. orgRolesFromTeams,
  46. orgRoleList,
  47. effectiveOrgRole,
  48. });
  49. Sentry.captureException(new Error('OrgMember has no effectiveOrgRole.'));
  50. });
  51. }
  52. }, [effectiveOrgRole, member, orgRoleFromMember, orgRolesFromTeams, orgRoleList]);
  53. // This code path should not happen, so this weird UI is fine.
  54. if (!orgRoleFromMember) {
  55. return <Fragment>{t('Error Role')}</Fragment>;
  56. }
  57. if (orgRolesFromTeams.length === 0) {
  58. return <Fragment>{orgRoleFromMember.name}</Fragment>;
  59. }
  60. if (!effectiveOrgRole) {
  61. return <Fragment>{orgRoleFromMember.name}</Fragment>;
  62. }
  63. const urlPrefix = `/settings/${organization.slug}/`;
  64. const tooltipBody = (
  65. <TooltipWrapper>
  66. <div>{t('This user recieved org-level roles from several sources.')}</div>
  67. <div>
  68. <TeamRow>
  69. <TeamLink to={`${urlPrefix}member/${member.id}/`}>
  70. {t('User-specific')}
  71. </TeamLink>
  72. <div>: {orgRoleFromMember.name}</div>
  73. </TeamRow>
  74. </div>
  75. <div>
  76. <div>{t('Teams')}:</div>
  77. {orgRolesFromTeams
  78. .sort((a, b) => a.teamSlug.localeCompare(b.teamSlug))
  79. .map(r => (
  80. <TeamRow key={r.teamSlug}>
  81. <TeamLink to={`${urlPrefix}teams/${r.teamSlug}/`}>#{r.teamSlug}</TeamLink>
  82. <div>: {r.role.name}</div>
  83. </TeamRow>
  84. ))}
  85. </div>
  86. <div>
  87. {tct(
  88. 'Sentry will grant them permissions equivalent to the union-set of all their role. [docsLink:See docs here].',
  89. {
  90. docsLink: (
  91. <ExternalLink href="https://docs.sentry.io/product/accounts/membership/#roles" />
  92. ),
  93. }
  94. )}
  95. </div>
  96. </TooltipWrapper>
  97. );
  98. return (
  99. <Wrapper>
  100. {effectiveOrgRole.name}
  101. <QuestionTooltip isHoverable size="sm" title={tooltipBody} />
  102. </Wrapper>
  103. );
  104. }
  105. const Wrapper = styled('span')`
  106. display: inline-flex;
  107. gap: ${space(0.5)};
  108. `;
  109. const TooltipWrapper = styled('div')`
  110. width: 200px;
  111. display: grid;
  112. row-gap: ${space(1.5)};
  113. text-align: left;
  114. overflow: hidden;
  115. `;
  116. const TeamRow = styled('div')`
  117. display: grid;
  118. grid-template-columns: auto 1fr;
  119. > * {
  120. white-space: nowrap;
  121. overflow: hidden;
  122. text-overflow: ellipsis;
  123. }
  124. `;
  125. const TeamLink = styled(Link)`
  126. max-width: 130px;
  127. font-weight: 700;
  128. `;