replaysList.tsx 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. import {Fragment, useMemo} from 'react';
  2. import {browserHistory} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import type {Location} from 'history';
  5. import Pagination from 'sentry/components/pagination';
  6. import {t, tct} from 'sentry/locale';
  7. import type {Organization} from 'sentry/types';
  8. import {trackAnalytics} from 'sentry/utils/analytics';
  9. import EventView from 'sentry/utils/discover/eventView';
  10. import {decodeScalar} from 'sentry/utils/queryString';
  11. import {DEFAULT_SORT} from 'sentry/utils/replays/fetchReplayList';
  12. import useReplayList from 'sentry/utils/replays/hooks/useReplayList';
  13. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  14. import {useLocation} from 'sentry/utils/useLocation';
  15. import useOrganization from 'sentry/utils/useOrganization';
  16. import usePageFilters from 'sentry/utils/usePageFilters';
  17. import useProjectSdkNeedsUpdate from 'sentry/utils/useProjectSdkNeedsUpdate';
  18. import ReplayTable from 'sentry/views/replays/replayTable';
  19. import {ReplayColumn} from 'sentry/views/replays/replayTable/types';
  20. import type {ReplayListLocationQuery} from 'sentry/views/replays/types';
  21. import {REPLAY_LIST_FIELDS} from 'sentry/views/replays/types';
  22. function ReplaysList() {
  23. const location = useLocation<ReplayListLocationQuery>();
  24. const organization = useOrganization();
  25. const eventView = useMemo(() => {
  26. const query = decodeScalar(location.query.query, '');
  27. const conditions = new MutableSearch(query);
  28. return EventView.fromNewQueryWithLocation(
  29. {
  30. id: '',
  31. name: '',
  32. version: 2,
  33. fields: REPLAY_LIST_FIELDS,
  34. projects: [],
  35. query: conditions.formatString(),
  36. orderby: decodeScalar(location.query.sort, DEFAULT_SORT),
  37. },
  38. location
  39. );
  40. }, [location]);
  41. return (
  42. <ReplaysListTable
  43. eventView={eventView}
  44. location={location}
  45. organization={organization}
  46. />
  47. );
  48. }
  49. const MIN_REPLAY_CLICK_SDK = '7.44.0';
  50. function ReplaysListTable({
  51. eventView,
  52. location,
  53. organization,
  54. }: {
  55. eventView: EventView;
  56. location: Location;
  57. organization: Organization;
  58. }) {
  59. const {replays, pageLinks, isFetching, fetchError} = useReplayList({
  60. eventView,
  61. location,
  62. organization,
  63. });
  64. const {
  65. selection: {projects},
  66. } = usePageFilters();
  67. const {needsUpdate: allSelectedProjectsNeedUpdates} = useProjectSdkNeedsUpdate({
  68. minVersion: MIN_REPLAY_CLICK_SDK,
  69. organization,
  70. projectId: projects.map(String),
  71. });
  72. const conditions = useMemo(() => {
  73. return new MutableSearch(decodeScalar(location.query.query, ''));
  74. }, [location.query.query]);
  75. const hasReplayClick = conditions.getFilterKeys().some(k => k.startsWith('click.'));
  76. const visibleCols = [
  77. ReplayColumn.REPLAY,
  78. ReplayColumn.OS,
  79. ReplayColumn.BROWSER,
  80. ReplayColumn.DURATION,
  81. ReplayColumn.COUNT_DEAD_CLICKS,
  82. ReplayColumn.COUNT_RAGE_CLICKS,
  83. ReplayColumn.COUNT_ERRORS,
  84. ReplayColumn.ACTIVITY,
  85. ];
  86. return (
  87. <Fragment>
  88. <ReplayTable
  89. fetchError={fetchError}
  90. isFetching={isFetching}
  91. replays={replays}
  92. sort={eventView.sorts[0]}
  93. visibleColumns={visibleCols}
  94. showDropdownFilters
  95. emptyMessage={
  96. allSelectedProjectsNeedUpdates && hasReplayClick ? (
  97. <Fragment>
  98. {t('Unindexed search field')}
  99. <EmptyStateSubheading>
  100. {tct('Field [field] requires an [sdkPrompt]', {
  101. field: <strong>'click'</strong>,
  102. sdkPrompt: <strong>{t('SDK version >= 7.44.0')}</strong>,
  103. })}
  104. </EmptyStateSubheading>
  105. </Fragment>
  106. ) : undefined
  107. }
  108. />
  109. <ReplayPagination
  110. pageLinks={pageLinks}
  111. onCursor={(cursor, path, searchQuery) => {
  112. trackAnalytics('replay.list-paginated', {
  113. organization,
  114. direction: cursor?.endsWith(':1') ? 'prev' : 'next',
  115. });
  116. browserHistory.push({
  117. pathname: path,
  118. query: {...searchQuery, cursor},
  119. });
  120. }}
  121. />
  122. </Fragment>
  123. );
  124. }
  125. const EmptyStateSubheading = styled('div')`
  126. color: ${p => p.theme.subText};
  127. font-size: ${p => p.theme.fontSizeMedium};
  128. `;
  129. const ReplayPagination = styled(Pagination)`
  130. margin-top: 0;
  131. `;
  132. export default ReplaysList;