diff options
author | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2018-10-24 16:18:56 +0200 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2018-11-16 20:21:05 +0100 |
commit | 83144d4988f1f5ecc1f07852e3d67fcd07a8024e (patch) | |
tree | 5dc2f67e9f9ecb9da4a67030c06cc20a4495c7e3 /server/sonar-web/src/main/js | |
parent | 7c133fcc9d877837e18fef5c9d83cce463adbd7f (diff) | |
download | sonarqube-83144d4988f1f5ecc1f07852e3d67fcd07a8024e.tar.gz sonarqube-83144d4988f1f5ecc1f07852e3d67fcd07a8024e.zip |
SONAR-11325 Enable to continue an unfinished alm application installation
Diffstat (limited to 'server/sonar-web/src/main/js')
15 files changed, 404 insertions, 58 deletions
diff --git a/server/sonar-web/src/main/js/api/alm-integration.ts b/server/sonar-web/src/main/js/api/alm-integration.ts index 9b9df940713..05ba477a887 100644 --- a/server/sonar-web/src/main/js/api/alm-integration.ts +++ b/server/sonar-web/src/main/js/api/alm-integration.ts @@ -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; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx index 02d69091357..a3093d3e2fb 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx @@ -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} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMeta-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMeta-test.tsx.snap index b0f425fc541..35f80d6ab41 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMeta-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMeta-test.tsx.snap @@ -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={ diff --git a/server/sonar-web/src/main/js/app/styles/init/misc.css b/server/sonar-web/src/main/js/app/styles/init/misc.css index 9cdc8dafa40..d208189636e 100644 --- a/server/sonar-web/src/main/js/app/styles/init/misc.css +++ b/server/sonar-web/src/main/js/app/styles/init/misc.css @@ -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 { diff --git a/server/sonar-web/src/main/js/app/types.ts b/server/sonar-web/src/main/js/app/types.ts index 01b41f6dbfd..e5b615e42d5 100644 --- a/server/sonar-web/src/main/js/app/types.ts +++ b/server/sonar-web/src/main/js/app/types.ts @@ -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[]; diff --git a/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx b/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx index 92de554862d..f24b6c44f92 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx @@ -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} /> ); } diff --git a/server/sonar-web/src/main/js/apps/create/organization/ChooseRemoteOrganizationStep.tsx b/server/sonar-web/src/main/js/apps/create/organization/ChooseRemoteOrganizationStep.tsx index 80e80a5f193..9a562150fec 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/ChooseRemoteOrganizationStep.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/ChooseRemoteOrganizationStep.tsx @@ -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); diff --git a/server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx b/server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx index 1023408535c..763ebef5e17 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx @@ -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( diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationCreate-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationCreate-test.tsx index b58cd5adf3d..6cd346021e2 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationCreate-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationCreate-test.tsx @@ -105,6 +105,7 @@ function shallowRender(props: Partial<AutoOrganizationCreate['props']> = {}) { key: 'bitbucket', name: 'BitBucket' }} + almUnboundApplications={[]} createOrganization={jest.fn()} onOrgCreated={jest.fn()} unboundOrganizations={[]} diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/ChooseRemoteOrganizationStep-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/ChooseRemoteOrganizationStep-test.tsx index c11537a0ef4..c9fa537806c 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/ChooseRemoteOrganizationStep-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/ChooseRemoteOrganizationStep-test.tsx @@ -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(); diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx index aa8b0b6c155..b73259b9076 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx @@ -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 diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationCreate-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationCreate-test.tsx.snap index 56a6fe28fbb..b0aa2e02a1a 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationCreate-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationCreate-test.tsx.snap @@ -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 []} /> `; diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/ChooseRemoteOrganizationStep-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/ChooseRemoteOrganizationStep-test.tsx.snap index 6a1c633b1a2..77778ab5130 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/ChooseRemoteOrganizationStep-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/ChooseRemoteOrganizationStep-test.tsx.snap @@ -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> diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap index 11acf9d7aba..741ed46784e 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap @@ -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 [ diff --git a/server/sonar-web/src/main/js/apps/create/organization/utils.ts b/server/sonar-web/src/main/js/apps/create/organization/utils.ts index 71795b85c78..e5c92d28164 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/utils.ts +++ b/server/sonar-web/src/main/js/apps/create/organization/utils.ts @@ -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 + }); |