assigneeSelector.tsx 5.8 KB

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