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, groupOrgRoles} = 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. if (!groupOrgRoles) {
  26. return orgRoleFromMember;
  27. }
  28. const memberOrgRoles = groupOrgRoles.map(r => r.role.id).concat([orgRole]);
  29. return getEffectiveOrgRole(memberOrgRoles, orgRoleList);
  30. }, [orgRole, groupOrgRoles, orgRoleList, orgRoleFromMember]);
  31. useEffect(() => {
  32. if (!orgRoleFromMember) {
  33. Sentry.withScope(scope => {
  34. scope.setExtra('context', {
  35. memberId: member.id,
  36. orgRole: member.orgRole,
  37. });
  38. Sentry.captureException(new Error('OrgMember has an invalid orgRole.'));
  39. });
  40. }
  41. }, [orgRoleFromMember, member]);
  42. useEffect(() => {
  43. if (!effectiveOrgRole) {
  44. Sentry.withScope(scope => {
  45. scope.setExtra('context', {
  46. memberId: member.id,
  47. orgRoleFromMember,
  48. groupOrgRoles,
  49. orgRoleList,
  50. effectiveOrgRole,
  51. });
  52. Sentry.captureException(new Error('OrgMember has no effectiveOrgRole.'));
  53. });
  54. }
  55. }, [effectiveOrgRole, member, orgRoleFromMember, groupOrgRoles, orgRoleList]);
  56. // This code path should not happen, so this weird UI is fine.
  57. if (!orgRoleFromMember) {
  58. return <Fragment>{t('Error Role')}</Fragment>;
  59. }
  60. if (groupOrgRoles?.length === 0 || !effectiveOrgRole || !groupOrgRoles) {
  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. {groupOrgRoles &&
  78. groupOrgRoles
  79. .sort((a, b) => a.teamSlug.localeCompare(b.teamSlug))
  80. .map(r => (
  81. <TeamRow key={r.teamSlug}>
  82. <TeamLink to={`${urlPrefix}teams/${r.teamSlug}/`}>#{r.teamSlug}</TeamLink>
  83. <div>: {r.role.name}</div>
  84. </TeamRow>
  85. ))}
  86. </div>
  87. <div>
  88. {tct(
  89. 'Sentry will grant them permissions equivalent to the union-set of all their role. [docsLink:See docs here].',
  90. {
  91. docsLink: (
  92. <ExternalLink href="https://docs.sentry.io/product/accounts/membership/#roles" />
  93. ),
  94. }
  95. )}
  96. </div>
  97. </TooltipWrapper>
  98. );
  99. return (
  100. <Wrapper>
  101. {effectiveOrgRole.name}
  102. <QuestionTooltip isHoverable size="sm" title={tooltipBody} />
  103. </Wrapper>
  104. );
  105. }
  106. const Wrapper = styled('span')`
  107. display: inline-flex;
  108. gap: ${space(0.5)};
  109. `;
  110. const TooltipWrapper = styled('div')`
  111. width: 200px;
  112. display: grid;
  113. row-gap: ${space(1.5)};
  114. text-align: left;
  115. overflow: hidden;
  116. `;
  117. const TeamRow = styled('div')`
  118. display: grid;
  119. grid-template-columns: auto 1fr;
  120. > * {
  121. white-space: nowrap;
  122. overflow: hidden;
  123. text-overflow: ellipsis;
  124. }
  125. `;
  126. const TeamLink = styled(Link)`
  127. max-width: 130px;
  128. font-weight: 700;
  129. `;