replayTable.tsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. import {Fragment} from 'react';
  2. import {useTheme} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import Duration from 'sentry/components/duration';
  5. import ProjectBadge from 'sentry/components/idBadge/projectBadge';
  6. import UserBadge from 'sentry/components/idBadge/userBadge';
  7. import Link from 'sentry/components/links/link';
  8. import {PanelTable} from 'sentry/components/panels';
  9. import ReplayHighlight from 'sentry/components/replays/replayHighlight';
  10. import {StringWalker} from 'sentry/components/replays/walker/urlWalker';
  11. import TimeSince from 'sentry/components/timeSince';
  12. import {IconArrow, IconCalendar} from 'sentry/icons';
  13. import {t} from 'sentry/locale';
  14. import space from 'sentry/styles/space';
  15. import type {Organization} from 'sentry/types';
  16. import type {Sort} from 'sentry/utils/discover/fields';
  17. import {useLocation} from 'sentry/utils/useLocation';
  18. import useMedia from 'sentry/utils/useMedia';
  19. import useOrganization from 'sentry/utils/useOrganization';
  20. import useProjects from 'sentry/utils/useProjects';
  21. import type {ReplayListLocationQuery, ReplayListRecord} from 'sentry/views/replays/types';
  22. type Props = {
  23. isFetching: boolean;
  24. replays: undefined | ReplayListRecord[];
  25. showProjectColumn: boolean;
  26. sort: Sort;
  27. };
  28. type RowProps = {
  29. minWidthIsSmall: boolean;
  30. organization: Organization;
  31. replay: ReplayListRecord;
  32. showProjectColumn: boolean;
  33. };
  34. function ReplayTable({isFetching, replays, showProjectColumn, sort}: Props) {
  35. const location = useLocation<ReplayListLocationQuery>();
  36. const organization = useOrganization();
  37. const theme = useTheme();
  38. const minWidthIsSmall = useMedia(`(min-width: ${theme.breakpoints.small})`);
  39. const {pathname} = location;
  40. const arrowDirection = sort.kind === 'asc' ? 'up' : 'down';
  41. const sortArrow = <IconArrow color="gray300" size="xs" direction={arrowDirection} />;
  42. return (
  43. <StyledPanelTable
  44. isLoading={isFetching}
  45. isEmpty={replays?.length === 0}
  46. headers={[
  47. t('Session'),
  48. showProjectColumn && minWidthIsSmall ? t('Project') : null,
  49. <SortLink
  50. key="startedAt"
  51. role="columnheader"
  52. aria-sort={
  53. sort.field === 'startedAt'
  54. ? sort.kind === 'asc'
  55. ? 'ascending'
  56. : 'descending'
  57. : 'none'
  58. }
  59. to={{
  60. pathname,
  61. query: {
  62. ...location.query,
  63. sort: sort.kind === 'desc' ? 'startedAt' : '-startedAt',
  64. },
  65. }}
  66. >
  67. {t('Start Time')} {sort.field.endsWith('startedAt') && sortArrow}
  68. </SortLink>,
  69. <SortLink
  70. key="duration"
  71. role="columnheader"
  72. aria-sort={
  73. sort.field.endsWith('duration')
  74. ? sort.kind === 'asc'
  75. ? 'ascending'
  76. : 'descending'
  77. : 'none'
  78. }
  79. to={{
  80. pathname,
  81. query: {
  82. ...location.query,
  83. sort: sort.kind === 'desc' ? 'duration' : '-duration',
  84. },
  85. }}
  86. >
  87. {t('Duration')} {sort.field === 'duration' && sortArrow}
  88. </SortLink>,
  89. t('Errors'),
  90. t('Interest'),
  91. ]}
  92. >
  93. {replays?.map(replay => (
  94. <ReplayTableRow
  95. key={replay.id}
  96. replay={replay}
  97. organization={organization}
  98. showProjectColumn={showProjectColumn}
  99. minWidthIsSmall={minWidthIsSmall}
  100. />
  101. ))}
  102. </StyledPanelTable>
  103. );
  104. }
  105. function ReplayTableRow({
  106. minWidthIsSmall,
  107. organization,
  108. replay,
  109. showProjectColumn,
  110. }: RowProps) {
  111. const {projects} = useProjects();
  112. const project = projects.find(p => p.id === replay.projectId);
  113. return (
  114. <Fragment>
  115. <UserBadge
  116. avatarSize={32}
  117. displayName={
  118. <Link
  119. to={`/organizations/${organization.slug}/replays/${project?.slug}:${replay.id}/`}
  120. >
  121. {replay.user.username ||
  122. replay.user.name ||
  123. replay.user.email ||
  124. replay.user.ip_address ||
  125. replay.user.id ||
  126. ''}
  127. </Link>
  128. }
  129. user={replay.user}
  130. // this is the subheading for the avatar, so displayEmail in this case is a misnomer
  131. displayEmail={<StringWalker urls={replay.urls} />}
  132. />
  133. {showProjectColumn && minWidthIsSmall && (
  134. <Item>{project ? <ProjectBadge project={project} avatarSize={16} /> : null}</Item>
  135. )}
  136. <Item>
  137. <TimeSinceWrapper>
  138. {minWidthIsSmall && <StyledIconCalendarWrapper color="gray500" size="sm" />}
  139. <TimeSince date={replay.startedAt} />
  140. </TimeSinceWrapper>
  141. </Item>
  142. <Item>
  143. <Duration seconds={Math.floor(replay.duration)} exact abbreviation />
  144. </Item>
  145. <Item>{replay.countErrors || 0}</Item>
  146. <Item>
  147. <ReplayHighlight replay={replay} />
  148. </Item>
  149. </Fragment>
  150. );
  151. }
  152. const StyledPanelTable = styled(PanelTable)`
  153. grid-template-columns: minmax(0, 1fr) max-content max-content max-content max-content max-content;
  154. @media (max-width: ${p => p.theme.breakpoints.small}) {
  155. grid-template-columns: minmax(0, 1fr) max-content max-content max-content max-content;
  156. }
  157. `;
  158. const SortLink = styled(Link)`
  159. color: inherit;
  160. :hover {
  161. color: inherit;
  162. }
  163. svg {
  164. vertical-align: top;
  165. }
  166. `;
  167. const Item = styled('div')`
  168. display: flex;
  169. align-items: center;
  170. `;
  171. const TimeSinceWrapper = styled('div')`
  172. display: grid;
  173. grid-template-columns: repeat(2, minmax(auto, max-content));
  174. align-items: center;
  175. gap: ${space(1)};
  176. `;
  177. const StyledIconCalendarWrapper = styled(IconCalendar)`
  178. position: relative;
  179. top: -1px;
  180. `;
  181. export default ReplayTable;