|
@@ -2,7 +2,9 @@ import React from 'react';
|
|
|
import {RouteComponentProps} from 'react-router';
|
|
|
import styled from '@emotion/styled';
|
|
|
|
|
|
+import {updateProjects} from 'app/actionCreators/globalSelection';
|
|
|
import Feature from 'app/components/acl/feature';
|
|
|
+import Alert from 'app/components/alert';
|
|
|
import Breadcrumbs from 'app/components/breadcrumbs';
|
|
|
import Button from 'app/components/button';
|
|
|
import ButtonBar from 'app/components/buttonBar';
|
|
@@ -13,11 +15,14 @@ import * as Layout from 'app/components/layouts/thirds';
|
|
|
import LightWeightNoProjectMessage from 'app/components/lightWeightNoProjectMessage';
|
|
|
import GlobalSelectionHeader from 'app/components/organizations/globalSelectionHeader';
|
|
|
import TextOverflow from 'app/components/textOverflow';
|
|
|
-import {IconSettings} from 'app/icons';
|
|
|
+import {IconSettings, IconWarning} from 'app/icons';
|
|
|
import {t} from 'app/locale';
|
|
|
import {PageContent} from 'app/styles/organization';
|
|
|
-import {Organization, Project} from 'app/types';
|
|
|
+import {GlobalSelection, Organization, Project} from 'app/types';
|
|
|
+import {defined} from 'app/utils';
|
|
|
import routeTitleGen from 'app/utils/routeTitle';
|
|
|
+import withGlobalSelection from 'app/utils/withGlobalSelection';
|
|
|
+import withProjects from 'app/utils/withProjects';
|
|
|
import AsyncView from 'app/views/asyncView';
|
|
|
|
|
|
import ProjectScoreCards from './projectScoreCards/projectScoreCards';
|
|
@@ -35,11 +40,12 @@ type RouteParams = {
|
|
|
|
|
|
type Props = RouteComponentProps<RouteParams, {}> & {
|
|
|
organization: Organization;
|
|
|
+ projects: Project[];
|
|
|
+ loadingProjects: boolean;
|
|
|
+ selection: GlobalSelection;
|
|
|
};
|
|
|
|
|
|
-type State = {
|
|
|
- project: Project | null;
|
|
|
-} & AsyncView['state'];
|
|
|
+type State = AsyncView['state'];
|
|
|
|
|
|
class ProjectDetail extends AsyncView<Props, State> {
|
|
|
getTitle() {
|
|
@@ -48,26 +54,95 @@ class ProjectDetail extends AsyncView<Props, State> {
|
|
|
return routeTitleGen(t('Project %s', params.projectId), params.orgId, false);
|
|
|
}
|
|
|
|
|
|
- getEndpoints(): ReturnType<AsyncView['getEndpoints']> {
|
|
|
- const {params} = this.props;
|
|
|
+ componentDidMount() {
|
|
|
+ this.syncProjectWithSlug();
|
|
|
+ }
|
|
|
+
|
|
|
+ componentDidUpdate() {
|
|
|
+ this.syncProjectWithSlug();
|
|
|
+ }
|
|
|
+
|
|
|
+ get project() {
|
|
|
+ const {projects, params} = this.props;
|
|
|
+
|
|
|
+ return projects.find(p => p.slug === params.projectId);
|
|
|
+ }
|
|
|
|
|
|
- if (this.state?.project) {
|
|
|
- return [];
|
|
|
+ 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({
|
|
|
+ pathname: `/organizations/${organization.slug}/projects/${newlySelectedProject.slug}/`,
|
|
|
+ query: {
|
|
|
+ ...location.query,
|
|
|
+ project: newlySelectedProject.id,
|
|
|
+ environment: undefined,
|
|
|
+ },
|
|
|
+ });
|
|
|
}
|
|
|
+ };
|
|
|
+
|
|
|
+ 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);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ isProjectStabilized() {
|
|
|
+ const {selection, location} = this.props;
|
|
|
+ const projectId = this.project?.id;
|
|
|
|
|
|
- return [['project', `/projects/${params.orgId}/${params.projectId}/`]];
|
|
|
+ return (
|
|
|
+ defined(projectId) &&
|
|
|
+ projectId === location.query.project &&
|
|
|
+ projectId === String(selection.projects[0])
|
|
|
+ );
|
|
|
}
|
|
|
|
|
|
renderLoading() {
|
|
|
return this.renderBody();
|
|
|
}
|
|
|
|
|
|
+ renderProjectNotFound() {
|
|
|
+ return (
|
|
|
+ <PageContent>
|
|
|
+ <Alert type="error" icon={<IconWarning />}>
|
|
|
+ {t('This project could not be found.')}
|
|
|
+ </Alert>
|
|
|
+ </PageContent>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
renderBody() {
|
|
|
- const {organization, params, location, router} = this.props;
|
|
|
- const {project} = this.state;
|
|
|
+ const {
|
|
|
+ organization,
|
|
|
+ params,
|
|
|
+ location,
|
|
|
+ router,
|
|
|
+ loadingProjects,
|
|
|
+ selection,
|
|
|
+ } = this.props;
|
|
|
+ const project = this.project;
|
|
|
+ const isProjectStabilized = this.isProjectStabilized();
|
|
|
+
|
|
|
+ if (!loadingProjects && !project) {
|
|
|
+ return this.renderProjectNotFound();
|
|
|
+ }
|
|
|
|
|
|
return (
|
|
|
- <GlobalSelectionHeader shouldForceProject forceProject={project}>
|
|
|
+ <GlobalSelectionHeader
|
|
|
+ disableMultipleProjectSelection
|
|
|
+ skipLoadLastUsed
|
|
|
+ onUpdateProjects={this.handleProjectChange}
|
|
|
+ >
|
|
|
<LightWeightNoProjectMessage organization={organization}>
|
|
|
<StyledPageContent>
|
|
|
<Layout.Header>
|
|
@@ -122,17 +197,25 @@ class ProjectDetail extends AsyncView<Props, State> {
|
|
|
<Layout.Body>
|
|
|
<StyledSdkUpdatesAlert />
|
|
|
<Layout.Main>
|
|
|
- <ProjectScoreCards organization={organization} />
|
|
|
- {[0, 1].map(id => (
|
|
|
- <ProjectCharts
|
|
|
- location={location}
|
|
|
- organization={organization}
|
|
|
- router={router}
|
|
|
- key={`project-charts-${id}`}
|
|
|
- index={id}
|
|
|
- />
|
|
|
- ))}
|
|
|
- <ProjectIssues organization={organization} location={location} />
|
|
|
+ <ProjectScoreCards
|
|
|
+ organization={organization}
|
|
|
+ isProjectStabilized={isProjectStabilized}
|
|
|
+ selection={selection}
|
|
|
+ />
|
|
|
+ {isProjectStabilized && (
|
|
|
+ <React.Fragment>
|
|
|
+ {[0, 1].map(id => (
|
|
|
+ <ProjectCharts
|
|
|
+ location={location}
|
|
|
+ organization={organization}
|
|
|
+ router={router}
|
|
|
+ key={`project-charts-${id}`}
|
|
|
+ index={id}
|
|
|
+ />
|
|
|
+ ))}
|
|
|
+ <ProjectIssues organization={organization} location={location} />
|
|
|
+ </React.Fragment>
|
|
|
+ )}
|
|
|
</Layout.Main>
|
|
|
<Layout.Side>
|
|
|
<ProjectTeamAccess organization={organization} project={project} />
|
|
@@ -141,6 +224,7 @@ class ProjectDetail extends AsyncView<Props, State> {
|
|
|
organization={organization}
|
|
|
projectSlug={params.projectId}
|
|
|
location={location}
|
|
|
+ isProjectStabilized={isProjectStabilized}
|
|
|
/>
|
|
|
</Feature>
|
|
|
<ProjectLatestReleases
|
|
@@ -148,6 +232,7 @@ class ProjectDetail extends AsyncView<Props, State> {
|
|
|
projectSlug={params.projectId}
|
|
|
projectId={project?.id}
|
|
|
location={location}
|
|
|
+ isProjectStabilized={isProjectStabilized}
|
|
|
/>
|
|
|
<ProjectQuickLinks
|
|
|
organization={organization}
|
|
@@ -177,4 +262,4 @@ StyledSdkUpdatesAlert.defaultProps = {
|
|
|
Wrapper: p => <Layout.Main fullWidth {...p} />,
|
|
|
};
|
|
|
|
|
|
-export default ProjectDetail;
|
|
|
+export default withProjects(withGlobalSelection(ProjectDetail));
|