diff --git a/frontend/src/pages/sys-admin/index.js b/frontend/src/pages/sys-admin/index.js index 9169baea7c..dea64d15a7 100644 --- a/frontend/src/pages/sys-admin/index.js +++ b/frontend/src/pages/sys-admin/index.js @@ -25,6 +25,7 @@ import UserGroups from './users/user-groups'; import AllRepos from './repos/all-repos'; import SystemRepo from './repos/system-repo'; import TrashRepos from './repos/trash-repos'; +import SearchRepos from './repos/search-repos'; import DirView from './repos/dir-view'; import Groups from './groups/groups'; @@ -91,7 +92,7 @@ class SysAdmin extends React.Component { }, { tab: 'libraries', - urlPartList: ['all-libraries', 'system-library', 'trash-libraries', 'libraries/'] + urlPartList: ['all-libraries', 'search-libraries', 'system-library', 'trash-libraries', 'libraries/'] }, { tab: 'users', @@ -154,6 +155,7 @@ class SysAdmin extends React.Component { + diff --git a/frontend/src/pages/sys-admin/main-panel-topbar.js b/frontend/src/pages/sys-admin/main-panel-topbar.js index ff21478cf3..667e0b0bf2 100644 --- a/frontend/src/pages/sys-admin/main-panel-topbar.js +++ b/frontend/src/pages/sys-admin/main-panel-topbar.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import Account from '../../components/common/account'; const propTypes = { - children: PropTypes.object, + children: PropTypes.object }; class MainPanelTopbar extends Component { @@ -18,6 +18,7 @@ class MainPanelTopbar extends Component {
+ {this.props.search && this.props.search}
diff --git a/frontend/src/pages/sys-admin/repos/all-repos.js b/frontend/src/pages/sys-admin/repos/all-repos.js index 02aedca1d1..1cd5ecf5a9 100644 --- a/frontend/src/pages/sys-admin/repos/all-repos.js +++ b/frontend/src/pages/sys-admin/repos/all-repos.js @@ -1,297 +1,15 @@ import React, { Component, Fragment } from 'react'; -import { Link } from '@reach/router'; +import { navigate } from '@reach/router'; import { Button } from 'reactstrap'; import { Utils } from '../../../utils/utils'; import { seafileAPI } from '../../../utils/seafile-api'; -import { loginUrl, gettext, siteRoot, isPro } from '../../../utils/constants'; +import { loginUrl, gettext, siteRoot } from '../../../utils/constants'; import toaster from '../../../components/toast'; -import EmptyTip from '../../../components/empty-tip'; -import Loading from '../../../components/loading'; -import Paginator from '../../../components/paginator'; -import ModalPortal from '../../../components/modal-portal'; -import TransferDialog from '../../../components/dialog/transfer-dialog'; -import DeleteRepoDialog from '../../../components/dialog/delete-repo-dialog'; -import SysAdminShareDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-share-dialog'; -import SysAdminLibHistorySettingDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-lib-history-setting-dialog'; import SysAdminCreateRepoDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-create-repo-dialog'; import MainPanelTopbar from '../main-panel-topbar'; -import UserLink from '../user-link'; +import Search from '../search'; import ReposNav from './repos-nav'; -import RepoOpMenu from './repo-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}); - } - - 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 libraries')}

-
- ); - const table = ( - - - - - - - - - - - - - - {items.map((item, index) => { - return (); - })} - -
{/*icon*/}{gettext('Name')}{gettext('Files')}{' / '}{gettext('Size')}ID{gettext('Owner')}{/*Operations*/}
- -
- ); - - return items.length ? table : emptyTip; - } - } -} - -class Item extends Component { - - constructor(props) { - super(props); - this.state = { - isOpIconShown: false, - highlight: false, - isShareDialogOpen: false, - isDeleteDialogOpen: false, - isTransferDialogOpen: false, - isHistorySettingDialogOpen: false - }; - } - - onDeleteRepo = (repo) => { - seafileAPI.sysAdminDeleteRepo(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); - }); - this.toggleDeleteDialog(); - } - - onTransferRepo = (owner) => { - seafileAPI.sysAdminTransferRepo(this.props.repo.id, owner.email).then((res) => { - this.props.onTransferRepo(res.data); - let message = gettext('Successfully transferred the library.'); - toaster.success(message); - }).catch(error => { - let errMessage = Utils.getErrorMsg(error); - toaster.danger(errMessage); - }); - this.toggleTransferDialog(); - } - - handleMouseOver = () => { - if (!this.props.isItemFreezed) { - this.setState({ - isOpIconShown: true, - highlight: true - }); - } - } - - handleMouseOut = () => { - if (!this.props.isItemFreezed) { - this.setState({ - isOpIconShown: false, - highlight: false - }); - } - } - - onUnfreezedItem = () => { - this.setState({ - highlight: false, - isOpIconShow: false - }); - this.props.onUnfreezedItem(); - } - - onMenuItemClick = (operation) => { - switch(operation) { - case 'Share': - this.toggleShareDialog(); - break; - case 'Delete': - this.toggleDeleteDialog(); - break; - case 'Transfer': - this.toggleTransferDialog(); - break; - case 'History Setting': - this.toggleHistorySettingDialog(); - break; - default: - break; - } - } - - toggleShareDialog = () => { - this.setState({isShareDialogOpen: !this.state.isShareDialogOpen}); - } - - toggleDeleteDialog = () => { - this.setState({isDeleteDialogOpen: !this.state.isDeleteDialogOpen}); - } - - toggleTransferDialog = () => { - this.setState({isTransferDialogOpen: !this.state.isTransferDialogOpen}); - } - - toggleHistorySettingDialog = () => { - this.setState({isHistorySettingDialogOpen: !this.state.isHistorySettingDialogOpen}); - } - - renderRepoName = () => { - const { repo } = this.props; - if (repo.name) { - if (isPro && enableSysAdminViewRepo && !repo.encrypted) { - return {repo.name}; - } else { - return repo.name; - } - } else { - return '--'; - } - } - - render () { - let { repo } = this.props; - let { isOpIconShown, - isShareDialogOpen, isDeleteDialogOpen, - isTransferDialogOpen, isHistorySettingDialogOpen - } = this.state; - let iconUrl = Utils.getLibIconUrl(repo); - let iconTitle = Utils.getLibIconTitle(repo); - let isGroupOwnedRepo = repo.owner.indexOf('@seafile_group') != -1; - - return ( - - - {iconTitle} - {this.renderRepoName()} - {`${repo.file_count} / ${Utils.bytesToSize(repo.size)}`} - {repo.id} - - {isGroupOwnedRepo ? - {repo.group_name} : - - } - - - {(!isGroupOwnedRepo && isOpIconShown) && - - } - - - {isShareDialogOpen && - - - - } - {isDeleteDialogOpen && - - - - } - {isTransferDialogOpen && - - - - } - {isHistorySettingDialogOpen && - - - - } - - ); - } -} +import Content from './repos'; class AllRepos extends Component { @@ -375,11 +93,22 @@ class AllRepos extends Component { }); } + getSearch = () => { + return ; + } + + searchRepos = (repoName) => { + navigate(`${siteRoot}sys/search-libraries/?name=${encodeURIComponent(repoName)}`); + } + render() { let { isCreateRepoDialogOpen } = this.state; return ( - + diff --git a/frontend/src/pages/sys-admin/repos/repos.js b/frontend/src/pages/sys-admin/repos/repos.js new file mode 100644 index 0000000000..c7358ef623 --- /dev/null +++ b/frontend/src/pages/sys-admin/repos/repos.js @@ -0,0 +1,294 @@ +import React, { Component, Fragment } from 'react'; +import { Link } from '@reach/router'; +import { Utils } from '../../../utils/utils'; +import { seafileAPI } from '../../../utils/seafile-api'; +import { gettext, siteRoot, isPro } from '../../../utils/constants'; +import toaster from '../../../components/toast'; +import EmptyTip from '../../../components/empty-tip'; +import Loading from '../../../components/loading'; +import Paginator from '../../../components/paginator'; +import ModalPortal from '../../../components/modal-portal'; +import TransferDialog from '../../../components/dialog/transfer-dialog'; +import DeleteRepoDialog from '../../../components/dialog/delete-repo-dialog'; +import SysAdminShareDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-share-dialog'; +import SysAdminLibHistorySettingDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-lib-history-setting-dialog'; +import UserLink from '../user-link'; +import RepoOpMenu from './repo-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}); + } + + 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 libraries')}

+
+ ); + const table = ( + + + + + + + + + + + + + + {items.map((item, index) => { + return (); + })} + +
{/*icon*/}{gettext('Name')}{gettext('Files')}{' / '}{gettext('Size')}ID{gettext('Owner')}{/*Operations*/}
+ {pageInfo && + + } +
+ ); + + return items.length ? table : emptyTip; + } + } +} + +class Item extends Component { + + constructor(props) { + super(props); + this.state = { + isOpIconShown: false, + highlight: false, + isShareDialogOpen: false, + isDeleteDialogOpen: false, + isTransferDialogOpen: false, + isHistorySettingDialogOpen: false + }; + } + + onDeleteRepo = (repo) => { + seafileAPI.sysAdminDeleteRepo(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); + }); + this.toggleDeleteDialog(); + } + + onTransferRepo = (owner) => { + seafileAPI.sysAdminTransferRepo(this.props.repo.id, owner.email).then((res) => { + this.props.onTransferRepo(res.data); + let message = gettext('Successfully transferred the library.'); + toaster.success(message); + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + this.toggleTransferDialog(); + } + + handleMouseOver = () => { + if (!this.props.isItemFreezed) { + this.setState({ + isOpIconShown: true, + highlight: true + }); + } + } + + handleMouseOut = () => { + if (!this.props.isItemFreezed) { + this.setState({ + isOpIconShown: false, + highlight: false + }); + } + } + + onUnfreezedItem = () => { + this.setState({ + highlight: false, + isOpIconShow: false + }); + this.props.onUnfreezedItem(); + } + + onMenuItemClick = (operation) => { + switch(operation) { + case 'Share': + this.toggleShareDialog(); + break; + case 'Delete': + this.toggleDeleteDialog(); + break; + case 'Transfer': + this.toggleTransferDialog(); + break; + case 'History Setting': + this.toggleHistorySettingDialog(); + break; + default: + break; + } + } + + toggleShareDialog = () => { + this.setState({isShareDialogOpen: !this.state.isShareDialogOpen}); + } + + toggleDeleteDialog = () => { + this.setState({isDeleteDialogOpen: !this.state.isDeleteDialogOpen}); + } + + toggleTransferDialog = () => { + this.setState({isTransferDialogOpen: !this.state.isTransferDialogOpen}); + } + + toggleHistorySettingDialog = () => { + this.setState({isHistorySettingDialogOpen: !this.state.isHistorySettingDialogOpen}); + } + + renderRepoName = () => { + const { repo } = this.props; + if (repo.name) { + if (isPro && enableSysAdminViewRepo && !repo.encrypted) { + return {repo.name}; + } else { + return repo.name; + } + } else { + return '--'; + } + } + + render () { + let { repo } = this.props; + let { isOpIconShown, + isShareDialogOpen, isDeleteDialogOpen, + isTransferDialogOpen, isHistorySettingDialogOpen + } = this.state; + let iconUrl = Utils.getLibIconUrl(repo); + let iconTitle = Utils.getLibIconTitle(repo); + let isGroupOwnedRepo = repo.owner.indexOf('@seafile_group') != -1; + + return ( + + + {iconTitle} + {this.renderRepoName()} + {`${repo.file_count} / ${Utils.bytesToSize(repo.size)}`} + {repo.id} + + {isGroupOwnedRepo ? + {repo.group_name} : + + } + + + {(!isGroupOwnedRepo && isOpIconShown) && + + } + + + {isShareDialogOpen && + + + + } + {isDeleteDialogOpen && + + + + } + {isTransferDialogOpen && + + + + } + {isHistorySettingDialogOpen && + + + + } + + ); + } +} + +export default Content; diff --git a/frontend/src/pages/sys-admin/repos/search-repos.js b/frontend/src/pages/sys-admin/repos/search-repos.js new file mode 100644 index 0000000000..2d933ec031 --- /dev/null +++ b/frontend/src/pages/sys-admin/repos/search-repos.js @@ -0,0 +1,154 @@ +import React, { Component, Fragment } from 'react'; +import { Form, FormGroup, Input, Label, Col } from 'reactstrap'; +import { seafileAPI } from '../../../utils/seafile-api'; +import { loginUrl, gettext } from '../../../utils/constants'; +import MainPanelTopbar from '../main-panel-topbar'; +import Content from './repos'; + + +class SearchRepos extends Component { + + constructor(props) { + super(props); + this.state = { + name: '', + owner: '', + isSubmitBtnActive: false, + loading: true, + errorMsg: '', + repos: [] + }; + } + + componentDidMount() { + let params = (new URL(document.location)).searchParams; + this.setState({ + name: params.get('name') || '', + owner: params.get('owner') || '' + }, this.getRepos); + } + + getRepos = () => { + const { name, owner } = this.state; + seafileAPI.sysAdminSearchRepos(name, owner).then((res) => { + this.setState({ + loading: false, + repos: 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.') + }); + } + }); + } + + searchRepos = () => { + this.getRepos(); + } + + onDeleteRepo = (targetRepo) => { + let repos = this.state.repos.filter(repo => { + return repo.id != targetRepo.id; + }); + this.setState({ + repos: repos + }); + } + + onTransferRepo = (targetRepo) => { + let repos = this.state.repos.map((item) => { + return item.id == targetRepo.id ? targetRepo : item; + }); + this.setState({ + repos: repos + }); + } + + handleNameInputChange = (e) => { + this.setState({ + name: e.target.value + }, this.checkSubmitBtnActive); + } + + handleOwnerInputChange = (e) => { + this.setState({ + owner: e.target.value + }, this.checkSubmitBtnActive); + } + + checkSubmitBtnActive = () => { + const { name, owner } = this.state; + this.setState({ + isSubmitBtnActive: name.trim() || owner.trim() + }); + } + + render() { + const { name, owner, isSubmitBtnActive } = this.state; + return ( + + +
+
+
+

{gettext('Libraries')}

+
+
+
+

{gettext('Search Libraries')}

+

{gettext('Tip: you can search by keyword in name or owner or both.')}

+
+ + + + + + + + + + + + + + + + + +
+
+
+

{gettext('Result')}

+ +
+
+
+
+
+ ); + } +} + +export default SearchRepos; diff --git a/frontend/src/pages/sys-admin/repos/trash-repos.js b/frontend/src/pages/sys-admin/repos/trash-repos.js index c16aad7f1f..dcb57f476e 100644 --- a/frontend/src/pages/sys-admin/repos/trash-repos.js +++ b/frontend/src/pages/sys-admin/repos/trash-repos.js @@ -3,16 +3,17 @@ import { Button } from 'reactstrap'; import moment from 'moment'; import { Utils } from '../../../utils/utils'; import { seafileAPI } from '../../../utils/seafile-api'; -import { gettext } from '../../../utils/constants'; +import { loginUrl, gettext } from '../../../utils/constants'; import toaster from '../../../components/toast'; import EmptyTip from '../../../components/empty-tip'; 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 MainPanelTopbar from '../main-panel-topbar'; +import Search from '../search'; import UserLink from '../user-link'; import ReposNav from './repos-nav'; -import MainPanelTopbar from '../main-panel-topbar'; const { trashReposExpireDays } = window.sysadmin.pageOptions; @@ -66,6 +67,7 @@ class Content extends Component { })} + {pageInfo && + }
); @@ -227,8 +230,25 @@ class TrashRepos extends Component { loading: false }); }).catch((error) => { - let errMessage = Utils.getErrorMsg(error); - toaster.danger(errMessage); + 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.') + }); + } }); } @@ -260,12 +280,50 @@ class TrashRepos extends Component { }); } + getSearch = () => { + return ; + } + + searchRepos = (owner) => { + seafileAPI.sysAdminSearchTrashRepos(owner).then((res) => { + this.setState({ + repos: res.data.repos, + pageInfo: null, + errorMsg: '', // necessary! + 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() { const { isCleanTrashDialogOpen } = this.state; return ( {this.state.repos.length ? ( - + ) : diff --git a/frontend/src/pages/sys-admin/search.js b/frontend/src/pages/sys-admin/search.js new file mode 100644 index 0000000000..66b45c645b --- /dev/null +++ b/frontend/src/pages/sys-admin/search.js @@ -0,0 +1,60 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const propTypes = { + placeholder: PropTypes.string.isRequired, + submit: PropTypes.func.isRequired +}; + +class Search extends React.Component { + + constructor(props) { + super(props); + this.state = { + value: '' + }; + } + + handleInputChange = (e) => { + this.setState({ + value: e.target.value + }); + } + + handleKeyPress = (e) => { + if (e.key == 'Enter') { + e.preventDefault(); + this.handleSubmit(); + } + } + + handleSubmit = () => { + const value = this.state.value.trim(); + if (!value) { + return false; + } + this.props.submit(value); + } + + render() { + return ( +
+ + +
+ ); + } +} + +Search.propTypes = propTypes; + +export default Search; diff --git a/seahub/urls.py b/seahub/urls.py index 57e6be36ba..66a8636ba2 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -698,6 +698,7 @@ urlpatterns = [ url(r'^sys/notifications/$', sysadmin_react_fake_view, name="sys_notifications"), 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/search-libraries/$', sysadmin_react_fake_view, name="sys_search_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"),