issueContext.tsx 6.2 KB

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