content.tsx 8.8 KB

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