replaysErroneousDeadRageCards.tsx 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. import {ComponentProps, Fragment, ReactNode, useEffect, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import {Location} from 'history';
  4. import {LinkButton} from 'sentry/components/button';
  5. import {IconClose, IconSearch} from 'sentry/icons';
  6. import {t} from 'sentry/locale';
  7. import {space} from 'sentry/styles/space';
  8. import {trackAnalytics} from 'sentry/utils/analytics';
  9. import EventView from 'sentry/utils/discover/eventView';
  10. import useReplayList from 'sentry/utils/replays/hooks/useReplayList';
  11. import {useLocation} from 'sentry/utils/useLocation';
  12. import useOrganization from 'sentry/utils/useOrganization';
  13. import ReplayTable from 'sentry/views/replays/replayTable';
  14. import {ReplayColumn} from 'sentry/views/replays/replayTable/types';
  15. import {ReplayListLocationQuery} from 'sentry/views/replays/types';
  16. function ReplaysErroneousDeadRageCards() {
  17. const location = useLocation<ReplayListLocationQuery>();
  18. const {project, environment, start, statsPeriod, utc, end} = location.query;
  19. const searchLocation: Location<ReplayListLocationQuery> = useMemo(() => {
  20. return {
  21. pathname: '',
  22. search: '',
  23. hash: '',
  24. state: '',
  25. action: 'PUSH' as const,
  26. key: '',
  27. query: {project, environment, start, statsPeriod, utc, end},
  28. };
  29. }, [project, environment, start, statsPeriod, utc, end]);
  30. return (
  31. <SplitCardContainer>
  32. <DeadClickTable searchLocation={searchLocation} />
  33. <RageClickTable searchLocation={searchLocation} />
  34. </SplitCardContainer>
  35. );
  36. }
  37. function DeadClickTable({
  38. searchLocation,
  39. }: {
  40. searchLocation: Location<ReplayListLocationQuery>;
  41. }) {
  42. const organization = useOrganization();
  43. const eventView = useMemo(
  44. () =>
  45. EventView.fromNewQueryWithLocation(
  46. {
  47. id: '',
  48. name: '',
  49. version: 2,
  50. fields: [
  51. 'activity',
  52. 'duration',
  53. 'count_dead_clicks',
  54. 'id',
  55. 'project_id',
  56. 'user',
  57. 'finished_at',
  58. 'is_archived',
  59. 'started_at',
  60. ],
  61. projects: [],
  62. query: 'count_dead_clicks:>0',
  63. orderby: '-count_dead_clicks',
  64. },
  65. searchLocation
  66. ),
  67. [searchLocation]
  68. );
  69. useEffect(() => {
  70. trackAnalytics('replay.dead-click-card.rendered', {organization});
  71. }, []); // eslint-disable-line react-hooks/exhaustive-deps
  72. return (
  73. <CardTable
  74. eventView={eventView}
  75. location={searchLocation}
  76. visibleColumns={[
  77. ReplayColumn.MOST_DEAD_CLICKS,
  78. ReplayColumn.COUNT_DEAD_CLICKS_NO_HEADER,
  79. ]}
  80. >
  81. <SearchButton
  82. analyticsEventKey="replay.dead-click-card.click_search"
  83. analyticsEventName="Replay Dead Click Card Search Click"
  84. analyticsParams={{}}
  85. eventView={eventView}
  86. label={t('Show all replays with dead clicks')}
  87. />
  88. </CardTable>
  89. );
  90. }
  91. function RageClickTable({
  92. searchLocation,
  93. }: {
  94. searchLocation: Location<ReplayListLocationQuery>;
  95. }) {
  96. const organization = useOrganization();
  97. const eventView = useMemo(
  98. () =>
  99. EventView.fromNewQueryWithLocation(
  100. {
  101. id: '',
  102. name: '',
  103. version: 2,
  104. fields: [
  105. 'activity',
  106. 'duration',
  107. 'count_rage_clicks',
  108. 'id',
  109. 'project_id',
  110. 'user',
  111. 'finished_at',
  112. 'is_archived',
  113. 'started_at',
  114. ],
  115. projects: [],
  116. query: 'count_rage_clicks:>0',
  117. orderby: '-count_rage_clicks',
  118. },
  119. searchLocation
  120. ),
  121. [searchLocation]
  122. );
  123. useEffect(() => {
  124. trackAnalytics('replay.rage-click-card.rendered', {organization});
  125. }, []); // eslint-disable-line react-hooks/exhaustive-deps
  126. return (
  127. <CardTable
  128. eventView={eventView}
  129. location={searchLocation}
  130. visibleColumns={[
  131. ReplayColumn.MOST_RAGE_CLICKS,
  132. ReplayColumn.COUNT_RAGE_CLICKS_NO_HEADER,
  133. ]}
  134. >
  135. <SearchButton
  136. analyticsEventKey="replay.rage-click-card.click_search"
  137. analyticsEventName="Replay Rage Click Card Search Click"
  138. analyticsParams={{}}
  139. eventView={eventView}
  140. label={t('Show all replays with rage clicks')}
  141. />
  142. </CardTable>
  143. );
  144. }
  145. function CardTable({
  146. children,
  147. eventView,
  148. location,
  149. visibleColumns,
  150. }: {
  151. children: ReactNode;
  152. eventView: EventView;
  153. location: Location<ReplayListLocationQuery>;
  154. visibleColumns: ReplayColumn[];
  155. }) {
  156. const organization = useOrganization();
  157. const {replays, isFetching, fetchError} = useReplayList({
  158. eventView,
  159. location,
  160. organization,
  161. perPage: 3,
  162. });
  163. const length = replays?.length ?? 0;
  164. const rows = length > 0 ? 3 : 1;
  165. return (
  166. <Fragment>
  167. <ReplayTable
  168. fetchError={fetchError}
  169. isFetching={isFetching}
  170. replays={replays}
  171. sort={undefined}
  172. visibleColumns={visibleColumns}
  173. saveLocation
  174. gridRows={`auto repeat(${rows}, 1fr)`}
  175. showDropdownFilters={false}
  176. />
  177. {children}
  178. </Fragment>
  179. );
  180. }
  181. function SearchButton({
  182. eventView,
  183. label,
  184. ...props
  185. }: {
  186. eventView: EventView;
  187. label: ReactNode;
  188. } & Omit<ComponentProps<typeof LinkButton>, 'size' | 'to' | 'icon'>) {
  189. const location = useLocation();
  190. const isActive = location.query.query === eventView.query;
  191. return (
  192. <StyledButton
  193. {...props}
  194. size="sm"
  195. to={{
  196. pathname: location.pathname,
  197. query: {
  198. ...location.query,
  199. cursor: undefined,
  200. query: isActive ? '' : eventView.query,
  201. sort: isActive ? '' : eventView.sorts[0].field,
  202. },
  203. }}
  204. icon={isActive ? <IconClose size="xs" /> : <IconSearch size="xs" />}
  205. >
  206. {isActive ? t('Clear filter') : label}
  207. </StyledButton>
  208. );
  209. }
  210. const SplitCardContainer = styled('div')`
  211. display: grid;
  212. grid-template-columns: 1fr 1fr;
  213. grid-template-rows: max-content max-content;
  214. grid-auto-flow: column;
  215. gap: 0 ${space(2)};
  216. align-items: stretch;
  217. `;
  218. const StyledButton = styled(LinkButton)`
  219. width: 100%;
  220. border-top: none;
  221. border-radius: ${p => p.theme.borderRadiusBottom};
  222. padding: ${space(3)};
  223. `;
  224. export default ReplaysErroneousDeadRageCards;