@@ -18,7 +18,12 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { getJSON, postJSON, post } from '../helpers/request'; | |||
import { AlmRepository, AlmApplication, AlmOrganization } from '../app/types'; | |||
import { | |||
AlmApplication, | |||
AlmOrganization, | |||
AlmRepository, | |||
AlmUnboundApplication | |||
} from '../app/types'; | |||
import throwGlobalError from '../app/utils/throwGlobalError'; | |||
export function bindAlmOrganization(data: { installationId: string; organization: string }) { | |||
@@ -59,6 +64,10 @@ export function getRepositories(data: { | |||
return getJSON('/api/alm_integration/list_repositories', data).catch(throwGlobalError); | |||
} | |||
export function listUnboundApplications(): Promise<{ applications: AlmUnboundApplication[] }> { | |||
return getJSON('/api/alm_integration/list_unbound_applications').catch(throwGlobalError); | |||
} | |||
export function provisionProject(data: { | |||
installationKeys: string[]; | |||
organization: string; |
@@ -115,7 +115,7 @@ export function ComponentNavMeta({ | |||
{branchMeasures && | |||
branchMeasures.length > 0 && ( | |||
<> | |||
<span className="vertical-separator" /> | |||
<span className="vertical-separator big-spacer-left big-spacer-right" /> | |||
<BranchMeasures | |||
branchLike={branchLike} | |||
componentKey={component.key} |
@@ -104,7 +104,7 @@ exports[`renders status of short-living branch 1`] = ` | |||
} | |||
/> | |||
<span | |||
className="vertical-separator" | |||
className="vertical-separator big-spacer-left big-spacer-right" | |||
/> | |||
<BranchMeasures | |||
branchLike={ |
@@ -299,6 +299,11 @@ td.big-spacer-top { | |||
align-items: center; | |||
} | |||
.display-flex-stretch { | |||
display: flex !important; | |||
align-items: stretch; | |||
} | |||
.display-inline-flex-baseline { | |||
display: inline-flex !important; | |||
align-items: baseline; | |||
@@ -354,13 +359,20 @@ td.big-spacer-top { | |||
} | |||
.vertical-separator { | |||
margin-left: calc(2 * var(--gridSize)); | |||
margin-right: calc(2 * var(--gridSize)); | |||
width: 1px; | |||
min-height: 16px; | |||
flex-grow: 1; | |||
background-color: var(--barBorderColor); | |||
} | |||
.vertical-pipe-separator { | |||
display: flex; | |||
flex-direction: column; | |||
margin-right: 60px; | |||
} | |||
.vertical-separator:after { | |||
content: '|'; | |||
color: var(--barBorderColor); | |||
.vertical-pipe-separator > .vertical-separator { | |||
margin: 4px auto; | |||
} | |||
.capitalize { |
@@ -26,6 +26,7 @@ export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>; | |||
export interface AlmApplication extends IdentityProvider { | |||
installationUrl: string; | |||
} | |||
export interface AlmOrganization extends OrganizationBase { | |||
key: string; | |||
personal: boolean; | |||
@@ -38,6 +39,11 @@ export interface AlmRepository { | |||
linkedProjectName?: string; | |||
} | |||
export interface AlmUnboundApplication { | |||
installationId: string; | |||
name: string; | |||
} | |||
export interface Analysis { | |||
date: string; | |||
events: AnalysisEvent[]; |
@@ -27,8 +27,9 @@ import RadioToggle from '../../../components/controls/RadioToggle'; | |||
import { | |||
AlmApplication, | |||
AlmOrganization, | |||
OrganizationBase, | |||
Organization | |||
AlmUnboundApplication, | |||
Organization, | |||
OrganizationBase | |||
} from '../../../app/types'; | |||
import { bindAlmOrganization } from '../../../api/alm-integration'; | |||
import { sanitizeAlmId } from '../../../helpers/almIntegrations'; | |||
@@ -45,6 +46,7 @@ interface Props { | |||
almApplication: AlmApplication; | |||
almInstallId?: string; | |||
almOrganization?: AlmOrganization; | |||
almUnboundApplications: AlmUnboundApplication[]; | |||
createOrganization: ( | |||
organization: OrganizationBase & { installationId?: string } | |||
) => Promise<Organization>; | |||
@@ -166,6 +168,7 @@ export default class AutoOrganizationCreate extends React.PureComponent<Props, S | |||
<ChooseRemoteOrganizationStep | |||
almApplication={this.props.almApplication} | |||
almInstallId={almInstallId} | |||
almUnboundApplications={this.props.almUnboundApplications} | |||
/> | |||
); | |||
} |
@@ -18,20 +18,72 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { WithRouterProps, withRouter } from 'react-router'; | |||
import { sortBy } from 'lodash'; | |||
import { serializeQuery } from './utils'; | |||
import IdentityProviderLink from '../../../components/ui/IdentityProviderLink'; | |||
import Select from '../../../components/controls/Select'; | |||
import Step from '../../tutorials/components/Step'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { AlmApplication } from '../../../app/types'; | |||
import { Alert } from '../../../components/ui/Alert'; | |||
import { SubmitButton } from '../../../components/ui/buttons'; | |||
import { AlmApplication, AlmUnboundApplication } from '../../../app/types'; | |||
import { getBaseUrl } from '../../../helpers/urls'; | |||
import { sanitizeAlmId } from '../../../helpers/almIntegrations'; | |||
import { translate } from '../../../helpers/l10n'; | |||
interface Props { | |||
almApplication: AlmApplication; | |||
almInstallId?: string; | |||
almUnboundApplications: AlmUnboundApplication[]; | |||
} | |||
interface State { | |||
unboundInstallationId: string; | |||
} | |||
export default class ChooseRemoteOrganizationStep extends React.PureComponent<Props> { | |||
export class ChooseRemoteOrganizationStep extends React.PureComponent< | |||
Props & WithRouterProps, | |||
State | |||
> { | |||
state: State = { unboundInstallationId: '' }; | |||
handleSubmit = (event: React.FormEvent<HTMLFormElement>) => { | |||
event.preventDefault(); | |||
const { unboundInstallationId } = this.state; | |||
if (unboundInstallationId) { | |||
this.props.router.push({ | |||
pathname: '/create-organization', | |||
query: serializeQuery({ | |||
almInstallId: unboundInstallationId, | |||
almKey: this.props.almApplication.key | |||
}) | |||
}); | |||
} | |||
}; | |||
handleInstallationChange = ({ installationId }: AlmUnboundApplication) => { | |||
this.setState({ unboundInstallationId: installationId }); | |||
}; | |||
renderOption = (organization: AlmUnboundApplication) => { | |||
const { almApplication } = this.props; | |||
return ( | |||
<span> | |||
<img | |||
alt={almApplication.name} | |||
className="spacer-right" | |||
height={14} | |||
src={`${getBaseUrl()}/images/sonarcloud/${sanitizeAlmId(almApplication.key)}.svg`} | |||
/> | |||
{organization.name} | |||
</span> | |||
); | |||
}; | |||
renderForm = () => { | |||
const { almApplication, almInstallId } = this.props; | |||
const { almApplication, almInstallId, almUnboundApplications } = this.props; | |||
const { unboundInstallationId } = this.state; | |||
return ( | |||
<div className="boxed-group-inner"> | |||
{almInstallId && ( | |||
@@ -43,16 +95,55 @@ export default class ChooseRemoteOrganizationStep extends React.PureComponent<Pr | |||
</ul> | |||
</Alert> | |||
)} | |||
<IdentityProviderLink | |||
className="display-inline-block" | |||
identityProvider={almApplication} | |||
small={true} | |||
url={almApplication.installationUrl}> | |||
{translate( | |||
'onboarding.import_organization.choose_organization_button', | |||
almApplication.key | |||
<div className="display-flex-center"> | |||
<div className="display-inline-block abs-width-400"> | |||
<IdentityProviderLink | |||
className="display-inline-block" | |||
identityProvider={almApplication} | |||
small={true} | |||
url={almApplication.installationUrl}> | |||
{translate( | |||
'onboarding.import_organization.choose_organization_button', | |||
almApplication.key | |||
)} | |||
</IdentityProviderLink> | |||
</div> | |||
{almUnboundApplications.length > 0 && ( | |||
<div className="display-flex-stretch"> | |||
<div className="vertical-pipe-separator"> | |||
<div className="vertical-separator " /> | |||
<span className="note">{translate('or')}</span> | |||
<div className="vertical-separator" /> | |||
</div> | |||
<form className="big-spacer-top big-spacer-bottom" onSubmit={this.handleSubmit}> | |||
<div className="form-field abs-width-400"> | |||
<label htmlFor="select-unbound-installation"> | |||
{translate( | |||
'onboarding.import_organization.choose_unbound_installation', | |||
almApplication.key | |||
)} | |||
</label> | |||
<Select | |||
className="input-super-large" | |||
clearable={false} | |||
id="select-unbound-installation" | |||
labelKey="name" | |||
onChange={this.handleInstallationChange} | |||
optionRenderer={this.renderOption} | |||
options={sortBy(almUnboundApplications, o => o.name.toLowerCase())} | |||
placeholder={translate('onboarding.import_organization.choose_organization')} | |||
value={unboundInstallationId} | |||
valueKey="installationId" | |||
valueRenderer={this.renderOption} | |||
/> | |||
</div> | |||
<SubmitButton disabled={!unboundInstallationId}> | |||
{translate('continue')} | |||
</SubmitButton> | |||
</form> | |||
</div> | |||
)} | |||
</IdentityProviderLink> | |||
</div> | |||
</div> | |||
); | |||
}; | |||
@@ -75,3 +166,5 @@ export default class ChooseRemoteOrganizationStep extends React.PureComponent<Pr | |||
); | |||
} | |||
} | |||
export default withRouter(ChooseRemoteOrganizationStep); |
@@ -34,18 +34,20 @@ import Tabs from '../../../components/controls/Tabs'; | |||
import { whenLoggedIn } from '../../../components/hoc/whenLoggedIn'; | |||
import { withUserOrganizations } from '../../../components/hoc/withUserOrganizations'; | |||
import { | |||
bindAlmOrganization, | |||
getAlmAppInfo, | |||
getAlmOrganization, | |||
bindAlmOrganization | |||
listUnboundApplications | |||
} from '../../../api/alm-integration'; | |||
import { getSubscriptionPlans } from '../../../api/billing'; | |||
import { | |||
LoggedInUser, | |||
Organization, | |||
SubscriptionPlan, | |||
AlmApplication, | |||
AlmOrganization, | |||
OrganizationBase | |||
AlmUnboundApplication, | |||
LoggedInUser, | |||
Organization, | |||
OrganizationBase, | |||
SubscriptionPlan | |||
} from '../../../app/types'; | |||
import { hasAdvancedALMIntegration, isPersonal } from '../../../helpers/almIntegrations'; | |||
import { translate } from '../../../helpers/l10n'; | |||
@@ -72,6 +74,7 @@ interface State { | |||
almApplication?: AlmApplication; | |||
almOrganization?: AlmOrganization; | |||
almOrgLoading: boolean; | |||
almUnboundApplications: AlmUnboundApplication[]; | |||
loading: boolean; | |||
organization?: Organization; | |||
subscriptionPlans?: SubscriptionPlan[]; | |||
@@ -86,7 +89,7 @@ interface LocationState { | |||
export class CreateOrganization extends React.PureComponent<Props & WithRouterProps, State> { | |||
mounted = false; | |||
state: State = { almOrgLoading: false, loading: true }; | |||
state: State = { almOrgLoading: false, almUnboundApplications: [], loading: true }; | |||
componentDidMount() { | |||
this.mounted = true; | |||
@@ -101,11 +104,26 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr | |||
const query = parseQuery(this.props.location.query); | |||
if (query.almInstallId) { | |||
this.fetchAlmOrganization(query.almInstallId); | |||
} else { | |||
initRequests.push(this.fetchAlmUnboundApplications()); | |||
} | |||
} | |||
Promise.all(initRequests).then(this.stopLoading, this.stopLoading); | |||
} | |||
componentDidUpdate(prevProps: WithRouterProps) { | |||
const prevQuery = parseQuery(prevProps.location.query); | |||
const query = parseQuery(this.props.location.query); | |||
if (this.state.almApplication && prevQuery.almInstallId !== query.almInstallId) { | |||
if (query.almInstallId) { | |||
this.fetchAlmOrganization(query.almInstallId); | |||
} else { | |||
this.setState({ almOrganization: undefined, loading: true }); | |||
this.fetchAlmUnboundApplications().then(this.stopLoading, this.stopLoading); | |||
} | |||
} | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
document.body.classList.remove('white-page'); | |||
@@ -119,6 +137,14 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr | |||
}); | |||
}; | |||
fetchAlmUnboundApplications = () => { | |||
return listUnboundApplications().then(({ applications }) => { | |||
if (this.mounted) { | |||
this.setState({ almUnboundApplications: applications }); | |||
} | |||
}); | |||
}; | |||
fetchValidOrgKey = (almOrganization: AlmOrganization) => { | |||
const key = slugify(almOrganization.key); | |||
const keys = [key, ...times(9, i => `${key}-${i + 1}`)]; | |||
@@ -237,6 +263,7 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr | |||
almApplication={almApplication} | |||
almInstallId={almInstallId} | |||
almOrganization={almOrganization} | |||
almUnboundApplications={this.state.almUnboundApplications} | |||
createOrganization={this.props.createOrganization} | |||
onOrgCreated={this.handleOrgCreated} | |||
unboundOrganizations={this.props.userOrganizations.filter( |
@@ -105,6 +105,7 @@ function shallowRender(props: Partial<AutoOrganizationCreate['props']> = {}) { | |||
key: 'bitbucket', | |||
name: 'BitBucket' | |||
}} | |||
almUnboundApplications={[]} | |||
createOrganization={jest.fn()} | |||
onOrgCreated={jest.fn()} | |||
unboundOrganizations={[]} |
@@ -19,7 +19,8 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import ChooseRemoteOrganizationStep from '../ChooseRemoteOrganizationStep'; | |||
import { ChooseRemoteOrganizationStep } from '../ChooseRemoteOrganizationStep'; | |||
import { mockRouter, submit } from '../../../../helpers/testUtils'; | |||
it('should render', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
@@ -29,8 +30,26 @@ it('should display an alert message', () => { | |||
expect(shallowRender({ almInstallId: 'foo' }).find('Alert')).toMatchSnapshot(); | |||
}); | |||
it('should display unbound installations', () => { | |||
const installation = { installationId: '12345', name: 'Foo' }; | |||
const push = jest.fn(); | |||
const wrapper = shallowRender({ | |||
almUnboundApplications: [installation], | |||
router: mockRouter({ push }) | |||
}); | |||
expect(wrapper).toMatchSnapshot(); | |||
wrapper.find('Select').prop<Function>('onChange')(installation); | |||
submit(wrapper.find('form')); | |||
expect(push).toHaveBeenCalledWith({ | |||
pathname: '/create-organization', | |||
query: { installation_id: installation.installationId } // eslint-disable-line camelcase | |||
}); | |||
}); | |||
function shallowRender(props: Partial<ChooseRemoteOrganizationStep['props']> = {}) { | |||
return shallow( | |||
// @ts-ignore avoid passing everything from WithRouterProps | |||
<ChooseRemoteOrganizationStep | |||
almApplication={{ | |||
backgroundColor: 'blue', | |||
@@ -39,6 +58,8 @@ function shallowRender(props: Partial<ChooseRemoteOrganizationStep['props']> = { | |||
key: 'github', | |||
name: 'GitHub' | |||
}} | |||
almUnboundApplications={[]} | |||
router={mockRouter()} | |||
{...props} | |||
/> | |||
).dive(); |
@@ -18,12 +18,19 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { times } from 'lodash'; | |||
import { Location } from 'history'; | |||
import { shallow } from 'enzyme'; | |||
import { CreateOrganization } from '../CreateOrganization'; | |||
import { mockRouter, waitAndUpdate } from '../../../../helpers/testUtils'; | |||
import { LoggedInUser } from '../../../../app/types'; | |||
import { getAlmOrganization } from '../../../../api/alm-integration'; | |||
import { | |||
getAlmAppInfo, | |||
getAlmOrganization, | |||
listUnboundApplications | |||
} from '../../../../api/alm-integration'; | |||
import { getSubscriptionPlans } from '../../../../api/billing'; | |||
import { getOrganizations } from '../../../../api/organizations'; | |||
jest.mock('../../../../api/billing', () => ({ | |||
getSubscriptionPlans: jest | |||
@@ -42,17 +49,18 @@ jest.mock('../../../../api/alm-integration', () => ({ | |||
} | |||
}), | |||
getAlmOrganization: jest.fn().mockResolvedValue({ | |||
avatar: 'https://avatars3.githubusercontent.com/u/37629810?v=4', | |||
avatar: 'my-avatar', | |||
description: 'Continuous Code Quality', | |||
key: 'sonarsource', | |||
name: 'SonarSource', | |||
personal: false, | |||
url: 'https://www.sonarsource.com' | |||
}) | |||
}), | |||
listUnboundApplications: jest.fn().mockResolvedValue({ applications: [] }) | |||
})); | |||
jest.mock('../../../../api/organizations', () => ({ | |||
getOrganization: jest.fn().mockResolvedValue(undefined) | |||
getOrganizations: jest.fn().mockResolvedValue({ organizations: [] }) | |||
})); | |||
const user: LoggedInUser = { | |||
@@ -64,10 +72,20 @@ const user: LoggedInUser = { | |||
showOnboardingTutorial: false | |||
}; | |||
beforeEach(() => { | |||
(getAlmAppInfo as jest.Mock<any>).mockClear(); | |||
(getAlmOrganization as jest.Mock<any>).mockClear(); | |||
(listUnboundApplications as jest.Mock<any>).mockClear(); | |||
(getSubscriptionPlans as jest.Mock<any>).mockClear(); | |||
(getOrganizations as jest.Mock<any>).mockClear(); | |||
}); | |||
it('should render with manual tab displayed', async () => { | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper).toMatchSnapshot(); | |||
expect(getSubscriptionPlans).toHaveBeenCalled(); | |||
expect(getAlmAppInfo).not.toHaveBeenCalled(); | |||
}); | |||
it('should preselect paid plan on manual creation', async () => { | |||
@@ -82,6 +100,8 @@ it('should render with auto tab displayed', async () => { | |||
const wrapper = shallowRender({ currentUser: { ...user, externalProvider: 'github' } }); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper).toMatchSnapshot(); | |||
expect(getAlmAppInfo).toHaveBeenCalled(); | |||
expect(listUnboundApplications).toHaveBeenCalled(); | |||
}); | |||
it('should render with auto tab selected and manual disabled', async () => { | |||
@@ -92,13 +112,16 @@ it('should render with auto tab selected and manual disabled', async () => { | |||
expect(wrapper).toMatchSnapshot(); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper).toMatchSnapshot(); | |||
expect(getAlmAppInfo).toHaveBeenCalled(); | |||
expect(getAlmOrganization).toHaveBeenCalled(); | |||
expect(getOrganizations).toHaveBeenCalled(); | |||
}); | |||
it('should render with auto personal organization bind page', async () => { | |||
(getAlmOrganization as jest.Mock<any>).mockResolvedValueOnce({ | |||
key: 'foo', | |||
name: 'Foo', | |||
avatar: 'https://avatars3.githubusercontent.com/u/37629810?v=4', | |||
avatar: 'my-avatar', | |||
personal: true | |||
}); | |||
const wrapper = shallowRender({ | |||
@@ -112,18 +135,24 @@ it('should render with auto personal organization bind page', async () => { | |||
it('should slugify and find a uniq organization key', async () => { | |||
(getAlmOrganization as jest.Mock<any>).mockResolvedValueOnce({ | |||
avatar: 'https://avatars3.githubusercontent.com/u/37629810?v=4', | |||
key: 'Foo&Bar', | |||
name: 'Foo & Bar', | |||
avatar: 'https://avatars3.githubusercontent.com/u/37629810?v=4', | |||
type: 'USER' | |||
personal: true | |||
}); | |||
(getOrganizations as jest.Mock<any>).mockResolvedValueOnce({ | |||
organizations: [{ key: 'foo-and-bar' }, { key: 'foo-and-bar-1' }] | |||
}); | |||
const wrapper = shallowRender({ | |||
currentUser: { ...user, externalProvider: 'github' }, | |||
location: { query: { installation_id: 'foo' } } as Location // eslint-disable-line camelcase | |||
}); | |||
await waitAndUpdate(wrapper); | |||
expect(getOrganizations).toHaveBeenCalledWith({ | |||
organizations: ['foo-and-bar', ...times(9, i => `foo-and-bar-${i + 1}`)].join(',') | |||
}); | |||
expect(wrapper.find('AutoOrganizationCreate').prop('almOrganization')).toMatchObject({ | |||
key: 'foo-and-bar' | |||
key: 'foo-and-bar-2' | |||
}); | |||
}); | |||
@@ -147,6 +176,17 @@ it('should switch tabs', async () => { | |||
expect(wrapper.find('AutoOrganizationCreate').exists()).toBeTruthy(); | |||
}); | |||
it('should reload the alm organization when the url query changes', async () => { | |||
const wrapper = shallowRender({ currentUser: { ...user, externalProvider: 'github' } }); | |||
await waitAndUpdate(wrapper); | |||
expect(getAlmOrganization).not.toHaveBeenCalled(); | |||
wrapper.setProps({ location: { query: { installation_id: 'foo' } } }); // eslint-disable-line camelcase | |||
expect(getAlmOrganization).toHaveBeenCalledWith({ installationId: 'foo' }); | |||
wrapper.setProps({ location: { query: {} } }); | |||
expect(wrapper.state('almOrganization')).toBeUndefined(); | |||
expect(listUnboundApplications).toHaveBeenCalledTimes(2); | |||
}); | |||
function shallowRender(props: Partial<CreateOrganization['props']> = {}) { | |||
return shallow( | |||
<CreateOrganization |
@@ -56,7 +56,7 @@ exports[`should display choice between import or creation 1`] = ` | |||
}, | |||
] | |||
} | |||
value="none" | |||
value={null} | |||
/> | |||
</div> | |||
</OrganizationDetailsStep> | |||
@@ -121,7 +121,7 @@ exports[`should render prefilled and create org 1`] = ` | |||
`; | |||
exports[`should render with import org button 1`] = ` | |||
<ChooseRemoteOrganizationStep | |||
<withRouter(ChooseRemoteOrganizationStep) | |||
almApplication={ | |||
Object { | |||
"backgroundColor": "#0052CC", | |||
@@ -131,5 +131,6 @@ exports[`should render with import org button 1`] = ` | |||
"name": "BitBucket", | |||
} | |||
} | |||
almUnboundApplications={Array []} | |||
/> | |||
`; |
@@ -17,6 +17,115 @@ exports[`should display an alert message 1`] = ` | |||
</Alert> | |||
`; | |||
exports[`should display unbound installations 1`] = ` | |||
<div | |||
className="boxed-group onboarding-step is-open" | |||
> | |||
<div | |||
className="onboarding-step-number" | |||
> | |||
1 | |||
</div> | |||
<div | |||
className="boxed-group-header" | |||
> | |||
<h2> | |||
onboarding.import_organization.import_org_details | |||
</h2> | |||
</div> | |||
<div | |||
className="" | |||
> | |||
<div | |||
className="boxed-group-inner" | |||
> | |||
<div | |||
className="display-flex-center" | |||
> | |||
<div | |||
className="display-inline-block abs-width-400" | |||
> | |||
<IdentityProviderLink | |||
className="display-inline-block" | |||
identityProvider={ | |||
Object { | |||
"backgroundColor": "blue", | |||
"iconPath": "icon/path", | |||
"installationUrl": "https://alm.application.url", | |||
"key": "github", | |||
"name": "GitHub", | |||
} | |||
} | |||
small={true} | |||
url="https://alm.application.url" | |||
> | |||
onboarding.import_organization.choose_organization_button.github | |||
</IdentityProviderLink> | |||
</div> | |||
<div | |||
className="display-flex-stretch" | |||
> | |||
<div | |||
className="vertical-pipe-separator" | |||
> | |||
<div | |||
className="vertical-separator " | |||
/> | |||
<span | |||
className="note" | |||
> | |||
or | |||
</span> | |||
<div | |||
className="vertical-separator" | |||
/> | |||
</div> | |||
<form | |||
className="big-spacer-top big-spacer-bottom" | |||
onSubmit={[Function]} | |||
> | |||
<div | |||
className="form-field abs-width-400" | |||
> | |||
<label | |||
htmlFor="select-unbound-installation" | |||
> | |||
onboarding.import_organization.choose_unbound_installation.github | |||
</label> | |||
<Select | |||
className="input-super-large" | |||
clearable={false} | |||
id="select-unbound-installation" | |||
labelKey="name" | |||
onChange={[Function]} | |||
optionRenderer={[Function]} | |||
options={ | |||
Array [ | |||
Object { | |||
"installationId": "12345", | |||
"name": "Foo", | |||
}, | |||
] | |||
} | |||
placeholder="onboarding.import_organization.choose_organization" | |||
value="" | |||
valueKey="installationId" | |||
valueRenderer={[Function]} | |||
/> | |||
</div> | |||
<SubmitButton | |||
disabled={true} | |||
> | |||
continue | |||
</SubmitButton> | |||
</form> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
`; | |||
exports[`should render 1`] = ` | |||
<div | |||
className="boxed-group onboarding-step is-open" | |||
@@ -39,22 +148,30 @@ exports[`should render 1`] = ` | |||
<div | |||
className="boxed-group-inner" | |||
> | |||
<IdentityProviderLink | |||
className="display-inline-block" | |||
identityProvider={ | |||
Object { | |||
"backgroundColor": "blue", | |||
"iconPath": "icon/path", | |||
"installationUrl": "https://alm.application.url", | |||
"key": "github", | |||
"name": "GitHub", | |||
} | |||
} | |||
small={true} | |||
url="https://alm.application.url" | |||
<div | |||
className="display-flex-center" | |||
> | |||
onboarding.import_organization.choose_organization_button.github | |||
</IdentityProviderLink> | |||
<div | |||
className="display-inline-block abs-width-400" | |||
> | |||
<IdentityProviderLink | |||
className="display-inline-block" | |||
identityProvider={ | |||
Object { | |||
"backgroundColor": "blue", | |||
"iconPath": "icon/path", | |||
"installationUrl": "https://alm.application.url", | |||
"key": "github", | |||
"name": "GitHub", | |||
} | |||
} | |||
small={true} | |||
url="https://alm.application.url" | |||
> | |||
onboarding.import_organization.choose_organization_button.github | |||
</IdentityProviderLink> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
</div> |
@@ -61,7 +61,7 @@ exports[`should render with auto personal organization bind page 2`] = ` | |||
almInstallId="foo" | |||
almOrganization={ | |||
Object { | |||
"avatar": "https://avatars3.githubusercontent.com/u/37629810?v=4", | |||
"avatar": "my-avatar", | |||
"key": "foo", | |||
"name": "Foo", | |||
"personal": true, | |||
@@ -148,6 +148,7 @@ exports[`should render with auto tab displayed 1`] = ` | |||
"name": "GitHub", | |||
} | |||
} | |||
almUnboundApplications={Array []} | |||
onOrgCreated={[Function]} | |||
unboundOrganizations={ | |||
Array [ | |||
@@ -240,7 +241,7 @@ exports[`should render with auto tab selected and manual disabled 2`] = ` | |||
almInstallId="foo" | |||
almOrganization={ | |||
Object { | |||
"avatar": "https://avatars3.githubusercontent.com/u/37629810?v=4", | |||
"avatar": "my-avatar", | |||
"description": "Continuous Code Quality", | |||
"key": "sonarsource", | |||
"name": "SonarSource", | |||
@@ -248,6 +249,7 @@ exports[`should render with auto tab selected and manual disabled 2`] = ` | |||
"url": "https://www.sonarsource.com", | |||
} | |||
} | |||
almUnboundApplications={Array []} | |||
onOrgCreated={[Function]} | |||
unboundOrganizations={ | |||
Array [ | |||
@@ -392,6 +394,7 @@ exports[`should switch tabs 1`] = ` | |||
"name": "GitHub", | |||
} | |||
} | |||
almUnboundApplications={Array []} | |||
onOrgCreated={[Function]} | |||
unboundOrganizations={ | |||
Array [ |
@@ -20,7 +20,13 @@ | |||
import { memoize } from 'lodash'; | |||
import { translateWithParameters } from '../../../helpers/l10n'; | |||
import { formatMeasure } from '../../../helpers/measures'; | |||
import { RawQuery, parseAsOptionalString } from '../../../helpers/query'; | |||
import { | |||
RawQuery, | |||
parseAsOptionalString, | |||
cleanQuery, | |||
serializeString | |||
} from '../../../helpers/query'; | |||
import { isBitbucket, isGithub } from '../../../helpers/almIntegrations'; | |||
export function formatPrice(price?: number, noSign?: boolean) { | |||
const priceFormatted = formatMeasure(price, 'FLOAT') | |||
@@ -47,3 +53,10 @@ export const parseQuery = memoize( | |||
}; | |||
} | |||
); | |||
export const serializeQuery = (query: Query): RawQuery => | |||
cleanQuery({ | |||
// eslint-disable-next-line camelcase | |||
installation_id: isGithub(query.almKey) ? serializeString(query.almInstallId) : undefined, | |||
clientKey: isBitbucket(query.almKey) ? serializeString(query.almInstallId) : undefined | |||
}); |
@@ -112,6 +112,7 @@ no_tags=No tags | |||
not_now=Not now | |||
off=Off | |||
on=On | |||
or=Or | |||
organization_key=Organization Key | |||
open=Open | |||
optional=Optional | |||
@@ -2754,6 +2755,8 @@ onboarding.create_organization.enter_your_coupon=Enter your coupon | |||
onboarding.create_organization.create_and_upgrade=Create Organization and Upgrade | |||
onboarding.create_organization.ready=All set! Your organization is now ready to go | |||
onboarding.import_organization.bind=Bind Organization | |||
onboarding.import_organization.choose_unbound_installation.bitbucket=Choose one of your Bitbucket teams that already have the SonarCloud application installed: | |||
onboarding.import_organization.choose_unbound_installation.github=Choose one of your GitHub organizations that already have the SonarCloud application installed: | |||
onboarding.import_organization.import=Import Organization | |||
onboarding.import_organization.import_org_details=Import organization details | |||
onboarding.import_organization.org_not_found=We were not able to find the requested organization, here are a few tips to help you troubleshoot the issue: |