aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorStas Vilchik <stas-vilchik@users.noreply.github.com>2017-05-04 11:19:55 +0200
committerGitHub <noreply@github.com>2017-05-04 11:19:55 +0200
commite10f04d5b4055e2813ac01a811b325a414797c4e (patch)
tree74c640ed1c6d2ef7bd0bc95b03168a130dd3a5ed
parentb09e7a4604b75b7e24e9f0543971db1e70d963b4 (diff)
downloadsonarqube-e10f04d5b4055e2813ac01a811b325a414797c4e.tar.gz
sonarqube-e10f04d5b4055e2813ac01a811b325a414797c4e.zip
SONAR-9122 prevent setting a project as private (#2015)
-rw-r--r--it/it-plugins/fake-billing-plugin/src/main/resources/org/sonar/l10n/billing.properties2
-rw-r--r--it/it-tests/src/test/java/it/organization/BillingTest.java24
-rw-r--r--it/it-tests/src/test/java/pageobjects/ProjectPermissionsPage.java7
-rw-r--r--server/sonar-web/src/main/js/app/components/extensions/OrganizationPageExtension.js12
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/project/components/AllHoldersList.js3
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/project/components/App.js11
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.js3
-rw-r--r--server/sonar-web/src/main/js/apps/projects-admin/ChangeVisibilityForm.js47
-rw-r--r--server/sonar-web/src/main/js/apps/projects-admin/CreateProjectForm.js10
-rw-r--r--server/sonar-web/src/main/js/apps/projects-admin/header.js2
-rw-r--r--server/sonar-web/src/main/js/components/common/UpgradeOrganizationBox.css4
-rw-r--r--server/sonar-web/src/main/js/components/common/UpgradeOrganizationBox.js50
-rw-r--r--server/sonar-web/src/main/js/components/common/VisibilitySelector.js36
-rw-r--r--server/sonar-web/src/main/js/store/organizations/duck.js1
-rw-r--r--server/sonar-web/src/main/less/init/misc.less4
15 files changed, 181 insertions, 35 deletions
diff --git a/it/it-plugins/fake-billing-plugin/src/main/resources/org/sonar/l10n/billing.properties b/it/it-plugins/fake-billing-plugin/src/main/resources/org/sonar/l10n/billing.properties
new file mode 100644
index 00000000000..f8ac8fcaef5
--- /dev/null
+++ b/it/it-plugins/fake-billing-plugin/src/main/resources/org/sonar/l10n/billing.properties
@@ -0,0 +1,2 @@
+billing.upgrade_box.header=The fake billing plugin is installed
+billing.upgrade_box.text=It shows how to change the wording and hide the "Upgrade" button. \ No newline at end of file
diff --git a/it/it-tests/src/test/java/it/organization/BillingTest.java b/it/it-tests/src/test/java/it/organization/BillingTest.java
index 79dd8fe8ca8..82af2341319 100644
--- a/it/it-tests/src/test/java/it/organization/BillingTest.java
+++ b/it/it-tests/src/test/java/it/organization/BillingTest.java
@@ -38,6 +38,7 @@ import org.sonarqube.ws.client.organization.CreateWsRequest;
import org.sonarqube.ws.client.organization.UpdateProjectVisibilityWsRequest;
import org.sonarqube.ws.client.project.CreateRequest;
import org.sonarqube.ws.client.project.UpdateVisibilityRequest;
+import pageobjects.Navigation;
import util.ItUtils;
import util.user.UserRule;
@@ -67,6 +68,9 @@ public class BillingTest {
@Rule
public ExpectedException expectedException = ExpectedException.none();
+ @Rule
+ public Navigation nav = Navigation.get(orchestrator);
+
private static WsClient adminClient;
@BeforeClass
@@ -214,6 +218,26 @@ public class BillingTest {
}
}
+ @Test
+ public void ui_does_not_allow_to_turn_project_to_private() {
+ String projectKey = createPublicProject(createOrganization());
+ setServerProperty(orchestrator, "sonar.billing.preventUpdatingProjectsVisibilityToPrivate", "true");
+
+ nav.logIn().asAdmin().openProjectPermissions(projectKey)
+ .shouldBePublic()
+ .shouldNotAllowPrivate();
+ }
+
+ @Test
+ public void ui_allows_to_turn_project_to_private() {
+ String projectKey = createPublicProject(createOrganization());
+ setServerProperty(orchestrator, "sonar.billing.preventUpdatingProjectsVisibilityToPrivate", "false");
+
+ nav.logIn().asAdmin().openProjectPermissions(projectKey)
+ .shouldBePublic()
+ .turnToPrivate();
+ }
+
private static String createOrganization() {
String key = newOrganizationKey();
adminClient.organizations().create(new CreateWsRequest.Builder().setKey(key).setName(key).build()).getOrganization();
diff --git a/it/it-tests/src/test/java/pageobjects/ProjectPermissionsPage.java b/it/it-tests/src/test/java/pageobjects/ProjectPermissionsPage.java
index 11f23543177..2bb8a07ba89 100644
--- a/it/it-tests/src/test/java/pageobjects/ProjectPermissionsPage.java
+++ b/it/it-tests/src/test/java/pageobjects/ProjectPermissionsPage.java
@@ -19,6 +19,7 @@
*/
package pageobjects;
+import static com.codeborne.selenide.Condition.cssClass;
import static com.codeborne.selenide.Condition.exist;
import static com.codeborne.selenide.Condition.visible;
import static com.codeborne.selenide.Selenide.$;
@@ -51,4 +52,10 @@ public class ProjectPermissionsPage {
shouldBePrivate();
return this;
}
+
+ public ProjectPermissionsPage shouldNotAllowPrivate() {
+ $("#visibility-private").shouldHave(cssClass("text-muted"));
+ $(".upgrade-organization-box").shouldBe(visible);
+ return this;
+ }
}
diff --git a/server/sonar-web/src/main/js/app/components/extensions/OrganizationPageExtension.js b/server/sonar-web/src/main/js/app/components/extensions/OrganizationPageExtension.js
index 1dbc52d85a9..c0428df1cba 100644
--- a/server/sonar-web/src/main/js/app/components/extensions/OrganizationPageExtension.js
+++ b/server/sonar-web/src/main/js/app/components/extensions/OrganizationPageExtension.js
@@ -23,11 +23,13 @@ import { connect } from 'react-redux';
import Extension from './Extension';
import ExtensionNotFound from './ExtensionNotFound';
import { getOrganizationByKey } from '../../../store/rootReducer';
+import { fetchOrganization } from '../../../apps/organizations/actions';
import type { Organization } from '../../../store/organizations/duck';
type Props = {
- organization: Organization,
+ fetchOrganization: string => void,
location: {},
+ organization: Organization,
params: {
extensionKey: string,
organizationKey: string,
@@ -38,6 +40,8 @@ type Props = {
class OrganizationPageExtension extends React.PureComponent {
props: Props;
+ refreshOrganization = () => this.props.fetchOrganization(this.props.organization.key);
+
render() {
const { extensionKey, pluginKey } = this.props.params;
const { organization } = this.props;
@@ -51,8 +55,8 @@ class OrganizationPageExtension extends React.PureComponent {
return extension
? <Extension
extension={extension}
- options={{ organization }}
location={this.props.location}
+ options={{ organization, refreshOrganization: this.refreshOrganization }}
/>
: <ExtensionNotFound />;
}
@@ -62,4 +66,6 @@ const mapStateToProps = (state, ownProps: Props) => ({
organization: getOrganizationByKey(state, ownProps.params.organizationKey)
});
-export default connect(mapStateToProps)(OrganizationPageExtension);
+const mapDispatchToProps = { fetchOrganization };
+
+export default connect(mapStateToProps, mapDispatchToProps)(OrganizationPageExtension);
diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/AllHoldersList.js b/server/sonar-web/src/main/js/apps/permissions/project/components/AllHoldersList.js
index 8d6489bbfab..bbf41cfc88e 100644
--- a/server/sonar-web/src/main/js/apps/permissions/project/components/AllHoldersList.js
+++ b/server/sonar-web/src/main/js/apps/permissions/project/components/AllHoldersList.js
@@ -28,7 +28,8 @@ import { PERMISSIONS_ORDER_BY_QUALIFIER } from '../constants';
type Props = {|
component: {
configuration?: {
- canApplyPermissionTemplate: boolean
+ canApplyPermissionTemplate: boolean,
+ canUpdateProjectVisibilityToPrivate: boolean
},
key: string,
organization: string,
diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/App.js b/server/sonar-web/src/main/js/apps/permissions/project/components/App.js
index 85b0528cdd3..7590041e302 100644
--- a/server/sonar-web/src/main/js/apps/permissions/project/components/App.js
+++ b/server/sonar-web/src/main/js/apps/permissions/project/components/App.js
@@ -21,6 +21,7 @@
import React from 'react';
import { without } from 'lodash';
import PageHeader from './PageHeader';
+import UpgradeOrganizationBox from '../../../../components/common/UpgradeOrganizationBox';
import VisibilitySelector from '../../../../components/common/VisibilitySelector';
import AllHoldersList from './AllHoldersList';
import PublicProjectDisclaimer from './PublicProjectDisclaimer';
@@ -33,7 +34,8 @@ import '../../styles.css';
export type Props = {|
component: {
configuration?: {
- canApplyPermissionTemplate: boolean
+ canApplyPermissionTemplate: boolean,
+ canUpdateProjectVisibilityToPrivate: boolean
},
key: string,
name: string,
@@ -331,6 +333,10 @@ export default class App extends React.PureComponent {
};
render() {
+ const canTurnToPrivate =
+ this.props.component.configuration != null &&
+ this.props.component.configuration.canUpdateProjectVisibilityToPrivate;
+
return (
<div className="page page-limited" id="project-permissions-page">
<PageHeader
@@ -341,10 +347,13 @@ export default class App extends React.PureComponent {
<PageError />
{this.props.component.qualifier === 'TRK' &&
<VisibilitySelector
+ canTurnToPrivate={canTurnToPrivate}
className="big-spacer-top big-spacer-bottom"
onChange={this.handleVisibilityChange}
visibility={this.props.component.visibility}
/>}
+ {!canTurnToPrivate &&
+ <UpgradeOrganizationBox organization={this.props.component.organization} />}
{this.state.disclaimer &&
<PublicProjectDisclaimer
component={this.props.component}
diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.js b/server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.js
index cee4ec29d2c..b746b0ed969 100644
--- a/server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.js
+++ b/server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.js
@@ -25,7 +25,8 @@ import ApplyTemplateView from '../views/ApplyTemplateView';
type Props = {|
component: {
configuration?: {
- canApplyPermissionTemplate: boolean
+ canApplyPermissionTemplate: boolean,
+ canUpdateProjectVisibilityToPrivate: boolean
},
key: string,
qualifier: string,
diff --git a/server/sonar-web/src/main/js/apps/projects-admin/ChangeVisibilityForm.js b/server/sonar-web/src/main/js/apps/projects-admin/ChangeVisibilityForm.js
index ae88b6317b7..fcb2d35b142 100644
--- a/server/sonar-web/src/main/js/apps/projects-admin/ChangeVisibilityForm.js
+++ b/server/sonar-web/src/main/js/apps/projects-admin/ChangeVisibilityForm.js
@@ -21,12 +21,14 @@
import React from 'react';
import Modal from 'react-modal';
import classNames from 'classnames';
+import UpgradeOrganizationBox from '../../components/common/UpgradeOrganizationBox';
import { translate } from '../../helpers/l10n';
+import type { Organization } from '../../store/organizations/duck';
type Props = {
onClose: () => void,
onConfirm: string => void,
- visibility: string
+ organization: Organization
};
type State = {
@@ -39,7 +41,7 @@ export default class ChangeVisibilityForm extends React.PureComponent {
constructor(props: Props) {
super(props);
- this.state = { visibility: props.visibility };
+ this.state = { visibility: props.organization.projectVisibility };
}
handleCancelClick = (event: Event) => {
@@ -62,6 +64,8 @@ export default class ChangeVisibilityForm extends React.PureComponent {
};
render() {
+ const { canUpdateProjectsVisibilityToPrivate } = this.props.organization;
+
return (
<Modal
isOpen={true}
@@ -78,17 +82,26 @@ export default class ChangeVisibilityForm extends React.PureComponent {
{['public', 'private'].map(visibility => (
<div className="big-spacer-bottom" key={visibility}>
<p>
- <a
- className="link-base-color link-no-underline"
- href="#"
- onClick={this.handleVisibilityClick(visibility)}>
- <i
- className={classNames('icon-radio', 'spacer-right', {
- 'is-checked': this.state.visibility === visibility
- })}
- />
- {translate('visibility', visibility)}
- </a>
+ {visibility === 'private' && !canUpdateProjectsVisibilityToPrivate
+ ? <span className="text-muted cursor-not-allowed">
+ <i
+ className={classNames('icon-radio', 'spacer-right', {
+ 'is-checked': this.state.visibility === visibility
+ })}
+ />
+ {translate('visibility', visibility)}
+ </span>
+ : <a
+ className="link-base-color link-no-underline"
+ href="#"
+ onClick={this.handleVisibilityClick(visibility)}>
+ <i
+ className={classNames('icon-radio', 'spacer-right', {
+ 'is-checked': this.state.visibility === visibility
+ })}
+ />
+ {translate('visibility', visibility)}
+ </a>}
</p>
<p className="text-muted spacer-top" style={{ paddingLeft: 22 }}>
{translate('visibility', visibility, 'description.short')}
@@ -96,9 +109,11 @@ export default class ChangeVisibilityForm extends React.PureComponent {
</div>
))}
- <div className="alert alert-warning">
- {translate('organization.change_visibility_form.warning')}
- </div>
+ {canUpdateProjectsVisibilityToPrivate
+ ? <div className="alert alert-warning">
+ {translate('organization.change_visibility_form.warning')}
+ </div>
+ : <UpgradeOrganizationBox organization={this.props.organization.key} />}
</div>
<footer className="modal-foot">
diff --git a/server/sonar-web/src/main/js/apps/projects-admin/CreateProjectForm.js b/server/sonar-web/src/main/js/apps/projects-admin/CreateProjectForm.js
index 25f72ad7dc1..acc503123ed 100644
--- a/server/sonar-web/src/main/js/apps/projects-admin/CreateProjectForm.js
+++ b/server/sonar-web/src/main/js/apps/projects-admin/CreateProjectForm.js
@@ -21,6 +21,7 @@
import React from 'react';
import Modal from 'react-modal';
import { Link } from 'react-router';
+import UpgradeOrganizationBox from '../../components/common/UpgradeOrganizationBox';
import VisibilitySelector from '../../components/common/VisibilitySelector';
import { createProject } from '../../api/components';
import { translate } from '../../helpers/l10n';
@@ -112,6 +113,7 @@ export default class CreateProjectForm extends React.PureComponent {
};
render() {
+ const { organization } = this.props;
const { createdProject } = this.state;
return (
@@ -197,10 +199,18 @@ export default class CreateProjectForm extends React.PureComponent {
<div className="modal-field">
<label> {translate('visibility')} </label>
<VisibilitySelector
+ canTurnToPrivate={
+ organization == null || organization.canUpdateProjectsVisibilityToPrivate
+ }
className="little-spacer-top"
onChange={this.handleVisibilityChange}
visibility={this.state.visibility}
/>
+ {organization != null &&
+ !organization.canUpdateProjectsVisibilityToPrivate &&
+ <div className="spacer-top">
+ <UpgradeOrganizationBox organization={organization.key} />
+ </div>}
</div>
</div>
diff --git a/server/sonar-web/src/main/js/apps/projects-admin/header.js b/server/sonar-web/src/main/js/apps/projects-admin/header.js
index c7fbd4e473c..a6171607dc8 100644
--- a/server/sonar-web/src/main/js/apps/projects-admin/header.js
+++ b/server/sonar-web/src/main/js/apps/projects-admin/header.js
@@ -86,7 +86,7 @@ export default class Header extends React.PureComponent {
<ChangeVisibilityForm
onClose={this.closeVisiblityForm}
onConfirm={this.props.onVisibilityChange}
- visibility={organization.projectVisibility}
+ organization={organization}
/>}
</header>
);
diff --git a/server/sonar-web/src/main/js/components/common/UpgradeOrganizationBox.css b/server/sonar-web/src/main/js/components/common/UpgradeOrganizationBox.css
new file mode 100644
index 00000000000..eb5a45aa786
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/common/UpgradeOrganizationBox.css
@@ -0,0 +1,4 @@
+.upgrade-organization-box {
+ max-width: 400px;
+ background-color: #f3f3f3 !important;
+} \ No newline at end of file
diff --git a/server/sonar-web/src/main/js/components/common/UpgradeOrganizationBox.js b/server/sonar-web/src/main/js/components/common/UpgradeOrganizationBox.js
new file mode 100644
index 00000000000..0a521fd2391
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/common/UpgradeOrganizationBox.js
@@ -0,0 +1,50 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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.
+ */
+// @flow
+import React from 'react';
+import { Link } from 'react-router';
+import { translate, hasMessage } from '../../helpers/l10n';
+import './UpgradeOrganizationBox.css';
+
+type Props = {
+ organization: string
+};
+
+export default function UpgradeOrganizationBox(props: Props) {
+ 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>
+
+ {hasMessage('billing.upgrade_box.button') &&
+ <div className="big-spacer-top">
+ <Link
+ className="button"
+ to={{
+ pathname: `organizations/${props.organization}/extension/billing/billing`,
+ query: { page: 'upgrade' }
+ }}>
+ {translate('billing.upgrade_box.button')}
+ </Link>
+ </div>}
+ </div>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/common/VisibilitySelector.js b/server/sonar-web/src/main/js/components/common/VisibilitySelector.js
index 5a9e4536a6a..08f204e48ab 100644
--- a/server/sonar-web/src/main/js/components/common/VisibilitySelector.js
+++ b/server/sonar-web/src/main/js/components/common/VisibilitySelector.js
@@ -23,6 +23,7 @@ import classNames from 'classnames';
import { translate } from '../../helpers/l10n';
type Props = {|
+ canTurnToPrivate: boolean,
className?: string,
onChange: string => void,
visibility: string
@@ -59,18 +60,29 @@ export default class VisibilitySelector extends React.PureComponent {
<span className="spacer-left">{translate('visibility.public')}</span>
</a>
- <a
- className="link-base-color link-no-underline huge-spacer-left"
- id="visibility-private"
- href="#"
- onClick={this.handlePrivateClick}>
- <i
- className={classNames('icon-radio', {
- 'is-checked': this.props.visibility === 'private'
- })}
- />
- <span className="spacer-left">{translate('visibility.private')}</span>
- </a>
+ {this.props.canTurnToPrivate
+ ? <a
+ className="link-base-color link-no-underline huge-spacer-left"
+ id="visibility-private"
+ href="#"
+ onClick={this.handlePrivateClick}>
+ <i
+ className={classNames('icon-radio', {
+ 'is-checked': this.props.visibility === 'private'
+ })}
+ />
+ <span className="spacer-left">{translate('visibility.private')}</span>
+ </a>
+ : <span
+ className="huge-spacer-left text-muted cursor-not-allowed"
+ id="visibility-private">
+ <i
+ className={classNames('icon-radio', {
+ 'is-checked': this.props.visibility === 'private'
+ })}
+ />
+ <span className="spacer-left">{translate('visibility.private')}</span>
+ </span>}
</div>
);
}
diff --git a/server/sonar-web/src/main/js/store/organizations/duck.js b/server/sonar-web/src/main/js/store/organizations/duck.js
index 9bc74a7550b..5c8ee446cf7 100644
--- a/server/sonar-web/src/main/js/store/organizations/duck.js
+++ b/server/sonar-web/src/main/js/store/organizations/duck.js
@@ -27,6 +27,7 @@ export type Organization = {
canAdmin?: boolean,
canDelete?: boolean,
canProvisionProjects?: boolean,
+ canUpdateProjectsVisibilityToPrivate?: boolean,
description?: string,
key: string,
name: string,
diff --git a/server/sonar-web/src/main/less/init/misc.less b/server/sonar-web/src/main/less/init/misc.less
index 86c49ad83e7..91b035a151b 100644
--- a/server/sonar-web/src/main/less/init/misc.less
+++ b/server/sonar-web/src/main/less/init/misc.less
@@ -148,6 +148,10 @@ td.big-spacer-top { padding-top: 16px; }
text-transform: capitalize;
}
+.cursor-not-allowed {
+ cursor: not-allowed;
+}
+
// Background Color