@@ -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 */ | |||
} | |||
); | |||
} | |||
} |
@@ -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> | |||
); | |||
} |
@@ -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} | |||
/> | |||
); | |||
} | |||
} |
@@ -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 /> | |||
))} | |||
</> | |||
); | |||
} |
@@ -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> | |||
); | |||
} |
@@ -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)} |
@@ -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 |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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( |
@@ -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({ |
@@ -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> | |||
`; |
@@ -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} | |||
/> | |||
`; |
@@ -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> | |||
`; |
@@ -35,6 +35,37 @@ exports[`should render correctly: default 1`] = ` | |||
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} | |||
@@ -153,6 +184,37 @@ exports[`should render correctly: invalid configs 1`] = ` | |||
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} | |||
@@ -286,6 +348,29 @@ exports[`should render correctly: loading instances 1`] = ` | |||
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} |
@@ -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 |
@@ -19,6 +19,7 @@ | |||
*/ | |||
export enum CreateProjectModes { | |||
Manual = 'manual', | |||
AzureDevOps = 'azure', | |||
BitbucketServer = 'bitbucket', | |||
GitHub = 'github', | |||
GitLab = 'gitlab' |
@@ -1854,6 +1854,7 @@ my_account.create_new.VW=Create Portfolio | |||
my_account.create_new.APP=Create Application | |||
my_account.add_project=Add Project | |||
my_account.add_project.manual=Manually | |||
my_account.add_project.azure=Azure DevOps | |||
my_account.add_project.bitbucket=Bitbucket | |||
my_account.add_project.github=GitHub | |||
my_account.add_project.gitlab=GitLab | |||
@@ -3202,6 +3203,7 @@ footer.web_api=Web API | |||
# ONBOARDING | |||
# | |||
#------------------------------------------------------------------------------ | |||
onboarding.alm.azure=Azure DevOps Server | |||
onboarding.alm.bitbucket=Bitbucket Server | |||
onboarding.alm.gitlab=GitLab | |||
@@ -3211,6 +3213,7 @@ onboarding.project_analysis.guide_to_integrate_pipelines=follow the guide to int | |||
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 | |||
@@ -3239,10 +3242,14 @@ onboarding.create_project.from_bbs=Create a project from Bitbucket Server | |||
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. | |||
@@ -3253,10 +3260,14 @@ onboarding.create_project.zero_alm_instances.gitlab=You must first configure a G | |||
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 | |||
@@ -3274,6 +3285,8 @@ onboarding.create_project.no_bbs_repos.filter=No repositories match your filter. | |||
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 |