replayTable.tsx 6.2 KB

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