mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-13 05:39:59 +00:00
sysadmin recontruct devices page (#3925)
This commit is contained in:
@@ -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;
|
58
frontend/src/components/paginator.js
Normal file
58
frontend/src/components/paginator.js
Normal 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;
|
@@ -22,6 +22,5 @@
|
|||||||
margin-bottom: 0.7em;
|
margin-bottom: 0.7em;
|
||||||
}
|
}
|
||||||
.btn {
|
.btn {
|
||||||
margin-top: 1rem;
|
|
||||||
min-width: 60px;
|
min-width: 60px;
|
||||||
}
|
}
|
29
frontend/src/pages/sys-admin/devices/desktop-devices.js
Normal file
29
frontend/src/pages/sys-admin/devices/desktop-devices.js
Normal 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;
|
213
frontend/src/pages/sys-admin/devices/devices-by-platform.js
Normal file
213
frontend/src/pages/sys-admin/devices/devices-by-platform.js
Normal 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;
|
175
frontend/src/pages/sys-admin/devices/devices-errors.js
Normal file
175
frontend/src/pages/sys-admin/devices/devices-errors.js
Normal 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;
|
41
frontend/src/pages/sys-admin/devices/devices-nav.js
Normal file
41
frontend/src/pages/sys-admin/devices/devices-nav.js
Normal 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;
|
29
frontend/src/pages/sys-admin/devices/mobile-devices.js
Normal file
29
frontend/src/pages/sys-admin/devices/mobile-devices.js
Normal 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;
|
@@ -7,6 +7,9 @@ import MainPanel from './main-panel';
|
|||||||
import FileScanRecords from './file-scan-records';
|
import FileScanRecords from './file-scan-records';
|
||||||
import WorkWeixinDepartments from './work-weixin-departments';
|
import WorkWeixinDepartments from './work-weixin-departments';
|
||||||
import Info from './info';
|
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-solid.css';
|
||||||
import '../../assets/css/fa-regular.css';
|
import '../../assets/css/fa-regular.css';
|
||||||
@@ -25,7 +28,24 @@ class SysAdmin extends React.Component {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
let href = window.location.href.split('/');
|
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 = () => {
|
onCloseSidePanel = () => {
|
||||||
@@ -37,18 +57,17 @@ class SysAdmin extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let { currentTab, isSidePanelClosed, } = this.state;
|
let { currentTab, isSidePanelClosed } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="main">
|
<div id="main">
|
||||||
<SidePanel isSidePanelClosed={isSidePanelClosed} onCloseSidePanel={this.onCloseSidePanel} currentTab={currentTab}/>
|
<SidePanel isSidePanelClosed={isSidePanelClosed} onCloseSidePanel={this.onCloseSidePanel} currentTab={currentTab}/>
|
||||||
<MainPanel>
|
<MainPanel>
|
||||||
<Router className="reach-router">
|
<Router className="reach-router">
|
||||||
<Info
|
<Info path={siteRoot + 'sys/info'} />
|
||||||
path={siteRoot + 'sys/info'}
|
<DesktopDevices path={siteRoot + 'sys/desktop-devices'} />
|
||||||
currentTab={currentTab}
|
<MobileDevices path={siteRoot + 'sys/mobile-devices'} />
|
||||||
tabItemClick={this.tabItemClick}
|
<DeviceErrors path={siteRoot + 'sys/device-errors'} />
|
||||||
/>
|
|
||||||
<FileScanRecords
|
<FileScanRecords
|
||||||
path={siteRoot + 'sys/file-scan-records'}
|
path={siteRoot + 'sys/file-scan-records'}
|
||||||
currentTab={currentTab}
|
currentTab={currentTab}
|
||||||
|
30
frontend/src/pages/sys-admin/main-panel-topbar.js
Normal file
30
frontend/src/pages/sys-admin/main-panel-topbar.js
Normal 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;
|
@@ -48,10 +48,10 @@ class SidePanel extends React.Component {
|
|||||||
}
|
}
|
||||||
{isDefaultAdmin &&
|
{isDefaultAdmin &&
|
||||||
<li className="nav-item">
|
<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="sf2-icon-monitor" aria-hidden="true"></span>
|
||||||
<span className="nav-text">{gettext('Devices')}</span>
|
<span className="nav-text">{gettext('Devices')}</span>
|
||||||
</a>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
{constanceEnabled && canConfigSystem &&
|
{constanceEnabled && canConfigSystem &&
|
||||||
|
@@ -546,6 +546,12 @@ ul,ol,li {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
border: none;
|
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 */
|
||||||
.side-panel {
|
.side-panel {
|
||||||
|
@@ -646,6 +646,9 @@ urlpatterns = [
|
|||||||
url(r'^useradmin/batchadduser/example/$', batch_add_user_example, name='batch_add_user_example'),
|
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/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'^sys/work-weixin/departments/$', sysadmin_react_fake_view, name="sys_work_weixin_departments"),
|
||||||
|
|
||||||
url(r'^client-login/$', client_token_login, name='client_token_login'),
|
url(r'^client-login/$', client_token_login, name='client_token_login'),
|
||||||
|
Reference in New Issue
Block a user