diff options
14 files changed, 542 insertions, 86 deletions
diff --git a/server/sonar-web/src/main/js/api/permissions.jsx b/server/sonar-web/src/main/js/api/permissions.jsx new file mode 100644 index 00000000000..fb565c22c32 --- /dev/null +++ b/server/sonar-web/src/main/js/api/permissions.jsx @@ -0,0 +1,92 @@ +function _request(options) { + let $ = jQuery; + return $.ajax(options); +} + +function _url(path) { + return window.baseUrl + path; +} + +function _typeError(method, message) { + throw new TypeError(`permissions#${method}: ${message}`); +} + + +export function getUsers(data) { + let url = _url('/api/permissions/users'); + return _request({ type: 'GET', url: url, data: 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'); + } + + let url = _url('/api/permissions/add_user'); + let data = { permission: permission, login: user }; + if (project) { + data.projectId = project; + } + return _request({ type: 'POST', url: url, data: 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'); + } + + let url = _url('/api/permissions/remove_user'); + let data = { permission: permission, login: user }; + if (project) { + data.projectId = project; + } + return _request({ type: 'POST', url: url, data: data }); +} + + +export function getGroups(data) { + let url = _url('/api/permissions/groups'); + return _request({ type: 'GET', url: url, data: 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'); + } + + let url = _url('/api/permissions/add_group'); + let data = { permission: permission, groupName: group }; + if (project) { + data.projectId = project; + } + return _request({ type: 'POST', url: url, data: data }); +} + + +export function revokeFromGroup(permission, group, project) { + if (typeof permission !== 'string' || !permission.length) { + return _typeError('revokeFromGroup', 'please provide permission'); + } + if (typeof group !== 'string' || !group.length) { + return _typeError('revokeFromGroup', 'please provide group name'); + } + + let url = _url('/api/permissions/remove_group'); + let data = { permission: permission, groupName: group }; + if (project) { + data.projectId = project; + } + return _request({ type: 'POST', url: url, data: data }); +} 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 index 5b25ec62db2..3e515d94031 100644 --- 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 @@ -1,36 +1,43 @@ define([ 'components/common/modals', - 'components/common/select-list', + 'react', + 'components/select-list/main', + '../../api/permissions', './templates' -], function (Modal) { +], function (Modal, React, SelectList, Permissions) { return Modal.extend({ template: Templates['global-permissions-groups'], onRender: function () { + var that = this; this._super(); - new window.SelectList({ - el: this.$('#global-permissions-groups'), - width: '100%', - readOnly: false, - focusSearch: false, - format: function (item) { - return item.name; + var props = { + loadItems: function (options, callback) { + var _data = { permission: that.options.permission, p: options.page, ps: 100 }; + options.query ? _.extend(_data, { q: options.query }) : _.extend(_data, { selected: options.selection }); + Permissions.getGroups(_data).done(function (r) { + var paging = _.defaults({}, r.paging, { total: 0, pageIndex: 1 }); + callback(r.groups, paging); + }); }, - queryParam: 'q', - searchUrl: baseUrl + '/api/permissions/groups?ps=100&permission=' + this.options.permission, - selectUrl: baseUrl + '/api/permissions/add_group', - deselectUrl: baseUrl + '/api/permissions/remove_group', - extra: { - permission: this.options.permission + renderItem: function (group) { + return group.name; }, - selectParameter: 'groupName', - selectParameterValue: 'name', - parse: function (r) { - this.more = false; - return r.groups; + getItemKey: function (group) { + return group.name; + }, + selectItem: function (group, callback) { + Permissions.grantToGroup(that.options.permission, group.name).done(callback); + }, + deselectItem: function (group, callback) { + Permissions.revokeFromGroup(that.options.permission, group.name).done(callback); } - }); + }; + React.render( + React.createElement(SelectList, props), + this.$('#global-permissions-groups')[0] + ); }, onDestroy: function () { 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 index b5660c650da..54c41342127 100644 --- 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 @@ -1,36 +1,43 @@ define([ 'components/common/modals', - 'components/common/select-list', + 'react', + 'components/select-list/main', + '../../api/permissions', './templates' -], function (Modal) { +], function (Modal, React, SelectList, Permissions) { return Modal.extend({ template: Templates['global-permissions-users'], onRender: function () { + var that = this; this._super(); - new window.SelectList({ - el: this.$('#global-permissions-users'), - width: '100%', - readOnly: false, - focusSearch: false, - format: function (item) { - return item.name + '<br><span class="note">' + item.login + '</span>'; + var props = { + loadItems: function (options, callback) { + var data = { permission: that.options.permission, p: options.page, ps: 100 }; + options.query ? _.extend(data, { q: options.query }) : _.extend(data, { selected: options.selection }); + Permissions.getUsers(data).done(function (r) { + var paging = _.defaults({}, r.paging, { total: 0, pageIndex: 1 }); + callback(r.users, paging); + }); }, - queryParam: 'q', - searchUrl: baseUrl + '/api/permissions/users?ps=100&permission=' + this.options.permission, - selectUrl: baseUrl + '/api/permissions/add_user', - deselectUrl: baseUrl + '/api/permissions/remove_user', - extra: { - permission: this.options.permission + renderItem: function (user) { + return user.name + '<br><span class="note">' + user.login + '</span>'; }, - selectParameter: 'login', - selectParameterValue: 'login', - parse: function (r) { - this.more = false; - return r.users; + getItemKey: function (user) { + return user.login; + }, + selectItem: function (user, callback) { + Permissions.grantToUser(that.options.permission, user.login).done(callback); + }, + deselectItem: function (user, callback) { + Permissions.revokeFromUser(that.options.permission, user.login).done(callback); } - }); + }; + React.render( + React.createElement(SelectList, props), + this.$('#global-permissions-users')[0] + ); }, onDestroy: function () { 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 index ba98ca7e525..0c49e3c5584 100644 --- 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 @@ -1,37 +1,43 @@ define([ 'components/common/modals', - 'components/common/select-list', + 'react', + 'components/select-list/main', + '../../api/permissions', './templates' -], function (Modal) { +], function (Modal, React, SelectList, Permissions) { return Modal.extend({ template: Templates['project-permissions-groups'], onRender: function () { + var that = this; this._super(); - new window.SelectList({ - el: this.$('#project-permissions-groups'), - width: '100%', - readOnly: false, - focusSearch: false, - format: function (item) { - return item.name; + var props = { + loadItems: function (options, callback) { + var _data = { permission: that.options.permission, projectId: that.options.project, p: options.page, ps: 100 }; + options.query ? _.extend(_data, { q: options.query }) : _.extend(_data, { selected: options.selection }); + Permissions.getGroups(_data).done(function (r) { + var paging = _.defaults({}, r.paging, { total: 0, pageIndex: 1 }); + callback(r.groups, paging); + }); }, - queryParam: 'q', - searchUrl: baseUrl + '/api/permissions/groups?ps=100&permission=' + this.options.permission + '&projectId=' + this.options.project, - selectUrl: baseUrl + '/api/permissions/add_group', - deselectUrl: baseUrl + '/api/permissions/remove_group', - extra: { - permission: this.options.permission, - projectId: this.options.project + renderItem: function (group) { + return group.name; }, - selectParameter: 'groupName', - selectParameterValue: 'name', - parse: function (r) { - this.more = false; - return r.groups; + getItemKey: function (group) { + return group.name; + }, + selectItem: function (group, callback) { + Permissions.grantToGroup(that.options.permission, group.name, that.options.project).done(callback); + }, + deselectItem: function (group, callback) { + Permissions.revokeFromGroup(that.options.permission, group.name, that.options.project).done(callback); } - }); + }; + React.render( + React.createElement(SelectList, props), + this.$('#project-permissions-groups')[0] + ); }, onDestroy: function () { 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 index 6a715835ba7..0ed4f2cc000 100644 --- 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 @@ -1,37 +1,43 @@ define([ 'components/common/modals', - 'components/common/select-list', + 'react', + 'components/select-list/main', + '../../api/permissions', './templates' -], function (Modal) { +], function (Modal, React, SelectList, Permissions) { return Modal.extend({ template: Templates['project-permissions-users'], onRender: function () { + var that = this; this._super(); - new window.SelectList({ - el: this.$('#project-permissions-users'), - width: '100%', - readOnly: false, - focusSearch: false, - format: function (item) { - return item.name + '<br><span class="note">' + item.login + '</span>'; + var props = { + loadItems: function (options, callback) { + var data = { permission: that.options.permission, projectId: that.options.project, p: options.page, ps: 100 }; + options.query ? _.extend(data, { q: options.query }) : _.extend(data, { selected: options.selection }); + Permissions.getUsers(data).done(function (r) { + var paging = _.defaults({}, r.paging, { total: 0, pageIndex: 1 }); + callback(r.users, paging); + }); }, - queryParam: 'q', - searchUrl: baseUrl + '/api/permissions/users?ps=100&permission=' + this.options.permission + '&projectId=' + this.options.project, - selectUrl: baseUrl + '/api/permissions/add_user', - deselectUrl: baseUrl + '/api/permissions/remove_user', - extra: { - permission: this.options.permission, - projectId: this.options.project + renderItem: function (user) { + return user.name + '<br><span class="note">' + user.login + '</span>'; }, - selectParameter: 'login', - selectParameterValue: 'login', - parse: function (r) { - this.more = false; - return r.users; + getItemKey: function (user) { + return user.login; + }, + selectItem: function (user, callback) { + Permissions.grantToUser(that.options.permission, user.login, that.options.project).done(callback); + }, + deselectItem: function (user, callback) { + Permissions.revokeFromUser(that.options.permission, user.login, that.options.project).done(callback); } - }); + }; + React.render( + React.createElement(SelectList, props), + this.$('#project-permissions-users')[0] + ); }, onDestroy: function () { diff --git a/server/sonar-web/src/main/js/components/select-list/controls.jsx b/server/sonar-web/src/main/js/components/select-list/controls.jsx new file mode 100644 index 00000000000..47c5bb46531 --- /dev/null +++ b/server/sonar-web/src/main/js/components/select-list/controls.jsx @@ -0,0 +1,53 @@ +import React from 'react'; +import classNames from '../../libs/third-party/classNames'; +import RadioToggle from '../shared/radio-toggle'; + +export default React.createClass({ + componentWillMount() { + this.search = _.debounce(this.search, 100); + }, + + search() { + let query = React.findDOMNode(this.refs.search).value; + this.props.search(query); + }, + + onCheck(value) { + switch (value) { + case 'selected': + this.props.loadSelected(); + break; + case 'deselected': + this.props.loadDeselected(); + break; + default: + this.props.loadAll() + } + }, + + render() { + let selectionDisabled = !!this.props.query; + + let selectionOptions = [ + { value: 'selected', label: 'Selected' }, + { value: 'deselected', label: 'Not Selected' }, + { value: 'all', label: 'All' } + ]; + + return ( + <div className="select-list-control"> + <div className="pull-left"> + <RadioToggle + name="select-list-selection" + options={selectionOptions} + onCheck={this.onCheck} + value={this.props.selection} + disabled={selectionDisabled}/> + </div> + <div className="pull-right"> + <input onChange={this.search} ref="search" type="search" placeholder="Search" initialValue={this.props.query}/> + </div> + </div> + ); + } +}); diff --git a/server/sonar-web/src/main/js/components/select-list/footer.jsx b/server/sonar-web/src/main/js/components/select-list/footer.jsx new file mode 100644 index 00000000000..b60e32e9a73 --- /dev/null +++ b/server/sonar-web/src/main/js/components/select-list/footer.jsx @@ -0,0 +1,32 @@ +import React from 'react'; +import Checkbox from '../shared/checkbox'; + +export default React.createClass({ + propTypes: { + count: React.PropTypes.number.isRequired, + total: React.PropTypes.number.isRequired, + loadMore: React.PropTypes.func.isRequired + }, + + loadMore(e) { + e.preventDefault(); + this.props.loadMore(); + }, + + renderLoadMoreLink() { + let hasMore = this.props.total > this.props.count; + if (!hasMore) { + return null; + } + return <a onClick={this.loadMore} className="spacer-left" href="#">show more</a>; + }, + + render() { + return ( + <footer className="spacer-top note text-center"> + {this.props.count}/{this.props.total} shown + {this.renderLoadMoreLink()} + </footer> + ); + } +}); diff --git a/server/sonar-web/src/main/js/components/select-list/item.jsx b/server/sonar-web/src/main/js/components/select-list/item.jsx new file mode 100644 index 00000000000..2f0fe6da345 --- /dev/null +++ b/server/sonar-web/src/main/js/components/select-list/item.jsx @@ -0,0 +1,27 @@ +import React from 'react'; +import Checkbox from '../shared/checkbox'; + +export default React.createClass({ + propTypes: { + item: React.PropTypes.any.isRequired, + renderItem: React.PropTypes.func.isRequired, + selectItem: React.PropTypes.func.isRequired, + deselectItem: React.PropTypes.func.isRequired + }, + + onCheck(checked) { + checked ? this.props.selectItem(this.props.item) : this.props.deselectItem(this.props.item); + }, + + render() { + let renderedItem = this.props.renderItem(this.props.item); + return ( + <li className="panel panel-vertical"> + <div className="display-inline-block text-middle spacer-right"> + <Checkbox onCheck={this.onCheck} initiallyChecked={!!this.props.item.selected}/> + </div> + <div className="display-inline-block text-middle" dangerouslySetInnerHTML={{ __html: renderedItem }}/> + </li> + ); + } +}); diff --git a/server/sonar-web/src/main/js/components/select-list/list.jsx b/server/sonar-web/src/main/js/components/select-list/list.jsx new file mode 100644 index 00000000000..f8182f5c7aa --- /dev/null +++ b/server/sonar-web/src/main/js/components/select-list/list.jsx @@ -0,0 +1,22 @@ +import React from 'react'; +import Item from './item'; + +export default React.createClass({ + propTypes: { + items: React.PropTypes.array.isRequired, + renderItem: React.PropTypes.func.isRequired, + getItemKey: React.PropTypes.func.isRequired, + selectItem: React.PropTypes.func.isRequired, + deselectItem: React.PropTypes.func.isRequired + }, + + render() { + let renderedItems = this.props.items.map(item => { + let key = this.props.getItemKey(item); + return <Item key={key} {...this.props} item={item} />; + }); + return ( + <ul>{renderedItems}</ul> + ); + } +}); diff --git a/server/sonar-web/src/main/js/components/select-list/main.jsx b/server/sonar-web/src/main/js/components/select-list/main.jsx new file mode 100644 index 00000000000..bc043a8e0e9 --- /dev/null +++ b/server/sonar-web/src/main/js/components/select-list/main.jsx @@ -0,0 +1,82 @@ +import React from 'react'; +import Controls from './controls'; +import List from './list'; +import Footer from './footer'; + +export default React.createClass({ + propTypes: { + loadItems: React.PropTypes.func.isRequired, + renderItem: React.PropTypes.func.isRequired, + getItemKey: React.PropTypes.func.isRequired, + selectItem: React.PropTypes.func.isRequired, + deselectItem: React.PropTypes.func.isRequired + }, + + + getInitialState() { + return { items: [], total: 0, selection: 'selected', query: null }; + }, + + componentDidMount() { + this.loadItems(); + }, + + loadItems() { + let options = { + selection: this.state.selection, + query: this.state.query, + page: 1 + }; + this.props.loadItems(options, (items, paging) => { + this.setState({ items: items, total: paging.total, page: paging.pageIndex }); + }); + }, + + loadMoreItems() { + let options = { + selection: this.state.selection, + query: this.state.query, + page: this.state.page + 1 + }; + this.props.loadItems(options, (items, paging) => { + let newItems = [].concat(this.state.items, items); + this.setState({ items: newItems, total: paging.total, page: paging.pageIndex }); + }); + }, + + loadSelected() { + this.setState({ selection: 'selected', query: null }, this.loadItems); + }, + + loadDeselected() { + this.setState({ selection: 'deselected', query: null }, this.loadItems); + }, + + loadAll() { + this.setState({ selection: 'all', query: null }, this.loadItems); + }, + + search(query) { + this.setState({ query: query }, this.loadItems); + }, + + render() { + return ( + <div className="select-list-container"> + <Controls + selection={this.state.selection} + query={this.state.query} + loadSelected={this.loadSelected} + loadDeselected={this.loadDeselected} + loadAll={this.loadAll} + search={this.search}/> + + <div className="select-list-wrapper"> + <List {...this.props} items={this.state.items}/> + </div> + + <Footer count={this.state.items.length} total={this.state.total} loadMore={this.loadMoreItems}/> + </div> + ); + } +}); diff --git a/server/sonar-web/src/main/js/components/shared/checkbox.jsx b/server/sonar-web/src/main/js/components/shared/checkbox.jsx new file mode 100644 index 00000000000..8504da5038d --- /dev/null +++ b/server/sonar-web/src/main/js/components/shared/checkbox.jsx @@ -0,0 +1,29 @@ +import React from 'react'; + +export default React.createClass({ + propTypes: { + onCheck: React.PropTypes.func.isRequired, + initiallyChecked: React.PropTypes.bool + }, + + getInitialState() { + return { checked: this.props.initiallyChecked || false }; + }, + + componentWillReceiveProps(nextProps) { + if (nextProps.initiallyChecked != null) { + this.setState({ checked: nextProps.initiallyChecked }); + } + }, + + toggle(e) { + e.preventDefault(); + this.props.onCheck(!this.state.checked); + this.setState({ checked: !this.state.checked }); + }, + + render() { + const className = this.state.checked ? 'icon-checkbox icon-checkbox-checked' : 'icon-checkbox'; + return <a onClick={this.toggle} className={className} href="#"/>; + } +}); diff --git a/server/sonar-web/src/main/js/components/shared/radio-toggle.jsx b/server/sonar-web/src/main/js/components/shared/radio-toggle.jsx new file mode 100644 index 00000000000..541a906c181 --- /dev/null +++ b/server/sonar-web/src/main/js/components/shared/radio-toggle.jsx @@ -0,0 +1,37 @@ +import React from 'react'; + +export default React.createClass({ + propTypes: { + options: React.PropTypes.array.isRequired, + name: React.PropTypes.string.isRequired, + onCheck: React.PropTypes.func.isRequired + }, + + getDefaultProps: function () { + return { disabled: false, value: null }; + }, + + onChange(e) { + let newValue = e.currentTarget.value; + this.props.onCheck(newValue); + }, + + renderOption(option) { + let checked = option.value === this.props.value; + let htmlId = this.props.name + '__' + option.value; + return ( + <li key={option.value}> + <input onChange={this.onChange} type="radio" name={this.props.name} value={option.value} id={htmlId} + checked={checked} disabled={this.props.disabled}/> + <label htmlFor={htmlId}>{option.label}</label> + </li> + ); + }, + + render() { + let options = this.props.options.map(this.renderOption); + return ( + <ul className="radio-toggle">{options}</ul> + ); + } +}); diff --git a/server/sonar-web/src/main/js/libs/third-party/classNames.js b/server/sonar-web/src/main/js/libs/third-party/classNames.js new file mode 100644 index 00000000000..afb8eed33a7 --- /dev/null +++ b/server/sonar-web/src/main/js/libs/third-party/classNames.js @@ -0,0 +1,49 @@ +/*! + Copyright (c) 2015 Jed Watson. + Licensed under the MIT License (MIT), see + http://jedwatson.github.io/classnames +*/ + +(function () { + 'use strict'; + + function classNames () { + + var classes = ''; + + for (var i = 0; i < arguments.length; i++) { + var arg = arguments[i]; + if (!arg) continue; + + var argType = typeof arg; + + if ('string' === argType || 'number' === argType) { + classes += ' ' + arg; + + } else if (Array.isArray(arg)) { + classes += ' ' + classNames.apply(null, arg); + + } else if ('object' === argType) { + for (var key in arg) { + if (arg.hasOwnProperty(key) && arg[key]) { + classes += ' ' + key; + } + } + } + } + + return classes.substr(1); + } + + if (typeof module !== 'undefined' && module.exports) { + module.exports = classNames; + } else if (typeof define === 'function' && typeof define.amd === 'object' && define.amd){ + // AMD. Register as an anonymous module. + define(function () { + return classNames; + }); + } else { + window.classNames = classNames; + } + +}()); diff --git a/server/sonar-web/src/main/less/components/select-list.less b/server/sonar-web/src/main/less/components/select-list.less index 25b35d46c71..98c07e2b2cd 100644 --- a/server/sonar-web/src/main/less/components/select-list.less +++ b/server/sonar-web/src/main/less/components/select-list.less @@ -216,3 +216,10 @@ float: left; margin-left: 20px; } + +.select-list-wrapper { + height: 30vw; + border-top: 1px solid @barBorderColor; + border-bottom: 1px solid @barBorderColor; + overflow: auto; +} |