content.tsx 7.9 KB

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