1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-02 15:38:15 +00:00

sysadmin recontruct devices page (#3925)

This commit is contained in:
Leo
2019-08-27 21:48:01 +08:00
committed by Daniel Pan
parent a36922ce5d
commit 298880cf89
13 changed files with 668 additions and 11 deletions

View File

@@ -0,0 +1,55 @@
import React from 'react';
import PropTypes from 'prop-types';
import { gettext } from '../../../utils/constants';
import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
const propTypes = {
unlinkDevice: PropTypes.func.isRequired,
toggleDialog: PropTypes.func.isRequired
};
class SysAdminUnlinkDevice extends React.Component {
constructor(props) {
super(props);
this.state = {
inputChecked: false
};
}
handleInputChange = (e) => {
this.setState({
inputChecked: e.target.checked
});
}
unlinkDevice = () => {
this.props.toggleDialog();
this.props.unlinkDevice(this.state.inputChecked);
}
render() {
const { inputChecked } = this.state;
const toggle = this.props.toggleDialog;
return (
<Modal isOpen={true} toggle={toggle}>
<ModalHeader toggle={toggle}>{gettext('Unlink device')}</ModalHeader>
<ModalBody>
<p>{gettext('Are you sure you want to unlink this device?')}</p>
<div className="d-flex align-items-center">
<input id="delete-files" className="mr-1" type="checkbox" checked={inputChecked} onChange={this.handleInputChange} />
<label htmlFor="delete-files" className="m-0">{gettext('Delete files from this device the next time it comes online.')}</label>
</div>
</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={toggle}>{gettext('Cancel')}</Button>
<Button color="primary" onClick={this.unlinkDevice}>{gettext('Unlink')}</Button>
</ModalFooter>
</Modal>
);
}
}
SysAdminUnlinkDevice.propTypes = propTypes;
export default SysAdminUnlinkDevice;

View File

@@ -0,0 +1,58 @@
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,
gotoNextPage: PropTypes.func.isRequired,
currentPage: PropTypes.number.isRequired,
hasNextPage: PropTypes.bool.isRequired,
canResetPerPage: PropTypes.bool.isRequired,
resetPerPage: PropTypes.func
};
class Paginator extends Component {
resetPerPage = (perPage) => {
this.props.resetPerPage(perPage);
}
goToPrevious = (e) => {
e.preventDefault();
this.props.gotoPreviousPage();
}
goToNext = (e) => {
e.preventDefault();
this.props.gotoNextPage();
}
render() {
return (
<Fragment>
<div className="my-6 text-center">
{this.props.currentPage != 1 &&
<a href="#" onClick={this.goToPrevious}>{gettext('Previous')}</a>
}
{this.props.hasNextPage &&
<a href="#" onClick={this.goToNext} className="ml-4">{gettext('Next')}</a>
}
</div>
{this.props.canResetPerPage &&
<div>
{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>
</div>
}
</Fragment>
);
}
}
Paginator.propTypes = propTypes;
export default Paginator;

View File

@@ -22,6 +22,5 @@
margin-bottom: 0.7em;
}
.btn {
margin-top: 1rem;
min-width: 60px;
}
}

View File

@@ -0,0 +1,29 @@
import React, { Component, Fragment } from 'react';
import DevicesNav from './devices-nav';
import DevicesByPlatform from './devices-by-platform';
import MainPanelTopbar from '../main-panel-topbar';
class DesktopDevices extends Component {
constructor(props) {
super(props);
}
render() {
return (
<Fragment>
<MainPanelTopbar />
<div className="main-panel-center flex-row">
<div className="cur-view-container">
<DevicesNav currentItem="desktop" />
<DevicesByPlatform
devicesPlatform={'desktop'}
/>
</div>
</div>
</Fragment>
);
}
}
export default DesktopDevices;

View File

@@ -0,0 +1,213 @@
import React, { Component, Fragment } from 'react';
import { seafileAPI } from '../../../utils/seafile-api';
import { loginUrl, gettext } 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 SysAdminUnlinkDevice from '../../../components/dialog/sysadmin-dialog/sysadmin-unlink-device-dialog';
class Content extends Component {
constructor(props) {
super(props);
}
getPreviousPageDevicesList = () => {
this.props.getDevicesListByPage(this.props.pageInfo.current_page - 1);
}
getNextPageDevicesList = () => {
this.props.getDevicesListByPage(this.props.pageInfo.current_page + 1);
}
render() {
const { loading, errorMsg, items, pageInfo } = this.props;
if (loading) {
return <Loading />;
} else if (errorMsg) {
return <p className="error text-center">{errorMsg}</p>;
} else {
const emptyTip = (
<EmptyTip>
<h2>{gettext('No connected devices')}</h2>
</EmptyTip>
);
const table = (
<Fragment>
<table className="table-hover">
<thead>
<tr>
<th width="19%">{gettext('User')}</th>
<th width="19%">{gettext('Platform')}{' / '}{gettext('Version')}</th>
<th width="19%">{gettext('Device Name')}</th>
<th width="19%">{gettext('IP')}</th>
<th width="19%">{gettext('Last Access')}</th>
<th width="5%">{/*Operations*/}</th>
</tr>
</thead>
<tbody>
{items.map((item, index) => {
return (<Item key={index} item={item} />);
})}
</tbody>
</table>
<Paginator
gotoPreviousPage={this.getPreviousPageDevicesList}
gotoNextPage={this.getNextPageDevicesList}
currentPage={pageInfo.current_page}
hasNextPage={pageInfo.has_next_page}
canResetPerPage={false}
/>
</Fragment>
);
return items.length ? table : emptyTip;
}
}
}
class Item extends Component {
constructor(props) {
super(props);
this.state = {
unlinked: false,
isOpIconShown: false,
isUnlinkDeviceDialogOpen: false
};
}
handleMouseOver = () => {
this.setState({isOpIconShown: true});
}
handleMouseOut = () => {
this.setState({isOpIconShown: false});
}
handleUnlink = (e) => {
e.preventDefault();
if (this.props.item.is_desktop_client) {
this.toggleUnlinkDeviceDialog();
} else {
this.unlinkDevice(true);
}
}
toggleUnlinkDeviceDialog = () => {
this.setState({isUnlinkDeviceDialogOpen: !this.state.isUnlinkDeviceDialogOpen});
}
unlinkDevice = (deleteFiles) => {
const { platform, device_id, user } = this.props.item;
seafileAPI.sysAdminUnlinkDevice(platform, device_id, user, deleteFiles).then((res) => {
this.setState({unlinked: true});
let message = gettext('Successfully unlinked the device.');
toaster.success(message);
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
render() {
const item = this.props.item;
const { unlinked, isUnlinkDeviceDialogOpen, isOpIconShown } = this.state;
if (unlinked) {
return null;
}
return (
<Fragment>
<tr onMouseEnter={this.handleMouseOver} onMouseLeave={this.handleMouseOut}>
<td>{item.user_name}</td>
<td>{item.platform}{' / '}{item.client_version}</td>
<td>{item.device_name}</td>
<td>{item.last_login_ip}</td>
<td>
<span title={moment(item.last_accessed).format('llll')}>{moment(item.last_accessed).fromNow()}</span>
</td>
<td>
<a href="#" className={`sf2-icon-delete action-icon ${isOpIconShown ? '' : 'invisible'}`} title={gettext('Unlink')} onClick={this.handleUnlink}></a>
</td>
</tr>
{isUnlinkDeviceDialogOpen &&
<SysAdminUnlinkDevice
unlinkDevice={this.unlinkDevice}
toggleDialog={this.toggleUnlinkDeviceDialog}
/>
}
</Fragment>
);
}
}
class DevicesByPlatform extends Component {
constructor(props) {
super(props);
this.state = {
loading: true,
errorMsg: '',
devicesData: {},
pageInfo: {},
perPage: 50
};
}
componentDidMount () {
this.getDevicesListByPage(1);
}
getDevicesListByPage = (page) => {
let platform = this.props.devicesPlatform;
let per_page = this.state.perPage;
seafileAPI.sysAdminListDevices(platform, page, per_page).then((res) => {
this.setState({
devicesData: res.data.devices,
pageInfo: res.data.page_info,
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() {
return (
<div className="cur-view-content">
<Content
getDevicesListByPage={this.getDevicesListByPage}
loading={this.state.loading}
errorMsg={this.state.errorMsg}
items={this.state.devicesData}
pageInfo={this.state.pageInfo}
/>
</div>
);
}
}
export default DevicesByPlatform;

View File

@@ -0,0 +1,175 @@
import React, { Component, Fragment } from 'react';
import { Button } from 'reactstrap';
import { seafileAPI } from '../../../utils/seafile-api';
import { siteRoot, loginUrl, gettext } 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 { Link } from '@reach/router';
import DevicesNav from './devices-nav';
import MainPanelTopbar from '../main-panel-topbar';
class Content extends Component {
constructor(props) {
super(props);
}
render() {
const { loading, errorMsg, items } = this.props;
if (loading) {
return <Loading />;
} else if (errorMsg) {
return <p className="error text-center">{errorMsg}</p>;
} else {
const emptyTip = (
<EmptyTip>
<h2>{gettext('No sync errors')}</h2>
</EmptyTip>
);
const table = (
<Fragment>
<table className="table-hover">
<thead>
<tr>
<th width="16%">{gettext('User')}</th>
<th width="20%">{gettext('Device')}{' / '}{gettext('Version')}</th>
<th width="16%">{gettext('IP')}</th>
<th width="16%">{gettext('Library')}</th>
<th width="16%">{gettext('Error')}</th>
<th width="16%">{gettext('Time')}</th>
</tr>
</thead>
<tbody>
{items.map((item, index) => {
return (<Item key={index} item={item} />);
})}
</tbody>
</table>
</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});
}
render() {
let item = this.props.item;
return (
<tr onMouseEnter={this.handleMouseOver} onMouseLeave={this.handleMouseOut}>
<td><Link to={`${siteRoot}useradmin/info/${encodeURIComponent(item.email)}/`}>{item.name}</Link></td>
<td>{item.device_name}{' / '}{item.client_version}</td>
<td>{item.device_ip}</td>
<td><Link to={`${siteRoot}sysadmin/#libs/${item.repo_id}`}>{item.repo_name}</Link></td>
<td>{item.error_msg}</td>
<td>
<span className="item-meta-info" title={moment(item.last_accessed).format('llll')}>{moment(item.error_time).fromNow()}</span>
</td>
</tr>
);
}
}
class DeviceErrors extends Component {
constructor(props) {
super(props);
this.state = {
loading: true,
errorMsg: '',
devicesErrors: [],
isCleanBtnShown: false
};
}
componentDidMount () {
seafileAPI.sysAdminListDeviceErrors().then((res) => {
this.setState({
loading: false,
devicesErrors: res.data,
isCleanBtnShown: res.data.length > 0
});
}).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.')
});
}
});
}
clean = () => {
seafileAPI.sysAdminClearDeviceErrors().then((res) => {
this.setState({
devicesErrors: [],
isCleanBtnShown: false
});
let message = gettext('Successfully cleaned all errors.');
toaster.success(message);
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
render() {
return (
<Fragment>
{this.state.isCleanBtnShown ? (
<MainPanelTopbar>
<Button className="operation-item" onClick={this.clean}>{gettext('Clean')}</Button>
</MainPanelTopbar>
) : (
<MainPanelTopbar />
)}
<div className="main-panel-center flex-row">
<div className="cur-view-container">
<DevicesNav currentItem="errors" />
<div className="cur-view-content">
<Content
loading={this.state.loading}
errorMsg={this.state.errorMsg}
items={this.state.devicesErrors}
/>
</div>
</div>
</div>
</Fragment>
);
}
}
export default DeviceErrors;

View File

@@ -0,0 +1,41 @@
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: 'desktop', urlPart:'desktop-devices', text: gettext('Desktop')},
{name: 'mobile', urlPart:'mobile-devices', text: gettext('Mobile')},
{name: 'errors', urlPart:'device-errors', text: gettext('Errors')}
];
}
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,29 @@
import React, { Component, Fragment } from 'react';
import DevicesNav from './devices-nav';
import DevicesByPlatform from './devices-by-platform';
import MainPanelTopbar from '../main-panel-topbar';
class MobileDevices extends Component {
constructor(props) {
super(props);
}
render() {
return (
<Fragment>
<MainPanelTopbar />
<div className="main-panel-center flex-row">
<div className="cur-view-container">
<DevicesNav currentItem="mobile" />
<DevicesByPlatform
devicesPlatform={'mobile'}
/>
</div>
</div>
</Fragment>
);
}
}
export default MobileDevices;

View File

@@ -7,6 +7,9 @@ import MainPanel from './main-panel';
import FileScanRecords from './file-scan-records';
import WorkWeixinDepartments from './work-weixin-departments';
import Info from './info';
import DesktopDevices from './devices/desktop-devices';
import MobileDevices from './devices/mobile-devices';
import DeviceErrors from './devices/devices-errors';
import '../../assets/css/fa-solid.css';
import '../../assets/css/fa-regular.css';
@@ -25,7 +28,24 @@ class SysAdmin extends React.Component {
componentDidMount() {
let href = window.location.href.split('/');
this.setState({currentTab: href[href.length - 2]});
let currentTab = href[href.length - 2];
let tmpTab;
const devicesUrlParts = ['desktop-devices', 'mobile-devices', 'device-errors'];
const devicesTab = 'devices';
tmpTab = this.getCurrentTabForPageList(devicesUrlParts, devicesTab);
currentTab = tmpTab ? tmpTab : currentTab;
this.setState({currentTab: currentTab});
}
getCurrentTabForPageList = (pageUrlPartList, curTab) => {
const urlBase = `${siteRoot}sys/`;
for (let i = 0, len = pageUrlPartList.length; i < len; i++) {
if (location.href.indexOf(`${urlBase}${pageUrlPartList[i]}`) != -1) {
return curTab;
}
}
}
onCloseSidePanel = () => {
@@ -37,18 +57,17 @@ class SysAdmin extends React.Component {
}
render() {
let { currentTab, isSidePanelClosed, } = this.state;
let { currentTab, isSidePanelClosed } = this.state;
return (
<div id="main">
<SidePanel isSidePanelClosed={isSidePanelClosed} onCloseSidePanel={this.onCloseSidePanel} currentTab={currentTab}/>
<MainPanel>
<Router className="reach-router">
<Info
path={siteRoot + 'sys/info'}
currentTab={currentTab}
tabItemClick={this.tabItemClick}
/>
<Info path={siteRoot + 'sys/info'} />
<DesktopDevices path={siteRoot + 'sys/desktop-devices'} />
<MobileDevices path={siteRoot + 'sys/mobile-devices'} />
<DeviceErrors path={siteRoot + 'sys/device-errors'} />
<FileScanRecords
path={siteRoot + 'sys/file-scan-records'}
currentTab={currentTab}

View File

@@ -0,0 +1,30 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Account from '../../components/common/account';
const propTypes = {
children: PropTypes.object,
};
class MainPanelTopbar extends Component {
render() {
return (
<div className={`main-panel-north ${this.props.children ? 'border-left-show' : ''}`}>
<div className="cur-view-toolbar">
<span className="sf2-icon-menu side-nav-toggle hidden-md-up d-md-none" title="Side Nav Menu"></span>
<div className="operation">
{this.props.children}
</div>
</div>
<div className="common-toolbar">
<Account isAdminPanel={true} />
</div>
</div>
);
}
}
MainPanelTopbar.propTypes = propTypes;
export default MainPanelTopbar;

View File

@@ -48,10 +48,10 @@ class SidePanel extends React.Component {
}
{isDefaultAdmin &&
<li className="nav-item">
<a className='nav-link ellipsis' href={siteRoot + 'sysadmin/#desktop-devices/'}>
<Link className={`nav-link ellipsis ${this.getActiveClass('devices')}`} to={siteRoot + 'sys/desktop-devices/'}>
<span className="sf2-icon-monitor" aria-hidden="true"></span>
<span className="nav-text">{gettext('Devices')}</span>
</a>
</Link>
</li>
}
{constanceEnabled && canConfigSystem &&

View File

@@ -546,6 +546,12 @@ ul,ol,li {
color: #fff;
border: none;
}
.cur-view-path.tab-nav-container {
padding: 0 16px;
}
.cur-view-path.tab-nav-container .nav .nav-item .nav-link {
margin: 0 0.75rem;
}
/* side-panel */
.side-panel {

View File

@@ -646,6 +646,9 @@ urlpatterns = [
url(r'^useradmin/batchadduser/example/$', batch_add_user_example, name='batch_add_user_example'),
url(r'^sys/info/$', sysadmin_react_fake_view, name="sys_info"),
url(r'^sys/desktop-devices/$', sysadmin_react_fake_view, name="sys_desktop_devices"),
url(r'^sys/mobile-devices/$', sysadmin_react_fake_view, name="sys_mobile_devices"),
url(r'^sys/device-errors/$', sysadmin_react_fake_view, name="sys_device_errors"),
url(r'^sys/work-weixin/departments/$', sysadmin_react_fake_view, name="sys_work_weixin_departments"),
url(r'^client-login/$', client_token_login, name='client_token_login'),