1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-07 01:41:39 +00:00

sysadmin reconstruct links page (#4153)

* sysadmin reconstruct links page

* optimize code

* optimize code
This commit is contained in:
Leo
2019-10-19 11:21:49 +08:00
committed by Daniel Pan
parent 0fd1fad77c
commit 36a734d9b4
8 changed files with 513 additions and 11 deletions

View File

@@ -1,8 +1,6 @@
import React, { Component, Fragment } from 'react'; import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { gettext } from '../utils/constants'; import { gettext } from '../utils/constants';
import { Label } from 'reactstrap';
const propTypes = { const propTypes = {
gotoPreviousPage: PropTypes.func.isRequired, gotoPreviousPage: PropTypes.func.isRequired,
@@ -10,7 +8,8 @@ const propTypes = {
currentPage: PropTypes.number.isRequired, currentPage: PropTypes.number.isRequired,
hasNextPage: PropTypes.bool.isRequired, hasNextPage: PropTypes.bool.isRequired,
canResetPerPage: PropTypes.bool.isRequired, canResetPerPage: PropTypes.bool.isRequired,
resetPerPage: PropTypes.func resetPerPage: PropTypes.func,
curPerPage: PropTypes.number,
}; };
class Paginator extends Component { class Paginator extends Component {
@@ -30,6 +29,7 @@ class Paginator extends Component {
} }
render() { render() {
let { curPerPage } = this.props;
return ( return (
<Fragment> <Fragment>
<div className="my-6 text-center"> <div className="my-6 text-center">
@@ -41,11 +41,11 @@ class Paginator extends Component {
} }
</div> </div>
{this.props.canResetPerPage && {this.props.canResetPerPage &&
<div> <div className="text-center">
{gettext('Per page:')}{' '} {gettext('Per page:')}{' '}
<Label onClick={() => {return this.resetPerPage(25);}}>25</Label> <span className={`${curPerPage === 25 ? '' : 'a-simulate '} mr-1`} onClick={() => {return this.resetPerPage(25);}}>25</span>
<Label onClick={() => {return this.resetPerPage(50);}}>50</Label> <span className={`${curPerPage === 50 ? '' : 'a-simulate '} mr-1`} onClick={() => {return this.resetPerPage(50);}}>50</span>
<Label onClick={() => {return this.resetPerPage(100);}}>100</Label> <span className={`${curPerPage === 100 ? '' : 'a-simulate '} mr-1`} onClick={() => {return this.resetPerPage(100);}}>100</span>
</div> </div>
} }
</Fragment> </Fragment>

View File

@@ -20,6 +20,9 @@ import Groups from './groups/groups';
import GroupRepos from './groups/group-repos'; import GroupRepos from './groups/group-repos';
import GroupMembers from './groups/group-members'; 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 WebSettings from './web-settings/web-settings';
import Notifications from './notifications/notifications'; import Notifications from './notifications/notifications';
import FileScanRecords from './file-scan-records'; import FileScanRecords from './file-scan-records';
@@ -112,6 +115,8 @@ class SysAdmin extends React.Component {
<Groups path={siteRoot + 'sys/groups'} /> <Groups path={siteRoot + 'sys/groups'} />
<GroupRepos path={siteRoot + 'sys/groups/:groupID/libraries'} /> <GroupRepos path={siteRoot + 'sys/groups/:groupID/libraries'} />
<GroupMembers path={siteRoot + 'sys/groups/:groupID/members'} /> <GroupMembers path={siteRoot + 'sys/groups/:groupID/members'} />
<ShareLinks path={siteRoot + 'sys/share-links'} />
<UploadLinks path={siteRoot + 'sys/upload-links'} />
<FileScanRecords <FileScanRecords
path={siteRoot + 'sys/file-scan-records'} path={siteRoot + 'sys/file-scan-records'}
currentTab={currentTab} currentTab={currentTab}

View 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;

View 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;

View 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;

View File

@@ -145,10 +145,14 @@ class SidePanel extends React.Component {
} }
{isDefaultAdmin && {isDefaultAdmin &&
<li className="nav-item"> <li className="nav-item">
<a className='nav-link ellipsis' href={siteRoot + 'sys/publinkadmin/'}> <Link
<span className="sf2-icon-link" aria-hidden="true"></span> 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> <span className="nav-text">{gettext('Links')}</span>
</a> </Link>
</li> </li>
} }
{sysadminExtraEnabled && canViewUserLog && {sysadminExtraEnabled && canViewUserLog &&

View File

@@ -1192,6 +1192,12 @@ export const Utils = {
result = '00:' + result; result = '00:' + result;
} }
return 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;
} }
}; };

View File

@@ -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+)/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+)/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/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/$', sysadmin_react_fake_view, name="sys_work_weixin"),
url(r'^sys/work-weixin/departments/$', sysadmin_react_fake_view, name="sys_work_weixin_departments"), url(r'^sys/work-weixin/departments/$', sysadmin_react_fake_view, name="sys_work_weixin_departments"),