groupReplays.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. import {useCallback, useEffect, useMemo, useState} from 'react';
  2. import {useTheme} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import * as Sentry from '@sentry/react';
  5. import {Location} from 'history';
  6. import first from 'lodash/first';
  7. import * as Layout from 'sentry/components/layouts/thirds';
  8. import Pagination from 'sentry/components/pagination';
  9. import type {Group, Organization} from 'sentry/types';
  10. import {TableData} from 'sentry/utils/discover/discoverQuery';
  11. import EventView from 'sentry/utils/discover/eventView';
  12. import {doDiscoverQuery} from 'sentry/utils/discover/genericDiscoverQuery';
  13. import {decodeScalar} from 'sentry/utils/queryString';
  14. import {DEFAULT_SORT, REPLAY_LIST_FIELDS} from 'sentry/utils/replays/fetchReplayList';
  15. import useReplayList from 'sentry/utils/replays/hooks/useReplayList';
  16. import useApi from 'sentry/utils/useApi';
  17. import useCleanQueryParamsOnRouteLeave from 'sentry/utils/useCleanQueryParamsOnRouteLeave';
  18. import {useLocation} from 'sentry/utils/useLocation';
  19. import useMedia from 'sentry/utils/useMedia';
  20. import useOrganization from 'sentry/utils/useOrganization';
  21. import ReplayTable from 'sentry/views/replays/replayTable';
  22. import {ReplayColumns} from 'sentry/views/replays/replayTable/types';
  23. import type {ReplayListLocationQuery} from 'sentry/views/replays/types';
  24. type Props = {
  25. group: Group;
  26. };
  27. function GroupReplays({group}: Props) {
  28. const api = useApi();
  29. const location = useLocation<ReplayListLocationQuery>();
  30. const organization = useOrganization();
  31. const theme = useTheme();
  32. const hasRoomForColumns = useMedia(`(min-width: ${theme.breakpoints.small})`);
  33. const [response, setResponse] = useState<{
  34. pageLinks: null | string;
  35. replayIds: undefined | string[];
  36. }>({pageLinks: null, replayIds: undefined});
  37. const [fetchError, setFetchError] = useState();
  38. const {cursor} = location.query;
  39. const fetchReplayIds = useCallback(async () => {
  40. const eventView = EventView.fromSavedQuery({
  41. id: '',
  42. name: `Errors within replay`,
  43. version: 2,
  44. fields: ['replayId', 'count()'],
  45. query: `issue.id:${group.id} !replayId:""`,
  46. projects: [Number(group.project.id)],
  47. });
  48. try {
  49. const [{data}, _textStatus, resp] = await doDiscoverQuery<TableData>(
  50. api,
  51. `/organizations/${organization.slug}/events/`,
  52. eventView.getEventsAPIPayload({
  53. query: {cursor},
  54. } as Location<ReplayListLocationQuery>)
  55. );
  56. setResponse({
  57. pageLinks: resp?.getResponseHeader('Link') ?? '',
  58. replayIds: data.map(record => String(record.replayId)),
  59. });
  60. } catch (err) {
  61. Sentry.captureException(err);
  62. setFetchError(err);
  63. }
  64. }, [api, cursor, organization.slug, group.id, group.project.id]);
  65. const eventView = useMemo(() => {
  66. if (!response.replayIds) {
  67. return null;
  68. }
  69. return EventView.fromSavedQuery({
  70. id: '',
  71. name: '',
  72. version: 2,
  73. fields: REPLAY_LIST_FIELDS,
  74. projects: [Number(group.project.id)],
  75. query: `id:[${String(response.replayIds)}]`,
  76. orderby: decodeScalar(location.query.sort, DEFAULT_SORT),
  77. });
  78. }, [location.query.sort, group.project.id, response.replayIds]);
  79. useCleanQueryParamsOnRouteLeave({fieldsToClean: ['cursor']});
  80. useEffect(() => {
  81. fetchReplayIds();
  82. }, [fetchReplayIds]);
  83. if (!eventView) {
  84. return (
  85. <StyledLayoutPage withPadding>
  86. <ReplayTable
  87. fetchError={fetchError}
  88. isFetching
  89. replays={[]}
  90. sort={undefined}
  91. visibleColumns={[
  92. ReplayColumns.session,
  93. ...(hasRoomForColumns ? [ReplayColumns.startedAt] : []),
  94. ReplayColumns.duration,
  95. ReplayColumns.countErrors,
  96. ReplayColumns.activity,
  97. ]}
  98. />
  99. <Pagination pageLinks={null} />
  100. </StyledLayoutPage>
  101. );
  102. }
  103. return (
  104. <GroupReplaysTable
  105. eventView={eventView}
  106. organization={organization}
  107. pageLinks={response.pageLinks}
  108. />
  109. );
  110. }
  111. const GroupReplaysTable = ({
  112. eventView,
  113. organization,
  114. pageLinks,
  115. }: {
  116. eventView: EventView;
  117. organization: Organization;
  118. pageLinks: string | null;
  119. }) => {
  120. const location = useMemo(() => ({query: {}} as Location<ReplayListLocationQuery>), []);
  121. const theme = useTheme();
  122. const hasRoomForColumns = useMedia(`(min-width: ${theme.breakpoints.small})`);
  123. const {replays, isFetching, fetchError} = useReplayList({
  124. eventView,
  125. location,
  126. organization,
  127. });
  128. return (
  129. <StyledLayoutPage withPadding>
  130. <ReplayTable
  131. fetchError={fetchError}
  132. isFetching={isFetching}
  133. replays={replays}
  134. sort={first(eventView.sorts)}
  135. visibleColumns={[
  136. ReplayColumns.session,
  137. ...(hasRoomForColumns ? [ReplayColumns.startedAt] : []),
  138. ReplayColumns.duration,
  139. ReplayColumns.countErrors,
  140. ReplayColumns.activity,
  141. ]}
  142. />
  143. <Pagination pageLinks={pageLinks} />
  144. </StyledLayoutPage>
  145. );
  146. };
  147. const StyledLayoutPage = styled(Layout.Page)`
  148. box-shadow: 0px 0px 1px ${p => p.theme.gray200};
  149. background-color: ${p => p.theme.background};
  150. `;
  151. export default GroupReplays;