@@ -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<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> | |||
); |
@@ -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()] }); | |||
@@ -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')} | |||
</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> |