replays.tsx 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. import {Fragment, useEffect, useState} from 'react';
  2. import {browserHistory} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import FeatureBadge from 'sentry/components/featureBadge';
  5. import Link from 'sentry/components/links/link';
  6. import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container';
  7. import PageHeading from 'sentry/components/pageHeading';
  8. import Pagination from 'sentry/components/pagination';
  9. import {PanelTable} from 'sentry/components/panels';
  10. import {IconArrow} from 'sentry/icons';
  11. import {t} from 'sentry/locale';
  12. import {PageContent, PageHeader} from 'sentry/styles/organization';
  13. import space from 'sentry/styles/space';
  14. import {NewQuery} from 'sentry/types';
  15. import DiscoverQuery from 'sentry/utils/discover/discoverQuery';
  16. import EventView from 'sentry/utils/discover/eventView';
  17. import {getQueryParamAsString} from 'sentry/utils/replays/getQueryParamAsString';
  18. import theme from 'sentry/utils/theme';
  19. import {useLocation} from 'sentry/utils/useLocation';
  20. import useMedia from 'sentry/utils/useMedia';
  21. import useOrganization from 'sentry/utils/useOrganization';
  22. import usePageFilters from 'sentry/utils/usePageFilters';
  23. import ReplaysFilters from './filters';
  24. import ReplayTable from './replayTable';
  25. import {Replay} from './types';
  26. const columns = [t('Session'), t('Project')];
  27. function Replays() {
  28. const location = useLocation();
  29. const organization = useOrganization();
  30. const {selection} = usePageFilters();
  31. const isScreenLarge = useMedia(`(min-width: ${theme.breakpoints.small})`);
  32. const [searchQuery, setSearchQuery] = useState<string>(
  33. getQueryParamAsString(location.query.query)
  34. );
  35. useEffect(() => {
  36. setSearchQuery(getQueryParamAsString(location.query.query));
  37. }, [location.query.query]);
  38. const getEventView = () => {
  39. const {query} = location;
  40. const eventQueryParams: NewQuery = {
  41. id: '',
  42. name: '',
  43. version: 2,
  44. fields: [
  45. // 'id' is always returned, don't need to list it here.
  46. 'eventID',
  47. 'project',
  48. 'timestamp',
  49. 'url',
  50. 'user.display',
  51. 'user.email',
  52. 'user.id',
  53. 'user.ip_address',
  54. 'user.name',
  55. 'user.username',
  56. ],
  57. orderby: getQueryParamAsString(query.sort) || '-timestamp',
  58. environment: selection.environments,
  59. projects: selection.projects,
  60. query: `title:sentry-replay ${searchQuery}`,
  61. };
  62. if (selection.datetime.period) {
  63. eventQueryParams.range = selection.datetime.period;
  64. }
  65. return EventView.fromNewQueryWithLocation(eventQueryParams, location);
  66. };
  67. const handleSearchQuery = (query: string) => {
  68. browserHistory.push({
  69. pathname: location.pathname,
  70. query: {
  71. ...location.query,
  72. cursor: undefined,
  73. query: String(query).trim() || undefined,
  74. },
  75. });
  76. };
  77. const {query} = location;
  78. const {cursor: _cursor, page: _page, ...currentQuery} = query;
  79. const sort: {
  80. field: string;
  81. } = {
  82. field: getQueryParamAsString(query.sort) || '-timestamp',
  83. };
  84. const arrowDirection = sort.field.startsWith('-') ? 'down' : 'up';
  85. const sortArrow = <IconArrow color="gray300" size="xs" direction={arrowDirection} />;
  86. return (
  87. <Fragment>
  88. <StyledPageHeader>
  89. <HeaderTitle>
  90. <div>
  91. {t('Replays')} <FeatureBadge type="alpha" />
  92. </div>
  93. </HeaderTitle>
  94. </StyledPageHeader>
  95. <PageFiltersContainer>
  96. <StyledPageContent>
  97. <DiscoverQuery
  98. eventView={getEventView()}
  99. location={location}
  100. orgSlug={organization.slug}
  101. limit={15}
  102. >
  103. {data => {
  104. return (
  105. <Fragment>
  106. <ReplaysFilters
  107. query={searchQuery}
  108. organization={organization}
  109. handleSearchQuery={handleSearchQuery}
  110. />
  111. <StyledPanelTable
  112. isLoading={data.isLoading}
  113. isEmpty={data.tableData?.data.length === 0}
  114. headers={[
  115. ...(!isScreenLarge
  116. ? columns.filter(col => col === t('Session'))
  117. : columns),
  118. <SortLink
  119. key="timestamp"
  120. role="columnheader"
  121. aria-sort={
  122. !sort.field.endsWith('timestamp')
  123. ? 'none'
  124. : sort.field === '-timestamp'
  125. ? 'descending'
  126. : 'ascending'
  127. }
  128. to={{
  129. pathname: location.pathname,
  130. query: {
  131. ...currentQuery,
  132. // sort by timestamp should start by ascending on first click
  133. sort:
  134. sort.field === '-timestamp' ? 'timestamp' : '-timestamp',
  135. },
  136. }}
  137. >
  138. {t('Timestamp')} {sort.field.endsWith('timestamp') && sortArrow}
  139. </SortLink>,
  140. t('Duration'),
  141. t('Errors'),
  142. ]}
  143. >
  144. {data.tableData ? (
  145. <ReplayTable
  146. idKey="id"
  147. showProjectColumn
  148. replayList={data.tableData.data as Replay[]}
  149. />
  150. ) : null}
  151. </StyledPanelTable>
  152. <Pagination pageLinks={data.pageLinks} />
  153. </Fragment>
  154. );
  155. }}
  156. </DiscoverQuery>
  157. </StyledPageContent>
  158. </PageFiltersContainer>
  159. </Fragment>
  160. );
  161. }
  162. const StyledPageHeader = styled(PageHeader)`
  163. background-color: ${p => p.theme.surface100};
  164. min-width: max-content;
  165. margin: ${space(3)} ${space(0)} ${space(4)} ${space(4)};
  166. `;
  167. const StyledPageContent = styled(PageContent)`
  168. box-shadow: 0px 0px 1px ${p => p.theme.gray200};
  169. background-color: ${p => p.theme.background};
  170. `;
  171. const StyledPanelTable = styled(PanelTable)`
  172. grid-template-columns: minmax(0, 1fr) max-content max-content max-content max-content;
  173. @media (max-width: ${p => p.theme.breakpoints.small}) {
  174. grid-template-columns: minmax(0, 1fr) max-content max-content max-content;
  175. }
  176. `;
  177. const HeaderTitle = styled(PageHeading)`
  178. display: flex;
  179. align-items: center;
  180. justify-content: space-between;
  181. flex: 1;
  182. `;
  183. const SortLink = styled(Link)`
  184. color: inherit;
  185. :hover {
  186. color: inherit;
  187. }
  188. svg {
  189. vertical-align: top;
  190. }
  191. `;
  192. export default Replays;