diff options
author | Stas Vilchik <vilchiks@gmail.com> | 2017-05-01 16:57:55 +0200 |
---|---|---|
committer | Stas Vilchik <stas-vilchik@users.noreply.github.com> | 2017-05-02 14:45:47 +0200 |
commit | 2d9e5533515dd5969897ba1ad3435fea27da28f0 (patch) | |
tree | 71c6a4e9919e9ebbd8ace505ba751331148767d2 | |
parent | 19f9ad34bdca23ae4cfcc0bd601581f7c9c5442e (diff) | |
download | sonarqube-2d9e5533515dd5969897ba1ad3435fea27da28f0.tar.gz sonarqube-2d9e5533515dd5969897ba1ad3435fea27da28f0.zip |
SONAR-9166 Allow to change default project visibility for organization
12 files changed, 275 insertions, 71 deletions
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} /> - <TooltipsContainer> - <List - organization={this.props.organization} - permissionTemplates={this.props.permissionTemplates} - permissions={this.props.permissions} - topQualifiers={this.props.topQualifiers} - refresh={this.props.refresh} - /> - </TooltipsContainer> + <List + organization={this.props.organization} + permissionTemplates={this.props.permissionTemplates} + permissions={this.props.permissions} + topQualifiers={this.props.topQualifiers} + refresh={this.props.refresh} + /> </div> ); } 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 ( <table id="permission-templates" className="data zebra permissions-table"> - <ListHeader permissions={this.props.permissions} /> + <ListHeader organization={this.props.organization} permissions={this.props.permissions} /> <tbody>{permissionTemplates}</tbody> </table> ); 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') + ? <div> + {permission.description} + <div className="alert alert-warning spacer-top"> + {translate('projects_role', permission.key, 'public_projects_warning')} + </div> + </div> + : permission.description); + render() { - const cells = this.props.permissions.map(p => ( - <th key={p.key} className="permission-column"> - {p.name} - <i className="icon-help little-spacer-left" title={p.description} data-toggle="tooltip" /> + const cells = this.props.permissions.map(permission => ( + <th key={permission.key} className="permission-column"> + {permission.name} + <Tooltip overlay={this.renderTooltip(permission)}> + <i className="icon-help little-spacer-left" /> + </Tooltip> </th> )); 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) { <Main hasProvisionPermission={hasProvisionPermission} topLevelQualifiers={topLevelQualifiers} + onVisibilityChange={props.onVisibilityChange} organization={props.organization} /> ); @@ -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 ( + <Modal + isOpen={true} + contentLabel="modal form" + className="modal" + overlayClassName="modal-overlay" + onRequestClose={this.props.onClose}> + + <header className="modal-head"> + <h2>{translate('organization.change_visibility_form.header')}</h2> + </header> + + <div className="modal-body"> + {['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> + </p> + <p className="text-muted spacer-top" style={{ paddingLeft: 22 }}> + {translate('visibility', visibility, 'description.short')} + </p> + </div> + ))} + + <div className="alert alert-warning"> + {translate('organization.change_visibility_form.warning')} + </div> + </div> + + <footer className="modal-foot"> + <button onClick={this.handleConfirmClick}> + {translate('organization.change_visibility_form.submit')} + </button> + <a href="#" onClick={this.handleCancelClick}>{translate('cancel')}</a> + </footer> + + </Modal> + ); + } +} 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 ( - <li> - <button onClick={this.createProject.bind(this)}> - Create Project - </button> - </li> - ); - } - - renderBulkApplyTemplateButton() { - return ( - <li> - <button onClick={this.bulkApplyTemplate.bind(this)}> - Bulk Apply Permission Template - </button> - </li> + <button onClick={this.createProject.bind(this)}> + Create Project + </button> ); } render() { + const { organization } = this.props; + return ( <header className="page-header"> - <h1 className="page-title">Projects Management</h1> + <h1 className="page-title">{translate('projects_management')}</h1> <div className="page-actions"> - <ul className="list-inline"> - {this.renderCreateButton()} - {this.renderBulkApplyTemplateButton()} - </ul> + {organization != null && + <span className="big-spacer-right"> + {translate('organization.default_visibility_of_new_projects')} + {' '} + <strong> + {translate('visibility', organization.projectVisibility)} + </strong> + <a + className="spacer-left icon-edit" + href="#" + onClick={this.handleChangeVisibilityClick} + /> + </span>} + {this.renderCreateButton()} </div> <p className="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. + {translate('projects_management.page.description')} </p> + + {this.state.visibilityForm && + organization != null && + <ChangeVisibilityForm + onClose={this.closeVisiblityForm} + onConfirm={this.props.onVisibilityChange} + visibility={organization.projectVisibility} + />} </header> ); } 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<string> +}; 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 { <div className="page page-limited"> <Header hasProvisionPermission={this.props.hasProvisionPermission} - selection={this.state.selection} - total={this.state.total} - query={this.state.query} - qualifier={this.state.qualifiers} refresh={this.requestProjects} + onVisibilityChange={this.props.onVisibilityChange} organization={this.props.organization} /> 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 { /> </form> </td> - <td className="thin text-middle"> + <td className="thin nowrap text-middle"> + <button className="spacer-right" onClick={this.bulkApplyTemplate}> + {translate('permission_templates.bulk_apply_permission_template')} + </button> <button onClick={this.deleteProjects} className="button-red" disabled={!isSomethingSelected}> - Delete + {translate('delete')} </button> </td> </tr> 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 |