replayTable.tsx 6.0 KB

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