tableCell.tsx 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. import {useTheme} from '@emotion/react';
  2. import styled from '@emotion/styled';
  3. import Avatar from 'sentry/components/avatar';
  4. import UserBadge from 'sentry/components/idBadge/userBadge';
  5. import Link from 'sentry/components/links/link';
  6. import ContextIcon from 'sentry/components/replays/contextIcon';
  7. import ErrorCount from 'sentry/components/replays/header/errorCount';
  8. import {formatTime} from 'sentry/components/replays/utils';
  9. import {StringWalker} from 'sentry/components/replays/walker/urlWalker';
  10. import ScoreBar from 'sentry/components/scoreBar';
  11. import TimeSince from 'sentry/components/timeSince';
  12. import CHART_PALETTE from 'sentry/constants/chartPalette';
  13. import {IconCalendar} from 'sentry/icons';
  14. import {space, ValidSize} from 'sentry/styles/space';
  15. import type {Organization} from 'sentry/types';
  16. import EventView from 'sentry/utils/discover/eventView';
  17. import {spanOperationRelativeBreakdownRenderer} from 'sentry/utils/discover/fieldRenderers';
  18. import {getShortEventId} from 'sentry/utils/events';
  19. import {useLocation} from 'sentry/utils/useLocation';
  20. import useMedia from 'sentry/utils/useMedia';
  21. import useProjects from 'sentry/utils/useProjects';
  22. import type {ReplayListRecordWithTx} from 'sentry/views/performance/transactionSummary/transactionReplays/useReplaysWithTxData';
  23. import type {ReplayListRecord} from 'sentry/views/replays/types';
  24. type Props = {
  25. replay: ReplayListRecord | ReplayListRecordWithTx;
  26. };
  27. export function ReplayCell({
  28. eventView,
  29. organization,
  30. referrer,
  31. replay,
  32. }: Props & {eventView: EventView; organization: Organization; referrer: string}) {
  33. const {projects} = useProjects();
  34. const project = projects.find(p => p.id === replay.project_id);
  35. const replayDetails = {
  36. pathname: `/organizations/${organization.slug}/replays/${project?.slug}:${replay.id}/`,
  37. query: {
  38. referrer,
  39. ...eventView.generateQueryStringObject(),
  40. },
  41. };
  42. return (
  43. <Item>
  44. <UserBadgeFullWidth
  45. avatarSize={24}
  46. displayName={
  47. <MainLink to={replayDetails}>{replay.user.display_name || ''}</MainLink>
  48. }
  49. user={{
  50. username: replay.user.display_name || '',
  51. email: replay.user.email || '',
  52. id: replay.user.id || '',
  53. ip_address: replay.user.ip || '',
  54. name: replay.user.username || '',
  55. }}
  56. // this is the subheading for the avatar, so displayEmail in this case is a misnomer
  57. displayEmail={
  58. <Cols>
  59. <StringWalker urls={replay.urls} />
  60. <Row gap={1}>
  61. <Row gap={0.5}>
  62. {project ? <Avatar size={12} project={project} /> : null}
  63. <Link to={replayDetails}>{getShortEventId(replay.id)}</Link>
  64. </Row>
  65. <Row gap={0.5}>
  66. <IconCalendar color="gray300" size="xs" />
  67. <TimeSince date={replay.started_at} />
  68. </Row>
  69. </Row>
  70. </Cols>
  71. }
  72. />
  73. </Item>
  74. );
  75. }
  76. // Need to be full width for StringWalker to take up full width and truncate properly
  77. const UserBadgeFullWidth = styled(UserBadge)`
  78. width: 100%;
  79. `;
  80. const Cols = styled('div')`
  81. display: flex;
  82. flex-direction: column;
  83. gap: ${space(0.25)};
  84. width: 100%;
  85. `;
  86. const Row = styled('div')<{gap: ValidSize}>`
  87. display: flex;
  88. gap: ${p => space(p.gap)};
  89. align-items: center;
  90. `;
  91. const MainLink = styled(Link)`
  92. font-size: ${p => p.theme.fontSizeLarge};
  93. `;
  94. export function TransactionCell({
  95. organization,
  96. replay,
  97. }: Props & {organization: Organization}) {
  98. const location = useLocation();
  99. const hasTxEvent = 'txEvent' in replay;
  100. const txDuration = hasTxEvent ? replay.txEvent?.['transaction.duration'] : undefined;
  101. return hasTxEvent ? (
  102. <SpanOperationBreakdown>
  103. {txDuration ? <div>{txDuration}ms</div> : null}
  104. {spanOperationRelativeBreakdownRenderer(
  105. replay.txEvent,
  106. {organization, location},
  107. {enableOnClick: false}
  108. )}
  109. </SpanOperationBreakdown>
  110. ) : null;
  111. }
  112. export function OSCell({replay}: Props) {
  113. const {name, version} = replay.os;
  114. const theme = useTheme();
  115. const hasRoomForColumns = useMedia(`(min-width: ${theme.breakpoints.large})`);
  116. return (
  117. <Item>
  118. <ContextIcon
  119. name={name ?? ''}
  120. version={version && hasRoomForColumns ? version : undefined}
  121. />
  122. </Item>
  123. );
  124. }
  125. export function BrowserCell({replay}: Props) {
  126. const {name, version} = replay.browser;
  127. const theme = useTheme();
  128. const hasRoomForColumns = useMedia(`(min-width: ${theme.breakpoints.large})`);
  129. return (
  130. <Item>
  131. <ContextIcon
  132. name={name ?? ''}
  133. version={version && hasRoomForColumns ? version : undefined}
  134. />
  135. </Item>
  136. );
  137. }
  138. export function DurationCell({replay}: Props) {
  139. return (
  140. <Item>
  141. <Time>{formatTime(replay.duration.asMilliseconds())}</Time>
  142. </Item>
  143. );
  144. }
  145. export function ErrorCountCell({replay}: Props) {
  146. return (
  147. <Item data-test-id="replay-table-count-errors">
  148. <ErrorCount countErrors={replay.count_errors} />
  149. </Item>
  150. );
  151. }
  152. export function ActivityCell({replay}: Props) {
  153. const scoreBarPalette = new Array(10).fill([CHART_PALETTE[0][0]]);
  154. return (
  155. <Item>
  156. <ScoreBar
  157. size={20}
  158. score={replay?.activity ?? 1}
  159. palette={scoreBarPalette}
  160. radius={0}
  161. />
  162. </Item>
  163. );
  164. }
  165. const Item = styled('div')`
  166. display: flex;
  167. align-items: center;
  168. gap: ${space(1)};
  169. padding: ${space(1.5)};
  170. `;
  171. const Time = styled('span')`
  172. font-variant-numeric: tabular-nums;
  173. `;
  174. const SpanOperationBreakdown = styled('div')`
  175. width: 100%;
  176. display: flex;
  177. flex-direction: column;
  178. gap: ${space(0.5)};
  179. color: ${p => p.theme.gray500};
  180. font-size: ${p => p.theme.fontSizeMedium};
  181. text-align: right;
  182. `;