content.tsx 7.6 KB

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