From f8f36e7876d1d0d20972d17bb66a63ff7e836340 Mon Sep 17 00:00:00 2001 From: 7PH Date: Wed, 30 Aug 2023 09:14:39 +0200 Subject: [PATCH] SONAR-19435 Infer component type from URL to make component not found error more explicit --- .../sonar-web/src/main/js/api/components.ts | 4 -- .../sonar-web/src/main/js/api/navigation.ts | 2 +- .../js/app/components/ComponentContainer.tsx | 18 ++++---- .../components/ComponentContainerNotFound.tsx | 16 +++++-- .../ComponentContainerNotFound-test.tsx | 42 +++++++++++++++++++ .../ComponentContainer-test.tsx.snap | 6 ++- .../components/ApplicationCreation.tsx | 19 +++++---- .../projectsManagement/ProjectRowActions.tsx | 26 ++++++------ .../resources/org/sonar/l10n/core.properties | 6 ++- 9 files changed, 98 insertions(+), 41 deletions(-) create mode 100644 server/sonar-web/src/main/js/app/components/__tests__/ComponentContainerNotFound-test.tsx diff --git a/server/sonar-web/src/main/js/api/components.ts b/server/sonar-web/src/main/js/api/components.ts index e57e665cfa5..50c26818095 100644 --- a/server/sonar-web/src/main/js/api/components.ts +++ b/server/sonar-web/src/main/js/api/components.ts @@ -160,10 +160,6 @@ export function getComponentShow(data: { component: string } & BranchParameters) return getComponentData(data).catch(throwGlobalError); } -export function getParents(component: string): Promise { - return getComponentShow({ component }).then((r) => r.ancestors); -} - export function getBreadcrumbs( data: { component: string } & BranchParameters ): Promise>> { diff --git a/server/sonar-web/src/main/js/api/navigation.ts b/server/sonar-web/src/main/js/api/navigation.ts index a6a5f87698b..001b6b97fb0 100644 --- a/server/sonar-web/src/main/js/api/navigation.ts +++ b/server/sonar-web/src/main/js/api/navigation.ts @@ -26,7 +26,7 @@ import { Extension, NavigationComponent } from '../types/types'; export function getComponentNavigation( data: { component: string } & BranchParameters ): Promise { - return getJSON('/api/navigation/component', data).catch(throwGlobalError); + return getJSON('/api/navigation/component', data); } export function getMarketplaceNavigation(): Promise<{ serverId: string; ncloc: number }> { diff --git a/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx b/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx index 25e731d7e2e..60d59d84e53 100644 --- a/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx @@ -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 { * 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 { 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 { const { component, loading } = this.state; if (!loading && !component) { - return ; + return ( + + ); } const { currentTask, isPending, projectBindingErrors, tasksInProgress } = this.state; @@ -335,11 +339,8 @@ export class ComponentContainer extends React.PureComponent { 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 { />, this.portalAnchor )} - {loading ? (
diff --git a/server/sonar-web/src/main/js/app/components/ComponentContainerNotFound.tsx b/server/sonar-web/src/main/js/app/components/ComponentContainerNotFound.tsx index 6e783932f97..9c47e9eb6c6 100644 --- a/server/sonar-web/src/main/js/app/components/ComponentContainerNotFound.tsx +++ b/server/sonar-web/src/main/js/app/components/ComponentContainerNotFound.tsx @@ -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 ( <>
-

{translate('dashboard.project_not_found')}

-

{translate('dashboard.project_not_found.2')}

+

+ {translate('dashboard', componentType, 'not_found')} +

+

{translate('dashboard', componentType, 'not_found.2')}

{translate('go_back_to_homepage')}

diff --git a/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainerNotFound-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainerNotFound-test.tsx new file mode 100644 index 00000000000..ec1f455c92b --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainerNotFound-test.tsx @@ -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(); +} diff --git a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/ComponentContainer-test.tsx.snap b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/ComponentContainer-test.tsx.snap index b71a0038760..a2eba82bd3d 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/ComponentContainer-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/ComponentContainer-test.tsx.snap @@ -1,3 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should show component not found if it does not exist 1`] = ``; +exports[`should show component not found if it does not exist 1`] = ` + +`; diff --git a/server/sonar-web/src/main/js/apps/projects/components/ApplicationCreation.tsx b/server/sonar-web/src/main/js/apps/projects/components/ApplicationCreation.tsx index cd4cb176e5f..c56f5ca0957 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/ApplicationCreation.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/ApplicationCreation.tsx @@ -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 ( diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx index 64a78adfdd5..2f221c82070 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx @@ -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 = () => { diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index c923765068a..b1be77cd82c 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -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. #------------------------------------------------------------------------------ -- 2.39.5