diff --git a/frontend/src/pages/org-admin/index.js b/frontend/src/pages/org-admin/index.js index eb42e5cb51..7ac17290e7 100644 --- a/frontend/src/pages/org-admin/index.js +++ b/frontend/src/pages/org-admin/index.js @@ -24,7 +24,8 @@ import OrgGroupsSearchGroups from './org-groups-search-groups'; import OrgGroupInfo from './org-group-info'; import OrgGroupRepos from './org-group-repos'; import OrgGroupMembers from './org-group-members'; -import OrgLibraries from './org-libraries'; +import OrgAllRepos from './libraries/org-all-repos'; +import OrgTrashRepos from './libraries/org-repo-trash'; import OrgInfo from './org-info'; import OrgLinks from './org-links'; import OrgDepartments from './org-departments'; @@ -106,7 +107,8 @@ class Org extends React.Component { - + + diff --git a/frontend/src/pages/org-admin/libraries/org-all-repos.js b/frontend/src/pages/org-admin/libraries/org-all-repos.js new file mode 100644 index 0000000000..8f48a067be --- /dev/null +++ b/frontend/src/pages/org-admin/libraries/org-all-repos.js @@ -0,0 +1,427 @@ +import React, { Component, Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap'; +import { Utils } from '../../../utils/utils'; +import { seafileAPI } from '../../../utils/seafile-api'; +import { gettext, siteRoot, mediaUrl, orgID } from '../../../utils/constants'; +import toaster from '../../../components/toast/index'; +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 { navigate } from '@gatsbyjs/reach-router'; +import OrgAdminRepo from '../../../models/org-admin-repo'; +import MainPanelTopbar from '../main-panel-topbar'; +import ReposNav from './org-repo-nav'; + + +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); + }; + + sortByFileCount = (e) => { + e.preventDefault(); + this.props.sortItems('file_count'); + }; + + sortBySize = (e) => { + e.preventDefault(); + this.props.sortItems('size'); + }; + + render() { + // offer 'sort' only for 'all repos' + const { loading, errorMsg, items, pageInfo, curPerPage, sortBy } = this.props; + if (loading) { + return ; + } else if (errorMsg) { + return

{errorMsg}

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

{gettext('No libraries')}

+
+ ); + const initialSortIcon = ; + const sortIcon = ; + const table = ( + + + + + + + + + + + + + + {items.map((item, index) => { + return (); + })} + +
{/*icon*/}{gettext('Name')} + {sortBy != undefined ? + + {gettext('Files')} {sortBy == 'file_count' ? sortIcon : initialSortIcon}{' / '} + {gettext('Size')} {sortBy == 'size' ? sortIcon : initialSortIcon} + : + gettext('Files') / gettext('Size') + } + ID{gettext('Owner')}{/*Operations*/}
+ {pageInfo && + + } +
+ ); + + return items.length ? table : emptyTip; + } + } +} + +Content.propTypes = { + loading: PropTypes.bool.isRequired, + errorMsg: PropTypes.string.isRequired, + items: PropTypes.array.isRequired, + deleteItem: PropTypes.func, + onDeleteRepo: PropTypes.func.isRequired, + onRestoreRepo: PropTypes.func, + getListByPage: PropTypes.func.isRequired, + resetPerPage: PropTypes.func, + pageInfo: PropTypes.object, + curPerPage: PropTypes.number, + sortItems: PropTypes.func, + sortBy: PropTypes.string, + transferRepoItem: PropTypes.func.isRequired, +}; + +const propTypes = { + repo: PropTypes.object.isRequired, + isItemFreezed: PropTypes.bool, + onFreezedItem: PropTypes.func.isRequired, + onUnfreezedItem: PropTypes.func.isRequired, + onDeleteRepo: PropTypes.func.isRequired, + transferRepoItem: PropTypes.func.isRequired, +}; + +class RepoItem extends React.Component { + + constructor(props) { + super(props); + this.state = { + highlight: false, + showMenu: false, + isItemMenuShow: false, + isTransferDialogShow: false, + }; + } + + onMouseEnter = () => { + if (!this.props.isItemFreezed) { + this.setState({ + showMenu: true, + highlight: true, + }); + } + }; + + onMouseLeave = () => { + if (!this.props.isItemFreezed) { + this.setState({ + showMenu: false, + highlight: false + }); + } + }; + + onDropdownToggleClick = (e) => { + e.preventDefault(); + this.toggleOperationMenu(e); + }; + + toggleOperationMenu = (e) => { + e.stopPropagation(); + this.setState( + {isItemMenuShow: !this.state.isItemMenuShow }, () => { + if (this.state.isItemMenuShow) { + this.props.onFreezedItem(); + } else { + this.setState({ + highlight: false, + showMenu: false, + }); + this.props.onUnfreezedItem(); + } + } + ); + }; + + toggleDelete = () => { + this.props.onDeleteRepo(this.props.repo); + }; + + renderLibIcon = (repo) => { + let href; + let iconTitle; + if (repo.encrypted) { + href = mediaUrl + 'img/lib/48/lib-encrypted.png'; + iconTitle = gettext('Encrypted library'); + } else { + href = mediaUrl + 'img/lib/48/lib.png'; + iconTitle = gettext('Read-Write library'); + } + return {iconTitle}; + }; + + renderRepoOwnerHref = (repo) => { + let href; + if (repo.isDepartmentRepo) { + href = siteRoot + 'org/groupadmin/' + repo.groupID + '/'; + } else { + href = siteRoot + 'org/useradmin/info/' + repo.ownerEmail + '/'; + } + return href; + }; + + toggleTransfer = () => { + this.setState({isTransferDialogShow: !this.state.isTransferDialogShow}); + }; + + onTransferRepo = (user) => { + let repo = this.props.repo; + seafileAPI.orgAdminTransferOrgRepo(orgID, repo.repoID, user.email).then(res => { + this.props.transferRepoItem(repo.repoID, user); + let msg = gettext('Successfully transferred the library.'); + toaster.success(msg); + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + this.toggleTransfer(); + }; + + render() { + let { repo } = this.props; + let isOperationMenuShow = this.state.showMenu; + return ( + + + {this.renderLibIcon(repo)} + {repo.repoName} + {`${repo.file_count} / ${Utils.bytesToSize(repo.size)}`} + {repo.repoID} + {repo.ownerName} + + {isOperationMenuShow && + + + + {gettext('Delete')} + {gettext('Transfer')} + + + } + + + {this.state.isTransferDialogShow && ( + + + + )} + + ); + } +} + +RepoItem.propTypes = propTypes; + +class OrgAllRepos extends Component { + + constructor(props) { + super(props); + this.state = { + loading: true, + errorMsg: '', + repos: [], + pageInfo: {}, + perPage: 25, + sortBy: '', + }; + } + + componentDidMount () { + let urlParams = (new URL(window.location)).searchParams; + const { currentPage = 1, perPage, sortBy } = this.state; + this.setState({ + sortBy: urlParams.get('order_by') || sortBy, + perPage: parseInt(urlParams.get('per_page') || perPage), + currentPage: parseInt(urlParams.get('page') || currentPage) + }, () => { + this.getReposByPage(this.state.currentPage); + }); + } + + getReposByPage = (page) => { + let { perPage } = this.state; + seafileAPI.orgAdminListOrgRepos(orgID, page, perPage, this.state.sortBy).then((res) => { + let orgRepos = res.data.repo_list.map(item => { + return new OrgAdminRepo(item); + }); + let page_info = {}; + if(res.data.page_info === undefined){ + let page = res.data.page; + let has_next_page = res.data.page_next; + page_info = { + 'current_page': page, + 'has_next_page': has_next_page + }; + }else{ + page_info = res.data.page_info; + } + this.setState({ + loading: false, + repos: orgRepos, + pageInfo: page_info + }); + }).catch((error) => { + this.setState({ + loading: false, + errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403 + }); + }); + }; + + sortItems = (sortBy) => { + this.setState({ + currentPage: 1, + sortBy: sortBy + }, () => { + let url = new URL(location.href); + let searchParams = new URLSearchParams(url.search); + const { currentPage, sortBy } = this.state; + searchParams.set('page', currentPage); + searchParams.set('order_by', sortBy); + url.search = searchParams.toString(); + navigate(url.toString()); + this.getReposByPage(currentPage); + }); + }; + + resetPerPage = (perPage) => { + this.setState({ + perPage: perPage + }, () => { + this.getReposByPage(1); + }); + }; + + + deleteRepoItem = (repo) => { + seafileAPI.orgAdminDeleteOrgRepo(orgID, repo.repoID).then(res => { + this.setState({ + repos: this.state.repos.filter(item => item.repoID !== repo.repoID) + }); + let msg = gettext('Successfully deleted {name}'); + msg = msg.replace('{name}', repo.repoName); + toaster.success(msg); + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + }; + + transferRepoItem = (repoID, user) => { + this.setState({ + repos: this.state.repos.map(item =>{ + if (item.repoID == repoID) { + item.ownerEmail = user.email; + item.ownerName = user.value; + } + return item; + }) + }); + }; + + render() { + return ( + + +
+
+ +
+ +
+
+
+
+ ); + } +} + +export default OrgAllRepos; diff --git a/frontend/src/pages/org-admin/libraries/org-repo-nav.js b/frontend/src/pages/org-admin/libraries/org-repo-nav.js new file mode 100644 index 0000000000..42ad81c2ab --- /dev/null +++ b/frontend/src/pages/org-admin/libraries/org-repo-nav.js @@ -0,0 +1,40 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Link } from '@gatsbyjs/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: 'repoadmin', text: gettext('All')}, + {name: 'trash', urlPart: 'repoadmin-trash', 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/org-admin/libraries/org-repo-trash.js b/frontend/src/pages/org-admin/libraries/org-repo-trash.js new file mode 100644 index 0000000000..343fd05816 --- /dev/null +++ b/frontend/src/pages/org-admin/libraries/org-repo-trash.js @@ -0,0 +1,411 @@ +import React, { Component, Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { Button } from 'reactstrap'; +import moment from 'moment'; +import { Utils } from '../../../utils/utils'; +import { seafileAPI } from '../../../utils/seafile-api'; +import { gettext, orgID } from '../../../utils/constants'; +import toaster from '../../../components/toast/index'; +import EmptyTip from '../../../components/empty-tip'; +import Loading from '../../../components/loading'; +import Paginator from '../../../components/paginator'; +import ModalPortal from '../../../components/modal-portal'; +import OpMenu from '../../../components/dialog/op-menu'; +import CommonOperationConfirmationDialog from '../../../components/dialog/common-operation-confirmation-dialog'; +import MainPanelTopbar from '../main-panel-topbar'; +import UserLink from '../user-link'; +import ReposNav from './org-repo-nav'; + + +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, curPerPage } = 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*/}
+ {pageInfo && + + } +
+ ); + + return items.length ? table : emptyTip; + } + } +} + +Content.propTypes = { + loading: PropTypes.bool.isRequired, + errorMsg: PropTypes.string.isRequired, + items: PropTypes.array.isRequired, + deleteItem: PropTypes.func, + onDeleteRepo: PropTypes.func.isRequired, + onRestoreRepo: PropTypes.func, + getListByPage: PropTypes.func.isRequired, + resetPerPage: PropTypes.func, + pageInfo: PropTypes.object, + curPerPage: PropTypes.number, +}; + +class Item extends Component { + + constructor(props) { + super(props); + this.state = { + highlight: false, + isOpIconShown: false, + isDeleteRepoDialogOpen: false, + isRestoreRepoDialogOpen: false + }; + } + + 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(); + }; + + onDeleteRepo = () => { + const repo = this.props.repo; + seafileAPI.orgAdminDeleteTrashRepo(orgID, 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.orgAdminRestoreTrashRepo(orgID, 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); + }); + }; + + toggleDeleteRepoDialog = (e) => { + if (e) { + e.preventDefault(); + } + this.setState({isDeleteRepoDialogOpen: !this.state.isDeleteRepoDialogOpen}); + }; + + toggleRestoreRepoDialog = (e) => { + if (e) { + e.preventDefault(); + } + this.setState({isRestoreRepoDialogOpen: !this.state.isRestoreRepoDialogOpen}); + }; + + translateOperations = (item) => { + let translateResult = ''; + switch(item) { + case 'Restore': + translateResult = gettext('Restore'); + break; + case 'Delete': + translateResult = gettext('Delete'); + break; + default: + break; + } + + return translateResult; + }; + + onMenuItemClick = (operation) => { + switch(operation) { + case 'Restore': + this.toggleRestoreRepoDialog(); + break; + case 'Delete': + this.toggleDeleteRepoDialog(); + break; + default: + break; + } + }; + + 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.group_name} + + {moment(repo.delete_time).fromNow()} + + {isOpIconShown && ( + + )} + + + {isDeleteRepoDialogOpen && + + + + } + {isRestoreRepoDialogOpen && + + + + } + + ); + } +} + +Item.propTypes = { + repo: PropTypes.object.isRequired, + isItemFreezed: PropTypes.bool.isRequired, + onFreezedItem: PropTypes.func.isRequired, + onUnfreezedItem: PropTypes.func.isRequired, + onDeleteRepo: PropTypes.func.isRequired, + onRestoreRepo: PropTypes.func, +}; + +class TrashRepos extends Component { + + constructor(props) { + super(props); + this.state = { + loading: true, + errorMsg: '', + repos: [], + pageInfo: {}, + perPage: 25, + isCleanTrashDialogOpen: false + }; + } + + componentDidMount () { + let urlParams = (new URL(window.location)).searchParams; + const { currentPage = 1, perPage } = this.state; + this.setState({ + perPage: parseInt(urlParams.get('per_page') || perPage), + currentPage: parseInt(urlParams.get('page') || currentPage) + }, () => { + this.getReposByPage(this.state.currentPage); + }); + } + + toggleCleanTrashDialog = () => { + this.setState({isCleanTrashDialogOpen: !this.state.isCleanTrashDialogOpen}); + }; + + getReposByPage = (page) => { + let { perPage } = this.state; + seafileAPI.orgAdminListTrashRepos(orgID, page, perPage).then((res) => { + this.setState({ + repos: res.data.repos, + pageInfo: res.data.page_info, + loading: false + }); + }).catch((error) => { + this.setState({ + loading: false, + errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403 + }); + }); + }; + + resetPerPage = (perPage) => { + this.setState({ + perPage: perPage + }, () => { + this.getReposByPage(1); + }); + }; + + 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.orgAdminCleanTrashRepo(orgID).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; + + // enable 'search': + return ( + + {this.state.repos.length ? ( + + + + ) : + } +
+
+ +
+ +
+
+
+ {isCleanTrashDialogOpen && + + } +
+ ); + } +} + +export default TrashRepos; diff --git a/frontend/src/pages/org-admin/org-libraries.js b/frontend/src/pages/org-admin/org-libraries.js deleted file mode 100644 index c2a567624d..0000000000 --- a/frontend/src/pages/org-admin/org-libraries.js +++ /dev/null @@ -1,341 +0,0 @@ -import React, { Fragment, Component } from 'react'; -import { navigate } from '@gatsbyjs/reach-router'; -import PropTypes from 'prop-types'; -import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap'; -import MainPanelTopbar from './main-panel-topbar'; -import OrgAdminRepo from '../../models/org-admin-repo'; -import toaster from '../../components/toast'; -import TransferDialog from '../../components/dialog/transfer-dialog'; -import ModalPortal from '../../components/modal-portal'; -import { seafileAPI } from '../../utils/seafile-api'; -import { Utils } from '../../utils/utils'; -import { mediaUrl, siteRoot, gettext, orgID } from '../../utils/constants'; - -class OrgLibraries extends Component { - - constructor(props) { - super(props); - this.state = { - page: 1, - pageNext: false, - orgRepos: [], - sortBy: '', - isItemFreezed: false - }; - } - - componentDidMount() { - let urlParams = (new URL(window.location)).searchParams; - const { page, /*currentPage = 1, perPage, */sortBy } = this.state; - this.setState({ - sortBy: urlParams.get('order_by') || sortBy, - //perPage: parseInt(urlParams.get('per_page') || perPage), - //currentPage: parseInt(urlParams.get('page') || currentPage) - page: parseInt(urlParams.get('page') || page) - }, () => { - this.listRepos(this.state.page); - }); - } - - listRepos = (page) => { - seafileAPI.orgAdminListOrgRepos(orgID, page, this.state.sortBy).then(res => { - let orgRepos = res.data.repo_list.map(item => { - return new OrgAdminRepo(item); - }); - - this.setState({ - orgRepos: orgRepos, - pageNext: res.data.page_next, - page: res.data.page, - }); - }).catch(error => { - let errMessage = Utils.getErrorMsg(error); - toaster.danger(errMessage); - }); - }; - - - onChangePageNum = (e, num) => { - e.preventDefault(); - let page = this.state.page; - - if (num == 1) { - page = page + 1; - } else { - page = page - 1; - } - this.listRepos(page); - }; - - onFreezedItem = () => { - this.setState({isItemFreezed: true}); - }; - - onUnfreezedItem = () => { - this.setState({isItemFreezed: false}); - }; - - deleteRepoItem = (repo) => { - seafileAPI.orgAdminDeleteOrgRepo(orgID, repo.repoID).then(res => { - this.setState({ - orgRepos: this.state.orgRepos.filter(item => item.repoID != repo.repoID) - }); - let msg = gettext('Successfully deleted {name}'); - msg = msg.replace('{name}', repo.repoName); - toaster.success(msg); - }).catch(error => { - let errMessage = Utils.getErrorMsg(error); - toaster.danger(errMessage); - }); - }; - - transferRepoItem = (repoID, user) => { - this.setState({ - orgRepos: this.state.orgRepos.map(item =>{ - if (item.repoID == repoID) { - item.ownerEmail = user.email; - item.ownerName = user.value; - } - return item; - }) - }); - }; - - sortItems = (sortBy) => { - this.setState({ - page: 1, - sortBy: sortBy - }, () => { - let url = new URL(location.href); - let searchParams = new URLSearchParams(url.search); - const { page, sortBy } = this.state; - searchParams.set('page', page); - searchParams.set('order_by', sortBy); - url.search = searchParams.toString(); - navigate(url.toString()); - this.listRepos(page); - }); - }; - - sortByFileCount = (e) => { - e.preventDefault(); - this.sortItems('file_count'); - }; - - sortBySize = (e) => { - e.preventDefault(); - this.sortItems('size'); - }; - - render() { - const { orgRepos, sortBy } = this.state; - const initialSortIcon = ; - const sortIcon = ; - return ( - - -
-
-
-

{gettext('All Libraries')}

-
-
- - - - - - - - - - - - - {orgRepos.map(item => { - return ( - - );} - )} - -
{/*icon*/}{gettext('Name')} - {gettext('Files')} {sortBy == 'file_count' ? sortIcon : initialSortIcon}{' / '} - {gettext('Size')} {sortBy == 'size' ? sortIcon : initialSortIcon} - ID{gettext('Owner')}{/*Operations*/}
-
- {this.state.page != 1 && this.onChangePageNum(e, -1)}>{gettext('Previous')}} - {(this.state.page != 1 && this.state.pageNext) && | } - {this.state.pageNext && this.onChangePageNum(e, 1)}>{gettext('Next')}} -
-
-
-
-
- ); - } -} - -const propTypes = { - repo: PropTypes.object.isRequired, - isItemFreezed: PropTypes.bool, - onFreezedItem: PropTypes.func.isRequired, - onUnfreezedItem: PropTypes.func.isRequired, - deleteRepoItem: PropTypes.func.isRequired, - transferRepoItem: PropTypes.func.isRequired, -}; - -class RepoItem extends React.Component { - - constructor(props) { - super(props); - this.state = { - highlight: false, - showMenu: false, - isItemMenuShow: false, - isTransferDialogShow: false, - orgAdmin: true - }; - } - - onMouseEnter = () => { - if (!this.props.isItemFreezed) { - this.setState({ - showMenu: true, - highlight: true, - }); - } - }; - - onMouseLeave = () => { - if (!this.props.isItemFreezed) { - this.setState({ - showMenu: false, - highlight: false - }); - } - }; - - onDropdownToggleClick = (e) => { - e.preventDefault(); - this.toggleOperationMenu(e); - }; - - toggleOperationMenu = (e) => { - e.stopPropagation(); - this.setState( - {isItemMenuShow: !this.state.isItemMenuShow }, () => { - if (this.state.isItemMenuShow) { - this.props.onFreezedItem(); - } else { - this.setState({ - highlight: false, - showMenu: false, - }); - this.props.onUnfreezedItem(); - } - } - ); - }; - - toggleDelete = () => { - this.props.deleteRepoItem(this.props.repo); - }; - - renderLibIcon = (repo) => { - let href; - let iconTitle; - if (repo.encrypted) { - href = mediaUrl + 'img/lib/48/lib-encrypted.png'; - iconTitle = gettext('Encrypted library'); - } else { - href = mediaUrl + 'img/lib/48/lib.png'; - iconTitle = gettext('Read-Write library'); - } - return {iconTitle}; - }; - - renderRepoOwnerHref = (repo) => { - let href; - if (repo.isDepartmentRepo) { - href = siteRoot + 'org/admin/#address-book/groups/' + repo.groupID + '/'; - } else { - href = siteRoot + 'org/useradmin/info/' + repo.ownerEmail + '/'; - } - return href; - }; - - toggleTransfer = () => { - this.setState({isTransferDialogShow: !this.state.isTransferDialogShow}); - }; - - onTransferRepo = (user) => { - let repo = this.props.repo; - seafileAPI.orgAdminTransferOrgRepo(orgID, repo.repoID, user.email).then(res => { - this.props.transferRepoItem(repo.repoID, user); - let msg = gettext('Successfully transferred the library.'); - toaster.success(msg); - }).catch(error => { - let errMessage = Utils.getErrorMsg(error); - toaster.danger(errMessage); - }); - this.toggleTransfer(); - }; - - render() { - let { repo } = this.props; - - let isOperationMenuShow = this.state.showMenu; - return ( - - - {this.renderLibIcon(repo)} - {repo.repoName} - {`${repo.file_count} / ${Utils.bytesToSize(repo.size)}`} - {repo.repoID} - {repo.ownerName} - - {isOperationMenuShow && - - - - {gettext('Delete')} - {gettext('Transfer')} - - - } - - - {this.state.isTransferDialogShow && ( - - - - )} - - ); - } -} - -RepoItem.propTypes = propTypes; - -export default OrgLibraries; diff --git a/seahub/organizations/api/admin/trash_libraries.py b/seahub/organizations/api/admin/trash_libraries.py new file mode 100644 index 0000000000..54445af0cc --- /dev/null +++ b/seahub/organizations/api/admin/trash_libraries.py @@ -0,0 +1,175 @@ +import logging + +from rest_framework.authentication import SessionAuthentication +from rest_framework.response import Response +from rest_framework.views import APIView +from rest_framework import status + +from seaserv import seafile_api, ccnet_api +from pysearpc import SearpcError + +from seahub.utils import is_valid_username +from seahub.utils.db_api import SeafileDB +from seahub.utils.timeutils import timestamp_to_isoformat_timestr +from seahub.api2.permissions import IsProVersion, IsOrgAdminUser +from seahub.api2.authentication import TokenAuthentication +from seahub.api2.throttling import UserRateThrottle +from seahub.api2.utils import api_error +from seahub.base.templatetags.seahub_tags import email2nickname +from seahub.group.utils import group_id_to_name + +from seahub.api2.endpoints.group_owned_libraries import get_group_id_by_repo_owner +from seahub.organizations.views import org_user_exists + +logger = logging.getLogger(__name__) + +def get_trash_repo_info(repo): + result = {} + owner = repo.owner_id + + result['name'] = repo.repo_name + result['id'] = repo.repo_id + result['owner'] = owner + result['owner_name'] = email2nickname(owner) + result['delete_time'] = timestamp_to_isoformat_timestr(repo.del_time) + + if '@seafile_group' in owner: + group_id = get_group_id_by_repo_owner(owner) + result['group_name'] = group_id_to_name(group_id) + + return result + + +class OrgAdminTrashLibraries(APIView): + + authentication_classes = (TokenAuthentication, SessionAuthentication) + throttle_classes = (UserRateThrottle,) + permission_classes = (IsProVersion, IsOrgAdminUser ) + + def get(self, request, org_id): + """ List deleted repos (by team admin) + + Permission checking: + 1. only admin can perform this action. + """ + + org_id = int(org_id) + # list by page + try: + current_page = int(request.GET.get('page', '1')) + per_page = int(request.GET.get('per_page', '100')) + except ValueError: + current_page = 1 + per_page = 100 + + start = (current_page - 1) * per_page + limit = per_page + 1 + + try: + db_api = SeafileDB() + repos_all = db_api.get_org_trash_repo_list(int(org_id), start, limit) + except Exception as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + if len(repos_all) > per_page: + repos_all = repos_all[:per_page] + has_next_page = True + else: + has_next_page = False + + return_results = [] + for repo in repos_all: + repo_info = get_trash_repo_info(repo) + return_results.append(repo_info) + + page_info = { + 'has_next_page': has_next_page, + 'current_page': current_page + } + + return Response({"page_info": page_info, "repos": return_results}) + + def delete(self, request, org_id): + """ clean all deleted org libraries(by team admin) + + Permission checking: + 1. only org admin can perform this action. + """ + + org_id = int(org_id) + try: + + db_api = SeafileDB() + db_api.empty_org_repo_trash(int(org_id)) + except Exception as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + return Response({'success': True}) + +class OrgAdminTrashLibrary(APIView): + + authentication_classes = (TokenAuthentication, SessionAuthentication) + throttle_classes = (UserRateThrottle,) + permission_classes = (IsProVersion, IsOrgAdminUser ) + + def put(self, request, org_id, repo_id): + """ restore a deleted library + + Permission checking: + 1. only org admin can perform this action. + """ + + org_id = int(org_id) + owner = seafile_api.get_trash_repo_owner(repo_id) + if not owner: + error_msg = "Library does not exist in trash." + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + if '@seafile_group' in owner: + group_org_id = ccnet_api.get_org_id_by_group(int(owner.split('@')[0])) + if group_org_id != org_id: + return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.') + else: + if not org_user_exists(org_id, owner): + return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.') + + try: + seafile_api.restore_repo_from_trash(repo_id) + except SearpcError as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + return Response({'success': True}) + + def delete(self, request, org_id, repo_id): + """ permanently delete a deleted library + + Permission checking: + 1. only org admin can perform this action. + """ + org_id = int(org_id) + owner = seafile_api.get_trash_repo_owner(repo_id) + if not owner: + error_msg = "Library does not exist in trash." + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + if '@seafile_group' in owner: + group_org_id = ccnet_api.get_org_id_by_group(int(owner.split('@')[0])) + if group_org_id != org_id: + return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.') + else: + if not org_user_exists(org_id, owner): + return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.') + + try: + seafile_api.del_repo_from_trash(repo_id) + except SearpcError as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + return Response({'success': True}) \ No newline at end of file diff --git a/seahub/organizations/api_urls.py b/seahub/organizations/api_urls.py index 5ee36afa50..fae63fd71e 100644 --- a/seahub/organizations/api_urls.py +++ b/seahub/organizations/api_urls.py @@ -15,6 +15,7 @@ from .api.admin.users import OrgAdminUser, OrgAdminUsers, OrgAdminSearchUser, \ from .api.admin.user_set_password import OrgAdminUserSetPassword from .api.admin.groups import OrgAdminGroups, OrgAdminGroup, OrgAdminSearchGroup, OrgAdminDepartments from .api.admin.repos import OrgAdminRepos, OrgAdminRepo +from .api.admin.trash_libraries import OrgAdminTrashLibraries, OrgAdminTrashLibrary from .api.admin.info import OrgAdminInfo from .api.admin.links import OrgAdminLinks, OrgAdminLink from .api.admin.web_settings import OrgAdminWebSettings @@ -87,6 +88,8 @@ urlpatterns = [ path('/admin/users//repos/', OrgAdminUserRepos.as_view(), name='api-v2.1-org-admin-user-repos'), path('/admin/users//beshared-repos/', OrgAdminUserBesharedRepos.as_view(), name='api-v2.1-org-admin-user-beshared-repos'), path('/admin/repos/', OrgAdminRepos.as_view(), name='api-v2.1-org-admin-repos'), + re_path(r'^(?P\d+)/admin/trash-libraries/$', OrgAdminTrashLibraries.as_view(), name='api-v2.1-org-admin-trash-libraries'), + re_path(r'^(?P\d+)/admin/trash-libraries/(?P[-0-9a-f]{36})/$', OrgAdminTrashLibrary.as_view(), name='api-v2.1-org-admin-trash-librarie'), re_path(r'^(?P\d+)/admin/repos/(?P[-0-9a-f]{36})/$', OrgAdminRepo.as_view(), name='api-v2.1-org-admin-repo'), path('/admin/web-settings/', OrgAdminWebSettings.as_view(), name='api-v2.1-org-admin-web-settings'), path('admin/info/', OrgAdminInfo.as_view(), name='api-v2.1-org-admin-info'), diff --git a/seahub/organizations/urls.py b/seahub/organizations/urls.py index c663626022..88b1ff5291 100644 --- a/seahub/organizations/urls.py +++ b/seahub/organizations/urls.py @@ -24,6 +24,7 @@ urlpatterns = [ path('useradmin/info//repos/', react_fake_view, name='org_user_repos'), path('useradmin/info//shared-repos/', react_fake_view, name='org_user_shared_repos'), path('repoadmin/', react_fake_view, name='org_repo_admin'), + path('repoadmin-trash/', react_fake_view, name='org_repo_trash'), path('groupadmin/', react_fake_view, name='org_group_admin'), path('groupadmin/search-groups/', react_fake_view, name='org_group_admin_search_groups'), diff --git a/seahub/utils/db_api.py b/seahub/utils/db_api.py index 583e7657f6..437a7df86e 100644 --- a/seahub/utils/db_api.py +++ b/seahub/utils/db_api.py @@ -3,6 +3,17 @@ import configparser from django.db import connection +class RepoTrash(object): + + def __init__(self, **kwargs): + self.repo_id = kwargs.get('repo_id') + self.repo_name = kwargs.get('repo_name') + self.head_id = kwargs.get('head_id') + self.owner_id = kwargs.get('owner_id') + self.size = kwargs.get('size') + self.del_time = kwargs.get('del_time') + + class SeafileDB: def __init__(self): @@ -259,3 +270,81 @@ class SeafileDB: return device_errors + def get_org_trash_repo_list(self, org_id, start, limit): + + sql = f""" + SELECT repo_id, repo_name, head_id, owner_id, `size`, del_time + FROM `{self.db_name}`.`RepoTrash` + WHERE org_id = {org_id} + ORDER BY del_time DESC + LIMIT {limit} OFFSET {start} + """ + trash_repo_list = [] + with connection.cursor() as cursor: + cursor.execute(sql) + for item in cursor.fetchall(): + repo_id = item[0] + repo_name = item[1] + head_id = item[2] + owner_id = item[3] + size = item[4] + del_time = item[5] + params = { + 'repo_id': repo_id, + 'repo_name': repo_name, + 'head_id': head_id, + 'owner_id': owner_id, + 'size': size, + 'del_time': del_time, + } + trash_repo_obj = RepoTrash(**params) + trash_repo_list.append(trash_repo_obj) + cursor.close() + return trash_repo_list + + def empty_org_repo_trash(self, org_id): + """ + empty org repo trash + """ + def del_repo_trash(cursor,repo_ids): + del_file_count_sql = """ + DELETE FROM + `%s`.`RepoFileCount` + WHERE + repo_id in %%s; + """ % self.db_name + cursor.execute(del_file_count_sql, (repo_ids, )) + + del_repo_info_sql = """ + DELETE FROM + `%s`.`RepoInfo` + WHERE + repo_id in %%s; + """ % self.db_name + cursor.execute(del_repo_info_sql, (repo_ids, )) + + del_trash_sql = """ + DELETE FROM + `%s`.`RepoTrash` + WHERE + repo_id in %%s; + """ % self.db_name + cursor.execute(del_trash_sql, (repo_ids,)) + + + sql_list_repo_id = f""" + SELECT + t.repo_id + FROM + `{self.db_name}`.`RepoTrash` t + WHERE + org_id={org_id}; + """ + with connection.cursor() as cursor: + cursor.execute(sql_list_repo_id) + repo_ids = [] + for item in cursor.fetchall(): + repo_id = item[0] + repo_ids.append(repo_id) + del_repo_trash(cursor, repo_ids) + cursor.close()