pageLayout.tsx 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. import {useCallback, useState} from 'react';
  2. import {browserHistory} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import {Location} from 'history';
  5. import Feature from 'sentry/components/acl/feature';
  6. import {Alert} from 'sentry/components/alert';
  7. import * as Layout from 'sentry/components/layouts/thirds';
  8. import NoProjectMessage from 'sentry/components/noProjectMessage';
  9. import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container';
  10. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  11. import {Tabs} from 'sentry/components/tabs';
  12. import {t} from 'sentry/locale';
  13. import {Organization, Project} from 'sentry/types';
  14. import {defined} from 'sentry/utils';
  15. import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  16. import EventView from 'sentry/utils/discover/eventView';
  17. import {useMetricsCardinalityContext} from 'sentry/utils/performance/contexts/metricsCardinality';
  18. import {PerformanceEventViewProvider} from 'sentry/utils/performance/contexts/performanceEventViewContext';
  19. import {decodeScalar} from 'sentry/utils/queryString';
  20. import {normalizeUrl} from 'sentry/utils/withDomainRequired';
  21. import {getSelectedProjectPlatforms, getTransactionName} from '../utils';
  22. import {anomaliesRouteWithQuery} from './transactionAnomalies/utils';
  23. import {eventsRouteWithQuery} from './transactionEvents/utils';
  24. import {profilesRouteWithQuery} from './transactionProfiles/utils';
  25. import {replaysRouteWithQuery} from './transactionReplays/utils';
  26. import {spansRouteWithQuery} from './transactionSpans/utils';
  27. import {tagsRouteWithQuery} from './transactionTags/utils';
  28. import {vitalsRouteWithQuery} from './transactionVitals/utils';
  29. import TransactionHeader from './header';
  30. import Tab from './tabs';
  31. import {TransactionThresholdMetric} from './transactionThresholdModal';
  32. import {transactionSummaryRouteWithQuery} from './utils';
  33. type TabEvents =
  34. | 'performance_views.vitals.vitals_tab_clicked'
  35. | 'performance_views.tags.tags_tab_clicked'
  36. | 'performance_views.events.events_tab_clicked'
  37. | 'performance_views.spans.spans_tab_clicked'
  38. | 'performance_views.anomalies.anomalies_tab_clicked';
  39. const TAB_ANALYTICS: Partial<Record<Tab, TabEvents>> = {
  40. [Tab.WebVitals]: 'performance_views.vitals.vitals_tab_clicked',
  41. [Tab.Tags]: 'performance_views.tags.tags_tab_clicked',
  42. [Tab.Events]: 'performance_views.events.events_tab_clicked',
  43. [Tab.Spans]: 'performance_views.spans.spans_tab_clicked',
  44. [Tab.Anomalies]: 'performance_views.anomalies.anomalies_tab_clicked',
  45. };
  46. export type ChildProps = {
  47. eventView: EventView;
  48. location: Location;
  49. organization: Organization;
  50. projectId: string;
  51. projects: Project[];
  52. setError: React.Dispatch<React.SetStateAction<string | undefined>>;
  53. transactionName: string;
  54. // These are used to trigger a reload when the threshold/metric changes.
  55. transactionThreshold?: number;
  56. transactionThresholdMetric?: TransactionThresholdMetric;
  57. };
  58. type Props = {
  59. childComponent: (props: ChildProps) => JSX.Element;
  60. generateEventView: (props: {
  61. location: Location;
  62. organization: Organization;
  63. transactionName: string;
  64. }) => EventView;
  65. getDocumentTitle: (name: string) => string;
  66. location: Location;
  67. organization: Organization;
  68. projects: Project[];
  69. tab: Tab;
  70. features?: string[];
  71. };
  72. function PageLayout(props: Props) {
  73. const {
  74. location,
  75. organization,
  76. projects,
  77. tab,
  78. getDocumentTitle,
  79. generateEventView,
  80. childComponent: ChildComponent,
  81. features = [],
  82. } = props;
  83. const projectId = decodeScalar(location.query.project);
  84. const transactionName = getTransactionName(location);
  85. const [error, setError] = useState<string | undefined>();
  86. const metricsCardinality = useMetricsCardinalityContext();
  87. const [transactionThreshold, setTransactionThreshold] = useState<number | undefined>();
  88. const [transactionThresholdMetric, setTransactionThresholdMetric] = useState<
  89. TransactionThresholdMetric | undefined
  90. >();
  91. const getNewRoute = useCallback(
  92. (newTab: Tab) => {
  93. if (!transactionName) {
  94. return {};
  95. }
  96. const routeQuery = {
  97. orgSlug: organization.slug,
  98. transaction: transactionName,
  99. projectID: projectId,
  100. query: location.query,
  101. };
  102. switch (newTab) {
  103. case Tab.Tags:
  104. return tagsRouteWithQuery(routeQuery);
  105. case Tab.Events:
  106. return eventsRouteWithQuery(routeQuery);
  107. case Tab.Spans:
  108. return spansRouteWithQuery(routeQuery);
  109. case Tab.Anomalies:
  110. return anomaliesRouteWithQuery(routeQuery);
  111. case Tab.Replays:
  112. return replaysRouteWithQuery(routeQuery);
  113. case Tab.Profiling: {
  114. return profilesRouteWithQuery(routeQuery);
  115. }
  116. case Tab.WebVitals:
  117. return vitalsRouteWithQuery({
  118. orgSlug: organization.slug,
  119. transaction: transactionName,
  120. projectID: decodeScalar(location.query.project),
  121. query: location.query,
  122. });
  123. case Tab.TransactionSummary:
  124. default:
  125. return transactionSummaryRouteWithQuery(routeQuery);
  126. }
  127. },
  128. [location.query, organization.slug, projectId, transactionName]
  129. );
  130. const onTabChange = useCallback(
  131. (newTab: Tab) => {
  132. // Prevent infinite rerenders
  133. if (newTab === tab) {
  134. return;
  135. }
  136. const analyticsKey = TAB_ANALYTICS[newTab];
  137. if (analyticsKey) {
  138. trackAdvancedAnalyticsEvent(analyticsKey, {
  139. organization,
  140. project_platforms: getSelectedProjectPlatforms(location, projects),
  141. });
  142. }
  143. browserHistory.push(normalizeUrl(getNewRoute(newTab)));
  144. },
  145. [getNewRoute, tab, organization, location, projects]
  146. );
  147. if (!defined(projectId) || !defined(transactionName)) {
  148. redirectToPerformanceHomepage(organization, location);
  149. return null;
  150. }
  151. const project = projects.find(p => p.id === projectId);
  152. const eventView = generateEventView({location, transactionName, organization});
  153. return (
  154. <SentryDocumentTitle
  155. title={getDocumentTitle(transactionName)}
  156. orgSlug={organization.slug}
  157. projectSlug={project?.slug}
  158. >
  159. <Feature
  160. features={['performance-view', ...features]}
  161. organization={organization}
  162. renderDisabled={NoAccess}
  163. >
  164. <PerformanceEventViewProvider value={{eventView}}>
  165. <PageFiltersContainer
  166. shouldForceProject={defined(project)}
  167. forceProject={project}
  168. specificProjectSlugs={defined(project) ? [project.slug] : []}
  169. >
  170. <Tabs value={tab} onChange={onTabChange}>
  171. <Layout.Page>
  172. <NoProjectMessage organization={organization}>
  173. <TransactionHeader
  174. eventView={eventView}
  175. location={location}
  176. organization={organization}
  177. projects={projects}
  178. projectId={projectId}
  179. transactionName={transactionName}
  180. currentTab={tab}
  181. hasWebVitals={tab === Tab.WebVitals ? 'yes' : 'maybe'}
  182. onChangeThreshold={(threshold, metric) => {
  183. setTransactionThreshold(threshold);
  184. setTransactionThresholdMetric(metric);
  185. }}
  186. metricsCardinality={metricsCardinality}
  187. />
  188. <Layout.Body>
  189. {defined(error) && (
  190. <StyledAlert type="error" showIcon>
  191. {error}
  192. </StyledAlert>
  193. )}
  194. <ChildComponent
  195. location={location}
  196. organization={organization}
  197. projects={projects}
  198. eventView={eventView}
  199. projectId={projectId}
  200. transactionName={transactionName}
  201. setError={setError}
  202. transactionThreshold={transactionThreshold}
  203. transactionThresholdMetric={transactionThresholdMetric}
  204. />
  205. </Layout.Body>
  206. </NoProjectMessage>
  207. </Layout.Page>
  208. </Tabs>
  209. </PageFiltersContainer>
  210. </PerformanceEventViewProvider>
  211. </Feature>
  212. </SentryDocumentTitle>
  213. );
  214. }
  215. export function NoAccess() {
  216. return <Alert type="warning">{t("You don't have access to this feature")}</Alert>;
  217. }
  218. const StyledAlert = styled(Alert)`
  219. grid-column: 1/3;
  220. margin: 0;
  221. `;
  222. export function redirectToPerformanceHomepage(
  223. organization: Organization,
  224. location: Location
  225. ) {
  226. // If there is no transaction name, redirect to the Performance landing page
  227. browserHistory.replace(
  228. normalizeUrl({
  229. pathname: `/organizations/${organization.slug}/performance/`,
  230. query: {
  231. ...location.query,
  232. },
  233. })
  234. );
  235. }
  236. export default PageLayout;