projectDetail.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. import {Fragment, useCallback, useEffect, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import pick from 'lodash/pick';
  4. import {fetchOrganizationDetails} from 'sentry/actionCreators/organization';
  5. import {updateProjects} from 'sentry/actionCreators/pageFilters';
  6. import {fetchTagValues} from 'sentry/actionCreators/tags';
  7. import Feature from 'sentry/components/acl/feature';
  8. import {Breadcrumbs} from 'sentry/components/breadcrumbs';
  9. import {LinkButton} from 'sentry/components/button';
  10. import ButtonBar from 'sentry/components/buttonBar';
  11. import CreateAlertButton from 'sentry/components/createAlertButton';
  12. import ErrorBoundary from 'sentry/components/errorBoundary';
  13. import FeedbackWidgetButton from 'sentry/components/feedback/widget/feedbackWidgetButton';
  14. import GlobalEventProcessingAlert from 'sentry/components/globalEventProcessingAlert';
  15. import IdBadge from 'sentry/components/idBadge';
  16. import * as Layout from 'sentry/components/layouts/thirds';
  17. import LoadingError from 'sentry/components/loadingError';
  18. import {usePrefersStackedNav} from 'sentry/components/nav/prefersStackedNav';
  19. import NoProjectMessage from 'sentry/components/noProjectMessage';
  20. import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container';
  21. import MissingProjectMembership from 'sentry/components/projects/missingProjectMembership';
  22. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  23. import {DEFAULT_RELATIVE_PERIODS} from 'sentry/constants';
  24. import {IconSettings} from 'sentry/icons';
  25. import {t} from 'sentry/locale';
  26. import {space} from 'sentry/styles/space';
  27. import type {RouteComponentProps} from 'sentry/types/legacyReactRouter';
  28. import type {Organization} from 'sentry/types/organization';
  29. import {defined} from 'sentry/utils';
  30. import routeTitleGen from 'sentry/utils/routeTitle';
  31. import useApi from 'sentry/utils/useApi';
  32. import usePageFilters from 'sentry/utils/usePageFilters';
  33. import {useParams} from 'sentry/utils/useParams';
  34. import useProjects from 'sentry/utils/useProjects';
  35. import {ERRORS_BASIC_CHART_PERIODS} from './charts/projectErrorsBasicChart';
  36. import ProjectScoreCards from './projectScoreCards/projectScoreCards';
  37. import ProjectCharts from './projectCharts';
  38. import ProjectFilters from './projectFilters';
  39. import ProjectIssues from './projectIssues';
  40. import ProjectLatestAlerts from './projectLatestAlerts';
  41. import ProjectLatestReleases from './projectLatestReleases';
  42. import ProjectQuickLinks from './projectQuickLinks';
  43. import ProjectTeamAccess from './projectTeamAccess';
  44. type RouteParams = {
  45. orgId: string;
  46. projectId: string;
  47. };
  48. type Props = RouteComponentProps<RouteParams> & {
  49. organization: Organization;
  50. };
  51. export default function ProjectDetail({router, location, organization}: Props) {
  52. const api = useApi();
  53. const params = useParams();
  54. const {projects, fetching: loadingProjects} = useProjects();
  55. const {selection} = usePageFilters();
  56. const project = projects.find(p => p.slug === params.projectId);
  57. const {query} = location.query;
  58. const hasPerformance = organization.features.includes('performance-view');
  59. const hasDiscover = organization.features.includes('discover-basic');
  60. const hasTransactions = hasPerformance && project?.firstTransactionEvent;
  61. const projectId = project?.id;
  62. const isProjectStabilized =
  63. defined(project?.id) &&
  64. project.id === location.query.project &&
  65. project.id === String(selection.projects[0]);
  66. const hasSessions = project?.hasSessions ?? null;
  67. const hasOnlyBasicChart = !hasPerformance && !hasDiscover && !hasSessions;
  68. const title = routeTitleGen(
  69. t('Project %s', params.projectId),
  70. organization.slug,
  71. false
  72. );
  73. const prefersStackedNav = usePrefersStackedNav();
  74. const visibleCharts = useMemo(() => {
  75. if (hasTransactions || hasSessions) {
  76. return ['chart1', 'chart2'];
  77. }
  78. return ['chart1'];
  79. }, [hasTransactions, hasSessions]);
  80. const onRetryProjects = useCallback(() => {
  81. fetchOrganizationDetails(api, params.orgId!);
  82. }, [api, params.orgId]);
  83. const handleSearch = useCallback(
  84. (searchQuery: string) => {
  85. router.replace({
  86. pathname: location.pathname,
  87. query: {
  88. ...location.query,
  89. query: searchQuery,
  90. },
  91. });
  92. },
  93. [router, location.query, location.pathname]
  94. );
  95. const tagValueLoader = useCallback(
  96. (key: string, search: string) => {
  97. return fetchTagValues({
  98. api,
  99. orgSlug: organization.slug,
  100. tagKey: key,
  101. search,
  102. projectIds: location.query.project ? [location.query.project] : undefined,
  103. endpointParams: location.query,
  104. });
  105. },
  106. [api, organization.slug, location.query]
  107. );
  108. useEffect(() => {
  109. function syncProjectWithSlug() {
  110. if (projectId && projectId !== location.query.project) {
  111. // if someone visits /organizations/sentry/projects/javascript/ (without ?project=XXX) we need to update URL and globalSelection with the right project ID
  112. updateProjects([Number(projectId)], router);
  113. }
  114. }
  115. syncProjectWithSlug();
  116. }, [location.query.project, router, projectId]);
  117. if (!loadingProjects && !project) {
  118. return (
  119. <Layout.Page withPadding>
  120. <LoadingError
  121. message={t('This project could not be found.')}
  122. onRetry={onRetryProjects}
  123. />
  124. </Layout.Page>
  125. );
  126. }
  127. if (!loadingProjects && project && !project.hasAccess) {
  128. return (
  129. <Layout.Page>
  130. <MissingProjectMembership organization={organization} project={project} />
  131. </Layout.Page>
  132. );
  133. }
  134. return (
  135. <SentryDocumentTitle title={title}>
  136. <PageFiltersContainer
  137. disablePersistence
  138. skipLoadLastUsed
  139. showAbsolute={!hasOnlyBasicChart}
  140. >
  141. <Layout.Page>
  142. <NoProjectMessage organization={organization}>
  143. <Layout.Header unified={prefersStackedNav}>
  144. <Layout.HeaderContent unified={prefersStackedNav}>
  145. <Breadcrumbs
  146. crumbs={[
  147. {
  148. to: `/organizations/${params.orgId}/projects/`,
  149. label: t('Projects'),
  150. },
  151. {label: t('Project Details')},
  152. ]}
  153. />
  154. <Layout.Title>
  155. {project ? (
  156. <IdBadge
  157. project={project}
  158. avatarSize={28}
  159. hideOverflow="100%"
  160. disableLink
  161. hideName
  162. />
  163. ) : null}
  164. {project?.slug}
  165. </Layout.Title>
  166. </Layout.HeaderContent>
  167. <Layout.HeaderActions>
  168. <ButtonBar gap={1}>
  169. <FeedbackWidgetButton />
  170. <LinkButton
  171. size="sm"
  172. to={
  173. // if we are still fetching project, we can use project slug to build issue stream url and let the redirect handle it
  174. project?.id
  175. ? `/organizations/${params.orgId}/issues/?project=${project.id}`
  176. : `/${params.orgId}/${params.projectId}`
  177. }
  178. >
  179. {t('View All Issues')}
  180. </LinkButton>
  181. <CreateAlertButton
  182. size="sm"
  183. organization={organization}
  184. projectSlug={params.projectId}
  185. aria-label={t('Create Alert')}
  186. />
  187. <LinkButton
  188. size="sm"
  189. icon={<IconSettings />}
  190. aria-label={t('Settings')}
  191. to={`/settings/${params.orgId}/projects/${params.projectId}/`}
  192. />
  193. </ButtonBar>
  194. </Layout.HeaderActions>
  195. </Layout.Header>
  196. <Layout.Body noRowGap>
  197. <ErrorBoundary customComponent={null}>
  198. {project && <StyledGlobalEventProcessingAlert projects={[project]} />}
  199. </ErrorBoundary>
  200. <Layout.Main>
  201. <ProjectFiltersWrapper>
  202. <ProjectFilters
  203. query={query}
  204. onSearch={handleSearch}
  205. relativeDateOptions={
  206. hasOnlyBasicChart
  207. ? pick(DEFAULT_RELATIVE_PERIODS, ERRORS_BASIC_CHART_PERIODS)
  208. : undefined
  209. }
  210. tagValueLoader={tagValueLoader}
  211. />
  212. </ProjectFiltersWrapper>
  213. <ProjectScoreCards
  214. organization={organization}
  215. isProjectStabilized={isProjectStabilized}
  216. selection={selection}
  217. hasSessions={hasSessions}
  218. hasTransactions={hasTransactions}
  219. query={query}
  220. project={project}
  221. location={location}
  222. />
  223. {isProjectStabilized && (
  224. <Fragment>
  225. {visibleCharts.map((id, index) => (
  226. <ErrorBoundary mini key={`project-charts-${id}`}>
  227. <ProjectCharts
  228. location={location}
  229. organization={organization}
  230. router={router}
  231. chartId={id}
  232. chartIndex={index}
  233. projectId={project?.id}
  234. hasSessions={hasSessions}
  235. hasTransactions={!!hasTransactions}
  236. visibleCharts={visibleCharts}
  237. query={query}
  238. project={project}
  239. />
  240. </ErrorBoundary>
  241. ))}
  242. <ProjectIssues
  243. organization={organization}
  244. location={location}
  245. projectId={selection.projects[0]!}
  246. query={query}
  247. api={api}
  248. />
  249. </Fragment>
  250. )}
  251. </Layout.Main>
  252. <Layout.Side>
  253. <ProjectTeamAccess organization={organization} project={project} />
  254. <Feature features="incidents" organization={organization}>
  255. <ProjectLatestAlerts
  256. organization={organization}
  257. projectSlug={params.projectId!}
  258. location={location}
  259. isProjectStabilized={isProjectStabilized}
  260. />
  261. </Feature>
  262. <ProjectLatestReleases
  263. organization={organization}
  264. projectSlug={params.projectId!}
  265. location={location}
  266. isProjectStabilized={isProjectStabilized}
  267. project={project}
  268. />
  269. <ProjectQuickLinks
  270. organization={organization}
  271. project={project}
  272. location={location}
  273. />
  274. </Layout.Side>
  275. </Layout.Body>
  276. </NoProjectMessage>
  277. </Layout.Page>
  278. </PageFiltersContainer>
  279. </SentryDocumentTitle>
  280. );
  281. }
  282. const ProjectFiltersWrapper = styled('div')`
  283. margin-bottom: ${space(2)};
  284. `;
  285. const StyledGlobalEventProcessingAlert = styled(GlobalEventProcessingAlert)`
  286. @media (min-width: ${p => p.theme.breakpoints.medium}) {
  287. margin-bottom: 0;
  288. }
  289. `;