pageLayout.tsx 8.9 KB

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