/*
* 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
this.setState({ governanceReady: true });
}
},
- () => {}
+ () => {
+ /* error handled globally */
+ }
);
}
}
--- /dev/null
+/*
+ * 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>
+ );
+}
--- /dev/null
+/*
+ * 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}
+ />
+ );
+ }
+}
--- /dev/null
+/*
+ * 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 />
+ ))}
+ </>
+ );
+}
--- /dev/null
+/*
+ * 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>
+ );
+}
</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)}
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';
}
interface State {
+ azureSettings: AlmSettingsInstance[];
bitbucketSettings: AlmSettingsInstance[];
githubSettings: AlmSettingsInstance[];
gitlabSettings: AlmSettingsInstance[];
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 {
.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),
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
}
default: {
const almCounts = {
- [AlmKeys.Azure]: 0,
+ [AlmKeys.Azure]: azureSettings.length,
[AlmKeys.Bitbucket]: bitbucketSettings.length,
[AlmKeys.GitHub]: githubSettings.length,
[AlmKeys.GitLab]: gitlabSettings.length
--- /dev/null
+/*
+ * 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}
+ />
+ );
+}
--- /dev/null
+/*
+ * 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}
+ />
+ );
+}
--- /dev/null
+/*
+ * 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}
+ />
+ );
+}
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(
).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({
--- /dev/null
+// 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>
+`;
--- /dev/null
+// 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}
+/>
+`;
--- /dev/null
+// 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>
+`;
onboarding.create_project.select_method.manual
</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}
onboarding.create_project.select_method.manual
</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"
disabled={true}
onboarding.create_project.select_method.manual
</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>
+ <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}
</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
*/
export enum CreateProjectModes {
Manual = 'manual',
+ AzureDevOps = 'azure',
BitbucketServer = 'bitbucket',
GitHub = 'github',
GitLab = 'gitlab'
my_account.create_new.APP=Create Application
my_account.add_project=Add Project
my_account.add_project.manual=Manually
+my_account.add_project.azure=Azure DevOps
my_account.add_project.bitbucket=Bitbucket
my_account.add_project.github=GitHub
my_account.add_project.gitlab=GitLab
# ONBOARDING
#
#------------------------------------------------------------------------------
+onboarding.alm.azure=Azure DevOps Server
onboarding.alm.bitbucket=Bitbucket Server
onboarding.alm.gitlab=GitLab
onboarding.create_project.setup_manually=Create a project
onboarding.create_project.select_method.manual=Manually
+onboarding.create_project.select_method.azure=From Azure DevOps Server
onboarding.create_project.select_method.bitbucket=From Bitbucket Server
onboarding.create_project.select_method.github=From GitHub
onboarding.create_project.select_method.gitlab=From GitLab
onboarding.create_application.key.description=If specified, this value is used as the key instead of generating it from the name of the Application. Only letters, digits, dashes and underscores can be used.
+onboarding.create_project.pat_form.title.azure=Allow SonarQube to access and list your Azure DevOps Server repositories
onboarding.create_project.pat_form.title.bitbucket=Grant access to your repositories
onboarding.create_project.pat_form.title.gitlab=Grant access to your projects
+onboarding.create_project.pat_form.help.azure=SonarQube needs a personal access token to access and list your repositories from Azure DevOps Server.
onboarding.create_project.pat_form.help.bitbucket=SonarQube needs a personal access token to access and list your repositories from Bitbucket Server.
onboarding.create_project.pat_form.help.gitlab=SonarQube needs a personal access token to access and list your projects from GitLab.
+onboarding.create_project.pat_form.pat_required=Please enter a personal access token
+onboarding.create_project.pat_form.list_repositories=List repositories
onboarding.create_project.select_method=How do you want to create your project?
onboarding.create_project.too_many_alm_instances.bitbucket=You must have exactly 1 Bitbucket Server instance configured in order to use this method.
onboarding.create_project.too_many_alm_instances.github=You must have exactly 1 Bitbucket Server instance configured in order to use this method.
onboarding.create_project.wrong_binding_count=You must have exactly 1 {alm} instance configured in order to use this method, but none were found. Either create the project manually, or contact your system administrator.
onboarding.create_project.wrong_binding_count.admin=You must have exactly 1 {alm} instance configured in order to use this method. You can configure instances under {url}.
onboarding.create_project.enter_pat=Enter personal access token
+onboarding.create_project.pat_incorrect.azure=Your personal access couldn't be validated.
onboarding.create_project.pat_incorrect.bitbucket=Your personal access couldn't be validated.
onboarding.create_project.pat_incorrect.gitlab=Your personal access couldn't be validated. Please make sure it has the right scope and that it is not expired.
onboarding.create_project.pat_help.title=How to create a personal access token?
+onboarding.create_project.pat_help.instructions.azure=Create and provide an Azure DevOps Server {link}. You need to select the {scope} scope so we can display a list of your repositories which are available for analysis.
+onboarding.create_project.pat_help.instructions.link.azure=personal access token
+
onboarding.create_project.pat_help.instructions=Click the following link to generate a token in {alm}, and copy-paste it into the personal access token field.
onboarding.create_project.pat_help.instructions2.bitbucket=Set a name, for example "SonarQube", and select the following permissions:
onboarding.create_project.pat_help.link=Create personal access token
onboarding.create_project.only_showing_X_first_repos=We're only displaying the first {0} repositories. If you're looking for a repository that's not in this list, use the search above.
onboarding.create_project.import_selected_repo=Set up selected repository
onboarding.create_project.go_to_project=Go to project
+
+onboarding.create_project.azure.title=Which Azure DevOps Server repository do you want to set up?
onboarding.create_project.github.title=Which GitHub repository do you want to set up?
onboarding.create_project.github.choose_organization=Choose organization
onboarding.create_project.github.warning.title=Could not connect to GitHub