replayTable.tsx 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  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 SortableHeader({
  35. fieldName,
  36. label,
  37. sort,
  38. }: {
  39. fieldName: string;
  40. label: string;
  41. sort: Sort;
  42. }) {
  43. const location = useLocation<ReplayListLocationQuery>();
  44. const arrowDirection = sort.kind === 'asc' ? 'up' : 'down';
  45. const sortArrow = <IconArrow color="gray300" size="xs" direction={arrowDirection} />;
  46. return (
  47. <SortLink
  48. role="columnheader"
  49. aria-sort={
  50. sort.field.endsWith(fieldName)
  51. ? sort.kind === 'asc'
  52. ? 'ascending'
  53. : 'descending'
  54. : 'none'
  55. }
  56. to={{
  57. pathname: location.pathname,
  58. query: {
  59. ...location.query,
  60. sort: sort.kind === 'desc' ? fieldName : '-' + fieldName,
  61. },
  62. }}
  63. >
  64. {label} {sort.field === fieldName && sortArrow}
  65. </SortLink>
  66. );
  67. }
  68. function ReplayTable({isFetching, replays, showProjectColumn, sort}: Props) {
  69. const organization = useOrganization();
  70. const theme = useTheme();
  71. const minWidthIsSmall = useMedia(`(min-width: ${theme.breakpoints.small})`);
  72. return (
  73. <StyledPanelTable
  74. isLoading={isFetching}
  75. isEmpty={replays?.length === 0}
  76. showProjectColumn={showProjectColumn}
  77. headers={[
  78. t('Session'),
  79. showProjectColumn && minWidthIsSmall ? (
  80. <SortableHeader
  81. key="projectId"
  82. sort={sort}
  83. fieldName="projectId"
  84. label={t('Project')}
  85. />
  86. ) : null,
  87. <SortableHeader
  88. key="startedAt"
  89. sort={sort}
  90. fieldName="startedAt"
  91. label={t('Start Time')}
  92. />,
  93. <SortableHeader
  94. key="duration"
  95. sort={sort}
  96. fieldName="duration"
  97. label={t('Duration')}
  98. />,
  99. <SortableHeader
  100. key="countErrors"
  101. sort={sort}
  102. fieldName="countErrors"
  103. label={t('Errors')}
  104. />,
  105. t('Activity'),
  106. ].filter(Boolean)}
  107. >
  108. {replays?.map(replay => (
  109. <ReplayTableRow
  110. key={replay.id}
  111. replay={replay}
  112. organization={organization}
  113. showProjectColumn={showProjectColumn}
  114. minWidthIsSmall={minWidthIsSmall}
  115. />
  116. ))}
  117. </StyledPanelTable>
  118. );
  119. }
  120. function ReplayTableRow({
  121. minWidthIsSmall,
  122. organization,
  123. replay,
  124. showProjectColumn,
  125. }: RowProps) {
  126. const {projects} = useProjects();
  127. const project = projects.find(p => p.id === replay.projectId);
  128. return (
  129. <Fragment>
  130. <UserBadge
  131. avatarSize={32}
  132. displayName={
  133. <Link
  134. to={`/organizations/${organization.slug}/replays/${project?.slug}:${replay.id}/`}
  135. >
  136. {replay.user.username ||
  137. replay.user.name ||
  138. replay.user.email ||
  139. replay.user.ip_address ||
  140. replay.user.id ||
  141. ''}
  142. </Link>
  143. }
  144. user={replay.user}
  145. // this is the subheading for the avatar, so displayEmail in this case is a misnomer
  146. displayEmail={<StringWalker urls={replay.urls} />}
  147. />
  148. {showProjectColumn && minWidthIsSmall && (
  149. <Item>{project ? <ProjectBadge project={project} avatarSize={16} /> : null}</Item>
  150. )}
  151. <Item>
  152. <TimeSinceWrapper>
  153. {minWidthIsSmall && <StyledIconCalendarWrapper color="gray500" size="sm" />}
  154. <TimeSince date={replay.startedAt} />
  155. </TimeSinceWrapper>
  156. </Item>
  157. <Item>
  158. <Duration seconds={Math.floor(replay.duration)} exact abbreviation />
  159. </Item>
  160. <Item>{replay.countErrors || 0}</Item>
  161. <Item>
  162. <ReplayHighlight replay={replay} />
  163. </Item>
  164. </Fragment>
  165. );
  166. }
  167. const StyledPanelTable = styled(PanelTable)<{showProjectColumn: boolean}>`
  168. ${p =>
  169. p.showProjectColumn
  170. ? `grid-template-columns: minmax(0, 1fr) repeat(5, max-content);`
  171. : `grid-template-columns: minmax(0, 1fr) repeat(4, max-content);`}
  172. @media (max-width: ${p => p.theme.breakpoints.small}) {
  173. grid-template-columns: minmax(0, 1fr) repeat(4, max-content);
  174. }
  175. `;
  176. const SortLink = styled(Link)`
  177. color: inherit;
  178. :hover {
  179. color: inherit;
  180. }
  181. svg {
  182. vertical-align: top;
  183. }
  184. `;
  185. const Item = styled('div')`
  186. display: flex;
  187. align-items: center;
  188. `;
  189. const TimeSinceWrapper = styled('div')`
  190. display: grid;
  191. grid-template-columns: repeat(2, minmax(auto, max-content));
  192. align-items: center;
  193. gap: ${space(1)};
  194. `;
  195. const StyledIconCalendarWrapper = styled(IconCalendar)`
  196. position: relative;
  197. top: -1px;
  198. `;
  199. export default ReplayTable;