@@ -23,7 +23,8 @@ import { | |||
BitbucketProject, | |||
BitbucketRepository, | |||
GithubOrganization, | |||
GithubRepository | |||
GithubRepository, | |||
GitlabProject | |||
} from '../types/alm-integration'; | |||
import { ProjectBase } from './components'; | |||
@@ -120,16 +121,33 @@ export function getGithubOrganizations( | |||
export function getGithubRepositories(data: { | |||
almSetting: string; | |||
organization: string; | |||
ps: number; | |||
p?: number; | |||
pageSize: number; | |||
page?: number; | |||
query?: string; | |||
}): Promise<{ repositories: GithubRepository[]; paging: T.Paging }> { | |||
const { almSetting, organization, ps, p = 1, query } = data; | |||
const { almSetting, organization, pageSize, page = 1, query } = data; | |||
return getJSON('/api/alm_integrations/list_github_repositories', { | |||
almSetting, | |||
organization, | |||
p, | |||
ps, | |||
p: page, | |||
ps: pageSize, | |||
q: query || undefined | |||
}).catch(throwGlobalError); | |||
} | |||
export function getGitlabProjects(data: { | |||
almSetting: string; | |||
page?: number; | |||
pageSize?: number; | |||
query?: string; | |||
}): Promise<{ projects: GitlabProject[]; projectsPaging: T.Paging }> { | |||
const { almSetting, pageSize, page, query } = data; | |||
return getJSON('/api/alm_integrations/search_gitlab_repos', { | |||
almSetting, | |||
projectName: query || undefined, | |||
p: page, | |||
ps: pageSize | |||
}) | |||
.then(({ repositories, paging }) => ({ projects: repositories, projectsPaging: paging })) | |||
.catch(throwGlobalError); | |||
} |
@@ -138,6 +138,7 @@ export class CreateProjectPage extends React.PureComponent<Props, State> { | |||
loadingBindings={loading} | |||
location={location} | |||
onProjectCreate={this.handleProjectCreate} | |||
router={router} | |||
settings={gitlabSettings} | |||
/> | |||
); |
@@ -176,8 +176,8 @@ export default class GitHubProjectCreate extends React.Component<Props, State> { | |||
const data = await getGithubRepositories({ | |||
almSetting: settings.key, | |||
organization: organizationKey, | |||
ps: REPOSITORY_PAGE_SIZE, | |||
p: page, | |||
pageSize: REPOSITORY_PAGE_SIZE, | |||
page, | |||
query | |||
}); | |||
@@ -21,12 +21,14 @@ import * as React from 'react'; | |||
import { WithRouterProps } from 'react-router'; | |||
import { | |||
checkPersonalAccessTokenIsValid, | |||
getGitlabProjects, | |||
setAlmPersonalAccessToken | |||
} from '../../../api/alm-integrations'; | |||
import { GitlabProject } from '../../../types/alm-integration'; | |||
import { AlmSettingsInstance } from '../../../types/alm-settings'; | |||
import GitlabProjectCreateRenderer from './GitlabProjectCreateRenderer'; | |||
interface Props extends Pick<WithRouterProps, 'location'> { | |||
interface Props extends Pick<WithRouterProps, 'location' | 'router'> { | |||
canAdmin: boolean; | |||
loadingBindings: boolean; | |||
onProjectCreate: (projectKeys: string[]) => void; | |||
@@ -35,20 +37,32 @@ interface Props extends Pick<WithRouterProps, 'location'> { | |||
interface State { | |||
loading: boolean; | |||
loadingMore: boolean; | |||
projects?: GitlabProject[]; | |||
projectsPaging: T.Paging; | |||
submittingToken: boolean; | |||
tokenIsValid: boolean; | |||
tokenValidationFailed: boolean; | |||
searching: boolean; | |||
searchQuery: string; | |||
settings?: AlmSettingsInstance; | |||
} | |||
const GITLAB_PROJECTS_PAGESIZE = 30; | |||
export default class GitlabProjectCreate extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { | |||
loading: false, | |||
loadingMore: false, | |||
projectsPaging: { pageIndex: 1, total: 0, pageSize: GITLAB_PROJECTS_PAGESIZE }, | |||
tokenIsValid: false, | |||
searching: false, | |||
searchQuery: '', | |||
settings: props.settings.length === 1 ? props.settings[0] : undefined, | |||
submittingToken: false, | |||
tokenValidationFailed: false | |||
@@ -78,11 +92,27 @@ export default class GitlabProjectCreate extends React.PureComponent<Props, Stat | |||
const tokenIsValid = await this.checkPersonalAccessToken(); | |||
let result; | |||
if (tokenIsValid) { | |||
result = await this.fetchProjects(); | |||
} | |||
if (this.mounted) { | |||
this.setState({ | |||
tokenIsValid, | |||
loading: false | |||
}); | |||
if (result) { | |||
const { projects, projectsPaging } = result; | |||
this.setState({ | |||
tokenIsValid, | |||
loading: false, | |||
projects, | |||
projectsPaging | |||
}); | |||
} else { | |||
this.setState({ | |||
tokenIsValid, | |||
loading: false | |||
}); | |||
} | |||
} | |||
}; | |||
@@ -96,7 +126,61 @@ export default class GitlabProjectCreate extends React.PureComponent<Props, Stat | |||
return checkPersonalAccessTokenIsValid(settings.key).catch(() => false); | |||
}; | |||
handlePersonalAccessTokenCreate = (token: string) => { | |||
fetchProjects = (pageIndex = 1, query?: string) => { | |||
const { settings } = this.state; | |||
if (!settings) { | |||
return Promise.resolve(undefined); | |||
} | |||
return getGitlabProjects({ | |||
almSetting: settings.key, | |||
page: pageIndex, | |||
pageSize: GITLAB_PROJECTS_PAGESIZE, | |||
query | |||
}).catch(() => undefined); | |||
}; | |||
handleLoadMore = async () => { | |||
this.setState({ loadingMore: true }); | |||
const { | |||
projectsPaging: { pageIndex }, | |||
searchQuery | |||
} = this.state; | |||
const result = await this.fetchProjects(pageIndex + 1, searchQuery); | |||
if (this.mounted) { | |||
this.setState(({ projects = [], projectsPaging }) => ({ | |||
loadingMore: false, | |||
projects: result ? [...projects, ...result.projects] : projects, | |||
projectsPaging: result ? result.projectsPaging : projectsPaging | |||
})); | |||
} | |||
}; | |||
handleSearch = async (searchQuery: string) => { | |||
this.setState({ searching: true, searchQuery }); | |||
const result = await this.fetchProjects(1, searchQuery); | |||
if (this.mounted) { | |||
this.setState(({ projects, projectsPaging }) => ({ | |||
searching: false, | |||
projects: result ? result.projects : projects, | |||
projectsPaging: result ? result.projectsPaging : projectsPaging | |||
})); | |||
} | |||
}; | |||
cleanUrl = () => { | |||
const { location, router } = this.props; | |||
delete location.query.resetPat; | |||
router.replace(location); | |||
}; | |||
handlePersonalAccessTokenCreate = async (token: string) => { | |||
const { settings } = this.state; | |||
if (!settings || token.length < 1) { | |||
@@ -104,37 +188,59 @@ export default class GitlabProjectCreate extends React.PureComponent<Props, Stat | |||
} | |||
this.setState({ submittingToken: true, tokenValidationFailed: false }); | |||
setAlmPersonalAccessToken(settings.key, token) | |||
.then(this.checkPersonalAccessToken) | |||
.then(patIsValid => { | |||
if (this.mounted) { | |||
this.setState({ | |||
submittingToken: false, | |||
tokenIsValid: patIsValid, | |||
tokenValidationFailed: !patIsValid | |||
}); | |||
if (patIsValid) { | |||
this.fetchInitialData(); | |||
} | |||
} | |||
}) | |||
.catch(() => { | |||
if (this.mounted) { | |||
this.setState({ submittingToken: false }); | |||
try { | |||
await setAlmPersonalAccessToken(settings.key, token); | |||
const patIsValid = await this.checkPersonalAccessToken(); | |||
if (this.mounted) { | |||
this.setState({ | |||
submittingToken: false, | |||
tokenIsValid: patIsValid, | |||
tokenValidationFailed: !patIsValid | |||
}); | |||
if (patIsValid) { | |||
this.cleanUrl(); | |||
await this.fetchInitialData(); | |||
} | |||
}); | |||
} | |||
} catch (e) { | |||
if (this.mounted) { | |||
this.setState({ submittingToken: false }); | |||
} | |||
} | |||
}; | |||
render() { | |||
const { canAdmin, loadingBindings, location } = this.props; | |||
const { loading, tokenIsValid, settings, submittingToken, tokenValidationFailed } = this.state; | |||
const { | |||
loading, | |||
loadingMore, | |||
projects, | |||
projectsPaging, | |||
tokenIsValid, | |||
searching, | |||
searchQuery, | |||
settings, | |||
submittingToken, | |||
tokenValidationFailed | |||
} = this.state; | |||
return ( | |||
<GitlabProjectCreateRenderer | |||
settings={settings} | |||
canAdmin={canAdmin} | |||
loading={loading || loadingBindings} | |||
loadingMore={loadingMore} | |||
onLoadMore={this.handleLoadMore} | |||
onPersonalAccessTokenCreate={this.handlePersonalAccessTokenCreate} | |||
onSearch={this.handleSearch} | |||
projects={projects} | |||
projectsPaging={projectsPaging} | |||
searching={searching} | |||
searchQuery={searchQuery} | |||
showPersonalAccessTokenForm={!tokenIsValid || Boolean(location.query.resetPat)} | |||
submittingToken={submittingToken} | |||
tokenValidationFailed={tokenValidationFailed} |
@@ -20,15 +20,24 @@ | |||
import * as React from 'react'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { getBaseUrl } from 'sonar-ui-common/helpers/urls'; | |||
import { GitlabProject } from '../../../types/alm-integration'; | |||
import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings'; | |||
import CreateProjectPageHeader from './CreateProjectPageHeader'; | |||
import GitlabProjectSelectionForm from './GitlabProjectSelectionForm'; | |||
import PersonalAccessTokenForm from './PersonalAccessTokenForm'; | |||
import WrongBindingCountAlert from './WrongBindingCountAlert'; | |||
export interface GitlabProjectCreateRendererProps { | |||
canAdmin?: boolean; | |||
loading: boolean; | |||
loadingMore: boolean; | |||
onLoadMore: () => void; | |||
onPersonalAccessTokenCreate: (pat: string) => void; | |||
onSearch: (searchQuery: string) => void; | |||
projects?: GitlabProject[]; | |||
projectsPaging: T.Paging; | |||
searching: boolean; | |||
searchQuery: string; | |||
settings?: AlmSettingsInstance; | |||
showPersonalAccessTokenForm?: boolean; | |||
submittingToken?: boolean; | |||
@@ -39,6 +48,11 @@ export default function GitlabProjectCreateRenderer(props: GitlabProjectCreateRe | |||
const { | |||
canAdmin, | |||
loading, | |||
loadingMore, | |||
projects, | |||
projectsPaging, | |||
searching, | |||
searchQuery, | |||
settings, | |||
showPersonalAccessTokenForm, | |||
submittingToken, | |||
@@ -77,7 +91,15 @@ export default function GitlabProjectCreateRenderer(props: GitlabProjectCreateRe | |||
validationFailed={tokenValidationFailed} | |||
/> | |||
) : ( | |||
<div>Token is valid!</div> | |||
<GitlabProjectSelectionForm | |||
loadingMore={loadingMore} | |||
onLoadMore={props.onLoadMore} | |||
onSearch={props.onSearch} | |||
projects={projects} | |||
projectsPaging={projectsPaging} | |||
searching={searching} | |||
searchQuery={searchQuery} | |||
/> | |||
))} | |||
</> | |||
); |
@@ -0,0 +1,149 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import { Link } from 'react-router'; | |||
import ListFooter from 'sonar-ui-common/components/controls/ListFooter'; | |||
import SearchBox from 'sonar-ui-common/components/controls/SearchBox'; | |||
import Tooltip from 'sonar-ui-common/components/controls/Tooltip'; | |||
import CheckIcon from 'sonar-ui-common/components/icons/CheckIcon'; | |||
import DetachIcon from 'sonar-ui-common/components/icons/DetachIcon'; | |||
import QualifierIcon from 'sonar-ui-common/components/icons/QualifierIcon'; | |||
import { Alert } from 'sonar-ui-common/components/ui/Alert'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { getProjectUrl } from '../../../helpers/urls'; | |||
import { GitlabProject } from '../../../types/alm-integration'; | |||
import { ComponentQualifier } from '../../../types/component'; | |||
import { CreateProjectModes } from './types'; | |||
export interface GitlabProjectSelectionFormProps { | |||
loadingMore: boolean; | |||
onLoadMore: () => void; | |||
onSearch: (searchQuery: string) => void; | |||
projects?: GitlabProject[]; | |||
projectsPaging: T.Paging; | |||
searching: boolean; | |||
searchQuery: string; | |||
} | |||
export default function GitlabProjectSelectionForm(props: GitlabProjectSelectionFormProps) { | |||
const { loadingMore, projects = [], projectsPaging, searching, searchQuery } = props; | |||
if (projects.length === 0 && searchQuery.length === 0 && !searching) { | |||
return ( | |||
<Alert className="spacer-top" variant="warning"> | |||
<FormattedMessage | |||
defaultMessage={translate('onboarding.create_project.gitlab.no_projects')} | |||
id="onboarding.create_project.gitlab.no_projects" | |||
values={{ | |||
link: ( | |||
<Link | |||
to={{ | |||
pathname: '/projects/create', | |||
query: { mode: CreateProjectModes.GitLab, resetPat: 1 } | |||
}}> | |||
{translate('onboarding.create_project.update_your_token')} | |||
</Link> | |||
) | |||
}} | |||
/> | |||
</Alert> | |||
); | |||
} | |||
return ( | |||
<div className="boxed-group big-padded create-project-import-gitlab"> | |||
<SearchBox | |||
className="spacer" | |||
loading={searching} | |||
minLength={3} | |||
onChange={props.onSearch} | |||
placeholder={translate('onboarding.create_project.gitlab.search_prompt')} | |||
/> | |||
<hr /> | |||
{projects.length === 0 ? ( | |||
<div className="padded">{translate('no_results')}</div> | |||
) : ( | |||
<table className="data zebra zebra-hover"> | |||
<tbody> | |||
{projects.map(project => ( | |||
<tr key={project.id}> | |||
<td> | |||
<Tooltip overlay={project.slug}> | |||
<strong className="project-name display-inline-block text-ellipsis"> | |||
{project.name} | |||
</strong> | |||
</Tooltip> | |||
<br /> | |||
<Tooltip overlay={project.pathSlug}> | |||
<span className="text-muted project-path display-inline-block text-ellipsis"> | |||
{project.pathName} | |||
</span> | |||
</Tooltip> | |||
</td> | |||
<td> | |||
<a | |||
className="display-inline-flex-center big-spacer-right" | |||
href={project.url} | |||
rel="noopener noreferrer" | |||
target="_blank"> | |||
<DetachIcon className="little-spacer-right" /> | |||
{translate('onboarding.create_project.gitlab.link')} | |||
</a> | |||
</td> | |||
{project.sqProjectKey ? ( | |||
<> | |||
<td> | |||
<span className="display-flex-center display-flex-justify-end already-set-up"> | |||
<CheckIcon className="little-spacer-right" size={12} /> | |||
{translate('onboarding.create_project.repository_imported')}: | |||
</span> | |||
</td> | |||
<td> | |||
<div className="sq-project-link text-ellipsis"> | |||
<Link to={getProjectUrl(project.sqProjectKey)}> | |||
<QualifierIcon | |||
className="spacer-right" | |||
qualifier={ComponentQualifier.Project} | |||
/> | |||
{project.sqProjectName} | |||
</Link> | |||
</div> | |||
</td> | |||
</> | |||
) : ( | |||
<td colSpan={2}> </td> | |||
)} | |||
</tr> | |||
))} | |||
</tbody> | |||
</table> | |||
)} | |||
<ListFooter | |||
count={projects.length} | |||
loadMore={props.onLoadMore} | |||
loading={loadingMore} | |||
total={projectsPaging.total} | |||
/> | |||
</div> | |||
); | |||
} |
@@ -186,8 +186,8 @@ it('should handle search', async () => { | |||
expect(getGithubRepositories).toBeCalledWith({ | |||
almSetting: 'a', | |||
organization: 'o1', | |||
p: 1, | |||
ps: 30, | |||
page: 1, | |||
pageSize: 30, | |||
query: 'query' | |||
}); | |||
expect(wrapper.state().repositories).toEqual(repositories); |
@@ -23,16 +23,19 @@ import * as React from 'react'; | |||
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; | |||
import { | |||
checkPersonalAccessTokenIsValid, | |||
getGitlabProjects, | |||
setAlmPersonalAccessToken | |||
} from '../../../../api/alm-integrations'; | |||
import { mockGitlabProject } from '../../../../helpers/mocks/alm-integrations'; | |||
import { mockAlmSettingsInstance } from '../../../../helpers/mocks/alm-settings'; | |||
import { mockLocation } from '../../../../helpers/testMocks'; | |||
import { mockLocation, mockRouter } from '../../../../helpers/testMocks'; | |||
import { AlmKeys } from '../../../../types/alm-settings'; | |||
import GitlabProjectCreate from '../GitlabProjectCreate'; | |||
jest.mock('../../../../api/alm-integrations', () => ({ | |||
checkPersonalAccessTokenIsValid: jest.fn().mockResolvedValue(true), | |||
setAlmPersonalAccessToken: jest.fn().mockResolvedValue(null) | |||
setAlmPersonalAccessToken: jest.fn().mockResolvedValue(null), | |||
getGitlabProjects: jest.fn().mockRejectedValue('error') | |||
})); | |||
beforeEach(jest.clearAllMocks); | |||
@@ -75,7 +78,12 @@ it('should correctly handle an invalid PAT', async () => { | |||
}); | |||
describe('setting a new PAT', () => { | |||
const wrapper = shallowRender(); | |||
const routerReplace = jest.fn(); | |||
const wrapper = shallowRender({ router: mockRouter({ replace: routerReplace }) }); | |||
beforeEach(() => { | |||
jest.clearAllMocks(); | |||
}); | |||
it('should correctly handle it if invalid', async () => { | |||
(checkPersonalAccessTokenIsValid as jest.Mock).mockResolvedValueOnce(false); | |||
@@ -99,9 +107,99 @@ describe('setting a new PAT', () => { | |||
expect(checkPersonalAccessTokenIsValid).toBeCalled(); | |||
expect(wrapper.state().submittingToken).toBe(false); | |||
expect(wrapper.state().tokenValidationFailed).toBe(false); | |||
expect(routerReplace).toBeCalled(); | |||
}); | |||
}); | |||
it('should fetch more projects and preserve search', async () => { | |||
(checkPersonalAccessTokenIsValid as jest.Mock).mockResolvedValueOnce(true); | |||
const projects = [ | |||
mockGitlabProject({ id: '1' }), | |||
mockGitlabProject({ id: '2' }), | |||
mockGitlabProject({ id: '3' }), | |||
mockGitlabProject({ id: '4' }), | |||
mockGitlabProject({ id: '5' }), | |||
mockGitlabProject({ id: '6' }) | |||
]; | |||
(getGitlabProjects as jest.Mock) | |||
.mockResolvedValueOnce({ | |||
projects: projects.slice(0, 5), | |||
projectsPaging: { | |||
pageIndex: 1, | |||
pageSize: 4, | |||
total: 6 | |||
} | |||
}) | |||
.mockResolvedValueOnce({ | |||
projects: projects.slice(5), | |||
projectsPaging: { | |||
pageIndex: 2, | |||
pageSize: 4, | |||
total: 6 | |||
} | |||
}); | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
wrapper.setState({ searchQuery: 'query' }); | |||
wrapper.instance().handleLoadMore(); | |||
expect(wrapper.state().loadingMore).toBe(true); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.state().loadingMore).toBe(false); | |||
expect(wrapper.state().projects).toEqual(projects); | |||
expect(getGitlabProjects).toBeCalledWith(expect.objectContaining({ query: 'query' })); | |||
}); | |||
it('should search for projects', async () => { | |||
(checkPersonalAccessTokenIsValid as jest.Mock).mockResolvedValueOnce(true); | |||
const projects = [ | |||
mockGitlabProject({ id: '1' }), | |||
mockGitlabProject({ id: '2' }), | |||
mockGitlabProject({ id: '3' }), | |||
mockGitlabProject({ id: '4' }), | |||
mockGitlabProject({ id: '5' }), | |||
mockGitlabProject({ id: '6' }) | |||
]; | |||
(getGitlabProjects as jest.Mock) | |||
.mockResolvedValueOnce({ | |||
projects, | |||
projectsPaging: { | |||
pageIndex: 1, | |||
pageSize: 6, | |||
total: 6 | |||
} | |||
}) | |||
.mockResolvedValueOnce({ | |||
projects: projects.slice(3, 5), | |||
projectsPaging: { | |||
pageIndex: 1, | |||
pageSize: 6, | |||
total: 2 | |||
} | |||
}); | |||
const query = 'query'; | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
wrapper.instance().handleSearch(query); | |||
expect(wrapper.state().searching).toBe(true); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.state().searching).toBe(false); | |||
expect(wrapper.state().searchQuery).toBe(query); | |||
expect(wrapper.state().projects).toEqual([projects[3], projects[4]]); | |||
expect(getGitlabProjects).toBeCalledWith(expect.objectContaining({ query })); | |||
}); | |||
function shallowRender(props: Partial<GitlabProjectCreate['props']> = {}) { | |||
return shallow<GitlabProjectCreate>( | |||
<GitlabProjectCreate | |||
@@ -109,6 +207,7 @@ function shallowRender(props: Partial<GitlabProjectCreate['props']> = {}) { | |||
loadingBindings={false} | |||
location={mockLocation()} | |||
onProjectCreate={jest.fn()} | |||
router={mockRouter()} | |||
settings={[mockAlmSettingsInstance({ alm: AlmKeys.GitLab, key: almSettingKey })]} | |||
{...props} | |||
/> |
@@ -33,6 +33,9 @@ it('should render correctly', () => { | |||
'invalid settings, admin user' | |||
); | |||
expect(shallowRender()).toMatchSnapshot('pat form'); | |||
expect(shallowRender({ showPersonalAccessTokenForm: false })).toMatchSnapshot( | |||
'project selection form' | |||
); | |||
}); | |||
function shallowRender(props: Partial<GitlabProjectCreateRendererProps> = {}) { | |||
@@ -40,7 +43,14 @@ function shallowRender(props: Partial<GitlabProjectCreateRendererProps> = {}) { | |||
<GitlabProjectCreateRenderer | |||
canAdmin={false} | |||
loading={false} | |||
loadingMore={false} | |||
onLoadMore={jest.fn()} | |||
onPersonalAccessTokenCreate={jest.fn()} | |||
onSearch={jest.fn()} | |||
projects={undefined} | |||
projectsPaging={{ pageIndex: 1, pageSize: 30, total: 0 }} | |||
searching={false} | |||
searchQuery="" | |||
showPersonalAccessTokenForm={true} | |||
submittingToken={false} | |||
tokenValidationFailed={false} |
@@ -0,0 +1,68 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { mockGitlabProject } from '../../../../helpers/mocks/alm-integrations'; | |||
import GitlabProjectSelectionForm, { | |||
GitlabProjectSelectionFormProps | |||
} from '../GitlabProjectSelectionForm'; | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot('projects'); | |||
expect(shallowRender({ projects: undefined, projectsPaging: mockPaging() })).toMatchSnapshot( | |||
'undefined projects' | |||
); | |||
expect(shallowRender({ projects: [], projectsPaging: mockPaging() })).toMatchSnapshot( | |||
'no projects' | |||
); | |||
expect( | |||
shallowRender({ projects: [], projectsPaging: mockPaging(), searchQuery: 'findme' }) | |||
).toMatchSnapshot('no projects when searching'); | |||
}); | |||
function shallowRender(props: Partial<GitlabProjectSelectionFormProps> = {}) { | |||
const projects = [ | |||
mockGitlabProject(), | |||
mockGitlabProject({ | |||
id: '2', | |||
sqProjectKey: 'already-imported', | |||
sqProjectName: 'Already Imported' | |||
}) | |||
]; | |||
return shallow<GitlabProjectSelectionFormProps>( | |||
<GitlabProjectSelectionForm | |||
loadingMore={false} | |||
onLoadMore={jest.fn()} | |||
onSearch={jest.fn()} | |||
projects={projects} | |||
projectsPaging={mockPaging(projects.length)} | |||
searching={false} | |||
searchQuery="" | |||
{...props} | |||
/> | |||
); | |||
} | |||
function mockPaging(total = 0) { | |||
return { total, pageIndex: 1, pageSize: 30 }; | |||
} |
@@ -174,6 +174,19 @@ exports[`should render correctly if the GitLab method is selected 1`] = ` | |||
} | |||
} | |||
onProjectCreate={[Function]} | |||
router={ | |||
Object { | |||
"createHref": [MockFunction], | |||
"createPath": [MockFunction], | |||
"go": [MockFunction], | |||
"goBack": [MockFunction], | |||
"goForward": [MockFunction], | |||
"isActive": [MockFunction], | |||
"push": [MockFunction], | |||
"replace": [MockFunction], | |||
"setRouteLeaveHook": [MockFunction], | |||
} | |||
} | |||
settings={Array []} | |||
/> | |||
</div> |
@@ -4,7 +4,19 @@ exports[`should render correctly 1`] = ` | |||
<GitlabProjectCreateRenderer | |||
canAdmin={false} | |||
loading={true} | |||
loadingMore={false} | |||
onLoadMore={[Function]} | |||
onPersonalAccessTokenCreate={[Function]} | |||
onSearch={[Function]} | |||
projectsPaging={ | |||
Object { | |||
"pageIndex": 1, | |||
"pageSize": 30, | |||
"total": 0, | |||
} | |||
} | |||
searchQuery="" | |||
searching={false} | |||
settings={ | |||
Object { | |||
"alm": "gitlab", |
@@ -101,3 +101,37 @@ exports[`should render correctly: pat form 1`] = ` | |||
/> | |||
</Fragment> | |||
`; | |||
exports[`should render correctly: project selection form 1`] = ` | |||
<Fragment> | |||
<CreateProjectPageHeader | |||
title={ | |||
<span | |||
className="text-middle" | |||
> | |||
<img | |||
alt="" | |||
className="spacer-right" | |||
height="24" | |||
src="/images/alm/gitlab.svg" | |||
/> | |||
onboarding.create_project.gitlab.title | |||
</span> | |||
} | |||
/> | |||
<GitlabProjectSelectionForm | |||
loadingMore={false} | |||
onLoadMore={[MockFunction]} | |||
onSearch={[MockFunction]} | |||
projectsPaging={ | |||
Object { | |||
"pageIndex": 1, | |||
"pageSize": 30, | |||
"total": 0, | |||
} | |||
} | |||
searchQuery="" | |||
searching={false} | |||
/> | |||
</Fragment> | |||
`; |
@@ -0,0 +1,234 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly: no projects 1`] = ` | |||
<Alert | |||
className="spacer-top" | |||
variant="warning" | |||
> | |||
<FormattedMessage | |||
defaultMessage="onboarding.create_project.gitlab.no_projects" | |||
id="onboarding.create_project.gitlab.no_projects" | |||
values={ | |||
Object { | |||
"link": <Link | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/projects/create", | |||
"query": Object { | |||
"mode": "gitlab", | |||
"resetPat": 1, | |||
}, | |||
} | |||
} | |||
> | |||
onboarding.create_project.update_your_token | |||
</Link>, | |||
} | |||
} | |||
/> | |||
</Alert> | |||
`; | |||
exports[`should render correctly: no projects when searching 1`] = ` | |||
<div | |||
className="boxed-group big-padded create-project-import-gitlab" | |||
> | |||
<SearchBox | |||
className="spacer" | |||
loading={false} | |||
minLength={3} | |||
onChange={[MockFunction]} | |||
placeholder="onboarding.create_project.gitlab.search_prompt" | |||
/> | |||
<hr /> | |||
<div | |||
className="padded" | |||
> | |||
no_results | |||
</div> | |||
<ListFooter | |||
count={0} | |||
loadMore={[MockFunction]} | |||
loading={false} | |||
total={0} | |||
/> | |||
</div> | |||
`; | |||
exports[`should render correctly: projects 1`] = ` | |||
<div | |||
className="boxed-group big-padded create-project-import-gitlab" | |||
> | |||
<SearchBox | |||
className="spacer" | |||
loading={false} | |||
minLength={3} | |||
onChange={[MockFunction]} | |||
placeholder="onboarding.create_project.gitlab.search_prompt" | |||
/> | |||
<hr /> | |||
<table | |||
className="data zebra zebra-hover" | |||
> | |||
<tbody> | |||
<tr | |||
key="id1234" | |||
> | |||
<td> | |||
<Tooltip | |||
overlay="awesome-project-exclamation" | |||
> | |||
<strong | |||
className="project-name display-inline-block text-ellipsis" | |||
> | |||
Awesome Project ! | |||
</strong> | |||
</Tooltip> | |||
<br /> | |||
<Tooltip | |||
overlay="company/best-projects" | |||
> | |||
<span | |||
className="text-muted project-path display-inline-block text-ellipsis" | |||
> | |||
Company / Best Projects | |||
</span> | |||
</Tooltip> | |||
</td> | |||
<td> | |||
<a | |||
className="display-inline-flex-center big-spacer-right" | |||
href="https://gitlab.company.com/best-projects/awesome-project-exclamation" | |||
rel="noopener noreferrer" | |||
target="_blank" | |||
> | |||
<DetachIcon | |||
className="little-spacer-right" | |||
/> | |||
onboarding.create_project.gitlab.link | |||
</a> | |||
</td> | |||
<td | |||
colSpan={2} | |||
> | |||
</td> | |||
</tr> | |||
<tr | |||
key="2" | |||
> | |||
<td> | |||
<Tooltip | |||
overlay="awesome-project-exclamation" | |||
> | |||
<strong | |||
className="project-name display-inline-block text-ellipsis" | |||
> | |||
Awesome Project ! | |||
</strong> | |||
</Tooltip> | |||
<br /> | |||
<Tooltip | |||
overlay="company/best-projects" | |||
> | |||
<span | |||
className="text-muted project-path display-inline-block text-ellipsis" | |||
> | |||
Company / Best Projects | |||
</span> | |||
</Tooltip> | |||
</td> | |||
<td> | |||
<a | |||
className="display-inline-flex-center big-spacer-right" | |||
href="https://gitlab.company.com/best-projects/awesome-project-exclamation" | |||
rel="noopener noreferrer" | |||
target="_blank" | |||
> | |||
<DetachIcon | |||
className="little-spacer-right" | |||
/> | |||
onboarding.create_project.gitlab.link | |||
</a> | |||
</td> | |||
<td> | |||
<span | |||
className="display-flex-center display-flex-justify-end already-set-up" | |||
> | |||
<CheckIcon | |||
className="little-spacer-right" | |||
size={12} | |||
/> | |||
onboarding.create_project.repository_imported | |||
: | |||
</span> | |||
</td> | |||
<td> | |||
<div | |||
className="sq-project-link text-ellipsis" | |||
> | |||
<Link | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/dashboard", | |||
"query": Object { | |||
"branch": undefined, | |||
"id": "already-imported", | |||
}, | |||
} | |||
} | |||
> | |||
<QualifierIcon | |||
className="spacer-right" | |||
qualifier="TRK" | |||
/> | |||
Already Imported | |||
</Link> | |||
</div> | |||
</td> | |||
</tr> | |||
</tbody> | |||
</table> | |||
<ListFooter | |||
count={2} | |||
loadMore={[MockFunction]} | |||
loading={false} | |||
total={2} | |||
/> | |||
</div> | |||
`; | |||
exports[`should render correctly: undefined projects 1`] = ` | |||
<Alert | |||
className="spacer-top" | |||
variant="warning" | |||
> | |||
<FormattedMessage | |||
defaultMessage="onboarding.create_project.gitlab.no_projects" | |||
id="onboarding.create_project.gitlab.no_projects" | |||
values={ | |||
Object { | |||
"link": <Link | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/projects/create", | |||
"query": Object { | |||
"mode": "gitlab", | |||
"resetPat": 1, | |||
}, | |||
} | |||
} | |||
> | |||
onboarding.create_project.update_your_token | |||
</Link>, | |||
} | |||
} | |||
/> | |||
</Alert> | |||
`; |
@@ -60,3 +60,20 @@ | |||
.create-project-github-repository .notice svg { | |||
color: var(--green); | |||
} | |||
.create-project-import-gitlab table > tbody > tr > td { | |||
vertical-align: middle; | |||
} | |||
.create-project-import-gitlab .project-name, | |||
.create-project-import-gitlab .project-path { | |||
max-width: 400px; | |||
} | |||
.create-project-import-gitlab .sq-project-link { | |||
max-width: 300px; | |||
} | |||
.create-project-import-gitlab .already-set-up svg { | |||
color: var(--green); | |||
} |
@@ -20,7 +20,8 @@ | |||
import { | |||
BitbucketProject, | |||
BitbucketRepository, | |||
GithubRepository | |||
GithubRepository, | |||
GitlabProject | |||
} from '../../types/alm-integration'; | |||
export function mockBitbucketProject(overrides: Partial<BitbucketProject> = {}): BitbucketProject { | |||
@@ -54,3 +55,16 @@ export function mockGitHubRepository(overrides: Partial<GithubRepository> = {}): | |||
...overrides | |||
}; | |||
} | |||
export function mockGitlabProject(overrides: Partial<GitlabProject> = {}): GitlabProject { | |||
return { | |||
id: 'id1234', | |||
name: 'Awesome Project !', | |||
slug: 'awesome-project-exclamation', | |||
pathName: 'Company / Best Projects', | |||
pathSlug: 'company/best-projects', | |||
sqProjectKey: '', | |||
url: 'https://gitlab.company.com/best-projects/awesome-project-exclamation', | |||
...overrides | |||
}; | |||
} |
@@ -48,3 +48,14 @@ export interface GithubRepository { | |||
url: string; | |||
sqProjectKey: string; | |||
} | |||
export interface GitlabProject { | |||
id: string; | |||
name: string; | |||
pathName: string; | |||
pathSlug: string; | |||
sqProjectKey?: string; | |||
sqProjectName?: string; | |||
slug: string; | |||
url: string; | |||
} |
@@ -3172,6 +3172,9 @@ onboarding.create_project.github.warning.message_admin.link=ALM integration sett | |||
onboarding.create_project.github.no_orgs=We couldn't load any organizations with your key. Contact an administrator. | |||
onboarding.create_project.github.no_orgs_admin=We couldn't load any organizations. Make sure the GitHub App is installed in at least one organization and check the GitHub instance configuration in the {link}. | |||
onboarding.create_project.gitlab.title=Which GitLab project do you want to setup? | |||
onboarding.create_project.gitlab.no_projects=No projects could be fetched from Gitlab. Contact your system administrator, or {link}. | |||
onboarding.create_project.gitlab.link=See on GitLab | |||
onboarding.create_project.gitlab.search_prompt=Search for projects | |||
onboarding.create_organization.page.header=Create Organization | |||
onboarding.create_organization.page.description=An organization is a space where a team or a whole company can collaborate accross many projects. |