issueContext.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  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 {getAssignedToDisplayName} from 'sentry/components/group/assignedTo';
  6. import {IconWrapper} from 'sentry/components/sidebarSection';
  7. import {Tooltip} from 'sentry/components/tooltip';
  8. import {IconCheckmark, IconMute, IconNot, IconUser} from 'sentry/icons';
  9. import {t} from 'sentry/locale';
  10. import {space} from 'sentry/styles/space';
  11. import type {Group} from 'sentry/types/group';
  12. import {trackAnalytics} from 'sentry/utils/analytics';
  13. import {useApiQuery} from 'sentry/utils/queryClient';
  14. import {makeFetchGroupQueryKey} from 'sentry/views/issueDetails/useGroup';
  15. import {NoContext} from './quickContextWrapper';
  16. import {
  17. ContextBody,
  18. ContextContainer,
  19. ContextHeader,
  20. ContextRow,
  21. ContextTitle,
  22. Wrapper,
  23. } from './styles';
  24. import type {BaseContextProps} from './utils';
  25. import {ContextType} from './utils';
  26. function IssueContext(props: BaseContextProps) {
  27. const {dataRow, organization} = props;
  28. useEffect(() => {
  29. trackAnalytics('discover_v2.quick_context_hover_contexts', {
  30. organization,
  31. contextType: ContextType.ISSUE,
  32. });
  33. }, [organization]);
  34. const {
  35. isPending: issueLoading,
  36. isError: issueError,
  37. data: issue,
  38. } = useApiQuery<Group>(
  39. makeFetchGroupQueryKey({
  40. groupId: dataRow['issue.id'],
  41. organizationSlug: organization.slug,
  42. // The link to issue details doesn't seem to currently pass selected environments
  43. environments: [],
  44. }),
  45. {
  46. staleTime: 30_000,
  47. }
  48. );
  49. const title = issue?.title;
  50. const renderTitle = () =>
  51. issue && (
  52. <IssueContextContainer data-test-id="quick-context-issue-title-container">
  53. <ContextHeader>
  54. <ContextTitle>{t('Title')}</ContextTitle>
  55. </ContextHeader>
  56. <Tooltip showOnlyOnOverflow skipWrapper title={title}>
  57. <IssueTitleBody>{title}</IssueTitleBody>
  58. </Tooltip>
  59. </IssueContextContainer>
  60. );
  61. const renderStatusAndCounts = () =>
  62. issue && (
  63. <IssueContextContainer data-test-id="quick-context-issue-status-container">
  64. <ContextRow>
  65. <div>
  66. <ContextHeader>
  67. <ContextTitle>{t('Events')}</ContextTitle>
  68. </ContextHeader>
  69. <ContextBody>
  70. <Count className="count" value={issue.count} />
  71. </ContextBody>
  72. </div>
  73. <div>
  74. <ContextHeader>
  75. <ContextTitle>{t('Users')}</ContextTitle>
  76. </ContextHeader>
  77. <ContextBody>
  78. <Count className="count" value={issue.userCount} />
  79. </ContextBody>
  80. </div>
  81. <div>
  82. <ContextHeader>
  83. <ContextTitle>{t('Issue Status')}</ContextTitle>
  84. </ContextHeader>
  85. <ContextBody>
  86. {issue.status === 'ignored' ? (
  87. <IconMute
  88. data-test-id="quick-context-ignored-icon"
  89. color="gray500"
  90. size="xs"
  91. />
  92. ) : issue.status === 'resolved' ? (
  93. <IconCheckmark color="gray500" size="xs" />
  94. ) : (
  95. <IconNot
  96. data-test-id="quick-context-unresolved-icon"
  97. color="gray500"
  98. size="xs"
  99. />
  100. )}
  101. <StatusText>{issue.status}</StatusText>
  102. </ContextBody>
  103. </div>
  104. </ContextRow>
  105. </IssueContextContainer>
  106. );
  107. const renderAssignee = () =>
  108. issue && (
  109. <IssueContextContainer data-test-id="quick-context-assigned-to-container">
  110. <ContextHeader>
  111. <ContextTitle>{t('Assigned To')}</ContextTitle>
  112. </ContextHeader>
  113. <AssignedToBody>
  114. {issue.assignedTo ? (
  115. <ActorAvatar
  116. data-test-id="assigned-avatar"
  117. actor={issue.assignedTo}
  118. hasTooltip={false}
  119. size={24}
  120. />
  121. ) : (
  122. <StyledIconWrapper>
  123. <IconUser size="md" />
  124. </StyledIconWrapper>
  125. )}
  126. {getAssignedToDisplayName(issue) ?? t('No one')}
  127. </AssignedToBody>
  128. </IssueContextContainer>
  129. );
  130. if (issueLoading || issueError) {
  131. return <NoContext isLoading={issueLoading} />;
  132. }
  133. return (
  134. <Wrapper data-test-id="quick-context-hover-body">
  135. {renderTitle()}
  136. {renderStatusAndCounts()}
  137. {renderAssignee()}
  138. </Wrapper>
  139. );
  140. }
  141. const IssueTitleBody = styled(ContextBody)`
  142. margin: 0;
  143. max-width: 300px;
  144. ${p => p.theme.overflowEllipsis}
  145. `;
  146. const IssueContextContainer = styled(ContextContainer)`
  147. & + & {
  148. margin-top: ${space(2)};
  149. }
  150. `;
  151. const StatusText = styled('span')`
  152. margin-left: ${space(0.5)};
  153. text-transform: capitalize;
  154. `;
  155. const AssignedToBody = styled(ContextBody)`
  156. gap: ${space(1)};
  157. `;
  158. const StyledIconWrapper = styled(IconWrapper)`
  159. margin: 0;
  160. `;
  161. export default IssueContext;