mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-08 02:10:24 +00:00
sysadmin reconstruct links page (#4153)
* sysadmin reconstruct links page * optimize code * optimize code
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { gettext } from '../utils/constants';
|
||||
import { Label } from 'reactstrap';
|
||||
|
||||
|
||||
const propTypes = {
|
||||
gotoPreviousPage: PropTypes.func.isRequired,
|
||||
@@ -10,7 +8,8 @@ const propTypes = {
|
||||
currentPage: PropTypes.number.isRequired,
|
||||
hasNextPage: PropTypes.bool.isRequired,
|
||||
canResetPerPage: PropTypes.bool.isRequired,
|
||||
resetPerPage: PropTypes.func
|
||||
resetPerPage: PropTypes.func,
|
||||
curPerPage: PropTypes.number,
|
||||
};
|
||||
|
||||
class Paginator extends Component {
|
||||
@@ -30,6 +29,7 @@ class Paginator extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
let { curPerPage } = this.props;
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="my-6 text-center">
|
||||
@@ -41,11 +41,11 @@ class Paginator extends Component {
|
||||
}
|
||||
</div>
|
||||
{this.props.canResetPerPage &&
|
||||
<div>
|
||||
<div className="text-center">
|
||||
{gettext('Per page:')}{' '}
|
||||
<Label onClick={() => {return this.resetPerPage(25);}}>25</Label>
|
||||
<Label onClick={() => {return this.resetPerPage(50);}}>50</Label>
|
||||
<Label onClick={() => {return this.resetPerPage(100);}}>100</Label>
|
||||
<span className={`${curPerPage === 25 ? '' : 'a-simulate '} mr-1`} onClick={() => {return this.resetPerPage(25);}}>25</span>
|
||||
<span className={`${curPerPage === 50 ? '' : 'a-simulate '} mr-1`} onClick={() => {return this.resetPerPage(50);}}>50</span>
|
||||
<span className={`${curPerPage === 100 ? '' : 'a-simulate '} mr-1`} onClick={() => {return this.resetPerPage(100);}}>100</span>
|
||||
</div>
|
||||
}
|
||||
</Fragment>
|
||||
|
@@ -20,6 +20,9 @@ import Groups from './groups/groups';
|
||||
import GroupRepos from './groups/group-repos';
|
||||
import GroupMembers from './groups/group-members';
|
||||
|
||||
import ShareLinks from './links/share-links';
|
||||
import UploadLinks from './links/upload-links';
|
||||
|
||||
import WebSettings from './web-settings/web-settings';
|
||||
import Notifications from './notifications/notifications';
|
||||
import FileScanRecords from './file-scan-records';
|
||||
@@ -112,6 +115,8 @@ class SysAdmin extends React.Component {
|
||||
<Groups path={siteRoot + 'sys/groups'} />
|
||||
<GroupRepos path={siteRoot + 'sys/groups/:groupID/libraries'} />
|
||||
<GroupMembers path={siteRoot + 'sys/groups/:groupID/members'} />
|
||||
<ShareLinks path={siteRoot + 'sys/share-links'} />
|
||||
<UploadLinks path={siteRoot + 'sys/upload-links'} />
|
||||
<FileScanRecords
|
||||
path={siteRoot + 'sys/file-scan-records'}
|
||||
currentTab={currentTab}
|
||||
|
40
frontend/src/pages/sys-admin/links/links-nav.js
Normal file
40
frontend/src/pages/sys-admin/links/links-nav.js
Normal file
@@ -0,0 +1,40 @@
|
||||
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: 'shareLinks', urlPart:'share-links', text: gettext('Share Links')},
|
||||
{name: 'uploadLinks', urlPart:'upload-links', text: gettext('Upload Links')},
|
||||
];
|
||||
}
|
||||
|
||||
render() {
|
||||
const { currentItem } = this.props;
|
||||
return (
|
||||
<div className="cur-view-path tab-nav-container">
|
||||
<ul className="nav">
|
||||
{this.navItems.map((item, index) => {
|
||||
return (
|
||||
<li className="nav-item" key={index}>
|
||||
<Link to={`${siteRoot}sys/${item.urlPart}/`} className={`nav-link${currentItem == item.name ? ' active' : ''}`}>{item.text}</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Nav.propTypes = propTypes;
|
||||
|
||||
export default Nav;
|
222
frontend/src/pages/sys-admin/links/share-links.js
Normal file
222
frontend/src/pages/sys-admin/links/share-links.js
Normal file
@@ -0,0 +1,222 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import { seafileAPI } from '../../../utils/seafile-api';
|
||||
import { gettext, loginUrl, 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 LinksNav from './links-nav';
|
||||
import MainPanelTopbar from '../main-panel-topbar';
|
||||
|
||||
|
||||
class Content extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
getPreviousPage = () => {
|
||||
this.props.getShareLinksByPage(this.props.currentPage - 1);
|
||||
}
|
||||
|
||||
getNextPage = () => {
|
||||
this.props.getShareLinksByPage(this.props.currentPage + 1);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { loading, errorMsg, items, perPage, currentPage , hasNextPage }= this.props;
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
} else if (errorMsg) {
|
||||
return <p className="error text-center">{errorMsg}</p>;
|
||||
} else {
|
||||
const emptyTip = (
|
||||
<EmptyTip>
|
||||
<h2>{gettext('No Share Links.')}</h2>
|
||||
</EmptyTip>
|
||||
);
|
||||
const table = (
|
||||
<Fragment>
|
||||
<table className="table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="18%">{gettext('Name')}</th>
|
||||
<th width="18%">{gettext('Token')}</th>
|
||||
<th width="18%">{gettext('Owner')}</th>
|
||||
<th width="18%">{gettext('Created At')}</th>
|
||||
<th width="18%">{gettext('Count')}</th>
|
||||
<th width="10%">{/*Operations*/}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{items &&
|
||||
<tbody>
|
||||
{items.map((item, index) => {
|
||||
return (<Item
|
||||
key={index}
|
||||
item={item}
|
||||
deleteShareLink={this.props.deleteShareLink}
|
||||
/>);
|
||||
})}
|
||||
</tbody>
|
||||
}
|
||||
</table>
|
||||
<Paginator
|
||||
gotoPreviousPage={this.getPreviousPage}
|
||||
gotoNextPage={this.getNextPage}
|
||||
currentPage={currentPage}
|
||||
hasNextPage={hasNextPage}
|
||||
canResetPerPage={true}
|
||||
curPerPage={perPage}
|
||||
resetPerPage={this.props.resetPerPage}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
return items.length ? table : emptyTip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Item extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isOpIconShown: false,
|
||||
};
|
||||
}
|
||||
|
||||
handleMouseOver = () => {
|
||||
this.setState({
|
||||
isOpIconShown: true
|
||||
});
|
||||
}
|
||||
|
||||
handleMouseOut = () => {
|
||||
this.setState({
|
||||
isOpIconShown: false
|
||||
});
|
||||
}
|
||||
|
||||
deleteShareLink = () => {
|
||||
this.props.deleteShareLink(this.props.item.token);
|
||||
}
|
||||
|
||||
render() {
|
||||
let { isOpIconShown } = this.state;
|
||||
let { item } = this.props;
|
||||
let deleteIcon = `op-icon sf2-icon-delete ${isOpIconShown ? '' : 'invisible'}`;
|
||||
return (
|
||||
<tr onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut}>
|
||||
<td>{item.obj_name}</td>
|
||||
<td>{item.token}</td>
|
||||
<td><a href={siteRoot + 'useradmin/info/' + item.creator_email + '/'}>{item.creator_name}</a></td>
|
||||
<td>{moment(item.ctime).fromNow()}</td>
|
||||
<td>{item.view_cnt}</td>
|
||||
<td>
|
||||
<a href="#" className={deleteIcon} title={gettext('delete')} onClick={this.deleteShareLink}></a>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ShareLinks extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
loading: true,
|
||||
errorMsg: '',
|
||||
shareLinkList: [],
|
||||
perPage: 100,
|
||||
currentPage: 1,
|
||||
hasNextPage: false,
|
||||
};
|
||||
this.initPage = 1;
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.getShareLinksByPage(this.initPage);
|
||||
}
|
||||
|
||||
getShareLinksByPage = (page) => {
|
||||
let { perPage } = this.state;
|
||||
seafileAPI.sysAdminListAllShareLinks(page, perPage).then((res) => {
|
||||
this.setState({
|
||||
shareLinkList: res.data.share_link_list,
|
||||
loading: false,
|
||||
currentPage: page,
|
||||
hasNextPage: Utils.hasNextPage(page, perPage, res.data.count),
|
||||
});
|
||||
}).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.')
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deleteShareLink = (linkToken) => {
|
||||
seafileAPI.sysAdminDeleteShareLink(linkToken).then(res => {
|
||||
let newShareLinkList = this.state.shareLinkList.filter(item =>
|
||||
item.token != linkToken
|
||||
);
|
||||
this.setState({shareLinkList: newShareLinkList});
|
||||
}).catch(error => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
});
|
||||
}
|
||||
|
||||
resetPerPage = (newPerPage) => {
|
||||
this.setState({
|
||||
perPage: newPerPage,
|
||||
}, () => this.getShareLinksByPage(this.initPage));
|
||||
}
|
||||
|
||||
render() {
|
||||
let { shareLinkList, currentPage, perPage, hasNextPage } = this.state;
|
||||
return (
|
||||
<Fragment>
|
||||
<MainPanelTopbar />
|
||||
<div className="main-panel-center flex-row">
|
||||
<div className="cur-view-container">
|
||||
<LinksNav currentItem="shareLinks" />
|
||||
<div className="cur-view-content">
|
||||
<Content
|
||||
loading={this.state.loading}
|
||||
errorMsg={this.state.errorMsg}
|
||||
items={shareLinkList}
|
||||
currentPage={currentPage}
|
||||
perPage={perPage}
|
||||
hasNextPage={hasNextPage}
|
||||
getShareLinksByPage={this.getShareLinksByPage}
|
||||
resetPerPage={this.resetPerPage}
|
||||
deleteShareLink={this.deleteShareLink}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ShareLinks;
|
224
frontend/src/pages/sys-admin/links/upload-links.js
Normal file
224
frontend/src/pages/sys-admin/links/upload-links.js
Normal file
@@ -0,0 +1,224 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import { seafileAPI } from '../../../utils/seafile-api';
|
||||
import { gettext, loginUrl, 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 LinksNav from './links-nav';
|
||||
import MainPanelTopbar from '../main-panel-topbar';
|
||||
|
||||
|
||||
class Content extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
getPreviousPage = () => {
|
||||
this.props.getUploadLinksByPage(this.props.currentPage - 1);
|
||||
}
|
||||
|
||||
getNextPage = () => {
|
||||
this.props.getUploadLinksByPage(this.props.currentPage + 1);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { loading, errorMsg, items, perPage, currentPage, hasNextPage } = this.props;
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
} else if (errorMsg) {
|
||||
return <p className="error text-center">{errorMsg}</p>;
|
||||
} else {
|
||||
const emptyTip = (
|
||||
<EmptyTip>
|
||||
<h2>{gettext('No Upload Links.')}</h2>
|
||||
</EmptyTip>
|
||||
);
|
||||
const table = (
|
||||
<Fragment>
|
||||
<table className="table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="18%">{gettext('Name')}</th>
|
||||
<th width="18%">{gettext('Token')}</th>
|
||||
<th width="18%">{gettext('Owner')}</th>
|
||||
<th width="18%">{gettext('Created At')}</th>
|
||||
<th width="18%">{gettext('Count')}</th>
|
||||
<th width="10%">{/*Operations*/}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{items &&
|
||||
<tbody>
|
||||
{items.map((item, index) => {
|
||||
return (<Item
|
||||
key={index}
|
||||
item={item}
|
||||
deleteUploadLink={this.props.deleteUploadLink}
|
||||
/>);
|
||||
})}
|
||||
</tbody>
|
||||
}
|
||||
</table>
|
||||
<Paginator
|
||||
gotoPreviousPage={this.getPreviousPage}
|
||||
gotoNextPage={this.getNextPage}
|
||||
currentPage={currentPage}
|
||||
hasNextPage={hasNextPage}
|
||||
canResetPerPage={true}
|
||||
curPerPage={perPage}
|
||||
resetPerPage={this.props.resetPerPage}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
return items.length ? table : emptyTip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Item extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isOpIconShown: false,
|
||||
};
|
||||
}
|
||||
|
||||
handleMouseOver = () => {
|
||||
this.setState({
|
||||
isOpIconShown: true
|
||||
});
|
||||
}
|
||||
|
||||
handleMouseOut = () => {
|
||||
this.setState({
|
||||
isOpIconShown: false
|
||||
});
|
||||
}
|
||||
|
||||
deleteUploadLink = () => {
|
||||
this.props.deleteUploadLink(this.props.item.token);
|
||||
}
|
||||
|
||||
render() {
|
||||
let { isOpIconShown } = this.state;
|
||||
let { item } = this.props;
|
||||
let deleteIcon = `op-icon sf2-icon-delete ${isOpIconShown ? '' : 'invisible'}`;
|
||||
return (
|
||||
<Fragment>
|
||||
<tr onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut}>
|
||||
<td>{item.path}</td>
|
||||
<td>{item.token}</td>
|
||||
<td><a href={siteRoot + 'useradmin/info/' + item.creator_email + '/'}>{item.creator_name}</a></td>
|
||||
<td>{moment(item.ctime).fromNow()}</td>
|
||||
<td>{item.view_cnt}</td>
|
||||
<td>
|
||||
<a href="#" className={deleteIcon} title={gettext('delete')} onClick={this.deleteUploadLink}></a>
|
||||
</td>
|
||||
</tr>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UploadLinks extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
loading: true,
|
||||
errorMsg: '',
|
||||
uploadLinkList: [],
|
||||
perPage: 100,
|
||||
currentPage: 1,
|
||||
hasNextPage: false,
|
||||
};
|
||||
this.initPage = 1;
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.getUploadLinksByPage(this.initPage);
|
||||
}
|
||||
|
||||
getUploadLinksByPage = (page) => {
|
||||
let { perPage } = this.state;
|
||||
seafileAPI.sysAdminListAllUploadLinks(page, perPage).then((res) => {
|
||||
this.setState({
|
||||
uploadLinkList: res.data.upload_link_list,
|
||||
loading: false,
|
||||
currentPage: page,
|
||||
hasNextPage: Utils.hasNextPage(page, perPage, res.data.count),
|
||||
});
|
||||
}).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.')
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deleteUploadLink = (linkToken) => {
|
||||
seafileAPI.sysAdminDeleteUploadLink(linkToken).then(res => {
|
||||
let newUploadLinkList = this.state.uploadLinkList.filter(item =>
|
||||
item.token != linkToken
|
||||
);
|
||||
this.setState({uploadLinkList: newUploadLinkList});
|
||||
}).catch(error => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
});
|
||||
}
|
||||
|
||||
resetPerPage = (newPerPage) => {
|
||||
this.setState({
|
||||
perPage: newPerPage,
|
||||
}, () => this.getShareLinksByPage(this.initPage));
|
||||
}
|
||||
|
||||
render() {
|
||||
let { uploadLinkList, currentPage, perPage, hasNextPage } = this.state;
|
||||
return (
|
||||
<Fragment>
|
||||
<MainPanelTopbar />
|
||||
<div className="main-panel-center flex-row">
|
||||
<div className="cur-view-container">
|
||||
<LinksNav currentItem="uploadLinks" />
|
||||
<div className="cur-view-content">
|
||||
<Content
|
||||
loading={this.state.loading}
|
||||
errorMsg={this.state.errorMsg}
|
||||
items={uploadLinkList}
|
||||
currentPage={currentPage}
|
||||
perPage={perPage}
|
||||
hasNextPage={hasNextPage}
|
||||
getUploadLinksByPage={this.getUploadLinksByPage}
|
||||
resetPerPage={this.resetPerPage}
|
||||
deleteUploadLink={this.deleteUploadLink}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default UploadLinks;
|
@@ -145,10 +145,14 @@ class SidePanel extends React.Component {
|
||||
}
|
||||
{isDefaultAdmin &&
|
||||
<li className="nav-item">
|
||||
<a className='nav-link ellipsis' href={siteRoot + 'sys/publinkadmin/'}>
|
||||
<span className="sf2-icon-link" aria-hidden="true"></span>
|
||||
<Link
|
||||
className={`nav-link ellipsis ${this.getActiveClass('links')}`}
|
||||
to={siteRoot + 'sys/share-links/'}
|
||||
onClick={() => this.props.tabItemClick('links')}
|
||||
>
|
||||
<span className="sf2-icon-msgs" aria-hidden="true"></span>
|
||||
<span className="nav-text">{gettext('Links')}</span>
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
}
|
||||
{sysadminExtraEnabled && canViewUserLog &&
|
||||
|
@@ -1192,6 +1192,12 @@ export const Utils = {
|
||||
result = '00:' + result;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
hasNextPage(curPage, perPage, totalCount) {
|
||||
// when curPage * perPage >= totalCount, do not have next page.
|
||||
// so hasNextPage = true, when curPage * perPage < totalCount
|
||||
return curPage * perPage < totalCount;
|
||||
}
|
||||
|
||||
};
|
||||
|
@@ -675,7 +675,8 @@ urlpatterns = [
|
||||
url(r'^sys/organizations/(?P<org_id>\d+)/groups/$', sysadmin_org_react_fake_view, name="sys_organization_groups"),
|
||||
url(r'^sys/organizations/(?P<org_id>\d+)/libraries/$', sysadmin_org_react_fake_view, name="sys_organization_repos"),
|
||||
url(r'^sys/organizations/(?P<org_id>\d+)/settings/$', sysadmin_org_react_fake_view, name="sys_organization_settings"),
|
||||
|
||||
url(r'^sys/share-links/$', sysadmin_react_fake_view, name="sys_share_links"),
|
||||
url(r'^sys/upload-links/$', sysadmin_react_fake_view, name="sys_upload_links"),
|
||||
url(r'^sys/work-weixin/$', sysadmin_react_fake_view, name="sys_work_weixin"),
|
||||
url(r'^sys/work-weixin/departments/$', sysadmin_react_fake_view, name="sys_work_weixin_departments"),
|
||||
|
||||
|
Reference in New Issue
Block a user