Browse Source

SONAR-20463 Do not display permission warning in case the project has not been analyzed

tags/10.5.0.89998
David Cho-Lerat 3 months ago
parent
commit
c6cb86cf12

+ 31
- 15
server/sonar-web/src/main/js/apps/overview/components/EmptyOverview.tsx View File

* along with this program; if not, write to the Free Software Foundation, * along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/ */

import { FlagMessage, LargeCenteredLayout, PageContentFontWrapper, Spinner } from 'design-system'; import { FlagMessage, LargeCenteredLayout, PageContentFontWrapper, Spinner } from 'design-system';
import * as React from 'react'; import * as React from 'react';
import { Navigate } from 'react-router-dom'; import { Navigate } from 'react-router-dom';
import { getScannableProjects } from '../../../api/components';
import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext'; import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext';
import { getBranchLikeDisplayName, isBranch, isMainBranch } from '../../../helpers/branch-like'; import { getBranchLikeDisplayName, isBranch, isMainBranch } from '../../../helpers/branch-like';
import { translate, translateWithParameters } from '../../../helpers/l10n'; import { translate, translateWithParameters } from '../../../helpers/l10n';
import { getProjectTutorialLocation } from '../../../helpers/urls'; import { getProjectTutorialLocation } from '../../../helpers/urls';
import { hasGlobalPermission } from '../../../helpers/users';
import { useTaskForComponentQuery } from '../../../queries/component'; import { useTaskForComponentQuery } from '../../../queries/component';
import { BranchLike } from '../../../types/branch-like'; import { BranchLike } from '../../../types/branch-like';
import { ComponentQualifier } from '../../../types/component'; import { ComponentQualifier } from '../../../types/component';
import { Permissions } from '../../../types/permissions';
import { TaskTypes } from '../../../types/tasks'; import { TaskTypes } from '../../../types/tasks';
import { Component } from '../../../types/types'; import { Component } from '../../../types/types';
import { CurrentUser, isLoggedIn } from '../../../types/users'; import { CurrentUser, isLoggedIn } from '../../../types/users';
currentUser: CurrentUser; currentUser: CurrentUser;
} }


export function EmptyOverview(props: EmptyOverviewProps) {
export function EmptyOverview(props: Readonly<EmptyOverviewProps>) {
const { branchLike, branchLikes, component, currentUser } = props; const { branchLike, branchLikes, component, currentUser } = props;

const [currentUserCanScanProject, setCurrentUserCanScanProject] = React.useState(
hasGlobalPermission(currentUser, Permissions.Scan),
);

const { data, isLoading } = useTaskForComponentQuery(component); const { data, isLoading } = useTaskForComponentQuery(component);

const hasQueuedAnalyses = const hasQueuedAnalyses =
data && data.queue.filter((task) => task.type === TaskTypes.Report).length > 0; data && data.queue.filter((task) => task.type === TaskTypes.Report).length > 0;

const hasPermissionSyncInProgess = const hasPermissionSyncInProgess =
data && data &&
data.queue.filter((task) => task.type === TaskTypes.GithubProjectPermissionsProvisioning) data.queue.filter((task) => task.type === TaskTypes.GithubProjectPermissionsProvisioning)
.length > 0; .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) { if (isLoading) {
return <Spinner loading />; return <Spinner loading />;
} }
} }


const hasBranches = branchLikes.length > 1; const hasBranches = branchLikes.length > 1;

const hasBadBranchConfig = const hasBadBranchConfig =
branchLikes.length > 2 || branchLikes.length > 2 ||
(branchLikes.length === 2 && branchLikes.some((branch) => isBranch(branch))); (branchLikes.length === 2 && branchLikes.some((branch) => isBranch(branch)));
); );
} }


const showTutorial = isMainBranch(branchLike) && !hasBranches && !hasQueuedAnalyses;
const showTutorial =
currentUserCanScanProject && isMainBranch(branchLike) && !hasBranches && !hasQueuedAnalyses;


if (showTutorial && isLoggedIn(currentUser)) { if (showTutorial && isLoggedIn(currentUser)) {
return <Navigate replace to={getProjectTutorialLocation(component.key)} />; return <Navigate replace to={getProjectTutorialLocation(component.key)} />;
} }


let warning; let warning;

if (isLoggedIn(currentUser) && isMainBranch(branchLike) && hasBranches && hasBadBranchConfig) { if (isLoggedIn(currentUser) && isMainBranch(branchLike) && hasBranches && hasBadBranchConfig) {
warning = translateWithParameters( warning = translateWithParameters(
'provisioning.no_analysis_on_main_branch.bad_configuration', 'provisioning.no_analysis_on_main_branch.bad_configuration',
return ( return (
<LargeCenteredLayout className="sw-pt-8"> <LargeCenteredLayout className="sw-pt-8">
<PageContentFontWrapper> <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> </PageContentFontWrapper>
</LargeCenteredLayout> </LargeCenteredLayout>
); );

+ 31
- 2
server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx View File

*/ */
import { screen, waitFor } from '@testing-library/react'; import { screen, waitFor } from '@testing-library/react';
import * as React from 'react'; import * as React from 'react';
import { getScannableProjects } from '../../../../api/components';
import BranchesServiceMock from '../../../../api/mocks/BranchesServiceMock'; import BranchesServiceMock from '../../../../api/mocks/BranchesServiceMock';
import ComputeEngineServiceMock from '../../../../api/mocks/ComputeEngineServiceMock'; import ComputeEngineServiceMock from '../../../../api/mocks/ComputeEngineServiceMock';
import CurrentUserContextProvider from '../../../../app/components/current-user/CurrentUserContextProvider'; 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 { mockComponent } from '../../../../helpers/mocks/component';
import { mockTask } from '../../../../helpers/mocks/tasks'; import { mockTask } from '../../../../helpers/mocks/tasks';
import { mockCurrentUser } from '../../../../helpers/testMocks';
import { mockCurrentUser, mockLoggedInUser } from '../../../../helpers/testMocks';
import { renderComponent } from '../../../../helpers/testReactTestingUtils'; import { renderComponent } from '../../../../helpers/testReactTestingUtils';
import { getProjectTutorialLocation } from '../../../../helpers/urls';
import { ComponentQualifier } from '../../../../types/component'; import { ComponentQualifier } from '../../../../types/component';
import { TaskStatuses, TaskTypes } from '../../../../types/tasks'; import { TaskStatuses, TaskTypes } from '../../../../types/tasks';
import { App } from '../App'; 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 handlerBranches = new BranchesServiceMock();
const handlerCe = new ComputeEngineServiceMock(); const handlerCe = new ComputeEngineServiceMock();


).toBeInTheDocument(); ).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 () => { it('should render Empty Overview on main branch with multiple branches with bad configuration', async () => {
renderApp({ branchLikes: [mockBranch(), mockBranch()] }); renderApp({ branchLikes: [mockBranch(), mockBranch()] });



+ 12
- 2
server/sonar-web/src/main/js/components/tutorials/TutorialSelectionRenderer.tsx View File

* along with this program; if not, write to the Free Software Foundation, * along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/ */

import { import {
Breadcrumbs, Breadcrumbs,
FlagMessage,
GreyCard, GreyCard,
HoverLink, HoverLink,
LightLabel, LightLabel,
import { MainBranch } from '../../types/branch-like'; import { MainBranch } from '../../types/branch-like';
import { Component } from '../../types/types'; import { Component } from '../../types/types';
import { LoggedInUser } from '../../types/users'; import { LoggedInUser } from '../../types/users';
import { Alert } from '../ui/Alert';
import AzurePipelinesTutorial from './azure-pipelines/AzurePipelinesTutorial'; import AzurePipelinesTutorial from './azure-pipelines/AzurePipelinesTutorial';
import BitbucketPipelinesTutorial from './bitbucket-pipelines/BitbucketPipelinesTutorial'; import BitbucketPipelinesTutorial from './bitbucket-pipelines/BitbucketPipelinesTutorial';
import GitHubActionTutorial from './github-action/GitHubActionTutorial'; import GitHubActionTutorial from './github-action/GitHubActionTutorial';
{translate('onboarding.mode.help.manual')} {translate('onboarding.mode.help.manual')}
</LightLabel> </LightLabel>
)} )}

{mode === TutorialModes.OtherCI && ( {mode === TutorialModes.OtherCI && (
<LightLabel as="p" className="sw-mt-3"> <LightLabel as="p" className="sw-mt-3">
{translate('onboarding.mode.help.otherci')} {translate('onboarding.mode.help.otherci')}
} }


if (!currentUserCanScanProject) { 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; let showGitHubActions = true;
showGitLabCICD = projectBinding.alm === AlmKeys.GitLab; showGitLabCICD = projectBinding.alm === AlmKeys.GitLab;
showBitbucketPipelines = projectBinding.alm === AlmKeys.BitbucketCloud; showBitbucketPipelines = projectBinding.alm === AlmKeys.BitbucketCloud;
showAzurePipelines = [AlmKeys.Azure, AlmKeys.GitHub].includes(projectBinding.alm); showAzurePipelines = [AlmKeys.Azure, AlmKeys.GitHub].includes(projectBinding.alm);

showJenkins = [ showJenkins = [
AlmKeys.BitbucketCloud, AlmKeys.BitbucketCloud,
AlmKeys.BitbucketServer, AlmKeys.BitbucketServer,
<Title className="sw-mb-6 sw-heading-lg"> <Title className="sw-mb-6 sw-heading-lg">
{translate('onboarding.tutorial.page.title')} {translate('onboarding.tutorial.page.title')}
</Title> </Title>

<LightPrimary>{translate('onboarding.tutorial.page.description')}</LightPrimary> <LightPrimary>{translate('onboarding.tutorial.page.description')}</LightPrimary>


<SubTitle className="sw-mt-12 sw-mb-4 sw-heading-md"> <SubTitle className="sw-mt-12 sw-mb-4 sw-heading-md">
)} )}


{renderAlm(TutorialModes.OtherCI, component.key)} {renderAlm(TutorialModes.OtherCI, component.key)}

{renderAlm(TutorialModes.Local, component.key)} {renderAlm(TutorialModes.Local, component.key)}
</div> </div>
</div> </div>
<HoverLink to={getProjectTutorialLocation(component.key)}> <HoverLink to={getProjectTutorialLocation(component.key)}>
{translate('onboarding.tutorial.breadcrumbs.home')} {translate('onboarding.tutorial.breadcrumbs.home')}
</HoverLink> </HoverLink>

<HoverLink to={getProjectTutorialLocation(component.key, selectedTutorial)}> <HoverLink to={getProjectTutorialLocation(component.key, selectedTutorial)}>
{translate('onboarding.tutorial.breadcrumbs', selectedTutorial)} {translate('onboarding.tutorial.breadcrumbs', selectedTutorial)}
</HoverLink> </HoverLink>

Loading…
Cancel
Save