assigneeSelector.tsx 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import {
  4. AssigneeSelectorDropdown,
  5. AssigneeSelectorDropdownProps,
  6. SuggestedAssignee,
  7. } from 'sentry/components/assigneeSelectorDropdown';
  8. import ActorAvatar from 'sentry/components/avatar/actorAvatar';
  9. import SuggestedAvatarStack from 'sentry/components/avatar/suggestedAvatarStack';
  10. import DropdownBubble from 'sentry/components/dropdownBubble';
  11. import ExternalLink from 'sentry/components/links/externalLink';
  12. import LoadingIndicator from 'sentry/components/loadingIndicator';
  13. import Tooltip from 'sentry/components/tooltip';
  14. import {IconChevron, IconUser} from 'sentry/icons';
  15. import {t, tct, tn} from 'sentry/locale';
  16. import space from 'sentry/styles/space';
  17. import type {Actor, SuggestedOwnerReason} from 'sentry/types';
  18. interface AssigneeSelectorProps extends Omit<AssigneeSelectorDropdownProps, 'children'> {
  19. noDropdown?: boolean;
  20. }
  21. function AssigneeSelector({noDropdown, ...props}: AssigneeSelectorProps) {
  22. function getActorElement(
  23. assignedTo?: Actor,
  24. suggestedActors: SuggestedAssignee[] = []
  25. ) {
  26. const suggestedReasons: Record<SuggestedOwnerReason, React.ReactNode> = {
  27. suspectCommit: tct('Based on [commit:commit data]', {
  28. commit: (
  29. <TooltipSubExternalLink href="https://docs.sentry.io/product/sentry-basics/integrate-frontend/configure-scms/" />
  30. ),
  31. }),
  32. releaseCommit: '',
  33. ownershipRule: t('Matching Issue Owners Rule'),
  34. codeowners: t('Matching Codeowners Rule'),
  35. };
  36. const assignedToSuggestion = suggestedActors.find(
  37. actor => actor.id === assignedTo?.id
  38. );
  39. return assignedTo ? (
  40. <ActorAvatar
  41. actor={assignedTo}
  42. className="avatar"
  43. size={24}
  44. tooltip={
  45. <TooltipWrapper>
  46. {tct('Assigned to [name]', {
  47. name: assignedTo.type === 'team' ? `#${assignedTo.name}` : assignedTo.name,
  48. })}
  49. {assignedToSuggestion &&
  50. suggestedReasons[assignedToSuggestion.suggestedReason] && (
  51. <TooltipSubtext>
  52. {suggestedReasons[assignedToSuggestion.suggestedReason]}
  53. </TooltipSubtext>
  54. )}
  55. </TooltipWrapper>
  56. }
  57. />
  58. ) : suggestedActors && suggestedActors.length > 0 ? (
  59. <SuggestedAvatarStack
  60. size={28}
  61. owners={suggestedActors}
  62. tooltipOptions={{isHoverable: true}}
  63. tooltip={
  64. <TooltipWrapper>
  65. <div>
  66. {tct('Suggestion: [name]', {
  67. name:
  68. suggestedActors[0].type === 'team'
  69. ? `#${suggestedActors[0].name}`
  70. : suggestedActors[0].name,
  71. })}
  72. {suggestedActors.length > 1 &&
  73. tn(' + %s other', ' + %s others', suggestedActors.length - 1)}
  74. </div>
  75. <TooltipSubtext>
  76. {suggestedReasons[suggestedActors[0].suggestedReason]}
  77. </TooltipSubtext>
  78. </TooltipWrapper>
  79. }
  80. />
  81. ) : (
  82. <Tooltip
  83. isHoverable
  84. skipWrapper
  85. title={
  86. <TooltipWrapper>
  87. <div>{t('Unassigned')}</div>
  88. <TooltipSubtext>
  89. {tct(
  90. 'You can auto-assign issues by adding [issueOwners:Issue Owner rules].',
  91. {
  92. issueOwners: (
  93. <TooltipSubExternalLink href="https://docs.sentry.io/product/error-monitoring/issue-owners/" />
  94. ),
  95. }
  96. )}
  97. </TooltipSubtext>
  98. </TooltipWrapper>
  99. }
  100. >
  101. <StyledIconUser data-test-id="unassigned" size="20px" color="gray400" />
  102. </Tooltip>
  103. );
  104. }
  105. return (
  106. <AssigneeWrapper>
  107. <AssigneeSelectorDropdown {...props}>
  108. {({loading, isOpen, assignedTo, getActorProps, suggestedAssignees}) => {
  109. const avatarElement = getActorElement(assignedTo, suggestedAssignees);
  110. return (
  111. <Fragment>
  112. {loading && (
  113. <LoadingIndicator
  114. mini
  115. style={{height: '24px', margin: 0, marginRight: 11}}
  116. />
  117. )}
  118. {!loading && !noDropdown && (
  119. <DropdownButton data-test-id="assignee-selector" {...getActorProps({})}>
  120. {avatarElement}
  121. <StyledChevron direction={isOpen ? 'up' : 'down'} size="xs" />
  122. </DropdownButton>
  123. )}
  124. {!loading && noDropdown && avatarElement}
  125. </Fragment>
  126. );
  127. }}
  128. </AssigneeSelectorDropdown>
  129. </AssigneeWrapper>
  130. );
  131. }
  132. export default AssigneeSelector;
  133. const AssigneeWrapper = styled('div')`
  134. display: flex;
  135. justify-content: flex-end;
  136. /* manually align menu underneath dropdown caret */
  137. ${DropdownBubble} {
  138. right: -14px;
  139. }
  140. `;
  141. const StyledIconUser = styled(IconUser)`
  142. /* We need this to center with Avatar */
  143. margin-right: 2px;
  144. `;
  145. const StyledChevron = styled(IconChevron)`
  146. margin-left: ${space(1)};
  147. `;
  148. const DropdownButton = styled('div')`
  149. display: flex;
  150. align-items: center;
  151. font-size: 20px;
  152. `;
  153. const TooltipWrapper = styled('div')`
  154. text-align: left;
  155. `;
  156. const TooltipSubtext = styled('div')`
  157. color: ${p => p.theme.subText};
  158. `;
  159. const TooltipSubExternalLink = styled(ExternalLink)`
  160. color: ${p => p.theme.subText};
  161. text-decoration: underline;
  162. :hover {
  163. color: ${p => p.theme.subText};
  164. }
  165. `;