]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19435 Infer component type from URL to make component not found error more...
author7PH <benjamin.raymond@sonarsource.com>
Wed, 30 Aug 2023 07:14:39 +0000 (09:14 +0200)
committersonartech <sonartech@sonarsource.com>
Thu, 31 Aug 2023 20:02:56 +0000 (20:02 +0000)
server/sonar-web/src/main/js/api/components.ts
server/sonar-web/src/main/js/api/navigation.ts
server/sonar-web/src/main/js/app/components/ComponentContainer.tsx
server/sonar-web/src/main/js/app/components/ComponentContainerNotFound.tsx
server/sonar-web/src/main/js/app/components/__tests__/ComponentContainerNotFound-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/ComponentContainer-test.tsx.snap
server/sonar-web/src/main/js/apps/projects/components/ApplicationCreation.tsx
server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index e57e665cfa5e2eb7d3c9da33934d6faeea7e956e..50c26818095b36086fef856f627dac75b9d2c688 100644 (file)
@@ -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'>>> {
index a6a5f87698bea920fb84fcbd4e2f90bdd0c45b90..001b6b97fb09eda07523055b51ebed8d915b3e81 100644 (file)
@@ -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 }> {
index 25e731d7e2e1419dc5dbf01451baf67b38001e7f..60d59d84e5307a26c6091a843904fc17bf1b8ef7 100644 (file)
@@ -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" />
index 6e783932f97857a4202abb68df980d9c84cf4449..9c47e9eb6c62613986f7a6c9a66dc956d3fce1f8 100644 (file)
@@ -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>
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 (file)
index 0000000..ec1f455
--- /dev/null
@@ -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} />);
+}
index b71a0038760a37e3f142b25e25d8d177ff4c68c4..a2eba82bd3d3e89be270c7607ecf71f46717a0d3 100644 (file)
@@ -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}
+/>
+`;
index cd4cb176e5f5f6f50d03aa85514426e48d17e0de..c56f5ca09576577e93577591e95e4579a2e3172b 100644 (file)
@@ -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 (
index 64a78adfdd5c1ac49a907175b4ce20d064784abc..2f221c82070309c5b27b330980f20eb8c0e60a9d 100644 (file)
@@ -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 = () => {
index c923765068afd27b29b2e345f62174cabd862ab9..b1be77cd82c6f9970803dee6994b8ef485d3aa5c 100644 (file)
@@ -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.
 
 
 #------------------------------------------------------------------------------