]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-13628 Add PAT form for GitLab onboarding
authorJeremy Davis <jeremy.davis@sonarsource.com>
Wed, 22 Jul 2020 08:57:38 +0000 (10:57 +0200)
committersonartech <sonartech@sonarsource.com>
Mon, 17 Aug 2020 20:06:22 +0000 (20:06 +0000)
26 files changed:
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx
server/sonar-web/src/main/js/apps/create/project/BitbucketPersonalAccessTokenForm.tsx [deleted file]
server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreateRenderer.tsx
server/sonar-web/src/main/js/apps/create/project/CreateProjectModeSelection.tsx
server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreate.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreateRenderer.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/PersonalAccessTokenForm.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/WrongBindingCountAlert.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketPersonalAccessTokenForm-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectModeSelection-test.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectCreate-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectCreateRenderer-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/__tests__/PersonalAccessTokenForm-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/__tests__/WrongBindingCountAlert-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketPersonalAccessTokenForm-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectCreateRenderer-test.tsx.snap
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectModeSelection-test.tsx.snap
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 [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectCreateRenderer-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/PersonalAccessTokenForm-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/WrongBindingCountAlert-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/types.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 0b58cdc94912e18887f9ec8a08a5c3e859876b11..65eb8c75bfe7af30ab68a24c8efef6227bfa8e11 100644 (file)
@@ -47,7 +47,7 @@ interface State {
 /*
  * ALMs for which the import feature has been implemented
  */
-const IMPORT_COMPATIBLE_ALMS = [AlmKeys.Bitbucket, AlmKeys.GitHub];
+const IMPORT_COMPATIBLE_ALMS = [AlmKeys.Bitbucket, AlmKeys.GitHub, AlmKeys.GitLab];
 
 export class GlobalNavPlus extends React.PureComponent<Props, State> {
   mounted = false;
diff --git a/server/sonar-web/src/main/js/apps/create/project/BitbucketPersonalAccessTokenForm.tsx b/server/sonar-web/src/main/js/apps/create/project/BitbucketPersonalAccessTokenForm.tsx
deleted file mode 100644 (file)
index 4f6826b..0000000
+++ /dev/null
@@ -1,152 +0,0 @@
-/*
- * 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 classNames from 'classnames';
-import * as React from 'react';
-import { FormattedMessage } from 'react-intl';
-import { SubmitButton } from 'sonar-ui-common/components/controls/buttons';
-import ValidationInput from 'sonar-ui-common/components/controls/ValidationInput';
-import { Alert } from 'sonar-ui-common/components/ui/Alert';
-import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
-import { translate } from 'sonar-ui-common/helpers/l10n';
-import { getBaseUrl } from 'sonar-ui-common/helpers/urls';
-import { AlmSettingsInstance } from '../../../types/alm-settings';
-
-export interface BitbucketPersonalAccessTokenFormProps {
-  bitbucketSetting: AlmSettingsInstance;
-  onPersonalAccessTokenCreate: (token: string) => void;
-  submitting?: boolean;
-  validationFailed: boolean;
-}
-
-export default function BitbucketPersonalAccessTokenForm(
-  props: BitbucketPersonalAccessTokenFormProps
-) {
-  const {
-    bitbucketSetting: { url },
-    submitting = false,
-    validationFailed
-  } = props;
-  const [touched, setTouched] = React.useState(false);
-
-  React.useEffect(() => {
-    setTouched(false);
-  }, [submitting]);
-
-  const isInvalid = validationFailed && !touched;
-
-  return (
-    <div className="display-flex-start">
-      <form
-        onSubmit={(e: React.SyntheticEvent<HTMLFormElement>) => {
-          e.preventDefault();
-          const value = new FormData(e.currentTarget).get('personal_access_token') as string;
-          props.onPersonalAccessTokenCreate(value);
-        }}>
-        <h2 className="big">{translate('onboarding.create_project.grant_access_to_bbs.title')}</h2>
-        <p className="big-spacer-top big-spacer-bottom">
-          {translate('onboarding.create_project.grant_access_to_bbs.help')}
-        </p>
-
-        <ValidationInput
-          error={isInvalid ? translate('onboarding.create_project.pat_incorrect') : undefined}
-          id="personal_access_token"
-          isInvalid={isInvalid}
-          isValid={false}
-          label={translate('onboarding.create_project.enter_pat')}
-          required={true}>
-          <input
-            autoFocus={true}
-            className={classNames('input-super-large', {
-              'is-invalid': isInvalid
-            })}
-            id="personal_access_token"
-            minLength={1}
-            name="personal_access_token"
-            onChange={() => {
-              setTouched(true);
-            }}
-            type="text"
-          />
-        </ValidationInput>
-
-        <SubmitButton disabled={isInvalid || submitting || !touched}>
-          {translate('save')}
-        </SubmitButton>
-        <DeferredSpinner className="spacer-left" loading={submitting} />
-      </form>
-
-      <Alert className="big-spacer-left big-spacer-top" display="block" variant="info">
-        <h3>{translate('onboarding.create_project.pat_help.title')}</h3>
-
-        <p className="big-spacer-top big-spacer-bottom">
-          {translate('onboarding.create_project.pat_help.bbs_help_1')}
-        </p>
-
-        {url && (
-          <div className="text-middle">
-            <img
-              alt="" // Should be ignored by screen readers
-              className="spacer-right"
-              height="16"
-              src={`${getBaseUrl()}/images/alm/bitbucket.svg`}
-            />
-            <a
-              href={`${url.replace(/\/$/, '')}/plugins/servlet/access-tokens/add`}
-              rel="noopener noreferrer"
-              target="_blank">
-              {translate('onboarding.create_project.pat_help.link')}
-            </a>
-          </div>
-        )}
-
-        <p className="big-spacer-top big-spacer-bottom">
-          {translate('onboarding.create_project.pat_help.bbs_help_2')}
-        </p>
-
-        <ul>
-          <li>
-            <FormattedMessage
-              defaultMessage={translate(
-                'onboarding.create_project.pat_help.bbs_permission_projects'
-              )}
-              id="onboarding.create_project.pat_help.bbs_permission_projects"
-              values={{
-                perm: (
-                  <strong>{translate('onboarding.create_project.pat_help.read_permission')}</strong>
-                )
-              }}
-            />
-          </li>
-          <li>
-            <FormattedMessage
-              defaultMessage={translate('onboarding.create_project.pat_help.bbs_permission_repos')}
-              id="onboarding.create_project.pat_help.bbs_permission_repos"
-              values={{
-                perm: (
-                  <strong>{translate('onboarding.create_project.pat_help.read_permission')}</strong>
-                )
-              }}
-            />
-          </li>
-        </ul>
-      </Alert>
-    </div>
-  );
-}
index 878a11b609c98eddbf469caed86c97c85053c95b..88eaf370202ac68e1ff65245dd3cc83e46593da2 100644 (file)
  * 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 { Button } from 'sonar-ui-common/components/controls/buttons';
-import { Alert } from 'sonar-ui-common/components/ui/Alert';
 import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
 import { translate } from 'sonar-ui-common/helpers/l10n';
 import { getBaseUrl } from 'sonar-ui-common/helpers/urls';
@@ -30,11 +27,11 @@ import {
   BitbucketProjectRepositories,
   BitbucketRepository
 } from '../../../types/alm-integration';
-import { AlmSettingsInstance } from '../../../types/alm-settings';
-import { ALM_INTEGRATION } from '../../settings/components/AdditionalCategoryKeys';
+import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings';
 import BitbucketImportRepositoryForm from './BitbucketImportRepositoryForm';
-import BitbucketPersonalAccessTokenForm from './BitbucketPersonalAccessTokenForm';
 import CreateProjectPageHeader from './CreateProjectPageHeader';
+import PersonalAccessTokenForm from './PersonalAccessTokenForm';
+import WrongBindingCountAlert from './WrongBindingCountAlert';
 
 export interface BitbucketProjectCreateRendererProps {
   bitbucketSetting?: AlmSettingsInstance;
@@ -104,34 +101,14 @@ export default function BitbucketProjectCreateRenderer(props: BitbucketProjectCr
       {loading && <i className="spinner" />}
 
       {!loading && !bitbucketSetting && (
-        <Alert variant="error">
-          {canAdmin ? (
-            <FormattedMessage
-              defaultMessage={translate('onboarding.create_project.no_bbs_binding.admin')}
-              id="onboarding.create_project.no_bbs_binding.admin"
-              values={{
-                url: (
-                  <Link
-                    to={{
-                      pathname: '/admin/settings',
-                      query: { category: ALM_INTEGRATION }
-                    }}>
-                    {translate('settings.page')}
-                  </Link>
-                )
-              }}
-            />
-          ) : (
-            translate('onboarding.create_project.no_bbs_binding')
-          )}
-        </Alert>
+        <WrongBindingCountAlert alm={AlmKeys.Bitbucket} canAdmin={!!canAdmin} />
       )}
 
       {!loading &&
         bitbucketSetting &&
         (showPersonalAccessTokenForm ? (
-          <BitbucketPersonalAccessTokenForm
-            bitbucketSetting={bitbucketSetting}
+          <PersonalAccessTokenForm
+            almSetting={bitbucketSetting}
             onPersonalAccessTokenCreate={props.onPersonalAccessTokenCreate}
             submitting={submittingToken}
             validationFailed={tokenValidationFailed}
index 43229c38eeffe45d5c6e6551414e1496391b884e..4508e2762c20852688ab0cfb4126fced5dc2a2db 100644 (file)
@@ -44,7 +44,7 @@ function renderAlmOption(
   return (
     <button
       className={classNames(
-        'button button-huge big-spacer-left display-flex-column create-project-mode-type-bbs',
+        'button button-huge big-spacer-left display-flex-column create-project-mode-type-alm',
         { disabled }
       )}
       disabled={disabled}
@@ -114,6 +114,7 @@ export default function CreateProjectModeSelection(props: CreateProjectModeSelec
 
         {renderAlmOption(props, AlmKeys.Bitbucket, CreateProjectModes.BitbucketServer)}
         {renderAlmOption(props, AlmKeys.GitHub, CreateProjectModes.GitHub)}
+        {renderAlmOption(props, AlmKeys.GitLab, CreateProjectModes.GitLab)}
       </div>
     </>
   );
index 3b4183beae4b2319228e60a8a510e6988f6237a5..5b79dd82e6b189b8e11816029cd4d7730c7b26eb 100644 (file)
@@ -30,6 +30,7 @@ import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings';
 import BitbucketProjectCreate from './BitbucketProjectCreate';
 import CreateProjectModeSelection from './CreateProjectModeSelection';
 import GitHubProjectCreate from './GitHubProjectCreate';
+import GitlabProjectCreate from './GitlabProjectCreate';
 import ManualProjectCreate from './ManualProjectCreate';
 import './style.css';
 import { CreateProjectModes } from './types';
@@ -42,12 +43,13 @@ interface Props extends Pick<WithRouterProps, 'router' | 'location'> {
 interface State {
   bitbucketSettings: AlmSettingsInstance[];
   githubSettings: AlmSettingsInstance[];
+  gitlabSettings: AlmSettingsInstance[];
   loading: boolean;
 }
 
 export class CreateProjectPage extends React.PureComponent<Props, State> {
   mounted = false;
-  state: State = { bitbucketSettings: [], githubSettings: [], loading: true };
+  state: State = { bitbucketSettings: [], githubSettings: [], gitlabSettings: [], loading: true };
 
   componentDidMount() {
     const {
@@ -71,6 +73,7 @@ export class CreateProjectPage extends React.PureComponent<Props, State> {
           this.setState({
             bitbucketSettings: almSettings.filter(s => s.alm === AlmKeys.Bitbucket),
             githubSettings: almSettings.filter(s => s.alm === AlmKeys.GitHub),
+            gitlabSettings: almSettings.filter(s => s.alm === AlmKeys.GitLab),
             loading: false
           });
         }
@@ -102,7 +105,7 @@ export class CreateProjectPage extends React.PureComponent<Props, State> {
       location,
       router
     } = this.props;
-    const { bitbucketSettings, githubSettings, loading } = this.state;
+    const { bitbucketSettings, githubSettings, gitlabSettings, loading } = this.state;
 
     switch (mode) {
       case CreateProjectModes.BitbucketServer: {
@@ -128,6 +131,17 @@ export class CreateProjectPage extends React.PureComponent<Props, State> {
           />
         );
       }
+      case CreateProjectModes.GitLab: {
+        return (
+          <GitlabProjectCreate
+            canAdmin={!!canAdmin}
+            loadingBindings={loading}
+            location={location}
+            onProjectCreate={this.handleProjectCreate}
+            settings={gitlabSettings}
+          />
+        );
+      }
       case CreateProjectModes.Manual: {
         return <ManualProjectCreate onProjectCreate={this.handleProjectCreate} />;
       }
@@ -136,7 +150,7 @@ export class CreateProjectPage extends React.PureComponent<Props, State> {
           [AlmKeys.Azure]: 0,
           [AlmKeys.Bitbucket]: bitbucketSettings.length,
           [AlmKeys.GitHub]: githubSettings.length,
-          [AlmKeys.GitLab]: 0
+          [AlmKeys.GitLab]: gitlabSettings.length
         };
         return (
           <CreateProjectModeSelection
diff --git a/server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreate.tsx
new file mode 100644 (file)
index 0000000..33d0535
--- /dev/null
@@ -0,0 +1,144 @@
+/*
+ * 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 { WithRouterProps } from 'react-router';
+import {
+  checkPersonalAccessTokenIsValid,
+  setAlmPersonalAccessToken
+} from '../../../api/alm-integrations';
+import { AlmSettingsInstance } from '../../../types/alm-settings';
+import GitlabProjectCreateRenderer from './GitlabProjectCreateRenderer';
+
+interface Props extends Pick<WithRouterProps, 'location'> {
+  canAdmin: boolean;
+  loadingBindings: boolean;
+  onProjectCreate: (projectKeys: string[]) => void;
+  settings: AlmSettingsInstance[];
+}
+
+interface State {
+  loading: boolean;
+  submittingToken: boolean;
+  tokenIsValid: boolean;
+  tokenValidationFailed: boolean;
+  settings?: AlmSettingsInstance;
+}
+
+export default class GitlabProjectCreate extends React.PureComponent<Props, State> {
+  mounted = false;
+
+  constructor(props: Props) {
+    super(props);
+    this.state = {
+      loading: false,
+      tokenIsValid: false,
+      settings: props.settings.length === 1 ? props.settings[0] : undefined,
+      submittingToken: false,
+      tokenValidationFailed: false
+    };
+  }
+
+  componentDidMount() {
+    this.mounted = true;
+    this.fetchInitialData();
+  }
+
+  componentDidUpdate(prevProps: Props) {
+    if (prevProps.settings.length === 0 && this.props.settings.length > 0) {
+      this.setState(
+        { settings: this.props.settings.length === 1 ? this.props.settings[0] : undefined },
+        () => this.fetchInitialData()
+      );
+    }
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  fetchInitialData = async () => {
+    this.setState({ loading: true });
+
+    const tokenIsValid = await this.checkPersonalAccessToken();
+
+    if (this.mounted) {
+      this.setState({
+        tokenIsValid,
+        loading: false
+      });
+    }
+  };
+
+  checkPersonalAccessToken = () => {
+    const { settings } = this.state;
+
+    if (!settings) {
+      return Promise.resolve(false);
+    }
+
+    return checkPersonalAccessTokenIsValid(settings.key).catch(() => false);
+  };
+
+  handlePersonalAccessTokenCreate = (token: string) => {
+    const { settings } = this.state;
+
+    if (!settings || token.length < 1) {
+      return;
+    }
+
+    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 });
+        }
+      });
+  };
+
+  render() {
+    const { canAdmin, loadingBindings, location } = this.props;
+    const { loading, tokenIsValid, settings, submittingToken, tokenValidationFailed } = this.state;
+
+    return (
+      <GitlabProjectCreateRenderer
+        settings={settings}
+        canAdmin={canAdmin}
+        loading={loading || loadingBindings}
+        onPersonalAccessTokenCreate={this.handlePersonalAccessTokenCreate}
+        showPersonalAccessTokenForm={!tokenIsValid || Boolean(location.query.resetPat)}
+        submittingToken={submittingToken}
+        tokenValidationFailed={tokenValidationFailed}
+      />
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreateRenderer.tsx b/server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreateRenderer.tsx
new file mode 100644 (file)
index 0000000..90f47b8
--- /dev/null
@@ -0,0 +1,84 @@
+/*
+ * 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 { translate } from 'sonar-ui-common/helpers/l10n';
+import { getBaseUrl } from 'sonar-ui-common/helpers/urls';
+import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings';
+import CreateProjectPageHeader from './CreateProjectPageHeader';
+import PersonalAccessTokenForm from './PersonalAccessTokenForm';
+import WrongBindingCountAlert from './WrongBindingCountAlert';
+
+export interface GitlabProjectCreateRendererProps {
+  canAdmin?: boolean;
+  loading: boolean;
+  onPersonalAccessTokenCreate: (pat: string) => void;
+  settings?: AlmSettingsInstance;
+  showPersonalAccessTokenForm?: boolean;
+  submittingToken?: boolean;
+  tokenValidationFailed: boolean;
+}
+
+export default function GitlabProjectCreateRenderer(props: GitlabProjectCreateRendererProps) {
+  const {
+    canAdmin,
+    loading,
+    settings,
+    showPersonalAccessTokenForm,
+    submittingToken,
+    tokenValidationFailed
+  } = props;
+
+  return (
+    <>
+      <CreateProjectPageHeader
+        title={
+          <span className="text-middle">
+            <img
+              alt="" // Should be ignored by screen readers
+              className="spacer-right"
+              height="24"
+              src={`${getBaseUrl()}/images/alm/gitlab.svg`}
+            />
+            {translate('onboarding.create_project.gitlab.title')}
+          </span>
+        }
+      />
+
+      {loading && <i className="spinner" />}
+
+      {!loading && !settings && (
+        <WrongBindingCountAlert alm={AlmKeys.GitLab} canAdmin={!!canAdmin} />
+      )}
+
+      {!loading &&
+        settings &&
+        (showPersonalAccessTokenForm ? (
+          <PersonalAccessTokenForm
+            almSetting={settings}
+            onPersonalAccessTokenCreate={props.onPersonalAccessTokenCreate}
+            submitting={submittingToken}
+            validationFailed={tokenValidationFailed}
+          />
+        ) : (
+          <div>Token is valid!</div>
+        ))}
+    </>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/create/project/PersonalAccessTokenForm.tsx b/server/sonar-web/src/main/js/apps/create/project/PersonalAccessTokenForm.tsx
new file mode 100644 (file)
index 0000000..2a0342a
--- /dev/null
@@ -0,0 +1,179 @@
+/*
+ * 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 classNames from 'classnames';
+import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
+import { SubmitButton } from 'sonar-ui-common/components/controls/buttons';
+import ValidationInput from 'sonar-ui-common/components/controls/ValidationInput';
+import { Alert } from 'sonar-ui-common/components/ui/Alert';
+import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import { getBaseUrl } from 'sonar-ui-common/helpers/urls';
+import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings';
+
+export interface PersonalAccessTokenFormProps {
+  almSetting: AlmSettingsInstance;
+  onPersonalAccessTokenCreate: (token: string) => void;
+  submitting?: boolean;
+  validationFailed: boolean;
+}
+
+function getPatUrl(alm: AlmKeys, url: string) {
+  if (alm === AlmKeys.Bitbucket) {
+    return `${url.replace(/\/$/, '')}/plugins/servlet/access-tokens/add`;
+  } else {
+    // GitLab
+    return url.endsWith('/api/v4')
+      ? `${url.replace('/api/v4', '').replace(/\/$/, '')}/profile/personal_access_tokens`
+      : 'https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#creating-a-personal-access-token';
+  }
+}
+
+export default function PersonalAccessTokenForm(props: PersonalAccessTokenFormProps) {
+  const {
+    almSetting: { alm, url },
+    submitting = false,
+    validationFailed
+  } = props;
+  const [touched, setTouched] = React.useState(false);
+
+  React.useEffect(() => {
+    setTouched(false);
+  }, [submitting]);
+
+  const isInvalid = validationFailed && !touched;
+
+  return (
+    <div className="display-flex-start">
+      <form
+        onSubmit={(e: React.SyntheticEvent<HTMLFormElement>) => {
+          e.preventDefault();
+          const value = new FormData(e.currentTarget).get('personal_access_token') as string;
+          props.onPersonalAccessTokenCreate(value);
+        }}>
+        <h2 className="big">{translate('onboarding.create_project.pat_form.title', alm)}</h2>
+        <p className="big-spacer-top big-spacer-bottom">
+          {translate('onboarding.create_project.pat_form.help', alm)}
+        </p>
+
+        <ValidationInput
+          error={isInvalid ? translate('onboarding.create_project.pat_incorrect') : undefined}
+          id="personal_access_token"
+          isInvalid={isInvalid}
+          isValid={false}
+          label={translate('onboarding.create_project.enter_pat')}
+          required={true}>
+          <input
+            autoFocus={true}
+            className={classNames('input-super-large', {
+              'is-invalid': isInvalid
+            })}
+            id="personal_access_token"
+            minLength={1}
+            name="personal_access_token"
+            onChange={() => {
+              setTouched(true);
+            }}
+            type="text"
+          />
+        </ValidationInput>
+
+        <SubmitButton disabled={isInvalid || submitting || !touched}>
+          {translate('save')}
+        </SubmitButton>
+        <DeferredSpinner className="spacer-left" loading={submitting} />
+      </form>
+
+      <Alert className="big-spacer-left big-spacer-top" display="block" variant="info">
+        <h3>{translate('onboarding.create_project.pat_help.title')}</h3>
+
+        <p className="big-spacer-top big-spacer-bottom">
+          <FormattedMessage
+            id="onboarding.create_project.pat_help.instructions"
+            defaultMessage={translate('onboarding.create_project.pat_help.instructions')}
+            values={{ alm: translate('onboarding.alm', alm) }}
+          />
+        </p>
+
+        {url && (
+          <div className="text-middle">
+            <img
+              alt="" // Should be ignored by screen readers
+              className="spacer-right"
+              height="16"
+              src={`${getBaseUrl()}/images/alm/${alm}.svg`}
+            />
+            <a href={getPatUrl(alm, url)} rel="noopener noreferrer" target="_blank">
+              {translate('onboarding.create_project.pat_help.link')}
+            </a>
+          </div>
+        )}
+
+        <p className="big-spacer-top big-spacer-bottom">
+          {translate('onboarding.create_project.pat_help.instructions2', alm)}
+        </p>
+
+        <ul>
+          {alm === AlmKeys.Bitbucket && (
+            <>
+              <li>
+                <FormattedMessage
+                  defaultMessage={translate(
+                    'onboarding.create_project.pat_help.bbs_permission_projects'
+                  )}
+                  id="onboarding.create_project.pat_help.bbs_permission_projects"
+                  values={{
+                    perm: (
+                      <strong>
+                        {translate('onboarding.create_project.pat_help.read_permission')}
+                      </strong>
+                    )
+                  }}
+                />
+              </li>
+              <li>
+                <FormattedMessage
+                  defaultMessage={translate(
+                    'onboarding.create_project.pat_help.bbs_permission_repos'
+                  )}
+                  id="onboarding.create_project.pat_help.bbs_permission_repos"
+                  values={{
+                    perm: (
+                      <strong>
+                        {translate('onboarding.create_project.pat_help.read_permission')}
+                      </strong>
+                    )
+                  }}
+                />
+              </li>
+            </>
+          )}
+          {alm === AlmKeys.GitLab && (
+            <li className="spacer-bottom">
+              <strong>
+                {translate('onboarding.create_project.pat_help.gitlab.read_api_permission')}
+              </strong>
+            </li>
+          )}
+        </ul>
+      </Alert>
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/create/project/WrongBindingCountAlert.tsx b/server/sonar-web/src/main/js/apps/create/project/WrongBindingCountAlert.tsx
new file mode 100644 (file)
index 0000000..afab498
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * 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 { Alert } from 'sonar-ui-common/components/ui/Alert';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import { AlmKeys } from '../../../types/alm-settings';
+import { ALM_INTEGRATION } from '../../settings/components/AdditionalCategoryKeys';
+
+export interface WrongBindingCountAlertProps {
+  alm: AlmKeys;
+  canAdmin: boolean;
+}
+
+export default function WrongBindingCountAlert(props: WrongBindingCountAlertProps) {
+  const { alm, canAdmin } = props;
+
+  return (
+    <Alert variant="error">
+      {canAdmin ? (
+        <FormattedMessage
+          defaultMessage={translate('onboarding.create_project.wrong_binding_count.admin')}
+          id="onboarding.create_project.wrong_binding_count.admin"
+          values={{
+            alm: translate('onboarding.alm', alm),
+            url: (
+              <Link
+                to={{
+                  pathname: '/admin/settings',
+                  query: { category: ALM_INTEGRATION }
+                }}>
+                {translate('settings.page')}
+              </Link>
+            )
+          }}
+        />
+      ) : (
+        <FormattedMessage
+          defaultMessage={translate('onboarding.create_project.wrong_binding_count')}
+          id="onboarding.create_project.wrong_binding_count"
+          values={{
+            alm: translate('onboarding.alm', alm)
+          }}
+        />
+      )}
+    </Alert>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketPersonalAccessTokenForm-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketPersonalAccessTokenForm-test.tsx
deleted file mode 100644 (file)
index 5708339..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * 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 { SubmitButton } from 'sonar-ui-common/components/controls/buttons';
-import { change, submit } from 'sonar-ui-common/helpers/testUtils';
-import { mockAlmSettingsInstance } from '../../../../helpers/mocks/alm-settings';
-import { AlmKeys } from '../../../../types/alm-settings';
-import BitbucketPersonalAccessTokenForm, {
-  BitbucketPersonalAccessTokenFormProps
-} from '../BitbucketPersonalAccessTokenForm';
-
-it('should render correctly', () => {
-  expect(shallowRender()).toMatchSnapshot('default');
-  expect(shallowRender({ submitting: true })).toMatchSnapshot('submitting');
-  expect(shallowRender({ validationFailed: true })).toMatchSnapshot('validation failed');
-});
-
-it('should correctly handle form interactions', () => {
-  const onPersonalAccessTokenCreate = jest.fn();
-  const wrapper = shallowRender({ onPersonalAccessTokenCreate });
-
-  // Submit button disabled by default.
-  expect(wrapper.find(SubmitButton).prop('disabled')).toBe(true);
-
-  // Submit button enabled if there's a value.
-  change(wrapper.find('input'), 'token');
-  expect(wrapper.find(SubmitButton).prop('disabled')).toBe(false);
-
-  // Expect correct calls to be made when submitting.
-  submit(wrapper.find('form'));
-  expect(onPersonalAccessTokenCreate).toBeCalled();
-
-  // If validation fails, we toggle the submitting flag and call useEffect()
-  // to set the `touched` flag to false again. Trigger a re-render, and mock
-  // useEffect(). This should de-activate the submit button again.
-  jest.spyOn(React, 'useEffect').mockImplementationOnce(f => f());
-  wrapper.setProps({ submitting: false });
-  expect(wrapper.find(SubmitButton).prop('disabled')).toBe(true);
-});
-
-function shallowRender(props: Partial<BitbucketPersonalAccessTokenFormProps> = {}) {
-  return shallow<BitbucketPersonalAccessTokenFormProps>(
-    <BitbucketPersonalAccessTokenForm
-      bitbucketSetting={mockAlmSettingsInstance({
-        alm: AlmKeys.Bitbucket,
-        url: 'http://www.example.com'
-      })}
-      onPersonalAccessTokenCreate={jest.fn()}
-      validationFailed={false}
-      {...props}
-    />
-  );
-}
index 2e11297a12d6d7cb6b0116523e2a0200e01ac2b4..6d0717eecafa1853fc7110c414ddf5a2d656e5a9 100644 (file)
@@ -42,10 +42,10 @@ it('should correctly pass the selected mode up', () => {
   click(wrapper.find('button.create-project-mode-type-manual'));
   expect(onSelectMode).toBeCalledWith(CreateProjectModes.Manual);
 
-  click(wrapper.find('button.create-project-mode-type-bbs').at(0));
+  click(wrapper.find('button.create-project-mode-type-alm').at(0));
   expect(onSelectMode).toBeCalledWith(CreateProjectModes.BitbucketServer);
 
-  click(wrapper.find('button.create-project-mode-type-bbs').at(1));
+  click(wrapper.find('button.create-project-mode-type-alm').at(1));
   expect(onSelectMode).toBeCalledWith(CreateProjectModes.GitHub);
 });
 
index 8368b8eb386f6d2ed93513d7101c831e3f9b31eb..e2d16dd4ed7eb8b0dad932f60a2d63322d50970f 100644 (file)
@@ -65,6 +65,13 @@ it('should render correctly if the GitHub method is selected', () => {
   expect(wrapper).toMatchSnapshot();
 });
 
+it('should render correctly if the GitLab method is selected', () => {
+  const wrapper = shallowRender({
+    location: mockLocation({ query: { mode: CreateProjectModes.GitLab } })
+  });
+  expect(wrapper).toMatchSnapshot();
+});
+
 function shallowRender(props: Partial<CreateProjectPage['props']> = {}) {
   return shallow<CreateProjectPage>(
     <CreateProjectPage
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectCreate-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectCreate-test.tsx
new file mode 100644 (file)
index 0000000..da88c06
--- /dev/null
@@ -0,0 +1,116 @@
+/*
+ * 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 { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
+import {
+  checkPersonalAccessTokenIsValid,
+  setAlmPersonalAccessToken
+} from '../../../../api/alm-integrations';
+import { mockAlmSettingsInstance } from '../../../../helpers/mocks/alm-settings';
+import { mockLocation } 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)
+}));
+
+beforeEach(jest.clearAllMocks);
+
+const almSettingKey = 'gitlab-setting';
+
+it('should render correctly', () => {
+  expect(shallowRender()).toMatchSnapshot();
+});
+
+it('should correctly check PAT on mount', async () => {
+  const wrapper = shallowRender();
+  await waitAndUpdate(wrapper);
+  expect(checkPersonalAccessTokenIsValid).toBeCalledWith(almSettingKey);
+});
+
+it('should correctly check PAT when settings are added after mount', async () => {
+  const wrapper = shallowRender({ settings: [] });
+  await waitAndUpdate(wrapper);
+
+  wrapper.setProps({
+    settings: [mockAlmSettingsInstance({ alm: AlmKeys.GitLab, key: 'otherKey' })]
+  });
+
+  expect(checkPersonalAccessTokenIsValid).toBeCalledWith('otherKey');
+});
+
+it('should correctly handle a valid PAT', async () => {
+  (checkPersonalAccessTokenIsValid as jest.Mock).mockResolvedValueOnce(true);
+  const wrapper = shallowRender();
+  await waitAndUpdate(wrapper);
+  expect(wrapper.state().tokenIsValid).toBe(true);
+});
+
+it('should correctly handle an invalid PAT', async () => {
+  (checkPersonalAccessTokenIsValid as jest.Mock).mockResolvedValueOnce(false);
+  const wrapper = shallowRender();
+  await waitAndUpdate(wrapper);
+  expect(wrapper.state().tokenIsValid).toBe(false);
+});
+
+describe('setting a new PAT', () => {
+  const wrapper = shallowRender();
+
+  it('should correctly handle it if invalid', async () => {
+    (checkPersonalAccessTokenIsValid as jest.Mock).mockResolvedValueOnce(false);
+
+    wrapper.instance().handlePersonalAccessTokenCreate('invalidtoken');
+    expect(setAlmPersonalAccessToken).toBeCalledWith(almSettingKey, 'invalidtoken');
+    expect(wrapper.state().submittingToken).toBe(true);
+    await waitAndUpdate(wrapper);
+    expect(checkPersonalAccessTokenIsValid).toBeCalled();
+    expect(wrapper.state().submittingToken).toBe(false);
+    expect(wrapper.state().tokenValidationFailed).toBe(true);
+  });
+
+  it('should correctly handle it if valid', async () => {
+    (checkPersonalAccessTokenIsValid as jest.Mock).mockResolvedValueOnce(true);
+
+    wrapper.instance().handlePersonalAccessTokenCreate('validtoken');
+    expect(setAlmPersonalAccessToken).toBeCalledWith(almSettingKey, 'validtoken');
+    expect(wrapper.state().submittingToken).toBe(true);
+    await waitAndUpdate(wrapper);
+    expect(checkPersonalAccessTokenIsValid).toBeCalled();
+    expect(wrapper.state().submittingToken).toBe(false);
+    expect(wrapper.state().tokenValidationFailed).toBe(false);
+  });
+});
+
+function shallowRender(props: Partial<GitlabProjectCreate['props']> = {}) {
+  return shallow<GitlabProjectCreate>(
+    <GitlabProjectCreate
+      canAdmin={false}
+      loadingBindings={false}
+      location={mockLocation()}
+      onProjectCreate={jest.fn()}
+      settings={[mockAlmSettingsInstance({ alm: AlmKeys.GitLab, key: almSettingKey })]}
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectCreateRenderer-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectCreateRenderer-test.tsx
new file mode 100644 (file)
index 0000000..c824f5e
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * 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 { mockAlmSettingsInstance } from '../../../../helpers/mocks/alm-settings';
+import { AlmKeys } from '../../../../types/alm-settings';
+import GitlabProjectCreateRenderer, {
+  GitlabProjectCreateRendererProps
+} from '../GitlabProjectCreateRenderer';
+
+it('should render correctly', () => {
+  expect(shallowRender({ loading: true })).toMatchSnapshot('loading');
+  expect(shallowRender({ settings: undefined })).toMatchSnapshot('invalid settings');
+  expect(shallowRender({ canAdmin: true, settings: undefined })).toMatchSnapshot(
+    'invalid settings, admin user'
+  );
+  expect(shallowRender()).toMatchSnapshot('pat form');
+});
+
+function shallowRender(props: Partial<GitlabProjectCreateRendererProps> = {}) {
+  return shallow<GitlabProjectCreateRendererProps>(
+    <GitlabProjectCreateRenderer
+      canAdmin={false}
+      loading={false}
+      onPersonalAccessTokenCreate={jest.fn()}
+      showPersonalAccessTokenForm={true}
+      submittingToken={false}
+      tokenValidationFailed={false}
+      settings={mockAlmSettingsInstance({ alm: AlmKeys.GitLab })}
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/PersonalAccessTokenForm-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/PersonalAccessTokenForm-test.tsx
new file mode 100644 (file)
index 0000000..e096aed
--- /dev/null
@@ -0,0 +1,83 @@
+/*
+ * 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 { SubmitButton } from 'sonar-ui-common/components/controls/buttons';
+import { change, submit } from 'sonar-ui-common/helpers/testUtils';
+import { mockAlmSettingsInstance } from '../../../../helpers/mocks/alm-settings';
+import { AlmKeys } from '../../../../types/alm-settings';
+import PersonalAccessTokenForm, { PersonalAccessTokenFormProps } from '../PersonalAccessTokenForm';
+
+it('should render correctly', () => {
+  expect(shallowRender()).toMatchSnapshot('bitbucket');
+  expect(shallowRender({ submitting: true })).toMatchSnapshot('submitting');
+  expect(shallowRender({ validationFailed: true })).toMatchSnapshot('validation failed');
+  expect(
+    shallowRender({
+      almSetting: mockAlmSettingsInstance({ alm: AlmKeys.GitLab, url: 'https://gitlab.com/api/v4' })
+    })
+  ).toMatchSnapshot('gitlab');
+  expect(
+    shallowRender({
+      almSetting: mockAlmSettingsInstance({
+        alm: AlmKeys.GitLab,
+        url: 'https://gitlabapi.unexpectedurl.org'
+      })
+    })
+  ).toMatchSnapshot('gitlab with non-standard api path');
+});
+
+it('should correctly handle form interactions', () => {
+  const onPersonalAccessTokenCreate = jest.fn();
+  const wrapper = shallowRender({ onPersonalAccessTokenCreate });
+
+  // Submit button disabled by default.
+  expect(wrapper.find(SubmitButton).prop('disabled')).toBe(true);
+
+  // Submit button enabled if there's a value.
+  change(wrapper.find('input'), 'token');
+  expect(wrapper.find(SubmitButton).prop('disabled')).toBe(false);
+
+  // Expect correct calls to be made when submitting.
+  submit(wrapper.find('form'));
+  expect(onPersonalAccessTokenCreate).toBeCalled();
+
+  // If validation fails, we toggle the submitting flag and call useEffect()
+  // to set the `touched` flag to false again. Trigger a re-render, and mock
+  // useEffect(). This should de-activate the submit button again.
+  jest.spyOn(React, 'useEffect').mockImplementationOnce(f => f());
+  wrapper.setProps({ submitting: false });
+  expect(wrapper.find(SubmitButton).prop('disabled')).toBe(true);
+});
+
+function shallowRender(props: Partial<PersonalAccessTokenFormProps> = {}) {
+  return shallow<PersonalAccessTokenFormProps>(
+    <PersonalAccessTokenForm
+      almSetting={mockAlmSettingsInstance({
+        alm: AlmKeys.Bitbucket,
+        url: 'http://www.example.com'
+      })}
+      onPersonalAccessTokenCreate={jest.fn()}
+      validationFailed={false}
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/WrongBindingCountAlert-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/WrongBindingCountAlert-test.tsx
new file mode 100644 (file)
index 0000000..9f67f5f
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * 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 { AlmKeys } from '../../../../types/alm-settings';
+import WrongBindingCountAlert, { WrongBindingCountAlertProps } from '../WrongBindingCountAlert';
+
+it('should render correctly', () => {
+  expect(shallowRender({ canAdmin: true })).toMatchSnapshot('for admin');
+  expect(shallowRender({ alm: AlmKeys.Bitbucket })).toMatchSnapshot('bitbucket');
+  expect(shallowRender({ alm: AlmKeys.GitLab })).toMatchSnapshot('gitlab');
+});
+
+function shallowRender(props: Partial<WrongBindingCountAlertProps> = {}) {
+  return shallow(<WrongBindingCountAlert alm={AlmKeys.Bitbucket} canAdmin={false} {...props} />);
+}
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketPersonalAccessTokenForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketPersonalAccessTokenForm-test.tsx.snap
deleted file mode 100644 (file)
index 18a3bf5..0000000
+++ /dev/null
@@ -1,338 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly: default 1`] = `
-<div
-  className="display-flex-start"
->
-  <form
-    onSubmit={[Function]}
-  >
-    <h2
-      className="big"
-    >
-      onboarding.create_project.grant_access_to_bbs.title
-    </h2>
-    <p
-      className="big-spacer-top big-spacer-bottom"
-    >
-      onboarding.create_project.grant_access_to_bbs.help
-    </p>
-    <ValidationInput
-      id="personal_access_token"
-      isInvalid={false}
-      isValid={false}
-      label="onboarding.create_project.enter_pat"
-      required={true}
-    >
-      <input
-        autoFocus={true}
-        className="input-super-large"
-        id="personal_access_token"
-        minLength={1}
-        name="personal_access_token"
-        onChange={[Function]}
-        type="text"
-      />
-    </ValidationInput>
-    <SubmitButton
-      disabled={true}
-    >
-      save
-    </SubmitButton>
-    <DeferredSpinner
-      className="spacer-left"
-      loading={false}
-      timeout={100}
-    />
-  </form>
-  <Alert
-    className="big-spacer-left big-spacer-top"
-    display="block"
-    variant="info"
-  >
-    <h3>
-      onboarding.create_project.pat_help.title
-    </h3>
-    <p
-      className="big-spacer-top big-spacer-bottom"
-    >
-      onboarding.create_project.pat_help.bbs_help_1
-    </p>
-    <div
-      className="text-middle"
-    >
-      <img
-        alt=""
-        className="spacer-right"
-        height="16"
-        src="/images/alm/bitbucket.svg"
-      />
-      <a
-        href="http://www.example.com/plugins/servlet/access-tokens/add"
-        rel="noopener noreferrer"
-        target="_blank"
-      >
-        onboarding.create_project.pat_help.link
-      </a>
-    </div>
-    <p
-      className="big-spacer-top big-spacer-bottom"
-    >
-      onboarding.create_project.pat_help.bbs_help_2
-    </p>
-    <ul>
-      <li>
-        <FormattedMessage
-          defaultMessage="onboarding.create_project.pat_help.bbs_permission_projects"
-          id="onboarding.create_project.pat_help.bbs_permission_projects"
-          values={
-            Object {
-              "perm": <strong>
-                onboarding.create_project.pat_help.read_permission
-              </strong>,
-            }
-          }
-        />
-      </li>
-      <li>
-        <FormattedMessage
-          defaultMessage="onboarding.create_project.pat_help.bbs_permission_repos"
-          id="onboarding.create_project.pat_help.bbs_permission_repos"
-          values={
-            Object {
-              "perm": <strong>
-                onboarding.create_project.pat_help.read_permission
-              </strong>,
-            }
-          }
-        />
-      </li>
-    </ul>
-  </Alert>
-</div>
-`;
-
-exports[`should render correctly: submitting 1`] = `
-<div
-  className="display-flex-start"
->
-  <form
-    onSubmit={[Function]}
-  >
-    <h2
-      className="big"
-    >
-      onboarding.create_project.grant_access_to_bbs.title
-    </h2>
-    <p
-      className="big-spacer-top big-spacer-bottom"
-    >
-      onboarding.create_project.grant_access_to_bbs.help
-    </p>
-    <ValidationInput
-      id="personal_access_token"
-      isInvalid={false}
-      isValid={false}
-      label="onboarding.create_project.enter_pat"
-      required={true}
-    >
-      <input
-        autoFocus={true}
-        className="input-super-large"
-        id="personal_access_token"
-        minLength={1}
-        name="personal_access_token"
-        onChange={[Function]}
-        type="text"
-      />
-    </ValidationInput>
-    <SubmitButton
-      disabled={true}
-    >
-      save
-    </SubmitButton>
-    <DeferredSpinner
-      className="spacer-left"
-      loading={true}
-      timeout={100}
-    />
-  </form>
-  <Alert
-    className="big-spacer-left big-spacer-top"
-    display="block"
-    variant="info"
-  >
-    <h3>
-      onboarding.create_project.pat_help.title
-    </h3>
-    <p
-      className="big-spacer-top big-spacer-bottom"
-    >
-      onboarding.create_project.pat_help.bbs_help_1
-    </p>
-    <div
-      className="text-middle"
-    >
-      <img
-        alt=""
-        className="spacer-right"
-        height="16"
-        src="/images/alm/bitbucket.svg"
-      />
-      <a
-        href="http://www.example.com/plugins/servlet/access-tokens/add"
-        rel="noopener noreferrer"
-        target="_blank"
-      >
-        onboarding.create_project.pat_help.link
-      </a>
-    </div>
-    <p
-      className="big-spacer-top big-spacer-bottom"
-    >
-      onboarding.create_project.pat_help.bbs_help_2
-    </p>
-    <ul>
-      <li>
-        <FormattedMessage
-          defaultMessage="onboarding.create_project.pat_help.bbs_permission_projects"
-          id="onboarding.create_project.pat_help.bbs_permission_projects"
-          values={
-            Object {
-              "perm": <strong>
-                onboarding.create_project.pat_help.read_permission
-              </strong>,
-            }
-          }
-        />
-      </li>
-      <li>
-        <FormattedMessage
-          defaultMessage="onboarding.create_project.pat_help.bbs_permission_repos"
-          id="onboarding.create_project.pat_help.bbs_permission_repos"
-          values={
-            Object {
-              "perm": <strong>
-                onboarding.create_project.pat_help.read_permission
-              </strong>,
-            }
-          }
-        />
-      </li>
-    </ul>
-  </Alert>
-</div>
-`;
-
-exports[`should render correctly: validation failed 1`] = `
-<div
-  className="display-flex-start"
->
-  <form
-    onSubmit={[Function]}
-  >
-    <h2
-      className="big"
-    >
-      onboarding.create_project.grant_access_to_bbs.title
-    </h2>
-    <p
-      className="big-spacer-top big-spacer-bottom"
-    >
-      onboarding.create_project.grant_access_to_bbs.help
-    </p>
-    <ValidationInput
-      error="onboarding.create_project.pat_incorrect"
-      id="personal_access_token"
-      isInvalid={true}
-      isValid={false}
-      label="onboarding.create_project.enter_pat"
-      required={true}
-    >
-      <input
-        autoFocus={true}
-        className="input-super-large is-invalid"
-        id="personal_access_token"
-        minLength={1}
-        name="personal_access_token"
-        onChange={[Function]}
-        type="text"
-      />
-    </ValidationInput>
-    <SubmitButton
-      disabled={true}
-    >
-      save
-    </SubmitButton>
-    <DeferredSpinner
-      className="spacer-left"
-      loading={false}
-      timeout={100}
-    />
-  </form>
-  <Alert
-    className="big-spacer-left big-spacer-top"
-    display="block"
-    variant="info"
-  >
-    <h3>
-      onboarding.create_project.pat_help.title
-    </h3>
-    <p
-      className="big-spacer-top big-spacer-bottom"
-    >
-      onboarding.create_project.pat_help.bbs_help_1
-    </p>
-    <div
-      className="text-middle"
-    >
-      <img
-        alt=""
-        className="spacer-right"
-        height="16"
-        src="/images/alm/bitbucket.svg"
-      />
-      <a
-        href="http://www.example.com/plugins/servlet/access-tokens/add"
-        rel="noopener noreferrer"
-        target="_blank"
-      >
-        onboarding.create_project.pat_help.link
-      </a>
-    </div>
-    <p
-      className="big-spacer-top big-spacer-bottom"
-    >
-      onboarding.create_project.pat_help.bbs_help_2
-    </p>
-    <ul>
-      <li>
-        <FormattedMessage
-          defaultMessage="onboarding.create_project.pat_help.bbs_permission_projects"
-          id="onboarding.create_project.pat_help.bbs_permission_projects"
-          values={
-            Object {
-              "perm": <strong>
-                onboarding.create_project.pat_help.read_permission
-              </strong>,
-            }
-          }
-        />
-      </li>
-      <li>
-        <FormattedMessage
-          defaultMessage="onboarding.create_project.pat_help.bbs_permission_repos"
-          id="onboarding.create_project.pat_help.bbs_permission_repos"
-          values={
-            Object {
-              "perm": <strong>
-                onboarding.create_project.pat_help.read_permission
-              </strong>,
-            }
-          }
-        />
-      </li>
-    </ul>
-  </Alert>
-</div>
-`;
index 5b0b50bc580539c63cc8f18f708c2cc0adb7fd31..8d767ba357d405f3da539ac2214322a762d1cfa6 100644 (file)
@@ -171,32 +171,10 @@ exports[`should render correctly: invalid config, admin user 1`] = `
       </span>
     }
   />
-  <Alert
-    variant="error"
-  >
-    <FormattedMessage
-      defaultMessage="onboarding.create_project.no_bbs_binding.admin"
-      id="onboarding.create_project.no_bbs_binding.admin"
-      values={
-        Object {
-          "url": <Link
-            onlyActiveOnIndex={false}
-            style={Object {}}
-            to={
-              Object {
-                "pathname": "/admin/settings",
-                "query": Object {
-                  "category": "almintegration",
-                },
-              }
-            }
-          >
-            settings.page
-          </Link>,
-        }
-      }
-    />
-  </Alert>
+  <WrongBindingCountAlert
+    alm="bitbucket"
+    canAdmin={true}
+  />
 </Fragment>
 `;
 
@@ -235,11 +213,10 @@ exports[`should render correctly: invalid config, regular user 1`] = `
       </span>
     }
   />
-  <Alert
-    variant="error"
-  >
-    onboarding.create_project.no_bbs_binding
-  </Alert>
+  <WrongBindingCountAlert
+    alm="bitbucket"
+    canAdmin={false}
+  />
 </Fragment>
 `;
 
@@ -302,8 +279,8 @@ exports[`should render correctly: pat form 1`] = `
       </span>
     }
   />
-  <BitbucketPersonalAccessTokenForm
-    bitbucketSetting={
+  <PersonalAccessTokenForm
+    almSetting={
       Object {
         "alm": "bitbucket",
         "key": "key",
index b0e0b0f9f3d52eef83d4ae41901cd5e21d71723b..cd06179f636ddd0c9a1117c43e2823774805a11f 100644 (file)
@@ -36,7 +36,7 @@ exports[`should render correctly: default 1`] = `
       </div>
     </button>
     <button
-      className="button button-huge big-spacer-left display-flex-column create-project-mode-type-bbs"
+      className="button button-huge big-spacer-left display-flex-column create-project-mode-type-alm"
       disabled={false}
       onClick={[Function]}
       type="button"
@@ -53,7 +53,7 @@ exports[`should render correctly: default 1`] = `
       </div>
     </button>
     <button
-      className="button button-huge big-spacer-left display-flex-column create-project-mode-type-bbs disabled"
+      className="button button-huge big-spacer-left display-flex-column create-project-mode-type-alm disabled"
       disabled={true}
       onClick={[Function]}
       type="button"
@@ -83,6 +83,37 @@ exports[`should render correctly: default 1`] = `
         />
       </div>
     </button>
+    <button
+      className="button button-huge big-spacer-left display-flex-column create-project-mode-type-alm disabled"
+      disabled={true}
+      onClick={[Function]}
+      type="button"
+    >
+      <img
+        alt=""
+        height={80}
+        src="/images/alm/gitlab.svg"
+      />
+      <div
+        className="medium big-spacer-top"
+      >
+        onboarding.create_project.select_method.gitlab
+      </div>
+      <div
+        className="text-muted small spacer-top"
+        style={
+          Object {
+            "lineHeight": 1.5,
+          }
+        }
+      >
+        onboarding.create_project.alm_not_configured
+        <HelpTooltip
+          className="little-spacer-left"
+          overlay="onboarding.create_project.zero_alm_instances.gitlab"
+        />
+      </div>
+    </button>
   </div>
 </Fragment>
 `;
@@ -123,7 +154,7 @@ exports[`should render correctly: invalid configs 1`] = `
       </div>
     </button>
     <button
-      className="button button-huge big-spacer-left display-flex-column create-project-mode-type-bbs disabled"
+      className="button button-huge big-spacer-left display-flex-column create-project-mode-type-alm disabled"
       disabled={true}
       onClick={[Function]}
       type="button"
@@ -154,7 +185,7 @@ exports[`should render correctly: invalid configs 1`] = `
       </div>
     </button>
     <button
-      className="button button-huge big-spacer-left display-flex-column create-project-mode-type-bbs disabled"
+      className="button button-huge big-spacer-left display-flex-column create-project-mode-type-alm disabled"
       disabled={true}
       onClick={[Function]}
       type="button"
@@ -185,6 +216,37 @@ exports[`should render correctly: invalid configs 1`] = `
         />
       </div>
     </button>
+    <button
+      className="button button-huge big-spacer-left display-flex-column create-project-mode-type-alm disabled"
+      disabled={true}
+      onClick={[Function]}
+      type="button"
+    >
+      <img
+        alt=""
+        height={80}
+        src="/images/alm/gitlab.svg"
+      />
+      <div
+        className="medium big-spacer-top"
+      >
+        onboarding.create_project.select_method.gitlab
+      </div>
+      <div
+        className="text-muted small spacer-top"
+        style={
+          Object {
+            "lineHeight": 1.5,
+          }
+        }
+      >
+        onboarding.create_project.alm_not_configured
+        <HelpTooltip
+          className="little-spacer-left"
+          overlay="onboarding.create_project.zero_alm_instances.gitlab"
+        />
+      </div>
+    </button>
   </div>
 </Fragment>
 `;
@@ -225,7 +287,7 @@ exports[`should render correctly: loading instances 1`] = `
       </div>
     </button>
     <button
-      className="button button-huge big-spacer-left display-flex-column create-project-mode-type-bbs disabled"
+      className="button button-huge big-spacer-left display-flex-column create-project-mode-type-alm disabled"
       disabled={true}
       onClick={[Function]}
       type="button"
@@ -248,7 +310,7 @@ exports[`should render correctly: loading instances 1`] = `
       </span>
     </button>
     <button
-      className="button button-huge big-spacer-left display-flex-column create-project-mode-type-bbs disabled"
+      className="button button-huge big-spacer-left display-flex-column create-project-mode-type-alm disabled"
       disabled={true}
       onClick={[Function]}
       type="button"
@@ -270,6 +332,29 @@ exports[`should render correctly: loading instances 1`] = `
         />
       </span>
     </button>
+    <button
+      className="button button-huge big-spacer-left display-flex-column create-project-mode-type-alm disabled"
+      disabled={true}
+      onClick={[Function]}
+      type="button"
+    >
+      <img
+        alt=""
+        height={80}
+        src="/images/alm/gitlab.svg"
+      />
+      <div
+        className="medium big-spacer-top"
+      >
+        onboarding.create_project.select_method.gitlab
+      </div>
+      <span>
+        onboarding.create_project.check_alm_supported
+        <i
+          className="little-spacer-left spinner"
+        />
+      </span>
+    </button>
   </div>
 </Fragment>
 `;
index 3a1c1edc18beb4967b84e993a11cde62e617cf32..3c54a4b2d4d937e96755b96a70c1c9e68ec38b72 100644 (file)
@@ -142,6 +142,44 @@ exports[`should render correctly if the GitHub method is selected 1`] = `
 </Fragment>
 `;
 
+exports[`should render correctly if the GitLab method is selected 1`] = `
+<Fragment>
+  <Helmet
+    defer={true}
+    encodeSpecialCharacters={true}
+    title="my_account.create_new.TRK"
+    titleTemplate="%s"
+  />
+  <A11ySkipTarget
+    anchor="create_project_main"
+  />
+  <div
+    className="page page-limited huge-spacer-bottom position-relative"
+    id="create-project"
+  >
+    <GitlabProjectCreate
+      canAdmin={false}
+      loadingBindings={true}
+      location={
+        Object {
+          "action": "PUSH",
+          "hash": "",
+          "key": "key",
+          "pathname": "/path",
+          "query": Object {
+            "mode": "gitlab",
+          },
+          "search": "",
+          "state": Object {},
+        }
+      }
+      onProjectCreate={[Function]}
+      settings={Array []}
+    />
+  </div>
+</Fragment>
+`;
+
 exports[`should render correctly if the manual method is selected 1`] = `
 <Fragment>
   <Helmet
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectCreate-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectCreate-test.tsx.snap
new file mode 100644 (file)
index 0000000..8bf4d37
--- /dev/null
@@ -0,0 +1,18 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<GitlabProjectCreateRenderer
+  canAdmin={false}
+  loading={true}
+  onPersonalAccessTokenCreate={[Function]}
+  settings={
+    Object {
+      "alm": "gitlab",
+      "key": "gitlab-setting",
+    }
+  }
+  showPersonalAccessTokenForm={true}
+  submittingToken={false}
+  tokenValidationFailed={false}
+/>
+`;
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectCreateRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectCreateRenderer-test.tsx.snap
new file mode 100644 (file)
index 0000000..d0d2a72
--- /dev/null
@@ -0,0 +1,103 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: invalid settings 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>
+    }
+  />
+  <WrongBindingCountAlert
+    alm="gitlab"
+    canAdmin={false}
+  />
+</Fragment>
+`;
+
+exports[`should render correctly: invalid settings, admin user 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>
+    }
+  />
+  <WrongBindingCountAlert
+    alm="gitlab"
+    canAdmin={true}
+  />
+</Fragment>
+`;
+
+exports[`should render correctly: loading 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>
+    }
+  />
+  <i
+    className="spinner"
+  />
+</Fragment>
+`;
+
+exports[`should render correctly: pat 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>
+    }
+  />
+  <PersonalAccessTokenForm
+    almSetting={
+      Object {
+        "alm": "gitlab",
+        "key": "key",
+      }
+    }
+    onPersonalAccessTokenCreate={[MockFunction]}
+    submitting={false}
+    validationFailed={false}
+  />
+</Fragment>
+`;
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/PersonalAccessTokenForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/PersonalAccessTokenForm-test.tsx.snap
new file mode 100644 (file)
index 0000000..c735dca
--- /dev/null
@@ -0,0 +1,564 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: bitbucket 1`] = `
+<div
+  className="display-flex-start"
+>
+  <form
+    onSubmit={[Function]}
+  >
+    <h2
+      className="big"
+    >
+      onboarding.create_project.pat_form.title.bitbucket
+    </h2>
+    <p
+      className="big-spacer-top big-spacer-bottom"
+    >
+      onboarding.create_project.pat_form.help.bitbucket
+    </p>
+    <ValidationInput
+      id="personal_access_token"
+      isInvalid={false}
+      isValid={false}
+      label="onboarding.create_project.enter_pat"
+      required={true}
+    >
+      <input
+        autoFocus={true}
+        className="input-super-large"
+        id="personal_access_token"
+        minLength={1}
+        name="personal_access_token"
+        onChange={[Function]}
+        type="text"
+      />
+    </ValidationInput>
+    <SubmitButton
+      disabled={true}
+    >
+      save
+    </SubmitButton>
+    <DeferredSpinner
+      className="spacer-left"
+      loading={false}
+      timeout={100}
+    />
+  </form>
+  <Alert
+    className="big-spacer-left big-spacer-top"
+    display="block"
+    variant="info"
+  >
+    <h3>
+      onboarding.create_project.pat_help.title
+    </h3>
+    <p
+      className="big-spacer-top big-spacer-bottom"
+    >
+      <FormattedMessage
+        defaultMessage="onboarding.create_project.pat_help.instructions"
+        id="onboarding.create_project.pat_help.instructions"
+        values={
+          Object {
+            "alm": "onboarding.alm.bitbucket",
+          }
+        }
+      />
+    </p>
+    <div
+      className="text-middle"
+    >
+      <img
+        alt=""
+        className="spacer-right"
+        height="16"
+        src="/images/alm/bitbucket.svg"
+      />
+      <a
+        href="http://www.example.com/plugins/servlet/access-tokens/add"
+        rel="noopener noreferrer"
+        target="_blank"
+      >
+        onboarding.create_project.pat_help.link
+      </a>
+    </div>
+    <p
+      className="big-spacer-top big-spacer-bottom"
+    >
+      onboarding.create_project.pat_help.instructions2.bitbucket
+    </p>
+    <ul>
+      <li>
+        <FormattedMessage
+          defaultMessage="onboarding.create_project.pat_help.bbs_permission_projects"
+          id="onboarding.create_project.pat_help.bbs_permission_projects"
+          values={
+            Object {
+              "perm": <strong>
+                onboarding.create_project.pat_help.read_permission
+              </strong>,
+            }
+          }
+        />
+      </li>
+      <li>
+        <FormattedMessage
+          defaultMessage="onboarding.create_project.pat_help.bbs_permission_repos"
+          id="onboarding.create_project.pat_help.bbs_permission_repos"
+          values={
+            Object {
+              "perm": <strong>
+                onboarding.create_project.pat_help.read_permission
+              </strong>,
+            }
+          }
+        />
+      </li>
+    </ul>
+  </Alert>
+</div>
+`;
+
+exports[`should render correctly: gitlab 1`] = `
+<div
+  className="display-flex-start"
+>
+  <form
+    onSubmit={[Function]}
+  >
+    <h2
+      className="big"
+    >
+      onboarding.create_project.pat_form.title.gitlab
+    </h2>
+    <p
+      className="big-spacer-top big-spacer-bottom"
+    >
+      onboarding.create_project.pat_form.help.gitlab
+    </p>
+    <ValidationInput
+      id="personal_access_token"
+      isInvalid={false}
+      isValid={false}
+      label="onboarding.create_project.enter_pat"
+      required={true}
+    >
+      <input
+        autoFocus={true}
+        className="input-super-large"
+        id="personal_access_token"
+        minLength={1}
+        name="personal_access_token"
+        onChange={[Function]}
+        type="text"
+      />
+    </ValidationInput>
+    <SubmitButton
+      disabled={true}
+    >
+      save
+    </SubmitButton>
+    <DeferredSpinner
+      className="spacer-left"
+      loading={false}
+      timeout={100}
+    />
+  </form>
+  <Alert
+    className="big-spacer-left big-spacer-top"
+    display="block"
+    variant="info"
+  >
+    <h3>
+      onboarding.create_project.pat_help.title
+    </h3>
+    <p
+      className="big-spacer-top big-spacer-bottom"
+    >
+      <FormattedMessage
+        defaultMessage="onboarding.create_project.pat_help.instructions"
+        id="onboarding.create_project.pat_help.instructions"
+        values={
+          Object {
+            "alm": "onboarding.alm.gitlab",
+          }
+        }
+      />
+    </p>
+    <div
+      className="text-middle"
+    >
+      <img
+        alt=""
+        className="spacer-right"
+        height="16"
+        src="/images/alm/gitlab.svg"
+      />
+      <a
+        href="https://gitlab.com/profile/personal_access_tokens"
+        rel="noopener noreferrer"
+        target="_blank"
+      >
+        onboarding.create_project.pat_help.link
+      </a>
+    </div>
+    <p
+      className="big-spacer-top big-spacer-bottom"
+    >
+      onboarding.create_project.pat_help.instructions2.gitlab
+    </p>
+    <ul>
+      <li
+        className="spacer-bottom"
+      >
+        <strong>
+          onboarding.create_project.pat_help.gitlab.read_api_permission
+        </strong>
+      </li>
+    </ul>
+  </Alert>
+</div>
+`;
+
+exports[`should render correctly: gitlab with non-standard api path 1`] = `
+<div
+  className="display-flex-start"
+>
+  <form
+    onSubmit={[Function]}
+  >
+    <h2
+      className="big"
+    >
+      onboarding.create_project.pat_form.title.gitlab
+    </h2>
+    <p
+      className="big-spacer-top big-spacer-bottom"
+    >
+      onboarding.create_project.pat_form.help.gitlab
+    </p>
+    <ValidationInput
+      id="personal_access_token"
+      isInvalid={false}
+      isValid={false}
+      label="onboarding.create_project.enter_pat"
+      required={true}
+    >
+      <input
+        autoFocus={true}
+        className="input-super-large"
+        id="personal_access_token"
+        minLength={1}
+        name="personal_access_token"
+        onChange={[Function]}
+        type="text"
+      />
+    </ValidationInput>
+    <SubmitButton
+      disabled={true}
+    >
+      save
+    </SubmitButton>
+    <DeferredSpinner
+      className="spacer-left"
+      loading={false}
+      timeout={100}
+    />
+  </form>
+  <Alert
+    className="big-spacer-left big-spacer-top"
+    display="block"
+    variant="info"
+  >
+    <h3>
+      onboarding.create_project.pat_help.title
+    </h3>
+    <p
+      className="big-spacer-top big-spacer-bottom"
+    >
+      <FormattedMessage
+        defaultMessage="onboarding.create_project.pat_help.instructions"
+        id="onboarding.create_project.pat_help.instructions"
+        values={
+          Object {
+            "alm": "onboarding.alm.gitlab",
+          }
+        }
+      />
+    </p>
+    <div
+      className="text-middle"
+    >
+      <img
+        alt=""
+        className="spacer-right"
+        height="16"
+        src="/images/alm/gitlab.svg"
+      />
+      <a
+        href="https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#creating-a-personal-access-token"
+        rel="noopener noreferrer"
+        target="_blank"
+      >
+        onboarding.create_project.pat_help.link
+      </a>
+    </div>
+    <p
+      className="big-spacer-top big-spacer-bottom"
+    >
+      onboarding.create_project.pat_help.instructions2.gitlab
+    </p>
+    <ul>
+      <li
+        className="spacer-bottom"
+      >
+        <strong>
+          onboarding.create_project.pat_help.gitlab.read_api_permission
+        </strong>
+      </li>
+    </ul>
+  </Alert>
+</div>
+`;
+
+exports[`should render correctly: submitting 1`] = `
+<div
+  className="display-flex-start"
+>
+  <form
+    onSubmit={[Function]}
+  >
+    <h2
+      className="big"
+    >
+      onboarding.create_project.pat_form.title.bitbucket
+    </h2>
+    <p
+      className="big-spacer-top big-spacer-bottom"
+    >
+      onboarding.create_project.pat_form.help.bitbucket
+    </p>
+    <ValidationInput
+      id="personal_access_token"
+      isInvalid={false}
+      isValid={false}
+      label="onboarding.create_project.enter_pat"
+      required={true}
+    >
+      <input
+        autoFocus={true}
+        className="input-super-large"
+        id="personal_access_token"
+        minLength={1}
+        name="personal_access_token"
+        onChange={[Function]}
+        type="text"
+      />
+    </ValidationInput>
+    <SubmitButton
+      disabled={true}
+    >
+      save
+    </SubmitButton>
+    <DeferredSpinner
+      className="spacer-left"
+      loading={true}
+      timeout={100}
+    />
+  </form>
+  <Alert
+    className="big-spacer-left big-spacer-top"
+    display="block"
+    variant="info"
+  >
+    <h3>
+      onboarding.create_project.pat_help.title
+    </h3>
+    <p
+      className="big-spacer-top big-spacer-bottom"
+    >
+      <FormattedMessage
+        defaultMessage="onboarding.create_project.pat_help.instructions"
+        id="onboarding.create_project.pat_help.instructions"
+        values={
+          Object {
+            "alm": "onboarding.alm.bitbucket",
+          }
+        }
+      />
+    </p>
+    <div
+      className="text-middle"
+    >
+      <img
+        alt=""
+        className="spacer-right"
+        height="16"
+        src="/images/alm/bitbucket.svg"
+      />
+      <a
+        href="http://www.example.com/plugins/servlet/access-tokens/add"
+        rel="noopener noreferrer"
+        target="_blank"
+      >
+        onboarding.create_project.pat_help.link
+      </a>
+    </div>
+    <p
+      className="big-spacer-top big-spacer-bottom"
+    >
+      onboarding.create_project.pat_help.instructions2.bitbucket
+    </p>
+    <ul>
+      <li>
+        <FormattedMessage
+          defaultMessage="onboarding.create_project.pat_help.bbs_permission_projects"
+          id="onboarding.create_project.pat_help.bbs_permission_projects"
+          values={
+            Object {
+              "perm": <strong>
+                onboarding.create_project.pat_help.read_permission
+              </strong>,
+            }
+          }
+        />
+      </li>
+      <li>
+        <FormattedMessage
+          defaultMessage="onboarding.create_project.pat_help.bbs_permission_repos"
+          id="onboarding.create_project.pat_help.bbs_permission_repos"
+          values={
+            Object {
+              "perm": <strong>
+                onboarding.create_project.pat_help.read_permission
+              </strong>,
+            }
+          }
+        />
+      </li>
+    </ul>
+  </Alert>
+</div>
+`;
+
+exports[`should render correctly: validation failed 1`] = `
+<div
+  className="display-flex-start"
+>
+  <form
+    onSubmit={[Function]}
+  >
+    <h2
+      className="big"
+    >
+      onboarding.create_project.pat_form.title.bitbucket
+    </h2>
+    <p
+      className="big-spacer-top big-spacer-bottom"
+    >
+      onboarding.create_project.pat_form.help.bitbucket
+    </p>
+    <ValidationInput
+      error="onboarding.create_project.pat_incorrect"
+      id="personal_access_token"
+      isInvalid={true}
+      isValid={false}
+      label="onboarding.create_project.enter_pat"
+      required={true}
+    >
+      <input
+        autoFocus={true}
+        className="input-super-large is-invalid"
+        id="personal_access_token"
+        minLength={1}
+        name="personal_access_token"
+        onChange={[Function]}
+        type="text"
+      />
+    </ValidationInput>
+    <SubmitButton
+      disabled={true}
+    >
+      save
+    </SubmitButton>
+    <DeferredSpinner
+      className="spacer-left"
+      loading={false}
+      timeout={100}
+    />
+  </form>
+  <Alert
+    className="big-spacer-left big-spacer-top"
+    display="block"
+    variant="info"
+  >
+    <h3>
+      onboarding.create_project.pat_help.title
+    </h3>
+    <p
+      className="big-spacer-top big-spacer-bottom"
+    >
+      <FormattedMessage
+        defaultMessage="onboarding.create_project.pat_help.instructions"
+        id="onboarding.create_project.pat_help.instructions"
+        values={
+          Object {
+            "alm": "onboarding.alm.bitbucket",
+          }
+        }
+      />
+    </p>
+    <div
+      className="text-middle"
+    >
+      <img
+        alt=""
+        className="spacer-right"
+        height="16"
+        src="/images/alm/bitbucket.svg"
+      />
+      <a
+        href="http://www.example.com/plugins/servlet/access-tokens/add"
+        rel="noopener noreferrer"
+        target="_blank"
+      >
+        onboarding.create_project.pat_help.link
+      </a>
+    </div>
+    <p
+      className="big-spacer-top big-spacer-bottom"
+    >
+      onboarding.create_project.pat_help.instructions2.bitbucket
+    </p>
+    <ul>
+      <li>
+        <FormattedMessage
+          defaultMessage="onboarding.create_project.pat_help.bbs_permission_projects"
+          id="onboarding.create_project.pat_help.bbs_permission_projects"
+          values={
+            Object {
+              "perm": <strong>
+                onboarding.create_project.pat_help.read_permission
+              </strong>,
+            }
+          }
+        />
+      </li>
+      <li>
+        <FormattedMessage
+          defaultMessage="onboarding.create_project.pat_help.bbs_permission_repos"
+          id="onboarding.create_project.pat_help.bbs_permission_repos"
+          values={
+            Object {
+              "perm": <strong>
+                onboarding.create_project.pat_help.read_permission
+              </strong>,
+            }
+          }
+        />
+      </li>
+    </ul>
+  </Alert>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/WrongBindingCountAlert-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/WrongBindingCountAlert-test.tsx.snap
new file mode 100644 (file)
index 0000000..b30756a
--- /dev/null
@@ -0,0 +1,63 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: bitbucket 1`] = `
+<Alert
+  variant="error"
+>
+  <FormattedMessage
+    defaultMessage="onboarding.create_project.wrong_binding_count"
+    id="onboarding.create_project.wrong_binding_count"
+    values={
+      Object {
+        "alm": "onboarding.alm.bitbucket",
+      }
+    }
+  />
+</Alert>
+`;
+
+exports[`should render correctly: for admin 1`] = `
+<Alert
+  variant="error"
+>
+  <FormattedMessage
+    defaultMessage="onboarding.create_project.wrong_binding_count.admin"
+    id="onboarding.create_project.wrong_binding_count.admin"
+    values={
+      Object {
+        "alm": "onboarding.alm.bitbucket",
+        "url": <Link
+          onlyActiveOnIndex={false}
+          style={Object {}}
+          to={
+            Object {
+              "pathname": "/admin/settings",
+              "query": Object {
+                "category": "almintegration",
+              },
+            }
+          }
+        >
+          settings.page
+        </Link>,
+      }
+    }
+  />
+</Alert>
+`;
+
+exports[`should render correctly: gitlab 1`] = `
+<Alert
+  variant="error"
+>
+  <FormattedMessage
+    defaultMessage="onboarding.create_project.wrong_binding_count"
+    id="onboarding.create_project.wrong_binding_count"
+    values={
+      Object {
+        "alm": "onboarding.alm.gitlab",
+      }
+    }
+  />
+</Alert>
+`;
index 0fe8944a40557863fc3d08cc8d85272343db582b..c84dd2c1d4cc3647c3d68642cb9a8f15eacb3c61 100644 (file)
@@ -20,5 +20,6 @@
 export enum CreateProjectModes {
   Manual = 'manual',
   BitbucketServer = 'bitbucket',
-  GitHub = 'github'
+  GitHub = 'github',
+  GitLab = 'gitlab'
 }
index e45d4af803539046164d0289305de6a9c714199a..e051a4109cab5db1abf259323c41775ad37cf0ae 100644 (file)
@@ -1800,8 +1800,9 @@ my_account.create_new.VW=Create Portfolio
 my_account.create_new.APP=Create Application
 my_account.add_project=Add Project
 my_account.add_project.manual=Manually
-my_account.add_project.github=GitHub
 my_account.add_project.bitbucket=Bitbucket
+my_account.add_project.github=GitHub
+my_account.add_project.gitlab=GitLab
 
 my_account.create_new_project_portfolio_or_application=Analyze new project / Create new portfolio or application
 
@@ -3095,6 +3096,9 @@ footer.web_api=Web API
 # ONBOARDING
 #
 #------------------------------------------------------------------------------
+onboarding.alm.bitbucket=Bitbucket Server
+onboarding.alm.gitlab=GitLab
+
 onboarding.project_analysis.header=Analyze your project
 onboarding.project_analysis.description=We initialized your project on {instance}, now it's up to you to launch analyses!
 onboarding.project_analysis.guide_to_integrate_pipelines=follow the guide to integrating with Pipelines
@@ -3103,6 +3107,7 @@ onboarding.create_project.setup_manually=Create a project
 onboarding.create_project.select_method.manual=Manually
 onboarding.create_project.select_method.bitbucket=From Bitbucket Server
 onboarding.create_project.select_method.github=From GitHub
+onboarding.create_project.select_method.gitlab=From GitLab
 onboarding.create_project.alm_not_configured=Currently not active
 onboarding.create_project.check_alm_supported=Checking if available
 onboarding.create_project.project_key=Project key
@@ -3125,25 +3130,33 @@ onboarding.create_project.search_repositories=Search for a repository
 onboarding.create_project.select_repositories=Select repositories
 onboarding.create_project.select_all_repositories=Select all available repositories
 onboarding.create_project.from_bbs=Create a project from Bitbucket Server
-onboarding.create_project.grant_access_to_bbs.title=Grant access to your repositories
-onboarding.create_project.grant_access_to_bbs.help=SonarQube needs a personal access token to access and list your repositories from Bitbucket Server.
+
+onboarding.create_project.pat_form.title.bitbucket=Grant access to your repositories
+onboarding.create_project.pat_form.title.gitlab=Grant access to your projects
+onboarding.create_project.pat_form.help.bitbucket=SonarQube needs a personal access token to access and list your repositories from Bitbucket Server.
+onboarding.create_project.pat_form.help.gitlab=SonarQube needs a personal access token to access and list your projects from GitLab.
 onboarding.create_project.select_method=How do you want to create your project?
 onboarding.create_project.too_many_alm_instances.bitbucket=You must have exactly 1 Bitbucket Server instance configured in order to use this method.
 onboarding.create_project.too_many_alm_instances.github=You must have exactly 1 Bitbucket Server instance configured in order to use this method.
 onboarding.create_project.alm_instances_count_X=You currently have {0}.
 onboarding.create_project.zero_alm_instances.bitbucket=You must first configure a Bitbucket Server instance.
 onboarding.create_project.zero_alm_instances.github=You must first configure a GitHub instance.
-onboarding.create_project.no_bbs_binding=You must have exactly at least 1 Bitbucket Server instance configured in order to use this method, but none were found. Either create the project manually, or contact your system administrator.
-onboarding.create_project.no_bbs_binding.admin=You must have exactly at least 1 Bitbucket Server instance configured in order to use this method. You can configure instances under {url}.
+onboarding.create_project.wrong_binding_count=You must have exactly 1 {alm} instance configured in order to use this method, but none were found. Either create the project manually, or contact your system administrator.
+onboarding.create_project.wrong_binding_count.admin=You must have exactly 1 {alm} instance configured in order to use this method. You can configure instances under {url}.
 onboarding.create_project.enter_pat=Enter personal access token
 onboarding.create_project.pat_incorrect=Your personal access token failed to validate.
 onboarding.create_project.pat_help.title=How to create a personal access token?
-onboarding.create_project.pat_help.bbs_help_1=Click the following link to generate a token in Bitbucket Server, and copy-paste it into the personal access token field.
-onboarding.create_project.pat_help.bbs_help_2=Set a name, for example "SonarQube", and select the following permissions:
+
+onboarding.create_project.pat_help.instructions=Click the following link to generate a token in {alm}, and copy-paste it into the personal access token field.
+onboarding.create_project.pat_help.instructions2.bitbucket=Set a name, for example "SonarQube", and select the following permissions:
 onboarding.create_project.pat_help.link=Create personal access token
 onboarding.create_project.pat_help.bbs_permission_projects=Projects: {perm}
 onboarding.create_project.pat_help.bbs_permission_repos=Repositories: {perm}
 onboarding.create_project.pat_help.read_permission=Read
+
+onboarding.create_project.pat_help.instructions2.gitlab=Set a name, for example "SonarQube", and select the following scope:
+onboarding.create_project.pat_help.gitlab.read_api_permission=read_api
+
 onboarding.create_project.no_bbs_projects=No projects could be fetched from Bitbucket Server. Contact your system administrator, or {link}.
 onboarding.create_project.no_bbs_repos=No repositories were found for this project. Contact your system administrator, or {link}.
 onboarding.create_project.update_your_token=update your personal access token
@@ -3159,6 +3172,7 @@ onboarding.create_project.github.warning.message_admin=Please make sure the GitH
 onboarding.create_project.github.warning.message_admin.link=ALM integration settings
 onboarding.create_project.github.no_orgs=We couldn't load any organizations with your key. Contact an administrator.
 onboarding.create_project.github.no_orgs_admin=We couldn't load any organizations. Make sure the GitHub App is installed in at least one organization and check the GitHub instance configuration in the {link}.
+onboarding.create_project.gitlab.title=Which GitLab project do you want to setup?
 
 onboarding.create_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.