content.tsx 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  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 Alert from 'app/components/alert';
  8. import Button from 'app/components/button';
  9. import GlobalSdkUpdateAlert from 'app/components/globalSdkUpdateAlert';
  10. import LightWeightNoProjectMessage from 'app/components/lightWeightNoProjectMessage';
  11. import GlobalSelectionHeader from 'app/components/organizations/globalSelectionHeader';
  12. import PageHeading from 'app/components/pageHeading';
  13. import SentryDocumentTitle from 'app/components/sentryDocumentTitle';
  14. import {ALL_ACCESS_PROJECTS} from 'app/constants/globalSelectionHeader';
  15. import {IconFlag} from 'app/icons';
  16. import {t} from 'app/locale';
  17. import {PageContent, PageHeader} from 'app/styles/organization';
  18. import {GlobalSelection, Organization, Project} from 'app/types';
  19. import {trackAnalyticsEvent} from 'app/utils/analytics';
  20. import EventView from 'app/utils/discover/eventView';
  21. import {decodeScalar} from 'app/utils/queryString';
  22. import {QueryResults, tokenizeSearch} from 'app/utils/tokenizeSearch';
  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_MAX_DURATION} from './trends/utils';
  29. import {DEFAULT_STATS_PERIOD, generatePerformanceEventView} from './data';
  30. import Onboarding from './onboarding';
  31. import {addRoutePerformanceContext, getPerformanceTrendsUrl} 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. });
  74. }
  75. componentDidUpdate(prevProps: Props) {
  76. const {api, organization, selection} = this.props;
  77. if (
  78. !isEqual(prevProps.selection.projects, selection.projects) ||
  79. !isEqual(prevProps.selection.datetime, selection.datetime)
  80. ) {
  81. loadOrganizationTags(api, organization.slug, selection);
  82. addRoutePerformanceContext(selection);
  83. }
  84. }
  85. renderError() {
  86. const {error} = this.state;
  87. if (!error) {
  88. return null;
  89. }
  90. return (
  91. <Alert type="error" icon={<IconFlag size="md" />}>
  92. {error}
  93. </Alert>
  94. );
  95. }
  96. setError = (error: string | undefined) => {
  97. this.setState({error});
  98. };
  99. handleSearch = (searchQuery: string) => {
  100. const {location, organization} = this.props;
  101. trackAnalyticsEvent({
  102. eventKey: 'performance_views.overview.search',
  103. eventName: 'Performance Views: Transaction overview search',
  104. organization_id: parseInt(organization.id, 10),
  105. });
  106. browserHistory.push({
  107. pathname: location.pathname,
  108. query: {
  109. ...location.query,
  110. cursor: undefined,
  111. query: String(searchQuery).trim() || undefined,
  112. },
  113. });
  114. };
  115. handleTrendsClick() {
  116. const {location, organization} = this.props;
  117. const newQuery = {
  118. ...location.query,
  119. };
  120. const query = decodeScalar(location.query.query, '');
  121. const conditions = tokenizeSearch(query);
  122. trackAnalyticsEvent({
  123. eventKey: 'performance_views.change_view',
  124. eventName: 'Performance Views: Change View',
  125. organization_id: parseInt(organization.id, 10),
  126. view_name: 'TRENDS',
  127. });
  128. const modifiedConditions = new QueryResults([]);
  129. if (conditions.hasTag('tpm()')) {
  130. modifiedConditions.setTagValues('tpm()', conditions.getTagValues('tpm()'));
  131. } else {
  132. modifiedConditions.setTagValues('tpm()', ['>0.01']);
  133. }
  134. if (conditions.hasTag('transaction.duration')) {
  135. modifiedConditions.setTagValues(
  136. 'transaction.duration',
  137. conditions.getTagValues('transaction.duration')
  138. );
  139. } else {
  140. modifiedConditions.setTagValues('transaction.duration', [
  141. '>0',
  142. `<${DEFAULT_MAX_DURATION}`,
  143. ]);
  144. }
  145. newQuery.query = modifiedConditions.formatString();
  146. browserHistory.push({
  147. pathname: getPerformanceTrendsUrl(organization),
  148. query: {...newQuery},
  149. });
  150. }
  151. shouldShowOnboarding() {
  152. const {projects, demoMode} = this.props;
  153. const {eventView} = this.state;
  154. // XXX used by getsentry to bypass onboarding for the upsell demo state.
  155. if (demoMode) {
  156. return false;
  157. }
  158. if (projects.length === 0) {
  159. return false;
  160. }
  161. // Current selection is 'my projects' or 'all projects'
  162. if (eventView.project.length === 0 || eventView.project === [ALL_ACCESS_PROJECTS]) {
  163. return (
  164. projects.filter(p => p.firstTransactionEvent === false).length === projects.length
  165. );
  166. }
  167. // Any other subset of projects.
  168. return (
  169. projects.filter(
  170. p =>
  171. eventView.project.includes(parseInt(p.id, 10)) &&
  172. p.firstTransactionEvent === false
  173. ).length === eventView.project.length
  174. );
  175. }
  176. renderBody() {
  177. const {organization, projects} = this.props;
  178. const eventView = this.state.eventView;
  179. const showOnboarding = this.shouldShowOnboarding();
  180. return (
  181. <PageContent>
  182. <LightWeightNoProjectMessage organization={organization}>
  183. <PageHeader>
  184. <PageHeading>{t('Performance')}</PageHeading>
  185. {!showOnboarding && (
  186. <Button
  187. priority="primary"
  188. data-test-id="landing-header-trends"
  189. onClick={() => this.handleTrendsClick()}
  190. >
  191. {t('View Trends')}
  192. </Button>
  193. )}
  194. </PageHeader>
  195. <GlobalSdkUpdateAlert />
  196. {this.renderError()}
  197. {showOnboarding ? (
  198. <Onboarding organization={organization} />
  199. ) : (
  200. <LandingContent
  201. eventView={eventView}
  202. projects={projects}
  203. organization={organization}
  204. setError={this.setError}
  205. handleSearch={this.handleSearch}
  206. />
  207. )}
  208. </LightWeightNoProjectMessage>
  209. </PageContent>
  210. );
  211. }
  212. render() {
  213. const {organization} = this.props;
  214. return (
  215. <SentryDocumentTitle title={t('Performance')} orgSlug={organization.slug}>
  216. <GlobalSelectionHeader
  217. defaultSelection={{
  218. datetime: {
  219. start: null,
  220. end: null,
  221. utc: false,
  222. period: DEFAULT_STATS_PERIOD,
  223. },
  224. }}
  225. >
  226. {this.renderBody()}
  227. </GlobalSelectionHeader>
  228. </SentryDocumentTitle>
  229. );
  230. }
  231. }
  232. export default withApi(
  233. withOrganization(withProjects(withGlobalSelection(PerformanceContent)))
  234. );