pageLayout.tsx 12 KB

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