@@ -26,5 +26,5 @@ import org.sonar.db.organization.OrganizationDto; | |||
@ServerSide | |||
public interface OrganizationAlmBinding { | |||
void bindOrganization(DbSession dbSession, OrganizationDto organization, String installationId, boolean enableMembersSync); | |||
void bindOrganization(DbSession dbSession, OrganizationDto organization, String installationId, boolean isNewOrganization); | |||
} |
@@ -24,6 +24,7 @@ import DeferredSpinner from '../../../components/common/DeferredSpinner'; | |||
import { Alert } from '../../../components/ui/Alert'; | |||
import { SubmitButton } from '../../../components/ui/buttons'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import { isGithub } from '../../../helpers/almIntegrations'; | |||
interface Props { | |||
almKey: string; | |||
@@ -79,6 +80,7 @@ export default class AutoOrganizationBind extends React.PureComponent<Props, Sta | |||
}; | |||
render() { | |||
const { almKey } = this.props; | |||
const { organization, submitting } = this.state; | |||
return ( | |||
<form id="bind-organization-form" onSubmit={this.handleSubmit}> | |||
@@ -87,18 +89,20 @@ export default class AutoOrganizationBind extends React.PureComponent<Props, Sta | |||
organization={organization} | |||
organizations={this.props.unboundOrganizations} | |||
/> | |||
<Alert className="abs-width-400 big-spacer-top" display="block" variant="info"> | |||
{translateWithParameters( | |||
'onboarding.import_organization.bind_members_not_sync_info_x', | |||
translate('organization', this.props.almKey) | |||
)} | |||
<Link | |||
className="spacer-left" | |||
target="_blank" | |||
to={{ pathname: '/documentation/organizations/manage-team/' }}> | |||
{translate('learn_more')} | |||
</Link> | |||
</Alert> | |||
{isGithub(almKey) && ( | |||
<Alert className="abs-width-400 big-spacer-top" display="block" variant="info"> | |||
{translateWithParameters( | |||
'onboarding.import_organization.bind_members_not_sync_info_x', | |||
translate('organization', almKey) | |||
)} | |||
<Link | |||
className="spacer-left" | |||
target="_blank" | |||
to={{ pathname: '/documentation/organizations/manage-team/' }}> | |||
{translate('learn_more')} | |||
</Link> | |||
</Alert> | |||
)} | |||
<div className="display-flex-center big-spacer-top"> | |||
<SubmitButton disabled={submitting || !organization}> | |||
{translate('onboarding.import_organization.bind')} |
@@ -28,7 +28,7 @@ import { Alert } from '../../../components/ui/Alert'; | |||
import { DeleteButton } from '../../../components/ui/buttons'; | |||
import RadioToggle from '../../../components/controls/RadioToggle'; | |||
import { bindAlmOrganization } from '../../../api/alm-integration'; | |||
import { sanitizeAlmId, getAlmMembersUrl } from '../../../helpers/almIntegrations'; | |||
import { sanitizeAlmId, getAlmMembersUrl, isGithub } from '../../../helpers/almIntegrations'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import { getBaseUrl } from '../../../helpers/urls'; | |||
@@ -164,25 +164,27 @@ export default class AutoOrganizationCreate extends React.PureComponent<Props, S | |||
{filter === Filters.Create && ( | |||
<OrganizationDetailsForm | |||
infoBlock={ | |||
<Alert className="abs-width-600 big-spacer-top" display="block" variant="info"> | |||
<p> | |||
{translateWithParameters( | |||
'onboarding.import_organization.members_sync_info_x', | |||
translate('organization', almKey), | |||
almOrganization.name, | |||
translate(almKey) | |||
)} | |||
</p> | |||
<a | |||
href={getAlmMembersUrl(almApplication.key, almOrganization.almUrl)} | |||
rel="noopener noreferrer" | |||
target="_blank"> | |||
{translateWithParameters( | |||
'organization.members.see_all_members_on_x', | |||
translate(almKey) | |||
)} | |||
</a> | |||
</Alert> | |||
isGithub(almKey) && ( | |||
<Alert className="abs-width-600 big-spacer-top" display="block" variant="info"> | |||
<p> | |||
{translateWithParameters( | |||
'onboarding.import_organization.members_sync_info_x', | |||
translate('organization', almKey), | |||
almOrganization.name, | |||
translate(almKey) | |||
)} | |||
</p> | |||
<a | |||
href={getAlmMembersUrl(almApplication.key, almOrganization.almUrl)} | |||
rel="noopener noreferrer" | |||
target="_blank"> | |||
{translateWithParameters( | |||
'organization.members.see_all_members_on_x', | |||
translate(almKey) | |||
)} | |||
</a> | |||
</Alert> | |||
) | |||
} | |||
onContinue={this.props.handleOrgDetailsFinish} | |||
organization={almOrganization} |
@@ -32,6 +32,14 @@ it('should render correctly', () => { | |||
expect(onBindOrganization).toHaveBeenCalled(); | |||
}); | |||
it('should not show member sync info box for Bitbucket', () => { | |||
expect( | |||
shallowRender({ almKey: 'bitbucket' }) | |||
.find('Alert') | |||
.exists() | |||
).toBe(false); | |||
}); | |||
function shallowRender(props: Partial<AutoOrganizationBind['props']> = {}) { | |||
return shallow( | |||
<AutoOrganizationBind |
@@ -34,10 +34,8 @@ const organization = mockAlmOrganization(); | |||
it('should render prefilled and create org', async () => { | |||
const createOrganization = jest.fn().mockResolvedValue({ key: 'foo' }); | |||
const handleOrgDetailsFinish = jest.fn(); | |||
const almApplication = mockAlmApplication({ key: 'github' }); | |||
const almOrganization = mockAlmOrganization({ almUrl: 'http://github.com/thing' }); | |||
const wrapper = shallowRender({ | |||
almApplication, | |||
almOrganization, | |||
createOrganization, | |||
handleOrgDetailsFinish | |||
@@ -96,6 +94,14 @@ it('should bind existing organization', async () => { | |||
expect(onOrgCreated).toHaveBeenCalledWith('foo'); | |||
}); | |||
it('should not show member sync info box for Bitbucket', () => { | |||
expect( | |||
shallowRender({ almApplication: mockAlmApplication({ key: 'bitbucket-cloud' }) }) | |||
.find('Alert') | |||
.exists() | |||
).toBe(false); | |||
}); | |||
function shallowRender(props: Partial<AutoOrganizationCreate['props']> = {}) { | |||
return shallow( | |||
<AutoOrganizationCreate |
@@ -20,9 +20,9 @@ exports[`should display choice between import or creation 1`] = ` | |||
values={ | |||
Object { | |||
"avatar": <img | |||
alt="BitBucket" | |||
alt="GitHub" | |||
className="little-spacer-left" | |||
src="/images/sonarcloud/bitbucket.svg" | |||
src="/images/sonarcloud/github.svg" | |||
width={16} | |||
/>, | |||
"name": <strong> | |||
@@ -59,11 +59,11 @@ exports[`should display choice between import or creation 1`] = ` | |||
<PlanStep | |||
almApplication={ | |||
Object { | |||
"backgroundColor": "#0052CC", | |||
"iconPath": "\\"/static/authbitbucket/bitbucket.svg\\"", | |||
"installationUrl": "https://bitbucket.org/install/app", | |||
"key": "bitbucket", | |||
"name": "BitBucket", | |||
"backgroundColor": "#444444", | |||
"iconPath": "/images/sonarcloud/github-white.svg", | |||
"installationUrl": "https://github.com/apps/greg-sonarcloud/installations/new", | |||
"key": "github", | |||
"name": "GitHub", | |||
} | |||
} | |||
almOrganization={ | |||
@@ -119,7 +119,7 @@ exports[`should render prefilled and create org 1`] = ` | |||
values={ | |||
Object { | |||
"avatar": <img | |||
alt="BitBucket" | |||
alt="GitHub" | |||
className="little-spacer-left" | |||
src="/images/sonarcloud/github.svg" | |||
width={16} | |||
@@ -175,11 +175,11 @@ exports[`should render prefilled and create org 1`] = ` | |||
<PlanStep | |||
almApplication={ | |||
Object { | |||
"backgroundColor": "#0052CC", | |||
"iconPath": "\\"/static/authbitbucket/bitbucket.svg\\"", | |||
"installationUrl": "https://bitbucket.org/install/app", | |||
"backgroundColor": "#444444", | |||
"iconPath": "/images/sonarcloud/github-white.svg", | |||
"installationUrl": "https://github.com/apps/greg-sonarcloud/installations/new", | |||
"key": "github", | |||
"name": "BitBucket", | |||
"name": "GitHub", | |||
} | |||
} | |||
almOrganization={ |
@@ -24,7 +24,7 @@ import AddMemberForm from './AddMemberForm'; | |||
import SyncMemberForm from './SyncMemberForm'; | |||
import DeferredSpinner from '../../components/common/DeferredSpinner'; | |||
import DocTooltip from '../../components/docs/DocTooltip'; | |||
import { sanitizeAlmId } from '../../helpers/almIntegrations'; | |||
import { sanitizeAlmId, isGithub } from '../../helpers/almIntegrations'; | |||
import { translate, translateWithParameters } from '../../helpers/l10n'; | |||
import { Alert } from '../../components/ui/Alert'; | |||
@@ -51,6 +51,7 @@ export default function MembersPageHeader(props: Props) { | |||
{isAdmin && ( | |||
<div className="page-actions text-right"> | |||
{almKey && | |||
isGithub(almKey) && | |||
!showSyncNotif && ( | |||
<SyncMemberForm organization={organization} refreshMembers={refreshMembers} /> | |||
)} | |||
@@ -82,6 +83,7 @@ export default function MembersPageHeader(props: Props) { | |||
}} | |||
/> | |||
{almKey && | |||
isGithub(almKey) && | |||
showSyncNotif && ( | |||
<Alert className="spacer-top" display="inline" variant="info"> | |||
{translateWithParameters( |
@@ -25,7 +25,7 @@ import RadioCard from '../../components/controls/RadioCard'; | |||
import { Alert } from '../../components/ui/Alert'; | |||
import { Button } from '../../components/ui/buttons'; | |||
import { setOrganizationMemberSync, syncMembers } from '../../api/organizations'; | |||
import { sanitizeAlmId, isGithub } from '../../helpers/almIntegrations'; | |||
import { sanitizeAlmId } from '../../helpers/almIntegrations'; | |||
import { translate, translateWithParameters } from '../../helpers/l10n'; | |||
import { fetchOrganization } from '../../store/rootActions'; | |||
@@ -56,7 +56,7 @@ export class SyncMemberForm extends React.PureComponent<Props, State> { | |||
enabled: membersSync | |||
}).then(() => { | |||
this.props.fetchOrganization(organization.key); | |||
if (membersSync && isGithub(organization.alm && organization.alm.key)) { | |||
if (membersSync) { | |||
return this.handleMemberSync(); | |||
} | |||
return Promise.resolve(); |
@@ -36,7 +36,14 @@ it('should render for admin', () => { | |||
).toMatchSnapshot(); | |||
}); | |||
it('should render for bound organization without sync', () => { | |||
it('should render for Bitbucket bound organization', () => { | |||
const organization = mockOrganizationWithAlm(mockOrganizationWithAdminActions(), { | |||
key: 'bitbucket' | |||
}); | |||
expect(shallowRender({ organization })).toMatchSnapshot(); | |||
}); | |||
it('should render for GitHub bound organization without sync', () => { | |||
const organization = mockOrganizationWithAlm(mockOrganizationWithAdminActions()); | |||
expect(shallowRender({ organization })).toMatchSnapshot(); | |||
}); |
@@ -33,7 +33,7 @@ beforeEach(() => { | |||
jest.clearAllMocks(); | |||
}); | |||
it('should allow to switch to automatic mode with github', async () => { | |||
it('should allow to switch to automatic mode', async () => { | |||
const fetchOrganization = jest.fn(); | |||
const refreshMembers = jest.fn().mockResolvedValue({}); | |||
const wrapper = shallowRender({ fetchOrganization, refreshMembers }); | |||
@@ -49,23 +49,6 @@ it('should allow to switch to automatic mode with github', async () => { | |||
expect(refreshMembers).toBeCalled(); | |||
}); | |||
it('should allow to switch to automatic mode with bitbucket', async () => { | |||
const fetchOrganization = jest.fn(); | |||
const wrapper = shallowRender({ | |||
fetchOrganization, | |||
organization: mockOrganizationWithAlm({}, { key: 'bitbucket' }) | |||
}); | |||
expect(wrapper).toMatchSnapshot(); | |||
wrapper.setState({ membersSync: true }); | |||
wrapper.find('ConfirmButton').prop<Function>('onConfirm')(); | |||
expect(setOrganizationMemberSync).toHaveBeenCalledWith({ organization: 'foo', enabled: true }); | |||
await waitAndUpdate(wrapper); | |||
expect(fetchOrganization).toHaveBeenCalledWith('foo'); | |||
expect(syncMembers).not.toHaveBeenCalled(); | |||
}); | |||
it('should allow to switch to manual mode', async () => { | |||
const fetchOrganization = jest.fn(); | |||
const wrapper = shallowRender({ |
@@ -36,7 +36,7 @@ exports[`should render correctly 1`] = ` | |||
</header> | |||
`; | |||
exports[`should render for admin 1`] = ` | |||
exports[`should render for Bitbucket bound organization 1`] = ` | |||
<header | |||
className="page-header" | |||
> | |||
@@ -63,6 +63,11 @@ exports[`should render for admin 1`] = ` | |||
"actions": Object { | |||
"admin": true, | |||
}, | |||
"alm": Object { | |||
"key": "bitbucket", | |||
"membersSync": false, | |||
"url": "https://github.com/foo", | |||
}, | |||
"key": "foo", | |||
"name": "Foo", | |||
} | |||
@@ -97,7 +102,7 @@ exports[`should render for admin 1`] = ` | |||
</header> | |||
`; | |||
exports[`should render for bound organization without sync 1`] = ` | |||
exports[`should render for GitHub bound organization without sync 1`] = ` | |||
<header | |||
className="page-header" | |||
> | |||
@@ -190,3 +195,64 @@ exports[`should render for bound organization without sync 1`] = ` | |||
</div> | |||
</header> | |||
`; | |||
exports[`should render for admin 1`] = ` | |||
<header | |||
className="page-header" | |||
> | |||
<h1 | |||
className="page-title" | |||
> | |||
organization.members.page | |||
</h1> | |||
<DeferredSpinner | |||
loading={false} | |||
timeout={100} | |||
/> | |||
<div | |||
className="page-actions text-right" | |||
> | |||
<div | |||
className="display-inline-block spacer-left spacer-bottom" | |||
> | |||
<AddMemberForm | |||
addMember={[MockFunction]} | |||
memberLogins={Array []} | |||
organization={ | |||
Object { | |||
"actions": Object { | |||
"admin": true, | |||
}, | |||
"key": "foo", | |||
"name": "Foo", | |||
} | |||
} | |||
/> | |||
<DocTooltip | |||
className="spacer-left" | |||
doc={Promise {}} | |||
/> | |||
</div> | |||
</div> | |||
<div | |||
className="page-description" | |||
> | |||
<FormattedMessage | |||
defaultMessage="organization.members.page.description" | |||
id="organization.members.page.description" | |||
values={ | |||
Object { | |||
"link": <Link | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
target="_blank" | |||
to="/documentation/organizations/manage-team/" | |||
> | |||
organization.members.manage_a_team | |||
</Link>, | |||
} | |||
} | |||
/> | |||
</div> | |||
</header> | |||
`; |
@@ -1,102 +1,6 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should allow to switch to automatic mode with bitbucket 1`] = ` | |||
<ConfirmButton | |||
cancelButtonText="close" | |||
confirmButtonText="save" | |||
confirmDisable={true} | |||
modalBody={ | |||
<div | |||
className="display-flex-stretch big-spacer-top" | |||
> | |||
<RadioCard | |||
onClick={[Function]} | |||
selected={true} | |||
title="organization.members.management.manual" | |||
> | |||
<div | |||
className="spacer-left" | |||
> | |||
<ul | |||
className="big-spacer-left note" | |||
> | |||
<li | |||
className="spacer-bottom" | |||
> | |||
organization.members.management.manual.add_members_manually | |||
</li> | |||
<li> | |||
organization.members.management.choose_members_permissions | |||
</li> | |||
</ul> | |||
</div> | |||
</RadioCard> | |||
<RadioCard | |||
onClick={[Function]} | |||
selected={false} | |||
title="organization.members.management.automatic.bitbucket" | |||
> | |||
<div | |||
className="spacer-left" | |||
> | |||
<ul | |||
className="big-spacer-left note" | |||
> | |||
<React.Fragment> | |||
<li | |||
className="spacer-bottom" | |||
> | |||
organization.members.management.automatic.synchronized_from_x.organization.bitbucket | |||
</li> | |||
<li | |||
className="spacer-bottom" | |||
> | |||
organization.members.management.automatic.members_changes_reflected.bitbucket | |||
</li> | |||
</React.Fragment> | |||
<li> | |||
organization.members.management.choose_members_permissions | |||
</li> | |||
</ul> | |||
</div> | |||
<Alert | |||
className="big-spacer-top" | |||
variant="warning" | |||
> | |||
organization.members.management.automatic.warning | |||
</Alert> | |||
</RadioCard> | |||
</div> | |||
} | |||
modalHeader="organization.members.management.title" | |||
modalHeaderDescription={ | |||
<p | |||
className="spacer-top" | |||
> | |||
organization.members.management.description | |||
<Link | |||
className="spacer-left" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
target="_blank" | |||
to={ | |||
Object { | |||
"pathname": "/documentation/organizations/manage-team/", | |||
} | |||
} | |||
> | |||
learn_more | |||
</Link> | |||
</p> | |||
} | |||
onConfirm={[Function]} | |||
size="medium" | |||
> | |||
<Component /> | |||
</ConfirmButton> | |||
`; | |||
exports[`should allow to switch to automatic mode with github 1`] = ` | |||
exports[`should allow to switch to automatic mode 1`] = ` | |||
<ConfirmButton | |||
cancelButtonText="close" | |||
confirmButtonText="save" |
@@ -23,11 +23,11 @@ import { Profile } from '../apps/quality-profiles/types'; | |||
export function mockAlmApplication(overrides: Partial<T.AlmApplication> = {}): T.AlmApplication { | |||
return { | |||
backgroundColor: '#0052CC', | |||
iconPath: '"/static/authbitbucket/bitbucket.svg"', | |||
installationUrl: 'https://bitbucket.org/install/app', | |||
key: 'bitbucket', | |||
name: 'BitBucket', | |||
backgroundColor: '#444444', | |||
iconPath: '/images/sonarcloud/github-white.svg', | |||
installationUrl: 'https://github.com/apps/greg-sonarcloud/installations/new', | |||
key: 'github', | |||
name: 'GitHub', | |||
...overrides | |||
}; | |||
} |
@@ -2680,7 +2680,6 @@ organization.members.add_to_members=Add to members | |||
organization.members.config_synchro=Configure Synchronization | |||
organization.members.auto_sync_with_x=Automatic sync with {0} | |||
organization.members.auto_sync_members_from_org_x=Now your members can be automatically synchronized with your {0}. | |||
organization.members.auto_sync_total_help.bitbucket=You might not see all members from your Bitbucket team yet, as they need to reconnect to SonarCloud to be members of the organization. | |||
organization.members.auto_sync_total_help.github=You might not see all members from your GitHub organization yet, as they need to connect to SonarCloud at least once to appear in this list. | |||
organization.members.see_all_members_on_x=See all members on {0} | |||
organization.members.management.title=Members Management | |||
@@ -2689,7 +2688,6 @@ organization.members.management.manual=Manual | |||
organization.members.management.manual.add_members_manually=Admin add members manually from SonarCloud existing users | |||
organization.members.management.automatic=Automatic sync with {0} | |||
organization.members.management.automatic.synchronized_from_x=Members are synchronized automatically from your {0} | |||
organization.members.management.automatic.members_changes_reflected.bitbucket=Your team members must reconnect to SonarCloud to be automatically added to correct SonarCloud organization | |||
organization.members.management.automatic.members_changes_reflected.github=If you add or remove a member on GitHub, SonarCloud immediately reflects the changes | |||
organization.members.management.automatic.warning=This will override your current Members and Permissions configuration | |||
organization.members.management.choose_members_permissions=Admin manages permissions for each member in SonarCloud |