@@ -160,10 +160,6 @@ export function getComponentShow(data: { component: string } & BranchParameters) | |||
return getComponentData(data).catch(throwGlobalError); | |||
} | |||
export function getParents(component: string): Promise<any> { | |||
return getComponentShow({ component }).then((r) => r.ancestors); | |||
} | |||
export function getBreadcrumbs( | |||
data: { component: string } & BranchParameters | |||
): Promise<Array<Omit<ComponentRaw, 'tags'>>> { |
@@ -26,7 +26,7 @@ import { Extension, NavigationComponent } from '../types/types'; | |||
export function getComponentNavigation( | |||
data: { component: string } & BranchParameters | |||
): Promise<NavigationComponent> { | |||
return getJSON('/api/navigation/component', data).catch(throwGlobalError); | |||
return getJSON('/api/navigation/component', data); | |||
} | |||
export function getMarketplaceNavigation(): Promise<{ serverId: string; ncloc: number }> { |
@@ -32,7 +32,7 @@ import { translateWithParameters } from '../../helpers/l10n'; | |||
import { HttpStatus } from '../../helpers/request'; | |||
import { getPortfolioUrl, getProjectUrl } from '../../helpers/urls'; | |||
import { ProjectAlmBindingConfigurationErrors } from '../../types/alm-settings'; | |||
import { ComponentQualifier, isPortfolioLike } from '../../types/component'; | |||
import { ComponentQualifier, isFile, isPortfolioLike } from '../../types/component'; | |||
import { Feature } from '../../types/features'; | |||
import { Task, TaskStatuses, TaskTypes } from '../../types/tasks'; | |||
import { Component } from '../../types/types'; | |||
@@ -118,7 +118,7 @@ export class ComponentContainer extends React.PureComponent<Props, State> { | |||
* This is a fail-safe in case there are still some faulty links remaining. | |||
*/ | |||
if ( | |||
this.props.location.pathname.match('dashboard') && | |||
this.props.location.pathname.includes('dashboard') && | |||
isPortfolioLike(componentWithQualifier.qualifier) | |||
) { | |||
this.props.router.replace(getPortfolioUrl(componentWithQualifier.key)); | |||
@@ -131,7 +131,7 @@ export class ComponentContainer extends React.PureComponent<Props, State> { | |||
loading: false, | |||
}, | |||
() => { | |||
if (shouldRedirectToDashboard && this.props.location.pathname.match('tutorials')) { | |||
if (shouldRedirectToDashboard && this.props.location.pathname.includes('tutorials')) { | |||
this.props.router.replace(getProjectUrl(key)); | |||
} | |||
} | |||
@@ -319,7 +319,11 @@ export class ComponentContainer extends React.PureComponent<Props, State> { | |||
const { component, loading } = this.state; | |||
if (!loading && !component) { | |||
return <ComponentContainerNotFound />; | |||
return ( | |||
<ComponentContainerNotFound | |||
isPortfolioLike={this.props.location.pathname.includes('portfolio')} | |||
/> | |||
); | |||
} | |||
const { currentTask, isPending, projectBindingErrors, tasksInProgress } = this.state; | |||
@@ -335,11 +339,8 @@ export class ComponentContainer extends React.PureComponent<Props, State> { | |||
component?.name ?? '' | |||
)} | |||
/> | |||
{component && | |||
!([ComponentQualifier.File, ComponentQualifier.TestFile] as string[]).includes( | |||
component.qualifier | |||
) && | |||
!isFile(component.qualifier) && | |||
this.portalAnchor && | |||
/* Use a portal to fix positioning until we can fully review the layout */ | |||
createPortal( | |||
@@ -352,7 +353,6 @@ export class ComponentContainer extends React.PureComponent<Props, State> { | |||
/>, | |||
this.portalAnchor | |||
)} | |||
{loading ? ( | |||
<div className="page page-limited"> | |||
<i className="spinner" /> |
@@ -22,14 +22,24 @@ import { Helmet } from 'react-helmet-async'; | |||
import Link from '../../components/common/Link'; | |||
import { translate } from '../../helpers/l10n'; | |||
export default function ComponentContainerNotFound() { | |||
export interface ComponentContainerNotFoundProps { | |||
isPortfolioLike: boolean; | |||
} | |||
export default function ComponentContainerNotFound({ | |||
isPortfolioLike, | |||
}: ComponentContainerNotFoundProps) { | |||
const componentType = isPortfolioLike ? 'portfolio' : 'project'; | |||
return ( | |||
<> | |||
<Helmet defaultTitle={translate('404_not_found')} defer={false} /> | |||
<div className="page-wrapper-simple" id="bd"> | |||
<div className="page-simple" id="nonav"> | |||
<h2 className="big-spacer-bottom">{translate('dashboard.project_not_found')}</h2> | |||
<p className="spacer-bottom">{translate('dashboard.project_not_found.2')}</p> | |||
<h2 className="big-spacer-bottom"> | |||
{translate('dashboard', componentType, 'not_found')} | |||
</h2> | |||
<p className="spacer-bottom">{translate('dashboard', componentType, 'not_found.2')}</p> | |||
<p> | |||
<Link to="/">{translate('go_back_to_homepage')}</Link> | |||
</p> |
@@ -0,0 +1,42 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { screen } from '@testing-library/react'; | |||
import * as React from 'react'; | |||
import { renderComponent } from '../../../helpers/testReactTestingUtils'; | |||
import ComponentContainerNotFound from '../ComponentContainerNotFound'; | |||
it('should render portfolio 404 correctly', () => { | |||
renderComponentContainerNotFound(true); | |||
expect(screen.getByText('dashboard.portfolio.not_found')).toBeInTheDocument(); | |||
expect(screen.getByText('dashboard.portfolio.not_found.2')).toBeInTheDocument(); | |||
}); | |||
it('should render project 404 correctly', () => { | |||
renderComponentContainerNotFound(false); | |||
expect(screen.getByText('dashboard.project.not_found')).toBeInTheDocument(); | |||
expect(screen.getByText('dashboard.project.not_found.2')).toBeInTheDocument(); | |||
}); | |||
function renderComponentContainerNotFound(isPortfolioLike: boolean) { | |||
return renderComponent(<ComponentContainerNotFound isPortfolioLike={isPortfolioLike} />); | |||
} |
@@ -1,3 +1,7 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should show component not found if it does not exist 1`] = `<ComponentContainerNotFound />`; | |||
exports[`should show component not found if it does not exist 1`] = ` | |||
<ComponentContainerNotFound | |||
isPortfolioLike={false} | |||
/> | |||
`; |
@@ -24,6 +24,7 @@ import withAppStateContext from '../../../app/components/app-state/withAppStateC | |||
import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext'; | |||
import CreateApplicationForm from '../../../app/components/extensions/CreateApplicationForm'; | |||
import { Router, withRouter } from '../../../components/hoc/withRouter'; | |||
import { throwGlobalError } from '../../../helpers/error'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { getComponentAdminUrl, getComponentOverviewUrl } from '../../../helpers/urls'; | |||
import { hasGlobalPermission } from '../../../helpers/users'; | |||
@@ -59,14 +60,16 @@ export function ApplicationCreation(props: ApplicationCreationProps) { | |||
key: string; | |||
qualifier: ComponentQualifier; | |||
}) => { | |||
return getComponentNavigation({ component: key }).then(({ configuration }) => { | |||
if (configuration && configuration.showSettings) { | |||
router.push(getComponentAdminUrl(key, qualifier)); | |||
} else { | |||
router.push(getComponentOverviewUrl(key, qualifier)); | |||
} | |||
setShowForm(false); | |||
}); | |||
return getComponentNavigation({ component: key }) | |||
.then(({ configuration }) => { | |||
if (configuration?.showSettings) { | |||
router.push(getComponentAdminUrl(key, qualifier)); | |||
} else { | |||
router.push(getComponentOverviewUrl(key, qualifier)); | |||
} | |||
setShowForm(false); | |||
}) | |||
.catch(throwGlobalError); | |||
}; | |||
return ( |
@@ -22,6 +22,7 @@ import { getComponentNavigation } from '../../api/navigation'; | |||
import { Project } from '../../api/project-management'; | |||
import ActionsDropdown, { ActionsDropdownItem } from '../../components/controls/ActionsDropdown'; | |||
import Spinner from '../../components/ui/Spinner'; | |||
import { throwGlobalError } from '../../helpers/error'; | |||
import { translate, translateWithParameters } from '../../helpers/l10n'; | |||
import { getComponentPermissionsUrl } from '../../helpers/urls'; | |||
import { useGithubProvisioningEnabledQuery } from '../../queries/identity-provider'; | |||
@@ -41,20 +42,19 @@ export default function ProjectRowActions({ currentUser, project }: Props) { | |||
const [restoreAccessModal, setRestoreAccessModal] = useState(false); | |||
const { data: githubProvisioningEnabled } = useGithubProvisioningEnabledQuery(); | |||
const fetchPermissions = () => { | |||
const fetchPermissions = async () => { | |||
setLoading(true); | |||
getComponentNavigation({ component: project.key }).then( | |||
({ configuration }) => { | |||
const hasAccess = Boolean( | |||
configuration && configuration.showPermissions && configuration.canBrowseProject | |||
); | |||
setHasAccess(hasAccess); | |||
setLoading(false); | |||
}, | |||
() => { | |||
setLoading(false); | |||
} | |||
); | |||
try { | |||
const { configuration } = await getComponentNavigation({ component: project.key }); | |||
const hasAccess = Boolean(configuration?.showPermissions && configuration?.canBrowseProject); | |||
setHasAccess(hasAccess); | |||
setLoading(false); | |||
} catch (error) { | |||
throwGlobalError(error); | |||
} finally { | |||
setLoading(false); | |||
} | |||
}; | |||
const handleDropdownOpen = () => { |
@@ -1313,8 +1313,10 @@ projects.security_hotspots_reviewed=Hotspots Reviewed | |||
# | |||
#------------------------------------------------------------------------------ | |||
dashboard.project_not_found=The requested project does not exist. | |||
dashboard.project_not_found.2=Either it has never been analyzed successfully or it has been deleted. | |||
dashboard.project.not_found=The requested project could not be found. | |||
dashboard.project.not_found.2=Either it has never been analyzed successfully or it has been deleted. | |||
dashboard.portfolio.not_found=The requested portfolio could not be found. | |||
dashboard.portfolio.not_found.2=Either its parent has not been recomputed or it has been deleted. | |||
#------------------------------------------------------------------------------ |