]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20463 Do not display permission warning in case the project has not been analyzed
authorDavid Cho-Lerat <david.cho-lerat@sonarsource.com>
Tue, 6 Feb 2024 09:58:30 +0000 (10:58 +0100)
committersonartech <sonartech@sonarsource.com>
Thu, 8 Feb 2024 20:02:45 +0000 (20:02 +0000)
server/sonar-web/src/main/js/apps/overview/components/EmptyOverview.tsx
server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx
server/sonar-web/src/main/js/components/tutorials/TutorialSelectionRenderer.tsx

index b64b65d5302e5923136dab4beac6487e64c5c280..9c50ad01db4c8715116ce52798c255278122f1c7 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+
 import { FlagMessage, LargeCenteredLayout, PageContentFontWrapper, Spinner } from 'design-system';
 import * as React from 'react';
 import { Navigate } from 'react-router-dom';
+import { getScannableProjects } from '../../../api/components';
 import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext';
 import { getBranchLikeDisplayName, isBranch, isMainBranch } from '../../../helpers/branch-like';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { getProjectTutorialLocation } from '../../../helpers/urls';
+import { hasGlobalPermission } from '../../../helpers/users';
 import { useTaskForComponentQuery } from '../../../queries/component';
 import { BranchLike } from '../../../types/branch-like';
 import { ComponentQualifier } from '../../../types/component';
+import { Permissions } from '../../../types/permissions';
 import { TaskTypes } from '../../../types/tasks';
 import { Component } from '../../../types/types';
 import { CurrentUser, isLoggedIn } from '../../../types/users';
@@ -38,16 +42,35 @@ export interface EmptyOverviewProps {
   currentUser: CurrentUser;
 }
 
-export function EmptyOverview(props: EmptyOverviewProps) {
+export function EmptyOverview(props: Readonly<EmptyOverviewProps>) {
   const { branchLike, branchLikes, component, currentUser } = props;
+
+  const [currentUserCanScanProject, setCurrentUserCanScanProject] = React.useState(
+    hasGlobalPermission(currentUser, Permissions.Scan),
+  );
+
   const { data, isLoading } = useTaskForComponentQuery(component);
+
   const hasQueuedAnalyses =
     data && data.queue.filter((task) => task.type === TaskTypes.Report).length > 0;
+
   const hasPermissionSyncInProgess =
     data &&
     data.queue.filter((task) => task.type === TaskTypes.GithubProjectPermissionsProvisioning)
       .length > 0;
 
+  React.useEffect(() => {
+    if (currentUserCanScanProject || !isLoggedIn(currentUser)) {
+      return;
+    }
+
+    getScannableProjects()
+      .then(({ projects }) => {
+        setCurrentUserCanScanProject(projects.find((p) => p.key === component.key) !== undefined);
+      })
+      .catch(() => {});
+  }, [component.key, currentUser, currentUserCanScanProject]);
+
   if (isLoading) {
     return <Spinner loading />;
   }
@@ -65,6 +88,7 @@ export function EmptyOverview(props: EmptyOverviewProps) {
   }
 
   const hasBranches = branchLikes.length > 1;
+
   const hasBadBranchConfig =
     branchLikes.length > 2 ||
     (branchLikes.length === 2 && branchLikes.some((branch) => isBranch(branch)));
@@ -82,13 +106,15 @@ export function EmptyOverview(props: EmptyOverviewProps) {
     );
   }
 
-  const showTutorial = isMainBranch(branchLike) && !hasBranches && !hasQueuedAnalyses;
+  const showTutorial =
+    currentUserCanScanProject && isMainBranch(branchLike) && !hasBranches && !hasQueuedAnalyses;
 
   if (showTutorial && isLoggedIn(currentUser)) {
     return <Navigate replace to={getProjectTutorialLocation(component.key)} />;
   }
 
   let warning;
+
   if (isLoggedIn(currentUser) && isMainBranch(branchLike) && hasBranches && hasBadBranchConfig) {
     warning = translateWithParameters(
       'provisioning.no_analysis_on_main_branch.bad_configuration',
@@ -105,19 +131,9 @@ export function EmptyOverview(props: EmptyOverviewProps) {
   return (
     <LargeCenteredLayout className="sw-pt-8">
       <PageContentFontWrapper>
-        {isLoggedIn(currentUser) ? (
-          <>
-            {hasBranches && (
-              <FlagMessage className="sw-w-full" variant="warning">
-                {warning}
-              </FlagMessage>
-            )}
-          </>
-        ) : (
-          <FlagMessage className="sw-w-full" variant="warning">
-            {warning}
-          </FlagMessage>
-        )}
+        <FlagMessage className="sw-w-full" variant="warning">
+          {warning}
+        </FlagMessage>
       </PageContentFontWrapper>
     </LargeCenteredLayout>
   );
index 978b484420b9fc496a5b1fbbc267520fbfcbb84f..ae106dde1f1717174ffc6b120c96c21d54916464 100644 (file)
  */
 import { screen, waitFor } from '@testing-library/react';
 import * as React from 'react';
+import { getScannableProjects } from '../../../../api/components';
 import BranchesServiceMock from '../../../../api/mocks/BranchesServiceMock';
 import ComputeEngineServiceMock from '../../../../api/mocks/ComputeEngineServiceMock';
 import CurrentUserContextProvider from '../../../../app/components/current-user/CurrentUserContextProvider';
-import { mockBranch } from '../../../../helpers/mocks/branch-like';
+import { mockBranch, mockMainBranch } from '../../../../helpers/mocks/branch-like';
 import { mockComponent } from '../../../../helpers/mocks/component';
 import { mockTask } from '../../../../helpers/mocks/tasks';
-import { mockCurrentUser } from '../../../../helpers/testMocks';
+import { mockCurrentUser, mockLoggedInUser } from '../../../../helpers/testMocks';
 import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { getProjectTutorialLocation } from '../../../../helpers/urls';
 import { ComponentQualifier } from '../../../../types/component';
 import { TaskStatuses, TaskTypes } from '../../../../types/tasks';
 import { App } from '../App';
 
+jest.mock('../../../../api/components', () => ({
+  ...jest.requireActual('../../../../api/components'),
+  getScannableProjects: jest.fn().mockResolvedValue({ projects: [] }),
+}));
+
+jest.mock('../../../../helpers/urls', () => ({
+  ...jest.requireActual('../../../../helpers/urls'),
+  getProjectTutorialLocation: jest.fn().mockResolvedValue({ pathname: '/tutorial' }),
+}));
+
 const handlerBranches = new BranchesServiceMock();
 const handlerCe = new ComputeEngineServiceMock();
 
@@ -57,6 +69,23 @@ it('should render Empty Overview on main branch with no analysis', async () => {
   ).toBeInTheDocument();
 });
 
+it('should redirect to tutorial when the user can scan a project that has no analysis yet', async () => {
+  handlerBranches.emptyBranchesAndPullRequest();
+  handlerBranches.addBranch(mockMainBranch());
+
+  jest
+    .mocked(getScannableProjects)
+    .mockResolvedValueOnce({ projects: [{ key: 'my-project', name: 'MyProject' }] });
+
+  renderApp({}, mockLoggedInUser());
+
+  await appLoaded();
+
+  await waitFor(() => {
+    expect(getProjectTutorialLocation).toHaveBeenCalled();
+  });
+});
+
 it('should render Empty Overview on main branch with multiple branches with bad configuration', async () => {
   renderApp({ branchLikes: [mockBranch(), mockBranch()] });
 
index b84f6b21849cc96619ef5d8312e286a1ef57648c..b057238a387521ec783c186e75e8c9fa5f9d465c 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+
 import {
   Breadcrumbs,
+  FlagMessage,
   GreyCard,
   HoverLink,
   LightLabel,
@@ -38,7 +40,6 @@ import { AlmKeys, AlmSettingsInstance, ProjectAlmBindingResponse } from '../../t
 import { MainBranch } from '../../types/branch-like';
 import { Component } from '../../types/types';
 import { LoggedInUser } from '../../types/users';
-import { Alert } from '../ui/Alert';
 import AzurePipelinesTutorial from './azure-pipelines/AzurePipelinesTutorial';
 import BitbucketPipelinesTutorial from './bitbucket-pipelines/BitbucketPipelinesTutorial';
 import GitHubActionTutorial from './github-action/GitHubActionTutorial';
@@ -73,6 +74,7 @@ function renderAlm(mode: TutorialModes, project: string, icon?: React.ReactNode)
           {translate('onboarding.mode.help.manual')}
         </LightLabel>
       )}
+
       {mode === TutorialModes.OtherCI && (
         <LightLabel as="p" className="sw-mt-3">
           {translate('onboarding.mode.help.otherci')}
@@ -106,7 +108,11 @@ export default function TutorialSelectionRenderer(props: TutorialSelectionRender
   }
 
   if (!currentUserCanScanProject) {
-    return <Alert variant="warning">{translate('onboarding.tutorial.no_scan_rights')}</Alert>;
+    return (
+      <FlagMessage className="sw-w-full" variant="warning">
+        {translate('onboarding.tutorial.no_scan_rights')}
+      </FlagMessage>
+    );
   }
 
   let showGitHubActions = true;
@@ -120,6 +126,7 @@ export default function TutorialSelectionRenderer(props: TutorialSelectionRender
     showGitLabCICD = projectBinding.alm === AlmKeys.GitLab;
     showBitbucketPipelines = projectBinding.alm === AlmKeys.BitbucketCloud;
     showAzurePipelines = [AlmKeys.Azure, AlmKeys.GitHub].includes(projectBinding.alm);
+
     showJenkins = [
       AlmKeys.BitbucketCloud,
       AlmKeys.BitbucketServer,
@@ -137,6 +144,7 @@ export default function TutorialSelectionRenderer(props: TutorialSelectionRender
           <Title className="sw-mb-6 sw-heading-lg">
             {translate('onboarding.tutorial.page.title')}
           </Title>
+
           <LightPrimary>{translate('onboarding.tutorial.page.description')}</LightPrimary>
 
           <SubTitle className="sw-mt-12 sw-mb-4 sw-heading-md">
@@ -200,6 +208,7 @@ export default function TutorialSelectionRenderer(props: TutorialSelectionRender
               )}
 
             {renderAlm(TutorialModes.OtherCI, component.key)}
+
             {renderAlm(TutorialModes.Local, component.key)}
           </div>
         </div>
@@ -210,6 +219,7 @@ export default function TutorialSelectionRenderer(props: TutorialSelectionRender
           <HoverLink to={getProjectTutorialLocation(component.key)}>
             {translate('onboarding.tutorial.breadcrumbs.home')}
           </HoverLink>
+
           <HoverLink to={getProjectTutorialLocation(component.key, selectedTutorial)}>
             {translate('onboarding.tutorial.breadcrumbs', selectedTutorial)}
           </HoverLink>