pageLayout.tsx 11 KB

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