aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web
diff options
context:
space:
mode:
authorJeremy Davis <jeremy.davis@sonarsource.com>2020-11-09 17:16:09 +0100
committersonartech <sonartech@sonarsource.com>2020-11-25 20:06:26 +0000
commit753af65e1a768d7df755d0d6e22c5befba558790 (patch)
tree12747ce44d1b5167d7966ed681713aaceef40f61 /server/sonar-web
parent347294d72cb62a0420ebd0aee2d2433f829300ed (diff)
downloadsonarqube-753af65e1a768d7df755d0d6e22c5befba558790.tar.gz
sonarqube-753af65e1a768d7df755d0d6e22c5befba558790.zip
SONAR-14057 Add PAT form for azure onboarding
Diffstat (limited to 'server/sonar-web')
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx8
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/AzurePersonalAccessTokenForm.tsx134
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/AzureProjectCreate.tsx143
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/AzureProjectCreateRenderer.tsx87
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/AzureProjectsList.tsx31
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/CreateProjectModeSelection.tsx1
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx32
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/AzurePersonalAccessTokenForm-test.tsx72
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectCreate-test.tsx92
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectCreateRenderer-test.tsx49
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectModeSelection-test.tsx17
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx8
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzurePersonalAccessTokenForm-test.tsx.snap229
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreate-test.tsx.snap17
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreateRenderer-test.tsx.snap104
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectModeSelection-test.tsx.snap85
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap38
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/types.ts1
18 files changed, 1140 insertions, 8 deletions
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx
index 7226731ae5c..1c72c859fce 100644
--- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx
@@ -48,10 +48,10 @@ interface State {
/*
* ALMs for which the import feature has been implemented
*/
-const IMPORT_COMPATIBLE_ALMS = [AlmKeys.Bitbucket, AlmKeys.GitHub, AlmKeys.GitLab];
+const IMPORT_COMPATIBLE_ALMS = [AlmKeys.Azure, AlmKeys.Bitbucket, AlmKeys.GitHub, AlmKeys.GitLab];
const almSettingsValidators = {
- [AlmKeys.Azure]: (_: AlmSettingsInstance) => true,
+ [AlmKeys.Azure]: (settings: AlmSettingsInstance) => !!settings.url,
[AlmKeys.Bitbucket]: (_: AlmSettingsInstance) => true,
[AlmKeys.GitHub]: (_: AlmSettingsInstance) => true,
[AlmKeys.GitLab]: (settings: AlmSettingsInstance) => !!settings.url
@@ -73,7 +73,9 @@ export class GlobalNavPlus extends React.PureComponent<Props, State> {
this.setState({ governanceReady: true });
}
},
- () => {}
+ () => {
+ /* error handled globally */
+ }
);
}
}
diff --git a/server/sonar-web/src/main/js/apps/create/project/AzurePersonalAccessTokenForm.tsx b/server/sonar-web/src/main/js/apps/create/project/AzurePersonalAccessTokenForm.tsx
new file mode 100644
index 00000000000..203d1527bd0
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/project/AzurePersonalAccessTokenForm.tsx
@@ -0,0 +1,134 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
+import { SubmitButton } from 'sonar-ui-common/components/controls/buttons';
+import ValidationInput from 'sonar-ui-common/components/controls/ValidationInput';
+import DetachIcon from 'sonar-ui-common/components/icons/DetachIcon';
+import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import { AlmSettingsInstance } from '../../../types/alm-settings';
+
+export interface AzurePersonalAccessTokenFormProps {
+ almSetting: AlmSettingsInstance;
+ onPersonalAccessTokenCreate: (token: string) => void;
+ submitting?: boolean;
+ validationFailed: boolean;
+}
+
+function getAzurePatUrl(url: string) {
+ return `${url.replace(/\/$/, '')}/_usersSettings/tokens`;
+}
+
+export default function AzurePersonalAccessTokenForm(props: AzurePersonalAccessTokenFormProps) {
+ const {
+ almSetting: { alm, url },
+ submitting = false,
+ validationFailed
+ } = props;
+
+ const [touched, setTouched] = React.useState(false);
+ React.useEffect(() => {
+ setTouched(false);
+ }, [submitting]);
+
+ const [token, setToken] = React.useState('');
+
+ const isInvalid = (validationFailed && !touched) || (touched && !token);
+
+ let errorMessage;
+ if (!token) {
+ errorMessage = translate('onboarding.create_project.pat_form.pat_required');
+ } else if (isInvalid) {
+ errorMessage = translate('onboarding.create_project.pat_incorrect', alm);
+ }
+
+ return (
+ <div className="boxed-group abs-width-600">
+ <div className="boxed-group-inner">
+ <h2>{translate('onboarding.create_project.pat_form.title', alm)}</h2>
+
+ <div className="big-spacer-top big-spacer-bottom">
+ <FormattedMessage
+ id="onboarding.create_project.pat_help.instructions"
+ defaultMessage={translate('onboarding.create_project.pat_help.instructions', alm)}
+ values={{
+ link: url ? (
+ <a
+ className="link-with-icon"
+ href={getAzurePatUrl(url)}
+ rel="noopener noreferrer"
+ target="_blank">
+ <DetachIcon className="little-spacer-right" />
+ <span>
+ {translate('onboarding.create_project.pat_help.instructions.link', alm)}
+ </span>
+ </a>
+ ) : (
+ translate('onboarding.create_project.pat_help.instructions.link', alm)
+ ),
+ scope: (
+ <strong>
+ <em>Code (Read & Write)</em>
+ </strong>
+ )
+ }}
+ />
+ </div>
+
+ <form
+ onSubmit={(e: React.SyntheticEvent<HTMLFormElement>) => {
+ e.preventDefault();
+ props.onPersonalAccessTokenCreate(token);
+ }}>
+ <ValidationInput
+ error={errorMessage}
+ id="personal_access_token"
+ isInvalid={isInvalid}
+ isValid={false}
+ label={translate('onboarding.create_project.enter_pat')}
+ required={true}>
+ <input
+ autoFocus={true}
+ className={classNames('width-100 little-spacer-bottom', {
+ 'is-invalid': isInvalid
+ })}
+ id="personal_access_token"
+ minLength={1}
+ name="personal_access_token"
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
+ setToken(e.target.value);
+ setTouched(true);
+ }}
+ type="text"
+ value={token}
+ />
+ </ValidationInput>
+
+ <SubmitButton disabled={isInvalid || submitting || !touched}>
+ {translate('onboarding.create_project.pat_form.list_repositories')}
+ </SubmitButton>
+ <DeferredSpinner className="spacer-left" loading={submitting} />
+ </form>
+ </div>
+ </div>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/create/project/AzureProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/AzureProjectCreate.tsx
new file mode 100644
index 00000000000..292abc2e548
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/project/AzureProjectCreate.tsx
@@ -0,0 +1,143 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { WithRouterProps } from 'react-router';
+import {
+ checkPersonalAccessTokenIsValid,
+ setAlmPersonalAccessToken
+} from '../../../api/alm-integrations';
+import { AlmSettingsInstance } from '../../../types/alm-settings';
+import AzureCreateProjectRenderer from './AzureProjectCreateRenderer';
+
+interface Props extends Pick<WithRouterProps, 'location'> {
+ canAdmin: boolean;
+ loadingBindings: boolean;
+ onProjectCreate: (projectKeys: string[]) => void;
+ settings: AlmSettingsInstance[];
+}
+
+interface State {
+ loading: boolean;
+ patIsValid?: boolean;
+ settings?: AlmSettingsInstance;
+ submittingToken?: boolean;
+ tokenValidationFailed: boolean;
+}
+
+export default class AzureProjectCreate 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,
+ tokenValidationFailed: false
+ };
+ }
+
+ componentDidMount() {
+ this.mounted = true;
+ this.fetchInitialData();
+ }
+
+ componentDidUpdate(prevProps: Props) {
+ if (prevProps.settings.length === 0 && this.props.settings.length > 0) {
+ this.setState(
+ { settings: this.props.settings.length === 1 ? this.props.settings[0] : undefined },
+ () => this.fetchInitialData()
+ );
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ fetchInitialData = async () => {
+ this.setState({ loading: true });
+
+ const patIsValid = await this.checkPersonalAccessToken().catch(() => false);
+
+ if (this.mounted) {
+ this.setState({
+ patIsValid,
+ loading: false
+ });
+ }
+ };
+
+ checkPersonalAccessToken = () => {
+ const { settings } = this.state;
+
+ if (!settings) {
+ return Promise.resolve(false);
+ }
+
+ return checkPersonalAccessTokenIsValid(settings.key);
+ };
+
+ handlePersonalAccessTokenCreate = async (token: string) => {
+ const { settings } = this.state;
+
+ if (!settings || token.length < 1) {
+ return;
+ }
+
+ this.setState({ submittingToken: true, tokenValidationFailed: false });
+
+ try {
+ await setAlmPersonalAccessToken(settings.key, token);
+ const patIsValid = await this.checkPersonalAccessToken();
+
+ if (this.mounted) {
+ this.setState({ submittingToken: false, patIsValid, tokenValidationFailed: !patIsValid });
+
+ if (patIsValid) {
+ this.cleanUrl();
+ await this.fetchInitialData();
+ }
+ }
+ } catch (e) {
+ if (this.mounted) {
+ this.setState({ submittingToken: false });
+ }
+ }
+ };
+
+ render() {
+ const { canAdmin, loadingBindings, location } = this.props;
+ const { loading, patIsValid, settings, submittingToken, tokenValidationFailed } = this.state;
+
+ return (
+ <AzureCreateProjectRenderer
+ canAdmin={canAdmin}
+ loading={loading || loadingBindings}
+ onPersonalAccessTokenCreate={this.handlePersonalAccessTokenCreate}
+ settings={settings}
+ showPersonalAccessTokenForm={!patIsValid || Boolean(location.query.resetPat)}
+ submittingToken={submittingToken}
+ tokenValidationFailed={tokenValidationFailed}
+ />
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/create/project/AzureProjectCreateRenderer.tsx b/server/sonar-web/src/main/js/apps/create/project/AzureProjectCreateRenderer.tsx
new file mode 100644
index 00000000000..2d43a8bb26a
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/project/AzureProjectCreateRenderer.tsx
@@ -0,0 +1,87 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import { getBaseUrl } from 'sonar-ui-common/helpers/urls';
+import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings';
+import AzurePersonalAccessTokenForm from './AzurePersonalAccessTokenForm';
+import AzureProjectsList from './AzureProjectsList';
+import CreateProjectPageHeader from './CreateProjectPageHeader';
+import WrongBindingCountAlert from './WrongBindingCountAlert';
+
+export interface AzureProjectCreateRendererProps {
+ canAdmin?: boolean;
+ loading: boolean;
+ onPersonalAccessTokenCreate: (token: string) => void;
+ settings?: AlmSettingsInstance;
+ showPersonalAccessTokenForm?: boolean;
+ submittingToken?: boolean;
+ tokenValidationFailed: boolean;
+}
+
+export default function AzureProjectCreateRenderer(props: AzureProjectCreateRendererProps) {
+ const {
+ canAdmin,
+ loading,
+ showPersonalAccessTokenForm,
+ settings,
+ submittingToken,
+ tokenValidationFailed
+ } = props;
+
+ return (
+ <>
+ <CreateProjectPageHeader
+ title={
+ <span className="text-middle">
+ <img
+ alt="" // Should be ignored by screen readers
+ className="spacer-right"
+ height="24"
+ src={`${getBaseUrl()}/images/alm/azure.svg`}
+ />
+ {translate('onboarding.create_project.azure.title')}
+ </span>
+ }
+ />
+
+ {loading && <i className="spinner" />}
+
+ {!loading && !settings && (
+ <WrongBindingCountAlert alm={AlmKeys.Azure} canAdmin={!!canAdmin} />
+ )}
+
+ {!loading &&
+ settings &&
+ (showPersonalAccessTokenForm ? (
+ <div className="display-flex-justify-center">
+ <AzurePersonalAccessTokenForm
+ almSetting={settings}
+ onPersonalAccessTokenCreate={props.onPersonalAccessTokenCreate}
+ submitting={submittingToken}
+ validationFailed={tokenValidationFailed}
+ />
+ </div>
+ ) : (
+ <AzureProjectsList />
+ ))}
+ </>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/create/project/AzureProjectsList.tsx b/server/sonar-web/src/main/js/apps/create/project/AzureProjectsList.tsx
new file mode 100644
index 00000000000..c6f34ede827
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/project/AzureProjectsList.tsx
@@ -0,0 +1,31 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { Alert } from 'sonar-ui-common/components/ui/Alert';
+
+export interface AzureProjectsListProps {}
+
+export default function AzureProjectsList(_props: AzureProjectsListProps) {
+ return (
+ <div>
+ <Alert variant="warning">Coming soon!</Alert>
+ </div>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/create/project/CreateProjectModeSelection.tsx b/server/sonar-web/src/main/js/apps/create/project/CreateProjectModeSelection.tsx
index 4508e2762c2..6524aad672a 100644
--- a/server/sonar-web/src/main/js/apps/create/project/CreateProjectModeSelection.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/CreateProjectModeSelection.tsx
@@ -112,6 +112,7 @@ export default function CreateProjectModeSelection(props: CreateProjectModeSelec
</div>
</button>
+ {renderAlmOption(props, AlmKeys.Azure, CreateProjectModes.AzureDevOps)}
{renderAlmOption(props, AlmKeys.Bitbucket, CreateProjectModes.BitbucketServer)}
{renderAlmOption(props, AlmKeys.GitHub, CreateProjectModes.GitHub)}
{renderAlmOption(props, AlmKeys.GitLab, CreateProjectModes.GitLab)}
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 3976e1acb93..a2cbadb2912 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
@@ -27,6 +27,7 @@ import { whenLoggedIn } from '../../../components/hoc/whenLoggedIn';
import { withAppState } from '../../../components/hoc/withAppState';
import { getProjectUrl } from '../../../helpers/urls';
import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings';
+import AzureProjectCreate from './AzureProjectCreate';
import BitbucketProjectCreate from './BitbucketProjectCreate';
import CreateProjectModeSelection from './CreateProjectModeSelection';
import GitHubProjectCreate from './GitHubProjectCreate';
@@ -41,6 +42,7 @@ interface Props extends Pick<WithRouterProps, 'router' | 'location'> {
}
interface State {
+ azureSettings: AlmSettingsInstance[];
bitbucketSettings: AlmSettingsInstance[];
githubSettings: AlmSettingsInstance[];
gitlabSettings: AlmSettingsInstance[];
@@ -49,7 +51,13 @@ interface State {
export class CreateProjectPage extends React.PureComponent<Props, State> {
mounted = false;
- state: State = { bitbucketSettings: [], githubSettings: [], gitlabSettings: [], loading: true };
+ state: State = {
+ azureSettings: [],
+ bitbucketSettings: [],
+ githubSettings: [],
+ gitlabSettings: [],
+ loading: true
+ };
componentDidMount() {
const {
@@ -71,6 +79,7 @@ export class CreateProjectPage extends React.PureComponent<Props, State> {
.then(almSettings => {
if (this.mounted) {
this.setState({
+ azureSettings: almSettings.filter(s => s.alm === AlmKeys.Azure),
bitbucketSettings: almSettings.filter(s => s.alm === AlmKeys.Bitbucket),
githubSettings: almSettings.filter(s => s.alm === AlmKeys.GitHub),
gitlabSettings: almSettings.filter(s => s.alm === AlmKeys.GitLab),
@@ -105,9 +114,26 @@ export class CreateProjectPage extends React.PureComponent<Props, State> {
location,
router
} = this.props;
- const { bitbucketSettings, githubSettings, gitlabSettings, loading } = this.state;
+ const {
+ azureSettings,
+ bitbucketSettings,
+ githubSettings,
+ gitlabSettings,
+ loading
+ } = this.state;
switch (mode) {
+ case CreateProjectModes.AzureDevOps: {
+ return (
+ <AzureProjectCreate
+ canAdmin={!!canAdmin}
+ loadingBindings={loading}
+ location={location}
+ onProjectCreate={this.handleProjectCreate}
+ settings={azureSettings}
+ />
+ );
+ }
case CreateProjectModes.BitbucketServer: {
return (
<BitbucketProjectCreate
@@ -148,7 +174,7 @@ export class CreateProjectPage extends React.PureComponent<Props, State> {
}
default: {
const almCounts = {
- [AlmKeys.Azure]: 0,
+ [AlmKeys.Azure]: azureSettings.length,
[AlmKeys.Bitbucket]: bitbucketSettings.length,
[AlmKeys.GitHub]: githubSettings.length,
[AlmKeys.GitLab]: gitlabSettings.length
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/AzurePersonalAccessTokenForm-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/AzurePersonalAccessTokenForm-test.tsx
new file mode 100644
index 00000000000..d83088fd07c
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/AzurePersonalAccessTokenForm-test.tsx
@@ -0,0 +1,72 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { SubmitButton } from 'sonar-ui-common/components/controls/buttons';
+import { change, submit } from 'sonar-ui-common/helpers/testUtils';
+import { mockAlmSettingsInstance } from '../../../../helpers/mocks/alm-settings';
+import { AlmKeys } from '../../../../types/alm-settings';
+import AzurePersonalAccessTokenForm, {
+ AzurePersonalAccessTokenFormProps
+} from '../AzurePersonalAccessTokenForm';
+
+it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot('default');
+ expect(shallowRender({ submitting: true })).toMatchSnapshot('submitting');
+ expect(shallowRender({ validationFailed: true })).toMatchSnapshot('validation failed');
+});
+
+it('should correctly handle form interactions', () => {
+ const onPersonalAccessTokenCreate = jest.fn();
+ const wrapper = shallowRender({ onPersonalAccessTokenCreate });
+
+ // Submit button disabled by default.
+ expect(wrapper.find(SubmitButton).prop('disabled')).toBe(true);
+
+ // Submit button enabled if there's a value.
+ change(wrapper.find('input'), 'token');
+ expect(wrapper.find(SubmitButton).prop('disabled')).toBe(false);
+
+ // Expect correct calls to be made when submitting.
+ submit(wrapper.find('form'));
+ expect(onPersonalAccessTokenCreate).toBeCalled();
+
+ // If validation fails, we toggle the submitting flag and call useEffect()
+ // to set the `touched` flag to false again. Trigger a re-render, and mock
+ // useEffect(). This should de-activate the submit button again.
+ jest.spyOn(React, 'useEffect').mockImplementationOnce(f => f());
+ wrapper.setProps({ submitting: false });
+ expect(wrapper.find(SubmitButton).prop('disabled')).toBe(true);
+});
+
+function shallowRender(props: Partial<AzurePersonalAccessTokenFormProps> = {}) {
+ return shallow<AzurePersonalAccessTokenFormProps>(
+ <AzurePersonalAccessTokenForm
+ almSetting={mockAlmSettingsInstance({
+ alm: AlmKeys.Azure,
+ url: 'http://www.example.com'
+ })}
+ onPersonalAccessTokenCreate={jest.fn()}
+ validationFailed={false}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectCreate-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectCreate-test.tsx
new file mode 100644
index 00000000000..7ce89327f6e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectCreate-test.tsx
@@ -0,0 +1,92 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+/* eslint-disable sonarjs/no-duplicate-string */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
+import {
+ checkPersonalAccessTokenIsValid,
+ setAlmPersonalAccessToken
+} from '../../../../api/alm-integrations';
+import { mockAlmSettingsInstance } from '../../../../helpers/mocks/alm-settings';
+import { mockLocation } from '../../../../helpers/testMocks';
+import { AlmKeys } from '../../../../types/alm-settings';
+import AzureProjectCreate from '../AzureProjectCreate';
+
+jest.mock('../../../../api/alm-integrations', () => {
+ return {
+ checkPersonalAccessTokenIsValid: jest.fn().mockResolvedValue(true),
+ setAlmPersonalAccessToken: jest.fn().mockResolvedValue(null)
+ };
+});
+
+beforeEach(jest.clearAllMocks);
+
+it('should render correctly', () => {
+ 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(true);
+ const wrapper = shallowRender();
+ await waitAndUpdate(wrapper);
+ expect(checkPersonalAccessTokenIsValid).toBeCalled();
+ expect(wrapper.state().patIsValid).toBe(true);
+});
+
+it('should correctly handle an invalid PAT', async () => {
+ (checkPersonalAccessTokenIsValid as jest.Mock).mockResolvedValueOnce(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(false);
+ await waitAndUpdate(wrapper);
+ expect(checkPersonalAccessTokenIsValid).toBeCalled();
+ expect(wrapper.state().submittingToken).toBe(false);
+ expect(wrapper.state().tokenValidationFailed).toBe(true);
+});
+
+function shallowRender(overrides: Partial<AzureProjectCreate['props']> = {}) {
+ return shallow<AzureProjectCreate>(
+ <AzureProjectCreate
+ canAdmin={true}
+ loadingBindings={false}
+ location={mockLocation()}
+ onProjectCreate={jest.fn()}
+ settings={[mockAlmSettingsInstance({ alm: AlmKeys.Azure, key: 'foo' })]}
+ {...overrides}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectCreateRenderer-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectCreateRenderer-test.tsx
new file mode 100644
index 00000000000..d38e64d922e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/AzureProjectCreateRenderer-test.tsx
@@ -0,0 +1,49 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+/* eslint-disable sonarjs/no-duplicate-string */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { mockAlmSettingsInstance } from '../../../../helpers/mocks/alm-settings';
+import { AlmKeys } from '../../../../types/alm-settings';
+import AzureProjectCreateRenderer, {
+ AzureProjectCreateRendererProps
+} from '../AzureProjectCreateRenderer';
+
+it('should render correctly', () => {
+ expect(shallowRender({ loading: true })).toMatchSnapshot('loading');
+ expect(shallowRender({ settings: undefined })).toMatchSnapshot('no settings');
+ expect(shallowRender({ showPersonalAccessTokenForm: true })).toMatchSnapshot('token form');
+ expect(shallowRender({})).toMatchSnapshot('project list');
+});
+
+function shallowRender(overrides: Partial<AzureProjectCreateRendererProps>) {
+ return shallow(
+ <AzureProjectCreateRenderer
+ canAdmin={true}
+ loading={false}
+ onPersonalAccessTokenCreate={jest.fn()}
+ tokenValidationFailed={false}
+ settings={mockAlmSettingsInstance({ alm: AlmKeys.Azure })}
+ showPersonalAccessTokenForm={false}
+ submittingToken={false}
+ {...overrides}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectModeSelection-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectModeSelection-test.tsx
index 6d0717eecaf..2cfeddd296e 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectModeSelection-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectModeSelection-test.tsx
@@ -39,14 +39,27 @@ it('should correctly pass the selected mode up', () => {
const onSelectMode = jest.fn();
const wrapper = shallowRender({ onSelectMode });
+ const almButton = 'button.create-project-mode-type-alm';
+
click(wrapper.find('button.create-project-mode-type-manual'));
expect(onSelectMode).toBeCalledWith(CreateProjectModes.Manual);
+ onSelectMode.mockClear();
+
+ click(wrapper.find(almButton).at(0));
+ expect(onSelectMode).toBeCalledWith(CreateProjectModes.AzureDevOps);
+ onSelectMode.mockClear();
- click(wrapper.find('button.create-project-mode-type-alm').at(0));
+ click(wrapper.find(almButton).at(1));
expect(onSelectMode).toBeCalledWith(CreateProjectModes.BitbucketServer);
+ onSelectMode.mockClear();
- click(wrapper.find('button.create-project-mode-type-alm').at(1));
+ click(wrapper.find(almButton).at(2));
expect(onSelectMode).toBeCalledWith(CreateProjectModes.GitHub);
+ onSelectMode.mockClear();
+
+ click(wrapper.find(almButton).at(3));
+ expect(onSelectMode).toBeCalledWith(CreateProjectModes.GitLab);
+ onSelectMode.mockClear();
});
function shallowRender(
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx
index e2d16dd4ed7..cb0e4d074b3 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPage-test.tsx
@@ -50,6 +50,14 @@ it('should render correctly if the manual method is selected', () => {
).toMatchSnapshot();
});
+it('should render correctly if the Azure method is selected', () => {
+ expect(
+ shallowRender({
+ location: mockLocation({ query: { mode: CreateProjectModes.AzureDevOps } })
+ })
+ ).toMatchSnapshot();
+});
+
it('should render correctly if the BBS method is selected', () => {
expect(
shallowRender({
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzurePersonalAccessTokenForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzurePersonalAccessTokenForm-test.tsx.snap
new file mode 100644
index 00000000000..c8399e96c85
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzurePersonalAccessTokenForm-test.tsx.snap
@@ -0,0 +1,229 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: default 1`] = `
+<div
+ className="boxed-group abs-width-600"
+>
+ <div
+ className="boxed-group-inner"
+ >
+ <h2>
+ onboarding.create_project.pat_form.title.azure
+ </h2>
+ <div
+ className="big-spacer-top big-spacer-bottom"
+ >
+ <FormattedMessage
+ defaultMessage="onboarding.create_project.pat_help.instructions.azure"
+ id="onboarding.create_project.pat_help.instructions"
+ values={
+ Object {
+ "link": <a
+ className="link-with-icon"
+ href="http://www.example.com/_usersSettings/tokens"
+ rel="noopener noreferrer"
+ target="_blank"
+ >
+ <DetachIcon
+ className="little-spacer-right"
+ />
+ <span>
+ onboarding.create_project.pat_help.instructions.link.azure
+ </span>
+ </a>,
+ "scope": <strong>
+ <em>
+ Code (Read & Write)
+ </em>
+ </strong>,
+ }
+ }
+ />
+ </div>
+ <form
+ onSubmit={[Function]}
+ >
+ <ValidationInput
+ error="onboarding.create_project.pat_form.pat_required"
+ id="personal_access_token"
+ isInvalid={false}
+ isValid={false}
+ label="onboarding.create_project.enter_pat"
+ required={true}
+ >
+ <input
+ autoFocus={true}
+ className="width-100 little-spacer-bottom"
+ id="personal_access_token"
+ minLength={1}
+ name="personal_access_token"
+ onChange={[Function]}
+ type="text"
+ value=""
+ />
+ </ValidationInput>
+ <SubmitButton
+ disabled={true}
+ >
+ onboarding.create_project.pat_form.list_repositories
+ </SubmitButton>
+ <DeferredSpinner
+ className="spacer-left"
+ loading={false}
+ />
+ </form>
+ </div>
+</div>
+`;
+
+exports[`should render correctly: submitting 1`] = `
+<div
+ className="boxed-group abs-width-600"
+>
+ <div
+ className="boxed-group-inner"
+ >
+ <h2>
+ onboarding.create_project.pat_form.title.azure
+ </h2>
+ <div
+ className="big-spacer-top big-spacer-bottom"
+ >
+ <FormattedMessage
+ defaultMessage="onboarding.create_project.pat_help.instructions.azure"
+ id="onboarding.create_project.pat_help.instructions"
+ values={
+ Object {
+ "link": <a
+ className="link-with-icon"
+ href="http://www.example.com/_usersSettings/tokens"
+ rel="noopener noreferrer"
+ target="_blank"
+ >
+ <DetachIcon
+ className="little-spacer-right"
+ />
+ <span>
+ onboarding.create_project.pat_help.instructions.link.azure
+ </span>
+ </a>,
+ "scope": <strong>
+ <em>
+ Code (Read & Write)
+ </em>
+ </strong>,
+ }
+ }
+ />
+ </div>
+ <form
+ onSubmit={[Function]}
+ >
+ <ValidationInput
+ error="onboarding.create_project.pat_form.pat_required"
+ id="personal_access_token"
+ isInvalid={false}
+ isValid={false}
+ label="onboarding.create_project.enter_pat"
+ required={true}
+ >
+ <input
+ autoFocus={true}
+ className="width-100 little-spacer-bottom"
+ id="personal_access_token"
+ minLength={1}
+ name="personal_access_token"
+ onChange={[Function]}
+ type="text"
+ value=""
+ />
+ </ValidationInput>
+ <SubmitButton
+ disabled={true}
+ >
+ onboarding.create_project.pat_form.list_repositories
+ </SubmitButton>
+ <DeferredSpinner
+ className="spacer-left"
+ loading={true}
+ />
+ </form>
+ </div>
+</div>
+`;
+
+exports[`should render correctly: validation failed 1`] = `
+<div
+ className="boxed-group abs-width-600"
+>
+ <div
+ className="boxed-group-inner"
+ >
+ <h2>
+ onboarding.create_project.pat_form.title.azure
+ </h2>
+ <div
+ className="big-spacer-top big-spacer-bottom"
+ >
+ <FormattedMessage
+ defaultMessage="onboarding.create_project.pat_help.instructions.azure"
+ id="onboarding.create_project.pat_help.instructions"
+ values={
+ Object {
+ "link": <a
+ className="link-with-icon"
+ href="http://www.example.com/_usersSettings/tokens"
+ rel="noopener noreferrer"
+ target="_blank"
+ >
+ <DetachIcon
+ className="little-spacer-right"
+ />
+ <span>
+ onboarding.create_project.pat_help.instructions.link.azure
+ </span>
+ </a>,
+ "scope": <strong>
+ <em>
+ Code (Read & Write)
+ </em>
+ </strong>,
+ }
+ }
+ />
+ </div>
+ <form
+ onSubmit={[Function]}
+ >
+ <ValidationInput
+ error="onboarding.create_project.pat_form.pat_required"
+ id="personal_access_token"
+ isInvalid={true}
+ isValid={false}
+ label="onboarding.create_project.enter_pat"
+ required={true}
+ >
+ <input
+ autoFocus={true}
+ className="width-100 little-spacer-bottom is-invalid"
+ id="personal_access_token"
+ minLength={1}
+ name="personal_access_token"
+ onChange={[Function]}
+ type="text"
+ value=""
+ />
+ </ValidationInput>
+ <SubmitButton
+ disabled={true}
+ >
+ onboarding.create_project.pat_form.list_repositories
+ </SubmitButton>
+ <DeferredSpinner
+ className="spacer-left"
+ loading={false}
+ />
+ </form>
+ </div>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreate-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreate-test.tsx.snap
new file mode 100644
index 00000000000..47f70559c9f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreate-test.tsx.snap
@@ -0,0 +1,17 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<AzureProjectCreateRenderer
+ canAdmin={true}
+ loading={true}
+ onPersonalAccessTokenCreate={[Function]}
+ settings={
+ Object {
+ "alm": "azure",
+ "key": "foo",
+ }
+ }
+ showPersonalAccessTokenForm={true}
+ tokenValidationFailed={false}
+/>
+`;
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreateRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreateRenderer-test.tsx.snap
new file mode 100644
index 00000000000..dc15c4a504a
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AzureProjectCreateRenderer-test.tsx.snap
@@ -0,0 +1,104 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: loading 1`] = `
+<Fragment>
+ <CreateProjectPageHeader
+ title={
+ <span
+ className="text-middle"
+ >
+ <img
+ alt=""
+ className="spacer-right"
+ height="24"
+ src="/images/alm/azure.svg"
+ />
+ onboarding.create_project.azure.title
+ </span>
+ }
+ />
+ <i
+ className="spinner"
+ />
+</Fragment>
+`;
+
+exports[`should render correctly: no settings 1`] = `
+<Fragment>
+ <CreateProjectPageHeader
+ title={
+ <span
+ className="text-middle"
+ >
+ <img
+ alt=""
+ className="spacer-right"
+ height="24"
+ src="/images/alm/azure.svg"
+ />
+ onboarding.create_project.azure.title
+ </span>
+ }
+ />
+ <WrongBindingCountAlert
+ alm="azure"
+ canAdmin={true}
+ />
+</Fragment>
+`;
+
+exports[`should render correctly: project list 1`] = `
+<Fragment>
+ <CreateProjectPageHeader
+ title={
+ <span
+ className="text-middle"
+ >
+ <img
+ alt=""
+ className="spacer-right"
+ height="24"
+ src="/images/alm/azure.svg"
+ />
+ onboarding.create_project.azure.title
+ </span>
+ }
+ />
+ <AzureProjectsList />
+</Fragment>
+`;
+
+exports[`should render correctly: token form 1`] = `
+<Fragment>
+ <CreateProjectPageHeader
+ title={
+ <span
+ className="text-middle"
+ >
+ <img
+ alt=""
+ className="spacer-right"
+ height="24"
+ src="/images/alm/azure.svg"
+ />
+ onboarding.create_project.azure.title
+ </span>
+ }
+ />
+ <div
+ className="display-flex-justify-center"
+ >
+ <AzurePersonalAccessTokenForm
+ almSetting={
+ Object {
+ "alm": "azure",
+ "key": "key",
+ }
+ }
+ onPersonalAccessTokenCreate={[MockFunction]}
+ submitting={false}
+ validationFailed={false}
+ />
+ </div>
+</Fragment>
+`;
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectModeSelection-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectModeSelection-test.tsx.snap
index cd06179f636..cb6db4d27f5 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectModeSelection-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectModeSelection-test.tsx.snap
@@ -36,6 +36,37 @@ exports[`should render correctly: default 1`] = `
</div>
</button>
<button
+ className="button button-huge big-spacer-left display-flex-column create-project-mode-type-alm disabled"
+ disabled={true}
+ onClick={[Function]}
+ type="button"
+ >
+ <img
+ alt=""
+ height={80}
+ src="/images/alm/azure.svg"
+ />
+ <div
+ className="medium big-spacer-top"
+ >
+ onboarding.create_project.select_method.azure
+ </div>
+ <div
+ className="text-muted small spacer-top"
+ style={
+ Object {
+ "lineHeight": 1.5,
+ }
+ }
+ >
+ onboarding.create_project.alm_not_configured
+ <HelpTooltip
+ className="little-spacer-left"
+ overlay="onboarding.create_project.zero_alm_instances.azure"
+ />
+ </div>
+ </button>
+ <button
className="button button-huge big-spacer-left display-flex-column create-project-mode-type-alm"
disabled={false}
onClick={[Function]}
@@ -162,6 +193,37 @@ exports[`should render correctly: invalid configs 1`] = `
<img
alt=""
height={80}
+ src="/images/alm/azure.svg"
+ />
+ <div
+ className="medium big-spacer-top"
+ >
+ onboarding.create_project.select_method.azure
+ </div>
+ <div
+ className="text-muted small spacer-top"
+ style={
+ Object {
+ "lineHeight": 1.5,
+ }
+ }
+ >
+ onboarding.create_project.alm_not_configured
+ <HelpTooltip
+ className="little-spacer-left"
+ overlay="onboarding.create_project.zero_alm_instances.azure"
+ />
+ </div>
+ </button>
+ <button
+ className="button button-huge big-spacer-left display-flex-column create-project-mode-type-alm disabled"
+ disabled={true}
+ onClick={[Function]}
+ type="button"
+ >
+ <img
+ alt=""
+ height={80}
src="/images/alm/bitbucket.svg"
/>
<div
@@ -295,6 +357,29 @@ exports[`should render correctly: loading instances 1`] = `
<img
alt=""
height={80}
+ src="/images/alm/azure.svg"
+ />
+ <div
+ className="medium big-spacer-top"
+ >
+ onboarding.create_project.select_method.azure
+ </div>
+ <span>
+ onboarding.create_project.check_alm_supported
+ <i
+ className="little-spacer-left spinner"
+ />
+ </span>
+ </button>
+ <button
+ className="button button-huge big-spacer-left display-flex-column create-project-mode-type-alm disabled"
+ disabled={true}
+ onClick={[Function]}
+ type="button"
+ >
+ <img
+ alt=""
+ height={80}
src="/images/alm/bitbucket.svg"
/>
<div
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 8dd4ffc6bc8..7f37890764c 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
@@ -53,6 +53,44 @@ exports[`should render correctly if no branch support 1`] = `
</Fragment>
`;
+exports[`should render correctly if the Azure method is selected 1`] = `
+<Fragment>
+ <Helmet
+ defer={true}
+ encodeSpecialCharacters={true}
+ title="my_account.create_new.TRK"
+ titleTemplate="%s"
+ />
+ <A11ySkipTarget
+ anchor="create_project_main"
+ />
+ <div
+ className="page page-limited huge-spacer-bottom position-relative"
+ id="create-project"
+ >
+ <AzureProjectCreate
+ canAdmin={false}
+ loadingBindings={true}
+ location={
+ Object {
+ "action": "PUSH",
+ "hash": "",
+ "key": "key",
+ "pathname": "/path",
+ "query": Object {
+ "mode": "azure",
+ },
+ "search": "",
+ "state": Object {},
+ }
+ }
+ onProjectCreate={[Function]}
+ settings={Array []}
+ />
+ </div>
+</Fragment>
+`;
+
exports[`should render correctly if the BBS method is selected 1`] = `
<Fragment>
<Helmet
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 c84dd2c1d4c..c000fa481d5 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
@@ -19,6 +19,7 @@
*/
export enum CreateProjectModes {
Manual = 'manual',
+ AzureDevOps = 'azure',
BitbucketServer = 'bitbucket',
GitHub = 'github',
GitLab = 'gitlab'