From ad5b070c9fc8b6720ff040f4ef9a42d294d0cf00 Mon Sep 17 00:00:00 2001 From: llj Date: Sat, 2 Nov 2019 17:02:26 +0800 Subject: [PATCH] [system admin] users: rewrote user pages & 'ldap users' page (#4216) --- frontend/src/components/common/account.js | 2 +- .../sysadmin-import-user-dialog.js | 4 +- .../sysadmin-set-org-name-dialog.js | 1 - .../sysadmin-user-set-contact-email-dialog.js | 79 ---- .../sysadmin-user-set-loginid-dialog.js | 68 --- .../sysadmin-user-set-name-dialog.js | 68 --- .../sysadmin-user-set-quota-dialog.js | 88 ---- .../sysadmin-user-set-referenceid-dialog.js | 71 --- .../dialog/sysadmin-dialog/update-user.js | 70 +++ .../components/select-editor/select-editor.js | 6 +- frontend/src/pages/sys-admin/groups/groups.js | 9 +- frontend/src/pages/sys-admin/index.js | 16 +- .../src/pages/sys-admin/repos/all-repos.js | 7 +- .../users/{users-ldap.js => ldap-users.js} | 89 ++-- .../src/pages/sys-admin/users/user-groups.js | 220 +++++++--- .../src/pages/sys-admin/users/user-info.js | 415 ++++++++++-------- .../src/pages/sys-admin/users/user-links.js | 340 ++++++++++++++ .../src/pages/sys-admin/users/user-nav.js | 46 ++ .../pages/sys-admin/users/user-owned-repos.js | 236 ---------- .../src/pages/sys-admin/users/user-profile.js | 188 -------- .../src/pages/sys-admin/users/user-repos.js | 322 ++++++++++++++ .../sys-admin/users/user-share-in-repos.js | 154 ------- .../pages/sys-admin/users/user-share-links.js | 255 ----------- .../sys-admin/users/user-shared-repos.js | 183 ++++++++ .../src/pages/sys-admin/users/users-nav.js | 16 +- frontend/src/pages/sys-admin/users/users.js | 73 +-- frontend/src/utils/constants.js | 3 - .../sysadmin/sysadmin_react_app.html | 4 +- seahub/urls.py | 4 + seahub/views/sysadmin.py | 4 +- 30 files changed, 1471 insertions(+), 1570 deletions(-) delete mode 100644 frontend/src/components/dialog/sysadmin-dialog/sysadmin-user-set-contact-email-dialog.js delete mode 100644 frontend/src/components/dialog/sysadmin-dialog/sysadmin-user-set-loginid-dialog.js delete mode 100644 frontend/src/components/dialog/sysadmin-dialog/sysadmin-user-set-name-dialog.js delete mode 100644 frontend/src/components/dialog/sysadmin-dialog/sysadmin-user-set-quota-dialog.js delete mode 100644 frontend/src/components/dialog/sysadmin-dialog/sysadmin-user-set-referenceid-dialog.js create mode 100644 frontend/src/components/dialog/sysadmin-dialog/update-user.js rename frontend/src/pages/sys-admin/users/{users-ldap.js => ldap-users.js} (65%) create mode 100644 frontend/src/pages/sys-admin/users/user-links.js create mode 100644 frontend/src/pages/sys-admin/users/user-nav.js delete mode 100644 frontend/src/pages/sys-admin/users/user-owned-repos.js delete mode 100644 frontend/src/pages/sys-admin/users/user-profile.js create mode 100644 frontend/src/pages/sys-admin/users/user-repos.js delete mode 100644 frontend/src/pages/sys-admin/users/user-share-in-repos.js delete mode 100644 frontend/src/pages/sys-admin/users/user-share-links.js create mode 100644 frontend/src/pages/sys-admin/users/user-shared-repos.js diff --git a/frontend/src/components/common/account.js b/frontend/src/components/common/account.js index cf90c67752..9c660233df 100644 --- a/frontend/src/components/common/account.js +++ b/frontend/src/components/common/account.js @@ -116,7 +116,7 @@ class Account extends Component { } else { if (isStaff) { data = { - url: `${siteRoot}sys/useradmin/`, + url: `${siteRoot}sys/info/`, text: gettext('System Admin') }; } else if (isOrgStaff) { diff --git a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-import-user-dialog.js b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-import-user-dialog.js index 04bfc139a5..e393f6aaea 100644 --- a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-import-user-dialog.js +++ b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-import-user-dialog.js @@ -41,6 +41,7 @@ class SysAdminImportUserDialog extends React.Component { } const file = this.fileInputRef.current.files[0]; this.props.importUserInBatch(file); + this.toggle(); } render() { @@ -49,8 +50,7 @@ class SysAdminImportUserDialog extends React.Component { {gettext('Import users from a .xlsx file')} - {gettext('Download an example file')} -
+

{gettext('Download an example file')}

{errorMsg && {errorMsg}} diff --git a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-set-org-name-dialog.js b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-set-org-name-dialog.js index cf1dd95942..a185855ddf 100644 --- a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-set-org-name-dialog.js +++ b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-set-org-name-dialog.js @@ -53,7 +53,6 @@ class SysAdminSetOrgNameDialog extends React.Component { { - this.props.toggle(); - } - - handleContactEmailChange = (e) => { - this.setState({contactEmail: e.target.value.trim()}); - } - - handleKeyPress = (e) => { - if (e.key === 'Enter') { - this.handleSubmit(); - e.preventDefault(); - } - } - - handleSubmit = () => { - let { contactEmail } = this.state; - if(Utils.isValidEmail(contactEmail) || contactEmail === '') { - this.props.onContactEmailChanged(contactEmail); - } else { - this.setState({ - errorMsg: gettext('Contact email invalid.') - }); - } - } - - render() { - let { contactEmail, errorMsg } = this.state; - return ( - - {gettext('Set user contact email')} - -
- - - - -
- {errorMsg && {errorMsg}} -
- - - - -
- ); - } -} - -SysAdminUserSetContactEmailDialog.propTypes = propTypes; - -export default SysAdminUserSetContactEmailDialog; diff --git a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-user-set-loginid-dialog.js b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-user-set-loginid-dialog.js deleted file mode 100644 index 1b5e590ff1..0000000000 --- a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-user-set-loginid-dialog.js +++ /dev/null @@ -1,68 +0,0 @@ - -import React from 'react'; -import PropTypes from 'prop-types'; -import { Modal, ModalHeader, ModalBody, ModalFooter, Button, Form, FormGroup, Input } from 'reactstrap'; -import { gettext } from '../../../utils/constants'; - -const propTypes = { - toggle: PropTypes.func.isRequired, - onLoginIDChanged: PropTypes.func.isRequired -}; - -class SysAdminUserSetLoginIDDialog extends React.Component { - constructor(props) { - super(props); - this.state = { - loginID: '', - }; - } - - toggle = () => { - this.props.toggle(); - } - - handleLoginIDChange = (e) => { - this.setState({loginID: e.target.value.trim()}); - } - - handleKeyPress = (e) => { - if (e.key === 'Enter') { - this.handleSubmit(); - e.preventDefault(); - } - } - - handleSubmit = () => { - let { loginID } = this.state; - this.props.onLoginIDChanged(loginID); - } - - render() { - let { loginID } = this.state; - return ( - - {gettext('Set user Login ID')} - -
- - - -
-
- - - - -
- ); - } -} - -SysAdminUserSetLoginIDDialog.propTypes = propTypes; - -export default SysAdminUserSetLoginIDDialog; diff --git a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-user-set-name-dialog.js b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-user-set-name-dialog.js deleted file mode 100644 index 9838071b9f..0000000000 --- a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-user-set-name-dialog.js +++ /dev/null @@ -1,68 +0,0 @@ - -import React from 'react'; -import PropTypes from 'prop-types'; -import { Modal, ModalHeader, ModalBody, ModalFooter, Button, Form, FormGroup, Label, Input } from 'reactstrap'; -import { gettext } from '../../../utils/constants'; - -const propTypes = { - toggle: PropTypes.func.isRequired, - onNameChanged: PropTypes.func.isRequired -}; - -class SysAdminUserSetNameDialog extends React.Component { - constructor(props) { - super(props); - this.state = { - name: '', - }; - } - - toggle = () => { - this.props.toggle(); - } - - handleNameChange = (e) => { - this.setState({name: e.target.value.trim()}); - } - - handleKeyPress = (e) => { - if (e.key === 'Enter') { - this.handleSubmit(); - e.preventDefault(); - } - } - - handleSubmit = () => { - let { name } = this.state; - this.props.onNameChanged(name); - } - - render() { - let { name } = this.state; - return ( - - {gettext('Set user name')} - -
- - - -
-
- - - - -
- ); - } -} - -SysAdminUserSetNameDialog.propTypes = propTypes; - -export default SysAdminUserSetNameDialog; diff --git a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-user-set-quota-dialog.js b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-user-set-quota-dialog.js deleted file mode 100644 index ab1a2d23f1..0000000000 --- a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-user-set-quota-dialog.js +++ /dev/null @@ -1,88 +0,0 @@ - -import React from 'react'; -import PropTypes from 'prop-types'; -import { Alert, Modal, ModalHeader, ModalBody, ModalFooter, Button, Form, FormGroup, Label, Input } from 'reactstrap'; -import { gettext } from '../../../utils/constants'; -import { Utils } from '../../../utils/utils'; - -const propTypes = { - toggle: PropTypes.func.isRequired, - onQuotaChanged: PropTypes.func.isRequired -}; - -class SysAdminUserSetQuotaDialog extends React.Component { - constructor(props) { - super(props); - this.state = { - quota: '', - isSubmitBtnActive: false, - errorMsg: '', - }; - } - - toggle = () => { - this.props.toggle(); - } - - handleQuotaChange = (e) => { - if (!e.target.value.trim()) { - this.setState({isSubmitBtnActive: false}); - } else { - this.setState({ - isSubmitBtnActive: true, - errorMsg: '' - }); - } - this.setState({quota: e.target.value}); - } - - handleKeyPress = (e) => { - if (e.key === 'Enter') { - this.handleSubmit(); - e.preventDefault(); - } - } - - handleSubmit = () => { - let { quota } = this.state; - if(Utils.isInteger(quota) && quota >= 0) { - this.props.onQuotaChanged(quota); - } else { - this.setState({ - errorMsg: gettext('Invalid quota.') - }); - } - } - - render() { - let { quota, isSubmitBtnActive, errorMsg } = this.state; - return ( - - {gettext('Set quota')} - -
- - - - -
- {gettext('An integer that is greater than or equal to 0.')}{gettext('Tip: 0 means default limit')} - {errorMsg && {errorMsg}} -
- - - - -
- ); - } -} - -SysAdminUserSetQuotaDialog.propTypes = propTypes; - -export default SysAdminUserSetQuotaDialog; diff --git a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-user-set-referenceid-dialog.js b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-user-set-referenceid-dialog.js deleted file mode 100644 index 0f41b7fdfa..0000000000 --- a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-user-set-referenceid-dialog.js +++ /dev/null @@ -1,71 +0,0 @@ - -import React from 'react'; -import PropTypes from 'prop-types'; -import { Modal, ModalHeader, ModalBody, ModalFooter, Button, Form, FormGroup, Label, Input } from 'reactstrap'; -import { gettext } from '../../../utils/constants'; - -const propTypes = { - toggle: PropTypes.func.isRequired, - onReferenceIDChanged: PropTypes.func.isRequired -}; - -class SysAdminUserSetReferenceIDDialog extends React.Component { - constructor(props) { - super(props); - this.state = { - referenceID: '', - isSubmitBtnActive: false, - errorMsg: '', - }; - } - - toggle = () => { - this.props.toggle(); - } - - handleReferenceIDChange = (e) => { - this.setState({referenceID: e.target.value.trim()}); - } - - handleKeyPress = (e) => { - if (e.key === 'Enter') { - this.handleSubmit(); - e.preventDefault(); - } - } - - handleSubmit = () => { - let { referenceID } = this.state; - this.props.onReferenceIDChanged(referenceID); - } - - render() { - let { referenceID } = this.state; - return ( - - {gettext('Set user Reference ID')} - -
- - - - -
-
- - - - -
- ); - } -} - -SysAdminUserSetReferenceIDDialog.propTypes = propTypes; - -export default SysAdminUserSetReferenceIDDialog; diff --git a/frontend/src/components/dialog/sysadmin-dialog/update-user.js b/frontend/src/components/dialog/sysadmin-dialog/update-user.js new file mode 100644 index 0000000000..67bc735c49 --- /dev/null +++ b/frontend/src/components/dialog/sysadmin-dialog/update-user.js @@ -0,0 +1,70 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Alert, Modal, ModalHeader, ModalBody, ModalFooter, Button, Form, FormGroup, Input, InputGroup, InputGroupAddon, InputGroupText } from 'reactstrap'; +import { gettext } from '../../../utils/constants'; +import { Utils } from '../../../utils/utils'; + +const propTypes = { + dialogTitle: PropTypes.string.isRequired, + updateValue: PropTypes.func.isRequired, + toggleDialog: PropTypes.func.isRequired +}; + +class UpdateUser extends React.Component { + + constructor(props) { + super(props); + this.state = { + value: this.props.value, + isSubmitBtnActive: false + }; + } + + handleInputChange = (e) => { + const value = e.target.value.trim(); + this.setState({ + value: value + }); + } + + handleKeyPress = (e) => { + if (e.key == 'Enter') { + this.handleSubmit(); + e.preventDefault(); + } + } + + handleSubmit = () => { + this.props.updateValue(this.state.value); + this.props.toggleDialog(); + } + + render() { + const { dialogTitle, toggleDialog } = this.props; + return ( + + {this.props.dialogTitle} + +
+ + + +
+
+ + + + +
+ ); + } +} + +UpdateUser.propTypes = propTypes; + +export default UpdateUser; diff --git a/frontend/src/components/select-editor/select-editor.js b/frontend/src/components/select-editor/select-editor.js index 5f6a4dfd16..d14a592166 100644 --- a/frontend/src/components/select-editor/select-editor.js +++ b/frontend/src/components/select-editor/select-editor.js @@ -37,7 +37,11 @@ class SelectEditor extends React.Component { for (let i = 0, length = options.length; i < length; i++) { let option = {}; option.value = options[i]; - option.label =
{this.props.translateOption(options[i])}{ this.props.translateExplanation &&
{this.props.translateExplanation(options[i])}
}
; + if (!options[i].length) { // it's ''. for example, intitution option in 'system admin - users' page can be ''. + option.label =
; + } else { + option.label =
{this.props.translateOption(options[i])}{ this.props.translateExplanation &&
{this.props.translateExplanation(options[i])}
}
; + } this.options.push(option); } diff --git a/frontend/src/pages/sys-admin/groups/groups.js b/frontend/src/pages/sys-admin/groups/groups.js index 305b60aba9..70ff63a834 100644 --- a/frontend/src/pages/sys-admin/groups/groups.js +++ b/frontend/src/pages/sys-admin/groups/groups.js @@ -1,4 +1,5 @@ import React, { Component, Fragment } from 'react'; +import { Link } from '@reach/router'; import { Button } from 'reactstrap'; import moment from 'moment'; import { Utils } from '../../../utils/utils'; @@ -170,18 +171,18 @@ class Item extends Component { let groupName = '' + Utils.HTMLescape(item.name) + ''; let deleteDialogMsg = gettext('Are you sure you want to delete {placeholder} ?').replace('{placeholder}', groupName); - const libUrl = item.parent_group_id == 0 ? + const groupUrl = item.parent_group_id == 0 ? `${siteRoot}sys/groups/${item.id}/libraries/` : - `${siteRoot}sysadmin/#address-book/groups/${item.id}/`; + `${siteRoot}sys/departments/${item.id}/`; return ( - {item.name} + {item.name} {item.owner == 'system admin' ? '--' : - {item.owner_name} + {item.owner_name} } diff --git a/frontend/src/pages/sys-admin/index.js b/frontend/src/pages/sys-admin/index.js index fa8f072851..dc4a2ff686 100644 --- a/frontend/src/pages/sys-admin/index.js +++ b/frontend/src/pages/sys-admin/index.js @@ -14,8 +14,12 @@ import DeviceErrors from './devices/devices-errors'; import Users from './users/users'; import AdminUsers from './users/admin-users'; import LDAPImportedUsers from './users/ldap-imported-users'; -import UsersLDAP from './users/users-ldap'; +import LDAPUsers from './users/ldap-users'; import User from './users/user-info'; +import UserOwnedRepos from './users/user-repos'; +import UserSharedRepos from './users/user-shared-repos'; +import UserLinks from './users/user-links'; +import UserGroups from './users/user-groups'; import AllRepos from './repos/all-repos'; import SystemRepo from './repos/system-repo'; @@ -80,6 +84,10 @@ class SysAdmin extends React.Component { tab: 'libraries', urlPartList: ['all-libraries', 'system-library', 'trash-libraries', 'libraries/'] }, + { + tab: 'users', + urlPartList: ['users/'] + }, { tab: 'groups', urlPartList: ['groups/'] @@ -164,8 +172,12 @@ class SysAdmin extends React.Component { - + + + + + {repo.name}; + return {repo.name}; } else { return repo.name; } @@ -229,8 +230,8 @@ class Item extends Component { {repo.id} {isGroupOwnedRepo ? - {repo.group_name} : - {repo.owner_name} + {repo.group_name} : + {repo.owner_name} } diff --git a/frontend/src/pages/sys-admin/users/users-ldap.js b/frontend/src/pages/sys-admin/users/ldap-users.js similarity index 65% rename from frontend/src/pages/sys-admin/users/users-ldap.js rename to frontend/src/pages/sys-admin/users/ldap-users.js index 0675d3e974..dfaaac2440 100644 --- a/frontend/src/pages/sys-admin/users/users-ldap.js +++ b/frontend/src/pages/sys-admin/users/ldap-users.js @@ -1,9 +1,10 @@ import React, { Component, Fragment } from 'react'; -import { seafileAPI } from '../../../utils/seafile-api'; -import { gettext, siteRoot } from '../../../utils/constants'; -import { Utils } from '../../../utils/utils'; -import EmptyTip from '../../../components/empty-tip'; +import { Link } from '@reach/router'; import moment from 'moment'; +import { Utils } from '../../../utils/utils'; +import { seafileAPI } from '../../../utils/seafile-api'; +import { gettext, siteRoot, loginUrl } from '../../../utils/constants'; +import EmptyTip from '../../../components/empty-tip'; import Loading from '../../../components/loading'; import Paginator from '../../../components/paginator'; import UsersNav from './users-nav'; @@ -13,8 +14,6 @@ class Content extends Component { constructor(props) { super(props); - this.state = { - }; } getPreviousPage = () => { @@ -30,7 +29,7 @@ class Content extends Component { if (loading) { return ; } else if (errorMsg) { - return

{errorMsg}

; + return

{errorMsg}

; } else { const emptyTip = ( @@ -42,23 +41,19 @@ class Content extends Component { - - - + + + - {items && - - {items.map((item, index) => { - return (); - })} - - } + + {items.map((item, index) => { + return (); + })} +
{gettext('Email')}{gettext('Space Used')}{' / '}{gettext('Quota')}{gettext('Create At')}{' / '}{gettext('Last Login')}{gettext('Email')}{gettext('Space Used')}{' / '}{gettext('Quota')}{gettext('Last Login')}
{ - this.setState({isOpIconShown: true}); - } - - handleMouseOut = () => { - this.setState({isOpIconShown: false}); } render() { - let { status, role, quota_total, isOpIconShown } = this.state; - let {item} = this.props; - let iconVisibility = this.state.isOpIconShown ? '' : ' invisible'; - let pencilIconClassName = 'fa fa-pencil-alt attr-action-icon' + iconVisibility; - + const { item } = this.props; let email = '' + Utils.HTMLescape(item.email) + ''; - return ( - + - - - {Utils.bytesToSize(item.quota_usage)}{' / '} - {quota_total >= 0 ? Utils.bytesToSize(quota_total) : '--'} + {item.email} -
{moment(item.create_time).format('YYYY-MM-DD HH:mm') }{' /'}
-
{item.last_login == '' ? '--' : moment(item.last_login).fromNow()}
+ {`${Utils.bytesToSize(item.quota_usage)} / ${item.quota_total > 0 ? Utils.bytesToSize(item.quota_total) : '--'}`} + + + {item.last_login ? moment(item.last_login).fromNow() : '--'}
@@ -121,7 +99,7 @@ class Item extends Component { } } -class UsersLDAP extends Component { +class Users extends Component { constructor(props) { super(props); @@ -131,22 +109,22 @@ class UsersLDAP extends Component { userList: {}, hasNextPage: false, currentPage: 1, - perPage: 25, + perPage: 25 }; } componentDidMount () { - this.getUsersListByPage(1); // init enter the first page + this.getUsersListByPage(1); } getUsersListByPage = (page) => { let { perPage } = this.state; - seafileAPI.sysAdminListAllLDAPUsers(page, perPage).then(res => { + seafileAPI.sysAdminListLDAPUsers(page, perPage).then(res => { this.setState({ + loading: false, userList: res.data.ldap_user_list, hasNextPage: res.data.has_next_page, - loading: false, - currentPage: page, + currentPage: page }); }).catch((error) => { if (error.response) { @@ -155,6 +133,7 @@ class UsersLDAP extends Component { loading: false, errorMsg: gettext('Permission denied') }); + location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`; } else { this.setState({ loading: false, @@ -179,11 +158,9 @@ class UsersLDAP extends Component { } render() { - //let { } = this.state; return ( - - +
@@ -206,4 +183,4 @@ class UsersLDAP extends Component { } } -export default UsersLDAP; \ No newline at end of file +export default Users; diff --git a/frontend/src/pages/sys-admin/users/user-groups.js b/frontend/src/pages/sys-admin/users/user-groups.js index e1cfa7c7e6..f97d59b80f 100644 --- a/frontend/src/pages/sys-admin/users/user-groups.js +++ b/frontend/src/pages/sys-admin/users/user-groups.js @@ -1,17 +1,32 @@ import React, { Component, Fragment } from 'react'; -import { seafileAPI } from '../../../utils/seafile-api'; -import { gettext, siteRoot } from '../../../utils/constants'; -import toaster from '../../../components/toast'; -import { Utils } from '../../../utils/utils'; -import EmptyTip from '../../../components/empty-tip'; +import { Link } from '@reach/router'; import moment from 'moment'; +import { Utils } from '../../../utils/utils'; +import { seafileAPI } from '../../../utils/seafile-api'; +import { siteRoot, loginUrl, gettext } from '../../../utils/constants'; +import toaster from '../../../components/toast'; +import EmptyTip from '../../../components/empty-tip'; import Loading from '../../../components/loading'; import CommonOperationConfirmationDialog from '../../../components/dialog/common-operation-confirmation-dialog'; +import MainPanelTopbar from '../main-panel-topbar'; +import Nav from './user-nav'; +import OpMenu from './user-op-menu'; class Content extends Component { constructor(props) { super(props); + this.state = { + isItemFreezed: false + }; + } + + onFreezedItem = () => { + this.setState({isItemFreezed: true}); + } + + onUnfreezedItem = () => { + this.setState({isItemFreezed: false}); } render() { @@ -19,38 +34,39 @@ class Content extends Component { if (loading) { return ; } else if (errorMsg) { - return

{errorMsg}

; + return

{errorMsg}

; } else { const emptyTip = ( -

{gettext('This user has not created or joined any groups')}

+

{gettext('No groups')}

); const table = ( - +
- + - - + + {items.map((item, index) => { - return (); })}
{gettext('Name')}{gettext('Name')} {gettext('Role')}{gettext('Create At')}{gettext('Operations')}{gettext('Created At')}{/* Operations */}
-
); - return items.length ? table : emptyTip; } } @@ -61,60 +77,116 @@ class Item extends Component { constructor(props) { super(props); this.state = { - showOpIcon: false, - isDeleteDialogOpen: false, + isOpIconShown: false, + highlight: false, + isDeleteDialogOpen: false }; } - handleMouseOver = () => { - this.setState({showOpIcon: true}); + handleMouseEnter = () => { + if (!this.props.isItemFreezed) { + this.setState({ + isOpIconShown: true, + highlight: true + }); + } } - handleMouseOut = () => { - this.setState({showOpIcon: false}); + handleMouseLeave = () => { + if (!this.props.isItemFreezed) { + this.setState({ + isOpIconShown: false, + highlight: false + }); + } + } + + onUnfreezedItem = () => { + this.setState({ + highlight: false, + isOpIconShow: false + }); + this.props.onUnfreezedItem(); } toggleDeleteDialog = () => { this.setState({isDeleteDialogOpen: !this.state.isDeleteDialogOpen}); } - deleteGroup = () => { - this.props.deleteGroup(this.props.item.id); - this.toggleDeleteDialog(); + deleteItem = () => { + this.props.deleteItem(this.props.item.id); + } + + translateOperations = (item) => { + let translateResult = ''; + switch (item) { + case 'Delete': + translateResult = gettext('Delete'); + break; + } + + return translateResult; + } + + onMenuItemClick = (operation) => { + switch(operation) { + case 'Delete': + this.toggleDeleteDialog(); + break; + } + } + + getRoleText = () => { + let roleText; + const { item } = this.props; + switch(item.role) { + case 'Owner': + roleText = gettext('Owner'); + break; + case 'Admin': + roleText = gettext('Admin'); + break; + case 'Member': + roleText = gettext('Member'); + break; + } + return roleText; } render() { - let { showOpIcon, isDeleteDialogOpen } = this.state; - let { item } = this.props; - let roleText; - if (item.role == 'owner') { - roleText = gettext('Owner'); - } else if (item.role == 'admin') { - roleText = gettext('Admin'); - } else if (item.role == 'member') { - roleText = gettext('Member'); - } + const { item } = this.props; + const { isOpIconShown, isDeleteDialogOpen } = this.state; - let groupName = '' + Utils.HTMLescape(item.name) + ''; - let deleteDialogMsg = gettext('Are you sure you want to delete {placeholder} ?'.replace('{placeholder}', groupName)) + const itemName = '' + Utils.HTMLescape(item.name) + ''; + const deleteDialogMsg = gettext('Are you sure you want to delete {placeholder} ?').replace('{placeholder}', itemName); + + const url = item.parent_group_id == 0 ? + `${siteRoot}sys/groups/${item.id}/libraries/` : + `${siteRoot}sys/departments/${item.id}/`; - let iconVisibility = showOpIcon ? '' : ' invisible'; - let deleteIconClassName = 'op-icon sf2-icon-delete' + iconVisibility; return ( - - {item.name} - {roleText} - {moment(item.created_at).fromNow()} + + {item.name} + {this.getRoleText()} + {moment(item.created_at).format('YYYY-MM-DD HH:mm')} - + {(isOpIconShown && item.parent_group_id == 0) && + + } {isDeleteDialogOpen && - @@ -124,22 +196,28 @@ class Item extends Component { } } -class UserGroups extends Component { +class Groups extends Component { constructor(props) { super(props); this.state = { loading: true, errorMsg: '', - groupList: [], + userInfo: {}, + items: [] }; } componentDidMount () { - seafileAPI.sysAdminListAllGroupsJoinedByUser(this.props.email).then(res => { + seafileAPI.sysAdminGetUser(this.props.email).then((res) => { this.setState({ - groupList: res.data.group_list, - loading: false + userInfo: res.data + }); + }); + seafileAPI.sysAdminListGroupsJoinedByUser(this.props.email).then(res => { + this.setState({ + loading: false, + items: res.data.group_list }); }).catch((error) => { if (error.response) { @@ -147,12 +225,13 @@ class UserGroups extends Component { this.setState({ loading: false, errorMsg: gettext('Permission denied') - }); + }); + location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`; } else { this.setState({ loading: false, errorMsg: gettext('Error') - }); + }); } } else { this.setState({ @@ -163,14 +242,13 @@ class UserGroups extends Component { }); } - deleteGroup = (groupID) => { + deleteItem = (groupID) => { seafileAPI.sysAdminDismissGroupByID(groupID).then(res => { - let newGroupList = this.state.groupList.filter(item=> { + let items = this.state.items.filter(item => { return item.id != groupID; }); - this.setState({ - groupList: newGroupList - }); + this.setState({items: items}); + toaster.success(gettext('Successfully deleted 1 item.')); }).catch((error) => { let errMessage = Utils.getErrorMsg(error); toaster.danger(errMessage); @@ -179,16 +257,24 @@ class UserGroups extends Component { render() { return ( -
- -
+ + +
+
+
+
+
); } } -export default UserGroups; +export default Groups; diff --git a/frontend/src/pages/sys-admin/users/user-info.js b/frontend/src/pages/sys-admin/users/user-info.js index 924c5ea720..00e4d4ab23 100644 --- a/frontend/src/pages/sys-admin/users/user-info.js +++ b/frontend/src/pages/sys-admin/users/user-info.js @@ -1,32 +1,199 @@ import React, { Component, Fragment } from 'react'; -import { Nav, NavItem, NavLink, TabContent, TabPane, Label } from 'reactstrap'; -import { gettext, siteRoot } from '../../../utils/constants'; -import { seafileAPI } from '../../../utils/seafile-api'; -import classnames from 'classnames'; -import toaster from '../../../components/toast'; +import { FormGroup, Label, Input, Button } from 'reactstrap'; import { Utils } from '../../../utils/utils'; -import MainPanelTopbar from '../../org-admin/main-panel-topbar'; -import UserProfile from './user-profile'; -import UserOwnedRepos from './user-owned-repos'; -import UserSharedInRepos from './user-share-in-repos'; -import UserShareLinks from './user-share-links'; -import UserGroups from './user-groups'; +import { seafileAPI } from '../../../utils/seafile-api'; +import { loginUrl, gettext } from '../../../utils/constants'; +import toaster from '../../../components/toast'; +import Loading from '../../../components/loading'; +import SysAdminSetQuotaDialog from '../../../components/dialog/sysadmin-dialog/set-quota'; +import SysAdminUpdateUserDialog from '../../../components/dialog/sysadmin-dialog/update-user'; +import MainPanelTopbar from '../main-panel-topbar'; +import Nav from './user-nav'; -class UserInfo extends Component { +const { twoFactorAuthEnabled } = window.sysadmin.pageOptions; + +class Content extends Component { constructor(props) { super(props); this.state = { - activeTab: 'profile', + currentKey: '', + dialogTitle: '', + isSetQuotaDialogOpen: false, + isUpdateUserDialogOpen: false + }; + } + + toggleSetQuotaDialog = () => { + this.setState({isSetQuotaDialogOpen: !this.state.isSetQuotaDialogOpen}); + } + + updateQuota = (value) => { + this.props.updateUser('quota_total', value); + } + + toggleDialog = (key, dialogTitle) => { + this.setState({ + currentKey: key, + dialogTitle: dialogTitle, + isUpdateUserDialogOpen: !this.state.isUpdateUserDialogOpen + }); + } + + toggleSetNameDialog = () => { + this.toggleDialog('name', gettext('Set Name')); + } + + toggleSetUserLoginIDDialog = () => { + this.toggleDialog('login_id', gettext('Set Login ID')); + } + + toggleSetUserComtactEmailDialog = () => { + this.toggleDialog('contact_email', gettext('Set Contact Email')); + } + + toggleSetUserReferenceIDDialog = () => { + this.toggleDialog('reference_id', gettext('Set Reference ID')); + } + + updateValue = (value) => { + this.props.updateUser(this.state.currentKey, value); + } + + toggleUpdateUserDialog = () => { + this.toggleDialog('', ''); + } + + showEditIcon = (action) => { + return ( + + + ); + } + + render() { + const { loading, errorMsg, userInfo } = this.props; + if (loading) { + return ; + } else if (errorMsg) { + return

{errorMsg}

; + } else { + const user = this.props.userInfo; + const { + currentKey, dialogTitle, + isSetQuotaDialogOpen, isUpdateUserDialogOpen + } = this.state; + return ( + +
+
{gettext('Avatar')}
+
+ {user.name} +
+ +
{gettext('Email')}
+
{user.email}
+ + {user.org_name && + +
{gettext('Organization')}
+
{user.org_name}
+
+ } + +
{gettext('Name')}
+
+ {user.name || '--'} + {this.showEditIcon(this.toggleSetNameDialog)} +
+ +
{gettext('Login ID')}
+
+ {user.login_id || '--'} + {this.showEditIcon(this.toggleSetUserLoginIDDialog)} +
+ +
{gettext('Contact Email')}
+
+ {user.contact_email || '--'} + {this.showEditIcon(this.toggleSetUserComtactEmailDialog)} +
+ +
{gettext('Reference ID')}
+
+ {user.reference_id|| '--'} + {this.showEditIcon(this.toggleSetUserReferenceIDDialog)} +
+ +
{gettext('Space Used / Quota')}
+
+ {`${Utils.bytesToSize(user.quota_usage)} / ${user.quota_total > 0 ? Utils.bytesToSize(user.quota_total) : '--'}`} + {this.showEditIcon(this.toggleSetQuotaDialog)} +
+ + {twoFactorAuthEnabled && + +
{gettext('Two-Factor Authentication')}
+
+ {user.has_default_device ? + +

{gettext('Status: enabled')}

+ +
: + + + + } + + + +
+
+ } +
+ {isSetQuotaDialogOpen && + + } + {isUpdateUserDialogOpen && + + } +
+ ); + } + } +} + +class User extends Component { + + constructor(props) { + super(props); + this.state = { + loading: true, + errorMsg: '', userInfo: {} }; } - componentDidMount() { - seafileAPI.sysAdminGetUserInfo(this.props.email).then(res => { + componentDidMount () { + // avatar size: 160 + seafileAPI.sysAdminGetUser(this.props.email, 160).then((res) => { this.setState({ - userInfo: res.data, - loading: false + loading: false, + userInfo: res.data }); }).catch((error) => { if (error.response) { @@ -34,13 +201,14 @@ class UserInfo extends Component { this.setState({ loading: false, errorMsg: gettext('Permission denied') - }); + }); + location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`; } else { this.setState({ loading: false, errorMsg: gettext('Error') - }); - } + }); + } } else { this.setState({ loading: false, @@ -50,87 +218,44 @@ class UserInfo extends Component { }); } - toggle(tab) { - if (this.state.activeTab !== tab) { - this.setState({ - activeTab: tab - }); - } - } - - onNameChanged = (name) => { - seafileAPI.sysAdminUpdateUserInfo('name', name, this.props.email).then(res => { - this.setState({ - userInfo: res.data, - }); - }).catch((error) => { - let errMessage = Utils.getErrorMsg(error); - toaster.danger(errMessage); - }); - } - - onLoginIDChanged = (loginID) => { - seafileAPI.sysAdminUpdateUserInfo('login_id', loginID, this.props.email).then(res => { - this.setState({ - userInfo: res.data, - }); - }).catch((error) => { - let errMessage = Utils.getErrorMsg(error); - toaster.danger(errMessage); - }); - } - - - onContactEmailChanged = (contactEmail) => { - seafileAPI.sysAdminUpdateUserInfo('contact_email', contactEmail, this.props.email).then(res => { - this.setState({ - userInfo: res.data, - }); - }).catch((error) => { - let errMessage = Utils.getErrorMsg(error); - toaster.danger(errMessage); - }); - } - - onReferenceIDChanged = (referenceID) => { - seafileAPI.sysAdminUpdateUserInfo('reference_id', referenceID, this.props.email).then(res => { - this.setState({ - userInfo: res.data, - }); - }).catch((error) => { - let errMessage = Utils.getErrorMsg(error); - toaster.danger(errMessage); - }); - } - - onQuotaChanged = (quota) => { - seafileAPI.sysAdminUpdateUserInfo('quota_total', quota, this.props.email).then(res => { - this.setState({ - userInfo: res.data, - }); - }).catch((error) => { - let errMessage = Utils.getErrorMsg(error); - toaster.danger(errMessage); - }); - } - - toggleForce2FA = (isForce2FA) => { - seafileAPI.sysAdminToggleForceTwoFactorAuth(isForce2FA, this.props.email).then(res => { + updateUser = (key, value) => { + const email = this.state.userInfo.email; + seafileAPI.sysAdminUpdateUser(email, key, value).then(res => { let userInfo = this.state.userInfo; - userInfo.is_force_2fa = isForce2FA; - this.setState({userInfo: userInfo}); + userInfo[key]= res.data[key]; + this.setState({ + userInfo: userInfo + }); + toaster.success(gettext('Edit succeeded')); }).catch((error) => { let errMessage = Utils.getErrorMsg(error); toaster.danger(errMessage); - }); + }); } - deleteVerified2FADevices = () => { - seafileAPI.sysAdminDeleteVerifiedTwoFactorAuth(this.props.email).then(res => { + disable2FA = () => { + const email = this.state.userInfo.email; + seafileAPI.sysAdminDeleteTwoFactorAuth(email).then(res => { let userInfo = this.state.userInfo; userInfo.has_default_device = false; - this.setState({userInfo: userInfo}); - toaster.success(gettext('success')); + this.setState({ + userInfo: userInfo + }); + }).catch((error) => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + + toggleForce2fa = (e) => { + const email = this.state.userInfo.email; + const checked = e.target.checked; + seafileAPI.sysAdminSetForceTwoFactorAuth(email, checked).then(res => { + let userInfo = this.state.userInfo; + userInfo.is_force_2fa = checked; + this.setState({ + userInfo: userInfo + }); }).catch((error) => { let errMessage = Utils.getErrorMsg(error); toaster.danger(errMessage); @@ -138,102 +263,22 @@ class UserInfo extends Component { } render() { + const { userInfo } = this.state; return ( - - - {gettext('Users')} - {' / '} - - - +
-
- -
+
@@ -242,4 +287,4 @@ class UserInfo extends Component { } } -export default UserInfo; \ No newline at end of file +export default User; diff --git a/frontend/src/pages/sys-admin/users/user-links.js b/frontend/src/pages/sys-admin/users/user-links.js new file mode 100644 index 0000000000..ecd9a3e21a --- /dev/null +++ b/frontend/src/pages/sys-admin/users/user-links.js @@ -0,0 +1,340 @@ +import React, { Component, Fragment } from 'react'; +import { Utils } from '../../../utils/utils'; +import { seafileAPI } from '../../../utils/seafile-api'; +import { loginUrl, gettext } from '../../../utils/constants'; +import toaster from '../../../components/toast'; +import EmptyTip from '../../../components/empty-tip'; +import Loading from '../../../components/loading'; +import LinkDialog from '../../../components/dialog/share-admin-link'; +import MainPanelTopbar from '../main-panel-topbar'; +import Nav from './user-nav'; +import OpMenu from './user-op-menu'; + +class Content extends Component { + + constructor(props) { + super(props); + this.state = { + isItemFreezed: false + }; + } + + onFreezedItem = () => { + this.setState({isItemFreezed: true}); + } + + onUnfreezedItem = () => { + this.setState({isItemFreezed: false}); + } + + render() { + const { loading, errorMsg, items } = this.props; + if (loading) { + return ; + } else if (errorMsg) { + return

{errorMsg}

; + } else { + const emptyTip = ( + +

{gettext('No shared links')}

+
+ ); + const table = ( + + + + + + + + + + + + + + {items.map((item, index) => { + return (); + })} + +
{/* icon */}{gettext('Name')}{gettext('Size')}{gettext('Type')}{gettext('Visits')}{/* Operations */}
+
+ ); + return items.length ? table : emptyTip; + } + } +} + +class Item extends Component { + + constructor(props) { + super(props); + this.state = { + isOpIconShown: false, + highlight: false, + isLinkDialogOpen: false + }; + } + + handleMouseEnter = () => { + if (!this.props.isItemFreezed) { + this.setState({ + isOpIconShown: true, + highlight: true + }); + } + } + + handleMouseLeave = () => { + if (!this.props.isItemFreezed) { + this.setState({ + isOpIconShown: false, + highlight: false + }); + } + } + + onUnfreezedItem = () => { + this.setState({ + highlight: false, + isOpIconShow: false + }); + this.props.onUnfreezedItem(); + } + + toggleLinkDialog = () => { + this.setState({isLinkDialogOpen: !this.state.isLinkDialogOpen}); + } + + deleteItem = () => { + this.props.deleteItem(this.props.item); + } + + translateOperations = (item) => { + let translateResult = ''; + switch (item) { + case 'View': + translateResult = gettext('View'); + break; + case 'Delete': + translateResult = gettext('Delete'); + break; + } + + return translateResult; + } + + onMenuItemClick = (operation) => { + switch(operation) { + case 'View': + this.toggleLinkDialog(); + break; + case 'Delete': + this.deleteItem(); + break; + } + } + + getRoleText = () => { + let roleText; + const { item } = this.props; + switch(item.role) { + case 'Owner': + roleText = gettext('Owner'); + break; + case 'Admin': + roleText = gettext('Admin'); + break; + case 'Member': + roleText = gettext('Member'); + break; + } + return roleText; + } + + getIconUrl = () => { + const { item } = this.props; + let url; + if (item.type == 'upload') { + url = Utils.getFolderIconUrl(); + } else { // share link + if (item.is_dir) { + url = Utils.getFolderIconUrl(); + } else { + url = Utils.getFileIconUrl(item.obj_name); + } + } + return url; + } + + render() { + const { item } = this.props; + const { isOpIconShown, isLinkDialogOpen } = this.state; + + return ( + + + + {item.obj_name == '/' ? item.repo_name : item.obj_name} + {item.type == 'upload' ? + + + {gettext('Upload')} + : + + {item.is_dir ? null : Utils.bytesToSize(item.size)} + {gettext('Download')} + + } + {item.view_cnt} + + {isOpIconShown && + + } + + + {isLinkDialogOpen && + + } + + ); + } +} + +class Links extends Component { + + constructor(props) { + super(props); + this.state = { + loading: true, + errorMsg: '', + userInfo: {}, + uploadLinkItems: [], + shareLinkItems: [] + }; + } + + componentDidMount () { + seafileAPI.sysAdminGetUser(this.props.email).then((res) => { + this.setState({ + userInfo: res.data + }); + }); + + seafileAPI.sysAdminListShareLinksByUser(this.props.email).then(res => { + const items = res.data.share_link_list.map(item => { + item.type = 'download'; + return item; + }); + items.sort((a, b) => { + return a.is_dir ? -1 : 1; + }); + this.setState({ + loading: false, + shareLinkItems: items + }); + }); + seafileAPI.sysAdminListUploadLinksByUser(this.props.email).then(res => { + const items = res.data.upload_link_list.map(item => { + item.type = 'upload'; + return item; + }); + this.setState({ + loading: false, + uploadLinkItems: items + }); + }).catch((error) => { + if (error.response) { + if (error.response.status == 403) { + this.setState({ + loading: false, + errorMsg: gettext('Permission denied') + }); + location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`; + } else { + this.setState({ + loading: false, + errorMsg: gettext('Error') + }); + } + } else { + this.setState({ + loading: false, + errorMsg: gettext('Please check the network.') + }); + } + }); + } + + deleteItem = (item) => { + const type = item.type; + const token = item.token; + if (type == 'download') { + seafileAPI.sysAdminDeleteShareLink(token).then(res => { + let items = this.state.shareLinkItems.filter(item=> { + return item.token != token; + }); + this.setState({ + shareLinkItems: items + }); + toaster.success(gettext('Successfully deleted 1 item.')); + }).catch((error) => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } else { + seafileAPI.sysAdminDeleteUploadLink(token).then(res => { + let items = this.state.uploadLinkItems.filter(item=> { + return item.token != token; + }); + this.setState({ + uploadLinkItems: items + }); + toaster.success(gettext('Successfully deleted 1 item.')); + }).catch((error) => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + } + + render() { + const { shareLinkItems, uploadLinkItems } = this.state; + return ( + + +
+
+
+
+
+ ); + } +} + +export default Links; diff --git a/frontend/src/pages/sys-admin/users/user-nav.js b/frontend/src/pages/sys-admin/users/user-nav.js new file mode 100644 index 0000000000..b37068a6e6 --- /dev/null +++ b/frontend/src/pages/sys-admin/users/user-nav.js @@ -0,0 +1,46 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Link } from '@reach/router'; +import { siteRoot, gettext } from '../../../utils/constants'; + +const propTypes = { + currentItem: PropTypes.string.isRequired +}; + +class Nav extends React.Component { + + constructor(props) { + super(props); + this.navItems = [ + {name: 'info', urlPart: '', text: gettext('Info')}, + {name: 'owned-repos', urlPart: 'owned-libraries', text: gettext('Owned Libraries')}, + {name: 'shared-repos', urlPart: 'shared-libraries', text: gettext('Shared Libraries')}, + {name: 'links', urlPart: 'shared-links', text: gettext('Shared Links')}, + {name: 'groups', urlPart: 'groups', text: gettext('Groups')} + ]; + } + + render() { + const { currentItem, email, userName } = this.props; + return ( +
+
+

{gettext('Users')} / {userName}

+
+
    + {this.navItems.map((item, index) => { + return ( +
  • + {item.text} +
  • + ); + })} +
+
+ ); + } +} + +Nav.propTypes = propTypes; + +export default Nav; diff --git a/frontend/src/pages/sys-admin/users/user-owned-repos.js b/frontend/src/pages/sys-admin/users/user-owned-repos.js deleted file mode 100644 index 3699cb5a73..0000000000 --- a/frontend/src/pages/sys-admin/users/user-owned-repos.js +++ /dev/null @@ -1,236 +0,0 @@ -import React, { Component, Fragment } from 'react'; -import { seafileAPI } from '../../../utils/seafile-api'; -import { gettext } from '../../../utils/constants'; -import toaster from '../../../components/toast'; -import { Utils } from '../../../utils/utils'; -import { username } from '../../../utils/constants'; -import EmptyTip from '../../../components/empty-tip'; -import moment from 'moment'; -import Loading from '../../../components/loading'; -import CommonOperationConfirmationDialog from '../../../components/dialog/common-operation-confirmation-dialog'; -import SysAdminRepoTransferDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-repo-transfer-dialog'; - -class Content extends Component { - - constructor(props) { - super(props); - this.state = { - }; - } - - render() { - const { loading, errorMsg, items } = this.props; - if (loading) { - return ; - } else if (errorMsg) { - return

{errorMsg}

; - } else { - const emptyTip = ( - -

{gettext('This user has not created any libraries')}

-
- ); - const table = ( - - - - - - - - - - - - - {items.map((item, index) => { - return (); - })} - -
{/*icon*/}{gettext('Name')}{gettext('Size')}{gettext('Last Update')}{/*Operations*/}
-
- ); - return items.length ? table : emptyTip; - } - } -} - -class Item extends Component { - - constructor(props) { - super(props); - this.state = { - showOpIcon: false, - role: this.props.item.role, - quota_total: this.props.item.quota_total, - isDeleteDialogOpen: false, - isTransferDialogOpen: false, - }; - } - - handleMouseOver = () => { - this.setState({showOpIcon: true}); - } - - handleMouseOut = () => { - this.setState({showOpIcon: false}); - } - - toggleDeleteDialog = () => { - this.setState({isDeleteDialogOpen: !this.state.isDeleteDialogOpen}); - } - - toggleTransferDialog = () => { - this.setState({isTransferDialogOpen: !this.state.isTransferDialogOpen}); - } - - deleteRepo = () => { - this.props.deleteRepo(this.props.item.id); - this.toggleDeleteDialog(); - } - - transferRepo = (receiver) => { - this.props.transferRepo(receiver.email, this.props.item.id); - this.toggleTransferDialog(); - } - - render() { - let { showOpIcon, isDeleteDialogOpen, isTransferDialogOpen } = this.state; - let { item } = this.props; - - let iconUrl = Utils.getLibIconUrl(item); - let iconTitle = Utils.getLibIconTitle(item); - - let repoName = '' + Utils.HTMLescape(item.name) + ''; - let deleteDialogMsg = gettext('Are you sure you want to delete {placeholder} ?'.replace('{placeholder}', repoName)) - - let iconVisibility = this.state.showOpIcon ? '' : ' invisible'; - let deleteIconClassName = 'op-icon sf2-icon-delete' + iconVisibility; - let transferIconClassName = 'op-icon sf2-icon-move' + iconVisibility; - return ( - - - {iconTitle} - {item.name} - {Utils.bytesToSize(item.size)} - {moment(item.last_modify).fromNow()} - - {item.email != username && showOpIcon && - - - - - } - - - {isDeleteDialogOpen && - - } - {isTransferDialogOpen && - - } - - ); - } -} - -class UserOwnedRepos extends Component { - - constructor(props) { - super(props); - this.state = { - loading: true, - errorMsg: '', - repoList: [], - isShowImportWaitingDialog: false, - isShowAddUserWaitingDialog: false - }; - } - - componentDidMount () { - seafileAPI.sysAdminListAllRepoInfoByOwner(this.props.email).then(res => { - this.setState({ - repoList: res.data.repos, - loading: false - }); - }).catch((error) => { - if (error.response) { - if (error.response.status == 403) { - this.setState({ - loading: false, - errorMsg: gettext('Permission denied') - }); - } else { - this.setState({ - loading: false, - errorMsg: gettext('Error') - }); - } - } else { - this.setState({ - loading: false, - errorMsg: gettext('Please check the network.') - }); - } - }); - } - - deleteRepo = (repoID) => { - seafileAPI.sysAdminDeleteRepo(repoID).then(res => { - let newRepoList = this.state.repoList.filter(repo => { - return repo.id != repoID; - }); - this.setState({ - repoList: newRepoList - }); - }).catch((error) => { - let errMessage = Utils.getErrorMsg(error); - toaster.danger(errMessage); - }); - } - - transferRepo = (receiverEmail, repoID) => { - seafileAPI.sysAdminTransferRepo(repoID, receiverEmail).then(res => { - let newRepoList = this.state.repoList.filter(repo => { - return repo.id != repoID; - }); - this.setState({ - repoList: newRepoList - }); - }).catch((error) => { - let errMessage = Utils.getErrorMsg(error); - toaster.danger(errMessage); - }); - } - - render() { - return ( -
- -
- ); - } -} - -export default UserOwnedRepos; diff --git a/frontend/src/pages/sys-admin/users/user-profile.js b/frontend/src/pages/sys-admin/users/user-profile.js deleted file mode 100644 index e9266fdef3..0000000000 --- a/frontend/src/pages/sys-admin/users/user-profile.js +++ /dev/null @@ -1,188 +0,0 @@ -import React, { Component, Fragment } from 'react'; -import { Input, Button } from 'reactstrap'; -import { gettext, enableTwoFactorAuth } from '../../../utils/constants'; -import { Utils } from '../../../utils/utils'; -import PropTypes from 'prop-types'; -import Loading from '../../../components/loading'; -import SysAdminUserSetNameDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-user-set-name-dialog'; -import SysAdminUserSetContactEmailDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-user-set-contact-email-dialog'; -import SysAdminUserSetLoginIDDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-user-set-loginid-dialog'; -import SysAdminUserSetReferenceIDDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-user-set-referenceid-dialog'; -import SysAdminUserSetQuotaDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-user-set-quota-dialog'; -import '../../../css/system-info.css'; - -const { avatarURL } = window.app.config; - -const propTypes = { - email: PropTypes.string.isRequired, - userInfo: PropTypes.object.isRequired, - onNameChanged: PropTypes.func.isRequired, - onContactEmailChanged: PropTypes.func.isRequired, - onLoginIDChanged: PropTypes.func.isRequired, - onReferenceIDChanged: PropTypes.func.isRequired, - onQuotaChanged: PropTypes.func.isRequired -}; - -class UserProfile extends Component { - - constructor(props) { - super(props); - this.state = { - loading: true, - errorMsg: '', - userInfo: {}, - isSetUserNameDialogOpen: false, - isSetUserLoginIDDialogOpen: false, - isSetUserContactEmailDialogOpen: false, - isSetUserReferenceIDDialogOpen: false, - isSetUserQuotaDialogOpen: false - }; - } - - toggleSetUserNameDialog = () => { - this.setState({isSetUserNameDialogOpen: !this.state.isSetUserNameDialogOpen}); - } - - toggleSetUserLoginIDDialog = () => { - this.setState({isSetUserLoginIDDialogOpen: !this.state.isSetUserLoginIDDialogOpen}); - } - - toggleSetUserContactEmailDialog = () => { - this.setState({isSetUserContactEmailDialogOpen: !this.state.isSetUserContactEmailDialogOpen}); - } - - toggleSetUserReferenceIDDialog = () => { - this.setState({isSetUserReferenceIDDialogOpen: !this.state.isSetUserReferenceIDDialogOpen}); - } - - toggleSetUserQuotaDialog = () => { - this.setState({isSetUserQuotaDialogOpen: !this.state.isSetUserQuotaDialogOpen}); - } - - onNameChanged = (name) => { - this.props.onNameChanged(name); - this.toggleSetUserNameDialog(); - } - - onLoginIDChanged = (loginID) => { - this.props.onLoginIDChanged(loginID); - this.toggleSetUserLoginIDDialog(); - } - - onContactEmailChanged = (contactEmail) => { - this.props.onContactEmailChanged(contactEmail); - this.toggleSetUserContactEmailDialog(); - } - - onReferenceIDChanged = (referenceID) => { - this.props.onReferenceIDChanged(referenceID); - this.toggleSetUserReferenceIDDialog(); - } - - onQuotaChanged = (quota) => { - this.props.onQuotaChanged(quota); - this.toggleSetUserQuotaDialog(); - } - - toggleForce2FA = (is_force_2fa) => { - this.props.toggleForce2FA(is_force_2fa); - } - - deleteVerified2FADevices = () => { - this.props.deleteVerified2FADevices(); - } - - render() { - let { errorMsg, isSetUserContactEmailDialogOpen, isSetUserLoginIDDialogOpen, - isSetUserNameDialogOpen, isSetUserQuotaDialogOpen, isSetUserReferenceIDDialogOpen } = this.state; - let { email, name, login_id, contact_email, reference_id, quota_usage, quota_total, is_force_2fa, - has_default_device } = this.props.userInfo; - - if (!this.props.userInfo) { - return ; - } else if (errorMsg) { - return

{errorMsg}

; - } else { - return ( - -
-

{gettext('Avatar')}

- -
-
-

{gettext('Email')}

- {email} -
-
-

{gettext('Name')}

- {name ? name : '--'} - -
-
-

{gettext('Login ID')}

- {login_id ? login_id : '--'} - -
-
-

{gettext('Contact Email')}

- {contact_email ? contact_email : '--'} - -
-
-

{gettext('Reference ID')}

- {reference_id ? reference_id : '--'} - -
-
-

{gettext('Space Used')}{' / '}{gettext('Quota')}

- {Utils.bytesToSize(quota_usage)}{' / '} - {quota_total >= 0 ? Utils.bytesToSize(quota_total) : '--'} - -
- {enableTwoFactorAuth && -
-

{gettext('Two-Factor Authentication')}

- -
- -
- } - {isSetUserNameDialogOpen && - - } - {isSetUserContactEmailDialogOpen && - - } - {isSetUserLoginIDDialogOpen && - - } - {isSetUserReferenceIDDialogOpen && - - } - {isSetUserQuotaDialogOpen && - - } -
- ); - } - } -} - -UserProfile.propTypes = propTypes; - -export default UserProfile; \ No newline at end of file diff --git a/frontend/src/pages/sys-admin/users/user-repos.js b/frontend/src/pages/sys-admin/users/user-repos.js new file mode 100644 index 0000000000..4ce27da1dd --- /dev/null +++ b/frontend/src/pages/sys-admin/users/user-repos.js @@ -0,0 +1,322 @@ +import React, { Component, Fragment } from 'react'; +import { Link } from '@reach/router'; +import moment from 'moment'; +import { Utils } from '../../../utils/utils'; +import { seafileAPI } from '../../../utils/seafile-api'; +import { isPro, siteRoot, loginUrl, gettext } from '../../../utils/constants'; +import toaster from '../../../components/toast'; +import EmptyTip from '../../../components/empty-tip'; +import Loading from '../../../components/loading'; +import CommonOperationConfirmationDialog from '../../../components/dialog/common-operation-confirmation-dialog'; +import TransferDialog from '../../../components/dialog/transfer-dialog'; +import MainPanelTopbar from '../main-panel-topbar'; +import Nav from './user-nav'; +import OpMenu from './user-op-menu'; + +const { enableSysAdminViewRepo } = window.sysadmin.pageOptions; + +class Content extends Component { + + constructor(props) { + super(props); + this.state = { + isItemFreezed: false + }; + } + + onFreezedItem = () => { + this.setState({isItemFreezed: true}); + } + + onUnfreezedItem = () => { + this.setState({isItemFreezed: false}); + } + + render() { + const { loading, errorMsg, items } = this.props; + if (loading) { + return ; + } else if (errorMsg) { + return

{errorMsg}

; + } else { + const emptyTip = ( + +

{gettext('No libraries')}

+
+ ); + const table = ( + + + + + + + + + + + + + {items.map((item, index) => { + return (); + })} + +
{gettext('Name')}{gettext('Size')}{gettext('Last Update')}{/* Operations */}
+
+ ); + return items.length ? table : emptyTip; + } + } +} + +class Item extends Component { + + constructor(props) { + super(props); + this.state = { + isOpIconShown: false, + highlight: false, + isDeleteDialogOpen: false, + isTransferDialogOpen: false + }; + } + + handleMouseEnter = () => { + if (!this.props.isItemFreezed) { + this.setState({ + isOpIconShown: true, + highlight: true + }); + } + } + + handleMouseLeave = () => { + if (!this.props.isItemFreezed) { + this.setState({ + isOpIconShown: false, + highlight: false + }); + } + } + + onUnfreezedItem = () => { + this.setState({ + highlight: false, + isOpIconShow: false + }); + this.props.onUnfreezedItem(); + } + + toggleDeleteDialog = () => { + this.setState({isDeleteDialogOpen: !this.state.isDeleteDialogOpen}); + } + + deleteRepo = () => { + this.props.deleteRepo(this.props.item.id); + } + + toggleTransferDialog = () => { + this.setState({isTransferDialogOpen: !this.state.isTransferDialogOpen}); + } + + transferRepo = (owner) => { + this.props.transferRepo(this.props.item.id, owner.email); + this.toggleTransferDialog(); + } + + renderRepoName = () => { + const { item } = this.props; + const repo = item; + if (repo.name) { + if (isPro && enableSysAdminViewRepo && !repo.encrypted) { + return {repo.name}; + } else { + return repo.name; + } + } else { + return gettext('Broken ({repo_id_placeholder})') + .replace('{repo_id_placeholder}', repo.id); + } + } + + translateOperations = (item) => { + let translateResult = ''; + switch (item) { + case 'Delete': + translateResult = gettext('Delete'); + break; + case 'Transfer': + translateResult = gettext('Transfer'); + break; + } + + return translateResult; + } + + onMenuItemClick = (operation) => { + switch(operation) { + case 'Delete': + this.toggleDeleteDialog(); + break; + case 'Transfer': + this.toggleTransferDialog(); + break; + } + } + + render() { + const { item } = this.props; + const { isOpIconShown, isDeleteDialogOpen, isTransferDialogOpen } = this.state; + + const iconUrl = Utils.getLibIconUrl(item); + const iconTitle = Utils.getLibIconTitle(item); + + const itemName = '' + Utils.HTMLescape(item.name) + ''; + const deleteDialogMsg = gettext('Are you sure you want to delete {placeholder} ?').replace('{placeholder}', itemName); + + return ( + + + {iconTitle} + {this.renderRepoName()} + {Utils.bytesToSize(item.size)} + {moment(item.last_modify).fromNow()} + + {isOpIconShown && + + } + + + {isDeleteDialogOpen && + + } + {isTransferDialogOpen && + + } + + ); + } +} + +class Repos extends Component { + + constructor(props) { + super(props); + this.state = { + loading: true, + errorMsg: '', + userInfo: {}, + repoList: [] + }; + } + + componentDidMount () { + seafileAPI.sysAdminGetUser(this.props.email).then((res) => { + this.setState({ + userInfo: res.data + }); + }); + seafileAPI.sysAdminListReposByOwner(this.props.email).then(res => { + this.setState({ + loading: false, + repoList: res.data.repos + }); + }).catch((error) => { + if (error.response) { + if (error.response.status == 403) { + this.setState({ + loading: false, + errorMsg: gettext('Permission denied') + }); + location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`; + } else { + this.setState({ + loading: false, + errorMsg: gettext('Error') + }); + } + } else { + this.setState({ + loading: false, + errorMsg: gettext('Please check the network.') + }); + } + }); + } + + deleteRepo = (repoID) => { + seafileAPI.sysAdminDeleteRepo(repoID).then(res => { + let newRepoList = this.state.repoList.filter(item => { + return item.id != repoID; + }); + this.setState({repoList: newRepoList}); + toaster.success(gettext('Successfully deleted 1 item.')); + }).catch((error) => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + + transferRepo = (repoID, email) => { + seafileAPI.sysAdminTransferRepo(repoID, email).then((res) => { + let newRepoList = this.state.repoList.filter(item => { + return item.id != repoID; + }); + this.setState({repoList: newRepoList}); + let message = gettext('Successfully transferred the library.'); + toaster.success(message); + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + + render() { + return ( + + +
+
+
+
+
+ ); + } +} + +export default Repos; diff --git a/frontend/src/pages/sys-admin/users/user-share-in-repos.js b/frontend/src/pages/sys-admin/users/user-share-in-repos.js deleted file mode 100644 index dd1585f13c..0000000000 --- a/frontend/src/pages/sys-admin/users/user-share-in-repos.js +++ /dev/null @@ -1,154 +0,0 @@ -import React, { Component, Fragment } from 'react'; -import { seafileAPI } from '../../../utils/seafile-api'; -import { gettext } from '../../../utils/constants'; -import { Utils } from '../../../utils/utils'; -import EmptyTip from '../../../components/empty-tip'; -import moment from 'moment'; -import Loading from '../../../components/loading'; - -class Content extends Component { - - constructor(props) { - super(props); - this.state = { - }; - } - - render() { - const { loading, errorMsg, items } = this.props; - if (loading) { - return ; - } else if (errorMsg) { - return

{errorMsg}

; - } else { - const emptyTip = ( - -

{gettext('This user has no shared libraries')}

-
- ); - const table = ( - - - - - - - - - - - - - {items.map((item, index) => { - return (); - })} - -
{/*icon*/}{gettext('Name')}{gettext('Share From')}{gettext('Size')}{gettext('Last Update')}
- -
- ); - - return items.length ? table : emptyTip; - } - } -} - -class Item extends Component { - - constructor(props) { - super(props); - this.state = { - showOpIcon: false, - role: this.props.item.role, - quota_total: this.props.item.quota_total - }; - } - - handleMouseOver = () => { - this.setState({showOpIcon: true}); - } - - handleMouseOut = () => { - this.setState({showOpIcon: false}); - } - - render() { - let { item } = this.props; - - let iconUrl = Utils.getLibIconUrl(item); - let iconTitle = Utils.getLibIconTitle(item); - - return ( - - - {iconTitle} - {item.name} - {item.owner} - {Utils.bytesToSize(item.size)} - {moment(item.last_modify).fromNow()} - - - ); - } -} - -class UserShareInRepos extends Component { - - constructor(props) { - super(props); - this.state = { - loading: true, - errorMsg: '', - repoList: [], - isShowImportWaitingDialog: false, - isShowAddUserWaitingDialog: false - }; - } - - componentDidMount () { - seafileAPI.sysAdminListShareInRepo(this.props.email).then(res => { - this.setState({ - repoList: res.data.repo_list, - loading: false - }); - }).catch((error) => { - if (error.response) { - if (error.response.status == 403) { - this.setState({ - loading: false, - errorMsg: gettext('Permission denied') - }); - } else { - this.setState({ - loading: false, - errorMsg: gettext('Error') - }); - } - } else { - this.setState({ - loading: false, - errorMsg: gettext('Please check the network.') - }); - } - }); - } - - render() { - return ( -
- -
- ); - } -} - -export default UserShareInRepos; \ No newline at end of file diff --git a/frontend/src/pages/sys-admin/users/user-share-links.js b/frontend/src/pages/sys-admin/users/user-share-links.js deleted file mode 100644 index 40173f8eb6..0000000000 --- a/frontend/src/pages/sys-admin/users/user-share-links.js +++ /dev/null @@ -1,255 +0,0 @@ -import React, { Component, Fragment } from 'react'; -import { seafileAPI } from '../../../utils/seafile-api'; -import { gettext } from '../../../utils/constants'; -import toaster from '../../../components/toast'; -import { Utils } from '../../../utils/utils'; -import EmptyTip from '../../../components/empty-tip'; -import Loading from '../../../components/loading'; -import CommonOperationConfirmationDialog from '../../../components/dialog/common-operation-confirmation-dialog'; - -class Content extends Component { - - constructor(props) { - super(props); - } - - render() { - const { loading, errorMsg, shareLinkItems, uploadLinkItems } = this.props; - if (loading) { - return ; - } else if (errorMsg) { - return

{errorMsg}

; - } else { - const emptyTip = ( - -

{gettext('This user has not created any shared links')}

-
- ); - const table = ( - - - - - - - - - - - - - - {shareLinkItems.length > 0 && shareLinkItems.map((item, index) => { - return (); - })} - {uploadLinkItems.length > 0 && uploadLinkItems.map((item, index) => { - return (); - })} - -
{/*icon*/}{gettext('Name')}{gettext('Size')}{gettext('Type')}{gettext('Visits')}{/*Operations*/}
-
- ); - - return shareLinkItems.length || uploadLinkItems.length ? table : emptyTip; - } - } -} - -class Item extends Component { - - constructor(props) { - super(props); - this.state = { - showOpIcon: false, - isDeleteDialogOpen: false - }; - } - - handleMouseOver = () => { - this.setState({showOpIcon: true}); - } - - handleMouseOut = () => { - this.setState({showOpIcon: false}); - } - - toggleDeleteDialog = () => { - this.setState({isDeleteDialogOpen: !this.state.isDeleteDialogOpen}); - } - - deleteLink = () => { - this.props.deleteLink(this.props.item.token, this.props.type); - this.toggleDeleteDialog(); - } - - render() { - let { showOpIcon, isDeleteDialogOpen } = this.state; - let { item, type } = this.props; - - let iconUrl; - if (type == 'Upload' || (type== 'Download' && item.is_dir)) { - iconUrl = Utils.getFolderIconUrl(); - } else { - iconUrl = Utils.getFileIconUrl(item.obj_name); - } - - let itemName = '' + Utils.HTMLescape(item.obj_name == '/' ? item.repo_name : item.obj_name) + ''; - let deleteDialogMsg = gettext('Are you sure you want to delete link {placeholder} ?'.replace('{placeholder}', itemName)) - - let iconVisibility = showOpIcon ? '' : ' invisible'; - let deleteIconClassName = 'op-icon sf2-icon-delete' + iconVisibility; - return ( - - - - {item.obj_name == '/' ? item.repo_name : item.obj_name} - {Utils.bytesToSize(item.size)} - {type} - {item.view_cnt} - - - - - {isDeleteDialogOpen && - - } - - ); - } -} - -class UserShareLinks extends Component { - - constructor(props) { - super(props); - this.state = { - loading: true, - errorMsg: '', - repoList: {}, - shareLinkList: {}, - uploadLinkList: {}, - isShowImportWaitingDialog: false - }; - } - - componentDidMount () { - this.getShareLinkList(); - this.getUploadLinkList(); - } - - getShareLinkList = () => { - seafileAPI.sysAdminListShareLinksByUser(this.props.email).then(res => { - this.setState({ - shareLinkList: res.data.share_link_list, - loading: false - }); - }).catch((error) => { - if (error.response) { - if (error.response.status == 403) { - this.setState({ - loading: false, - errorMsg: gettext('Permission denied') - }); - } else { - this.setState({ - loading: false, - errorMsg: gettext('Error') - }); - } - } else { - this.setState({ - loading: false, - errorMsg: gettext('Please check the network.') - }); - } - }); - } - - getUploadLinkList = () => { - seafileAPI.sysAdminListUploadLinksByUser(this.props.email).then(res => { - this.setState({ - uploadLinkList: res.data.upload_link_list, - loading: false - }); - }).catch((error) => { - if (error.response) { - if (error.response.status == 403) { - this.setState({ - loading: false, - errorMsg: gettext('Permission denied') - }); - } else { - this.setState({ - loading: false, - errorMsg: gettext('Error') - }); - } - } else { - this.setState({ - loading: false, - errorMsg: gettext('Please check the network.') - }); - } - }); - } - - deleteLink = (token, type) => { - if (type == 'Download') { - seafileAPI.sysAdminRemoveShareLink(token).then(res => { - let newShareLinkList = this.state.shareLinkList.filter(item=> { - return item.token != token; - }); - this.setState({ - shareLinkList: newShareLinkList - }); - }).catch((error) => { - let errMessage = Utils.getErrorMsg(error); - toaster.danger(errMessage); - }); - } else if (type == 'Upload') { - seafileAPI.sysAdminRemoveShareLink(token).then(res => { - let newUploadLinkList = this.state.uploadLinkList.filter(item=> { - return item.token != token; - }); - this.setState({ - uploadLinkList: newUploadLinkList - }); - }).catch((error) => { - let errMessage = Utils.getErrorMsg(error); - toaster.danger(errMessage); - }); - } - } - - render() { - return ( -
- -
- ); - } -} - -export default UserShareLinks; diff --git a/frontend/src/pages/sys-admin/users/user-shared-repos.js b/frontend/src/pages/sys-admin/users/user-shared-repos.js new file mode 100644 index 0000000000..98077e6f1d --- /dev/null +++ b/frontend/src/pages/sys-admin/users/user-shared-repos.js @@ -0,0 +1,183 @@ +import React, { Component, Fragment } from 'react'; +import { Link } from '@reach/router'; +import moment from 'moment'; +import { Utils } from '../../../utils/utils'; +import { seafileAPI } from '../../../utils/seafile-api'; +import { isPro, siteRoot, loginUrl, gettext } from '../../../utils/constants'; +import toaster from '../../../components/toast'; +import EmptyTip from '../../../components/empty-tip'; +import Loading from '../../../components/loading'; +import CommonOperationConfirmationDialog from '../../../components/dialog/common-operation-confirmation-dialog'; +import TransferDialog from '../../../components/dialog/transfer-dialog'; +import MainPanelTopbar from '../main-panel-topbar'; +import Nav from './user-nav'; +import OpMenu from './user-op-menu'; + +const { enableSysAdminViewRepo } = window.sysadmin.pageOptions; + +class Content extends Component { + + constructor(props) { + super(props); + } + + render() { + const { loading, errorMsg, items } = this.props; + if (loading) { + return ; + } else if (errorMsg) { + return

{errorMsg}

; + } else { + const emptyTip = ( + +

{gettext('No libraries')}

+
+ ); + const table = ( + + + + + + + + + + + + + {items.map((item, index) => { + return (); + })} + +
{gettext('Name')}{gettext('Share From')}{gettext('Size')}{gettext('Last Update')}
+
+ ); + return items.length ? table : emptyTip; + } + } +} + +class Item extends Component { + + constructor(props) { + super(props); + } + + renderRepoName = () => { + const { item } = this.props; + const repo = item; + if (repo.name) { + if (isPro && enableSysAdminViewRepo && !repo.encrypted) { + return {repo.name}; + } else { + return repo.name; + } + } else { + return gettext('Broken ({repo_id_placeholder})') + .replace('{repo_id_placeholder}', repo.id); + } + } + + getOwnerUrl = () => { + let url; + const { item } = this.props; + const index = item.owner_email.indexOf('@seafile_group'); + if (index == -1) { + url = `${siteRoot}sys/users/${encodeURIComponent(item.owner_email)}/`; + } else { + const groupID = item.owner_email.substring(0, index); + url = `${siteRoot}sys/departments/${groupID}/`; + } + return url; + } + + render() { + const { item } = this.props; + const iconUrl = Utils.getLibIconUrl(item); + const iconTitle = Utils.getLibIconTitle(item); + return ( + + + {iconTitle} + {this.renderRepoName()} + {item.owner_name} + {Utils.bytesToSize(item.size)} + {moment(item.last_modify).fromNow()} + + + ); + } +} + +class Repos extends Component { + + constructor(props) { + super(props); + this.state = { + loading: true, + errorMsg: '', + userInfo: {}, + repoList: [] + }; + } + + componentDidMount () { + seafileAPI.sysAdminGetUser(this.props.email).then((res) => { + this.setState({ + userInfo: res.data + }); + }); + seafileAPI.sysAdminListShareInRepos(this.props.email).then(res => { + this.setState({ + loading: false, + repoList: res.data.repo_list + }); + }).catch((error) => { + if (error.response) { + if (error.response.status == 403) { + this.setState({ + loading: false, + errorMsg: gettext('Permission denied') + }); + location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`; + } else { + this.setState({ + loading: false, + errorMsg: gettext('Error') + }); + } + } else { + this.setState({ + loading: false, + errorMsg: gettext('Please check the network.') + }); + } + }); + } + + render() { + return ( + + +
+
+
+
+
+ ); + } +} + +export default Repos; diff --git a/frontend/src/pages/sys-admin/users/users-nav.js b/frontend/src/pages/sys-admin/users/users-nav.js index e10e2ff045..76020f725c 100644 --- a/frontend/src/pages/sys-admin/users/users-nav.js +++ b/frontend/src/pages/sys-admin/users/users-nav.js @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Link } from '@reach/router'; -import { siteRoot, gettext, haveLDAP } from '../../../utils/constants'; +import { siteRoot, gettext, haveLDAP, isDefaultAdmin } from '../../../utils/constants'; const propTypes = { currentItem: PropTypes.string.isRequired @@ -12,19 +12,19 @@ class Nav extends React.Component { constructor(props) { super(props); this.navItems = [ - {name: 'database', urlPart: 'users', text: gettext('Database')}, - {name: 'admin', urlPart: 'users/admins', text: gettext('Admin')} + {name: 'database', urlPart: 'users', text: gettext('Database')} ]; - /* if (haveLDAP) { - this.navItems.splice(1, 0, + this.navItems.push( + {name: 'ldap', urlPart: 'users/ldap', text: gettext('LDAP')}, {name: 'ldap-imported', urlPart: 'users/ldap-imported', text: gettext('LDAP(imported)')} ); - this.navItems.splice(1, 0, - {name: 'ldap', urlPart: 'users/ldap', text: gettext('LDAP')} + } + if (isDefaultAdmin) { + this.navItems.push( + {name: 'admin', urlPart: 'users/admins', text: gettext('Admin')} ); } - */ } render() { diff --git a/frontend/src/pages/sys-admin/users/users.js b/frontend/src/pages/sys-admin/users/users.js index 118d49bc8c..4d499a4d40 100644 --- a/frontend/src/pages/sys-admin/users/users.js +++ b/frontend/src/pages/sys-admin/users/users.js @@ -4,7 +4,7 @@ import { Button } from 'reactstrap'; import moment from 'moment'; import { Utils } from '../../../utils/utils'; import { seafileAPI } from '../../../utils/seafile-api'; -import { isPro, username, gettext, multiInstitution, siteRoot } from '../../../utils/constants'; +import { isPro, username, gettext, multiInstitution, siteRoot, loginUrl } from '../../../utils/constants'; import toaster from '../../../components/toast'; import EmptyTip from '../../../components/empty-tip'; import Loading from '../../../components/loading'; @@ -386,14 +386,14 @@ class Item extends Component { {(multiInstitution && !isAdmin) && - 0} - options={institutions} - currentOption={item.institution} - onOptionChanged={this.updateInstitution} - translateOption={this.translateInstitution} - /> + 0} + options={institutions} + currentOption={item.institution} + onOptionChanged={this.updateInstitution} + translateOption={this.translateInstitution} + /> } @@ -574,6 +574,7 @@ class UsersAll extends Component { loading: false, errorMsg: gettext('Permission denied') }); + location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`; } else { this.setState({ loading: false, @@ -597,7 +598,7 @@ class UsersAll extends Component { userList: users, loading: false, hasNextPage: Utils.hasNextPage(page, perPage, res.data.total_count), - currentPage: page, + currentPage: page }); }).catch((error) => { if (error.response) { @@ -659,17 +660,28 @@ class UsersAll extends Component { return user.email; }); seafileAPI.sysAdminDeleteUserInBatch(emails).then(res => { - let oldUserList = this.state.userList; - let newUserList = oldUserList.filter(oldUser => { - return !res.data.success.some(deletedUser =>{ - return deletedUser.email == oldUser.email; + if (res.data.success.length) { + let oldUserList = this.state.userList; + let newUserList = oldUserList.filter(oldUser => { + return !res.data.success.some(deletedUser =>{ + return deletedUser.email == oldUser.email; + }); }); + this.setState({ + userList: newUserList, + hasUserSelected: emails.length != res.data.success.length + }); + const length = res.data.success.length; + const msg = length == 1 ? + gettext('Successfully deleted 1 user.') : + gettext('Successfully deleted {user_number_placeholder} users.') + .replace('{user_number_placeholder}', length); + toaster.success(msg); + } + res.data.failed.map(item => { + const msg = `${item.email}: ${item.error_msg}`; + toaster.danger(msg); }); - this.setState({ - userList: newUserList, - hasUserSelected: emails.length != res .data.success.length - }); - // todo: msg }).catch((error) => { let errMessage = Utils.getErrorMsg(error); toaster.danger(errMessage); @@ -678,13 +690,22 @@ class UsersAll extends Component { importUserInBatch = (file) => { toaster.notify(gettext('It may take some time, please wait.')); - // TODO: the url needs to be changed seafileAPI.sysAdminImportUserViaFile(file).then((res) => { - // currently using old view in python, no return newUserList, - // so after import new users, just send a get user list again. - this.toggleImportUserDialog(); - this.getUsersListByPage(1); - toaster.success(gettext('Import succeeded.')); + if (res.data.success.length) { + const users = res.data.success.map(item => { + if (item.institution == undefined) { + item.institution = ''; + } + return new SysAdminUser(item); + }); + this.setState({ + userList: users.concat(this.state.userList) + }); + } + res.data.failed.map(item => { + const msg = `${item.email}: ${item.error_msg}`; + toaster.danger(msg); + }); }).catch((error) => { let errMsg = Utils.getErrorMsg(error); toaster.danger(errMsg); @@ -853,6 +874,7 @@ class UsersAll extends Component { hasNextPage={this.state.hasNextPage} curPerPage={this.state.perPage} resetPerPage={this.resetPerPage} + getListByPage={this.getUsersListByPage} updateUser={this.updateUser} deleteUser={this.deleteUser} updateAdminRole={this.updateAdminRole} @@ -860,7 +882,6 @@ class UsersAll extends Component { onUserSelected={this.onUserSelected} isAllUsersSelected={this.isAllUsersSelected} toggleSelectAllUsers={this.toggleSelectAllUsers} - getListByPage={this.getUsersListByPage} />
diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js index 4821fb1e95..861b1b995f 100644 --- a/frontend/src/utils/constants.js +++ b/frontend/src/utils/constants.js @@ -125,7 +125,4 @@ export const canViewUserLog = window.sysadmin ? window.sysadmin.pageOptions.admi export const canViewAdminLog = window.sysadmin ? window.sysadmin.pageOptions.admin_permissions.can_view_admin_log : ''; export const enableWorkWeixin = window.sysadmin ? window.sysadmin.pageOptions.enable_work_weixin : ''; export const enableSysAdminViewRepo = window.sysadmin ? window.sysadmin.pageOptions.enableSysAdminViewRepo : ''; -export const enableTwoFactorAuth = window.sysadmin ? window.sysadmin.pageOptions.enable_two_factor_auth : ''; -export const sendEmailOnResettingUserPasswd = window.sysadmin ? window.sysadmin.pageOptions.send_email_on_resetting_user_passwd : ''; -export const isEmailConfiguredInSysAdmin = window.sysadmin ? window.sysadmin.pageOptions.is_email_configured : ''; export const haveLDAP = window.sysadmin ? window.sysadmin.pageOptions.haveLDAP : ''; diff --git a/seahub/templates/sysadmin/sysadmin_react_app.html b/seahub/templates/sysadmin/sysadmin_react_app.html index ebe956e4b9..4c6f2de6d5 100644 --- a/seahub/templates/sysadmin/sysadmin_react_app.html +++ b/seahub/templates/sysadmin/sysadmin_react_app.html @@ -11,7 +11,7 @@ multi_tenancy: {% if multi_tenancy %} true {% else %} false {% endif %}, multi_institution: {% if multi_institution %} true {% else %} false {% endif %}, institutions: (function() { - var list = []; + var list = ['']; // institution can set to be '' {% for inst in institutions %} list.push('{{inst|escapejs}}'); {% endfor %} @@ -26,7 +26,7 @@ enableSysAdminViewRepo: {% if enable_sys_admin_view_repo %} true {% else %} false {% endif %}, trashReposExpireDays: {{ trash_repos_expire_days }}, send_email_on_adding_system_member: {% if send_email_on_adding_system_member %} true {% else %} false {% endif %}, - enable_two_factor_auth: {% if enable_two_factor_auth %} true {% else %} false {% endif %}, + twoFactorAuthEnabled: {% if two_factor_auth_enabled %} true {% else %} false {% endif %}, availableRoles: (function() { var list = []; {% for role in available_roles %} diff --git a/seahub/urls.py b/seahub/urls.py index 14464265a6..1539c329db 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -689,6 +689,10 @@ urlpatterns = [ url(r'^sys/users/ldap/$', sysadmin_react_fake_view, name="sys_users_ldap"), url(r'^sys/users/ldap-imported/$', sysadmin_react_fake_view, name="sys_users_ldap_imported"), url(r'^sys/users/(?P[^/]+)/$', sysadmin_react_fake_view, name="sys_user"), + url(r'^sys/users/(?P[^/]+)/owned-libraries/$', sysadmin_react_fake_view, name="sys_user_repos"), + url(r'^sys/users/(?P[^/]+)/shared-libraries/$', sysadmin_react_fake_view, name="sys_user_shared_repos"), + url(r'^sys/users/(?P[^/]+)/shared-links/$', sysadmin_react_fake_view, name="sys_user_shared_links"), + url(r'^sys/users/(?P[^/]+)/groups/$', sysadmin_react_fake_view, name="sys_user_groups"), url(r'^sys/logs/login/$', sysadmin_react_fake_view, name="sys_logs_login"), url(r'^sys/logs/file-access/$', sysadmin_react_fake_view, name="sys_logs_file_access"), url(r'^sys/logs/file-update/$', sysadmin_react_fake_view, name="sys_logs_file_update"), diff --git a/seahub/views/sysadmin.py b/seahub/views/sysadmin.py index d08030a565..d6b10a5541 100644 --- a/seahub/views/sysadmin.py +++ b/seahub/views/sysadmin.py @@ -76,7 +76,7 @@ import seahub.settings as settings from seahub.settings import INIT_PASSWD, SITE_ROOT, \ SEND_EMAIL_ON_ADDING_SYSTEM_MEMBER, SEND_EMAIL_ON_RESETTING_USER_PASSWD, \ ENABLE_SYS_ADMIN_VIEW_REPO, ENABLE_GUEST_INVITATION, \ - ENABLE_LIMIT_IPADDRESS, ENABLE_TWO_FACTOR_AUTH + ENABLE_LIMIT_IPADDRESS try: from seahub.settings import ENABLE_TRIAL_ACCOUNT except: @@ -164,10 +164,10 @@ def sysadmin_react_fake_view(request, **kwargs): 'enable_work_weixin': ENABLE_WORK_WEIXIN, 'enable_sys_admin_view_repo': ENABLE_SYS_ADMIN_VIEW_REPO, 'trash_repos_expire_days': expire_days if expire_days > 0 else 30, - 'enable_two_factor_auth': ENABLE_TWO_FACTOR_AUTH, 'available_roles': get_available_roles(), 'available_admin_roles': get_available_admin_roles(), 'have_ldap': get_ldap_info(), + 'two_factor_auth_enabled': has_two_factor_auth(), }) @login_required