import {Fragment} from 'react'; import type {RouteComponentProps} from 'react-router'; import styled from '@emotion/styled'; import pick from 'lodash/pick'; import {fetchOrganizationDetails} from 'sentry/actionCreators/organization'; import {updateProjects} from 'sentry/actionCreators/pageFilters'; import {fetchTagValues} from 'sentry/actionCreators/tags'; import Feature from 'sentry/components/acl/feature'; import {Breadcrumbs} from 'sentry/components/breadcrumbs'; import {Button} from 'sentry/components/button'; import ButtonBar from 'sentry/components/buttonBar'; import CreateAlertButton from 'sentry/components/createAlertButton'; import FloatingFeedbackWidget from 'sentry/components/feedback/widget/floatingFeedbackWidget'; import GlobalAppStoreConnectUpdateAlert from 'sentry/components/globalAppStoreConnectUpdateAlert'; import GlobalEventProcessingAlert from 'sentry/components/globalEventProcessingAlert'; import IdBadge from 'sentry/components/idBadge'; import * as Layout from 'sentry/components/layouts/thirds'; import LoadingError from 'sentry/components/loadingError'; import NoProjectMessage from 'sentry/components/noProjectMessage'; import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container'; import MissingProjectMembership from 'sentry/components/projects/missingProjectMembership'; import {DEFAULT_RELATIVE_PERIODS} from 'sentry/constants'; import {IconSettings} from 'sentry/icons'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {Organization, PageFilters, Project} from 'sentry/types'; import {defined} from 'sentry/utils'; import routeTitleGen from 'sentry/utils/routeTitle'; import {normalizeUrl} from 'sentry/utils/withDomainRequired'; import withPageFilters from 'sentry/utils/withPageFilters'; import withProjects from 'sentry/utils/withProjects'; import DeprecatedAsyncView from 'sentry/views/deprecatedAsyncView'; import {ERRORS_BASIC_CHART_PERIODS} from './charts/projectErrorsBasicChart'; import ProjectScoreCards from './projectScoreCards/projectScoreCards'; import ProjectCharts from './projectCharts'; import ProjectFilters from './projectFilters'; import ProjectIssues from './projectIssues'; import ProjectLatestAlerts from './projectLatestAlerts'; import ProjectLatestReleases from './projectLatestReleases'; import ProjectQuickLinks from './projectQuickLinks'; import ProjectTeamAccess from './projectTeamAccess'; type RouteParams = { orgId: string; projectId: string; }; type Props = RouteComponentProps & { loadingProjects: boolean; organization: Organization; projects: Project[]; selection: PageFilters; }; type State = DeprecatedAsyncView['state']; class ProjectDetail extends DeprecatedAsyncView { getTitle() { const {params, organization} = this.props; return routeTitleGen(t('Project %s', params.projectId), organization.slug, false); } componentDidMount() { super.componentDidMount(); this.syncProjectWithSlug(); } componentDidUpdate() { this.syncProjectWithSlug(); } get project() { const {projects, params} = this.props; return projects.find(p => p.slug === params.projectId); } handleProjectChange = (selectedProjects: number[]) => { const {projects, router, location, organization} = this.props; const newlySelectedProject = projects.find(p => p.id === String(selectedProjects[0])); // if we change project in global header, we need to sync the project slug in the URL if (newlySelectedProject?.id) { router.replace( normalizeUrl({ pathname: `/organizations/${organization.slug}/projects/${newlySelectedProject.slug}/`, query: { ...location.query, project: newlySelectedProject.id, environment: undefined, }, }) ); } }; handleSearch = (query: string) => { const {router, location} = this.props; router.replace({ pathname: location.pathname, query: { ...location.query, query, }, }); }; tagValueLoader = (key: string, search: string) => { const {location, organization} = this.props; const {project: projectId} = location.query; return fetchTagValues({ api: this.api, orgSlug: organization.slug, tagKey: key, search, projectIds: projectId ? [projectId] : undefined, endpointParams: location.query, }); }; syncProjectWithSlug() { const {router, location} = this.props; const projectId = this.project?.id; if (projectId && projectId !== location.query.project) { // if someone visits /organizations/sentry/projects/javascript/ (without ?project=XXX) we need to update URL and globalSelection with the right project ID updateProjects([Number(projectId)], router); } } onRetryProjects = () => { const {params} = this.props; fetchOrganizationDetails(this.api, params.orgId, true, false); }; isProjectStabilized() { const {selection, location} = this.props; const projectId = this.project?.id; return ( defined(projectId) && projectId === location.query.project && projectId === String(selection.projects[0]) ); } renderLoading() { return this.renderBody(); } renderNoAccess(project: Project) { const {organization} = this.props; return ( ); } renderProjectNotFound() { return ( ); } renderBody() { const {organization, params, location, router, loadingProjects, selection} = this.props; const project = this.project; const {query} = location.query; const hasPerformance = organization.features.includes('performance-view'); const hasDiscover = organization.features.includes('discover-basic'); const hasTransactions = hasPerformance && project?.firstTransactionEvent; const isProjectStabilized = this.isProjectStabilized(); const visibleCharts = ['chart1']; const hasSessions = project?.hasSessions ?? null; const hasOnlyBasicChart = !hasPerformance && !hasDiscover && !hasSessions; if (hasTransactions || hasSessions) { visibleCharts.push('chart2'); } if (!loadingProjects && !project) { return this.renderProjectNotFound(); } if (!loadingProjects && project && !project.hasAccess) { return this.renderNoAccess(project); } return ( {project ? ( ) : null} {project?.slug}