content.tsx 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735
  1. import {Fragment, useCallback, useEffect, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import type {Location} from 'history';
  4. import {Alert} from 'sentry/components/alert';
  5. import {Button, LinkButton} from 'sentry/components/button';
  6. import SearchBar from 'sentry/components/events/searchBar';
  7. import FeedbackWidgetButton from 'sentry/components/feedback/widget/feedbackWidgetButton';
  8. import * as Layout from 'sentry/components/layouts/thirds';
  9. import {DatePageFilter} from 'sentry/components/organizations/datePageFilter';
  10. import {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter';
  11. import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
  12. import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container';
  13. import {ProjectPageFilter} from 'sentry/components/organizations/projectPageFilter';
  14. import {PageHeadingQuestionTooltip} from 'sentry/components/pageHeadingQuestionTooltip';
  15. import Pagination from 'sentry/components/pagination';
  16. import {TransactionSearchQueryBuilder} from 'sentry/components/performance/transactionSearchQueryBuilder';
  17. import {
  18. ProfilingAM1OrMMXUpgrade,
  19. ProfilingBetaAlertBanner,
  20. ProfilingUpgradeButton,
  21. } from 'sentry/components/profiling/billing/alerts';
  22. import {ProfileEventsTable} from 'sentry/components/profiling/profileEventsTable';
  23. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  24. import {SidebarPanelKey} from 'sentry/components/sidebar/types';
  25. import type {SmartSearchBarProps} from 'sentry/components/smartSearchBar';
  26. import {TabList, Tabs} from 'sentry/components/tabs';
  27. import {MAX_QUERY_LENGTH} from 'sentry/constants';
  28. import {ALL_ACCESS_PROJECTS} from 'sentry/constants/pageFilters';
  29. import {t} from 'sentry/locale';
  30. import SidebarPanelStore from 'sentry/stores/sidebarPanelStore';
  31. import {space} from 'sentry/styles/space';
  32. import {trackAnalytics} from 'sentry/utils/analytics';
  33. import {browserHistory} from 'sentry/utils/browserHistory';
  34. import {useProfileEvents} from 'sentry/utils/profiling/hooks/useProfileEvents';
  35. import {formatError, formatSort} from 'sentry/utils/profiling/hooks/utils';
  36. import {decodeScalar} from 'sentry/utils/queryString';
  37. import {useLocation} from 'sentry/utils/useLocation';
  38. import useOrganization from 'sentry/utils/useOrganization';
  39. import usePageFilters from 'sentry/utils/usePageFilters';
  40. import useProjects from 'sentry/utils/useProjects';
  41. import {
  42. TableHeader,
  43. TableHeaderActions,
  44. TableHeaderTitle,
  45. } from 'sentry/views/explore/components/table';
  46. import {LandingAggregateFlamegraph} from 'sentry/views/profiling/landingAggregateFlamegraph';
  47. import {DEFAULT_PROFILING_DATETIME_SELECTION} from 'sentry/views/profiling/utils';
  48. import {LandingWidgetSelector} from './landing/landingWidgetSelector';
  49. import {ProfilesChart} from './landing/profileCharts';
  50. import {ProfilesChartWidget} from './landing/profilesChartWidget';
  51. import {ProfilingSlowestTransactionsPanel} from './landing/profilingSlowestTransactionsPanel';
  52. import {ProfilingOnboardingPanel} from './profilingOnboardingPanel';
  53. const LEFT_WIDGET_CURSOR = 'leftCursor';
  54. const RIGHT_WIDGET_CURSOR = 'rightCursor';
  55. const CURSOR_PARAMS = [LEFT_WIDGET_CURSOR, RIGHT_WIDGET_CURSOR];
  56. interface ProfilingContentProps {
  57. location: Location;
  58. }
  59. function ProfilingContentLegacy({location}: ProfilingContentProps) {
  60. const organization = useOrganization();
  61. const {selection} = usePageFilters();
  62. const cursor = decodeScalar(location.query.cursor);
  63. const query = decodeScalar(location.query.query, '');
  64. const fields = ALL_FIELDS;
  65. const sort = formatSort<FieldType>(decodeScalar(location.query.sort), fields, {
  66. key: 'count()',
  67. order: 'desc',
  68. });
  69. const {projects} = useProjects();
  70. const transactions = useProfileEvents<FieldType>({
  71. cursor,
  72. fields,
  73. query,
  74. sort,
  75. referrer: 'api.profiling.landing-table',
  76. });
  77. const transactionsError =
  78. transactions.status === 'error' ? formatError(transactions.error) : null;
  79. useEffect(() => {
  80. trackAnalytics('profiling_views.landing', {
  81. organization,
  82. });
  83. }, [organization]);
  84. const handleSearch: SmartSearchBarProps['onSearch'] = useCallback(
  85. (searchQuery: string) => {
  86. browserHistory.push({
  87. ...location,
  88. query: {
  89. ...location.query,
  90. cursor: undefined,
  91. query: searchQuery || undefined,
  92. },
  93. });
  94. },
  95. [location]
  96. );
  97. // Open the modal on demand
  98. const onSetupProfilingClick = useCallback(() => {
  99. trackAnalytics('profiling_views.onboarding', {
  100. organization,
  101. });
  102. SidebarPanelStore.activatePanel(SidebarPanelKey.PROFILING_ONBOARDING);
  103. }, [organization]);
  104. const shouldShowProfilingOnboardingPanel = useMemo((): boolean => {
  105. // if it's My Projects or All projects, only show onboarding if we can't
  106. // find any projects with profiles
  107. if (
  108. selection.projects.length === 0 ||
  109. selection.projects[0] === ALL_ACCESS_PROJECTS
  110. ) {
  111. return projects.every(project => !project.hasProfiles);
  112. }
  113. // otherwise, only show onboarding if we can't find any projects with profiles
  114. // from those that were selected
  115. const projectsWithProfiles = new Set(
  116. projects.filter(project => project.hasProfiles).map(project => project.id)
  117. );
  118. return selection.projects.every(
  119. project => !projectsWithProfiles.has(String(project))
  120. );
  121. }, [selection.projects, projects]);
  122. return (
  123. <SentryDocumentTitle title={t('Profiling')} orgSlug={organization.slug}>
  124. <PageFiltersContainer
  125. defaultSelection={{datetime: DEFAULT_PROFILING_DATETIME_SELECTION}}
  126. >
  127. <Layout.Page>
  128. <ProfilingBetaAlertBanner organization={organization} />
  129. <Layout.Header>
  130. <StyledHeaderContent>
  131. <Layout.Title>
  132. {t('Profiling')}
  133. <PageHeadingQuestionTooltip
  134. docsUrl="https://docs.sentry.io/product/profiling/"
  135. title={t(
  136. 'Profiling collects detailed information in production about the functions executing in your application and how long they take to run, giving you code-level visibility into your hot paths.'
  137. )}
  138. />
  139. </Layout.Title>
  140. <FeedbackWidgetButton />
  141. </StyledHeaderContent>
  142. </Layout.Header>
  143. <Layout.Body>
  144. <Layout.Main fullWidth>
  145. {transactionsError && (
  146. <Alert type="error" showIcon>
  147. {transactionsError}
  148. </Alert>
  149. )}
  150. <ActionBar>
  151. <PageFilterBar condensed>
  152. <ProjectPageFilter resetParamsOnChange={CURSOR_PARAMS} />
  153. <EnvironmentPageFilter resetParamsOnChange={CURSOR_PARAMS} />
  154. <DatePageFilter resetParamsOnChange={CURSOR_PARAMS} />
  155. </PageFilterBar>
  156. {organization.features.includes('search-query-builder-performance') ? (
  157. <TransactionSearchQueryBuilder
  158. projects={selection.projects}
  159. initialQuery={query}
  160. onSearch={handleSearch}
  161. searchSource="profile_landing"
  162. />
  163. ) : (
  164. <SearchBar
  165. searchSource="profile_landing"
  166. organization={organization}
  167. projectIds={selection.projects}
  168. query={query}
  169. onSearch={handleSearch}
  170. maxQueryLength={MAX_QUERY_LENGTH}
  171. />
  172. )}
  173. </ActionBar>
  174. {shouldShowProfilingOnboardingPanel ? (
  175. <Fragment>
  176. <ProfilingOnboardingPanel
  177. content={
  178. // If user is on m2, show default
  179. <ProfilingAM1OrMMXUpgrade
  180. organization={organization}
  181. fallback={
  182. <Fragment>
  183. <h3>{t('Function level insights')}</h3>
  184. <p>
  185. {t(
  186. 'Discover slow-to-execute or resource intensive functions within your application'
  187. )}
  188. </p>
  189. </Fragment>
  190. }
  191. />
  192. }
  193. >
  194. <ProfilingUpgradeButton
  195. data-test-id="profiling-upgrade"
  196. organization={organization}
  197. priority="primary"
  198. onClick={onSetupProfilingClick}
  199. fallback={
  200. <Button onClick={onSetupProfilingClick} priority="primary">
  201. {t('Set Up Profiling')}
  202. </Button>
  203. }
  204. >
  205. {t('Set Up Profiling')}
  206. </ProfilingUpgradeButton>
  207. <LinkButton href="https://docs.sentry.io/product/profiling/" external>
  208. {t('Read Docs')}
  209. </LinkButton>
  210. </ProfilingOnboardingPanel>
  211. </Fragment>
  212. ) : (
  213. <Fragment>
  214. {organization.features.includes(
  215. 'profiling-global-suspect-functions'
  216. ) ? (
  217. <Fragment>
  218. <ProfilesChartWidget
  219. chartHeight={150}
  220. referrer="api.profiling.landing-chart"
  221. userQuery={query}
  222. selection={selection}
  223. />
  224. <WidgetsContainer>
  225. <LandingWidgetSelector
  226. cursorName={LEFT_WIDGET_CURSOR}
  227. widgetHeight="340px"
  228. defaultWidget="slowest functions"
  229. query={query}
  230. storageKey="profiling-landing-widget-0"
  231. />
  232. <LandingWidgetSelector
  233. cursorName={RIGHT_WIDGET_CURSOR}
  234. widgetHeight="340px"
  235. defaultWidget="regressed functions"
  236. query={query}
  237. storageKey="profiling-landing-widget-1"
  238. />
  239. </WidgetsContainer>
  240. </Fragment>
  241. ) : (
  242. <PanelsGrid>
  243. <ProfilingSlowestTransactionsPanel />
  244. <ProfilesChart
  245. referrer="api.profiling.landing-chart"
  246. query={query}
  247. selection={selection}
  248. hideCount
  249. />
  250. </PanelsGrid>
  251. )}
  252. <ProfileEventsTable
  253. columns={fields.slice()}
  254. data={transactions.status === 'success' ? transactions.data : null}
  255. error={
  256. transactions.status === 'error'
  257. ? t('Unable to load profiles')
  258. : null
  259. }
  260. isLoading={transactions.status === 'pending'}
  261. sort={sort}
  262. sortableColumns={new Set(fields)}
  263. />
  264. <Pagination
  265. pageLinks={
  266. transactions.status === 'success'
  267. ? transactions.getResponseHeader?.('Link') ?? null
  268. : null
  269. }
  270. />
  271. </Fragment>
  272. )}
  273. </Layout.Main>
  274. </Layout.Body>
  275. </Layout.Page>
  276. </PageFiltersContainer>
  277. </SentryDocumentTitle>
  278. );
  279. }
  280. function validateTab(tab: unknown): tab is 'flamegraph' | 'transactions' {
  281. return tab === 'flamegraph' || tab === 'transactions';
  282. }
  283. function decodeTab(tab: unknown): 'flamegraph' | 'transactions' {
  284. // Fallback to transactions if tab is invalid. We default to transactions
  285. // because that is going to be the most common perf setup when we release.
  286. return validateTab(tab) ? tab : 'transactions';
  287. }
  288. function ProfilingContent({location}: ProfilingContentProps) {
  289. const organization = useOrganization();
  290. const {selection} = usePageFilters();
  291. const {projects} = useProjects();
  292. const tab = decodeTab(location.query.tab);
  293. useEffect(() => {
  294. trackAnalytics('profiling_views.landing', {
  295. organization,
  296. });
  297. }, [organization]);
  298. const onTabChange = useCallback(
  299. (newTab: 'flamegraph' | 'transactions') => {
  300. browserHistory.push({
  301. ...location,
  302. query: {
  303. ...location.query,
  304. tab: newTab,
  305. },
  306. });
  307. },
  308. [location]
  309. );
  310. const shouldShowProfilingOnboardingPanel = useMemo((): boolean => {
  311. // if it's My Projects or All projects, only show onboarding if we can't
  312. // find any projects with profiles
  313. if (
  314. selection.projects.length === 0 ||
  315. selection.projects[0] === ALL_ACCESS_PROJECTS
  316. ) {
  317. return projects.every(project => !project.hasProfiles);
  318. }
  319. // otherwise, only show onboarding if we can't find any projects with profiles
  320. // from those that were selected
  321. const projectsWithProfiles = new Set(
  322. projects.filter(project => project.hasProfiles).map(project => project.id)
  323. );
  324. return selection.projects.every(
  325. project => !projectsWithProfiles.has(String(project))
  326. );
  327. }, [selection.projects, projects]);
  328. return (
  329. <SentryDocumentTitle title={t('Profiling')} orgSlug={organization.slug}>
  330. <PageFiltersContainer
  331. defaultSelection={{datetime: DEFAULT_PROFILING_DATETIME_SELECTION}}
  332. >
  333. <Layout.Page>
  334. <ProfilingBetaAlertBanner organization={organization} />
  335. <ProfilingContentPageHeader tab={tab} onTabChange={onTabChange} />
  336. {tab === 'flamegraph' ? (
  337. <FlamegraphBody>
  338. <ProfilingFlamegraphTabContent
  339. tab={tab}
  340. shouldShowProfilingOnboardingPanel={shouldShowProfilingOnboardingPanel}
  341. />
  342. </FlamegraphBody>
  343. ) : tab === 'transactions' ? (
  344. <Layout.Body>
  345. <ProfilingTransactionsContent
  346. tab={tab}
  347. shouldShowProfilingOnboardingPanel={shouldShowProfilingOnboardingPanel}
  348. />
  349. </Layout.Body>
  350. ) : null}
  351. </Layout.Page>
  352. </PageFiltersContainer>
  353. </SentryDocumentTitle>
  354. );
  355. }
  356. interface ProfilingTabContentProps {
  357. shouldShowProfilingOnboardingPanel: boolean;
  358. tab: 'flamegraph' | 'transactions';
  359. }
  360. function ProfilingFlamegraphTabContent(props: ProfilingTabContentProps) {
  361. return (
  362. <FlamegraphMainLayout>
  363. <FlamegraphActionBar>
  364. <PageFilterBar condensed>
  365. <ProjectPageFilter resetParamsOnChange={CURSOR_PARAMS} />
  366. <EnvironmentPageFilter resetParamsOnChange={CURSOR_PARAMS} />
  367. <DatePageFilter resetParamsOnChange={CURSOR_PARAMS} />
  368. </PageFilterBar>
  369. </FlamegraphActionBar>
  370. <FlamegraphLayout>
  371. {props.shouldShowProfilingOnboardingPanel ? (
  372. <ProfilingOnboardingCTA />
  373. ) : (
  374. <LandingAggregateFlamegraphContainer>
  375. <LandingAggregateFlamegraph />
  376. </LandingAggregateFlamegraphContainer>
  377. )}
  378. <FlamegraphSidebar />
  379. </FlamegraphLayout>
  380. </FlamegraphMainLayout>
  381. );
  382. }
  383. function ProfilingTransactionsContent(props: ProfilingTabContentProps) {
  384. const organization = useOrganization();
  385. const location = useLocation();
  386. const {selection} = usePageFilters();
  387. const fields = ALL_FIELDS;
  388. const sort = formatSort<FieldType>(decodeScalar(location.query.sort), fields, {
  389. key: 'count()',
  390. order: 'desc',
  391. });
  392. const cursor = decodeScalar(location.query.cursor);
  393. const query = decodeScalar(location.query.query, '');
  394. const transactions = useProfileEvents<FieldType>({
  395. cursor,
  396. fields,
  397. query,
  398. sort,
  399. referrer: 'api.profiling.landing-table',
  400. continuousProfilingCompat: true,
  401. });
  402. const transactionsError =
  403. transactions.status === 'error' ? formatError(transactions.error) : null;
  404. const handleSearch: SmartSearchBarProps['onSearch'] = useCallback(
  405. (searchQuery: string) => {
  406. browserHistory.push({
  407. ...location,
  408. query: {
  409. ...location.query,
  410. cursor: undefined,
  411. query: searchQuery || undefined,
  412. },
  413. });
  414. },
  415. [location]
  416. );
  417. return (
  418. <Layout.Main fullWidth>
  419. {transactionsError && (
  420. <Alert type="error" showIcon>
  421. {transactionsError}
  422. </Alert>
  423. )}
  424. <ActionBar>
  425. <PageFilterBar condensed>
  426. <ProjectPageFilter resetParamsOnChange={CURSOR_PARAMS} />
  427. <EnvironmentPageFilter resetParamsOnChange={CURSOR_PARAMS} />
  428. <DatePageFilter resetParamsOnChange={CURSOR_PARAMS} />
  429. </PageFilterBar>
  430. {organization.features.includes('search-query-builder-performance') ? (
  431. <TransactionSearchQueryBuilder
  432. projects={selection.projects}
  433. initialQuery={query}
  434. onSearch={handleSearch}
  435. searchSource="profile_landing"
  436. />
  437. ) : (
  438. <SearchBar
  439. searchSource="profile_landing"
  440. organization={organization}
  441. projectIds={selection.projects}
  442. query={query}
  443. onSearch={handleSearch}
  444. maxQueryLength={MAX_QUERY_LENGTH}
  445. />
  446. )}
  447. </ActionBar>
  448. {props.shouldShowProfilingOnboardingPanel ? (
  449. <ProfilingOnboardingCTA />
  450. ) : (
  451. <Fragment>
  452. {organization.features.includes('profiling-global-suspect-functions') ? (
  453. <Fragment>
  454. <ProfilesChartWidget
  455. chartHeight={150}
  456. referrer="api.profiling.landing-chart"
  457. userQuery={query}
  458. selection={selection}
  459. continuousProfilingCompat
  460. />
  461. <WidgetsContainer>
  462. <LandingWidgetSelector
  463. cursorName={LEFT_WIDGET_CURSOR}
  464. widgetHeight="340px"
  465. defaultWidget="slowest functions"
  466. query={query}
  467. storageKey="profiling-landing-widget-0"
  468. />
  469. <LandingWidgetSelector
  470. cursorName={RIGHT_WIDGET_CURSOR}
  471. widgetHeight="340px"
  472. defaultWidget="regressed functions"
  473. query={query}
  474. storageKey="profiling-landing-widget-1"
  475. />
  476. </WidgetsContainer>
  477. </Fragment>
  478. ) : (
  479. <PanelsGrid>
  480. <ProfilingSlowestTransactionsPanel />
  481. <ProfilesChart
  482. referrer="api.profiling.landing-chart"
  483. query={query}
  484. selection={selection}
  485. hideCount
  486. />
  487. </PanelsGrid>
  488. )}
  489. <Fragment>
  490. <TableHeader>
  491. <TableHeaderTitle>{t('Transactions')}</TableHeaderTitle>
  492. <TableHeaderActions>
  493. <StyledPagination
  494. pageLinks={
  495. transactions.status === 'success'
  496. ? transactions.getResponseHeader?.('Link') ?? null
  497. : null
  498. }
  499. />
  500. </TableHeaderActions>
  501. </TableHeader>
  502. <ProfileEventsTable
  503. columns={fields.slice()}
  504. data={transactions.status === 'success' ? transactions.data : null}
  505. error={
  506. transactions.status === 'error' ? t('Unable to load profiles') : null
  507. }
  508. isLoading={transactions.status === 'pending'}
  509. sort={sort}
  510. sortableColumns={new Set(fields)}
  511. />
  512. </Fragment>
  513. </Fragment>
  514. )}
  515. </Layout.Main>
  516. );
  517. }
  518. function ProfilingOnboardingCTA() {
  519. const organization = useOrganization();
  520. // Open the modal on demand
  521. const onSetupProfilingClick = useCallback(() => {
  522. trackAnalytics('profiling_views.onboarding', {
  523. organization,
  524. });
  525. SidebarPanelStore.activatePanel(SidebarPanelKey.PROFILING_ONBOARDING);
  526. }, [organization]);
  527. return (
  528. <Fragment>
  529. <ProfilingOnboardingPanel
  530. content={
  531. // If user is on m2, show default
  532. <ProfilingAM1OrMMXUpgrade
  533. organization={organization}
  534. fallback={
  535. <Fragment>
  536. <h3>{t('Function level insights')}</h3>
  537. <p>
  538. {t(
  539. 'Discover slow-to-execute or resource intensive functions within your application'
  540. )}
  541. </p>
  542. </Fragment>
  543. }
  544. />
  545. }
  546. >
  547. <ProfilingUpgradeButton
  548. data-test-id="profiling-upgrade"
  549. organization={organization}
  550. priority="primary"
  551. onClick={onSetupProfilingClick}
  552. fallback={
  553. <Button onClick={onSetupProfilingClick} priority="primary">
  554. {t('Set Up Profiling')}
  555. </Button>
  556. }
  557. >
  558. {t('Set Up Profiling')}
  559. </ProfilingUpgradeButton>
  560. <LinkButton href="https://docs.sentry.io/product/profiling/" external>
  561. {t('Read Docs')}
  562. </LinkButton>
  563. </ProfilingOnboardingPanel>
  564. </Fragment>
  565. );
  566. }
  567. interface ProfilingContentPageHeaderProps {
  568. onTabChange: (newTab: 'flamegraph' | 'transactions') => void;
  569. tab: 'flamegraph' | 'transactions';
  570. }
  571. function ProfilingContentPageHeader(props: ProfilingContentPageHeaderProps) {
  572. return (
  573. <StyledLayoutHeader>
  574. <StyledHeaderContent>
  575. <Layout.Title>
  576. {t('Profiling')}
  577. <PageHeadingQuestionTooltip
  578. docsUrl="https://docs.sentry.io/product/profiling/"
  579. title={t(
  580. 'Profiling collects detailed information in production about the functions executing in your application and how long they take to run, giving you code-level visibility into your hot paths.'
  581. )}
  582. />
  583. </Layout.Title>
  584. <FeedbackWidgetButton />
  585. </StyledHeaderContent>
  586. <div>
  587. <Tabs value={props.tab} onChange={props.onTabChange}>
  588. <TabList hideBorder>
  589. <TabList.Item key="transactions">{t('Transactions')}</TabList.Item>
  590. <TabList.Item key="flamegraph">{t('Flamegraph')}</TabList.Item>
  591. </TabList>
  592. </Tabs>
  593. </div>
  594. </StyledLayoutHeader>
  595. );
  596. }
  597. const ALL_FIELDS = [
  598. 'transaction',
  599. 'project.id',
  600. 'last_seen()',
  601. 'p50()',
  602. 'p75()',
  603. 'p95()',
  604. 'p99()',
  605. 'count()',
  606. ] as const;
  607. type FieldType = (typeof ALL_FIELDS)[number];
  608. const FlamegraphBody = styled(Layout.Body)`
  609. display: grid;
  610. grid-template-rows: 1fr;
  611. `;
  612. const FlamegraphMainLayout = styled(Layout.Main)`
  613. display: grid;
  614. grid-column: 1 / -1;
  615. grid-template-rows: min-content 1fr;
  616. `;
  617. const FlamegraphLayout = styled('div')`
  618. display: grid;
  619. grid-template-areas: 'flamegraph sidebar';
  620. grid-template-columns: 1fr min-content;
  621. margin-top: ${space(2)};
  622. `;
  623. const FlamegraphActionBar = styled('div')``;
  624. const FlamegraphSidebar = styled('div')`
  625. grid-area: sidebar;
  626. `;
  627. const LandingAggregateFlamegraphContainer = styled('div')`
  628. height: 100%;
  629. min-height: 300px;
  630. position: relative;
  631. border: 1px solid ${p => p.theme.border};
  632. border-radius: ${p => p.theme.borderRadius};
  633. margin-bottom: ${space(2)};
  634. grid-area: flamegraph;
  635. `;
  636. const StyledLayoutHeader = styled(Layout.Header)`
  637. display: block;
  638. `;
  639. const StyledHeaderContent = styled(Layout.HeaderContent)`
  640. display: flex;
  641. align-items: center;
  642. justify-content: space-between;
  643. flex-direction: row;
  644. `;
  645. const ActionBar = styled('div')`
  646. display: grid;
  647. gap: ${space(2)};
  648. grid-template-columns: min-content auto;
  649. margin-bottom: ${space(2)};
  650. `;
  651. // TODO: another simple primitive that can easily be <Grid columns={2} />
  652. const PanelsGrid = styled('div')`
  653. display: grid;
  654. grid-template-columns: minmax(0, 1fr) 1fr;
  655. gap: ${space(2)};
  656. @media (max-width: ${p => p.theme.breakpoints.small}) {
  657. grid-template-columns: minmax(0, 1fr);
  658. }
  659. `;
  660. const WidgetsContainer = styled('div')`
  661. display: grid;
  662. grid-template-columns: 1fr 1fr;
  663. gap: ${space(2)};
  664. @media (max-width: ${p => p.theme.breakpoints.small}) {
  665. grid-template-columns: 1fr;
  666. }
  667. `;
  668. const StyledPagination = styled(Pagination)`
  669. margin: 0;
  670. `;
  671. function ProfilingContentWrapper(props: ProfilingContentProps) {
  672. const organization = useOrganization();
  673. if (organization.features.includes('continuous-profiling-compat')) {
  674. return <ProfilingContent {...props} />;
  675. }
  676. return <ProfilingContentLegacy {...props} />;
  677. }
  678. export default ProfilingContentWrapper;