]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-11037 SONAR-11038 Show list of personal public repositories and allow to provis...
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Wed, 25 Jul 2018 13:52:23 +0000 (15:52 +0200)
committerSonarTech <sonartech@sonarsource.com>
Fri, 10 Aug 2018 18:21:29 +0000 (20:21 +0200)
* SONAR-11043 List repositories with their types
* SONAR-11046 Check and disable already linked repositories
* SONAR-11044 Provision project and redirect to project dashboard
* SONAR-11048 Reload repositories after create failure
* SONAR-11038 Use new project create page everywhere on SonarCloud

19 files changed:
server/sonar-web/src/main/js/api/alm-integration.ts
server/sonar-web/src/main/js/app/components/StartupModal.tsx
server/sonar-web/src/main/js/app/styles/init/links.css
server/sonar-web/src/main/js/app/types.ts
server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap
server/sonar-web/src/main/js/apps/tutorials/Onboarding.tsx
server/sonar-web/src/main/js/apps/tutorials/__tests__/Onboarding-test.tsx
server/sonar-web/src/main/js/apps/tutorials/__tests__/__snapshots__/Onboarding-test.tsx.snap
server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/AlmRepositoryItem.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/AutoProjectCreate.tsx
server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/CreateProjectOnboarding.tsx
server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/AlmRepositoryItem-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/AutoProjectCreate-test.tsx
server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/__snapshots__/AlmRepositoryItem-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap
server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/__snapshots__/CreateProjectOnboarding-test.tsx.snap
server/sonar-web/src/main/js/apps/tutorials/routes.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 0232632ccc2ab24f9257fed45a56493e7e4c012e..798f16e6d89e90a07726fd39aeae60b9fd96a64e 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { getJSON } from '../helpers/request';
+import { getJSON, postJSON } from '../helpers/request';
+import { AlmRepository } from '../app/types';
 import throwGlobalError from '../app/utils/throwGlobalError';
 
 export function getRepositories(): Promise<{
-  installation: {
+  almIntegration: {
+    installed: boolean;
     installationUrl: string;
-    enabled: boolean;
   };
+  repositories: AlmRepository[];
 }> {
   return getJSON('/api/alm_integration/list_repositories').catch(throwGlobalError);
 }
+
+export function provisionProject(data: { repositories: string[] }) {
+  return postJSON('api/alm_integration/provision_projects', data).catch(throwGlobalError);
+}
index c49a77d20520b28f24896c459649195703c46c18..8a7e7e2f44469cf8be08b841249dfefe39289d63 100644 (file)
@@ -99,13 +99,11 @@ export class StartupModal extends React.PureComponent<Props, State> {
     this.tryAutoOpenLicense().catch(this.tryAutoOpenOnboarding);
   }
 
-  closeOnboarding = (doSkipOnboarding = true) => {
+  closeOnboarding = () => {
     this.setState(state => {
       if (state.modal !== ModalKey.license) {
-        if (doSkipOnboarding) {
-          skipOnboarding();
-          this.props.skipOnboardingAction();
-        }
+        skipOnboarding();
+        this.props.skipOnboardingAction();
         return { automatic: false, modal: undefined };
       }
       return undefined;
@@ -135,7 +133,12 @@ export class StartupModal extends React.PureComponent<Props, State> {
   };
 
   openProjectOnboarding = () => {
-    this.setState({ modal: ModalKey.projectOnboarding });
+    if (isSonarCloud()) {
+      this.setState({ automatic: false, modal: undefined });
+      this.context.router.push(`/onboarding`);
+    } else {
+      this.setState({ modal: ModalKey.projectOnboarding });
+    }
   };
 
   openTeamOnboarding = () => {
@@ -189,6 +192,7 @@ export class StartupModal extends React.PureComponent<Props, State> {
           <Onboarding
             onClose={this.closeOnboarding}
             onOpenOrganizationOnboarding={this.openOrganizationOnboarding}
+            onOpenProjectOnboarding={this.openProjectOnboarding}
             onOpenTeamOnboarding={this.openTeamOnboarding}
           />
         )}
index bc4b5507a76bf31573dc8cd5baf4580970734812..20edfe9dce691a2ad9a354b9b2f9d952c97bbfaf 100644 (file)
@@ -73,10 +73,8 @@ a:focus {
   border-bottom: none;
 }
 
-.link-checkbox.disabled {
-  cursor: not-allowed;
-}
-
+.link-checkbox.disabled,
+.link-checkbox.disabled:hover,
 .link-checkbox.disabled label {
   color: var(--secondFontColor);
   cursor: not-allowed;
index 4f1b00c3f761bf9f01951e8a7cd818f5ee0233db..ea3be59fc5c1aeab586c38a9a8cbb76aba8fa138 100644 (file)
@@ -21,6 +21,13 @@ export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
 
 // Type ordered alphabetically to prevent merge conflicts
 
+export interface AlmRepository {
+  label: string;
+  installationKey: string;
+  linkedProjectKey?: string;
+  linkedProjectName?: string;
+}
+
 export interface AppState {
   adminPages?: Extension[];
   authenticationError?: boolean;
index c572739793356a5171e983f35fa57dd73c277eb1..271cf360e29ada8580452ab66eb2336d9a896101 100644 (file)
@@ -22,13 +22,14 @@ import { Link } from 'react-router';
 import { connect } from 'react-redux';
 import * as PropTypes from 'prop-types';
 import { sortBy } from 'lodash';
-import { Organization } from '../../../app/types';
 import DropdownIcon from '../../../components/icons-components/DropdownIcon';
 import Dropdown from '../../../components/controls/Dropdown';
-import { getMyOrganizations } from '../../../store/rootReducer';
 import OrganizationListItem from '../../../components/ui/OrganizationListItem';
-import { translate } from '../../../helpers/l10n';
+import { Button } from '../../../components/ui/buttons';
+import { getMyOrganizations } from '../../../store/rootReducer';
 import { isSonarCloud } from '../../../helpers/system';
+import { Organization } from '../../../app/types';
+import { translate } from '../../../helpers/l10n';
 
 interface StateProps {
   organizations: Organization[];
@@ -39,9 +40,7 @@ export class NoFavoriteProjects extends React.PureComponent<StateProps> {
     openProjectOnboarding: PropTypes.func
   };
 
-  onAnalyzeProjectClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
-    event.preventDefault();
-    event.currentTarget.blur();
+  onAnalyzeProjectClick = () => {
     this.context.openProjectOnboarding();
   };
 
@@ -54,11 +53,12 @@ export class NoFavoriteProjects extends React.PureComponent<StateProps> {
           <div className="spacer-top">
             <p>{translate('projects.no_favorite_projects.how_to_add_projects')}</p>
             <div className="huge-spacer-top">
-              <a className="button" href="#" onClick={this.onAnalyzeProjectClick}>
+              <Button onClick={this.onAnalyzeProjectClick}>
                 {isSonarCloud()
                   ? translate('provisioning.create_new_project')
                   : translate('my_account.analyze_new_project')}
-              </a>
+              </Button>
+
               <Dropdown
                 className="display-inline-block big-spacer-left"
                 overlay={
index a73ab9e50444d756961eb118815391f804545c60..1c8cd9c5ba2cb97dd507e959f42398583f535c2e 100644 (file)
@@ -45,13 +45,11 @@ exports[`renders for SonarCloud 1`] = `
     <div
       className="huge-spacer-top"
     >
-      <a
-        className="button"
-        href="#"
+      <Button
         onClick={[Function]}
       >
         provisioning.create_new_project
-      </a>
+      </Button>
       <Dropdown
         className="display-inline-block big-spacer-left"
         overlay={
index e7edd09e99f328e8b3eb529e11c4a368b69d54db..6f19ade590d698495adfd74dd2f4990d63185d83 100644 (file)
@@ -18,7 +18,6 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import * as PropTypes from 'prop-types';
 import { connect } from 'react-redux';
 import handleRequiredAuthentication from '../../app/utils/handleRequiredAuthentication';
 import Modal from '../../components/controls/Modal';
@@ -32,8 +31,9 @@ import { getCurrentUser } from '../../store/rootReducer';
 import './styles.css';
 
 interface OwnProps {
-  onClose: (doSkipOnboarding?: boolean) => void;
+  onClose: () => void;
   onOpenOrganizationOnboarding: () => void;
+  onOpenProjectOnboarding: () => void;
   onOpenTeamOnboarding: () => void;
 }
 
@@ -44,25 +44,12 @@ interface StateProps {
 type Props = OwnProps & StateProps;
 
 export class Onboarding extends React.PureComponent<Props> {
-  static contextTypes = {
-    router: PropTypes.object
-  };
-
   componentDidMount() {
     if (!isLoggedIn(this.props.currentUser)) {
       handleRequiredAuthentication();
     }
   }
 
-  openProjectOnboarding = () => {
-    this.props.onClose(false);
-    this.context.router.push('/onboarding');
-  };
-
-  onFinish = () => {
-    this.props.onClose(true);
-  };
-
   render() {
     if (!isLoggedIn(this.props.currentUser)) {
       return null;
@@ -73,14 +60,14 @@ export class Onboarding extends React.PureComponent<Props> {
       <Modal
         contentLabel={header}
         medium={true}
-        onRequestClose={this.onFinish}
+        onRequestClose={this.props.onClose}
         shouldCloseOnOverlayClick={false}>
         <div className="modal-simple-head text-center">
           <h1>{translate('onboarding.header')}</h1>
           <p className="spacer-top">{translate('onboarding.header.description')}</p>
         </div>
         <div className="modal-simple-body text-center onboarding-choices">
-          <Button className="onboarding-choice" onClick={this.openProjectOnboarding}>
+          <Button className="onboarding-choice" onClick={this.props.onOpenProjectOnboarding}>
             <OnboardingProjectIcon />
             <span>{translate('onboarding.analyze_public_code')}</span>
             <p className="note">{translate('onboarding.analyze_public_code.note')}</p>
@@ -97,7 +84,7 @@ export class Onboarding extends React.PureComponent<Props> {
           </Button>
         </div>
         <div className="modal-simple-footer text-center">
-          <ResetButtonLink className="spacer-bottom" onClick={this.onFinish}>
+          <ResetButtonLink className="spacer-bottom" onClick={this.props.onClose}>
             {translate('not_now')}
           </ResetButtonLink>
           <p className="note">{translate('onboarding.footer')}</p>
index d0350d43cc08233d4702aca30ac8388bed1c5a8a..9bab36759a58a9ba2886329de86682eaaa7d79d0 100644 (file)
@@ -29,6 +29,7 @@ it('renders correctly', () => {
         currentUser={{ isLoggedIn: true }}
         onClose={jest.fn()}
         onOpenOrganizationOnboarding={jest.fn()}
+        onOpenProjectOnboarding={jest.fn()}
         onOpenTeamOnboarding={jest.fn()}
       />
     )
@@ -38,6 +39,7 @@ it('renders correctly', () => {
 it('should correctly open the different tutorials', () => {
   const onClose = jest.fn();
   const onOpenOrganizationOnboarding = jest.fn();
+  const onOpenProjectOnboarding = jest.fn();
   const onOpenTeamOnboarding = jest.fn();
   const push = jest.fn();
   const wrapper = shallow(
@@ -45,6 +47,7 @@ it('should correctly open the different tutorials', () => {
       currentUser={{ isLoggedIn: true }}
       onClose={onClose}
       onOpenOrganizationOnboarding={onOpenOrganizationOnboarding}
+      onOpenProjectOnboarding={onOpenProjectOnboarding}
       onOpenTeamOnboarding={onOpenTeamOnboarding}
     />,
     { context: { router: { push } } }
@@ -55,6 +58,6 @@ it('should correctly open the different tutorials', () => {
 
   wrapper.find('Button').forEach(button => click(button));
   expect(onOpenOrganizationOnboarding).toHaveBeenCalled();
+  expect(onOpenProjectOnboarding).toHaveBeenCalled();
   expect(onOpenTeamOnboarding).toHaveBeenCalled();
-  expect(push).toHaveBeenCalledWith('/onboarding');
 });
index e0a56e3fc79f9dcee707772b3519837cb03ba2c4..18b2f16f846eb7979a8dbe96aef96deb60f1f773 100644 (file)
@@ -4,7 +4,7 @@ exports[`renders correctly 1`] = `
 <Modal
   contentLabel="onboarding.header"
   medium={true}
-  onRequestClose={[Function]}
+  onRequestClose={[MockFunction]}
   shouldCloseOnOverlayClick={false}
 >
   <div
@@ -24,7 +24,7 @@ exports[`renders correctly 1`] = `
   >
     <Button
       className="onboarding-choice"
-      onClick={[Function]}
+      onClick={[MockFunction]}
     >
       <OnboardingProjectIcon />
       <span>
@@ -70,7 +70,7 @@ exports[`renders correctly 1`] = `
   >
     <ResetButtonLink
       className="spacer-bottom"
-      onClick={[Function]}
+      onClick={[MockFunction]}
     >
       not_now
     </ResetButtonLink>
diff --git a/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/AlmRepositoryItem.tsx b/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/AlmRepositoryItem.tsx
new file mode 100644 (file)
index 0000000..8e21b71
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 Checkbox from '../../../components/controls/Checkbox';
+import { AlmRepository, IdentityProvider } from '../../../app/types';
+import { getBaseUrl } from '../../../helpers/urls';
+import { translate } from '../../../helpers/l10n';
+import CheckIcon from '../../../components/icons-components/CheckIcon';
+
+interface Props {
+  identityProvider: IdentityProvider;
+  repository: AlmRepository;
+  selected: boolean;
+  toggleRepository: (repository: AlmRepository) => void;
+}
+
+export default class AlmRepositoryItem extends React.PureComponent<Props> {
+  handleChange = () => {
+    this.props.toggleRepository(this.props.repository);
+  };
+
+  render() {
+    const { identityProvider, repository, selected } = this.props;
+    const alreadyImported = Boolean(repository.linkedProjectKey);
+    return (
+      <Checkbox
+        checked={selected || alreadyImported}
+        disabled={alreadyImported}
+        onCheck={this.handleChange}>
+        <img
+          alt={identityProvider.name}
+          className="spacer-left"
+          height={14}
+          src={getBaseUrl() + identityProvider.iconPath}
+          style={{ filter: alreadyImported ? 'invert(50%)' : 'invert(100%)' }}
+          width={14}
+        />
+        <span className="spacer-left">{this.props.repository.label}</span>
+        {alreadyImported && (
+          <span className="big-spacer-left">
+            <CheckIcon className="little-spacer-right" />
+            {translate('onboarding.create_project.already_imported')}
+          </span>
+        )}
+      </Checkbox>
+    );
+  }
+}
index 58c86e34696753de88bcf26e04c8ac1ac80db2e6..078a54d13de01883f7f8d1bc6d6703c1597c655c 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
+import AlmRepositoryItem from './AlmRepositoryItem';
 import DeferredSpinner from '../../../components/common/DeferredSpinner';
 import IdentityProviderLink from '../../../components/ui/IdentityProviderLink';
 import { getIdentityProviders } from '../../../api/users';
-import { getRepositories } from '../../../api/alm-integration';
-import { translateWithParameters } from '../../../helpers/l10n';
-import { IdentityProvider, LoggedInUser } from '../../../app/types';
+import { getRepositories, provisionProject } from '../../../api/alm-integration';
+import { IdentityProvider, LoggedInUser, AlmRepository } from '../../../app/types';
+import { ProjectBase } from '../../../api/components';
+import { SubmitButton } from '../../../components/ui/buttons';
+import { translateWithParameters, translate } from '../../../helpers/l10n';
 
 interface Props {
   currentUser: LoggedInUser;
+  onProjectCreate: (project: ProjectBase[]) => void;
 }
 
 interface State {
@@ -34,11 +38,20 @@ interface State {
   installationUrl?: string;
   installed?: boolean;
   loading: boolean;
+  repositories: AlmRepository[];
+  selectedRepositories: { [key: string]: AlmRepository | undefined };
+  submitting: boolean;
 }
 
 export default class AutoProjectCreate extends React.PureComponent<Props, State> {
   mounted = false;
-  state: State = { identityProviders: [], loading: true };
+  state: State = {
+    identityProviders: [],
+    loading: true,
+    repositories: [],
+    selectedRepositories: {},
+    submitting: false
+  };
 
   componentDidMount() {
     this.mounted = true;
@@ -66,22 +79,63 @@ export default class AutoProjectCreate extends React.PureComponent<Props, State>
   };
 
   fetchRepositories = () => {
-    return getRepositories().then(({ installation }) => {
+    return getRepositories().then(({ almIntegration, repositories }) => {
       if (this.mounted) {
-        this.setState({
-          installationUrl: installation.installationUrl,
-          installed: installation.enabled
-        });
+        this.setState({ ...almIntegration, repositories });
       }
     });
   };
 
+  handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
+    event.preventDefault();
+
+    if (this.isValid()) {
+      const { selectedRepositories } = this.state;
+      this.setState({ submitting: true });
+      provisionProject({
+        repositories: Object.keys(selectedRepositories).filter(key =>
+          Boolean(selectedRepositories[key])
+        )
+      }).then(
+        ({ project }) => this.props.onProjectCreate([project]),
+        () => {
+          if (this.mounted) {
+            this.setState({ submitting: false });
+            this.reloadRepositories();
+          }
+        }
+      );
+    }
+  };
+
+  isValid = () => {
+    return this.state.repositories.some(repo =>
+      Boolean(this.state.selectedRepositories[repo.installationKey])
+    );
+  };
+
+  reloadRepositories = () => {
+    this.setState({ loading: true });
+    this.fetchRepositories().then(this.stopLoading, this.stopLoading);
+  };
+
   stopLoading = () => {
     if (this.mounted) {
       this.setState({ loading: false });
     }
   };
 
+  toggleRepository = (repository: AlmRepository) => {
+    this.setState(({ selectedRepositories }) => ({
+      selectedRepositories: {
+        ...selectedRepositories,
+        [repository.installationKey]: selectedRepositories[repository.installationKey]
+          ? undefined
+          : repository
+      }
+    }));
+  };
+
   render() {
     if (this.state.loading) {
       return <DeferredSpinner />;
@@ -96,6 +150,8 @@ export default class AutoProjectCreate extends React.PureComponent<Props, State>
       return null;
     }
 
+    const { selectedRepositories, submitting } = this.state;
+
     return (
       <>
         <p className="alert alert-info width-60 big-spacer-bottom">
@@ -105,7 +161,24 @@ export default class AutoProjectCreate extends React.PureComponent<Props, State>
           )}
         </p>
         {this.state.installed ? (
-          'Repositories list'
+          <form onSubmit={this.handleFormSubmit}>
+            <ul>
+              {this.state.repositories.map(repo => (
+                <li className="big-spacer-bottom" key={repo.installationKey}>
+                  <AlmRepositoryItem
+                    identityProvider={identityProvider}
+                    repository={repo}
+                    selected={Boolean(selectedRepositories[repo.installationKey])}
+                    toggleRepository={this.toggleRepository}
+                  />
+                </li>
+              ))}
+            </ul>
+            <SubmitButton disabled={!this.isValid() || submitting}>
+              {translate('onboarding.create_project.create_project')}
+            </SubmitButton>
+            <DeferredSpinner className="spacer-left" loading={submitting} />
+          </form>
         ) : (
           <div>
             <p className="spacer-bottom">
index 7803335143c43328e7de0e9ab93241f0583059ab..17bae7689e741be0435e997ca7ecd5103da2fc25 100644 (file)
@@ -156,7 +156,10 @@ export class CreateProjectOnboarding extends React.PureComponent<Props, State> {
           )}
 
           {activeTab === Tabs.AUTO ? (
-            <AutoProjectCreate currentUser={currentUser} />
+            <AutoProjectCreate
+              currentUser={currentUser}
+              onProjectCreate={this.handleProjectCreate}
+            />
           ) : (
             <ManualProjectCreate
               currentUser={currentUser}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/AlmRepositoryItem-test.tsx b/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/AlmRepositoryItem-test.tsx
new file mode 100644 (file)
index 0000000..72b25cb
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { shallow } from 'enzyme';
+import AlmRepositoryItem from '../AlmRepositoryItem';
+
+const identityProviders = {
+  backgroundColor: 'blue',
+  iconPath: 'icon/path',
+  key: 'foo',
+  name: 'Foo Provider'
+};
+
+const repositories = [
+  {
+    label: 'Cool Project',
+    installationKey: 'github/cool',
+    linkedProjectKey: 'proj_cool',
+    linkedProjectName: 'Proj Cool'
+  },
+  {
+    label: 'Awesome Project',
+    installationKey: 'github/awesome'
+  }
+];
+
+it('should render correctly', () => {
+  expect(getWrapper()).toMatchSnapshot();
+});
+
+it('should render selected', () => {
+  expect(getWrapper({ selected: true })).toMatchSnapshot();
+});
+
+it('should render disabled', () => {
+  expect(getWrapper({ repository: repositories[0] })).toMatchSnapshot();
+});
+
+function getWrapper(props = {}) {
+  return shallow(
+    <AlmRepositoryItem
+      identityProvider={identityProviders}
+      repository={repositories[1]}
+      selected={false}
+      toggleRepository={jest.fn()}
+      {...props}
+    />
+  );
+}
index 10ea4c7e2152ec64246247fbece03707c13e1ab7..bfe9543057806013e96208ba68692d92bfebbd0c 100644 (file)
@@ -40,14 +40,28 @@ jest.mock('../../../../api/users', () => ({
 
 jest.mock('../../../../api/alm-integration', () => ({
   getRepositories: jest.fn().mockResolvedValue({
-    installation: {
+    almIntegration: {
       installationUrl: 'https://alm.foo.com/install',
-      enabled: false
-    }
-  })
+      installed: false
+    },
+    repositories: []
+  }),
+  provisionProject: jest.fn().mockResolvedValue({ projects: [] })
 }));
 
 const user: LoggedInUser = { isLoggedIn: true, login: 'foo', name: 'Foo', externalProvider: 'foo' };
+const repositories = [
+  {
+    label: 'Cool Project',
+    installationKey: 'github/cool',
+    linkedProjectKey: 'proj_cool',
+    linkedProjectName: 'Proj Cool'
+  },
+  {
+    label: 'Awesome Project',
+    installationKey: 'github/awesome'
+  }
+];
 
 beforeEach(() => {
   (getIdentityProviders as jest.Mock<any>).mockClear();
@@ -64,6 +78,19 @@ it('should display the provider app install button', async () => {
   expect(wrapper).toMatchSnapshot();
 });
 
+it('should display the list of repositories', async () => {
+  (getRepositories as jest.Mock<any>).mockResolvedValue({
+    almIntegration: {
+      installationUrl: 'https://alm.foo.com/install',
+      installed: true
+    },
+    repositories
+  });
+  const wrapper = getWrapper();
+  await waitAndUpdate(wrapper);
+  expect(wrapper).toMatchSnapshot();
+});
+
 function getWrapper(props = {}) {
-  return shallow(<AutoProjectCreate currentUser={user} {...props} />);
+  return shallow(<AutoProjectCreate currentUser={user} onProjectCreate={jest.fn()} {...props} />);
 }
diff --git a/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/__snapshots__/AlmRepositoryItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/tutorials/createProjectOnboarding/__tests__/__snapshots__/AlmRepositoryItem-test.tsx.snap
new file mode 100644 (file)
index 0000000..ad71bfc
--- /dev/null
@@ -0,0 +1,90 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<Checkbox
+  checked={false}
+  disabled={false}
+  onCheck={[Function]}
+  thirdState={false}
+>
+  <img
+    alt="Foo Provider"
+    className="spacer-left"
+    height={14}
+    src="icon/path"
+    style={
+      Object {
+        "filter": "invert(100%)",
+      }
+    }
+    width={14}
+  />
+  <span
+    className="spacer-left"
+  >
+    Awesome Project
+  </span>
+</Checkbox>
+`;
+
+exports[`should render disabled 1`] = `
+<Checkbox
+  checked={true}
+  disabled={true}
+  onCheck={[Function]}
+  thirdState={false}
+>
+  <img
+    alt="Foo Provider"
+    className="spacer-left"
+    height={14}
+    src="icon/path"
+    style={
+      Object {
+        "filter": "invert(50%)",
+      }
+    }
+    width={14}
+  />
+  <span
+    className="spacer-left"
+  >
+    Cool Project
+  </span>
+  <span
+    className="big-spacer-left"
+  >
+    <CheckIcon
+      className="little-spacer-right"
+    />
+    onboarding.create_project.already_imported
+  </span>
+</Checkbox>
+`;
+
+exports[`should render selected 1`] = `
+<Checkbox
+  checked={true}
+  disabled={false}
+  onCheck={[Function]}
+  thirdState={false}
+>
+  <img
+    alt="Foo Provider"
+    className="spacer-left"
+    height={14}
+    src="icon/path"
+    style={
+      Object {
+        "filter": "invert(100%)",
+      }
+    }
+    width={14}
+  />
+  <span
+    className="spacer-left"
+  >
+    Awesome Project
+  </span>
+</Checkbox>
+`;
index 9320a2510469e1a95c35abc33ec4265c6b0c8181..6d6c03988645839993160e7710266423261a55e8 100644 (file)
@@ -1,5 +1,79 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`should display the list of repositories 1`] = `
+<React.Fragment>
+  <p
+    className="alert alert-info width-60 big-spacer-bottom"
+  >
+    onboarding.create_project.beta_feature_x.Foo Provider
+  </p>
+  <form
+    onSubmit={[Function]}
+  >
+    <ul>
+      <li
+        className="big-spacer-bottom"
+        key="github/cool"
+      >
+        <AlmRepositoryItem
+          identityProvider={
+            Object {
+              "backgroundColor": "blue",
+              "iconPath": "icon/path",
+              "key": "foo",
+              "name": "Foo Provider",
+            }
+          }
+          repository={
+            Object {
+              "installationKey": "github/cool",
+              "label": "Cool Project",
+              "linkedProjectKey": "proj_cool",
+              "linkedProjectName": "Proj Cool",
+            }
+          }
+          selected={false}
+          toggleRepository={[Function]}
+        />
+      </li>
+      <li
+        className="big-spacer-bottom"
+        key="github/awesome"
+      >
+        <AlmRepositoryItem
+          identityProvider={
+            Object {
+              "backgroundColor": "blue",
+              "iconPath": "icon/path",
+              "key": "foo",
+              "name": "Foo Provider",
+            }
+          }
+          repository={
+            Object {
+              "installationKey": "github/awesome",
+              "label": "Awesome Project",
+            }
+          }
+          selected={false}
+          toggleRepository={[Function]}
+        />
+      </li>
+    </ul>
+    <SubmitButton
+      disabled={true}
+    >
+      onboarding.create_project.create_project
+    </SubmitButton>
+    <DeferredSpinner
+      className="spacer-left"
+      loading={false}
+      timeout={100}
+    />
+  </form>
+</React.Fragment>
+`;
+
 exports[`should display the provider app install button 1`] = `
 <DeferredSpinner
   timeout={100}
index 9f5b34ba428f2db1bc64b58b0cc5c42c4e51bf42..5c8f9c886418f14ce10c679b69042e0c9c237dbd 100644 (file)
@@ -26,8 +26,8 @@ const routes = [
       component: lazyLoad(
         () =>
           isSonarCloud()
-            ? import('../../apps/tutorials/createProjectOnboarding/CreateProjectOnboarding')
-            : import('../../apps/tutorials/projectOnboarding/ProjectOnboardingPage')
+            ? import('./createProjectOnboarding/CreateProjectOnboarding')
+            : import('./projectOnboarding/ProjectOnboardingPage')
       )
     }
   }
index 954cfa39d18bf01dc355b163bff6d18118f6d175..2265fcc9f90de062faf3b105887ed959eeeebf9c 100644 (file)
@@ -2660,6 +2660,7 @@ onboarding.project.header=Analyze a project
 onboarding.project.header.description=Want to quickly analyze a first project? Follow these {0} easy steps.
 
 onboarding.create_project.header=Create project(s)
+onboarding.create_project.already_imported=Repository already imported
 onboarding.create_project.beta_feature_x=This feature is being beta tested. We offer to create projects from your {0} repositories only for public personal projects on your personal SonarCloud organization. For other kind of projects please create them maually.
 onboarding.create_project.create_manually=Create manually
 onboarding.create_project.create_new_org=I want to create another organization