issueContext.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. import {useEffect} from 'react';
  2. import styled from '@emotion/styled';
  3. import ActorAvatar from 'sentry/components/avatar/actorAvatar';
  4. import Count from 'sentry/components/count';
  5. import {QuickContextCommitRow} from 'sentry/components/discover/quickContextCommitRow';
  6. import {SuspectCommitHeader} from 'sentry/components/events/styles';
  7. import {StyledPanel, SuspectCommits} from 'sentry/components/events/suspectCommits';
  8. import {getAssignedToDisplayName} from 'sentry/components/group/assignedTo';
  9. import Panel from 'sentry/components/panels/panel';
  10. import {IconWrapper} from 'sentry/components/sidebarSection';
  11. import * as SidebarSection from 'sentry/components/sidebarSection';
  12. import {Tooltip} from 'sentry/components/tooltip';
  13. import {IconCheckmark, IconMute, IconNot, IconUser} from 'sentry/icons';
  14. import {t} from 'sentry/locale';
  15. import {space} from 'sentry/styles/space';
  16. import type {Event} from 'sentry/types/event';
  17. import type {Group} from 'sentry/types/group';
  18. import {trackAnalytics} from 'sentry/utils/analytics';
  19. import {useApiQuery} from 'sentry/utils/queryClient';
  20. import {NoContext} from './quickContextWrapper';
  21. import {
  22. ContextBody,
  23. ContextContainer,
  24. ContextHeader,
  25. ContextRow,
  26. ContextTitle,
  27. Wrapper,
  28. } from './styles';
  29. import type {BaseContextProps} from './utils';
  30. import {ContextType, tenSecondInMs} from './utils';
  31. function IssueContext(props: BaseContextProps) {
  32. const {dataRow, organization} = props;
  33. useEffect(() => {
  34. trackAnalytics('discover_v2.quick_context_hover_contexts', {
  35. organization,
  36. contextType: ContextType.ISSUE,
  37. });
  38. }, [organization]);
  39. const {
  40. isPending: issueLoading,
  41. isError: issueError,
  42. data: issue,
  43. } = useApiQuery<Group>(
  44. [
  45. `/issues/${dataRow['issue.id']}/`,
  46. {
  47. query: {
  48. collapse: 'release',
  49. expand: 'inbox',
  50. },
  51. },
  52. ],
  53. {
  54. staleTime: tenSecondInMs,
  55. }
  56. );
  57. // NOTE: Suspect commits are generated from the first event of an issue.
  58. // Therefore, all events for an issue have the same suspect commits.
  59. const {
  60. isPending: eventLoading,
  61. isError: eventError,
  62. data: event,
  63. } = useApiQuery<Event>([`/issues/${dataRow['issue.id']}/events/oldest/`], {
  64. staleTime: tenSecondInMs,
  65. });
  66. const title = issue?.title;
  67. const renderTitle = () =>
  68. issue && (
  69. <IssueContextContainer data-test-id="quick-context-issue-title-container">
  70. <ContextHeader>
  71. <ContextTitle>{t('Title')}</ContextTitle>
  72. </ContextHeader>
  73. <Tooltip showOnlyOnOverflow skipWrapper title={title}>
  74. <IssueTitleBody>{title}</IssueTitleBody>
  75. </Tooltip>
  76. </IssueContextContainer>
  77. );
  78. const renderStatusAndCounts = () =>
  79. issue && (
  80. <IssueContextContainer data-test-id="quick-context-issue-status-container">
  81. <ContextRow>
  82. <div>
  83. <ContextHeader>
  84. <ContextTitle>{t('Events')}</ContextTitle>
  85. </ContextHeader>
  86. <ContextBody>
  87. <Count className="count" value={issue.count} />
  88. </ContextBody>
  89. </div>
  90. <div>
  91. <ContextHeader>
  92. <ContextTitle>{t('Users')}</ContextTitle>
  93. </ContextHeader>
  94. <ContextBody>
  95. <Count className="count" value={issue.userCount} />
  96. </ContextBody>
  97. </div>
  98. <div>
  99. <ContextHeader>
  100. <ContextTitle>{t('Issue Status')}</ContextTitle>
  101. </ContextHeader>
  102. <ContextBody>
  103. {issue.status === 'ignored' ? (
  104. <IconMute
  105. data-test-id="quick-context-ignored-icon"
  106. color="gray500"
  107. size="xs"
  108. />
  109. ) : issue.status === 'resolved' ? (
  110. <IconCheckmark color="gray500" size="xs" />
  111. ) : (
  112. <IconNot
  113. data-test-id="quick-context-unresolved-icon"
  114. color="gray500"
  115. size="xs"
  116. />
  117. )}
  118. <StatusText>{issue.status}</StatusText>
  119. </ContextBody>
  120. </div>
  121. </ContextRow>
  122. </IssueContextContainer>
  123. );
  124. const renderAssignee = () =>
  125. issue && (
  126. <IssueContextContainer data-test-id="quick-context-assigned-to-container">
  127. <ContextHeader>
  128. <ContextTitle>{t('Assigned To')}</ContextTitle>
  129. </ContextHeader>
  130. <AssignedToBody>
  131. {issue.assignedTo ? (
  132. <ActorAvatar
  133. data-test-id="assigned-avatar"
  134. actor={issue.assignedTo}
  135. hasTooltip={false}
  136. size={24}
  137. />
  138. ) : (
  139. <StyledIconWrapper>
  140. <IconUser size="md" />
  141. </StyledIconWrapper>
  142. )}
  143. {getAssignedToDisplayName(issue) ?? t('No one')}
  144. </AssignedToBody>
  145. </IssueContextContainer>
  146. );
  147. const renderSuspectCommits = () =>
  148. event?.eventID &&
  149. issue && (
  150. <SuspectCommitsContainer data-test-id="quick-context-suspect-commits-container">
  151. <SuspectCommits
  152. project={issue.project}
  153. eventId={event.eventID}
  154. commitRow={QuickContextCommitRow}
  155. />
  156. </SuspectCommitsContainer>
  157. );
  158. const isLoading = issueLoading || eventLoading;
  159. const isError = issueError || eventError;
  160. if (isLoading || isError) {
  161. return <NoContext isLoading={isLoading} />;
  162. }
  163. return (
  164. <Wrapper data-test-id="quick-context-hover-body">
  165. {renderTitle()}
  166. {renderStatusAndCounts()}
  167. {renderAssignee()}
  168. {renderSuspectCommits()}
  169. </Wrapper>
  170. );
  171. }
  172. const SuspectCommitsContainer = styled(ContextContainer)`
  173. ${SidebarSection.Wrap}, ${Panel}, h6 {
  174. margin: 0;
  175. }
  176. ${StyledPanel} {
  177. border: none;
  178. box-shadow: none;
  179. }
  180. ${SuspectCommitHeader} {
  181. margin: ${space(2)} 0 ${space(0.75)};
  182. }
  183. `;
  184. const IssueTitleBody = styled(ContextBody)`
  185. margin: 0;
  186. max-width: 300px;
  187. ${p => p.theme.overflowEllipsis}
  188. `;
  189. const IssueContextContainer = styled(ContextContainer)`
  190. & + & {
  191. margin-top: ${space(2)};
  192. }
  193. `;
  194. const StatusText = styled('span')`
  195. margin-left: ${space(0.5)};
  196. text-transform: capitalize;
  197. `;
  198. const AssignedToBody = styled(ContextBody)`
  199. gap: ${space(1)};
  200. `;
  201. const StyledIconWrapper = styled(IconWrapper)`
  202. margin: 0;
  203. `;
  204. export default IssueContext;