|
@@ -1,383 +0,0 @@
|
|
|
-import {Fragment} from 'react';
|
|
|
-import {useTheme} from '@emotion/react';
|
|
|
-import styled from '@emotion/styled';
|
|
|
-
|
|
|
-import Alert from 'sentry/components/alert';
|
|
|
-import Duration from 'sentry/components/duration';
|
|
|
-import ProjectBadge from 'sentry/components/idBadge/projectBadge';
|
|
|
-import UserBadge from 'sentry/components/idBadge/userBadge';
|
|
|
-import Link from 'sentry/components/links/link';
|
|
|
-import {PanelTable} from 'sentry/components/panels';
|
|
|
-import QuestionTooltip from 'sentry/components/questionTooltip';
|
|
|
-import {StringWalker} from 'sentry/components/replays/walker/urlWalker';
|
|
|
-import ScoreBar from 'sentry/components/scoreBar';
|
|
|
-import TimeSince from 'sentry/components/timeSince';
|
|
|
-import CHART_PALETTE from 'sentry/constants/chartPalette';
|
|
|
-import {IconArrow, IconCalendar} from 'sentry/icons';
|
|
|
-import {t} from 'sentry/locale';
|
|
|
-import space from 'sentry/styles/space';
|
|
|
-import type {Organization} from 'sentry/types';
|
|
|
-import EventView from 'sentry/utils/discover/eventView';
|
|
|
-import {spanOperationRelativeBreakdownRenderer} from 'sentry/utils/discover/fieldRenderers';
|
|
|
-import type {Sort} from 'sentry/utils/discover/fields';
|
|
|
-import getRouteStringFromRoutes from 'sentry/utils/getRouteStringFromRoutes';
|
|
|
-import {useLocation} from 'sentry/utils/useLocation';
|
|
|
-import useMedia from 'sentry/utils/useMedia';
|
|
|
-import useOrganization from 'sentry/utils/useOrganization';
|
|
|
-import useProjects from 'sentry/utils/useProjects';
|
|
|
-import {useRoutes} from 'sentry/utils/useRoutes';
|
|
|
-import type {ReplayListRecordWithTx} from 'sentry/views/performance/transactionSummary/transactionReplays/useReplaysFromTransaction';
|
|
|
-import type {ReplayListLocationQuery, ReplayListRecord} from 'sentry/views/replays/types';
|
|
|
-
|
|
|
-type Props = {
|
|
|
- isFetching: boolean;
|
|
|
- replays: undefined | ReplayListRecord[] | ReplayListRecordWithTx[];
|
|
|
- showProjectColumn: boolean;
|
|
|
- sort: Sort | undefined;
|
|
|
- fetchError?: Error;
|
|
|
- showSlowestTxColumn?: boolean;
|
|
|
-};
|
|
|
-
|
|
|
-type TableProps = {
|
|
|
- showProjectColumn: boolean;
|
|
|
- showSlowestTxColumn: boolean;
|
|
|
-};
|
|
|
-
|
|
|
-type RowProps = {
|
|
|
- eventView: EventView;
|
|
|
- minWidthIsSmall: boolean;
|
|
|
- organization: Organization;
|
|
|
- referrer: string;
|
|
|
- replay: ReplayListRecord | ReplayListRecordWithTx;
|
|
|
- showProjectColumn: boolean;
|
|
|
- showSlowestTxColumn: boolean;
|
|
|
-};
|
|
|
-
|
|
|
-function SortableHeader({
|
|
|
- fieldName,
|
|
|
- label,
|
|
|
- sort,
|
|
|
- tooltip,
|
|
|
-}: {
|
|
|
- fieldName: string;
|
|
|
- label: string;
|
|
|
- sort: Props['sort'];
|
|
|
- tooltip?: string;
|
|
|
-}) {
|
|
|
- const location = useLocation<ReplayListLocationQuery>();
|
|
|
-
|
|
|
- const arrowDirection = sort?.kind === 'asc' ? 'up' : 'down';
|
|
|
- const sortArrow = <IconArrow color="gray300" size="xs" direction={arrowDirection} />;
|
|
|
-
|
|
|
- return (
|
|
|
- <Header>
|
|
|
- <SortLink
|
|
|
- role="columnheader"
|
|
|
- aria-sort={
|
|
|
- sort?.field.endsWith(fieldName)
|
|
|
- ? sort?.kind === 'asc'
|
|
|
- ? 'ascending'
|
|
|
- : 'descending'
|
|
|
- : 'none'
|
|
|
- }
|
|
|
- to={{
|
|
|
- pathname: location.pathname,
|
|
|
- query: {
|
|
|
- ...location.query,
|
|
|
- sort: sort?.field.endsWith(fieldName)
|
|
|
- ? sort?.kind === 'desc'
|
|
|
- ? fieldName
|
|
|
- : '-' + fieldName
|
|
|
- : '-' + fieldName,
|
|
|
- },
|
|
|
- }}
|
|
|
- >
|
|
|
- {label} {sort?.field === fieldName && sortArrow}
|
|
|
- </SortLink>
|
|
|
- {tooltip ? (
|
|
|
- <StyledQuestionTooltip size="xs" position="top" title={tooltip} />
|
|
|
- ) : null}
|
|
|
- </Header>
|
|
|
- );
|
|
|
-}
|
|
|
-
|
|
|
-function ReplayTable({
|
|
|
- isFetching,
|
|
|
- replays,
|
|
|
- showProjectColumn,
|
|
|
- sort,
|
|
|
- fetchError,
|
|
|
- showSlowestTxColumn = false,
|
|
|
-}: Props) {
|
|
|
- const routes = useRoutes();
|
|
|
- const location = useLocation();
|
|
|
- const referrer = getRouteStringFromRoutes(routes);
|
|
|
-
|
|
|
- const organization = useOrganization();
|
|
|
- const theme = useTheme();
|
|
|
- const minWidthIsSmall = useMedia(`(min-width: ${theme.breakpoints.small})`);
|
|
|
-
|
|
|
- const tableHeaders = [
|
|
|
- t('Session'),
|
|
|
- showProjectColumn && minWidthIsSmall && (
|
|
|
- <SortableHeader
|
|
|
- key="projectId"
|
|
|
- sort={sort}
|
|
|
- fieldName="projectId"
|
|
|
- label={t('Project')}
|
|
|
- />
|
|
|
- ),
|
|
|
- showSlowestTxColumn && minWidthIsSmall && (
|
|
|
- <Header key="slowestTransaction">
|
|
|
- {t('Slowest Transaction')}
|
|
|
- <StyledQuestionTooltip
|
|
|
- size="xs"
|
|
|
- position="top"
|
|
|
- title={t(
|
|
|
- 'Slowest single instance of this transaction captured by this session.'
|
|
|
- )}
|
|
|
- />
|
|
|
- </Header>
|
|
|
- ),
|
|
|
- minWidthIsSmall && (
|
|
|
- <SortableHeader
|
|
|
- key="startedAt"
|
|
|
- sort={sort}
|
|
|
- fieldName="startedAt"
|
|
|
- label={t('Start Time')}
|
|
|
- />
|
|
|
- ),
|
|
|
- <SortableHeader
|
|
|
- key="duration"
|
|
|
- sort={sort}
|
|
|
- fieldName="duration"
|
|
|
- label={t('Duration')}
|
|
|
- />,
|
|
|
- <SortableHeader
|
|
|
- key="countErrors"
|
|
|
- sort={sort}
|
|
|
- fieldName="countErrors"
|
|
|
- label={t('Errors')}
|
|
|
- />,
|
|
|
- <SortableHeader
|
|
|
- key="activity"
|
|
|
- sort={sort}
|
|
|
- fieldName="activity"
|
|
|
- label={t('Activity')}
|
|
|
- tooltip={t(
|
|
|
- 'Activity represents how much user activity happened in a replay. It is determined by the number of errors encountered, duration, and UI events.'
|
|
|
- )}
|
|
|
- />,
|
|
|
- ].filter(Boolean);
|
|
|
-
|
|
|
- if (fetchError && !isFetching) {
|
|
|
- return (
|
|
|
- <StyledPanelTable
|
|
|
- headers={tableHeaders}
|
|
|
- showProjectColumn={showProjectColumn}
|
|
|
- isLoading={false}
|
|
|
- showSlowestTxColumn={showSlowestTxColumn}
|
|
|
- >
|
|
|
- <StyledAlert type="error" showIcon>
|
|
|
- {typeof fetchError === 'string'
|
|
|
- ? fetchError
|
|
|
- : t(
|
|
|
- 'Sorry, the list of replays could not be loaded. This could be due to invalid search parameters or an internal systems error.'
|
|
|
- )}
|
|
|
- </StyledAlert>
|
|
|
- </StyledPanelTable>
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- const eventView = EventView.fromLocation(location);
|
|
|
-
|
|
|
- return (
|
|
|
- <StyledPanelTable
|
|
|
- isLoading={isFetching}
|
|
|
- isEmpty={replays?.length === 0}
|
|
|
- showProjectColumn={showProjectColumn}
|
|
|
- showSlowestTxColumn={showSlowestTxColumn}
|
|
|
- headers={tableHeaders}
|
|
|
- >
|
|
|
- {replays?.map(replay => (
|
|
|
- <ReplayTableRow
|
|
|
- eventView={eventView}
|
|
|
- key={replay.id}
|
|
|
- minWidthIsSmall={minWidthIsSmall}
|
|
|
- organization={organization}
|
|
|
- referrer={referrer}
|
|
|
- replay={replay}
|
|
|
- showProjectColumn={showProjectColumn}
|
|
|
- showSlowestTxColumn={showSlowestTxColumn}
|
|
|
- />
|
|
|
- ))}
|
|
|
- </StyledPanelTable>
|
|
|
- );
|
|
|
-}
|
|
|
-
|
|
|
-function ReplayTableRow({
|
|
|
- eventView,
|
|
|
- minWidthIsSmall,
|
|
|
- organization,
|
|
|
- referrer,
|
|
|
- replay,
|
|
|
- showProjectColumn,
|
|
|
- showSlowestTxColumn,
|
|
|
-}: RowProps) {
|
|
|
- const location = useLocation();
|
|
|
- const {projects} = useProjects();
|
|
|
- const project = projects.find(p => p.id === replay.projectId);
|
|
|
- const hasTxEvent = 'txEvent' in replay;
|
|
|
- const txDuration = hasTxEvent ? replay.txEvent?.['transaction.duration'] : undefined;
|
|
|
- const scoreBarPalette = new Array(10).fill([CHART_PALETTE[0][0]]);
|
|
|
-
|
|
|
- return (
|
|
|
- <Fragment>
|
|
|
- <UserBadge
|
|
|
- avatarSize={32}
|
|
|
- displayName={
|
|
|
- <Link
|
|
|
- to={{
|
|
|
- pathname: `/organizations/${organization.slug}/replays/${project?.slug}:${replay.id}/`,
|
|
|
- query: {
|
|
|
- referrer,
|
|
|
- ...eventView.generateQueryStringObject(),
|
|
|
- },
|
|
|
- }}
|
|
|
- >
|
|
|
- {replay.user.displayName || ''}
|
|
|
- </Link>
|
|
|
- }
|
|
|
- user={{
|
|
|
- username: replay.user.displayName || '',
|
|
|
- email: replay.user.email || '',
|
|
|
- id: replay.user.id || '',
|
|
|
- ip_address: replay.user.ip_address || '',
|
|
|
- name: replay.user.name || '',
|
|
|
- }}
|
|
|
- // this is the subheading for the avatar, so displayEmail in this case is a misnomer
|
|
|
- displayEmail={<StringWalker urls={replay.urls} />}
|
|
|
- />
|
|
|
- {showProjectColumn && minWidthIsSmall && (
|
|
|
- <Item>{project ? <ProjectBadge project={project} avatarSize={16} /> : null}</Item>
|
|
|
- )}
|
|
|
- {minWidthIsSmall && showSlowestTxColumn && (
|
|
|
- <Item>
|
|
|
- {hasTxEvent ? (
|
|
|
- <SpanOperationBreakdown>
|
|
|
- {txDuration ? <TxDuration>{txDuration}ms</TxDuration> : null}
|
|
|
- {spanOperationRelativeBreakdownRenderer(
|
|
|
- replay.txEvent,
|
|
|
- {
|
|
|
- organization,
|
|
|
- location,
|
|
|
- },
|
|
|
- {
|
|
|
- enableOnClick: false,
|
|
|
- }
|
|
|
- )}
|
|
|
- </SpanOperationBreakdown>
|
|
|
- ) : null}
|
|
|
- </Item>
|
|
|
- )}
|
|
|
- {minWidthIsSmall && (
|
|
|
- <Item>
|
|
|
- <TimeSinceWrapper>
|
|
|
- {minWidthIsSmall && <StyledIconCalendarWrapper color="gray500" size="sm" />}
|
|
|
- <TimeSince date={replay.startedAt} />
|
|
|
- </TimeSinceWrapper>
|
|
|
- </Item>
|
|
|
- )}
|
|
|
- <Item>
|
|
|
- <Duration seconds={replay.duration.asSeconds()} exact abbreviation />
|
|
|
- </Item>
|
|
|
- <Item data-test-id="replay-table-count-errors">{replay.countErrors || 0}</Item>
|
|
|
- <Item>
|
|
|
- <ScoreBar
|
|
|
- size={20}
|
|
|
- score={replay?.activity ?? 1}
|
|
|
- palette={scoreBarPalette}
|
|
|
- radius={0}
|
|
|
- />
|
|
|
- </Item>
|
|
|
- </Fragment>
|
|
|
- );
|
|
|
-}
|
|
|
-
|
|
|
-function getColCount(props: TableProps) {
|
|
|
- let colCount = 4;
|
|
|
- if (props.showSlowestTxColumn) {
|
|
|
- colCount += 1;
|
|
|
- }
|
|
|
- if (props.showProjectColumn) {
|
|
|
- colCount += 1;
|
|
|
- }
|
|
|
- return colCount;
|
|
|
-}
|
|
|
-
|
|
|
-const StyledPanelTable = styled(PanelTable)<TableProps>`
|
|
|
- ${p => `grid-template-columns: minmax(0, 1fr) repeat(${getColCount(p)}, max-content);`}
|
|
|
-
|
|
|
- @media (max-width: ${p => p.theme.breakpoints.small}) {
|
|
|
- grid-template-columns: minmax(0, 1fr) repeat(3, min-content);
|
|
|
- }
|
|
|
-`;
|
|
|
-
|
|
|
-const SortLink = styled(Link)`
|
|
|
- color: inherit;
|
|
|
-
|
|
|
- :hover {
|
|
|
- color: inherit;
|
|
|
- }
|
|
|
-
|
|
|
- svg {
|
|
|
- vertical-align: top;
|
|
|
- }
|
|
|
-`;
|
|
|
-
|
|
|
-const Item = styled('div')`
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
-`;
|
|
|
-
|
|
|
-const SpanOperationBreakdown = styled('div')`
|
|
|
- width: 100%;
|
|
|
- text-align: right;
|
|
|
-`;
|
|
|
-
|
|
|
-const TxDuration = styled('div')`
|
|
|
- color: ${p => p.theme.gray500};
|
|
|
- font-size: ${p => p.theme.fontSizeMedium};
|
|
|
- margin-bottom: ${space(0.5)};
|
|
|
-`;
|
|
|
-
|
|
|
-const TimeSinceWrapper = styled('div')`
|
|
|
- display: grid;
|
|
|
- grid-template-columns: repeat(2, minmax(auto, max-content));
|
|
|
- align-items: center;
|
|
|
- gap: ${space(1)};
|
|
|
-`;
|
|
|
-
|
|
|
-const StyledIconCalendarWrapper = styled(IconCalendar)`
|
|
|
- position: relative;
|
|
|
- top: -1px;
|
|
|
-`;
|
|
|
-
|
|
|
-const StyledAlert = styled(Alert)`
|
|
|
- border-radius: 0;
|
|
|
- border-width: 1px 0 0 0;
|
|
|
- grid-column: 1/-1;
|
|
|
- margin-bottom: 0;
|
|
|
-`;
|
|
|
-
|
|
|
-const Header = styled('div')`
|
|
|
- display: grid;
|
|
|
- grid-template-columns: repeat(2, max-content);
|
|
|
- align-items: center;
|
|
|
-`;
|
|
|
-
|
|
|
-const StyledQuestionTooltip = styled(QuestionTooltip)`
|
|
|
- margin-left: ${space(0.5)};
|
|
|
-`;
|
|
|
-
|
|
|
-export default ReplayTable;
|