export interface AlmOrganization extends OrganizationBase {
key: string;
personal: boolean;
+ privateRepos: number;
+ publicRepos: number;
}
export interface AlmRepository {
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+.card-plan {
+ display: flex;
+ flex-direction: column;
+ width: 450px;
+ height: 210px;
+ background-color: #fff;
+ border: solid 1px var(--barBorderColor);
+ border-radius: 3px;
+ box-sizing: border-box;
+ margin-right: calc(2 * var(--gridSize));
+}
+
+.card-plan:last-child {
+ margin-right: 0;
+}
+
+.card-plan-actionable {
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.card-plan-actionable:focus {
+ outline: none;
+}
+
+.card-plan-actionable:not(.disabled):hover {
+ box-shadow: var(--defaultShadow);
+ transform: translateY(-2px);
+}
+
+.card-plan-actionable.selected {
+ border-color: var(--darkBlue);
+}
+
+.card-plan-actionable.disabled {
+ cursor: not-allowed;
+ background-color: var(--disableGrayBg);
+ border-color: var(--disableGrayBorder);
+}
+
+.card-plan-actionable.disabled h2,
+.card-plan-actionable.disabled ul {
+ color: var(--disableGrayText);
+}
+
+.card-plan-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: calc(2 * var(--gridSize)) calc(2 * var(--gridSize)) 0;
+}
+
+.card-plan-price {
+ font-size: var(--bigFontSize);
+}
+
+.card-plan-body {
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ padding: 0 calc(2 * var(--gridSize)) calc(2 * var(--gridSize));
+}
+
+.card-plan-body ul > li {
+ margin-bottom: calc(var(--gridSize) / 2);
+}
+
+.card-plan-body .alert {
+ margin-bottom: 0;
+}
+
+.card-plan-recommended {
+ position: relative;
+ padding: 6px calc(var(--gridSize) * 2);
+ left: -1px;
+ bottom: -1px;
+ width: 450px;
+ color: #fff;
+ background-color: var(--darkBlue);
+ border-radius: 0 0 3px 3px;
+ box-sizing: border-box;
+ font-size: var(--smallFontSize);
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import * as classNames from 'classnames';
+import { FormattedMessage } from 'react-intl';
+import { Link } from 'react-router';
+import CheckIcon from '../../../components/icons-components/CheckIcon';
+import RecommendedIcon from '../../../components/icons-components/RecommendedIcon';
+import { Alert } from '../../../components/ui/Alert';
+import { formatPrice } from '../organization/utils';
+import { translate } from '../../../helpers/l10n';
+import * as theme from '../../../app/theme';
+import './CardPlan.css';
+
+interface Props {
+ className?: string;
+ disabled?: boolean;
+ onClick?: () => void;
+ selected?: boolean;
+ startingPrice?: string;
+}
+
+interface CardProps extends Props {
+ children: React.ReactNode;
+ recommended?: string;
+ title: string;
+}
+
+export default function CardPlan(props: CardProps) {
+ const { className, disabled, onClick, recommended, selected, startingPrice } = props;
+ const isActionable = Boolean(onClick);
+ return (
+ <div
+ aria-checked={selected}
+ className={classNames(
+ 'card-plan',
+ { 'card-plan-actionable': isActionable, disabled, selected },
+ className
+ )}
+ onClick={isActionable && !disabled ? onClick : undefined}
+ role="radio"
+ tabIndex={0}>
+ <h2 className="card-plan-header big-spacer-bottom">
+ <span className="display-flex-center">
+ {isActionable && (
+ <i className={classNames('icon-radio', 'spacer-right', { 'is-checked': selected })} />
+ )}
+ {props.title}
+ </span>
+ {startingPrice ? (
+ <FormattedMessage
+ defaultMessage={translate('billing.price_from_x')}
+ id="billing.price_from_x"
+ values={{
+ price: <span className="card-plan-price">{startingPrice}</span>
+ }}
+ />
+ ) : (
+ <span className="card-plan-price">{formatPrice(0)}</span>
+ )}
+ </h2>
+ <div className="card-plan-body">{props.children}</div>
+ {recommended && (
+ <div className="card-plan-recommended">
+ <RecommendedIcon className="spacer-right" />
+ <FormattedMessage
+ defaultMessage={recommended}
+ id={recommended}
+ values={{ recommended: <strong>{translate('recommended')}</strong> }}
+ />
+ </div>
+ )}
+ </div>
+ );
+}
+
+interface FreeProps extends Props {
+ almName?: string;
+ hasWarning: boolean;
+}
+
+export function FreeCardPlan({ almName, hasWarning, ...props }: FreeProps) {
+ const showInfo = almName && props.disabled;
+ const showWarning = almName && hasWarning && !props.disabled;
+
+ return (
+ <CardPlan title={translate('billing.free_plan.title')} {...props}>
+ <>
+ <ul className="note">
+ <li>{translate('billing.free_plan.all_projects_analyzed_public')}</li>
+ <li>{translate('billing.free_plan.anyone_can_browse_source_code')}</li>
+ </ul>
+ {showWarning && (
+ <Alert variant="warning">
+ <FormattedMessage
+ defaultMessage={translate('billing.free_plan.private_repo_warning')}
+ id="billing.free_plan.private_repo_warning"
+ values={{ alm: almName }}
+ />
+ </Alert>
+ )}
+ {showInfo && (
+ <Alert variant="info">
+ <FormattedMessage
+ defaultMessage={translate('billing.free_plan.not_available_info')}
+ id="billing.free_plan.not_available_info"
+ values={{ alm: almName }}
+ />
+ </Alert>
+ )}
+ </>
+ </CardPlan>
+ );
+}
+
+interface PaidProps extends Props {
+ isRecommended: boolean;
+}
+
+export function PaidCardPlan({ isRecommended, ...props }: PaidProps) {
+ const advantages = [
+ translate('billing.upgrade_box.unlimited_private_projects'),
+ translate('billing.upgrade_box.strict_control_private_data'),
+ translate('billing.upgrade_box.cancel_anytime'),
+ translate('billing.upgrade_box.free_trial')
+ ];
+
+ return (
+ <CardPlan
+ recommended={isRecommended ? translate('billing.paid_plan.recommended') : undefined}
+ title={translate('billing.paid_plan.title')}
+ {...props}>
+ <>
+ <ul className="note">
+ {advantages.map((text, idx) => (
+ <li className="display-flex-center" key={idx}>
+ <CheckIcon className="spacer-right" fill={theme.green} />
+ {text}
+ </li>
+ ))}
+ </ul>
+ <div className="big-spacer-left">
+ <Link className="spacer-left" target="_blank" to="/documentation/sonarcloud-pricing/">
+ {translate('learn_more')}
+ </Link>
+ </div>
+ </>
+ </CardPlan>
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import CardPlan, { FreeCardPlan, PaidCardPlan } from '../CardPlan';
+import { click } from '../../../../helpers/testUtils';
+
+it('should render correctly', () => {
+ expect(
+ shallow(
+ <CardPlan recommended="Recommended for you" startingPrice="$10" title="Paid Plan">
+ <div>content</div>
+ </CardPlan>
+ )
+ ).toMatchSnapshot();
+});
+
+it('should be actionable', () => {
+ const onClick = jest.fn();
+ const wrapper = shallow(
+ <CardPlan onClick={onClick} title="Free Plan">
+ <div>content</div>
+ </CardPlan>
+ );
+
+ expect(wrapper).toMatchSnapshot();
+ click(wrapper);
+ wrapper.setProps({ selected: true });
+ expect(wrapper).toMatchSnapshot();
+});
+
+describe('#FreeCardPlan', () => {
+ it('should render correctly', () => {
+ expect(shallow(<FreeCardPlan hasWarning={false} />)).toMatchSnapshot();
+ });
+
+ it('should render with warning', () => {
+ expect(
+ shallow(<FreeCardPlan almName="GitHub" hasWarning={true} selected={true} />)
+ ).toMatchSnapshot();
+ });
+
+ it('should render disabled with info', () => {
+ expect(
+ shallow(<FreeCardPlan almName="GitHub" disabled={true} hasWarning={false} />)
+ ).toMatchSnapshot();
+ });
+});
+
+describe('#PaidCardPlan', () => {
+ it('should render correctly', () => {
+ expect(shallow(<PaidCardPlan isRecommended={true} startingPrice="$10" />)).toMatchSnapshot();
+ });
+});
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`#FreeCardPlan should render correctly 1`] = `
+<CardPlan
+ title="billing.free_plan.title"
+>
+ <ul
+ className="note"
+ >
+ <li>
+ billing.free_plan.all_projects_analyzed_public
+ </li>
+ <li>
+ billing.free_plan.anyone_can_browse_source_code
+ </li>
+ </ul>
+</CardPlan>
+`;
+
+exports[`#FreeCardPlan should render disabled with info 1`] = `
+<CardPlan
+ disabled={true}
+ title="billing.free_plan.title"
+>
+ <ul
+ className="note"
+ >
+ <li>
+ billing.free_plan.all_projects_analyzed_public
+ </li>
+ <li>
+ billing.free_plan.anyone_can_browse_source_code
+ </li>
+ </ul>
+ <Alert
+ variant="info"
+ >
+ <FormattedMessage
+ defaultMessage="billing.free_plan.not_available_info"
+ id="billing.free_plan.not_available_info"
+ values={
+ Object {
+ "alm": "GitHub",
+ }
+ }
+ />
+ </Alert>
+</CardPlan>
+`;
+
+exports[`#FreeCardPlan should render with warning 1`] = `
+<CardPlan
+ selected={true}
+ title="billing.free_plan.title"
+>
+ <ul
+ className="note"
+ >
+ <li>
+ billing.free_plan.all_projects_analyzed_public
+ </li>
+ <li>
+ billing.free_plan.anyone_can_browse_source_code
+ </li>
+ </ul>
+ <Alert
+ variant="warning"
+ >
+ <FormattedMessage
+ defaultMessage="billing.free_plan.private_repo_warning"
+ id="billing.free_plan.private_repo_warning"
+ values={
+ Object {
+ "alm": "GitHub",
+ }
+ }
+ />
+ </Alert>
+</CardPlan>
+`;
+
+exports[`#PaidCardPlan should render correctly 1`] = `
+<CardPlan
+ recommended="billing.paid_plan.recommended"
+ startingPrice="$10"
+ title="billing.paid_plan.title"
+>
+ <ul
+ className="note"
+ >
+ <li
+ className="display-flex-center"
+ key="0"
+ >
+ <CheckIcon
+ className="spacer-right"
+ fill="#00aa00"
+ />
+ billing.upgrade_box.unlimited_private_projects
+ </li>
+ <li
+ className="display-flex-center"
+ key="1"
+ >
+ <CheckIcon
+ className="spacer-right"
+ fill="#00aa00"
+ />
+ billing.upgrade_box.strict_control_private_data
+ </li>
+ <li
+ className="display-flex-center"
+ key="2"
+ >
+ <CheckIcon
+ className="spacer-right"
+ fill="#00aa00"
+ />
+ billing.upgrade_box.cancel_anytime
+ </li>
+ <li
+ className="display-flex-center"
+ key="3"
+ >
+ <CheckIcon
+ className="spacer-right"
+ fill="#00aa00"
+ />
+ billing.upgrade_box.free_trial
+ </li>
+ </ul>
+ <div
+ className="big-spacer-left"
+ >
+ <Link
+ className="spacer-left"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ target="_blank"
+ to="/documentation/sonarcloud-pricing/"
+ >
+ learn_more
+ </Link>
+ </div>
+</CardPlan>
+`;
+
+exports[`should be actionable 1`] = `
+<div
+ className="card-plan card-plan-actionable"
+ onClick={[MockFunction]}
+ role="radio"
+ tabIndex={0}
+>
+ <h2
+ className="card-plan-header big-spacer-bottom"
+ >
+ <span
+ className="display-flex-center"
+ >
+ <i
+ className="icon-radio spacer-right"
+ />
+ Free Plan
+ </span>
+ <span
+ className="card-plan-price"
+ >
+ billing.price_format.0
+ </span>
+ </h2>
+ <div
+ className="card-plan-body"
+ >
+ <div>
+ content
+ </div>
+ </div>
+</div>
+`;
+
+exports[`should be actionable 2`] = `
+<div
+ aria-checked={true}
+ className="card-plan card-plan-actionable selected"
+ onClick={
+ [MockFunction] {
+ "calls": Array [
+ Array [
+ Object {
+ "currentTarget": Object {
+ "blur": [Function],
+ },
+ "preventDefault": [Function],
+ "stopPropagation": [Function],
+ "target": Object {
+ "blur": [Function],
+ },
+ },
+ ],
+ ],
+ "results": Array [
+ Object {
+ "isThrow": false,
+ "value": undefined,
+ },
+ ],
+ }
+ }
+ role="radio"
+ tabIndex={0}
+>
+ <h2
+ className="card-plan-header big-spacer-bottom"
+ >
+ <span
+ className="display-flex-center"
+ >
+ <i
+ className="icon-radio spacer-right is-checked"
+ />
+ Free Plan
+ </span>
+ <span
+ className="card-plan-price"
+ >
+ billing.price_format.0
+ </span>
+ </h2>
+ <div
+ className="card-plan-body"
+ >
+ <div>
+ content
+ </div>
+ </div>
+</div>
+`;
+
+exports[`should render correctly 1`] = `
+<div
+ className="card-plan"
+ role="radio"
+ tabIndex={0}
+>
+ <h2
+ className="card-plan-header big-spacer-bottom"
+ >
+ <span
+ className="display-flex-center"
+ >
+ Paid Plan
+ </span>
+ <FormattedMessage
+ defaultMessage="billing.price_from_x"
+ id="billing.price_from_x"
+ values={
+ Object {
+ "price": <span
+ className="card-plan-price"
+ >
+ $10
+ </span>,
+ }
+ }
+ />
+ </h2>
+ <div
+ className="card-plan-body"
+ >
+ <div>
+ content
+ </div>
+ </div>
+ <div
+ className="card-plan-recommended"
+ >
+ <RecommendedIcon
+ className="spacer-right"
+ />
+ <FormattedMessage
+ defaultMessage="Recommended for you"
+ id="Recommended for you"
+ values={
+ Object {
+ "recommended": <strong>
+ recommended
+ </strong>,
+ }
+ }
+ />
+ </div>
+</div>
+`;
{subscriptionPlans !== undefined &&
filter !== Filters.Bind && (
<PlanStep
+ almApplication={this.props.almApplication}
+ almOrganization={this.props.almOrganization}
createOrganization={this.handleCreateOrganization}
onDone={this.props.onDone}
onUpgradeFail={this.props.onUpgradeFail}
- onlyPaid={false /* TODO */}
open={step === Step.Plan}
subscriptionPlans={subscriptionPlans}
/>
</OrganizationDetailsStep>
{subscriptionPlans !== undefined && (
<PlanStep
+ almApplication={this.props.almApplication}
+ almOrganization={this.props.almOrganization}
createOrganization={this.handleCreateOrganization}
onDone={this.props.onDone}
- onlyPaid={false /* TODO */}
open={step === Step.Plan}
subscriptionPlans={subscriptionPlans}
/>
handleOrgDetailsFinish: (organization: T.Organization) => Promise<void>;
handleOrgDetailsStepOpen: () => void;
onDone: () => void;
- onlyPaid?: boolean;
organization?: T.Organization;
step: Step;
subscriptionPlans?: T.SubscriptionPlan[];
createOrganization={this.handleCreateOrganization}
onDone={this.props.onDone}
onUpgradeFail={this.props.onUpgradeFail}
- onlyPaid={this.props.onlyPaid}
open={this.props.step === Step.Plan}
subscriptionPlans={subscriptionPlans}
/>
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { FormattedMessage } from 'react-intl';
-import { Link } from 'react-router';
-import Radio from '../../../components/controls/Radio';
+import { FreeCardPlan, PaidCardPlan } from '../components/CardPlan';
import { translate } from '../../../helpers/l10n';
+import { AlmOrganization, AlmApplication } from '../../../app/types';
export enum Plan {
Free = 'free',
}
interface Props {
+ almApplication?: AlmApplication;
+ almOrganization?: AlmOrganization;
onChange: (plan: Plan) => void;
plan: Plan;
startingPrice: string;
};
render() {
- const { plan } = this.props;
+ const { almApplication, almOrganization, plan } = this.props;
+ const hasPrivateRepo = Boolean(almOrganization && almOrganization.privateRepos > 0);
+ const onlyPrivateRepo = Boolean(
+ hasPrivateRepo && almOrganization && almOrganization.publicRepos === 0
+ );
+
+ const cards = [
+ <PaidCardPlan
+ isRecommended={hasPrivateRepo}
+ key="paid"
+ onClick={this.handlePaidPlanClick}
+ selected={plan === Plan.Paid}
+ startingPrice={this.props.startingPrice}
+ />,
+ <FreeCardPlan
+ almName={almApplication && almApplication.name}
+ disabled={onlyPrivateRepo}
+ hasWarning={hasPrivateRepo && plan === Plan.Free}
+ key="free"
+ onClick={this.handleFreePlanClick}
+ selected={plan === Plan.Free}
+ />
+ ];
+
return (
<div
aria-label={translate('onboarding.create_organization.choose_plan')}
- className="huge-spacer-bottom"
+ className="display-flex-row huge-spacer-bottom"
role="radiogroup">
- <div>
- <Radio checked={plan === Plan.Free} onCheck={this.handleFreePlanClick}>
- <span>{translate('billing.free_plan.title')}</span>
- </Radio>
- <p className="note markdown little-spacer-top">
- {translate('billing.free_plan.description')}
- </p>
- </div>
- <div className="big-spacer-top">
- <Radio checked={plan === Plan.Paid} onCheck={this.handlePaidPlanClick}>
- <span>{translate('billing.paid_plan.title')}</span>
- </Radio>
- <p className="note markdown little-spacer-top">
- <FormattedMessage
- defaultMessage={translate('billing.paid_plan.description')}
- id="billing.paid_plan.description"
- values={{
- price: this.props.startingPrice,
- more: (
- <>
- {' '}
- <Link target="_blank" to="/documentation/sonarcloud-pricing/">
- {translate('learn_more')}
- </Link>
- <br />
- </>
- )
- }}
- />
- </p>
- </div>
+ {hasPrivateRepo ? cards : cards.reverse()}
</div>
);
}
import BillingFormShim from './BillingFormShim';
import PlanSelect, { Plan } from './PlanSelect';
import { formatPrice } from './utils';
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
import Step from '../../tutorials/components/Step';
+import { SubmitButton } from '../../../components/ui/buttons';
import { withCurrentUser } from '../../../components/hoc/withCurrentUser';
-import { translate } from '../../../helpers/l10n';
import { getExtensionStart } from '../../../app/components/extensions/utils';
-import { SubmitButton } from '../../../components/ui/buttons';
-import DeferredSpinner from '../../../components/common/DeferredSpinner';
+import { translate } from '../../../helpers/l10n';
const BillingForm = withCurrentUser(BillingFormShim);
interface Props {
+ almApplication?: T.AlmApplication;
+ almOrganization?: T.AlmOrganization;
createOrganization: () => Promise<string>;
onDone: () => void;
onUpgradeFail?: () => void;
- onlyPaid?: boolean;
open: boolean;
subscriptionPlans: T.SubscriptionPlan[];
}
constructor(props: Props) {
super(props);
this.state = {
- plan: props.onlyPaid ? Plan.Paid : Plan.Free,
+ plan: props.almOrganization && props.almOrganization.privateRepos > 0 ? Plan.Paid : Plan.Free,
ready: false,
submitting: false
};
<div className="boxed-group-inner">
{this.state.ready && (
<>
- {!this.props.onlyPaid && (
- <PlanSelect
- onChange={this.handlePlanChange}
- plan={this.state.plan}
- startingPrice={formatPrice(startedPrice)}
- />
- )}
+ <PlanSelect
+ almApplication={this.props.almApplication}
+ almOrganization={this.props.almOrganization}
+ onChange={this.handlePlanChange}
+ plan={this.state.plan}
+ startingPrice={formatPrice(startedPrice)}
+ />
{this.state.plan === Plan.Paid ? (
<BillingForm
};
render() {
+ const { almOrganization } = this.props;
const stepTitle = translate(
- this.props.onlyPaid
+ almOrganization && almOrganization.privateRepos > 0 && almOrganization.publicRepos === 0
? 'onboarding.create_organization.enter_payment_details'
: 'onboarding.create_organization.choose_plan'
);
description: 'description-foo',
key: 'key-foo',
name: 'name-foo',
+ privateRepos: 0,
+ publicRepos: 3,
url: 'http://example.com/foo'
};
key: 'key-foo',
name: 'name-foo',
personal: true,
+ privateRepos: 0,
+ publicRepos: 3,
url: 'http://example.com/foo'
};
key: 'sonarsource',
name: 'SonarSource',
personal: false,
+ privateRepos: 0,
+ publicRepos: 3,
url: 'https://www.sonarsource.com'
}
}),
showOnboardingTutorial: false
};
-const almOrganization = {
+const fooAlmOrganization = {
+ avatar: 'my-avatar',
+ key: 'foo',
+ name: 'Foo',
+ personal: true,
+ privateRepos: 0,
+ publicRepos: 3
+};
+
+const fooBarAlmOrganization = {
avatar: 'https://avatars3.githubusercontent.com/u/37629810?v=4',
key: 'Foo&Bar',
name: 'Foo & Bar',
- personal: true
+ personal: true,
+ privateRepos: 0,
+ publicRepos: 3
};
const boundOrganization = { key: 'foobar', name: 'Foo & Bar' };
it('should render with auto personal organization bind page', async () => {
(getAlmOrganization as jest.Mock<any>).mockResolvedValueOnce({
- almOrganization: {
- key: 'foo',
- name: 'Foo',
- avatar: 'my-avatar',
- personal: true
- }
+ almOrganization: fooAlmOrganization
});
const wrapper = shallowRender({
currentUser: { ...user, externalProvider: 'github', personalOrganization: 'foo' },
it('should render with organization bind page', async () => {
(getAlmOrganization as jest.Mock<any>).mockResolvedValueOnce({
- almOrganization: {
- key: 'foo',
- name: 'Foo',
- avatar: 'my-avatar',
- personal: false
- }
+ almOrganization: { ...fooAlmOrganization, personal: false }
});
const wrapper = shallowRender({
currentUser: { ...user, externalProvider: 'github' },
});
it('should slugify and find a uniq organization key', async () => {
- (getAlmOrganization as jest.Mock<any>).mockResolvedValueOnce({ almOrganization });
+ (getAlmOrganization as jest.Mock<any>).mockResolvedValueOnce({
+ almOrganization: fooBarAlmOrganization
+ });
(getOrganizations as jest.Mock<any>).mockResolvedValueOnce({
organizations: [{ key: 'foo-and-bar' }, { key: 'foo-and-bar-1' }]
});
state: { organization: 'foo', tab: 'manual' }
});
- wrapper.setState({
- almOrganization: { key: 'foo', name: 'Foo', avatar: 'my-avatar', personal: false }
- });
+ wrapper.setState({ almOrganization: { ...fooAlmOrganization, personal: false } });
(get as jest.Mock<any>).mockReturnValueOnce(Date.now().toString());
wrapper.instance().handleOrgCreated('foo');
expect(push).toHaveBeenCalledWith({
it('should display AutoOrganizationCreate with already bound organization', async () => {
(getAlmOrganization as jest.Mock<any>).mockResolvedValueOnce({
- almOrganization: { ...almOrganization, personal: false },
+ almOrganization: { ...fooBarAlmOrganization, personal: false },
boundOrganization
});
(get as jest.Mock<any>).mockReturnValueOnce(Date.now().toString());
it('should redirect to org page when already bound and no binding in progress', async () => {
(getAlmOrganization as jest.Mock<any>).mockResolvedValueOnce({
- almOrganization,
+ almOrganization: fooBarAlmOrganization,
boundOrganization
});
const push = jest.fn();
expect(wrapper).toMatchSnapshot();
});
-it('should preselect paid plan', async () => {
- const wrapper = shallowRender({ onlyPaid: true });
-
- await waitAndUpdate(wrapper);
- wrapper.find('OrganizationDetailsForm').prop<Function>('onContinue')(organization);
- await waitAndUpdate(wrapper);
- expect(wrapper.find('PlanStep').prop('onlyPaid')).toBe(true);
-});
-
function shallowRender(props: Partial<ManualOrganizationCreate['props']> = {}) {
return shallow(
<ManualOrganizationCreate
import * as React from 'react';
import { shallow } from 'enzyme';
import PlanSelect, { Plan } from '../PlanSelect';
+import { click } from '../../../../helpers/testUtils';
it('should render and select', () => {
const onChange = jest.fn();
- const wrapper = shallow(<PlanSelect onChange={onChange} plan={Plan.Free} startingPrice="10" />);
+ const wrapper = shallowRender({ onChange });
expect(wrapper).toMatchSnapshot();
- wrapper.find('Radio[checked=false]').prop<Function>('onCheck')();
+ click(wrapper.find('PaidCardPlan'));
expect(onChange).toBeCalledWith(Plan.Paid);
-
wrapper.setProps({ plan: Plan.Paid });
expect(wrapper).toMatchSnapshot();
});
+
+it('should recommend paid plan', () => {
+ const wrapper = shallowRender({
+ almOrganization: { key: 'foo', name: 'Foo', personal: false, privateRepos: 1, publicRepos: 5 },
+ plan: Plan.Paid
+ });
+ expect(wrapper.find('PaidCardPlan').prop('isRecommended')).toBe(true);
+ expect(wrapper.find('FreeCardPlan').prop('disabled')).toBe(false);
+ expect(wrapper.find('FreeCardPlan').prop('hasWarning')).toBe(false);
+
+ wrapper.setProps({ plan: Plan.Free });
+ expect(wrapper.find('FreeCardPlan').prop('hasWarning')).toBe(true);
+});
+
+it('should recommend paid plan and disable free plan', () => {
+ const wrapper = shallowRender({
+ almOrganization: { key: 'foo', name: 'Foo', personal: false, privateRepos: 1, publicRepos: 0 }
+ });
+ expect(wrapper.find('PaidCardPlan').prop('isRecommended')).toBe(true);
+ expect(wrapper.find('FreeCardPlan').prop('disabled')).toBe(true);
+});
+
+function shallowRender(props: Partial<PlanSelect['props']> = {}) {
+ return shallow(
+ <PlanSelect onChange={jest.fn()} plan={Plan.Free} startingPrice="10" {...props} />
+ );
+}
it('should preselect paid plan', async () => {
const wrapper = shallow(
<PlanStep
+ almOrganization={{
+ avatar: 'my-avatar',
+ key: 'foo',
+ name: 'Foo',
+ personal: true,
+ privateRepos: 5,
+ publicRepos: 0
+ }}
createOrganization={jest.fn()}
onDone={jest.fn()}
onUpgradeFail={jest.fn()}
- onlyPaid={true}
open={true}
subscriptionPlans={subscriptionPlans}
/>
);
await waitAndUpdate(wrapper);
- expect(wrapper).toMatchSnapshot();
expect(wrapper.dive()).toMatchSnapshot();
});
expect(
shallowRender({
almInstallId: 'foo',
- almOrganization: { avatar: 'foo-avatar', key: 'foo', name: 'Foo', personal: false },
+ almOrganization: {
+ avatar: 'foo-avatar',
+ key: 'foo',
+ name: 'Foo',
+ personal: false,
+ privateRepos: 0,
+ publicRepos: 3
+ },
boundOrganization: { avatar: 'bound-avatar', key: 'bound', name: 'Bound' }
}).find('Alert')
).toMatchSnapshot();
</div>
</OrganizationDetailsStep>
<PlanStep
+ almApplication={
+ Object {
+ "backgroundColor": "#0052CC",
+ "iconPath": "\\"/static/authbitbucket/bitbucket.svg\\"",
+ "installationUrl": "https://bitbucket.org/install/app",
+ "key": "bitbucket",
+ "name": "BitBucket",
+ }
+ }
+ almOrganization={
+ Object {
+ "avatar": "http://example.com/avatar",
+ "description": "description-foo",
+ "key": "key-foo",
+ "name": "name-foo",
+ "personal": false,
+ "privateRepos": 0,
+ "publicRepos": 3,
+ "url": "http://example.com/foo",
+ }
+ }
createOrganization={[Function]}
onDone={[MockFunction]}
onUpgradeFail={[MockFunction]}
- onlyPaid={false}
open={false}
subscriptionPlans={
Array [
"key": "key-foo",
"name": "name-foo",
"personal": false,
+ "privateRepos": 0,
+ "publicRepos": 3,
"url": "http://example.com/foo",
}
}
/>
</OrganizationDetailsStep>
<PlanStep
+ almApplication={
+ Object {
+ "backgroundColor": "#0052CC",
+ "iconPath": "\\"/static/authbitbucket/bitbucket.svg\\"",
+ "installationUrl": "https://bitbucket.org/install/app",
+ "key": "bitbucket",
+ "name": "BitBucket",
+ }
+ }
+ almOrganization={
+ Object {
+ "avatar": "http://example.com/avatar",
+ "description": "description-foo",
+ "key": "key-foo",
+ "name": "name-foo",
+ "personal": false,
+ "privateRepos": 0,
+ "publicRepos": 3,
+ "url": "http://example.com/foo",
+ }
+ }
createOrganization={[Function]}
onDone={[MockFunction]}
onUpgradeFail={[MockFunction]}
- onlyPaid={false}
open={false}
subscriptionPlans={
Array [
/>
</OrganizationDetailsStep>
<PlanStep
+ almApplication={
+ Object {
+ "backgroundColor": "#0052CC",
+ "iconPath": "\\"/static/authbitbucket/bitbucket.svg\\"",
+ "installationUrl": "https://bitbucket.org/install/app",
+ "key": "bitbucket",
+ "name": "BitBucket",
+ }
+ }
+ almOrganization={
+ Object {
+ "avatar": "http://example.com/avatar",
+ "description": "description-foo",
+ "key": "key-foo",
+ "name": "name-foo",
+ "personal": true,
+ "privateRepos": 0,
+ "publicRepos": 3,
+ "url": "http://example.com/foo",
+ }
+ }
createOrganization={[Function]}
onDone={[MockFunction]}
- onlyPaid={false}
open={false}
subscriptionPlans={
Array [
"key": "foo",
"name": "Foo",
"personal": true,
+ "privateRepos": 0,
+ "publicRepos": 3,
}
}
handleCancelImport={[Function]}
"key": "sonarsource",
"name": "SonarSource",
"personal": false,
+ "privateRepos": 0,
+ "publicRepos": 3,
"url": "https://www.sonarsource.com",
}
}
"key": "foo",
"name": "Foo",
"personal": false,
+ "privateRepos": 0,
+ "publicRepos": 3,
}
}
className=""
exports[`should render and select 1`] = `
<div
aria-label="onboarding.create_organization.choose_plan"
- className="huge-spacer-bottom"
+ className="display-flex-row huge-spacer-bottom"
role="radiogroup"
>
- <div>
- <Radio
- checked={true}
- onCheck={[Function]}
- >
- <span>
- billing.free_plan.title
- </span>
- </Radio>
- <p
- className="note markdown little-spacer-top"
- >
- billing.free_plan.description
- </p>
- </div>
- <div
- className="big-spacer-top"
- >
- <Radio
- checked={false}
- onCheck={[Function]}
- >
- <span>
- billing.paid_plan.title
- </span>
- </Radio>
- <p
- className="note markdown little-spacer-top"
- >
- <FormattedMessage
- defaultMessage="billing.paid_plan.description"
- id="billing.paid_plan.description"
- values={
- Object {
- "more": <React.Fragment>
-
- <Link
- onlyActiveOnIndex={false}
- style={Object {}}
- target="_blank"
- to="/documentation/sonarcloud-pricing/"
- >
- learn_more
- </Link>
- <br />
- </React.Fragment>,
- "price": "10",
- }
- }
- />
- </p>
- </div>
+ <FreeCardPlan
+ disabled={false}
+ hasWarning={false}
+ key="free"
+ onClick={[Function]}
+ selected={true}
+ />
+ <PaidCardPlan
+ isRecommended={false}
+ key="paid"
+ onClick={[Function]}
+ selected={false}
+ startingPrice="10"
+ />
</div>
`;
exports[`should render and select 2`] = `
<div
aria-label="onboarding.create_organization.choose_plan"
- className="huge-spacer-bottom"
+ className="display-flex-row huge-spacer-bottom"
role="radiogroup"
>
- <div>
- <Radio
- checked={false}
- onCheck={[Function]}
- >
- <span>
- billing.free_plan.title
- </span>
- </Radio>
- <p
- className="note markdown little-spacer-top"
- >
- billing.free_plan.description
- </p>
- </div>
- <div
- className="big-spacer-top"
- >
- <Radio
- checked={true}
- onCheck={[Function]}
- >
- <span>
- billing.paid_plan.title
- </span>
- </Radio>
- <p
- className="note markdown little-spacer-top"
- >
- <FormattedMessage
- defaultMessage="billing.paid_plan.description"
- id="billing.paid_plan.description"
- values={
- Object {
- "more": <React.Fragment>
-
- <Link
- onlyActiveOnIndex={false}
- style={Object {}}
- target="_blank"
- to="/documentation/sonarcloud-pricing/"
- >
- learn_more
- </Link>
- <br />
- </React.Fragment>,
- "price": "10",
- }
- }
- />
- </p>
- </div>
+ <FreeCardPlan
+ disabled={false}
+ hasWarning={false}
+ key="free"
+ onClick={[Function]}
+ selected={false}
+ />
+ <PaidCardPlan
+ isRecommended={false}
+ key="paid"
+ onClick={[Function]}
+ selected={true}
+ startingPrice="10"
+ />
</div>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should preselect paid plan 1`] = `
-<Step
- finished={false}
- onOpen={[Function]}
- open={true}
- renderForm={[Function]}
- renderResult={[Function]}
- stepNumber={2}
- stepTitle="onboarding.create_organization.enter_payment_details"
-/>
-`;
-
-exports[`should preselect paid plan 2`] = `
<div
className="boxed-group onboarding-step is-open"
>
<div
className="boxed-group-inner"
>
+ <PlanSelect
+ almOrganization={
+ Object {
+ "avatar": "my-avatar",
+ "key": "foo",
+ "name": "Foo",
+ "personal": true,
+ "privateRepos": 5,
+ "publicRepos": 0,
+ }
+ }
+ onChange={[Function]}
+ plan="paid"
+ startingPrice="billing.price_format.100"
+ />
<Connect(withCurrentUser(BillingFormShim))
onCommit={[MockFunction]}
onFailToUpgrade={[MockFunction]}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function RecommendedIcon({ className, fill = 'currentColor', size }: IconProps) {
+ return (
+ <Icon className={className} size={size}>
+ <path
+ d="M15.089 13.199l-1.742-3.736c-0.962 1.401-2.464 2.398-4.203 2.701l1.459 3.128c0.186 0.4 0.764 0.373 0.914-0.040l0.748-2.054 0.154-0.072 2.054 0.748c0.412 0.151 0.804-0.276 0.618-0.675zM8.040 0.384c-3.003 0-5.446 2.443-5.446 5.446s2.443 5.446 5.446 5.446c3.003 0 5.446-2.443 5.446-5.446s-2.443-5.446-5.446-5.446zM10.689 5.429l-0.966 0.941 0.228 1.33c0.070 0.406-0.358 0.711-0.718 0.522l-1.194-0.628-1.194 0.628c-0.363 0.19-0.788-0.118-0.718-0.522l0.228-1.33-0.966-0.941c-0.293-0.286-0.131-0.786 0.274-0.844l1.335-0.194 0.597-1.209c0.181-0.367 0.707-0.368 0.888 0l0.597 1.209 1.335 0.194c0.405 0.059 0.568 0.558 0.274 0.844zM2.732 9.463l-1.742 3.736c-0.187 0.4 0.208 0.825 0.618 0.674l2.054-0.748 0.154 0.072 0.748 2.054c0.15 0.412 0.727 0.441 0.914 0.040l1.459-3.128c-1.739-0.302-3.241-1.3-4.203-2.701z"
+ style={{ fill }}
+ />
+ </Icon>
+ );
+}
});
it('#isPersonal', () => {
- expect(isPersonal({ key: 'foo', name: 'Foo', personal: true })).toBeTruthy();
- expect(isPersonal({ key: 'foo', name: 'Foo', personal: false })).toBeFalsy();
+ expect(
+ isPersonal({ key: 'foo', name: 'Foo', personal: true, privateRepos: 0, publicRepos: 3 })
+ ).toBeTruthy();
+ expect(
+ isPersonal({ key: 'foo', name: 'Foo', personal: false, privateRepos: 0, publicRepos: 3 })
+ ).toBeFalsy();
});
it('#sanitizeAlmId', () => {
raw=Raw
recent_history=Recent History
recently_browsed=Recently Browsed
+recommended=Recommended
refresh=Refresh
reload=Reload
remove=Remove