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