From 8c007396e462c49d8ba7c0d0eb02aa6cd6c105e6 Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Fri, 27 Jun 2025 16:12:09 -0700 Subject: [PATCH 1/6] ref(replay): Convert ReplayTable to use SimpleTable under the hood --- .../eventReplay/replayPreviewPlayer.tsx | 11 +- .../components/replays/table/replayTable.tsx | 179 ++++++++++++ .../replays/table/replayTableColumns.tsx | 8 +- .../replays/table/useReplayTableSort.tsx | 51 ++++ .../utils/replays/hooks/useFetchReplayList.ts | 19 -- .../groupReplays/groupReplays.tsx | 46 ++- .../transactionReplays/transactionReplays.tsx | 29 +- .../deadRageClick/exampleReplaysList.tsx | 21 +- static/app/views/replays/list/listContent.tsx | 4 +- static/app/views/replays/list/replaysList.tsx | 151 ---------- .../views/replays/replayTable/headerCell.tsx | 37 --- .../app/views/replays/replayTable/index.tsx | 269 ------------------ .../replays/replayTable/sortableHeader.tsx | 118 -------- .../views/replays/replayTable/tableCell.tsx | 168 ----------- .../app/views/replays/replayTable/types.tsx | 12 - .../views/replays/table/replayIndexTable.tsx | 149 ++++++++++ 16 files changed, 444 insertions(+), 828 deletions(-) create mode 100644 static/app/components/replays/table/replayTable.tsx create mode 100644 static/app/components/replays/table/useReplayTableSort.tsx delete mode 100644 static/app/utils/replays/hooks/useFetchReplayList.ts delete mode 100644 static/app/views/replays/list/replaysList.tsx delete mode 100644 static/app/views/replays/replayTable/headerCell.tsx delete mode 100644 static/app/views/replays/replayTable/index.tsx delete mode 100644 static/app/views/replays/replayTable/sortableHeader.tsx delete mode 100644 static/app/views/replays/replayTable/tableCell.tsx delete mode 100644 static/app/views/replays/replayTable/types.tsx create mode 100644 static/app/views/replays/table/replayIndexTable.tsx diff --git a/static/app/components/events/eventReplay/replayPreviewPlayer.tsx b/static/app/components/events/eventReplay/replayPreviewPlayer.tsx index a1acc3223ade73..bd904aa0a14d90 100644 --- a/static/app/components/events/eventReplay/replayPreviewPlayer.tsx +++ b/static/app/components/events/eventReplay/replayPreviewPlayer.tsx @@ -13,6 +13,7 @@ import {ReplayFullscreenButton} from 'sentry/components/replays/replayFullscreen import ReplayPlayer from 'sentry/components/replays/replayPlayer'; import ReplayPlayPauseButton from 'sentry/components/replays/replayPlayPauseButton'; import {ReplaySidebarToggleButton} from 'sentry/components/replays/replaySidebarToggleButton'; +import {ReplaySessionColumn} from 'sentry/components/replays/table/replayTableColumns'; import TimeAndScrubberGrid from 'sentry/components/replays/timeAndScrubberGrid'; import {IconNext, IconPrevious} from 'sentry/icons'; import {t} from 'sentry/locale'; @@ -29,7 +30,6 @@ import Breadcrumbs from 'sentry/views/replays/detail/breadcrumbs'; import BrowserOSIcons from 'sentry/views/replays/detail/browserOSIcons'; import FluidHeight from 'sentry/views/replays/detail/layout/fluidHeight'; import {makeReplaysPathname} from 'sentry/views/replays/pathnames'; -import {ReplayCell} from 'sentry/views/replays/replayTable/tableCell'; import type {ReplayListRecord, ReplayRecord} from 'sentry/views/replays/types'; export default function ReplayPreviewPlayer({ @@ -93,10 +93,11 @@ export default function ReplayPreviewPlayer({ )} - void; + sort: Sort; + } + | {onSortClick?: never; sort?: never}; + +type Props = SortProps & { + columns: readonly ReplayTableColumn[]; + error: RequestError | null | undefined; + isPending: boolean; + replays: ListRecord[]; + showDropdownFilters: boolean; + onClickRow?: (props: {replay: ListRecord; rowIndex: number}) => void; +}; + +export default function ReplayTable({ + columns, + error, + isPending, + onClickRow, + onSortClick, + replays, + showDropdownFilters, + sort, +}: Props) { + if (isPending) { + return ( + + + + + + ); + } + + if (error) { + return ( + + + + {t('Sorry, the list of replays could not be loaded. ')} + {getErrorMessage(error)} + + + + ); + } + + return ( + + {replays.length === 0 && No data} + {replays.map((replay, rowIndex) => ( + + onClickRow?.({replay, rowIndex})}> + {onClickRow && } + {columns.map((column, columnIndex) => ( + + + + ))} + + + ))} + + ); +} + +type TableProps = { + children: ReactNode; + columns: readonly ReplayTableColumn[]; + className?: string; + onSortClick?: (key: string) => void; + sort?: Sort; +}; + +const ReplayTableWithColumns = styled( + ({children, className, columns, onSortClick, sort}: TableProps) => ( + + + {columns.map(column => ( + column.sortKey && onSortClick?.(column.sortKey)} + sort={ + column.sortKey && sort?.field === column.sortKey ? sort.kind : undefined + } + > + + {column.name} + + + ))} + + + {children} + + ) +)` + ${p => getGridTemplateColumns(p.columns)} + margin-bottom: 0; + overflow: auto; + + [data-clickable='true'] { + cursor: pointer; + } +`; + +function getGridTemplateColumns(columns: readonly ReplayTableColumn[]) { + return `grid-template-columns: ${columns + .map(column => + column === ReplaySessionColumn ? 'minmax(150px, 1fr)' : 'max-content' + ) + .join(' ')};`; +} + +function getErrorMessage(fetchError: RequestError) { + if (typeof fetchError === 'string') { + return fetchError; + } + if (typeof fetchError?.responseJSON?.detail === 'string') { + return fetchError.responseJSON.detail; + } + if (fetchError?.responseJSON?.detail?.message) { + return fetchError.responseJSON.detail.message; + } + if (fetchError.name === ERROR_MAP[500]) { + return t('There was an internal systems error.'); + } + return t( + 'This could be due to invalid search parameters or an internal systems error.' + ); +} + +const RowContentButton = styled('button')` + display: contents; + cursor: pointer; + + border: none; + background: transparent; + margin: 0; + padding: 0; +`; + +const RowCell = styled(SimpleTable.RowCell)` + position: relative; + overflow: auto; + + &:hover [data-visible-on-hover='true'] { + opacity: 1; + } +`; diff --git a/static/app/components/replays/table/replayTableColumns.tsx b/static/app/components/replays/table/replayTableColumns.tsx index bc248c0fb53549..0620ef692370c0 100644 --- a/static/app/components/replays/table/replayTableColumns.tsx +++ b/static/app/components/replays/table/replayTableColumns.tsx @@ -54,7 +54,7 @@ interface RenderProps { showDropdownFilters: boolean; } -interface ReplayTableColumn { +export interface ReplayTableColumn { Component: (props: RenderProps) => ReactNode; name: string; sortKey: undefined | ReplayRecordNestedFieldName; @@ -401,7 +401,7 @@ export const ReplaySessionColumn: ReplayTableColumn = { - + { + const newSort = { + field: key, + kind: + key === sortType.field ? (sortType.kind === 'asc' ? 'desc' : 'asc') : 'desc', + } satisfies Sort; + + setParamValue(encodeSort(newSort)); + + trackAnalytics('replay.list-sorted', { + organization, + column: key, + }); + }, + [organization, setParamValue, sortType] + ); + + return { + sortType, + sortQuery, + onSortClick: handleSortClick, + }; +} diff --git a/static/app/utils/replays/hooks/useFetchReplayList.ts b/static/app/utils/replays/hooks/useFetchReplayList.ts deleted file mode 100644 index df401ddc327b50..00000000000000 --- a/static/app/utils/replays/hooks/useFetchReplayList.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {type ApiQueryKey, useApiQuery} from 'sentry/utils/queryClient'; -import {mapResponseToReplayRecord} from 'sentry/utils/replays/replayDataUtils'; -import {type ReplayListRecord} from 'sentry/views/replays/types'; - -type Options = { - queryKey: ApiQueryKey; -}; - -export default function useFetchReplayList({queryKey}: Options) { - const {data, ...result} = useApiQuery<{data: any[]}>(queryKey, { - staleTime: 0, - enabled: true, - }); - - return { - data: data?.data?.map(mapResponseToReplayRecord), - ...result, - }; -} diff --git a/static/app/views/issueDetails/groupReplays/groupReplays.tsx b/static/app/views/issueDetails/groupReplays/groupReplays.tsx index f9d6fbf64a845a..f7dfe498040137 100644 --- a/static/app/views/issueDetails/groupReplays/groupReplays.tsx +++ b/static/app/views/issueDetails/groupReplays/groupReplays.tsx @@ -11,6 +11,8 @@ import { useSelectedReplayIndex, } from 'sentry/components/replays/queryParams/selectedReplayIndex'; import {Provider as ReplayContextProvider} from 'sentry/components/replays/replayContext'; +import ReplayTable from 'sentry/components/replays/table/replayTable'; +import * as ReplayTableColumns from 'sentry/components/replays/table/replayTableColumns'; import {replayMobilePlatforms} from 'sentry/data/platformCategories'; import {IconPlay, IconUser} from 'sentry/icons'; import {t, tn} from 'sentry/locale'; @@ -28,8 +30,6 @@ import {useParams} from 'sentry/utils/useParams'; import GroupReplaysPlayer from 'sentry/views/issueDetails/groupReplays/groupReplaysPlayer'; import {useHasStreamlinedUI} from 'sentry/views/issueDetails/utils'; import useAllMobileProj from 'sentry/views/replays/detail/useAllMobileProj'; -import ReplayTable from 'sentry/views/replays/replayTable'; -import {ReplayColumn} from 'sentry/views/replays/replayTable/types'; import type {ReplayListLocationQuery, ReplayListRecord} from 'sentry/views/replays/types'; import useReplaysFromIssue from './useReplaysFromIssue'; @@ -39,20 +39,20 @@ type Props = { }; const VISIBLE_COLUMNS = [ - ReplayColumn.REPLAY, - ReplayColumn.OS, - ReplayColumn.BROWSER, - ReplayColumn.DURATION, - ReplayColumn.COUNT_ERRORS, - ReplayColumn.ACTIVITY, + ReplayTableColumns.ReplaySessionColumn, + ReplayTableColumns.ReplayOSColumn, + ReplayTableColumns.ReplayBrowserColumn, + ReplayTableColumns.ReplayDurationColumn, + ReplayTableColumns.ReplayCountErrorsColumn, + ReplayTableColumns.ReplayActivityColumn, ]; const VISIBLE_COLUMNS_MOBILE = [ - ReplayColumn.REPLAY, - ReplayColumn.OS, - ReplayColumn.DURATION, - ReplayColumn.COUNT_ERRORS, - ReplayColumn.ACTIVITY, + ReplayTableColumns.ReplaySessionColumn, + ReplayTableColumns.ReplayOSColumn, + ReplayTableColumns.ReplayDurationColumn, + ReplayTableColumns.ReplayCountErrorsColumn, + ReplayTableColumns.ReplayActivityColumn, ]; function ReplayFilterMessage() { @@ -109,11 +109,10 @@ export default function GroupReplays({group}: Props) { @@ -235,16 +234,15 @@ function GroupReplaysTable({ const replayTable = ( setSelectedReplayIndex(rowIndex)} + replays={replays ?? []} showDropdownFilters={false} - onClickRow={setSelectedReplayIndex} - fetchError={replayListData.fetchError} - isFetching={replayListData.isFetching} - replays={replays} /> ); diff --git a/static/app/views/performance/transactionSummary/transactionReplays/transactionReplays.tsx b/static/app/views/performance/transactionSummary/transactionReplays/transactionReplays.tsx index 4b706fdb2399d9..d215b605fbfc5d 100644 --- a/static/app/views/performance/transactionSummary/transactionReplays/transactionReplays.tsx +++ b/static/app/views/performance/transactionSummary/transactionReplays/transactionReplays.tsx @@ -4,6 +4,8 @@ import type {Location} from 'history'; import * as Layout from 'sentry/components/layouts/thirds'; import LoadingIndicator from 'sentry/components/loadingIndicator'; +import ReplayTable from 'sentry/components/replays/table/replayTable'; +import * as ReplayTableColumns from 'sentry/components/replays/table/replayTableColumns'; import {t} from 'sentry/locale'; import type {Organization} from 'sentry/types/organization'; import EventView from 'sentry/utils/discover/eventView'; @@ -20,8 +22,6 @@ import type {ChildProps} from 'sentry/views/performance/transactionSummary/pageL import PageLayout from 'sentry/views/performance/transactionSummary/pageLayout'; import Tab from 'sentry/views/performance/transactionSummary/tabs'; import useAllMobileProj from 'sentry/views/replays/detail/useAllMobileProj'; -import ReplayTable from 'sentry/views/replays/replayTable'; -import {ReplayColumn} from 'sentry/views/replays/replayTable/types'; import type {ReplayListLocationQuery} from 'sentry/views/replays/types'; import type {EventSpanData} from './useReplaysFromTransaction'; @@ -168,19 +168,20 @@ function ReplaysContent({ return ( diff --git a/static/app/views/replays/deadRageClick/exampleReplaysList.tsx b/static/app/views/replays/deadRageClick/exampleReplaysList.tsx index 46574062fc5983..782115be36309a 100644 --- a/static/app/views/replays/deadRageClick/exampleReplaysList.tsx +++ b/static/app/views/replays/deadRageClick/exampleReplaysList.tsx @@ -1,15 +1,17 @@ import {Fragment, useMemo} from 'react'; +import styled from '@emotion/styled'; import type {Location} from 'history'; import AnalyticsArea from 'sentry/components/analyticsArea'; import EmptyStateWarning from 'sentry/components/emptyStateWarning'; import LoadingIndicator from 'sentry/components/loadingIndicator'; +import {ReplaySessionColumn} from 'sentry/components/replays/table/replayTableColumns'; import {t} from 'sentry/locale'; +import {space} from 'sentry/styles/space'; import EventView from 'sentry/utils/discover/eventView'; import useReplayList from 'sentry/utils/replays/hooks/useReplayList'; import useOrganization from 'sentry/utils/useOrganization'; import {StatusContainer} from 'sentry/views/profiling/landing/styles'; -import {ReplayCell} from 'sentry/views/replays/replayTable/tableCell'; export default function ExampleReplaysList({ location, @@ -81,11 +83,24 @@ export default function ExampleReplaysList({ ) : ( - {replays?.map(r => { - return ; + {replays?.map(replay => { + return ( + + + + ); })} )} ); } + +const Wrapper = styled('div')` + padding: ${space(0.75)} ${space(1.5)} ${space(1.5)} ${space(1.5)}; +`; diff --git a/static/app/views/replays/list/listContent.tsx b/static/app/views/replays/list/listContent.tsx index db821a39f956a4..d3424a0aa6e880 100644 --- a/static/app/views/replays/list/listContent.tsx +++ b/static/app/views/replays/list/listContent.tsx @@ -16,8 +16,8 @@ import DeadRageSelectorCards from 'sentry/views/replays/deadRageClick/deadRageSe import useAllMobileProj from 'sentry/views/replays/detail/useAllMobileProj'; import ReplaysFilters from 'sentry/views/replays/list/filters'; import ReplayOnboardingPanel from 'sentry/views/replays/list/replayOnboardingPanel'; -import ReplaysList from 'sentry/views/replays/list/replaysList'; import ReplaysSearch from 'sentry/views/replays/list/search'; +import ReplayIndexTable from 'sentry/views/replays/table/replayIndexTable'; export default function ListContent() { const organization = useOrganization(); @@ -76,7 +76,7 @@ export default function ListContent() { {widgetIsOpen && showDeadRageClickCards ? : null} - {isLoading ? : } + {isLoading ? : } ); } diff --git a/static/app/views/replays/list/replaysList.tsx b/static/app/views/replays/list/replaysList.tsx deleted file mode 100644 index d2275e95c42bf1..00000000000000 --- a/static/app/views/replays/list/replaysList.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import {Fragment, useMemo} from 'react'; -import styled from '@emotion/styled'; - -import Pagination from 'sentry/components/pagination'; -import {t, tct} from 'sentry/locale'; -import {trackAnalytics} from 'sentry/utils/analytics'; -import {decodeList, decodeScalar, decodeSorts} from 'sentry/utils/queryString'; -import useFetchReplayList from 'sentry/utils/replays/hooks/useFetchReplayList'; -import useReplayListQueryKey from 'sentry/utils/replays/hooks/useReplayListQueryKey'; -import {MIN_REPLAY_CLICK_SDK} from 'sentry/utils/replays/sdkVersions'; -import {MutableSearch} from 'sentry/utils/tokenizeSearch'; -import useLocationQuery from 'sentry/utils/url/useLocationQuery'; -import {useNavigate} from 'sentry/utils/useNavigate'; -import useOrganization from 'sentry/utils/useOrganization'; -import usePageFilters from 'sentry/utils/usePageFilters'; -import useProjectSdkNeedsUpdate from 'sentry/utils/useProjectSdkNeedsUpdate'; -import useAllMobileProj from 'sentry/views/replays/detail/useAllMobileProj'; -import { - JetpackComposePiiNotice, - useNeedsJetpackComposePiiNotice, -} from 'sentry/views/replays/jetpackComposePiiNotice'; -import ReplayTable from 'sentry/views/replays/replayTable'; -import {ReplayColumn} from 'sentry/views/replays/replayTable/types'; - -function ReplaysList() { - const organization = useOrganization(); - const navigate = useNavigate(); - - const query = useLocationQuery({ - fields: { - cursor: decodeScalar, - end: decodeScalar, - environment: decodeList, - project: decodeList, - query: decodeScalar, - sort: (value: any) => decodeScalar(value, '-started_at'), - start: decodeScalar, - statsPeriod: decodeScalar, - utc: decodeScalar, - }, - }); - - const queryKey = useReplayListQueryKey({ - options: {query}, - organization, - queryReferrer: 'replayList', - }); - const { - data: replays, - getResponseHeader, - isPending, - error, - } = useFetchReplayList({queryKey}); - const pageLinks = getResponseHeader?.('Link') ?? null; - - const { - selection: {projects}, - } = usePageFilters(); - - const {allMobileProj} = useAllMobileProj({}); - - const {needsUpdate: allSelectedProjectsNeedUpdates} = useProjectSdkNeedsUpdate({ - minVersion: MIN_REPLAY_CLICK_SDK.minVersion, - organization, - projectId: projects.map(String), - }); - - const conditions = useMemo(() => new MutableSearch(query.query), [query.query]); - const hasReplayClick = conditions.getFilterKeys().some(k => k.startsWith('click.')); - - // browser isn't applicable for mobile projects - // rage and dead clicks not available yet - const visibleCols = allMobileProj - ? [ - ReplayColumn.REPLAY, - ReplayColumn.OS, - ReplayColumn.DURATION, - ReplayColumn.COUNT_ERRORS, - ReplayColumn.ACTIVITY, - ] - : [ - ReplayColumn.REPLAY, - ReplayColumn.OS, - ReplayColumn.BROWSER, - ReplayColumn.DURATION, - ReplayColumn.COUNT_DEAD_CLICKS, - ReplayColumn.COUNT_RAGE_CLICKS, - ReplayColumn.COUNT_ERRORS, - ReplayColumn.ACTIVITY, - ]; - - const needsJetpackComposePiiWarning = useNeedsJetpackComposePiiNotice({ - replays, - }); - - return ( - - {needsJetpackComposePiiWarning && } - - {t('Unindexed search field')} - - {tct('Field [field] requires an [sdkPrompt]', { - field: 'click', - sdkPrompt: ( - - {t('SDK version >= %s', MIN_REPLAY_CLICK_SDK.minVersion)} - - ), - })} - - - ) : undefined - } - /> - { - trackAnalytics('replay.list-paginated', { - organization, - direction: cursor?.endsWith(':1') ? 'prev' : 'next', - }); - navigate({ - pathname: path, - query: {...searchQuery, cursor}, - }); - }} - /> - - ); -} - -const EmptyStateSubheading = styled('div')` - color: ${p => p.theme.subText}; - font-size: ${p => p.theme.fontSize.md}; -`; - -const ReplayPagination = styled(Pagination)` - margin-top: 0; -`; - -export default ReplaysList; diff --git a/static/app/views/replays/replayTable/headerCell.tsx b/static/app/views/replays/replayTable/headerCell.tsx deleted file mode 100644 index cd8c467476c32c..00000000000000 --- a/static/app/views/replays/replayTable/headerCell.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import * as ReplayTableColumns from 'sentry/components/replays/table/replayTableColumns'; -import type {Sort} from 'sentry/utils/discover/fields'; -import SortableHeader from 'sentry/views/replays/replayTable/sortableHeader'; -import {ReplayColumn} from 'sentry/views/replays/replayTable/types'; - -type Props = { - column: ReplayColumn; - sort?: Sort; -}; - -export default function HeaderCell({column, sort}: Props) { - const tableColumn = { - [ReplayColumn.ACTIVITY]: ReplayTableColumns.ReplayActivityColumn, - [ReplayColumn.BROWSER]: ReplayTableColumns.ReplayBrowserColumn, - [ReplayColumn.COUNT_DEAD_CLICKS]: ReplayTableColumns.ReplayCountDeadClicksColumn, - [ReplayColumn.COUNT_ERRORS]: ReplayTableColumns.ReplayCountErrorsColumn, - [ReplayColumn.COUNT_RAGE_CLICKS]: ReplayTableColumns.ReplayCountRageClicksColumn, - [ReplayColumn.DURATION]: ReplayTableColumns.ReplayDurationColumn, - [ReplayColumn.OS]: ReplayTableColumns.ReplayOSColumn, - [ReplayColumn.PLAY_PAUSE]: ReplayTableColumns.ReplayPlayPauseColumn, - [ReplayColumn.REPLAY]: ReplayTableColumns.ReplaySessionColumn, - [ReplayColumn.SLOWEST_TRANSACTION]: ReplayTableColumns.ReplaySlowestTransactionColumn, - }[column]; - - if (!tableColumn) { - return null; - } - - return ( - - ); -} diff --git a/static/app/views/replays/replayTable/index.tsx b/static/app/views/replays/replayTable/index.tsx deleted file mode 100644 index 1e83c5fdaf1875..00000000000000 --- a/static/app/views/replays/replayTable/index.tsx +++ /dev/null @@ -1,269 +0,0 @@ -import type {ReactNode} from 'react'; -import styled from '@emotion/styled'; - -import {Alert} from 'sentry/components/core/alert'; -import LoadingIndicator from 'sentry/components/loadingIndicator'; -import {PanelTable} from 'sentry/components/panels/panelTable'; -import {useSelectedReplayIndex} from 'sentry/components/replays/queryParams/selectedReplayIndex'; -import {t} from 'sentry/locale'; -import type {Sort} from 'sentry/utils/discover/fields'; -import type RequestError from 'sentry/utils/requestError/requestError'; -import {ERROR_MAP} from 'sentry/utils/requestError/requestError'; -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: null | undefined | RequestError; - isFetching: boolean; - replays: undefined | ReplayListRecord[] | ReplayListRecordWithTx[]; - sort: Sort | undefined; - visibleColumns: ReplayColumn[]; - emptyMessage?: ReactNode; - gridRows?: string; - onClickRow?: (index: number) => void; - referrerLocation?: string; - showDropdownFilters?: boolean; -}; - -function getErrorMessage(fetchError: RequestError) { - if (typeof fetchError === 'string') { - return fetchError; - } - if (typeof fetchError?.responseJSON?.detail === 'string') { - return fetchError.responseJSON.detail; - } - if (fetchError?.responseJSON?.detail?.message) { - return fetchError.responseJSON.detail.message; - } - if (fetchError.name === ERROR_MAP[500]) { - return t('There was an internal systems error.'); - } - return t( - 'This could be due to invalid search parameters or an internal systems error.' - ); -} - -function ReplayTable({ - fetchError, - isFetching, - replays, - sort, - visibleColumns, - emptyMessage, - gridRows, - showDropdownFilters, - onClickRow, - referrerLocation, -}: Props) { - const {index: selectedReplayIndex} = useSelectedReplayIndex(); - - const tableHeaders = visibleColumns - .filter(Boolean) - .map(column => ); - - if (fetchError && !isFetching) { - return ( - - - {t('Sorry, the list of replays could not be loaded. ')} - {getErrorMessage(fetchError)} - - - ); - } - - return ( - } - disableHeaderBorderBottom - > - {replays?.map( - (replay: ReplayListRecord | ReplayListRecordWithTx, index: number) => { - return ( - onClickRow?.(index)} - showCursor={onClickRow !== undefined} - referrerLocation={referrerLocation} - > - {visibleColumns.map(column => { - switch (column) { - case ReplayColumn.ACTIVITY: - return ( - - ); - - case ReplayColumn.BROWSER: - return ( - - ); - - case ReplayColumn.COUNT_DEAD_CLICKS: - return ( - - ); - - case ReplayColumn.COUNT_ERRORS: - return ( - - ); - - case ReplayColumn.COUNT_RAGE_CLICKS: - return ( - - ); - - case ReplayColumn.DURATION: - return ( - - ); - - case ReplayColumn.OS: - return ( - - ); - - case ReplayColumn.REPLAY: - return ; - - case ReplayColumn.PLAY_PAUSE: - return ; - - case ReplayColumn.SLOWEST_TRANSACTION: - return ( - - ); - - default: - return null; - } - })} - - ); - } - )} - - ); -} - -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; -`; - -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; - -const StyledLoadingIndicator = styled(LoadingIndicator)` - margin: 54px auto; -`; diff --git a/static/app/views/replays/replayTable/sortableHeader.tsx b/static/app/views/replays/replayTable/sortableHeader.tsx deleted file mode 100644 index 7b2e8a2dda6a5b..00000000000000 --- a/static/app/views/replays/replayTable/sortableHeader.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import type {ReactNode} from 'react'; -import styled from '@emotion/styled'; - -import Link from 'sentry/components/links/link'; -import QuestionTooltip from 'sentry/components/questionTooltip'; -import {IconArrow} from 'sentry/icons'; -import {space} from 'sentry/styles/space'; -import {trackAnalytics} from 'sentry/utils/analytics'; -import type {Sort} from 'sentry/utils/discover/fields'; -import {useLocation} from 'sentry/utils/useLocation'; -import useOrganization from 'sentry/utils/useOrganization'; -import type { - ReplayListLocationQuery, - ReplayRecordNestedFieldName, -} from 'sentry/views/replays/types'; - -type NotSortable = { - label: string; - tooltip: undefined | string | ReactNode; -}; - -type Sortable = { - fieldName: ReplayRecordNestedFieldName; - label: string; - sort: undefined | Sort; - tooltip: undefined | string | ReactNode; -}; - -type Props = NotSortable | Sortable; - -function SortableHeader(props: Props) { - const location = useLocation(); - const organization = useOrganization(); - - if (!('sort' in props) || !props.sort) { - const {label, tooltip} = props; - return ( -
- {label} - {tooltip ? ( - - ) : null} -
- ); - } - - const {fieldName, label, sort, tooltip} = props; - - const arrowDirection = sort?.kind === 'asc' ? 'up' : 'down'; - const sortArrow = ; - - return ( -
- { - const column = sort?.field.endsWith(fieldName) - ? sort?.kind === 'desc' - ? fieldName - : '-' + fieldName - : '-' + fieldName; - trackAnalytics('replay.list-sorted', { - organization, - column, - }); - }} - to={{ - pathname: location.pathname, - query: { - ...location.query, - sort: sort?.field.endsWith(fieldName) - ? sort?.kind === 'desc' - ? fieldName - : '-' + fieldName - : '-' + fieldName, - }, - }} - > - {label} {sort?.field === fieldName && sortArrow} - - {tooltip ? ( - - ) : null} -
- ); -} - -const Header = styled('div')` - display: grid; - grid-template-columns: repeat(2, max-content); - align-items: center; - padding: ${space(1.5)}; -`; - -const SortLink = styled(Link)` - color: inherit; - - :hover { - color: inherit; - } - - svg { - vertical-align: top; - } -`; - -const StyledQuestionTooltip = styled(QuestionTooltip)` - margin-left: ${space(0.5)}; -`; - -export default SortableHeader; diff --git a/static/app/views/replays/replayTable/tableCell.tsx b/static/app/views/replays/replayTable/tableCell.tsx deleted file mode 100644 index 8bf6002275ba0b..00000000000000 --- a/static/app/views/replays/replayTable/tableCell.tsx +++ /dev/null @@ -1,168 +0,0 @@ -import styled from '@emotion/styled'; - -import * as ReplayTableColumns from 'sentry/components/replays/table/replayTableColumns'; -import {space} from 'sentry/styles/space'; -import type {ReplayListRecordWithTx} from 'sentry/views/performance/transactionSummary/transactionReplays/useReplaysWithTxData'; -import type {ReplayListRecord} from 'sentry/views/replays/types'; - -type Props = { - replay: ReplayListRecord | ReplayListRecordWithTx; - rowIndex: number; - showDropdownFilters?: boolean; -}; - -export function ReplayCell({replay, rowIndex, showDropdownFilters}: Props) { - return ( - - {ReplayTableColumns.ReplaySessionColumn.Component({ - replay, - rowIndex, - columnIndex: 0, - showDropdownFilters: showDropdownFilters ?? false, - })} - - ); -} - -export function TransactionCell({replay, rowIndex, showDropdownFilters}: Props) { - const hasTxEvent = 'txEvent' in replay; - - if (!hasTxEvent) { - return null; - } - - return ( - - {ReplayTableColumns.ReplaySlowestTransactionColumn.Component({ - replay, - rowIndex, - columnIndex: 0, - showDropdownFilters: showDropdownFilters ?? false, - })} - - ); -} - -export function OSCell({replay, rowIndex, showDropdownFilters}: Props) { - return ( - - {ReplayTableColumns.ReplayOSColumn.Component({ - replay, - rowIndex, - columnIndex: 0, - showDropdownFilters: showDropdownFilters ?? false, - })} - - ); -} - -export function BrowserCell({replay, rowIndex, showDropdownFilters}: Props) { - return ( - - {ReplayTableColumns.ReplayBrowserColumn.Component({ - replay, - rowIndex, - columnIndex: 0, - showDropdownFilters: showDropdownFilters ?? false, - })} - - ); -} - -export function DurationCell({replay, rowIndex, showDropdownFilters}: Props) { - return ( - - {ReplayTableColumns.ReplayDurationColumn.Component({ - replay, - rowIndex, - columnIndex: 0, - showDropdownFilters: showDropdownFilters ?? false, - })} - - ); -} - -export function RageClickCountCell({replay, rowIndex, showDropdownFilters}: Props) { - return ( - - {ReplayTableColumns.ReplayCountRageClicksColumn.Component({ - replay, - rowIndex, - columnIndex: 0, - showDropdownFilters: showDropdownFilters ?? false, - })} - - ); -} - -export function DeadClickCountCell({replay, rowIndex, showDropdownFilters}: Props) { - return ( - - {ReplayTableColumns.ReplayCountDeadClicksColumn.Component({ - replay, - rowIndex, - columnIndex: 0, - showDropdownFilters: showDropdownFilters ?? false, - })} - - ); -} - -export function ErrorCountCell({replay, rowIndex, showDropdownFilters}: Props) { - return ( - - {ReplayTableColumns.ReplayCountErrorsColumn.Component({ - replay, - rowIndex, - columnIndex: 0, - showDropdownFilters: showDropdownFilters ?? false, - })} - - ); -} - -export function ActivityCell({replay, rowIndex, showDropdownFilters}: Props) { - return ( - - {ReplayTableColumns.ReplayActivityColumn.Component({ - replay, - rowIndex, - columnIndex: 0, - showDropdownFilters: showDropdownFilters ?? false, - })} - - ); -} - -export function PlayPauseCell({replay, rowIndex, showDropdownFilters}: Props) { - return ( - - {ReplayTableColumns.ReplayPlayPauseColumn.Component({ - replay, - rowIndex, - columnIndex: 0, - showDropdownFilters: showDropdownFilters ?? false, - })} - - ); -} - -const Item = styled('div')<{ - isArchived?: boolean; - isReplayCell?: boolean; - isWidget?: boolean; -}>` - display: flex; - align-items: center; - gap: ${space(1)}; - ${p => - p.isWidget - ? `padding: ${space(0.75)} ${space(1.5)} ${space(1.5)} ${space(1.5)};` - : `padding: ${space(1.5)};`}; - ${p => (p.isArchived ? 'opacity: 0.5;' : '')}; - ${p => (p.isReplayCell ? 'overflow: auto;' : '')}; - - &:hover [data-visible-on-hover='true'] { - opacity: 1; - } -`; diff --git a/static/app/views/replays/replayTable/types.tsx b/static/app/views/replays/replayTable/types.tsx deleted file mode 100644 index 7497b7d2e5fd1e..00000000000000 --- a/static/app/views/replays/replayTable/types.tsx +++ /dev/null @@ -1,12 +0,0 @@ -export enum ReplayColumn { - ACTIVITY = 'activity', - BROWSER = 'browser', - COUNT_DEAD_CLICKS = 'countDeadClicks', - COUNT_ERRORS = 'countErrors', - COUNT_RAGE_CLICKS = 'countRageClicks', - DURATION = 'duration', - OS = 'os', - REPLAY = 'replay', - SLOWEST_TRANSACTION = 'slowestTransaction', - PLAY_PAUSE = 'play_pause', -} diff --git a/static/app/views/replays/table/replayIndexTable.tsx b/static/app/views/replays/table/replayIndexTable.tsx new file mode 100644 index 00000000000000..210f4a4d7e13f5 --- /dev/null +++ b/static/app/views/replays/table/replayIndexTable.tsx @@ -0,0 +1,149 @@ +import {Fragment, useMemo} from 'react'; +import styled from '@emotion/styled'; + +import Pagination from 'sentry/components/pagination'; +import ReplayTable from 'sentry/components/replays/table/replayTable'; +import * as ReplayTableColumns from 'sentry/components/replays/table/replayTableColumns'; +import useReplayTableSort from 'sentry/components/replays/table/useReplayTableSort'; +import {t, tct} from 'sentry/locale'; +import {trackAnalytics} from 'sentry/utils/analytics'; +import {useApiQuery} from 'sentry/utils/queryClient'; +import {decodeList, decodeScalar} from 'sentry/utils/queryString'; +import useReplayListQueryKey from 'sentry/utils/replays/hooks/useReplayListQueryKey'; +import {mapResponseToReplayRecord} from 'sentry/utils/replays/replayDataUtils'; +import {MIN_REPLAY_CLICK_SDK} from 'sentry/utils/replays/sdkVersions'; +import {MutableSearch} from 'sentry/utils/tokenizeSearch'; +import useLocationQuery from 'sentry/utils/url/useLocationQuery'; +import {useNavigate} from 'sentry/utils/useNavigate'; +import useOrganization from 'sentry/utils/useOrganization'; +import usePageFilters from 'sentry/utils/usePageFilters'; +import useProjectSdkNeedsUpdate from 'sentry/utils/useProjectSdkNeedsUpdate'; +import useAllMobileProj from 'sentry/views/replays/detail/useAllMobileProj'; +import type {ReplayListRecord} from 'sentry/views/replays/types'; + +const COLUMNS_WEB = [ + ReplayTableColumns.ReplaySessionColumn, + ReplayTableColumns.ReplayOSColumn, + ReplayTableColumns.ReplayBrowserColumn, + ReplayTableColumns.ReplayDurationColumn, + ReplayTableColumns.ReplayCountDeadClicksColumn, + ReplayTableColumns.ReplayCountRageClicksColumn, + ReplayTableColumns.ReplayCountErrorsColumn, + ReplayTableColumns.ReplayActivityColumn, +] as const; + +const COLUMNS_MOBILE = [ + ReplayTableColumns.ReplaySessionColumn, + ReplayTableColumns.ReplayOSColumn, + ReplayTableColumns.ReplayDurationColumn, + ReplayTableColumns.ReplayCountErrorsColumn, + ReplayTableColumns.ReplayActivityColumn, +] as const; + +export default function ReplayIndexTable() { + const organization = useOrganization(); + + const {onSortClick, sortQuery, sortType} = useReplayTableSort(); + const query = useLocationQuery({ + fields: { + cursor: decodeScalar, + end: decodeScalar, + environment: decodeList, + project: decodeList, + query: decodeScalar, + start: decodeScalar, + statsPeriod: decodeScalar, + utc: decodeScalar, + }, + }); + const queryKey = useReplayListQueryKey({ + options: {query: {...query, sort: sortQuery}}, + organization, + queryReferrer: 'replayList', + }); + const {data, isPending, error, getResponseHeader} = useApiQuery<{ + data: ReplayListRecord[]; + }>(queryKey, {staleTime: 0}); + const replays = data?.data.map(mapResponseToReplayRecord); + + const {allMobileProj} = useAllMobileProj({}); + const needsSDKUpdateForClickSearch = useNeedsSDKUpdateForClickSearch(query); + + if (needsSDKUpdateForClickSearch) { + return ( + + {t('Unindexed search field')} + + {tct('Field [field] requires an [sdkPrompt]', { + field: 'click', + sdkPrompt: {t('SDK version >= 7.44.0')}, + })} + + + ); + } + + return ( + + + + + ); +} + +function useNeedsSDKUpdateForClickSearch({query}: {query: string}) { + const organization = useOrganization(); + const { + selection: {projects}, + } = usePageFilters(); + const {needsUpdate} = useProjectSdkNeedsUpdate({ + minVersion: MIN_REPLAY_CLICK_SDK.minVersion, + organization, + projectId: projects.map(String), + }); + + const conditions = useMemo(() => new MutableSearch(query), [query]); + const isSearchingForClicks = conditions + .getFilterKeys() + .some(k => k.startsWith('click.')); + + return needsUpdate && isSearchingForClicks; +} + +function Paginate({pageLinks}: {pageLinks: string | null}) { + const organization = useOrganization(); + const navigate = useNavigate(); + + return ( + { + trackAnalytics('replay.list-paginated', { + organization, + direction: cursor?.endsWith(':1') ? 'prev' : 'next', + }); + navigate({ + pathname: path, + query: {...searchQuery, cursor}, + }); + }} + /> + ); +} + +const EmptyStateSubheading = styled('div')` + color: ${p => p.theme.subText}; + font-size: ${p => p.theme.fontSize.md}; +`; + +const ReplayPagination = styled(Pagination)` + margin-top: 0; +`; From 14d5f7fffe1f6ca0487cc545cee8e16b84152f0d Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Fri, 27 Jun 2025 16:25:35 -0700 Subject: [PATCH 2/6] fixup clickable rows --- .../components/replays/table/replayTable.tsx | 47 +++++++++++-------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/static/app/components/replays/table/replayTable.tsx b/static/app/components/replays/table/replayTable.tsx index aed7002c947568..aac105418273f8 100644 --- a/static/app/components/replays/table/replayTable.tsx +++ b/static/app/components/replays/table/replayTable.tsx @@ -69,26 +69,33 @@ export default function ReplayTable({ return ( {replays.length === 0 && No data} - {replays.map((replay, rowIndex) => ( - - onClickRow?.({replay, rowIndex})}> - {onClickRow && } - {columns.map((column, columnIndex) => ( - - - - ))} - - - ))} + {replays.map((replay, rowIndex) => { + const rows = columns.map((column, columnIndex) => ( + + + + )); + return ( + + {onClickRow ? ( + onClickRow({replay, rowIndex})}> + + {rows} + + ) : ( + rows + )} + + ); + })} ); } From 05254c6c6c47fdb5dded5e275cfd53fff52f1636 Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Mon, 30 Jun 2025 09:52:29 -0700 Subject: [PATCH 3/6] improve types, and pass Div props into SimpleTable --- .../components/replays/table/replayTable.tsx | 39 ++++++++++++------- .../components/tables/simpleTable/index.tsx | 7 ++-- .../views/replays/jetpackComposePiiNotice.tsx | 14 +++---- .../views/replays/list/listContent.spec.tsx | 2 +- .../views/replays/table/replayIndexTable.tsx | 13 ++++++- 5 files changed, 45 insertions(+), 30 deletions(-) diff --git a/static/app/components/replays/table/replayTable.tsx b/static/app/components/replays/table/replayTable.tsx index aac105418273f8..1fd6f4a1c9f5fa 100644 --- a/static/app/components/replays/table/replayTable.tsx +++ b/static/app/components/replays/table/replayTable.tsx @@ -1,4 +1,4 @@ -import {type ReactNode} from 'react'; +import type {HTMLAttributes, ReactNode} from 'react'; import styled from '@emotion/styled'; import {Alert} from 'sentry/components/core/alert'; @@ -12,10 +12,7 @@ import {t} from 'sentry/locale'; import type {Sort} from 'sentry/utils/discover/fields'; import type RequestError from 'sentry/utils/requestError/requestError'; import {ERROR_MAP} from 'sentry/utils/requestError/requestError'; -import type {ReplayListRecordWithTx} from 'sentry/views/performance/transactionSummary/transactionReplays/useReplaysWithTxData'; -import type {ReplayListRecord} from 'sentry/views/replays/types'; - -type ListRecord = ReplayListRecord | ReplayListRecordWithTx; +import type {ReplayRecord} from 'sentry/views/replays/types'; type SortProps = | { @@ -28,9 +25,9 @@ type Props = SortProps & { columns: readonly ReplayTableColumn[]; error: RequestError | null | undefined; isPending: boolean; - replays: ListRecord[]; + replays: ReplayRecord[]; showDropdownFilters: boolean; - onClickRow?: (props: {replay: ListRecord; rowIndex: number}) => void; + onClickRow?: (props: {replay: ReplayRecord; rowIndex: number}) => void; }; export default function ReplayTable({ @@ -45,7 +42,12 @@ export default function ReplayTable({ }: Props) { if (isPending) { return ( - + @@ -55,7 +57,12 @@ export default function ReplayTable({ if (error) { return ( - + {t('Sorry, the list of replays could not be loaded. ')} @@ -67,7 +74,12 @@ export default function ReplayTable({ } return ( - + {replays.length === 0 && No data} {replays.map((replay, rowIndex) => { const rows = columns.map((column, columnIndex) => ( @@ -103,14 +115,13 @@ export default function ReplayTable({ type TableProps = { children: ReactNode; columns: readonly ReplayTableColumn[]; - className?: string; onSortClick?: (key: string) => void; sort?: Sort; -}; +} & HTMLAttributes; const ReplayTableWithColumns = styled( - ({children, className, columns, onSortClick, sort}: TableProps) => ( - + ({children, columns, onSortClick, sort, ...props}: TableProps) => ( + {columns.map(column => ( { children: React.ReactNode; - className?: string; } interface RowProps extends HTMLAttributes { variant?: 'default' | 'faded'; } -export function SimpleTable({className, children}: TableProps) { +export function SimpleTable({children, ...props}: TableProps) { return ( - + {children} ); diff --git a/static/app/views/replays/jetpackComposePiiNotice.tsx b/static/app/views/replays/jetpackComposePiiNotice.tsx index b228d1e43526c8..d16ea6a2671dd2 100644 --- a/static/app/views/replays/jetpackComposePiiNotice.tsx +++ b/static/app/views/replays/jetpackComposePiiNotice.tsx @@ -6,7 +6,7 @@ import {t, tct} from 'sentry/locale'; import {MIN_JETPACK_COMPOSE_VIEW_HIERARCHY_PII_FIX} from 'sentry/utils/replays/sdkVersions'; import useDismissAlert from 'sentry/utils/useDismissAlert'; import {semverCompare} from 'sentry/utils/versions/semverCompare'; -import type {ReplayListRecord} from 'sentry/views/replays/types'; +import type {ReplayRecord} from 'sentry/views/replays/types'; export function JetpackComposePiiNotice() { const LOCAL_STORAGE_KEY = 'jetpack-compose-pii-warning-dismissed'; @@ -48,16 +48,12 @@ export function JetpackComposePiiNotice() { ); } -export function useNeedsJetpackComposePiiNotice({ - replays, -}: { - replays: undefined | ReplayListRecord[]; -}) { - const needsJetpackComposePiiWarning = replays?.find(replay => { +export function useNeedsJetpackComposePiiNotice({replays}: {replays: ReplayRecord[]}) { + const needsJetpackComposePiiWarning = replays.find(replay => { return ( - replay?.sdk.name === 'sentry.java.android' && + replay.sdk.name === 'sentry.java.android' && semverCompare( - replay?.sdk.version ?? '', + replay.sdk.version ?? '', MIN_JETPACK_COMPOSE_VIEW_HIERARCHY_PII_FIX.minVersion ) === -1 ); diff --git a/static/app/views/replays/list/listContent.spec.tsx b/static/app/views/replays/list/listContent.spec.tsx index 27f70b1c950cab..52d0fb952f9403 100644 --- a/static/app/views/replays/list/listContent.spec.tsx +++ b/static/app/views/replays/list/listContent.spec.tsx @@ -55,7 +55,7 @@ describe('ReplayList', () => { mockUseHaveSelectedProjectsSentAnyReplayEvents.mockClear(); mockUseProjectSdkNeedsUpdate.mockClear(); mockUseDeadRageSelectors.mockClear(); - // mockUseAllMobileProj.mockClear(); + mockUseAllMobileProj.mockClear(); MockApiClient.clearMockResponses(); MockApiClient.addMockResponse({ url: '/organizations/org-slug/tags/', diff --git a/static/app/views/replays/table/replayIndexTable.tsx b/static/app/views/replays/table/replayIndexTable.tsx index 210f4a4d7e13f5..a663da63506212 100644 --- a/static/app/views/replays/table/replayIndexTable.tsx +++ b/static/app/views/replays/table/replayIndexTable.tsx @@ -19,6 +19,10 @@ import useOrganization from 'sentry/utils/useOrganization'; import usePageFilters from 'sentry/utils/usePageFilters'; import useProjectSdkNeedsUpdate from 'sentry/utils/useProjectSdkNeedsUpdate'; import useAllMobileProj from 'sentry/views/replays/detail/useAllMobileProj'; +import { + JetpackComposePiiNotice, + useNeedsJetpackComposePiiNotice, +} from 'sentry/views/replays/jetpackComposePiiNotice'; import type {ReplayListRecord} from 'sentry/views/replays/types'; const COLUMNS_WEB = [ @@ -64,11 +68,15 @@ export default function ReplayIndexTable() { const {data, isPending, error, getResponseHeader} = useApiQuery<{ data: ReplayListRecord[]; }>(queryKey, {staleTime: 0}); - const replays = data?.data.map(mapResponseToReplayRecord); + const replays = data?.data?.map(mapResponseToReplayRecord) ?? []; const {allMobileProj} = useAllMobileProj({}); const needsSDKUpdateForClickSearch = useNeedsSDKUpdateForClickSearch(query); + const needsJetpackComposePiiWarning = useNeedsJetpackComposePiiNotice({ + replays, + }); + if (needsSDKUpdateForClickSearch) { return ( @@ -85,12 +93,13 @@ export default function ReplayIndexTable() { return ( + {needsJetpackComposePiiWarning && } From 7ae2496a000c74dd21e8aa86d8ffd3500cbb7c82 Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Mon, 30 Jun 2025 10:00:16 -0700 Subject: [PATCH 4/6] tweak types --- static/app/components/replays/table/replayTable.tsx | 10 ++++++---- static/app/views/replays/jetpackComposePiiNotice.tsx | 8 ++++++-- static/app/views/replays/table/replayIndexTable.tsx | 6 ++---- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/static/app/components/replays/table/replayTable.tsx b/static/app/components/replays/table/replayTable.tsx index 1fd6f4a1c9f5fa..621edfa28d404c 100644 --- a/static/app/components/replays/table/replayTable.tsx +++ b/static/app/components/replays/table/replayTable.tsx @@ -10,9 +10,10 @@ import {ReplaySessionColumn} from 'sentry/components/replays/table/replayTableCo import {SimpleTable} from 'sentry/components/tables/simpleTable'; import {t} from 'sentry/locale'; import type {Sort} from 'sentry/utils/discover/fields'; +import {mapResponseToReplayRecord} from 'sentry/utils/replays/replayDataUtils'; import type RequestError from 'sentry/utils/requestError/requestError'; import {ERROR_MAP} from 'sentry/utils/requestError/requestError'; -import type {ReplayRecord} from 'sentry/views/replays/types'; +import type {ReplayListRecord, ReplayRecord} from 'sentry/views/replays/types'; type SortProps = | { @@ -25,7 +26,7 @@ type Props = SortProps & { columns: readonly ReplayTableColumn[]; error: RequestError | null | undefined; isPending: boolean; - replays: ReplayRecord[]; + replays: ReplayListRecord[]; showDropdownFilters: boolean; onClickRow?: (props: {replay: ReplayRecord; rowIndex: number}) => void; }; @@ -73,6 +74,7 @@ export default function ReplayTable({ ); } + const hydratedReplays = replays.map(mapResponseToReplayRecord); return ( - {replays.length === 0 && No data} - {replays.map((replay, rowIndex) => { + {hydratedReplays.length === 0 && No data} + {hydratedReplays.map((replay, rowIndex) => { const rows = columns.map((column, columnIndex) => ( { return ( replay.sdk.name === 'sentry.java.android' && diff --git a/static/app/views/replays/table/replayIndexTable.tsx b/static/app/views/replays/table/replayIndexTable.tsx index a663da63506212..46df9ac317ec88 100644 --- a/static/app/views/replays/table/replayIndexTable.tsx +++ b/static/app/views/replays/table/replayIndexTable.tsx @@ -10,7 +10,6 @@ import {trackAnalytics} from 'sentry/utils/analytics'; import {useApiQuery} from 'sentry/utils/queryClient'; import {decodeList, decodeScalar} from 'sentry/utils/queryString'; import useReplayListQueryKey from 'sentry/utils/replays/hooks/useReplayListQueryKey'; -import {mapResponseToReplayRecord} from 'sentry/utils/replays/replayDataUtils'; import {MIN_REPLAY_CLICK_SDK} from 'sentry/utils/replays/sdkVersions'; import {MutableSearch} from 'sentry/utils/tokenizeSearch'; import useLocationQuery from 'sentry/utils/url/useLocationQuery'; @@ -68,13 +67,12 @@ export default function ReplayIndexTable() { const {data, isPending, error, getResponseHeader} = useApiQuery<{ data: ReplayListRecord[]; }>(queryKey, {staleTime: 0}); - const replays = data?.data?.map(mapResponseToReplayRecord) ?? []; const {allMobileProj} = useAllMobileProj({}); const needsSDKUpdateForClickSearch = useNeedsSDKUpdateForClickSearch(query); const needsJetpackComposePiiWarning = useNeedsJetpackComposePiiNotice({ - replays, + replays: data?.data ?? [], }); if (needsSDKUpdateForClickSearch) { @@ -99,7 +97,7 @@ export default function ReplayIndexTable() { error={error} isPending={isPending} onSortClick={onSortClick} - replays={replays} + replays={data?.data ?? []} showDropdownFilters sort={sortType} /> From c442dde87d7817d98c3954b78e6dd1afc4f8aa18 Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Mon, 30 Jun 2025 12:01:18 -0700 Subject: [PATCH 5/6] fix tests! --- .../components/replays/table/replayTable.tsx | 20 +++++++++---------- .../replays/table/replayTableColumns.tsx | 5 ++++- .../groupReplays/groupReplays.spec.tsx | 16 +++++++-------- .../transactionReplays/index.spec.tsx | 10 +++++++--- .../views/replays/table/replayIndexTable.tsx | 6 ++++-- 5 files changed, 32 insertions(+), 25 deletions(-) diff --git a/static/app/components/replays/table/replayTable.tsx b/static/app/components/replays/table/replayTable.tsx index 621edfa28d404c..95993c011b6609 100644 --- a/static/app/components/replays/table/replayTable.tsx +++ b/static/app/components/replays/table/replayTable.tsx @@ -10,10 +10,9 @@ import {ReplaySessionColumn} from 'sentry/components/replays/table/replayTableCo import {SimpleTable} from 'sentry/components/tables/simpleTable'; import {t} from 'sentry/locale'; import type {Sort} from 'sentry/utils/discover/fields'; -import {mapResponseToReplayRecord} from 'sentry/utils/replays/replayDataUtils'; import type RequestError from 'sentry/utils/requestError/requestError'; import {ERROR_MAP} from 'sentry/utils/requestError/requestError'; -import type {ReplayListRecord, ReplayRecord} from 'sentry/views/replays/types'; +import type {ReplayListRecord} from 'sentry/views/replays/types'; type SortProps = | { @@ -28,7 +27,7 @@ type Props = SortProps & { isPending: boolean; replays: ReplayListRecord[]; showDropdownFilters: boolean; - onClickRow?: (props: {replay: ReplayRecord; rowIndex: number}) => void; + onClickRow?: (props: {replay: ReplayListRecord; rowIndex: number}) => void; }; export default function ReplayTable({ @@ -59,7 +58,7 @@ export default function ReplayTable({ if (error) { return ( - {hydratedReplays.length === 0 && No data} - {hydratedReplays.map((replay, rowIndex) => { + {replays.length === 0 && ( + {t('No replays found')} + )} + {replays.map((replay, rowIndex) => { const rows = columns.map((column, columnIndex) => ( {onClickRow ? ( - onClickRow({replay, rowIndex})}> + onClickRow({replay, rowIndex})}> {rows} @@ -125,9 +125,9 @@ const ReplayTableWithColumns = styled( ({children, columns, onSortClick, sort, ...props}: TableProps) => ( - {columns.map(column => ( + {columns.map((column, columnIndex) => ( column.sortKey && onSortClick?.(column.sortKey)} sort={ column.sortKey && sort?.field === column.sortKey ? sort.kind : undefined diff --git a/static/app/components/replays/table/replayTableColumns.tsx b/static/app/components/replays/table/replayTableColumns.tsx index 0620ef692370c0..e713ffad9432a9 100644 --- a/static/app/components/replays/table/replayTableColumns.tsx +++ b/static/app/components/replays/table/replayTableColumns.tsx @@ -183,7 +183,10 @@ export const ReplayCountErrorsColumn: ReplayTableColumn = { return null; } return ( - + {replay.count_errors ? ( diff --git a/static/app/views/issueDetails/groupReplays/groupReplays.spec.tsx b/static/app/views/issueDetails/groupReplays/groupReplays.spec.tsx index d7a97aa2cbee24..09b1d26fdbed16 100644 --- a/static/app/views/issueDetails/groupReplays/groupReplays.spec.tsx +++ b/static/app/views/issueDetails/groupReplays/groupReplays.spec.tsx @@ -246,9 +246,7 @@ describe('GroupReplays', () => { deprecatedRouterMocks: true, }); - expect( - await screen.findByText('There are no items to display') - ).toBeInTheDocument(); + expect(await screen.findByText('No replays found')).toBeInTheDocument(); expect(mockReplayCountApi).toHaveBeenCalled(); expect(mockReplayApi).toHaveBeenCalledTimes(1); }); @@ -447,14 +445,14 @@ describe('GroupReplays', () => { expect(screen.getByText('06:40')).toBeInTheDocument(); // Expect the first row to have the correct errors - expect(screen.getAllByTestId('replay-table-count-errors')[0]).toHaveTextContent( - '1' - ); + expect( + screen.getAllByTestId('replay-table-column-count-errors')[0] + ).toHaveTextContent('1'); // Expect the second row to have the correct errors - expect(screen.getAllByTestId('replay-table-count-errors')[1]).toHaveTextContent( - '4' - ); + expect( + screen.getAllByTestId('replay-table-column-count-errors')[1] + ).toHaveTextContent('4'); // Expect the first row to have the correct date expect(screen.getByText('14 days ago')).toBeInTheDocument(); diff --git a/static/app/views/performance/transactionSummary/transactionReplays/index.spec.tsx b/static/app/views/performance/transactionSummary/transactionReplays/index.spec.tsx index 515b4ec483dadf..7de238a7f47082 100644 --- a/static/app/views/performance/transactionSummary/transactionReplays/index.spec.tsx +++ b/static/app/views/performance/transactionSummary/transactionReplays/index.spec.tsx @@ -159,7 +159,7 @@ describe('TransactionReplays', () => { await waitFor(() => { expect(replaysMockApi).toHaveBeenCalledTimes(1); }); - expect(screen.getByText('There are no items to display')).toBeInTheDocument(); + expect(screen.getByText('No replays found')).toBeInTheDocument(); }); it('should show loading indicator when loading replays', async () => { @@ -251,10 +251,14 @@ describe('TransactionReplays', () => { expect(screen.getByText('06:40')).toBeInTheDocument(); // Expect the first row to have the correct errors - expect(screen.getAllByTestId('replay-table-count-errors')[0]).toHaveTextContent('1'); + expect( + screen.getAllByTestId('replay-table-column-count-errors')[0] + ).toHaveTextContent('1'); // Expect the second row to have the correct errors - expect(screen.getAllByTestId('replay-table-count-errors')[1]).toHaveTextContent('4'); + expect( + screen.getAllByTestId('replay-table-column-count-errors')[1] + ).toHaveTextContent('4'); // Expect the first row to have the correct date expect(screen.getByText('14 days ago')).toBeInTheDocument(); diff --git a/static/app/views/replays/table/replayIndexTable.tsx b/static/app/views/replays/table/replayIndexTable.tsx index 46df9ac317ec88..3807277dc3a377 100644 --- a/static/app/views/replays/table/replayIndexTable.tsx +++ b/static/app/views/replays/table/replayIndexTable.tsx @@ -10,6 +10,7 @@ import {trackAnalytics} from 'sentry/utils/analytics'; import {useApiQuery} from 'sentry/utils/queryClient'; import {decodeList, decodeScalar} from 'sentry/utils/queryString'; import useReplayListQueryKey from 'sentry/utils/replays/hooks/useReplayListQueryKey'; +import {mapResponseToReplayRecord} from 'sentry/utils/replays/replayDataUtils'; import {MIN_REPLAY_CLICK_SDK} from 'sentry/utils/replays/sdkVersions'; import {MutableSearch} from 'sentry/utils/tokenizeSearch'; import useLocationQuery from 'sentry/utils/url/useLocationQuery'; @@ -67,12 +68,13 @@ export default function ReplayIndexTable() { const {data, isPending, error, getResponseHeader} = useApiQuery<{ data: ReplayListRecord[]; }>(queryKey, {staleTime: 0}); + const replays = data?.data.map(mapResponseToReplayRecord) ?? []; const {allMobileProj} = useAllMobileProj({}); const needsSDKUpdateForClickSearch = useNeedsSDKUpdateForClickSearch(query); const needsJetpackComposePiiWarning = useNeedsJetpackComposePiiNotice({ - replays: data?.data ?? [], + replays, }); if (needsSDKUpdateForClickSearch) { @@ -97,7 +99,7 @@ export default function ReplayIndexTable() { error={error} isPending={isPending} onSortClick={onSortClick} - replays={data?.data ?? []} + replays={replays} showDropdownFilters sort={sortType} /> From 1d5a7551b5e59481c9d306f158b090fcfa86dd3b Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Mon, 30 Jun 2025 17:03:47 -0700 Subject: [PATCH 6/6] missed a spot --- static/app/views/replays/table/replayIndexTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/views/replays/table/replayIndexTable.tsx b/static/app/views/replays/table/replayIndexTable.tsx index 3807277dc3a377..a663da63506212 100644 --- a/static/app/views/replays/table/replayIndexTable.tsx +++ b/static/app/views/replays/table/replayIndexTable.tsx @@ -68,7 +68,7 @@ export default function ReplayIndexTable() { const {data, isPending, error, getResponseHeader} = useApiQuery<{ data: ReplayListRecord[]; }>(queryKey, {staleTime: 0}); - const replays = data?.data.map(mapResponseToReplayRecord) ?? []; + const replays = data?.data?.map(mapResponseToReplayRecord) ?? []; const {allMobileProj} = useAllMobileProj({}); const needsSDKUpdateForClickSearch = useNeedsSDKUpdateForClickSearch(query);