aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js
diff options
context:
space:
mode:
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>2018-10-24 16:18:56 +0200
committerSonarTech <sonartech@sonarsource.com>2018-11-16 20:21:05 +0100
commit83144d4988f1f5ecc1f07852e3d67fcd07a8024e (patch)
tree5dc2f67e9f9ecb9da4a67030c06cc20a4495c7e3 /server/sonar-web/src/main/js
parent7c133fcc9d877837e18fef5c9d83cce463adbd7f (diff)
downloadsonarqube-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')
-rw-r--r--server/sonar-web/src/main/js/api/alm-integration.ts11
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx2
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMeta-test.tsx.snap2
-rw-r--r--server/sonar-web/src/main/js/app/styles/init/misc.css22
-rw-r--r--server/sonar-web/src/main/js/app/types.ts6
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx7
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/ChooseRemoteOrganizationStep.tsx119
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx39
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationCreate-test.tsx1
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/ChooseRemoteOrganizationStep-test.tsx23
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx56
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationCreate-test.tsx.snap5
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/ChooseRemoteOrganizationStep-test.tsx.snap147
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap7
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/utils.ts15
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
+ });