]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-13630 import from gitlab
authorJeremy Davis <jeremy.davis@sonarsource.com>
Tue, 28 Jul 2020 14:05:04 +0000 (16:05 +0200)
committersonartech <sonartech@sonarsource.com>
Mon, 17 Aug 2020 20:06:22 +0000 (20:06 +0000)
server/sonar-web/src/main/js/api/alm-integrations.ts
server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreate.tsx
server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreateRenderer.tsx
server/sonar-web/src/main/js/apps/create/project/GitlabProjectSelectionForm.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectCreate-test.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectCreateRenderer-test.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectSelectionForm-test.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectCreate-test.tsx.snap
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectCreateRenderer-test.tsx.snap
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectSelectionForm-test.tsx.snap
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 51914405af59241e42484038863808515a08129a..e013a3721df6e371f4f75f4de4157f970d24fbab 100644 (file)
@@ -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);
+}
index d7f2ad3728382ce2a37d5a5a79f51412aea840ab..87e35b9dd72096da338be882626af25cde918637 100644 (file)
@@ -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}
index e6a0489d7e2d034b1207471ba236a06c331379d3..ddd0ce2015d82e0b25a8e620dab70bf27883e213 100644 (file)
@@ -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}
index 01da4fc885186f222a70d598c9ab75035ab195f4..9a5f885351539640072465ea44686b5badfe6f50 100644 (file)
@@ -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>
             ))}
index 4dffe5674f5fa92714765d41aee244006105eff2..b7c84ffdba484cbee9989a20406ac6a2039cb1aa 100644 (file)
@@ -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
index 08e6251ddbca84fabc982889b47254a51d32df37..bbee640a7394fcdb79d09e4f6f82bcee49d4fc0e 100644 (file)
@@ -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()}
index 2840d073908735536085a431963eb79041811dcc..1727f35275d9a0b851a5e5ab651dc1fad3e37bf6 100644 (file)
@@ -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}
index 08c98497cc115458eae6ea3c594ddf4792f2c802..7d9b8e454d457d1a2cb19b5f1bf60099d73088f9 100644 (file)
@@ -5,6 +5,7 @@ exports[`should render correctly 1`] = `
   canAdmin={false}
   loading={true}
   loadingMore={false}
+  onImport={[Function]}
   onLoadMore={[Function]}
   onPersonalAccessTokenCreate={[Function]}
   onSearch={[Function]}
index eb03b51d1d5997f665511756253a322fbc0da021..7a78bb9b2029d9bfb9f160fb8cc38258e7dbd54f 100644 (file)
@@ -121,6 +121,7 @@ exports[`should render correctly: project selection form 1`] = `
   />
   <GitlabProjectSelectionForm
     loadingMore={false}
+    onImport={[MockFunction]}
     onLoadMore={[MockFunction]}
     onSearch={[MockFunction]}
     projectsPaging={
index eb9bf997e3693d7a70f2058fc7e82a0c8bd0bc14..7c2d25d78cec134d45018eb07c26a9af0d4b0a60 100644 (file)
@@ -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
index 7a1b7ee02a08f2e5a2d803d7e004a6eb001fbf87..1c0e844aa9828a593abfdf5ff723902556a5fc8e 100644 (file)
@@ -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.