Просмотр исходного кода

SONAR-11325 Enable to continue an unfinished alm application installation

tags/7.5
Grégoire Aubert 5 лет назад
Родитель
Сommit
83144d4988
16 измененных файлов: 407 добавлений и 58 удалений
  1. 10
    1
      server/sonar-web/src/main/js/api/alm-integration.ts
  2. 1
    1
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx
  3. 1
    1
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMeta-test.tsx.snap
  4. 17
    5
      server/sonar-web/src/main/js/app/styles/init/misc.css
  5. 6
    0
      server/sonar-web/src/main/js/app/types.ts
  6. 5
    2
      server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx
  7. 106
    13
      server/sonar-web/src/main/js/apps/create/organization/ChooseRemoteOrganizationStep.tsx
  8. 33
    6
      server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx
  9. 1
    0
      server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationCreate-test.tsx
  10. 22
    1
      server/sonar-web/src/main/js/apps/create/organization/__tests__/ChooseRemoteOrganizationStep-test.tsx
  11. 48
    8
      server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx
  12. 3
    2
      server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationCreate-test.tsx.snap
  13. 132
    15
      server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/ChooseRemoteOrganizationStep-test.tsx.snap
  14. 5
    2
      server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap
  15. 14
    1
      server/sonar-web/src/main/js/apps/create/organization/utils.ts
  16. 3
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 10
- 1
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;

+ 1
- 1
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}

+ 1
- 1
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={

+ 17
- 5
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 {

+ 6
- 0
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[];

+ 5
- 2
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}
/>
);
}

+ 106
- 13
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);

+ 33
- 6
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(

+ 1
- 0
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={[]}

+ 22
- 1
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();

+ 48
- 8
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

+ 3
- 2
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 []}
/>
`;

+ 132
- 15
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>

+ 5
- 2
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 [

+ 14
- 1
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
});

+ 3
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties Просмотреть файл

@@ -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:

Загрузка…
Отмена
Сохранить