123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279 |
- import type {ReactNode} from 'react';
- import {memo} from 'react';
- import styled from '@emotion/styled';
- import {Alert} from 'sentry/components/alert';
- import LoadingIndicator from 'sentry/components/loadingIndicator';
- import {PanelTable} from 'sentry/components/panels/panelTable';
- import {t} from 'sentry/locale';
- import EventView from 'sentry/utils/discover/eventView';
- import type {Sort} from 'sentry/utils/discover/fields';
- import getRouteStringFromRoutes from 'sentry/utils/getRouteStringFromRoutes';
- import {useLocation} from 'sentry/utils/useLocation';
- import useOrganization from 'sentry/utils/useOrganization';
- import {useRoutes} from 'sentry/utils/useRoutes';
- import useUrlParams from 'sentry/utils/useUrlParams';
- import type {ReplayListRecordWithTx} from 'sentry/views/performance/transactionSummary/transactionReplays/useReplaysWithTxData';
- import HeaderCell from 'sentry/views/replays/replayTable/headerCell';
- import {
- ActivityCell,
- BrowserCell,
- DeadClickCountCell,
- DurationCell,
- ErrorCountCell,
- OSCell,
- PlayPauseCell,
- RageClickCountCell,
- ReplayCell,
- TransactionCell,
- } from 'sentry/views/replays/replayTable/tableCell';
- import {ReplayColumn} from 'sentry/views/replays/replayTable/types';
- import type {ReplayListRecord} from 'sentry/views/replays/types';
- type Props = {
- fetchError: undefined | Error;
- isFetching: boolean;
- replays: undefined | ReplayListRecord[] | ReplayListRecordWithTx[];
- sort: Sort | undefined;
- visibleColumns: ReplayColumn[];
- emptyMessage?: ReactNode;
- gridRows?: string;
- onClickPlay?: (index: number) => void;
- referrerLocation?: string;
- showDropdownFilters?: boolean;
- };
- // Memoizing this component to avoid unnecessary re-renders
- const ReplayTable = memo(
- ({
- fetchError,
- isFetching,
- replays,
- sort,
- visibleColumns,
- emptyMessage,
- gridRows,
- showDropdownFilters,
- onClickPlay,
- referrerLocation,
- }: Props) => {
- const routes = useRoutes();
- const location = useLocation();
- const organization = useOrganization();
- // we may have a selected replay index in the URLs
- const urlParams = useUrlParams();
- const rawReplayIndex = urlParams.getParamValue('selected_replay_index');
- const selectedReplayIndex = parseInt(
- typeof rawReplayIndex === 'string' ? rawReplayIndex : '0',
- 10
- );
- const tableHeaders = visibleColumns
- .filter(Boolean)
- .map(column => <HeaderCell key={column} column={column} sort={sort} />);
- if (fetchError && !isFetching) {
- return (
- <StyledPanelTable
- headers={tableHeaders}
- isLoading={false}
- visibleColumns={visibleColumns}
- data-test-id="replay-table"
- gridRows={undefined}
- >
- <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 referrer = getRouteStringFromRoutes(routes);
- const eventView = EventView.fromLocation(location);
- return (
- <StyledPanelTable
- headers={tableHeaders}
- isEmpty={replays?.length === 0}
- isLoading={isFetching}
- visibleColumns={visibleColumns}
- disablePadding
- data-test-id="replay-table"
- emptyMessage={emptyMessage}
- gridRows={isFetching ? undefined : gridRows}
- loader={<LoadingIndicator style={{margin: '54px auto'}} />}
- disableHeaderBorderBottom
- >
- {replays?.map(
- (replay: ReplayListRecord | ReplayListRecordWithTx, index: number) => {
- return (
- <Row
- key={replay.id}
- isPlaying={index === selectedReplayIndex && referrerLocation !== 'replay'}
- onClick={() => onClickPlay?.(index)}
- showCursor={onClickPlay !== undefined}
- referrerLocation={referrerLocation}
- >
- {visibleColumns.map(column => {
- switch (column) {
- case ReplayColumn.ACTIVITY:
- return (
- <ActivityCell
- key="activity"
- replay={replay}
- showDropdownFilters={showDropdownFilters}
- />
- );
- case ReplayColumn.BROWSER:
- return (
- <BrowserCell
- key="browser"
- replay={replay}
- showDropdownFilters={showDropdownFilters}
- />
- );
- case ReplayColumn.COUNT_DEAD_CLICKS:
- return (
- <DeadClickCountCell
- key="countDeadClicks"
- replay={replay}
- showDropdownFilters={showDropdownFilters}
- />
- );
- case ReplayColumn.COUNT_ERRORS:
- return (
- <ErrorCountCell
- key="countErrors"
- replay={replay}
- showDropdownFilters={showDropdownFilters}
- />
- );
- case ReplayColumn.COUNT_RAGE_CLICKS:
- return (
- <RageClickCountCell
- key="countRageClicks"
- replay={replay}
- showDropdownFilters={showDropdownFilters}
- />
- );
- case ReplayColumn.DURATION:
- return (
- <DurationCell
- key="duration"
- replay={replay}
- showDropdownFilters={showDropdownFilters}
- />
- );
- case ReplayColumn.OS:
- return (
- <OSCell
- key="os"
- replay={replay}
- showDropdownFilters={showDropdownFilters}
- />
- );
- case ReplayColumn.REPLAY:
- return (
- <ReplayCell
- key="session"
- replay={replay}
- eventView={eventView}
- organization={organization}
- referrer={referrer}
- referrer_table="main"
- />
- );
- case ReplayColumn.PLAY_PAUSE:
- return (
- <PlayPauseCell
- key="play"
- isSelected={selectedReplayIndex === index}
- handleClick={() => onClickPlay?.(index)}
- />
- );
- case ReplayColumn.SLOWEST_TRANSACTION:
- return (
- <TransactionCell
- key="slowestTransaction"
- replay={replay}
- organization={organization}
- />
- );
- default:
- return null;
- }
- })}
- </Row>
- );
- }
- )}
- </StyledPanelTable>
- );
- }
- );
- const StyledPanelTable = styled(PanelTable)<{
- visibleColumns: ReplayColumn[];
- gridRows?: string;
- }>`
- margin-bottom: 0;
- grid-template-columns: ${p =>
- p.visibleColumns
- .filter(Boolean)
- .map(column => (column === 'replay' ? 'minmax(100px, 1fr)' : 'max-content'))
- .join(' ')};
- ${props =>
- props.gridRows
- ? `grid-template-rows: ${props.gridRows};`
- : `grid-template-rows: 44px max-content;`}
- `;
- const StyledAlert = styled(Alert)`
- border-radius: 0;
- border-width: 1px 0 0 0;
- grid-column: 1/-1;
- margin-bottom: 0;
- `;
- const Row = styled('div')<{
- isPlaying?: boolean;
- referrerLocation?: string;
- showCursor?: boolean;
- }>`
- ${p =>
- p.referrerLocation === 'replay'
- ? `display: contents;
- & > * {
- border-top: 1px solid ${p.theme.border};
- }`
- : `display: contents;
- & > * {
- background-color: ${p.isPlaying ? p.theme.translucentGray200 : 'inherit'};
- border-top: 1px solid ${p.theme.border};
- cursor: ${p.showCursor ? 'pointer' : 'default'};
- }
- :hover {
- background-color: ${p.showCursor ? p.theme.translucentInnerBorder : 'inherit'};
- }
- :active {
- background-color: ${p.theme.translucentGray200};
- }
- `}
- `;
- export default ReplayTable;
|