projectDetail.tsx 11 KB

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