diff options
author | Stas Vilchik <vilchiks@gmail.com> | 2016-06-09 14:49:05 +0200 |
---|---|---|
committer | Stas Vilchik <vilchiks@gmail.com> | 2016-06-09 14:49:05 +0200 |
commit | 9ddbcb00f8f3a6b6a85d4e1bf68d0edea0d45df5 (patch) | |
tree | 2e188bff86ed45b3a31b3c414b9862a90dc1d8e0 /server/sonar-web | |
parent | 6b35da5b5dc82f89d62109a2556ffdc520e7e08e (diff) | |
download | sonarqube-9ddbcb00f8f3a6b6a85d4e1bf68d0edea0d45df5.tar.gz sonarqube-9ddbcb00f8f3a6b6a85d4e1bf68d0edea0d45df5.zip |
refactor permission templates page (#1025)
Diffstat (limited to 'server/sonar-web')
33 files changed, 982 insertions, 624 deletions
diff --git a/server/sonar-web/package.json b/server/sonar-web/package.json index 98bc1baba05..3dd9cbad49b 100644 --- a/server/sonar-web/package.json +++ b/server/sonar-web/package.json @@ -59,6 +59,7 @@ "react-addons-shallow-compare": "15.0.1", "react-addons-test-utils": "15.0.1", "react-dom": "15.0.1", + "react-helmet": "3.1.0", "react-redux": "4.4.1", "react-router": "2.0.1", "react-router-redux": "4.0.2", diff --git a/server/sonar-web/src/main/js/api/permissions.js b/server/sonar-web/src/main/js/api/permissions.js index 03f723e56d1..4139c0f7c56 100644 --- a/server/sonar-web/src/main/js/api/permissions.js +++ b/server/sonar-web/src/main/js/api/permissions.js @@ -19,6 +19,7 @@ */ import $ from 'jquery'; import _ from 'underscore'; +import { getJSON, post } from '../helpers/request'; function request (options) { return $.ajax(options); @@ -102,13 +103,13 @@ export function revokeFromGroup (permission, group, project) { return request({ type: 'POST', url, data }); } -export function getPermissionTemplates (query) { +/** + * Get list of permission templates + * @returns {Promise} + */ +export function getPermissionTemplates () { const url = window.baseUrl + '/api/permissions/search_templates'; - const data = { }; - if (query) { - data.q = query; - } - return request({ type: 'GET', url, data }); + return getJSON(url); } export function createPermissionTemplate (options) { @@ -126,14 +127,16 @@ export function deletePermissionTemplate (options) { return request(_.extend({ type: 'POST', url }, options)); } -export function setDefaultPermissionTemplate (template, qualifier) { - if (typeof template !== 'string' || !template.length) { - return typeError('setDefaultPermissionTemplate', 'please provide permission template ID'); - } - +/** + * Set default permission template for a given qualifier + * @param {string} templateName + * @param {string} qualifier + * @returns {Promise} + */ +export function setDefaultPermissionTemplate (templateName, qualifier) { const url = window.baseUrl + '/api/permissions/set_default_template'; - const data = { templateId: template, qualifier }; - return request({ type: 'POST', url, data }); + const data = { templateName, qualifier }; + return post(url, data); } export function applyTemplateToProject (options) { diff --git a/server/sonar-web/src/main/js/apps/permission-templates/__tests__/permission-templates-test.js b/server/sonar-web/src/main/js/apps/permission-templates/__tests__/permission-templates-test.js deleted file mode 100644 index ade4d7b5872..00000000000 --- a/server/sonar-web/src/main/js/apps/permission-templates/__tests__/permission-templates-test.js +++ /dev/null @@ -1,114 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2016 SonarSource SA - * mailto:contact 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. - */ -/* eslint no-unused-expressions: 0 */ -import React from 'react'; -import TestUtils from 'react-addons-test-utils'; -import { expect } from 'chai'; -import sinon from 'sinon'; - -import Defaults from '../permission-template-defaults'; -import SetDefaults from '../permission-template-set-defaults'; - -describe('Permission Templates', function () { - describe('Defaults', () => { - it('should display one qualifier', () => { - const permissionTemplate = { defaultFor: ['VW'] }; - const topQualifiers = ['TRK', 'VW']; - const result = TestUtils.renderIntoDocument( - <Defaults permissionTemplate={permissionTemplate} topQualifiers={topQualifiers}/> - ); - expect(TestUtils.scryRenderedDOMComponentsWithClass(result, 'icon-qualifier-trk')).to.be.empty; - expect(TestUtils.scryRenderedDOMComponentsWithClass(result, 'icon-qualifier-vw')).to.have.length(1); - }); - - it('should display two qualifiers', () => { - const permissionTemplate = { defaultFor: ['TRK', 'VW'] }; - const topQualifiers = ['TRK', 'VW']; - const result = TestUtils.renderIntoDocument( - <Defaults permissionTemplate={permissionTemplate} topQualifiers={topQualifiers}/> - ); - expect(TestUtils.scryRenderedDOMComponentsWithClass(result, 'icon-qualifier-trk')).to.have.length(1); - expect(TestUtils.scryRenderedDOMComponentsWithClass(result, 'icon-qualifier-vw')).to.have.length(1); - }); - - it('should not display qualifiers', () => { - const permissionTemplate = { defaultFor: [] }; - const topQualifiers = ['TRK', 'VW']; - const result = TestUtils.renderIntoDocument( - <Defaults permissionTemplate={permissionTemplate} topQualifiers={topQualifiers}/> - ); - expect(TestUtils.scryRenderedDOMComponentsWithClass(result, 'icon-qualifier-trk')).to.be.empty; - expect(TestUtils.scryRenderedDOMComponentsWithClass(result, 'icon-qualifier-vw')).to.be.empty; - }); - - it('should omit "project" if there is only one qualifier', () => { - const permissionTemplate = { defaultFor: ['TRK'] }; - const topQualifiers = ['TRK']; - const result = TestUtils.renderIntoDocument( - <Defaults permissionTemplate={permissionTemplate} topQualifiers={topQualifiers}/> - ); - expect(TestUtils.scryRenderedDOMComponentsWithClass(result, 'icon-qualifier-trk')).to.be.empty; - }); - }); - - describe('SetDefaults', () => { - const refresh = sinon.spy(); - - it('should display a dropdown with one option', () => { - const permissionTemplate = { defaultFor: ['VW'] }; - const topQualifiers = ['TRK', 'VW']; - const result = TestUtils.renderIntoDocument( - <SetDefaults permissionTemplate={permissionTemplate} topQualifiers={topQualifiers} refresh={refresh}/> - ); - expect(TestUtils.scryRenderedDOMComponentsWithClass(result, 'dropdown')).to.have.length(1); - expect(TestUtils.scryRenderedDOMComponentsWithTag(result, 'a')).to.have.length(1); - }); - - it('should display a dropdown with two options', () => { - const permissionTemplate = { defaultFor: [] }; - const topQualifiers = ['TRK', 'VW']; - const result = TestUtils.renderIntoDocument( - <SetDefaults permissionTemplate={permissionTemplate} topQualifiers={topQualifiers} refresh={refresh}/> - ); - expect(TestUtils.scryRenderedDOMComponentsWithClass(result, 'dropdown')).to.have.length(1); - expect(TestUtils.scryRenderedDOMComponentsWithTag(result, 'a')).to.have.length(2); - }); - - it('should not display a dropdown', () => { - const permissionTemplate = { defaultFor: ['TRK', 'VW'] }; - const topQualifiers = ['TRK', 'VW']; - const result = TestUtils.renderIntoDocument( - <SetDefaults permissionTemplate={permissionTemplate} topQualifiers={topQualifiers} refresh={refresh}/> - ); - expect(TestUtils.scryRenderedDOMComponentsWithClass(result, 'dropdown')).to.be.empty; - expect(TestUtils.scryRenderedDOMComponentsWithTag(result, 'a')).to.be.empty; - }); - - it('should omit dropdown if there is only one qualifier', () => { - const permissionTemplate = { defaultFor: [] }; - const topQualifiers = ['TRK']; - const result = TestUtils.renderIntoDocument( - <SetDefaults permissionTemplate={permissionTemplate} topQualifiers={topQualifiers} refresh={refresh}/> - ); - expect(TestUtils.scryRenderedDOMComponentsWithClass(result, 'dropdown')).to.be.empty; - expect(TestUtils.scryRenderedDOMComponentsWithTag(result, 'a')).to.have.length(1); - }); - }); -}); diff --git a/server/sonar-web/src/main/js/apps/permission-templates/app.js b/server/sonar-web/src/main/js/apps/permission-templates/app.js index f257ec4a95c..d3747022dc1 100644 --- a/server/sonar-web/src/main/js/apps/permission-templates/app.js +++ b/server/sonar-web/src/main/js/apps/permission-templates/app.js @@ -18,10 +18,12 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; -import ReactDOM from 'react-dom'; -import Main from './main'; +import { render } from 'react-dom'; +import App from './components/App'; window.sonarqube.appStarted.then(options => { const el = document.querySelector(options.el); - ReactDOM.render(<Main topQualifiers={options.rootQualifiers}/>, el); + render(( + <App topQualifiers={options.rootQualifiers}/> + ), el); }); diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.js b/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.js new file mode 100644 index 00000000000..46a1e0e4f6c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.js @@ -0,0 +1,159 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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 React from 'react'; +import difference from 'lodash/difference'; +import { PermissionTemplateType, CallbackType } from '../propTypes'; +import QualifierIcon from '../../../components/shared/qualifier-icon'; +import { translate } from '../../../helpers/l10n'; +import { setDefaultPermissionTemplate } from '../../../api/permissions'; + +export default class ActionsCell extends React.Component { + static propTypes = { + permissionTemplate: PermissionTemplateType.isRequired, + topQualifiers: React.PropTypes.array.isRequired, + onUpdate: CallbackType, + onDelete: CallbackType, + refresh: CallbackType + }; + + handleUpdateClick (e) { + e.preventDefault(); + this.props.onUpdate(); + } + + handleDeleteClick (e) { + e.preventDefault(); + this.props.onDelete(); + } + + setDefault (qualifier, e) { + e.preventDefault(); + setDefaultPermissionTemplate( + this.props.permissionTemplate.name, + qualifier + ).then(this.props.refresh); + } + + getAvailableQualifiers () { + return difference( + this.props.topQualifiers, + this.props.permissionTemplate.defaultFor); + } + + renderDropdownIcon (icon) { + const style = { + display: 'inline-block', + width: 16, + marginRight: 4, + textAlign: 'center' + }; + return ( + <div style={style}>{icon}</div> + ); + } + + renderSetDefaultsControl () { + const availableQualifiers = this.getAvailableQualifiers(); + + if (availableQualifiers.length === 0) { + return null; + } + + return this.props.topQualifiers.length === 1 ? + this.renderIfSingleTopQualifier(availableQualifiers) : + this.renderIfMultipleTopQualifiers(availableQualifiers); + } + + renderSetDefaultLink (qualifier, child) { + return ( + <li key={qualifier}> + <a href="#" + className="js-set-default" + data-qualifier={qualifier} + onClick={this.setDefault.bind(this, qualifier)}> + {this.renderDropdownIcon(<i className="icon-check"/>)} + {child} + </a> + </li> + ); + } + + renderIfSingleTopQualifier (availableQualifiers) { + return availableQualifiers.map(qualifier => ( + this.renderSetDefaultLink(qualifier, ( + <span>{translate('permission_templates.set_default')}</span> + ))) + ); + } + + renderIfMultipleTopQualifiers (availableQualifiers) { + return availableQualifiers.map(qualifier => ( + this.renderSetDefaultLink(qualifier, ( + <span> + {translate('permission_templates.set_default_for')} + {' '} + <QualifierIcon qualifier={qualifier}/> + {' '} + {translate('qualifiers', qualifier)} + </span> + ))) + ); + } + + render () { + const { permissionTemplate: t } = this.props; + + return ( + <td className="actions-column"> + <div className="dropdown"> + <button className="dropdown-toggle" data-toggle="dropdown"> + {translate('actions')} + {' '} + <i className="icon-dropdown"></i> + </button> + + <ul className="dropdown-menu dropdown-menu-right"> + {this.renderSetDefaultsControl()} + + <li> + <a href="#" + className="js-update" + onClick={this.handleUpdateClick.bind(this)}> + {this.renderDropdownIcon(<i className="icon-edit"/>)} + {translate('update_verb')} + </a> + </li> + + {t.defaultFor.length === 0 && ( + <li> + <a href="#" + className="js-delete" + onClick={this.handleDeleteClick.bind(this)}> + {this.renderDropdownIcon(<i className="icon-delete"/>)} + {translate('delete')} + </a> + </li> + )} + </ul> + </div> + </td> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/App.js b/server/sonar-web/src/main/js/apps/permission-templates/components/App.js new file mode 100644 index 00000000000..bbb92be633c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permission-templates/components/App.js @@ -0,0 +1,92 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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 React from 'react'; +import Helmet from 'react-helmet'; +import Header from './Header'; +import List from './List'; +import { TooltipsContainer } from '../../../components/mixins/tooltips-mixin'; +import { getPermissionTemplates } from '../../../api/permissions'; +import { sortPermissions, mergePermissionsToTemplates, mergeDefaultsToTemplates } from '../utils'; +import { translate } from '../../../helpers/l10n'; +import '../styles.css'; + +export default class App extends React.Component { + static propTypes = { + topQualifiers: React.PropTypes.array.isRequired + }; + + state = { + ready: false, + permissions: [], + permissionTemplates: [] + }; + + componentWillMount () { + this.requestPermissions = this.requestPermissions.bind(this); + } + + componentDidMount () { + this.mounted = true; + this.requestPermissions(); + } + + componentWillUnmount () { + this.mounted = false; + } + + requestPermissions () { + getPermissionTemplates().then(r => { + if (this.mounted) { + const permissions = sortPermissions(r.permissions); + const permissionTemplates = mergeDefaultsToTemplates( + mergePermissionsToTemplates(r.permissionTemplates, permissions), + r.defaultTemplates + ); + this.setState({ + ready: true, + permissionTemplates, + permissions + }); + } + }); + } + + render () { + return ( + <div className="page page-limited"> + <Helmet + title={translate('permission_templates.page')} + titleTemplate="SonarQube - %s"/> + + <Header + ready={this.state.ready} + refresh={this.requestPermissions}/> + + <TooltipsContainer> + <List + permissionTemplates={this.state.permissionTemplates} + permissions={this.state.permissions} + topQualifiers={this.props.topQualifiers} + refresh={this.requestPermissions}/> + </TooltipsContainer> + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/Defaults.js b/server/sonar-web/src/main/js/apps/permission-templates/components/Defaults.js new file mode 100644 index 00000000000..35776605f97 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permission-templates/components/Defaults.js @@ -0,0 +1,43 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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 React from 'react'; +import sortBy from 'lodash/sortBy'; +import { translate } from '../../../helpers/l10n'; +import { PermissionTemplateType } from '../propTypes'; + +export default class Defaults extends React.Component { + static propTypes = { + permissionTemplate: PermissionTemplateType.isRequired + }; + + render () { + const qualifiers = sortBy(this.props.permissionTemplate.defaultFor) + .map(qualifier => translate('qualifiers', qualifier)) + .join(', '); + + return ( + <div> + <span className="badge spacer-right"> + {translate('default')} for {qualifiers} + </span> + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/permission-templates/header.js b/server/sonar-web/src/main/js/apps/permission-templates/components/Header.js index fb42ae354ef..b22b9387f3e 100644 --- a/server/sonar-web/src/main/js/apps/permission-templates/header.js +++ b/server/sonar-web/src/main/js/apps/permission-templates/components/Header.js @@ -18,34 +18,49 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; -import CreateView from './create-view'; -import { translate } from '../../helpers/l10n'; +import CreateView from '../views/CreateView'; +import { translate } from '../../../helpers/l10n'; +import { CallbackType } from '../propTypes'; -export default React.createClass({ - onCreate(e) { +export default class Header extends React.Component { + static propTypes = { + ready: React.PropTypes.bool.isRequired, + refresh: CallbackType + }; + + componentWillMount () { + this.handleCreateClick = this.handleCreateClick.bind(this); + } + + handleCreateClick (e) { e.preventDefault(); + new CreateView({ refresh: this.props.refresh }).render(); - }, - - renderSpinner () { - if (this.props.ready) { - return null; - } - return <i className="spinner"/>; - }, + } - render() { + render () { return ( <header id="project-permissions-header" className="page-header"> - <h1 className="page-title">{translate('permission_templates.page')}</h1> - {this.renderSpinner()} + <h1 className="page-title"> + {translate('permission_templates.page')} + </h1> + + {!this.props.ready && ( + <i className="spinner"/> + )} + <div className="page-actions"> - <button onClick={this.onCreate}>Create</button> + <button onClick={this.handleCreateClick}> + {translate('create')} + </button> </div> - <p className="page-description">{translate('roles.page.description')}</p> + + <p className="page-description"> + {translate('permission_templates.page.description')} + </p> </header> ); } -}); +} diff --git a/server/sonar-web/src/main/js/apps/permission-templates/permission-templates.js b/server/sonar-web/src/main/js/apps/permission-templates/components/List.js index 2a6aff49bd6..11ad4cd42a9 100644 --- a/server/sonar-web/src/main/js/apps/permission-templates/permission-templates.js +++ b/server/sonar-web/src/main/js/apps/permission-templates/components/List.js @@ -17,34 +17,35 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import classNames from 'classnames'; import React from 'react'; +import ListHeader from './ListHeader'; +import ListItem from './ListItem'; +import { PermissionTemplateType, CallbackType } from '../propTypes'; -import PermissionsHeader from './permissions-header'; -import PermissionTemplate from './permission-template'; - -export default React.createClass({ - propTypes: { - permissionTemplates: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, - permissions: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, +export default class List extends React.Component { + static propTypes = { + permissionTemplates: React.PropTypes.arrayOf( + PermissionTemplateType).isRequired, + permissions: React.PropTypes.array.isRequired, topQualifiers: React.PropTypes.array.isRequired, - refresh: React.PropTypes.func.isRequired - }, + refresh: CallbackType + }; + + render () { + const permissionTemplates = this.props.permissionTemplates.map(p => ( + <ListItem + key={p.id} + permissionTemplate={p} + topQualifiers={this.props.topQualifiers} + refresh={this.props.refresh}/> + )); - render() { - const permissionTemplates = this.props.permissionTemplates.map(p => { - return <PermissionTemplate - key={p.id} - permissionTemplate={p} - topQualifiers={this.props.topQualifiers} - refresh={this.props.refresh}/>; - }); - const className = classNames('data zebra', { 'new-loading': !this.props.ready }); return ( - <table id="permission-templates" className={className}> - <PermissionsHeader permissions={this.props.permissions}/> + <table id="permission-templates" + className="data zebra permissions-table"> + <ListHeader permissions={this.props.permissions}/> <tbody>{permissionTemplates}</tbody> </table> ); } -}); +} diff --git a/server/sonar-web/src/main/js/apps/permission-templates/permissions-header.js b/server/sonar-web/src/main/js/apps/permission-templates/components/ListHeader.js index a459d2f60c1..5a820ef0d38 100644 --- a/server/sonar-web/src/main/js/apps/permission-templates/permissions-header.js +++ b/server/sonar-web/src/main/js/apps/permission-templates/components/ListHeader.js @@ -19,28 +19,30 @@ */ import React from 'react'; -export default React.createClass({ - propTypes: { - permissions: React.PropTypes.arrayOf(React.PropTypes.object).isRequired - }, +export default class ListHeader extends React.Component { + static propTypes = { + permissions: React.PropTypes.array.isRequired + }; + + 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"/> + </th> + )); - render() { - const cellWidth = (80 / this.props.permissions.length) + '%'; - const cells = this.props.permissions.map(p => { - return ( - <th key={p.key} style={{ width: cellWidth }}> - {p.name}<br/><span className="small">{p.description}</span> - </th> - ); - }); return ( <thead> <tr> - <th style={{ width: '20%' }}> </th> + <th> </th> {cells} - <th className="thin"> </th> + <th className="actions-column"> </th> </tr> </thead> ); } -}); +} diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/ListItem.js b/server/sonar-web/src/main/js/apps/permission-templates/components/ListItem.js new file mode 100644 index 00000000000..0e659a14f47 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permission-templates/components/ListItem.js @@ -0,0 +1,101 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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 Backbone from 'backbone'; +import React from 'react'; +import NameCell from './NameCell'; +import PermissionCell from './PermissionCell'; +import ActionsCell from './ActionsCell'; +import UsersView from '../views/UsersView'; +import GroupsView from '../views/GroupsView'; +import UpdateView from '../views/UpdateView'; +import DeleteView from '../views/DeleteView'; +import { PermissionTemplateType, CallbackType } from '../propTypes'; + +export default class ListItem extends React.Component { + static propTypes = { + permissionTemplate: PermissionTemplateType.isRequired, + topQualifiers: React.PropTypes.array.isRequired, + refresh: CallbackType + }; + + componentWillMount () { + this.handleShowGroups = this.handleShowGroups.bind(this); + this.handleShowUsers = this.handleShowUsers.bind(this); + } + + handleShowGroups (permission) { + new GroupsView({ + permission, + permissionTemplate: this.props.permissionTemplate, + refresh: this.props.refresh + }).render(); + } + + handleShowUsers (permission) { + new UsersView({ + permission, + permissionTemplate: this.props.permissionTemplate, + refresh: this.props.refresh + }).render(); + } + + onUpdate () { + new UpdateView({ + model: new Backbone.Model(this.props.permissionTemplate), + refresh: this.props.refresh + }).render(); + } + + onDelete () { + new DeleteView({ + model: new Backbone.Model(this.props.permissionTemplate), + refresh: this.props.refresh + }).render(); + } + + render () { + const permissions = this.props.permissionTemplate.permissions.map(p => ( + <PermissionCell + key={p.key} + permission={p} + onShowUsers={this.handleShowUsers} + onShowGroups={this.handleShowGroups}/> + )); + + return ( + <tr + data-id={this.props.permissionTemplate.id} + data-name={this.props.permissionTemplate.name}> + <NameCell + permissionTemplate={this.props.permissionTemplate} + topQualifiers={this.props.topQualifiers}/> + + {permissions} + + <ActionsCell + permissionTemplate={this.props.permissionTemplate} + topQualifiers={this.props.topQualifiers} + onUpdate={this.onUpdate.bind(this)} + onDelete={this.onDelete.bind(this)} + refresh={this.props.refresh}/> + </tr> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/NameCell.js b/server/sonar-web/src/main/js/apps/permission-templates/components/NameCell.js new file mode 100644 index 00000000000..6591f1c7020 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permission-templates/components/NameCell.js @@ -0,0 +1,57 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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 React from 'react'; +import Defaults from './Defaults'; +import { PermissionTemplateType } from '../propTypes'; + +export default class NameCell extends React.Component { + static propTypes = { + permissionTemplate: PermissionTemplateType.isRequired, + topQualifiers: React.PropTypes.array.isRequired + }; + + render () { + const { permissionTemplate: t } = this.props; + + return ( + <td> + <strong className="js-name">{t.name}</strong> + + {t.defaultFor.length > 0 && ( + <div className="spacer-top js-defaults"> + <Defaults permissionTemplate={this.props.permissionTemplate}/> + </div> + )} + + {!!t.description && ( + <div className="spacer-top js-description"> + {t.description} + </div> + )} + + {!!t.projectKeyPattern && ( + <div className="spacer-top js-project-key-pattern"> + Project Key Pattern: <code>{t.projectKeyPattern}</code> + </div> + )} + </td> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/PermissionCell.js b/server/sonar-web/src/main/js/apps/permission-templates/components/PermissionCell.js new file mode 100644 index 00000000000..44bf5183445 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permission-templates/components/PermissionCell.js @@ -0,0 +1,86 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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 React from 'react'; +import { PermissionType, CallbackType } from '../propTypes'; + +export default class PermissionCell extends React.Component { + static propTypes = { + permission: PermissionType.isRequired, + onShowUsers: CallbackType, + onShowGroups: CallbackType + }; + + handleShowUsers (e) { + e.preventDefault(); + this.props.onShowUsers(this.props.permission); + } + + handleShowGroups (e) { + e.preventDefault(); + this.props.onShowGroups(this.props.permission); + } + + render () { + const { permission: p } = this.props; + + return ( + <td + className="permission-column" + data-permission={p.key}> + <table> + <tbody> + <tr> + <td className="spacer-right"> + Users + </td> + <td className="spacer-left bordered-left"> + {p.usersCount} + </td> + <td className="spacer-left"> + <a + onClick={this.handleShowUsers.bind(this)} + className="icon-bullet-list" + title="Update Users" + data-toggle="tooltip" + href="#"/> + </td> + </tr> + <tr> + <td className="spacer-right"> + Groups + </td> + <td className="spacer-left bordered-left"> + {p.groupsCount} + </td> + <td className="spacer-left"> + <a + onClick={this.handleShowGroups.bind(this)} + className="icon-bullet-list" + title="Update Users" + data-toggle="tooltip" + href="#"/> + </td> + </tr> + </tbody> + </table> + </td> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/ActionsCell-test.js b/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/ActionsCell-test.js new file mode 100644 index 00000000000..09f83fd29ce --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/ActionsCell-test.js @@ -0,0 +1,103 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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 chai, { expect } from 'chai'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import { shallow } from 'enzyme'; +import React from 'react'; +import ActionsCell from '../ActionsCell'; + +chai.use(sinonChai); + +const SAMPLE = { + id: 'id', + name: 'name', + permissions: [], + defaultFor: [] +}; + +function renderActionsCell (props) { + return shallow( + <ActionsCell + permissionTemplate={SAMPLE} + topQualifiers={['TRK', 'VW']} + onUpdate={() => true} + onDelete={() => true} + refresh={() => true} + {...props}/> + ); +} + +function simulateClick (element) { + element.simulate('click', { + preventDefault() {} + }); +} + +describe('Permission Templates :: ActionsCell', () => { + it('should update', () => { + const onUpdate = sinon.spy(); + const updateButton = renderActionsCell({ onUpdate }).find('.js-update'); + + expect(updateButton).have.length(1); + expect(onUpdate).to.not.have.been.called; + + simulateClick(updateButton); + + expect(onUpdate).to.have.been.called; + }); + + it('should delete', () => { + const onDelete = sinon.spy(); + const deleteButton = renderActionsCell({ onDelete }).find('.js-delete'); + + expect(deleteButton).have.length(1); + expect(onDelete).to.not.have.been.called; + + simulateClick(deleteButton); + + expect(onDelete).to.have.been.called; + }); + + it('should not delete', () => { + const permissionTemplate = { ...SAMPLE, defaultFor: ['VW'] }; + const deleteButton = renderActionsCell({ permissionTemplate }) + .find('.js-delete'); + + expect(deleteButton).to.have.length(0); + }); + + it('should set default', () => { + const setDefault = renderActionsCell() + .find('.js-set-default'); + + expect(setDefault).to.have.length(2); + expect(setDefault.at(0).prop('data-qualifier')).to.equal('TRK'); + expect(setDefault.at(1).prop('data-qualifier')).to.equal('VW'); + }); + + it('should not set default', () => { + const permissionTemplate = { ...SAMPLE, defaultFor: ['TRK', 'VW'] }; + const setDefault = renderActionsCell({ permissionTemplate }) + .find('.js-set-default'); + + expect(setDefault).to.have.length(0); + }); +}); diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/Defaults-test.js b/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/Defaults-test.js new file mode 100644 index 00000000000..c74885f8ad4 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/Defaults-test.js @@ -0,0 +1,43 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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 { expect } from 'chai'; +import { shallow } from 'enzyme'; +import React from 'react'; +import Defaults from '../Defaults'; + +const SAMPLE = { + id: 'id', + name: 'name', + permissions: [] +}; + +describe('Permission Templates :: Defaults', () => { + it('should render one qualifier', () => { + const sample = { ...SAMPLE, defaultFor: ['DEV'] }; + const output = shallow(<Defaults permissionTemplate={sample}/>); + expect(output.text()).to.contain('DEV'); + }); + + it('should render several qualifiers', () => { + const sample = { ...SAMPLE, defaultFor: ['TRK', 'VW'] }; + const output = shallow(<Defaults permissionTemplate={sample}/>); + expect(output.text()).to.contain('TRK'); + }); +}); diff --git a/server/sonar-web/src/main/js/apps/permission-templates/main.js b/server/sonar-web/src/main/js/apps/permission-templates/main.js deleted file mode 100644 index d20b26aabcf..00000000000 --- a/server/sonar-web/src/main/js/apps/permission-templates/main.js +++ /dev/null @@ -1,96 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2016 SonarSource SA - * mailto:contact 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 _ from 'underscore'; -import React from 'react'; -import Header from './header'; -import PermissionTemplates from './permission-templates'; -import { getPermissionTemplates } from '../../api/permissions'; - -const PERMISSIONS_ORDER = ['user', 'codeviewer', 'issueadmin', 'admin', 'scan']; - -export default React.createClass({ - propTypes: { - topQualifiers: React.PropTypes.array.isRequired - }, - - getInitialState() { - return { ready: false, permissions: [], permissionTemplates: [] }; - }, - - componentDidMount() { - this.requestPermissions(); - }, - - sortPermissions(permissions) { - return _.sortBy(permissions, p => PERMISSIONS_ORDER.indexOf(p.key)); - }, - - mergePermissionsToTemplates(permissionTemplates, basePermissions) { - return permissionTemplates.map(permissionTemplate => { - // it's important to keep the order of the permission template's permissions - // the same as the order of base permissions - const permissions = basePermissions.map(basePermission => { - const projectPermission = _.findWhere(permissionTemplate.permissions, { key: basePermission.key }); - return _.extend({ usersCount: 0, groupsCount: 0 }, basePermission, projectPermission); - }); - return _.extend({}, permissionTemplate, { permissions }); - }); - }, - - mergeDefaultsToTemplates(permissionTemplates, defaultTemplates = []) { - return permissionTemplates.map(permissionTemplate => { - const defaultFor = []; - defaultTemplates.forEach(defaultTemplate => { - if (defaultTemplate.templateId === permissionTemplate.id) { - defaultFor.push(defaultTemplate.qualifier); - } - }); - return _.extend({}, permissionTemplate, { defaultFor }); - }); - }, - - requestPermissions() { - getPermissionTemplates().done(r => { - const permissions = this.sortPermissions(r.permissions); - const permissionTemplates = this.mergePermissionsToTemplates(r.permissionTemplates, permissions); - const permissionTemplatesWithDefaults = this.mergeDefaultsToTemplates(permissionTemplates, r.defaultTemplates); - this.setState({ - ready: true, - permissionTemplates: permissionTemplatesWithDefaults, - permissions - }); - }); - }, - - render() { - return ( - <div className="page"> - <Header ready={this.state.ready} refresh={this.requestPermissions}/> - - <PermissionTemplates - ready={this.state.ready} - permissionTemplates={this.state.permissionTemplates} - permissions={this.state.permissions} - topQualifiers={this.props.topQualifiers} - refresh={this.requestPermissions}/> - </div> - ); - } -}); diff --git a/server/sonar-web/src/main/js/apps/permission-templates/permission-template-defaults.js b/server/sonar-web/src/main/js/apps/permission-templates/permission-template-defaults.js deleted file mode 100644 index 4ca60b3d4db..00000000000 --- a/server/sonar-web/src/main/js/apps/permission-templates/permission-template-defaults.js +++ /dev/null @@ -1,59 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2016 SonarSource SA - * mailto:contact 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 _ from 'underscore'; -import React from 'react'; -import QualifierIcon from '../../components/shared/qualifier-icon'; -import { translate } from '../../helpers/l10n'; - -export default React.createClass({ - propTypes: { - permissionTemplate: React.PropTypes.object.isRequired, - topQualifiers: React.PropTypes.array.isRequired - }, - - renderIfSingleTopQualifier() { - return ( - <ul className="list-inline nowrap spacer-bottom"> - <li>Default</li> - </ul> - ); - }, - - renderIfMultipleTopQualifiers() { - const defaults = this.props.permissionTemplate.defaultFor.map(qualifier => { - return <li key={qualifier}><QualifierIcon qualifier={qualifier}/> {translate('qualifier', qualifier)}</li>; - }); - return ( - <ul className="list-inline nowrap spacer-bottom"> - <li>Default for</li> - {defaults} - </ul> - ); - }, - - render() { - if (_.size(this.props.permissionTemplate.defaultFor) === 0) { - return null; - } - return this.props.topQualifiers.length === 1 ? - this.renderIfSingleTopQualifier() : - this.renderIfMultipleTopQualifiers(); - } -}); diff --git a/server/sonar-web/src/main/js/apps/permission-templates/permission-template-set-defaults.js b/server/sonar-web/src/main/js/apps/permission-templates/permission-template-set-defaults.js deleted file mode 100644 index 27be58a70e2..00000000000 --- a/server/sonar-web/src/main/js/apps/permission-templates/permission-template-set-defaults.js +++ /dev/null @@ -1,85 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2016 SonarSource SA - * mailto:contact 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 _ from 'underscore'; -import React from 'react'; -import { setDefaultPermissionTemplate } from '../../api/permissions'; -import QualifierIcon from '../../components/shared/qualifier-icon'; -import { translate } from '../../helpers/l10n'; - -export default React.createClass({ - propTypes: { - permissionTemplate: React.PropTypes.object.isRequired, - topQualifiers: React.PropTypes.array.isRequired, - refresh: React.PropTypes.func.isRequired - }, - - getAvailableQualifiers() { - return _.difference(this.props.topQualifiers, this.props.permissionTemplate.defaultFor); - }, - - setDefault(qualifier, e) { - e.preventDefault(); - setDefaultPermissionTemplate(this.props.permissionTemplate.id, qualifier).done(() => this.props.refresh()); - }, - - renderIfSingleTopQualifier(availableQualifiers) { - const qualifiers = availableQualifiers.map(qualifier => { - return ( - <span key={qualifier} className="text-middle"> - <a onClick={this.setDefault.bind(this, qualifier)} className="button" href="#">Set Default</a> - </span> - ); - }); - - return <span className="little-spacer-right">{qualifiers}</span>; - }, - - renderIfMultipleTopQualifiers(availableQualifiers) { - const qualifiers = availableQualifiers.map(qualifier => { - return ( - <li key={qualifier}> - <a onClick={this.setDefault.bind(this, qualifier)} href="#"> - Set Default for <QualifierIcon qualifier={qualifier}/> {translate('qualifier', qualifier)} - </a> - </li> - ); - }); - - return ( - <span className="dropdown little-spacer-right"> - <button className="dropdown-toggle" data-toggle="dropdown"> - Set Default <i className="icon-dropdown"></i> - </button> - <ul className="dropdown-menu">{qualifiers}</ul> - </span> - ); - }, - - render() { - const availableQualifiers = this.getAvailableQualifiers(); - if (availableQualifiers.length === 0) { - return null; - } - - return this.props.topQualifiers.length === 1 ? - this.renderIfSingleTopQualifier(availableQualifiers) : - this.renderIfMultipleTopQualifiers(availableQualifiers); - } -}); diff --git a/server/sonar-web/src/main/js/apps/permission-templates/permission-template.js b/server/sonar-web/src/main/js/apps/permission-templates/permission-template.js deleted file mode 100644 index c2a3e2cb9c3..00000000000 --- a/server/sonar-web/src/main/js/apps/permission-templates/permission-template.js +++ /dev/null @@ -1,140 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2016 SonarSource SA - * mailto:contact 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 _ from 'underscore'; -import Backbone from 'backbone'; -import React from 'react'; -import Defaults from './permission-template-defaults'; -import SetDefaults from './permission-template-set-defaults'; -import UsersView from './users-view'; -import GroupsView from './groups-view'; -import UpdateView from './update-view'; -import DeleteView from './delete-view'; - -export default React.createClass({ - propTypes: { - permissionTemplate: React.PropTypes.object.isRequired, - topQualifiers: React.PropTypes.array.isRequired, - refresh: React.PropTypes.func.isRequired - }, - - showGroups(permission, e) { - e.preventDefault(); - new GroupsView({ - permission, - permissionTemplate: this.props.permissionTemplate, - refresh: this.props.refresh - }).render(); - }, - - showUsers(permission, e) { - e.preventDefault(); - new UsersView({ - permission, - permissionTemplate: this.props.permissionTemplate, - refresh: this.props.refresh - }).render(); - }, - - onUpdate(e) { - e.preventDefault(); - new UpdateView({ - model: new Backbone.Model(this.props.permissionTemplate), - refresh: this.props.refresh - }).render(); - }, - - onDelete(e) { - e.preventDefault(); - new DeleteView({ - model: new Backbone.Model(this.props.permissionTemplate), - refresh: this.props.refresh - }).render(); - }, - - renderAssociation() { - const projectKeyPattern = this.props.permissionTemplate.projectKeyPattern; - if (!projectKeyPattern) { - return null; - } - return <div className="spacer-bottom">Project Key Pattern: <code>{projectKeyPattern}</code></div>; - }, - - renderDeleteButton() { - if (_.size(this.props.permissionTemplate.defaultFor) > 0) { - return null; - } - return <button onClick={this.onDelete} className="button-red">Delete</button>; - }, - - render() { - const permissions = this.props.permissionTemplate.permissions.map(p => { - return ( - <td key={p.key}> - <table> - <tbody> - <tr> - <td className="spacer-right">Users</td> - <td className="spacer-left bordered-left">{p.usersCount}</td> - <td className="spacer-left"> - <a onClick={this.showUsers.bind(this, p)} className="icon-bullet-list" title="Update Users" - data-toggle="tooltip" href="#"></a> - </td> - </tr> - <tr> - <td className="spacer-right">Groups</td> - <td className="spacer-left bordered-left">{p.groupsCount}</td> - <td className="spacer-left"> - <a onClick={this.showGroups.bind(this, p)} className="icon-bullet-list" title="Update Users" - data-toggle="tooltip" href="#"></a> - </td> - </tr> - </tbody> - </table> - </td> - ); - }); - return ( - <tr> - <td> - <strong>{this.props.permissionTemplate.name}</strong> - <p className="note little-spacer-top">{this.props.permissionTemplate.description}</p> - </td> - {permissions} - <td className="thin text-right"> - {this.renderAssociation()} - <Defaults - permissionTemplate={this.props.permissionTemplate} - topQualifiers={this.props.topQualifiers}/> - <div className="nowrap"> - <SetDefaults - permissionTemplate={this.props.permissionTemplate} - topQualifiers={this.props.topQualifiers} - refresh={this.props.refresh}/> - - <div className="button-group"> - <button onClick={this.onUpdate}>Update</button> - {this.renderDeleteButton()} - </div> - </div> - </td> - </tr> - ); - } -}); diff --git a/server/sonar-web/src/main/js/apps/permission-templates/propTypes.js b/server/sonar-web/src/main/js/apps/permission-templates/propTypes.js new file mode 100644 index 00000000000..5bc6119de09 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permission-templates/propTypes.js @@ -0,0 +1,40 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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 { PropTypes } from 'react'; + +const { shape, arrayOf, string, number, func } = PropTypes; + +export const PermissionType = shape({ + key: string.isRequired, + name: string.isRequired, + description: string.isRequired, + usersCount: number.isRequired, + groupsCount: number.isRequired +}); + +export const PermissionTemplateType = shape({ + id: string.isRequired, + name: string.isRequired, + description: string, + permissions: arrayOf(PermissionType).isRequired, + defaultFor: arrayOf(string).isRequired +}); + +export const CallbackType = func.isRequired; diff --git a/server/sonar-web/src/main/js/apps/permission-templates/styles.css b/server/sonar-web/src/main/js/apps/permission-templates/styles.css new file mode 100644 index 00000000000..a19119a6da3 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permission-templates/styles.css @@ -0,0 +1,17 @@ +.permissions-table { + table-layout: fixed; +} + +.permissions-table > tbody > tr > td { + border-bottom: 20px solid #fff !important; +} + +.permissions-table .permission-column { + width: 130px; + white-space: nowrap; +} + +.permissions-table .actions-column { + width: 130px; + text-align: right; +} diff --git a/server/sonar-web/src/main/js/apps/permission-templates/templates/permission-templates-form.hbs b/server/sonar-web/src/main/js/apps/permission-templates/templates/permission-templates-form.hbs index 3478f988bdd..02ef4004747 100644 --- a/server/sonar-web/src/main/js/apps/permission-templates/templates/permission-templates-form.hbs +++ b/server/sonar-web/src/main/js/apps/permission-templates/templates/permission-templates-form.hbs @@ -8,6 +8,9 @@ <div class="modal-field"> <label for="permission-template-name">Name<em class="mandatory">*</em></label> <input id="permission-template-name" name="name" type="text" maxlength="256" required value="{{name}}"> + <div class="modal-field-description"> + Should be unique. + </div> </div> <div class="modal-field"> @@ -19,6 +22,9 @@ <label for="permission-template-project-key-pattern">Project Key Pattern</label> <input id="permission-template-project-key-pattern" name="keyPattern" type="text" maxlength="500" value="{{projectKeyPattern}}"> + <div class="modal-field-description"> + Should be a valid regular expression. + </div> </div> </div> <div class="modal-foot"> diff --git a/server/sonar-web/src/main/js/apps/permission-templates/utils.js b/server/sonar-web/src/main/js/apps/permission-templates/utils.js new file mode 100644 index 00000000000..c475b39b3a7 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permission-templates/utils.js @@ -0,0 +1,71 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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 sortBy from 'lodash/sortBy'; + +export const PERMISSIONS_ORDER = ['user', 'codeviewer', 'issueadmin', 'admin', 'scan']; + +/** + * Sort list of permissions based on predefined order + * @param {Array} permissions + * @returns {Array} + */ +export function sortPermissions (permissions) { + return sortBy(permissions, p => PERMISSIONS_ORDER.indexOf(p.key)); +} + +/** + * Populate permissions' details in the list of permission templates + * @param {Array} permissionTemplates + * @param {Array} basePermissions + * @returns {Array} + */ +export function mergePermissionsToTemplates (permissionTemplates, basePermissions) { + return permissionTemplates.map(permissionTemplate => { + + // it's important to keep the order of the permission template's permissions + // the same as the order of base permissions + const permissions = basePermissions.map(basePermission => { + const projectPermission = permissionTemplate.permissions.find(p => p.key === basePermission.key); + return { usersCount: 0, groupsCount: 0, ...basePermission, ...projectPermission }; + }); + + return { ...permissionTemplate, permissions }; + }); +} + +/** + * Mark default templates + * @param {Array} permissionTemplates + * @param {Array} defaultTemplates + * @returns {Array} + */ +export function mergeDefaultsToTemplates (permissionTemplates, defaultTemplates = []) { + return permissionTemplates.map(permissionTemplate => { + const defaultFor = []; + + defaultTemplates.forEach(defaultTemplate => { + if (defaultTemplate.templateId === permissionTemplate.id) { + defaultFor.push(defaultTemplate.qualifier); + } + }); + + return { ...permissionTemplate, defaultFor }; + }); +} diff --git a/server/sonar-web/src/main/js/apps/permission-templates/create-view.js b/server/sonar-web/src/main/js/apps/permission-templates/views/CreateView.js index 9e35862878f..516373a0aa9 100644 --- a/server/sonar-web/src/main/js/apps/permission-templates/create-view.js +++ b/server/sonar-web/src/main/js/apps/permission-templates/views/CreateView.js @@ -17,8 +17,8 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import FormView from './form-view'; -import { createPermissionTemplate } from '../../api/permissions'; +import FormView from './FormView'; +import { createPermissionTemplate } from '../../../api/permissions'; export default FormView.extend({ sendRequest () { diff --git a/server/sonar-web/src/main/js/apps/permission-templates/delete-view.js b/server/sonar-web/src/main/js/apps/permission-templates/views/DeleteView.js index 7c7dfac5593..84b645e4971 100644 --- a/server/sonar-web/src/main/js/apps/permission-templates/delete-view.js +++ b/server/sonar-web/src/main/js/apps/permission-templates/views/DeleteView.js @@ -17,9 +17,9 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import ModalForm from '../../components/common/modal-form'; -import { deletePermissionTemplate } from '../../api/permissions'; -import Template from './templates/permission-templates-delete.hbs'; +import ModalForm from '../../../components/common/modal-form'; +import { deletePermissionTemplate } from '../../../api/permissions'; +import Template from '../templates/permission-templates-delete.hbs'; export default ModalForm.extend({ template: Template, diff --git a/server/sonar-web/src/main/js/apps/permission-templates/form-view.js b/server/sonar-web/src/main/js/apps/permission-templates/views/FormView.js index a94290e1604..40e77062e8e 100644 --- a/server/sonar-web/src/main/js/apps/permission-templates/form-view.js +++ b/server/sonar-web/src/main/js/apps/permission-templates/views/FormView.js @@ -17,8 +17,8 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import ModalForm from '../../components/common/modal-form'; -import Template from './templates/permission-templates-form.hbs'; +import ModalForm from '../../../components/common/modal-form'; +import Template from '../templates/permission-templates-form.hbs'; export default ModalForm.extend({ template: Template, diff --git a/server/sonar-web/src/main/js/apps/permission-templates/groups-view.js b/server/sonar-web/src/main/js/apps/permission-templates/views/GroupsView.js index 9c8f359aedc..56ad56e4a41 100644 --- a/server/sonar-web/src/main/js/apps/permission-templates/groups-view.js +++ b/server/sonar-web/src/main/js/apps/permission-templates/views/GroupsView.js @@ -18,9 +18,9 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import _ from 'underscore'; -import Modal from '../../components/common/modals'; -import '../../components/SelectList'; -import Template from './templates/permission-templates-groups.hbs'; +import Modal from '../../../components/common/modals'; +import Template from '../templates/permission-templates-groups.hbs'; +import '../../../components/SelectList'; function getSearchUrl (permission, permissionTemplate) { return window.baseUrl + diff --git a/server/sonar-web/src/main/js/apps/permission-templates/update-view.js b/server/sonar-web/src/main/js/apps/permission-templates/views/UpdateView.js index d738e9286e2..99acd983e5f 100644 --- a/server/sonar-web/src/main/js/apps/permission-templates/update-view.js +++ b/server/sonar-web/src/main/js/apps/permission-templates/views/UpdateView.js @@ -17,8 +17,8 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import FormView from './form-view'; -import { updatePermissionTemplate } from '../../api/permissions'; +import FormView from './FormView'; +import { updatePermissionTemplate } from '../../../api/permissions'; export default FormView.extend({ sendRequest () { diff --git a/server/sonar-web/src/main/js/apps/permission-templates/users-view.js b/server/sonar-web/src/main/js/apps/permission-templates/views/UsersView.js index b14fbdef464..c54b673c981 100644 --- a/server/sonar-web/src/main/js/apps/permission-templates/users-view.js +++ b/server/sonar-web/src/main/js/apps/permission-templates/views/UsersView.js @@ -18,9 +18,9 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import _ from 'underscore'; -import Modal from '../../components/common/modals'; -import '../../components/SelectList'; -import Template from './templates/permission-templates-users.hbs'; +import Modal from '../../../components/common/modals'; +import Template from '../templates/permission-templates-users.hbs'; +import '../../../components/SelectList'; export default Modal.extend({ template: Template, diff --git a/server/sonar-web/src/main/js/apps/project-permissions/main.js b/server/sonar-web/src/main/js/apps/project-permissions/main.js index 499cd2f649e..eb947f85776 100644 --- a/server/sonar-web/src/main/js/apps/project-permissions/main.js +++ b/server/sonar-web/src/main/js/apps/project-permissions/main.js @@ -26,6 +26,8 @@ import PermissionsFooter from './permissions-footer'; import Search from './search'; import ApplyTemplateView from './apply-template-view'; import { translate } from '../../helpers/l10n'; +import { TooltipsContainer } from '../../components/mixins/tooltips-mixin'; +import '../permission-templates/styles.css'; const PERMISSIONS_ORDER = ['user', 'codeviewer', 'issueadmin', 'admin', 'scan']; @@ -130,34 +132,38 @@ export default React.createClass({ render() { return ( - <div className="page"> - <header id="project-permissions-header" className="page-header"> - <h1 className="page-title">{translate('roles.page')}</h1> - {this.renderSpinner()} - <div className="page-actions"> - {this.renderBulkApplyButton()} - </div> - <p className="page-description">{translate('roles.page.description2')}</p> - </header> - - <Search {...this.props} - filter={this.state.filter} - search={this.search} - onFilter={this.handleFilter}/> - - <Permissions - ready={this.state.ready} - projects={this.state.projects} - permissions={this.state.permissions} - permissionTemplates={this.props.permissionTemplates} - refresh={this.refresh}/> - - <PermissionsFooter {...this.props} - ready={this.state.ready} - count={this.state.projects.length} - total={this.state.total} - loadMore={this.loadMore}/> - </div> + <TooltipsContainer> + <div className="page"> + <header id="project-permissions-header" className="page-header"> + <h1 className="page-title">{translate('roles.page')}</h1> + {this.renderSpinner()} + <div className="page-actions"> + {this.renderBulkApplyButton()} + </div> + <p className="page-description"> + {translate('roles.page.description2')} + </p> + </header> + + <Search {...this.props} + filter={this.state.filter} + search={this.search} + onFilter={this.handleFilter}/> + + <Permissions + ready={this.state.ready} + projects={this.state.projects} + permissions={this.state.permissions} + permissionTemplates={this.props.permissionTemplates} + refresh={this.refresh}/> + + <PermissionsFooter {...this.props} + ready={this.state.ready} + count={this.state.projects.length} + total={this.state.total} + loadMore={this.loadMore}/> + </div> + </TooltipsContainer> ); } }); diff --git a/server/sonar-web/src/main/js/apps/project-permissions/permissions-header.js b/server/sonar-web/src/main/js/apps/project-permissions/permissions-header.js index a459d2f60c1..281616aaea9 100644 --- a/server/sonar-web/src/main/js/apps/project-permissions/permissions-header.js +++ b/server/sonar-web/src/main/js/apps/project-permissions/permissions-header.js @@ -25,21 +25,23 @@ export default React.createClass({ }, render() { - const cellWidth = (80 / this.props.permissions.length) + '%'; - const cells = this.props.permissions.map(p => { - return ( - <th key={p.key} style={{ width: cellWidth }}> - {p.name}<br/><span className="small">{p.description}</span> - </th> - ); - }); + 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"/> + </th> + )); + return ( <thead> - <tr> - <th style={{ width: '20%' }}> </th> - {cells} - <th className="thin"> </th> - </tr> + <tr> + <th> </th> + {cells} + <th className="actions-column"> </th> + </tr> </thead> ); } diff --git a/server/sonar-web/src/main/js/apps/project-permissions/permissions.js b/server/sonar-web/src/main/js/apps/project-permissions/permissions.js index 16789fce35d..c10323e8419 100644 --- a/server/sonar-web/src/main/js/apps/project-permissions/permissions.js +++ b/server/sonar-web/src/main/js/apps/project-permissions/permissions.js @@ -39,7 +39,9 @@ export default React.createClass({ permissionTemplates={this.props.permissionTemplates} refresh={this.props.refresh}/>; }); - const className = classNames('data zebra', { 'new-loading': !this.props.ready }); + const className = classNames( + 'data zebra permissions-table', + { 'new-loading': !this.props.ready }); return ( <table id="projects" className={className}> <PermissionsHeader permissions={this.props.permissions}/> diff --git a/server/sonar-web/src/main/less/init/icons.less b/server/sonar-web/src/main/less/init/icons.less index 375d4170c05..190fcba48c2 100644 --- a/server/sonar-web/src/main/less/init/icons.less +++ b/server/sonar-web/src/main/less/init/icons.less @@ -548,7 +548,7 @@ a[class^="icon-"], a[class*=" icon-"] { } .icon-edit:before { content: "\f040"; - font-size: @iconFontSize; + font-size: @iconSmallFontSize; } .icon-ellipsis-h:before { position: relative; |