From: Wouter Admiraal Date: Tue, 26 Jan 2021 08:30:35 +0000 (+0100) Subject: SONAR-14393 Add admin form for Bitbucket Cloud integration X-Git-Tag: 8.7.0.41497~56 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=eaa3c931777204492328dc49c93e2cfca3d9c307;p=sonarqube.git SONAR-14393 Add admin form for Bitbucket Cloud integration --- diff --git a/server/sonar-web/src/main/js/api/alm-settings.ts b/server/sonar-web/src/main/js/api/alm-settings.ts index 26953bca8a0..4c4f6430ad1 100644 --- a/server/sonar-web/src/main/js/api/alm-settings.ts +++ b/server/sonar-web/src/main/js/api/alm-settings.ts @@ -25,6 +25,7 @@ import { AzureBindingDefinition, AzureProjectAlmBindingParams, BitbucketBindingDefinition, + BitbucketCloudBindingDefinition, BitbucketProjectAlmBindingParams, GithubBindingDefinition, GithubProjectAlmBindingParams, @@ -87,6 +88,16 @@ export function updateBitbucketConfiguration( return post('/api/alm_settings/update_bitbucket', data).catch(throwGlobalError); } +export function createBitbucketCloudConfiguration(data: BitbucketCloudBindingDefinition) { + return post('/api/alm_settings/create_bitbucketcloud', data).catch(throwGlobalError); +} + +export function updateBitbucketCloudConfiguration( + data: BitbucketCloudBindingDefinition & { newKey: string } +) { + return post('/api/alm_settings/update_bitbucketcloud', data).catch(throwGlobalError); +} + export function createGitlabConfiguration(data: GitlabBindingDefinition) { return post('/api/alm_settings/create_gitlab', data).catch(throwGlobalError); } diff --git a/server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreateRenderer.tsx b/server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreateRenderer.tsx index a256fc3202e..0f71610155b 100644 --- a/server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreateRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/BitbucketProjectCreateRenderer.tsx @@ -101,7 +101,7 @@ export default function BitbucketProjectCreateRenderer(props: BitbucketProjectCr {loading && } {!loading && !bitbucketSetting && ( - + )} {!loading && 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 136fb393e5f..e14ba173049 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 @@ -29,7 +29,12 @@ import { ALM_INTEGRATION } from '../../settings/components/AdditionalCategoryKey import { CreateProjectModes } from './types'; export interface CreateProjectModeSelectionProps { - almCounts: { [key in AlmKeys]: number }; + almCounts: { + [AlmKeys.Azure]: number; + [AlmKeys.BitbucketServer]: number; + [AlmKeys.GitLab]: number; + [AlmKeys.GitHub]: number; + }; appState: Pick; loadingBindings: boolean; onSelectMode: (mode: CreateProjectModes) => void; @@ -37,7 +42,7 @@ export interface CreateProjectModeSelectionProps { function renderAlmOption( props: CreateProjectModeSelectionProps, - alm: AlmKeys, + alm: AlmKeys.Azure | AlmKeys.BitbucketServer | AlmKeys.GitHub | AlmKeys.GitLab, mode: CreateProjectModes ) { const { @@ -145,7 +150,7 @@ export function CreateProjectModeSelection(props: CreateProjectModeSelectionProp {renderAlmOption(props, AlmKeys.Azure, CreateProjectModes.AzureDevOps)} - {renderAlmOption(props, AlmKeys.Bitbucket, CreateProjectModes.BitbucketServer)} + {renderAlmOption(props, AlmKeys.BitbucketServer, 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 c2bdfd909e2..45feaadb2ad 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 @@ -75,7 +75,7 @@ export class CreateProjectPage extends React.PureComponent { if (this.mounted) { this.setState({ azureSettings: almSettings.filter(s => s.alm === AlmKeys.Azure), - bitbucketSettings: almSettings.filter(s => s.alm === AlmKeys.Bitbucket), + bitbucketSettings: almSettings.filter(s => s.alm === AlmKeys.BitbucketServer), githubSettings: almSettings.filter(s => s.alm === AlmKeys.GitHub), gitlabSettings: almSettings.filter(s => s.alm === AlmKeys.GitLab), loading: false @@ -171,7 +171,7 @@ export class CreateProjectPage extends React.PureComponent { default: { const almCounts = { [AlmKeys.Azure]: azureSettings.length, - [AlmKeys.Bitbucket]: bitbucketSettings.length, + [AlmKeys.BitbucketServer]: bitbucketSettings.length, [AlmKeys.GitHub]: githubSettings.length, [AlmKeys.GitLab]: gitlabSettings.length }; diff --git a/server/sonar-web/src/main/js/apps/create/project/PersonalAccessTokenForm.tsx b/server/sonar-web/src/main/js/apps/create/project/PersonalAccessTokenForm.tsx index 9062a7e84ac..51811a40ac1 100644 --- a/server/sonar-web/src/main/js/apps/create/project/PersonalAccessTokenForm.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/PersonalAccessTokenForm.tsx @@ -36,7 +36,7 @@ export interface PersonalAccessTokenFormProps { } function getPatUrl(alm: AlmKeys, url: string) { - if (alm === AlmKeys.Bitbucket) { + if (alm === AlmKeys.BitbucketServer) { return `${url.replace(/\/$/, '')}/plugins/servlet/access-tokens/add`; } else { // GitLab @@ -132,7 +132,7 @@ export default function PersonalAccessTokenForm(props: PersonalAccessTokenFormPr

    - {alm === AlmKeys.Bitbucket && ( + {alm === AlmKeys.BitbucketServer && ( <>
  • = {}) { return shallow( { function shallowRender(props: Partial = {}) { return shallow( { expect(shallowRender()).toMatchSnapshot('default'); expect(shallowRender({ loadingBindings: true })).toMatchSnapshot('loading instances'); - expect(shallowRender({}, { [AlmKeys.Bitbucket]: 0, [AlmKeys.GitHub]: 2 })).toMatchSnapshot( + expect(shallowRender({}, { [AlmKeys.BitbucketServer]: 0, [AlmKeys.GitHub]: 2 })).toMatchSnapshot( 'invalid configs, not admin' ); expect( - shallowRender({ appState: { canAdmin: true } }, { [AlmKeys.Bitbucket]: 0, [AlmKeys.GitHub]: 2 }) + shallowRender( + { appState: { canAdmin: true } }, + { [AlmKeys.BitbucketServer]: 0, [AlmKeys.GitHub]: 2 } + ) ).toMatchSnapshot('invalid configs, admin'); }); @@ -71,7 +74,7 @@ function shallowRender( ) { const almCounts = { [AlmKeys.Azure]: 0, - [AlmKeys.Bitbucket]: 1, + [AlmKeys.BitbucketServer]: 1, [AlmKeys.GitHub]: 0, [AlmKeys.GitLab]: 0, ...almCountOverrides 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 e59baf2bb09..dd9682364f3 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 @@ -26,7 +26,7 @@ import { CreateProjectPage } from '../CreateProjectPage'; import { CreateProjectModes } from '../types'; jest.mock('../../../../api/alm-settings', () => ({ - getAlmSettings: jest.fn().mockResolvedValue([{ alm: AlmKeys.Bitbucket, key: 'foo' }]) + getAlmSettings: jest.fn().mockResolvedValue([{ alm: AlmKeys.BitbucketServer, key: 'foo' }]) })); beforeEach(jest.clearAllMocks); diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/PersonalAccessTokenForm-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/PersonalAccessTokenForm-test.tsx index 2f4ee698f96..5e7571a211b 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/PersonalAccessTokenForm-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/PersonalAccessTokenForm-test.tsx @@ -71,7 +71,7 @@ function shallowRender(props: Partial = {}) { return shallow( { expect(shallowRender({ canAdmin: true })).toMatchSnapshot('for admin'); - expect(shallowRender({ alm: AlmKeys.Bitbucket })).toMatchSnapshot('bitbucket'); + expect(shallowRender({ alm: AlmKeys.BitbucketServer })).toMatchSnapshot('bitbucket'); expect(shallowRender({ alm: AlmKeys.GitLab })).toMatchSnapshot('gitlab'); }); function shallowRender(props: Partial = {}) { - return shallow(); + return shallow( + + ); } diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCreationMenu.tsx b/server/sonar-web/src/main/js/apps/projects/components/ProjectCreationMenu.tsx index 4a406a6a36f..fb282ba3f49 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/ProjectCreationMenu.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCreationMenu.tsx @@ -24,6 +24,7 @@ import DropdownIcon from 'sonar-ui-common/components/icons/DropdownIcon'; import { translate } from 'sonar-ui-common/helpers/l10n'; import { getAlmSettings } from '../../../api/alm-settings'; import { withCurrentUser } from '../../../components/hoc/withCurrentUser'; +import { IMPORT_COMPATIBLE_ALMS } from '../../../helpers/constants'; import { hasGlobalPermission } from '../../../helpers/users'; import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings'; import ProjectCreationMenuItem from './ProjectCreationMenuItem'; @@ -38,14 +39,11 @@ interface State { } const PROJECT_CREATION_PERMISSION = 'provisioning'; -/* - * ALMs for which the import feature has been implemented - */ -const IMPORT_COMPATIBLE_ALMS = [AlmKeys.Azure, AlmKeys.Bitbucket, AlmKeys.GitHub, AlmKeys.GitLab]; const almSettingsValidators = { [AlmKeys.Azure]: (settings: AlmSettingsInstance) => !!settings.url, - [AlmKeys.Bitbucket]: (_: AlmSettingsInstance) => true, + [AlmKeys.BitbucketServer]: (_: AlmSettingsInstance) => true, + [AlmKeys.BitbucketCloud]: (_: AlmSettingsInstance) => false, [AlmKeys.GitHub]: (_: AlmSettingsInstance) => true, [AlmKeys.GitLab]: (settings: AlmSettingsInstance) => !!settings.url }; diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCreationMenu-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCreationMenu-test.tsx index 5b2825de4b2..90e7b675473 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCreationMenu-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCreationMenu-test.tsx @@ -55,8 +55,8 @@ it('should not fetch alm bindings if user cannot create projects', async () => { it('should filter alm bindings appropriately', async () => { (getAlmSettings as jest.Mock).mockResolvedValueOnce([ { alm: AlmKeys.Azure }, - { alm: AlmKeys.Bitbucket, url: 'b1' }, - { alm: AlmKeys.Bitbucket, url: 'b2' }, + { alm: AlmKeys.BitbucketServer, url: 'b1' }, + { alm: AlmKeys.BitbucketServer, url: 'b2' }, { alm: AlmKeys.GitHub }, { alm: AlmKeys.GitLab, url: 'gitlab.com' } ]); diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCreationMenuItem-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCreationMenuItem-test.tsx index 65325429a39..2825b03ba1f 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCreationMenuItem-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCreationMenuItem-test.tsx @@ -28,5 +28,5 @@ it('should render correctly', () => { }); function shallowRender(overrides: Partial = {}) { - return shallow(); + return shallow(); } diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionBox.tsx b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionBox.tsx index c83889511c9..9f1d81371f9 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionBox.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionBox.tsx @@ -29,6 +29,7 @@ import EditIcon from 'sonar-ui-common/components/icons/EditIcon'; import { Alert } from 'sonar-ui-common/components/ui/Alert'; import { translate } from 'sonar-ui-common/helpers/l10n'; import { getEdition, getEditionUrl } from '../../../../helpers/editions'; +import { IMPORT_COMPATIBLE_ALMS } from '../../../../helpers/constants'; import { AlmBindingDefinition, AlmKeys, @@ -140,12 +141,12 @@ function getImportFeatureStatus( export default function AlmBindingDefinitionBox(props: AlmBindingDefinitionBoxProps) { const { alm, branchesEnabled, definition, multipleDefinitions, status = DEFAULT_STATUS } = props; - const importFeatureTitle = + const prDecoFeatureTitle = alm === AlmKeys.GitLab ? translate('settings.almintegration.feature.mr_decoration.title') : translate('settings.almintegration.feature.pr_decoration.title'); - const importFeatureDescription = + const prDecoFeatureDescription = alm === AlmKeys.GitLab ? translate('settings.almintegration.feature.mr_decoration.description') : translate('settings.almintegration.feature.pr_decoration.description'); @@ -178,20 +179,24 @@ export default function AlmBindingDefinitionBox(props: AlmBindingDefinitionBoxPr {status.type !== AlmSettingsBindingStatusType.Warning && (
    - - {importFeatureTitle} + + {prDecoFeatureTitle} {getPRDecorationFeatureStatus(branchesEnabled, status.type)}
    -
    - - {translate('settings.almintegration.feature.alm_repo_import.title')} - - {getImportFeatureStatus(definition, multipleDefinitions, status.type)} -
    + {IMPORT_COMPATIBLE_ALMS.includes(alm) && ( +
    + + + {translate('settings.almintegration.feature.alm_repo_import.title')} + + + {getImportFeatureStatus(definition, multipleDefinitions, status.type)} +
    + )}
    )} diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionFormModalRenderer.tsx b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionFormModalRenderer.tsx index 2c0ac91f58d..c3600b8bfe8 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionFormModalRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmBindingDefinitionFormModalRenderer.tsx @@ -58,10 +58,12 @@ export default function AlmBindingDefinitionFormModalRenderer(
    {children}
    - {help && ( + {help ? ( {help} + ) : ( +
    )}
    diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmIntegration.tsx b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmIntegration.tsx index 254cffa1d76..7845f393fb3 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmIntegration.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmIntegration.tsx @@ -62,7 +62,8 @@ export class AlmIntegration extends React.PureComponent { currentAlm: props.location.query.alm || AlmKeys.GitHub, definitions: { [AlmKeys.Azure]: [], - [AlmKeys.Bitbucket]: [], + [AlmKeys.BitbucketServer]: [], + [AlmKeys.BitbucketCloud]: [], [AlmKeys.GitHub]: [], [AlmKeys.GitLab]: [] }, diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmIntegrationRenderer.tsx b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmIntegrationRenderer.tsx index 2583b5c064e..92ced87796c 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmIntegrationRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmIntegrationRenderer.tsx @@ -68,7 +68,7 @@ const tabs = [ requiresBranchesEnabled: false }, { - key: AlmKeys.Bitbucket, + key: AlmKeys.BitbucketServer, label: ( <> - Bitbucket Server + Bitbucket ), requiresBranchesEnabled: false @@ -156,10 +156,10 @@ export default function AlmIntegrationRenderer(props: AlmIntegrationRendererProp onUpdateDefinitions={props.onUpdateDefinitions} /> )} - {currentAlm === AlmKeys.Bitbucket && ( + {currentAlm === AlmKeys.BitbucketServer && ( extends React.PureCo } handleCancel = () => { - this.setState({ - editedDefinition: undefined, - success: false - }); + this.setState({ editedDefinition: undefined, success: false }); }; handleCreate = () => { @@ -93,7 +90,11 @@ export default class AlmTab extends React.PureCo return call .then(() => { if (this.mounted) { - this.setState({ editedDefinition: undefined, submitting: false, success: true }); + this.setState({ + editedDefinition: undefined, + submitting: false, + success: true + }); } }) .then(this.props.onUpdateDefinitions) diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmTabRenderer.tsx b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmTabRenderer.tsx index c7c1cbd5a7f..e0f13c920db 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmTabRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmTabRenderer.tsx @@ -24,7 +24,8 @@ import { translate } from 'sonar-ui-common/helpers/l10n'; import { AlmBindingDefinition, AlmKeys, - AlmSettingsBindingStatus + AlmSettingsBindingStatus, + isBitbucketCloudBindingDefinition } from '../../../../types/alm-settings'; import AlmBindingDefinitionBox from './AlmBindingDefinitionBox'; import AlmBindingDefinitionForm, { @@ -93,7 +94,7 @@ export default function AlmTabRenderer( {definitions.map(def => ( void; - readOnly?: boolean; + formData: BitbucketBindingDefinition | BitbucketCloudBindingDefinition; + isCreating: boolean; + onFieldChange: ( + fieldId: keyof (BitbucketBindingDefinition & BitbucketCloudBindingDefinition), + value: string + ) => void; + onSelectVariant: (variant: AlmKeys.BitbucketServer | AlmKeys.BitbucketCloud) => void; + variant?: AlmKeys.BitbucketServer | AlmKeys.BitbucketCloud; } export default function BitbucketForm(props: BitbucketFormProps) { - const { formData, hideKeyField, onFieldChange, readOnly } = props; + const { formData, isCreating, variant } = props; return ( - <> - {!hideKeyField && ( - +
    + {isCreating && ( + <> + {translate('settings.almintegration.form.choose_bitbucket_variant')} + + )} - + + + } + id="url.bitbucket" + maxLength={2000} + onFieldChange={props.onFieldChange} + propKey="url" + value={formData.url} /> - } - id="url.bitbucket" - maxLength={2000} - onFieldChange={onFieldChange} - propKey="url" - readOnly={readOnly} - value={formData.url} - /> - - + +
    + )} + + {variant === AlmKeys.BitbucketCloud && isBitbucketCloudBindingDefinition(formData) && ( +
    + + + {'https://bitbucket.org/'} + {'{workspace}'} + {'/{repository}'} + + ) + }} + /> + } + id="workspace.bitbucketcloud" + maxLength={2000} + onFieldChange={props.onFieldChange} + propKey="workspace" + value={formData.workspace} + /> + + +
    + )} + ); } diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/BitbucketTab.tsx b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/BitbucketTab.tsx index 1ea3352965b..df05024ece7 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/BitbucketTab.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/BitbucketTab.tsx @@ -18,24 +18,24 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { Link } from 'react-router'; -import { translate } from 'sonar-ui-common/helpers/l10n'; import { + createBitbucketCloudConfiguration, createBitbucketConfiguration, + updateBitbucketCloudConfiguration, updateBitbucketConfiguration } from '../../../../api/alm-settings'; -import { ALM_DOCUMENTATION_PATHS } from '../../../../helpers/constants'; import { AlmKeys, AlmSettingsBindingStatus, - BitbucketBindingDefinition + BitbucketBindingDefinition, + BitbucketCloudBindingDefinition, + isBitbucketBindingDefinition } from '../../../../types/alm-settings'; -import AlmTab from './AlmTab'; -import BitbucketForm from './BitbucketForm'; +import BitbucketTabRenderer from './BitbucketTabRenderer'; -export interface BitbucketTabProps { +interface Props { branchesEnabled: boolean; - definitions: BitbucketBindingDefinition[]; + definitions: Array; definitionStatus: T.Dict; loadingAlmDefinitions: boolean; loadingProjectCount: boolean; @@ -45,54 +45,142 @@ export interface BitbucketTabProps { onUpdateDefinitions: () => void; } -export default function BitbucketTab(props: BitbucketTabProps) { - const { - branchesEnabled, - multipleAlmEnabled, - definitions, - definitionStatus, - loadingAlmDefinitions, - loadingProjectCount - } = props; - - return ( -
    - { + mounted = false; + state: State = { isCreating: false, submitting: false, success: false }; + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + handleCancel = () => { + this.setState({ + editedDefinition: undefined, + isCreating: false, + success: false, + variant: undefined + }); + }; + + handleCreate = () => { + this.setState({ + editedDefinition: DEFAULT_SERVER_BINDING, // Default to Bitbucket Server. + isCreating: true, + success: false, + variant: undefined + }); + }; + + handleSelectVariant = (variant: AlmKeys.BitbucketServer | AlmKeys.BitbucketCloud) => { + this.setState({ + variant, + editedDefinition: + variant === AlmKeys.BitbucketServer ? DEFAULT_SERVER_BINDING : DEFAULT_CLOUD_BINDING + }); + }; + + handleEdit = (definitionKey: string) => { + const editedDefinition = this.props.definitions.find(d => d.key === definitionKey); + const variant = isBitbucketBindingDefinition(editedDefinition) + ? AlmKeys.BitbucketServer + : AlmKeys.BitbucketCloud; + this.setState({ editedDefinition, variant, success: false }); + }; + + handleSubmit = ( + config: BitbucketBindingDefinition | BitbucketCloudBindingDefinition, + originalKey: string + ) => { + const call = originalKey + ? this.updateConfiguration({ newKey: config.key, ...config, key: originalKey }) + : this.createConfiguration({ ...config }); + + this.setState({ submitting: true }); + return call + .then(() => { + if (this.mounted) { + this.setState({ + editedDefinition: undefined, + isCreating: false, + submitting: false, + success: true + }); + } + }) + .then(this.props.onUpdateDefinitions) + .then(() => { + this.props.onCheck(config.key); + }) + .catch(() => { + if (this.mounted) { + this.setState({ submitting: false, success: false }); + } + }); + }; + + updateConfiguration = ( + config: (BitbucketBindingDefinition | BitbucketCloudBindingDefinition) & { newKey: string } + ) => { + if (isBitbucketBindingDefinition(config)) { + return updateBitbucketConfiguration(config); + } + return updateBitbucketCloudConfiguration(config); + }; + + createConfiguration = (config: BitbucketBindingDefinition | BitbucketCloudBindingDefinition) => { + if (isBitbucketBindingDefinition(config)) { + return createBitbucketConfiguration(config); + } + return createBitbucketCloudConfiguration(config); + }; + + render() { + const { + branchesEnabled, + definitions, + definitionStatus, + loadingAlmDefinitions, + loadingProjectCount, + multipleAlmEnabled + } = this.props; + const { editedDefinition, isCreating, submitting, success, variant } = this.state; + + return ( + } - help={ - <> -

    {translate('onboarding.create_project.pat_help.title')}

    - -

    - {translate('settings.almintegration.bitbucket.help_1')} -

    - -
      -
    • {translate('settings.almintegration.bitbucket.help_2')}
    • -
    • {translate('settings.almintegration.bitbucket.help_3')}
    • -
    - -

    - - {translate('learn_more')} - -

    - - } + editedDefinition={editedDefinition} + isCreating={isCreating} loadingAlmDefinitions={loadingAlmDefinitions} loadingProjectCount={loadingProjectCount} multipleAlmEnabled={multipleAlmEnabled} - onCheck={props.onCheck} - onDelete={props.onDelete} - onUpdateDefinitions={props.onUpdateDefinitions} - updateConfiguration={updateBitbucketConfiguration} + onCancel={this.handleCancel} + onCheck={this.props.onCheck} + onCreate={this.handleCreate} + onDelete={this.props.onDelete} + onEdit={this.handleEdit} + onSelectVariant={this.handleSelectVariant} + onSubmit={this.handleSubmit} + submitting={submitting} + success={success} + variant={variant} /> -
    - ); + ); + } } diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/BitbucketTabRenderer.tsx b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/BitbucketTabRenderer.tsx new file mode 100644 index 00000000000..e586f2b8d36 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/BitbucketTabRenderer.tsx @@ -0,0 +1,140 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { FormattedMessage } from 'react-intl'; +import { Link } from 'react-router'; +import { translate } from 'sonar-ui-common/helpers/l10n'; +import { ALM_DOCUMENTATION_PATHS } from '../../../../helpers/constants'; +import { + AlmKeys, + AlmSettingsBindingStatus, + BitbucketBindingDefinition, + BitbucketCloudBindingDefinition +} from '../../../../types/alm-settings'; +import AlmTabRenderer from './AlmTabRenderer'; +import BitbucketForm from './BitbucketForm'; + +export interface BitbucketTabRendererProps { + branchesEnabled: boolean; + definitionStatus: T.Dict; + editedDefinition?: BitbucketBindingDefinition | BitbucketCloudBindingDefinition; + definitions: Array; + isCreating: boolean; + loadingAlmDefinitions: boolean; + loadingProjectCount: boolean; + multipleAlmEnabled: boolean; + onCancel: () => void; + onCheck: (definitionKey: string) => void; + onCreate: () => void; + onDelete: (definitionKey: string) => void; + onEdit: (definitionKey: string) => void; + onSelectVariant: (variant: AlmKeys.BitbucketServer | AlmKeys.BitbucketCloud) => void; + onSubmit: ( + config: BitbucketBindingDefinition | BitbucketCloudBindingDefinition, + originalKey: string + ) => void; + submitting: boolean; + success: boolean; + variant?: AlmKeys.BitbucketServer | AlmKeys.BitbucketCloud; +} + +export default function BitbucketTabRenderer(props: BitbucketTabRendererProps) { + const { + branchesEnabled, + editedDefinition, + definitions, + definitionStatus, + isCreating, + loadingAlmDefinitions, + loadingProjectCount, + multipleAlmEnabled, + submitting, + success, + variant + } = props; + + let help; + if (variant === AlmKeys.BitbucketServer) { + help = ( + <> +

    {translate('onboarding.create_project.pat_help.title')}

    + +

    {translate('settings.almintegration.bitbucket.help_1')}

    + +
      +
    • {translate('settings.almintegration.bitbucket.help_2')}
    • +
    • {translate('settings.almintegration.bitbucket.help_3')}
    • +
    + +

    + + {translate('learn_more')} + +

    + + ); + } else if (variant === AlmKeys.BitbucketCloud) { + help = ( + + {translate('learn_more')} + + ) + }} + /> + ); + } + + return ( +
    + ( + + )} + help={help} + loadingAlmDefinitions={loadingAlmDefinitions} + loadingProjectCount={loadingProjectCount} + multipleAlmEnabled={multipleAlmEnabled} + onCancel={props.onCancel} + onCheck={props.onCheck} + onCreate={props.onCreate} + onDelete={props.onDelete} + onEdit={props.onEdit} + onSubmit={props.onSubmit} + submitting={submitting} + success={success} + /> +
    + ); +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/GithubForm.tsx b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/GithubForm.tsx index fe097ae0d60..87cb47d8652 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/GithubForm.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/GithubForm.tsx @@ -74,7 +74,7 @@ export default function GithubForm(props: GithubFormProps) { value={formData.appId} /> { shallowRender({ alm: AlmKeys.Azure, definition: mockAzureBindingDefinition() }) ).toMatchSnapshot('Azure DevOps'); + expect( + shallowRender({ + status: mockAlmSettingsBindingStatus({ + type: AlmSettingsBindingStatusType.Success + }), + alm: AlmKeys.GitLab, + definition: mockGitlabBindingDefinition() + }) + ).toMatchSnapshot('success for GitLab'); + + expect( + shallowRender({ + status: mockAlmSettingsBindingStatus({ + type: AlmSettingsBindingStatusType.Success + }), + alm: AlmKeys.BitbucketCloud, + definition: mockBitbucketCloudBindingDefinition() + }) + ).toMatchSnapshot('success for Bitbucket Cloud'); + expect( shallowRender({ branchesEnabled: false, diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmIntegration-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmIntegration-test.tsx index fe7e883bf6a..59d81720e26 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmIntegration-test.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmIntegration-test.tsx @@ -35,7 +35,7 @@ jest.mock('../../../../../api/alm-settings', () => ({ deleteConfiguration: jest.fn().mockResolvedValue(undefined), getAlmDefinitions: jest .fn() - .mockResolvedValue({ azure: [], bitbucket: [], github: [], gitlab: [] }), + .mockResolvedValue({ azure: [], bitbucket: [], bitbucketcloud: [], github: [], gitlab: [] }), validateAlmSettings: jest.fn().mockResolvedValue('') })); @@ -50,7 +50,8 @@ it('should render correctly', () => { it('should validate existing configurations', async () => { (getAlmDefinitions as jest.Mock).mockResolvedValueOnce({ [AlmKeys.Azure]: [{ key: 'a1' }], - [AlmKeys.Bitbucket]: [{ key: 'b1' }], + [AlmKeys.BitbucketServer]: [{ key: 'b1' }], + [AlmKeys.BitbucketCloud]: [{ key: 'bc1' }], [AlmKeys.GitHub]: [{ key: 'gh1' }, { key: 'gh2' }], [AlmKeys.GitLab]: [{ key: 'gl1' }] }); @@ -59,9 +60,10 @@ it('should validate existing configurations', async () => { await waitAndUpdate(wrapper); - expect(validateAlmSettings).toBeCalledTimes(5); + expect(validateAlmSettings).toBeCalledTimes(6); expect(validateAlmSettings).toBeCalledWith('a1'); expect(validateAlmSettings).toBeCalledWith('b1'); + expect(validateAlmSettings).toBeCalledWith('bc1'); expect(validateAlmSettings).toBeCalledWith('gh1'); expect(validateAlmSettings).toBeCalledWith('gh2'); expect(validateAlmSettings).toBeCalledWith('gl1'); @@ -111,6 +113,7 @@ it('should validate a configuration', async () => { (validateAlmSettings as jest.Mock) .mockRejectedValueOnce(undefined) .mockResolvedValueOnce(failureMessage) + .mockResolvedValueOnce('') .mockResolvedValueOnce(''); await wrapper.instance().handleCheck(definitionKey); @@ -141,7 +144,8 @@ it('should validate a configuration', async () => { it('should fetch settings', async () => { const definitions = { [AlmKeys.Azure]: [{ key: 'a1' }], - [AlmKeys.Bitbucket]: [{ key: 'b1' }], + [AlmKeys.BitbucketServer]: [{ key: 'b1' }], + [AlmKeys.BitbucketCloud]: [{ key: 'bc1' }], [AlmKeys.GitHub]: [{ key: 'gh1' }], [AlmKeys.GitLab]: [{ key: 'gl1' }] }; diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmIntegrationRenderer-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmIntegrationRenderer-test.tsx index cc4a11a4942..6b33832d728 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmIntegrationRenderer-test.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/AlmIntegrationRenderer-test.tsx @@ -31,7 +31,8 @@ it('should render correctly', () => { 'delete modal' ); expect(shallowRender({ currentAlm: AlmKeys.Azure })).toMatchSnapshot('azure'); - expect(shallowRender({ currentAlm: AlmKeys.Bitbucket })).toMatchSnapshot('bitbucket'); + expect(shallowRender({ currentAlm: AlmKeys.BitbucketServer })).toMatchSnapshot('bitbucket'); + expect(shallowRender({ currentAlm: AlmKeys.BitbucketCloud })).toMatchSnapshot('bitbucketcloud'); expect(shallowRender({ currentAlm: AlmKeys.GitLab })).toMatchSnapshot('gitlab'); }); @@ -40,7 +41,7 @@ function shallowRender(props: Partial = {}) { { defaultBinding: mockGithubBindingDefinition(), definitions: [mockGithubBindingDefinition()] }; - expect(shallowRender(githubProps)).toMatchSnapshot(); + expect(shallowRender(githubProps)).toMatchSnapshot('default'); expect(shallowRender({ ...githubProps, definitions: [] })).toMatchSnapshot('empty'); expect( @@ -77,6 +78,13 @@ it('should render correctly with validation', () => { editedDefinition: mockGithubBindingDefinition() }) ).toMatchSnapshot('create a first'); + + expect( + shallowRender({ + alm: AlmKeys.BitbucketServer, // BitbucketServer will be passed for both Bitbucket variants. + definitions: [mockBitbucketCloudBindingDefinition()] + }) + ).toMatchSnapshot('pass the correct key for bitbucket cloud'); }); function shallowRenderAzure(props: Partial> = {}) { diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/BitbucketForm-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/BitbucketForm-test.tsx index 8c758f307b9..daa8f83f497 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/BitbucketForm-test.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/BitbucketForm-test.tsx @@ -19,19 +19,41 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; -import { mockBitbucketBindingDefinition } from '../../../../../helpers/mocks/alm-settings'; +import { + mockBitbucketBindingDefinition, + mockBitbucketCloudBindingDefinition +} from '../../../../../helpers/mocks/alm-settings'; +import { AlmKeys } from '../../../../../types/alm-settings'; import BitbucketForm, { BitbucketFormProps } from '../BitbucketForm'; it('should render correctly', () => { - expect(shallowRender()).toMatchSnapshot(); - expect(shallowRender({ formData: mockBitbucketBindingDefinition() })).toMatchSnapshot(); + expect(shallowRender({ isCreating: true })).toMatchSnapshot('variant select'); + expect(shallowRender()).toMatchSnapshot('bitbucket server, empty'); + expect(shallowRender({ formData: mockBitbucketBindingDefinition() })).toMatchSnapshot( + 'bitbucket server, edit' + ); + expect( + shallowRender({ + formData: { key: '', clientId: '', clientSecret: '', workspace: '' }, + variant: AlmKeys.BitbucketCloud + }) + ).toMatchSnapshot('bitbucket cloud, empty'); + expect( + shallowRender({ + variant: AlmKeys.BitbucketCloud, + formData: mockBitbucketCloudBindingDefinition() + }) + ).toMatchSnapshot('bitbucket cloud, edit'); }); function shallowRender(props: Partial = {}) { return shallow( ); diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/BitbucketTab-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/BitbucketTab-test.tsx index 28551069145..5a95099ac5f 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/BitbucketTab-test.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/BitbucketTab-test.tsx @@ -19,15 +19,123 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; -import { mockBitbucketBindingDefinition } from '../../../../../helpers/mocks/alm-settings'; -import BitbucketTab, { BitbucketTabProps } from '../BitbucketTab'; +import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; +import { + createBitbucketCloudConfiguration, + createBitbucketConfiguration, + updateBitbucketCloudConfiguration, + updateBitbucketConfiguration +} from '../../../../../api/alm-settings'; +import { + mockBitbucketBindingDefinition, + mockBitbucketCloudBindingDefinition +} from '../../../../../helpers/mocks/alm-settings'; +import { AlmKeys } from '../../../../../types/alm-settings'; +import BitbucketTab, { DEFAULT_CLOUD_BINDING, DEFAULT_SERVER_BINDING } from '../BitbucketTab'; + +jest.mock('../../../../../api/alm-settings', () => ({ + createBitbucketConfiguration: jest.fn().mockResolvedValue(null), + createBitbucketCloudConfiguration: jest.fn().mockResolvedValue(null), + updateBitbucketConfiguration: jest.fn().mockResolvedValue(null), + updateBitbucketCloudConfiguration: jest.fn().mockResolvedValue(null) +})); it('should render correctly', () => { expect(shallowRender()).toMatchSnapshot(); }); -function shallowRender(props: Partial = {}) { - return shallow( +it('should handle cancel', async () => { + const wrapper = shallowRender(); + + wrapper.setState({ + editedDefinition: mockBitbucketBindingDefinition() + }); + + wrapper.instance().handleCancel(); + + await waitAndUpdate(wrapper); + + expect(wrapper.state().editedDefinition).toBeUndefined(); +}); + +it('should handle edit', async () => { + const config = mockBitbucketBindingDefinition(); + const wrapper = shallowRender({ definitions: [config] }); + wrapper.instance().handleEdit(config.key); + await waitAndUpdate(wrapper); + expect(wrapper.state().editedDefinition).toEqual(config); +}); + +it('should create config for Bitbucket Server', async () => { + const onUpdateDefinitions = jest.fn(); + const config = mockBitbucketBindingDefinition(); + const wrapper = shallowRender({ onUpdateDefinitions }); + + wrapper.instance().handleCreate(); + wrapper.instance().handleSelectVariant(AlmKeys.BitbucketServer); + expect(wrapper.state().editedDefinition).toBe(DEFAULT_SERVER_BINDING); + + wrapper.setState({ editedDefinition: config }); + await wrapper.instance().handleSubmit(config, ''); + + expect(createBitbucketConfiguration).toBeCalledWith(config); + expect(onUpdateDefinitions).toBeCalled(); + expect(wrapper.state().editedDefinition).toBeUndefined(); +}); + +it('should create config for Bitbucket Cloud', async () => { + const onUpdateDefinitions = jest.fn(); + const config = mockBitbucketCloudBindingDefinition(); + const wrapper = shallowRender({ onUpdateDefinitions }); + + wrapper.instance().handleCreate(); + wrapper.instance().handleSelectVariant(AlmKeys.BitbucketCloud); + expect(wrapper.state().editedDefinition).toBe(DEFAULT_CLOUD_BINDING); + + wrapper.setState({ editedDefinition: config }); + await wrapper.instance().handleSubmit(config, ''); + + expect(createBitbucketCloudConfiguration).toBeCalledWith(config); + expect(onUpdateDefinitions).toBeCalled(); + expect(wrapper.state().editedDefinition).toBeUndefined(); +}); + +it('should update config for Bitbucket Server', async () => { + const onUpdateDefinitions = jest.fn(); + const config = mockBitbucketBindingDefinition(); + const wrapper = shallowRender({ onUpdateDefinitions }); + wrapper.setState({ editedDefinition: config }); + + await wrapper.instance().handleSubmit(config, 'originalKey'); + + expect(updateBitbucketConfiguration).toBeCalledWith({ + newKey: 'key', + ...config, + key: 'originalKey' + }); + expect(onUpdateDefinitions).toBeCalled(); + expect(wrapper.state().editedDefinition).toBeUndefined(); +}); + +it('should update config for Bitbucket Cloud', async () => { + const onUpdateDefinitions = jest.fn(); + const config = mockBitbucketCloudBindingDefinition(); + const wrapper = shallowRender({ onUpdateDefinitions }); + wrapper.setState({ editedDefinition: config }); + + await wrapper.instance().handleSubmit(config, 'originalKey'); + + expect(updateBitbucketCloudConfiguration).toBeCalledWith({ + newKey: 'key', + ...config, + key: 'originalKey' + }); + expect(onUpdateDefinitions).toBeCalled(); + expect(wrapper.state().editedDefinition).toBeUndefined(); +}); + +function shallowRender(props: Partial = {}) { + return shallow( { + expect(shallowRender()).toMatchSnapshot('default'); + expect(shallowRender({ variant: AlmKeys.BitbucketServer })).toMatchSnapshot('bitbucket server'); + expect(shallowRender({ variant: AlmKeys.BitbucketCloud })).toMatchSnapshot('bitbucket cloud'); + + const almTab = shallowRender().find>( + AlmTabRenderer + ); + expect( + almTab.props().form({ formData: mockBitbucketBindingDefinition(), onFieldChange: jest.fn() }) + ).toMatchSnapshot('bitbucket form'); +}); + +function shallowRender(props: Partial = {}) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmBindingDefinitionBox-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmBindingDefinitionBox-test.tsx.snap index 07cd7a44076..4fce50260a7 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmBindingDefinitionBox-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmBindingDefinitionBox-test.tsx.snap @@ -283,6 +283,152 @@ exports[`should render correctly: success 1`] = ` `; +exports[`should render correctly: success for Bitbucket Cloud 1`] = ` +
    +
    + + +
    +
    +

    + key +

    +
    +
    +
    + + + settings.almintegration.feature.pr_decoration.title + + + +
    +
    +
    + +
    +`; + +exports[`should render correctly: success for GitLab 1`] = ` +
    +
    + + +
    +
    +

    + foo +

    +
    +
    +
    + + + settings.almintegration.feature.mr_decoration.title + + + +
    +
    + + + settings.almintegration.feature.alm_repo_import.title + + +
    + + settings.almintegration.feature.alm_repo_import.disabled + + +
    +
    +
    +
    + +
    +`; + exports[`should render correctly: success with alert 1`] = `
    +
    +
    - Bitbucket Server + Bitbucket , "requiresBranchesEnabled": false, }, @@ -133,7 +133,7 @@ exports[`should render correctly: bitbucket 1`] = ` height={16} src="/images/alm/bitbucket.svg" /> - Bitbucket Server + Bitbucket , "requiresBranchesEnabled": false, }, @@ -180,6 +180,85 @@ exports[`should render correctly: bitbucket 1`] = ` `; +exports[`should render correctly: bitbucketcloud 1`] = ` + +
    +

    + settings.almintegration.title +

    +
    +
    + settings.almintegration.description +
    + + github + GitHub + , + "requiresBranchesEnabled": false, + }, + Object { + "key": "bitbucket", + "label": + bitbucket + Bitbucket + , + "requiresBranchesEnabled": false, + }, + Object { + "key": "azure", + "label": + azure + Azure DevOps + , + "requiresBranchesEnabled": false, + }, + Object { + "key": "gitlab", + "label": + gitlab + GitLab + , + "requiresBranchesEnabled": false, + }, + ] + } + /> +
    +`; + exports[`should render correctly: default 1`] = `
    - Bitbucket Server + Bitbucket , "requiresBranchesEnabled": false, }, @@ -313,7 +392,7 @@ exports[`should render correctly: delete modal 1`] = ` height={16} src="/images/alm/bitbucket.svg" /> - Bitbucket Server + Bitbucket , "requiresBranchesEnabled": false, }, @@ -408,7 +487,7 @@ exports[`should render correctly: gitlab 1`] = ` height={16} src="/images/alm/bitbucket.svg" /> - Bitbucket Server + Bitbucket , "requiresBranchesEnabled": false, }, @@ -498,7 +577,7 @@ exports[`should render correctly: loading 1`] = ` height={16} src="/images/alm/bitbucket.svg" /> - Bitbucket Server + Bitbucket , "requiresBranchesEnabled": false, }, diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmTab-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmTab-test.tsx.snap index 76720ff1334..9822401deaf 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmTab-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmTab-test.tsx.snap @@ -4,13 +4,6 @@ exports[`should render correctly 1`] = ` `; -exports[`should render correctly with validation 1`] = ` +exports[`should render correctly with validation: create a first 1`] = `
    +

    + settings.almintegration.empty.github +

    - + help={
    } + isSecondInstance={false} + onCancel={[MockFunction]} + onSubmit={[MockFunction]} + > + +
    `; -exports[`should render correctly with validation: create a first 1`] = ` +exports[`should render correctly with validation: create a second 1`] = `
    -

    - settings.almintegration.empty.github -

    + } - isSecondInstance={false} + isSecondInstance={true} onCancel={[MockFunction]} onSubmit={[MockFunction]} > @@ -446,7 +464,7 @@ exports[`should render correctly with validation: create a first 1`] = `
    `; -exports[`should render correctly with validation: create a second 1`] = ` +exports[`should render correctly with validation: default 1`] = `
    @@ -488,24 +506,6 @@ exports[`should render correctly with validation: create a second 1`] = ` onDelete={[MockFunction]} onEdit={[MockFunction]} /> - } - isSecondInstance={true} - onCancel={[MockFunction]} - onSubmit={[MockFunction]} - > - -
    `; @@ -541,3 +541,47 @@ exports[`should render correctly with validation: empty 1`] = `
    `; + +exports[`should render correctly with validation: pass the correct key for bitbucket cloud 1`] = ` +
    + +
    + + + +
    + +
    +
    +`; diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/BitbucketForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/BitbucketForm-test.tsx.snap index e19b6f4af80..be3c262eca1 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/BitbucketForm-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/BitbucketForm-test.tsx.snap @@ -1,81 +1,255 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should render correctly 1`] = ` - - - +
    + + + https://bitbucket.org/ + + {workspace} + + /{repository} + , + } } - } - /> - } - id="url.bitbucket" - maxLength={2000} - onFieldChange={[MockFunction]} - propKey="url" - value="" - /> - - + /> + } + id="workspace.bitbucketcloud" + maxLength={2000} + onFieldChange={[MockFunction]} + propKey="workspace" + value="workspace" + /> + + +
    +
    `; -exports[`should render correctly 2`] = ` - - - +
    + + + https://bitbucket.org/ + + {workspace} + + /{repository} + , + } + } + /> + } + id="workspace.bitbucketcloud" + maxLength={2000} + onFieldChange={[MockFunction]} + propKey="workspace" + value="" + /> + + +
    + +`; + +exports[`should render correctly: bitbucket server, edit 1`] = ` +
    +
    + + + /> + } + id="url.bitbucket" + maxLength={2000} + onFieldChange={[MockFunction]} + propKey="url" + value="http://bbs.enterprise.com" + /> + +
    +
    +`; + +exports[`should render correctly: bitbucket server, empty 1`] = ` +
    +
    + + + } + id="url.bitbucket" + maxLength={2000} + onFieldChange={[MockFunction]} + propKey="url" + value="" + /> + +
    +
    +`; + +exports[`should render correctly: variant select 1`] = ` +
    + + settings.almintegration.form.choose_bitbucket_variant + + - - +
    + + + } + id="url.bitbucket" + maxLength={2000} + onFieldChange={[MockFunction]} + propKey="url" + value="" + /> + +
    +
    `; diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/BitbucketTab-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/BitbucketTab-test.tsx.snap index e7b2a23b1da..8100d78442d 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/BitbucketTab-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/BitbucketTab-test.tsx.snap @@ -1,72 +1,30 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should render correctly 1`] = ` -
    - -

    - onboarding.create_project.pat_help.title -

    -

    - settings.almintegration.bitbucket.help_1 -

    -
      -
    • - settings.almintegration.bitbucket.help_2 -
    • -
    • - settings.almintegration.bitbucket.help_3 -
    • -
    -

    - - learn_more - -

    - - } - loadingAlmDefinitions={false} - loadingProjectCount={false} - multipleAlmEnabled={true} - onCheck={[MockFunction]} - onDelete={[MockFunction]} - onUpdateDefinitions={[MockFunction]} - updateConfiguration={[Function]} - /> -
    + "key": "key", + "personalAccessToken": "asdf1234", + "url": "http://bbs.enterprise.com", + }, + ] + } + isCreating={false} + loadingAlmDefinitions={false} + loadingProjectCount={false} + multipleAlmEnabled={true} + onCancel={[Function]} + onCheck={[MockFunction]} + onCreate={[Function]} + onDelete={[MockFunction]} + onEdit={[Function]} + onSelectVariant={[Function]} + onSubmit={[Function]} + submitting={false} + success={false} +/> `; diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/BitbucketTabRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/BitbucketTabRenderer-test.tsx.snap new file mode 100644 index 00000000000..e742c9c3f31 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/BitbucketTabRenderer-test.tsx.snap @@ -0,0 +1,143 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly: bitbucket cloud 1`] = ` +
    + + learn_more + , + } + } + /> + } + loadingAlmDefinitions={false} + loadingProjectCount={false} + multipleAlmEnabled={true} + onCancel={[MockFunction]} + onCheck={[MockFunction]} + onCreate={[MockFunction]} + onDelete={[MockFunction]} + onEdit={[MockFunction]} + onSubmit={[MockFunction]} + submitting={true} + success={false} + /> +
    +`; + +exports[`should render correctly: bitbucket form 1`] = ` + +`; + +exports[`should render correctly: bitbucket server 1`] = ` +
    + +

    + onboarding.create_project.pat_help.title +

    +

    + settings.almintegration.bitbucket.help_1 +

    +
      +
    • + settings.almintegration.bitbucket.help_2 +
    • +
    • + settings.almintegration.bitbucket.help_3 +
    • +
    +

    + + learn_more + +

    + + } + loadingAlmDefinitions={false} + loadingProjectCount={false} + multipleAlmEnabled={true} + onCancel={[MockFunction]} + onCheck={[MockFunction]} + onCreate={[MockFunction]} + onDelete={[MockFunction]} + onEdit={[MockFunction]} + onSubmit={[MockFunction]} + submitting={true} + success={false} + /> +
    +`; + +exports[`should render correctly: default 1`] = ` +
    + +
    +`; diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/GithubForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/GithubForm-test.tsx.snap index 6ae40e3ac21..79359f8817a 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/GithubForm-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/GithubForm-test.tsx.snap @@ -41,14 +41,14 @@ exports[`should render correctly 1`] = ` value="" /> ); - case AlmKeys.Bitbucket: + case AlmKeys.BitbucketServer: return ( <> {renderField({ diff --git a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/PRDecorationBinding.tsx b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/PRDecorationBinding.tsx index 3d9a226923e..4f4d34a1564 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/PRDecorationBinding.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/PRDecorationBinding.tsx @@ -65,7 +65,8 @@ const REQUIRED_FIELDS_BY_ALM: { [almKey in AlmKeys]: Array>; } = { [AlmKeys.Azure]: ['repository', 'slug'], - [AlmKeys.Bitbucket]: ['repository', 'slug'], + [AlmKeys.BitbucketServer]: ['repository', 'slug'], + [AlmKeys.BitbucketCloud]: ['repository'], [AlmKeys.GitHub]: ['repository'], [AlmKeys.GitLab]: ['repository'] }; @@ -180,7 +181,7 @@ export class PRDecorationBinding extends React.PureComponent { const instances: AlmSettingsInstance[] = [ { key: 'github', alm: AlmKeys.GitHub }, { key: 'azure', alm: AlmKeys.Azure }, - { key: 'bitbucket', alm: AlmKeys.Bitbucket }, + { key: 'bitbucket', alm: AlmKeys.BitbucketServer }, { key: 'gitlab', alm: AlmKeys.GitLab } ]; @@ -263,9 +263,9 @@ it.each([ [AlmKeys.Azure, {}], [AlmKeys.Azure, { slug: 'test' }], [AlmKeys.Azure, { repository: 'test' }], - [AlmKeys.Bitbucket, {}], - [AlmKeys.Bitbucket, { slug: 'test' }], - [AlmKeys.Bitbucket, { repository: 'test' }], + [AlmKeys.BitbucketServer, {}], + [AlmKeys.BitbucketServer, { slug: 'test' }], + [AlmKeys.BitbucketServer, { repository: 'test' }], [AlmKeys.GitHub, {}], [AlmKeys.GitLab, {}] ])('should properly reject promise for %s & %s', async (almKey: AlmKeys, params: {}) => { @@ -289,7 +289,7 @@ it('should validate form', async () => { wrapper.setState({ instances: [ { key: 'azure', alm: AlmKeys.Azure }, - { key: 'bitbucket', alm: AlmKeys.Bitbucket }, + { key: 'bitbucket', alm: AlmKeys.BitbucketServer }, { key: 'github', alm: AlmKeys.GitHub }, { key: 'gitlab', alm: AlmKeys.GitLab } ] diff --git a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/PRDecorationBindingRenderer-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/PRDecorationBindingRenderer-test.tsx index 9bb9c158a99..b0520110cf0 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/PRDecorationBindingRenderer-test.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/PRDecorationBindingRenderer-test.tsx @@ -59,7 +59,7 @@ it('should render multiple instances correctly', () => { url: urls[0] }, { - alm: AlmKeys.Bitbucket, + alm: AlmKeys.BitbucketServer, key: 'i3', url: urls[1] }, diff --git a/server/sonar-web/src/main/js/components/tutorials/TutorialSelectionRenderer.tsx b/server/sonar-web/src/main/js/components/tutorials/TutorialSelectionRenderer.tsx index 3c4cc3ac579..40674894fd5 100644 --- a/server/sonar-web/src/main/js/components/tutorials/TutorialSelectionRenderer.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/TutorialSelectionRenderer.tsx @@ -45,7 +45,7 @@ export default function TutorialSelectionRenderer(props: TutorialSelectionRender } const jenkinsAvailable = - projectBinding && [AlmKeys.Bitbucket, AlmKeys.GitHub].includes(projectBinding.alm); + projectBinding && [AlmKeys.BitbucketServer, AlmKeys.GitHub].includes(projectBinding.alm); return ( <> diff --git a/server/sonar-web/src/main/js/components/tutorials/__tests__/TutorialSelection-test.tsx b/server/sonar-web/src/main/js/components/tutorials/__tests__/TutorialSelection-test.tsx index 5c593a74ac8..76b4dbe9d79 100644 --- a/server/sonar-web/src/main/js/components/tutorials/__tests__/TutorialSelection-test.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/__tests__/TutorialSelection-test.tsx @@ -50,7 +50,7 @@ it('should select manual if project is not bound', async () => { }); it('should not select anything if project is bound', async () => { - (getProjectAlmBinding as jest.Mock).mockResolvedValueOnce({ alm: AlmKeys.Bitbucket }); + (getProjectAlmBinding as jest.Mock).mockResolvedValueOnce({ alm: AlmKeys.BitbucketServer }); const wrapper = shallowRender(); await waitAndUpdate(wrapper); expect(wrapper.state().forceManual).toBe(false); @@ -59,9 +59,9 @@ it('should not select anything if project is bound', async () => { it('should correctly find the global ALM binding definition', async () => { const key = 'foo'; const almBinding = mockBitbucketBindingDefinition({ key }); - (getProjectAlmBinding as jest.Mock).mockResolvedValueOnce({ alm: AlmKeys.Bitbucket, key }); + (getProjectAlmBinding as jest.Mock).mockResolvedValueOnce({ alm: AlmKeys.BitbucketServer, key }); (getAlmDefinitionsNoCatch as jest.Mock).mockResolvedValueOnce({ - [AlmKeys.Bitbucket]: [almBinding] + [AlmKeys.BitbucketServer]: [almBinding] }); const wrapper = shallowRender(); await waitAndUpdate(wrapper); diff --git a/server/sonar-web/src/main/js/components/tutorials/jenkins/WebhookStep.tsx b/server/sonar-web/src/main/js/components/tutorials/jenkins/WebhookStep.tsx index a3373234346..2349c1f90e8 100644 --- a/server/sonar-web/src/main/js/components/tutorials/jenkins/WebhookStep.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/jenkins/WebhookStep.tsx @@ -46,7 +46,7 @@ function renderAlmSpecificInstructions(props: WebhookStepProps) { const { almBinding, branchesEnabled, projectBinding } = props; switch (projectBinding.alm) { - case AlmKeys.Bitbucket: + case AlmKeys.BitbucketServer: return ( { function shallowRender(props: Partial = {}) { return shallow( = {} +): BitbucketCloudBindingDefinition { + return { + key: 'key', + clientId: 'client1', + clientSecret: '**clientsecret**', + workspace: 'workspace', + ...overrides + }; +} + export function mockGithubBindingDefinition( overrides: Partial = {} ): GithubBindingDefinition { @@ -102,7 +115,7 @@ export function mockProjectBitbucketBindingResponse( overrides: Partial = {} ): ProjectBitbucketBindingResponse { return { - alm: AlmKeys.Bitbucket, + alm: AlmKeys.BitbucketServer, key: 'foo', repository: 'PROJECT_KEY', slug: 'repo-slug', diff --git a/server/sonar-web/src/main/js/types/alm-settings.ts b/server/sonar-web/src/main/js/types/alm-settings.ts index ce3c5a0cb90..0e81f6d70a3 100644 --- a/server/sonar-web/src/main/js/types/alm-settings.ts +++ b/server/sonar-web/src/main/js/types/alm-settings.ts @@ -19,7 +19,8 @@ */ export const enum AlmKeys { Azure = 'azure', - Bitbucket = 'bitbucket', + BitbucketServer = 'bitbucket', + BitbucketCloud = 'bitbucketcloud', GitHub = 'github', GitLab = 'gitlab' } @@ -39,6 +40,12 @@ export interface BitbucketBindingDefinition extends AlmBindingDefinition { url: string; } +export interface BitbucketCloudBindingDefinition extends AlmBindingDefinition { + clientId: string; + clientSecret: string; + workspace: string; +} + export interface GithubBindingDefinition extends AlmBindingDefinition { appId: string; clientId: string; @@ -70,7 +77,7 @@ export interface ProjectAzureBindingResponse extends ProjectAlmBindingResponse { } export interface ProjectBitbucketBindingResponse extends ProjectAlmBindingResponse { - alm: AlmKeys.Bitbucket; + alm: AlmKeys.BitbucketServer; repository: string; slug: string; monorepo: boolean; @@ -125,7 +132,8 @@ export interface AlmSettingsInstance { export interface AlmSettingsBindingDefinitions { [AlmKeys.Azure]: AzureBindingDefinition[]; - [AlmKeys.Bitbucket]: BitbucketBindingDefinition[]; + [AlmKeys.BitbucketServer]: BitbucketBindingDefinition[]; + [AlmKeys.BitbucketCloud]: BitbucketCloudBindingDefinition[]; [AlmKeys.GitHub]: GithubBindingDefinition[]; [AlmKeys.GitLab]: GitlabBindingDefinition[]; } @@ -146,7 +154,7 @@ export enum AlmSettingsBindingStatusType { export function isProjectBitbucketBindingResponse( binding: ProjectAlmBindingResponse ): binding is ProjectBitbucketBindingResponse { - return binding.alm === AlmKeys.Bitbucket; + return binding.alm === AlmKeys.BitbucketServer; } export function isProjectGitHubBindingResponse( @@ -168,20 +176,19 @@ export function isProjectAzureBindingResponse( } export function isBitbucketBindingDefinition( - binding?: AlmBindingDefinition & { url?: string; personalAccessToken?: string } + binding?: AlmBindingDefinition & { url?: string } ): binding is BitbucketBindingDefinition { - return ( - binding !== undefined && binding.url !== undefined && binding.personalAccessToken !== undefined - ); + return binding !== undefined && binding.url !== undefined; +} + +export function isBitbucketCloudBindingDefinition( + binding?: AlmBindingDefinition & { clientId?: string; workspace?: string } +): binding is BitbucketCloudBindingDefinition { + return binding !== undefined && binding.clientId !== undefined && binding.workspace !== undefined; } export function isGithubBindingDefinition( - binding?: AlmBindingDefinition & { appId?: string; privateKey?: string; url?: string } + binding?: AlmBindingDefinition & { appId?: string; url?: string } ): binding is GithubBindingDefinition { - return ( - binding !== undefined && - binding.appId !== undefined && - binding.privateKey !== undefined && - binding.url !== undefined - ); + return binding !== undefined && binding.appId !== undefined && binding.url !== undefined; } diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 3aff37f6d0a..779baa23258 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -361,10 +361,10 @@ Sa=Sa alm.azure=Azure DevOps alm.azure.short=Azure DevOps -alm.bitbucket=Bitbucket Server +alm.bitbucket=Bitbucket alm.bitbucket.short=Bitbucket -alm.github=Github -alm.github.short=Github +alm.github=GitHub +alm.github.short=GitHub alm.gitlab=GitLab alm.gitlab.short=GitLab @@ -1075,8 +1075,10 @@ settings.almintegration.gitlab.info=Accounts that will be used to decorate Merge settings.almintegration.bitbucket.help_1=SonarQube needs a Personal Access Token to communicate with Bitbucket Server. This token will be used to decorate Pull Requests. settings.almintegration.bitbucket.help_2=The account used for integration needs write permission. settings.almintegration.bitbucket.help_3=We recommend to integrate with SonarQube using a Bitbucket Server Service Account. +settings.almintegration.bitbucketcloud.info=You need to create an OAuth consumer in your Bitbucket Cloud workspace settings to decorate your Pull Requests. It needs to be a private consumer with Pull requests: Read permission. Bitbucket requires an OAuth callback URL, but it's not used by SonarQube so any URL works. {link} settings.almintegration.empty.azure=Create your first Azure DevOps configuration to start analyzing your repositories on SonarQube. settings.almintegration.empty.bitbucket=Create your first Bitbucket configuration to start analyzing your repositories on SonarQube. +settings.almintegration.empty.bitbucketcloud=Create your first Bitbucket Cloud configuration to start analyzing your repositories on SonarQube. settings.almintegration.empty.github=Create your first GitHub configuration to start analyzing your repositories on SonarQube. settings.almintegration.empty.gitlab=Create your first GitLab configuration to start analyzing your repositories on SonarQube. settings.almintegration.create=Create configuration @@ -1095,12 +1097,17 @@ settings.almintegration.form.header.edit=Edit the configuration settings.almintegration.form.second_instance_warning=Binding more than one instance of an ALM will deactivate the import of repositories from that ALM. settings.almintegration.form.name.azure=Configuration name settings.almintegration.form.name.azure.help=Give your configuration a clear and succinct name. This name will be used at project level to identify the correct configured Azure instance for a project. +settings.almintegration.form.choose_bitbucket_variant=Select which variant you want to configure settings.almintegration.form.name.bitbucket=Configuration name settings.almintegration.form.name.bitbucket.help=Give your configuration a clear and succinct name. This name will be used at project level to identify the correct configured Bitbucket instance for a project. +settings.almintegration.form.name.bitbucketcloud=Configuration name +settings.almintegration.form.name.bitbucketcloud.help=Give your configuration a clear and succinct name. This name will be used at project level to identify the correct configured Bitbucket Cloud instance for a project. settings.almintegration.form.name.github=Configuration name settings.almintegration.form.name.github.help=Give your configuration a clear and succinct name. This name will be used at project level to identify the correct configured GitHub App for a project. settings.almintegration.form.name.gitlab=Configuration name settings.almintegration.form.name.gitlab.help=Give your configuration a clear and succinct name. This name will be used at project level to identify the correct configured GitLab instance for a project. +settings.almintegration.form.workspace.bitbucketcloud=Workspace ID +settings.almintegration.form.workspace.bitbucketcloud.help=The Workspace ID settings.almintegration.form.url.azure=Azure DevOps URL settings.almintegration.form.url.azure.help1=For Azure DevOps Server, provide the full collection URL: settings.almintegration.form.url.azure.help2=For Azure DevOps Services, provide the full organization URL: @@ -1112,8 +1119,10 @@ settings.almintegration.form.url.github.help2=If using GitHub.com: settings.almintegration.form.url.gitlab=GitLab API URL settings.almintegration.form.url.gitlab.help=Provide the GitLab API URL. For example: settings.almintegration.form.app_id=GitHub App ID -settings.almintegration.form.client_id=GitHub Client ID -settings.almintegration.form.client_secret=GitHub Client Secret +settings.almintegration.form.client_id.github=Client ID +settings.almintegration.form.client_secret.github=Client Secret +settings.almintegration.form.client_id.bitbucketcloud=OAuth Key +settings.almintegration.form.client_secret.bitbucketcloud=OAuth Secret settings.almintegration.form.private_key=Private Key settings.almintegration.form.personal_access_token=Personal Access token settings.almintegration.form.personal_access_token.azure.help=Token of the user that will be used to decorate the Pull Requests. Needs authorized scope: "Code (read and write)".