From e2ad2fd023e2989f882b5a393de105dcd2ab6512 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E9=92=A6=E4=BA=AE?= Date: Wed, 27 Feb 2019 19:44:22 +0800 Subject: [PATCH] Org user page (#2941) * init org user * optimized code style * freezed item * update select-ediotr style * optimized code style * add state --- frontend/config/webpack.config.dev.js | 5 + frontend/config/webpack.config.prod.js | 1 + frontend/package-lock.json | 20 +-- frontend/package.json | 2 +- .../components/dialog/org-add-admin-dialog.js | 91 ++++++++++ .../components/dialog/org-add-user-dialog.js | 146 ++++++++++++++++ .../select-editor/user-status-editor.js | 43 +++++ frontend/src/css/layout.css | 4 - frontend/src/css/org-admin-paginator.css | 9 + frontend/src/css/select-editor.css | 19 ++- frontend/src/models/org-user.js | 21 +++ frontend/src/pages/org-admin/index.js | 82 +++++++++ frontend/src/pages/org-admin/main-panel.js | 30 ++++ .../src/pages/org-admin/org-admin-list.js | 125 ++++++++++++++ frontend/src/pages/org-admin/org-user-item.js | 160 ++++++++++++++++++ .../src/pages/org-admin/org-users-list.js | 142 ++++++++++++++++ frontend/src/pages/org-admin/org-users.js | 58 +++++++ frontend/src/pages/org-admin/side-panel.js | 78 +++++++++ frontend/src/utils/constants.js | 3 + media/css/seahub_react.css | 1 + seahub/api2/permissions.py | 9 + 21 files changed, 1033 insertions(+), 16 deletions(-) create mode 100644 frontend/src/components/dialog/org-add-admin-dialog.js create mode 100644 frontend/src/components/dialog/org-add-user-dialog.js create mode 100644 frontend/src/components/select-editor/user-status-editor.js create mode 100644 frontend/src/css/org-admin-paginator.css create mode 100644 frontend/src/models/org-user.js create mode 100644 frontend/src/pages/org-admin/index.js create mode 100644 frontend/src/pages/org-admin/main-panel.js create mode 100644 frontend/src/pages/org-admin/org-admin-list.js create mode 100644 frontend/src/pages/org-admin/org-user-item.js create mode 100644 frontend/src/pages/org-admin/org-users-list.js create mode 100644 frontend/src/pages/org-admin/org-users.js create mode 100644 frontend/src/pages/org-admin/side-panel.js diff --git a/frontend/config/webpack.config.dev.js b/frontend/config/webpack.config.dev.js index 36b0349d94..d161464859 100644 --- a/frontend/config/webpack.config.dev.js +++ b/frontend/config/webpack.config.dev.js @@ -113,6 +113,11 @@ module.exports = { require.resolve('./polyfills'), require.resolve('react-dev-utils/webpackHotDevClient'), paths.appSrc + "/view-file-xmind.js", + ], + orgAdmin: [ + require.resolve('./polyfills'), + require.resolve('react-dev-utils/webpackHotDevClient'), + paths.appSrc + "/pages/org-admin", ] }, diff --git a/frontend/config/webpack.config.prod.js b/frontend/config/webpack.config.prod.js index f74a890f3b..c8d44657b4 100644 --- a/frontend/config/webpack.config.prod.js +++ b/frontend/config/webpack.config.prod.js @@ -71,6 +71,7 @@ module.exports = { viewFileText: [require.resolve('./polyfills'), paths.appSrc + "/view-file-text.js"], viewFileImage: [require.resolve('./polyfills'), paths.appSrc + "/view-file-image.js"], viewFileXmind: [require.resolve('./polyfills'), paths.appSrc + "/view-file-xmind.js"], + orgAdmin: [require.resolve('./polyfills'), paths.appSrc + "/pages/org-admin"], }, output: { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d50afc24ab..3b4c31559d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -640,7 +640,7 @@ }, "axios": { "version": "0.18.0", - "resolved": "http://registry.npmjs.org/axios/-/axios-0.18.0.tgz", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.0.tgz", "integrity": "sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=", "requires": { "follow-redirects": "^1.3.0", @@ -5318,7 +5318,7 @@ }, "git-up": { "version": "1.2.1", - "resolved": "http://registry.npmjs.org/git-up/-/git-up-1.2.1.tgz", + "resolved": "https://registry.npmjs.org/git-up/-/git-up-1.2.1.tgz", "integrity": "sha1-JkSAoAax2EJhrB/gmjpRacV+oZ0=", "requires": { "is-ssh": "^1.0.0", @@ -5327,7 +5327,7 @@ }, "git-url-parse": { "version": "5.0.1", - "resolved": "http://registry.npmjs.org/git-url-parse/-/git-url-parse-5.0.1.tgz", + "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-5.0.1.tgz", "integrity": "sha1-/j15xnRq4FBIz6UIyB553du6OEM=", "requires": { "git-up": "^1.0.0" @@ -7927,7 +7927,7 @@ }, "node-status-codes": { "version": "1.0.0", - "resolved": "http://registry.npmjs.org/node-status-codes/-/node-status-codes-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/node-status-codes/-/node-status-codes-1.0.0.tgz", "integrity": "sha1-WuVUHQJGRdMqWPzdyc7s6nrjrC8=" }, "noop6": { @@ -8234,7 +8234,7 @@ }, "package.json": { "version": "2.0.1", - "resolved": "http://registry.npmjs.org/package.json/-/package.json-2.0.1.tgz", + "resolved": "https://registry.npmjs.org/package.json/-/package.json-2.0.1.tgz", "integrity": "sha1-+IYFnSpJ7QduZIg2ldc7K0bSHW0=", "requires": { "git-package-json": "^1.4.0", @@ -8244,7 +8244,7 @@ "dependencies": { "got": { "version": "5.7.1", - "resolved": "http://registry.npmjs.org/got/-/got-5.7.1.tgz", + "resolved": "https://registry.npmjs.org/got/-/got-5.7.1.tgz", "integrity": "sha1-X4FjWmHkplifGAVp6k44FoClHzU=", "requires": { "create-error-class": "^3.0.1", @@ -8266,7 +8266,7 @@ }, "package-json": { "version": "2.4.0", - "resolved": "http://registry.npmjs.org/package-json/-/package-json-2.4.0.tgz", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-2.4.0.tgz", "integrity": "sha1-DRW9Z9HLvduyyiIv8u24a8sxqLs=", "requires": { "got": "^5.0.0", @@ -10932,9 +10932,9 @@ } }, "seafile-js": { - "version": "0.2.64", - "resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.64.tgz", - "integrity": "sha512-gaurvv8Gwq1IjXkHh1BufbeQxkmBRzxpNt/TqzOFWwBG2xsUW878T7HxiO+uw6amsc+K7PjYk2BVVmo29MgHqg==", + "version": "0.2.66", + "resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.66.tgz", + "integrity": "sha512-a51numCHkkMzNSp/7HpC0o/WYRF2m3+1g4yRPqASEnVXRSiZHiHY1fSR0W5eLmDqAmMoYiWdk99Y+kdjfhxb4A==", "requires": { "axios": "^0.18.0", "form-data": "^2.3.2", diff --git a/frontend/package.json b/frontend/package.json index 6746c42b84..429dce9db8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -34,7 +34,7 @@ "react-responsive": "^6.1.1", "react-select": "^2.4.1", "reactstrap": "^6.4.0", - "seafile-js": "^0.2.64", + "seafile-js": "^0.2.66", "socket.io-client": "^2.2.0", "sw-precache-webpack-plugin": "0.11.4", "unified": "^7.0.0", diff --git a/frontend/src/components/dialog/org-add-admin-dialog.js b/frontend/src/components/dialog/org-add-admin-dialog.js new file mode 100644 index 0000000000..7376bef4d7 --- /dev/null +++ b/frontend/src/components/dialog/org-add-admin-dialog.js @@ -0,0 +1,91 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import AsyncSelect from 'react-select/lib/Async'; +import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'; +import { gettext } from '../../utils/constants'; +import { seafileAPI } from '../../utils/seafile-api'; + +const propTypes = { + toggle: PropTypes.func.isRequired, + addOrgAdmin: PropTypes.func.isRequired, +}; + +class AddOrgAdminDialog extends React.Component { + constructor(props) { + super(props); + this.state = { + selectedOption: null + }; + this.options = []; + } + + loadOptions = (value, callback) => { + if (value.trim().length > 0) { + seafileAPI.searchUsers(value.trim()).then((res) => { + this.options = []; + for (let i = 0 ; i < res.data.users.length; i++) { + let obj = {}; + obj.value = res.data.users[i].name; + obj.email = res.data.users[i].email; + obj.label = + + Avatar + {res.data.users[i].name} + ; + this.options.push(obj); + } + callback(this.options); + }); + } + } + + handleSelectChange = (option) => { + this.setState({selectedOption: option}); + this.options = []; + } + + addOrgAdmin = () => { + let users = []; + if (this.state.selectedOption && this.state.selectedOption.length > 0 ) { + for (let i = 0; i < this.state.selectedOption.length; i ++) { + users[i] = this.state.selectedOption[i].email; + } + } + this.props.addOrgAdmin(users) + } + + toggle = () => { + this.props.toggle(); + } + + render() { + return ( + + {gettext('Add Admins')} + + + + + + + + + ); + } +} + +AddOrgAdminDialog.propTypes = propTypes; + +export default AddOrgAdminDialog; diff --git a/frontend/src/components/dialog/org-add-user-dialog.js b/frontend/src/components/dialog/org-add-user-dialog.js new file mode 100644 index 0000000000..81bff95d70 --- /dev/null +++ b/frontend/src/components/dialog/org-add-user-dialog.js @@ -0,0 +1,146 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button, Modal, Input, ModalHeader, ModalBody, Label, Form, InputGroup, InputGroupAddon, FormGroup } from 'reactstrap'; +import { gettext } from '../../utils/constants'; + +const propTypes = { + toggle: PropTypes.func.isRequired, + handleSubmit: PropTypes.func.isRequired, +}; + +class AddOrgUserDialog extends React.Component { + constructor(props) { + super(props); + this.state = { + isPasswordVisible: true, + email: '', + name: '', + password: '', + passwdnew: '', + errMessage: '' + }; + } + + handleSubmit = () => { + let isValid = this.validateInputParams(); + if (isValid) { + let { email, name, password } = this.state; + this.props.handleSubmit(email, name, password); + } + } + + handleKeyPress = (e) => { + e.preventDefault(); + if (e.key == 'Enter') { + this.handleSubmit(e); + } + }; + + togglePasswordVisible = () => { + this.setState({ + isPasswordVisible: !this.state.isPasswordVisible + }); + } + + generatePassword = () => { + let val = Math.random().toString(36).substr(5); + this.setState({ + password: val, + passwdnew: val + }); + } + + inputEmail = (e) => { + let email = e.target.value.trim(); + this.setState({email: email}); + } + + inputName = (e) => { + let name = e.target.value.trim(); + this.setState({name: name}); + } + + + inputPassword = (e) => { + let passwd = e.target.value.trim(); + this.setState({password: passwd}); + } + + inputPasswordNew = (e) => { + let passwd = e.target.value.trim(); + this.setState({passwdnew: passwd}); + } + + toggle = () => { + this.props.toggle(); + }; + + validateInputParams() { + let errMessage = ''; + let email = this.state.email; + if (!email.length) { + errMessage = 'email is required'; + this.setState({errMessage: errMessage}); + return false; + } + + let password1 = this.state.password; + let password2 = this.state.passwdnew; + if (!password1.length) { + errMessage = 'Please enter password'; + this.setState({errMessage: errMessage}); + return false; + } + if (!password2.length) { + errMessage = 'Please enter the password again'; + this.setState({errMessage: errMessage}); + return false; + } + if (password1 !== password2) { + errMessage = 'Passwords don\'t match'; + this.setState({errMessage: errMessage}); + return false; + } + return true; + } + + render() { + return ( + + {gettext('Add User')} + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ ); + } +} + +AddOrgUserDialog.propTypes = propTypes; + +export default AddOrgUserDialog; diff --git a/frontend/src/components/select-editor/user-status-editor.js b/frontend/src/components/select-editor/user-status-editor.js new file mode 100644 index 0000000000..661eaeeee1 --- /dev/null +++ b/frontend/src/components/select-editor/user-status-editor.js @@ -0,0 +1,43 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { gettext } from '../../utils/constants'; +import SelectEditor from './select-editor'; + +const propTypes = { + isTextMode: PropTypes.bool.isRequired, + isEditIconShow: PropTypes.bool.isRequired, + statusArray: PropTypes.array.isRequired, + currentStatus: PropTypes.string.isRequired, + onStatusChanged: PropTypes.func.isRequired +}; + +class UserStatusEditor extends React.Component { + + translateStatus = (userStatus) => { + if (userStatus === 'active') { + return gettext('Active'); + } + + if (userStatus === 'inactive') { + return gettext('Inactive'); + } + } + + render() { + return ( + + ); + } + +} + +UserStatusEditor.propTypes = propTypes; + +export default UserStatusEditor; diff --git a/frontend/src/css/layout.css b/frontend/src/css/layout.css index 0790e949fa..e80bdf191a 100644 --- a/frontend/src/css/layout.css +++ b/frontend/src/css/layout.css @@ -129,10 +129,6 @@ border-radius: 2px; } -.cur-view-content .permission-editor-select .permission-editor__control { - height: 24px; - min-height: 24px; -} .cur-view-detail { flex: 0 0 20rem; display: flex; diff --git a/frontend/src/css/org-admin-paginator.css b/frontend/src/css/org-admin-paginator.css new file mode 100644 index 0000000000..4d6b43b9c4 --- /dev/null +++ b/frontend/src/css/org-admin-paginator.css @@ -0,0 +1,9 @@ +.paginator { + text-align: center; + margin: 10px 0; + font-size: 14px; +} + +.cur-view-path.org-user-nav { + padding: 0 1rem; +} diff --git a/frontend/src/css/select-editor.css b/frontend/src/css/select-editor.css index 2d90bd61f2..8b695be7d2 100644 --- a/frontend/src/css/select-editor.css +++ b/frontend/src/css/select-editor.css @@ -13,4 +13,21 @@ } .permission-editor .permission-editor__control .permission-editor-explanation { display: none; -} \ No newline at end of file +} + +.cur-view-content .permission-editor-select .permission-editor__control, +.cur-view-content .permission-editor-select .permission-editor__control div, +.cur-view-content .permission-editor-select .permission-editor__control .permission-editor__input, +.cur-view-content .permission-editor-select .permission-editor__indicators { + height: 1.5rem; + min-height: 1.5rem; +} + +.cur-view-content .permission-editor-select .permission-editor__value-container div:nth-child(2) { + margin: 0; + padding: 0; +} + +.cur-view-content .permission-editor-select .permission-editor__indicators .permission-editor__indicator { + padding: 0 0.5rem; +} diff --git a/frontend/src/models/org-user.js b/frontend/src/models/org-user.js new file mode 100644 index 0000000000..5efcbff769 --- /dev/null +++ b/frontend/src/models/org-user.js @@ -0,0 +1,21 @@ +import { Utils } from '../utils/utils'; +import { lang } from '../utils/constants'; +import moment from 'moment'; + +moment.locale(lang); + +class OrgUserInfo { + constructor(object) { + this.id = object.id; + this.name = object.name; + this.email = object.email; + this.contact_email = object.owner_contact_email; + this.is_active = object.is_active; + this.quota = object.quota > 0 ? Utils.bytesToSize(object.quota) : ''; + this.self_usage = Utils.bytesToSize(object.self_usage); + this.last_login = object.last_login ? moment(object.last_login).fromNow() : '--'; + this.ctime = moment(object.ctime).format('YYYY-MM-DD HH:mm:ss'); + } +} + +export default OrgUserInfo; diff --git a/frontend/src/pages/org-admin/index.js b/frontend/src/pages/org-admin/index.js new file mode 100644 index 0000000000..b0b8b6f66b --- /dev/null +++ b/frontend/src/pages/org-admin/index.js @@ -0,0 +1,82 @@ +// Import React! +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Router } from '@reach/router'; +import { siteRoot } from '../../utils/constants'; +import SidePanel from './side-panel'; +import MainPanel from './main-panel'; +import OrgUsers from './org-users'; +import OrgUsersList from './org-users-list'; +import OrgAdminList from './org-admin-list'; + +import '../../assets/css/fa-solid.css'; +import '../../assets/css/fa-regular.css'; +import '../../assets/css/fontawesome.css'; +import '../../css/layout.css'; +import '../../css/toolbar.css'; + +class Org extends React.Component { + constructor(props) { + super(props); + this.state = { + isSidePanelClosed: false, + isShowAddOrgUserDialog: false, + isShowAddOrgAdminDialog: false, + currentTab: 'users', + }; + } + + componentDidMount() { + let href = window.location.href.split('/'); + let currentTab = href[href.length - 2]; + if (currentTab == 'useradmin') { + currentTab = 'users'; + } + this.setState({currentTab: currentTab}); + } + + onCloseSidePanel = () => { + this.setState({isSidePanelClosed: !this.state.isSidePanelClosed}); + } + + tabItemClick = (param) => { + this.setState({currentTab: param}); + } + + toggleAddOrgUser = () => { + this.setState({isShowAddOrgUserDialog: !this.state.isShowAddOrgUserDialog}); + } + + toggleAddOrgAdmin = () => { + this.setState({isShowAddOrgAdminDialog: !this.state.isShowAddOrgAdminDialog}); + } + + render() { + + let { isSidePanelClosed, currentTab, isShowAddOrgUserDialog, isShowAddOrgAdminDialog } = this.state; + return ( +
+ + + + + + + + + +
+ ); + } +} + +ReactDOM.render( + , + document.getElementById('wrapper') +); diff --git a/frontend/src/pages/org-admin/main-panel.js b/frontend/src/pages/org-admin/main-panel.js new file mode 100644 index 0000000000..a3a1923742 --- /dev/null +++ b/frontend/src/pages/org-admin/main-panel.js @@ -0,0 +1,30 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import Account from '../../components/common/account'; + +const propTypes = { + children: PropTypes.object.isRequired, +}; + +class MainPanel extends Component { + + render() { + return ( +
+
+
+ +
+
+ +
+
+ {this.props.children} +
+ ); + } +} + +MainPanel.propTypes = propTypes; + +export default MainPanel; diff --git a/frontend/src/pages/org-admin/org-admin-list.js b/frontend/src/pages/org-admin/org-admin-list.js new file mode 100644 index 0000000000..26d5dfb5a1 --- /dev/null +++ b/frontend/src/pages/org-admin/org-admin-list.js @@ -0,0 +1,125 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { gettext, orgID } from '../../utils/constants'; +import { seafileAPI } from '../../utils/seafile-api'; +import Toast from '../../components/toast'; +import OrgUserInfo from '../../models/org-user'; +import ModalPortal from '../../components/modal-portal'; +import AddOrgAdminDialog from '../../components/dialog/org-add-admin-dialog'; +import UserItem from './org-user-item'; + +import '../../css/org-admin-paginator.css'; + +const propTypes = { + toggleAddOrgAdmin: PropTypes.func.isRequired, + currentTab: PropTypes.string.isRequired, + isShowAddOrgAdminDialog: PropTypes.bool.isRequired, +}; + +class OrgAdminList extends React.Component { + + constructor(props) { + super(props); + this.state = { + orgAdminUsers: [], + isItemFreezed: false, + }; + } + + componentDidMount() { + seafileAPI.listOrgUsers(orgID, true).then(res => { + let userList = res.data.user_list.map(item => { + return new OrgUserInfo(item); + }); + this.setState({orgAdminUsers: userList}); + }); + } + + onFreezedItem = () => { + this.setState({isItemFreezed: true}); + } + + onUnfreezedItem = () => { + this.setState({isItemFreezed: false}); + } + + toggleDelete = (email) => { + seafileAPI.deleteOrgUser(orgID, email).then(res => { + this.setState({ + orgAdminUsers: this.state.orgAdminUsers.filter(item => item.email != email) + }); + let msg = gettext('Successfully deleted %s'); + msg = msg.replace('%s', email); + Toast.success(msg); + }); + } + + toggleRevokeAdmin = (email) => { + seafileAPI.setOrgAdmin(orgID, email, false).then(res => { + this.setState({ + orgAdminUsers: this.state.orgAdminUsers.filter(item => item.email != email) + }); + let msg = gettext('Successfully revoke the admin permission of %s'); + msg = msg.replace('%s', email); + Toast.success(msg); + }); + } + + addOrgAdmin = (users) => { + seafileAPI.setOrgAdmin(orgID, users, true).then(res => { + let userInfo = new OrgUserInfo(res.data); + this.state.orgAdminUsers.unshift(userInfo); + this.setState({ + orgAdminUsers: this.state.orgAdminUsers + }); + this.props.toggleAddOrgAdmin(); + let msg = gettext('Successfully set %s as admin.'); + msg = msg.replace('%s', userInfo.email); + Toast.success(msg); + }); + } + + render() { + let orgAdminUsers = this.state.orgAdminUsers; + + return ( +
+ + + + + + + + + + + + {orgAdminUsers.map(item => { + return ( + + )})} + +
{gettext('Name')}{gettext('Status')}{gettext('Space Used')}{gettext('Create At / Last Login')}{gettext('Operations')}
+ {this.props.isShowAddOrgAdminDialog && ( + + + + )} +
+ ); + } +} + +OrgAdminList.propTypes = propTypes; + +export default OrgAdminList; diff --git a/frontend/src/pages/org-admin/org-user-item.js b/frontend/src/pages/org-admin/org-user-item.js new file mode 100644 index 0000000000..98a59b73d1 --- /dev/null +++ b/frontend/src/pages/org-admin/org-user-item.js @@ -0,0 +1,160 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap'; +import { gettext, siteRoot, orgID, username } from '../../utils/constants'; +import { seafileAPI } from '../../utils/seafile-api'; +import Toast from '../../components/toast'; +import UserStatusEditor from '../../components/select-editor/user-status-editor'; + +const propTypes = { + currentTab: PropTypes.string, + toggleRevokeAdmin: PropTypes.func, + isItemFreezed: PropTypes.bool.isRequired, + toggleDelete: PropTypes.func.isRequired, + onFreezedItem: PropTypes.func.isRequired, + onUnfreezedItem: PropTypes.func.isRequired, +}; + +class UserItem extends React.Component { + + constructor(props) { + super(props); + this.state = { + highlight: false, + showMenu: false, + currentStatus: this.props.user.is_active ? 'active' : 'inactive', + isItemMenuShow: false + }; + + this.statusArray = ['active', 'inactive']; + } + + onMouseEnter = () => { + if (!this.props.isItemFreezed) { + this.setState({ + showMenu: true, + highlight: true, + }); + } + } + + onMouseLeave = () => { + if (!this.props.isItemFreezed) { + this.setState({ + showMenu: false, + highlight: false + }); + } + } + + toggleDelete = () => { + const email = this.props.user.email; + this.props.toggleDelete(email); + } + + toggleResetPW = () => { + const email = this.props.user.email; + seafileAPI.resetOrgUserPassword(orgID, email).then(res => { + let msg; + msg = gettext('Successfully reset password to %(passwd)s for user %(user)s.'); + msg = msg.replace('%(passwd)s', res.data.new_password); + msg = msg.replace('%(user)s', email); + Toast.success(msg); + }); + } + + toggleRevokeAdmin = () => { + const email = this.props.user.email; + this.props.toggleRevokeAdmin(email); + } + + changeStatus = (st) => { + let statusCode; + if (st == 'active') { + statusCode = 1; + } else { + statusCode = 0; + } + + seafileAPI.changeOrgUserStatus(this.props.user.id, statusCode).then(res => { + this.setState({ + currentStatus: statusCode == 1 ? 'active' : 'inactive', + highlight: false, + showMenu: false, + }); + Toast.success(gettext('Edit succeeded.')); + }).catch(err => { + Toast.danger(gettext('Edit falied.')); + }); + } + + onDropdownToggleClick = (e) => { + e.preventDefault(); + this.toggleOperationMenu(e); + } + + toggleOperationMenu = (e) => { + e.stopPropagation(); + this.setState( + {isItemMenuShow: !this.state.isItemMenuShow }, () => { + if (this.state.isItemMenuShow) { + this.props.onFreezedItem(); + } else { + this.setState({ + highlight: false, + showMenu: false, + }); + this.props.onUnfreezedItem(); + } + } + ); + } + + render() { + let { user, currentTab } = this.props; + let href = siteRoot + 'org/useradmin/info/' + encodeURIComponent(user.email) + '/'; + let isOperationMenuShow = (user.email !== username) && this.state.showMenu; + let isEditIconShow = isOperationMenuShow; + return ( + + + {user.name} + + + + + {user.quota ? user.self_usage + ' / ' + user.quota : user.self_usage} + {user.ctime} / {user.last_login ? user.last_login : '--'} + + {isOperationMenuShow && ( + + + + {gettext('Delete')} + {gettext('ResetPwd')} + {currentTab == 'admins' && {gettext('Revoke Admin')}} + + + )} + + + ); + } +} + +UserItem.propTypes = propTypes; + +export default UserItem; diff --git a/frontend/src/pages/org-admin/org-users-list.js b/frontend/src/pages/org-admin/org-users-list.js new file mode 100644 index 0000000000..a77cd30083 --- /dev/null +++ b/frontend/src/pages/org-admin/org-users-list.js @@ -0,0 +1,142 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { gettext, orgID } from '../../utils/constants'; +import { seafileAPI } from '../../utils/seafile-api'; +import OrgUserInfo from '../../models/org-user'; +import Toast from '../../components/toast'; +import ModalPortal from '../../components/modal-portal'; +import AddOrgUserDialog from '../../components/dialog/org-add-user-dialog'; +import UserItem from './org-user-item'; + +const propTypes = { + toggleAddOrgUser: PropTypes.func.isRequired, + currentTab: PropTypes.string.isRequired, + isShowAddOrgUserDialog: PropTypes.bool.isRequired, +}; + +class OrgUsersList extends React.Component { + + constructor(props) { + super(props); + this.state = { + orgUsers: [], + isItemFreezed: false, + page: 1, + pageNext: 2, + }; + } + + componentDidMount() { + let page = this.state.page; + this.initData(page); + } + + initData = (page) => { + seafileAPI.listOrgUsers(orgID, '', page).then(res => { + let userList = res.data.user_list.map(item => { + return new OrgUserInfo(item); + }); + this.setState({ + orgUsers: userList, + pageNext: res.data.page_next, + page: res.data.page, + }); + }); + } + + onFreezedItem = () => { + this.setState({isItemFreezed: true}); + } + + onUnfreezedItem = () => { + this.setState({isItemFreezed: false}); + } + + toggleDelete = (email) => { + seafileAPI.deleteOrgUser(orgID, email).then(res => { + let users = this.state.orgUsers.filter(item => item.email != email); + this.setState({orgUsers: users}); + let msg = gettext('Successfully deleted %s'); + msg = msg.replace('%s', email); + Toast.success(msg); + }) + } + + handleSubmit = (email, name, password) => { + seafileAPI.addOrgUser(orgID, email, name, password).then(res => { + let userInfo = new OrgUserInfo(res.data); + this.state.orgUsers.unshift(userInfo); + this.setState({ + orgUsers: this.state.orgUsers + }); + this.props.toggleAddOrgUser(); + let msg; + msg = gettext('successfully added user %s.'); + msg = msg.replace('%s', email); + Toast.success(msg); + }).catch(err => { + Toast.danger(err.response.data.error_msg); + this.props.toggleAddOrgUser(); + }); + } + + onChangePageNum = (e, num) => { + e.preventDefault(); + let page = this.state.page; + + if (num == 1) { + page = page + 1; + } else { + page = page - 1; + } + + this.initData(page); + } + + render() { + let orgUsers = this.state.orgUsers; + + return ( +
+ + + + + + + + + + + + {orgUsers.map(item => { + return ( + + )})} + +
{gettext('Name')}{gettext('Status')}{gettext('Space Used')}{gettext('Create At / Last Login')}{gettext('Operations')}
+ + {this.props.isShowAddOrgUserDialog && ( + + + + )} +
+ ); + } +} + +OrgUsersList.propTypes = propTypes; + +export default OrgUsersList; diff --git a/frontend/src/pages/org-admin/org-users.js b/frontend/src/pages/org-admin/org-users.js new file mode 100644 index 0000000000..69dc8e76b8 --- /dev/null +++ b/frontend/src/pages/org-admin/org-users.js @@ -0,0 +1,58 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { Link } from '@reach/router'; + +import { siteRoot, gettext } from '../../utils/constants'; + +class OrgUsers extends Component { + + constructor(props) { + super(props); + } + + tabItemClick = (param) => { + this.props.tabItemClick(param); + } + + toggleAddOrgUser = () => { + this.props.toggleAddOrgUser(); + } + + toggleAddOrgAdmin = () => { + this.props.toggleAddOrgAdmin(); + } + + render() { + return ( +
+
+
+
    +
  • this.tabItemClick('users')}> + {gettext('All')} +
  • +
  • this.tabItemClick('admins')}> + {gettext('Admin')} +
  • +
+
+ {this.props.currentTab === 'users' && + + } + {this.props.currentTab === 'admins' && + + } +
+
+ {this.props.children} +
+
+ ); + } +} + +export default OrgUsers; diff --git a/frontend/src/pages/org-admin/side-panel.js b/frontend/src/pages/org-admin/side-panel.js new file mode 100644 index 0000000000..1a989d985d --- /dev/null +++ b/frontend/src/pages/org-admin/side-panel.js @@ -0,0 +1,78 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Link } from '@reach/router'; +import Logo from '../../components/logo'; +import { gettext, siteRoot } from '../../utils/constants'; + +const propTypes = { + isSidePanelClosed: PropTypes.bool.isRequired, + onCloseSidePanel: PropTypes.func.isRequired, +}; + +class SidePanel extends React.Component { + + render() { + return ( +
+
+ +
+ +
+ ); + } +} + +SidePanel.propTypes = propTypes; + +export default SidePanel; diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js index bb33b2d693..4c3500f183 100644 --- a/frontend/src/utils/constants.js +++ b/frontend/src/utils/constants.js @@ -75,3 +75,6 @@ export const author = window.draftReview ? window.draftReview.config.author : '' export const authorAvatar = window.draftReview ? window.draftReview.config.authorAvatar : ''; export const originFileExists = window.draftReview ? window.draftReview.config.originFileExists : ''; export const draftFileExists = window.draftReview ? window.draftReview.config.draftFileExists : ''; + +// org admin +export const orgID = window.org ? window.org.pageOptions.orgID : ''; diff --git a/media/css/seahub_react.css b/media/css/seahub_react.css index 3892bc5b09..b7cf088908 100644 --- a/media/css/seahub_react.css +++ b/media/css/seahub_react.css @@ -93,6 +93,7 @@ .sf2-icon-readme:before {content:"\e039"} .sf2-icon-drafts:before {content:"\e03a"} .sf2-icon-recycle:before {content:"\e03b"} +.sf2-icon-library:before { content:"\e00d"; } /* common class and element style*/ a { color:#eb8205; } diff --git a/seahub/api2/permissions.py b/seahub/api2/permissions.py index ab71d3b7e1..8daaaad029 100644 --- a/seahub/api2/permissions.py +++ b/seahub/api2/permissions.py @@ -94,3 +94,12 @@ class IsProVersion(BasePermission): def has_permission(self, request, *args, **kwargs): return is_pro_version() + +class IsOrgAdminUser(BasePermission): + """ + Check whether user is org admin + """ + def has_permission(self, request, view, obj=None): + org_id = int(view.kwargs.get('org_id', '')) + return True if request.user.org.is_staff and \ + request.user.org.org_id == org_id else False