aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>2018-11-27 14:13:26 +0100
committerSonarTech <sonartech@sonarsource.com>2018-12-07 20:21:05 +0100
commit8dea56b4c70d09fc069add79ab0617bf6bb0e16a (patch)
tree5c49ebbfdb3a82d09682c33fd27bce4f04e1f00c
parent5aedc697b7bba9e80a97ab86913b61ce662563a8 (diff)
downloadsonarqube-8dea56b4c70d09fc069add79ab0617bf6bb0e16a.tar.gz
sonarqube-8dea56b4c70d09fc069add79ab0617bf6bb0e16a.zip
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
-rw-r--r--server/sonar-web/src/main/js/api/plugins.ts3
-rw-r--r--server/sonar-web/src/main/js/app/styles/components/modals.css35
-rw-r--r--server/sonar-web/src/main/js/app/styles/init/misc.css14
-rw-r--r--server/sonar-web/src/main/js/app/theme.js7
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/Components.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/drilldown/BubbleChart.tsx7
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/create/components/BillingFormShim.tsx (renamed from server/sonar-web/src/main/js/apps/create/organization/BillingFormShim.tsx)10
-rw-r--r--server/sonar-web/src/main/js/apps/create/components/CardPlan.css25
-rw-r--r--server/sonar-web/src/main/js/apps/create/components/CardPlan.tsx66
-rw-r--r--server/sonar-web/src/main/js/apps/create/components/OrganizationSelect.tsx28
-rw-r--r--server/sonar-web/src/main/js/apps/create/components/UpgradeOrganizationAdvantages.tsx49
-rw-r--r--server/sonar-web/src/main/js/apps/create/components/UpgradeOrganizationBox.tsx115
-rw-r--r--server/sonar-web/src/main/js/apps/create/components/UpgradeOrganizationModal.tsx128
-rw-r--r--server/sonar-web/src/main/js/apps/create/components/__mocks__/BillingFormShim.tsx (renamed from server/sonar-web/src/main/js/apps/create/organization/__mocks__/BillingFormShim.tsx)4
-rw-r--r--server/sonar-web/src/main/js/apps/create/components/__tests__/BillingFormShim-test.tsx (renamed from server/sonar-web/src/main/js/apps/create/organization/__tests__/BillingFormShim-test.tsx)0
-rw-r--r--server/sonar-web/src/main/js/apps/create/components/__tests__/CardPlan-test.tsx10
-rw-r--r--server/sonar-web/src/main/js/apps/create/components/__tests__/UpgradeOrganizationAdvantages-test.tsx26
-rw-r--r--server/sonar-web/src/main/js/apps/create/components/__tests__/UpgradeOrganizationBox-test.tsx62
-rw-r--r--server/sonar-web/src/main/js/apps/create/components/__tests__/UpgradeOrganizationModal-test.tsx (renamed from server/sonar-web/src/main/js/components/common/UpgradeOrganizationBox.tsx)47
-rw-r--r--server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/BillingFormShim-test.tsx.snap (renamed from server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/BillingFormShim-test.tsx.snap)0
-rw-r--r--server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/CardPlan-test.tsx.snap139
-rw-r--r--server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/OrganizationSelect-test.tsx.snap72
-rw-r--r--server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/UpgradeOrganizationAdvantages-test.tsx.snap22
-rw-r--r--server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/UpgradeOrganizationBox-test.tsx.snap31
-rw-r--r--server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/UpgradeOrganizationModal-test.tsx.snap26
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx19
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/PlanSelect.tsx7
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/PlanStep.tsx7
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/PlanSelect-test.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap95
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/PlanSelect-test.tsx.snap4
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/PlanStep-test.tsx.snap6
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/utils.ts2
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/AlmRepositoryItem.tsx117
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/AutoProjectCreate.tsx18
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/RemoteRepositories.tsx129
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/SetupProjectBox.tsx141
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/AlmRepositoryItem-test.tsx8
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/AutoProjectCreate-test.tsx24
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/RemoteRepositories-test.tsx59
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/SetupProjectBox-test.tsx69
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AlmRepositoryItem-test.tsx.snap328
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap23
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/RemoteRepositories-test.tsx.snap211
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/SetupProjectBox-test.tsx.snap45
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/style.css104
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/OrganizationJustCreated.css2
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/project/components/App.tsx47
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/project/components/AppContainer.ts20
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/projects/routes.ts3
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/App.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/AppContainer.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/ChangeDefaultVisibilityForm.tsx (renamed from server/sonar-web/src/main/js/apps/projectsManagement/ChangeVisibilityForm.tsx)7
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/CreateProjectForm.tsx17
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx17
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx1
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ChangeDefaultVisibilityForm-test.tsx (renamed from server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ChangeVisibilityForm-test.tsx)6
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/__tests__/CreateProjectForm-test.tsx8
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Header-test.tsx12
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ChangeDefaultVisibilityForm-test.tsx.snap (renamed from server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ChangeVisibilityForm-test.tsx.snap)3
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/CreateProjectForm-test.tsx.snap75
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Header-test.tsx.snap2
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OnboardingModal-test.tsx.snap3
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/styles.css1
-rw-r--r--server/sonar-web/src/main/js/components/charts/AdvancedTimeline.tsx3
-rw-r--r--server/sonar-web/src/main/js/components/charts/BubbleChart.tsx2
-rw-r--r--server/sonar-web/src/main/js/components/charts/TreeMap.tsx1
-rw-r--r--server/sonar-web/src/main/js/components/charts/TreeMapRect.tsx1
-rw-r--r--server/sonar-web/src/main/js/components/controls/Modal.tsx10
-rw-r--r--server/sonar-web/src/main/js/components/controls/Tooltip.tsx23
-rw-r--r--server/sonar-web/src/main/js/components/controls/react-select.css11
-rw-r--r--server/sonar-web/src/main/js/components/hoc/__tests__/withUserOrganizations-test.tsx5
-rw-r--r--server/sonar-web/src/main/js/components/hoc/withUserOrganizations.tsx24
-rw-r--r--server/sonar-web/src/main/js/components/tags/TagsList.css1
-rw-r--r--server/sonar-web/src/main/js/components/ui/buttons.css6
-rw-r--r--server/sonar-web/src/main/js/helpers/types.ts (renamed from server/sonar-web/src/main/js/components/common/UpgradeOrganizationBox.css)6
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties18
81 files changed, 1900 insertions, 803 deletions
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 (
<table className="data boxed-padding zebra">
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<Props> {
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<T.ComponentMeasureEnhanced>[];
+ .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<Props, State> {
})
};
})
- .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
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<any>;
- renderSubmitGroup: (submitText?: string) => React.ReactElement<any>;
+ 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<any>;
+ 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}
</span>
- {startingPrice ? (
- <FormattedMessage
- defaultMessage={translate('billing.price_from_x')}
- id="billing.price_from_x"
- values={{
- price: <span className="card-plan-price">{startingPrice}</span>
- }}
- />
- ) : (
- <span className="card-plan-price">{formatPrice(0)}</span>
- )}
+ {startingPrice !== undefined &&
+ (startingPrice ? (
+ <FormattedMessage
+ defaultMessage={translate('billing.price_from_x')}
+ id="billing.price_from_x"
+ values={{
+ price: <span className="card-plan-price">{formatPrice(startingPrice)}</span>
+ }}
+ />
+ ) : (
+ <span className="card-plan-price">{formatPrice(0)}</span>
+ ))}
</h2>
<div className="card-plan-body">{props.children}</div>
{recommended && (
@@ -101,12 +101,16 @@ export function FreeCardPlan({ almName, hasWarning, ...props }: FreeProps) {
const showWarning = almName && hasWarning && !props.disabled;
return (
- <CardPlan title={translate('billing.free_plan.title')} {...props}>
+ <CardPlan startingPrice={0} title={translate('billing.free_plan.title')} {...props}>
<>
- <ul className="note">
- <li>{translate('billing.free_plan.all_projects_analyzed_public')}</li>
- <li>{translate('billing.free_plan.anyone_can_browse_source_code')}</li>
- </ul>
+ <div className="spacer-left">
+ <ul className="big-spacer-left note">
+ <li className="little-spacer-bottom">
+ {translate('billing.free_plan.all_projects_analyzed_public')}
+ </li>
+ <li>{translate('billing.free_plan.anyone_can_browse_source_code')}</li>
+ </ul>
+ </div>
{showWarning && (
<Alert variant="warning">
<FormattedMessage
@@ -135,31 +139,15 @@ interface PaidProps extends Props {
}
export function PaidCardPlan({ isRecommended, ...props }: PaidProps) {
- const advantages = [
- translate('billing.upgrade_box.unlimited_private_projects'),
- translate('billing.upgrade_box.strict_control_private_data'),
- translate('billing.upgrade_box.cancel_anytime'),
- <strong key="trial">
- {translateWithParameters('billing.upgrade_box.free_trial_x', TRIAL_DURATION_DAYS)}
- </strong>
- ];
-
return (
<CardPlan
recommended={isRecommended ? translate('billing.paid_plan.recommended') : undefined}
title={translate('billing.paid_plan.title')}
{...props}>
<>
- <ul className="note">
- {advantages.map((text, idx) => (
- <li className="display-flex-center" key={idx}>
- <CheckIcon className="spacer-right" fill={theme.lightGreen} />
- {text}
- </li>
- ))}
- </ul>
+ <UpgradeOrganizationAdvantages />
<div className="big-spacer-left">
- <Link className="spacer-left" target="_blank" to="/documentation/sonarcloud-pricing/">
+ <Link className="spacer-left" target="_blank" to="/about/pricing">
{translate('billing.pricing.learn_more')}
</Link>
</div>
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 (
- <span>
- {!hideIcons && (
- <img
- alt={organization.alm ? organization.alm.key : 'SonarCloud'}
- className="spacer-right"
- height={14}
- src={`${getBaseUrl()}/images/${icon}.svg`}
- />
+ <div className="display-flex-space-between">
+ <span className="text-ellipsis flex-1">
+ {!hideIcons && (
+ <img
+ alt={organization.alm ? organization.alm.key : 'SonarCloud'}
+ className="little-spacer-right"
+ height={14}
+ src={`${getBaseUrl()}/images/${icon}.svg`}
+ />
+ )}
+ {organization.name}
+ <span className="note little-spacer-left">{organization.key}</span>
+ </span>
+ {isPaidOrg && (
+ <div className="outline-badge">{translate('organization.paid_plan.badge')}</div>
)}
- {organization.name}
- <span className="note little-spacer-left">{organization.key}</span>
- </span>
+ </div>
);
};
}
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 (
+ <ul className="note">
+ <Advantage>{translate('billing.upgrade_box.unlimited_private_projects')}</Advantage>
+ <Advantage>{translate('billing.upgrade_box.strict_control_private_data')}</Advantage>
+ <Advantage>{translate('billing.upgrade_box.cancel_anytime')}</Advantage>
+ <Advantage>
+ <strong>
+ {translateWithParameters('billing.upgrade_box.free_trial_x', TRIAL_DURATION_DAYS)}
+ </strong>
+ </Advantage>
+ </ul>
+ );
+}
+
+export function Advantage({ children }: { children: React.ReactNode }) {
+ return (
+ <li className="display-flex-center little-spacer-bottom">
+ <CheckIcon className="spacer-right" fill={theme.lightGreen} />
+ {children}
+ </li>
+ );
+}
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<Props, State> {
+ 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 (
+ <>
+ <CardPlan
+ className={this.props.className}
+ startingPrice={startingPrice}
+ title={translate('billing.upgrade_box.header')}>
+ <>
+ <UpgradeOrganizationAdvantages />
+ <div className="big-spacer-left">
+ <Button className="js-upgrade-organization" onClick={this.handleUpgradeClick}>
+ {translate('billing.paid_plan.upgrade')}
+ </Button>
+ <Link className="spacer-left" target="_blank" to="/about/pricing">
+ {translate('billing.pricing.learn_more')}
+ </Link>
+ </div>
+ </>
+ </CardPlan>
+ {upgradeOrganizationModal && (
+ <UpgradeOrganizationModal
+ insideModal={this.props.insideModal}
+ onClose={this.handleUpgradeOrganizationModalClose}
+ onUpgradeDone={this.handleOrganizationUpgrade}
+ organization={this.props.organization}
+ subscriptionPlans={subscriptionPlans}
+ />
+ )}
+ </>
+ );
+ }
+}
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<Props, State> {
+ 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 (
+ <Modal
+ contentLabel={header}
+ medium={true}
+ noBackdrop={this.props.insideModal}
+ onRequestClose={this.props.onClose}
+ shouldCloseOnOverlayClick={false}
+ simple={true}>
+ <div className="modal-simple-head">
+ <h2>{header}</h2>
+ </div>
+ <BillingForm
+ onCommit={this.props.onUpgradeDone}
+ organizationKey={this.props.organization.key}
+ subscriptionPlans={this.props.subscriptionPlans}>
+ {({
+ onSubmit,
+ processingUpgrade,
+ renderFormFields,
+ renderNextCharge,
+ renderRecap,
+ renderSubmitButton
+ }) => (
+ <form id="organization-paid-plan-form" onSubmit={onSubmit}>
+ <div className="modal-simple-body modal-container">
+ <div className="huge-spacer-bottom">
+ <p className="spacer-bottom">
+ <FormattedMessage
+ defaultMessage={translate('billing.upgrade.org_x_advantages')}
+ id="billing.coupon.description"
+ values={{
+ org: <strong>{this.props.organization.name}</strong>
+ }}
+ />
+ </p>
+ <UpgradeOrganizationAdvantages />
+ </div>
+ {renderFormFields()}
+ <div className="big-spacer-top">{renderRecap()}</div>
+ </div>
+ <footer className="modal-simple-foot-action">
+ <span className="note">{renderNextCharge()}</span>
+ <div>
+ <DeferredSpinner className="spacer-right" loading={processingUpgrade} />
+ {renderSubmitButton()}
+ <ResetButtonLink onClick={this.props.onClose}>
+ {translate('cancel')}
+ </ResetButtonLink>
+ </div>
+ </footer>
+ </form>
+ )}
+ </BillingForm>
+ </Modal>
+ );
+ }
+}
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
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 }>
<div id="BillingFormShim">
{this.props.children({
onSubmit: jest.fn(),
+ processingUpgrade: true,
renderFormFields: () => <div id="form-fields" />,
+ renderNextCharge: () => <div id="form-next-charge" />,
+ renderRecap: () => <div id="form-recap" />,
+ renderSubmitButton: () => <div id="form-submit" />,
renderSubmitGroup: () => <div id="submit-group" />
})}
</div>
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
index 71119febaf6..71119febaf6 100644
--- 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
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(
- <CardPlan recommended="Recommended for you" startingPrice="$10" title="Paid Plan">
+ <CardPlan recommended="Recommended for you" startingPrice={10} title="Paid Plan">
<div>content</div>
</CardPlan>
)
@@ -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(<FreeCardPlan hasWarning={false} />)).toMatchSnapshot();
});
@@ -65,7 +65,7 @@ describe('#FreeCardPlan', () => {
});
describe('#PaidCardPlan', () => {
- it('should render correctly', () => {
- expect(shallow(<PaidCardPlan isRecommended={true} startingPrice="$10" />)).toMatchSnapshot();
+ it('should render', () => {
+ expect(shallow(<PaidCardPlan isRecommended={true} startingPrice={10} />)).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(<UpgradeOrganizationAdvantages />)).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<any>).mockClear();
+ (getSubscriptionPlans as jest.Mock<any>).mockClear();
+});
+
+it('should not render', () => {
+ (hasMessage as jest.Mock<any>).mockReturnValueOnce(false);
+ expect(
+ shallow(
+ <UpgradeOrganizationBox onOrganizationUpgrade={jest.fn()} organization={organization} />
+ ).type()
+ ).toBeNull();
+});
+
+it('should render correctly', async () => {
+ const wrapper = shallow(
+ <UpgradeOrganizationBox onOrganizationUpgrade={jest.fn()} organization={organization} />
+ );
+ 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
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 (
- <div className="boxed-group boxed-group-inner upgrade-organization-box">
- <h3 className="spacer-bottom">{translate('billing.upgrade_box.header')}</h3>
- <p>{translate('billing.upgrade_box.text')}</p>
- <div className="big-spacer-top">
- <Link
- className="button"
- to={{
- pathname: `organizations/${organization}/extension/billing/billing`,
- query: { page: 'upgrade' }
- }}>
- {translate('billing.upgrade_box.button')}
- </Link>
- </div>
- </div>
+const organization = { key: 'foo', name: 'Foo' };
+
+it('should render correctly', async () => {
+ const wrapper = shallow(
+ <UpgradeOrganizationModal
+ onClose={jest.fn()}
+ onUpgradeDone={jest.fn()}
+ organization={organization}
+ subscriptionPlans={[]}
+ />
);
-}
+ 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
index fc6fb48982e..fc6fb48982e 100644
--- 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
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`] = `
<CardPlan
+ startingPrice={0}
title="billing.free_plan.title"
>
- <ul
- className="note"
+ <div
+ className="spacer-left"
>
- <li>
- billing.free_plan.all_projects_analyzed_public
- </li>
- <li>
- billing.free_plan.anyone_can_browse_source_code
- </li>
- </ul>
+ <ul
+ className="big-spacer-left note"
+ >
+ <li
+ className="little-spacer-bottom"
+ >
+ billing.free_plan.all_projects_analyzed_public
+ </li>
+ <li>
+ billing.free_plan.anyone_can_browse_source_code
+ </li>
+ </ul>
+ </div>
</CardPlan>
`;
exports[`#FreeCardPlan should render disabled with info 1`] = `
<CardPlan
disabled={true}
+ startingPrice={0}
title="billing.free_plan.title"
>
- <ul
- className="note"
+ <div
+ className="spacer-left"
>
- <li>
- billing.free_plan.all_projects_analyzed_public
- </li>
- <li>
- billing.free_plan.anyone_can_browse_source_code
- </li>
- </ul>
+ <ul
+ className="big-spacer-left note"
+ >
+ <li
+ className="little-spacer-bottom"
+ >
+ billing.free_plan.all_projects_analyzed_public
+ </li>
+ <li>
+ billing.free_plan.anyone_can_browse_source_code
+ </li>
+ </ul>
+ </div>
<Alert
variant="info"
>
@@ -51,18 +65,25 @@ exports[`#FreeCardPlan should render disabled with info 1`] = `
exports[`#FreeCardPlan should render with warning 1`] = `
<CardPlan
selected={true}
+ startingPrice={0}
title="billing.free_plan.title"
>
- <ul
- className="note"
+ <div
+ className="spacer-left"
>
- <li>
- billing.free_plan.all_projects_analyzed_public
- </li>
- <li>
- billing.free_plan.anyone_can_browse_source_code
- </li>
- </ul>
+ <ul
+ className="big-spacer-left note"
+ >
+ <li
+ className="little-spacer-bottom"
+ >
+ billing.free_plan.all_projects_analyzed_public
+ </li>
+ <li>
+ billing.free_plan.anyone_can_browse_source_code
+ </li>
+ </ul>
+ </div>
<Alert
variant="warning"
>
@@ -79,60 +100,13 @@ exports[`#FreeCardPlan should render with warning 1`] = `
</CardPlan>
`;
-exports[`#PaidCardPlan should render correctly 1`] = `
+exports[`#PaidCardPlan should render 1`] = `
<CardPlan
recommended="billing.paid_plan.recommended"
- startingPrice="$10"
+ startingPrice={10}
title="billing.paid_plan.title"
>
- <ul
- className="note"
- >
- <li
- className="display-flex-center"
- key="0"
- >
- <CheckIcon
- className="spacer-right"
- fill="#b0d513"
- />
- billing.upgrade_box.unlimited_private_projects
- </li>
- <li
- className="display-flex-center"
- key="1"
- >
- <CheckIcon
- className="spacer-right"
- fill="#b0d513"
- />
- billing.upgrade_box.strict_control_private_data
- </li>
- <li
- className="display-flex-center"
- key="2"
- >
- <CheckIcon
- className="spacer-right"
- fill="#b0d513"
- />
- billing.upgrade_box.cancel_anytime
- </li>
- <li
- className="display-flex-center"
- key="3"
- >
- <CheckIcon
- className="spacer-right"
- fill="#b0d513"
- />
- <strong
- key="trial"
- >
- billing.upgrade_box.free_trial_x.14
- </strong>
- </li>
- </ul>
+ <UpgradeOrganizationAdvantages />
<div
className="big-spacer-left"
>
@@ -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
</Link>
@@ -167,11 +141,6 @@ exports[`should be actionable 1`] = `
/>
Free Plan
</span>
- <span
- className="card-plan-price"
- >
- billing.price_format.0
- </span>
</h2>
<div
className="card-plan-body"
@@ -263,7 +232,7 @@ exports[`should render correctly 1`] = `
"price": <span
className="card-plan-price"
>
- $10
+ billing.price_format.10
</span>,
}
}
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`] = `
-<span>
- <img
- alt="SonarCloud"
- className="spacer-right"
- height={14}
- src="/images/sonarcloud-square-logo.svg"
- />
- Foo
+<div
+ className="display-flex-space-between"
+>
<span
- className="note little-spacer-left"
+ className="text-ellipsis flex-1"
>
- foo
+ <img
+ alt="SonarCloud"
+ className="little-spacer-right"
+ height={14}
+ src="/images/sonarcloud-square-logo.svg"
+ />
+ Foo
+ <span
+ className="note little-spacer-left"
+ >
+ foo
+ </span>
</span>
-</span>
+</div>
`;
exports[`should render options correctly 2`] = `
-<span>
- <img
- alt="github"
- className="spacer-right"
- height={14}
- src="/images/sonarcloud/github.svg"
- />
- Bar
+<div
+ className="display-flex-space-between"
+>
<span
- className="note little-spacer-left"
+ className="text-ellipsis flex-1"
>
- bar
+ <img
+ alt="github"
+ className="little-spacer-right"
+ height={14}
+ src="/images/sonarcloud/github.svg"
+ />
+ Bar
+ <span
+ className="note little-spacer-left"
+ >
+ bar
+ </span>
</span>
-</span>
+</div>
`;
exports[`should render options correctly 3`] = `
-<span>
- Foo
+<div
+ className="display-flex-space-between"
+>
<span
- className="note little-spacer-left"
+ className="text-ellipsis flex-1"
>
- foo
+ Foo
+ <span
+ className="note little-spacer-left"
+ >
+ foo
+ </span>
</span>
-</span>
+</div>
`;
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`] = `
+<ul
+ className="note"
+>
+ <Advantage>
+ billing.upgrade_box.unlimited_private_projects
+ </Advantage>
+ <Advantage>
+ billing.upgrade_box.strict_control_private_data
+ </Advantage>
+ <Advantage>
+ billing.upgrade_box.cancel_anytime
+ </Advantage>
+ <Advantage>
+ <strong>
+ billing.upgrade_box.free_trial_x.14
+ </strong>
+ </Advantage>
+</ul>
+`;
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`] = `
+<Fragment>
+ <CardPlan
+ startingPrice={10}
+ title="billing.upgrade_box.header"
+ >
+ <UpgradeOrganizationAdvantages />
+ <div
+ className="big-spacer-left"
+ >
+ <Button
+ className="js-upgrade-organization"
+ onClick={[Function]}
+ >
+ billing.paid_plan.upgrade
+ </Button>
+ <Link
+ className="spacer-left"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ target="_blank"
+ to="/about/pricing"
+ >
+ billing.pricing.learn_more
+ </Link>
+ </div>
+ </CardPlan>
+</Fragment>
+`;
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`] = `
+<Modal
+ contentLabel="billing.upgrade_box.upgrade_to_paid_plan"
+ medium={true}
+ onRequestClose={[MockFunction]}
+ shouldCloseOnOverlayClick={false}
+ simple={true}
+>
+ <div
+ className="modal-simple-head"
+ >
+ <h2>
+ billing.upgrade_box.upgrade_to_paid_plan
+ </h2>
+ </div>
+ <Connect(withCurrentUser(BillingFormShim))
+ onCommit={[MockFunction]}
+ organizationKey="foo"
+ subscriptionPlans={Array []}
+ >
+ <Component />
+ </Connect(withCurrentUser(BillingFormShim))>
+</Modal>
+`;
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<Props & WithRouterPr
? translate('onboarding.import_organization.personal.page.header')
: translate('onboarding.create_organization.page.header');
const startedPrice = subscriptionPlans && subscriptionPlans[0] && subscriptionPlans[0].price;
- const formattedPrice = formatPrice(startedPrice);
return (
<>
@@ -421,19 +418,7 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr
{!importPersonalOrg &&
startedPrice !== undefined && (
<p className="page-description">
- <FormattedMessage
- defaultMessage={translate('onboarding.create_organization.page.description')}
- id="onboarding.create_organization.page.description"
- values={{
- break: <br />,
- price: formattedPrice,
- more: (
- <Link target="_blank" to="/documentation/sonarcloud-pricing/">
- {translate('learn_more')}
- </Link>
- )
- }}
- />
+ {translate('onboarding.create_organization.page.description')}
</p>
)}
</header>
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<Props> {
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<Props, State> {
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 (
<div className="boxed-group-inner">
{this.state.ready && (
@@ -106,7 +105,7 @@ export default class PlanStep extends React.PureComponent<Props, State> {
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<PlanSelect['props']> = {}) {
return shallow(
- <PlanSelect onChange={jest.fn()} plan={Plan.Free} startingPrice="10" {...props} />
+ <PlanSelect onChange={jest.fn()} plan={Plan.Free} startingPrice={10} {...props} />
);
}
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`] = `
<p
className="page-description"
>
- <FormattedMessage
- defaultMessage="onboarding.create_organization.page.description"
- id="onboarding.create_organization.page.description"
- values={
- Object {
- "break": <br />,
- "more": <Link
- onlyActiveOnIndex={false}
- style={Object {}}
- target="_blank"
- to="/documentation/sonarcloud-pricing/"
- >
- learn_more
- </Link>,
- "price": "billing.price_format.10",
- }
- }
- />
+ onboarding.create_organization.page.description
</p>
</header>
<Tabs
@@ -203,24 +186,7 @@ exports[`should render with auto tab selected and manual disabled 2`] = `
<p
className="page-description"
>
- <FormattedMessage
- defaultMessage="onboarding.create_organization.page.description"
- id="onboarding.create_organization.page.description"
- values={
- Object {
- "break": <br />,
- "more": <Link
- onlyActiveOnIndex={false}
- style={Object {}}
- target="_blank"
- to="/documentation/sonarcloud-pricing/"
- >
- learn_more
- </Link>,
- "price": "billing.price_format.10",
- }
- }
- />
+ onboarding.create_organization.page.description
</p>
</header>
<Tabs
@@ -342,24 +308,7 @@ exports[`should render with manual tab displayed 1`] = `
<p
className="page-description"
>
- <FormattedMessage
- defaultMessage="onboarding.create_organization.page.description"
- id="onboarding.create_organization.page.description"
- values={
- Object {
- "break": <br />,
- "more": <Link
- onlyActiveOnIndex={false}
- style={Object {}}
- target="_blank"
- to="/documentation/sonarcloud-pricing/"
- >
- learn_more
- </Link>,
- "price": "billing.price_format.10",
- }
- }
- />
+ onboarding.create_organization.page.description
</p>
</header>
<ManualOrganizationCreate
@@ -414,24 +363,7 @@ exports[`should render with organization bind page 2`] = `
<p
className="page-description"
>
- <FormattedMessage
- defaultMessage="onboarding.create_organization.page.description"
- id="onboarding.create_organization.page.description"
- values={
- Object {
- "break": <br />,
- "more": <Link
- onlyActiveOnIndex={false}
- style={Object {}}
- target="_blank"
- to="/documentation/sonarcloud-pricing/"
- >
- learn_more
- </Link>,
- "price": "billing.price_format.10",
- }
- }
- />
+ onboarding.create_organization.page.description
</p>
</header>
<Tabs
@@ -551,24 +483,7 @@ exports[`should switch tabs 1`] = `
<p
className="page-description"
>
- <FormattedMessage
- defaultMessage="onboarding.create_organization.page.description"
- id="onboarding.create_organization.page.description"
- values={
- Object {
- "break": <br />,
- "more": <Link
- onlyActiveOnIndex={false}
- style={Object {}}
- target="_blank"
- to="/documentation/sonarcloud-pricing/"
- >
- learn_more
- </Link>,
- "price": "billing.price_format.10",
- }
- }
- />
+ onboarding.create_organization.page.description
</p>
</header>
<Tabs
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/PlanSelect-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/PlanSelect-test.tsx.snap
index 25680a523c4..cb99c558c4a 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/PlanSelect-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/PlanSelect-test.tsx.snap
@@ -18,7 +18,7 @@ exports[`should render and select 1`] = `
key="paid"
onClick={[Function]}
selected={false}
- startingPrice="10"
+ startingPrice={10}
/>
</div>
`;
@@ -41,7 +41,7 @@ exports[`should render and select 2`] = `
key="paid"
onClick={[Function]}
selected={true}
- startingPrice="10"
+ startingPrice={10}
/>
</div>
`;
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}
/>
<Connect(withCurrentUser(BillingFormShim))
onCommit={[MockFunction]}
@@ -94,7 +94,7 @@ exports[`should render and use free plan 2`] = `
<PlanSelect
onChange={[Function]}
plan="free"
- startingPrice="billing.price_format.100"
+ startingPrice={100}
/>
<form
className="display-flex-center big-spacer-top"
@@ -137,7 +137,7 @@ exports[`should upgrade 1`] = `
<PlanSelect
onChange={[Function]}
plan="paid"
- startingPrice="billing.price_format.100"
+ startingPrice={100}
/>
<Connect(withCurrentUser(BillingFormShim))
onCommit={[MockFunction]}
diff --git a/server/sonar-web/src/main/js/apps/create/organization/utils.ts b/server/sonar-web/src/main/js/apps/create/organization/utils.ts
index 4d5d72200f2..bc2688be093 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/utils.ts
+++ b/server/sonar-web/src/main/js/apps/create/organization/utils.ts
@@ -35,8 +35,6 @@ export const ORGANIZATION_IMPORT_BINDING_IN_PROGRESS_TIMESTAMP =
export const ORGANIZATION_IMPORT_REDIRECT_TO_PROJECT_TIMESTAMP =
'sonarcloud.import_org.redirect_to_projects';
-export const TRIAL_DURATION_DAYS = 14;
-
export enum Step {
OrganizationDetails,
Plan
diff --git a/server/sonar-web/src/main/js/apps/create/project/AlmRepositoryItem.tsx b/server/sonar-web/src/main/js/apps/create/project/AlmRepositoryItem.tsx
index aa700e4ce57..fc25d51b8ec 100644
--- a/server/sonar-web/src/main/js/apps/create/project/AlmRepositoryItem.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/AlmRepositoryItem.tsx
@@ -18,6 +18,8 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import * as classNames from 'classnames';
+import { identity } from 'lodash';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router';
import * as theme from '../../../app/theme';
@@ -26,8 +28,11 @@ import CheckIcon from '../../../components/icons-components/CheckIcon';
import Tooltip from '../../../components/controls/Tooltip';
import { getBaseUrl, getProjectUrl } from '../../../helpers/urls';
import { translate } from '../../../helpers/l10n';
+import LockIcon from '../../../components/icons-components/LockIcon';
interface Props {
+ disabled: boolean;
+ highlightUpgradeBox: (highlight: boolean) => void;
identityProvider: T.IdentityProvider;
repository: T.AlmRepository;
selected: boolean;
@@ -35,51 +40,83 @@ interface Props {
}
export default class AlmRepositoryItem extends React.PureComponent<Props> {
- 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 (
- <>
- <Checkbox
- checked={selected || alreadyImported}
- disabled={alreadyImported || repository.private}
- onCheck={this.handleChange}>
- <img
- alt={identityProvider.name}
- className="spacer-left"
- height={14}
- src={`${getBaseUrl()}/images/sonarcloud/${identityProvider.key}.svg`}
- style={{ opacity: alreadyImported || repository.private ? 0.5 : 1 }}
- width={14}
- />
- <span className="spacer-left">{this.props.repository.label}</span>
- </Checkbox>
- {repository.linkedProjectKey && (
- <span className="big-spacer-left">
- <CheckIcon className="little-spacer-right" fill={theme.green} />
- <FormattedMessage
- defaultMessage={translate('onboarding.create_project.repository_imported')}
- id="onboarding.create_project.repository_imported"
- values={{
- link: (
- <Link to={getProjectUrl(repository.linkedProjectKey)}>
- {translate('onboarding.create_project.see_project')}
- </Link>
- )
- }}
- />
- </span>
- )}
- {repository.private && (
- <Tooltip overlay={translate('onboarding.import_organization.private.disabled')}>
- <div className="outline-badge spacer-left">{translate('visibility.private')}</div>
- </Tooltip>
- )}
- </>
+ <Tooltip
+ overlay={
+ disabled
+ ? translate('onboarding.create_project.subscribe_to_import_private_repositories')
+ : undefined
+ }>
+ <li>
+ <div
+ className={classNames('create-project-repository', {
+ disabled,
+ imported: alreadyImported,
+ selected
+ })}
+ onClick={this.handleToggle}
+ onMouseEnter={disabled ? this.handleMouseEnter : undefined}
+ onMouseLeave={disabled ? this.handleMouseLeave : undefined}
+ role="listitem">
+ <div className="flex-1 display-flex-center">
+ {disabled ? (
+ <LockIcon fill={theme.disableGrayText} />
+ ) : (
+ <Checkbox
+ checked={selected || alreadyImported}
+ disabled={disabled || alreadyImported}
+ onCheck={identity}
+ />
+ )}
+ <img
+ alt={identityProvider.name}
+ className={classNames('spacer-left', { 'icon-half-transparent': disabled })}
+ height={14}
+ src={`${getBaseUrl()}/images/sonarcloud/${identityProvider.key}.svg`}
+ width={14}
+ />
+ <span className="spacer-left">{this.props.repository.label}</span>
+ {repository.private && (
+ <div className="outline-badge spacer-left">{translate('visibility.private')}</div>
+ )}
+ </div>
+
+ {repository.linkedProjectKey && (
+ <span>
+ <CheckIcon className="little-spacer-right" fill={theme.green} />
+ <FormattedMessage
+ defaultMessage={translate('onboarding.create_project.repository_imported')}
+ id="onboarding.create_project.repository_imported"
+ values={{
+ link: (
+ <Link to={getProjectUrl(repository.linkedProjectKey)}>
+ {translate('onboarding.create_project.see_project')}
+ </Link>
+ )
+ }}
+ />
+ </span>
+ )}
+ </div>
+ </li>
+ </Tooltip>
);
}
}
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<Props, State> {
+ 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<Props, State>
}
const { selectedOrganization } = this.state;
+ const organization = boundOrganizations.find(o => o.key === selectedOrganization);
+
return (
<>
<OrganizationInput
@@ -97,11 +110,12 @@ export default class AutoProjectCreate extends React.PureComponent<Props, State>
organization={selectedOrganization}
organizations={this.props.boundOrganizations}
/>
- {selectedOrganization && (
+ {organization && (
<RemoteRepositories
almApplication={almApplication}
+ onOrganizationUpgrade={this.props.onOrganizationUpgrade}
onProjectCreate={onProjectCreate}
- organization={selectedOrganization}
+ organization={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<void>;
skipOnboarding: () => void;
userOrganizations: T.Organization[];
}
@@ -162,6 +164,7 @@ export class CreateProjectPage extends React.PureComponent<Props & WithRouterPro
boundOrganizations={userOrganizations.filter(
({ alm, actions = {} }) => 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<Props, State> {
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<Props, State
componentDidUpdate(prevProps: Props) {
const { organization } = this.props;
if (prevProps.organization !== organization) {
- this.setState({ loading: true });
+ this.setState({ loading: true, selectedRepositories: {} });
this.fetchRepositories();
}
}
@@ -62,7 +74,7 @@ export default class RemoteRepositories extends React.PureComponent<Props, State
fetchRepositories = () => {
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<Props, State
);
};
- handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
- 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<Props, State
updateSelectedRepositories[newRepository.installationKey] = newRepository;
}
});
- return { selectedRepositories: updateSelectedRepositories, submitting: false };
+ return { selectedRepositories: updateSelectedRepositories };
});
}
});
};
- isValid = () => {
- 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<Props, State
};
render() {
- const { loading, selectedRepositories, submitting } = this.state;
- const { almApplication } = this.props;
+ const { highlight, loading, repositories, selectedRepositories } = this.state;
+ const { almApplication, organization } = this.props;
+ const isPaidOrg = organization.subscription === 'PAID';
+ const hasPrivateRepositories = repositories.some(repository => Boolean(repository.private));
+ const showUpgradebox =
+ !isPaidOrg && hasPrivateRepositories && organization.actions && organization.actions.admin;
+
return (
- <DeferredSpinner loading={loading}>
- <form onSubmit={this.handleFormSubmit}>
- <div className="form-field">
+ <div className="create-project">
+ <div className="flex-1 huge-spacer-right">
+ {this.state.successfullyUpgraded && (
+ <Alert variant="success">
+ {translateWithParameters(
+ 'onboarding.create_project.subscribtion_success_x',
+ organization.name
+ )}
+ </Alert>
+ )}
+ <DeferredSpinner loading={loading}>
<ul>
- {this.state.repositories.map(repo => (
- <li className="big-spacer-bottom" key={repo.installationKey}>
- <AlmRepositoryItem
- identityProvider={almApplication}
- repository={repo}
- selected={Boolean(selectedRepositories[repo.installationKey])}
- toggleRepository={this.toggleRepository}
- />
- </li>
+ {repositories.map(repo => (
+ <AlmRepositoryItem
+ disabled={Boolean(repo.private && !isPaidOrg)}
+ highlightUpgradeBox={this.handleHighlightUpgradeBox}
+ identityProvider={almApplication}
+ key={repo.installationKey}
+ repository={repo}
+ selected={Boolean(selectedRepositories[repo.installationKey])}
+ toggleRepository={this.toggleRepository}
+ />
))}
</ul>
+ </DeferredSpinner>
+ </div>
+ {organization && (
+ <div className="huge-spacer-left">
+ <SetupProjectBox
+ onProjectCreate={this.props.onProjectCreate}
+ onProvisionFail={this.handleProvisionFail}
+ organization={organization}
+ selectedRepositories={Object.keys(selectedRepositories)
+ .map(r => selectedRepositories[r])
+ .filter(isDefined)}
+ />
+ {showUpgradebox && (
+ <UpgradeOrganizationBox
+ className={classNames({ highlight })}
+ onOrganizationUpgrade={this.handleOrganizationUpgrade}
+ organization={organization}
+ />
+ )}
</div>
- <SubmitButton disabled={!this.isValid() || submitting}>{translate('setup')}</SubmitButton>
- <DeferredSpinner className="spacer-left" loading={submitting} />
- </form>
- </DeferredSpinner>
+ )}
+ </div>
);
}
}
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<void>;
+ organization: T.Organization;
+ selectedRepositories: T.AlmRepository[];
+}
+
+interface State {
+ submitting: boolean;
+}
+
+export default class SetupProjectBox extends React.PureComponent<Props, State> {
+ 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<HTMLFormElement>) => {
+ 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 (
+ <form
+ className={classNames('create-project-setup boxed-group', {
+ open: hasSelectedRepositories
+ })}
+ onSubmit={this.handleFormSubmit}>
+ <div className="boxed-group-header">
+ <h2 className="spacer-top">
+ {selectedRepositories.length > 1
+ ? translateWithParameters(
+ 'onboarding.create_project.x_repositories_selected',
+ selectedRepositories.length
+ )
+ : translate('onboarding.create_project.1_repository_selected')}
+ </h2>
+ </div>
+ <div className="boxed-group-inner">
+ <div className="flex-1">
+ {publicRepos.length === 1 && (
+ <p>{translate('onboarding.create_project.1_repository_created_as_public')}</p>
+ )}
+ {publicRepos.length > 1 && (
+ <p>
+ {translateWithParameters(
+ 'onboarding.create_project.x_repository_created_as_public',
+ publicRepos.length
+ )}
+ </p>
+ )}
+ {privateRepos.length === 1 && (
+ <p>{translate('onboarding.create_project.1_repository_created_as_private')}</p>
+ )}
+ {privateRepos.length > 1 && (
+ <p>
+ {translateWithParameters(
+ 'onboarding.create_project.x_repository_created_as_private',
+ privateRepos.length
+ )}
+ </p>
+ )}
+ </div>
+ <div>
+ <SubmitButton className="button-large" disabled={this.state.submitting}>
+ {translate('setup')}
+ </SubmitButton>
+ <DeferredSpinner className="spacer-left" loading={this.state.submitting} />
+ </div>
+ </div>
+ </form>
+ );
+ }
+}
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(
<AlmRepositoryItem
+ disabled={false}
+ highlightUpgradeBox={jest.fn()}
identityProvider={identityProviders}
repository={repositories[1]}
selected={false}
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/AutoProjectCreate-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/AutoProjectCreate-test.tsx
index a7664c362be..4bdfc5a10fe 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/AutoProjectCreate-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/AutoProjectCreate-test.tsx
@@ -29,11 +29,27 @@ const almApplication = {
name: 'GitHub'
};
+const boundOrganizations: T.Organization[] = [
+ {
+ actions: { admin: true },
+ alm: { key: 'github', url: '' },
+ key: 'foo',
+ name: 'Foo',
+ subscription: 'FREE'
+ },
+ {
+ alm: { key: 'github', url: '' },
+ key: 'bar',
+ name: 'Bar',
+ subscription: 'FREE'
+ }
+];
+
it('should display the provider app install button', () => {
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<AutoProjectCreate['props']> = {}) {
return shallow(
<AutoProjectCreate
almApplication={almApplication}
- boundOrganizations={[
- { alm: { key: 'github', url: '' }, key: 'foo', name: 'Foo' },
- { alm: { key: 'github', url: '' }, key: 'bar', name: 'Bar' }
- ]}
+ boundOrganizations={boundOrganizations}
+ onOrganizationUpgrade={jest.fn()}
onProjectCreate={jest.fn()}
organization=""
{...props}
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/RemoteRepositories-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/RemoteRepositories-test.tsx
index 42529ab5b5b..c37b1d1f4fb 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/RemoteRepositories-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/RemoteRepositories-test.tsx
@@ -20,8 +20,8 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import RemoteRepositories from '../RemoteRepositories';
-import { getRepositories, provisionProject } from '../../../../api/alm-integration';
-import { waitAndUpdate, submit } from '../../../../helpers/testUtils';
+import { getRepositories } from '../../../../api/alm-integration';
+import { waitAndUpdate } from '../../../../helpers/testUtils';
jest.mock('../../../../api/alm-integration', () => ({
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<any>).mockClear();
- (provisionProject as jest.Mock<any>).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<any>).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<Function>('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<any>).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<RemoteRepositories['props']> = {}) {
return shallow(
<RemoteRepositories
almApplication={almApplication}
+ onOrganizationUpgrade={jest.fn()}
onProjectCreate={jest.fn()}
- organization="sonarsource"
+ organization={organization}
{...props}
/>
);
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<SetupProjectBox['props']> = {}) {
+ return shallow(
+ <SetupProjectBox
+ onProjectCreate={jest.fn()}
+ onProvisionFail={jest.fn()}
+ organization={organization}
+ selectedRepositories={selectedRepositories}
+ {...props}
+ />
+ );
+}
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`] = `
-<Fragment>
- <Checkbox
- checked={false}
- onCheck={[Function]}
- thirdState={false}
- >
- <img
- alt="Foo Provider"
- className="spacer-left"
- height={14}
- src="/images/sonarcloud/foo.svg"
- style={
- Object {
- "opacity": 1,
- }
- }
- width={14}
- />
- <span
- className="spacer-left"
+<Tooltip>
+ <li>
+ <div
+ className="create-project-repository"
+ onClick={[Function]}
+ role="listitem"
+ >
+ <div
+ className="flex-1 display-flex-center"
+ >
+ <Checkbox
+ checked={false}
+ disabled={false}
+ onCheck={[Function]}
+ thirdState={false}
+ />
+ <img
+ alt="Foo Provider"
+ className="spacer-left"
+ height={14}
+ src="/images/sonarcloud/foo.svg"
+ width={14}
+ />
+ <span
+ className="spacer-left"
+ >
+ Awesome Project
+ </span>
+ </div>
+ </div>
+ </li>
+</Tooltip>
+`;
+
+exports[`should render disabed 1`] = `
+<Tooltip
+ overlay="onboarding.create_project.subscribe_to_import_private_repositories"
+>
+ <li>
+ <div
+ className="create-project-repository disabled"
+ onClick={[Function]}
+ onMouseEnter={[Function]}
+ onMouseLeave={[Function]}
+ role="listitem"
>
- Awesome Project
- </span>
- </Checkbox>
-</Fragment>
+ <div
+ className="flex-1 display-flex-center"
+ >
+ <LockIcon
+ fill="#bbb"
+ />
+ <img
+ alt="Foo Provider"
+ className="spacer-left icon-half-transparent"
+ height={14}
+ src="/images/sonarcloud/foo.svg"
+ width={14}
+ />
+ <span
+ className="spacer-left"
+ >
+ Awesome Project
+ </span>
+ </div>
+ </div>
+ </li>
+</Tooltip>
`;
-exports[`should render disabled 1`] = `
-<Fragment>
- <Checkbox
- checked={true}
- disabled={true}
- onCheck={[Function]}
- thirdState={false}
- >
- <img
- alt="Foo Provider"
- className="spacer-left"
- height={14}
- src="/images/sonarcloud/foo.svg"
- style={
- Object {
- "opacity": 0.5,
- }
- }
- width={14}
- />
- <span
- className="spacer-left"
+exports[`should render imported 1`] = `
+<Tooltip>
+ <li>
+ <div
+ className="create-project-repository imported"
+ onClick={[Function]}
+ role="listitem"
>
- Cool Project
- </span>
- </Checkbox>
- <span
- className="big-spacer-left"
- >
- <CheckIcon
- className="little-spacer-right"
- fill="#00aa00"
- />
- <FormattedMessage
- defaultMessage="onboarding.create_project.repository_imported"
- id="onboarding.create_project.repository_imported"
- values={
- Object {
- "link": <Link
- onlyActiveOnIndex={false}
- style={Object {}}
- to={
- Object {
- "pathname": "/dashboard",
- "query": Object {
- "branch": undefined,
- "id": "proj_cool",
- },
- }
+ <div
+ className="flex-1 display-flex-center"
+ >
+ <Checkbox
+ checked={true}
+ disabled={true}
+ onCheck={[Function]}
+ thirdState={false}
+ />
+ <img
+ alt="Foo Provider"
+ className="spacer-left"
+ height={14}
+ src="/images/sonarcloud/foo.svg"
+ width={14}
+ />
+ <span
+ className="spacer-left"
+ >
+ Cool Project
+ </span>
+ </div>
+ <span>
+ <CheckIcon
+ className="little-spacer-right"
+ fill="#00aa00"
+ />
+ <FormattedMessage
+ defaultMessage="onboarding.create_project.repository_imported"
+ id="onboarding.create_project.repository_imported"
+ values={
+ Object {
+ "link": <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/dashboard",
+ "query": Object {
+ "branch": undefined,
+ "id": "proj_cool",
+ },
+ }
+ }
+ >
+ onboarding.create_project.see_project
+ </Link>,
}
- >
- onboarding.create_project.see_project
- </Link>,
- }
- }
- />
- </span>
-</Fragment>
+ }
+ />
+ </span>
+ </div>
+ </li>
+</Tooltip>
`;
exports[`should render private repositories 1`] = `
-<Fragment>
- <Checkbox
- checked={false}
- disabled={true}
- onCheck={[Function]}
- thirdState={false}
- >
- <img
- alt="Foo Provider"
- className="spacer-left"
- height={14}
- src="/images/sonarcloud/foo.svg"
- style={
- Object {
- "opacity": 0.5,
- }
- }
- width={14}
- />
- <span
- className="spacer-left"
- >
- Awesome Project
- </span>
- </Checkbox>
- <Tooltip
- overlay="onboarding.import_organization.private.disabled"
- >
+<Tooltip>
+ <li>
<div
- className="outline-badge spacer-left"
+ className="create-project-repository"
+ onClick={[Function]}
+ role="listitem"
>
- visibility.private
+ <div
+ className="flex-1 display-flex-center"
+ >
+ <Checkbox
+ checked={false}
+ disabled={false}
+ onCheck={[Function]}
+ thirdState={false}
+ />
+ <img
+ alt="Foo Provider"
+ className="spacer-left"
+ height={14}
+ src="/images/sonarcloud/foo.svg"
+ width={14}
+ />
+ <span
+ className="spacer-left"
+ >
+ Awesome Project
+ </span>
+ <div
+ className="outline-badge spacer-left"
+ >
+ visibility.private
+ </div>
+ </div>
</div>
- </Tooltip>
-</Fragment>
+ </li>
+</Tooltip>
`;
exports[`should render selected 1`] = `
-<Fragment>
- <Checkbox
- checked={true}
- onCheck={[Function]}
- thirdState={false}
- >
- <img
- alt="Foo Provider"
- className="spacer-left"
- height={14}
- src="/images/sonarcloud/foo.svg"
- style={
- Object {
- "opacity": 1,
- }
- }
- width={14}
- />
- <span
- className="spacer-left"
+<Tooltip>
+ <li>
+ <div
+ className="create-project-repository selected"
+ onClick={[Function]}
+ role="listitem"
>
- Awesome Project
- </span>
- </Checkbox>
-</Fragment>
+ <div
+ className="flex-1 display-flex-center"
+ >
+ <Checkbox
+ checked={true}
+ disabled={false}
+ onCheck={[Function]}
+ thirdState={false}
+ />
+ <img
+ alt="Foo Provider"
+ className="spacer-left"
+ height={14}
+ src="/images/sonarcloud/foo.svg"
+ width={14}
+ />
+ <span
+ className="spacer-left"
+ >
+ Awesome Project
+ </span>
+ </div>
+ </div>
+ </li>
+</Tooltip>
`;
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`] = `
<Fragment>
<withRouter(OrganizationInput)
autoImport={true}
@@ -9,12 +9,16 @@ exports[`should display the bounded organizations dropdown with the list of repo
organizations={
Array [
Object {
+ "actions": Object {
+ "admin": true,
+ },
"alm": Object {
"key": "github",
"url": "",
},
"key": "foo",
"name": "Foo",
+ "subscription": "FREE",
},
Object {
"alm": Object {
@@ -23,6 +27,7 @@ exports[`should display the bounded organizations dropdown with the list of repo
},
"key": "bar",
"name": "Bar",
+ "subscription": "FREE",
},
]
}
@@ -37,8 +42,22 @@ exports[`should display the bounded organizations dropdown with the list of repo
"name": "GitHub",
}
}
+ onOrganizationUpgrade={[MockFunction]}
onProjectCreate={[MockFunction]}
- organization="foo"
+ organization={
+ Object {
+ "actions": Object {
+ "admin": true,
+ },
+ "alm": Object {
+ "key": "github",
+ "url": "",
+ },
+ "key": "foo",
+ "name": "Foo",
+ "subscription": "FREE",
+ }
+ }
/>
</Fragment>
`;
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`] = `
-<SubmitButton
- disabled={false}
->
- setup
-</SubmitButton>
-`;
-
exports[`should display the list of repositories 1`] = `
-<DeferredSpinner
- loading={true}
- timeout={100}
+<div
+ className="create-project"
>
- <form
- onSubmit={[Function]}
+ <div
+ className="flex-1 huge-spacer-right"
>
- <div
- className="form-field"
- >
- <ul />
- </div>
- <SubmitButton
- disabled={true}
- >
- setup
- </SubmitButton>
<DeferredSpinner
- className="spacer-left"
- loading={false}
+ loading={true}
timeout={100}
+ >
+ <ul />
+ </DeferredSpinner>
+ </div>
+ <div
+ className="huge-spacer-left"
+ >
+ <SetupProjectBox
+ onProjectCreate={[MockFunction]}
+ onProvisionFail={[Function]}
+ organization={
+ Object {
+ "alm": Object {
+ "key": "github",
+ "url": "",
+ },
+ "key": "sonarsource",
+ "name": "SonarSource",
+ "subscription": "FREE",
+ }
+ }
+ selectedRepositories={Array []}
/>
- </form>
-</DeferredSpinner>
+ </div>
+</div>
`;
exports[`should display the list of repositories 2`] = `
-<DeferredSpinner
- loading={false}
- timeout={100}
+<div
+ className="create-project"
>
- <form
- onSubmit={[Function]}
+ <div
+ className="flex-1 huge-spacer-right"
>
- <div
- className="form-field"
+ <DeferredSpinner
+ loading={false}
+ timeout={100}
>
<ul>
- <li
- className="big-spacer-bottom"
+ <AlmRepositoryItem
+ disabled={false}
+ highlightUpgradeBox={[Function]}
+ identityProvider={
+ Object {
+ "backgroundColor": "blue",
+ "iconPath": "icon/path",
+ "installationUrl": "https://alm.installation.url",
+ "key": "github",
+ "name": "GitHub",
+ }
+ }
key="github/cool"
- >
- <AlmRepositoryItem
- identityProvider={
- Object {
- "backgroundColor": "blue",
- "iconPath": "icon/path",
- "installationUrl": "https://alm.installation.url",
- "key": "github",
- "name": "GitHub",
- }
+ repository={
+ Object {
+ "installationKey": "github/cool",
+ "label": "Cool Project",
+ "linkedProjectKey": "proj_cool",
+ "linkedProjectName": "Proj Cool",
}
- repository={
- Object {
- "installationKey": "github/cool",
- "label": "Cool Project",
- "linkedProjectKey": "proj_cool",
- "linkedProjectName": "Proj Cool",
- }
+ }
+ selected={false}
+ toggleRepository={[Function]}
+ />
+ <AlmRepositoryItem
+ disabled={false}
+ highlightUpgradeBox={[Function]}
+ identityProvider={
+ Object {
+ "backgroundColor": "blue",
+ "iconPath": "icon/path",
+ "installationUrl": "https://alm.installation.url",
+ "key": "github",
+ "name": "GitHub",
}
- selected={false}
- toggleRepository={[Function]}
- />
- </li>
- <li
- className="big-spacer-bottom"
+ }
key="github/awesome"
- >
- <AlmRepositoryItem
- identityProvider={
- Object {
- "backgroundColor": "blue",
- "iconPath": "icon/path",
- "installationUrl": "https://alm.installation.url",
- "key": "github",
- "name": "GitHub",
- }
+ repository={
+ Object {
+ "installationKey": "github/awesome",
+ "label": "Awesome Project",
}
- repository={
- Object {
- "installationKey": "github/awesome",
- "label": "Awesome Project",
- }
- }
- selected={false}
- toggleRepository={[Function]}
- />
- </li>
+ }
+ selected={false}
+ toggleRepository={[Function]}
+ />
</ul>
- </div>
- <SubmitButton
- disabled={true}
- >
- setup
- </SubmitButton>
- <DeferredSpinner
- className="spacer-left"
- loading={false}
- timeout={100}
+ </DeferredSpinner>
+ </div>
+ <div
+ className="huge-spacer-left"
+ >
+ <SetupProjectBox
+ onProjectCreate={[MockFunction]}
+ onProvisionFail={[Function]}
+ organization={
+ Object {
+ "alm": Object {
+ "key": "github",
+ "url": "",
+ },
+ "key": "sonarsource",
+ "name": "SonarSource",
+ "subscription": "FREE",
+ }
+ }
+ selectedRepositories={Array []}
/>
- </form>
-</DeferredSpinner>
+ </div>
+</div>
+`;
+
+exports[`should display the organization upgrade box 1`] = `
+<UpgradeOrganizationBox
+ className=""
+ onOrganizationUpgrade={[Function]}
+ organization={
+ Object {
+ "actions": Object {
+ "admin": true,
+ },
+ "alm": Object {
+ "key": "github",
+ "url": "",
+ },
+ "key": "sonarsource",
+ "name": "SonarSource",
+ "subscription": "FREE",
+ }
+ }
+/>
`;
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`] = `
+<form
+ className="create-project-setup boxed-group open"
+ onSubmit={[Function]}
+>
+ <div
+ className="boxed-group-header"
+ >
+ <h2
+ className="spacer-top"
+ >
+ onboarding.create_project.x_repositories_selected.2
+ </h2>
+ </div>
+ <div
+ className="boxed-group-inner"
+ >
+ <div
+ className="flex-1"
+ >
+ <p>
+ onboarding.create_project.1_repository_created_as_public
+ </p>
+ <p>
+ onboarding.create_project.1_repository_created_as_private
+ </p>
+ </div>
+ <div>
+ <SubmitButton
+ className="button-large"
+ disabled={false}
+ >
+ setup
+ </SubmitButton>
+ <DeferredSpinner
+ className="spacer-left"
+ loading={false}
+ timeout={100}
+ />
+ </div>
+ </div>
+</form>
+`;
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<T.Component>) => void;
+ organization?: T.Organization;
}
interface State {
@@ -299,6 +301,16 @@ export default class App extends React.PureComponent<Props, State> {
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<Props, State> {
};
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 (
<div className="page page-limited" id="project-permissions-page">
<Helmet title={translate('permissions.page')} />
<PageHeader
- component={this.props.component}
+ component={component}
loadHolders={this.loadHolders}
loading={this.state.loading}
/>
@@ -366,22 +387,26 @@ export default class App extends React.PureComponent<Props, State> {
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 && (
- <UpgradeOrganizationBox organization={this.props.component.organization} />
+ {showUpgradeBox &&
+ organization && (
+ <UpgradeOrganizationBox
+ className="big-spacer-bottom"
+ onOrganizationUpgrade={this.handleOrganizationUpgrade}
+ organization={organization}
+ />
)}
{this.state.disclaimer && (
<PublicProjectDisclaimer
- component={this.props.component}
+ component={component}
onClose={this.closeDisclaimer}
onConfirm={this.turnProjectToPublic}
/>
)}
</div>
<AllHoldersList
- component={this.props.component}
+ component={component}
filter={this.state.filter}
grantPermissionToGroup={this.grantPermissionToGroup}
grantPermissionToUser={this.grantPermissionToUser}
@@ -397,7 +422,7 @@ export default class App extends React.PureComponent<Props, State> {
selectedPermission={this.state.selectedPermission}
users={this.state.users}
usersPaging={this.state.usersPaging}
- visibility={this.props.component.visibility}
+ visibility={component.visibility}
/>
</div>
);
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<T.Component>) => 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<Props> {
left -= TOOLTIP_WIDTH;
placement = PopupPlacement.LeftTop;
}
- const tooltipContent = this.renderContent().filter(Boolean);
+ const tooltipContent = this.renderContent().filter(isDefined);
const addSeparator = tooltipContent.length > 0;
return (
<Popup
diff --git a/server/sonar-web/src/main/js/apps/projects/routes.ts b/server/sonar-web/src/main/js/apps/projects/routes.ts
index 5478d372823..e73a375e5ae 100644
--- a/server/sonar-web/src/main/js/apps/projects/routes.ts
+++ b/server/sonar-web/src/main/js/apps/projects/routes.ts
@@ -24,6 +24,7 @@ import { PROJECTS_DEFAULT_FILTER, PROJECTS_ALL } from './utils';
import { save } from '../../helpers/storage';
import { isSonarCloud } from '../../helpers/system';
import { lazyLoad } from '../../components/lazyLoad';
+import { isDefined } from '../../helpers/types';
const routes = [
{ indexRoute: { component: DefaultPageSelectorContainer } },
@@ -39,6 +40,6 @@ const routes = [
path: 'create',
component: lazyLoad(() => 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<Props, State> {
{this.state.createProjectForm && (
<CreateProjectForm
onClose={this.closeCreateProjectForm}
+ onOrganizationUpgrade={this.props.onOrganizationUpgrade}
onProjectCreated={this.requestProjects}
organization={this.props.organization}
/>
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<OwnProps & StateProps & DispatchP
}
}
+ handleOrganizationUpgrade = () => {
+ 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<OwnProps & StateProps & DispatchP
<App
currentUser={this.props.currentUser}
hasProvisionPermission={actions.provision}
+ onOrganizationUpgrade={this.handleOrganizationUpgrade}
onVisibilityChange={this.handleVisibilityChange}
organization={organization}
topLevelQualifiers={topLevelQualifiers}
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/ChangeVisibilityForm.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/ChangeDefaultVisibilityForm.tsx
index 04e41ea9b39..a035830d333 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/ChangeVisibilityForm.tsx
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/ChangeDefaultVisibilityForm.tsx
@@ -19,7 +19,6 @@
*/
import * as React from 'react';
import * as classNames from 'classnames';
-import UpgradeOrganizationBox from '../../components/common/UpgradeOrganizationBox';
import Modal from '../../components/controls/Modal';
import { Button, ResetButtonLink } from '../../components/ui/buttons';
import { translate } from '../../helpers/l10n';
@@ -35,7 +34,7 @@ interface State {
visibility: T.Visibility;
}
-export default class ChangeVisibilityForm extends React.PureComponent<Props, State> {
+export default class ChangeDefaultVisibilityForm extends React.PureComponent<Props, State> {
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<Props, Sta
</div>
))}
- {organization.canUpdateProjectsVisibilityToPrivate ? (
+ {organization.canUpdateProjectsVisibilityToPrivate && (
<Alert variant="warning">
{translate('organization.change_visibility_form.warning')}
</Alert>
- ) : (
- <UpgradeOrganizationBox organization={this.props.organization.key} />
)}
</div>
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<Props, State>
onChange={this.handleVisibilityChange}
visibility={this.state.visibility}
/>
- {!organization.canUpdateProjectsVisibilityToPrivate && (
- <div className="spacer-top">
- <UpgradeOrganizationBox organization={organization.key} />
+ </div>
+ {organization.actions &&
+ organization.actions.admin &&
+ !organization.canUpdateProjectsVisibilityToPrivate && (
+ <div className="spacer-top display-flex-space-around">
+ <UpgradeOrganizationBox
+ insideModal={true}
+ onOrganizationUpgrade={this.props.onOrganizationUpgrade}
+ organization={organization}
+ />
</div>
)}
- </div>
</div>
<footer className="modal-foot">
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<Props, State> {
<p className="page-description">{translate('projects_management.page.description')}</p>
- {this.state.visibilityForm && (
- <ChangeVisibilityForm
- onClose={this.closeVisiblityForm}
- onConfirm={this.props.onVisibilityChange}
- organization={organization}
- />
- )}
+ {!isSonarCloud() &&
+ this.state.visibilityForm && (
+ <ChangeDefaultVisibilityForm
+ onClose={this.closeVisiblityForm}
+ onConfirm={this.props.onVisibilityChange}
+ organization={organization}
+ />
+ )}
</header>
);
}
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] }) {
<App
currentUser={{ login: 'foo' }}
hasProvisionPermission={true}
+ onOrganizationUpgrade={jest.fn()}
onVisibilityChange={jest.fn()}
organization={organization}
topLevelQualifiers={['TRK', 'VW', 'APP']}
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ChangeVisibilityForm-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ChangeDefaultVisibilityForm-test.tsx
index d6f02e43602..75fd4e85a7a 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ChangeVisibilityForm-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ChangeDefaultVisibilityForm-test.tsx
@@ -19,7 +19,7 @@
*/
import * as React from 'react';
import { shallow } from 'enzyme';
-import ChangeVisibilityForm from '../ChangeVisibilityForm';
+import ChangeDefaultVisibilityForm from '../ChangeDefaultVisibilityForm';
import { click } from '../../../helpers/testUtils';
const organization: T.Organization = {
@@ -61,9 +61,9 @@ it('changes visibility', () => {
expect(onConfirm).toBeCalledWith('private');
});
-function shallowRender(props: Partial<ChangeVisibilityForm['props']> = {}) {
+function shallowRender(props: Partial<ChangeDefaultVisibilityForm['props']> = {}) {
return shallow(
- <ChangeVisibilityForm
+ <ChangeDefaultVisibilityForm
onClose={jest.fn()}
onConfirm={jest.fn()}
organization={organization}
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/CreateProjectForm-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/CreateProjectForm-test.tsx
index 4fb4271cbff..010a64e4845 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/CreateProjectForm-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/CreateProjectForm-test.tsx
@@ -31,12 +31,18 @@ import { change, submit, waitAndUpdate } from '../../../helpers/testUtils';
const createProject = require('../../../api/components').createProject as jest.Mock<any>;
-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(
<CreateProjectForm
onClose={jest.fn()}
+ onOrganizationUpgrade={jest.fn()}
onProjectCreated={jest.fn()}
organization={organization}
/>
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<Function>('onConfirm')('private');
expect(onVisibilityChange).toBeCalledWith('private');
modalWrapper.prop<Function>('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
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
</p>
</div>
- <UpgradeOrganizationBox
- organization="org"
- />
</div>
<footer
className="modal-foot"
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/CreateProjectForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/CreateProjectForm-test.tsx.snap
index a7ce31e715c..859052104d3 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/CreateProjectForm-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/CreateProjectForm-test.tsx.snap
@@ -77,13 +77,24 @@ exports[`creates project 1`] = `
onChange={[Function]}
visibility="public"
/>
- <div
- className="spacer-top"
- >
- <UpgradeOrganizationBox
- organization="org"
- />
- </div>
+ </div>
+ <div
+ className="spacer-top display-flex-space-around"
+ >
+ <UpgradeOrganizationBox
+ insideModal={true}
+ onOrganizationUpgrade={[MockFunction]}
+ organization={
+ Object {
+ "actions": Object {
+ "admin": true,
+ },
+ "key": "org",
+ "name": "org",
+ "projectVisibility": "public",
+ }
+ }
+ />
</div>
</div>
<footer
@@ -183,13 +194,24 @@ exports[`creates project 2`] = `
onChange={[Function]}
visibility="private"
/>
- <div
- className="spacer-top"
- >
- <UpgradeOrganizationBox
- organization="org"
- />
- </div>
+ </div>
+ <div
+ className="spacer-top display-flex-space-around"
+ >
+ <UpgradeOrganizationBox
+ insideModal={true}
+ onOrganizationUpgrade={[MockFunction]}
+ organization={
+ Object {
+ "actions": Object {
+ "admin": true,
+ },
+ "key": "org",
+ "name": "org",
+ "projectVisibility": "public",
+ }
+ }
+ />
</div>
</div>
<footer
@@ -289,13 +311,24 @@ exports[`creates project 3`] = `
onChange={[Function]}
visibility="private"
/>
- <div
- className="spacer-top"
- >
- <UpgradeOrganizationBox
- organization="org"
- />
- </div>
+ </div>
+ <div
+ className="spacer-top display-flex-space-around"
+ >
+ <UpgradeOrganizationBox
+ insideModal={true}
+ onOrganizationUpgrade={[MockFunction]}
+ organization={
+ Object {
+ "actions": Object {
+ "admin": true,
+ },
+ "key": "org",
+ "name": "org",
+ "projectVisibility": "public",
+ }
+ }
+ />
</div>
</div>
<footer
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Header-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Header-test.tsx.snap
index 456011bf7c6..fba62f83057 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Header-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Header-test.tsx.snap
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`changes default visibility 1`] = `
-<ChangeVisibilityForm
+<ChangeDefaultVisibilityForm
onClose={[Function]}
onConfirm={[MockFunction]}
organization={
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.tsx b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.tsx
index 6805ac22c76..1df6e78e993 100644
--- a/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.tsx
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.tsx
@@ -63,7 +63,8 @@ export class OnboardingModal extends React.PureComponent<Props> {
contentLabel={header}
medium={true}
onRequestClose={this.props.onClose}
- shouldCloseOnOverlayClick={false}>
+ shouldCloseOnOverlayClick={false}
+ simple={true}>
<div className="modal-simple-head text-center">
<h1>{translate('onboarding.header')}</h1>
<p className="spacer-top">{translate('onboarding.header.description')}</p>
@@ -80,7 +81,7 @@ export class OnboardingModal extends React.PureComponent<Props> {
</h6>
</Button>
</div>
- <div className="modal-simple-footer text-center">
+ <div className="modal-simple-foot text-center">
<ResetButtonLink className="spacer-bottom" onClick={this.props.onClose}>
{translate('not_now')}
</ResetButtonLink>
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}
>
<div
className="modal-simple-head text-center"
@@ -50,7 +51,7 @@ exports[`renders correctly 1`] = `
</Button>
</div>
<div
- className="modal-simple-footer text-center"
+ className="modal-simple-foot text-center"
>
<ResetButtonLink
className="spacer-bottom"
diff --git a/server/sonar-web/src/main/js/apps/tutorials/styles.css b/server/sonar-web/src/main/js/apps/tutorials/styles.css
index 41fbedac1b5..ae75cc4bca2 100644
--- a/server/sonar-web/src/main/js/apps/tutorials/styles.css
+++ b/server/sonar-web/src/main/js/apps/tutorials/styles.css
@@ -64,6 +64,7 @@
justify-content: space-around;
padding: 44px 100px;
background-color: var(--barBackgroundColor);
+ margin-top: var(--pagePadding);
}
.onboarding-choice {
diff --git a/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.tsx b/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.tsx
index e49532d7fe0..b985e38a3f8 100644
--- a/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.tsx
+++ b/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.tsx
@@ -25,6 +25,7 @@ import { scaleLinear, scalePoint, scaleTime, ScaleTime } from 'd3-scale';
import { line as d3Line, area, curveBasis } from 'd3-shape';
import * as theme from '../../app/theme';
import { Serie, Point } from '../../apps/projectActivity/utils';
+import { isDefined } from '../../helpers/types';
import './LineChart.css';
import './AdvancedTimeline.css';
@@ -412,7 +413,7 @@ export default class AdvancedTimeline extends React.PureComponent<Props, State>
/>
);
})
- .filter(Boolean)
+ .filter(isDefined)
)
.filter(dots => dots.length > 0)}
</g>
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<T> extends React.PureComponent<BubbleProps<T>> {
}
}
-export interface BubbleItem<T> {
+interface BubbleItem<T> {
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<ReactModal.Props, 'contentLabel'>;
@@ -35,9 +37,13 @@ type Props = Partial<ReactModal.Props> & MandatoryProps & OwnProps;
export default function Modal(props: Props) {
return (
<ReactModal
- className={classNames('modal', { 'modal-medium': props.medium, 'modal-large': props.large })}
+ className={classNames('modal', {
+ 'modal-medium': props.medium,
+ 'modal-large': props.large,
+ 'modal-simple': props.simple
+ })}
isOpen={true}
- overlayClassName="modal-overlay"
+ overlayClassName={classNames('modal-overlay', { 'modal-no-backdrop': props.noBackdrop })}
{...props}
/>
);
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<Props, State> {
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<Props, State> {
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<Props, State> {
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<void>;
+ userOrganizations: T.Organization[];
+}> {
render() {
return <div />;
}
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<void>;
+ userOrganizations: T.Organization[];
+}
+
export function withUserOrganizations<P>(
- WrappedComponent: React.ComponentClass<
- P & {
- personalOrganization?: T.Organization;
- userOrganizations: T.Organization[];
- }
- >
+ WrappedComponent: React.ComponentClass<P & Partial<OwnProps>>
) {
- type Props = P & {
- fetchMyOrganizations: () => Promise<void>;
- userOrganizations: T.Organization[];
- };
const wrappedDisplayName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
- class Wrapper extends React.Component<Props> {
+ class Wrapper extends React.Component<P & OwnProps> {
static displayName = `withUserOrganizations(${wrappedDisplayName})`;
componentDidMount() {
@@ -44,13 +40,11 @@ export function withUserOrganizations<P>(
}
render() {
- // @ts-ignore Rest operator not supported yet by TS for generics
- const { fetchMyOrganizations, ...other } = this.props;
- return <WrappedComponent {...other} />;
+ return <WrappedComponent {...this.props} />;
}
}
- 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
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<T>(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