]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20086 Migrating personal access token screen to adapt new designs
authorRevanshu Paliwal <revanshu.paliwal@sonarsource.com>
Thu, 10 Aug 2023 13:52:49 +0000 (15:52 +0200)
committersonartech <sonartech@sonarsource.com>
Mon, 14 Aug 2023 20:02:57 +0000 (20:02 +0000)
15 files changed:
server/sonar-web/src/main/js/apps/create/project/Azure/AzurePersonalAccessTokenForm.tsx
server/sonar-web/src/main/js/apps/create/project/BitbucketCloud/BitbucketCloudPersonalAccessTokenForm.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/BitbucketCloud/BitbucketCloudProjectCreateRender.tsx
server/sonar-web/src/main/js/apps/create/project/BitbucketServer/BitbucketProjectCreateRenderer.tsx
server/sonar-web/src/main/js/apps/create/project/BitbucketServer/BitbucketServerPersonalAccessTokenForm.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/Gitlab/GItlabPersonalAccessTokenForm.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/project/Gitlab/GitlabProjectCreateRenderer.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/Azure-it.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/Bitbucket-it.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketCloud-it.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/GitLab-it.tsx
server/sonar-web/src/main/js/apps/create/project/components/AlmRepoItem.tsx
server/sonar-web/src/main/js/apps/create/project/components/PersonalAccessTokenForm.tsx [deleted file]
server/sonar-web/src/main/js/apps/create/project/usePersonalAccessToken.ts [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index c3d621863aeef26eb6789ae6a959320277a05adb..3f71739e892474703122040bac51643449c222af 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import classNames from 'classnames';
+import {
+  ButtonPrimary,
+  DeferredSpinner,
+  FlagErrorIcon,
+  FlagMessage,
+  FormField,
+  InputField,
+  LightPrimary,
+  Link,
+} from 'design-system';
 import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
-import Link from '../../../../components/common/Link';
-import ValidationInput from '../../../../components/controls/ValidationInput';
-import { SubmitButton } from '../../../../components/controls/buttons';
-import { Alert } from '../../../../components/ui/Alert';
-import DeferredSpinner from '../../../../components/ui/DeferredSpinner';
 import { translate } from '../../../../helpers/l10n';
 import { AlmSettingsInstance } from '../../../../types/alm-settings';
 
@@ -42,7 +46,7 @@ function getAzurePatUrl(url: string) {
 
 export default function AzurePersonalAccessTokenForm(props: AzurePersonalAccessTokenFormProps) {
   const {
-    almSetting: { alm, url },
+    almSetting: { url },
     submitting = false,
     validationFailed,
     firstConnection,
@@ -61,79 +65,92 @@ export default function AzurePersonalAccessTokenForm(props: AzurePersonalAccessT
   if (!token) {
     errorMessage = translate('onboarding.create_project.pat_form.pat_required');
   } else if (isInvalid) {
-    errorMessage = translate('onboarding.create_project.pat_incorrect', alm);
+    errorMessage = translate('onboarding.create_project.pat_incorrect.azure');
   }
 
   return (
-    <div className="boxed-group abs-width-600">
-      <div className="boxed-group-inner">
-        <h2>{translate('onboarding.create_project.pat_form.title', alm)}</h2>
+    <form
+      className="sw-mt-3 sw-w-[50%]"
+      onSubmit={(e: React.SyntheticEvent<HTMLFormElement>) => {
+        e.preventDefault();
+        props.onPersonalAccessTokenCreate(token);
+      }}
+    >
+      <LightPrimary as="h2" className="sw-heading-md">
+        {translate('onboarding.create_project.pat_form.title')}
+      </LightPrimary>
+      <LightPrimary as="p" className="sw-mt-2 sw-mb-4 sw-body-sm">
+        {translate('onboarding.create_project.pat_form.help.azure')}
+      </LightPrimary>
 
-        <div className="big-spacer-top big-spacer-bottom">
-          <FormattedMessage
-            id="onboarding.create_project.pat_help.instructions"
-            defaultMessage={translate('onboarding.create_project.pat_help.instructions', alm)}
-            values={{
-              link: url ? (
-                <Link className="link-no-underline" to={getAzurePatUrl(url)} target="_blank">
-                  {translate('onboarding.create_project.pat_help.instructions.link', alm)}
-                </Link>
-              ) : (
-                translate('onboarding.create_project.pat_help.instructions.link', alm)
-              ),
-              scope: (
-                <strong>
-                  <em>Code (Read & Write)</em>
-                </strong>
-              ),
-            }}
-          />
+      {isInvalid && (
+        <div>
+          <FlagMessage variant="error" className="sw-mb-4">
+            <p>{errorMessage}</p>
+          </FlagMessage>
         </div>
+      )}
 
-        {!firstConnection && (
-          <Alert className="big-spacer-right" variant="warning">
-            <p>{translate('onboarding.create_project.pat.expired.info_message')}</p>
-            <p>{translate('onboarding.create_project.pat.expired.info_message_contact')}</p>
-          </Alert>
-        )}
+      {!firstConnection && (
+        <FlagMessage variant="warning">
+          <p>
+            {translate('onboarding.create_project.pat.expired.info_message')}{' '}
+            {translate('onboarding.create_project.pat.expired.info_message_contact')}
+          </p>
+        </FlagMessage>
+      )}
 
-        <form
-          onSubmit={(e: React.SyntheticEvent<HTMLFormElement>) => {
-            e.preventDefault();
-            props.onPersonalAccessTokenCreate(token);
-          }}
-        >
-          <ValidationInput
-            error={errorMessage}
-            labelHtmlFor="personal_access_token"
+      <FormField
+        htmlFor="personal_access_token"
+        className="sw-mt-6 sw-mb-3"
+        label={translate('onboarding.create_project.enter_pat')}
+        required
+      >
+        <div>
+          <InputField
+            autoFocus
+            id="personal_access_token"
+            minLength={1}
+            name="personal_access_token"
+            onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
+              setToken(e.target.value);
+              setTouched(true);
+            }}
+            type="text"
+            value={token}
+            size="large"
             isInvalid={isInvalid}
-            isValid={false}
-            label={translate('onboarding.create_project.enter_pat')}
-            required
-          >
-            <input
-              autoFocus
-              className={classNames('width-100 little-spacer-bottom', {
-                'is-invalid': isInvalid,
-              })}
-              id="personal_access_token"
-              minLength={1}
-              name="personal_access_token"
-              onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
-                setToken(e.target.value);
-                setTouched(true);
+          />
+          {isInvalid && <FlagErrorIcon className="sw-ml-2" />}
+        </div>
+      </FormField>
+
+      <div className="sw-mb-6">
+        <FlagMessage variant="info">
+          <p>
+            <FormattedMessage
+              id="onboarding.create_project.pat_help.instructions.azure"
+              defaultMessage={translate('onboarding.create_project.pat_help.instructions.azure')}
+              values={{
+                link: url ? (
+                  <Link to={getAzurePatUrl(url)}>
+                    {translate('onboarding.create_project.pat_help.instructions.link.azure')}
+                  </Link>
+                ) : (
+                  translate('onboarding.create_project.pat_help.instructions.link.azure')
+                ),
               }}
-              type="text"
-              value={token}
             />
-          </ValidationInput>
+          </p>
+        </FlagMessage>
+      </div>
 
-          <SubmitButton disabled={isInvalid || submitting || !touched}>
-            {translate('onboarding.create_project.pat_form.list_repositories')}
-          </SubmitButton>
-          <DeferredSpinner className="spacer-left" loading={submitting} />
-        </form>
+      <div className="sw-flex sw-items-center sw-mb-6">
+        <ButtonPrimary type="submit" disabled={isInvalid || submitting || !touched}>
+          {translate('save')}
+        </ButtonPrimary>
+        <DeferredSpinner className="sw-ml-2" loading={submitting} />
       </div>
-    </div>
+    </form>
   );
 }
diff --git a/server/sonar-web/src/main/js/apps/create/project/BitbucketCloud/BitbucketCloudPersonalAccessTokenForm.tsx b/server/sonar-web/src/main/js/apps/create/project/BitbucketCloud/BitbucketCloudPersonalAccessTokenForm.tsx
new file mode 100644 (file)
index 0000000..e703e78
--- /dev/null
@@ -0,0 +1,185 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 {
+  ButtonPrimary,
+  DeferredSpinner,
+  FlagErrorIcon,
+  FlagMessage,
+  FormField,
+  InputField,
+  LightPrimary,
+  Link,
+} from 'design-system';
+import React from 'react';
+import { FormattedMessage } from 'react-intl';
+import { translate } from '../../../../helpers/l10n';
+import { AlmSettingsInstance } from '../../../../types/alm-settings';
+import { usePersonalAccessToken } from '../usePersonalAccessToken';
+
+interface Props {
+  almSetting: AlmSettingsInstance;
+  resetPat: boolean;
+  onPersonalAccessTokenCreated: () => void;
+}
+
+export default function BitbucketCloudPersonalAccessTokenForm({
+  almSetting,
+  resetPat,
+  onPersonalAccessTokenCreated,
+}: Props) {
+  const {
+    username,
+    password,
+    firstConnection,
+    validationFailed,
+    touched,
+    submitting,
+    validationErrorMessage,
+    checkingPat,
+    handlePasswordChange,
+    handleUsernameChange,
+    handleSubmit,
+  } = usePersonalAccessToken(almSetting, resetPat, onPersonalAccessTokenCreated);
+
+  if (checkingPat) {
+    return <DeferredSpinner className="sw-ml-2" loading />;
+  }
+
+  const isInvalid = validationFailed && !touched;
+  const canSubmit = Boolean(password) && Boolean(username);
+  const submitButtonDiabled = isInvalid || submitting || !canSubmit;
+
+  const errorMessage =
+    validationErrorMessage ?? translate('onboarding.create_project.pat_incorrect.bitbucket_cloud');
+
+  return (
+    <form className="sw-mt-3 sw-w-[50%]" onSubmit={handleSubmit}>
+      <LightPrimary as="h2" className="sw-heading-md">
+        {translate('onboarding.create_project.pat_form.title')}
+      </LightPrimary>
+      <LightPrimary as="p" className="sw-mt-2 sw-mb-4 sw-body-sm">
+        {translate('onboarding.create_project.pat_form.help.bitbucket_cloud')}
+      </LightPrimary>
+
+      {isInvalid && (
+        <div>
+          <FlagMessage variant="error" className="sw-mb-4">
+            <p>{errorMessage}</p>
+          </FlagMessage>
+        </div>
+      )}
+
+      {!firstConnection && (
+        <FlagMessage variant="warning">
+          <p>
+            {translate('onboarding.create_project.pat.expired.info_message')}{' '}
+            {translate('onboarding.create_project.pat.expired.info_message_contact')}
+          </p>
+        </FlagMessage>
+      )}
+
+      <FormField
+        htmlFor="enter_username_validation"
+        className="sw-mt-6 sw-mb-3"
+        label={translate('onboarding.create_project.bitbucket_cloud.enter_username')}
+        required
+      >
+        <div>
+          <InputField
+            size="large"
+            id="enter_username_validation"
+            minLength={1}
+            value={username}
+            onChange={handleUsernameChange}
+            type="text"
+            isInvalid={isInvalid}
+          />
+          {isInvalid && <FlagErrorIcon className="sw-ml-2" />}
+        </div>
+      </FormField>
+
+      <div className="sw-mb-6">
+        <FlagMessage variant="info">
+          <p>
+            <FormattedMessage
+              id="onboarding.enter_username.instructions.bitbucket_cloud"
+              defaultMessage={translate('onboarding.enter_username.instructions.bitbucket_cloud')}
+              values={{
+                link: (
+                  <Link to="https://bitbucket.org/account/settings/">
+                    {translate('onboarding.enter_username.instructions.bitbucket_cloud.link')}
+                  </Link>
+                ),
+              }}
+            />
+          </p>
+        </FlagMessage>
+      </div>
+
+      <FormField
+        htmlFor="enter_password_validation"
+        className="sw-mt-6 sw-mb-3"
+        label={translate('onboarding.create_project.bitbucket_cloud.enter_password')}
+        required
+      >
+        <div>
+          <InputField
+            size="large"
+            id="enter_password_validation"
+            minLength={1}
+            value={password}
+            onChange={handlePasswordChange}
+            type="text"
+            isInvalid={isInvalid}
+          />
+          {isInvalid && <FlagErrorIcon className="sw-ml-2" />}
+        </div>
+      </FormField>
+
+      <div className="sw-mb-6">
+        <FlagMessage variant="info">
+          <p>
+            <FormattedMessage
+              id="onboarding.create_project.enter_password.instructions.bitbucket_cloud"
+              defaultMessage={translate(
+                'onboarding.create_project.enter_password.instructions.bitbucket_cloud'
+              )}
+              values={{
+                link: (
+                  <Link to="https://bitbucket.org/account/settings/app-passwords/new">
+                    {translate(
+                      'onboarding.create_project.enter_password.instructions.bitbucket_cloud.link'
+                    )}
+                  </Link>
+                ),
+              }}
+            />
+          </p>
+        </FlagMessage>
+      </div>
+
+      <ButtonPrimary type="submit" disabled={submitButtonDiabled} className="sw-mb-6">
+        {translate('save')}
+      </ButtonPrimary>
+      <DeferredSpinner className="sw-ml-2" loading={submitting} />
+    </form>
+  );
+}
index 992f8046e4f645d0a90f7552210675cc50f55c23..8cd8154af3150e778ac365ad06fc43ef85d83543 100644 (file)
@@ -23,8 +23,8 @@ import { translate } from '../../../../helpers/l10n';
 import { BitbucketCloudRepository } from '../../../../types/alm-integration';
 import { AlmKeys, AlmSettingsInstance } from '../../../../types/alm-settings';
 import AlmSettingsInstanceDropdown from '../components/AlmSettingsInstanceDropdown';
-import PersonalAccessTokenForm from '../components/PersonalAccessTokenForm';
 import WrongBindingCountAlert from '../components/WrongBindingCountAlert';
+import BitbucketCloudPersonalAccessTokenForm from './BitbucketCloudPersonalAccessTokenForm';
 import BitbucketCloudSearchForm from './BitbucketCloudSearchForm';
 
 export interface BitbucketCloudProjectCreateRendererProps {
@@ -90,7 +90,7 @@ export default function BitbucketCloudProjectCreateRenderer(
       {!loading &&
         selectedAlmInstance &&
         (showPersonalAccessTokenForm ? (
-          <PersonalAccessTokenForm
+          <BitbucketCloudPersonalAccessTokenForm
             almSetting={selectedAlmInstance}
             resetPat={resetPat}
             onPersonalAccessTokenCreated={props.onPersonalAccessTokenCreated}
index 0b435d15b5dc0222bda460a9806c8b6a33742969..f6b4caeedd9f1f345a73e3f2679119cae6fd2122 100644 (file)
@@ -27,9 +27,9 @@ import {
 } from '../../../../types/alm-integration';
 import { AlmKeys, AlmSettingsInstance } from '../../../../types/alm-settings';
 import AlmSettingsInstanceDropdown from '../components/AlmSettingsInstanceDropdown';
-import PersonalAccessTokenForm from '../components/PersonalAccessTokenForm';
 import WrongBindingCountAlert from '../components/WrongBindingCountAlert';
 import BitbucketImportRepositoryForm from './BitbucketImportRepositoryForm';
+import BitbucketServerPersonalAccessTokenForm from './BitbucketServerPersonalAccessTokenForm';
 
 export interface BitbucketProjectCreateRendererProps {
   selectedAlmInstance?: AlmSettingsInstance;
@@ -88,7 +88,7 @@ export default function BitbucketProjectCreateRenderer(props: BitbucketProjectCr
 
         {selectedAlmInstance &&
           (showPersonalAccessTokenForm ? (
-            <PersonalAccessTokenForm
+            <BitbucketServerPersonalAccessTokenForm
               almSetting={selectedAlmInstance}
               onPersonalAccessTokenCreated={props.onPersonalAccessTokenCreated}
               resetPat={resetPat}
diff --git a/server/sonar-web/src/main/js/apps/create/project/BitbucketServer/BitbucketServerPersonalAccessTokenForm.tsx b/server/sonar-web/src/main/js/apps/create/project/BitbucketServer/BitbucketServerPersonalAccessTokenForm.tsx
new file mode 100644 (file)
index 0000000..914cee8
--- /dev/null
@@ -0,0 +1,149 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 {
+  ButtonPrimary,
+  DeferredSpinner,
+  FlagErrorIcon,
+  FlagMessage,
+  FormField,
+  InputField,
+  LightPrimary,
+  Link,
+} from 'design-system';
+import React from 'react';
+import { FormattedMessage } from 'react-intl';
+import { translate } from '../../../../helpers/l10n';
+import { AlmSettingsInstance } from '../../../../types/alm-settings';
+import { usePersonalAccessToken } from '../usePersonalAccessToken';
+
+interface Props {
+  almSetting: AlmSettingsInstance;
+  resetPat: boolean;
+  onPersonalAccessTokenCreated: () => void;
+}
+
+export default function BitbucketServerPersonalAccessTokenForm({
+  almSetting,
+  resetPat,
+  onPersonalAccessTokenCreated,
+}: Props) {
+  const {
+    password,
+    firstConnection,
+    validationFailed,
+    touched,
+    submitting,
+    validationErrorMessage,
+    checkingPat,
+    handlePasswordChange,
+    handleSubmit,
+  } = usePersonalAccessToken(almSetting, resetPat, onPersonalAccessTokenCreated);
+
+  if (checkingPat) {
+    return <DeferredSpinner className="sw-ml-2" loading />;
+  }
+
+  const { url } = almSetting;
+  const isInvalid = validationFailed && !touched;
+  const canSubmit = Boolean(password);
+  const submitButtonDiabled = isInvalid || submitting || !canSubmit;
+
+  const errorMessage =
+    validationErrorMessage ?? translate('onboarding.create_project.pat_incorrect.bitbucket');
+
+  return (
+    <form className="sw-mt-3 sw-w-[50%]" onSubmit={handleSubmit}>
+      <LightPrimary as="h2" className="sw-heading-md">
+        {translate('onboarding.create_project.pat_form.title')}
+      </LightPrimary>
+      <LightPrimary as="p" className="sw-mt-2 sw-mb-4 sw-body-sm">
+        {translate('onboarding.create_project.pat_form.help.bitbucket')}
+      </LightPrimary>
+
+      {isInvalid && (
+        <div>
+          <FlagMessage variant="error" className="sw-mb-4">
+            <p>{errorMessage}</p>
+          </FlagMessage>
+        </div>
+      )}
+
+      {!firstConnection && (
+        <FlagMessage variant="warning">
+          <p>
+            {translate('onboarding.create_project.pat.expired.info_message')}{' '}
+            {translate('onboarding.create_project.pat.expired.info_message_contact')}
+          </p>
+        </FlagMessage>
+      )}
+
+      <FormField
+        htmlFor="personal_access_token_validation"
+        className="sw-mt-6 sw-mb-3"
+        label={translate('onboarding.create_project.enter_pat')}
+        required
+      >
+        <div>
+          <InputField
+            autoFocus
+            size="large"
+            id="personal_access_token_validation"
+            minLength={1}
+            value={password}
+            onChange={handlePasswordChange}
+            type="text"
+            isInvalid={isInvalid}
+          />
+          {isInvalid && <FlagErrorIcon className="sw-ml-2" />}
+        </div>
+      </FormField>
+
+      <div className="sw-mb-6">
+        <FlagMessage variant="info">
+          <p>
+            <FormattedMessage
+              id="onboarding.create_project.pat_help.instructions.bitbucket_server"
+              defaultMessage={translate(
+                'onboarding.create_project.pat_help.instructions.bitbucket_server'
+              )}
+              values={{
+                link: url ? (
+                  <Link to={`${url.replace(/\/$/, '')}/account`}>
+                    {translate(
+                      'onboarding.create_project.pat_help.instructions.bitbucket_server.link'
+                    )}
+                  </Link>
+                ) : (
+                  translate('onboarding.create_project.pat_help.instructions.bitbucket_server.link')
+                ),
+              }}
+            />
+          </p>
+        </FlagMessage>
+      </div>
+
+      <ButtonPrimary type="submit" disabled={submitButtonDiabled} className="sw-mb-6">
+        {translate('save')}
+      </ButtonPrimary>
+      <DeferredSpinner className="sw-ml-2" loading={submitting} />
+    </form>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/create/project/Gitlab/GItlabPersonalAccessTokenForm.tsx b/server/sonar-web/src/main/js/apps/create/project/Gitlab/GItlabPersonalAccessTokenForm.tsx
new file mode 100644 (file)
index 0000000..2b12662
--- /dev/null
@@ -0,0 +1,142 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 {
+  ButtonPrimary,
+  DeferredSpinner,
+  FlagErrorIcon,
+  FlagMessage,
+  FormField,
+  InputField,
+  LightPrimary,
+  Link,
+} from 'design-system';
+import React from 'react';
+import { FormattedMessage } from 'react-intl';
+import { translate } from '../../../../helpers/l10n';
+import { AlmSettingsInstance } from '../../../../types/alm-settings';
+import { usePersonalAccessToken } from '../usePersonalAccessToken';
+
+interface Props {
+  almSetting: AlmSettingsInstance;
+  resetPat: boolean;
+  onPersonalAccessTokenCreated: () => void;
+}
+
+export default function GitlabPersonalAccessTokenForm({
+  almSetting,
+  resetPat,
+  onPersonalAccessTokenCreated,
+}: Props) {
+  const {
+    password,
+    firstConnection,
+    validationFailed,
+    touched,
+    submitting,
+    validationErrorMessage,
+    checkingPat,
+    handlePasswordChange,
+    handleSubmit,
+  } = usePersonalAccessToken(almSetting, resetPat, onPersonalAccessTokenCreated);
+
+  if (checkingPat) {
+    return <DeferredSpinner className="sw-ml-2" loading />;
+  }
+
+  const isInvalid = validationFailed && !touched;
+  const canSubmit = Boolean(password);
+  const submitButtonDiabled = isInvalid || submitting || !canSubmit;
+
+  const errorMessage =
+    validationErrorMessage ?? translate('onboarding.create_project.pat_incorrect.gitlab');
+
+  return (
+    <form className="sw-mt-3 sw-w-[50%]" onSubmit={handleSubmit}>
+      <LightPrimary as="h2" className="sw-heading-md">
+        {translate('onboarding.create_project.pat_form.title')}
+      </LightPrimary>
+      <LightPrimary as="p" className="sw-mt-2 sw-mb-4 sw-body-sm">
+        {translate('onboarding.create_project.pat_form.help.gitlab')}
+      </LightPrimary>
+
+      {isInvalid && (
+        <div>
+          <FlagMessage variant="error" className="sw-mb-4">
+            <p>{errorMessage}</p>
+          </FlagMessage>
+        </div>
+      )}
+
+      {!firstConnection && (
+        <FlagMessage variant="warning">
+          <p>
+            {translate('onboarding.create_project.pat.expired.info_message')}{' '}
+            {translate('onboarding.create_project.pat.expired.info_message_contact')}
+          </p>
+        </FlagMessage>
+      )}
+
+      <FormField
+        htmlFor="personal_access_token_validation"
+        className="sw-mt-6 sw-mb-3"
+        label={translate('onboarding.create_project.enter_pat')}
+        required
+      >
+        <div>
+          <InputField
+            autoFocus
+            size="large"
+            id="personal_access_token_validation"
+            minLength={1}
+            value={password}
+            onChange={handlePasswordChange}
+            type="text"
+            isInvalid={isInvalid}
+          />
+          {isInvalid && <FlagErrorIcon className="sw-ml-2" />}
+        </div>
+      </FormField>
+
+      <div className="sw-mb-6">
+        <FlagMessage variant="info">
+          <p>
+            <FormattedMessage
+              id="onboarding.create_project.pat_help.instructions.gitlab"
+              defaultMessage={translate('onboarding.create_project.pat_help.instructions.gitlab')}
+              values={{
+                link: (
+                  <Link to="https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html">
+                    {translate('onboarding.create_project.pat_help.instructions.gitlab.link')}
+                  </Link>
+                ),
+              }}
+            />
+          </p>
+        </FlagMessage>
+      </div>
+
+      <ButtonPrimary type="submit" disabled={submitButtonDiabled} className="sw-mb-6">
+        {translate('save')}
+      </ButtonPrimary>
+      <DeferredSpinner className="sw-ml-2" loading={submitting} />
+    </form>
+  );
+}
index 5bbf169a8e5ef681eb6466bd2eaa2d91187e2645..0b843afff802a383f7f4f34336ea731fc019fb7a 100644 (file)
@@ -25,8 +25,8 @@ import { GitlabProject } from '../../../../types/alm-integration';
 import { AlmKeys, AlmSettingsInstance } from '../../../../types/alm-settings';
 import { Paging } from '../../../../types/types';
 import AlmSettingsInstanceDropdown from '../components/AlmSettingsInstanceDropdown';
-import PersonalAccessTokenForm from '../components/PersonalAccessTokenForm';
 import WrongBindingCountAlert from '../components/WrongBindingCountAlert';
+import GitlabPersonalAccessTokenForm from './GItlabPersonalAccessTokenForm';
 import GitlabProjectSelectionForm from './GitlabProjectSelectionForm';
 
 export interface GitlabProjectCreateRendererProps {
@@ -88,7 +88,7 @@ export default function GitlabProjectCreateRenderer(props: GitlabProjectCreateRe
       {!loading &&
         selectedAlmInstance &&
         (showPersonalAccessTokenForm ? (
-          <PersonalAccessTokenForm
+          <GitlabPersonalAccessTokenForm
             almSetting={selectedAlmInstance}
             resetPat={resetPat}
             onPersonalAccessTokenCreated={props.onPersonalAccessTokenCreated}
index 3460c7656d581af0c2b85a5ba7840b2ec5429235..db9b1a3b2bad835e5408fe7247b258c470197fc9 100644 (file)
@@ -74,16 +74,12 @@ it('should ask for PAT when it is not set yet and show the import project featur
   expect(screen.getByText('alm.configuration.selector.label.alm.azure.long')).toBeInTheDocument();
 
   expect(screen.getByText('onboarding.create_project.enter_pat')).toBeInTheDocument();
-  expect(screen.getByText('onboarding.create_project.pat_form.title.azure')).toBeInTheDocument();
-  expect(
-    screen.getByRole('button', { name: 'onboarding.create_project.pat_form.list_repositories' })
-  ).toBeInTheDocument();
+  expect(screen.getByText('onboarding.create_project.pat_form.title')).toBeInTheDocument();
+  expect(screen.getByRole('button', { name: 'save' })).toBeInTheDocument();
 
   await user.click(ui.personalAccessTokenInput.get());
   await user.keyboard('secret');
-  await user.click(
-    screen.getByRole('button', { name: 'onboarding.create_project.pat_form.list_repositories' })
-  );
+  await user.click(screen.getByRole('button', { name: 'save' }));
 
   expect(screen.getByText('Azure project')).toBeInTheDocument();
   expect(screen.getByText('Azure project 2')).toBeInTheDocument();
index 6b807c894857da2fcd4c8610854ef064347c25b1..8a9ace4db8267d7ce3466c5f94873f221d8f1810 100644 (file)
@@ -74,9 +74,7 @@ it('should ask for PAT when it is not set yet and show the import project featur
   expect(screen.getByText('onboarding.create_project.from_bbs')).toBeInTheDocument();
   expect(await ui.instanceSelector.find()).toBeInTheDocument();
 
-  expect(
-    screen.getByText('onboarding.create_project.pat_form.title.bitbucket')
-  ).toBeInTheDocument();
+  expect(screen.getByText('onboarding.create_project.pat_form.title')).toBeInTheDocument();
 
   expect(screen.getByRole('button', { name: 'save' })).toBeDisabled();
 
index af4d30aa07c8c4a7aea053282be6a7e9a5a571bb..9cd94893473a1da680031f05b0eaaf54435f0685 100644 (file)
@@ -79,26 +79,23 @@ it('should ask for PAT when it is not set yet and show the import project featur
   expect(await ui.instanceSelector.find()).toBeInTheDocument();
 
   expect(
-    screen.getByText('onboarding.create_project.enter_pat.bitbucketcloud')
+    screen.getByText('onboarding.create_project.bitbucket_cloud.enter_password')
   ).toBeInTheDocument();
   expect(
-    screen.getByText(
-      'onboarding.create_project.pat_help.instructions_username.bitbucketcloud.title'
-    )
+    screen.getByText('onboarding.create_project.enter_password.instructions.bitbucket_cloud')
   ).toBeInTheDocument();
 
   expect(
-    screen.getByText('onboarding.create_project.pat.expired.info_message')
-  ).toBeInTheDocument();
-  expect(
-    screen.getByText('onboarding.create_project.pat.expired.info_message_contact')
+    screen.getByText(
+      'onboarding.create_project.pat.expired.info_message onboarding.create_project.pat.expired.info_message_contact'
+    )
   ).toBeInTheDocument();
 
   expect(screen.getByRole('button', { name: 'save' })).toBeDisabled();
 
   await user.click(
     screen.getByRole('textbox', {
-      name: /onboarding.create_project.enter_username/,
+      name: /onboarding.create_project.bitbucket_cloud.enter_username/,
     })
   );
 
@@ -106,7 +103,7 @@ it('should ask for PAT when it is not set yet and show the import project featur
 
   await user.click(
     screen.getByRole('textbox', {
-      name: /onboarding.create_project.enter_pat.bitbucketcloud/,
+      name: /onboarding.create_project.bitbucket_cloud.enter_password/,
     })
   );
 
index cc5494759827e75161dc3f4ee2a3821660b1edd5..1fa6e4ee7517089b51ad9fdd5586706aa3298b49 100644 (file)
@@ -76,7 +76,9 @@ it('should ask for PAT when it is not set yet and show the import project featur
   expect(ui.instanceSelector.get()).toBeInTheDocument();
 
   expect(screen.getByText('onboarding.create_project.enter_pat')).toBeInTheDocument();
-  expect(screen.getByText('onboarding.create_project.pat_help.title')).toBeInTheDocument();
+  expect(
+    screen.getByText('onboarding.create_project.pat_help.instructions.gitlab')
+  ).toBeInTheDocument();
   expect(screen.getByRole('button', { name: 'save' })).toBeInTheDocument();
   await act(async () => {
     await user.click(ui.personalAccessTokenInput.get());
index af61167b9d21e6979eb58805da432d6c18da7b64..4b341da893b9fe977da92a796809d8974c5b0ad5 100644 (file)
@@ -83,8 +83,8 @@ export default function AlmRepoItem({
             </LightPrimary>
           )}
         </div>
-        <div className="sw-max-w-[50%] sw-min-w-0 sw-ml-2 sw-flex sw-items-center sw-truncate">
-          <LightLabel className="sw-body-sm">{secondaryTextNode}</LightLabel>
+        <div className="sw-max-w-[50%] sw-min-w-0 sw-ml-2 sw-flex sw-items-center">
+          <LightLabel className="sw-body-sm sw-truncate">{secondaryTextNode}</LightLabel>
         </div>
       </div>
       {almUrl !== undefined && (
diff --git a/server/sonar-web/src/main/js/apps/create/project/components/PersonalAccessTokenForm.tsx b/server/sonar-web/src/main/js/apps/create/project/components/PersonalAccessTokenForm.tsx
deleted file mode 100644 (file)
index c7ebeba..0000000
+++ /dev/null
@@ -1,428 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 classNames from 'classnames';
-import * as React from 'react';
-import { FormattedMessage } from 'react-intl';
-import {
-  checkPersonalAccessTokenIsValid,
-  setAlmPersonalAccessToken,
-} from '../../../../api/alm-integrations';
-import ValidationInput from '../../../../components/controls/ValidationInput';
-import { SubmitButton } from '../../../../components/controls/buttons';
-import { Alert } from '../../../../components/ui/Alert';
-import DeferredSpinner from '../../../../components/ui/DeferredSpinner';
-import { translate } from '../../../../helpers/l10n';
-import { getBaseUrl } from '../../../../helpers/system';
-import { AlmKeys, AlmSettingsInstance } from '../../../../types/alm-settings';
-import { tokenExistedBefore } from '../utils';
-
-interface Props {
-  almSetting: AlmSettingsInstance;
-  resetPat: boolean;
-  onPersonalAccessTokenCreated: () => void;
-}
-
-interface State {
-  validationFailed: boolean;
-  validationErrorMessage?: string;
-  touched: boolean;
-  password: string;
-  username?: string;
-  submitting: boolean;
-  checkingPat: boolean;
-  firstConnection: boolean;
-}
-
-function getPatUrl(alm: AlmKeys, url = '') {
-  if (alm === AlmKeys.BitbucketServer) {
-    return `${url.replace(/\/$/, '')}/account`;
-  } else if (alm === AlmKeys.BitbucketCloud) {
-    return 'https://bitbucket.org/account/settings/app-passwords/new';
-  } else if (alm === AlmKeys.GitLab) {
-    return 'https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html';
-  }
-
-  return '';
-}
-
-export default class PersonalAccessTokenForm extends React.PureComponent<Props, State> {
-  mounted = false;
-
-  constructor(props: Props) {
-    super(props);
-
-    this.state = {
-      checkingPat: false,
-      touched: false,
-      password: '',
-      submitting: false,
-      validationFailed: false,
-      firstConnection: false,
-    };
-  }
-
-  componentDidMount() {
-    this.mounted = true;
-    this.checkPATAndUpdateView();
-  }
-
-  componentDidUpdate(prevProps: Props) {
-    if (this.props.almSetting !== prevProps.almSetting) {
-      this.checkPATAndUpdateView();
-    }
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  checkPATAndUpdateView = async () => {
-    const {
-      almSetting: { key },
-      resetPat,
-    } = this.props;
-
-    // We don't need to check PAT if we want to reset
-    if (!resetPat) {
-      this.setState({ checkingPat: true });
-      const { patIsValid, error } = await checkPersonalAccessTokenIsValid(key)
-        .then(({ status, error }) => ({ patIsValid: status, error }))
-        .catch(() => ({ patIsValid: status, error: translate('default_error_message') }));
-      if (patIsValid) {
-        this.props.onPersonalAccessTokenCreated();
-      }
-      if (this.mounted) {
-        // This is the initial message when no token was provided
-        if (tokenExistedBefore(error)) {
-          this.setState({
-            checkingPat: false,
-            firstConnection: true,
-          });
-        } else {
-          this.setState({
-            checkingPat: false,
-            validationFailed: true,
-            validationErrorMessage: error,
-          });
-        }
-      }
-    }
-  };
-
-  handleUsernameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
-    this.setState({
-      touched: true,
-      username: event.target.value,
-    });
-  };
-
-  handlePasswordChange = (event: React.ChangeEvent<HTMLInputElement>) => {
-    this.setState({
-      touched: true,
-      password: event.target.value,
-    });
-  };
-
-  handleSubmit = async (e: React.SyntheticEvent<HTMLFormElement>) => {
-    const { password, username } = this.state;
-    const {
-      almSetting: { key },
-    } = this.props;
-
-    e.preventDefault();
-    if (password) {
-      this.setState({ submitting: true });
-
-      await setAlmPersonalAccessToken(key, password, username).catch(() => {
-        /* Set will not check pat validity. We need to check again so we will catch issue after */
-      });
-
-      const { status, error } = await checkPersonalAccessTokenIsValid(key)
-        .then(({ status, error }) => ({ status, error }))
-        .catch(() => ({ status: false, error: translate('default_error_message') }));
-
-      if (this.mounted && status) {
-        // Let's reset status,
-        this.setState({
-          checkingPat: false,
-          touched: false,
-          password: '',
-          submitting: false,
-          username: '',
-          validationFailed: false,
-        });
-        this.props.onPersonalAccessTokenCreated();
-      } else if (this.mounted) {
-        this.setState({
-          submitting: false,
-          touched: false,
-          validationFailed: true,
-          validationErrorMessage: error,
-        });
-      }
-    }
-  };
-
-  renderHelpBox(suffixTranslationKey: string) {
-    const {
-      almSetting: { alm, url },
-    } = this.props;
-
-    return (
-      <Alert className="big-spacer-left width-50" display="block" variant="info">
-        {alm === AlmKeys.BitbucketCloud && (
-          <>
-            <h3>
-              {translate(
-                'onboarding.create_project.pat_help.instructions_username.bitbucketcloud.title'
-              )}
-            </h3>
-            <p className="big-spacer-top big-spacer-bottom">
-              {translate('onboarding.create_project.pat_help.instructions_username.bitbucketcloud')}
-            </p>
-
-            <div className="text-middle big-spacer-bottom">
-              <img
-                alt="" // Should be ignored by screen readers
-                className="spacer-right"
-                height="16"
-                src={`${getBaseUrl()}/images/alm/${AlmKeys.BitbucketServer}.svg`}
-              />
-              <a
-                href="https://bitbucket.org/account/settings/"
-                rel="noopener noreferrer"
-                target="_blank"
-              >
-                {translate(
-                  'onboarding.create_project.pat_help.instructions_username.bitbucketcloud.link'
-                )}
-              </a>
-            </div>
-          </>
-        )}
-
-        <h3>{translate(`onboarding.create_project.pat_help${suffixTranslationKey}.title`)}</h3>
-
-        <p className="big-spacer-top big-spacer-bottom">
-          {alm === AlmKeys.BitbucketServer ? (
-            <FormattedMessage
-              id="onboarding.create_project.pat_help.instructions"
-              defaultMessage={translate(
-                `onboarding.create_project.pat_help.bitbucket.instructions`
-              )}
-              values={{
-                menu: (
-                  <strong>
-                    {translate('onboarding.create_project.pat_help.bitbucket.instructions.menu')}
-                  </strong>
-                ),
-                button: (
-                  <strong>
-                    {translate('onboarding.create_project.pat_help.bitbucket.instructions.button')}
-                  </strong>
-                ),
-              }}
-            />
-          ) : (
-            <FormattedMessage
-              id="onboarding.create_project.pat_help.instructions"
-              defaultMessage={translate(
-                `onboarding.create_project.pat_help${suffixTranslationKey}.instructions`
-              )}
-              values={{
-                alm: translate('onboarding.alm', alm),
-              }}
-            />
-          )}
-        </p>
-
-        {(url || alm === AlmKeys.BitbucketCloud) && (
-          <div className="text-middle">
-            <img
-              alt="" // Should be ignored by screen readers
-              className="spacer-right"
-              height="16"
-              src={`${getBaseUrl()}/images/alm/${
-                alm === AlmKeys.BitbucketCloud ? AlmKeys.BitbucketServer : alm
-              }.svg`}
-            />
-            <a href={getPatUrl(alm, url)} rel="noopener noreferrer" target="_blank">
-              {translate(`onboarding.create_project.pat_help${suffixTranslationKey}.link`)}
-            </a>
-          </div>
-        )}
-
-        <p className="big-spacer-top big-spacer-bottom">
-          {translate('onboarding.create_project.pat_help.instructions2', alm)}
-        </p>
-
-        <ul>
-          {alm === AlmKeys.BitbucketServer && (
-            <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>
-          )}
-          {(alm === AlmKeys.BitbucketServer || alm === AlmKeys.BitbucketCloud) && (
-            <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>
-    );
-  }
-
-  render() {
-    const {
-      almSetting: { alm },
-    } = this.props;
-    const {
-      checkingPat,
-      submitting,
-      touched,
-      password,
-      username,
-      validationFailed,
-      validationErrorMessage,
-      firstConnection,
-    } = this.state;
-
-    if (checkingPat) {
-      return <DeferredSpinner className="spacer-left" loading />;
-    }
-
-    const suffixTranslationKey = alm === AlmKeys.BitbucketCloud ? '.bitbucketcloud' : '';
-
-    const isInvalid = validationFailed && !touched;
-    const canSubmit = Boolean(password) && (alm !== AlmKeys.BitbucketCloud || Boolean(username));
-    const submitButtonDiabled = isInvalid || submitting || !canSubmit;
-
-    const errorMessage =
-      validationErrorMessage ?? translate('onboarding.create_project.pat_incorrect', alm);
-
-    return (
-      <div className="display-flex-start">
-        <form className="width-50" onSubmit={this.handleSubmit}>
-          <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>
-
-          {!firstConnection && (
-            <Alert className="big-spacer-right" variant="warning">
-              <p>{translate('onboarding.create_project.pat.expired.info_message')}</p>
-              <p>{translate('onboarding.create_project.pat.expired.info_message_contact')}</p>
-            </Alert>
-          )}
-
-          {alm === AlmKeys.BitbucketCloud && (
-            <ValidationInput
-              error={undefined}
-              labelHtmlFor="enter_username_validation"
-              isInvalid={false}
-              isValid={false}
-              label={translate('onboarding.create_project.enter_username')}
-              required
-            >
-              <input
-                autoFocus
-                className={classNames('input-super-large', {
-                  'is-invalid': isInvalid,
-                })}
-                id="enter_username_validation"
-                minLength={1}
-                name="username"
-                value={username}
-                onChange={this.handleUsernameChange}
-                type="text"
-              />
-            </ValidationInput>
-          )}
-
-          <ValidationInput
-            error={errorMessage}
-            labelHtmlFor="personal_access_token_validation"
-            isInvalid={false}
-            isValid={false}
-            label={translate(`onboarding.create_project.enter_pat${suffixTranslationKey}`)}
-            required
-          >
-            <input
-              autoFocus={alm !== AlmKeys.BitbucketCloud}
-              className={classNames('input-super-large', {
-                'is-invalid': isInvalid,
-              })}
-              id="personal_access_token_validation"
-              minLength={1}
-              value={password}
-              onChange={this.handlePasswordChange}
-              type="text"
-            />
-          </ValidationInput>
-
-          <ValidationInput
-            error={errorMessage}
-            labelHtmlFor="personal_access_token_submit"
-            isInvalid={isInvalid}
-            isValid={false}
-            label={null}
-          >
-            <SubmitButton disabled={submitButtonDiabled}>{translate('save')}</SubmitButton>
-            <DeferredSpinner className="spacer-left" loading={submitting} />
-          </ValidationInput>
-        </form>
-
-        {this.renderHelpBox(suffixTranslationKey)}
-      </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/create/project/usePersonalAccessToken.ts b/server/sonar-web/src/main/js/apps/create/project/usePersonalAccessToken.ts
new file mode 100644 (file)
index 0000000..ab4c5e2
--- /dev/null
@@ -0,0 +1,143 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { useEffect, useState } from 'react';
+import {
+  checkPersonalAccessTokenIsValid,
+  setAlmPersonalAccessToken,
+} from '../../../api/alm-integrations';
+import { translate } from '../../../helpers/l10n';
+import { AlmSettingsInstance } from '../../../types/alm-settings';
+import { tokenExistedBefore } from './utils';
+
+export interface PATType {
+  validationFailed: boolean;
+  validationErrorMessage?: string;
+  touched: boolean;
+  password: string;
+  username?: string;
+  submitting: boolean;
+  checkingPat: boolean;
+  firstConnection: boolean;
+  handleUsernameChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
+  handlePasswordChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
+  handleSubmit: (e: React.SyntheticEvent<HTMLFormElement>) => Promise<void>;
+}
+
+export const usePersonalAccessToken = (
+  almSetting: AlmSettingsInstance,
+  resetPat: boolean,
+  onPersonalAccessTokenCreated: () => void
+): PATType => {
+  const [checkingPat, setCheckingPat] = useState(false);
+  const [touched, setTouched] = useState(false);
+  const [password, setPassword] = useState('');
+  const [submitting, setSubmitting] = useState(false);
+  const [validationFailed, setValidationFailed] = useState(false);
+  const [validationErrorMessage, setValidationErrorMessage] = useState<string | undefined>();
+  const [firstConnection, setFirstConnection] = useState(false);
+  const [username, setUsername] = useState('');
+
+  useEffect(() => {
+    const checkPATAndUpdateView = async () => {
+      const { key } = almSetting;
+
+      // We don't need to check PAT if we want to reset
+      if (!resetPat) {
+        setCheckingPat(true);
+        const { patIsValid, error } = await checkPersonalAccessTokenIsValid(key)
+          .then(({ status, error }) => ({ patIsValid: status, error }))
+          .catch(() => ({ patIsValid: status, error: translate('default_error_message') }));
+        if (patIsValid) {
+          onPersonalAccessTokenCreated();
+          return;
+        }
+        // This is the initial message when no token was provided
+        if (tokenExistedBefore(error)) {
+          setCheckingPat(false);
+          setFirstConnection(true);
+        } else {
+          setCheckingPat(false);
+          setValidationFailed(true);
+          setValidationErrorMessage(error);
+        }
+      }
+    };
+    checkPATAndUpdateView();
+  }, [almSetting, resetPat, onPersonalAccessTokenCreated]);
+
+  const handleUsernameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+    setTouched(true);
+    setUsername(event.target.value);
+  };
+
+  const handlePasswordChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+    setTouched(true);
+    setPassword(event.target.value);
+  };
+
+  const handleSubmit = async (e: React.SyntheticEvent<HTMLFormElement>) => {
+    const { key } = almSetting;
+
+    e.preventDefault();
+    if (password) {
+      setSubmitting(true);
+
+      await setAlmPersonalAccessToken(key, password, username).catch(() => {
+        /* Set will not check pat validity. We need to check again so we will catch issue after */
+      });
+
+      const { status, error } = await checkPersonalAccessTokenIsValid(key)
+        .then(({ status, error }) => ({ status, error }))
+        .catch(() => ({ status: false, error: translate('default_error_message') }));
+
+      if (status) {
+        // Let's reset status,
+        setCheckingPat(false);
+        setTouched(false);
+        setPassword('');
+        setSubmitting(false);
+        setUsername('');
+        setValidationFailed(false);
+
+        onPersonalAccessTokenCreated();
+      } else {
+        setSubmitting(false);
+        setTouched(false);
+        setValidationFailed(true);
+        setValidationErrorMessage(error);
+      }
+    }
+  };
+
+  return {
+    username,
+    password,
+    firstConnection,
+    validationFailed,
+    touched,
+    submitting,
+    checkingPat,
+    validationErrorMessage,
+    handleUsernameChange,
+    handlePasswordChange,
+    handleSubmit,
+  };
+};
index 7dc5a3b10abf70ccf7bc29006334da7a760aaa9c..cd7c7f23d0d79ab4f6b54a860e76517d1872e6ed 100644 (file)
@@ -3888,56 +3888,43 @@ onboarding.create_project.select_all_repositories=Select all available repositor
 onboarding.create_project.from_bbs=Bitbucket Server project onboarding
 
 onboarding.create_application.key.description=If specified, this value is used as the key instead of generating it from the name of the Application. Only letters, digits, dashes and underscores can be used.
-
-onboarding.create_project.pat_form.title.azure=Allow SonarQube to access and list your Azure DevOps repositories
-onboarding.create_project.pat_form.title.bitbucket=Grant access to your repositories
-onboarding.create_project.pat_form.title.bitbucketcloud=Grant access to your repositories
-onboarding.create_project.pat_form.title.gitlab=Grant access to your projects
+onboarding.create_project.pat_form.title=Grant access to your repositories
 onboarding.create_project.pat_form.help.azure=SonarQube needs a personal access token to access and list your repositories from Azure DevOps.
 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.bitbucketcloud=SonarQube needs an app password to access and list your repositories from Bitbucket Cloud.
+onboarding.create_project.pat_form.help.bitbucket_cloud=SonarQube needs an app password to access and list your repositories from Bitbucket Cloud.
 onboarding.create_project.pat_form.help.gitlab=SonarQube needs a personal access token to access and list your projects from GitLab.
 onboarding.create_project.pat_form.pat_required=Please enter a personal access token
-onboarding.create_project.pat_form.list_repositories=List repositories
 onboarding.create_project.wrong_binding_count=You must have at least 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 at least 1 {alm} instance configured in order to use this method. You can configure instances under {url}.
 onboarding.create_project.azure.no_url.admin=Your Azure DevOps instance configuration is missing a URL. We cannot import projects in the current state. You can configure instances under {url}.
 onboarding.create_project.azure.no_url=Your Azure DevOps instance configuration is missing a URL. We cannot import projects in the current state. Please contact your system administrator.
-onboarding.create_project.enter_pat=Enter personal access token
-onboarding.create_project.enter_pat.bitbucketcloud=Enter your app password
-onboarding.create_project.enter_username=Enter your Bitbucket username
+onboarding.create_project.enter_pat=Personal Access Token
+onboarding.create_project.bitbucket_cloud.enter_password=App password
+onboarding.create_project.bitbucket_cloud.enter_username=BitBucket username
 onboarding.create_project.pat_incorrect.azure=Your personal access couldn't be validated.
 onboarding.create_project.pat_incorrect.bitbucket=Your personal access couldn't be validated.
-onboarding.create_project.pat_incorrect.bitbucketcloud=Your app password couldn't be validated.
+onboarding.create_project.pat_incorrect.bitbucket_cloud=Your app password couldn't be validated.
 onboarding.create_project.pat_incorrect.gitlab=Your personal access couldn't be validated. Please make sure it has the right scope and that it is not expired.
-onboarding.create_project.pat_help.title=How to create a personal access token?
-onboarding.create_project.pat_help.bitbucketcloud.title=How to create an app password?
 onboarding.create_project.pat.expired.info_message=You have to do this again as your token may have expired or has been revoked.
 onboarding.create_project.pat.expired.info_message_contact=If this does not fix the issue, please contact your system administrator.
 
-onboarding.create_project.pat_help.instructions.azure=Create and provide an Azure DevOps {link}. You need to select the {scope} scope so we can display a list of your repositories which are available for analysis.
-onboarding.create_project.pat_help.instructions.link.azure=personal access token
-
-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.bitbucketcloud.instructions=Click the following link to generate an app password, and copy-paste it into the app password field.
-onboarding.create_project.pat_help.bitbucket.instructions=Click the following link and go to {menu} and click {button} to generate a token. Then, copy-paste it into the personal access token field.
-onboarding.create_project.pat_help.bitbucket.instructions.menu=HTTP access tokens
-onboarding.create_project.pat_help.bitbucket.instructions.button=Create token
+onboarding.create_project.pat_help.instructions.azure=To create a Personal Access Token on Azure, {link} and make sure to select the “Code (Read & Write)” scope.
+onboarding.create_project.pat_help.instructions.link.azure=generate a token
 
+onboarding.create_project.pat_help.instructions.gitlab=To create a Personal Access Token on GitLab, {link} by setting a name, for example “SonarQube” and selecting the “read_api” scope.
+onboarding.create_project.pat_help.instructions.gitlab.link=generate a token
 
-onboarding.create_project.pat_help.instructions2.bitbucket=Set a name, for example "SonarQube", and select the following permissions:
-onboarding.create_project.pat_help.instructions2.bitbucketcloud=Set a name, for example "SonarQube", and select the following permissions:
+onboarding.enter_username.instructions.bitbucket_cloud=You can find your username in your {link}
+onboarding.enter_username.instructions.bitbucket_cloud.link=BitBucket profile settings
 
+onboarding.create_project.enter_password.instructions.bitbucket_cloud=To create an app password on BitBucket, {link} by setting a name, for example “SonarQube” and selecting the “Repositories: Read” permissions.
+onboarding.create_project.enter_password.instructions.bitbucket_cloud.link=add an app password
 
-onboarding.create_project.pat_help.instructions_username.bitbucketcloud.title=How to find your username?
-onboarding.create_project.pat_help.instructions_username.bitbucketcloud=Click the following link to find your username in the Bitbucket profile settings
-onboarding.create_project.pat_help.instructions_username.bitbucketcloud.link=Personal settings
+onboarding.create_project.pat_help.instructions.bitbucket_server=To create a Personal Access Token on BitBucket Server, go to {link} and click on “Create token”. Set a name, for example “SonarQube” and select the following permissions “Projects: Read” “Repositories: Read”.
+onboarding.create_project.pat_help.instructions.bitbucket_server.link=HTTP access tokens
 
-onboarding.create_project.pat_help.link=Create personal access token
-onboarding.create_project.pat_help.bitbucketcloud.link=Add app password
-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.bitbucket=Set a name, for example "SonarQube", and select the following permissions:
+onboarding.create_project.pat_help.instructions2.bitbucketcloud=Set a name, for example "SonarQube", and select the following permissions:
 
 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