pageLayout.tsx 8.6 KB

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