replayTable.tsx 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. import React, {Fragment, useMemo} 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 {generateEventSlug} from 'sentry/utils/discover/urls';
  12. import getUrlPathname from 'sentry/utils/getUrlPathname';
  13. import useDiscoverQuery from 'sentry/utils/replays/hooks/useDiscoveryQuery';
  14. import theme from 'sentry/utils/theme';
  15. import useMedia from 'sentry/utils/useMedia';
  16. import useOrganization from 'sentry/utils/useOrganization';
  17. import useProjects from 'sentry/utils/useProjects';
  18. import {Replay} from './types';
  19. type Props = {
  20. idKey: string;
  21. replayList: Replay[];
  22. showProjectColumn?: boolean;
  23. };
  24. type ReplayDurationAndErrors = {
  25. count_if_event_type_equals_error: number;
  26. 'equation[0]': number;
  27. id: string;
  28. max_timestamp: string;
  29. min_timestamp: string;
  30. replayId: string;
  31. };
  32. function ReplayTable({replayList, idKey, showProjectColumn}: Props) {
  33. const organization = useOrganization();
  34. const {projects} = useProjects();
  35. const isScreenLarge = useMedia(`(min-width: ${theme.breakpoints.small})`);
  36. const query = replayList.map(item => `replayId:${item[idKey]}`).join(' OR ');
  37. const discoverQuery = useMemo(
  38. () => ({
  39. fields: [
  40. 'replayId',
  41. 'max(timestamp)',
  42. 'min(timestamp)',
  43. 'equation|max(timestamp)-min(timestamp)',
  44. 'count_if(event.type,equals,error)',
  45. ],
  46. orderby: '-min_timestamp',
  47. query: `(title:"sentry-replay-event-*" OR event.type:error) AND (${query})`,
  48. }),
  49. [query]
  50. );
  51. const {data} = useDiscoverQuery<ReplayDurationAndErrors>({discoverQuery});
  52. const dataEntries = data
  53. ? Object.fromEntries(data.map(item => [item.replayId, item]))
  54. : {};
  55. return (
  56. <Fragment>
  57. {replayList?.map(replay => (
  58. <Fragment key={replay.id}>
  59. <UserBadge
  60. avatarSize={32}
  61. displayName={
  62. <Link
  63. to={`/organizations/${organization.slug}/replays/${generateEventSlug({
  64. project: replay.project,
  65. id: replay[idKey],
  66. })}/`}
  67. >
  68. {replay['user.display']}
  69. </Link>
  70. }
  71. user={{
  72. username: replay['user.username'] ?? '',
  73. id: replay['user.id'] ?? '',
  74. ip_address: replay['user.ip_address'] ?? '',
  75. name: replay['user.name'] ?? '',
  76. email: replay['user.email'] ?? '',
  77. }}
  78. // this is the subheading for the avatar, so displayEmail in this case is a misnomer
  79. displayEmail={getUrlPathname(replay.url) ?? ''}
  80. />
  81. {isScreenLarge && showProjectColumn && (
  82. <Item>
  83. <ProjectBadge
  84. project={
  85. projects.find(p => p.slug === replay.project) || {
  86. slug: replay.project,
  87. }
  88. }
  89. avatarSize={16}
  90. />
  91. </Item>
  92. )}
  93. <Item>
  94. <TimeSinceWrapper>
  95. {isScreenLarge && <StyledIconCalendarWrapper color="gray500" size="sm" />}
  96. <TimeSince date={replay.timestamp} />
  97. </TimeSinceWrapper>
  98. </Item>
  99. {data ? (
  100. <React.Fragment>
  101. <Item>
  102. <Duration
  103. seconds={
  104. Math.floor(
  105. dataEntries[replay[idKey]]
  106. ? dataEntries[replay[idKey]]['equation[0]']
  107. : 0
  108. ) || 1
  109. }
  110. exact
  111. abbreviation
  112. />
  113. </Item>
  114. <Item>
  115. {dataEntries[replay[idKey]]
  116. ? dataEntries[replay[idKey]]?.count_if_event_type_equals_error
  117. : 0}
  118. </Item>
  119. </React.Fragment>
  120. ) : (
  121. <React.Fragment>
  122. <Item>
  123. <Placeholder height="24px" />
  124. </Item>
  125. <Item>
  126. <Placeholder height="24px" />
  127. </Item>
  128. </React.Fragment>
  129. )}
  130. </Fragment>
  131. ))}
  132. </Fragment>
  133. );
  134. }
  135. const Item = styled('div')`
  136. display: flex;
  137. align-items: center;
  138. `;
  139. const TimeSinceWrapper = styled('div')`
  140. display: grid;
  141. grid-template-columns: repeat(2, minmax(auto, max-content));
  142. align-items: center;
  143. gap: ${space(1)};
  144. `;
  145. const StyledIconCalendarWrapper = styled(IconCalendar)`
  146. position: relative;
  147. top: -1px;
  148. `;
  149. export default ReplayTable;