issueList.tsx 4.7 KB

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