diff options
author | Stas Vilchik <vilchiks@gmail.com> | 2016-06-29 09:52:31 +0200 |
---|---|---|
committer | Stas Vilchik <vilchiks@gmail.com> | 2016-07-12 10:18:55 +0200 |
commit | d5ca0eb5782c29c613a53b76cfe169dbe4ceab81 (patch) | |
tree | b69d3c2999251dc78f6770332a8687a2e027cfee /server/sonar-web | |
parent | 0fbbe800ee3ae1f68df6e5d4c868a2910b981a55 (diff) | |
download | sonarqube-d5ca0eb5782c29c613a53b76cfe169dbe4ceab81.tar.gz sonarqube-d5ca0eb5782c29c613a53b76cfe169dbe4ceab81.zip |
SONAR-7840 SONAR-7879 Improve UX on permissions pages
Diffstat (limited to 'server/sonar-web')
61 files changed, 1867 insertions, 1371 deletions
diff --git a/server/sonar-web/src/main/js/api/permissions.js b/server/sonar-web/src/main/js/api/permissions.js index 273c7872735..80a77e56539 100644 --- a/server/sonar-web/src/main/js/api/permissions.js +++ b/server/sonar-web/src/main/js/api/permissions.js @@ -21,86 +21,56 @@ import $ from 'jquery'; import _ from 'underscore'; import { getJSON, post } from '../helpers/request'; +const PAGE_SIZE = 100; + function request (options) { return $.ajax(options); } -function typeError (method, message) { - throw new TypeError(`permissions#${method}: ${message}`); -} - -export function getUsers (data) { - const url = window.baseUrl + '/api/permissions/users'; - return request({ type: 'GET', url, data }); +export function getPermissionUsers (data) { + const url = '/api/permissions/users'; + return getJSON(url, data); } -export function grantToUser (permission, user, project) { - if (typeof permission !== 'string' || !permission.length) { - return typeError('grantToUser', 'please provide permission'); - } - if (typeof user !== 'string' || !user.length) { - return typeError('grantToUser', 'please provide user login'); - } - - const url = window.baseUrl + '/api/permissions/add_user'; - const data = { permission, login: user }; - if (project) { - data.projectId = project; +export function grantPermissionToUser (projectKey, login, permission) { + const url = '/api/permissions/add_user'; + const data = { login, permission }; + if (projectKey) { + data.projectKey = projectKey; } - return request({ type: 'POST', url, data }); + return post(url, data); } -export function revokeFromUser (permission, user, project) { - if (typeof permission !== 'string' || !permission.length) { - return typeError('revokeFromUser', 'please provide permission'); - } - if (typeof user !== 'string' || !user.length) { - return typeError('revokeFromUser', 'please provide user login'); - } - - const url = window.baseUrl + '/api/permissions/remove_user'; - const data = { permission, login: user }; - if (project) { - data.projectId = project; +export function revokePermissionFromUser (projectKey, login, permission) { + const url = '/api/permissions/remove_user'; + const data = { login, permission }; + if (projectKey) { + data.projectKey = projectKey; } - return request({ type: 'POST', url, data }); + return post(url, data); } -export function getGroups (data) { - const url = window.baseUrl + '/api/permissions/groups'; - return request({ type: 'GET', url, data }); +export function getPermissionGroups (data) { + const url = '/api/permissions/groups'; + return getJSON(url, data); } -export function grantToGroup (permission, group, project) { - if (typeof permission !== 'string' || !permission.length) { - return typeError('grantToGroup', 'please provide permission'); - } - if (typeof group !== 'string' || !group.length) { - return typeError('grantToGroup', 'please provide group name'); +export function grantPermissionToGroup (projectKey, groupName, permission) { + const url = '/api/permissions/add_group'; + const data = { groupName, permission }; + if (projectKey) { + data.projectKey = projectKey; } - - const url = window.baseUrl + '/api/permissions/add_group'; - const data = { permission, groupName: group }; - if (project) { - data.projectId = project; - } - return request({ type: 'POST', url, data }); + return post(url, data); } -export function revokeFromGroup (permission, group, project) { - if (typeof permission !== 'string' || !permission.length) { - return typeError('revokeFromGroup', 'please provide permission'); +export function revokePermissionFromGroup (projectKey, groupName, permission) { + const url = '/api/permissions/remove_group'; + const data = { groupName, permission }; + if (projectKey) { + data.projectKey = projectKey; } - if (typeof group !== 'string' || !group.length) { - return typeError('revokeFromGroup', 'please provide group name'); - } - - const url = window.baseUrl + '/api/permissions/remove_group'; - const data = { permission, groupName: group }; - if (project) { - data.projectId = project; - } - return request({ type: 'POST', url, data }); + return post(url, data); } /** @@ -139,9 +109,9 @@ export function setDefaultPermissionTemplate (templateName, qualifier) { return post(url, data); } -export function applyTemplateToProject (options) { - const url = window.baseUrl + '/api/permissions/apply_template'; - return request(_.extend({ type: 'POST', url }, options)); +export function applyTemplateToProject (data) { + const url = '/api/permissions/apply_template'; + return post(url, data); } export function bulkApplyTemplateToProject (options) { @@ -160,3 +130,51 @@ export function removeProjectCreatorFromTemplate (templateName, permission) { const data = { templateName, permission }; return post(url, data); } + +export function getPermissionsUsersForComponent (projectKey, query = '', permission = null) { + const url = '/api/permissions/users'; + const data = { projectKey, ps: PAGE_SIZE }; + if (query) { + data.q = query; + } + if (permission) { + data.permission = permission; + } + return getJSON(url, data).then(r => r.users); +} + +export function getPermissionsGroupsForComponent (projectKey, query = '', permission = null) { + const url = '/api/permissions/groups'; + const data = { projectKey, ps: PAGE_SIZE }; + if (query) { + data.q = query; + } + if (permission) { + data.permission = permission; + } + return getJSON(url, data).then(r => r.groups); +} + +export function getGlobalPermissionsUsers (query = '', permission = null) { + const url = '/api/permissions/users'; + const data = { ps: PAGE_SIZE }; + if (query) { + data.q = query; + } + if (permission) { + data.permission = permission; + } + return getJSON(url, data).then(r => r.users); +} + +export function getGlobalPermissionsGroups (query = '', permission = null) { + const url = '/api/permissions/groups'; + const data = { ps: PAGE_SIZE }; + if (query) { + data.q = query; + } + if (permission) { + data.permission = permission; + } + return getJSON(url, data).then(r => r.groups); +} diff --git a/server/sonar-web/src/main/js/apps/global-permissions/groups-view.js b/server/sonar-web/src/main/js/apps/global-permissions/groups-view.js deleted file mode 100644 index dac2ad2d4f9..00000000000 --- a/server/sonar-web/src/main/js/apps/global-permissions/groups-view.js +++ /dev/null @@ -1,72 +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 Modal from '../../components/common/modals'; -import Template from './templates/global-permissions-groups.hbs'; -import '../../components/SelectList'; - -function getSearchUrl (permission, project) { - let url = window.baseUrl + '/api/permissions/groups?ps=100&permission=' + permission; - if (project) { - url = url + '&projectId=' + project; - } - return url; -} - -function getExtra (permission, project) { - const extra = { permission }; - if (project) { - extra.projectId = project; - } - return extra; -} - -export default Modal.extend({ - template: Template, - - onRender () { - Modal.prototype.onRender.apply(this, arguments); - new window.SelectList({ - el: this.$('#global-permissions-groups'), - width: '100%', - readOnly: false, - focusSearch: false, - format (item) { - return item.name; - }, - queryParam: 'q', - searchUrl: getSearchUrl(this.options.permission, this.options.project), - selectUrl: window.baseUrl + '/api/permissions/add_group', - deselectUrl: window.baseUrl + '/api/permissions/remove_group', - extra: getExtra(this.options.permission, this.options.project), - selectParameter: 'groupName', - selectParameterValue: 'name', - parse (r) { - this.more = false; - return r.groups; - } - }); - }, - - onDestroy () { - this.options.refresh(); - Modal.prototype.onDestroy.apply(this, arguments); - } -}); - diff --git a/server/sonar-web/src/main/js/apps/global-permissions/main.js b/server/sonar-web/src/main/js/apps/global-permissions/main.js deleted file mode 100644 index 85571020b10..00000000000 --- a/server/sonar-web/src/main/js/apps/global-permissions/main.js +++ /dev/null @@ -1,60 +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 'jquery'; -import React from 'react'; -import PermissionsList from './permissions-list'; -import { translate } from '../../helpers/l10n'; - -export default React.createClass({ - getInitialState() { - return { ready: false, permissions: [] }; - }, - - componentDidMount() { - this.requestPermissions(); - }, - - requestPermissions() { - const url = window.baseUrl + '/api/permissions/search_global_permissions'; - $.get(url).done(r => { - this.setState({ ready: true, permissions: r.permissions }); - }); - }, - - renderSpinner () { - if (this.state.ready) { - return null; - } - return <i className="spinner"/>; - }, - - render() { - return ( - <div className="page"> - <header id="global-permissions-header" className="page-header"> - <h1 className="page-title">{translate('global_permissions.page')}</h1> - {this.renderSpinner()} - <p className="page-description">{translate('global_permissions.page.description')}</p> - </header> - <PermissionsList ready={this.state.ready} permissions={this.state.permissions}/> - </div> - ); - } -}); diff --git a/server/sonar-web/src/main/js/apps/global-permissions/permission-groups.js b/server/sonar-web/src/main/js/apps/global-permissions/permission-groups.js deleted file mode 100644 index 38b08fb68c5..00000000000 --- a/server/sonar-web/src/main/js/apps/global-permissions/permission-groups.js +++ /dev/null @@ -1,53 +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 React from 'react'; -import PermissionsUsersGroupsMixin from './permission-users-groups-mixin'; -import GroupsView from './groups-view'; - -export default React.createClass({ - mixins: [PermissionsUsersGroupsMixin], - - renderUpdateLink() { - return ( - <a onClick={this.updateGroups} - className="icon-bullet-list" - title="Update Groups" - data-toggle="tooltip" - href="#"></a> - ); - }, - - renderItem(item) { - return item.name; - }, - - renderTitle() { - return 'Groups'; - }, - - updateGroups(e) { - e.preventDefault(); - new GroupsView({ - permission: this.props.permission.key, - project: this.props.project, - refresh: this.props.refresh - }).render(); - } -}); diff --git a/server/sonar-web/src/main/js/apps/global-permissions/permission-users-groups-mixin.js b/server/sonar-web/src/main/js/apps/global-permissions/permission-users-groups-mixin.js deleted file mode 100644 index a83ec2c14d0..00000000000 --- a/server/sonar-web/src/main/js/apps/global-permissions/permission-users-groups-mixin.js +++ /dev/null @@ -1,73 +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 React from 'react'; - -export default { - propTypes: { - permission: React.PropTypes.object.isRequired, - max: React.PropTypes.number.isRequired, - items: React.PropTypes.array, - total: React.PropTypes.number, - refresh: React.PropTypes.func.isRequired - }, - - renderNotDisplayed() { - const notDisplayedCount = this.props.total - this.props.max; - return notDisplayedCount > 0 ? - <span className="note spacer-right" href="#">and {notDisplayedCount} more</span> : null; - }, - - renderItems() { - const displayed = this.props.items.map(item => { - return <li key={item.name} className="spacer-left little-spacer-bottom">{this.renderItem(item)}</li>; - }); - return ( - <ul className="overflow-hidden bordered-left"> - {displayed} - <li className="spacer-left little-spacer-bottom"> - {this.renderNotDisplayed()} - {this.renderUpdateLink()} - </li> - </ul> - ); - }, - - renderCount() { - return ( - <ul className="overflow-hidden bordered-left"> - <li className="spacer-left little-spacer-bottom"> - <span className="spacer-right">{this.props.total}</span> - {this.renderUpdateLink()} - </li> - </ul> - ); - }, - - render() { - return ( - <li className="abs-width-400"> - <div className="pull-left spacer-right"> - <strong>{this.renderTitle()}</strong> - </div> - {this.props.items ? this.renderItems() : this.renderCount()} - </li> - ); - } -}; diff --git a/server/sonar-web/src/main/js/apps/global-permissions/permission-users.js b/server/sonar-web/src/main/js/apps/global-permissions/permission-users.js deleted file mode 100644 index c04664b27dd..00000000000 --- a/server/sonar-web/src/main/js/apps/global-permissions/permission-users.js +++ /dev/null @@ -1,53 +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 React from 'react'; -import PermissionsUsersGroupsMixin from './permission-users-groups-mixin'; -import UsersView from './users-view'; - -export default React.createClass({ - mixins: [PermissionsUsersGroupsMixin], - - renderUpdateLink() { - return ( - <a onClick={this.updateUsers} - className="icon-bullet-list" - title="Update Users" - data-toggle="tooltip" - href="#"></a> - ); - }, - - renderItem(item) { - return item.name; - }, - - renderTitle() { - return 'Users'; - }, - - updateUsers(e) { - e.preventDefault(); - new UsersView({ - permission: this.props.permission.key, - project: this.props.project, - refresh: this.props.refresh - }).render(); - } -}); diff --git a/server/sonar-web/src/main/js/apps/global-permissions/permission.js b/server/sonar-web/src/main/js/apps/global-permissions/permission.js deleted file mode 100644 index 3e99b67abe0..00000000000 --- a/server/sonar-web/src/main/js/apps/global-permissions/permission.js +++ /dev/null @@ -1,82 +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 'jquery'; -import React from 'react'; -import PermissionUsers from './permission-users'; -import PermissionGroups from './permission-groups'; - -// Maximum number of displayed groups -const MAX_ITEMS = 3; - -export default React.createClass({ - propTypes: { - permission: React.PropTypes.object.isRequired - }, - - getInitialState() { - return {}; - }, - - componentDidMount() { - this.requestUsers(); - this.requestGroups(); - }, - - requestUsers() { - const url = window.baseUrl + '/api/permissions/users'; - const data = { permission: this.props.permission.key, ps: MAX_ITEMS }; - if (this.props.project) { - data.projectId = this.props.project; - } - $.get(url, data).done(r => this.setState({ users: r.users, totalUsers: r.paging && r.paging.total })); - }, - - requestGroups() { - const url = window.baseUrl + '/api/permissions/groups'; - const data = { permission: this.props.permission.key, ps: MAX_ITEMS }; - if (this.props.project) { - data.projectId = this.props.project; - } - $.get(url, data).done(r => this.setState({ groups: r.groups, totalGroups: r.paging && r.paging.total })); - }, - - render() { - return ( - <li className="panel panel-vertical" data-id={this.props.permission.key}> - <h3>{this.props.permission.name}</h3> - <p className="spacer-top" dangerouslySetInnerHTML={{ __html: this.props.permission.description }}/> - <ul className="list-inline spacer-top"> - <PermissionUsers permission={this.props.permission} - project={this.props.project} - max={MAX_ITEMS} - items={this.state.users} - total={this.state.totalUsers || this.props.permission.usersCount} - refresh={this.requestUsers}/> - <PermissionGroups permission={this.props.permission} - project={this.props.project} - max={MAX_ITEMS} - items={this.state.groups} - total={this.state.totalGroups || this.props.permission.groupsCount} - refresh={this.requestGroups}/> - </ul> - </li> - ); - } -}); diff --git a/server/sonar-web/src/main/js/apps/global-permissions/templates/global-permissions-groups.hbs b/server/sonar-web/src/main/js/apps/global-permissions/templates/global-permissions-groups.hbs deleted file mode 100644 index d921ae9c0b5..00000000000 --- a/server/sonar-web/src/main/js/apps/global-permissions/templates/global-permissions-groups.hbs +++ /dev/null @@ -1,10 +0,0 @@ -<div class="modal-head"> - <h2>Update Groups</h2> -</div> -<div class="modal-body"> - <div class="js-modal-messages"></div> - <div id="global-permissions-groups"></div> -</div> -<div class="modal-foot"> - <a href="#" class="js-modal-close" id="global-permissions-groups-done">Done</a> -</div> diff --git a/server/sonar-web/src/main/js/apps/global-permissions/templates/global-permissions-users.hbs b/server/sonar-web/src/main/js/apps/global-permissions/templates/global-permissions-users.hbs deleted file mode 100644 index cea5a6682ce..00000000000 --- a/server/sonar-web/src/main/js/apps/global-permissions/templates/global-permissions-users.hbs +++ /dev/null @@ -1,10 +0,0 @@ -<div class="modal-head"> - <h2>Update Users</h2> -</div> -<div class="modal-body"> - <div class="js-modal-messages"></div> - <div id="global-permissions-users"></div> -</div> -<div class="modal-foot"> - <a href="#" class="js-modal-close" id="global-permissions-users-done">Done</a> -</div> diff --git a/server/sonar-web/src/main/js/apps/global-permissions/users-view.js b/server/sonar-web/src/main/js/apps/global-permissions/users-view.js deleted file mode 100644 index 32456c6d8f8..00000000000 --- a/server/sonar-web/src/main/js/apps/global-permissions/users-view.js +++ /dev/null @@ -1,72 +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 Modal from '../../components/common/modals'; -import Template from './templates/global-permissions-users.hbs'; -import '../../components/SelectList'; - -function getSearchUrl (permission, project) { - let url = window.baseUrl + '/api/permissions/users?ps=100&permission=' + permission; - if (project) { - url = url + '&projectId=' + project; - } - return url; -} - -function getExtra (permission, project) { - const extra = { permission }; - if (project) { - extra.projectId = project; - } - return extra; -} - -export default Modal.extend({ - template: Template, - - onRender () { - Modal.prototype.onRender.apply(this, arguments); - new window.SelectList({ - el: this.$('#global-permissions-users'), - width: '100%', - readOnly: false, - focusSearch: false, - format (item) { - return `${item.name}<br><span class="note">${item.login}</span>`; - }, - queryParam: 'q', - searchUrl: getSearchUrl(this.options.permission, this.options.project), - selectUrl: window.baseUrl + '/api/permissions/add_user', - deselectUrl: window.baseUrl + '/api/permissions/remove_user', - extra: getExtra(this.options.permission, this.options.project), - selectParameter: 'login', - selectParameterValue: 'login', - parse (r) { - this.more = false; - return r.users; - } - }); - }, - - onDestroy () { - this.options.refresh(); - Modal.prototype.onDestroy.apply(this, arguments); - } -}); - 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 index a19119a6da3..02b1e863c83 100644 --- a/server/sonar-web/src/main/js/apps/permission-templates/styles.css +++ b/server/sonar-web/src/main/js/apps/permission-templates/styles.css @@ -3,12 +3,12 @@ } .permissions-table > tbody > tr > td { - border-bottom: 20px solid #fff !important; + border-bottom: 10px solid #fff !important; } .permissions-table .permission-column { - width: 130px; - white-space: nowrap; + width: 12%; + min-width: 112px; } .permissions-table .actions-column { diff --git a/server/sonar-web/src/main/js/apps/project-permissions/app.js b/server/sonar-web/src/main/js/apps/permissions/global/app.js index c90cd10cec1..db8e2981d9c 100644 --- a/server/sonar-web/src/main/js/apps/project-permissions/app.js +++ b/server/sonar-web/src/main/js/apps/permissions/global/app.js @@ -17,20 +17,19 @@ * 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 'jquery'; import React from 'react'; -import ReactDOM from 'react-dom'; -import Main from './main'; - -function requestPermissionTemplates () { - return $.get(window.baseUrl + '/api/permissions/search_templates'); -} +import { render } from 'react-dom'; +import { Provider } from 'react-redux'; +import App from './components/App'; +import configureStore from '../../../components/store/configureStore'; +import rootReducer from '../shared/store/rootReducer'; window.sonarqube.appStarted.then(options => { - requestPermissionTemplates().done(r => { - const el = document.querySelector(options.el); - ReactDOM.render(<Main permissionTemplates={r.permissionTemplates} - componentId={window.sonarqube.componentId} - rootQualifiers={options.rootQualifiers}/>, el); - }); + const el = document.querySelector(options.el); + const store = configureStore(rootReducer); + render(( + <Provider store={store}> + <App/> + </Provider> + ), el); }); diff --git a/server/sonar-web/src/main/js/apps/permissions/global/components/AllHoldersList.js b/server/sonar-web/src/main/js/apps/permissions/global/components/AllHoldersList.js new file mode 100644 index 00000000000..fe97b7c3233 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/global/components/AllHoldersList.js @@ -0,0 +1,131 @@ +/* + * 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 { connect } from 'react-redux'; +import SearchForm from '../../shared/components/SearchForm'; +import HoldersList from '../../shared/components/HoldersList'; +import { + loadHolders, + grantToUser, + revokeFromUser, + grantToGroup, + revokeFromGroup, + updateFilter, + updateQuery, + selectPermission +} from '../store/actions'; +import { + getUsers, + getGroups, + getQuery, + getFilter, + getSelectedPermission +} from '../../shared/store/rootReducer'; +import { translate } from '../../../../helpers/l10n'; + +const PERMISSIONS_ORDER = [ + 'admin', + 'profileadmin', + 'gateadmin', + 'shareDashboard', + 'scan', + 'provisioning' +]; + +class AllHoldersList extends React.Component { + componentDidMount () { + this.props.loadHolders(); + } + + handleToggleUser (user, permission) { + const hasPermission = user.permissions.includes(permission); + + if (hasPermission) { + this.props.revokePermissionFromUser(user.login, permission); + } else { + this.props.grantPermissionToUser(user.login, permission); + } + } + + handleToggleGroup (group, permission) { + const hasPermission = group.permissions.includes(permission); + + if (hasPermission) { + this.props.revokePermissionFromGroup(group.name, permission); + } else { + this.props.grantPermissionToGroup(group.name, permission); + } + } + + render () { + const permissions = PERMISSIONS_ORDER.map(p => ({ + key: p, + name: translate('global_permissions', p), + description: translate('global_permissions', p, 'desc') + })); + + return ( + <HoldersList + permissions={permissions} + selectedPermission={this.props.selectedPermission} + users={this.props.users} + groups={this.props.groups} + onSelectPermission={this.props.onSelectPermission} + onToggleUser={this.handleToggleUser.bind(this)} + onToggleGroup={this.handleToggleGroup.bind(this)}> + + <SearchForm + query={this.props.query} + filter={this.props.filter} + onSearch={this.props.onSearch} + onFilter={this.props.onFilter}/> + + </HoldersList> + ); + } +} + +const mapStateToProps = state => ({ + users: getUsers(state), + groups: getGroups(state), + query: getQuery(state), + filter: getFilter(state), + selectedPermission: getSelectedPermission(state) +}); + +const mapDispatchToProps = dispatch => ({ + loadHolders: () => dispatch(loadHolders()), + onSearch: query => dispatch(updateQuery(query)), + onFilter: filter => dispatch(updateFilter(filter)), + onSelectPermission: permission => dispatch(selectPermission(permission)), + grantPermissionToUser: (login, permission) => + dispatch(grantToUser(login, permission)), + revokePermissionFromUser: (login, permission) => + dispatch(revokeFromUser(login, permission)), + grantPermissionToGroup: (groupName, permission) => + dispatch(grantToGroup(groupName, permission)), + revokePermissionFromGroup: (groupName, permission) => + dispatch(revokeFromGroup(groupName, permission)) +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(AllHoldersList); diff --git a/server/sonar-web/src/main/js/apps/permissions/global/components/App.js b/server/sonar-web/src/main/js/apps/permissions/global/components/App.js new file mode 100644 index 00000000000..71ff949fd87 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/global/components/App.js @@ -0,0 +1,38 @@ +/* + * 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 PageHeader from './PageHeader'; +import AllHoldersList from './AllHoldersList'; +import PageError from '../../shared/components/PageError'; +import '../../styles.css'; + +// TODO helmet + +export default class App extends React.Component { + render () { + return ( + <div className="page page-limited"> + <PageHeader/> + <PageError/> + <AllHoldersList/> + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/permissions/global/components/PageHeader.js b/server/sonar-web/src/main/js/apps/permissions/global/components/PageHeader.js new file mode 100644 index 00000000000..23b750a2dd2 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/global/components/PageHeader.js @@ -0,0 +1,66 @@ +/* + * 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 { connect } from 'react-redux'; +import { translate } from '../../../../helpers/l10n'; +import { loadHolders } from '../store/actions'; +import { isLoading } from '../../shared/store/rootReducer'; + +class PageHeader extends React.Component { + static propTypes = { + loadHolders: React.PropTypes.func.isRequired, + loading: React.PropTypes.bool + }; + + static defaultProps = { + loading: false + }; + + render () { + return ( + <header className="page-header"> + <h1 className="page-title"> + {translate('global_permissions.page')} + </h1> + + {this.props.loading && ( + <i className="spinner"/> + )} + + <div className="page-description"> + {translate('global_permissions.page.description')} + </div> + </header> + ); + } +} + +const mapStateToProps = state => ({ + loading: isLoading(state) +}); + +const mapDispatchToProps = dispatch => ({ + loadHolders: () => dispatch(loadHolders()) +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(PageHeader); diff --git a/server/sonar-web/src/main/js/apps/permissions/global/store/actions.js b/server/sonar-web/src/main/js/apps/permissions/global/store/actions.js new file mode 100644 index 00000000000..3a7d1fa158d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/global/store/actions.js @@ -0,0 +1,122 @@ +/* + * 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 * as api from '../../../../api/permissions'; +import { parseError } from '../../../code/utils'; +import { raiseError } from '../../shared/store/actions'; +import { + getQuery, + getFilter, + getSelectedPermission +} from '../../shared/store/rootReducer'; + +export const loadHolders = () => (dispatch, getState) => { + const query = getQuery(getState()); + const filter = getFilter(getState()); + const selectedPermission = getSelectedPermission(getState()); + + dispatch({ type: 'REQUEST_HOLDERS', query }); + + const requests = []; + + if (filter !== 'groups') { + requests.push(api.getGlobalPermissionsUsers(query, selectedPermission)); + } else { + requests.push(Promise.resolve([])); + } + + if (filter !== 'users') { + requests.push(api.getGlobalPermissionsGroups(query, selectedPermission)); + } else { + requests.push(Promise.resolve([])); + } + + return Promise.all(requests).then(responses => ( + dispatch({ + type: 'RECEIVE_HOLDERS_SUCCESS', + users: responses[0], + groups: responses[1], + query + }) + )).catch(e => { + return parseError(e).then(message => dispatch(raiseError(message))); + }); +}; + +export const updateQuery = (query = '') => dispatch => { + dispatch({ type: 'UPDATE_QUERY', query }); + if (query.length === 0 || query.length > 2) { + dispatch(loadHolders()); + } +}; + +export const updateFilter = filter => dispatch => { + dispatch({ type: 'UPDATE_FILTER', filter }); + dispatch(loadHolders()); +}; + +export const selectPermission = permission => (dispatch, getState) => { + const selectedPermission = getSelectedPermission(getState()); + if (selectedPermission !== permission) { + dispatch({ type: 'SELECT_PERMISSION', permission }); + } else { + dispatch({ type: 'SELECT_PERMISSION', permission: null }); + } + dispatch(loadHolders()); +}; + +export const grantToUser = (login, permission) => dispatch => { + api.grantPermissionToUser(null, login, permission).then(() => { + dispatch({ type: 'GRANT_PERMISSION_TO_USER', login, permission }); + }).catch(e => { + return parseError(e).then(message => dispatch(raiseError(message))); + }); +}; + +export const revokeFromUser = (login, permission) => dispatch => { + api.revokePermissionFromUser(null, login, permission).then(() => { + dispatch({ type: 'REVOKE_PERMISSION_TO_USER', login, permission }); + }).catch(e => { + return parseError(e).then(message => dispatch(raiseError(message))); + }); +}; + +export const grantToGroup = (groupName, permission) => dispatch => { + api.grantPermissionToGroup(null, groupName, permission).then(() => { + dispatch({ + type: 'GRANT_PERMISSION_TO_GROUP', + groupName, + permission + }); + }).catch(e => { + return parseError(e).then(message => dispatch(raiseError(message))); + }); +}; + +export const revokeFromGroup = (groupName, permission) => dispatch => { + api.revokePermissionFromGroup(null, groupName, permission).then(() => { + dispatch({ + type: 'REVOKE_PERMISSION_FROM_GROUP', + groupName, + permission + }); + }).catch(e => { + return parseError(e).then(message => dispatch(raiseError(message))); + }); +}; diff --git a/server/sonar-web/src/main/js/apps/permissions/project/app.js b/server/sonar-web/src/main/js/apps/permissions/project/app.js new file mode 100644 index 00000000000..1646df7ef24 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/project/app.js @@ -0,0 +1,35 @@ +/* + * 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 { render } from 'react-dom'; +import { Provider } from 'react-redux'; +import App from './components/App'; +import configureStore from '../../../components/store/configureStore'; +import rootReducer from '../shared/store/rootReducer'; + +window.sonarqube.appStarted.then(options => { + const el = document.querySelector(options.el); + const store = configureStore(rootReducer); + render(( + <Provider store={store}> + <App project={options.component}/> + </Provider> + ), el); +}); diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/AllHoldersList.js b/server/sonar-web/src/main/js/apps/permissions/project/components/AllHoldersList.js new file mode 100644 index 00000000000..880c4d2a195 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/project/components/AllHoldersList.js @@ -0,0 +1,163 @@ +/* + * 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 { connect } from 'react-redux'; +import SearchForm from '../../shared/components/SearchForm'; +import HoldersList from '../../shared/components/HoldersList'; +import { + loadHolders, + grantToUser, + revokeFromUser, + grantToGroup, + revokeFromGroup, + updateQuery, + updateFilter, + selectPermission +} from '../store/actions'; +import { + getUsers, + getGroups, + getQuery, + getFilter, + getSelectedPermission +} from '../../shared/store/rootReducer'; +import { translate } from '../../../../helpers/l10n'; + +export const PERMISSIONS_ORDER = [ + 'user', + 'codeviewer', + 'issueadmin', + 'admin', + 'scan' +]; + +class AllHoldersList extends React.Component { + static propTypes = { + project: React.PropTypes.object.isRequired + }; + + componentDidMount () { + this.props.loadHolders(this.props.project.key); + } + + handleSearch (query) { + this.props.onSearch(this.props.project.key, query); + } + + handleFilter (filter) { + this.props.onFilter(this.props.project.key, filter); + } + + handleToggleUser (user, permission) { + const hasPermission = user.permissions.includes(permission); + + if (hasPermission) { + this.props.revokePermissionFromUser( + this.props.project.key, + user.login, + permission + ); + } else { + this.props.grantPermissionToUser( + this.props.project.key, + user.login, + permission + ); + } + } + + handleToggleGroup (group, permission) { + const hasPermission = group.permissions.includes(permission); + + if (hasPermission) { + this.props.revokePermissionFromGroup( + this.props.project.key, + group.name, + permission + ); + } else { + this.props.grantPermissionToGroup( + this.props.project.key, + group.name, + permission + ); + } + } + + handleSelectPermission (permission) { + this.props.onSelectPermission(this.props.project.key, permission); + } + + render () { + const permissions = PERMISSIONS_ORDER.map(p => ({ + key: p, + name: translate('projects_role', p), + description: translate('projects_role', p, 'desc') + })); + + return ( + <HoldersList + permissions={permissions} + selectedPermission={this.props.selectedPermission} + users={this.props.users} + groups={this.props.groups} + onSelectPermission={this.handleSelectPermission.bind(this)} + onToggleUser={this.handleToggleUser.bind(this)} + onToggleGroup={this.handleToggleGroup.bind(this)}> + + <SearchForm + query={this.props.query} + filter={this.props.filter} + onSearch={this.handleSearch.bind(this)} + onFilter={this.handleFilter.bind(this)}/> + + </HoldersList> + ); + } +} + +const mapStateToProps = state => ({ + users: getUsers(state), + groups: getGroups(state), + query: getQuery(state), + filter: getFilter(state), + selectedPermission: getSelectedPermission(state) +}); + +const mapDispatchToProps = dispatch => ({ + loadHolders: projectKey => dispatch(loadHolders(projectKey)), + onSearch: (projectKey, query) => dispatch(updateQuery(projectKey, query)), + onFilter: (projectKey, filter) => dispatch(updateFilter(projectKey, filter)), + onSelectPermission: (projectKey, permission) => + dispatch(selectPermission(projectKey, permission)), + grantPermissionToUser: (projectKey, login, permission) => + dispatch(grantToUser(projectKey, login, permission)), + revokePermissionFromUser: (projectKey, login, permission) => + dispatch(revokeFromUser(projectKey, login, permission)), + grantPermissionToGroup: (projectKey, groupName, permission) => + dispatch(grantToGroup(projectKey, groupName, permission)), + revokePermissionFromGroup: (projectKey, groupName, permission) => + dispatch(revokeFromGroup(projectKey, groupName, permission)) +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(AllHoldersList); diff --git a/server/sonar-web/src/main/js/apps/project-permissions/permissions-header.js b/server/sonar-web/src/main/js/apps/permissions/project/components/App.js index 281616aaea9..d8bc447fa08 100644 --- a/server/sonar-web/src/main/js/apps/project-permissions/permissions-header.js +++ b/server/sonar-web/src/main/js/apps/permissions/project/components/App.js @@ -18,31 +18,25 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; +import PageHeader from './PageHeader'; +import AllHoldersList from './AllHoldersList'; +import PageError from '../../shared/components/PageError'; +import '../../styles.css'; -export default React.createClass({ - propTypes: { - permissions: React.PropTypes.arrayOf(React.PropTypes.object).isRequired - }, +// TODO helmet - 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> - )); +export default class App extends React.Component { + static propTypes = { + project: React.PropTypes.object.isRequired + }; + render () { return ( - <thead> - <tr> - <th> </th> - {cells} - <th className="actions-column"> </th> - </tr> - </thead> + <div className="page page-limited"> + <PageHeader project={this.props.project}/> + <PageError/> + <AllHoldersList project={this.props.project}/> + </div> ); } -}); +} diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.js b/server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.js new file mode 100644 index 00000000000..39fcaa5e925 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.js @@ -0,0 +1,89 @@ +/* + * 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 { connect } from 'react-redux'; +import { translate } from '../../../../helpers/l10n'; +import ApplyTemplateView from '../views/ApplyTemplateView'; +import { loadHolders } from '../store/actions'; +import { isLoading } from '../../shared/store/rootReducer'; + +class PageHeader extends React.Component { + static propTypes = { + project: React.PropTypes.object.isRequired, + loadHolders: React.PropTypes.func.isRequired, + loading: React.PropTypes.bool + }; + + static defaultProps = { + loading: false + }; + + componentWillMount () { + this.handleApplyTemplate = this.handleApplyTemplate.bind(this); + } + + handleApplyTemplate (e) { + e.preventDefault(); + e.target.blur(); + const { project, loadHolders } = this.props; + new ApplyTemplateView({ project }) + .on('done', () => loadHolders(project.key)) + .render(); + } + + render () { + return ( + <header className="page-header"> + <h1 className="page-title"> + {translate('permissions.page')} + </h1> + + {this.props.loading && ( + <i className="spinner"/> + )} + + <div className="page-actions"> + <button + className="js-apply-template" + onClick={this.handleApplyTemplate}> + Apply Template + </button> + </div> + + <div className="page-description"> + {translate('roles.page.description2')} + </div> + </header> + ); + } +} + +const mapStateToProps = state => ({ + loading: isLoading(state) +}); + +const mapDispatchToProps = dispatch => ({ + loadHolders: projectKey => dispatch(loadHolders(projectKey)) +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(PageHeader); diff --git a/server/sonar-web/src/main/js/apps/permissions/project/store/actions.js b/server/sonar-web/src/main/js/apps/permissions/project/store/actions.js new file mode 100644 index 00000000000..5011dc075ad --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/project/store/actions.js @@ -0,0 +1,124 @@ +/* + * 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 * as api from '../../../../api/permissions'; +import { parseError } from '../../../code/utils'; +import { raiseError } from '../../shared/store/actions'; +import { + getQuery, + getFilter, + getSelectedPermission +} from '../../shared/store/rootReducer'; + +export const loadHolders = projectKey => (dispatch, getState) => { + const query = getQuery(getState()); + const filter = getFilter(getState()); + const selectedPermission = getSelectedPermission(getState()); + + dispatch({ type: 'REQUEST_HOLDERS', query }); + + const requests = []; + + if (filter !== 'groups') { + requests.push(api.getPermissionsUsersForComponent(projectKey, query, + selectedPermission)); + } else { + requests.push(Promise.resolve([])); + } + + if (filter !== 'users') { + requests.push(api.getPermissionsGroupsForComponent(projectKey, query, + selectedPermission)); + } else { + requests.push(Promise.resolve([])); + } + + return Promise.all(requests).then(responses => ( + dispatch({ + type: 'RECEIVE_HOLDERS_SUCCESS', + users: responses[0], + groups: responses[1], + query + }) + )).catch(e => { + return parseError(e).then(message => dispatch(raiseError(message))); + }); +}; + +export const updateQuery = (projectKey, query = '') => dispatch => { + dispatch({ type: 'UPDATE_QUERY', query }); + if (query.length === 0 || query.length > 2) { + dispatch(loadHolders(projectKey)); + } +}; + +export const updateFilter = (projectKey, filter) => dispatch => { + dispatch({ type: 'UPDATE_FILTER', filter }); + dispatch(loadHolders(projectKey)); +}; + +export const selectPermission = (projectKey, permission) => (dispatch, getState) => { + const selectedPermission = getSelectedPermission(getState()); + if (selectedPermission !== permission) { + dispatch({ type: 'SELECT_PERMISSION', permission }); + } else { + dispatch({ type: 'SELECT_PERMISSION', permission: null }); + } + dispatch(loadHolders(projectKey)); +}; + +export const grantToUser = (projectKey, login, permission) => dispatch => { + api.grantPermissionToUser(projectKey, login, permission).then(() => { + dispatch({ type: 'GRANT_PERMISSION_TO_USER', login, permission }); + }).catch(e => { + return parseError(e).then(message => dispatch(raiseError(message))); + }); +}; + +export const revokeFromUser = (projectKey, login, permission) => dispatch => { + api.revokePermissionFromUser(projectKey, login, permission).then(() => { + dispatch({ type: 'REVOKE_PERMISSION_TO_USER', login, permission }); + }).catch(e => { + return parseError(e).then(message => dispatch(raiseError(message))); + }); +}; + +export const grantToGroup = (projectKey, groupName, permission) => dispatch => { + api.grantPermissionToGroup(projectKey, groupName, permission).then(() => { + dispatch({ + type: 'GRANT_PERMISSION_TO_GROUP', + groupName, + permission + }); + }).catch(e => { + return parseError(e).then(message => dispatch(raiseError(message))); + }); +}; + +export const revokeFromGroup = (projectKey, groupName, permission) => dispatch => { + api.revokePermissionFromGroup(projectKey, groupName, permission).then(() => { + dispatch({ + type: 'REVOKE_PERMISSION_FROM_GROUP', + groupName, + permission + }); + }).catch(e => { + return parseError(e).then(message => dispatch(raiseError(message))); + }); +}; diff --git a/server/sonar-web/src/main/js/apps/permissions/project/templates/ApplyTemplateTemplate.hbs b/server/sonar-web/src/main/js/apps/permissions/project/templates/ApplyTemplateTemplate.hbs new file mode 100644 index 00000000000..c7307670cbe --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/project/templates/ApplyTemplateTemplate.hbs @@ -0,0 +1,30 @@ +<form id="project-permissions-apply-template-form" autocomplete="off"> + <div class="modal-head"> + <h2>Apply Permission Template to "{{project.name}}"</h2> + </div> + + <div class="modal-body"> + <div class="js-modal-messages"></div> + {{#notNull permissionTemplates}} + <div class="modal-field"> + <label for="project-permissions-template"> + Template<em class="mandatory">*</em> + </label> + <select id="project-permissions-template"> + {{#each permissionTemplates}} + <option value="{{id}}">{{name}}</option> + {{/each}} + </select> + </div> + {{else}} + <i class="spinner"></i> + {{/notNull}} + </div> + + <div class="modal-foot"> + {{#notNull permissionTemplates}} + <button id="project-permissions-apply-template">Apply</button> + {{/notNull}} + <a href="#" class="js-modal-close">Cancel</a> + </div> +</form> diff --git a/server/sonar-web/src/main/js/apps/permissions/project/views/ApplyTemplateView.js b/server/sonar-web/src/main/js/apps/permissions/project/views/ApplyTemplateView.js new file mode 100644 index 00000000000..6aa2537c3f6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/project/views/ApplyTemplateView.js @@ -0,0 +1,74 @@ +/* + * 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 ModalForm from '../../../../components/common/modal-form'; +import { + applyTemplateToProject, + getPermissionTemplates +} from '../../../../api/permissions'; +import Template from '../templates/ApplyTemplateTemplate.hbs'; + +export default ModalForm.extend({ + template: Template, + + initialize () { + this.loadPermissionTemplates(); + }, + + loadPermissionTemplates () { + return getPermissionTemplates().then(r => { + this.permissionTemplates = r.permissionTemplates; + this.render(); + }); + }, + + onRender () { + ModalForm.prototype.onRender.apply(this, arguments); + this.$('#project-permissions-template').select2({ + width: '250px', + minimumResultsForSearch: 20 + }); + }, + + onFormSubmit () { + ModalForm.prototype.onFormSubmit.apply(this, arguments); + const permissionTemplate = this.$('#project-permissions-template').val(); + this.disableForm(); + + applyTemplateToProject({ + projectKey: this.options.project.key, + templateId: permissionTemplate + }).then(() => { + this.trigger('done'); + this.destroy(); + }).catch(function (e) { + e.response.json().then(r => { + this.showErrors(r.errors, r.warnings); + this.enableForm(); + }); + }); + }, + + serializeData () { + return { + permissionTemplates: this.permissionTemplates, + project: this.options.project + }; + } +}); diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/GroupHolder.js b/server/sonar-web/src/main/js/apps/permissions/shared/components/GroupHolder.js new file mode 100644 index 00000000000..2d5ec48e164 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/shared/components/GroupHolder.js @@ -0,0 +1,83 @@ +/* + * 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 shallowCompare from 'react-addons-shallow-compare'; +import GroupIcon from './GroupIcon'; + +export default class GroupHolder extends React.Component { + static propTypes = { + group: React.PropTypes.object.isRequired, + permissions: React.PropTypes.array.isRequired, + selectedPermission: React.PropTypes.string, + permissionsOrder: React.PropTypes.array.isRequired, + onToggle: React.PropTypes.func.isRequired + }; + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + handleClick (permission, e) { + e.preventDefault(); + e.target.blur(); + this.props.onToggle(this.props.group, permission); + } + + render () { + const { selectedPermission } = this.props; + const permissionCells = this.props.permissionsOrder.map(p => ( + <td key={p.key} + className="text-center text-middle" + style={{ backgroundColor: p.key === selectedPermission ? '#d9edf7' : 'transparent' }}> + <button + className="button-clean" + onClick={this.handleClick.bind(this, p.key)}> + {this.props.permissions.includes(p.key) ? ( + <i className="icon-checkbox icon-checkbox-checked"/> + ) : ( + <i className="icon-checkbox"/> + )} + </button> + </td> + )); + + const { group } = this.props; + + return ( + <tr> + <td className="nowrap"> + <div className="display-inline-block text-middle big-spacer-right"> + <GroupIcon/> + </div> + <div className="display-inline-block text-middle"> + <div> + <strong>{group.name}</strong> + </div> + <div className="little-spacer-top" + style={{ whiteSpace: 'normal' }}> + {group.description} + </div> + </div> + </td> + {permissionCells} + </tr> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/GroupIcon.js b/server/sonar-web/src/main/js/apps/permissions/shared/components/GroupIcon.js new file mode 100644 index 00000000000..63f9746f107 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/shared/components/GroupIcon.js @@ -0,0 +1,38 @@ +/* + * 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'; + +const GroupIcon = () => { + /* eslint max-len: 0 */ + return ( + <div style={{ padding: '4px 3px 0' }}> + <svg xmlns="http://www.w3.org/2000/svg" + width="30" + height="28" + viewBox="0 0 480 448"> + <path + fill="#aaa" + d="M148.25 224q-40.5 1.25-66.25 32H48.5Q28 256 14 245.875T0 216.25Q0 128 31 128q1.5 0 10.875 5.25t24.375 10.625T96 149.25q16.75 0 33.25-5.75Q128 152.75 128 160q0 34.75 20.25 64zM416 383.25q0 30-18.25 47.375T349.25 448h-218.5q-30.25 0-48.5-17.375T64 383.25q0-13.25.875-25.875t3.5-27.25T75 303t10.75-24.375 15.5-20.25T122.625 245t27.875-5q2.5 0 10.75 5.375t18.25 12 26.75 12T240 274.75t33.75-5.375 26.75-12 18.25-12T329.5 240q15.25 0 27.875 5t21.375 13.375 15.5 20.25T405 303t6.625 27.125 3.5 27.25.875 25.875zM160 64q0 26.5-18.75 45.25T96 128t-45.25-18.75T32 64t18.75-45.25T96 0t45.25 18.75T160 64zm176 96q0 39.75-28.125 67.875T240 256t-67.875-28.125T144 160t28.125-67.875T240 64t67.875 28.125T336 160zm144 56.25q0 19.5-14 29.625T431.5 256H398q-25.75-30.75-66.25-32Q352 194.75 352 160q0-7.25-1.25-16.5 16.5 5.75 33.25 5.75 14.75 0 29.75-5.375t24.375-10.625T449 128q31 0 31 88.25zM448 64q0 26.5-18.75 45.25T384 128t-45.25-18.75T320 64t18.75-45.25T384 0t45.25 18.75T448 64z"/> + </svg> + </div> + ); +}; + +export default GroupIcon; diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/HoldersList.js b/server/sonar-web/src/main/js/apps/permissions/shared/components/HoldersList.js new file mode 100644 index 00000000000..d7fb6fb6364 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/shared/components/HoldersList.js @@ -0,0 +1,98 @@ +/* + * 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 UserHolder from './UserHolder'; +import GroupHolder from './GroupHolder'; + +export default class HoldersList extends React.Component { + static propTypes = { + permissions: React.PropTypes.array.isRequired, + users: React.PropTypes.array.isRequired, + groups: React.PropTypes.array.isRequired, + selectedPermission: React.PropTypes.string, + onSelectPermission: React.PropTypes.func.isRequired, + onToggleUser: React.PropTypes.func.isRequired, + onToggleGroup: React.PropTypes.func.isRequired + }; + + handlePermissionClick (permission, e) { + e.preventDefault(); + e.target.blur(); + this.props.onSelectPermission(permission); + } + + renderTableHeader () { + const { selectedPermission } = this.props; + const cells = this.props.permissions.map(p => ( + <th key={p.key} + className="permission-column text-center" + style={{ backgroundColor: p.key === selectedPermission ? '#d9edf7' : 'transparent' }}> + <a href="#" onClick={this.handlePermissionClick.bind(this, p.key)}> + {p.name} + </a> + <i className="icon-help little-spacer-left" + title={p.description} + data-toggle="tooltip"/> + </th> + )); + return ( + <thead> + <tr> + <td className="bordered-bottom"> + {this.props.children} + </td> + {cells} + </tr> + </thead> + ); + } + + render () { + const users = this.props.users.map(user => ( + <UserHolder + key={'user-' + user.login} + user={user} + permissions={user.permissions} + selectedPermission={this.props.selectedPermission} + permissionsOrder={this.props.permissions} + onToggle={this.props.onToggleUser}/> + )); + + const groups = this.props.groups.map(group => ( + <GroupHolder + key={'group-' + group.id} + group={group} + permissions={group.permissions} + selectedPermission={this.props.selectedPermission} + permissionsOrder={this.props.permissions} + onToggle={this.props.onToggleGroup}/> + )); + + return ( + <table className="data zebra permissions-table"> + {this.renderTableHeader()} + <tbody> + {users} + {groups} + </tbody> + </table> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/global-permissions/permissions-list.js b/server/sonar-web/src/main/js/apps/permissions/shared/components/PageError.js index a8f62b304d3..3449153a921 100644 --- a/server/sonar-web/src/main/js/apps/global-permissions/permissions-list.js +++ b/server/sonar-web/src/main/js/apps/permissions/shared/components/PageError.js @@ -17,24 +17,34 @@ * 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 { connect } from 'react-redux'; +import { getError } from '../store/rootReducer'; -import Permission from './permission'; +class PageError extends React.Component { + static propTypes = { + message: React.PropTypes.string + }; -export default React.createClass({ - propTypes: { - permissions: React.PropTypes.arrayOf(React.PropTypes.object).isRequired - }, + render () { + const { message } = this.props; - renderPermissions() { - return this.props.permissions.map(permission => { - return <Permission key={permission.key} permission={permission} project={this.props.project}/>; - }); - }, + if (!message) { + return null; + } - render() { - const className = classNames({ 'new-loading': !this.props.ready }); - return <ul id="global-permissions-list" className={className}>{this.renderPermissions()}</ul>; + return ( + <div className="alert alert-danger"> + {message} + </div> + ); } +} + +const mapStateToProps = state => ({ + message: getError(state) }); + +export default connect( + mapStateToProps +)(PageError); diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/SearchForm.js b/server/sonar-web/src/main/js/apps/permissions/shared/components/SearchForm.js new file mode 100644 index 00000000000..35a4836ded4 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/shared/components/SearchForm.js @@ -0,0 +1,95 @@ +/* + * 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 RadioToggle from '../../../../components/controls/RadioToggle'; +import { translate, translateWithParameters } from '../../../../helpers/l10n'; + +export default class SearchForm extends React.Component { + static propTypes = { + query: React.PropTypes.string, + filter: React.PropTypes.oneOf(['all', 'users', 'groups']), + onSearch: React.PropTypes.func, + onFilter: React.PropTypes.func + }; + + componentWillMount () { + this.handleSubmit = this.handleSubmit.bind(this); + this.handleSearch = this.handleSearch.bind(this); + } + + handleSubmit (e) { + e.preventDefault(); + this.handleSearch(); + } + + handleSearch () { + const { value } = this.refs.searchInput; + this.props.onSearch(value); + } + + handleFilter (filter) { + this.props.onFilter(filter); + } + + render () { + const { query, filter } = this.props; + + const filterOptions = [ + { value: 'all', label: 'All' }, + { value: 'users', label: 'Users' }, + { value: 'groups', label: 'Groups' } + ]; + + return ( + <div> + + <RadioToggle + value={filter} + options={filterOptions} + name="users-or-groups" + onCheck={this.handleFilter.bind(this)}/> + + <form + className="search-box display-inline-block text-middle big-spacer-left" + onSubmit={this.handleSubmit}> + <button className="search-box-submit button-clean"> + <i className="icon-search"></i> + </button> + <input + ref="searchInput" + value={query} + className="search-box-input" + style={{ width: 100 }} + type="search" + placeholder={translate('search_verb')} + onChange={this.handleSearch.bind(this)}/> + {query.length > 0 && query.length < 3 && ( + <div className="search-box-input-note tooltip bottom fade in"> + <div className="tooltip-inner"> + {translateWithParameters('select2.tooShort', 3)} + </div> + <div className="tooltip-arrow" style={{ left: 23 }}/> + </div> + )} + </form> + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/UserHolder.js b/server/sonar-web/src/main/js/apps/permissions/shared/components/UserHolder.js new file mode 100644 index 00000000000..9b25cc74db5 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/shared/components/UserHolder.js @@ -0,0 +1,82 @@ +/* + * 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 shallowCompare from 'react-addons-shallow-compare'; +import Avatar from '../../../../components/ui/Avatar'; + +export default class UserHolder extends React.Component { + static propTypes = { + user: React.PropTypes.object.isRequired, + permissions: React.PropTypes.array.isRequired, + selectedPermission: React.PropTypes.string, + permissionsOrder: React.PropTypes.array.isRequired, + onToggle: React.PropTypes.func.isRequired + }; + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + handleClick (permission, e) { + e.preventDefault(); + e.target.blur(); + this.props.onToggle(this.props.user, permission); + } + + render () { + const { selectedPermission } = this.props; + const permissionCells = this.props.permissionsOrder.map(p => ( + <td key={p.key} + className="text-center text-middle" + style={{ backgroundColor: p.key === selectedPermission ? '#d9edf7' : 'transparent' }}> + <button + className="button-clean" + onClick={this.handleClick.bind(this, p.key)}> + {this.props.permissions.includes(p.key) ? ( + <i className="icon-checkbox icon-checkbox-checked"/> + ) : ( + <i className="icon-checkbox"/> + )} + </button> + </td> + )); + + const { user } = this.props; + + return ( + <tr> + <td className="nowrap"> + <Avatar + email={user.email} + size={36} + className="text-middle big-spacer-right"/> + <div className="display-inline-block text-middle"> + <div> + <strong>{user.name}</strong> + <span className="note spacer-left">{user.login}</span> + </div> + <div className="little-spacer-top">{user.email}</div> + </div> + </td> + {permissionCells} + </tr> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/global-permissions/app.js b/server/sonar-web/src/main/js/apps/permissions/shared/store/actions.js index 463b7a1cf0d..be7cee4a4b7 100644 --- a/server/sonar-web/src/main/js/apps/global-permissions/app.js +++ b/server/sonar-web/src/main/js/apps/permissions/shared/store/actions.js @@ -17,11 +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. */ -import React from 'react'; -import ReactDOM from 'react-dom'; -import Main from './main'; - -window.sonarqube.appStarted.then(options => { - const el = document.querySelector(options.el); - ReactDOM.render(<Main/>, el); +export const raiseError = message => ({ + type: 'ERROR', + message }); diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/store/error.js b/server/sonar-web/src/main/js/apps/permissions/shared/store/error.js new file mode 100644 index 00000000000..6fe0ec764c3 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/shared/store/error.js @@ -0,0 +1,35 @@ +/* + * 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. + */ +const error = (state = null, action = {}) => { + switch (action.type) { + case 'RECEIVE_HOLDERS_SUCCESS': + case 'GRANT_PERMISSION_TO_USER': + case 'REVOKE_PERMISSION_TO_USER': + case 'GRANT_PERMISSION_TO_GROUP': + case 'REVOKE_PERMISSION_FROM_GROUP': + return null; + case 'ERROR': + return action.message; + default: + return state; + } +}; + +export default error; diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/store/filter.js b/server/sonar-web/src/main/js/apps/permissions/shared/store/filter.js new file mode 100644 index 00000000000..6934582b425 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/shared/store/filter.js @@ -0,0 +1,29 @@ +/* + * 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. + */ +const filter = (state = 'all', action = {}) => { + switch (action.type) { + case 'UPDATE_FILTER': + return action.filter; + default: + return state; + } +}; + +export default filter; diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/store/groups/byName.js b/server/sonar-web/src/main/js/apps/permissions/shared/store/groups/byName.js new file mode 100644 index 00000000000..c1475aa6763 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/shared/store/groups/byName.js @@ -0,0 +1,44 @@ +/* + * 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 keyBy from 'lodash/keyBy'; + +const byName = (state = {}, action = {}) => { + if (action.type === 'RECEIVE_HOLDERS_SUCCESS') { + const newGroups = keyBy(action.groups, 'name'); + return { ...state, ...newGroups }; + } else if (action.type === 'GRANT_PERMISSION_TO_GROUP') { + const newGroup = { ...state[action.groupName] }; + newGroup.permissions = [...newGroup.permissions, action.permission]; + return { ...state, [action.groupName]: newGroup }; + } else if (action.type === 'REVOKE_PERMISSION_FROM_GROUP') { + const newGroup = { ...state[action.groupName] }; + newGroup.permissions = newGroup.permissions + .filter(p => p !== action.permission); + return { ...state, [action.groupName]: newGroup }; + } else { + return state; + } +}; + +export default byName; + +export const getGroups = state => state; + +export const getGroupByName = (state, name) => state[name]; diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/store/groups/groups.js b/server/sonar-web/src/main/js/apps/permissions/shared/store/groups/groups.js new file mode 100644 index 00000000000..8eba086373f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/shared/store/groups/groups.js @@ -0,0 +1,30 @@ +/* + * 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 { combineReducers } from 'redux'; +import byName, { getGroupByName } from './byName'; +import names, { getNames } from './names'; + +export default combineReducers({ + byName, + names +}); + +export const getGroups = state => + getNames(state.names).map(name => getGroupByName(state.byName, name)); diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/store/groups/names.js b/server/sonar-web/src/main/js/apps/permissions/shared/store/groups/names.js new file mode 100644 index 00000000000..8451a26999e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/shared/store/groups/names.js @@ -0,0 +1,30 @@ +/* + * 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. + */ +const names = (state = [], action = {}) => { + if (action.type === 'RECEIVE_HOLDERS_SUCCESS') { + return action.groups.map(group => group.name); + } else { + return state; + } +}; + +export default names; + +export const getNames = state => state; diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/store/loading.js b/server/sonar-web/src/main/js/apps/permissions/shared/store/loading.js new file mode 100644 index 00000000000..b7ff6d172bc --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/shared/store/loading.js @@ -0,0 +1,33 @@ +/* + * 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. + */ +const loading = (state = false, action = {}) => { + switch (action.type) { + case 'REQUEST_HOLDERS': + return true; + case 'RECEIVE_HOLDERS_SUCCESS': + return false; + default: + return state; + } +}; + +export default loading; + +export const isLoading = state => state; diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/store/query.js b/server/sonar-web/src/main/js/apps/permissions/shared/store/query.js new file mode 100644 index 00000000000..49a755a3fee --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/shared/store/query.js @@ -0,0 +1,30 @@ +/* + * 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. + */ +const query = (state = '', action = {}) => { + switch (action.type) { + case 'UPDATE_QUERY': + case 'REQUEST_HOLDERS': + return action.query; + default: + return state; + } +}; + +export default query; diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/store/rootReducer.js b/server/sonar-web/src/main/js/apps/permissions/shared/store/rootReducer.js new file mode 100644 index 00000000000..26dbd367379 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/shared/store/rootReducer.js @@ -0,0 +1,51 @@ +/* + * 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 { combineReducers } from 'redux'; +import users, * as fromUsers from './users/users'; +import groups, * as fromGroups from './groups/groups'; +import loading, * as fromLoading from './loading'; +import query from './query'; +import filter from './filter'; +import selectedPermission from './selectedPermission'; +import error from './error'; + +export default combineReducers({ + users, + groups, + loading, + query, + filter, + selectedPermission, + error +}); + +export const getUsers = state => fromUsers.getUsers(state.users); + +export const getGroups = state => fromGroups.getGroups(state.groups); + +export const isLoading = state => fromLoading.isLoading(state.loading); + +export const getQuery = state => state.query; + +export const getFilter = state => state.filter; + +export const getSelectedPermission = state => state.selectedPermission; + +export const getError = state => state.error; diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/store/selectedPermission.js b/server/sonar-web/src/main/js/apps/permissions/shared/store/selectedPermission.js new file mode 100644 index 00000000000..c4e951745d7 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/shared/store/selectedPermission.js @@ -0,0 +1,29 @@ +/* + * 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. + */ +const selectedPermission = (state = null, action = {}) => { + switch (action.type) { + case 'SELECT_PERMISSION': + return action.permission; + default: + return state; + } +}; + +export default selectedPermission; diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/store/users/byLogin.js b/server/sonar-web/src/main/js/apps/permissions/shared/store/users/byLogin.js new file mode 100644 index 00000000000..a571966cd92 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/shared/store/users/byLogin.js @@ -0,0 +1,44 @@ +/* + * 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 keyBy from 'lodash/keyBy'; + +const byLogin = (state = {}, action = {}) => { + if (action.type === 'RECEIVE_HOLDERS_SUCCESS') { + const newUsers = keyBy(action.users, 'login'); + return { ...state, ...newUsers }; + } else if (action.type === 'GRANT_PERMISSION_TO_USER') { + const newUser = { ...state[action.login] }; + newUser.permissions = [...newUser.permissions, action.permission]; + return { ...state, [action.login]: newUser }; + } else if (action.type === 'REVOKE_PERMISSION_TO_USER') { + const newUser = { ...state[action.login] }; + newUser.permissions = newUser.permissions + .filter(p => p !== action.permission); + return { ...state, [action.login]: newUser }; + } else { + return state; + } +}; + +export default byLogin; + +export const getUsers = state => state; + +export const getUserByLogin = (state, login) => state[login]; diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/store/users/logins.js b/server/sonar-web/src/main/js/apps/permissions/shared/store/users/logins.js new file mode 100644 index 00000000000..b0bc1e31ca0 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/shared/store/users/logins.js @@ -0,0 +1,30 @@ +/* + * 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. + */ +const logins = (state = [], action = {}) => { + if (action.type === 'RECEIVE_HOLDERS_SUCCESS') { + return action.users.map(user => user.login); + } else { + return state; + } +}; + +export default logins; + +export const getLogins = state => state; diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/store/users/users.js b/server/sonar-web/src/main/js/apps/permissions/shared/store/users/users.js new file mode 100644 index 00000000000..021a674951a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/shared/store/users/users.js @@ -0,0 +1,30 @@ +/* + * 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 { combineReducers } from 'redux'; +import byLogin, { getUserByLogin } from './byLogin'; +import logins, { getLogins } from './logins'; + +export default combineReducers({ + byLogin, + logins +}); + +export const getUsers = state => + getLogins(state.logins).map(login => getUserByLogin(state.byLogin, login)); diff --git a/server/sonar-web/src/main/js/apps/permissions/styles.css b/server/sonar-web/src/main/js/apps/permissions/styles.css new file mode 100644 index 00000000000..02b1e863c83 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/styles.css @@ -0,0 +1,17 @@ +.permissions-table { + table-layout: fixed; +} + +.permissions-table > tbody > tr > td { + border-bottom: 10px solid #fff !important; +} + +.permissions-table .permission-column { + width: 12%; + min-width: 112px; +} + +.permissions-table .actions-column { + width: 130px; + text-align: right; +} diff --git a/server/sonar-web/src/main/js/apps/project-permissions/apply-template-view.js b/server/sonar-web/src/main/js/apps/project-permissions/apply-template-view.js deleted file mode 100644 index b97c4e442d4..00000000000 --- a/server/sonar-web/src/main/js/apps/project-permissions/apply-template-view.js +++ /dev/null @@ -1,78 +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 ModalForm from '../../components/common/modal-form'; -import { applyTemplateToProject, bulkApplyTemplateToProject } from '../../api/permissions'; -import Template from './templates/project-permissions-apply-template.hbs'; - -export default ModalForm.extend({ - template: Template, - - onRender () { - ModalForm.prototype.onRender.apply(this, arguments); - this.$('#project-permissions-template').select2({ - width: '250px', - minimumResultsForSearch: 20 - }); - }, - - onFormSubmit () { - ModalForm.prototype.onFormSubmit.apply(this, arguments); - const that = this; - const permissionTemplate = this.$('#project-permissions-template').val(); - this.disableForm(); - - if (this.options.project) { - applyTemplateToProject({ - data: { projectId: this.options.project.id, templateId: permissionTemplate } - }).done(function () { - that.options.refresh(); - that.destroy(); - }).fail(function (jqXHR) { - that.enableForm(); - that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings); - }); - } else { - const data = { templateId: permissionTemplate }; - if (this.options.query) { - data.q = this.options.query; - } - if (this.options.filter && this.options.filter !== '__ALL__') { - data.qualifier = this.options.filter; - } - - bulkApplyTemplateToProject({ data }).done(function () { - that.options.refresh(); - that.destroy(); - }).fail(function (jqXHR) { - that.enableForm(); - that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings); - }); - } - }, - - serializeData () { - return _.extend(ModalForm.prototype.serializeData.apply(this, arguments), { - permissionTemplates: this.options.permissionTemplates, - project: this.options.project, - projectsCount: _.size(this.options.projects) - }); - } -}); diff --git a/server/sonar-web/src/main/js/apps/project-permissions/groups-view.js b/server/sonar-web/src/main/js/apps/project-permissions/groups-view.js deleted file mode 100644 index 30fe7999af6..00000000000 --- a/server/sonar-web/src/main/js/apps/project-permissions/groups-view.js +++ /dev/null @@ -1,72 +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 Modal from '../../components/common/modals'; -import '../../components/SelectList'; -import Template from './templates/project-permissions-groups.hbs'; - -function getSearchUrl (permission, project) { - return `${window.baseUrl}/api/permissions/groups?ps=100&permission=${permission}&projectId=${project}`; -} - -export default Modal.extend({ - template: Template, - - onRender () { - Modal.prototype.onRender.apply(this, arguments); - new window.SelectList({ - el: this.$('#project-permissions-groups'), - width: '100%', - readOnly: false, - focusSearch: false, - format (item) { - return item.name; - }, - queryParam: 'q', - searchUrl: getSearchUrl(this.options.permission, this.options.project), - selectUrl: window.baseUrl + '/api/permissions/add_group', - deselectUrl: window.baseUrl + '/api/permissions/remove_group', - extra: { - permission: this.options.permission, - projectId: this.options.project - }, - selectParameter: 'groupName', - selectParameterValue: 'name', - parse (r) { - this.more = false; - return r.groups; - } - }); - }, - - onDestroy () { - if (this.options.refresh) { - this.options.refresh(); - } - Modal.prototype.onDestroy.apply(this, arguments); - }, - - serializeData () { - return _.extend(Modal.prototype.serializeData.apply(this, arguments), { - projectName: this.options.projectName - }); - } -}); - 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 deleted file mode 100644 index eb947f85776..00000000000 --- a/server/sonar-web/src/main/js/apps/project-permissions/main.js +++ /dev/null @@ -1,169 +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 'jquery'; -import _ from 'underscore'; -import React from 'react'; - -import Permissions from './permissions'; -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']; - -export default React.createClass({ - propTypes: { - permissionTemplates: React.PropTypes.arrayOf(React.PropTypes.object).isRequired - }, - - getInitialState() { - return { ready: false, permissions: [], projects: [], total: 0, filter: '__ALL__' }; - }, - - componentDidMount() { - this.requestPermissions(); - }, - - sortPermissions(permissions) { - return _.sortBy(permissions, p => PERMISSIONS_ORDER.indexOf(p.key)); - }, - - mergePermissionsToProjects(projects, basePermissions) { - return projects.map(project => { - // it's important to keep the order of the project permissions the same as the order of base permissions - const permissions = basePermissions.map(basePermission => { - const projectPermission = _.findWhere(project.permissions, { key: basePermission.key }); - return _.extend({ usersCount: 0, groupsCount: 0 }, basePermission, projectPermission); - }); - return _.extend({}, project, { permissions }); - }); - }, - - requestPermissions(page = 1, query = '', filter = this.state.filter) { - const url = window.baseUrl + '/api/permissions/search_project_permissions'; - let data = { p: page, q: query }; - if (filter !== '__ALL__') { - data.qualifier = filter; - } - if (this.props.componentId) { - data = { projectId: this.props.componentId }; - } - this.setState({ ready: false }, () => { - $.get(url, data).done(r => { - const permissions = this.sortPermissions(r.permissions); - let projects = this.mergePermissionsToProjects(r.projects, permissions); - if (page > 1) { - projects = [].concat(this.state.projects, projects); - } - this.setState({ - ready: true, - projects, - permissions, - total: r.paging.total, - page: r.paging.pageIndex, - query, - filter - }); - }); - }); - }, - - loadMore() { - this.requestPermissions(this.state.page + 1, this.state.query); - }, - - search(query) { - this.requestPermissions(1, query); - }, - - handleFilter(filter) { - this.requestPermissions(1, this.state.query, filter); - }, - - refresh() { - this.requestPermissions(1, this.state.query); - }, - - bulkApplyTemplate(e) { - e.preventDefault(); - new ApplyTemplateView({ - query: this.state.query, - filter: this.state.filter, - permissionTemplates: this.props.permissionTemplates, - refresh: () => this.requestPermissions(1, this.state.query, this.state.filter) - }).render(); - }, - - renderBulkApplyButton() { - if (this.props.componentId) { - return null; - } - return ( - <button onClick={this.bulkApplyTemplate} className="js-bulk-apply-template">Bulk Apply Template</button> - ); - }, - - renderSpinner () { - if (this.state.ready) { - return null; - } - return <i className="spinner"/>; - }, - - render() { - return ( - <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-footer.js b/server/sonar-web/src/main/js/apps/project-permissions/permissions-footer.js deleted file mode 100644 index 1e247dccd01..00000000000 --- a/server/sonar-web/src/main/js/apps/project-permissions/permissions-footer.js +++ /dev/null @@ -1,49 +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 classNames from 'classnames'; -import React from 'react'; - -export default React.createClass({ - propTypes: { - count: React.PropTypes.number.isRequired, - total: React.PropTypes.number.isRequired, - loadMore: React.PropTypes.func.isRequired - }, - - handleLoadMore (e) { - e.preventDefault(); - this.props.loadMore(); - }, - - render() { - if (this.props.componentId) { - return null; - } - const hasMore = this.props.total > this.props.count; - const loadMoreLink = <a onClick={this.handleLoadMore} className="spacer-left" href="#">show more</a>; - const className = classNames('spacer-top note text-center', { 'new-loading': !this.props.ready }); - return ( - <footer className={className}> - {this.props.count}/{this.props.total} shown - {hasMore ? loadMoreLink : null} - </footer> - ); - } -}); 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 deleted file mode 100644 index c10323e8419..00000000000 --- a/server/sonar-web/src/main/js/apps/project-permissions/permissions.js +++ /dev/null @@ -1,52 +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 classNames from 'classnames'; -import React from 'react'; - -import PermissionsHeader from './permissions-header'; -import Project from './project'; - -export default React.createClass({ - propTypes: { - projects: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, - permissions: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, - permissionTemplates: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, - refresh: React.PropTypes.func.isRequired - }, - - render() { - const projects = this.props.projects.map(p => { - return <Project - key={p.id} - project={p} - permissionTemplates={this.props.permissionTemplates} - refresh={this.props.refresh}/>; - }); - const className = classNames( - 'data zebra permissions-table', - { 'new-loading': !this.props.ready }); - return ( - <table id="projects" className={className}> - <PermissionsHeader permissions={this.props.permissions}/> - <tbody>{projects}</tbody> - </table> - ); - } -}); diff --git a/server/sonar-web/src/main/js/apps/project-permissions/project.js b/server/sonar-web/src/main/js/apps/project-permissions/project.js deleted file mode 100644 index 2a3c77a4af5..00000000000 --- a/server/sonar-web/src/main/js/apps/project-permissions/project.js +++ /dev/null @@ -1,105 +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 React from 'react'; -import UsersView from './users-view'; -import GroupsView from './groups-view'; -import ApplyTemplateView from './apply-template-view'; -import { getComponentUrl } from '../../helpers/urls'; -import QualifierIcon from '../../components/shared/qualifier-icon'; - -export default React.createClass({ - propTypes: { - project: React.PropTypes.object.isRequired, - permissionTemplates: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, - refresh: React.PropTypes.func.isRequired - }, - - showGroups(permission, e) { - e.preventDefault(); - new GroupsView({ - permission, - project: this.props.project.id, - projectName: this.props.project.name, - refresh: this.props.refresh - }).render(); - }, - - showUsers(permission, e) { - e.preventDefault(); - new UsersView({ - permission, - project: this.props.project.id, - projectName: this.props.project.name, - refresh: this.props.refresh - }).render(); - }, - - applyTemplate(e) { - e.preventDefault(); - new ApplyTemplateView({ - permissionTemplates: this.props.permissionTemplates, - project: this.props.project, - refresh: this.props.refresh - }).render(); - }, - - render() { - const permissions = this.props.project.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.key)} 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.key)} className="icon-bullet-list" title="Update Users" - data-toggle="tooltip" href="#"></a> - </td> - </tr> - </tbody> - </table> - </td> - ); - }); - return ( - <tr> - <td> - <span className="little-spacer-right"> - <QualifierIcon qualifier={this.props.project.qualifier}/> - </span> - <a href={getComponentUrl(this.props.project.key)}>{this.props.project.name}</a> - </td> - {permissions} - <td className="thin nowrap text-right"> - <button onClick={this.applyTemplate} className="js-apply-template">Apply Template</button> - </td> - </tr> - ); - } -}); diff --git a/server/sonar-web/src/main/js/apps/project-permissions/qualifier-filter.js b/server/sonar-web/src/main/js/apps/project-permissions/qualifier-filter.js deleted file mode 100644 index 3056cdc2994..00000000000 --- a/server/sonar-web/src/main/js/apps/project-permissions/qualifier-filter.js +++ /dev/null @@ -1,45 +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 React from 'react'; -import { translate } from '../../helpers/l10n'; - -import RadioToggle from '../../components/controls/RadioToggle'; - -const rootQualifiersToOptions = (qualifiers) => { - return qualifiers.map(q => { - return { - value: q, - label: translate('qualifiers', q) - }; - }); -}; - -export const QualifierFilter = ({ rootQualifiers, filter, onFilter }) => { - const options = [{ value: '__ALL__', label: 'All' }, ...rootQualifiersToOptions(rootQualifiers)]; - - return ( - <div className="display-inline-block text-top nowrap big-spacer-right"> - <RadioToggle value={filter} - options={options} - name="qualifier" - onCheck={onFilter}/> - </div> - ); -}; diff --git a/server/sonar-web/src/main/js/apps/project-permissions/search.js b/server/sonar-web/src/main/js/apps/project-permissions/search.js deleted file mode 100644 index 1b1951504f1..00000000000 --- a/server/sonar-web/src/main/js/apps/project-permissions/search.js +++ /dev/null @@ -1,68 +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 { QualifierFilter } from './qualifier-filter'; - -export default React.createClass({ - propTypes: { - search: React.PropTypes.func.isRequired - }, - - componentWillMount () { - this.search = _.debounce(this.search, 250); - }, - - onSubmit(e) { - e.preventDefault(); - this.search(); - }, - - search() { - const q = this.refs.input.value; - this.props.search(q); - }, - - render() { - if (this.props.componentId) { - return null; - } - return ( - <div className="panel panel-vertical bordered-bottom spacer-bottom"> - - {this.props.rootQualifiers.length > 1 && <QualifierFilter filter={this.props.filter} - rootQualifiers={this.props.rootQualifiers} - onFilter={this.props.onFilter}/>} - - <form onSubmit={this.onSubmit} className="search-box display-inline-block text-top"> - <button className="search-box-submit button-clean"> - <i className="icon-search"></i> - </button> - <input onChange={this.search} - ref="input" - className="search-box-input" - type="search" - placeholder="Search"/> - </form> - </div> - ); - } -}); diff --git a/server/sonar-web/src/main/js/apps/project-permissions/templates/project-permissions-apply-template.hbs b/server/sonar-web/src/main/js/apps/project-permissions/templates/project-permissions-apply-template.hbs deleted file mode 100644 index 25f494340e0..00000000000 --- a/server/sonar-web/src/main/js/apps/project-permissions/templates/project-permissions-apply-template.hbs +++ /dev/null @@ -1,25 +0,0 @@ -<form id="project-permissions-apply-template-form" autocomplete="off"> - <div class="modal-head"> - {{#if project}} - <h2>Apply Permission Template to "{{project.name}}"</h2> - {{else}} - <h2>Bulk Apply Permission Template</h2> - {{/if}} - </div> - <div class="modal-body"> - <div class="js-modal-messages"></div> - - <div class="modal-field"> - <label for="project-permissions-template">Template<em class="mandatory">*</em></label> - <select id="project-permissions-template"> - {{#each permissionTemplates}} - <option value="{{id}}">{{name}}</option> - {{/each}} - </select> - </div> - </div> - <div class="modal-foot"> - <button id="project-permissions-apply-template">Apply</button> - <a href="#" class="js-modal-close">Cancel</a> - </div> -</form> diff --git a/server/sonar-web/src/main/js/apps/project-permissions/templates/project-permissions-groups.hbs b/server/sonar-web/src/main/js/apps/project-permissions/templates/project-permissions-groups.hbs deleted file mode 100644 index 68ceacf26b0..00000000000 --- a/server/sonar-web/src/main/js/apps/project-permissions/templates/project-permissions-groups.hbs +++ /dev/null @@ -1,10 +0,0 @@ -<div class="modal-head"> - <h2>Update Groups of "{{projectName}}"</h2> -</div> -<div class="modal-body"> - <div class="js-modal-messages"></div> - <div id="project-permissions-groups"></div> -</div> -<div class="modal-foot"> - <a href="#" class="js-modal-close">Done</a> -</div> diff --git a/server/sonar-web/src/main/js/apps/project-permissions/templates/project-permissions-users.hbs b/server/sonar-web/src/main/js/apps/project-permissions/templates/project-permissions-users.hbs deleted file mode 100644 index b21ac813bcc..00000000000 --- a/server/sonar-web/src/main/js/apps/project-permissions/templates/project-permissions-users.hbs +++ /dev/null @@ -1,10 +0,0 @@ -<div class="modal-head"> - <h2>Update Users of "{{projectName}}"</h2> -</div> -<div class="modal-body"> - <div class="js-modal-messages"></div> - <div id="project-permissions-users"></div> -</div> -<div class="modal-foot"> - <a href="#" class="js-modal-close">Done</a> -</div> diff --git a/server/sonar-web/src/main/js/apps/project-permissions/users-view.js b/server/sonar-web/src/main/js/apps/project-permissions/users-view.js deleted file mode 100644 index 04399fad78b..00000000000 --- a/server/sonar-web/src/main/js/apps/project-permissions/users-view.js +++ /dev/null @@ -1,70 +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 Modal from '../../components/common/modals'; -import '../../components/SelectList'; -import Template from './templates/project-permissions-users.hbs'; - -export default Modal.extend({ - template: Template, - - onRender () { - Modal.prototype.onRender.apply(this, arguments); - const searchUrl = window.baseUrl + '/api/permissions/users?ps=100&permission=' + this.options.permission + - '&projectId=' + this.options.project; - new window.SelectList({ - searchUrl, - el: this.$('#project-permissions-users'), - width: '100%', - readOnly: false, - focusSearch: false, - format (item) { - return `${item.name}<br><span class="note">${item.login}</span>`; - }, - queryParam: 'q', - selectUrl: window.baseUrl + '/api/permissions/add_user', - deselectUrl: window.baseUrl + '/api/permissions/remove_user', - extra: { - permission: this.options.permission, - projectId: this.options.project - }, - selectParameter: 'login', - selectParameterValue: 'login', - parse (r) { - this.more = false; - return r.users; - } - }); - }, - - onDestroy () { - if (this.options.refresh) { - this.options.refresh(); - } - Modal.prototype.onDestroy.apply(this, arguments); - }, - - serializeData () { - return _.extend(Modal.prototype.serializeData.apply(this, arguments), { - projectName: this.options.projectName - }); - } -}); - diff --git a/server/sonar-web/src/main/js/components/ui/Avatar.js b/server/sonar-web/src/main/js/components/ui/Avatar.js index f7b78320c85..b52d9ff23ef 100644 --- a/server/sonar-web/src/main/js/components/ui/Avatar.js +++ b/server/sonar-web/src/main/js/components/ui/Avatar.js @@ -19,11 +19,13 @@ */ import React from 'react'; import md5 from 'blueimp-md5'; +import classNames from 'classnames'; export default class Avatar extends React.Component { static propTypes = { email: React.PropTypes.string, - size: React.PropTypes.number.isRequired + size: React.PropTypes.number.isRequired, + className: React.PropTypes.string }; render () { @@ -37,8 +39,10 @@ export default class Avatar extends React.Component { .replace('{EMAIL_MD5}', emailHash) .replace('{SIZE}', this.props.size * 2); + const className = classNames(this.props.className, 'rounded'); + return ( - <img className="rounded" + <img className={className} src={url} width={this.props.size} height={this.props.size} diff --git a/server/sonar-web/src/main/less/components/search.less b/server/sonar-web/src/main/less/components/search.less index 3ff1f28a928..164052e30f4 100644 --- a/server/sonar-web/src/main/less/components/search.less +++ b/server/sonar-web/src/main/less/components/search.less @@ -46,3 +46,14 @@ top: 1px; } } + +.search-box-input-note { + position: absolute; + top: 100%; + left: 0; + line-height: 1; + color: #777; + font-size: @smallFontSize; + white-space: nowrap; +} + diff --git a/server/sonar-web/src/main/less/init/forms.less b/server/sonar-web/src/main/less/init/forms.less index d0cd92873eb..ffb3a31395a 100644 --- a/server/sonar-web/src/main/less/init/forms.less +++ b/server/sonar-web/src/main/less/init/forms.less @@ -137,8 +137,10 @@ input[type="submit"].button-red { .button-clean:focus { margin: 0; padding: 0; + line-height: 1; border: none; background: transparent; + box-shadow: none; color: @baseFontColor; } diff --git a/server/sonar-web/src/main/less/init/tables.less b/server/sonar-web/src/main/less/init/tables.less index d5802264ca9..905993a73c2 100644 --- a/server/sonar-web/src/main/less/init/tables.less +++ b/server/sonar-web/src/main/less/init/tables.less @@ -47,7 +47,7 @@ table.data > thead:after { table.data > thead > tr > th { vertical-align: top; - line-height: 24px; + line-height: 18px; padding: 8px 10px; border-bottom: 1px solid @barBorderColor; font-weight: 500; diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/project_roles/index.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/project_roles/index.html.erb index 2ffe436320a..df0126ffd09 100644 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/project_roles/index.html.erb +++ b/server/sonar-web/src/main/webapp/WEB-INF/app/views/project_roles/index.html.erb @@ -1,6 +1,3 @@ <% content_for :extra_script do %> - <script> - window.sonarqube.componentId = '<%= @project.uuid -%>'; - </script> <script src="<%= ApplicationController.root_context -%>/js/bundles/project-permissions.js?v=<%= sonar_version -%>"></script> <% end %> diff --git a/server/sonar-web/webpack.config.js b/server/sonar-web/webpack.config.js index c70942bef74..d64f64ab975 100644 --- a/server/sonar-web/webpack.config.js +++ b/server/sonar-web/webpack.config.js @@ -36,7 +36,7 @@ module.exports = { 'component-measures': './src/main/js/apps/component-measures/app.js', 'custom-measures': './src/main/js/apps/custom-measures/app.js', 'dashboard': './src/main/js/apps/dashboard/app.js', - 'global-permissions': './src/main/js/apps/global-permissions/app.js', + 'global-permissions': './src/main/js/apps/permissions/global/app.js', 'groups': './src/main/js/apps/groups/app.js', 'issues': './src/main/js/apps/issues/app.js', 'maintenance': './src/main/js/apps/maintenance/app.js', @@ -45,7 +45,7 @@ module.exports = { 'metrics': './src/main/js/apps/metrics/app.js', 'overview': './src/main/js/apps/overview/app.js', 'permission-templates': './src/main/js/apps/permission-templates/app.js', - 'project-permissions': './src/main/js/apps/project-permissions/app.js', + 'project-permissions': './src/main/js/apps/permissions/project/app.js', 'projects': './src/main/js/apps/projects/app.js', 'quality-gates': './src/main/js/apps/quality-gates/app.js', 'quality-profiles': './src/main/js/apps/quality-profiles/app.js', @@ -92,7 +92,10 @@ module.exports = { { test: require.resolve('jquery'), loader: 'expose?$!expose?jQuery' }, { test: require.resolve('underscore'), loader: 'expose?_' }, { test: require.resolve('backbone'), loader: 'expose?Backbone' }, - { test: require.resolve('backbone.marionette'), loader: 'expose?Marionette' }, + { + test: require.resolve('backbone.marionette'), + loader: 'expose?Marionette' + }, { test: require.resolve('d3'), loader: 'expose?d3' }, { test: require.resolve('react'), loader: 'expose?React' }, { test: require.resolve('react-dom'), loader: 'expose?ReactDOM' }, |