pageLayout.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. import {useCallback, useState} from 'react';
  2. import {browserHistory} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import {isString} from '@sentry/utils';
  5. import {Location} from 'history';
  6. import {addErrorMessage} from 'sentry/actionCreators/indicator';
  7. import Feature from 'sentry/components/acl/feature';
  8. import {Alert} from 'sentry/components/alert';
  9. import {COL_WIDTH_UNDEFINED} from 'sentry/components/gridEditable';
  10. import * as Layout from 'sentry/components/layouts/thirds';
  11. import LoadingIndicator from 'sentry/components/loadingIndicator';
  12. import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container';
  13. import PickProjectToContinue from 'sentry/components/pickProjectToContinue';
  14. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  15. import {Tabs} from 'sentry/components/tabs';
  16. import {t} from 'sentry/locale';
  17. import {Organization, Project} from 'sentry/types';
  18. import {defined} from 'sentry/utils';
  19. import {trackAnalytics} from 'sentry/utils/analytics';
  20. import DiscoverQuery from 'sentry/utils/discover/discoverQuery';
  21. import EventView from 'sentry/utils/discover/eventView';
  22. import {useMetricsCardinalityContext} from 'sentry/utils/performance/contexts/metricsCardinality';
  23. import {PerformanceEventViewProvider} from 'sentry/utils/performance/contexts/performanceEventViewContext';
  24. import {decodeScalar} from 'sentry/utils/queryString';
  25. import useRouter from 'sentry/utils/useRouter';
  26. import {normalizeUrl} from 'sentry/utils/withDomainRequired';
  27. import {aggregateWaterfallRouteWithQuery} from 'sentry/views/performance/transactionSummary/aggregateSpanWaterfall/utils';
  28. import {getSelectedProjectPlatforms, getTransactionName} from '../utils';
  29. import {anomaliesRouteWithQuery} from './transactionAnomalies/utils';
  30. import {eventsRouteWithQuery} from './transactionEvents/utils';
  31. import {profilesRouteWithQuery} from './transactionProfiles/utils';
  32. import {replaysRouteWithQuery} from './transactionReplays/utils';
  33. import {spansRouteWithQuery} from './transactionSpans/utils';
  34. import {tagsRouteWithQuery} from './transactionTags/utils';
  35. import {vitalsRouteWithQuery} from './transactionVitals/utils';
  36. import TransactionHeader from './header';
  37. import Tab from './tabs';
  38. import {TransactionThresholdMetric} from './transactionThresholdModal';
  39. import {generateTransactionSummaryRoute, transactionSummaryRouteWithQuery} from './utils';
  40. type TabEvents =
  41. | 'performance_views.vitals.vitals_tab_clicked'
  42. | 'performance_views.tags.tags_tab_clicked'
  43. | 'performance_views.events.events_tab_clicked'
  44. | 'performance_views.spans.spans_tab_clicked'
  45. | 'performance_views.anomalies.anomalies_tab_clicked';
  46. const TAB_ANALYTICS: Partial<Record<Tab, TabEvents>> = {
  47. [Tab.WEB_VITALS]: 'performance_views.vitals.vitals_tab_clicked',
  48. [Tab.TAGS]: 'performance_views.tags.tags_tab_clicked',
  49. [Tab.EVENTS]: 'performance_views.events.events_tab_clicked',
  50. [Tab.SPANS]: 'performance_views.spans.spans_tab_clicked',
  51. [Tab.ANOMALIES]: 'performance_views.anomalies.anomalies_tab_clicked',
  52. };
  53. export type ChildProps = {
  54. eventView: EventView;
  55. location: Location;
  56. organization: Organization;
  57. projectId: string;
  58. projects: Project[];
  59. setError: React.Dispatch<React.SetStateAction<string | undefined>>;
  60. transactionName: string;
  61. // These are used to trigger a reload when the threshold/metric changes.
  62. transactionThreshold?: number;
  63. transactionThresholdMetric?: TransactionThresholdMetric;
  64. };
  65. type Props = {
  66. childComponent: (props: ChildProps) => JSX.Element;
  67. generateEventView: (props: {
  68. location: Location;
  69. organization: Organization;
  70. transactionName: string;
  71. }) => EventView;
  72. getDocumentTitle: (name: string) => string;
  73. location: Location;
  74. organization: Organization;
  75. projects: Project[];
  76. tab: Tab;
  77. features?: string[];
  78. };
  79. function PageLayout(props: Props) {
  80. const {
  81. location,
  82. organization,
  83. projects,
  84. tab,
  85. getDocumentTitle,
  86. generateEventView,
  87. childComponent: ChildComponent,
  88. features = [],
  89. } = props;
  90. let projectId: string | undefined;
  91. const filterProjects = location.query.project;
  92. if (isString(filterProjects) && filterProjects !== '-1') {
  93. projectId = filterProjects;
  94. }
  95. const router = useRouter();
  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.PROFILING: {
  126. return profilesRouteWithQuery(routeQuery);
  127. }
  128. case Tab.AGGREGATE_WATERFALL:
  129. return aggregateWaterfallRouteWithQuery(routeQuery);
  130. case Tab.WEB_VITALS:
  131. return vitalsRouteWithQuery({
  132. orgSlug: organization.slug,
  133. transaction: transactionName,
  134. projectID: decodeScalar(location.query.project),
  135. query: location.query,
  136. });
  137. case Tab.TRANSACTION_SUMMARY:
  138. default:
  139. return transactionSummaryRouteWithQuery(routeQuery);
  140. }
  141. },
  142. [location.query, organization.slug, projectId, transactionName]
  143. );
  144. const onTabChange = useCallback(
  145. (newTab: Tab) => {
  146. // Prevent infinite rerenders
  147. if (newTab === tab) {
  148. return;
  149. }
  150. const analyticsKey = TAB_ANALYTICS[newTab];
  151. if (analyticsKey) {
  152. trackAnalytics(analyticsKey, {
  153. organization,
  154. project_platforms: getSelectedProjectPlatforms(location, projects),
  155. });
  156. }
  157. browserHistory.push(normalizeUrl(getNewRoute(newTab)));
  158. },
  159. [getNewRoute, tab, organization, location, projects]
  160. );
  161. if (!defined(transactionName)) {
  162. redirectToPerformanceHomepage(organization, location);
  163. return null;
  164. }
  165. const eventView = generateEventView({location, transactionName, organization});
  166. if (!defined(projectId)) {
  167. // Using a discover query to get the projects associated
  168. // with a transaction name
  169. const nextView = eventView.clone();
  170. nextView.query = `transaction:"${transactionName}"`;
  171. nextView.fields = [
  172. {
  173. field: 'project',
  174. width: COL_WIDTH_UNDEFINED,
  175. },
  176. {
  177. field: 'count()',
  178. width: COL_WIDTH_UNDEFINED,
  179. },
  180. ];
  181. return (
  182. <DiscoverQuery
  183. eventView={nextView}
  184. location={location}
  185. orgSlug={organization.slug}
  186. queryExtras={{project: filterProjects ? filterProjects : undefined}}
  187. referrer="api.performance.transaction-summary"
  188. >
  189. {({isLoading, tableData, error: discoverQueryError}) => {
  190. if (discoverQueryError) {
  191. addErrorMessage(t('Unable to get projects associated with transaction'));
  192. redirectToPerformanceHomepage(organization, location);
  193. return null;
  194. }
  195. if (isLoading) {
  196. return <LoadingIndicator />;
  197. }
  198. const selectableProjects = tableData?.data
  199. .map(row => projects.find(project => project.slug === row.project))
  200. .filter((p): p is Project => p !== undefined);
  201. return (
  202. selectableProjects && (
  203. <PickProjectToContinue
  204. data-test-id="transaction-sumamry-project-picker-modal"
  205. projects={selectableProjects}
  206. router={router}
  207. nextPath={{
  208. pathname: generateTransactionSummaryRoute({orgSlug: organization.slug}),
  209. query: {
  210. project: projectId,
  211. transaction: transactionName,
  212. statsPeriod: eventView.statsPeriod,
  213. referrer: 'performance-transaction-summary',
  214. ...location.query,
  215. },
  216. }}
  217. noProjectRedirectPath="/performance/"
  218. allowAllProjectsSelection
  219. />
  220. )
  221. );
  222. }}
  223. </DiscoverQuery>
  224. );
  225. }
  226. const project = projects.find(p => p.id === projectId);
  227. return (
  228. <SentryDocumentTitle
  229. title={getDocumentTitle(transactionName)}
  230. orgSlug={organization.slug}
  231. projectSlug={project?.slug}
  232. >
  233. <Feature
  234. features={['performance-view', ...features]}
  235. organization={organization}
  236. renderDisabled={NoAccess}
  237. >
  238. <PerformanceEventViewProvider value={{eventView}}>
  239. <PageFiltersContainer
  240. shouldForceProject={defined(project)}
  241. forceProject={project}
  242. specificProjectSlugs={defined(project) ? [project.slug] : []}
  243. >
  244. <Tabs value={tab} onChange={onTabChange}>
  245. <Layout.Page>
  246. <TransactionHeader
  247. eventView={eventView}
  248. location={location}
  249. organization={organization}
  250. projects={projects}
  251. projectId={projectId}
  252. transactionName={transactionName}
  253. currentTab={tab}
  254. hasWebVitals={tab === Tab.WEB_VITALS ? 'yes' : 'maybe'}
  255. onChangeThreshold={(threshold, metric) => {
  256. setTransactionThreshold(threshold);
  257. setTransactionThresholdMetric(metric);
  258. }}
  259. metricsCardinality={metricsCardinality}
  260. />
  261. <Layout.Body>
  262. {defined(error) && (
  263. <StyledAlert type="error" showIcon>
  264. {error}
  265. </StyledAlert>
  266. )}
  267. <ChildComponent
  268. location={location}
  269. organization={organization}
  270. projects={projects}
  271. eventView={eventView}
  272. projectId={projectId}
  273. transactionName={transactionName}
  274. setError={setError}
  275. transactionThreshold={transactionThreshold}
  276. transactionThresholdMetric={transactionThresholdMetric}
  277. />
  278. </Layout.Body>
  279. </Layout.Page>
  280. </Tabs>
  281. </PageFiltersContainer>
  282. </PerformanceEventViewProvider>
  283. </Feature>
  284. </SentryDocumentTitle>
  285. );
  286. }
  287. export function NoAccess() {
  288. return <Alert type="warning">{t("You don't have access to this feature")}</Alert>;
  289. }
  290. const StyledAlert = styled(Alert)`
  291. grid-column: 1/3;
  292. margin: 0;
  293. `;
  294. export function redirectToPerformanceHomepage(
  295. organization: Organization,
  296. location: Location
  297. ) {
  298. // If there is no transaction name, redirect to the Performance landing page
  299. browserHistory.replace(
  300. normalizeUrl({
  301. pathname: `/organizations/${organization.slug}/performance/`,
  302. query: {
  303. ...location.query,
  304. },
  305. })
  306. );
  307. }
  308. export default PageLayout;