]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-11038 Fix project provisioning
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Fri, 3 Aug 2018 08:06:20 +0000 (10:06 +0200)
committerSonarTech <sonartech@sonarsource.com>
Fri, 10 Aug 2018 18:21:30 +0000 (20:21 +0200)
16 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/components/nav/global/GlobalNavMenu.tsx
server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.tsx.snap
server/sonar-web/src/main/js/apps/projects/create/AlmRepositoryItem.tsx
server/sonar-web/src/main/js/apps/projects/create/AutoProjectCreate.tsx
server/sonar-web/src/main/js/apps/projects/create/CreateProjectPage.tsx
server/sonar-web/src/main/js/apps/projects/create/ManualProjectCreate.tsx
server/sonar-web/src/main/js/apps/projects/create/__tests__/AutoProjectCreate-test.tsx
server/sonar-web/src/main/js/apps/projects/create/__tests__/CreateProjectPage-test.tsx
server/sonar-web/src/main/js/apps/projects/create/__tests__/ManualProjectCreate-test.tsx
server/sonar-web/src/main/js/apps/projects/create/__tests__/__snapshots__/AlmRepositoryItem-test.tsx.snap
server/sonar-web/src/main/js/apps/projects/create/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap
server/sonar-web/src/main/js/apps/projects/create/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap
server/sonar-web/src/main/js/apps/projects/create/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap
server/sonar-web/src/main/js/apps/tutorials/components/OrganizationStep.tsx

index 798f16e6d89e90a07726fd39aeae60b9fd96a64e..d35ff9995e0e0dcc39dd04832683bf8751487e23 100644 (file)
@@ -31,6 +31,11 @@ export function getRepositories(): Promise<{
   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);
+export function provisionProject(data: {
+  installationKeys: string[];
+}): Promise<{ projects: Array<{ projectKey: string }> }> {
+  return postJSON('/api/alm_integration/provision_projects', {
+    ...data,
+    installationKeys: data.installationKeys.join(',')
+  }).catch(throwGlobalError);
 }
index b85885bbcd0af8fd0ba0c79a198904431ccd66b8..7930a9b7f214fce93fb8918cf279ccc04afb7dd2 100644 (file)
@@ -171,7 +171,9 @@ export class StartupModal extends React.PureComponent<Props, State> {
     const { currentUser, location } = this.props;
     if (
       currentUser.showOnboardingTutorial &&
-      !['about', 'documentation', 'onboarding'].some(path => location.pathname.startsWith(path))
+      !['about', 'documentation', 'onboarding', 'projects/create'].some(path =>
+        location.pathname.startsWith(path)
+      )
     ) {
       this.setState({ automatic: true });
       if (isSonarCloud()) {
index d734d6c1401a7425b76ce8082e416ac749006ed8..ccb40acc1f64f00c4da939f6b5db805d1256342a 100644 (file)
@@ -18,8 +18,8 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import { Link } from 'react-router';
 import * as classNames from 'classnames';
+import { Link } from 'react-router';
 import { isLoggedIn, CurrentUser, AppState, Extension } from '../../../types';
 import { translate } from '../../../../helpers/l10n';
 import { getQualityGatesUrl, getBaseUrl } from '../../../../helpers/urls';
@@ -44,9 +44,13 @@ export default class GlobalNavMenu extends React.PureComponent<Props> {
       return null;
     }
 
+    const active =
+      this.props.location.pathname.startsWith('projects') &&
+      this.props.location.pathname !== 'projects/create';
+
     return (
       <li>
-        <Link activeClassName="active" to="/projects">
+        <Link activeClassName={classNames({ active })} to="/projects">
           {isSonarCloud() ? translate('my_projects') : translate('projects.page')}
         </Link>
       </li>
@@ -74,7 +78,7 @@ export default class GlobalNavMenu extends React.PureComponent<Props> {
       return (
         <li>
           <Link
-            className={active ? 'active' : undefined}
+            className={classNames({ active })}
             to={{ pathname: '/issues', query: { resolved: 'false' } }}>
             {translate('my_issues')}
           </Link>
@@ -88,7 +92,7 @@ export default class GlobalNavMenu extends React.PureComponent<Props> {
         : { resolved: 'false' };
     return (
       <li>
-        <Link className={active ? 'active' : undefined} to={{ pathname: '/issues', query }}>
+        <Link className={classNames({ active })} to={{ pathname: '/issues', query }}>
           {translate('issues.page')}
         </Link>
       </li>
index b886f31dab472c22b6b21d4cfe1208fbe08d067b..2acadf725f88fee6ac5815fecc4e25eda9f78488 100644 (file)
@@ -6,7 +6,7 @@ exports[`should show administration menu if the user has the rights 1`] = `
 >
   <li>
     <Link
-      activeClassName="active"
+      activeClassName=""
       onlyActiveOnIndex={false}
       style={Object {}}
       to="/projects"
@@ -16,6 +16,7 @@ exports[`should show administration menu if the user has the rights 1`] = `
   </li>
   <li>
     <Link
+      className=""
       onlyActiveOnIndex={false}
       style={Object {}}
       to={
index e01c79c32dc8218cac7ff5ee40ac0c9115a548cd..8fd083fb222a05702b5830f936a6515a4ca8180f 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
+import { Link } from 'react-router';
 import * as theme from '../../../app/theme';
 import Checkbox from '../../../components/controls/Checkbox';
 import CheckIcon from '../../../components/icons-components/CheckIcon';
 import { AlmRepository, IdentityProvider } from '../../../app/types';
-import { getBaseUrl } from '../../../helpers/urls';
+import { getBaseUrl, getProjectUrl } from '../../../helpers/urls';
 import { translate } from '../../../helpers/l10n';
 
 interface Props {
@@ -41,26 +42,30 @@ export default class AlmRepositoryItem extends React.PureComponent<Props> {
     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()}/images/sonarcloud/${identityProvider.key}.svg`}
-          style={{ opacity: alreadyImported ? 0.5 : 1 }}
-          width={14}
-        />
-        <span className="spacer-left">{this.props.repository.label}</span>
-        {alreadyImported && (
+      <>
+        <Checkbox
+          checked={selected || alreadyImported}
+          disabled={alreadyImported}
+          onCheck={this.handleChange}>
+          <img
+            alt={identityProvider.name}
+            className="spacer-left"
+            height={14}
+            src={`${getBaseUrl()}/images/sonarcloud/${identityProvider.key}.svg`}
+            style={{ opacity: alreadyImported ? 0.5 : 1 }}
+            width={14}
+          />
+          <span className="spacer-left">{this.props.repository.label}</span>
+        </Checkbox>
+        {repository.linkedProjectKey && (
           <span className="big-spacer-left">
             <CheckIcon className="little-spacer-right" fill={theme.green} />
-            {translate('onboarding.create_project.already_imported')}
+            <Link to={getProjectUrl(repository.linkedProjectKey)}>
+              {translate('onboarding.create_project.already_imported')}
+            </Link>
           </span>
         )}
-      </Checkbox>
+      </>
     );
   }
 }
index 078a54d13de01883f7f8d1bc6d6703c1597c655c..920be1639382b19c07a6b6819406eea005ba95b8 100644 (file)
@@ -21,20 +21,17 @@ 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, provisionProject } from '../../../api/alm-integration';
-import { IdentityProvider, LoggedInUser, AlmRepository } from '../../../app/types';
-import { ProjectBase } from '../../../api/components';
+import { IdentityProvider, AlmRepository } from '../../../app/types';
 import { SubmitButton } from '../../../components/ui/buttons';
 import { translateWithParameters, translate } from '../../../helpers/l10n';
 
 interface Props {
-  currentUser: LoggedInUser;
-  onProjectCreate: (project: ProjectBase[]) => void;
+  identityProvider: IdentityProvider;
+  onProjectCreate: (projectKeys: string[]) => void;
 }
 
 interface State {
-  identityProviders: IdentityProvider[];
   installationUrl?: string;
   installed?: boolean;
   loading: boolean;
@@ -46,7 +43,6 @@ interface State {
 export default class AutoProjectCreate extends React.PureComponent<Props, State> {
   mounted = false;
   state: State = {
-    identityProviders: [],
     loading: true,
     repositories: [],
     selectedRepositories: {},
@@ -55,37 +51,28 @@ export default class AutoProjectCreate extends React.PureComponent<Props, State>
 
   componentDidMount() {
     this.mounted = true;
-    Promise.all([this.fetchIdentityProviders(), this.fetchRepositories()]).then(
-      this.stopLoading,
-      this.stopLoading
-    );
+    this.fetchRepositories();
   }
 
   componentWillUnmount() {
     this.mounted = false;
   }
 
-  fetchIdentityProviders = () => {
-    return getIdentityProviders().then(
-      ({ identityProviders }) => {
+  fetchRepositories = () => {
+    getRepositories().then(
+      ({ almIntegration, repositories }) => {
         if (this.mounted) {
-          this.setState({ identityProviders });
+          this.setState({ ...almIntegration, loading: false, repositories });
         }
       },
       () => {
-        return Promise.resolve();
+        if (this.mounted) {
+          this.setState({ loading: false });
+        }
       }
     );
   };
 
-  fetchRepositories = () => {
-    return getRepositories().then(({ almIntegration, repositories }) => {
-      if (this.mounted) {
-        this.setState({ ...almIntegration, repositories });
-      }
-    });
-  };
-
   handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
     event.preventDefault();
 
@@ -93,15 +80,15 @@ export default class AutoProjectCreate extends React.PureComponent<Props, State>
       const { selectedRepositories } = this.state;
       this.setState({ submitting: true });
       provisionProject({
-        repositories: Object.keys(selectedRepositories).filter(key =>
+        installationKeys: Object.keys(selectedRepositories).filter(key =>
           Boolean(selectedRepositories[key])
         )
       }).then(
-        ({ project }) => this.props.onProjectCreate([project]),
+        ({ projects }) => this.props.onProjectCreate(projects.map(project => project.projectKey)),
         () => {
           if (this.mounted) {
-            this.setState({ submitting: false });
-            this.reloadRepositories();
+            this.setState({ loading: true, submitting: false });
+            this.fetchRepositories();
           }
         }
       );
@@ -114,17 +101,6 @@ export default class AutoProjectCreate extends React.PureComponent<Props, State>
     );
   };
 
-  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: {
@@ -136,21 +112,57 @@ export default class AutoProjectCreate extends React.PureComponent<Props, State>
     }));
   };
 
-  render() {
-    if (this.state.loading) {
-      return <DeferredSpinner />;
-    }
-
-    const { currentUser } = this.props;
-    const identityProvider = this.state.identityProviders.find(
-      identityProvider => identityProvider.key === currentUser.externalProvider
-    );
+  renderContent = () => {
+    const { identityProvider } = this.props;
+    const { selectedRepositories, submitting } = this.state;
 
-    if (!identityProvider) {
-      return null;
+    if (this.state.installed) {
+      return (
+        <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('create')}
+          </SubmitButton>
+          <DeferredSpinner className="spacer-left" loading={submitting} />
+        </form>
+      );
     }
+    return (
+      <div>
+        <p className="spacer-bottom">
+          {translateWithParameters(
+            'onboarding.create_project.install_app_x',
+            identityProvider.name
+          )}
+        </p>
+        <IdentityProviderLink
+          className="display-inline-block"
+          identityProvider={identityProvider}
+          small={true}
+          url={this.state.installationUrl}>
+          {translateWithParameters(
+            'onboarding.create_project.install_app_x.button',
+            identityProvider.name
+          )}
+        </IdentityProviderLink>
+      </div>
+    );
+  };
 
-    const { selectedRepositories, submitting } = this.state;
+  render() {
+    const { identityProvider } = this.props;
+    const { loading } = this.state;
 
     return (
       <>
@@ -160,45 +172,7 @@ export default class AutoProjectCreate extends React.PureComponent<Props, State>
             identityProvider.name
           )}
         </p>
-        {this.state.installed ? (
-          <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">
-              {translateWithParameters(
-                'onboarding.create_project.install_app_x',
-                identityProvider.name
-              )}
-            </p>
-            <IdentityProviderLink
-              className="display-inline-block"
-              identityProvider={identityProvider}
-              small={true}
-              url={this.state.installationUrl}>
-              {translateWithParameters(
-                'onboarding.create_project.install_app_x.button',
-                identityProvider.name
-              )}
-            </IdentityProviderLink>
-          </div>
-        )}
+        {loading ? <DeferredSpinner /> : this.renderContent()}
       </>
     );
   }
index 63224ebaceccefd29b914208f505c8824b7ea29d..a3cdbb510a09dae1e1f0e8c1f48a6362bb3f23d5 100644 (file)
@@ -26,18 +26,18 @@ import Helmet from 'react-helmet';
 import AutoProjectCreate from './AutoProjectCreate';
 import ManualProjectCreate from './ManualProjectCreate';
 import { serializeQuery, Query, parseQuery } from './utils';
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
 import handleRequiredAuthentication from '../../../app/utils/handleRequiredAuthentication';
 import { getCurrentUser } from '../../../store/rootReducer';
-import { skipOnboarding } from '../../../store/users/actions';
-import { CurrentUser, isLoggedIn } from '../../../app/types';
+import { skipOnboarding as skipOnboardingAction } from '../../../store/users/actions';
+import { CurrentUser, IdentityProvider, isLoggedIn, LoggedInUser } from '../../../app/types';
+import { skipOnboarding, getIdentityProviders } from '../../../api/users';
 import { translate } from '../../../helpers/l10n';
-import { ProjectBase } from '../../../api/components';
-import { getProjectUrl, getOrganizationUrl } from '../../../helpers/urls';
+import { getProjectUrl } from '../../../helpers/urls';
 import '../../../app/styles/sonarcloud.css';
 
 interface OwnProps {
   location: Location;
-  onFinishOnboarding: () => void;
   router: Pick<InjectedRouter, 'push' | 'replace'>;
 }
 
@@ -46,16 +46,24 @@ interface StateProps {
 }
 
 interface DispatchProps {
-  skipOnboarding: () => void;
+  skipOnboardingAction: () => void;
 }
 
-type Props = OwnProps & StateProps & DispatchProps;
+interface Props extends OwnProps, StateProps, DispatchProps {
+  currentUser: LoggedInUser;
+}
+
+interface State {
+  identityProvider?: IdentityProvider;
+  loading: boolean;
+}
 
-export class CreateProjectPage extends React.PureComponent<Props> {
+export class CreateProjectPage extends React.PureComponent<Props, State> {
   mounted = false;
 
   constructor(props: Props) {
     super(props);
+    this.state = { loading: true };
     if (!this.canAutoCreate(props)) {
       this.updateQuery({ manual: true });
     }
@@ -76,18 +84,37 @@ export class CreateProjectPage extends React.PureComponent<Props> {
     document.documentElement.classList.remove('white-page');
   }
 
-  handleProjectCreate = (projects: Pick<ProjectBase, 'key'>[], organization?: string) => {
-    if (projects.length > 1 && organization) {
-      this.props.router.push(getOrganizationUrl(organization) + '/projects');
-    } else if (projects.length === 1) {
-      this.props.router.push(getProjectUrl(projects[0].key));
+  handleProjectCreate = (projectKeys: string[]) => {
+    skipOnboarding().catch(() => {});
+    this.props.skipOnboardingAction();
+    if (projectKeys.length > 1) {
+      this.props.router.push({ pathname: '/projects' });
+    } else if (projectKeys.length === 1) {
+      this.props.router.push(getProjectUrl(projectKeys[0]));
     }
   };
 
   canAutoCreate = ({ currentUser } = this.props) => {
-    return (
-      isLoggedIn(currentUser) &&
-      ['bitbucket', 'github'].includes(currentUser.externalProvider || '')
+    return ['bitbucket', 'github'].includes(currentUser.externalProvider || '');
+  };
+
+  fetchIdentityProviders = () => {
+    getIdentityProviders().then(
+      ({ identityProviders }) => {
+        if (this.mounted) {
+          this.setState({
+            identityProvider: identityProviders.find(
+              identityProvider => identityProvider.key === this.props.currentUser.externalProvider
+            ),
+            loading: false
+          });
+        }
+      },
+      () => {
+        if (this.mounted) {
+          this.setState({ loading: false });
+        }
+      }
     );
   };
 
@@ -110,11 +137,10 @@ export class CreateProjectPage extends React.PureComponent<Props> {
 
   render() {
     const { currentUser } = this.props;
-    if (!isLoggedIn(currentUser)) {
-      return null;
-    }
+    const { identityProvider, loading } = this.state;
     const displayManual = parseQuery(this.props.location.query).manual;
     const header = translate('onboarding.create_project.header');
+    const hasAutoProvisioning = this.canAutoCreate() && identityProvider;
     return (
       <>
         <Helmet title={header} titleTemplate="%s" />
@@ -122,48 +148,53 @@ export class CreateProjectPage extends React.PureComponent<Props> {
           <div className="page-header">
             <h1 className="page-title">{header}</h1>
           </div>
-
-          {this.canAutoCreate() && (
-            <ul className="flex-tabs">
-              <li>
-                <a
-                  className={classNames('js-auto', { selected: !displayManual })}
-                  href="#"
-                  onClick={this.showAuto}>
-                  {translate('onboarding.create_project.select_repositories')}
-                  <span
-                    className={classNames(
-                      'rounded alert alert-small spacer-left display-inline-block',
-                      {
-                        'alert-info': !displayManual,
-                        'alert-muted': displayManual
-                      }
-                    )}>
-                    {translate('beta')}
-                  </span>
-                </a>
-              </li>
-              <li>
-                <a
-                  className={classNames('js-manual', { selected: displayManual })}
-                  href="#"
-                  onClick={this.showManual}>
-                  {translate('onboarding.create_project.create_manually')}
-                </a>
-              </li>
-            </ul>
-          )}
-
-          {displayManual || !this.canAutoCreate() ? (
-            <ManualProjectCreate
-              currentUser={currentUser}
-              onProjectCreate={this.handleProjectCreate}
-            />
+          {loading ? (
+            <DeferredSpinner />
           ) : (
-            <AutoProjectCreate
-              currentUser={currentUser}
-              onProjectCreate={this.handleProjectCreate}
-            />
+            <>
+              {hasAutoProvisioning && (
+                <ul className="flex-tabs">
+                  <li>
+                    <a
+                      className={classNames('js-auto', { selected: !displayManual })}
+                      href="#"
+                      onClick={this.showAuto}>
+                      {translate('onboarding.create_project.select_repositories')}
+                      <span
+                        className={classNames(
+                          'rounded alert alert-small spacer-left display-inline-block',
+                          {
+                            'alert-info': !displayManual,
+                            'alert-muted': displayManual
+                          }
+                        )}>
+                        {translate('beta')}
+                      </span>
+                    </a>
+                  </li>
+                  <li>
+                    <a
+                      className={classNames('js-manual', { selected: displayManual })}
+                      href="#"
+                      onClick={this.showManual}>
+                      {translate('onboarding.create_project.create_manually')}
+                    </a>
+                  </li>
+                </ul>
+              )}
+
+              {displayManual || !hasAutoProvisioning ? (
+                <ManualProjectCreate
+                  currentUser={currentUser}
+                  onProjectCreate={this.handleProjectCreate}
+                />
+              ) : (
+                <AutoProjectCreate
+                  identityProvider={identityProvider!}
+                  onProjectCreate={this.handleProjectCreate}
+                />
+              )}
+            </>
           )}
         </div>
       </>
@@ -177,7 +208,7 @@ const mapStateToProps = (state: any): StateProps => {
   };
 };
 
-const mapDispatchToProps: DispatchProps = { skipOnboarding };
+const mapDispatchToProps: DispatchProps = { skipOnboardingAction };
 
 export default connect<StateProps, DispatchProps, OwnProps>(mapStateToProps, mapDispatchToProps)(
   CreateProjectPage
index 041b09bdd4d6c0f5122853c904f3690824dedf86..d1df2b59ec053479d18a1eab2b8aa36872847b2a 100644 (file)
@@ -27,7 +27,7 @@ import { LoggedInUser, Organization } from '../../../app/types';
 import { fetchMyOrganizations } from '../../account/organizations/actions';
 import { getMyOrganizations } from '../../../store/rootReducer';
 import { translate } from '../../../helpers/l10n';
-import { createProject, ProjectBase } from '../../../api/components';
+import { createProject } from '../../../api/components';
 import DeferredSpinner from '../../../components/common/DeferredSpinner';
 
 interface StateProps {
@@ -40,7 +40,7 @@ interface DispatchProps {
 
 interface OwnProps {
   currentUser: LoggedInUser;
-  onProjectCreate: (project: ProjectBase[]) => void;
+  onProjectCreate: (projectKeys: string[]) => void;
 }
 
 type Props = OwnProps & StateProps & DispatchProps;
@@ -91,7 +91,7 @@ export class ManualProjectCreate extends React.PureComponent<Props, State> {
         name: projectName,
         organization: selectedOrganization
       }).then(
-        ({ project }) => this.props.onProjectCreate([project]),
+        ({ project }) => this.props.onProjectCreate([project.key]),
         () => {
           if (this.mounted) {
             this.setState({ submitting: false });
@@ -198,7 +198,7 @@ export class ManualProjectCreate extends React.PureComponent<Props, State> {
             />
           </div>
           <SubmitButton disabled={!this.isValid() || submitting}>
-            {translate('onboarding.create_project.create_project')}
+            {translate('create')}
           </SubmitButton>
           <DeferredSpinner className="spacer-left" loading={submitting} />
         </form>
index bfe9543057806013e96208ba68692d92bfebbd0c..91e189f4204a23fd2a240f9ff104e0fa740522a8 100644 (file)
 import * as React from 'react';
 import { shallow } from 'enzyme';
 import AutoProjectCreate from '../AutoProjectCreate';
-import { getIdentityProviders } from '../../../../api/users';
 import { getRepositories } from '../../../../api/alm-integration';
-import { LoggedInUser } from '../../../../app/types';
 import { waitAndUpdate } from '../../../../helpers/testUtils';
 
-jest.mock('../../../../api/users', () => ({
-  getIdentityProviders: jest.fn().mockResolvedValue({
-    identityProviders: [
-      {
-        backgroundColor: 'blue',
-        iconPath: 'icon/path',
-        key: 'foo',
-        name: 'Foo Provider'
-      }
-    ]
-  })
-}));
-
 jest.mock('../../../../api/alm-integration', () => ({
   getRepositories: jest.fn().mockResolvedValue({
     almIntegration: {
@@ -49,7 +34,13 @@ jest.mock('../../../../api/alm-integration', () => ({
   provisionProject: jest.fn().mockResolvedValue({ projects: [] })
 }));
 
-const user: LoggedInUser = { isLoggedIn: true, login: 'foo', name: 'Foo', externalProvider: 'foo' };
+const identityProvider = {
+  backgroundColor: 'blue',
+  iconPath: 'icon/path',
+  key: 'foo',
+  name: 'Foo Provider'
+};
+
 const repositories = [
   {
     label: 'Cool Project',
@@ -64,14 +55,12 @@ const repositories = [
 ];
 
 beforeEach(() => {
-  (getIdentityProviders as jest.Mock<any>).mockClear();
   (getRepositories as jest.Mock<any>).mockClear();
 });
 
 it('should display the provider app install button', async () => {
   const wrapper = getWrapper();
   expect(wrapper).toMatchSnapshot();
-  expect(getIdentityProviders).toHaveBeenCalled();
   expect(getRepositories).toHaveBeenCalled();
 
   await waitAndUpdate(wrapper);
@@ -92,5 +81,7 @@ it('should display the list of repositories', async () => {
 });
 
 function getWrapper(props = {}) {
-  return shallow(<AutoProjectCreate currentUser={user} onProjectCreate={jest.fn()} {...props} />);
+  return shallow(
+    <AutoProjectCreate identityProvider={identityProvider} onProjectCreate={jest.fn()} {...props} />
+  );
 }
index 6f995afd252b1b4918c5eaefcab541996de204df..42f39b9fb616e1833042f96ef088020cebbb393f 100644 (file)
@@ -21,8 +21,22 @@ import * as React from 'react';
 import { shallow } from 'enzyme';
 import { Location } from 'history';
 import { CreateProjectPage } from '../CreateProjectPage';
+import { getIdentityProviders } from '../../../../api/users';
 import { LoggedInUser } from '../../../../app/types';
-import { click } from '../../../../helpers/testUtils';
+import { click, waitAndUpdate } from '../../../../helpers/testUtils';
+
+jest.mock('../../../../api/users', () => ({
+  getIdentityProviders: jest.fn().mockResolvedValue({
+    identityProviders: [
+      {
+        backgroundColor: 'blue',
+        iconPath: 'icon/path',
+        key: 'github',
+        name: 'GitHub'
+      }
+    ]
+  })
+}));
 
 const user: LoggedInUser = {
   externalProvider: 'github',
@@ -31,21 +45,32 @@ const user: LoggedInUser = {
   name: 'Foo'
 };
 
-it('should render correctly', () => {
-  expect(getWrapper()).toMatchSnapshot();
+beforeEach(() => {
+  (getIdentityProviders as jest.Mock<any>).mockClear();
+});
+
+it('should render correctly', async () => {
+  const wrapper = getWrapper();
+  expect(wrapper).toMatchSnapshot();
+  await waitAndUpdate(wrapper);
+  expect(wrapper).toMatchSnapshot();
 });
 
 it('should render with Manual creation only', () => {
   expect(getWrapper({ currentUser: { ...user, externalProvider: 'microsoft' } })).toMatchSnapshot();
 });
 
-it('should switch tabs', () => {
+it('should switch tabs', async () => {
   const replace = jest.fn();
   const wrapper = getWrapper({ router: { replace } });
   replace.mockImplementation(location => {
     wrapper.setProps({ location }).update();
   });
 
+  await waitAndUpdate(wrapper);
+
+  expect(wrapper).toMatchSnapshot();
+
   click(wrapper.find('.js-manual'));
   expect(wrapper.find('Connect(ManualProjectCreate)').exists()).toBeTruthy();
   click(wrapper.find('.js-auto'));
@@ -57,9 +82,8 @@ function getWrapper(props = {}) {
     <CreateProjectPage
       currentUser={user}
       location={{ pathname: 'foo', query: { manual: 'false' } } as Location}
-      onFinishOnboarding={jest.fn()}
       router={{ push: jest.fn(), replace: jest.fn() }}
-      skipOnboarding={jest.fn()}
+      skipOnboardingAction={jest.fn()}
       {...props}
     />
   );
index b79b4e4ae352cd87a66ca7c58ea7af0c5e800104..f67db69e464def67490d548cd2cc382fdee393ce 100644 (file)
@@ -63,7 +63,7 @@ it('should correctly create a project', async () => {
   expect(createProject).toBeCalledWith({ project: 'bar', name: 'Bar', organization: 'foo' });
 
   await waitAndUpdate(wrapper);
-  expect(onProjectCreate).toBeCalledWith([{ key: 'bar', name: 'Bar' }]);
+  expect(onProjectCreate).toBeCalledWith(['bar']);
 });
 
 function getWrapper(props = {}) {
index eddcbf0214b76338ea640be74a113a20ec35e00b..292a0a839c0d2f57053583fc2b334de207a1a404 100644 (file)
@@ -1,56 +1,60 @@
 // 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="/images/sonarcloud/foo.svg"
-    style={
-      Object {
-        "opacity": 1,
-      }
-    }
-    width={14}
-  />
-  <span
-    className="spacer-left"
+<React.Fragment>
+  <Checkbox
+    checked={false}
+    disabled={false}
+    onCheck={[Function]}
+    thirdState={false}
   >
-    Awesome Project
-  </span>
-</Checkbox>
+    <img
+      alt="Foo Provider"
+      className="spacer-left"
+      height={14}
+      src="/images/sonarcloud/foo.svg"
+      style={
+        Object {
+          "opacity": 1,
+        }
+      }
+      width={14}
+    />
+    <span
+      className="spacer-left"
+    >
+      Awesome Project
+    </span>
+  </Checkbox>
+</React.Fragment>
 `;
 
 exports[`should render disabled 1`] = `
-<Checkbox
-  checked={true}
-  disabled={true}
-  onCheck={[Function]}
-  thirdState={false}
->
-  <img
-    alt="Foo Provider"
-    className="spacer-left"
-    height={14}
-    src="/images/sonarcloud/foo.svg"
-    style={
-      Object {
-        "opacity": 0.5,
-      }
-    }
-    width={14}
-  />
-  <span
-    className="spacer-left"
+<React.Fragment>
+  <Checkbox
+    checked={true}
+    disabled={true}
+    onCheck={[Function]}
+    thirdState={false}
   >
-    Cool Project
-  </span>
+    <img
+      alt="Foo Provider"
+      className="spacer-left"
+      height={14}
+      src="/images/sonarcloud/foo.svg"
+      style={
+        Object {
+          "opacity": 0.5,
+        }
+      }
+      width={14}
+    />
+    <span
+      className="spacer-left"
+    >
+      Cool Project
+    </span>
+  </Checkbox>
   <span
     className="big-spacer-left"
   >
@@ -58,34 +62,50 @@ exports[`should render disabled 1`] = `
       className="little-spacer-right"
       fill="#00aa00"
     />
-    onboarding.create_project.already_imported
+    <Link
+      onlyActiveOnIndex={false}
+      style={Object {}}
+      to={
+        Object {
+          "pathname": "/dashboard",
+          "query": Object {
+            "branch": undefined,
+            "id": "proj_cool",
+          },
+        }
+      }
+    >
+      onboarding.create_project.already_imported
+    </Link>
   </span>
-</Checkbox>
+</React.Fragment>
 `;
 
 exports[`should render selected 1`] = `
-<Checkbox
-  checked={true}
-  disabled={false}
-  onCheck={[Function]}
-  thirdState={false}
->
-  <img
-    alt="Foo Provider"
-    className="spacer-left"
-    height={14}
-    src="/images/sonarcloud/foo.svg"
-    style={
-      Object {
-        "opacity": 1,
-      }
-    }
-    width={14}
-  />
-  <span
-    className="spacer-left"
+<React.Fragment>
+  <Checkbox
+    checked={true}
+    disabled={false}
+    onCheck={[Function]}
+    thirdState={false}
   >
-    Awesome Project
-  </span>
-</Checkbox>
+    <img
+      alt="Foo Provider"
+      className="spacer-left"
+      height={14}
+      src="/images/sonarcloud/foo.svg"
+      style={
+        Object {
+          "opacity": 1,
+        }
+      }
+      width={14}
+    />
+    <span
+      className="spacer-left"
+    >
+      Awesome Project
+    </span>
+  </Checkbox>
+</React.Fragment>
 `;
index 6d6c03988645839993160e7710266423261a55e8..7a0c37609290e604994f50dfce56709fdea761a8 100644 (file)
@@ -63,7 +63,7 @@ exports[`should display the list of repositories 1`] = `
     <SubmitButton
       disabled={true}
     >
-      onboarding.create_project.create_project
+      create
     </SubmitButton>
     <DeferredSpinner
       className="spacer-left"
@@ -75,9 +75,16 @@ exports[`should display the list of repositories 1`] = `
 `;
 
 exports[`should display the provider app install button 1`] = `
-<DeferredSpinner
-  timeout={100}
-/>
+<React.Fragment>
+  <p
+    className="alert alert-info width-60 big-spacer-bottom"
+  >
+    onboarding.create_project.beta_feature_x.Foo Provider
+  </p>
+  <DeferredSpinner
+    timeout={100}
+  />
+</React.Fragment>
 `;
 
 exports[`should display the provider app install button 2`] = `
index 1880a16ba4cc6e9775efa72e2f0cee4d20050831..fffc678a6db7609959e9a72ccd39f5ebc3d6f428 100644 (file)
@@ -20,44 +20,73 @@ exports[`should render correctly 1`] = `
         onboarding.create_project.header
       </h1>
     </div>
-    <ul
-      className="flex-tabs"
+    <DeferredSpinner
+      timeout={100}
+    />
+  </div>
+</React.Fragment>
+`;
+
+exports[`should render correctly 2`] = `
+<React.Fragment>
+  <HelmetWrapper
+    defer={true}
+    encodeSpecialCharacters={true}
+    title="onboarding.create_project.header"
+    titleTemplate="%s"
+  />
+  <div
+    className="sonarcloud page page-limited"
+  >
+    <div
+      className="page-header"
     >
-      <li>
-        <a
-          className="js-auto selected"
-          href="#"
-          onClick={[Function]}
-        >
-          onboarding.create_project.select_repositories
-          <span
-            className="rounded alert alert-small spacer-left display-inline-block alert-info"
+      <h1
+        className="page-title"
+      >
+        onboarding.create_project.header
+      </h1>
+    </div>
+    <React.Fragment>
+      <ul
+        className="flex-tabs"
+      >
+        <li>
+          <a
+            className="js-auto selected"
+            href="#"
+            onClick={[Function]}
           >
-            beta
-          </span>
-        </a>
-      </li>
-      <li>
-        <a
-          className="js-manual"
-          href="#"
-          onClick={[Function]}
-        >
-          onboarding.create_project.create_manually
-        </a>
-      </li>
-    </ul>
-    <AutoProjectCreate
-      currentUser={
-        Object {
-          "externalProvider": "github",
-          "isLoggedIn": true,
-          "login": "foo",
-          "name": "Foo",
+            onboarding.create_project.select_repositories
+            <span
+              className="rounded alert alert-small spacer-left display-inline-block alert-info"
+            >
+              beta
+            </span>
+          </a>
+        </li>
+        <li>
+          <a
+            className="js-manual"
+            href="#"
+            onClick={[Function]}
+          >
+            onboarding.create_project.create_manually
+          </a>
+        </li>
+      </ul>
+      <AutoProjectCreate
+        identityProvider={
+          Object {
+            "backgroundColor": "blue",
+            "iconPath": "icon/path",
+            "key": "github",
+            "name": "GitHub",
+          }
         }
-      }
-      onProjectCreate={[Function]}
-    />
+        onProjectCreate={[Function]}
+      />
+    </React.Fragment>
   </div>
 </React.Fragment>
 `;
@@ -82,17 +111,83 @@ exports[`should render with Manual creation only 1`] = `
         onboarding.create_project.header
       </h1>
     </div>
-    <Connect(ManualProjectCreate)
-      currentUser={
-        Object {
-          "externalProvider": "microsoft",
-          "isLoggedIn": true,
-          "login": "foo",
-          "name": "Foo",
+    <React.Fragment>
+      <Connect(ManualProjectCreate)
+        currentUser={
+          Object {
+            "externalProvider": "microsoft",
+            "isLoggedIn": true,
+            "login": "foo",
+            "name": "Foo",
+          }
         }
-      }
-      onProjectCreate={[Function]}
-    />
+        onProjectCreate={[Function]}
+      />
+    </React.Fragment>
+  </div>
+</React.Fragment>
+`;
+
+exports[`should switch tabs 1`] = `
+<React.Fragment>
+  <HelmetWrapper
+    defer={true}
+    encodeSpecialCharacters={true}
+    title="onboarding.create_project.header"
+    titleTemplate="%s"
+  />
+  <div
+    className="sonarcloud page page-limited"
+  >
+    <div
+      className="page-header"
+    >
+      <h1
+        className="page-title"
+      >
+        onboarding.create_project.header
+      </h1>
+    </div>
+    <React.Fragment>
+      <ul
+        className="flex-tabs"
+      >
+        <li>
+          <a
+            className="js-auto selected"
+            href="#"
+            onClick={[Function]}
+          >
+            onboarding.create_project.select_repositories
+            <span
+              className="rounded alert alert-small spacer-left display-inline-block alert-info"
+            >
+              beta
+            </span>
+          </a>
+        </li>
+        <li>
+          <a
+            className="js-manual"
+            href="#"
+            onClick={[Function]}
+          >
+            onboarding.create_project.create_manually
+          </a>
+        </li>
+      </ul>
+      <AutoProjectCreate
+        identityProvider={
+          Object {
+            "backgroundColor": "blue",
+            "iconPath": "icon/path",
+            "key": "github",
+            "name": "GitHub",
+          }
+        }
+        onProjectCreate={[Function]}
+      />
+    </React.Fragment>
   </div>
 </React.Fragment>
 `;
index fafb751c9bbbbe7d203eb0bf39ab3c3641fba2a4..8f1ddb22631adc61da137b43787b8bf96c5980bc 100644 (file)
@@ -4,7 +4,7 @@ exports[`should correctly create a project 1`] = `
 <SubmitButton
   disabled={true}
 >
-  onboarding.create_project.create_project
+  create
 </SubmitButton>
 `;
 
@@ -12,7 +12,7 @@ exports[`should correctly create a project 2`] = `
 <SubmitButton
   disabled={false}
 >
-  onboarding.create_project.create_project
+  create
 </SubmitButton>
 `;
 
@@ -113,7 +113,7 @@ exports[`should render correctly 1`] = `
     <SubmitButton
       disabled={true}
     >
-      onboarding.create_project.create_project
+      create
     </SubmitButton>
     <DeferredSpinner
       className="spacer-left"
index 934965c7559c521bd0fabb01a24821e87dcac310..d7287c5cf66ee6f8dd4c80e02a23b750ddd10cc6 100644 (file)
@@ -29,6 +29,8 @@ import Select from '../../../components/controls/Select';
 import { translate } from '../../../helpers/l10n';
 import { Button } from '../../../components/ui/buttons';
 
+type Selection = 'personal' | 'existing' | 'new';
+
 interface Props {
   currentUser: { login: string; isLoggedIn: boolean };
   finished: boolean;
@@ -44,7 +46,7 @@ interface State {
   existingOrganization?: string;
   existingOrganizations: Array<string>;
   personalOrganization?: string;
-  selection: 'personal' | 'existing' | 'new';
+  selection: Selection;
 }
 
 export default class OrganizationStep extends React.PureComponent<Props, State> {
@@ -74,9 +76,10 @@ export default class OrganizationStep extends React.PureComponent<Props, State>
           const personalOrganization =
             organizationKeys.length === 1 ? organizationKeys[0] : undefined;
           const existingOrganizations = organizationKeys.length > 1 ? sortBy(organizationKeys) : [];
-          const selection = personalOrganization
-            ? 'personal'
-            : existingOrganizations.length > 0 ? 'existing' : 'new';
+          let selection: Selection = 'personal';
+          if (!personalOrganization) {
+            selection = existingOrganizations.length > 0 ? 'existing' : 'new';
+          }
           this.setState({
             loading: false,
             existingOrganizations,