Bläddra i källkod

SONAR-13630 import from gitlab

tags/8.5.0.37579
Jeremy Davis 3 år sedan
förälder
incheckning
88bca79492

+ 11
- 0
server/sonar-web/src/main/js/api/alm-integrations.ts Visa fil

@@ -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);
}

+ 28
- 0
server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreate.tsx Visa fil

@@ -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}

+ 5
- 0
server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreateRenderer.tsx Visa fil

@@ -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}

+ 22
- 2
server/sonar-web/src/main/js/apps/create/project/GitlabProjectSelectionForm.tsx Visa fil

@@ -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}>&nbsp;</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>
))}

+ 51
- 1
server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectCreate-test.tsx Visa fil

@@ -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

+ 1
- 0
server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectCreateRenderer-test.tsx Visa fil

@@ -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()}

+ 40
- 0
server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectSelectionForm-test.tsx Visa fil

@@ -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}

+ 1
- 0
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectCreate-test.tsx.snap Visa fil

@@ -5,6 +5,7 @@ exports[`should render correctly 1`] = `
canAdmin={false}
loading={true}
loadingMore={false}
onImport={[Function]}
onLoadMore={[Function]}
onPersonalAccessTokenCreate={[Function]}
onSearch={[Function]}

+ 1
- 0
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectCreateRenderer-test.tsx.snap Visa fil

@@ -121,6 +121,7 @@ exports[`should render correctly: project selection form 1`] = `
/>
<GitlabProjectSelectionForm
loadingMore={false}
onImport={[MockFunction]}
onLoadMore={[MockFunction]}
onSearch={[MockFunction]}
projectsPaging={

+ 158
- 1
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectSelectionForm-test.tsx.snap Visa fil

@@ -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

+ 3
- 2
sonar-core/src/main/resources/org/sonar/l10n/core.properties Visa fil

@@ -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.

Laddar…
Avbryt
Spara