From 2d9e5533515dd5969897ba1ad3435fea27da28f0 Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Mon, 1 May 2017 16:57:55 +0200 Subject: [PATCH] SONAR-9166 Allow to change default project visibility for organization --- .../src/main/js/api/organizations.js | 3 + .../permission-templates/components/Home.js | 17 ++- .../permission-templates/components/List.js | 2 +- .../components/ListHeader.js | 23 +++- .../js/apps/projects-admin/AppContainer.js | 19 ++- .../projects-admin/ChangeVisibilityForm.js | 114 ++++++++++++++++++ .../src/main/js/apps/projects-admin/header.js | 92 ++++++++------ .../src/main/js/apps/projects-admin/main.js | 44 ++++--- .../src/main/js/apps/projects-admin/search.js | 18 ++- .../src/main/js/store/organizations/duck.js | 1 + .../src/main/less/components/tooltips.less | 5 + .../resources/org/sonar/l10n/core.properties | 8 ++ 12 files changed, 275 insertions(+), 71 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/projects-admin/ChangeVisibilityForm.js diff --git a/server/sonar-web/src/main/js/api/organizations.js b/server/sonar-web/src/main/js/api/organizations.js index 25fb80bcaf8..097a17ce5cb 100644 --- a/server/sonar-web/src/main/js/api/organizations.js +++ b/server/sonar-web/src/main/js/api/organizations.js @@ -68,3 +68,6 @@ export const addMember = (data: { login: string, organization: string }) => export const removeMember = (data: { login: string, organization: string }) => post('/api/organizations/remove_member', data); + +export const changeProjectVisibility = (organization: string, projectVisibility: string) => + post('/api/organizations/update_project_visibility', { organization, projectVisibility }); diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/Home.js b/server/sonar-web/src/main/js/apps/permission-templates/components/Home.js index b649d54728e..0f70164617e 100644 --- a/server/sonar-web/src/main/js/apps/permission-templates/components/Home.js +++ b/server/sonar-web/src/main/js/apps/permission-templates/components/Home.js @@ -21,7 +21,6 @@ import React from 'react'; import Helmet from 'react-helmet'; import Header from './Header'; import List from './List'; -import { TooltipsContainer } from '../../../components/mixins/tooltips-mixin'; import { translate } from '../../../helpers/l10n'; export default class Home extends React.PureComponent { @@ -45,15 +44,13 @@ export default class Home extends React.PureComponent { refresh={this.props.refresh} /> - - - + ); } diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/List.js b/server/sonar-web/src/main/js/apps/permission-templates/components/List.js index 84a834f6689..275489d3500 100644 --- a/server/sonar-web/src/main/js/apps/permission-templates/components/List.js +++ b/server/sonar-web/src/main/js/apps/permission-templates/components/List.js @@ -44,7 +44,7 @@ export default class List extends React.PureComponent { return ( - + {permissionTemplates}
); diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/ListHeader.js b/server/sonar-web/src/main/js/apps/permission-templates/components/ListHeader.js index 19181043a58..29d3e159a37 100644 --- a/server/sonar-web/src/main/js/apps/permission-templates/components/ListHeader.js +++ b/server/sonar-web/src/main/js/apps/permission-templates/components/ListHeader.js @@ -18,17 +18,32 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; +import Tooltip from '../../../components/controls/Tooltip'; +import { translate } from '../../../helpers/l10n'; export default class ListHeader extends React.PureComponent { static propTypes = { + organization: React.PropTypes.object, permissions: React.PropTypes.array.isRequired }; + renderTooltip = permission => + (this.props.organization && (permission.key === 'user' || permission.key === 'codeviewer') + ?
+ {permission.description} +
+ {translate('projects_role', permission.key, 'public_projects_warning')} +
+
+ : permission.description); + render() { - const cells = this.props.permissions.map(p => ( - - {p.name} - + const cells = this.props.permissions.map(permission => ( + + {permission.name} + + + )); diff --git a/server/sonar-web/src/main/js/apps/projects-admin/AppContainer.js b/server/sonar-web/src/main/js/apps/projects-admin/AppContainer.js index baf6f106889..8507504ea85 100644 --- a/server/sonar-web/src/main/js/apps/projects-admin/AppContainer.js +++ b/server/sonar-web/src/main/js/apps/projects-admin/AppContainer.js @@ -20,8 +20,11 @@ import React from 'react'; import { connect } from 'react-redux'; import Main from './main'; +import { onFail } from '../../store/rootActions'; import { getCurrentUser, getAppState } from '../../store/rootReducer'; import { getRootQualifiers } from '../../store/appState/duck'; +import { receiveOrganizations } from '../../store/organizations/duck'; +import { changeProjectVisibility } from '../../api/organizations'; function AppContainer(props) { const hasProvisionPermission = props.organization @@ -36,6 +39,7 @@ function AppContainer(props) {
); @@ -46,4 +50,17 @@ const mapStateToProps = state => ({ user: getCurrentUser(state) }); -export default connect(mapStateToProps)(AppContainer); +const onVisibilityChange = (organization, visibility) => dispatch => { + const currentVisibility = organization.projectVisibility; + dispatch(receiveOrganizations([{ ...organization, projectVisibility: visibility }])); + changeProjectVisibility(organization.key, visibility).catch(error => { + onFail(dispatch)(error); + dispatch(receiveOrganizations([{ ...organization, projectVisibility: currentVisibility }])); + }); +}; + +const mapDispatchToProps = (dispatch, ownProps) => ({ + onVisibilityChange: visibility => dispatch(onVisibilityChange(ownProps.organization, visibility)) +}); + +export default connect(mapStateToProps, mapDispatchToProps)(AppContainer); 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 new file mode 100644 index 00000000000..ae88b6317b7 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects-admin/ChangeVisibilityForm.js @@ -0,0 +1,114 @@ +/* + * 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 Modal from 'react-modal'; +import classNames from 'classnames'; +import { translate } from '../../helpers/l10n'; + +type Props = { + onClose: () => void, + onConfirm: string => void, + visibility: string +}; + +type State = { + visibility: string +}; + +export default class ChangeVisibilityForm extends React.PureComponent { + props: Props; + state: State; + + constructor(props: Props) { + super(props); + this.state = { visibility: props.visibility }; + } + + handleCancelClick = (event: Event) => { + event.preventDefault(); + this.props.onClose(); + }; + + handleConfirmClick = (event: Event) => { + event.preventDefault(); + this.props.onConfirm(this.state.visibility); + this.props.onClose(); + }; + + handleVisibilityClick = (visibility: string) => ( + event: Event & { currentTarget: HTMLElement } + ) => { + event.preventDefault(); + event.currentTarget.blur(); + this.setState({ visibility }); + }; + + render() { + return ( + + +
+

{translate('organization.change_visibility_form.header')}

+
+ +
+ {['public', 'private'].map(visibility => ( +
+

+ + + {translate('visibility', visibility)} + +

+

+ {translate('visibility', visibility, 'description.short')} +

+
+ ))} + +
+ {translate('organization.change_visibility_form.warning')} +
+
+ + + +
+ ); + } +} 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 e2bc02294d9..ba3ee626561 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 @@ -17,14 +17,27 @@ * 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 CreateView from './create-view'; -import BulkApplyTemplateView from './views/BulkApplyTemplateView'; +import ChangeVisibilityForm from './ChangeVisibilityForm'; +import { translate } from '../../helpers/l10n'; +import type { Organization } from '../../store/organizations/duck'; + +type Props = {| + hasProvisionPermission: boolean, + onVisibilityChange: string => void, + organization?: Organization, + refresh: () => void +|}; + +type State = { + visibilityForm: boolean +}; export default class Header extends React.PureComponent { - static propTypes = { - hasProvisionPermission: React.PropTypes.bool.isRequired - }; + props: Props; + state: State = { visibilityForm: false }; createProject() { new CreateView({ @@ -33,56 +46,59 @@ export default class Header extends React.PureComponent { }).render(); } - bulkApplyTemplate() { - new BulkApplyTemplateView({ - total: this.props.total, - selection: this.props.selection, - query: this.props.query, - qualifier: this.props.qualifier, - organization: this.props.organization - }).render(); - } + handleChangeVisibilityClick = (event: Event) => { + event.preventDefault(); + this.setState({ visibilityForm: true }); + }; + + closeVisiblityForm = () => { + this.setState({ visibilityForm: false }); + }; renderCreateButton() { if (!this.props.hasProvisionPermission) { return null; } return ( -
  • - -
  • - ); - } - - renderBulkApplyTemplateButton() { - return ( -
  • - -
  • + ); } render() { + const { organization } = this.props; + return (
    -

    Projects Management

    +

    {translate('projects_management')}

    -
      - {this.renderCreateButton()} - {this.renderBulkApplyTemplateButton()} -
    + {organization != null && + + {translate('organization.default_visibility_of_new_projects')} + {' '} + + {translate('visibility', organization.projectVisibility)} + + + } + {this.renderCreateButton()}

    - Use this page to delete multiple projects at once, or to provision projects - {' '} - if you would like to configure them before the first analysis. Note that once - {' '} - a project is provisioned, you have access to perform all project configurations on it. + {translate('projects_management.page.description')}

    + + {this.state.visibilityForm && + organization != null && + }
    ); } diff --git a/server/sonar-web/src/main/js/apps/projects-admin/main.js b/server/sonar-web/src/main/js/apps/projects-admin/main.js index 3079aae3c63..60942300cf4 100644 --- a/server/sonar-web/src/main/js/apps/projects-admin/main.js +++ b/server/sonar-web/src/main/js/apps/projects-admin/main.js @@ -17,6 +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. */ +// @flow import React from 'react'; import { debounce, uniq, without } from 'lodash'; import Header from './header'; @@ -25,14 +26,30 @@ import Projects from './projects'; import { PAGE_SIZE, TYPE } from './constants'; import { getComponents, getProvisioned, getGhosts, deleteComponents } from '../../api/components'; import ListFooter from '../../components/controls/ListFooter'; +import type { Organization } from '../../store/organizations/duck'; + +type Props = {| + hasProvisionPermission: boolean, + onVisibilityChange: string => void, + organization?: Organization +|}; + +type State = { + ready: boolean, + projects: Array<{ key: string }>, + total: number, + page: number, + query: string, + qualifiers: string, + type: string, + selection: Array +}; export default class Main extends React.PureComponent { - static propTypes = { - hasProvisionPermission: React.PropTypes.bool.isRequired, - organization: React.PropTypes.object - }; + props: Props; + state: State; - constructor(props) { + constructor(props: Props) { super(props); this.state = { ready: false, @@ -52,7 +69,7 @@ export default class Main extends React.PureComponent { } getFilters = () => { - const filters = { ps: PAGE_SIZE }; + const filters: { [string]: string | number } = { ps: PAGE_SIZE }; if (this.state.page !== 1) { filters.p = this.state.page; } @@ -128,7 +145,7 @@ export default class Main extends React.PureComponent { this.setState({ ready: false, page: this.state.page + 1 }, this.requestProjects); }; - onSearch = query => { + onSearch = (query: string) => { this.setState( { ready: false, @@ -140,7 +157,7 @@ export default class Main extends React.PureComponent { ); }; - onTypeChanged = newType => { + onTypeChanged = (newType: string) => { this.setState( { ready: false, @@ -154,7 +171,7 @@ export default class Main extends React.PureComponent { ); }; - onQualifierChanged = newQualifier => { + onQualifierChanged = (newQualifier: string) => { this.setState( { ready: false, @@ -168,12 +185,12 @@ export default class Main extends React.PureComponent { ); }; - onProjectSelected = project => { + onProjectSelected = (project: { key: string }) => { const newSelection = uniq([].concat(this.state.selection, project.key)); this.setState({ selection: newSelection }); }; - onProjectDeselected = project => { + onProjectDeselected = (project: { key: string }) => { const newSelection = without(this.state.selection, project.key); this.setState({ selection: newSelection }); }; @@ -204,11 +221,8 @@ export default class Main extends React.PureComponent {
    diff --git a/server/sonar-web/src/main/js/apps/projects-admin/search.js b/server/sonar-web/src/main/js/apps/projects-admin/search.js index 2e7d3def391..65e6e0fb9d3 100644 --- a/server/sonar-web/src/main/js/apps/projects-admin/search.js +++ b/server/sonar-web/src/main/js/apps/projects-admin/search.js @@ -21,6 +21,7 @@ import React from 'react'; import { sortBy } from 'lodash'; import { TYPE, QUALIFIERS_ORDER } from './constants'; import DeleteView from './delete-view'; +import BulkApplyTemplateView from './views/BulkApplyTemplateView'; import RadioToggle from '../../components/controls/RadioToggle'; import Checkbox from '../../components/controls/Checkbox'; import { translate } from '../../helpers/l10n'; @@ -69,6 +70,16 @@ export default class Search extends React.PureComponent { }).render(); }; + bulkApplyTemplate = () => { + new BulkApplyTemplateView({ + total: this.props.total, + selection: this.props.selection, + query: this.props.query, + qualifier: this.props.qualifier, + organization: this.props.organization + }).render(); + }; + renderCheckbox = () => { const isAllChecked = this.props.projects.length > 0 && this.props.selection.length === this.props.projects.length; @@ -144,12 +155,15 @@ export default class Search extends React.PureComponent { /> - + + 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 89cb815c7a6..9bc74a7550b 100644 --- a/server/sonar-web/src/main/js/store/organizations/duck.js +++ b/server/sonar-web/src/main/js/store/organizations/duck.js @@ -31,6 +31,7 @@ export type Organization = { key: string, name: string, pages?: Array<{ key: string, name: string }>, + projectVisibility: string, url?: string }; diff --git a/server/sonar-web/src/main/less/components/tooltips.less b/server/sonar-web/src/main/less/components/tooltips.less index eb225f10112..a0a073940c5 100644 --- a/server/sonar-web/src/main/less/components/tooltips.less +++ b/server/sonar-web/src/main/less/components/tooltips.less @@ -78,6 +78,11 @@ border-radius: 4px; letter-spacing: 0.04em; overflow: hidden; + + .alert { + margin-bottom: 5px /* align with side padding */ ; + border-radius: 4px; + } } .tooltip-arrow, 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 5b5e056dfd3..abc8eb8f5c5 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -531,6 +531,7 @@ project_history.page.description=Edit snapshot metadata, or delete snapshots fro project_roles.page=Project Permissions project_roles.page.description=Grant and revoke permissions to this project to Browse (view a project's metrics), See Source Code, and Administer. Permissions can be granted to groups or individual users. project_roles.page.description2=Grant and revoke project-level permissions. Permissions can be granted to groups or individual users. +projects_management.page.description=Use this page to delete multiple projects at once, or to provision projects if you would like to configure them before the first analysis. Note that once a project is provisioned, you have access to perform all project configurations on it. settings.page=General Settings settings.page.description=Edit global settings for this SonarQube instance. system_info.page=System Info @@ -2424,8 +2425,10 @@ projects_role.issueadmin=Administer Issues projects_role.issueadmin.desc=Perform advanced editing on issues: marking an issue False Positive / Won't Fix, and changing an Issue's severity. (Users will also need "Browse" permission) projects_role.user=Browse projects_role.user.desc=Access a project, browse its measures, and create/edit issues for it. +projects_role.user.public_projects_warning=This option is not editable for public projects. Anyone will still be able to browse even if you apply a template with this option unchecked. projects_role.codeviewer=See Source Code projects_role.codeviewer.desc=View the project's source code. (Users will also need "Browse" permission) +projects_role.codeviewer.public_projects_warning=This option is not editable for public projects. Source code will will always be visible even if you apply a template with this option unchecked. projects_role.scan=Execute Analysis projects_role.scan.desc=Ability to get all settings required to perform an analysis (including the secured settings like passwords) and to push analysis results to the SonarQube server. projects_role.bulk_change=Bulk Change @@ -2466,6 +2469,7 @@ permission_template.default_for=Default for {0} permission_templates.project_creators=Project Creators permission_templates.project_creators.explanation=When a new project is created, the user who creates the project will receive this permission on the project. permission_templates.grant_permission_to_project_creators=Grant the "{0}" permission to project creators +permission_templates.bulk_apply_permission_template=Bulk Apply Permission Template #------------------------------------------------------------------------------ @@ -2860,3 +2864,7 @@ organization.members.remove_x=Are you sure you want to remove {0} from {1}'s mem organization.members.manage_groups=Manage groups organization.members.members_groups={0}'s groups: organization.members.add_to_members=Add to members +organization.default_visibility_of_new_projects=Default visibility of new projects: +organization.change_visibility_form.header=Set Default Visibility of New Projects +organization.change_visibility_form.warning=This will not change the visibility of already existing projects. +organization.change_visibility_form.submit=Change Default Visibility -- 2.39.5