issues.tsx 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. import styled from '@emotion/styled';
  2. import ActorAvatar from 'sentry/components/avatar/actorAvatar';
  3. import Count from 'sentry/components/count';
  4. import EventOrGroupExtraDetails from 'sentry/components/eventOrGroupExtraDetails';
  5. import LoadingError from 'sentry/components/loadingError';
  6. import LoadingIndicator from 'sentry/components/loadingIndicator';
  7. import Panel from 'sentry/components/panels/panel';
  8. import PanelHeader from 'sentry/components/panels/panelHeader';
  9. import PanelItem from 'sentry/components/panels/panelItem';
  10. import {IconWrapper} from 'sentry/components/sidebarSection';
  11. import GroupChart from 'sentry/components/stream/groupChart';
  12. import {IconUser} from 'sentry/icons';
  13. import {t, tct, tn} from 'sentry/locale';
  14. import {space} from 'sentry/styles/space';
  15. import type {Group, Organization} from 'sentry/types';
  16. import type {TraceErrorOrIssue} from 'sentry/utils/performance/quickTrace/types';
  17. import {useApiQuery} from 'sentry/utils/queryClient';
  18. import type {
  19. TraceTree,
  20. TraceTreeNode,
  21. } from 'sentry/views/performance/newTraceDetails/traceTree';
  22. import {IssueSummary} from './issueSummary';
  23. type IssueProps = {
  24. issue: TraceErrorOrIssue;
  25. organization: Organization;
  26. };
  27. const MAX_DISPLAYED_ISSUES_COUNT = 10;
  28. function Issue(props: IssueProps) {
  29. const {
  30. isLoading,
  31. data: fetchedIssue,
  32. isError,
  33. } = useApiQuery<Group>(
  34. [
  35. `/issues/${props.issue.issue_id}/`,
  36. {
  37. query: {
  38. collapse: 'release',
  39. expand: 'inbox',
  40. },
  41. },
  42. ],
  43. {
  44. staleTime: 2 * 60 * 1000,
  45. }
  46. );
  47. return isLoading ? (
  48. <StyledLoadingIndicatorWrapper>
  49. <LoadingIndicator size={24} mini />
  50. </StyledLoadingIndicatorWrapper>
  51. ) : fetchedIssue ? (
  52. <StyledPanelItem>
  53. <IssueSummaryWrapper>
  54. <IssueSummary
  55. data={fetchedIssue}
  56. organization={props.organization}
  57. event_id={props.issue.event_id}
  58. />
  59. <EventOrGroupExtraDetails data={fetchedIssue} />
  60. </IssueSummaryWrapper>
  61. <ChartWrapper>
  62. <GroupChart
  63. statsPeriod={'24h'}
  64. data={fetchedIssue}
  65. showSecondaryPoints
  66. showMarkLine
  67. />
  68. </ChartWrapper>
  69. <ColumnWrapper>
  70. <PrimaryCount
  71. value={fetchedIssue.filtered ? fetchedIssue.filtered.count : fetchedIssue.count}
  72. />
  73. </ColumnWrapper>
  74. <ColumnWrapper>
  75. <PrimaryCount
  76. value={
  77. fetchedIssue.filtered
  78. ? fetchedIssue.filtered.userCount
  79. : fetchedIssue.userCount
  80. }
  81. />
  82. </ColumnWrapper>
  83. <ColumnWrapper>
  84. {fetchedIssue.assignedTo ? (
  85. <ActorAvatar actor={fetchedIssue.assignedTo} hasTooltip size={24} />
  86. ) : (
  87. <StyledIconWrapper>
  88. <IconUser size="md" />
  89. </StyledIconWrapper>
  90. )}
  91. </ColumnWrapper>
  92. </StyledPanelItem>
  93. ) : isError ? (
  94. <LoadingError message={t('Failed to fetch issue')} />
  95. ) : null;
  96. }
  97. type IssueListProps = {
  98. issues: TraceErrorOrIssue[];
  99. node: TraceTreeNode<TraceTree.NodeValue>;
  100. organization: Organization;
  101. };
  102. export function IssueList({issues, node, organization}: IssueListProps) {
  103. if (!issues.length) {
  104. return null;
  105. }
  106. return (
  107. <StyledPanel>
  108. <IssueListHeader node={node} />
  109. {issues.slice(0, MAX_DISPLAYED_ISSUES_COUNT).map((issue, index) => (
  110. <Issue key={index} issue={issue} organization={organization} />
  111. ))}
  112. </StyledPanel>
  113. );
  114. }
  115. function IssueListHeader({node}: {node: TraceTreeNode<TraceTree.NodeValue>}) {
  116. const {errors, performance_issues} = node;
  117. return (
  118. <StyledPanelHeader disablePadding>
  119. <IssueHeading>
  120. {errors.length + performance_issues.length > MAX_DISPLAYED_ISSUES_COUNT
  121. ? t(`%s+ issues`, MAX_DISPLAYED_ISSUES_COUNT)
  122. : errors.length > 0 && performance_issues.length === 0
  123. ? tct('[count] [text]', {
  124. count: errors.length,
  125. text: tn('Error', 'Errors', errors.length),
  126. })
  127. : performance_issues.length > 0 && errors.length === 0
  128. ? tct('[count] [text]', {
  129. count: performance_issues.length,
  130. text: tn(
  131. 'Performance issue',
  132. 'Performance Issues',
  133. performance_issues.length
  134. ),
  135. })
  136. : tct(
  137. '[errors] [errorsText] and [performance_issues] [performanceIssuesText]',
  138. {
  139. errors: errors.length,
  140. performance_issues: performance_issues.length,
  141. errorsText: tn('Error', 'Errors', errors.length),
  142. performanceIssuesText: tn(
  143. 'performance issue',
  144. 'performance issues',
  145. performance_issues.length
  146. ),
  147. }
  148. )}
  149. </IssueHeading>
  150. <GraphHeading>{t('Graph')}</GraphHeading>
  151. <Heading>{t('Events')}</Heading>
  152. <UsersHeading>{t('Users')}</UsersHeading>
  153. <Heading>{t('Assignee')}</Heading>
  154. </StyledPanelHeader>
  155. );
  156. }
  157. const Heading = styled('div')`
  158. display: flex;
  159. align-self: center;
  160. margin: 0 ${space(2)};
  161. width: 60px;
  162. color: ${p => p.theme.subText};
  163. `;
  164. const IssueHeading = styled(Heading)`
  165. flex: 1;
  166. width: 66.66%;
  167. @media (min-width: ${p => p.theme.breakpoints.medium}) {
  168. width: 50%;
  169. }
  170. `;
  171. const GraphHeading = styled(Heading)`
  172. width: 160px;
  173. display: flex;
  174. justify-content: center;
  175. `;
  176. const UsersHeading = styled(Heading)`
  177. display: flex;
  178. justify-content: center;
  179. `;
  180. const StyledPanel = styled(Panel)`
  181. margin-bottom: 0;
  182. border: 1px solid ${p => p.theme.red200};
  183. `;
  184. const StyledPanelHeader = styled(PanelHeader)`
  185. padding-top: ${space(1)};
  186. padding-bottom: ${space(1)};
  187. border-bottom: 1px solid ${p => p.theme.red200};
  188. `;
  189. const StyledLoadingIndicatorWrapper = styled('div')`
  190. display: flex;
  191. justify-content: center;
  192. width: 100%;
  193. padding: ${space(2)} 0;
  194. height: 84px;
  195. /* Add a border between two rows of loading issue states */
  196. & + & {
  197. border-top: 1px solid ${p => p.theme.border};
  198. }
  199. `;
  200. const StyledIconWrapper = styled(IconWrapper)`
  201. margin: 0;
  202. `;
  203. const IssueSummaryWrapper = styled('div')`
  204. overflow: hidden;
  205. flex: 1;
  206. width: 66.66%;
  207. @media (min-width: ${p => p.theme.breakpoints.medium}) {
  208. width: 50%;
  209. }
  210. `;
  211. const ChartWrapper = styled('div')`
  212. width: 200px;
  213. align-self: center;
  214. `;
  215. const ColumnWrapper = styled('div')`
  216. display: flex;
  217. justify-content: flex-end;
  218. align-self: center;
  219. width: 60px;
  220. margin: 0 ${space(2)};
  221. `;
  222. const PrimaryCount = styled(Count)`
  223. font-size: ${p => p.theme.fontSizeLarge};
  224. font-variant-numeric: tabular-nums;
  225. `;
  226. const StyledPanelItem = styled(PanelItem)`
  227. padding-top: ${space(1)};
  228. padding-bottom: ${space(1)};
  229. height: 84px;
  230. `;