issueContext.tsx 6.3 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 {DataSection, 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, 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 type {BaseContextProps} from './utils';
  29. import {ContextType, tenSecondInMs} from './utils';
  30. function IssueContext(props: BaseContextProps) {
  31. const {dataRow, organization} = props;
  32. useEffect(() => {
  33. trackAnalytics('discover_v2.quick_context_hover_contexts', {
  34. organization,
  35. contextType: ContextType.ISSUE,
  36. });
  37. }, [organization]);
  38. const {
  39. isLoading: issueLoading,
  40. isError: issueError,
  41. data: issue,
  42. } = useApiQuery<Group>(
  43. [
  44. `/issues/${dataRow['issue.id']}/`,
  45. {
  46. query: {
  47. collapse: 'release',
  48. expand: 'inbox',
  49. },
  50. },
  51. ],
  52. {
  53. staleTime: tenSecondInMs,
  54. }
  55. );
  56. // NOTE: Suspect commits are generated from the first event of an issue.
  57. // Therefore, all events for an issue have the same suspect commits.
  58. const {
  59. isLoading: eventLoading,
  60. isError: eventError,
  61. data: event,
  62. } = useApiQuery<Event>([`/issues/${dataRow['issue.id']}/events/oldest/`], {
  63. staleTime: tenSecondInMs,
  64. });
  65. const title = issue?.title;
  66. const renderTitle = () =>
  67. issue && (
  68. <IssueContextContainer data-test-id="quick-context-issue-title-container">
  69. <ContextHeader>
  70. <ContextTitle>{t('Title')}</ContextTitle>
  71. </ContextHeader>
  72. <Tooltip showOnlyOnOverflow skipWrapper title={title}>
  73. <IssueTitleBody>{title}</IssueTitleBody>
  74. </Tooltip>
  75. </IssueContextContainer>
  76. );
  77. const renderStatusAndCounts = () =>
  78. issue && (
  79. <IssueContextContainer data-test-id="quick-context-issue-status-container">
  80. <ContextRow>
  81. <div>
  82. <ContextHeader>
  83. <ContextTitle>{t('Events')}</ContextTitle>
  84. </ContextHeader>
  85. <ContextBody>
  86. <Count className="count" value={issue.count} />
  87. </ContextBody>
  88. </div>
  89. <div>
  90. <ContextHeader>
  91. <ContextTitle>{t('Users')}</ContextTitle>
  92. </ContextHeader>
  93. <ContextBody>
  94. <Count className="count" value={issue.userCount} />
  95. </ContextBody>
  96. </div>
  97. <div>
  98. <ContextHeader>
  99. <ContextTitle>{t('Issue Status')}</ContextTitle>
  100. </ContextHeader>
  101. <ContextBody>
  102. {issue.status === 'ignored' ? (
  103. <IconMute
  104. data-test-id="quick-context-ignored-icon"
  105. color="gray500"
  106. size="xs"
  107. />
  108. ) : issue.status === 'resolved' ? (
  109. <IconCheckmark color="gray500" size="xs" />
  110. ) : (
  111. <IconNot
  112. data-test-id="quick-context-unresolved-icon"
  113. color="gray500"
  114. size="xs"
  115. />
  116. )}
  117. <StatusText>{issue.status}</StatusText>
  118. </ContextBody>
  119. </div>
  120. </ContextRow>
  121. </IssueContextContainer>
  122. );
  123. const renderAssignee = () =>
  124. issue && (
  125. <IssueContextContainer data-test-id="quick-context-assigned-to-container">
  126. <ContextHeader>
  127. <ContextTitle>{t('Assigned To')}</ContextTitle>
  128. </ContextHeader>
  129. <AssignedToBody>
  130. {issue.assignedTo ? (
  131. <ActorAvatar
  132. data-test-id="assigned-avatar"
  133. actor={issue.assignedTo}
  134. hasTooltip={false}
  135. size={24}
  136. />
  137. ) : (
  138. <StyledIconWrapper>
  139. <IconUser size="md" />
  140. </StyledIconWrapper>
  141. )}
  142. {getAssignedToDisplayName(issue) ?? t('No one')}
  143. </AssignedToBody>
  144. </IssueContextContainer>
  145. );
  146. const renderSuspectCommits = () =>
  147. event?.eventID &&
  148. issue && (
  149. <SuspectCommitsContainer data-test-id="quick-context-suspect-commits-container">
  150. <SuspectCommits
  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. ${SuspectCommitHeader} {
  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;