content.tsx 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. import {useEffect, useState} from 'react';
  2. import {browserHistory, InjectedRouter} from 'react-router';
  3. import * as Sentry from '@sentry/react';
  4. import {Location} from 'history';
  5. import isEqual from 'lodash/isEqual';
  6. import {loadOrganizationTags} from 'sentry/actionCreators/tags';
  7. import Feature from 'sentry/components/acl/feature';
  8. import Alert from 'sentry/components/alert';
  9. import Button from 'sentry/components/button';
  10. import GlobalSdkUpdateAlert from 'sentry/components/globalSdkUpdateAlert';
  11. import NoProjectMessage from 'sentry/components/noProjectMessage';
  12. import GlobalSelectionHeader from 'sentry/components/organizations/globalSelectionHeader';
  13. import PageHeading from 'sentry/components/pageHeading';
  14. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  15. import {ALL_ACCESS_PROJECTS} from 'sentry/constants/pageFilters';
  16. import {IconFlag} from 'sentry/icons';
  17. import {t} from 'sentry/locale';
  18. import {PageContent, PageHeader} from 'sentry/styles/organization';
  19. import {GlobalSelection} from 'sentry/types';
  20. import {trackAnalyticsEvent} from 'sentry/utils/analytics';
  21. import {PerformanceEventViewProvider} from 'sentry/utils/performance/contexts/performanceEventViewContext';
  22. import useApi from 'sentry/utils/useApi';
  23. import useOrganization from 'sentry/utils/useOrganization';
  24. import usePrevious from 'sentry/utils/usePrevious';
  25. import useProjects from 'sentry/utils/useProjects';
  26. import withGlobalSelection from 'sentry/utils/withGlobalSelection';
  27. import LandingContent from './landing/content';
  28. import {DEFAULT_STATS_PERIOD, generatePerformanceEventView} from './data';
  29. import {PerformanceLanding} from './landing';
  30. import {useMetricsSwitch} from './metricsSwitch';
  31. import Onboarding from './onboarding';
  32. import {addRoutePerformanceContext, handleTrendsClick} from './utils';
  33. type Props = {
  34. selection: GlobalSelection;
  35. location: Location;
  36. router: InjectedRouter;
  37. demoMode?: boolean;
  38. };
  39. type State = {
  40. error?: string;
  41. };
  42. function PerformanceContent({selection, location, demoMode}: Props) {
  43. const api = useApi();
  44. const organization = useOrganization();
  45. const {projects} = useProjects();
  46. const {isMetricsData} = useMetricsSwitch();
  47. const previousDateTime = usePrevious(selection.datetime);
  48. const [state, setState] = useState<State>({error: undefined});
  49. useEffect(() => {
  50. loadOrganizationTags(api, organization.slug, selection);
  51. addRoutePerformanceContext(selection);
  52. trackAnalyticsEvent({
  53. eventKey: 'performance_views.overview.view',
  54. eventName: 'Performance Views: Transaction overview view',
  55. organization_id: parseInt(organization.id, 10),
  56. show_onboarding: shouldShowOnboarding(),
  57. });
  58. }, []);
  59. useEffect(() => {
  60. loadOrganizationTags(api, organization.slug, selection);
  61. addRoutePerformanceContext(selection);
  62. }, [selection.projects]);
  63. useEffect(() => {
  64. if (!isEqual(previousDateTime, selection.datetime)) {
  65. loadOrganizationTags(api, organization.slug, selection);
  66. addRoutePerformanceContext(selection);
  67. }
  68. }, [selection.datetime]);
  69. const {error} = state;
  70. function setError(newError?: string) {
  71. if (
  72. typeof newError === 'object' ||
  73. (Array.isArray(newError) && typeof newError[0] === 'object')
  74. ) {
  75. Sentry.withScope(scope => {
  76. scope.setExtra('error', newError);
  77. Sentry.captureException(new Error('setError failed with error type.'));
  78. });
  79. return;
  80. }
  81. setState({...state, error: newError});
  82. }
  83. function handleSearch(searchQuery: string) {
  84. trackAnalyticsEvent({
  85. eventKey: 'performance_views.overview.search',
  86. eventName: 'Performance Views: Transaction overview search',
  87. organization_id: parseInt(organization.id, 10),
  88. });
  89. browserHistory.push({
  90. pathname: location.pathname,
  91. query: {
  92. ...location.query,
  93. cursor: undefined,
  94. query: String(searchQuery).trim() || undefined,
  95. },
  96. });
  97. }
  98. function renderError() {
  99. if (!error) {
  100. return null;
  101. }
  102. return (
  103. <Alert type="error" icon={<IconFlag size="md" />}>
  104. {error}
  105. </Alert>
  106. );
  107. }
  108. const eventView = generatePerformanceEventView(location, organization, projects, {
  109. isMetricsData,
  110. });
  111. function shouldShowOnboarding() {
  112. // XXX used by getsentry to bypass onboarding for the upsell demo state.
  113. if (demoMode) {
  114. return false;
  115. }
  116. if (projects.length === 0) {
  117. return false;
  118. }
  119. // Current selection is 'my projects' or 'all projects'
  120. if (eventView.project.length === 0 || eventView.project === [ALL_ACCESS_PROJECTS]) {
  121. return (
  122. projects.filter(p => p.firstTransactionEvent === false).length === projects.length
  123. );
  124. }
  125. // Any other subset of projects.
  126. return (
  127. projects.filter(
  128. p =>
  129. eventView.project.includes(parseInt(p.id, 10)) &&
  130. p.firstTransactionEvent === false
  131. ).length === eventView.project.length
  132. );
  133. }
  134. function renderBody() {
  135. const showOnboarding = shouldShowOnboarding();
  136. return (
  137. <PageContent>
  138. <NoProjectMessage organization={organization}>
  139. <PageHeader>
  140. <PageHeading>{t('Performance')}</PageHeading>
  141. {!showOnboarding && (
  142. <Button
  143. priority="primary"
  144. data-test-id="landing-header-trends"
  145. onClick={() => handleTrendsClick({location, organization})}
  146. >
  147. {t('View Trends')}
  148. </Button>
  149. )}
  150. </PageHeader>
  151. <GlobalSdkUpdateAlert />
  152. {renderError()}
  153. {showOnboarding ? (
  154. <Onboarding
  155. organization={organization}
  156. project={
  157. selection.projects.length > 0
  158. ? // If some projects selected, use the first selection
  159. projects.find(
  160. project => selection.projects[0].toString() === project.id
  161. ) || projects[0]
  162. : // Otherwise, use the first project in the org
  163. projects[0]
  164. }
  165. />
  166. ) : (
  167. <LandingContent
  168. eventView={eventView}
  169. projects={projects}
  170. organization={organization}
  171. setError={setError}
  172. handleSearch={handleSearch}
  173. />
  174. )}
  175. </NoProjectMessage>
  176. </PageContent>
  177. );
  178. }
  179. function renderLandingV3() {
  180. return (
  181. <PerformanceLanding
  182. eventView={eventView}
  183. setError={setError}
  184. handleSearch={handleSearch}
  185. handleTrendsClick={() => handleTrendsClick({location, organization})}
  186. shouldShowOnboarding={shouldShowOnboarding()}
  187. organization={organization}
  188. location={location}
  189. projects={projects}
  190. selection={selection}
  191. />
  192. );
  193. }
  194. return (
  195. <SentryDocumentTitle title={t('Performance')} orgSlug={organization.slug}>
  196. <PerformanceEventViewProvider value={{eventView}}>
  197. <GlobalSelectionHeader
  198. defaultSelection={{
  199. datetime: {
  200. start: null,
  201. end: null,
  202. utc: false,
  203. period: DEFAULT_STATS_PERIOD,
  204. },
  205. }}
  206. >
  207. <Feature features={['organizations:performance-landing-widgets']}>
  208. {({hasFeature}) => (hasFeature ? renderLandingV3() : renderBody())}
  209. </Feature>
  210. </GlobalSelectionHeader>
  211. </PerformanceEventViewProvider>
  212. </SentryDocumentTitle>
  213. );
  214. }
  215. export default withGlobalSelection(PerformanceContent);