pageLayout.tsx 8.9 KB

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