aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web
diff options
context:
space:
mode:
authorMathieu Suen <mathieu.suen@sonarsource.com>2021-05-12 09:49:12 +0200
committersonartech <sonartech@sonarsource.com>2021-05-21 20:03:37 +0000
commitac3d6f0496fd79c9ba17cb8551060ad424fd10be (patch)
tree73e2b3a796cca264e42a9fdf396e4dec32c93bf4 /server/sonar-web
parentbe8b3d7aa7efccf0a7b8c6f650acfca069450484 (diff)
downloadsonarqube-ac3d6f0496fd79c9ba17cb8551060ad424fd10be.tar.gz
sonarqube-ac3d6f0496fd79c9ba17cb8551060ad424fd10be.zip
SONAR-14801 Adding App password form for Bitbucket cloud onboarding
Diffstat (limited to 'server/sonar-web')
-rw-r--r--server/sonar-web/src/main/js/api/alm-integrations.ts10
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/BitbucketCloudProjectCreate.tsx97
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/BitbucketCloudProjectCreateRender.tsx76
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreate.tsx104
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreateRenderer.tsx13
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx18
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreate.tsx106
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreateRenderer.tsx16
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/PersonalAccessTokenForm.tsx370
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketCloudProjectCreate-test.tsx65
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketCloudProjectCreateRender-test.tsx49
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketProjectCreate-test.tsx66
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketProjectCreateRenderer-test.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectCreate-test.tsx119
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectCreateRenderer-test.tsx7
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/PersonalAccessTokenForm-test.tsx131
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketCloudProjectCreate-test.tsx.snap33
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketCloudProjectCreateRender-test.tsx.snap101
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectCreate-test.tsx.snap58
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectCreateRenderer-test.tsx.snap4
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap13
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectCreate-test.tsx.snap6
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitlabProjectCreateRenderer-test.tsx.snap37
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/PersonalAccessTokenForm-test.tsx.snap429
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/types.ts1
-rw-r--r--server/sonar-web/src/main/js/helpers/mocks/alm-settings.ts10
26 files changed, 1189 insertions, 754 deletions
diff --git a/server/sonar-web/src/main/js/api/alm-integrations.ts b/server/sonar-web/src/main/js/api/alm-integrations.ts
index 7bdd9e14622..2a7124f19ed 100644
--- a/server/sonar-web/src/main/js/api/alm-integrations.ts
+++ b/server/sonar-web/src/main/js/api/alm-integrations.ts
@@ -30,8 +30,14 @@ import {
} from '../types/alm-integration';
import { ProjectBase } from './components';
-export function setAlmPersonalAccessToken(almSetting: string, pat: string): Promise<void> {
- return post('/api/alm_integrations/set_pat', { almSetting, pat }).catch(throwGlobalError);
+export function setAlmPersonalAccessToken(
+ almSetting: string,
+ pat: string,
+ username?: string
+): Promise<void> {
+ return post('/api/alm_integrations/set_pat', { almSetting, pat, username }).catch(
+ throwGlobalError
+ );
}
export function checkPersonalAccessTokenIsValid(
diff --git a/server/sonar-web/src/main/js/apps/create/project/BitbucketCloudProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/BitbucketCloudProjectCreate.tsx
new file mode 100644
index 00000000000..ffd78a7a2f7
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/project/BitbucketCloudProjectCreate.tsx
@@ -0,0 +1,97 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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 { BitbucketProjectRepositories, BitbucketRepository } from '../../../types/alm-integration';
+import { AlmSettingsInstance } from '../../../types/alm-settings';
+import BitbucketCloudProjectCreateRenderer from './BitbucketCloudProjectCreateRender';
+
+interface Props extends Pick<WithRouterProps, 'location' | 'router'> {
+ canAdmin: boolean;
+ settings: AlmSettingsInstance[];
+ loadingBindings: boolean;
+ onProjectCreate: (projectKeys: string[]) => void;
+}
+
+interface State {
+ settings: AlmSettingsInstance;
+ loading: boolean;
+ projectRepositories?: BitbucketProjectRepositories;
+ searchResults?: BitbucketRepository[];
+ selectedRepository?: BitbucketRepository;
+ showPersonalAccessTokenForm: boolean;
+}
+
+export default class BitbucketCloudProjectCreate extends React.PureComponent<Props, State> {
+ mounted = false;
+
+ constructor(props: Props) {
+ super(props);
+ this.state = {
+ // For now, we only handle a single instance. So we always use the first
+ // one from the list.
+ settings: props.settings[0],
+ loading: false,
+ showPersonalAccessTokenForm: true
+ };
+ }
+
+ componentDidMount() {
+ this.mounted = true;
+ this.fetchData();
+ }
+
+ componentDidUpdate(prevProps: Props) {
+ if (prevProps.settings.length === 0 && this.props.settings.length > 0) {
+ this.setState({ settings: this.props.settings[0] }, () => this.fetchData());
+ }
+ }
+
+ handlePersonalAccessTokenCreated = async () => {
+ this.setState({ showPersonalAccessTokenForm: false });
+ this.cleanUrl();
+ await this.fetchData();
+ };
+
+ cleanUrl = () => {
+ const { location, router } = this.props;
+ delete location.query.resetPat;
+ router.replace(location);
+ };
+
+ async fetchData() {}
+
+ render() {
+ const { canAdmin, loadingBindings, location } = this.props;
+ const { settings, loading, showPersonalAccessTokenForm } = this.state;
+ return (
+ <BitbucketCloudProjectCreateRenderer
+ settings={settings}
+ canAdmin={canAdmin}
+ loading={loading || loadingBindings}
+ onPersonalAccessTokenCreated={this.handlePersonalAccessTokenCreated}
+ resetPat={Boolean(location.query.resetPat)}
+ showPersonalAccessTokenForm={
+ showPersonalAccessTokenForm || Boolean(location.query.resetPat)
+ }
+ />
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/create/project/BitbucketCloudProjectCreateRender.tsx b/server/sonar-web/src/main/js/apps/create/project/BitbucketCloudProjectCreateRender.tsx
new file mode 100644
index 00000000000..a3c50b0dc0f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/project/BitbucketCloudProjectCreateRender.tsx
@@ -0,0 +1,76 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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 BitbucketCloudProjectCreateRendererProps {
+ settings?: AlmSettingsInstance;
+ canAdmin?: boolean;
+ loading: boolean;
+ onPersonalAccessTokenCreated: () => void;
+ resetPat: boolean;
+ showPersonalAccessTokenForm: boolean;
+}
+
+export default function BitbucketCloudProjectCreateRenderer(
+ props: BitbucketCloudProjectCreateRendererProps
+) {
+ const { settings, canAdmin, loading, resetPat, showPersonalAccessTokenForm } = 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/bitbucket.svg`}
+ />
+ {translate('onboarding.create_project.bitbucketcloud.title')}
+ </span>
+ }
+ />
+ {loading && <i className="spinner" />}
+
+ {!loading && !settings && (
+ <WrongBindingCountAlert alm={AlmKeys.BitbucketCloud} canAdmin={!!canAdmin} />
+ )}
+
+ {!loading &&
+ settings &&
+ (showPersonalAccessTokenForm ? (
+ <PersonalAccessTokenForm
+ almSetting={settings}
+ resetPat={resetPat}
+ onPersonalAccessTokenCreated={props.onPersonalAccessTokenCreated}
+ />
+ ) : (
+ <p>Placeholder for next step</p>
+ ))}
+ </>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreate.tsx
index c3d57d0cfbc..bb019e40aae 100644
--- a/server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreate.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreate.tsx
@@ -20,12 +20,10 @@
import * as React from 'react';
import { WithRouterProps } from 'react-router';
import {
- checkPersonalAccessTokenIsValid,
getBitbucketServerProjects,
getBitbucketServerRepositories,
importBitbucketServerProject,
- searchForBitbucketServerRepositories,
- setAlmPersonalAccessToken
+ searchForBitbucketServerRepositories
} from '../../../api/alm-integrations';
import {
BitbucketProject,
@@ -36,7 +34,7 @@ import { AlmSettingsInstance } from '../../../types/alm-settings';
import BitbucketCreateProjectRenderer from './BitbucketProjectCreateRenderer';
import { DEFAULT_BBS_PAGE_SIZE } from './constants';
-interface Props extends Pick<WithRouterProps, 'location'> {
+interface Props extends Pick<WithRouterProps, 'location' | 'router'> {
canAdmin: boolean;
bitbucketSettings: AlmSettingsInstance[];
loadingBindings: boolean;
@@ -47,14 +45,12 @@ interface State {
bitbucketSetting?: AlmSettingsInstance;
importing: boolean;
loading: boolean;
- patIsValid?: boolean;
projects?: BitbucketProject[];
projectRepositories?: BitbucketProjectRepositories;
searching: boolean;
searchResults?: BitbucketRepository[];
selectedRepository?: BitbucketRepository;
- submittingToken?: boolean;
- tokenValidationFailed: boolean;
+ showPersonalAccessTokenForm: boolean;
}
export default class BitbucketProjectCreate extends React.PureComponent<Props, State> {
@@ -69,13 +65,12 @@ export default class BitbucketProjectCreate extends React.PureComponent<Props, S
importing: false,
loading: false,
searching: false,
- tokenValidationFailed: false
+ showPersonalAccessTokenForm: true
};
}
componentDidMount() {
this.mounted = true;
- this.fetchInitialData();
}
componentDidUpdate(prevProps: Props) {
@@ -91,38 +86,27 @@ export default class BitbucketProjectCreate extends React.PureComponent<Props, S
}
fetchInitialData = async () => {
- this.setState({ loading: true });
+ const { showPersonalAccessTokenForm } = this.state;
- const patIsValid = await this.checkPersonalAccessToken().catch(() => false);
+ if (!showPersonalAccessTokenForm) {
+ this.setState({ loading: true });
+ const projects = await this.fetchBitbucketProjects().catch(() => undefined);
- let projects;
- if (patIsValid) {
- projects = await this.fetchBitbucketProjects().catch(() => undefined);
- }
-
- let projectRepositories;
- if (projects && projects.length > 0) {
- projectRepositories = await this.fetchBitbucketRepositories(projects).catch(() => undefined);
- }
-
- if (this.mounted) {
- this.setState({
- patIsValid,
- projects,
- projectRepositories,
- loading: false
- });
- }
- };
-
- checkPersonalAccessToken = () => {
- const { bitbucketSetting } = this.state;
-
- if (!bitbucketSetting) {
- return Promise.resolve(false);
+ let projectRepositories;
+ if (projects && projects.length > 0) {
+ projectRepositories = await this.fetchBitbucketRepositories(projects).catch(
+ () => undefined
+ );
+ }
+
+ if (this.mounted) {
+ this.setState({
+ projects,
+ projectRepositories,
+ loading: false
+ });
+ }
}
-
- return checkPersonalAccessTokenIsValid(bitbucketSetting.key).then(({ status }) => status);
};
fetchBitbucketProjects = (): Promise<BitbucketProject[] | undefined> => {
@@ -184,29 +168,16 @@ export default class BitbucketProjectCreate extends React.PureComponent<Props, S
});
};
- handlePersonalAccessTokenCreate = (token: string) => {
- const { bitbucketSetting } = this.state;
-
- if (!bitbucketSetting || token.length < 1) {
- return;
- }
+ cleanUrl = () => {
+ const { location, router } = this.props;
+ delete location.query.resetPat;
+ router.replace(location);
+ };
- this.setState({ submittingToken: true, tokenValidationFailed: false });
- setAlmPersonalAccessToken(bitbucketSetting.key, token)
- .then(this.checkPersonalAccessToken)
- .then(patIsValid => {
- if (this.mounted) {
- this.setState({ submittingToken: false, patIsValid, tokenValidationFailed: !patIsValid });
- if (patIsValid) {
- this.fetchInitialData();
- }
- }
- })
- .catch(() => {
- if (this.mounted) {
- this.setState({ submittingToken: false });
- }
- });
+ handlePersonalAccessTokenCreated = async () => {
+ this.setState({ showPersonalAccessTokenForm: false });
+ this.cleanUrl();
+ await this.fetchInitialData();
};
handleImportRepository = () => {
@@ -271,14 +242,12 @@ export default class BitbucketProjectCreate extends React.PureComponent<Props, S
bitbucketSetting,
importing,
loading,
- patIsValid,
projectRepositories,
projects,
searching,
searchResults,
selectedRepository,
- submittingToken,
- tokenValidationFailed
+ showPersonalAccessTokenForm
} = this.state;
return (
@@ -288,18 +257,19 @@ export default class BitbucketProjectCreate extends React.PureComponent<Props, S
importing={importing}
loading={loading || loadingBindings}
onImportRepository={this.handleImportRepository}
- onPersonalAccessTokenCreate={this.handlePersonalAccessTokenCreate}
+ onPersonalAccessTokenCreated={this.handlePersonalAccessTokenCreated}
onProjectCreate={this.props.onProjectCreate}
onSearch={this.handleSearch}
onSelectRepository={this.handleSelectRepository}
projectRepositories={projectRepositories}
projects={projects}
+ resetPat={Boolean(location.query.resetPat)}
searchResults={searchResults}
searching={searching}
selectedRepository={selectedRepository}
- showPersonalAccessTokenForm={!patIsValid || Boolean(location.query.resetPat)}
- submittingToken={submittingToken}
- tokenValidationFailed={tokenValidationFailed}
+ showPersonalAccessTokenForm={
+ showPersonalAccessTokenForm || Boolean(location.query.resetPat)
+ }
/>
);
}
diff --git a/server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreateRenderer.tsx b/server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreateRenderer.tsx
index 0f71610155b..7b832f45b78 100644
--- a/server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreateRenderer.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreateRenderer.tsx
@@ -41,16 +41,15 @@ export interface BitbucketProjectCreateRendererProps {
onImportRepository: () => void;
onSearch: (query: string) => void;
onSelectRepository: (repo: BitbucketRepository) => void;
- onPersonalAccessTokenCreate: (token: string) => void;
+ onPersonalAccessTokenCreated: () => void;
onProjectCreate: (projectKeys: string[]) => void;
projects?: BitbucketProject[];
projectRepositories?: BitbucketProjectRepositories;
+ resetPat: boolean;
searching: boolean;
searchResults?: BitbucketRepository[];
selectedRepository?: BitbucketRepository;
showPersonalAccessTokenForm?: boolean;
- submittingToken?: boolean;
- tokenValidationFailed: boolean;
}
export default function BitbucketProjectCreateRenderer(props: BitbucketProjectCreateRendererProps) {
@@ -65,8 +64,7 @@ export default function BitbucketProjectCreateRenderer(props: BitbucketProjectCr
searching,
searchResults,
showPersonalAccessTokenForm,
- submittingToken,
- tokenValidationFailed
+ resetPat
} = props;
return (
@@ -109,9 +107,8 @@ export default function BitbucketProjectCreateRenderer(props: BitbucketProjectCr
(showPersonalAccessTokenForm ? (
<PersonalAccessTokenForm
almSetting={bitbucketSetting}
- onPersonalAccessTokenCreate={props.onPersonalAccessTokenCreate}
- submitting={submittingToken}
- validationFailed={tokenValidationFailed}
+ onPersonalAccessTokenCreated={props.onPersonalAccessTokenCreated}
+ resetPat={resetPat}
/>
) : (
<BitbucketImportRepositoryForm
diff --git a/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx b/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
index 45feaadb2ad..f49ab34161b 100644
--- a/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
@@ -28,6 +28,7 @@ import { withAppState } from '../../../components/hoc/withAppState';
import { getProjectUrl } from '../../../helpers/urls';
import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings';
import AzureProjectCreate from './AzureProjectCreate';
+import BitbucketCloudProjectCreate from './BitbucketCloudProjectCreate';
import BitbucketProjectCreate from './BitbucketProjectCreate';
import CreateProjectModeSelection from './CreateProjectModeSelection';
import GitHubProjectCreate from './GitHubProjectCreate';
@@ -44,6 +45,7 @@ interface Props extends Pick<WithRouterProps, 'router' | 'location'> {
interface State {
azureSettings: AlmSettingsInstance[];
bitbucketSettings: AlmSettingsInstance[];
+ bitbucketCloudSettings: AlmSettingsInstance[];
githubSettings: AlmSettingsInstance[];
gitlabSettings: AlmSettingsInstance[];
loading: boolean;
@@ -54,6 +56,7 @@ export class CreateProjectPage extends React.PureComponent<Props, State> {
state: State = {
azureSettings: [],
bitbucketSettings: [],
+ bitbucketCloudSettings: [],
githubSettings: [],
gitlabSettings: [],
loading: true
@@ -76,6 +79,7 @@ export class CreateProjectPage extends React.PureComponent<Props, State> {
this.setState({
azureSettings: almSettings.filter(s => s.alm === AlmKeys.Azure),
bitbucketSettings: almSettings.filter(s => s.alm === AlmKeys.BitbucketServer),
+ bitbucketCloudSettings: almSettings.filter(s => s.alm === AlmKeys.BitbucketCloud),
githubSettings: almSettings.filter(s => s.alm === AlmKeys.GitHub),
gitlabSettings: almSettings.filter(s => s.alm === AlmKeys.GitLab),
loading: false
@@ -112,6 +116,7 @@ export class CreateProjectPage extends React.PureComponent<Props, State> {
const {
azureSettings,
bitbucketSettings,
+ bitbucketCloudSettings,
githubSettings,
gitlabSettings,
loading
@@ -138,6 +143,19 @@ export class CreateProjectPage extends React.PureComponent<Props, State> {
loadingBindings={loading}
location={location}
onProjectCreate={this.handleProjectCreate}
+ router={router}
+ />
+ );
+ }
+ case CreateProjectModes.BitbucketCloud: {
+ return (
+ <BitbucketCloudProjectCreate
+ canAdmin={!!canAdmin}
+ loadingBindings={loading}
+ location={location}
+ onProjectCreate={this.handleProjectCreate}
+ router={router}
+ settings={bitbucketCloudSettings}
/>
);
}
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
index 39b8537581f..a52bd7a2d54 100644
--- a/server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreate.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreate.tsx
@@ -19,15 +19,9 @@
*/
import * as React from 'react';
import { WithRouterProps } from 'react-router';
-import { translate } from 'sonar-ui-common/helpers/l10n';
-import {
- checkPersonalAccessTokenIsValid,
- getGitlabProjects,
- importGitlabProject,
- setAlmPersonalAccessToken
-} from '../../../api/alm-integrations';
+import { getGitlabProjects, importGitlabProject } from '../../../api/alm-integrations';
import { GitlabProject } from '../../../types/alm-integration';
-import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings';
+import { AlmSettingsInstance } from '../../../types/alm-settings';
import GitlabProjectCreateRenderer from './GitlabProjectCreateRenderer';
interface Props extends Pick<WithRouterProps, 'location' | 'router'> {
@@ -43,12 +37,11 @@ interface State {
loadingMore: boolean;
projects?: GitlabProject[];
projectsPaging: T.Paging;
- submittingToken: boolean;
- tokenIsValid: boolean;
- tokenValidationErrorMessage?: string;
+ resetPat: boolean;
searching: boolean;
searchQuery: string;
settings?: AlmSettingsInstance;
+ showPersonalAccessTokenForm: boolean;
}
const GITLAB_PROJECTS_PAGESIZE = 30;
@@ -63,17 +56,16 @@ export default class GitlabProjectCreate extends React.PureComponent<Props, Stat
loading: false,
loadingMore: false,
projectsPaging: { pageIndex: 1, total: 0, pageSize: GITLAB_PROJECTS_PAGESIZE },
- tokenIsValid: false,
+ resetPat: false,
+ showPersonalAccessTokenForm: true,
searching: false,
searchQuery: '',
- settings: props.settings.length === 1 ? props.settings[0] : undefined,
- submittingToken: false
+ settings: props.settings.length === 1 ? props.settings[0] : undefined
};
}
componentDidMount() {
this.mounted = true;
- this.fetchInitialData();
}
componentDidUpdate(prevProps: Props) {
@@ -90,50 +82,30 @@ export default class GitlabProjectCreate extends React.PureComponent<Props, Stat
}
fetchInitialData = async () => {
- this.setState({ loading: true });
+ const { showPersonalAccessTokenForm } = this.state;
- const { status, error } = await this.checkPersonalAccessToken();
-
- let result;
- if (status) {
- result = await this.fetchProjects();
- }
-
- if (this.mounted) {
- if (result) {
+ if (!showPersonalAccessTokenForm) {
+ this.setState({ loading: true });
+ const result = await this.fetchProjects();
+ if (this.mounted && result) {
const { projects, projectsPaging } = result;
this.setState({
- tokenIsValid: status,
loading: false,
projects,
projectsPaging
});
} else {
this.setState({
- loading: false,
- tokenValidationErrorMessage: !status ? error : undefined
+ loading: false
});
}
}
};
- checkPersonalAccessToken = () => {
- const { settings } = this.state;
-
- if (!settings) {
- return Promise.resolve({
- status: false,
- error: translate('onboarding.create_project.pat_incorrect', AlmKeys.GitLab)
- });
- }
-
- return checkPersonalAccessTokenIsValid(settings.key);
- };
-
handleError = () => {
if (this.mounted) {
- this.setState({ tokenIsValid: false });
+ this.setState({ resetPat: true, showPersonalAccessTokenForm: true });
}
return undefined;
@@ -141,7 +113,6 @@ export default class GitlabProjectCreate extends React.PureComponent<Props, Stat
fetchProjects = async (pageIndex = 1, query?: string) => {
const { settings } = this.state;
-
if (!settings) {
return Promise.resolve(undefined);
}
@@ -228,37 +199,10 @@ export default class GitlabProjectCreate extends React.PureComponent<Props, Stat
router.replace(location);
};
- handlePersonalAccessTokenCreate = async (token: string) => {
- const { settings } = this.state;
-
- if (!settings || token.length < 1) {
- return;
- }
-
- this.setState({ submittingToken: true, tokenValidationErrorMessage: undefined });
-
- try {
- await setAlmPersonalAccessToken(settings.key, token);
-
- const { status, error } = await this.checkPersonalAccessToken();
-
- if (this.mounted) {
- this.setState({
- submittingToken: false,
- tokenIsValid: status,
- tokenValidationErrorMessage: error
- });
-
- if (status) {
- this.cleanUrl();
- await this.fetchInitialData();
- }
- }
- } catch (e) {
- if (this.mounted) {
- this.setState({ submittingToken: false });
- }
- }
+ handlePersonalAccessTokenCreated = async () => {
+ this.setState({ showPersonalAccessTokenForm: false, resetPat: false });
+ this.cleanUrl();
+ await this.fetchInitialData();
};
render() {
@@ -269,12 +213,11 @@ export default class GitlabProjectCreate extends React.PureComponent<Props, Stat
loadingMore,
projects,
projectsPaging,
- tokenIsValid,
+ resetPat,
searching,
searchQuery,
settings,
- submittingToken,
- tokenValidationErrorMessage
+ showPersonalAccessTokenForm
} = this.state;
return (
@@ -286,15 +229,16 @@ export default class GitlabProjectCreate extends React.PureComponent<Props, Stat
loadingMore={loadingMore}
onImport={this.handleImport}
onLoadMore={this.handleLoadMore}
- onPersonalAccessTokenCreate={this.handlePersonalAccessTokenCreate}
+ onPersonalAccessTokenCreated={this.handlePersonalAccessTokenCreated}
onSearch={this.handleSearch}
projects={projects}
projectsPaging={projectsPaging}
+ resetPat={resetPat || Boolean(location.query.resetPat)}
searching={searching}
searchQuery={searchQuery}
- showPersonalAccessTokenForm={!tokenIsValid || Boolean(location.query.resetPat)}
- submittingToken={submittingToken}
- tokenValidationErrorMessage={tokenValidationErrorMessage}
+ showPersonalAccessTokenForm={
+ showPersonalAccessTokenForm || Boolean(location.query.resetPat)
+ }
/>
);
}
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
index e75f5809845..25795300b5b 100644
--- a/server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreateRenderer.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreateRenderer.tsx
@@ -34,16 +34,15 @@ export interface GitlabProjectCreateRendererProps {
loadingMore: boolean;
onImport: (gitlabProjectId: string) => void;
onLoadMore: () => void;
- onPersonalAccessTokenCreate: (pat: string) => void;
+ onPersonalAccessTokenCreated: () => void;
onSearch: (searchQuery: string) => void;
projects?: GitlabProject[];
projectsPaging: T.Paging;
+ resetPat: boolean;
searching: boolean;
searchQuery: string;
settings?: AlmSettingsInstance;
showPersonalAccessTokenForm?: boolean;
- submittingToken?: boolean;
- tokenValidationErrorMessage?: string;
}
export default function GitlabProjectCreateRenderer(props: GitlabProjectCreateRendererProps) {
@@ -54,12 +53,11 @@ export default function GitlabProjectCreateRenderer(props: GitlabProjectCreateRe
loadingMore,
projects,
projectsPaging,
+ resetPat,
searching,
searchQuery,
settings,
- showPersonalAccessTokenForm,
- submittingToken,
- tokenValidationErrorMessage
+ showPersonalAccessTokenForm
} = props;
return (
@@ -89,10 +87,8 @@ export default function GitlabProjectCreateRenderer(props: GitlabProjectCreateRe
(showPersonalAccessTokenForm ? (
<PersonalAccessTokenForm
almSetting={settings}
- onPersonalAccessTokenCreate={props.onPersonalAccessTokenCreate}
- submitting={submittingToken}
- validationFailed={Boolean(tokenValidationErrorMessage)}
- validationErrorMessage={tokenValidationErrorMessage}
+ resetPat={resetPat}
+ onPersonalAccessTokenCreated={props.onPersonalAccessTokenCreated}
/>
) : (
<GitlabProjectSelectionForm
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
index 3fb0a2fe63f..469061ef70e 100644
--- a/server/sonar-web/src/main/js/apps/create/project/PersonalAccessTokenForm.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/PersonalAccessTokenForm.tsx
@@ -26,19 +26,33 @@ 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 {
+ checkPersonalAccessTokenIsValid,
+ setAlmPersonalAccessToken
+} from '../../../api/alm-integrations';
import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings';
-export interface PersonalAccessTokenFormProps {
+interface Props {
almSetting: AlmSettingsInstance;
- onPersonalAccessTokenCreate: (token: string) => void;
- submitting?: boolean;
+ resetPat: boolean;
+ onPersonalAccessTokenCreated: () => void;
+}
+
+interface State {
validationFailed: boolean;
validationErrorMessage?: string;
+ touched: boolean;
+ password: string;
+ username?: string;
+ submitting: boolean;
+ checkingPat: boolean;
}
-function getPatUrl(alm: AlmKeys, url: string) {
+function getPatUrl(alm: AlmKeys, url = '') {
if (alm === AlmKeys.BitbucketServer) {
return `${url.replace(/\/$/, '')}/plugins/servlet/access-tokens/add`;
+ } else if (alm === AlmKeys.BitbucketCloud) {
+ return 'https://bitbucket.org/account/settings/app-passwords/new';
} else {
// GitLab
return url.endsWith('/api/v4')
@@ -47,97 +61,236 @@ function getPatUrl(alm: AlmKeys, url: string) {
}
}
-export default function PersonalAccessTokenForm(props: PersonalAccessTokenFormProps) {
- const {
- almSetting: { alm, url },
- submitting = false,
- validationFailed,
- validationErrorMessage
- } = props;
- const [touched, setTouched] = React.useState(false);
-
- React.useEffect(() => {
- setTouched(false);
- }, [submitting]);
-
- const isInvalid = validationFailed && !touched;
- const errorMessage =
- validationErrorMessage ?? translate('onboarding.create_project.pat_incorrect', alm);
-
- return (
- <div className="display-flex-start">
- <form
- className="width-50"
- 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 ? errorMessage : 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 width-50" 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`}
+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
+ };
+ }
+
+ async componentDidMount() {
+ const {
+ almSetting: { key },
+ resetPat
+ } = this.props;
+ this.mounted = true;
+
+ // 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 (error === `personal access token for '${key}' is missing`) {
+ this.setState({
+ checkingPat: false
+ });
+ } else {
+ this.setState({
+ checkingPat: false,
+ validationFailed: true,
+ validationErrorMessage: error
+ });
+ }
+ }
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ 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
+ });
+ }
+ }
+ };
+
+ render() {
+ const {
+ almSetting: { alm, url }
+ } = this.props;
+ const {
+ checkingPat,
+ submitting,
+ touched,
+ password,
+ username,
+ validationFailed,
+ validationErrorMessage
+ } = this.state;
+
+ if (checkingPat) {
+ return <DeferredSpinner className="spacer-left" loading={true} />;
+ }
+
+ 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>
+
+ {alm === AlmKeys.BitbucketCloud && (
+ <ValidationInput
+ error={undefined}
+ id="enter_username_validation"
+ isInvalid={false}
+ isValid={false}
+ label={translate('onboarding.create_project.enter_username')}
+ required={true}>
+ <input
+ autoFocus={true}
+ className={classNames('input-super-large', {
+ 'is-invalid': isInvalid
+ })}
+ id="username"
+ minLength={1}
+ name="username"
+ value={username}
+ onChange={this.handleUsernameChange}
+ type="text"
+ />
+ </ValidationInput>
+ )}
+
+ <ValidationInput
+ error={errorMessage}
+ id="personal_access_token_validation"
+ isInvalid={false}
+ isValid={false}
+ label={translate(`onboarding.create_project.enter_pat${suffixTranslationKey}`)}
+ required={true}>
+ <input
+ autoFocus={alm !== AlmKeys.BitbucketCloud}
+ className={classNames('input-super-large', {
+ 'is-invalid': isInvalid
+ })}
+ id="personal_access_token"
+ minLength={1}
+ value={password}
+ onChange={this.handlePasswordChange}
+ type="text"
+ />
+ </ValidationInput>
+
+ <ValidationInput
+ error={errorMessage}
+ id="personal_access_token_submit"
+ isInvalid={isInvalid}
+ isValid={false}
+ label={null}>
+ <SubmitButton disabled={submitButtonDiabled}>{translate('save')}</SubmitButton>
+ <DeferredSpinner className="spacer-left" loading={submitting} />
+ </ValidationInput>
+ </form>
+
+ <Alert className="big-spacer-left width-50" display="block" variant="info">
+ <h3>{translate(`onboarding.create_project.pat_help${suffixTranslationKey}.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${suffixTranslationKey}.instructions`
+ )}
+ values={{ alm: translate('onboarding.alm', alm) }}
/>
- <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.BitbucketServer && (
- <>
+ </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(
@@ -153,6 +306,8 @@ export default function PersonalAccessTokenForm(props: PersonalAccessTokenFormPr
}}
/>
</li>
+ )}
+ {(alm === AlmKeys.BitbucketServer || alm === AlmKeys.BitbucketCloud) && (
<li>
<FormattedMessage
defaultMessage={translate(
@@ -168,17 +323,18 @@ export default function PersonalAccessTokenForm(props: PersonalAccessTokenFormPr
}}
/>
</li>
- </>
- )}
- {alm === AlmKeys.GitLab && (
- <li className="spacer-bottom">
- <strong>
- {translate('onboarding.create_project.pat_help.gitlab.read_api_permission')}
- </strong>
- </li>
- )}
- </ul>
- </Alert>
- </div>
- );
+ )}
+
+ {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/__tests__/BitbucketCloudProjectCreate-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketCloudProjectCreate-test.tsx
new file mode 100644
index 00000000000..031e730d056
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketCloudProjectCreate-test.tsx
@@ -0,0 +1,65 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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 } from '../../../../api/alm-integrations';
+import { mockBitbucketCloudAlmSettingsInstance } from '../../../../helpers/mocks/alm-settings';
+import { mockLocation, mockRouter } from '../../../../helpers/testMocks';
+import BitbucketCloudProjectCreate from '../BitbucketCloudProjectCreate';
+
+jest.mock('../../../../api/alm-integrations', () => {
+ return {
+ checkPersonalAccessTokenIsValid: jest.fn().mockResolvedValue({ status: true }),
+ setAlmPersonalAccessToken: jest.fn().mockResolvedValue(null)
+ };
+});
+
+it('Should render correctly', async () => {
+ let wrapper = shallowRender();
+ await waitAndUpdate(wrapper);
+ expect(wrapper).toMatchSnapshot();
+ (checkPersonalAccessTokenIsValid as jest.Mock).mockRejectedValueOnce({});
+ wrapper = shallowRender();
+ await waitAndUpdate(wrapper);
+ expect(wrapper).toMatchSnapshot('Need App password');
+});
+
+it('Should handle app password correctly', async () => {
+ const wrapper = shallowRender();
+
+ await waitAndUpdate(wrapper);
+ await wrapper.instance().handlePersonalAccessTokenCreated();
+ expect(wrapper.state().showPersonalAccessTokenForm).toBe(false);
+});
+
+function shallowRender(props?: Partial<BitbucketCloudProjectCreate['props']>) {
+ return shallow<BitbucketCloudProjectCreate>(
+ <BitbucketCloudProjectCreate
+ onProjectCreate={jest.fn()}
+ loadingBindings={false}
+ location={mockLocation()}
+ canAdmin={true}
+ router={mockRouter()}
+ settings={[mockBitbucketCloudAlmSettingsInstance()]}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketCloudProjectCreateRender-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketCloudProjectCreateRender-test.tsx
new file mode 100644
index 00000000000..228467a76d6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketCloudProjectCreateRender-test.tsx
@@ -0,0 +1,49 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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 { mockBitbucketCloudAlmSettingsInstance } from '../../../../helpers/mocks/alm-settings';
+import BitbucketCloudProjectCreateRenderer, {
+ BitbucketCloudProjectCreateRendererProps
+} from '../BitbucketCloudProjectCreateRender';
+
+it('Should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot();
+ expect(shallowRender({ settings: undefined })).toMatchSnapshot('Wrong config');
+ expect(shallowRender({ loading: true })).toMatchSnapshot('Loading...');
+ expect(
+ shallowRender({
+ showPersonalAccessTokenForm: true
+ })
+ ).toMatchSnapshot('Need App password');
+});
+
+function shallowRender(props?: Partial<BitbucketCloudProjectCreateRendererProps>) {
+ return shallow(
+ <BitbucketCloudProjectCreateRenderer
+ onPersonalAccessTokenCreated={jest.fn()}
+ loading={false}
+ settings={mockBitbucketCloudAlmSettingsInstance()}
+ resetPat={false}
+ showPersonalAccessTokenForm={false}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketProjectCreate-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketProjectCreate-test.tsx
index bb3fd5ae7df..3abe3432dca 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketProjectCreate-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketProjectCreate-test.tsx
@@ -21,16 +21,17 @@ import { shallow } from 'enzyme';
import * as React from 'react';
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import {
- checkPersonalAccessTokenIsValid,
getBitbucketServerProjects,
getBitbucketServerRepositories,
importBitbucketServerProject,
- searchForBitbucketServerRepositories,
- setAlmPersonalAccessToken
+ searchForBitbucketServerRepositories
} from '../../../../api/alm-integrations';
-import { mockBitbucketRepository } from '../../../../helpers/mocks/alm-integrations';
+import {
+ mockBitbucketProject,
+ mockBitbucketRepository
+} from '../../../../helpers/mocks/alm-integrations';
import { mockAlmSettingsInstance } from '../../../../helpers/mocks/alm-settings';
-import { mockLocation } from '../../../../helpers/testMocks';
+import { mockLocation, mockRouter } from '../../../../helpers/testMocks';
import { AlmKeys } from '../../../../types/alm-settings';
import BitbucketProjectCreate from '../BitbucketProjectCreate';
@@ -39,7 +40,6 @@ jest.mock('../../../../api/alm-integrations', () => {
'../../../../helpers/mocks/alm-integrations'
);
return {
- checkPersonalAccessTokenIsValid: jest.fn().mockResolvedValue({ status: true }),
getBitbucketServerProjects: jest.fn().mockResolvedValue({
projects: [
mockBitbucketProject({ key: 'project1', name: 'Project 1' }),
@@ -53,7 +53,6 @@ jest.mock('../../../../api/alm-integrations', () => {
]
}),
importBitbucketServerProject: jest.fn().mockResolvedValue({ project: { key: 'baz' } }),
- setAlmPersonalAccessToken: jest.fn().mockResolvedValue(null),
searchForBitbucketServerRepositories: jest.fn().mockResolvedValue({
repositories: [
mockBitbucketRepository(),
@@ -65,50 +64,21 @@ jest.mock('../../../../api/alm-integrations', () => {
beforeEach(jest.clearAllMocks);
-it('should render correctly', () => {
+it('should render correctly', async () => {
expect(shallowRender()).toMatchSnapshot();
-});
-
-it('should correctly fetch binding info on mount', async () => {
- const wrapper = shallowRender();
- await waitAndUpdate(wrapper);
- expect(checkPersonalAccessTokenIsValid).toBeCalledWith('foo');
-});
-
-it('should correctly handle a valid PAT', async () => {
- (checkPersonalAccessTokenIsValid as jest.Mock).mockResolvedValueOnce({ status: true });
- const wrapper = shallowRender();
- await waitAndUpdate(wrapper);
- expect(checkPersonalAccessTokenIsValid).toBeCalled();
- expect(wrapper.state().patIsValid).toBe(true);
-});
+ expect(shallowRender({ bitbucketSettings: [] })).toMatchSnapshot('No setting');
-it('should correctly handle an invalid PAT', async () => {
- (checkPersonalAccessTokenIsValid as jest.Mock).mockResolvedValueOnce({ status: false });
const wrapper = shallowRender();
- await waitAndUpdate(wrapper);
- expect(checkPersonalAccessTokenIsValid).toBeCalled();
- expect(wrapper.state().patIsValid).toBe(false);
-});
-
-it('should correctly handle setting a new PAT', async () => {
- const wrapper = shallowRender();
- wrapper.instance().handlePersonalAccessTokenCreate('token');
- expect(setAlmPersonalAccessToken).toBeCalledWith('foo', 'token');
- expect(wrapper.state().submittingToken).toBe(true);
-
- (checkPersonalAccessTokenIsValid as jest.Mock).mockResolvedValueOnce({ status: false });
- await waitAndUpdate(wrapper);
- expect(checkPersonalAccessTokenIsValid).toBeCalled();
- expect(wrapper.state().submittingToken).toBe(false);
- expect(wrapper.state().tokenValidationFailed).toBe(true);
+ (getBitbucketServerRepositories as jest.Mock).mockRejectedValueOnce({});
+ await wrapper.instance().handlePersonalAccessTokenCreated();
+ expect(wrapper).toMatchSnapshot('No repository');
});
it('should correctly fetch projects and repos', async () => {
const wrapper = shallowRender();
+ await wrapper.instance().handlePersonalAccessTokenCreated();
// Opens first project on mount.
- await waitAndUpdate(wrapper);
expect(getBitbucketServerProjects).toBeCalledWith('foo');
expect(wrapper.state().projects).toHaveLength(2);
@@ -159,6 +129,17 @@ it('should correctly handle search', async () => {
expect(wrapper.state().searchResults).toHaveLength(2);
});
+it('should behave correctly when no setting', async () => {
+ const wrapper = shallowRender({ bitbucketSettings: [] });
+ await wrapper.instance().handleSearch('');
+ await wrapper.instance().handleImportRepository();
+ await wrapper.instance().fetchBitbucketRepositories([mockBitbucketProject()]);
+
+ expect(searchForBitbucketServerRepositories).not.toHaveBeenCalled();
+ expect(importBitbucketServerProject).not.toHaveBeenCalled();
+ expect(getBitbucketServerRepositories).not.toHaveBeenCalled();
+});
+
function shallowRender(props: Partial<BitbucketProjectCreate['props']> = {}) {
return shallow<BitbucketProjectCreate>(
<BitbucketProjectCreate
@@ -166,6 +147,7 @@ function shallowRender(props: Partial<BitbucketProjectCreate['props']> = {}) {
bitbucketSettings={[mockAlmSettingsInstance({ alm: AlmKeys.BitbucketServer, key: 'foo' })]}
loadingBindings={false}
location={mockLocation()}
+ router={mockRouter()}
onProjectCreate={jest.fn()}
{...props}
/>
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketProjectCreateRenderer-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketProjectCreateRenderer-test.tsx
index 127da7584f4..5d523fe6705 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketProjectCreateRenderer-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/BitbucketProjectCreateRenderer-test.tsx
@@ -52,14 +52,14 @@ function shallowRender(props: Partial<BitbucketProjectCreateRendererProps> = {})
importing={false}
loading={false}
onImportRepository={jest.fn()}
- onPersonalAccessTokenCreate={jest.fn()}
+ onPersonalAccessTokenCreated={jest.fn()}
onProjectCreate={jest.fn()}
onSearch={jest.fn()}
onSelectRepository={jest.fn()}
projectRepositories={{ foo: { allShown: true, repositories: [mockBitbucketRepository()] } }}
projects={[mockBitbucketProject({ key: 'foo' })]}
+ resetPat={false}
searching={false}
- tokenValidationFailed={false}
{...props}
/>
);
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
index e5663ff7734..1f677f1bef7 100644
--- 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
@@ -20,12 +20,7 @@
import { shallow } from 'enzyme';
import * as React from 'react';
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
-import {
- checkPersonalAccessTokenIsValid,
- getGitlabProjects,
- importGitlabProject,
- setAlmPersonalAccessToken
-} from '../../../../api/alm-integrations';
+import { getGitlabProjects, importGitlabProject } from '../../../../api/alm-integrations';
import { mockGitlabProject } from '../../../../helpers/mocks/alm-integrations';
import { mockAlmSettingsInstance } from '../../../../helpers/mocks/alm-settings';
import { mockLocation, mockRouter } from '../../../../helpers/testMocks';
@@ -33,8 +28,6 @@ import { AlmKeys } from '../../../../types/alm-settings';
import GitlabProjectCreate from '../GitlabProjectCreate';
jest.mock('../../../../api/alm-integrations', () => ({
- checkPersonalAccessTokenIsValid: jest.fn().mockResolvedValue({ status: true }),
- setAlmPersonalAccessToken: jest.fn().mockResolvedValue(null),
getGitlabProjects: jest.fn().mockRejectedValue('error'),
importGitlabProject: jest.fn().mockRejectedValue('error')
}));
@@ -47,84 +40,7 @@ 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({ status: true });
- (getGitlabProjects as jest.Mock).mockResolvedValueOnce({
- projects: [mockGitlabProject()],
- projectsPaging: {
- pageIndex: 1,
- pageSize: 10,
- total: 1
- }
- });
- 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({ status: false });
- const wrapper = shallowRender();
- await waitAndUpdate(wrapper);
- expect(wrapper.state().tokenIsValid).toBe(false);
-});
-
-describe('setting a new PAT', () => {
- const routerReplace = jest.fn();
- const wrapper = shallowRender({ router: mockRouter({ replace: routerReplace }) });
-
- beforeEach(() => {
- jest.clearAllMocks();
- });
-
- it('should correctly handle it if invalid', async () => {
- const error = 'error message';
- (checkPersonalAccessTokenIsValid as jest.Mock).mockResolvedValueOnce({ status: false, error });
-
- 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().tokenValidationErrorMessage).toBe(error);
- });
-
- it('should correctly handle it if valid', async () => {
- (checkPersonalAccessTokenIsValid as jest.Mock).mockResolvedValueOnce({ status: 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().tokenValidationErrorMessage).toBeUndefined();
-
- expect(routerReplace).toBeCalled();
- });
-});
-
it('should fetch more projects and preserve search', async () => {
- (checkPersonalAccessTokenIsValid as jest.Mock).mockResolvedValueOnce({ status: true });
-
const projects = [
mockGitlabProject({ id: '1' }),
mockGitlabProject({ id: '2' }),
@@ -153,7 +69,7 @@ it('should fetch more projects and preserve search', async () => {
const wrapper = shallowRender();
- await waitAndUpdate(wrapper);
+ await wrapper.instance().handlePersonalAccessTokenCreated();
wrapper.setState({ searchQuery: 'query' });
wrapper.instance().handleLoadMore();
@@ -167,8 +83,6 @@ it('should fetch more projects and preserve search', async () => {
});
it('should search for projects', async () => {
- (checkPersonalAccessTokenIsValid as jest.Mock).mockResolvedValueOnce({ status: true });
-
const projects = [
mockGitlabProject({ id: '1' }),
mockGitlabProject({ id: '2' }),
@@ -197,11 +111,10 @@ it('should search for projects', async () => {
const query = 'query';
const wrapper = shallowRender();
- await waitAndUpdate(wrapper);
+ await wrapper.instance().handlePersonalAccessTokenCreated();
wrapper.instance().handleSearch(query);
expect(wrapper.state().searching).toBe(true);
-
await waitAndUpdate(wrapper);
expect(wrapper.state().searching).toBe(false);
expect(wrapper.state().searchQuery).toBe(query);
@@ -211,8 +124,6 @@ it('should search for projects', async () => {
});
it('should import', async () => {
- (checkPersonalAccessTokenIsValid as jest.Mock).mockResolvedValueOnce({ status: true });
-
const projects = [mockGitlabProject({ id: '1' }), mockGitlabProject({ id: '2' })];
(getGitlabProjects as jest.Mock).mockResolvedValueOnce({
projects,
@@ -231,7 +142,7 @@ it('should import', async () => {
const onProjectCreate = jest.fn();
const wrapper = shallowRender({ onProjectCreate });
- await waitAndUpdate(wrapper);
+ await wrapper.instance().handlePersonalAccessTokenCreated();
wrapper.instance().handleImport(projects[1].id);
expect(wrapper.state().importingGitlabProjectId).toBe(projects[1].id);
@@ -245,17 +156,13 @@ it('should import', async () => {
it('should do nothing with missing settings', async () => {
const wrapper = shallowRender({ settings: [] });
- await waitAndUpdate(wrapper);
-
- wrapper.instance().handleLoadMore();
- wrapper.instance().handleSearch('whatever');
- wrapper.instance().handlePersonalAccessTokenCreate('token');
- wrapper.instance().handleImport('gitlab project id');
+ await wrapper.instance().handleLoadMore();
+ await wrapper.instance().handleSearch('whatever');
+ await wrapper.instance().handlePersonalAccessTokenCreated();
+ await wrapper.instance().handleImport('gitlab project id');
- expect(checkPersonalAccessTokenIsValid).not.toHaveBeenCalled();
expect(getGitlabProjects).not.toHaveBeenCalled();
expect(importGitlabProject).not.toHaveBeenCalled();
- expect(setAlmPersonalAccessToken).not.toHaveBeenCalled();
});
it('should handle errors when fetching projects', async () => {
@@ -263,8 +170,10 @@ it('should handle errors when fetching projects', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);
+ await wrapper.instance().handlePersonalAccessTokenCreated();
- expect(wrapper.state().tokenIsValid).toBe(false);
+ expect(wrapper.state().resetPat).toBe(true);
+ expect(wrapper.state().showPersonalAccessTokenForm).toBe(true);
});
it('should handle errors when importing a project', async () => {
@@ -279,14 +188,12 @@ it('should handle errors when importing a project', async () => {
});
const wrapper = shallowRender();
- await waitAndUpdate(wrapper);
-
- expect(wrapper.state().tokenIsValid).toBe(true);
+ await wrapper.instance().handlePersonalAccessTokenCreated();
await wrapper.instance().handleImport('gitlabId');
await waitAndUpdate(wrapper);
- expect(wrapper.state().tokenIsValid).toBe(false);
+ expect(wrapper.state().showPersonalAccessTokenForm).toBe(true);
});
function shallowRender(props: Partial<GitlabProjectCreate['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
index 3a41f9c6fde..39e4b9fc289 100644
--- 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
@@ -35,9 +35,6 @@ it('should render correctly', () => {
expect(shallowRender({ showPersonalAccessTokenForm: false })).toMatchSnapshot(
'project selection form'
);
- expect(shallowRender({ tokenValidationErrorMessage: 'error' })).toMatchSnapshot(
- 'pat validation error'
- );
});
function shallowRender(props: Partial<GitlabProjectCreateRendererProps> = {}) {
@@ -48,14 +45,14 @@ function shallowRender(props: Partial<GitlabProjectCreateRendererProps> = {}) {
loadingMore={false}
onImport={jest.fn()}
onLoadMore={jest.fn()}
- onPersonalAccessTokenCreate={jest.fn()}
+ onPersonalAccessTokenCreated={jest.fn()}
onSearch={jest.fn()}
projects={undefined}
projectsPaging={{ pageIndex: 1, pageSize: 30, total: 0 }}
searching={false}
searchQuery=""
+ resetPat={false}
showPersonalAccessTokenForm={true}
- submittingToken={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
index 23fc3ad3138..f8d55114a1e 100644
--- 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
@@ -20,37 +20,59 @@
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 { change, submit, waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
+import {
+ checkPersonalAccessTokenIsValid,
+ setAlmPersonalAccessToken
+} from '../../../../api/alm-integrations';
+import {
+ mockAlmSettingsInstance,
+ mockBitbucketCloudAlmSettingsInstance
+} 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({ validationFailed: true, validationErrorMessage: 'error' })
- ).toMatchSnapshot('validation failed, custom error message');
- 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'
- })
+import PersonalAccessTokenForm from '../PersonalAccessTokenForm';
+
+jest.mock('../../../../api/alm-integrations', () => ({
+ checkPersonalAccessTokenIsValid: jest.fn().mockResolvedValue({ status: true }),
+ setAlmPersonalAccessToken: jest.fn().mockResolvedValue({})
+}));
+
+it('should render correctly', async () => {
+ expect(shallowRender()).toMatchSnapshot('no token needed');
+
+ (checkPersonalAccessTokenIsValid as jest.Mock).mockResolvedValueOnce({ status: false });
+ let wrapper = shallowRender();
+ await waitAndUpdate(wrapper);
+ expect(wrapper).toMatchSnapshot('bitbucket');
+
+ (checkPersonalAccessTokenIsValid as jest.Mock).mockResolvedValueOnce({ status: false });
+ wrapper = shallowRender({ almSetting: mockBitbucketCloudAlmSettingsInstance() });
+ await waitAndUpdate(wrapper);
+ expect(wrapper).toMatchSnapshot('bitbucket cloud');
+
+ (checkPersonalAccessTokenIsValid as jest.Mock).mockResolvedValueOnce({ status: false });
+ wrapper = shallowRender({
+ almSetting: mockAlmSettingsInstance({ alm: AlmKeys.GitLab, url: 'https://gitlab.com/api/v4' })
+ });
+ await waitAndUpdate(wrapper);
+ expect(wrapper).toMatchSnapshot('gitlab');
+
+ (checkPersonalAccessTokenIsValid as jest.Mock).mockResolvedValueOnce({ status: false });
+ wrapper = shallowRender({
+ almSetting: mockAlmSettingsInstance({
+ alm: AlmKeys.GitLab,
+ url: 'https://gitlabapi.unexpectedurl.org'
})
- ).toMatchSnapshot('gitlab with non-standard api path');
+ });
+ await waitAndUpdate(wrapper);
+ expect(wrapper).toMatchSnapshot('gitlab with non-standard api path');
});
-it('should correctly handle form interactions', () => {
- const onPersonalAccessTokenCreate = jest.fn();
- const wrapper = shallowRender({ onPersonalAccessTokenCreate });
+it('should correctly handle form interactions', async () => {
+ const onPersonalAccessTokenCreated = jest.fn();
+ const wrapper = shallowRender({ onPersonalAccessTokenCreated });
+ await waitAndUpdate(wrapper);
// Submit button disabled by default.
expect(wrapper.find(SubmitButton).prop('disabled')).toBe(true);
@@ -60,25 +82,62 @@ it('should correctly handle form interactions', () => {
// Expect correct calls to be made when submitting.
submit(wrapper.find('form'));
- expect(onPersonalAccessTokenCreate).toBeCalled();
+ expect(onPersonalAccessTokenCreated).toBeCalled();
+ expect(setAlmPersonalAccessToken).toBeCalledWith('key', 'token', undefined);
+});
+
+it('should correctly handle form for bitbucket interactions', async () => {
+ const onPersonalAccessTokenCreated = jest.fn();
+ const wrapper = shallowRender({
+ almSetting: mockBitbucketCloudAlmSettingsInstance(),
+ onPersonalAccessTokenCreated
+ });
+
+ await waitAndUpdate(wrapper);
+ // Submit button disabled by default.
+ expect(wrapper.find(SubmitButton).prop('disabled')).toBe(true);
- // 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 });
+ change(wrapper.find('#personal_access_token'), 'token');
expect(wrapper.find(SubmitButton).prop('disabled')).toBe(true);
+
+ // Submit button enabled if there's a value.
+ change(wrapper.find('#username'), 'username');
+ expect(wrapper.find(SubmitButton).prop('disabled')).toBe(false);
+
+ // Expect correct calls to be made when submitting.
+ submit(wrapper.find('form'));
+ expect(onPersonalAccessTokenCreated).toBeCalled();
+ expect(setAlmPersonalAccessToken).toBeCalledWith('key', 'token', 'username');
+});
+
+it('should show error when issue', async () => {
+ (checkPersonalAccessTokenIsValid as jest.Mock).mockRejectedValueOnce({});
+ const wrapper = shallowRender({
+ almSetting: mockBitbucketCloudAlmSettingsInstance()
+ });
+
+ await waitAndUpdate(wrapper);
+
+ (checkPersonalAccessTokenIsValid as jest.Mock).mockRejectedValueOnce({});
+
+ change(wrapper.find('#personal_access_token'), 'token');
+ change(wrapper.find('#username'), 'username');
+
+ // Expect correct calls to be made when submitting.
+ submit(wrapper.find('form'));
+ await waitAndUpdate(wrapper);
+ expect(wrapper).toMatchSnapshot('issue submitting token');
});
-function shallowRender(props: Partial<PersonalAccessTokenFormProps> = {}) {
- return shallow<PersonalAccessTokenFormProps>(
+function shallowRender(props: Partial<PersonalAccessTokenForm['props']> = {}) {
+ return shallow<PersonalAccessTokenForm>(
<PersonalAccessTokenForm
almSetting={mockAlmSettingsInstance({
alm: AlmKeys.BitbucketServer,
url: 'http://www.example.com'
})}
- onPersonalAccessTokenCreate={jest.fn()}
- validationFailed={false}
+ onPersonalAccessTokenCreated={jest.fn()}
+ resetPat={false}
{...props}
/>
);
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketCloudProjectCreate-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketCloudProjectCreate-test.tsx.snap
new file mode 100644
index 00000000000..77cd3e633d1
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketCloudProjectCreate-test.tsx.snap
@@ -0,0 +1,33 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Should render correctly 1`] = `
+<BitbucketCloudProjectCreateRenderer
+ canAdmin={true}
+ loading={false}
+ onPersonalAccessTokenCreated={[Function]}
+ resetPat={false}
+ settings={
+ Object {
+ "alm": "bitbucketcloud",
+ "key": "key",
+ }
+ }
+ showPersonalAccessTokenForm={true}
+/>
+`;
+
+exports[`Should render correctly: Need App password 1`] = `
+<BitbucketCloudProjectCreateRenderer
+ canAdmin={true}
+ loading={false}
+ onPersonalAccessTokenCreated={[Function]}
+ resetPat={false}
+ settings={
+ Object {
+ "alm": "bitbucketcloud",
+ "key": "key",
+ }
+ }
+ showPersonalAccessTokenForm={true}
+/>
+`;
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketCloudProjectCreateRender-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketCloudProjectCreateRender-test.tsx.snap
new file mode 100644
index 00000000000..aac4d670be2
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketCloudProjectCreateRender-test.tsx.snap
@@ -0,0 +1,101 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Should render correctly 1`] = `
+<Fragment>
+ <CreateProjectPageHeader
+ title={
+ <span
+ className="text-middle"
+ >
+ <img
+ alt=""
+ className="spacer-right"
+ height="24"
+ src="/images/alm/bitbucket.svg"
+ />
+ onboarding.create_project.bitbucketcloud.title
+ </span>
+ }
+ />
+ <p>
+ Placeholder for next step
+ </p>
+</Fragment>
+`;
+
+exports[`Should render correctly: Loading... 1`] = `
+<Fragment>
+ <CreateProjectPageHeader
+ title={
+ <span
+ className="text-middle"
+ >
+ <img
+ alt=""
+ className="spacer-right"
+ height="24"
+ src="/images/alm/bitbucket.svg"
+ />
+ onboarding.create_project.bitbucketcloud.title
+ </span>
+ }
+ />
+ <i
+ className="spinner"
+ />
+</Fragment>
+`;
+
+exports[`Should render correctly: Need App password 1`] = `
+<Fragment>
+ <CreateProjectPageHeader
+ title={
+ <span
+ className="text-middle"
+ >
+ <img
+ alt=""
+ className="spacer-right"
+ height="24"
+ src="/images/alm/bitbucket.svg"
+ />
+ onboarding.create_project.bitbucketcloud.title
+ </span>
+ }
+ />
+ <PersonalAccessTokenForm
+ almSetting={
+ Object {
+ "alm": "bitbucketcloud",
+ "key": "key",
+ }
+ }
+ onPersonalAccessTokenCreated={[MockFunction]}
+ resetPat={false}
+ />
+</Fragment>
+`;
+
+exports[`Should render correctly: Wrong config 1`] = `
+<Fragment>
+ <CreateProjectPageHeader
+ title={
+ <span
+ className="text-middle"
+ >
+ <img
+ alt=""
+ className="spacer-right"
+ height="24"
+ src="/images/alm/bitbucket.svg"
+ />
+ onboarding.create_project.bitbucketcloud.title
+ </span>
+ }
+ />
+ <WrongBindingCountAlert
+ alm="bitbucketcloud"
+ canAdmin={false}
+ />
+</Fragment>
+`;
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectCreate-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectCreate-test.tsx.snap
index 31df0b466dc..4b190649475 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectCreate-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectCreate-test.tsx.snap
@@ -10,14 +10,66 @@ exports[`should render correctly 1`] = `
}
canAdmin={false}
importing={false}
- loading={true}
+ loading={false}
onImportRepository={[Function]}
- onPersonalAccessTokenCreate={[Function]}
+ onPersonalAccessTokenCreated={[Function]}
onProjectCreate={[MockFunction]}
onSearch={[Function]}
onSelectRepository={[Function]}
+ resetPat={false}
+ searching={false}
+ showPersonalAccessTokenForm={true}
+/>
+`;
+
+exports[`should render correctly: No repository 1`] = `
+<BitbucketProjectCreateRenderer
+ bitbucketSetting={
+ Object {
+ "alm": "bitbucket",
+ "key": "foo",
+ }
+ }
+ canAdmin={false}
+ importing={false}
+ loading={false}
+ onImportRepository={[Function]}
+ onPersonalAccessTokenCreated={[Function]}
+ onProjectCreate={[MockFunction]}
+ onSearch={[Function]}
+ onSelectRepository={[Function]}
+ projects={
+ Array [
+ Object {
+ "id": 1,
+ "key": "project1",
+ "name": "Project 1",
+ },
+ Object {
+ "id": 2,
+ "key": "project2",
+ "name": "Project",
+ },
+ ]
+ }
+ resetPat={false}
+ searching={false}
+ showPersonalAccessTokenForm={false}
+/>
+`;
+
+exports[`should render correctly: No setting 1`] = `
+<BitbucketProjectCreateRenderer
+ canAdmin={false}
+ importing={false}
+ loading={false}
+ onImportRepository={[Function]}
+ onPersonalAccessTokenCreated={[Function]}
+ onProjectCreate={[MockFunction]}
+ onSearch={[Function]}
+ onSelectRepository={[Function]}
+ resetPat={false}
searching={false}
showPersonalAccessTokenForm={true}
- tokenValidationFailed={false}
/>
`;
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectCreateRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectCreateRenderer-test.tsx.snap
index aeb87f01dec..c499c0accea 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectCreateRenderer-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/BitbucketProjectCreateRenderer-test.tsx.snap
@@ -281,8 +281,8 @@ exports[`should render correctly: pat form 1`] = `
"key": "key",
}
}
- onPersonalAccessTokenCreate={[MockFunction]}
- validationFailed={false}
+ onPersonalAccessTokenCreated={[MockFunction]}
+ resetPat={false}
/>
</Fragment>
`;
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap
index 99f3a717d69..2d14d0bc3c0 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap
@@ -115,6 +115,19 @@ exports[`should render correctly if the BBS method is selected 1`] = `
}
}
onProjectCreate={[Function]}
+ router={
+ Object {
+ "createHref": [MockFunction],
+ "createPath": [MockFunction],
+ "go": [MockFunction],
+ "goBack": [MockFunction],
+ "goForward": [MockFunction],
+ "isActive": [MockFunction],
+ "push": [MockFunction],
+ "replace": [MockFunction],
+ "setRouteLeaveHook": [MockFunction],
+ }
+ }
/>
</div>
</Fragment>
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
index 8b9520b04ec..577f859ef13 100644
--- 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
@@ -3,11 +3,11 @@
exports[`should render correctly 1`] = `
<GitlabProjectCreateRenderer
canAdmin={false}
- loading={true}
+ loading={false}
loadingMore={false}
onImport={[Function]}
onLoadMore={[Function]}
- onPersonalAccessTokenCreate={[Function]}
+ onPersonalAccessTokenCreated={[Function]}
onSearch={[Function]}
projectsPaging={
Object {
@@ -16,6 +16,7 @@ exports[`should render correctly 1`] = `
"total": 0,
}
}
+ resetPat={false}
searchQuery=""
searching={false}
settings={
@@ -25,6 +26,5 @@ exports[`should render correctly 1`] = `
}
}
showPersonalAccessTokenForm={true}
- submittingToken={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
index 72c564b381a..68e71143079 100644
--- 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
@@ -95,41 +95,8 @@ exports[`should render correctly: pat form 1`] = `
"key": "key",
}
}
- onPersonalAccessTokenCreate={[MockFunction]}
- submitting={false}
- validationFailed={false}
- />
-</Fragment>
-`;
-
-exports[`should render correctly: pat validation error 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}
- validationErrorMessage="error"
- validationFailed={true}
+ onPersonalAccessTokenCreated={[MockFunction]}
+ resetPat={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
index c2d3a437231..a8ea7ab0c05 100644
--- 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
@@ -19,7 +19,8 @@ exports[`should render correctly: bitbucket 1`] = `
onboarding.create_project.pat_form.help.bitbucket
</p>
<ValidationInput
- id="personal_access_token"
+ error="onboarding.create_project.pat_incorrect.bitbucket"
+ id="personal_access_token_validation"
isInvalid={false}
isValid={false}
label="onboarding.create_project.enter_pat"
@@ -27,23 +28,31 @@ exports[`should render correctly: bitbucket 1`] = `
>
<input
autoFocus={true}
- className="input-super-large"
+ className="input-super-large is-invalid"
id="personal_access_token"
minLength={1}
- name="personal_access_token"
onChange={[Function]}
type="text"
+ value=""
+ />
+ </ValidationInput>
+ <ValidationInput
+ error="onboarding.create_project.pat_incorrect.bitbucket"
+ id="personal_access_token_submit"
+ isInvalid={true}
+ isValid={false}
+ label={null}
+ >
+ <SubmitButton
+ disabled={true}
+ >
+ save
+ </SubmitButton>
+ <DeferredSpinner
+ className="spacer-left"
+ loading={false}
/>
</ValidationInput>
- <SubmitButton
- disabled={true}
- >
- save
- </SubmitButton>
- <DeferredSpinner
- className="spacer-left"
- loading={false}
- />
</form>
<Alert
className="big-spacer-left width-50"
@@ -120,7 +129,7 @@ exports[`should render correctly: bitbucket 1`] = `
</div>
`;
-exports[`should render correctly: gitlab 1`] = `
+exports[`should render correctly: bitbucket cloud 1`] = `
<div
className="display-flex-start"
>
@@ -131,39 +140,65 @@ exports[`should render correctly: gitlab 1`] = `
<h2
className="big"
>
- onboarding.create_project.pat_form.title.gitlab
+ onboarding.create_project.pat_form.title.bitbucketcloud
</h2>
<p
className="big-spacer-top big-spacer-bottom"
>
- onboarding.create_project.pat_form.help.gitlab
+ onboarding.create_project.pat_form.help.bitbucketcloud
</p>
<ValidationInput
- id="personal_access_token"
+ id="enter_username_validation"
isInvalid={false}
isValid={false}
- label="onboarding.create_project.enter_pat"
+ label="onboarding.create_project.enter_username"
required={true}
>
<input
autoFocus={true}
- className="input-super-large"
+ className="input-super-large is-invalid"
+ id="username"
+ minLength={1}
+ name="username"
+ onChange={[Function]}
+ type="text"
+ />
+ </ValidationInput>
+ <ValidationInput
+ error="onboarding.create_project.pat_incorrect.bitbucketcloud"
+ id="personal_access_token_validation"
+ isInvalid={false}
+ isValid={false}
+ label="onboarding.create_project.enter_pat.bitbucketcloud"
+ required={true}
+ >
+ <input
+ autoFocus={false}
+ className="input-super-large is-invalid"
id="personal_access_token"
minLength={1}
- name="personal_access_token"
onChange={[Function]}
type="text"
+ value=""
+ />
+ </ValidationInput>
+ <ValidationInput
+ error="onboarding.create_project.pat_incorrect.bitbucketcloud"
+ id="personal_access_token_submit"
+ isInvalid={true}
+ isValid={false}
+ label={null}
+ >
+ <SubmitButton
+ disabled={true}
+ >
+ save
+ </SubmitButton>
+ <DeferredSpinner
+ className="spacer-left"
+ loading={false}
/>
</ValidationInput>
- <SubmitButton
- disabled={true}
- >
- save
- </SubmitButton>
- <DeferredSpinner
- className="spacer-left"
- loading={false}
- />
</form>
<Alert
className="big-spacer-left width-50"
@@ -171,17 +206,17 @@ exports[`should render correctly: gitlab 1`] = `
variant="info"
>
<h3>
- onboarding.create_project.pat_help.title
+ onboarding.create_project.pat_help.bitbucketcloud.title
</h3>
<p
className="big-spacer-top big-spacer-bottom"
>
<FormattedMessage
- defaultMessage="onboarding.create_project.pat_help.instructions"
+ defaultMessage="onboarding.create_project.pat_help.bitbucketcloud.instructions"
id="onboarding.create_project.pat_help.instructions"
values={
Object {
- "alm": "onboarding.alm.gitlab",
+ "alm": "onboarding.alm.bitbucketcloud",
}
}
/>
@@ -193,35 +228,41 @@ exports[`should render correctly: gitlab 1`] = `
alt=""
className="spacer-right"
height="16"
- src="/images/alm/gitlab.svg"
+ src="/images/alm/bitbucket.svg"
/>
<a
- href="https://gitlab.com/profile/personal_access_tokens"
+ href="https://bitbucket.org/account/settings/app-passwords/new"
rel="noopener noreferrer"
target="_blank"
>
- onboarding.create_project.pat_help.link
+ onboarding.create_project.pat_help.bitbucketcloud.link
</a>
</div>
<p
className="big-spacer-top big-spacer-bottom"
>
- onboarding.create_project.pat_help.instructions2.gitlab
+ onboarding.create_project.pat_help.instructions2.bitbucketcloud
</p>
<ul>
- <li
- className="spacer-bottom"
- >
- <strong>
- onboarding.create_project.pat_help.gitlab.read_api_permission
- </strong>
+ <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 with non-standard api path 1`] = `
+exports[`should render correctly: gitlab 1`] = `
<div
className="display-flex-start"
>
@@ -240,7 +281,8 @@ exports[`should render correctly: gitlab with non-standard api path 1`] = `
onboarding.create_project.pat_form.help.gitlab
</p>
<ValidationInput
- id="personal_access_token"
+ error="onboarding.create_project.pat_incorrect.gitlab"
+ id="personal_access_token_validation"
isInvalid={false}
isValid={false}
label="onboarding.create_project.enter_pat"
@@ -248,23 +290,31 @@ exports[`should render correctly: gitlab with non-standard api path 1`] = `
>
<input
autoFocus={true}
- className="input-super-large"
+ className="input-super-large is-invalid"
id="personal_access_token"
minLength={1}
- name="personal_access_token"
onChange={[Function]}
type="text"
+ value=""
+ />
+ </ValidationInput>
+ <ValidationInput
+ error="onboarding.create_project.pat_incorrect.gitlab"
+ id="personal_access_token_submit"
+ isInvalid={true}
+ isValid={false}
+ label={null}
+ >
+ <SubmitButton
+ disabled={true}
+ >
+ save
+ </SubmitButton>
+ <DeferredSpinner
+ className="spacer-left"
+ loading={false}
/>
</ValidationInput>
- <SubmitButton
- disabled={true}
- >
- save
- </SubmitButton>
- <DeferredSpinner
- className="spacer-left"
- loading={false}
- />
</form>
<Alert
className="big-spacer-left width-50"
@@ -297,7 +347,7 @@ exports[`should render correctly: gitlab with non-standard api path 1`] = `
src="/images/alm/gitlab.svg"
/>
<a
- href="https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#creating-a-personal-access-token"
+ href="https://gitlab.com/profile/personal_access_tokens"
rel="noopener noreferrer"
target="_blank"
>
@@ -322,7 +372,7 @@ exports[`should render correctly: gitlab with non-standard api path 1`] = `
</div>
`;
-exports[`should render correctly: submitting 1`] = `
+exports[`should render correctly: gitlab with non-standard api path 1`] = `
<div
className="display-flex-start"
>
@@ -333,15 +383,16 @@ exports[`should render correctly: submitting 1`] = `
<h2
className="big"
>
- onboarding.create_project.pat_form.title.bitbucket
+ onboarding.create_project.pat_form.title.gitlab
</h2>
<p
className="big-spacer-top big-spacer-bottom"
>
- onboarding.create_project.pat_form.help.bitbucket
+ onboarding.create_project.pat_form.help.gitlab
</p>
<ValidationInput
- id="personal_access_token"
+ error="onboarding.create_project.pat_incorrect.gitlab"
+ id="personal_access_token_validation"
isInvalid={false}
isValid={false}
label="onboarding.create_project.enter_pat"
@@ -349,23 +400,31 @@ exports[`should render correctly: submitting 1`] = `
>
<input
autoFocus={true}
- className="input-super-large"
+ className="input-super-large is-invalid"
id="personal_access_token"
minLength={1}
- name="personal_access_token"
onChange={[Function]}
type="text"
+ value=""
+ />
+ </ValidationInput>
+ <ValidationInput
+ error="onboarding.create_project.pat_incorrect.gitlab"
+ id="personal_access_token_submit"
+ isInvalid={true}
+ isValid={false}
+ label={null}
+ >
+ <SubmitButton
+ disabled={true}
+ >
+ save
+ </SubmitButton>
+ <DeferredSpinner
+ className="spacer-left"
+ loading={false}
/>
</ValidationInput>
- <SubmitButton
- disabled={true}
- >
- save
- </SubmitButton>
- <DeferredSpinner
- className="spacer-left"
- loading={true}
- />
</form>
<Alert
className="big-spacer-left width-50"
@@ -383,7 +442,7 @@ exports[`should render correctly: submitting 1`] = `
id="onboarding.create_project.pat_help.instructions"
values={
Object {
- "alm": "onboarding.alm.bitbucket",
+ "alm": "onboarding.alm.gitlab",
}
}
/>
@@ -395,10 +454,10 @@ exports[`should render correctly: submitting 1`] = `
alt=""
className="spacer-right"
height="16"
- src="/images/alm/bitbucket.svg"
+ src="/images/alm/gitlab.svg"
/>
<a
- href="http://www.example.com/plugins/servlet/access-tokens/add"
+ href="https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#creating-a-personal-access-token"
rel="noopener noreferrer"
target="_blank"
>
@@ -408,41 +467,29 @@ exports[`should render correctly: submitting 1`] = `
<p
className="big-spacer-top big-spacer-bottom"
>
- onboarding.create_project.pat_help.instructions2.bitbucket
+ onboarding.create_project.pat_help.instructions2.gitlab
</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
+ className="spacer-bottom"
+ >
+ <strong>
+ onboarding.create_project.pat_help.gitlab.read_api_permission
+ </strong>
</li>
</ul>
</Alert>
</div>
`;
-exports[`should render correctly: validation failed 1`] = `
+exports[`should render correctly: no token needed 1`] = `
+<DeferredSpinner
+ className="spacer-left"
+ loading={true}
+/>
+`;
+
+exports[`should show error when issue: issue submitting token 1`] = `
<div
className="display-flex-start"
>
@@ -453,161 +500,66 @@ exports[`should render correctly: validation failed 1`] = `
<h2
className="big"
>
- onboarding.create_project.pat_form.title.bitbucket
+ onboarding.create_project.pat_form.title.bitbucketcloud
</h2>
<p
className="big-spacer-top big-spacer-bottom"
>
- onboarding.create_project.pat_form.help.bitbucket
+ onboarding.create_project.pat_form.help.bitbucketcloud
</p>
<ValidationInput
- error="onboarding.create_project.pat_incorrect.bitbucket"
- id="personal_access_token"
- isInvalid={true}
+ id="enter_username_validation"
+ isInvalid={false}
isValid={false}
- label="onboarding.create_project.enter_pat"
+ label="onboarding.create_project.enter_username"
required={true}
>
<input
autoFocus={true}
className="input-super-large is-invalid"
- id="personal_access_token"
+ id="username"
minLength={1}
- name="personal_access_token"
+ name="username"
onChange={[Function]}
type="text"
+ value="username"
/>
</ValidationInput>
- <SubmitButton
- disabled={true}
- >
- save
- </SubmitButton>
- <DeferredSpinner
- className="spacer-left"
- loading={false}
- />
- </form>
- <Alert
- className="big-spacer-left width-50"
- 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, custom error message 1`] = `
-<div
- className="display-flex-start"
->
- <form
- className="width-50"
- 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="error"
- id="personal_access_token"
- isInvalid={true}
+ error="default_error_message"
+ id="personal_access_token_validation"
+ isInvalid={false}
isValid={false}
- label="onboarding.create_project.enter_pat"
+ label="onboarding.create_project.enter_pat.bitbucketcloud"
required={true}
>
<input
- autoFocus={true}
+ autoFocus={false}
className="input-super-large is-invalid"
id="personal_access_token"
minLength={1}
- name="personal_access_token"
onChange={[Function]}
type="text"
+ value="token"
+ />
+ </ValidationInput>
+ <ValidationInput
+ error="default_error_message"
+ id="personal_access_token_submit"
+ isInvalid={true}
+ isValid={false}
+ label={null}
+ >
+ <SubmitButton
+ disabled={true}
+ >
+ save
+ </SubmitButton>
+ <DeferredSpinner
+ className="spacer-left"
+ loading={false}
/>
</ValidationInput>
- <SubmitButton
- disabled={true}
- >
- save
- </SubmitButton>
- <DeferredSpinner
- className="spacer-left"
- loading={false}
- />
</form>
<Alert
className="big-spacer-left width-50"
@@ -615,17 +567,17 @@ exports[`should render correctly: validation failed, custom error message 1`] =
variant="info"
>
<h3>
- onboarding.create_project.pat_help.title
+ onboarding.create_project.pat_help.bitbucketcloud.title
</h3>
<p
className="big-spacer-top big-spacer-bottom"
>
<FormattedMessage
- defaultMessage="onboarding.create_project.pat_help.instructions"
+ defaultMessage="onboarding.create_project.pat_help.bitbucketcloud.instructions"
id="onboarding.create_project.pat_help.instructions"
values={
Object {
- "alm": "onboarding.alm.bitbucket",
+ "alm": "onboarding.alm.bitbucketcloud",
}
}
/>
@@ -640,34 +592,21 @@ exports[`should render correctly: validation failed, custom error message 1`] =
src="/images/alm/bitbucket.svg"
/>
<a
- href="http://www.example.com/plugins/servlet/access-tokens/add"
+ href="https://bitbucket.org/account/settings/app-passwords/new"
rel="noopener noreferrer"
target="_blank"
>
- onboarding.create_project.pat_help.link
+ onboarding.create_project.pat_help.bitbucketcloud.link
</a>
</div>
<p
className="big-spacer-top big-spacer-bottom"
>
- onboarding.create_project.pat_help.instructions2.bitbucket
+ onboarding.create_project.pat_help.instructions2.bitbucketcloud
</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={
diff --git a/server/sonar-web/src/main/js/apps/create/project/types.ts b/server/sonar-web/src/main/js/apps/create/project/types.ts
index 9eb31ca5f37..a00a7966525 100644
--- a/server/sonar-web/src/main/js/apps/create/project/types.ts
+++ b/server/sonar-web/src/main/js/apps/create/project/types.ts
@@ -21,6 +21,7 @@ export enum CreateProjectModes {
Manual = 'manual',
AzureDevOps = 'azure',
BitbucketServer = 'bitbucket',
+ BitbucketCloud = 'bitbucketcloud',
GitHub = 'github',
GitLab = 'gitlab'
}
diff --git a/server/sonar-web/src/main/js/helpers/mocks/alm-settings.ts b/server/sonar-web/src/main/js/helpers/mocks/alm-settings.ts
index 93aa836d8ee..ffd3989095e 100644
--- a/server/sonar-web/src/main/js/helpers/mocks/alm-settings.ts
+++ b/server/sonar-web/src/main/js/helpers/mocks/alm-settings.ts
@@ -44,6 +44,16 @@ export function mockAlmSettingsInstance(
};
}
+export function mockBitbucketCloudAlmSettingsInstance(
+ overrides: Partial<AlmSettingsInstance> = {}
+): AlmSettingsInstance {
+ return {
+ alm: AlmKeys.BitbucketCloud,
+ key: 'key',
+ ...overrides
+ };
+}
+
export function mockAzureBindingDefinition(
overrides: Partial<AzureBindingDefinition> = {}
): AzureBindingDefinition {