]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-13629 Display gitlab projects
authorJeremy Davis <jeremy.davis@sonarsource.com>
Mon, 27 Jul 2020 15:30:15 +0000 (17:30 +0200)
committersonartech <sonartech@sonarsource.com>
Mon, 17 Aug 2020 20:06:22 +0000 (20:06 +0000)
18 files changed:
server/sonar-web/src/main/js/api/alm-integrations.ts
server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
server/sonar-web/src/main/js/apps/create/project/GitHubProjectCreate.tsx
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 [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/__tests__/GitHubProjectCreate-test.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 [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap
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 [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/style.css
server/sonar-web/src/main/js/helpers/mocks/alm-integrations.ts
server/sonar-web/src/main/js/types/alm-integration.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index b0007b3c48018d589fdfe25e3520cfce5b3c5e3c..51914405af59241e42484038863808515a08129a 100644 (file)
@@ -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);
+}
index 5b79dd82e6b189b8e11816029cd4d7730c7b26eb..3976e1acb93e51d578e99facc36bb262ef60a7c0 100644 (file)
@@ -138,6 +138,7 @@ export class CreateProjectPage extends React.PureComponent<Props, State> {
             loadingBindings={loading}
             location={location}
             onProjectCreate={this.handleProjectCreate}
+            router={router}
             settings={gitlabSettings}
           />
         );
index 1c03f8c3b50152ea760a1ce5b5651f618fff85e3..f438d2486d5babcd46f25a4a1ce1cb81879fafed 100644 (file)
@@ -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
       });
 
index 33d05358658444c2d80b93f4994240968643407b..d7f2ad3728382ce2a37d5a5a79f51412aea840ab 100644 (file)
@@ -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}
index 90f47b85d01065476bd7e9bdf7e3ded4a49e16e1..e6a0489d7e2d034b1207471ba236a06c331379d3 100644 (file)
 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}
+          />
         ))}
     </>
   );
diff --git a/server/sonar-web/src/main/js/apps/create/project/GitlabProjectSelectionForm.tsx b/server/sonar-web/src/main/js/apps/create/project/GitlabProjectSelectionForm.tsx
new file mode 100644 (file)
index 0000000..01da4fc
--- /dev/null
@@ -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}>&nbsp;</td>
+                )}
+              </tr>
+            ))}
+          </tbody>
+        </table>
+      )}
+      <ListFooter
+        count={projects.length}
+        loadMore={props.onLoadMore}
+        loading={loadingMore}
+        total={projectsPaging.total}
+      />
+    </div>
+  );
+}
index 4577ba4a0dfdb49fd59cea331cfbd292ba18f97b..072f321528f925c68fb65a5b8cc7bc7bd40ff995 100644 (file)
@@ -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);
index da88c061d273e3505743c22c1193a0a23aee140e..4dffe5674f5fa92714765d41aee244006105eff2 100644 (file)
@@ -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}
     />
index c824f5ea0412ec3172f6265e85c2fe0af910dc14..08e6251ddbca84fabc982889b47254a51d32df37 100644 (file)
@@ -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}
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectSelectionForm-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectSelectionForm-test.tsx
new file mode 100644 (file)
index 0000000..2840d07
--- /dev/null
@@ -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 };
+}
index 3c54a4b2d4d937e96755b96a70c1c9e68ec38b72..8dd4ffc6bc8d732be7c3ec1d4fe2ecb00d393883 100644 (file)
@@ -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>
index 8bf4d3701b3b1107a93d1ff0a4b20b05114cbdb6..08c98497cc115458eae6ea3c594ddf4792f2c802 100644 (file)
@@ -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",
index d0d2a7237376219409ac86b81ad076ae180c9047..eb03b51d1d5997f665511756253a322fbc0da021 100644 (file)
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectSelectionForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectSelectionForm-test.tsx.snap
new file mode 100644 (file)
index 0000000..eb9bf99
--- /dev/null
@@ -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>
+`;
index 7930a3b216e6a8ca468f03a7daa6e3e4f8c0c940..b02012dfe230abaac6392ba204e6d418e4f9f8fc 100644 (file)
 .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);
+}
index b4a1275fb586d589cb5f497a66ccb742a69d1a2f..1fdd37225e6e75989f71e18fff97abe85212137e 100644 (file)
@@ -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
+  };
+}
index 4a2a5864e8c5a56aa8aa4c23907b7113150f6f9b..cf70f73017a915b4bbce6c56288188fae5dad21a 100644 (file)
@@ -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;
+}
index dcf07e58c1a364bf1b761ea66f7cef506f7ea462..7a1b7ee02a08f2e5a2d803d7e004a6eb001fbf87 100644 (file)
@@ -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.