From d0e28c0365fadab33c9fb06cab22b3267a8f16b7 Mon Sep 17 00:00:00 2001 From: Leo Date: Tue, 24 Sep 2019 12:18:53 +0800 Subject: [PATCH] sysadmin reconstruct repo page (#3980) --- .../common-operation-confirmation-dialog.js | 44 ++ .../components/dialog/delete-repo-dialog.js | 2 +- .../sysadmin-create-repo-dialog.js | 106 +++++ .../sysadmin-lib-history-setting-dialog.js | 153 +++++++ .../sysadmin-dialog/sysadmin-share-dialog.js | 96 ++++ .../sysadmin-share-to-group.js | 302 +++++++++++++ .../sysadmin-dialog/sysadmin-share-to-user.js | 282 ++++++++++++ .../src/components/dialog/transfer-dialog.js | 10 +- frontend/src/models/system-admin/dirent.js | 14 + frontend/src/pages/sys-admin/index.js | 40 +- .../src/pages/sys-admin/repos/all-repos.js | 412 ++++++++++++++++++ .../src/pages/sys-admin/repos/dir-content.js | 129 ++++++ .../src/pages/sys-admin/repos/dir-path-bar.js | 71 +++ .../src/pages/sys-admin/repos/dir-view.js | 231 ++++++++++ .../src/pages/sys-admin/repos/repo-op-menu.js | 96 ++++ .../src/pages/sys-admin/repos/repos-nav.js | 41 ++ .../src/pages/sys-admin/repos/system-repo.js | 134 ++++++ .../src/pages/sys-admin/repos/trash-repos.js | 301 +++++++++++++ frontend/src/pages/sys-admin/side-panel.js | 8 +- frontend/src/utils/utils.js | 10 + media/css/seahub_react.css | 1 + .../sysadmin/sysadmin_react_app.html | 2 + seahub/urls.py | 6 + seahub/views/sysadmin.py | 10 +- 24 files changed, 2486 insertions(+), 15 deletions(-) create mode 100644 frontend/src/components/dialog/common-operation-confirmation-dialog.js create mode 100644 frontend/src/components/dialog/sysadmin-dialog/sysadmin-create-repo-dialog.js create mode 100644 frontend/src/components/dialog/sysadmin-dialog/sysadmin-lib-history-setting-dialog.js create mode 100644 frontend/src/components/dialog/sysadmin-dialog/sysadmin-share-dialog.js create mode 100644 frontend/src/components/dialog/sysadmin-dialog/sysadmin-share-to-group.js create mode 100644 frontend/src/components/dialog/sysadmin-dialog/sysadmin-share-to-user.js create mode 100644 frontend/src/models/system-admin/dirent.js create mode 100644 frontend/src/pages/sys-admin/repos/all-repos.js create mode 100644 frontend/src/pages/sys-admin/repos/dir-content.js create mode 100644 frontend/src/pages/sys-admin/repos/dir-path-bar.js create mode 100644 frontend/src/pages/sys-admin/repos/dir-view.js create mode 100644 frontend/src/pages/sys-admin/repos/repo-op-menu.js create mode 100644 frontend/src/pages/sys-admin/repos/repos-nav.js create mode 100644 frontend/src/pages/sys-admin/repos/system-repo.js create mode 100644 frontend/src/pages/sys-admin/repos/trash-repos.js diff --git a/frontend/src/components/dialog/common-operation-confirmation-dialog.js b/frontend/src/components/dialog/common-operation-confirmation-dialog.js new file mode 100644 index 0000000000..b4b2ceb9b5 --- /dev/null +++ b/frontend/src/components/dialog/common-operation-confirmation-dialog.js @@ -0,0 +1,44 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap'; +import { gettext } from '../../utils/constants'; + +const propTypes = { + title: PropTypes.string.isRequired, + message: PropTypes.string.isRequired, + confirmBtnText: PropTypes.string, + executeOperation: PropTypes.func.isRequired, + toggleDialog: PropTypes.func.isRequired +}; + +class CommonOperationConfirmationDialog extends Component { + + toggle = () => { + this.props.toggleDialog(); + } + + executeOperation = () => { + this.toggle(); + this.props.executeOperation(); + } + + render() { + let { title, message, confirmBtnText } = this.props; + return ( + + {title} + +

+
+ + + + +
+ ); + } +} + +CommonOperationConfirmationDialog.propTypes = propTypes; + +export default CommonOperationConfirmationDialog; diff --git a/frontend/src/components/dialog/delete-repo-dialog.js b/frontend/src/components/dialog/delete-repo-dialog.js index ea77d80214..b01ef1f468 100644 --- a/frontend/src/components/dialog/delete-repo-dialog.js +++ b/frontend/src/components/dialog/delete-repo-dialog.js @@ -23,7 +23,7 @@ class DeleteRepoDialog extends Component { render() { const repo = this.props.repo; - const repoName = '' + Utils.HTMLescape(repo.repo_name) + ''; + const repoName = '' + Utils.HTMLescape(repo.repo_name || repo.name) + ''; let message = gettext('Are you sure you want to delete %s ?'); message = message.replace('%s', repoName); diff --git a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-create-repo-dialog.js b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-create-repo-dialog.js new file mode 100644 index 0000000000..3bf676ea4c --- /dev/null +++ b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-create-repo-dialog.js @@ -0,0 +1,106 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button, Modal, ModalHeader, Input, ModalBody, ModalFooter, Form, FormGroup, Label, Alert } from 'reactstrap'; +import { gettext } from '../../../utils/constants'; +import UserSelect from '../../user-select'; + + +const propTypes = { + createRepo: PropTypes.func.isRequired, + toggleDialog: PropTypes.func.isRequired, +}; + +class SysAdminCreateRepoDialog extends React.Component { + constructor(props) { + super(props); + this.state = { + repoName: '', + ownerEmail: '', + errMessage: '', + isSubmitBtnActive: false + }; + this.newInput = React.createRef(); + } + + handleRepoNameChange = (e) => { + if (!e.target.value.trim()) { + this.setState({isSubmitBtnActive: false}); + } else { + this.setState({isSubmitBtnActive: true}); + } + + this.setState({repoName: e.target.value}); + } + + handleSubmit = () => { + const { repoName, ownerEmail } = this.state; + this.props.createRepo(repoName.trim(), ownerEmail); + this.toggle(); + } + + handleSelectChange = (option) => { + // option can be null + this.setState({ + ownerEmail: option ? option.email : '' + }); + } + + handleKeyPress = (e) => { + if (e.key === 'Enter') { + this.handleSubmit(); + e.preventDefault(); + } + } + + toggle = () => { + this.props.toggleDialog(); + } + + componentDidMount() { + this.newInput.focus(); + } + + render() { + return ( + + {gettext('New Library')} + +
+ + + {this.newInput = input;}} + value={this.state.repoName} + onChange={this.handleRepoNameChange} + /> + + + + + +
+ {this.state.errMessage && {this.state.errMessage}} +
+ + + + +
+ ); + } +} + +SysAdminCreateRepoDialog.propTypes = propTypes; + +export default SysAdminCreateRepoDialog; diff --git a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-lib-history-setting-dialog.js b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-lib-history-setting-dialog.js new file mode 100644 index 0000000000..f5a9839196 --- /dev/null +++ b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-lib-history-setting-dialog.js @@ -0,0 +1,153 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button, Modal, ModalHeader, ModalBody, ModalFooter, Form, FormGroup, Label, Input, Alert } from 'reactstrap'; +import { gettext } from '../../../utils/constants'; +import { seafileAPI } from '../../../utils/seafile-api.js'; +import { Utils } from '../../../utils/utils'; +import toaster from '../../toast'; + +const propTypes = { + itemName: PropTypes.string.isRequired, + toggleDialog: PropTypes.func.isRequired, + repoID: PropTypes.string.isRequired, +}; + +class SysAdminLibHistorySettingDialog extends React.Component { + + constructor(props) { + super(props); + this.state = { + keepDays: -1, + expireDays: 30, + disabled: true, + allHistory: true, + noHistory: false, + autoHistory: false, + errorInfo: '' + }; + } + + componentDidMount() { + seafileAPI.sysAdminGetRepoHistorySetting(this.props.repoID).then(res => { + this.setState({ + keepDays: res.data.keep_days, + allHistory: res.data.keep_days < 0 ? true : false, + noHistory: res.data.keep_days === 0 ? true : false, + autoHistory: res.data.keep_days > 0 ? true : false, + disabled: res.data.keep_days > 0 ? false : true, + expireDays: res.data.keep_days > 0 ? res.data.keep_days : 30, + }); + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + + submit = () => { + let days = this.state.keepDays; + if (this.state.autoHistory) { + days = this.state.expireDays; + } + let repoID = this.props.repoID; + let reg = /^-?\d+$/; + let flag = reg.test(days); + if (flag) { + let message = gettext('Successfully set library history.'); + seafileAPI.sysAdminUpdateRepoHistorySetting(repoID, days).then(res => { + toaster.success(message); + this.setState({keepDays: res.data.keep_days}); + this.props.toggleDialog(); + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } else { + this.setState({ + errorInfo: gettext('Please enter a non-negative integer'), + }); + } + } + + handleKeyPress = (e) => { + if (e.key === 'Enter') { + this.submit(); + e.preventDefault(); + } + } + + onChange = (e) => { + let num = e.target.value; + this.setState({ + keepDays: num, + expireDays: num, + }); + } + + setLimitDays = (type) => { + if (type === 'allHistory') { + this.setState({ + keepDays: -1, + }); + } else if (type === 'noHistory') { + this.setState({ + keepDays: 0, + }); + } else { + this.setState({ + disabled: false + }); + } + + this.setState({ + allHistory: type === 'allHistory' ? true : false, + noHistory: type === 'noHistory' ? true : false, + autoHistory: type === 'autoHistory' ? true : false, + }); + } + + render() { + const itemName = this.props.itemName; + return ( + + + {itemName}{' '} + {gettext('History Setting')} + + +
+ + {this.setLimitDays('allHistory');}}/>{' '} + + + + {this.setLimitDays('noHistory');}}/>{' '} + + + + {this.setLimitDays('autoHistory');}}/>{' '} + + {' '} + + + {this.state.errorInfo && {this.state.errorInfo}} +
+
+ + + + +
+ ); + } +} + +SysAdminLibHistorySettingDialog.propTypes = propTypes; + +export default SysAdminLibHistorySettingDialog; diff --git a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-share-dialog.js b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-share-dialog.js new file mode 100644 index 0000000000..14b787c678 --- /dev/null +++ b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-share-dialog.js @@ -0,0 +1,96 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { Modal, ModalHeader, ModalBody, TabContent, TabPane, Nav, NavItem, NavLink } from 'reactstrap'; +import { gettext } from '../../../utils/constants'; +import SysAdminShareToUser from './sysadmin-share-to-user'; +import SysAdminShareToGroup from './sysadmin-share-to-group'; +import '../../../css/share-link-dialog.css'; + +const propTypes = { + itemName: PropTypes.string.isRequired, + itemPath: PropTypes.string.isRequired, + toggleDialog: PropTypes.func.isRequired, + repoID: PropTypes.string.isRequired, + isGroupOwnedRepo: PropTypes.bool.isRequired, + repoEncrypted: PropTypes.bool, + userPerm: PropTypes.string, + enableDirPrivateShare: PropTypes.bool +}; + +class SysAdminShareDialog extends React.Component { + constructor(props) { + super(props); + this.state = { + activeTab: this.getInitialActiveTab(), + isRepoOwner: false + }; + } + + getInitialActiveTab = () => { + return 'shareToUser'; + } + + toggle = (tab) => { + if (this.state.activeTab !== tab) { + this.setState({activeTab: tab}); + } + } + + renderDirContent = () => { + let activeTab = this.state.activeTab; + const { enableDirPrivateShare, isGroupOwnedRepo } = this.props; + return ( + +
+ +
+
+ + {enableDirPrivateShare && + + + + + + + + + } + +
+
+ ); + } + + render() { + return ( +
+ + {gettext('Share')} {this.props.itemName} + + {this.renderDirContent()} + + +
+ ); + } +} + +SysAdminShareDialog.propTypes = propTypes; + +export default SysAdminShareDialog; diff --git a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-share-to-group.js b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-share-to-group.js new file mode 100644 index 0000000000..eae868be10 --- /dev/null +++ b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-share-to-group.js @@ -0,0 +1,302 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { Button } from 'reactstrap'; +import Select from 'react-select'; +import makeAnimated from 'react-select/lib/animated'; +import { isPro, gettext } from '../../../utils/constants'; +import { seafileAPI } from '../../../utils/seafile-api.js'; +import { Utils } from '../../../utils/utils'; +import toaster from '../../toast'; +import SharePermissionEditor from '../../select-editor/share-permission-editor'; + +class GroupItem extends React.Component { + + constructor(props) { + super(props); + this.state = { + isOperationShow: false + }; + } + + onMouseEnter = () => { + this.setState({isOperationShow: true}); + } + + onMouseLeave = () => { + this.setState({isOperationShow: false}); + } + + deleteShareItem = () => { + let item = this.props.item; + this.props.deleteShareItem(item.group_id); + } + + onChangeUserPermission = (permission) => { + let item = this.props.item; + this.props.onChangeUserPermission(item, permission); + } + + render() { + let item = this.props.item; + let currentPermission = item.is_admin ? 'admin' : item.permission; + return ( + + {item.group_name} + + + + + + + + + ); + } +} + +class GroupList extends React.Component { + + render() { + let items = this.props.items; + return ( + + {items.map((item, index) => { + return ( + + ); + })} + + ); + } +} + +const propTypes = { + isGroupOwnedRepo: PropTypes.bool, + itemPath: PropTypes.string.isRequired, + itemType: PropTypes.string.isRequired, + repoID: PropTypes.string.isRequired, + isRepoOwner: PropTypes.bool.isRequired, +}; + +const NoOptionsMessage = (props) => { + return ( +
{gettext('Group not found')}
+ ); +}; + +class SysAdminShareToGroup extends React.Component { + + constructor(props) { + super(props); + this.state = { + selectedOption: null, + errorMsg: [], + permission: 'rw', + sharedItems: [] + }; + this.options = []; + this.permissions = ['rw', 'r']; + if (isPro) { + this.permissions.push('admin', 'cloud-edit', 'preview'); + } + } + + handleSelectChange = (option) => { + this.setState({selectedOption: option}); + } + + componentDidMount() { + this.loadOptions(); + this.listSharedGroups(); + } + + loadOptions = () => { + seafileAPI.shareableGroups().then((res) => { + this.options = []; + for (let i = 0 ; i < res.data.length; i++) { + let obj = {}; + obj.value = res.data[i].name; + obj.id = res.data[i].id; + obj.label = res.data[i].name; + this.options.push(obj); + } + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + + listSharedGroups = () => { + let repoID = this.props.repoID; + seafileAPI.sysAdminListRepoSharedItems(repoID, 'group').then((res) => { + if(res.data.length !== 0) { + this.setState({ + sharedItems: res.data + }); + } + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + + setPermission = (permission) => { + this.setState({permission: permission}); + } + + shareToGroup = () => { + let groups = []; + let repoID = this.props.repoID; + if (this.state.selectedOption && this.state.selectedOption.length > 0 ) { + for (let i = 0; i < this.state.selectedOption.length; i ++) { + groups[i] = this.state.selectedOption[i].id; + } + } + seafileAPI.sysAdminAddRepoSharedItem(repoID, 'group', groups, this.state.permission).then(res => { + let errorMsg = []; + if (res.data.failed.length > 0) { + for (let i = 0 ; i < res.data.failed.length ; i++) { + errorMsg[i] = res.data.failed[i]; + } + } + let items = res.data.success; + this.setState({ + errorMsg: errorMsg, + sharedItems: this.state.sharedItems.concat(items), + selectedOption: null, + permission: 'rw', + }); + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + + deleteShareItem = (groupID) => { + let repoID = this.props.repoID; + seafileAPI.sysAdminDeleteRepoSharedItem(repoID, 'group', groupID).then(() => { + this.setState({ + sharedItems: this.state.sharedItems.filter(item => { return item.group_id !== groupID; }) + }); + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + + onChangeUserPermission = (item, permission) => { + let repoID = this.props.repoID; + let groupID = item.group_id; + seafileAPI.sysAdminUpdateRepoSharedItemPermission(repoID, 'group', groupID, permission).then(() => { + this.updateSharedItems(item, permission); + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + + updateSharedItems = (item, permission) => { + let groupID = item.group_id; + let sharedItems = this.state.sharedItems.map(sharedItem => { + let sharedItemGroupID = sharedItem.group_id; + if (groupID === sharedItemGroupID) { + sharedItem.permission = permission; + sharedItem.is_admin = permission === 'admin' ? true : false; + } + return sharedItem; + }); + this.setState({sharedItems: sharedItems}); + } + + render() { + return ( + + + + + + + + + + + + + + + + {this.state.errorMsg.length > 0 && + this.state.errorMsg.map((item, index) => { + let errMessage = item.group_name + ': ' + item.error_msg; + return ( + + + + ); + }) + } + +
{gettext('Group')}{gettext('Permission')}
+ + + + +

{errMessage}

+
+ + + + + + + + + +
{gettext('Group')}{gettext('Permission')}
+
+
+ ); + } +} + +SysAdminShareToGroup.propTypes = propTypes; + +export default SysAdminShareToGroup; diff --git a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-share-to-user.js b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-share-to-user.js new file mode 100644 index 0000000000..31d39dd678 --- /dev/null +++ b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-share-to-user.js @@ -0,0 +1,282 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { isPro, gettext, siteRoot } from '../../../utils/constants'; +import { Button } from 'reactstrap'; +import { seafileAPI } from '../../../utils/seafile-api.js'; +import { Utils } from '../../../utils/utils'; +import toaster from '../../toast'; +import UserSelect from '../../user-select'; +import SharePermissionEditor from '../../select-editor/share-permission-editor'; + +class UserItem extends React.Component { + + constructor(props) { + super(props); + this.state = { + isOperationShow: false + }; + } + + onMouseEnter = () => { + this.setState({isOperationShow: true}); + } + + onMouseLeave = () => { + this.setState({isOperationShow: false}); + } + + deleteShareItem = () => { + let item = this.props.item; + this.props.deleteShareItem(item.user_email); + } + + onChangeUserPermission = (permission) => { + let item = this.props.item; + this.props.onChangeUserPermission(item, permission); + } + + render() { + let item = this.props.item; + let currentPermission = item.is_admin ? 'admin' : item.permission; + return ( + + {item.user_name} + + + + + + + + + ); + } +} + +class UserList extends React.Component { + + render() { + let items = this.props.items; + return ( + + {items.map((item, index) => { + return ( + + ); + })} + + ); + } +} + +const propTypes = { + isGroupOwnedRepo: PropTypes.bool, + itemPath: PropTypes.string.isRequired, + itemType: PropTypes.string.isRequired, + repoID: PropTypes.string.isRequired +}; + +class SysAdminShareToUser extends React.Component { + + constructor(props) { + super(props); + this.state = { + selectedOption: null, + errorMsg: [], + permission: 'rw', + sharedItems: [] + }; + this.options = []; + this.permissions = ['rw', 'r']; + if (isPro) { + this.permissions.push('admin', 'cloud-edit', 'preview'); + } + } + + handleSelectChange = (option) => { + this.setState({selectedOption: option}); + this.options = []; + } + + componentDidMount() { + let repoID = this.props.repoID; + seafileAPI.sysAdminListRepoSharedItems(repoID, 'user').then((res) => { + if(res.data.length !== 0) { + this.setState({sharedItems: res.data}); + } + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + + setPermission = (permission) => { + this.setState({permission: permission}); + } + + shareToUser = () => { + let users = []; + let repoID = this.props.repoID; + 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; + } + } + seafileAPI.sysAdminAddRepoSharedItem(repoID, 'user' , users, this.state.permission).then(res => { + let errorMsg = []; + if (res.data.failed.length > 0) { + for (let i = 0 ; i < res.data.failed.length ; i++) { + errorMsg[i] = res.data.failed[i]; + } + } + let newItems = res.data.success; + this.setState({ + errorMsg: errorMsg, + sharedItems: this.state.sharedItems.concat(newItems), + selectedOption: null, + permission: 'rw', + }); + this.refs.userSelect.clearSelect(); + }).catch(error => { + if (error.response) { + let message = gettext('Library can not be shared to owner.'); + let errMessage = []; + errMessage.push(message); + this.setState({ + errorMsg: errMessage, + selectedOption: null, + }); + } + }); + } + + deleteShareItem = (useremail) => { + let repoID = this.props.repoID; + seafileAPI.sysAdminDeleteRepoSharedItem(repoID, 'user', useremail).then(res => { + this.setState({ + sharedItems: this.state.sharedItems.filter( item => { return item.user_email !== useremail; }) + }); + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + + onChangeUserPermission = (item, permission) => { + let repoID = this.props.repoID; + let userEmail = item.user_email; + seafileAPI.sysAdminUpdateRepoSharedItemPermission(repoID, 'user', userEmail, permission).then(() => { + this.updateSharedItems(item, permission); + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + + updateSharedItems = (item, permission) => { + let username = item.user_name; + let sharedItems = this.state.sharedItems.map(sharedItem => { + let sharedItemUsername = sharedItem.user_name; + if (username === sharedItemUsername) { + sharedItem.permission = permission; + sharedItem.is_admin = permission === 'admin' ? true : false; + } + return sharedItem; + }); + this.setState({sharedItems: sharedItems}); + } + + render() { + let { sharedItems } = this.state; + return ( + + + + + + + + + + + + + + + + {this.state.errorMsg.length > 0 && + this.state.errorMsg.map((item, index) => { + let errMessage = ''; + if (item.email) { + errMessage = item.email + ': ' + item.error_msg; + } else { + errMessage = item; + } + return ( + + + + ); + }) + } + +
{gettext('User')}{gettext('Permission')}
+ + + + + +

{errMessage}

+
+ + + + + + + + + +
{gettext('User')}{gettext('Permission')}
+
+
+ ); + } +} + +SysAdminShareToUser.propTypes = propTypes; + +export default SysAdminShareToUser; diff --git a/frontend/src/components/dialog/transfer-dialog.js b/frontend/src/components/dialog/transfer-dialog.js index a5f4ad9f00..f631d25385 100644 --- a/frontend/src/components/dialog/transfer-dialog.js +++ b/frontend/src/components/dialog/transfer-dialog.js @@ -13,6 +13,7 @@ const propTypes = { itemName: PropTypes.string.isRequired, toggleDialog: PropTypes.func.isRequired, submit: PropTypes.func.isRequired, + canTransferToDept: PropTypes.bool }; class TransferDialog extends React.Component { @@ -61,6 +62,11 @@ class TransferDialog extends React.Component { const innerSpan = '' + itemName +''; let msg = gettext('Transfer Library {library_name}'); let message = msg.replace('{library_name}', innerSpan); + + let canTransferToDept = true; + if (this.props.canTransferToDept != undefined) { + canTransferToDept = this.props.canTransferToDept; + } return ( @@ -72,7 +78,7 @@ class TransferDialog extends React.Component { ref="userSelect" isMulti={false} className="reviewer-select" - placeholder={gettext('Search users')} + placeholder={gettext('Select a user')} onSelectChange={this.handleSelectChange} /> : + + + + : + } +
+
+
+ +
+
+ +
+
+
+ {isNewFolderDialogOpen && + + } + + ); + } +} + +export default DirView; diff --git a/frontend/src/pages/sys-admin/repos/repo-op-menu.js b/frontend/src/pages/sys-admin/repos/repo-op-menu.js new file mode 100644 index 0000000000..ca95a6e417 --- /dev/null +++ b/frontend/src/pages/sys-admin/repos/repo-op-menu.js @@ -0,0 +1,96 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Dropdown, DropdownMenu, DropdownToggle, DropdownItem } from 'reactstrap'; +import { gettext } from '../../../utils/constants'; +import { Utils } from '../../../utils/utils'; + +const propTypes = { + repo: PropTypes.object.isRequired, + onFreezedItem: PropTypes.func.isRequired, + onUnfreezedItem: PropTypes.func.isRequired, + onMenuItemClick: PropTypes.func.isRequired, +}; + +class RepoOpMenu extends React.Component { + + constructor(props) { + super(props); + this.state = { + isItemMenuShow: false + }; + } + + onMenuItemClick = (e) => { + let operation = Utils.getEventData(e, 'op'); + this.props.onMenuItemClick(operation); + } + + onDropdownToggleClick = (e) => { + this.toggleOperationMenu(e); + } + + toggleOperationMenu = (e) => { + this.setState( + {isItemMenuShow: !this.state.isItemMenuShow}, + () => { + if (this.state.isItemMenuShow) { + this.props.onFreezedItem(); + } else { + this.props.onUnfreezedItem(); + } + } + ); + } + + translateOperations = (item) => { + let translateResult = ''; + switch(item) { + case 'Share': + translateResult = gettext('Share'); + break; + case 'Delete': + translateResult = gettext('Delete'); + break; + case 'Transfer': + translateResult = gettext('Transfer'); + break; + case 'History Setting': + translateResult = gettext('History Setting'); + break; + default: + break; + } + + return translateResult; + } + + render() { + const repo = this.props.repo; + let operations = ['Delete', 'Transfer']; + if (!repo.encrypted) { + operations.push('Share'); + } + operations.push('History Setting'); + + return ( + + + + {operations.map((item, index )=> { + return ({this.translateOperations(item)}); + })} + + + ); + } +} + +RepoOpMenu.propTypes = propTypes; + +export default RepoOpMenu; diff --git a/frontend/src/pages/sys-admin/repos/repos-nav.js b/frontend/src/pages/sys-admin/repos/repos-nav.js new file mode 100644 index 0000000000..70f1cd7e87 --- /dev/null +++ b/frontend/src/pages/sys-admin/repos/repos-nav.js @@ -0,0 +1,41 @@ +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: 'all', urlPart: 'all-libraries', text: gettext('All')}, + {name: 'system', urlPart: 'system-library', text: gettext('System')}, + {name: 'trash', urlPart: 'trash-libraries', text: gettext('Trash')} + ]; + } + + render() { + const { currentItem } = this.props; + return ( +
+
    + {this.navItems.map((item, index) => { + return ( +
  • + {item.text} +
  • + ); + })} +
+
+ ); + } +} + +Nav.propTypes = propTypes; + +export default Nav; diff --git a/frontend/src/pages/sys-admin/repos/system-repo.js b/frontend/src/pages/sys-admin/repos/system-repo.js new file mode 100644 index 0000000000..f7f4f0effa --- /dev/null +++ b/frontend/src/pages/sys-admin/repos/system-repo.js @@ -0,0 +1,134 @@ +import React, { Component, Fragment } from 'react'; +import { Link } from '@reach/router'; +import { Utils } from '../../../utils/utils'; +import { seafileAPI } from '../../../utils/seafile-api'; +import { loginUrl, gettext, siteRoot } from '../../../utils/constants'; +import toaster from '../../../components/toast'; +import Loading from '../../../components/loading'; +import EmptyTip from '../../../components/empty-tip'; +import MainPanelTopbar from '../main-panel-topbar'; +import ReposNav from './repos-nav'; + +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 System Library.')}

+
+ ); + const table = ( + + + + + + + + + + + {items.map((item, index) => { + return (); + })} + +
{gettext('Name')}{gettext('ID')}{gettext('Description')}
+
+ ); + return items.length ? table : emptyTip; + } + } +} + +class Item extends Component { + + constructor(props) { + super(props); + } + + render() { + const item = this.props.item; + return ( + + {item.name} + {item.id} + {item.description} + + ); + } +} + +class SystemRepo extends Component { + + constructor(props) { + super(props); + this.state = { + loading: true, + errorMsg: '', + items: [] + }; + } + + componentDidMount () { + seafileAPI.sysAdminGetSystemRepoInfo().then((res) => { + let items = []; + items.push(res.data); + this.setState({ + items: items, + loading: false + }); + }).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 SystemRepo; diff --git a/frontend/src/pages/sys-admin/repos/trash-repos.js b/frontend/src/pages/sys-admin/repos/trash-repos.js new file mode 100644 index 0000000000..0362efc223 --- /dev/null +++ b/frontend/src/pages/sys-admin/repos/trash-repos.js @@ -0,0 +1,301 @@ +import React, { Component, Fragment } from 'react'; +import { Button } from 'reactstrap'; +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 moment from 'moment'; +import Loading from '../../../components/loading'; +import Paginator from '../../../components/paginator'; +import ModalPortal from '../../../components/modal-portal'; +import CommonOperationConfirmationDialog from '../../../components/dialog/common-operation-confirmation-dialog'; +import ReposNav from './repos-nav'; +import MainPanelTopbar from '../main-panel-topbar'; + +const { trashReposExpireDays } = window.sysadmin.pageOptions; + +class Content extends Component { + + constructor(props) { + super(props); + } + + getPreviousPageList = () => { + this.props.getListByPage(this.props.pageInfo.current_page - 1); + } + + getNextPageList = () => { + this.props.getListByPage(this.props.pageInfo.current_page + 1); + } + + render() { + const { loading, errorMsg, items, pageInfo } = this.props; + if (loading) { + return ; + } else if (errorMsg) { + return

{errorMsg}

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

{gettext('No deleted libraries.')}

+
+ ); + const table = ( + +

{gettext('Tip: libraries deleted {trashReposExpireDays} days ago will be cleaned automatically.').replace('{trashReposExpireDays}', trashReposExpireDays)}

+ + + + + + + + + + + + {items.map((item, index) => { + return (); + })} + +
{/*icon*/}{gettext('Name')}{gettext('Owner')}{gettext('Deleted Time')}{/*Operations*/}
+ +
+ ); + + return items.length ? table : emptyTip; + } + } +} + +class Item extends Component { + + constructor(props) { + super(props); + this.state = { + isOpIconShown: false, + isDeleteRepoDialogOpen: false, + isRestoreRepoDialogOpen: false + }; + } + + onDeleteRepo = () => { + const repo = this.props.repo; + seafileAPI.sysAdminDeleteTrashRepo(repo.id).then((res) => { + this.props.onDeleteRepo(repo); + const msg = gettext('Successfully deleted {name}.').replace('{name}', repo.name); + toaster.success(msg); + }).catch((error) => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + + onRestoreRepo = () => { + const repo = this.props.repo; + seafileAPI.sysAdminRestoreTrashRepo(repo.id).then((res) => { + this.props.onRestoreRepo(repo); + let message = gettext('Successfully restored the library.'); + toaster.success(message); + }).catch((error) => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + + handleMouseOver = () => { + this.setState({ + isOpIconShown: true + }); + } + + handleMouseOut = () => { + this.setState({ + isOpIconShown: false + }); + } + + toggleDeleteRepoDialog = (e) => { + if (e) { + e.preventDefault(); + } + this.setState({isDeleteRepoDialogOpen: !this.state.isDeleteRepoDialogOpen}); + } + + toggleRestoreRepoDialog = (e) => { + if (e) { + e.preventDefault(); + } + this.setState({isRestoreRepoDialogOpen: !this.state.isRestoreRepoDialogOpen}); + } + + render () { + const { repo } = this.props; + const { isOpIconShown, isDeleteRepoDialogOpen, isRestoreRepoDialogOpen } = this.state; + const iconUrl = Utils.getLibIconUrl(repo); + const iconTitle = Utils.getLibIconTitle(repo); + const repoName = '' + Utils.HTMLescape(repo.name) + ''; + + return ( + + + {iconTitle} + {repo.name} + + {repo.owner.indexOf('@seafile_group') == -1 ? + {repo.owner_name} : repo.group_name} + + {moment(repo.delete_time).fromNow()} + + {isOpIconShown && ( + + + + + )} + + + {isDeleteRepoDialogOpen && + + + + } + {isRestoreRepoDialogOpen && + + + + } + + ); + } +} + +class TrashRepos extends Component { + + constructor(props) { + super(props); + this.state = { + loading: true, + errorMsg: '', + repos: [], + pageInfo: {}, + perPage: 100, + isCleanTrashDialogOpen: false + }; + } + + componentDidMount () { + this.getReposByPage(1); + } + + toggleCleanTrashDialog = () => { + this.setState({isCleanTrashDialogOpen: !this.state.isCleanTrashDialogOpen}); + } + + getReposByPage = (page) => { + let perPage = this.state.perPage; + seafileAPI.sysAdminListTrashRepos(page, perPage).then((res) => { + this.setState({ + repos: res.data.repos, + pageInfo: res.data.page_info, + loading: false + }); + }).catch((error) => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + + onDeleteRepo = (targetRepo) => { + let repos = this.state.repos.filter(repo => { + return repo.id != targetRepo.id; + }); + this.setState({ + repos: repos + }); + } + + onRestoreRepo = (targetRepo) => { + let repos = this.state.repos.filter(repo => { + return repo.id != targetRepo.id; + }); + this.setState({ + repos: repos + }); + } + + cleanTrash = () => { + seafileAPI.sysAdminCleanTrashRepos().then(res => { + this.setState({repos: []}); + toaster.success(gettext('Successfully cleared trash.')); + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + + render() { + const { isCleanTrashDialogOpen } = this.state; + return ( + + {this.state.repos.length ? ( + + + + ) : + } +
+
+ +
+ +
+
+
+ {isCleanTrashDialogOpen && + + } +
+ ); + } +} + +export default TrashRepos; diff --git a/frontend/src/pages/sys-admin/side-panel.js b/frontend/src/pages/sys-admin/side-panel.js index e3cabadab9..d4851c53e8 100644 --- a/frontend/src/pages/sys-admin/side-panel.js +++ b/frontend/src/pages/sys-admin/side-panel.js @@ -73,10 +73,14 @@ class SidePanel extends React.Component { } {canManageLibrary &&
  • - + this.props.tabItemClick('libraries')} + > {gettext('Libraries')} - +
  • } {canManageUser && diff --git a/frontend/src/utils/utils.js b/frontend/src/utils/utils.js index fe8bb23189..72098f0e9b 100644 --- a/frontend/src/utils/utils.js +++ b/frontend/src/utils/utils.js @@ -312,6 +312,16 @@ export const Utils = { } }, + getAdminTemplateDirentIcon: function (dirent, isBig) { + let size = this.isHiDPI() ? 48 : 24; + size = isBig ? 192 : size; + if (dirent.is_file) { + return this.getFileIconUrl(dirent.obj_name, size); + } else { + return this.getFolderIconUrl(); + } + }, + getFolderIconUrl: function(readonly = false, size) { if (!size) { size = Utils.isHiDPI() ? 48 : 24; diff --git a/media/css/seahub_react.css b/media/css/seahub_react.css index 86d8b55251..666871fbae 100644 --- a/media/css/seahub_react.css +++ b/media/css/seahub_react.css @@ -553,6 +553,7 @@ ul,ol,li { padding: 0 16px; } .cur-view-path.tab-nav-container .nav .nav-item .nav-link { + justify-content: center; /* make short word like 'All' in the center */ margin: 0 0.75rem; } diff --git a/seahub/templates/sysadmin/sysadmin_react_app.html b/seahub/templates/sysadmin/sysadmin_react_app.html index bc1a5eb039..cee0cff401 100644 --- a/seahub/templates/sysadmin/sysadmin_react_app.html +++ b/seahub/templates/sysadmin/sysadmin_react_app.html @@ -16,6 +16,8 @@ is_default_admin: {% if is_default_admin %} true {% else %} false {% endif %}, enable_file_scan: {% if enable_file_scan %} true {% else %} false {% endif %}, enable_work_weixin: {% if enable_work_weixin %} true {% else %} false {% endif %}, + enableSysAdminViewRepo: {% if enable_sys_admin_view_repo %} true {% else %} false {% endif %}, + trashReposExpireDays: {{ trash_repos_expire_days }}, admin_permissions: { "can_view_system_info": {% if user.admin_permissions.can_view_system_info %} true {% else %} false {% endif %}, "can_view_statistic": {% if user.admin_permissions.can_view_statistic %} true {% else %} false {% endif %}, diff --git a/seahub/urls.py b/seahub/urls.py index 4bcf0dbc15..7d881460a9 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -659,6 +659,12 @@ urlpatterns = [ url(r'^sys/mobile-devices/$', sysadmin_react_fake_view, name="sys_mobile_devices"), url(r'^sys/device-errors/$', sysadmin_react_fake_view, name="sys_device_errors"), url(r'^sys/web-settings/$', sysadmin_react_fake_view, name="sys_web_settings"), + url(r'^sys/all-libraries/$', sysadmin_react_fake_view, name="sys_all_libraries"), + url(r'^sys/system-library/$', sysadmin_react_fake_view, name="sys_system_library"), + url(r'^sys/trash-libraries/$', sysadmin_react_fake_view, name="sys_trash_libraries"), + url(r'^sys/libraries/(?P[-0-9a-f]{36})/$', sysadmin_react_fake_view, name="sys_libraries_template"), + url(r'^sys/libraries/(?P[-0-9a-f]{36})/(?P[^/]+)/(?P.*)$', sysadmin_react_fake_view, name="sys_libraries_template_dirent"), + url(r'^sys/work-weixin/$', sysadmin_react_fake_view, name="sys_work_weixin"), url(r'^client-login/$', client_token_login, name='client_token_login'), diff --git a/seahub/views/sysadmin.py b/seahub/views/sysadmin.py index dfeab5e933..b51fdbb735 100644 --- a/seahub/views/sysadmin.py +++ b/seahub/views/sysadmin.py @@ -138,7 +138,13 @@ def sysadmin(request): @login_required @sys_staff_required -def sysadmin_react_fake_view(request): +def sysadmin_react_fake_view(request, **kwargs): + + try: + expire_days = seafile_api.get_server_config_int('library_trash', 'expire_days') + except Exception as e: + logger.error(e) + expire_days = -1 return render(request, 'sysadmin/sysadmin_react_app.html', { 'constance_enabled': dj_settings.CONSTANCE_ENABLED, @@ -149,6 +155,8 @@ def sysadmin_react_fake_view(request): 'enable_terms_and_conditions': config.ENABLE_TERMS_AND_CONDITIONS, 'enable_file_scan': ENABLE_FILE_SCAN, '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, }) @login_required