content.tsx 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  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 {decodeScalar} from 'app/utils/queryString';
  24. import {MutableSearch} from 'app/utils/tokenizeSearch';
  25. import withApi from 'app/utils/withApi';
  26. import withGlobalSelection from 'app/utils/withGlobalSelection';
  27. import withOrganization from 'app/utils/withOrganization';
  28. import withProjects from 'app/utils/withProjects';
  29. import LandingContent from './landing/content';
  30. import {DEFAULT_MAX_DURATION} from './trends/utils';
  31. import {DEFAULT_STATS_PERIOD, generatePerformanceEventView} from './data';
  32. import {PerformanceLanding} from './landing';
  33. import Onboarding from './onboarding';
  34. import {addRoutePerformanceContext, getPerformanceTrendsUrl} from './utils';
  35. type Props = {
  36. api: Client;
  37. organization: Organization;
  38. selection: GlobalSelection;
  39. location: Location;
  40. router: InjectedRouter;
  41. projects: Project[];
  42. loadingProjects: boolean;
  43. demoMode?: boolean;
  44. };
  45. type State = {
  46. eventView: EventView;
  47. error: string | undefined;
  48. };
  49. class PerformanceContent extends Component<Props, State> {
  50. static getDerivedStateFromProps(nextProps: Readonly<Props>, prevState: State): State {
  51. return {
  52. ...prevState,
  53. eventView: generatePerformanceEventView(
  54. nextProps.organization,
  55. nextProps.location,
  56. nextProps.projects
  57. ),
  58. };
  59. }
  60. state: State = {
  61. eventView: generatePerformanceEventView(
  62. this.props.organization,
  63. this.props.location,
  64. this.props.projects
  65. ),
  66. error: undefined,
  67. };
  68. componentDidMount() {
  69. const {api, organization, selection} = this.props;
  70. loadOrganizationTags(api, organization.slug, selection);
  71. addRoutePerformanceContext(selection);
  72. trackAnalyticsEvent({
  73. eventKey: 'performance_views.overview.view',
  74. eventName: 'Performance Views: Transaction overview view',
  75. organization_id: parseInt(organization.id, 10),
  76. show_onboarding: this.shouldShowOnboarding(),
  77. });
  78. }
  79. componentDidUpdate(prevProps: Props) {
  80. const {api, organization, selection} = this.props;
  81. if (
  82. !isEqual(prevProps.selection.projects, selection.projects) ||
  83. !isEqual(prevProps.selection.datetime, selection.datetime)
  84. ) {
  85. loadOrganizationTags(api, organization.slug, selection);
  86. addRoutePerformanceContext(selection);
  87. }
  88. }
  89. renderError() {
  90. const {error} = this.state;
  91. if (!error) {
  92. return null;
  93. }
  94. return (
  95. <Alert type="error" icon={<IconFlag size="md" />}>
  96. {error}
  97. </Alert>
  98. );
  99. }
  100. setError = (error: string | undefined) => {
  101. this.setState({error});
  102. };
  103. handleSearch = (searchQuery: string) => {
  104. const {location, organization} = this.props;
  105. trackAnalyticsEvent({
  106. eventKey: 'performance_views.overview.search',
  107. eventName: 'Performance Views: Transaction overview search',
  108. organization_id: parseInt(organization.id, 10),
  109. });
  110. browserHistory.push({
  111. pathname: location.pathname,
  112. query: {
  113. ...location.query,
  114. cursor: undefined,
  115. query: String(searchQuery).trim() || undefined,
  116. },
  117. });
  118. };
  119. handleTrendsClick = () => {
  120. const {location, organization} = this.props;
  121. const newQuery = {
  122. ...location.query,
  123. };
  124. const query = decodeScalar(location.query.query, '');
  125. const conditions = new MutableSearch(query);
  126. trackAnalyticsEvent({
  127. eventKey: 'performance_views.change_view',
  128. eventName: 'Performance Views: Change View',
  129. organization_id: parseInt(organization.id, 10),
  130. view_name: 'TRENDS',
  131. });
  132. const modifiedConditions = new MutableSearch([]);
  133. if (conditions.hasFilter('tpm()')) {
  134. modifiedConditions.setFilterValues('tpm()', conditions.getFilterValues('tpm()'));
  135. } else {
  136. modifiedConditions.setFilterValues('tpm()', ['>0.01']);
  137. }
  138. if (conditions.hasFilter('transaction.duration')) {
  139. modifiedConditions.setFilterValues(
  140. 'transaction.duration',
  141. conditions.getFilterValues('transaction.duration')
  142. );
  143. } else {
  144. modifiedConditions.setFilterValues('transaction.duration', [
  145. '>0',
  146. `<${DEFAULT_MAX_DURATION}`,
  147. ]);
  148. }
  149. newQuery.query = modifiedConditions.formatString();
  150. browserHistory.push({
  151. pathname: getPerformanceTrendsUrl(organization),
  152. query: {...newQuery},
  153. });
  154. };
  155. shouldShowOnboarding() {
  156. const {projects, demoMode} = this.props;
  157. const {eventView} = this.state;
  158. // XXX used by getsentry to bypass onboarding for the upsell demo state.
  159. if (demoMode) {
  160. return false;
  161. }
  162. if (projects.length === 0) {
  163. return false;
  164. }
  165. // Current selection is 'my projects' or 'all projects'
  166. if (eventView.project.length === 0 || eventView.project === [ALL_ACCESS_PROJECTS]) {
  167. return (
  168. projects.filter(p => p.firstTransactionEvent === false).length === projects.length
  169. );
  170. }
  171. // Any other subset of projects.
  172. return (
  173. projects.filter(
  174. p =>
  175. eventView.project.includes(parseInt(p.id, 10)) &&
  176. p.firstTransactionEvent === false
  177. ).length === eventView.project.length
  178. );
  179. }
  180. renderBody() {
  181. const {organization, projects, selection} = this.props;
  182. const eventView = this.state.eventView;
  183. const showOnboarding = this.shouldShowOnboarding();
  184. return (
  185. <PageContent>
  186. <NoProjectMessage organization={organization}>
  187. <PageHeader>
  188. <PageHeading>{t('Performance')}</PageHeading>
  189. {!showOnboarding && (
  190. <Button
  191. priority="primary"
  192. data-test-id="landing-header-trends"
  193. onClick={this.handleTrendsClick}
  194. >
  195. {t('View Trends')}
  196. </Button>
  197. )}
  198. </PageHeader>
  199. <GlobalSdkUpdateAlert />
  200. {this.renderError()}
  201. {showOnboarding ? (
  202. <Onboarding
  203. organization={organization}
  204. project={
  205. selection.projects.length > 0
  206. ? // If some projects selected, use the first selection
  207. projects.find(
  208. project => selection.projects[0].toString() === project.id
  209. ) || projects[0]
  210. : // Otherwise, use the first project in the org
  211. projects[0]
  212. }
  213. />
  214. ) : (
  215. <LandingContent
  216. eventView={eventView}
  217. projects={projects}
  218. organization={organization}
  219. setError={this.setError}
  220. handleSearch={this.handleSearch}
  221. />
  222. )}
  223. </NoProjectMessage>
  224. </PageContent>
  225. );
  226. }
  227. renderLandingV3() {
  228. return (
  229. <PerformanceLanding
  230. eventView={this.state.eventView}
  231. setError={this.setError}
  232. handleSearch={this.handleSearch}
  233. handleTrendsClick={this.handleTrendsClick}
  234. shouldShowOnboarding={this.shouldShowOnboarding()}
  235. {...this.props}
  236. />
  237. );
  238. }
  239. render() {
  240. const {organization} = this.props;
  241. return (
  242. <SentryDocumentTitle title={t('Performance')} orgSlug={organization.slug}>
  243. <PerformanceEventViewProvider value={{eventView: this.state.eventView}}>
  244. <GlobalSelectionHeader
  245. defaultSelection={{
  246. datetime: {
  247. start: null,
  248. end: null,
  249. utc: false,
  250. period: DEFAULT_STATS_PERIOD,
  251. },
  252. }}
  253. >
  254. <Feature features={['organizations:performance-landing-widgets']}>
  255. {({hasFeature}) =>
  256. hasFeature ? this.renderLandingV3() : this.renderBody()
  257. }
  258. </Feature>
  259. </GlobalSelectionHeader>
  260. </PerformanceEventViewProvider>
  261. </SentryDocumentTitle>
  262. );
  263. }
  264. }
  265. export default withApi(
  266. withOrganization(withProjects(withGlobalSelection(PerformanceContent)))
  267. );