diff options
author | Stas Vilchik <stas-vilchik@users.noreply.github.com> | 2017-05-04 11:19:55 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-05-04 11:19:55 +0200 |
commit | e10f04d5b4055e2813ac01a811b325a414797c4e (patch) | |
tree | 74c640ed1c6d2ef7bd0bc95b03168a130dd3a5ed | |
parent | b09e7a4604b75b7e24e9f0543971db1e70d963b4 (diff) | |
download | sonarqube-e10f04d5b4055e2813ac01a811b325a414797c4e.tar.gz sonarqube-e10f04d5b4055e2813ac01a811b325a414797c4e.zip |
SONAR-9122 prevent setting a project as private (#2015)
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 |