From c6cb86cf12a585711a8a82305f2ab68a4140fe55 Mon Sep 17 00:00:00 2001 From: David Cho-Lerat Date: Tue, 6 Feb 2024 10:58:30 +0100 Subject: [PATCH] SONAR-20463 Do not display permission warning in case the project has not been analyzed --- .../overview/components/EmptyOverview.tsx | 46 +++++++++++++------ .../components/__tests__/App-test.tsx | 33 ++++++++++++- .../tutorials/TutorialSelectionRenderer.tsx | 14 +++++- 3 files changed, 74 insertions(+), 19 deletions(-) diff --git a/server/sonar-web/src/main/js/apps/overview/components/EmptyOverview.tsx b/server/sonar-web/src/main/js/apps/overview/components/EmptyOverview.tsx index b64b65d5302..9c50ad01db4 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/EmptyOverview.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/EmptyOverview.tsx @@ -17,16 +17,20 @@ * 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) { 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 ; } @@ -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 ; } 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 ( - {isLoggedIn(currentUser) ? ( - <> - {hasBranches && ( - - {warning} - - )} - - ) : ( - - {warning} - - )} + + {warning} + ); diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx index 978b484420b..ae106dde1f1 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx @@ -19,18 +19,30 @@ */ 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()] }); diff --git a/server/sonar-web/src/main/js/components/tutorials/TutorialSelectionRenderer.tsx b/server/sonar-web/src/main/js/components/tutorials/TutorialSelectionRenderer.tsx index b84f6b21849..b057238a387 100644 --- a/server/sonar-web/src/main/js/components/tutorials/TutorialSelectionRenderer.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/TutorialSelectionRenderer.tsx @@ -17,8 +17,10 @@ * 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')} )} + {mode === TutorialModes.OtherCI && ( {translate('onboarding.mode.help.otherci')} @@ -106,7 +108,11 @@ export default function TutorialSelectionRenderer(props: TutorialSelectionRender } if (!currentUserCanScanProject) { - return {translate('onboarding.tutorial.no_scan_rights')}; + return ( + + {translate('onboarding.tutorial.no_scan_rights')} + + ); } 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 {translate('onboarding.tutorial.page.title')} + {translate('onboarding.tutorial.page.description')} @@ -200,6 +208,7 @@ export default function TutorialSelectionRenderer(props: TutorialSelectionRender )} {renderAlm(TutorialModes.OtherCI, component.key)} + {renderAlm(TutorialModes.Local, component.key)} @@ -210,6 +219,7 @@ export default function TutorialSelectionRenderer(props: TutorialSelectionRender {translate('onboarding.tutorial.breadcrumbs.home')} + {translate('onboarding.tutorial.breadcrumbs', selectedTutorial)} -- 2.39.5