content.tsx 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. import {Fragment, useCallback, useEffect, useMemo} from 'react';
  2. import {browserHistory, InjectedRouter} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import {Location} from 'history';
  5. import {openModal} from 'sentry/actionCreators/modal';
  6. import Button from 'sentry/components/button';
  7. import DatePageFilter from 'sentry/components/datePageFilter';
  8. import EnvironmentPageFilter from 'sentry/components/environmentPageFilter';
  9. import {FeatureFeedback} from 'sentry/components/featureFeedback';
  10. import * as Layout from 'sentry/components/layouts/thirds';
  11. import NoProjectMessage from 'sentry/components/noProjectMessage';
  12. import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
  13. import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container';
  14. import PageHeading from 'sentry/components/pageHeading';
  15. import Pagination from 'sentry/components/pagination';
  16. import {ProfileTransactionsTable} from 'sentry/components/profiling/profileTransactionsTable';
  17. import {ProfilingOnboardingModal} from 'sentry/components/profiling/ProfilingOnboarding/profilingOnboardingModal';
  18. import ProjectPageFilter from 'sentry/components/projectPageFilter';
  19. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  20. import SmartSearchBar, {SmartSearchBarProps} from 'sentry/components/smartSearchBar';
  21. import {MAX_QUERY_LENGTH} from 'sentry/constants';
  22. import {ALL_ACCESS_PROJECTS} from 'sentry/constants/pageFilters';
  23. import {t} from 'sentry/locale';
  24. import {PageContent} from 'sentry/styles/organization';
  25. import space from 'sentry/styles/space';
  26. import {Project} from 'sentry/types';
  27. import {PageFilters} from 'sentry/types/core';
  28. import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  29. import {useProfileFilters} from 'sentry/utils/profiling/hooks/useProfileFilters';
  30. import {useProfileTransactions} from 'sentry/utils/profiling/hooks/useProfileTransactions';
  31. import {decodeScalar} from 'sentry/utils/queryString';
  32. import useOrganization from 'sentry/utils/useOrganization';
  33. import usePageFilters from 'sentry/utils/usePageFilters';
  34. import useProjects from 'sentry/utils/useProjects';
  35. import {ProfileCharts} from './landing/profileCharts';
  36. import {ProfilingOnboardingPanel} from './profilingOnboardingPanel';
  37. function hasSetupProfilingForAtLeastOneProject(
  38. selectedProjects: PageFilters['projects'],
  39. projects: Project[]
  40. ): boolean {
  41. const projectIDsToProjectTable = projects.reduce<Record<string, Project>>(
  42. (acc, project) => {
  43. acc[project.id] = project;
  44. return acc;
  45. },
  46. {}
  47. );
  48. if (selectedProjects[0] === ALL_ACCESS_PROJECTS || selectedProjects.length === 0) {
  49. const projectWithProfiles = projects.find(p => {
  50. const project = projectIDsToProjectTable[String(p)];
  51. if (!project) {
  52. // Shouldnt happen, but lets be safe and just not do anything
  53. return false;
  54. }
  55. return project.hasProfiles;
  56. });
  57. return projectWithProfiles !== undefined;
  58. }
  59. const projectWithProfiles = selectedProjects.find(p => {
  60. const project = projectIDsToProjectTable[String(p)];
  61. if (!project) {
  62. // Shouldnt happen, but lets be safe and just not do anything
  63. return false;
  64. }
  65. return project.hasProfiles;
  66. });
  67. return projectWithProfiles !== undefined;
  68. }
  69. interface ProfilingContentProps {
  70. location: Location;
  71. router: InjectedRouter;
  72. }
  73. function ProfilingContent({location, router}: ProfilingContentProps) {
  74. const organization = useOrganization();
  75. const {selection} = usePageFilters();
  76. const cursor = decodeScalar(location.query.cursor);
  77. const query = decodeScalar(location.query.query, '');
  78. const transactionsSort = decodeScalar(location.query.sort, '-count()');
  79. const profileFilters = useProfileFilters({query: '', selection});
  80. const transactions = useProfileTransactions({
  81. cursor,
  82. query,
  83. selection,
  84. sort: transactionsSort,
  85. });
  86. const {projects} = useProjects();
  87. useEffect(() => {
  88. trackAdvancedAnalyticsEvent('profiling_views.landing', {
  89. organization,
  90. });
  91. }, [organization]);
  92. const handleSearch: SmartSearchBarProps['onSearch'] = useCallback(
  93. (searchQuery: string) => {
  94. browserHistory.push({
  95. ...location,
  96. query: {
  97. ...location.query,
  98. cursor: undefined,
  99. query: searchQuery || undefined,
  100. },
  101. });
  102. },
  103. [location]
  104. );
  105. // Open the modal on demand
  106. const onSetupProfilingClick = useCallback(() => {
  107. openModal(props => {
  108. return <ProfilingOnboardingModal {...props} organization={organization} />;
  109. });
  110. }, [organization]);
  111. const shouldShowProfilingOnboardingPanel = useMemo((): boolean => {
  112. if (transactions.type !== 'resolved') {
  113. return false;
  114. }
  115. if (transactions.data.transactions.length > 0) {
  116. return false;
  117. }
  118. return !hasSetupProfilingForAtLeastOneProject(selection.projects, projects);
  119. }, [selection.projects, projects, transactions]);
  120. return (
  121. <SentryDocumentTitle title={t('Profiling')} orgSlug={organization.slug}>
  122. <PageFiltersContainer>
  123. <NoProjectMessage organization={organization}>
  124. <StyledPageContent>
  125. <Layout.Header>
  126. <StyledLayoutHeaderContent>
  127. <StyledHeading>{t('Profiling')}</StyledHeading>
  128. <HeadingActions>
  129. <Button onClick={onSetupProfilingClick}>{t('Set Up Profiling')}</Button>
  130. <FeatureFeedback featureName="profiling" />
  131. </HeadingActions>
  132. </StyledLayoutHeaderContent>
  133. </Layout.Header>
  134. <Layout.Body>
  135. <Layout.Main fullWidth>
  136. <ActionBar>
  137. <PageFilterBar condensed>
  138. <ProjectPageFilter />
  139. <EnvironmentPageFilter />
  140. <DatePageFilter alignDropdown="left" />
  141. </PageFilterBar>
  142. <SmartSearchBar
  143. organization={organization}
  144. hasRecentSearches
  145. searchSource="profile_landing"
  146. supportedTags={profileFilters}
  147. query={query}
  148. onSearch={handleSearch}
  149. maxQueryLength={MAX_QUERY_LENGTH}
  150. />
  151. </ActionBar>
  152. {shouldShowProfilingOnboardingPanel ? (
  153. <ProfilingOnboardingPanel>
  154. <Button href="https://docs.sentry.io/" external>
  155. {t('Read Docs')}
  156. </Button>
  157. <Button onClick={onSetupProfilingClick} priority="primary">
  158. {t('Set Up Profiling')}
  159. </Button>
  160. </ProfilingOnboardingPanel>
  161. ) : (
  162. <Fragment>
  163. <ProfileCharts router={router} query={query} selection={selection} />
  164. <ProfileTransactionsTable
  165. error={
  166. transactions.type === 'errored'
  167. ? t('Unable to load profiles')
  168. : null
  169. }
  170. isLoading={transactions.type === 'loading'}
  171. sort={transactionsSort}
  172. transactions={
  173. transactions.type === 'resolved'
  174. ? transactions.data.transactions
  175. : []
  176. }
  177. />
  178. <Pagination
  179. pageLinks={
  180. transactions.type === 'resolved'
  181. ? transactions.data.pageLinks
  182. : null
  183. }
  184. />
  185. </Fragment>
  186. )}
  187. </Layout.Main>
  188. </Layout.Body>
  189. </StyledPageContent>
  190. </NoProjectMessage>
  191. </PageFiltersContainer>
  192. </SentryDocumentTitle>
  193. );
  194. }
  195. const StyledPageContent = styled(PageContent)`
  196. padding: 0;
  197. `;
  198. const StyledLayoutHeaderContent = styled(Layout.HeaderContent)`
  199. display: flex;
  200. justify-content: space-between;
  201. flex-direction: row;
  202. `;
  203. const HeadingActions = styled('div')`
  204. display: flex;
  205. align-items: center;
  206. button:not(:last-child) {
  207. margin-right: ${space(1)};
  208. }
  209. `;
  210. const StyledHeading = styled(PageHeading)`
  211. line-height: 40px;
  212. `;
  213. const ActionBar = styled('div')`
  214. display: grid;
  215. gap: ${space(2)};
  216. grid-template-columns: min-content auto;
  217. margin-bottom: ${space(2)};
  218. `;
  219. export default ProfilingContent;