replayTable.tsx 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. import React, {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import Duration from 'sentry/components/duration';
  4. import ProjectBadge from 'sentry/components/idBadge/projectBadge';
  5. import UserBadge from 'sentry/components/idBadge/userBadge';
  6. import Link from 'sentry/components/links/link';
  7. import Placeholder from 'sentry/components/placeholder';
  8. import TimeSince from 'sentry/components/timeSince';
  9. import {IconCalendar} from 'sentry/icons';
  10. import space from 'sentry/styles/space';
  11. import {NewQuery} from 'sentry/types';
  12. import DiscoverQuery from 'sentry/utils/discover/discoverQuery';
  13. import EventView from 'sentry/utils/discover/eventView';
  14. import {generateEventSlug} from 'sentry/utils/discover/urls';
  15. import getUrlPathname from 'sentry/utils/getUrlPathname';
  16. import theme from 'sentry/utils/theme';
  17. import {useLocation} from 'sentry/utils/useLocation';
  18. import useMedia from 'sentry/utils/useMedia';
  19. import useOrganization from 'sentry/utils/useOrganization';
  20. import usePageFilters from 'sentry/utils/usePageFilters';
  21. import useProjects from 'sentry/utils/useProjects';
  22. import {Replay} from './types';
  23. type Props = {
  24. replayList: Replay[];
  25. };
  26. type ReplayDurationAndErrors = {
  27. count_if_event_type_equals_error: number;
  28. 'equation[0]': number;
  29. id: string;
  30. max_timestamp: string;
  31. min_timestamp: string;
  32. replayId: string;
  33. };
  34. function ReplayTable({replayList}: Props) {
  35. const location = useLocation();
  36. const organization = useOrganization();
  37. const {projects} = useProjects();
  38. const {selection} = usePageFilters();
  39. const isScreenLarge = useMedia(`(min-width: ${theme.breakpoints[0]})`);
  40. const getEventView = () => {
  41. const query = replayList.map(item => `replayId:${item.id}`).join(' OR ');
  42. const eventQueryParams: NewQuery = {
  43. id: '',
  44. name: '',
  45. version: 2,
  46. fields: [
  47. 'replayId',
  48. 'max(timestamp)',
  49. 'min(timestamp)',
  50. 'equation|max(timestamp)-min(timestamp)',
  51. 'count_if(event.type,equals,error)',
  52. ],
  53. orderby: '-min_timestamp',
  54. environment: selection.environments,
  55. projects: selection.projects,
  56. query: `(title:"sentry-replay-event-*" OR event.type:error) AND (${query})`,
  57. };
  58. if (selection.datetime.period) {
  59. eventQueryParams.range = selection.datetime.period;
  60. }
  61. return EventView.fromNewQueryWithLocation(eventQueryParams, location);
  62. };
  63. return (
  64. <DiscoverQuery
  65. eventView={getEventView()}
  66. location={location}
  67. orgSlug={organization.slug}
  68. >
  69. {data => {
  70. const dataEntries = data.tableData
  71. ? Object.fromEntries(
  72. (data.tableData?.data as ReplayDurationAndErrors[]).map(item => [
  73. item.replayId,
  74. item,
  75. ])
  76. )
  77. : {};
  78. return replayList?.map(replay => {
  79. return (
  80. <Fragment key={replay.id}>
  81. <UserBadge
  82. avatarSize={32}
  83. displayName={
  84. <Link
  85. to={`/organizations/${organization.slug}/replays/${generateEventSlug({
  86. project: replay.project,
  87. id: replay.id,
  88. })}/`}
  89. >
  90. {replay['user.display']}
  91. </Link>
  92. }
  93. user={{
  94. username: replay['user.username'] ?? '',
  95. id: replay['user.id'] ?? '',
  96. ip_address: replay['user.ip_address'] ?? '',
  97. name: replay['user.name'] ?? '',
  98. email: replay['user.email'] ?? '',
  99. }}
  100. // this is the subheading for the avatar, so displayEmail in this case is a misnomer
  101. displayEmail={getUrlPathname(replay.url) ?? ''}
  102. />
  103. {isScreenLarge && (
  104. <Item>
  105. <ProjectBadge
  106. project={
  107. projects.find(p => p.slug === replay.project) || {
  108. slug: replay.project,
  109. }
  110. }
  111. avatarSize={16}
  112. />
  113. </Item>
  114. )}
  115. <Item>
  116. <TimeSinceWrapper>
  117. {isScreenLarge && (
  118. <StyledIconCalendarWrapper color="gray500" size="sm" />
  119. )}
  120. <TimeSince date={replay.timestamp} />
  121. </TimeSinceWrapper>
  122. </Item>
  123. {data.tableData ? (
  124. <React.Fragment>
  125. <Item>
  126. <Duration
  127. seconds={
  128. Math.floor(
  129. dataEntries[replay.id]
  130. ? dataEntries[replay.id]['equation[0]']
  131. : 0
  132. ) || 1
  133. }
  134. exact
  135. abbreviation
  136. />
  137. </Item>
  138. <Item>
  139. {dataEntries[replay.id]
  140. ? dataEntries[replay.id]?.count_if_event_type_equals_error
  141. : 0}
  142. </Item>
  143. </React.Fragment>
  144. ) : (
  145. <React.Fragment>
  146. <Item>
  147. <Placeholder height="24px" />
  148. </Item>
  149. <Item>
  150. <Placeholder height="24px" />
  151. </Item>
  152. </React.Fragment>
  153. )}
  154. </Fragment>
  155. );
  156. });
  157. }}
  158. </DiscoverQuery>
  159. );
  160. }
  161. const Item = styled('div')`
  162. display: flex;
  163. align-items: center;
  164. `;
  165. const TimeSinceWrapper = styled('div')`
  166. display: grid;
  167. grid-template-columns: repeat(2, minmax(auto, max-content));
  168. align-items: center;
  169. gap: ${space(1)};
  170. `;
  171. const StyledIconCalendarWrapper = styled(IconCalendar)`
  172. position: relative;
  173. top: -1px;
  174. `;
  175. export default ReplayTable;