@@ -151,3 +151,14 @@ export function getGitlabProjects(data: { | |||
.then(({ repositories, paging }) => ({ projects: repositories, projectsPaging: paging })) | |||
.catch(throwGlobalError); | |||
} | |||
export function importGitlabProject(data: { | |||
almSetting: string; | |||
gitlabProjectId: string; | |||
}): Promise<{ project: ProjectBase }> { | |||
const { almSetting, gitlabProjectId } = data; | |||
return postJSON('/api/alm_integrations/import_gitlab_project', { | |||
almSetting, | |||
gitlabProjectId | |||
}).catch(throwGlobalError); | |||
} |
@@ -22,6 +22,7 @@ import { WithRouterProps } from 'react-router'; | |||
import { | |||
checkPersonalAccessTokenIsValid, | |||
getGitlabProjects, | |||
importGitlabProject, | |||
setAlmPersonalAccessToken | |||
} from '../../../api/alm-integrations'; | |||
import { GitlabProject } from '../../../types/alm-integration'; | |||
@@ -36,6 +37,7 @@ interface Props extends Pick<WithRouterProps, 'location' | 'router'> { | |||
} | |||
interface State { | |||
importingGitlabProjectId?: string; | |||
loading: boolean; | |||
loadingMore: boolean; | |||
projects?: GitlabProject[]; | |||
@@ -141,6 +143,29 @@ export default class GitlabProjectCreate extends React.PureComponent<Props, Stat | |||
}).catch(() => undefined); | |||
}; | |||
handleImport = async (gitlabProjectId: string) => { | |||
const { settings } = this.state; | |||
if (!settings) { | |||
return; | |||
} | |||
this.setState({ importingGitlabProjectId: gitlabProjectId }); | |||
const result = await importGitlabProject({ | |||
almSetting: settings.key, | |||
gitlabProjectId | |||
}).catch(() => undefined); | |||
if (this.mounted) { | |||
this.setState({ importingGitlabProjectId: undefined }); | |||
if (result) { | |||
this.props.onProjectCreate([result.project.key]); | |||
} | |||
} | |||
}; | |||
handleLoadMore = async () => { | |||
this.setState({ loadingMore: true }); | |||
@@ -216,6 +241,7 @@ export default class GitlabProjectCreate extends React.PureComponent<Props, Stat | |||
render() { | |||
const { canAdmin, loadingBindings, location } = this.props; | |||
const { | |||
importingGitlabProjectId, | |||
loading, | |||
loadingMore, | |||
projects, | |||
@@ -232,8 +258,10 @@ export default class GitlabProjectCreate extends React.PureComponent<Props, Stat | |||
<GitlabProjectCreateRenderer | |||
settings={settings} | |||
canAdmin={canAdmin} | |||
importingGitlabProjectId={importingGitlabProjectId} | |||
loading={loading || loadingBindings} | |||
loadingMore={loadingMore} | |||
onImport={this.handleImport} | |||
onLoadMore={this.handleLoadMore} | |||
onPersonalAccessTokenCreate={this.handlePersonalAccessTokenCreate} | |||
onSearch={this.handleSearch} |
@@ -29,8 +29,10 @@ import WrongBindingCountAlert from './WrongBindingCountAlert'; | |||
export interface GitlabProjectCreateRendererProps { | |||
canAdmin?: boolean; | |||
importingGitlabProjectId?: string; | |||
loading: boolean; | |||
loadingMore: boolean; | |||
onImport: (gitlabProjectId: string) => void; | |||
onLoadMore: () => void; | |||
onPersonalAccessTokenCreate: (pat: string) => void; | |||
onSearch: (searchQuery: string) => void; | |||
@@ -47,6 +49,7 @@ export interface GitlabProjectCreateRendererProps { | |||
export default function GitlabProjectCreateRenderer(props: GitlabProjectCreateRendererProps) { | |||
const { | |||
canAdmin, | |||
importingGitlabProjectId, | |||
loading, | |||
loadingMore, | |||
projects, | |||
@@ -92,7 +95,9 @@ export default function GitlabProjectCreateRenderer(props: GitlabProjectCreateRe | |||
/> | |||
) : ( | |||
<GitlabProjectSelectionForm | |||
importingGitlabProjectId={importingGitlabProjectId} | |||
loadingMore={loadingMore} | |||
onImport={props.onImport} | |||
onLoadMore={props.onLoadMore} | |||
onSearch={props.onSearch} | |||
projects={projects} |
@@ -20,6 +20,7 @@ | |||
import * as React from 'react'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import { Link } from 'react-router'; | |||
import { Button } from 'sonar-ui-common/components/controls/buttons'; | |||
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'; | |||
@@ -27,6 +28,7 @@ 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 DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { getProjectUrl } from '../../../helpers/urls'; | |||
import { GitlabProject } from '../../../types/alm-integration'; | |||
@@ -34,7 +36,9 @@ import { ComponentQualifier } from '../../../types/component'; | |||
import { CreateProjectModes } from './types'; | |||
export interface GitlabProjectSelectionFormProps { | |||
importingGitlabProjectId?: string; | |||
loadingMore: boolean; | |||
onImport: (gitlabProjectId: string) => void; | |||
onLoadMore: () => void; | |||
onSearch: (searchQuery: string) => void; | |||
projects?: GitlabProject[]; | |||
@@ -44,7 +48,14 @@ export interface GitlabProjectSelectionFormProps { | |||
} | |||
export default function GitlabProjectSelectionForm(props: GitlabProjectSelectionFormProps) { | |||
const { loadingMore, projects = [], projectsPaging, searching, searchQuery } = props; | |||
const { | |||
importingGitlabProjectId, | |||
loadingMore, | |||
projects = [], | |||
projectsPaging, | |||
searching, | |||
searchQuery | |||
} = props; | |||
if (projects.length === 0 && searchQuery.length === 0 && !searching) { | |||
return ( | |||
@@ -131,7 +142,16 @@ export default function GitlabProjectSelectionForm(props: GitlabProjectSelection | |||
</td> | |||
</> | |||
) : ( | |||
<td colSpan={2}> </td> | |||
<td colSpan={2} className="text-right"> | |||
<Button | |||
disabled={!!importingGitlabProjectId} | |||
onClick={() => props.onImport(project.id)}> | |||
{translate('onboarding.create_project.gitlab.set_up')} | |||
{importingGitlabProjectId === project.id && ( | |||
<DeferredSpinner className="spacer-left" /> | |||
)} | |||
</Button> | |||
</td> | |||
)} | |||
</tr> | |||
))} |
@@ -24,6 +24,7 @@ import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; | |||
import { | |||
checkPersonalAccessTokenIsValid, | |||
getGitlabProjects, | |||
importGitlabProject, | |||
setAlmPersonalAccessToken | |||
} from '../../../../api/alm-integrations'; | |||
import { mockGitlabProject } from '../../../../helpers/mocks/alm-integrations'; | |||
@@ -35,7 +36,8 @@ import GitlabProjectCreate from '../GitlabProjectCreate'; | |||
jest.mock('../../../../api/alm-integrations', () => ({ | |||
checkPersonalAccessTokenIsValid: jest.fn().mockResolvedValue(true), | |||
setAlmPersonalAccessToken: jest.fn().mockResolvedValue(null), | |||
getGitlabProjects: jest.fn().mockRejectedValue('error') | |||
getGitlabProjects: jest.fn().mockRejectedValue('error'), | |||
importGitlabProject: jest.fn().mockRejectedValue('error') | |||
})); | |||
beforeEach(jest.clearAllMocks); | |||
@@ -200,6 +202,54 @@ it('should search for projects', async () => { | |||
expect(getGitlabProjects).toBeCalledWith(expect.objectContaining({ query })); | |||
}); | |||
it('should import', async () => { | |||
(checkPersonalAccessTokenIsValid as jest.Mock).mockResolvedValueOnce(true); | |||
const projects = [mockGitlabProject({ id: '1' }), mockGitlabProject({ id: '2' })]; | |||
(getGitlabProjects as jest.Mock).mockResolvedValueOnce({ | |||
projects, | |||
projectsPaging: { | |||
pageIndex: 1, | |||
pageSize: 6, | |||
total: 2 | |||
} | |||
}); | |||
const createdProjectkey = 'imported_project_key'; | |||
(importGitlabProject as jest.Mock).mockResolvedValueOnce({ | |||
project: { key: createdProjectkey } | |||
}); | |||
const onProjectCreate = jest.fn(); | |||
const wrapper = shallowRender({ onProjectCreate }); | |||
await waitAndUpdate(wrapper); | |||
wrapper.instance().handleImport(projects[1].id); | |||
expect(wrapper.state().importingGitlabProjectId).toBe(projects[1].id); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.state().importingGitlabProjectId).toBeUndefined(); | |||
expect(onProjectCreate).toBeCalledWith([createdProjectkey]); | |||
}); | |||
it('should do nothing with missing settings', async () => { | |||
const wrapper = shallowRender({ settings: [] }); | |||
await waitAndUpdate(wrapper); | |||
wrapper.instance().handleLoadMore(); | |||
wrapper.instance().handleSearch('whatever'); | |||
wrapper.instance().handlePersonalAccessTokenCreate('token'); | |||
wrapper.instance().handleImport('gitlab project id'); | |||
expect(checkPersonalAccessTokenIsValid).not.toHaveBeenCalled(); | |||
expect(getGitlabProjects).not.toHaveBeenCalled(); | |||
expect(importGitlabProject).not.toHaveBeenCalled(); | |||
expect(setAlmPersonalAccessToken).not.toHaveBeenCalled(); | |||
}); | |||
function shallowRender(props: Partial<GitlabProjectCreate['props']> = {}) { | |||
return shallow<GitlabProjectCreate>( | |||
<GitlabProjectCreate |
@@ -44,6 +44,7 @@ function shallowRender(props: Partial<GitlabProjectCreateRendererProps> = {}) { | |||
canAdmin={false} | |||
loading={false} | |||
loadingMore={false} | |||
onImport={jest.fn()} | |||
onLoadMore={jest.fn()} | |||
onPersonalAccessTokenCreate={jest.fn()} | |||
onSearch={jest.fn()} |
@@ -20,6 +20,9 @@ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { Button } from 'sonar-ui-common/components/controls/buttons'; | |||
import ListFooter from 'sonar-ui-common/components/controls/ListFooter'; | |||
import SearchBox from 'sonar-ui-common/components/controls/SearchBox'; | |||
import { mockGitlabProject } from '../../../../helpers/mocks/alm-integrations'; | |||
import GitlabProjectSelectionForm, { | |||
GitlabProjectSelectionFormProps | |||
@@ -37,6 +40,42 @@ it('should render correctly', () => { | |||
expect( | |||
shallowRender({ projects: [], projectsPaging: mockPaging(), searchQuery: 'findme' }) | |||
).toMatchSnapshot('no projects when searching'); | |||
expect(shallowRender({ importingGitlabProjectId: '2' })).toMatchSnapshot('importing'); | |||
}); | |||
describe('appropriate callback', () => { | |||
const onImport = jest.fn(); | |||
const onLoadMore = jest.fn(); | |||
const onSearch = jest.fn(); | |||
const wrapper = shallowRender({ onImport, onLoadMore, onSearch }); | |||
it('should be called when clicking to import', () => { | |||
wrapper | |||
.find(Button) | |||
.first() | |||
.simulate('click'); | |||
expect(onImport).toBeCalled(); | |||
}); | |||
it('should be assigned to the list footer', () => { | |||
const { loadMore } = wrapper | |||
.find(ListFooter) | |||
.first() | |||
.props(); | |||
expect(loadMore).toBe(onLoadMore); | |||
}); | |||
it('should be assigned to the search box', () => { | |||
const { onChange } = wrapper | |||
.find(SearchBox) | |||
.first() | |||
.props(); | |||
expect(onChange).toBe(onSearch); | |||
}); | |||
}); | |||
function shallowRender(props: Partial<GitlabProjectSelectionFormProps> = {}) { | |||
@@ -52,6 +91,7 @@ function shallowRender(props: Partial<GitlabProjectSelectionFormProps> = {}) { | |||
return shallow<GitlabProjectSelectionFormProps>( | |||
<GitlabProjectSelectionForm | |||
loadingMore={false} | |||
onImport={jest.fn()} | |||
onLoadMore={jest.fn()} | |||
onSearch={jest.fn()} | |||
projects={projects} |
@@ -5,6 +5,7 @@ exports[`should render correctly 1`] = ` | |||
canAdmin={false} | |||
loading={true} | |||
loadingMore={false} | |||
onImport={[Function]} | |||
onLoadMore={[Function]} | |||
onPersonalAccessTokenCreate={[Function]} | |||
onSearch={[Function]} |
@@ -121,6 +121,7 @@ exports[`should render correctly: project selection form 1`] = ` | |||
/> | |||
<GitlabProjectSelectionForm | |||
loadingMore={false} | |||
onImport={[MockFunction]} | |||
onLoadMore={[MockFunction]} | |||
onSearch={[MockFunction]} | |||
projectsPaging={ |
@@ -1,5 +1,156 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly: importing 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 | |||
className="text-right" | |||
colSpan={2} | |||
> | |||
<Button | |||
disabled={true} | |||
onClick={[Function]} | |||
> | |||
onboarding.create_project.gitlab.set_up | |||
</Button> | |||
</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: no projects 1`] = ` | |||
<Alert | |||
className="spacer-top" | |||
@@ -111,9 +262,15 @@ exports[`should render correctly: projects 1`] = ` | |||
</a> | |||
</td> | |||
<td | |||
className="text-right" | |||
colSpan={2} | |||
> | |||
<Button | |||
disabled={false} | |||
onClick={[Function]} | |||
> | |||
onboarding.create_project.gitlab.set_up | |||
</Button> | |||
</td> | |||
</tr> | |||
<tr |
@@ -3163,7 +3163,7 @@ onboarding.create_project.no_bbs_repos.filter=No repositories match your filter. | |||
onboarding.create_project.only_showing_X_first_repos=We're only displaying the first {0} repositories. If you're looking for a repository that's not in this list, use the search above. | |||
onboarding.create_project.import_selected_repo=Set up selected repository | |||
onboarding.create_project.go_to_project=Go to project | |||
onboarding.create_project.github.title=Which GitHub repository do you want to setup? | |||
onboarding.create_project.github.title=Which GitHub repository do you want to set up? | |||
onboarding.create_project.github.choose_organization=Choose organization | |||
onboarding.create_project.github.warning.title=Could not connect to GitHub | |||
onboarding.create_project.github.warning.message=Please contact an administrator to configure GitHub integration. | |||
@@ -3171,10 +3171,11 @@ onboarding.create_project.github.warning.message_admin=Please make sure the GitH | |||
onboarding.create_project.github.warning.message_admin.link=ALM integration settings | |||
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.title=Which GitLab project do you want to set up? | |||
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_project.gitlab.set_up=Set up | |||
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. |