From 8dea56b4c70d09fc069add79ab0617bf6bb0e16a Mon Sep 17 00:00:00 2001 From: =?utf8?q?Gr=C3=A9goire=20Aubert?= Date: Tue, 27 Nov 2018 14:13:26 +0100 Subject: [PATCH] SONARCLOUD-176 Allow upgrading to paid organization when provisioning projects * Update UpgradeOrganizationBox component with new cardPlan component * Display upgrade box only when needed * Introduce isDefined type guard * Update repositories selection workflow --- server/sonar-web/src/main/js/api/plugins.ts | 3 +- .../main/js/app/styles/components/modals.css | 35 +- .../src/main/js/app/styles/init/misc.css | 14 + server/sonar-web/src/main/js/app/theme.js | 7 + .../js/apps/code/components/Components.tsx | 3 +- .../drilldown/BubbleChart.tsx | 7 +- .../drilldown/TreeMapView.tsx | 3 +- .../BillingFormShim.tsx | 10 +- .../js/apps/create/components/CardPlan.css | 25 +- .../js/apps/create/components/CardPlan.tsx | 66 ++-- .../create/components/OrganizationSelect.tsx | 28 +- .../UpgradeOrganizationAdvantages.tsx | 49 +++ .../components/UpgradeOrganizationBox.tsx | 115 ++++++ .../components/UpgradeOrganizationModal.tsx | 128 +++++++ .../__mocks__/BillingFormShim.tsx | 4 + .../__tests__/BillingFormShim-test.tsx | 0 .../components/__tests__/CardPlan-test.tsx | 10 +- .../UpgradeOrganizationAdvantages-test.tsx | 26 ++ .../__tests__/UpgradeOrganizationBox-test.tsx | 62 ++++ .../UpgradeOrganizationModal-test.tsx} | 47 ++- .../BillingFormShim-test.tsx.snap | 0 .../__snapshots__/CardPlan-test.tsx.snap | 139 +++----- .../OrganizationSelect-test.tsx.snap | 72 ++-- ...pgradeOrganizationAdvantages-test.tsx.snap | 22 ++ .../UpgradeOrganizationBox-test.tsx.snap | 31 ++ .../UpgradeOrganizationModal-test.tsx.snap | 26 ++ .../organization/CreateOrganization.tsx | 19 +- .../apps/create/organization/PlanSelect.tsx | 7 +- .../js/apps/create/organization/PlanStep.tsx | 7 +- .../__tests__/PlanSelect-test.tsx | 2 +- .../CreateOrganization-test.tsx.snap | 95 +---- .../__snapshots__/PlanSelect-test.tsx.snap | 4 +- .../__snapshots__/PlanStep-test.tsx.snap | 6 +- .../main/js/apps/create/organization/utils.ts | 2 - .../apps/create/project/AlmRepositoryItem.tsx | 117 ++++--- .../apps/create/project/AutoProjectCreate.tsx | 18 +- .../apps/create/project/CreateProjectPage.tsx | 3 + .../create/project/RemoteRepositories.tsx | 129 ++++--- .../apps/create/project/SetupProjectBox.tsx | 141 ++++++++ .../__tests__/AlmRepositoryItem-test.tsx | 8 +- .../__tests__/AutoProjectCreate-test.tsx | 24 +- .../__tests__/RemoteRepositories-test.tsx | 59 ++-- .../__tests__/SetupProjectBox-test.tsx | 69 ++++ .../AlmRepositoryItem-test.tsx.snap | 328 ++++++++++-------- .../AutoProjectCreate-test.tsx.snap | 23 +- .../RemoteRepositories-test.tsx.snap | 211 ++++++----- .../SetupProjectBox-test.tsx.snap | 45 +++ .../src/main/js/apps/create/project/style.css | 104 ++++++ .../components/OrganizationJustCreated.css | 2 +- .../permissions/project/components/App.tsx | 47 ++- .../project/components/AppContainer.ts | 20 +- .../components/GraphsTooltips.tsx | 3 +- .../src/main/js/apps/projects/routes.ts | 3 +- .../main/js/apps/projectsManagement/App.tsx | 2 + .../apps/projectsManagement/AppContainer.tsx | 5 + ...rm.tsx => ChangeDefaultVisibilityForm.tsx} | 7 +- .../projectsManagement/CreateProjectForm.tsx | 17 +- .../js/apps/projectsManagement/Header.tsx | 17 +- .../projectsManagement/__tests__/App-test.tsx | 1 + ...x => ChangeDefaultVisibilityForm-test.tsx} | 6 +- .../__tests__/CreateProjectForm-test.tsx | 8 +- .../__tests__/Header-test.tsx | 12 +- ...ChangeDefaultVisibilityForm-test.tsx.snap} | 3 - .../CreateProjectForm-test.tsx.snap | 75 ++-- .../__snapshots__/Header-test.tsx.snap | 2 +- .../tutorials/onboarding/OnboardingModal.tsx | 5 +- .../OnboardingModal-test.tsx.snap | 3 +- .../src/main/js/apps/tutorials/styles.css | 1 + .../js/components/charts/AdvancedTimeline.tsx | 3 +- .../main/js/components/charts/BubbleChart.tsx | 2 +- .../src/main/js/components/charts/TreeMap.tsx | 1 + .../main/js/components/charts/TreeMapRect.tsx | 1 + .../src/main/js/components/controls/Modal.tsx | 10 +- .../main/js/components/controls/Tooltip.tsx | 23 +- .../js/components/controls/react-select.css | 11 + .../__tests__/withUserOrganizations-test.tsx | 5 +- .../components/hoc/withUserOrganizations.tsx | 24 +- .../src/main/js/components/tags/TagsList.css | 1 + .../src/main/js/components/ui/buttons.css | 6 + .../types.ts} | 6 +- .../resources/org/sonar/l10n/core.properties | 18 +- 81 files changed, 1900 insertions(+), 803 deletions(-) rename server/sonar-web/src/main/js/apps/create/{organization => components}/BillingFormShim.tsx (80%) create mode 100644 server/sonar-web/src/main/js/apps/create/components/UpgradeOrganizationAdvantages.tsx create mode 100644 server/sonar-web/src/main/js/apps/create/components/UpgradeOrganizationBox.tsx create mode 100644 server/sonar-web/src/main/js/apps/create/components/UpgradeOrganizationModal.tsx rename server/sonar-web/src/main/js/apps/create/{organization => components}/__mocks__/BillingFormShim.tsx (85%) rename server/sonar-web/src/main/js/apps/create/{organization => components}/__tests__/BillingFormShim-test.tsx (100%) create mode 100644 server/sonar-web/src/main/js/apps/create/components/__tests__/UpgradeOrganizationAdvantages-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/create/components/__tests__/UpgradeOrganizationBox-test.tsx rename server/sonar-web/src/main/js/{components/common/UpgradeOrganizationBox.tsx => apps/create/components/__tests__/UpgradeOrganizationModal-test.tsx} (50%) rename server/sonar-web/src/main/js/apps/create/{organization => components}/__tests__/__snapshots__/BillingFormShim-test.tsx.snap (100%) create mode 100644 server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/UpgradeOrganizationAdvantages-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/UpgradeOrganizationBox-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/UpgradeOrganizationModal-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/create/project/SetupProjectBox.tsx create mode 100644 server/sonar-web/src/main/js/apps/create/project/__tests__/SetupProjectBox-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/SetupProjectBox-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/create/project/style.css rename server/sonar-web/src/main/js/apps/projectsManagement/{ChangeVisibilityForm.tsx => ChangeDefaultVisibilityForm.tsx} (92%) rename server/sonar-web/src/main/js/apps/projectsManagement/__tests__/{ChangeVisibilityForm-test.tsx => ChangeDefaultVisibilityForm-test.tsx} (91%) rename server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/{ChangeVisibilityForm-test.tsx.snap => ChangeDefaultVisibilityForm-test.tsx.snap} (98%) rename server/sonar-web/src/main/js/{components/common/UpgradeOrganizationBox.css => helpers/types.ts} (88%) diff --git a/server/sonar-web/src/main/js/api/plugins.ts b/server/sonar-web/src/main/js/api/plugins.ts index faf104eba0b..c1ed5678bc8 100644 --- a/server/sonar-web/src/main/js/api/plugins.ts +++ b/server/sonar-web/src/main/js/api/plugins.ts @@ -20,6 +20,7 @@ import { findLastIndex } from 'lodash'; import { getJSON, post } from '../helpers/request'; import throwGlobalError from '../app/utils/throwGlobalError'; +import { isDefined } from '../helpers/types'; export interface Plugin { key: string; @@ -94,7 +95,7 @@ function getLastUpdates(updates: undefined | Update[]): Update[] { return index > -1 ? updates[index] : undefined; } ); - return lastUpdate.filter(Boolean) as Update[]; + return lastUpdate.filter(isDefined); } function addChangelog(update: Update, updates?: Update[]) { diff --git a/server/sonar-web/src/main/js/app/styles/components/modals.css b/server/sonar-web/src/main/js/app/styles/components/modals.css index 1a4274b1451..20d02df96f9 100644 --- a/server/sonar-web/src/main/js/app/styles/components/modals.css +++ b/server/sonar-web/src/main/js/app/styles/components/modals.css @@ -72,6 +72,10 @@ opacity: 1; } +.modal-no-backdrop { + background-color: transparent; +} + .modal-open, .ReactModal__Body--open { overflow: hidden; @@ -101,22 +105,45 @@ padding: 10px; } +.modal-simple { + border-radius: 3px; +} + .modal-simple-head { - padding: calc(2 * var(--pagePadding)) calc(3 * var(--pagePadding)); + padding: var(--pagePadding) calc(2 * var(--pagePadding)); } .modal-simple-head h1 { + margin-top: var(--pagePadding); font-size: var(--hugeFontSize); font-weight: bold; line-height: 30px; } +.modal-simple-head h2 { + font-size: var(--bigFontSize); + font-weight: bold; + line-height: 24px; +} + .modal-simple-body { - padding: 0 calc(3 * var(--pagePadding)); + padding: 0 calc(2 * var(--pagePadding)) var(--pagePadding); } -.modal-simple-footer { - padding: calc(2 * var(--pagePadding)) calc(3 * var(--pagePadding)); +.modal-simple-foot { + padding: calc(2 * var(--pagePadding)) calc(2 * var(--pagePadding)); + border-radius: 3px; +} + +.modal-simple-foot-action { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--pagePadding) calc(2 * var(--pagePadding)); + border-top: 1px solid var(--barBorderColor); + background-color: var(--barBackgroundColor); + text-align: right; + border-radius: 3px; } .modal-field, 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 0e080164b07..e8cc9b5fa24 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 @@ -101,6 +101,10 @@ th.nowrap { margin-left: 40px !important; } +.huge-spacer-right { + margin-right: 40px !important; +} + .little-spacer-left { margin-left: 4px !important; } @@ -299,6 +303,16 @@ td.big-spacer-top { align-items: center; } +.display-flex-space-around { + display: flex !important; + justify-content: space-around; +} + +.display-flex-space-between { + display: flex !important; + justify-content: space-between; +} + .display-flex-stretch { display: flex !important; align-items: stretch; diff --git a/server/sonar-web/src/main/js/app/theme.js b/server/sonar-web/src/main/js/app/theme.js index 82a207470a7..1a22613180b 100644 --- a/server/sonar-web/src/main/js/app/theme.js +++ b/server/sonar-web/src/main/js/app/theme.js @@ -90,6 +90,7 @@ module.exports = { bigFontSize: '16px', hugeFontSize: '24px', + largeControlHeight: `${4 * grid}px`, controlHeight: `${3 * grid}px`, smallControlHeight: `${2.5 * grid}px`, tinyControlHeight: `${2 * grid}px`, @@ -137,6 +138,7 @@ module.exports = { // sonarcloud sonarcloudOrange500: '#fd6a00', sonarcloudOrange700: '#db5700', + sonarcloudBlack100: '#ffffff', sonarcloudBlack200: '#f9f9fb', sonarcloudBlack250: '#e6e8ea', @@ -145,6 +147,11 @@ module.exports = { sonarcloudBlack700: '#434447', sonarcloudBlack800: '#2d3032', sonarcloudBlack900: '#070706', + + sonarcloudBlue500: '#4c9bd6', + sonarcloudBlue600: '#327bb3', + sonarcloudBlue900: '#0b3c62', + sonarcloudBorderGray: 'rgba(207, 211, 215, 0.5)', sonarcloudFontFamily: "Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif" diff --git a/server/sonar-web/src/main/js/apps/code/components/Components.tsx b/server/sonar-web/src/main/js/apps/code/components/Components.tsx index c8672d01b7e..03331dadc14 100644 --- a/server/sonar-web/src/main/js/apps/code/components/Components.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/Components.tsx @@ -22,6 +22,7 @@ import * as classNames from 'classnames'; import Component from './Component'; import ComponentsEmpty from './ComponentsEmpty'; import ComponentsHeader from './ComponentsHeader'; +import { isDefined } from '../../../helpers/types'; import { getCodeMetrics, showLeakMeasure } from '../utils'; interface Props { @@ -36,7 +37,7 @@ interface Props { export default function Components(props: Props) { const { baseComponent, branchLike, components, rootComponent, selected } = props; const metricKeys = getCodeMetrics(rootComponent.qualifier, branchLike); - const metrics = metricKeys.map(metric => props.metrics[metric]).filter(Boolean); + const metrics = metricKeys.map(metric => props.metrics[metric]).filter(isDefined); const isLeak = Boolean(baseComponent && showLeakMeasure(branchLike)); return ( diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/BubbleChart.tsx b/server/sonar-web/src/main/js/apps/component-measures/drilldown/BubbleChart.tsx index c52981c4872..7ed3d2a44b0 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/BubbleChart.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/BubbleChart.tsx @@ -19,7 +19,7 @@ */ import * as React from 'react'; import EmptyResult from './EmptyResult'; -import OriginalBubbleChart, { BubbleItem } from '../../../components/charts/BubbleChart'; +import OriginalBubbleChart from '../../../components/charts/BubbleChart'; import ColorRatingsLegend from '../../../components/charts/ColorRatingsLegend'; import HelpTooltip from '../../../components/controls/HelpTooltip'; import { formatMeasure, isDiffMetric } from '../../../helpers/measures'; @@ -31,6 +31,7 @@ import { } from '../../../helpers/l10n'; import { getBubbleMetrics, getBubbleYDomain, isProjectOverview } from '../utils'; import { RATING_COLORS } from '../../../helpers/constants'; +import { isDefined } from '../../../helpers/types'; const HEIGHT = 500; @@ -113,13 +114,13 @@ export default class BubbleChart extends React.PureComponent { size, color: colors !== undefined - ? RATING_COLORS[Math.max(...colors.filter(Boolean) as number[]) - 1] + ? RATING_COLORS[Math.max(...colors.filter(isDefined)) - 1] : undefined, data: component, tooltip: this.getTooltip(component.name, { x, y, size, colors }, metrics) }; }) - .filter(Boolean) as BubbleItem[]; + .filter(isDefined); const formatXTick = (tick: string | number | undefined) => formatMeasure(tick, metrics.x.type); const formatYTick = (tick: string | number | undefined) => formatMeasure(tick, metrics.y.type); diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.tsx b/server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.tsx index 2f07bb3545a..a86c27e135f 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.tsx @@ -30,6 +30,7 @@ import TreeMap, { TreeMapItem } from '../../../components/charts/TreeMap'; import { translate, translateWithParameters, getLocalizedMetricName } from '../../../helpers/l10n'; import { formatMeasure, isDiffMetric } from '../../../helpers/measures'; import { getBranchLikeUrl } from '../../../helpers/urls'; +import { isDefined } from '../../../helpers/types'; interface Props { branchLike?: T.BranchLike; @@ -96,7 +97,7 @@ export default class TreeMapView extends React.PureComponent { }) }; }) - .filter(Boolean) as TreeMapItem[]; + .filter(isDefined); }; getLevelColorScale = () => diff --git a/server/sonar-web/src/main/js/apps/create/organization/BillingFormShim.tsx b/server/sonar-web/src/main/js/apps/create/components/BillingFormShim.tsx similarity index 80% rename from server/sonar-web/src/main/js/apps/create/organization/BillingFormShim.tsx rename to server/sonar-web/src/main/js/apps/create/components/BillingFormShim.tsx index ce44586f84a..c0efc0882ff 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/BillingFormShim.tsx +++ b/server/sonar-web/src/main/js/apps/create/components/BillingFormShim.tsx @@ -21,12 +21,16 @@ import * as React from 'react'; interface ChildrenProps { onSubmit: React.FormEventHandler; - renderFormFields: () => React.ReactElement; - renderSubmitGroup: (submitText?: string) => React.ReactElement; + processingUpgrade: boolean; + renderFormFields: () => React.ReactNode; + renderNextCharge: () => React.ReactNode; + renderRecap: () => React.ReactNode; + renderSubmitButton: (submitText?: string) => React.ReactNode; + renderSubmitGroup: (submitText?: string) => React.ReactNode; } interface Props { - children: (props: ChildrenProps) => React.ReactElement; + children: (props: ChildrenProps) => React.ReactNode; initialCountry?: string; currentUser: T.CurrentUser; onCommit: () => void; diff --git a/server/sonar-web/src/main/js/apps/create/components/CardPlan.css b/server/sonar-web/src/main/js/apps/create/components/CardPlan.css index 7b9f04e90c1..02d7ddb056d 100644 --- a/server/sonar-web/src/main/js/apps/create/components/CardPlan.css +++ b/server/sonar-web/src/main/js/apps/create/components/CardPlan.css @@ -28,19 +28,23 @@ border-radius: 3px; box-sizing: border-box; margin-right: calc(2 * var(--gridSize)); + transition: all 0.2s ease; +} + +.card-plan.highlight { + box-shadow: var(--defaultShadow); } .card-plan:last-child { margin-right: 0; } -.card-plan-actionable { - cursor: pointer; - transition: all 0.2s ease; +.card-plan:focus { + outline: none; } -.card-plan-actionable:focus { - outline: none; +.card-plan-actionable { + cursor: pointer; } .card-plan-actionable:not(.disabled):hover { @@ -52,6 +56,11 @@ border-color: var(--darkBlue); } +.card-plan-actionable.selected .card-plan-recommended { + border: solid 1px var(--darkBlue); + border-top: none; +} + .card-plan-actionable.disabled { cursor: not-allowed; background-color: var(--disableGrayBg); @@ -82,10 +91,6 @@ 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; } @@ -97,7 +102,7 @@ bottom: -1px; width: 450px; color: #fff; - background-color: var(--darkBlue); + background-color: var(--blue); border-radius: 0 0 3px 3px; box-sizing: border-box; font-size: var(--smallFontSize); diff --git a/server/sonar-web/src/main/js/apps/create/components/CardPlan.tsx b/server/sonar-web/src/main/js/apps/create/components/CardPlan.tsx index 372e64bc526..a2970d28577 100644 --- a/server/sonar-web/src/main/js/apps/create/components/CardPlan.tsx +++ b/server/sonar-web/src/main/js/apps/create/components/CardPlan.tsx @@ -21,12 +21,11 @@ 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 UpgradeOrganizationAdvantages from './UpgradeOrganizationAdvantages'; import RecommendedIcon from '../../../components/icons-components/RecommendedIcon'; import { Alert } from '../../../components/ui/Alert'; -import { formatPrice, TRIAL_DURATION_DAYS } from '../organization/utils'; -import { translate, translateWithParameters } from '../../../helpers/l10n'; -import * as theme from '../../../app/theme'; +import { formatPrice } from '../organization/utils'; +import { translate } from '../../../helpers/l10n'; import './CardPlan.css'; interface Props { @@ -34,7 +33,7 @@ interface Props { disabled?: boolean; onClick?: () => void; selected?: boolean; - startingPrice?: string; + startingPrice?: number; } interface CardProps extends Props { @@ -64,17 +63,18 @@ export default function CardPlan(props: CardProps) { )} {props.title} - {startingPrice ? ( - {startingPrice} - }} - /> - ) : ( - {formatPrice(0)} - )} + {startingPrice !== undefined && + (startingPrice ? ( + {formatPrice(startingPrice)} + }} + /> + ) : ( + {formatPrice(0)} + ))}
{props.children}
{recommended && ( @@ -101,12 +101,16 @@ export function FreeCardPlan({ almName, hasWarning, ...props }: FreeProps) { const showWarning = almName && hasWarning && !props.disabled; return ( - + <> -
    -
  • {translate('billing.free_plan.all_projects_analyzed_public')}
  • -
  • {translate('billing.free_plan.anyone_can_browse_source_code')}
  • -
+
+
    +
  • + {translate('billing.free_plan.all_projects_analyzed_public')} +
  • +
  • {translate('billing.free_plan.anyone_can_browse_source_code')}
  • +
+
{showWarning && ( - {translateWithParameters('billing.upgrade_box.free_trial_x', TRIAL_DURATION_DAYS)} - - ]; - return ( <> -
    - {advantages.map((text, idx) => ( -
  • - - {text} -
  • - ))} -
+
- + {translate('billing.pricing.learn_more')}
diff --git a/server/sonar-web/src/main/js/apps/create/components/OrganizationSelect.tsx b/server/sonar-web/src/main/js/apps/create/components/OrganizationSelect.tsx index 84751b58556..7e424b54a56 100644 --- a/server/sonar-web/src/main/js/apps/create/components/OrganizationSelect.tsx +++ b/server/sonar-web/src/main/js/apps/create/components/OrganizationSelect.tsx @@ -62,19 +62,25 @@ export function getOptionRenderer(hideIcons?: boolean) { const icon = organization.alm ? `sonarcloud/${sanitizeAlmId(organization.alm.key)}` : 'sonarcloud-square-logo'; + const isPaidOrg = organization.subscription === 'PAID'; return ( - - {!hideIcons && ( - {organization.alm +
+ + {!hideIcons && ( + {organization.alm + )} + {organization.name} + {organization.key} + + {isPaidOrg && ( +
{translate('organization.paid_plan.badge')}
)} - {organization.name} - {organization.key} - +
); }; } diff --git a/server/sonar-web/src/main/js/apps/create/components/UpgradeOrganizationAdvantages.tsx b/server/sonar-web/src/main/js/apps/create/components/UpgradeOrganizationAdvantages.tsx new file mode 100644 index 00000000000..7b4935b8107 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/components/UpgradeOrganizationAdvantages.tsx @@ -0,0 +1,49 @@ +/* + * 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 CheckIcon from '../../../components/icons-components/CheckIcon'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; +import * as theme from '../../../app/theme'; + +const TRIAL_DURATION_DAYS = 14; + +export default function UpgradeOrganizationAdvantages() { + return ( +
    + {translate('billing.upgrade_box.unlimited_private_projects')} + {translate('billing.upgrade_box.strict_control_private_data')} + {translate('billing.upgrade_box.cancel_anytime')} + + + {translateWithParameters('billing.upgrade_box.free_trial_x', TRIAL_DURATION_DAYS)} + + +
+ ); +} + +export function Advantage({ children }: { children: React.ReactNode }) { + return ( +
  • + + {children} +
  • + ); +} diff --git a/server/sonar-web/src/main/js/apps/create/components/UpgradeOrganizationBox.tsx b/server/sonar-web/src/main/js/apps/create/components/UpgradeOrganizationBox.tsx new file mode 100644 index 00000000000..aede9e520bd --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/components/UpgradeOrganizationBox.tsx @@ -0,0 +1,115 @@ +/* + * 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 { Link } from 'react-router'; +import UpgradeOrganizationAdvantages from './UpgradeOrganizationAdvantages'; +import UpgradeOrganizationModal from './UpgradeOrganizationModal'; +import CardPlan from './CardPlan'; +import { Button } from '../../../components/ui/buttons'; +import { translate, hasMessage } from '../../../helpers/l10n'; +import { getSubscriptionPlans } from '../../../api/billing'; + +interface Props { + className?: string; + insideModal?: boolean; + onOrganizationUpgrade: () => void; + organization: T.Organization; +} + +interface State { + subscriptionPlans: T.SubscriptionPlan[]; + upgradeOrganizationModal: boolean; +} + +export default class UpgradeOrganizationBox extends React.PureComponent { + mounted = false; + state: State = { subscriptionPlans: [], upgradeOrganizationModal: false }; + + componentDidMount() { + this.mounted = true; + this.fetchSubscriptionPlans(); + } + + componentWillUnmount() { + this.mounted = false; + } + + fetchSubscriptionPlans = () => { + return getSubscriptionPlans().then(subscriptionPlans => { + if (this.mounted) { + this.setState({ subscriptionPlans }); + } + }); + }; + + handleUpgradeClick = () => { + this.setState({ upgradeOrganizationModal: true }); + }; + + handleUpgradeOrganizationModalClose = () => { + if (this.mounted) { + this.setState({ upgradeOrganizationModal: false }); + } + }; + + handleOrganizationUpgrade = () => { + this.props.onOrganizationUpgrade(); + this.handleUpgradeOrganizationModalClose(); + }; + + render() { + if (!hasMessage('billing.upgrade_box.header')) { + return null; + } + + const { subscriptionPlans, upgradeOrganizationModal } = this.state; + const startingPrice = subscriptionPlans[0] && subscriptionPlans[0].price; + + return ( + <> + + <> + +
    + + + {translate('billing.pricing.learn_more')} + +
    + +
    + {upgradeOrganizationModal && ( + + )} + + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/create/components/UpgradeOrganizationModal.tsx b/server/sonar-web/src/main/js/apps/create/components/UpgradeOrganizationModal.tsx new file mode 100644 index 00000000000..fbcd02d76fe --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/components/UpgradeOrganizationModal.tsx @@ -0,0 +1,128 @@ +/* + * 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 { FormattedMessage } from 'react-intl'; +import UpgradeOrganizationAdvantages from './UpgradeOrganizationAdvantages'; +import BillingFormShim from './BillingFormShim'; +import DeferredSpinner from '../../../components/common/DeferredSpinner'; +import Modal from '../../../components/controls/Modal'; +import { ResetButtonLink } from '../../../components/ui/buttons'; +import { getExtensionStart } from '../../../app/components/extensions/utils'; +import { translate } from '../../../helpers/l10n'; +import { withCurrentUser } from '../../../components/hoc/withCurrentUser'; + +const BillingForm = withCurrentUser(BillingFormShim); + +interface Props { + insideModal?: boolean; + onUpgradeDone: () => void; + onClose: () => void; + organization: T.Organization; + subscriptionPlans: T.SubscriptionPlan[]; +} + +interface State { + ready: boolean; +} + +export default class UpgradeOrganizationModal extends React.PureComponent { + mounted = false; + state: State = { ready: false }; + + componentDidMount() { + this.mounted = true; + getExtensionStart('billing/billing').then( + () => { + if (this.mounted) { + this.setState({ ready: true }); + } + }, + () => {} + ); + } + + componentWillUnmount() { + this.mounted = false; + } + + render() { + const header = translate('billing.upgrade_box.upgrade_to_paid_plan'); + + if (!this.state.ready) { + return null; + } + + return ( + +
    +

    {header}

    +
    + + {({ + onSubmit, + processingUpgrade, + renderFormFields, + renderNextCharge, + renderRecap, + renderSubmitButton + }) => ( +
    +
    +
    +

    + {this.props.organization.name} + }} + /> +

    + +
    + {renderFormFields()} +
    {renderRecap()}
    +
    +
    + {renderNextCharge()} +
    + + {renderSubmitButton()} + + {translate('cancel')} + +
    +
    + + )} +
    +
    + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/create/organization/__mocks__/BillingFormShim.tsx b/server/sonar-web/src/main/js/apps/create/components/__mocks__/BillingFormShim.tsx similarity index 85% rename from server/sonar-web/src/main/js/apps/create/organization/__mocks__/BillingFormShim.tsx rename to server/sonar-web/src/main/js/apps/create/components/__mocks__/BillingFormShim.tsx index 48f5e0dc984..a5a43958cca 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/__mocks__/BillingFormShim.tsx +++ b/server/sonar-web/src/main/js/apps/create/components/__mocks__/BillingFormShim.tsx @@ -25,7 +25,11 @@ export default class BillingFormShim extends React.Component<{ children: any }>
    {this.props.children({ onSubmit: jest.fn(), + processingUpgrade: true, renderFormFields: () =>
    , + renderNextCharge: () =>
    , + renderRecap: () =>
    , + renderSubmitButton: () =>
    , renderSubmitGroup: () =>
    })}
    diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/BillingFormShim-test.tsx b/server/sonar-web/src/main/js/apps/create/components/__tests__/BillingFormShim-test.tsx similarity index 100% rename from server/sonar-web/src/main/js/apps/create/organization/__tests__/BillingFormShim-test.tsx rename to server/sonar-web/src/main/js/apps/create/components/__tests__/BillingFormShim-test.tsx diff --git a/server/sonar-web/src/main/js/apps/create/components/__tests__/CardPlan-test.tsx b/server/sonar-web/src/main/js/apps/create/components/__tests__/CardPlan-test.tsx index 2190a5073db..3f848bbc6e4 100644 --- a/server/sonar-web/src/main/js/apps/create/components/__tests__/CardPlan-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/components/__tests__/CardPlan-test.tsx @@ -25,7 +25,7 @@ import { click } from '../../../../helpers/testUtils'; it('should render correctly', () => { expect( shallow( - +
    content
    ) @@ -42,12 +42,12 @@ it('should be actionable', () => { expect(wrapper).toMatchSnapshot(); click(wrapper); - wrapper.setProps({ selected: true }); + wrapper.setProps({ selected: true, startingPrice: 0 }); expect(wrapper).toMatchSnapshot(); }); describe('#FreeCardPlan', () => { - it('should render correctly', () => { + it('should render', () => { expect(shallow()).toMatchSnapshot(); }); @@ -65,7 +65,7 @@ describe('#FreeCardPlan', () => { }); describe('#PaidCardPlan', () => { - it('should render correctly', () => { - expect(shallow()).toMatchSnapshot(); + it('should render', () => { + expect(shallow()).toMatchSnapshot(); }); }); diff --git a/server/sonar-web/src/main/js/apps/create/components/__tests__/UpgradeOrganizationAdvantages-test.tsx b/server/sonar-web/src/main/js/apps/create/components/__tests__/UpgradeOrganizationAdvantages-test.tsx new file mode 100644 index 00000000000..e922c42cd24 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/components/__tests__/UpgradeOrganizationAdvantages-test.tsx @@ -0,0 +1,26 @@ +/* + * 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 UpgradeOrganizationAdvantages from '../UpgradeOrganizationAdvantages'; + +it('should render correctly ', () => { + expect(shallow()).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/create/components/__tests__/UpgradeOrganizationBox-test.tsx b/server/sonar-web/src/main/js/apps/create/components/__tests__/UpgradeOrganizationBox-test.tsx new file mode 100644 index 00000000000..65823c7454a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/components/__tests__/UpgradeOrganizationBox-test.tsx @@ -0,0 +1,62 @@ +/* + * 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 UpgradeOrganizationBox from '../UpgradeOrganizationBox'; +import { hasMessage } from '../../../../helpers/l10n'; +import { getSubscriptionPlans } from '../../../../api/billing'; +import { waitAndUpdate, click } from '../../../../helpers/testUtils'; + +jest.mock('../../../../helpers/l10n', () => { + const l10n = require.requireActual('../../../../helpers/l10n'); + l10n.hasMessage = jest.fn().mockReturnValue(true); + return l10n; +}); + +jest.mock('../../../../api/billing', () => ({ + getSubscriptionPlans: jest.fn().mockResolvedValue([{ maxNcloc: 100000, price: 10 }]) +})); + +const organization = { key: 'foo', name: 'Foo' }; + +beforeEach(() => { + (hasMessage as jest.Mock).mockClear(); + (getSubscriptionPlans as jest.Mock).mockClear(); +}); + +it('should not render', () => { + (hasMessage as jest.Mock).mockReturnValueOnce(false); + expect( + shallow( + + ).type() + ).toBeNull(); +}); + +it('should render correctly', async () => { + const wrapper = shallow( + + ); + await waitAndUpdate(wrapper); + expect(getSubscriptionPlans).toHaveBeenCalled(); + expect(wrapper).toMatchSnapshot(); + click(wrapper.find('Button')); + expect(wrapper.find('UpgradeOrganizationModal').exists()).toBe(true); +}); diff --git a/server/sonar-web/src/main/js/components/common/UpgradeOrganizationBox.tsx b/server/sonar-web/src/main/js/apps/create/components/__tests__/UpgradeOrganizationModal-test.tsx similarity index 50% rename from server/sonar-web/src/main/js/components/common/UpgradeOrganizationBox.tsx rename to server/sonar-web/src/main/js/apps/create/components/__tests__/UpgradeOrganizationModal-test.tsx index 1d6254b8a37..b15d78bb077 100644 --- a/server/sonar-web/src/main/js/components/common/UpgradeOrganizationBox.tsx +++ b/server/sonar-web/src/main/js/apps/create/components/__tests__/UpgradeOrganizationModal-test.tsx @@ -18,32 +18,27 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { Link } from 'react-router'; -import { translate, hasMessage } from '../../helpers/l10n'; -import './UpgradeOrganizationBox.css'; +import { shallow } from 'enzyme'; +import UpgradeOrganizationModal from '../UpgradeOrganizationModal'; +import { getExtensionStart } from '../../../../app/components/extensions/utils'; +import { waitAndUpdate } from '../../../../helpers/testUtils'; -interface Props { - organization: string; -} +jest.mock('../../../../app/components/extensions/utils', () => ({ + getExtensionStart: jest.fn().mockResolvedValue(undefined) +})); -export default function UpgradeOrganizationBox({ organization }: Props) { - if (!hasMessage('billing.upgrade_box.button')) { - return null; - } - return ( -
    -

    {translate('billing.upgrade_box.header')}

    -

    {translate('billing.upgrade_box.text')}

    -
    - - {translate('billing.upgrade_box.button')} - -
    -
    +const organization = { key: 'foo', name: 'Foo' }; + +it('should render correctly', async () => { + const wrapper = shallow( + ); -} + await waitAndUpdate(wrapper); + expect(getExtensionStart).toHaveBeenCalled(); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/BillingFormShim-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/BillingFormShim-test.tsx.snap similarity index 100% rename from server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/BillingFormShim-test.tsx.snap rename to server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/BillingFormShim-test.tsx.snap diff --git a/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/CardPlan-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/CardPlan-test.tsx.snap index 028ea133bbe..f98ce7f4c44 100644 --- a/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/CardPlan-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/CardPlan-test.tsx.snap @@ -1,37 +1,51 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`#FreeCardPlan should render correctly 1`] = ` +exports[`#FreeCardPlan should render 1`] = ` -
      -
    • - billing.free_plan.all_projects_analyzed_public -
    • -
    • - billing.free_plan.anyone_can_browse_source_code -
    • -
    +
      +
    • + billing.free_plan.all_projects_analyzed_public +
    • +
    • + billing.free_plan.anyone_can_browse_source_code +
    • +
    +
    `; exports[`#FreeCardPlan should render disabled with info 1`] = ` -
      -
    • - billing.free_plan.all_projects_analyzed_public -
    • -
    • - billing.free_plan.anyone_can_browse_source_code -
    • -
    +
      +
    • + billing.free_plan.all_projects_analyzed_public +
    • +
    • + billing.free_plan.anyone_can_browse_source_code +
    • +
    +
    @@ -51,18 +65,25 @@ exports[`#FreeCardPlan should render disabled with info 1`] = ` exports[`#FreeCardPlan should render with warning 1`] = ` -
      -
    • - billing.free_plan.all_projects_analyzed_public -
    • -
    • - billing.free_plan.anyone_can_browse_source_code -
    • -
    +
      +
    • + billing.free_plan.all_projects_analyzed_public +
    • +
    • + billing.free_plan.anyone_can_browse_source_code +
    • +
    +
    @@ -79,60 +100,13 @@ exports[`#FreeCardPlan should render with warning 1`] = ` `; -exports[`#PaidCardPlan should render correctly 1`] = ` +exports[`#PaidCardPlan should render 1`] = ` -
      -
    • - - billing.upgrade_box.unlimited_private_projects -
    • -
    • - - billing.upgrade_box.strict_control_private_data -
    • -
    • - - billing.upgrade_box.cancel_anytime -
    • -
    • - - - billing.upgrade_box.free_trial_x.14 - -
    • -
    +
    @@ -141,7 +115,7 @@ exports[`#PaidCardPlan should render correctly 1`] = ` onlyActiveOnIndex={false} style={Object {}} target="_blank" - to="/documentation/sonarcloud-pricing/" + to="/about/pricing" > billing.pricing.learn_more @@ -167,11 +141,6 @@ exports[`should be actionable 1`] = ` /> Free Plan - - billing.price_format.0 -
    - $10 + billing.price_format.10 , } } diff --git a/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/OrganizationSelect-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/OrganizationSelect-test.tsx.snap index 4b5035cd307..1003db90587 100644 --- a/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/OrganizationSelect-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/OrganizationSelect-test.tsx.snap @@ -34,46 +34,64 @@ exports[`should render correctly 1`] = ` `; exports[`should render options correctly 1`] = ` - - SonarCloud - Foo +
    - foo + SonarCloud + Foo + + foo + - +
    `; exports[`should render options correctly 2`] = ` - - github - Bar +
    - bar + github + Bar + + bar + - +
    `; exports[`should render options correctly 3`] = ` - - Foo +
    - foo + Foo + + foo + - +
    `; diff --git a/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/UpgradeOrganizationAdvantages-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/UpgradeOrganizationAdvantages-test.tsx.snap new file mode 100644 index 00000000000..9a93403230e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/UpgradeOrganizationAdvantages-test.tsx.snap @@ -0,0 +1,22 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` +
      + + billing.upgrade_box.unlimited_private_projects + + + billing.upgrade_box.strict_control_private_data + + + billing.upgrade_box.cancel_anytime + + + + billing.upgrade_box.free_trial_x.14 + + +
    +`; diff --git a/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/UpgradeOrganizationBox-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/UpgradeOrganizationBox-test.tsx.snap new file mode 100644 index 00000000000..249f04ebdce --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/UpgradeOrganizationBox-test.tsx.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` + + + +
    + + + billing.pricing.learn_more + +
    +
    +
    +`; diff --git a/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/UpgradeOrganizationModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/UpgradeOrganizationModal-test.tsx.snap new file mode 100644 index 00000000000..f826be7df3e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/UpgradeOrganizationModal-test.tsx.snap @@ -0,0 +1,26 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` + +
    +

    + billing.upgrade_box.upgrade_to_paid_plan +

    +
    + + + +
    +`; 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 fecfbdb42b2..9977f8a739b 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 @@ -24,10 +24,8 @@ import { times } from 'lodash'; import { connect } from 'react-redux'; import { Dispatch } from 'redux'; import { Helmet } from 'react-helmet'; -import { FormattedMessage } from 'react-intl'; -import { Link, withRouter, WithRouterProps } from 'react-router'; +import { withRouter, WithRouterProps } from 'react-router'; import { - formatPrice, ORGANIZATION_IMPORT_REDIRECT_TO_PROJECT_TIMESTAMP, parseQuery, serializeQuery, @@ -410,7 +408,6 @@ export class CreateOrganization extends React.PureComponent @@ -421,19 +418,7 @@ export class CreateOrganization extends React.PureComponent - , - price: formattedPrice, - more: ( - - {translate('learn_more')} - - ) - }} - /> + {translate('onboarding.create_organization.page.description')}

    )} diff --git a/server/sonar-web/src/main/js/apps/create/organization/PlanSelect.tsx b/server/sonar-web/src/main/js/apps/create/organization/PlanSelect.tsx index 25a9964d360..c9ba5a9cc0d 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/PlanSelect.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/PlanSelect.tsx @@ -20,7 +20,6 @@ import * as React from 'react'; import { FreeCardPlan, PaidCardPlan } from '../components/CardPlan'; import { translate } from '../../../helpers/l10n'; -import { AlmOrganization, AlmApplication } from '../../../app/types'; export enum Plan { Free = 'free', @@ -28,11 +27,11 @@ export enum Plan { } interface Props { - almApplication?: AlmApplication; - almOrganization?: AlmOrganization; + almApplication?: T.AlmApplication; + almOrganization?: T.AlmOrganization; onChange: (plan: Plan) => void; plan: Plan; - startingPrice: string; + startingPrice: number; } export default class PlanSelect extends React.PureComponent { diff --git a/server/sonar-web/src/main/js/apps/create/organization/PlanStep.tsx b/server/sonar-web/src/main/js/apps/create/organization/PlanStep.tsx index adf99b376d2..7d3cfbfe798 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/PlanStep.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/PlanStep.tsx @@ -18,9 +18,8 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import BillingFormShim from './BillingFormShim'; import PlanSelect, { Plan } from './PlanSelect'; -import { formatPrice } from './utils'; +import BillingFormShim from '../components/BillingFormShim'; import DeferredSpinner from '../../../components/common/DeferredSpinner'; import Step from '../../tutorials/components/Step'; import { SubmitButton } from '../../../components/ui/buttons'; @@ -96,7 +95,7 @@ export default class PlanStep extends React.PureComponent { renderForm = () => { const { submitting } = this.state; const { subscriptionPlans } = this.props; - const startedPrice = subscriptionPlans && subscriptionPlans[0] && subscriptionPlans[0].price; + const startingPrice = subscriptionPlans && subscriptionPlans[0] && subscriptionPlans[0].price; return (
    {this.state.ready && ( @@ -106,7 +105,7 @@ export default class PlanStep extends React.PureComponent { almOrganization={this.props.almOrganization} onChange={this.handlePlanChange} plan={this.state.plan} - startingPrice={formatPrice(startedPrice)} + startingPrice={startingPrice} /> {this.state.plan === Plan.Paid ? ( diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/PlanSelect-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/PlanSelect-test.tsx index 8851e7d869a..fcc2a755f11 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/PlanSelect-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/PlanSelect-test.tsx @@ -56,6 +56,6 @@ it('should recommend paid plan and disable free plan', () => { function shallowRender(props: Partial = {}) { return shallow( - + ); } 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 f2bb69deb5d..5b7b823d209 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 @@ -101,24 +101,7 @@ exports[`should render with auto tab displayed 1`] = `

    - , - "more": - learn_more - , - "price": "billing.price_format.10", - } - } - /> + onboarding.create_organization.page.description

    - , - "more": - learn_more - , - "price": "billing.price_format.10", - } - } - /> + onboarding.create_organization.page.description

    - , - "more": - learn_more - , - "price": "billing.price_format.10", - } - } - /> + onboarding.create_organization.page.description

    - , - "more": - learn_more - , - "price": "billing.price_format.10", - } - } - /> + onboarding.create_organization.page.description

    - , - "more": - learn_more - , - "price": "billing.price_format.10", - } - } - /> + onboarding.create_organization.page.description

    `; @@ -41,7 +41,7 @@ exports[`should render and select 2`] = ` key="paid" onClick={[Function]} selected={true} - startingPrice="10" + startingPrice={10} />
    `; diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/PlanStep-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/PlanStep-test.tsx.snap index a979c50685f..451e76df78d 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/PlanStep-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/PlanStep-test.tsx.snap @@ -35,7 +35,7 @@ exports[`should preselect paid plan 1`] = ` } onChange={[Function]} plan="paid" - startingPrice="billing.price_format.100" + startingPrice={100} />
    void; identityProvider: T.IdentityProvider; repository: T.AlmRepository; selected: boolean; @@ -35,51 +40,83 @@ interface Props { } export default class AlmRepositoryItem extends React.PureComponent { - handleChange = () => { - this.props.toggleRepository(this.props.repository); + handleMouseEnter = () => { + this.props.highlightUpgradeBox(true); + }; + + handleMouseLeave = () => { + this.props.highlightUpgradeBox(false); + }; + + handleToggle = () => { + if (!this.props.disabled && !this.props.repository.linkedProjectKey) { + this.props.toggleRepository(this.props.repository); + } }; render() { - const { identityProvider, repository, selected } = this.props; + const { disabled, identityProvider, repository, selected } = this.props; const alreadyImported = Boolean(repository.linkedProjectKey); return ( - <> - - {identityProvider.name} - {this.props.repository.label} - - {repository.linkedProjectKey && ( - - - - {translate('onboarding.create_project.see_project')} - - ) - }} - /> - - )} - {repository.private && ( - -
    {translate('visibility.private')}
    -
    - )} - + +
  • +
    +
    + {disabled ? ( + + ) : ( + + )} + {identityProvider.name} + {this.props.repository.label} + {repository.private && ( +
    {translate('visibility.private')}
    + )} +
    + + {repository.linkedProjectKey && ( + + + + {translate('onboarding.create_project.see_project')} + + ) + }} + /> + + )} +
    +
  • +
    ); } } diff --git a/server/sonar-web/src/main/js/apps/create/project/AutoProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/AutoProjectCreate.tsx index 70f4f76cacd..f7e3bf6fb91 100644 --- a/server/sonar-web/src/main/js/apps/create/project/AutoProjectCreate.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/AutoProjectCreate.tsx @@ -31,6 +31,7 @@ import { save } from '../../../helpers/storage'; interface Props { almApplication: T.AlmApplication; boundOrganizations: T.Organization[]; + onOrganizationUpgrade: () => void; onProjectCreate: (projectKeys: string[], organization: string) => void; organization?: string; } @@ -40,11 +41,21 @@ interface State { } export default class AutoProjectCreate extends React.PureComponent { + mounted = false; + constructor(props: Props) { super(props); this.state = { selectedOrganization: this.getInitialSelectedOrganization(props) }; } + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + getInitialSelectedOrganization(props: Props) { if (props.organization) { return props.organization; @@ -89,6 +100,8 @@ export default class AutoProjectCreate extends React.PureComponent } const { selectedOrganization } = this.state; + const organization = boundOrganizations.find(o => o.key === selectedOrganization); + return ( <> organization={selectedOrganization} organizations={this.props.boundOrganizations} /> - {selectedOrganization && ( + {organization && ( )} diff --git a/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx b/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx index 882bf5b36a9..27e03f15e47 100644 --- a/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx @@ -33,9 +33,11 @@ import { hasAdvancedALMIntegration } from '../../../helpers/almIntegrations'; import { translate } from '../../../helpers/l10n'; import { getProjectUrl, getOrganizationUrl } from '../../../helpers/urls'; import '../../../app/styles/sonarcloud.css'; +import './style.css'; interface Props { currentUser: T.LoggedInUser; + fetchMyOrganizations: () => Promise; skipOnboarding: () => void; userOrganizations: T.Organization[]; } @@ -162,6 +164,7 @@ export class CreateProjectPage extends React.PureComponent alm && actions.provision )} + onOrganizationUpgrade={this.props.fetchMyOrganizations} onProjectCreate={this.handleProjectCreate} organization={state.organization} /> diff --git a/server/sonar-web/src/main/js/apps/create/project/RemoteRepositories.tsx b/server/sonar-web/src/main/js/apps/create/project/RemoteRepositories.tsx index c2a895ecc60..60f2e637ff0 100644 --- a/server/sonar-web/src/main/js/apps/create/project/RemoteRepositories.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/RemoteRepositories.tsx @@ -18,30 +18,42 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import * as classNames from 'classnames'; import AlmRepositoryItem from './AlmRepositoryItem'; +import SetupProjectBox from './SetupProjectBox'; import DeferredSpinner from '../../../components/common/DeferredSpinner'; -import { getRepositories, provisionProject } from '../../../api/alm-integration'; -import { SubmitButton } from '../../../components/ui/buttons'; -import { translate } from '../../../helpers/l10n'; +import UpgradeOrganizationBox from '../components/UpgradeOrganizationBox'; +import { getRepositories } from '../../../api/alm-integration'; +import { isDefined } from '../../../helpers/types'; +import { translateWithParameters } from '../../../helpers/l10n'; +import { Alert } from '../../../components/ui/Alert'; interface Props { almApplication: T.AlmApplication; + onOrganizationUpgrade: () => void; onProjectCreate: (projectKeys: string[], organization: string) => void; - organization: string; + organization: T.Organization; } type SelectedRepositories = { [key: string]: T.AlmRepository | undefined }; interface State { + highlight: boolean; loading: boolean; repositories: T.AlmRepository[]; selectedRepositories: SelectedRepositories; - submitting: boolean; + successfullyUpgraded: boolean; } export default class RemoteRepositories extends React.PureComponent { mounted = false; - state: State = { loading: true, repositories: [], selectedRepositories: {}, submitting: false }; + state: State = { + highlight: false, + loading: true, + repositories: [], + selectedRepositories: {}, + successfullyUpgraded: false + }; componentDidMount() { this.mounted = true; @@ -51,7 +63,7 @@ export default class RemoteRepositories extends React.PureComponent { const { organization } = this.props; - return getRepositories({ organization }).then( + return getRepositories({ organization: organization.key }).then( ({ repositories }) => { if (this.mounted) { this.setState({ loading: false, repositories }); @@ -76,26 +88,14 @@ export default class RemoteRepositories extends React.PureComponent) => { - event.preventDefault(); + handleHighlightUpgradeBox = (highlight: boolean) => { + this.setState({ highlight }); + }; - if (this.isValid()) { - const { selectedRepositories } = this.state; - this.setState({ submitting: true }); - provisionProject({ - installationKeys: Object.keys(selectedRepositories).filter(key => { - const repositories = selectedRepositories[key]; - return repositories && !repositories.private; - }), - organization: this.props.organization - }).then( - ({ projects }) => - this.props.onProjectCreate( - projects.map(project => project.projectKey), - this.props.organization - ), - this.handleProvisionFail - ); + handleOrganizationUpgrade = () => { + this.props.onOrganizationUpgrade(); + if (this.mounted) { + this.setState({ successfullyUpgraded: true }); } }; @@ -110,18 +110,12 @@ export default class RemoteRepositories extends React.PureComponent { - return this.state.repositories.some(repo => - Boolean(this.state.selectedRepositories[repo.installationKey]) - ); - }; - toggleRepository = (repository: T.AlmRepository) => { this.setState(({ selectedRepositories }) => ({ selectedRepositories: { @@ -134,29 +128,60 @@ export default class RemoteRepositories extends React.PureComponent Boolean(repository.private)); + const showUpgradebox = + !isPaidOrg && hasPrivateRepositories && organization.actions && organization.actions.admin; + return ( - - -
    +
    +
    + {this.state.successfullyUpgraded && ( + + {translateWithParameters( + 'onboarding.create_project.subscribtion_success_x', + organization.name + )} + + )} +
      - {this.state.repositories.map(repo => ( -
    • - -
    • + {repositories.map(repo => ( + ))}
    +
    +
    + {organization && ( +
    + selectedRepositories[r]) + .filter(isDefined)} + /> + {showUpgradebox && ( + + )}
    - {translate('setup')} - - - + )} +
    ); } } diff --git a/server/sonar-web/src/main/js/apps/create/project/SetupProjectBox.tsx b/server/sonar-web/src/main/js/apps/create/project/SetupProjectBox.tsx new file mode 100644 index 00000000000..2a086905dda --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/project/SetupProjectBox.tsx @@ -0,0 +1,141 @@ +/* + * 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 { partition } from 'lodash'; +import DeferredSpinner from '../../../components/common/DeferredSpinner'; +import { SubmitButton } from '../../../components/ui/buttons'; +import { provisionProject } from '../../../api/alm-integration'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; + +interface Props { + onProjectCreate: (projectKeys: string[], organization: string) => void; + onProvisionFail: () => Promise; + organization: T.Organization; + selectedRepositories: T.AlmRepository[]; +} + +interface State { + submitting: boolean; +} + +export default class SetupProjectBox extends React.PureComponent { + mounted = false; + state: State = { submitting: false }; + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + canSubmit = () => { + return !this.state.submitting && this.props.selectedRepositories.length > 0; + }; + + handleFormSubmit = (event: React.FormEvent) => { + event.preventDefault(); + + if (this.canSubmit()) { + const { selectedRepositories } = this.props; + this.setState({ submitting: true }); + provisionProject({ + installationKeys: selectedRepositories.map(repo => repo.installationKey), + organization: this.props.organization.key + }).then( + ({ projects }) => + this.props.onProjectCreate( + projects.map(project => project.projectKey), + this.props.organization.key + ), + this.handleProvisionFail + ); + } + }; + + handleProvisionFail = () => { + return this.props.onProvisionFail().then(() => { + if (this.mounted) { + this.setState({ submitting: false }); + } + }); + }; + + render() { + const { selectedRepositories } = this.props; + const hasSelectedRepositories = selectedRepositories.length > 0; + const [privateRepos = [], publicRepos = []] = partition( + selectedRepositories, + repo => repo.private + ); + return ( +
    +
    +

    + {selectedRepositories.length > 1 + ? translateWithParameters( + 'onboarding.create_project.x_repositories_selected', + selectedRepositories.length + ) + : translate('onboarding.create_project.1_repository_selected')} +

    +
    +
    +
    + {publicRepos.length === 1 && ( +

    {translate('onboarding.create_project.1_repository_created_as_public')}

    + )} + {publicRepos.length > 1 && ( +

    + {translateWithParameters( + 'onboarding.create_project.x_repository_created_as_public', + publicRepos.length + )} +

    + )} + {privateRepos.length === 1 && ( +

    {translate('onboarding.create_project.1_repository_created_as_private')}

    + )} + {privateRepos.length > 1 && ( +

    + {translateWithParameters( + 'onboarding.create_project.x_repository_created_as_private', + privateRepos.length + )} +

    + )} +
    +
    + + {translate('setup')} + + +
    +
    + + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/AlmRepositoryItem-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/AlmRepositoryItem-test.tsx index 79edff1ed9a..94a6c184f1e 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/AlmRepositoryItem-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/AlmRepositoryItem-test.tsx @@ -49,10 +49,14 @@ it('should render selected', () => { expect(getWrapper({ selected: true })).toMatchSnapshot(); }); -it('should render disabled', () => { +it('should render imported', () => { expect(getWrapper({ repository: repositories[0] })).toMatchSnapshot(); }); +it('should render disabed', () => { + expect(getWrapper({ disabled: true, repository: repositories[1] })).toMatchSnapshot(); +}); + it('should render private repositories', () => { expect(getWrapper({ repository: { ...repositories[1], private: true } })).toMatchSnapshot(); }); @@ -60,6 +64,8 @@ it('should render private repositories', () => { function getWrapper(props = {}) { return shallow( { expect(shallowRender({ boundOrganizations: [] })).toMatchSnapshot(); }); -it('should display the bounded organizations dropdown with the list of repositories', () => { +it('should display the bound organizations dropdown with the remote repositories', () => { expect(shallowRender({ organization: 'foo' })).toMatchSnapshot(); }); @@ -41,10 +57,8 @@ function shallowRender(props: Partial = {}) { return shallow( ({ getRepositories: jest.fn().mockResolvedValue({ @@ -32,13 +32,9 @@ jest.mock('../../../../api/alm-integration', () => ({ linkedProjectKey: 'proj_cool', linkedProjectName: 'Proj Cool' }, - { - label: 'Awesome Project', - installationKey: 'github/awesome' - } + { label: 'Awesome Project', installationKey: 'github/awesome' } ] - }), - provisionProject: jest.fn().mockResolvedValue({ projects: [{ projectKey: 'awesome' }] }) + }) })); const almApplication = { @@ -49,45 +45,60 @@ const almApplication = { name: 'GitHub' }; +const organization: T.Organization = { + alm: { key: 'github', url: '' }, + key: 'sonarsource', + name: 'SonarSource', + subscription: 'FREE' +}; + beforeEach(() => { (getRepositories as jest.Mock).mockClear(); - (provisionProject as jest.Mock).mockClear(); }); it('should display the list of repositories', async () => { const wrapper = shallowRender(); expect(wrapper).toMatchSnapshot(); await waitAndUpdate(wrapper); - expect(getRepositories).toHaveBeenCalledWith({ organization: 'sonarsource' }); expect(wrapper).toMatchSnapshot(); + expect(getRepositories).toHaveBeenCalledWith({ organization: 'sonarsource' }); }); -it('should correctly create a project', async () => { - const onProjectCreate = jest.fn(); - const wrapper = shallowRender({ onProjectCreate }); - (wrapper.instance() as RemoteRepositories).toggleRepository({ - label: 'Awesome Project', - installationKey: 'github/awesome' +it('should display the organization upgrade box', async () => { + (getRepositories as jest.Mock).mockResolvedValueOnce({ + repositories: [{ label: 'Foo Project', installationKey: 'github/foo', private: true }] }); + const wrapper = shallowRender({ organization: { ...organization, actions: { admin: true } } }); await waitAndUpdate(wrapper); + expect(wrapper.find('UpgradeOrganizationBox')).toMatchSnapshot(); + wrapper.find('UpgradeOrganizationBox').prop('onOrganizationUpgrade')(); + expect(wrapper.find('Alert[variant="success"]').exists()).toBe(true); +}); - expect(wrapper.find('SubmitButton')).toMatchSnapshot(); - submit(wrapper.find('form')); - expect(provisionProject).toBeCalledWith({ - installationKeys: ['github/awesome'], - organization: 'sonarsource' +it('should not display the organization upgrade box', () => { + (getRepositories as jest.Mock).mockResolvedValueOnce({ + repositories: [{ label: 'Bar Project', installationKey: 'github/bar', private: true }] + }); + const wrapper = shallowRender({ + organization: { + actions: { admin: true }, + alm: { key: 'github', url: '' }, + key: 'foobar', + name: 'FooBar', + subscription: 'PAID' + } }); - await waitAndUpdate(wrapper); - expect(onProjectCreate).toBeCalledWith(['awesome'], 'sonarsource'); + expect(wrapper.find('UpgradeOrganizationBox').exists()).toBe(false); }); function shallowRender(props: Partial = {}) { return shallow( ); diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/SetupProjectBox-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/SetupProjectBox-test.tsx new file mode 100644 index 00000000000..d2168beaf42 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/SetupProjectBox-test.tsx @@ -0,0 +1,69 @@ +/* + * 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 SetupProjectBox from '../SetupProjectBox'; +import { waitAndUpdate, submit } from '../../../../helpers/testUtils'; +import { provisionProject } from '../../../../api/alm-integration'; + +jest.mock('../../../../api/alm-integration', () => ({ + provisionProject: jest + .fn() + .mockResolvedValue({ projects: [{ projectKey: 'awesome' }, { projectKey: 'foo' }] }) +})); + +const organization: T.Organization = { + alm: { key: 'github', url: '' }, + key: 'sonarsource', + name: 'SonarSource', + subscription: 'FREE' +}; + +const selectedRepositories = [ + { label: 'Awesome Project', installationKey: 'github/awesome' }, + { label: 'Foo', installationKey: 'github/foo', private: true } +]; + +it('should correctly create projects', async () => { + const onProjectCreate = jest.fn(); + const wrapper = shallowRender({ onProjectCreate }); + + expect(wrapper).toMatchSnapshot(); + submit(wrapper.find('form')); + expect(provisionProject).toBeCalledWith({ + installationKeys: ['github/awesome', 'github/foo'], + organization: 'sonarsource' + }); + + await waitAndUpdate(wrapper); + expect(onProjectCreate).toBeCalledWith(['awesome', 'foo'], 'sonarsource'); +}); + +function shallowRender(props: Partial = {}) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AlmRepositoryItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AlmRepositoryItem-test.tsx.snap index e2c87e1c800..12bde76d9dc 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AlmRepositoryItem-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AlmRepositoryItem-test.tsx.snap @@ -1,155 +1,211 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should render correctly 1`] = ` - - - Foo Provider - +
  • +
    +
    + + Foo Provider + + Awesome Project + +
    +
    +
  • + +`; + +exports[`should render disabed 1`] = ` + +
  • +
    - Awesome Project - - - +
    + + Foo Provider + + Awesome Project + +
    +
    +
  • +
    `; -exports[`should render disabled 1`] = ` - - - Foo Provider - +
  • +
    - Cool Project - - - - - + + Foo Provider + + Cool Project + +
    + + + + onboarding.create_project.see_project + , } - > - onboarding.create_project.see_project - , - } - } - /> - - + } + /> + +
  • + + `; exports[`should render private repositories 1`] = ` - - - Foo Provider - - Awesome Project - - - + +
  • - visibility.private +
    + + Foo Provider + + Awesome Project + +
    + visibility.private +
    +
    - - +
  • +
    `; exports[`should render selected 1`] = ` - - - Foo Provider - +
  • +
    - Awesome Project - - - +
    + + Foo Provider + + Awesome Project + +
    +
    +
  • +
    `; diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap index 8db84dbe8df..0f3247f2994 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should display the bounded organizations dropdown with the list of repositories 1`] = ` +exports[`should display the bound organizations dropdown with the remote repositories 1`] = ` `; diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/RemoteRepositories-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/RemoteRepositories-test.tsx.snap index 8379a51721e..b265a69a05e 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/RemoteRepositories-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/RemoteRepositories-test.tsx.snap @@ -1,114 +1,143 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should correctly create a project 1`] = ` - - setup - -`; - exports[`should display the list of repositories 1`] = ` - -
    -
    -
      -
    - - setup - +
      + +
    +
    + - - +
    +
    `; exports[`should display the list of repositories 2`] = ` - -
    -
      -
    • - + -
    • -
    • - -
    • + } + selected={false} + toggleRepository={[Function]} + />
    -
    - - setup - - +
    +
    + - - +
    + +`; + +exports[`should display the organization upgrade box 1`] = ` + `; diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/SetupProjectBox-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/SetupProjectBox-test.tsx.snap new file mode 100644 index 00000000000..0606885275a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/SetupProjectBox-test.tsx.snap @@ -0,0 +1,45 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should correctly create projects 1`] = ` +
    +
    +

    + onboarding.create_project.x_repositories_selected.2 +

    +
    +
    +
    +

    + onboarding.create_project.1_repository_created_as_public +

    +

    + onboarding.create_project.1_repository_created_as_private +

    +
    +
    + + setup + + +
    +
    + +`; diff --git a/server/sonar-web/src/main/js/apps/create/project/style.css b/server/sonar-web/src/main/js/apps/create/project/style.css new file mode 100644 index 00000000000..b1481cd6d13 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/project/style.css @@ -0,0 +1,104 @@ +/* + * 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. + */ +.create-project { + display: flex !important; + justify-content: space-between; +} + +.create-project-repository { + display: flex; + align-items: center; + min-width: 500px; + height: 40px; + border: 1px solid var(--barBorderColor); + padding: var(--gridSize) calc(var(--gridSize) * 2); + margin-bottom: calc(var(--gridSize)); + box-sizing: border-box; + cursor: pointer; + transition: all 0.3s ease; +} + +.create-project-repository.disabled { + background-color: var(--disableGrayBg); + border-color: var(--disableGrayBorder); + cursor: default; +} + +.create-project-repository.imported { + cursor: default; +} + +.create-project-repository.selected { + background-color: var(--lightBlue); + border-color: var(--darkBlue); +} + +.create-project-repository:not(.imported):not(.disabled):hover, +.create-project-repository:not(.imported):not(.disabled):focus, +.create-project-repository:not(.imported):not(.disabled):active { + border-color: var(--blue); + box-shadow: none; +} + +.create-project-setup { + display: flex; + overflow: hidden; + opacity: 0; + flex-direction: column; + height: 0; + width: 450px; + margin-bottom: 0; + color: #fff; + background-color: var(--sonarcloudBlue900); + border: none; + border-radius: 3px; + transition: height 0.5s ease, opacity 0.4s ease-out, margin-bottom 0.5s ease-in; +} + +.create-project-setup.open { + opacity: 1; + height: 160px; + margin-bottom: calc(2.5 * var(--gridSize)); +} + +.create-project-setup h2 { + color: #fff; + font-weight: 700; + font-size: var(--bigFontSize); +} + +.create-project-setup .boxed-group-inner { + display: flex; + flex-direction: column; + flex-grow: 1; +} + +.create-project-setup .button { + border-color: var(--sonarcloudBlue500); + background-color: var(--sonarcloudBlue500); + color: #fff; + transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease; +} + +.create-project-setup .button:hover, +.create-project-setup .button:focus { + border-color: var(--sonarcloudBlue600); + background-color: var(--sonarcloudBlue600); +} diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationJustCreated.css b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationJustCreated.css index 2405d972144..17bec1f2bca 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationJustCreated.css +++ b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationJustCreated.css @@ -19,5 +19,5 @@ */ .organization-just-created { margin: 120px auto 0; - width: 480px; + width: 700px; } diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/App.tsx b/server/sonar-web/src/main/js/apps/permissions/project/components/App.tsx index 0f82247d151..748b4cbd025 100644 --- a/server/sonar-web/src/main/js/apps/permissions/project/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/permissions/project/components/App.tsx @@ -23,7 +23,7 @@ import { without } from 'lodash'; import AllHoldersList from './AllHoldersList'; import PageHeader from './PageHeader'; import PublicProjectDisclaimer from './PublicProjectDisclaimer'; -import UpgradeOrganizationBox from '../../../../components/common/UpgradeOrganizationBox'; +import UpgradeOrganizationBox from '../../../create/components/UpgradeOrganizationBox'; import VisibilitySelector from '../../../../components/common/VisibilitySelector'; import * as api from '../../../../api/permissions'; import { translate } from '../../../../helpers/l10n'; @@ -31,7 +31,9 @@ import '../../styles.css'; interface Props { component: T.Component; + fetchOrganization: (organization: string) => void; onComponentChange: (changes: Partial) => void; + organization?: T.Organization; } interface State { @@ -299,6 +301,16 @@ export default class App extends React.PureComponent { return Promise.resolve(); }; + handleOrganizationUpgrade = () => { + const { component, organization } = this.props; + if (organization) { + this.props.onComponentChange({ + configuration: { ...component.configuration, canUpdateProjectVisibilityToPrivate: true } + }); + this.props.fetchOrganization(organization.key); + } + }; + handleVisibilityChange = (visibility: string) => { if (visibility === 'public') { this.openDisclaimer(); @@ -348,16 +360,25 @@ export default class App extends React.PureComponent { }; render() { + const { component, organization } = this.props; const canTurnToPrivate = - this.props.component.configuration != null && - this.props.component.configuration.canUpdateProjectVisibilityToPrivate; + component.configuration && component.configuration.canUpdateProjectVisibilityToPrivate; + + let showUpgradeBox; + if (organization && !canTurnToPrivate) { + const isOrgAdmin = organization.actions && organization.actions.admin; + showUpgradeBox = + isOrgAdmin && + this.props.component.qualifier === 'TRK' && + !organization.canUpdateProjectsVisibilityToPrivate; + } return (
    @@ -366,22 +387,26 @@ export default class App extends React.PureComponent { canTurnToPrivate={canTurnToPrivate} className="big-spacer-top big-spacer-bottom" onChange={this.handleVisibilityChange} - visibility={this.props.component.visibility} + visibility={component.visibility} /> - {this.props.component.qualifier === 'TRK' && - !canTurnToPrivate && ( - + {showUpgradeBox && + organization && ( + )} {this.state.disclaimer && ( )}
    { selectedPermission={this.state.selectedPermission} users={this.state.users} usersPaging={this.state.usersPaging} - visibility={this.props.component.visibility} + visibility={component.visibility} /> ); diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/AppContainer.ts b/server/sonar-web/src/main/js/apps/permissions/project/components/AppContainer.ts index b8f0073e5bb..ac9900803c7 100644 --- a/server/sonar-web/src/main/js/apps/permissions/project/components/AppContainer.ts +++ b/server/sonar-web/src/main/js/apps/permissions/project/components/AppContainer.ts @@ -19,10 +19,22 @@ */ import { connect } from 'react-redux'; import App from './App'; -import { getCurrentUser, Store } from '../../../../store/rootReducer'; +import { getCurrentUser, getOrganizationByKey, Store } from '../../../../store/rootReducer'; +import { fetchOrganization } from '../../../organizations/actions'; -const mapStateToProps = (state: Store) => ({ - currentUser: getCurrentUser(state) +interface OwnProps { + component: T.Component; + onComponentChange: (changes: Partial) => void; +} + +const mapStateToProps = (state: Store, { component }: OwnProps) => ({ + currentUser: getCurrentUser(state), + organization: getOrganizationByKey(state, component.organization) }); -export default connect(mapStateToProps)(App); +const mapDispatchToProps = { fetchOrganization }; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(App); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.tsx index b8542ee8be4..bb553eea77b 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.tsx @@ -26,6 +26,7 @@ import GraphsTooltipsContentIssues from './GraphsTooltipsContentIssues'; import { DEFAULT_GRAPH, MeasureHistory, Serie } from '../utils'; import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; import { Popup, PopupPlacement } from '../../../components/ui/popups'; +import { isDefined } from '../../../helpers/types'; interface Props { events: T.AnalysisEvent[]; @@ -85,7 +86,7 @@ export default class GraphsTooltips extends React.PureComponent { left -= TOOLTIP_WIDTH; placement = PopupPlacement.LeftTop; } - const tooltipContent = this.renderContent().filter(Boolean); + const tooltipContent = this.renderContent().filter(isDefined); const addSeparator = tooltipContent.length > 0; return ( import('../create/project/CreateProjectPage')) } -].filter(Boolean); +].filter(isDefined); export default routes; diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx index 293e9f98836..83385c72f07 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx @@ -33,6 +33,7 @@ import { translate } from '../../helpers/l10n'; export interface Props { currentUser: { login: string }; hasProvisionPermission?: boolean; + onOrganizationUpgrade: () => void; onVisibilityChange: (visibility: T.Visibility) => void; organization: T.Organization; topLevelQualifiers: string[]; @@ -233,6 +234,7 @@ export default class App extends React.PureComponent { {this.state.createProjectForm && ( diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/AppContainer.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/AppContainer.tsx index 99437f0da4f..35419fc25b6 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/AppContainer.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/AppContainer.tsx @@ -51,6 +51,10 @@ class AppContainer extends React.PureComponent { + this.props.fetchOrganization(this.props.organization.key); + }; + handleVisibilityChange = (visibility: T.Visibility) => { if (this.props.organization) { this.props.onVisibilityChange(this.props.organization, visibility); @@ -71,6 +75,7 @@ class AppContainer extends React.PureComponent { +export default class ChangeDefaultVisibilityForm extends React.PureComponent { constructor(props: Props) { super(props); this.state = { visibility: props.organization.projectVisibility as T.Visibility }; @@ -95,12 +94,10 @@ export default class ChangeVisibilityForm extends React.PureComponent ))} - {organization.canUpdateProjectsVisibilityToPrivate ? ( + {organization.canUpdateProjectsVisibilityToPrivate && ( {translate('organization.change_visibility_form.warning')} - ) : ( - )} diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/CreateProjectForm.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/CreateProjectForm.tsx index e434616ecc1..133b5973a72 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/CreateProjectForm.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/CreateProjectForm.tsx @@ -21,7 +21,7 @@ import * as React from 'react'; import { Link } from 'react-router'; import { FormattedMessage } from 'react-intl'; import { createProject } from '../../api/components'; -import UpgradeOrganizationBox from '../../components/common/UpgradeOrganizationBox'; +import UpgradeOrganizationBox from '../create/components/UpgradeOrganizationBox'; import VisibilitySelector from '../../components/common/VisibilitySelector'; import Modal from '../../components/controls/Modal'; import { SubmitButton, ResetButtonLink } from '../../components/ui/buttons'; @@ -32,6 +32,7 @@ import { Alert } from '../../components/ui/Alert'; interface Props { onClose: () => void; onProjectCreated: () => void; + onOrganizationUpgrade: () => void; organization: T.Organization; } @@ -194,12 +195,18 @@ export default class CreateProjectForm extends React.PureComponent onChange={this.handleVisibilityChange} visibility={this.state.visibility} /> - {!organization.canUpdateProjectsVisibilityToPrivate && ( -
    - +
    + {organization.actions && + organization.actions.admin && + !organization.canUpdateProjectsVisibilityToPrivate && ( +
    +
    )} -
    diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx index 70d207a00c4..e5a75ac6911 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import ChangeVisibilityForm from './ChangeVisibilityForm'; +import ChangeDefaultVisibilityForm from './ChangeDefaultVisibilityForm'; import { EditButton, Button } from '../../components/ui/buttons'; import { translate } from '../../helpers/l10n'; import { isSonarCloud } from '../../helpers/system'; @@ -75,13 +75,14 @@ export default class Header extends React.PureComponent {

    {translate('projects_management.page.description')}

    - {this.state.visibilityForm && ( - - )} + {!isSonarCloud() && + this.state.visibilityForm && ( + + )} ); } diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx index f27f9b4eaf3..3a51882532d 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx @@ -137,6 +137,7 @@ function shallowRender(props?: { [P in keyof Props]?: Props[P] }) { { expect(onConfirm).toBeCalledWith('private'); }); -function shallowRender(props: Partial = {}) { +function shallowRender(props: Partial = {}) { return shallow( - ; -const organization: T.Organization = { key: 'org', name: 'org', projectVisibility: 'public' }; +const organization: T.Organization = { + actions: { admin: true }, + key: 'org', + name: 'org', + projectVisibility: 'public' +}; it('creates project', async () => { const wrapper = shallow( diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Header-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Header-test.tsx index 6eddf971a9f..aae7beee4ed 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Header-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Header-test.tsx @@ -22,7 +22,13 @@ import { shallow } from 'enzyme'; import Header, { Props } from '../Header'; import { click } from '../../../helpers/testUtils'; -const organization: T.Organization = { key: 'org', name: 'org', projectVisibility: 'public' }; +jest.mock('../../../helpers/system', () => ({ isSonarCloud: jest.fn().mockReturnValue(false) })); + +const organization: T.Organization = { + key: 'org', + name: 'org', + projectVisibility: 'public' +}; it('renders', () => { expect(shallowRender()).toMatchSnapshot(); @@ -41,14 +47,14 @@ it('changes default visibility', () => { click(wrapper.find('.js-change-visibility')); - const modalWrapper = wrapper.find('ChangeVisibilityForm'); + const modalWrapper = wrapper.find('ChangeDefaultVisibilityForm'); expect(modalWrapper).toMatchSnapshot(); modalWrapper.prop('onConfirm')('private'); expect(onVisibilityChange).toBeCalledWith('private'); modalWrapper.prop('onClose')(); wrapper.update(); - expect(wrapper.find('ChangeVisibilityForm').exists()).toBeFalsy(); + expect(wrapper.find('ChangeDefaultVisibilityForm').exists()).toBeFalsy(); }); function shallowRender(props?: { [P in keyof Props]?: Props[P] }) { diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ChangeVisibilityForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ChangeDefaultVisibilityForm-test.tsx.snap similarity index 98% rename from server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ChangeVisibilityForm-test.tsx.snap rename to server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ChangeDefaultVisibilityForm-test.tsx.snap index 0c5b0d3ed6b..a80e794b95a 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ChangeVisibilityForm-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ChangeDefaultVisibilityForm-test.tsx.snap @@ -260,9 +260,6 @@ exports[`renders disabled 1`] = ` visibility.private.description.short

    -
    -
    - -
    + +
    +
    -
    - -
    + +
    +
    -
    - -
    + +
    +
    { contentLabel={header} medium={true} onRequestClose={this.props.onClose} - shouldCloseOnOverlayClick={false}> + shouldCloseOnOverlayClick={false} + simple={true}>

    {translate('onboarding.header')}

    {translate('onboarding.header.description')}

    @@ -80,7 +81,7 @@ export class OnboardingModal extends React.PureComponent {
    -
    +
    {translate('not_now')} diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OnboardingModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OnboardingModal-test.tsx.snap index 1b5da9e7124..423382e72c1 100644 --- a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OnboardingModal-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OnboardingModal-test.tsx.snap @@ -6,6 +6,7 @@ exports[`renders correctly 1`] = ` medium={true} onRequestClose={[MockFunction]} shouldCloseOnOverlayClick={false} + simple={true} >
    /> ); }) - .filter(Boolean) + .filter(isDefined) ) .filter(dots => dots.length > 0)} diff --git a/server/sonar-web/src/main/js/components/charts/BubbleChart.tsx b/server/sonar-web/src/main/js/components/charts/BubbleChart.tsx index 8d5b8c4f1ca..903d1323345 100644 --- a/server/sonar-web/src/main/js/components/charts/BubbleChart.tsx +++ b/server/sonar-web/src/main/js/components/charts/BubbleChart.tsx @@ -77,7 +77,7 @@ export class Bubble extends React.PureComponent> { } } -export interface BubbleItem { +interface BubbleItem { color?: string; key?: string; link?: string | Location; diff --git a/server/sonar-web/src/main/js/components/charts/TreeMap.tsx b/server/sonar-web/src/main/js/components/charts/TreeMap.tsx index 5116b3b726e..f5f94e7611c 100644 --- a/server/sonar-web/src/main/js/components/charts/TreeMap.tsx +++ b/server/sonar-web/src/main/js/components/charts/TreeMap.tsx @@ -21,6 +21,7 @@ import * as React from 'react'; import { treemap as d3Treemap, hierarchy as d3Hierarchy } from 'd3-hierarchy'; import TreeMapRect from './TreeMapRect'; import { translate } from '../../helpers/l10n'; +import { Location } from '../../helpers/urls'; import './TreeMap.css'; export interface TreeMapItem { diff --git a/server/sonar-web/src/main/js/components/charts/TreeMapRect.tsx b/server/sonar-web/src/main/js/components/charts/TreeMapRect.tsx index 3c3341028b5..1149b46903d 100644 --- a/server/sonar-web/src/main/js/components/charts/TreeMapRect.tsx +++ b/server/sonar-web/src/main/js/components/charts/TreeMapRect.tsx @@ -23,6 +23,7 @@ import * as classNames from 'classnames'; import { scaleLinear } from 'd3-scale'; import LinkIcon from '../icons-components/LinkIcon'; import Tooltip, { Placement } from '../controls/Tooltip'; +import { Location } from '../../helpers/urls'; const SIZE_SCALE = scaleLinear() .domain([3, 15]) diff --git a/server/sonar-web/src/main/js/components/controls/Modal.tsx b/server/sonar-web/src/main/js/components/controls/Modal.tsx index d57ba7d4ea1..14d5ea0de7c 100644 --- a/server/sonar-web/src/main/js/components/controls/Modal.tsx +++ b/server/sonar-web/src/main/js/components/controls/Modal.tsx @@ -25,7 +25,9 @@ ReactModal.setAppElement('#content'); interface OwnProps { medium?: boolean; + noBackdrop?: boolean; large?: boolean; + simple?: true; } type MandatoryProps = Pick; @@ -35,9 +37,13 @@ type Props = Partial & MandatoryProps & OwnProps; export default function Modal(props: Props) { return ( ); diff --git a/server/sonar-web/src/main/js/components/controls/Tooltip.tsx b/server/sonar-web/src/main/js/components/controls/Tooltip.tsx index 802be846f8b..529cb1ed269 100644 --- a/server/sonar-web/src/main/js/components/controls/Tooltip.tsx +++ b/server/sonar-web/src/main/js/components/controls/Tooltip.tsx @@ -207,13 +207,15 @@ export class TooltipInner extends React.Component { handleMouseEnter = () => { this.mouseEnterTimeout = window.setTimeout(() => { - if (this.mounted) { - // for some reason even after the `this.mouseEnterTimeout` is cleared, it still triggers - // to workaround this issue, check that its value is not `undefined` - // (if it's `undefined`, it means the timer has been reset) - if (this.props.visible === undefined && this.mouseEnterTimeout !== undefined) { - this.setState({ visible: true }); - } + // for some reason even after the `this.mouseEnterTimeout` is cleared, it still triggers + // to workaround this issue, check that its value is not `undefined` + // (if it's `undefined`, it means the timer has been reset) + if ( + this.mounted && + this.props.visible === undefined && + this.mouseEnterTimeout !== undefined + ) { + this.setState({ visible: true }); } }, (this.props.mouseEnterDelay || 0) * 1000); @@ -230,10 +232,8 @@ export class TooltipInner extends React.Component { if (!this.mouseIn) { this.mouseLeaveTimeout = window.setTimeout(() => { - if (this.mounted) { - if (this.props.visible === undefined && !this.mouseIn) { - this.setState({ visible: false }); - } + if (this.mounted && this.props.visible === undefined && !this.mouseIn) { + this.setState({ visible: false }); } }, (this.props.mouseLeaveDelay || 0) * 1000); @@ -254,7 +254,6 @@ export class TooltipInner extends React.Component { render() { const { classNameSpace = 'tooltip' } = this.props; - return ( <> {React.cloneElement(this.props.children, { diff --git a/server/sonar-web/src/main/js/components/controls/react-select.css b/server/sonar-web/src/main/js/components/controls/react-select.css index ac5829d0daf..dae9dbd8cd8 100644 --- a/server/sonar-web/src/main/js/components/controls/react-select.css +++ b/server/sonar-web/src/main/js/components/controls/react-select.css @@ -90,6 +90,10 @@ border-color: var(--blue); } +.Select-placeholder { + color: var(--secondFontColor); +} + .Select-placeholder, :not(.Select--multi) > .Select-control .Select-value { bottom: 0; @@ -115,6 +119,13 @@ padding-top: 4px; } +.Select-value .outline-badge, +.Select-option .outline-badge { + height: 20px; + line-height: 19px; + margin-top: 1px; +} + .Select-option svg, .Select-option img, .Select-option [class^='icon-'] { diff --git a/server/sonar-web/src/main/js/components/hoc/__tests__/withUserOrganizations-test.tsx b/server/sonar-web/src/main/js/components/hoc/__tests__/withUserOrganizations-test.tsx index 637d54e55ea..c6cfd29cb57 100644 --- a/server/sonar-web/src/main/js/components/hoc/__tests__/withUserOrganizations-test.tsx +++ b/server/sonar-web/src/main/js/components/hoc/__tests__/withUserOrganizations-test.tsx @@ -24,7 +24,10 @@ import { withUserOrganizations } from '../withUserOrganizations'; jest.mock('../../../api/organizations', () => ({ getOrganizations: jest.fn() })); -class X extends React.Component<{ userOrganizations: T.Organization[] }> { +class X extends React.Component<{ + fetchMyOrganizations: () => Promise; + userOrganizations: T.Organization[]; +}> { render() { return
    ; } diff --git a/server/sonar-web/src/main/js/components/hoc/withUserOrganizations.tsx b/server/sonar-web/src/main/js/components/hoc/withUserOrganizations.tsx index 485c9b6ac96..e09189b3799 100644 --- a/server/sonar-web/src/main/js/components/hoc/withUserOrganizations.tsx +++ b/server/sonar-web/src/main/js/components/hoc/withUserOrganizations.tsx @@ -22,21 +22,17 @@ import { connect } from 'react-redux'; import { Store, getMyOrganizations } from '../../store/rootReducer'; import { fetchMyOrganizations } from '../../apps/account/organizations/actions'; +interface OwnProps { + fetchMyOrganizations: () => Promise; + userOrganizations: T.Organization[]; +} + export function withUserOrganizations

    ( - WrappedComponent: React.ComponentClass< - P & { - personalOrganization?: T.Organization; - userOrganizations: T.Organization[]; - } - > + WrappedComponent: React.ComponentClass

    > ) { - type Props = P & { - fetchMyOrganizations: () => Promise; - userOrganizations: T.Organization[]; - }; const wrappedDisplayName = WrappedComponent.displayName || WrappedComponent.name || 'Component'; - class Wrapper extends React.Component { + class Wrapper extends React.Component

    { static displayName = `withUserOrganizations(${wrappedDisplayName})`; componentDidMount() { @@ -44,13 +40,11 @@ export function withUserOrganizations

    ( } render() { - // @ts-ignore Rest operator not supported yet by TS for generics - const { fetchMyOrganizations, ...other } = this.props; - return ; + return ; } } - const mapDispatchToProps = { fetchMyOrganizations }; + const mapDispatchToProps = { fetchMyOrganizations: fetchMyOrganizations as any }; function mapStateToProps(state: Store) { return { userOrganizations: getMyOrganizations(state) }; diff --git a/server/sonar-web/src/main/js/components/tags/TagsList.css b/server/sonar-web/src/main/js/components/tags/TagsList.css index a93001c80b7..9168d930510 100644 --- a/server/sonar-web/src/main/js/components/tags/TagsList.css +++ b/server/sonar-web/src/main/js/components/tags/TagsList.css @@ -19,6 +19,7 @@ */ .tags-list { white-space: nowrap; + line-height: 16px; } .tags-list i::before { diff --git a/server/sonar-web/src/main/js/components/ui/buttons.css b/server/sonar-web/src/main/js/components/ui/buttons.css index ed7a6499ecb..d7e7a0e37cc 100644 --- a/server/sonar-web/src/main/js/components/ui/buttons.css +++ b/server/sonar-web/src/main/js/components/ui/buttons.css @@ -163,6 +163,12 @@ font-size: 11px; } +.button-large { + height: var(--largeControlHeight); + padding: 0 16px; + font-size: var(--mediumFontSize); +} + /* #region .button-group */ .button-group { display: inline-block; diff --git a/server/sonar-web/src/main/js/components/common/UpgradeOrganizationBox.css b/server/sonar-web/src/main/js/helpers/types.ts similarity index 88% rename from server/sonar-web/src/main/js/components/common/UpgradeOrganizationBox.css rename to server/sonar-web/src/main/js/helpers/types.ts index fc9e3c38ace..205e3f2094a 100644 --- a/server/sonar-web/src/main/js/components/common/UpgradeOrganizationBox.css +++ b/server/sonar-web/src/main/js/helpers/types.ts @@ -17,7 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -.upgrade-organization-box { - max-width: 400px; - background-color: var(--barBackgroundColor) !important; + +export function isDefined(x: T | undefined | null): x is T { + return x !== undefined && x !== null; } diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index ea32be1e07b..da207482207 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -2740,10 +2740,16 @@ onboarding.project_analysis.suggestions.github=If you are using Travis CI, the S onboarding.create_project.header=Analyze projects onboarding.create_project.setup_manually=Setup manually -onboarding.create_project.create_new_org=I want to create another organization -onboarding.create_project.import_new_org=I want to import another organization +onboarding.create_project.create_new_org=Create another organization +onboarding.create_project.import_new_org=Import another organization onboarding.create_project.install_app_description.bitbucket=We need you to install the SonarCloud Bitbucket application on one of your team in order to select which repositories you want to analyze. onboarding.create_project.install_app_description.github=We need you to install the SonarCloud GitHub application on one of your organization in order to select which repositories you want to analyze. +onboarding.create_project.1_repository_selected=1 repository selected +onboarding.create_project.x_repositories_selected={0} repositories selected +onboarding.create_project.1_repository_created_as_public=1 repository will be created as a public project on SonarCloud +onboarding.create_project.x_repository_created_as_public={0} repositories will be created as public projects on SonarCloud +onboarding.create_project.1_repository_created_as_private=1 repository will be created as a private project on SonarCloud +onboarding.create_project.x_repository_created_as_private={0} repositories will be created as private projects on SonarCloud onboarding.create_project.organization=Organization onboarding.create_project.project_key=Project key onboarding.create_project.project_key.description=Up to 400 characters. All letters, digits, dash, underscore, point or colon. @@ -2756,9 +2762,11 @@ onboarding.create_project.display_name.description=Up to 500 characters onboarding.create_project.repository_imported=Already imported: {link} onboarding.create_project.see_project=See the project onboarding.create_project.select_repositories=Select repositories +onboarding.create_project.subscribe_to_import_private_repositories=You need to subscribe your organization to a paid plan to import private projects +onboarding.create_project.subscribtion_success_x={0} has been successfully upgraded to paid plan. You can now import and analyze private projects. onboarding.create_organization.page.header=Create Organization -onboarding.create_organization.page.description=An organization is a space where a team or a whole company can collaborate accross many projects.{break}To analyze a private project you must subscribe your organization to a paid plan. From {price} a month. {more} +onboarding.create_organization.page.description=An organization is a space where a team or a whole company can collaborate accross many projects. onboarding.create_organization.organization_name=Key onboarding.create_organization.organization_name.description=Up to 255 characters. All chars must be lower-case letters (a to z), digits or dash (but dash can neither be trailing nor heading). The display name can be specified in the additional info. onboarding.create_organization.organization_name.error=The provided value doesn't match the expected format. @@ -2806,13 +2814,13 @@ onboarding.import_organization.github=Import from GitHub organizations onboarding.import_organization.bind_existing=Bind to an existing SonarCloud organization onboarding.import_organization.create_new=Create new SonarCloud organization from it onboarding.import_organization.already_bound_x=Your organization {avatar} {name} is already bound to the SonarCloud organization {boundAvatar} {boundName}. Try again and choose a different organization. -onboarding.import_organization_x=Import {avatar} {name} into SonarCloud organization +onboarding.import_organization_x=Import {avatar} {name} into a SonarCloud organization onboarding.import_personal_organization_x=Bind {avatar} {name} with your personal SonarCloud organization {personalAvatar} {personalName} onboarding.team.header=Join a team onboarding.team.first_step=Well congrats, the first step is done! -onboarding.team.how_to_join=To join a team, the only thing you need to do is to be a user registered on Sonarcloud. The administrator of the Sonarcloud organization you wish to join has to add you to his organization's members {link}. Ask him to do so! +onboarding.team.how_to_join=To join a team, the only thing you need to do is to be a user registered on Sonarcloud. The administrator of the Sonarcloud organization you wish to join has to add you to their organization's members {link}. Ask them to do so! onboarding.team.work_in_progress=We are currently working on a better way to join a team or invite people to yours. onboarding.analyze_your_code.note=Free -- 2.39.5