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 = (
-
-
-
-
- {/*icon*/} |
- {gettext('Name')} |
- {gettext('Files')}{' / '}{gettext('Size')} |
- ID |
- {gettext('Owner')} |
- {/*Operations*/} |
-
-
-
- {items.map((item, index) => {
- return ( );
- })}
-
-
-
-
- );
-
- 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 (
-
-
-  |
- {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 = (
+
+
+
+
+ {/*icon*/} |
+ {gettext('Name')} |
+ {gettext('Files')}{' / '}{gettext('Size')} |
+ ID |
+ {gettext('Owner')} |
+ {/*Operations*/} |
+
+
+
+ {items.map((item, index) => {
+ return ( );
+ })}
+
+
+ {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 (
+
+
+  |
+ {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"),