issueList.tsx 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. import {Fragment, useCallback, useEffect, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import * as Sentry from '@sentry/react';
  4. import EventOrGroupExtraDetails from 'sentry/components/eventOrGroupExtraDetails';
  5. import EventOrGroupHeader from 'sentry/components/eventOrGroupHeader';
  6. import {PanelTable} from 'sentry/components/panels';
  7. import ReplayCountContext from 'sentry/components/replays/replayCountContext';
  8. import useReplaysCount from 'sentry/components/replays/useReplaysCount';
  9. import {DEFAULT_STREAM_GROUP_STATS_PERIOD} from 'sentry/components/stream/group';
  10. import GroupChart from 'sentry/components/stream/groupChart';
  11. import {t} from 'sentry/locale';
  12. import {space} from 'sentry/styles/space';
  13. import {Group, Organization} from 'sentry/types';
  14. import RequestError from 'sentry/utils/requestError/requestError';
  15. import theme from 'sentry/utils/theme';
  16. import useApi from 'sentry/utils/useApi';
  17. import useMedia from 'sentry/utils/useMedia';
  18. import useOrganization from 'sentry/utils/useOrganization';
  19. type Props = {
  20. projectId: string;
  21. replayId: string;
  22. };
  23. const columns = [t('Issue'), t('Graph'), t('Events'), t('Users')];
  24. type State = {
  25. fetchError: undefined | RequestError;
  26. fetching: boolean;
  27. issues: Group[];
  28. };
  29. function IssueList({projectId, replayId}: Props) {
  30. const organization = useOrganization();
  31. const api = useApi();
  32. const isScreenLarge = useMedia(`(min-width: ${theme.breakpoints.large})`);
  33. const [state, setState] = useState<State>({
  34. fetchError: undefined,
  35. fetching: true,
  36. issues: [],
  37. });
  38. const fetchIssueData = useCallback(async () => {
  39. setState(prev => ({
  40. ...prev,
  41. fetching: true,
  42. }));
  43. try {
  44. const issues = await api.requestPromise(
  45. `/organizations/${organization.slug}/issues/`,
  46. {
  47. query: {
  48. query: `replayId:${replayId}`,
  49. },
  50. headers: {
  51. 'x-sentry-replay-request': '1',
  52. },
  53. }
  54. );
  55. setState({
  56. fetchError: undefined,
  57. fetching: false,
  58. issues,
  59. });
  60. } catch (fetchError) {
  61. Sentry.captureException(fetchError);
  62. setState({
  63. fetchError,
  64. fetching: false,
  65. issues: [],
  66. });
  67. }
  68. }, [api, organization.slug, replayId]);
  69. useEffect(() => {
  70. fetchIssueData();
  71. }, [fetchIssueData]);
  72. const counts = useReplaysCount({
  73. groupIds: state.issues.map(issue => issue.id),
  74. organization,
  75. });
  76. return (
  77. <ReplayCountContext.Provider value={counts}>
  78. <StyledPanelTable
  79. isEmpty={state.issues.length === 0}
  80. emptyMessage={t('No Issues are related')}
  81. isLoading={state.fetching}
  82. headers={
  83. isScreenLarge ? columns : columns.filter(column => column !== t('Graph'))
  84. }
  85. >
  86. {state.issues
  87. // prioritize the replay issues first
  88. .sort(a => (a.project.id === projectId ? -1 : 1))
  89. .map(issue => (
  90. <TableRow
  91. key={issue.id}
  92. isScreenLarge={isScreenLarge}
  93. issue={issue}
  94. organization={organization}
  95. />
  96. )) || null}
  97. </StyledPanelTable>
  98. </ReplayCountContext.Provider>
  99. );
  100. }
  101. function TableRow({
  102. isScreenLarge,
  103. issue,
  104. organization,
  105. }: {
  106. isScreenLarge: boolean;
  107. issue: Group;
  108. organization: Organization;
  109. }) {
  110. return (
  111. <Fragment>
  112. <IssueDetailsWrapper>
  113. <EventOrGroupHeader
  114. includeLink
  115. data={issue}
  116. organization={organization}
  117. size="normal"
  118. source="replay"
  119. />
  120. <EventOrGroupExtraDetails data={issue} />
  121. </IssueDetailsWrapper>
  122. {isScreenLarge && (
  123. <ChartWrapper>
  124. <GroupChart
  125. statsPeriod={DEFAULT_STREAM_GROUP_STATS_PERIOD}
  126. data={issue}
  127. showSecondaryPoints
  128. showMarkLine
  129. />
  130. </ChartWrapper>
  131. )}
  132. <Item>{issue.count}</Item>
  133. <Item>{issue.userCount}</Item>
  134. </Fragment>
  135. );
  136. }
  137. const ChartWrapper = styled('div')`
  138. width: 200px;
  139. margin-left: -${space(2)};
  140. padding-left: ${space(0)};
  141. `;
  142. const Item = styled('div')`
  143. display: flex;
  144. align-items: center;
  145. `;
  146. const IssueDetailsWrapper = styled('div')`
  147. overflow: hidden;
  148. line-height: normal;
  149. `;
  150. const StyledPanelTable = styled(PanelTable)`
  151. /* overflow: visible allows the tooltip to be completely shown */
  152. overflow: visible;
  153. grid-template-columns: minmax(1fr, max-content) repeat(3, max-content);
  154. @media (max-width: ${p => p.theme.breakpoints.large}) {
  155. grid-template-columns: minmax(0, 1fr) repeat(2, max-content);
  156. }
  157. `;
  158. export default IssueList;