mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-07 18:03:48 +00:00
Org admin page (#5298)
* orgadmin import users * orgadmin devices page * orgadmin statistic page * orgadmin devices page use seafile_api.list_org_repo_sync_errors * [org admin] bugfix & improvements Co-authored-by: lian <lian@seafile.com> Co-authored-by: llj <lingjun.li1@gmail.com>
This commit is contained in:
@@ -122,7 +122,7 @@ class Account extends Component {
|
|||||||
};
|
};
|
||||||
} else if (isOrgStaff) {
|
} else if (isOrgStaff) {
|
||||||
data = {
|
data = {
|
||||||
url: `${siteRoot}org/useradmin/`,
|
url: `${siteRoot}org/info/`,
|
||||||
text: gettext('Organization Admin')
|
text: gettext('Organization Admin')
|
||||||
};
|
};
|
||||||
} else if (isInstAdmin) {
|
} else if (isInstAdmin) {
|
||||||
|
67
frontend/src/components/dialog/org-import-users-dialog.js
Normal file
67
frontend/src/components/dialog/org-import-users-dialog.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Alert, Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap';
|
||||||
|
import { gettext, siteRoot } from '../../utils/constants';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
toggle: PropTypes.func.isRequired,
|
||||||
|
importUsersInBatch: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
class ImportOrgUsersDialog extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.fileInputRef = React.createRef();
|
||||||
|
this.state = {
|
||||||
|
errorMsg: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle = () => {
|
||||||
|
this.props.toggle();
|
||||||
|
}
|
||||||
|
|
||||||
|
openFileInput = () => {
|
||||||
|
this.fileInputRef.current.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadFile = (e) => {
|
||||||
|
// no file selected
|
||||||
|
if (!this.fileInputRef.current.files.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// check file extension
|
||||||
|
let fileName = this.fileInputRef.current.files[0].name;
|
||||||
|
if(fileName.substr(fileName.lastIndexOf('.') + 1) != 'xlsx') {
|
||||||
|
this.setState({
|
||||||
|
errorMsg: gettext('Please choose a .xlsx file.')
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const file = this.fileInputRef.current.files[0];
|
||||||
|
this.props.importUsersInBatch(file);
|
||||||
|
this.toggle();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let { errorMsg } = this.state;
|
||||||
|
return (
|
||||||
|
<Modal isOpen={true} toggle={this.toggle}>
|
||||||
|
<ModalHeader toggle={this.toggle}>{gettext('Import users from a .xlsx file')}</ModalHeader>
|
||||||
|
<ModalBody>
|
||||||
|
<p><a className="text-secondary small" href={`${siteRoot}useradmin/batchadduser/example/`}>{gettext('Download an example file')}</a></p>
|
||||||
|
<button className="btn btn-outline-primary" onClick={this.openFileInput}>{gettext('Upload file')}</button>
|
||||||
|
<input className="d-none" type="file" onChange={this.uploadFile} ref={this.fileInputRef} />
|
||||||
|
{errorMsg && <Alert color="danger">{errorMsg}</Alert>}
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button color="secondary" onClick={this.toggle}>{gettext('Cancel')}</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportOrgUsersDialog.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default ImportOrgUsersDialog;
|
29
frontend/src/pages/org-admin/devices/desktop-devices.js
Normal file
29
frontend/src/pages/org-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 OrgDesktopDevices 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 OrgDesktopDevices;
|
216
frontend/src/pages/org-admin/devices/devices-by-platform.js
Normal file
216
frontend/src/pages/org-admin/devices/devices-by-platform.js
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
import React, { Component, Fragment } from 'react';
|
||||||
|
import { seafileAPI } from '../../../utils/seafile-api';
|
||||||
|
import { orgID, 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, curPerPage } = 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}
|
||||||
|
curPerPage={curPerPage}
|
||||||
|
resetPerPage={this.props.resetPerPage}
|
||||||
|
/>
|
||||||
|
</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.orgAdminUnlinkDevice(orgID, 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: 25
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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.getDevicesListByPage(this.state.currentPage);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getDevicesListByPage = (page) => {
|
||||||
|
let platform = this.props.devicesPlatform;
|
||||||
|
let per_page = this.state.perPage;
|
||||||
|
seafileAPI.orgAdminListDevices(orgID, platform, page, per_page).then((res) => {
|
||||||
|
this.setState({
|
||||||
|
devicesData: res.data.devices,
|
||||||
|
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.getDevicesListByPage(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="cur-view-content">
|
||||||
|
<Content
|
||||||
|
loading={this.state.loading}
|
||||||
|
errorMsg={this.state.errorMsg}
|
||||||
|
items={this.state.devicesData}
|
||||||
|
getDevicesListByPage={this.getDevicesListByPage}
|
||||||
|
curPerPage={this.state.perPage}
|
||||||
|
resetPerPage={this.resetPerPage}
|
||||||
|
pageInfo={this.state.pageInfo}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DevicesByPlatform;
|
204
frontend/src/pages/org-admin/devices/devices-errors.js
Normal file
204
frontend/src/pages/org-admin/devices/devices-errors.js
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
import React, { Component, Fragment } from 'react';
|
||||||
|
import { Button } from 'reactstrap';
|
||||||
|
import { seafileAPI } from '../../../utils/seafile-api';
|
||||||
|
import { siteRoot, gettext, orgID } 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';
|
||||||
|
import UserLink from '../user-link';
|
||||||
|
import Paginator from '../../../components/paginator';
|
||||||
|
|
||||||
|
class Content extends Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPreviousPageDeviceErrorsList = () => {
|
||||||
|
this.props.getDeviceErrorsListByPage(this.props.pageInfo.current_page - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
getNextPageDeviceErrorsList = () => {
|
||||||
|
this.props.getDeviceErrorsListByPage(this.props.pageInfo.current_page + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { loading, errorMsg, items, pageInfo, curPerPage } = 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>
|
||||||
|
<Paginator
|
||||||
|
gotoPreviousPage={this.getPreviousPageDeviceErrorsList}
|
||||||
|
gotoNextPage={this.getNextPageDeviceErrorsList}
|
||||||
|
currentPage={pageInfo.current_page}
|
||||||
|
hasNextPage={pageInfo.has_next_page}
|
||||||
|
curPerPage={curPerPage}
|
||||||
|
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});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let item = this.props.item;
|
||||||
|
return (
|
||||||
|
<tr onMouseEnter={this.handleMouseOver} onMouseLeave={this.handleMouseOut}>
|
||||||
|
<td><UserLink email={item.email} name={item.name} /></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 OrgDevicesErrors extends Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
loading: true,
|
||||||
|
errorMsg: '',
|
||||||
|
devicesErrors: [],
|
||||||
|
isCleanBtnShown: false,
|
||||||
|
pageInfo: {},
|
||||||
|
perPage: 25
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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.getDeviceErrorsListByPage(this.state.currentPage);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getDeviceErrorsListByPage = (page) => {
|
||||||
|
let per_page = this.state.perPage;
|
||||||
|
seafileAPI.orgAdminListDevicesErrors(orgID, page, per_page).then((res) => {
|
||||||
|
this.setState({
|
||||||
|
loading: false,
|
||||||
|
devicesErrors: res.data.device_errors,
|
||||||
|
pageInfo: res.data.page_info,
|
||||||
|
isCleanBtnShown: res.data.length > 0
|
||||||
|
});
|
||||||
|
}).catch((error) => {
|
||||||
|
this.setState({
|
||||||
|
loading: false,
|
||||||
|
errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resetPerPage = (perPage) => {
|
||||||
|
this.setState({
|
||||||
|
perPage: perPage
|
||||||
|
}, () => {
|
||||||
|
this.getDeviceErrorsListByPage(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
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}
|
||||||
|
getDeviceErrorsListByPage={this.getDeviceErrorsListByPage}
|
||||||
|
curPerPage={this.state.perPage}
|
||||||
|
resetPerPage={this.resetPerPage}
|
||||||
|
pageInfo={this.state.pageInfo}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OrgDevicesErrors;
|
41
frontend/src/pages/org-admin/devices/devices-nav.js
Normal file
41
frontend/src/pages/org-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:'devices-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}org/deviceadmin/${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/org-admin/devices/mobile-devices.js
Normal file
29
frontend/src/pages/org-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;
|
@@ -3,6 +3,15 @@ import ReactDOM from 'react-dom';
|
|||||||
import { Router } from '@reach/router';
|
import { Router } from '@reach/router';
|
||||||
import { siteRoot } from '../../utils/constants';
|
import { siteRoot } from '../../utils/constants';
|
||||||
import SidePanel from './side-panel';
|
import SidePanel from './side-panel';
|
||||||
|
|
||||||
|
import OrgStatisticFile from './statistic/statistic-file';
|
||||||
|
import OrgStatisticStorage from './statistic/statistic-storage';
|
||||||
|
import OrgStatisticTraffic from './statistic/statistic-traffic';
|
||||||
|
import OrgStatisticUsers from './statistic/statistic-users';
|
||||||
|
import OrgStatisticReport from './statistic/statistic-reports';
|
||||||
|
import OrgDesktopDevices from './devices/desktop-devices.js';
|
||||||
|
import OrgMobileDevices from './devices/mobile-devices.js';
|
||||||
|
import OrgDevicesErrors from './devices/devices-errors.js';
|
||||||
import OrgUsers from './org-users-users';
|
import OrgUsers from './org-users-users';
|
||||||
import OrgUsersSearchUsers from './org-users-search-users';
|
import OrgUsersSearchUsers from './org-users-search-users';
|
||||||
import OrgAdmins from './org-users-admins';
|
import OrgAdmins from './org-users-admins';
|
||||||
@@ -44,6 +53,12 @@ class Org extends React.Component {
|
|||||||
if (location.href.indexOf(`${siteRoot}org/useradmin`) != -1) {
|
if (location.href.indexOf(`${siteRoot}org/useradmin`) != -1) {
|
||||||
currentTab = 'users';
|
currentTab = 'users';
|
||||||
}
|
}
|
||||||
|
if (location.href.indexOf(`${siteRoot}org/statistics-admin/`) != -1) {
|
||||||
|
currentTab = 'statistics-admin';
|
||||||
|
}
|
||||||
|
if (location.href.indexOf(`${siteRoot}org/deviceadmin/`) != -1) {
|
||||||
|
currentTab = 'deviceadmin';
|
||||||
|
}
|
||||||
if (location.href.indexOf(`${siteRoot}org/groupadmin`) != -1) {
|
if (location.href.indexOf(`${siteRoot}org/groupadmin`) != -1) {
|
||||||
currentTab = 'groupadmin';
|
currentTab = 'groupadmin';
|
||||||
}
|
}
|
||||||
@@ -68,7 +83,15 @@ class Org extends React.Component {
|
|||||||
<SidePanel isSidePanelClosed={isSidePanelClosed} onCloseSidePanel={this.onCloseSidePanel} currentTab={currentTab} tabItemClick={this.tabItemClick}/>
|
<SidePanel isSidePanelClosed={isSidePanelClosed} onCloseSidePanel={this.onCloseSidePanel} currentTab={currentTab} tabItemClick={this.tabItemClick}/>
|
||||||
<div className="main-panel o-hidden">
|
<div className="main-panel o-hidden">
|
||||||
<Router className="reach-router">
|
<Router className="reach-router">
|
||||||
<OrgInfo path={siteRoot + 'org/orgmanage'}/>
|
<OrgInfo path={siteRoot + 'org/info/'} />
|
||||||
|
<OrgStatisticFile path={siteRoot + 'org/statistics-admin/file/'} />
|
||||||
|
<OrgStatisticStorage path={siteRoot + 'org/statistics-admin/total-storage/'} />
|
||||||
|
<OrgStatisticUsers path={siteRoot + 'org/statistics-admin/active-users/'} />
|
||||||
|
<OrgStatisticTraffic path={siteRoot + 'org/statistics-admin/traffic/'} />
|
||||||
|
<OrgStatisticReport path={siteRoot + 'org/statistics-admin/reports/'} />
|
||||||
|
<OrgDesktopDevices path={siteRoot + 'org/deviceadmin/desktop-devices/'} />
|
||||||
|
<OrgMobileDevices path={siteRoot + 'org/deviceadmin/mobile-devices/'} />
|
||||||
|
<OrgDevicesErrors path={siteRoot + 'org/deviceadmin/devices-errors/'} />
|
||||||
<OrgUsers path={siteRoot + 'org/useradmin'} />
|
<OrgUsers path={siteRoot + 'org/useradmin'} />
|
||||||
<OrgUsersSearchUsers path={siteRoot + 'org/useradmin/search-users'} />
|
<OrgUsersSearchUsers path={siteRoot + 'org/useradmin/search-users'} />
|
||||||
<OrgAdmins path={siteRoot + 'org/useradmin/admins/'} />
|
<OrgAdmins path={siteRoot + 'org/useradmin/admins/'} />
|
||||||
|
@@ -4,6 +4,7 @@ import Nav from './org-users-nav';
|
|||||||
import OrgUsersList from './org-users-list';
|
import OrgUsersList from './org-users-list';
|
||||||
import MainPanelTopbar from './main-panel-topbar';
|
import MainPanelTopbar from './main-panel-topbar';
|
||||||
import ModalPortal from '../../components/modal-portal';
|
import ModalPortal from '../../components/modal-portal';
|
||||||
|
import ImportOrgUsersDialog from '../../components/dialog/org-import-users-dialog';
|
||||||
import AddOrgUserDialog from '../../components/dialog/org-add-user-dialog';
|
import AddOrgUserDialog from '../../components/dialog/org-add-user-dialog';
|
||||||
import InviteUserDialog from '../../components/dialog/org-admin-invite-user-dialog';
|
import InviteUserDialog from '../../components/dialog/org-admin-invite-user-dialog';
|
||||||
import toaster from '../../components/toast';
|
import toaster from '../../components/toast';
|
||||||
@@ -72,6 +73,7 @@ class OrgUsers extends Component {
|
|||||||
sortBy: '',
|
sortBy: '',
|
||||||
sortOrder: 'asc',
|
sortOrder: 'asc',
|
||||||
isShowAddOrgUserDialog: false,
|
isShowAddOrgUserDialog: false,
|
||||||
|
isImportOrgUsersDialogOpen: false,
|
||||||
isInviteUserDialogOpen: false
|
isInviteUserDialogOpen: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -110,6 +112,10 @@ class OrgUsers extends Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleImportOrgUsersDialog = () => {
|
||||||
|
this.setState({isImportOrgUsersDialogOpen: !this.state.isImportOrgUsersDialogOpen});
|
||||||
|
}
|
||||||
|
|
||||||
toggleAddOrgUser = () => {
|
toggleAddOrgUser = () => {
|
||||||
this.setState({isShowAddOrgUserDialog: !this.state.isShowAddOrgUserDialog});
|
this.setState({isShowAddOrgUserDialog: !this.state.isShowAddOrgUserDialog});
|
||||||
}
|
}
|
||||||
@@ -135,6 +141,30 @@ class OrgUsers extends Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
importOrgUsers = (file) => {
|
||||||
|
toaster.notify(gettext('It may take some time, please wait.'));
|
||||||
|
seafileAPI.orgAdminImportUsersViaFile(orgID, file).then((res) => {
|
||||||
|
if (res.data.success.length) {
|
||||||
|
const users = res.data.success.map(item => {
|
||||||
|
if (item.institution == undefined) {
|
||||||
|
item.institution = '';
|
||||||
|
}
|
||||||
|
return new OrgUserInfo(item);
|
||||||
|
});
|
||||||
|
this.setState({
|
||||||
|
orgUsers: users.concat(this.state.orgUsers)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
res.data.failed.map(item => {
|
||||||
|
const msg = `${item.email}: ${item.error_msg}`;
|
||||||
|
toaster.danger(msg);
|
||||||
|
});
|
||||||
|
}).catch((error) => {
|
||||||
|
let errMsg = Utils.getErrorMsg(error);
|
||||||
|
toaster.danger(errMsg);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
addOrgUser = (email, name, password) => {
|
addOrgUser = (email, name, password) => {
|
||||||
seafileAPI.orgAdminAddOrgUser(orgID, email, name, password).then(res => {
|
seafileAPI.orgAdminAddOrgUser(orgID, email, name, password).then(res => {
|
||||||
let userInfo = new OrgUserInfo(res.data);
|
let userInfo = new OrgUserInfo(res.data);
|
||||||
@@ -198,12 +228,18 @@ class OrgUsers extends Component {
|
|||||||
let topbarChildren;
|
let topbarChildren;
|
||||||
topbarChildren = (
|
topbarChildren = (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<button className={topBtn} title={gettext('Add user')} onClick={this.toggleAddOrgUser}>
|
<button className="btn btn-secondary operation-item" onClick={this.toggleImportOrgUsersDialog}>{gettext('Import Users')}</button>
|
||||||
<i className="fas fa-plus-square text-secondary mr-1"></i>{gettext('Add user')}</button>
|
<button className={topBtn} title={gettext('Add User')} onClick={this.toggleAddOrgUser}>
|
||||||
|
<i className="fas fa-plus-square text-secondary mr-1"></i>{gettext('Add User')}</button>
|
||||||
{invitationLink &&
|
{invitationLink &&
|
||||||
<button className={topBtn} title={gettext('Invite user')} onClick={this.toggleInviteUserDialog}>
|
<button className={topBtn} title={gettext('Invite user')} onClick={this.toggleInviteUserDialog}>
|
||||||
<i className="fas fa-plus-square text-secondary mr-1"></i>{gettext('Invite user')}</button>
|
<i className="fas fa-plus-square text-secondary mr-1"></i>{gettext('Invite user')}</button>
|
||||||
}
|
}
|
||||||
|
{this.state.isImportOrgUsersDialogOpen &&
|
||||||
|
<ModalPortal>
|
||||||
|
<ImportOrgUsersDialog importUsersInBatch={this.importOrgUsers} toggle={this.toggleImportOrgUsersDialog}/>
|
||||||
|
</ModalPortal>
|
||||||
|
}
|
||||||
{this.state.isShowAddOrgUserDialog &&
|
{this.state.isShowAddOrgUserDialog &&
|
||||||
<ModalPortal>
|
<ModalPortal>
|
||||||
<AddOrgUserDialog handleSubmit={this.addOrgUser} toggle={this.toggleAddOrgUser}/>
|
<AddOrgUserDialog handleSubmit={this.addOrgUser} toggle={this.toggleAddOrgUser}/>
|
||||||
|
@@ -33,11 +33,23 @@ class SidePanel extends React.Component {
|
|||||||
<h3 className="sf-heading" style={{ 'color': '#f7941d' }}>{gettext('Admin')}</h3>
|
<h3 className="sf-heading" style={{ 'color': '#f7941d' }}>{gettext('Admin')}</h3>
|
||||||
<ul className="nav nav-pills flex-column nav-container">
|
<ul className="nav nav-pills flex-column nav-container">
|
||||||
<li className="nav-item">
|
<li className="nav-item">
|
||||||
<Link className={`nav-link ellipsis ${this.getActiveClass('orgmanage')}`} to={siteRoot + 'org/orgmanage/'} onClick={() => this.tabItemClick('orgmanage')} >
|
<Link className={`nav-link ellipsis ${this.getActiveClass('info')}`} to={siteRoot + 'org/info/'} onClick={() => this.tabItemClick('info')} >
|
||||||
<span className="sf2-icon-info"></span>
|
<span className="sf2-icon-info"></span>
|
||||||
<span className="nav-text">{gettext('Info')}</span>
|
<span className="nav-text">{gettext('Info')}</span>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
|
<li className="nav-item">
|
||||||
|
<Link className={`nav-link ellipsis ${this.getActiveClass('statistics-admin')}`} to={siteRoot + 'org/statistics-admin/file/'} onClick={() => this.tabItemClick('statistics-admin')} >
|
||||||
|
<span className="sf2-icon-histogram"></span>
|
||||||
|
<span className="nav-text">{gettext('Statistic')}</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li className="nav-item">
|
||||||
|
<Link className={`nav-link ellipsis ${this.getActiveClass('deviceadmin')}`} to={siteRoot + 'org/deviceadmin/desktop-devices/'} onClick={() => this.tabItemClick('deviceadmin')} >
|
||||||
|
<span className="sf2-icon-monitor"></span>
|
||||||
|
<span className="nav-text">{gettext('Devices')}</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
<li className="nav-item">
|
<li className="nav-item">
|
||||||
<Link className={`nav-link ellipsis ${this.getActiveClass('repoadmin')}`} to={siteRoot + 'org/repoadmin/'} onClick={() => this.tabItemClick('repoadmin')} >
|
<Link className={`nav-link ellipsis ${this.getActiveClass('repoadmin')}`} to={siteRoot + 'org/repoadmin/'} onClick={() => this.tabItemClick('repoadmin')} >
|
||||||
<span className="sf2-icon-library"></span>
|
<span className="sf2-icon-library"></span>
|
||||||
|
66
frontend/src/pages/org-admin/statistic/picker.js
Normal file
66
frontend/src/pages/org-admin/statistic/picker.js
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import moment from 'moment';
|
||||||
|
import Calendar from '@seafile/seafile-calendar';
|
||||||
|
import DatePicker from '@seafile/seafile-calendar/lib/Picker';
|
||||||
|
import { translateCalendar } from '../../../utils/date-format-utils';
|
||||||
|
|
||||||
|
import '@seafile/seafile-calendar/assets/index.css';
|
||||||
|
|
||||||
|
const propsTypes = {
|
||||||
|
disabledDate: PropTypes.func.isRequired,
|
||||||
|
value: PropTypes.object,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
const FORMAT = 'YYYY-MM-DD';
|
||||||
|
|
||||||
|
class Picker extends React.Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.defaultCalendarValue = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
let lang = window.app.config.lang;
|
||||||
|
this.defaultCalendarValue = moment().locale(lang).clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const props = this.props;
|
||||||
|
const calendar = (<Calendar
|
||||||
|
defaultValue={this.defaultCalendarValue}
|
||||||
|
disabledDate={props.disabledDate}
|
||||||
|
format={FORMAT}
|
||||||
|
locale={translateCalendar()}
|
||||||
|
/>);
|
||||||
|
return (
|
||||||
|
<DatePicker
|
||||||
|
calendar={calendar}
|
||||||
|
value={props.value}
|
||||||
|
onChange={props.onChange}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
({value}) => {
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
<input
|
||||||
|
placeholder="yyyy-mm-dd"
|
||||||
|
tabIndex="-1"
|
||||||
|
readOnly
|
||||||
|
value={value && value.format(FORMAT) || ''}
|
||||||
|
className="form-control system-statistic-input"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</DatePicker>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Picker.propsTypes = propsTypes;
|
||||||
|
|
||||||
|
export default Picker;
|
123
frontend/src/pages/org-admin/statistic/statistic-chart.js
Normal file
123
frontend/src/pages/org-admin/statistic/statistic-chart.js
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Line } from 'react-chartjs-2';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Utils } from '../../../utils/utils';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
labels: PropTypes.array.isRequired,
|
||||||
|
filesData: PropTypes.array.isRequired,
|
||||||
|
suggestedMaxNumbers: PropTypes.number.isRequired,
|
||||||
|
isLegendStatus: PropTypes.bool.isRequired,
|
||||||
|
chartTitle: PropTypes.string.isRequired,
|
||||||
|
isTitleCallback: PropTypes.bool,
|
||||||
|
isTicksCallback: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
class StatisticChart extends React.Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
data: {},
|
||||||
|
opations: {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
let { labels, filesData, isTitleCallback, isTicksCallback, suggestedMaxNumbers, isLegendStatus, chartTitle } = this.props;
|
||||||
|
let _this = this;
|
||||||
|
let data = {
|
||||||
|
labels: labels,
|
||||||
|
datasets: filesData
|
||||||
|
};
|
||||||
|
let options = {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
fontSize: 14,
|
||||||
|
text: chartTitle,
|
||||||
|
},
|
||||||
|
elements: {
|
||||||
|
line: {
|
||||||
|
fill: false,
|
||||||
|
tension: 0, // disable bezier curves, i.e, draw straight lines
|
||||||
|
borderWidth: 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: isLegendStatus,
|
||||||
|
labels: {
|
||||||
|
usePointStyle: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tooltips: {
|
||||||
|
callbacks: {
|
||||||
|
label: function(tooltipItem, data) {
|
||||||
|
if (isTitleCallback) {
|
||||||
|
return _this.titleCallback(tooltipItem, data);
|
||||||
|
}
|
||||||
|
return data.datasets[tooltipItem.datasetIndex].label + ': ' + tooltipItem.yLabel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
padding: {
|
||||||
|
right: 100,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
yAxes: [{
|
||||||
|
ticks: {
|
||||||
|
beginAtZero: true,
|
||||||
|
suggestedMax: suggestedMaxNumbers,
|
||||||
|
callback: function(value, index, values) {
|
||||||
|
if (isTicksCallback) {
|
||||||
|
return _this.ticksCallback(value, index, values);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
xAxes: [{
|
||||||
|
ticks: {
|
||||||
|
maxTicksLimit: 20
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.setState({
|
||||||
|
data: data,
|
||||||
|
options: options
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
let data = {
|
||||||
|
labels: nextProps.labels,
|
||||||
|
datasets: nextProps.filesData
|
||||||
|
};
|
||||||
|
this.setState({data: data});
|
||||||
|
}
|
||||||
|
|
||||||
|
titleCallback = (tooltipItem, data) => {
|
||||||
|
return data.datasets[tooltipItem.datasetIndex].label + ': ' + Utils.bytesToSize(tooltipItem.yLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
ticksCallback = (value, index, values) => {
|
||||||
|
return Utils.bytesToSize(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
|
||||||
|
let { data, options } = this.state;
|
||||||
|
return (
|
||||||
|
<Line
|
||||||
|
data={data}
|
||||||
|
options={options}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StatisticChart.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default StatisticChart;
|
138
frontend/src/pages/org-admin/statistic/statistic-common-tool.js
Normal file
138
frontend/src/pages/org-admin/statistic/statistic-common-tool.js
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import React, { Fragment } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Button } from 'reactstrap';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { gettext } from '../../../utils/constants';
|
||||||
|
import Picker from './picker';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
getActiviesFiles: PropTypes.func.isRequired,
|
||||||
|
children: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
class StatisticCommonTool extends React.Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
statisticType: 'oneWeek',
|
||||||
|
startValue: null,
|
||||||
|
endValue: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
let today = moment().format('YYYY-MM-DD 00:00:00');
|
||||||
|
let endTime = today;
|
||||||
|
let startTime = moment().subtract(6,'d').format('YYYY-MM-DD 00:00:00');
|
||||||
|
let group_by = 'day';
|
||||||
|
this.props.getActiviesFiles(startTime, endTime, group_by);
|
||||||
|
}
|
||||||
|
|
||||||
|
changeActive = (statisticTypeName) => {
|
||||||
|
let { statisticType } = this.state;
|
||||||
|
if (statisticType === statisticTypeName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let today = moment().format('YYYY-MM-DD 00:00:00');
|
||||||
|
let endTime = today;
|
||||||
|
let startTime;
|
||||||
|
switch(statisticTypeName) {
|
||||||
|
case 'oneWeek' :
|
||||||
|
startTime = moment().subtract(6,'d').format('YYYY-MM-DD 00:00:00');
|
||||||
|
break;
|
||||||
|
case 'oneMonth' :
|
||||||
|
startTime = moment().subtract(29,'d').format('YYYY-MM-DD 00:00:00');
|
||||||
|
break;
|
||||||
|
case 'oneYear' :
|
||||||
|
startTime = moment().subtract(364,'d').format('YYYY-MM-DD 00:00:00');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
statisticType: statisticTypeName,
|
||||||
|
});
|
||||||
|
let group_by = 'day';
|
||||||
|
this.props.getActiviesFiles(startTime, endTime, group_by);
|
||||||
|
}
|
||||||
|
|
||||||
|
disabledStartDate = (startValue) => {
|
||||||
|
if (!startValue) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let today = moment().format();
|
||||||
|
|
||||||
|
const endValue = this.state.endValue;
|
||||||
|
if (!endValue) {
|
||||||
|
let startTime = moment(startValue).format();
|
||||||
|
return today < startTime;
|
||||||
|
}
|
||||||
|
return endValue.isBefore(startValue) || moment(startValue).format() > today;
|
||||||
|
}
|
||||||
|
|
||||||
|
disabledEndDate = (endValue) => {
|
||||||
|
if (!endValue) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let today = moment().format();
|
||||||
|
const startValue = this.state.startValue;
|
||||||
|
if (!startValue) {
|
||||||
|
let endTime = moment(endValue).format();
|
||||||
|
return today < endTime;
|
||||||
|
}
|
||||||
|
return endValue.isBefore(startValue) || moment(endValue).format() > today;
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange = (field, value) => {
|
||||||
|
this.setState({
|
||||||
|
[field]: value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit = () => {
|
||||||
|
let { startValue, endValue } = this.state;
|
||||||
|
if(!startValue || !endValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
statisticType: 'itemButton',
|
||||||
|
});
|
||||||
|
let startTime = moment(startValue).format('YYYY-MM-DD 00:00:00');
|
||||||
|
let endTime = moment(endValue).format('YYYY-MM-DD 00:00:00');
|
||||||
|
let group_by = 'day';
|
||||||
|
this.props.getActiviesFiles(startTime, endTime, group_by);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let { statisticType, endValue, startValue } = this.state;
|
||||||
|
return(
|
||||||
|
<Fragment>
|
||||||
|
{this.props.children}
|
||||||
|
<div className="system-statistic-time-range">
|
||||||
|
<div className="sys-stat-tool">
|
||||||
|
<div className={`system-statistic-item border-right-0 rounded-left ${statisticType === 'oneWeek' ? 'item-active' : ''}`} onClick={this.changeActive.bind(this, 'oneWeek')}>{gettext('7 Days')}</div>
|
||||||
|
<div className={`system-statistic-item border-right-0 ${statisticType === 'oneMonth' ? 'item-active' : ''}`} onClick={this.changeActive.bind(this, 'oneMonth')}>{gettext('30 Days')}</div>
|
||||||
|
<div className={`system-statistic-item rounded-right ${statisticType === 'oneYear' ? 'item-active' : ''}`} onClick={this.changeActive.bind(this, 'oneYear')}>{gettext('1 Year')}</div>
|
||||||
|
</div>
|
||||||
|
<div className="system-statistic-input-container">
|
||||||
|
<Picker
|
||||||
|
disabledDate={this.disabledStartDate}
|
||||||
|
value={startValue}
|
||||||
|
onChange={this.onChange.bind(this, 'startValue')}
|
||||||
|
/>
|
||||||
|
<span className="system-statistic-connect">-</span>
|
||||||
|
<Picker
|
||||||
|
disabledDate={this.disabledEndDate}
|
||||||
|
value={endValue}
|
||||||
|
onChange={this.onChange.bind(this, 'endValue')}
|
||||||
|
/>
|
||||||
|
<Button className="operation-item system-statistic-button" onClick={this.onSubmit}>{gettext('Submit')}</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StatisticCommonTool.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default StatisticCommonTool;
|
104
frontend/src/pages/org-admin/statistic/statistic-file.js
Normal file
104
frontend/src/pages/org-admin/statistic/statistic-file.js
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import React, { Fragment } from 'react';
|
||||||
|
import moment from 'moment';
|
||||||
|
import MainPanelTopbar from '../main-panel-topbar';
|
||||||
|
import StatisticNav from './statistic-nav';
|
||||||
|
import StatisticCommonTool from './statistic-common-tool';
|
||||||
|
import { seafileAPI } from '../../../utils/seafile-api';
|
||||||
|
import StatisticChart from './statistic-chart';
|
||||||
|
import Loading from '../../../components/loading';
|
||||||
|
import { gettext, orgID } from '../../../utils/constants';
|
||||||
|
import { Utils } from '../../../utils/utils';
|
||||||
|
import toaster from '../../../components/toast';
|
||||||
|
|
||||||
|
import '../../../css/system-stat.css';
|
||||||
|
|
||||||
|
class OrgStatisticFile extends React.Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
filesData: [],
|
||||||
|
labels: [],
|
||||||
|
isLoading: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getActiviesFiles = (startTime, endTime, groupBy) => {
|
||||||
|
let { filesData } = this.state;
|
||||||
|
|
||||||
|
seafileAPI.orgAdminStatisticFiles(orgID, startTime, endTime, groupBy).then((res) => {
|
||||||
|
let labels = [],
|
||||||
|
added = [],
|
||||||
|
deleted = [],
|
||||||
|
visited = [],
|
||||||
|
modified = [];
|
||||||
|
let data = res.data;
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
data.forEach(item => {
|
||||||
|
labels.push(moment(item.datetime).format('YYYY-MM-DD'));
|
||||||
|
added.push(item.added);
|
||||||
|
deleted.push(item.deleted);
|
||||||
|
modified.push(item.modified);
|
||||||
|
visited.push(item.visited);
|
||||||
|
});
|
||||||
|
let addedData = {
|
||||||
|
label: gettext('Added'),
|
||||||
|
data: added,
|
||||||
|
borderColor: '#57cd6b',
|
||||||
|
backgroundColor: '#57cd6b'};
|
||||||
|
let visitedData = {
|
||||||
|
label: gettext('Visited'),
|
||||||
|
data: visited,
|
||||||
|
borderColor: '#fd913a',
|
||||||
|
backgroundColor: '#fd913a'};
|
||||||
|
let modifiedData = {
|
||||||
|
label: gettext('Modified'),
|
||||||
|
data: modified,
|
||||||
|
borderColor: '#72c3fc',
|
||||||
|
backgroundColor: '#72c3fc'};
|
||||||
|
let deletedData = {
|
||||||
|
label: gettext('Deleted'),
|
||||||
|
data: deleted,
|
||||||
|
borderColor: '#f75356',
|
||||||
|
backgroundColor: '#f75356'};
|
||||||
|
filesData = [visitedData, addedData, modifiedData, deletedData];
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
filesData: filesData,
|
||||||
|
labels: labels,
|
||||||
|
isLoading: false
|
||||||
|
});
|
||||||
|
}).catch(err => {
|
||||||
|
let errMessage = Utils.getErrorMsg(err);
|
||||||
|
toaster.danger(errMessage);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let { labels, filesData, isLoading } = this.state;
|
||||||
|
|
||||||
|
return(
|
||||||
|
<Fragment>
|
||||||
|
<MainPanelTopbar />
|
||||||
|
<div className="cur-view-container">
|
||||||
|
<StatisticNav currentItem="fileStatistic" />
|
||||||
|
<div className="cur-view-content">
|
||||||
|
<StatisticCommonTool getActiviesFiles={this.getActiviesFiles} />
|
||||||
|
{isLoading && <Loading />}
|
||||||
|
{!isLoading && labels.length > 0 &&
|
||||||
|
<StatisticChart
|
||||||
|
labels={labels}
|
||||||
|
filesData={filesData}
|
||||||
|
suggestedMaxNumbers={10}
|
||||||
|
isLegendStatus={true}
|
||||||
|
chartTitle={gettext('File Operations')}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OrgStatisticFile;
|
43
frontend/src/pages/org-admin/statistic/statistic-nav.js
Normal file
43
frontend/src/pages/org-admin/statistic/statistic-nav.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
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: 'fileStatistic', urlPart: 'statistics-admin/file', text: gettext('File')},
|
||||||
|
{name: 'storageStatistic', urlPart: 'statistics-admin/total-storage', text: gettext('Storage')},
|
||||||
|
{name: 'usersStatistic', urlPart: 'statistics-admin/active-users', text: gettext('Users')},
|
||||||
|
{name: 'trafficStatistic', urlPart: 'statistics-admin/traffic', text: gettext('Traffic')},
|
||||||
|
{name: 'reportsStatistic', urlPart: 'statistics-admin/reports', text: gettext('Reports')},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
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}org/${item.urlPart}/`} className={`nav-link${currentItem == item.name ? ' active' : ''}`}>{item.text}</Link>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Nav.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default Nav;
|
88
frontend/src/pages/org-admin/statistic/statistic-reports.js
Normal file
88
frontend/src/pages/org-admin/statistic/statistic-reports.js
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import React, { Fragment } from 'react';
|
||||||
|
import MainPanelTopbar from '../main-panel-topbar';
|
||||||
|
import moment from 'moment';
|
||||||
|
import StatisticNav from './statistic-nav';
|
||||||
|
import { Button, Input } from 'reactstrap';
|
||||||
|
import { siteRoot, gettext, orgID } from '../../../utils/constants';
|
||||||
|
|
||||||
|
class OrgStatisticReports extends React.Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
month: moment().format('YYYYMM'),
|
||||||
|
errorMessage: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange = (e) => {
|
||||||
|
let month = e.target.value;
|
||||||
|
this.setState({
|
||||||
|
month: month
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onGenerateReports = (type) => {
|
||||||
|
let url = siteRoot + 'api/v2.1/org/' + orgID + '/admin/statistics/';
|
||||||
|
let { month } = this.state;
|
||||||
|
if (!month) {
|
||||||
|
let errorMessage = gettext('It is required.');
|
||||||
|
this.setState({
|
||||||
|
errorMessage: errorMessage
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (type === 'month') {
|
||||||
|
let pattern = /^([012]\d{3})(0[1-9]|1[012])$/;
|
||||||
|
if (!pattern.test(month)) {
|
||||||
|
let errorMessage = gettext('Invalid month, should be yyyymm.');
|
||||||
|
this.setState({
|
||||||
|
errorMessage: errorMessage
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch(type) {
|
||||||
|
case 'month':
|
||||||
|
url += 'user-traffic/excel/?month=' + month;
|
||||||
|
break;
|
||||||
|
case 'storage':
|
||||||
|
url += 'user-storage/excel/?';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
errorMessage: ''
|
||||||
|
});
|
||||||
|
window.location.href = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
|
||||||
|
let { errorMessage } = this.state;
|
||||||
|
return(
|
||||||
|
<Fragment>
|
||||||
|
<MainPanelTopbar />
|
||||||
|
<div className="cur-view-container">
|
||||||
|
<StatisticNav currentItem="reportsStatistic" />
|
||||||
|
<div className="cur-view-content">
|
||||||
|
<div className="statistic-reports">
|
||||||
|
<div className="statistic-reports-title">{gettext('Monthly User Traffic')}</div>
|
||||||
|
<div className="d-flex align-items-center mt-4">
|
||||||
|
<span className="statistic-reports-tip">{gettext('Month:')}</span>
|
||||||
|
<Input className="statistic-reports-input" defaultValue={moment().format('YYYYMM')} onChange={this.handleChange} />
|
||||||
|
<Button className="statistic-reports-submit operation-item" onClick={this.onGenerateReports.bind(this, 'month')}>{gettext('Create Report')}</Button>
|
||||||
|
</div>
|
||||||
|
{errorMessage && <div className="error">{errorMessage}</div>}
|
||||||
|
</div>
|
||||||
|
<div className="statistic-reports">
|
||||||
|
<div className="statistic-reports-title">{gettext('User Storage')}</div>
|
||||||
|
<Button className="mt-4 operation-item" onClick={this.onGenerateReports.bind(this, 'storage')}>{gettext('Create Report')}</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OrgStatisticReports;
|
81
frontend/src/pages/org-admin/statistic/statistic-storage.js
Normal file
81
frontend/src/pages/org-admin/statistic/statistic-storage.js
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import React, { Fragment } from 'react';
|
||||||
|
import moment from 'moment';
|
||||||
|
import MainPanelTopbar from '../main-panel-topbar';
|
||||||
|
import StatisticNav from './statistic-nav';
|
||||||
|
import StatisticCommonTool from './statistic-common-tool';
|
||||||
|
import { seafileAPI } from '../../../utils/seafile-api';
|
||||||
|
import StatisticChart from './statistic-chart';
|
||||||
|
import Loading from '../../../components/loading';
|
||||||
|
import { gettext, orgID } from '../../../utils/constants';
|
||||||
|
import { Utils } from '../../../utils/utils';
|
||||||
|
import toaster from '../../../components/toast';
|
||||||
|
|
||||||
|
class OrgStatisticStorage extends React.Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
filesData: [],
|
||||||
|
labels: [],
|
||||||
|
isLoading: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getActiviesFiles = (startTime, endTime, groupBy) => {
|
||||||
|
let { filesData } = this.state;
|
||||||
|
seafileAPI.orgAdminStatisticStorages(orgID, startTime, endTime, groupBy).then((res) => {
|
||||||
|
let labels = [],
|
||||||
|
totalStorage = [];
|
||||||
|
let data = res.data;
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
data.forEach(item => {
|
||||||
|
labels.push(moment(item.datetime).format('YYYY-MM-DD'));
|
||||||
|
totalStorage.push(item.total_storage);
|
||||||
|
});
|
||||||
|
let total_storage = {
|
||||||
|
label: gettext('Total Storage'),
|
||||||
|
data: totalStorage,
|
||||||
|
borderColor: '#fd913a',
|
||||||
|
backgroundColor: '#fd913a'};
|
||||||
|
filesData = [total_storage];
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
filesData: filesData,
|
||||||
|
labels: labels,
|
||||||
|
isLoading: false
|
||||||
|
});
|
||||||
|
}).catch(err => {
|
||||||
|
let errMessage = Utils.getErrorMsg(err);
|
||||||
|
toaster.danger(errMessage);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let { labels, filesData, isLoading } = this.state;
|
||||||
|
return(
|
||||||
|
<Fragment>
|
||||||
|
<MainPanelTopbar />
|
||||||
|
<div className="cur-view-container">
|
||||||
|
<StatisticNav currentItem="storageStatistic" />
|
||||||
|
<div className="cur-view-content">
|
||||||
|
<StatisticCommonTool getActiviesFiles={this.getActiviesFiles} />
|
||||||
|
{isLoading && <Loading />}
|
||||||
|
{!isLoading && labels.length > 0 &&
|
||||||
|
<StatisticChart
|
||||||
|
labels={labels}
|
||||||
|
filesData={filesData}
|
||||||
|
suggestedMaxNumbers={10*1000*1000}
|
||||||
|
isTitleCallback={true}
|
||||||
|
isTicksCallback={true}
|
||||||
|
isLegendStatus={false}
|
||||||
|
chartTitle={gettext('Total Storage')}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OrgStatisticStorage;
|
@@ -0,0 +1,158 @@
|
|||||||
|
import React, { Fragment } from 'react';
|
||||||
|
import { Input } from 'reactstrap';
|
||||||
|
import TrafficTable from './traffic-table';
|
||||||
|
import TrafficTableBody from './traffic-table-body';
|
||||||
|
import { seafileAPI } from '../../../utils/seafile-api';
|
||||||
|
import Paginator from '../../../components/paginator';
|
||||||
|
import moment from 'moment';
|
||||||
|
import Loading from '../../../components/loading';
|
||||||
|
import { gettext, orgID } from '../../../utils/constants';
|
||||||
|
import { Utils } from '../../../utils/utils';
|
||||||
|
import toaster from '../../../components/toast';
|
||||||
|
|
||||||
|
class UsersTraffic extends React.Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
userTrafficList: [],
|
||||||
|
hasNextPage: false,
|
||||||
|
perPage: 25,
|
||||||
|
currentPage: 1,
|
||||||
|
month: moment().format('YYYYMM'),
|
||||||
|
isLoading: false,
|
||||||
|
errorMessage: '',
|
||||||
|
sortBy: 'link_file_download',
|
||||||
|
sortOrder: 'desc'
|
||||||
|
};
|
||||||
|
this.initPage = 1;
|
||||||
|
this.initMonth = moment().format('YYYYMM');
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
let urlParams = (new URL(window.location)).searchParams;
|
||||||
|
const { currentPage, perPage } = this.state;
|
||||||
|
this.setState({
|
||||||
|
perPage: parseInt(urlParams.get('per_page') || perPage),
|
||||||
|
currentPage: parseInt(urlParams.get('page') || currentPage)
|
||||||
|
}, () => {
|
||||||
|
this.getTrafficList(this.initMonth, this.state.currentPage);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getPreviousPage = () => {
|
||||||
|
this.getTrafficList(this.state.month, this.state.currentPage - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
getNextPage = () => {
|
||||||
|
this.getTrafficList(this.state.month, this.state.currentPage + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange = (e) => {
|
||||||
|
let month = e.target.value;
|
||||||
|
this.setState({
|
||||||
|
month: month
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyPress = (e) => {
|
||||||
|
let { month } = this.state;
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
let pattern = /^([012]\d{3})(0[1-9]|1[012])$/;
|
||||||
|
if (!pattern.test(month)) {
|
||||||
|
let errorMessage = gettext('Invalid month, should be yyyymm.');
|
||||||
|
this.setState({
|
||||||
|
errorMessage: errorMessage
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.getTrafficList(month, this.initPage);
|
||||||
|
e.target.blur();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getTrafficList = (month, page) => {
|
||||||
|
const { perPage, sortBy, sortOrder } = this.state;
|
||||||
|
const orderBy = sortOrder == 'asc' ? sortBy : `${sortBy}_${sortOrder}`;
|
||||||
|
this.setState({
|
||||||
|
isLoading: true,
|
||||||
|
errorMessage: ''
|
||||||
|
});
|
||||||
|
seafileAPI.orgAdminListUserTraffic(orgID, month, page, perPage, orderBy).then(res => {
|
||||||
|
let userTrafficList = res.data.user_monthly_traffic_list.slice(0);
|
||||||
|
this.setState({
|
||||||
|
month: month,
|
||||||
|
currentPage: page,
|
||||||
|
userTrafficList: userTrafficList,
|
||||||
|
hasNextPage: res.data.has_next_page,
|
||||||
|
isLoading: false
|
||||||
|
});
|
||||||
|
}).catch(err => {
|
||||||
|
let errMessage = Utils.getErrorMsg(err);
|
||||||
|
toaster.danger(errMessage);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sortItems = (sortBy) => {
|
||||||
|
this.setState({
|
||||||
|
sortBy: sortBy,
|
||||||
|
sortOrder: this.state.sortOrder == 'asc' ? 'desc' : 'asc'
|
||||||
|
}, () => {
|
||||||
|
const { month, currentPage } = this.state;
|
||||||
|
this.getTrafficList(month, currentPage);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resetPerPage = (newPerPage) => {
|
||||||
|
this.setState({
|
||||||
|
perPage: newPerPage,
|
||||||
|
}, () => this.getTrafficList(this.initMonth, this.initPage));
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isLoading, errorMessage, userTrafficList,
|
||||||
|
currentPage, hasNextPage, perPage,
|
||||||
|
sortBy, sortOrder
|
||||||
|
} = this.state;
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<div className="d-flex align-items-center mt-4">
|
||||||
|
<span className="statistic-reports-tip">{gettext('Month:')}</span>
|
||||||
|
<Input
|
||||||
|
className="statistic-reports-input"
|
||||||
|
defaultValue={moment().format('YYYYMM')}
|
||||||
|
onChange={this.handleChange}
|
||||||
|
onKeyPress={this.handleKeyPress}
|
||||||
|
/>
|
||||||
|
{errorMessage && <div className="error">{errorMessage}</div>}
|
||||||
|
</div>
|
||||||
|
{isLoading && <Loading />}
|
||||||
|
{!isLoading &&
|
||||||
|
<TrafficTable type={'user'} sortItems={this.sortItems} sortBy={sortBy} sortOrder={sortOrder}>
|
||||||
|
{userTrafficList.length > 0 && userTrafficList.map((item, index) => {
|
||||||
|
return(
|
||||||
|
<TrafficTableBody
|
||||||
|
key={index}
|
||||||
|
userTrafficItem={item}
|
||||||
|
type={'user'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TrafficTable>
|
||||||
|
}
|
||||||
|
<Paginator
|
||||||
|
gotoPreviousPage={this.getPreviousPage}
|
||||||
|
gotoNextPage={this.getNextPage}
|
||||||
|
currentPage={currentPage}
|
||||||
|
hasNextPage={hasNextPage}
|
||||||
|
curPerPage={perPage}
|
||||||
|
resetPerPage={this.resetPerPage}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UsersTraffic;
|
213
frontend/src/pages/org-admin/statistic/statistic-traffic.js
Normal file
213
frontend/src/pages/org-admin/statistic/statistic-traffic.js
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
import React, { Fragment } from 'react';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { gettext, orgID } from '../../../utils/constants';
|
||||||
|
import { seafileAPI } from '../../../utils/seafile-api';
|
||||||
|
import MainPanelTopbar from '../main-panel-topbar';
|
||||||
|
import StatisticNav from './statistic-nav';
|
||||||
|
import StatisticCommonTool from './statistic-common-tool';
|
||||||
|
import Loading from '../../../components/loading';
|
||||||
|
import UsersTraffic from './statistic-traffic-users';
|
||||||
|
import StatisticChart from './statistic-chart';
|
||||||
|
import { Utils } from '../../../utils/utils';
|
||||||
|
import toaster from '../../../components/toast';
|
||||||
|
|
||||||
|
class OrgStatisticTraffic extends React.Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
filesData: [],
|
||||||
|
linkData: [],
|
||||||
|
syncData: [],
|
||||||
|
webData: [],
|
||||||
|
labels: [],
|
||||||
|
isLoading: true,
|
||||||
|
tabActive: 'system'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
changeTabActive = activeName => {
|
||||||
|
this.setState({tabActive: activeName});
|
||||||
|
}
|
||||||
|
|
||||||
|
getActiviesFiles = (startTime, endTime, groupBy) => {
|
||||||
|
seafileAPI.orgAdminStatisticSystemTraffic(orgID, startTime, endTime, groupBy).then((res) => {
|
||||||
|
let labels = [];
|
||||||
|
let total_upload = [],
|
||||||
|
total_download = [],
|
||||||
|
link_upload = [],
|
||||||
|
link_download = [],
|
||||||
|
sync_upload = [],
|
||||||
|
sync_download = [],
|
||||||
|
web_upload = [],
|
||||||
|
web_download = [];
|
||||||
|
let data = res.data;
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
data.forEach(item => {
|
||||||
|
labels.push(moment(item.datetime).format('YYYY-MM-DD'));
|
||||||
|
link_upload.push(item['link-file-upload']);
|
||||||
|
link_download.push(item['link-file-download']);
|
||||||
|
sync_upload.push(item['sync-file-upload']);
|
||||||
|
sync_download.push(item['sync-file-download']);
|
||||||
|
web_upload.push(item['web-file-upload']);
|
||||||
|
web_download.push(item['web-file-download']);
|
||||||
|
total_upload.push(item['link-file-upload'] + item['sync-file-upload'] + item['web-file-upload']);
|
||||||
|
total_download.push(item['link-file-download'] + item['sync-file-download'] + item['web-file-download']);
|
||||||
|
});
|
||||||
|
let linkUpload = {
|
||||||
|
label: gettext('Upload'),
|
||||||
|
data: link_upload,
|
||||||
|
borderColor: '#fd913a',
|
||||||
|
backgroundColor: '#fd913a'};
|
||||||
|
let linkDownload = {
|
||||||
|
label: gettext('Download'),
|
||||||
|
data: link_download,
|
||||||
|
borderColor: '#57cd6b',
|
||||||
|
backgroundColor: '#57cd6b'};
|
||||||
|
let syncUpload = {
|
||||||
|
label: gettext('Upload'),
|
||||||
|
data: sync_upload,
|
||||||
|
borderColor: '#fd913a',
|
||||||
|
backgroundColor: '#fd913a'};
|
||||||
|
let syncDownload = {
|
||||||
|
label: gettext('Download'),
|
||||||
|
data: sync_download,
|
||||||
|
borderColor: '#57cd6b',
|
||||||
|
backgroundColor: '#57cd6b'};
|
||||||
|
let webUpload = {
|
||||||
|
label: gettext('Upload'),
|
||||||
|
data: web_upload,
|
||||||
|
borderColor: '#fd913a',
|
||||||
|
backgroundColor: '#fd913a'};
|
||||||
|
let webDownload = {
|
||||||
|
label: gettext('Download'),
|
||||||
|
data: web_download,
|
||||||
|
borderColor: '#57cd6b',
|
||||||
|
backgroundColor: '#57cd6b'};
|
||||||
|
let totalUpload = {
|
||||||
|
label: gettext('Upload'),
|
||||||
|
data: total_upload,
|
||||||
|
borderColor: '#fd913a',
|
||||||
|
backgroundColor: '#fd913a'};
|
||||||
|
let totalDownload = {
|
||||||
|
label: gettext('Download'),
|
||||||
|
data: total_download,
|
||||||
|
borderColor: '#57cd6b',
|
||||||
|
backgroundColor: '#57cd6b'};
|
||||||
|
let linkData = [linkUpload, linkDownload];
|
||||||
|
let syncData = [syncUpload, syncDownload];
|
||||||
|
let webData = [webUpload, webDownload];
|
||||||
|
let filesData = [totalUpload, totalDownload];
|
||||||
|
this.setState({
|
||||||
|
linkData: linkData,
|
||||||
|
syncData: syncData,
|
||||||
|
webData: webData,
|
||||||
|
filesData: filesData,
|
||||||
|
labels: labels,
|
||||||
|
isLoading: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
let errMessage = Utils.getErrorMsg(err);
|
||||||
|
toaster.danger(errMessage);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCommonTool = () => {
|
||||||
|
let { tabActive } = this.state;
|
||||||
|
if (tabActive === 'system') {
|
||||||
|
return (
|
||||||
|
<StatisticCommonTool getActiviesFiles={this.getActiviesFiles}>
|
||||||
|
<div className="statistic-traffic-tab">
|
||||||
|
<div className={`statistic-traffic-tab-item ${tabActive === 'system' ? 'active' : ''}`} onClick={this.changeTabActive.bind(this, 'system')}>{gettext('System')}</div>
|
||||||
|
<div className={`statistic-traffic-tab-item ${tabActive === 'user' ? 'active' : ''}`} onClick={this.changeTabActive.bind(this, 'user')}>{gettext('Users')}</div>
|
||||||
|
</div>
|
||||||
|
</StatisticCommonTool>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="statistic-traffic-tab">
|
||||||
|
<div className={`statistic-traffic-tab-item ${tabActive === 'system' ? 'active' : ''}`} onClick={this.changeTabActive.bind(this, 'system')}>{gettext('System')}</div>
|
||||||
|
<div className={`statistic-traffic-tab-item ${tabActive === 'user' ? 'active' : ''}`} onClick={this.changeTabActive.bind(this, 'user')}>{gettext('Users')}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let { labels, filesData, linkData, syncData, webData, isLoading, tabActive } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<MainPanelTopbar />
|
||||||
|
<div className="cur-view-container">
|
||||||
|
<StatisticNav currentItem="trafficStatistic" />
|
||||||
|
<div className="cur-view-content">
|
||||||
|
{this.renderCommonTool()}
|
||||||
|
{isLoading && <Loading />}
|
||||||
|
{!isLoading && tabActive === 'system' &&
|
||||||
|
<div className="statistic-traffic-chart-container">
|
||||||
|
<div className="mb-4">
|
||||||
|
{labels.length > 0 &&
|
||||||
|
<StatisticChart
|
||||||
|
labels={labels}
|
||||||
|
filesData={filesData}
|
||||||
|
chartTitle={gettext('Total Traffic')}
|
||||||
|
suggestedMaxNumbers={10*1000*1000}
|
||||||
|
isTitleCallback={true}
|
||||||
|
isTicksCallback={true}
|
||||||
|
isLegendStatus={true}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div className="mb-4">
|
||||||
|
{labels.length > 0 &&
|
||||||
|
<StatisticChart
|
||||||
|
labels={labels}
|
||||||
|
filesData={webData}
|
||||||
|
chartTitle={gettext('Web Traffic')}
|
||||||
|
suggestedMaxNumbers={10*1000*1000}
|
||||||
|
isTitleCallback={true}
|
||||||
|
isTicksCallback={true}
|
||||||
|
isLegendStatus={true}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div className="mb-4">
|
||||||
|
{labels.length > 0 &&
|
||||||
|
<StatisticChart
|
||||||
|
labels={labels}
|
||||||
|
filesData={linkData}
|
||||||
|
chartTitle={gettext('Share Link Traffic')}
|
||||||
|
suggestedMaxNumbers={10*1000*1000}
|
||||||
|
isTitleCallback={true}
|
||||||
|
isTicksCallback={true}
|
||||||
|
isLegendStatus={true}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div className="mb-4">
|
||||||
|
{labels.length > 0 &&
|
||||||
|
<StatisticChart
|
||||||
|
labels={labels}
|
||||||
|
filesData={syncData}
|
||||||
|
chartTitle={gettext('Sync Traffic')}
|
||||||
|
suggestedMaxNumbers={10*1000*1000}
|
||||||
|
isTitleCallback={true}
|
||||||
|
isTicksCallback={true}
|
||||||
|
isLegendStatus={true}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{!isLoading && tabActive === 'user' &&
|
||||||
|
<UsersTraffic />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OrgStatisticTraffic;
|
79
frontend/src/pages/org-admin/statistic/statistic-users.js
Normal file
79
frontend/src/pages/org-admin/statistic/statistic-users.js
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import React, { Fragment } from 'react';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { gettext, orgID } from '../../../utils/constants';
|
||||||
|
import MainPanelTopbar from '../main-panel-topbar';
|
||||||
|
import StatisticNav from './statistic-nav';
|
||||||
|
import StatisticCommonTool from './statistic-common-tool';
|
||||||
|
import { seafileAPI } from '../../../utils/seafile-api';
|
||||||
|
import StatisticChart from './statistic-chart';
|
||||||
|
import Loading from '../../../components/loading';
|
||||||
|
import { Utils } from '../../../utils/utils';
|
||||||
|
import toaster from '../../../components/toast';
|
||||||
|
|
||||||
|
class OrgStatisticUsers extends React.Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
filesData: [],
|
||||||
|
labels: [],
|
||||||
|
isLoading: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getActiviesFiles = (startTime, endTime, groupBy) => {
|
||||||
|
let { filesData } = this.state;
|
||||||
|
seafileAPI.orgAdminStatisticActiveUsers(orgID, startTime, endTime, groupBy).then((res) => {
|
||||||
|
let labels = [],
|
||||||
|
count = [];
|
||||||
|
let data = res.data;
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
data.forEach(item => {
|
||||||
|
labels.push(moment(item.datetime).format('YYYY-MM-DD'));
|
||||||
|
count.push(item.count);
|
||||||
|
});
|
||||||
|
let userCount = {
|
||||||
|
label: gettext('Active Users'),
|
||||||
|
data: count,
|
||||||
|
borderColor: '#fd913a',
|
||||||
|
backgroundColor: '#fd913a'};
|
||||||
|
filesData = [userCount];
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
filesData: filesData,
|
||||||
|
labels: labels,
|
||||||
|
isLoading: false
|
||||||
|
});
|
||||||
|
}).catch(err => {
|
||||||
|
let errMessage = Utils.getErrorMsg(err);
|
||||||
|
toaster.danger(errMessage);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let { labels, filesData, isLoading } = this.state;
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<MainPanelTopbar />
|
||||||
|
<div className="cur-view-container">
|
||||||
|
<StatisticNav currentItem="usersStatistic" />
|
||||||
|
<div className="cur-view-content">
|
||||||
|
<StatisticCommonTool getActiviesFiles={this.getActiviesFiles} />
|
||||||
|
{isLoading && <Loading />}
|
||||||
|
{!isLoading && labels.length > 0 &&
|
||||||
|
<StatisticChart
|
||||||
|
labels={labels}
|
||||||
|
filesData={filesData}
|
||||||
|
suggestedMaxNumbers={10}
|
||||||
|
isLegendStatus={false}
|
||||||
|
chartTitle={gettext('Active Users')}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OrgStatisticUsers;
|
54
frontend/src/pages/org-admin/statistic/traffic-table-body.js
Normal file
54
frontend/src/pages/org-admin/statistic/traffic-table-body.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Utils } from '../../../utils/utils';
|
||||||
|
import { siteRoot } from '../../../utils/constants';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
type: PropTypes.string.isRequired,
|
||||||
|
userTrafficItem: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
class TrafficTableBody extends React.Component {
|
||||||
|
|
||||||
|
trafficName = () => {
|
||||||
|
let { userTrafficItem, type } = this.props;
|
||||||
|
switch(type) {
|
||||||
|
case 'user':
|
||||||
|
if (userTrafficItem.name) {
|
||||||
|
return (
|
||||||
|
<a href={siteRoot + 'useradmin/info/' + userTrafficItem.email + '/'}>{userTrafficItem.name}</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return(<span>{'--'}</span>);
|
||||||
|
case 'org':
|
||||||
|
return(<span>{userTrafficItem.org_name}</span>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let { userTrafficItem } = this.props;
|
||||||
|
|
||||||
|
let syncUploadSize = Utils.bytesToSize(userTrafficItem.sync_file_upload);
|
||||||
|
let syncDownloadSize = Utils.bytesToSize(userTrafficItem.sync_file_download);
|
||||||
|
let webUploadSize = Utils.bytesToSize(userTrafficItem.web_file_upload);
|
||||||
|
let webDownloadSize = Utils.bytesToSize(userTrafficItem.web_file_download);
|
||||||
|
let linkUploadSize = Utils.bytesToSize(userTrafficItem.link_file_upload);
|
||||||
|
let linkDownloadSize = Utils.bytesToSize(userTrafficItem.link_file_download);
|
||||||
|
|
||||||
|
return(
|
||||||
|
<tr>
|
||||||
|
<td>{this.trafficName()}</td>
|
||||||
|
<td>{syncUploadSize}</td>
|
||||||
|
<td>{syncDownloadSize}</td>
|
||||||
|
<td>{webUploadSize}</td>
|
||||||
|
<td>{webDownloadSize}</td>
|
||||||
|
<td>{linkUploadSize}</td>
|
||||||
|
<td>{linkDownloadSize}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TrafficTableBody.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default TrafficTableBody;
|
46
frontend/src/pages/org-admin/statistic/traffic-table.js
Normal file
46
frontend/src/pages/org-admin/statistic/traffic-table.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { gettext } from '../../../utils/constants';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
type: PropTypes.string.isRequired,
|
||||||
|
sortBy: PropTypes.string.isRequired,
|
||||||
|
sortOrder: PropTypes.string.isRequired,
|
||||||
|
sortItems: PropTypes.func.isRequired,
|
||||||
|
children: PropTypes.oneOfType([PropTypes.bool, PropTypes.array]),
|
||||||
|
};
|
||||||
|
|
||||||
|
class TrafficTable extends React.Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { type, sortBy, sortOrder } = this.props;
|
||||||
|
const sortIcon = sortOrder == 'asc' ? <span className="fas fa-caret-up"></span> : <span className="fas fa-caret-down"></span>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<table className="table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th width="16%">{type == 'user' ? gettext('User') : gettext('Organization')}</th>
|
||||||
|
<th width="11%"><div className="d-block table-sort-op cursor-pointer" onClick={this.props.sortItems.bind(this, 'sync_file_upload')}>{gettext('Sync Upload')} {sortBy === 'sync_file_upload' && sortIcon}</div></th>
|
||||||
|
<th width="14%"><div className="d-block table-sort-op cursor-pointer" onClick={this.props.sortItems.bind(this, 'sync_file_download')}>{gettext('Sync Download')} {sortBy === 'sync_file_download' && sortIcon}</div></th>
|
||||||
|
<th width="11%"><div className="d-block table-sort-op cursor-pointer" onClick={this.props.sortItems.bind(this, 'web_file_upload')}>{gettext('Web Upload')} {sortBy === 'web_file_upload' && sortIcon}</div></th>
|
||||||
|
<th width="14%"><div className="d-block table-sort-op cursor-pointer" onClick={this.props.sortItems.bind(this, 'web_file_download')}>{gettext('Web Download')} {sortBy === 'web_file_download' && sortIcon}</div></th>
|
||||||
|
<th width="17%"><div className="d-block table-sort-op cursor-pointer" onClick={this.props.sortItems.bind(this, 'link_file_upload')}>{gettext('Share link upload')} {sortBy === 'link_file_upload' && sortIcon}</div></th>
|
||||||
|
<th width="17%"><div className="d-block table-sort-op cursor-pointer" onClick={this.props.sortItems.bind(this, 'link_file_download')}>{gettext('Share link download')} {sortBy === 'link_file_download' && sortIcon}</div></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{this.props.children}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TrafficTable.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default TrafficTable;
|
20
frontend/src/pages/org-admin/user-link.js
Normal file
20
frontend/src/pages/org-admin/user-link.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Link } from '@reach/router';
|
||||||
|
import { siteRoot, orgID } from '../../utils/constants';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
email: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
class UserLink extends Component {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <Link to={`${siteRoot}org/useradmin/info/${encodeURIComponent(this.props.email)}/`}>{this.props.name}</Link>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UserLink.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default UserLink;
|
220
seahub/organizations/api/admin/devices.py
Normal file
220
seahub/organizations/api/admin/devices.py
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
# Copyright (c) 2012-2016 Seafile Ltd.
|
||||||
|
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.devices import do_unlink_device
|
||||||
|
from seahub.utils.timeutils import datetime_to_isoformat_timestr, \
|
||||||
|
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.api2.models import TokenV2, DESKTOP_PLATFORMS, MOBILE_PLATFORMS
|
||||||
|
from seahub.base.templatetags.seahub_tags import email2nickname
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class OrgAdminDevices(APIView):
|
||||||
|
|
||||||
|
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||||
|
throttle_classes = (UserRateThrottle,)
|
||||||
|
permission_classes = (IsProVersion, IsOrgAdminUser)
|
||||||
|
|
||||||
|
def get(self, request, org_id):
|
||||||
|
|
||||||
|
org_id = int(org_id)
|
||||||
|
org = ccnet_api.get_org_by_id(org_id)
|
||||||
|
if not org:
|
||||||
|
error_msg = 'Organization %s not found.' % org_id
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
current_page = int(request.GET.get('page', '1'))
|
||||||
|
per_page = int(request.GET.get('per_page', '50'))
|
||||||
|
except ValueError:
|
||||||
|
current_page = 1
|
||||||
|
per_page = 50
|
||||||
|
|
||||||
|
start = (current_page - 1) * per_page
|
||||||
|
end = current_page * per_page + 1
|
||||||
|
|
||||||
|
platform = request.GET.get('platform', None)
|
||||||
|
org_users = ccnet_api.get_org_users_by_url_prefix(org.url_prefix, -1, -1)
|
||||||
|
org_user_emails = [user.email for user in org_users]
|
||||||
|
|
||||||
|
devices = TokenV2.objects.filter(wiped_at=None)
|
||||||
|
|
||||||
|
if platform == 'desktop':
|
||||||
|
devices = devices.filter(platform__in=DESKTOP_PLATFORMS) \
|
||||||
|
.filter(user__in=org_user_emails) \
|
||||||
|
.order_by('-last_accessed')[start: end]
|
||||||
|
|
||||||
|
elif platform == 'mobile':
|
||||||
|
devices = devices.filter(platform__in=MOBILE_PLATFORMS) \
|
||||||
|
.filter(user__in=org_user_emails) \
|
||||||
|
.order_by('-last_accessed')[start: end]
|
||||||
|
else:
|
||||||
|
devices = devices.order_by('-last_accessed') \
|
||||||
|
.filter(user__in=org_user_emails)[start: end]
|
||||||
|
|
||||||
|
if len(devices) == end - start:
|
||||||
|
devices = devices[:per_page]
|
||||||
|
has_next_page = True
|
||||||
|
else:
|
||||||
|
has_next_page = False
|
||||||
|
|
||||||
|
return_results = []
|
||||||
|
for device in devices:
|
||||||
|
result = {}
|
||||||
|
result['client_version'] = device.client_version
|
||||||
|
result['device_id'] = device.device_id
|
||||||
|
result['device_name'] = device.device_name
|
||||||
|
result['last_accessed'] = datetime_to_isoformat_timestr(device.last_accessed)
|
||||||
|
result['last_login_ip'] = device.last_login_ip
|
||||||
|
result['user'] = device.user
|
||||||
|
result['user_name'] = email2nickname(device.user)
|
||||||
|
result['platform'] = device.platform
|
||||||
|
|
||||||
|
result['is_desktop_client'] = False
|
||||||
|
if result['platform'] in DESKTOP_PLATFORMS:
|
||||||
|
result['is_desktop_client'] = True
|
||||||
|
|
||||||
|
return_results.append(result)
|
||||||
|
|
||||||
|
page_info = {
|
||||||
|
'has_next_page': has_next_page,
|
||||||
|
'current_page': current_page
|
||||||
|
}
|
||||||
|
return Response({"page_info": page_info, "devices": return_results})
|
||||||
|
|
||||||
|
def delete(self, request, org_id):
|
||||||
|
|
||||||
|
org_id = int(org_id)
|
||||||
|
org = ccnet_api.get_org_by_id(org_id)
|
||||||
|
if not org:
|
||||||
|
error_msg = 'Organization %s not found.' % org_id
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
|
platform = request.data.get('platform', '')
|
||||||
|
device_id = request.data.get('device_id', '')
|
||||||
|
remote_wipe = request.data.get('wipe_device', '')
|
||||||
|
user = request.data.get('user', '')
|
||||||
|
|
||||||
|
if not platform:
|
||||||
|
error_msg = 'platform invalid.'
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
|
if not device_id:
|
||||||
|
error_msg = 'device_id invalid.'
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
error_msg = 'user invalid.'
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
|
remote_wipe = True if remote_wipe == 'true' else False
|
||||||
|
|
||||||
|
try:
|
||||||
|
do_unlink_device(user, platform, device_id, remote_wipe=remote_wipe)
|
||||||
|
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})
|
||||||
|
|
||||||
|
|
||||||
|
class OrgAdminDevicesErrors(APIView):
|
||||||
|
|
||||||
|
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||||
|
throttle_classes = (UserRateThrottle, )
|
||||||
|
permission_classes = (IsProVersion, IsOrgAdminUser)
|
||||||
|
|
||||||
|
def get(self, request, org_id):
|
||||||
|
|
||||||
|
org_id = int(org_id)
|
||||||
|
org = ccnet_api.get_org_by_id(org_id)
|
||||||
|
if not org:
|
||||||
|
error_msg = 'Organization %s not found.' % org_id
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
return_results = []
|
||||||
|
try:
|
||||||
|
device_errors = seafile_api.list_org_repo_sync_errors(org_id, start, limit)
|
||||||
|
except SearpcError as e:
|
||||||
|
logger.error(e)
|
||||||
|
error_msg = 'Internal Server Error'
|
||||||
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||||
|
|
||||||
|
if len(device_errors) > per_page:
|
||||||
|
device_errors = device_errors[:per_page]
|
||||||
|
has_next_page = True
|
||||||
|
else:
|
||||||
|
has_next_page = False
|
||||||
|
|
||||||
|
for error in device_errors:
|
||||||
|
result = {}
|
||||||
|
result['email'] = error.email if error.email else ''
|
||||||
|
result['name'] = email2nickname(error.email)
|
||||||
|
result['device_ip'] = error.peer_ip if error.peer_ip else ''
|
||||||
|
result['repo_name'] = error.repo_name if error.repo_name else ''
|
||||||
|
result['repo_id'] = error.repo_id if error.repo_id else ''
|
||||||
|
result['error_msg'] = error.error_con if error.error_con else ''
|
||||||
|
|
||||||
|
tokens = TokenV2.objects.filter(device_id=error.peer_id)
|
||||||
|
if tokens:
|
||||||
|
result['device_name'] = tokens[0].device_name
|
||||||
|
result['client_version'] = tokens[0].client_version
|
||||||
|
else:
|
||||||
|
result['device_name'] = ''
|
||||||
|
result['client_version'] = ''
|
||||||
|
|
||||||
|
if error.error_time:
|
||||||
|
result['error_time'] = timestamp_to_isoformat_timestr(error.error_time)
|
||||||
|
else:
|
||||||
|
result['error_time'] = ''
|
||||||
|
|
||||||
|
return_results.append(result)
|
||||||
|
|
||||||
|
page_info = {
|
||||||
|
'has_next_page': has_next_page,
|
||||||
|
'current_page': current_page
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response({"page_info": page_info, "device_errors": return_results})
|
||||||
|
|
||||||
|
def delete(self, request, org_id):
|
||||||
|
|
||||||
|
org_id = int(org_id)
|
||||||
|
org = ccnet_api.get_org_by_id(org_id)
|
||||||
|
if not org:
|
||||||
|
error_msg = 'Organization %s not found.' % org_id
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
seafile_api.clear_repo_sync_errors()
|
||||||
|
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})
|
411
seahub/organizations/api/admin/statistics.py
Normal file
411
seahub/organizations/api/admin/statistics.py
Normal file
@@ -0,0 +1,411 @@
|
|||||||
|
# Copyright (c) 2012-2016 Seafile Ltd.
|
||||||
|
import datetime
|
||||||
|
import pytz
|
||||||
|
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 django.utils import timezone
|
||||||
|
from django.utils.translation import ugettext as _
|
||||||
|
from django.http import HttpResponse
|
||||||
|
|
||||||
|
from seaserv import ccnet_api
|
||||||
|
|
||||||
|
from seahub.utils import get_org_file_ops_stats_by_day, \
|
||||||
|
get_org_total_storage_stats_by_day, get_org_user_activity_stats_by_day, \
|
||||||
|
get_org_traffic_by_day, is_pro_version, EVENTS_ENABLED, \
|
||||||
|
seafevents_api
|
||||||
|
from seahub.utils.timeutils import datetime_to_isoformat_timestr
|
||||||
|
from seahub.utils.ms_excel import write_xls
|
||||||
|
from seahub.utils.file_size import byte_to_mb
|
||||||
|
from seahub.views.sysadmin import _populate_user_quota_usage
|
||||||
|
from seahub.base.templatetags.seahub_tags import email2nickname, \
|
||||||
|
email2contact_email
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def get_time_offset():
|
||||||
|
timezone_name = timezone.get_current_timezone_name()
|
||||||
|
offset = pytz.timezone(timezone_name).localize(datetime.datetime.now()).strftime('%z')
|
||||||
|
return offset[:3] + ':' + offset[3:]
|
||||||
|
|
||||||
|
|
||||||
|
def get_init_data(start_time, end_time, init_data=0):
|
||||||
|
res = {}
|
||||||
|
start_time = start_time.replace(hour=0).replace(minute=0).replace(second=0)
|
||||||
|
end_time = end_time.replace(hour=0).replace(minute=0).replace(second=0)
|
||||||
|
time_delta = end_time - start_time
|
||||||
|
date_length = time_delta.days + 1
|
||||||
|
for offset in range(date_length):
|
||||||
|
offset = offset * 24
|
||||||
|
dt = start_time + datetime.timedelta(hours=offset)
|
||||||
|
if isinstance(init_data, dict):
|
||||||
|
res[dt] = init_data.copy()
|
||||||
|
else:
|
||||||
|
res[dt] = init_data
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def check_parameter(func):
|
||||||
|
|
||||||
|
def _decorated(view, request, org_id, *args, **kwargs):
|
||||||
|
|
||||||
|
if not is_pro_version() or not EVENTS_ENABLED:
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, 'Events not enabled.')
|
||||||
|
|
||||||
|
start_time = request.GET.get("start", "")
|
||||||
|
end_time = request.GET.get("end", "")
|
||||||
|
|
||||||
|
if not start_time:
|
||||||
|
error_msg = "Start time can not be empty"
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
|
if not end_time:
|
||||||
|
error_msg = "End time can not be empty"
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
start_time = datetime.datetime.strptime(start_time, "%Y-%m-%d %H:%M:%S")
|
||||||
|
except Exception:
|
||||||
|
error_msg = "Start time %s invalid" % start_time
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
end_time = datetime.datetime.strptime(end_time, "%Y-%m-%d %H:%M:%S")
|
||||||
|
except Exception:
|
||||||
|
error_msg = "End time %s invalid" % end_time
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
|
return func(view, request, org_id, start_time, end_time, *args, **kwargs)
|
||||||
|
|
||||||
|
return _decorated
|
||||||
|
|
||||||
|
|
||||||
|
class OrgFileOperationsView(APIView):
|
||||||
|
|
||||||
|
"""
|
||||||
|
Get file operations statistics.
|
||||||
|
Permission checking:
|
||||||
|
1. only org admin can perform this action.
|
||||||
|
"""
|
||||||
|
|
||||||
|
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||||
|
throttle_classes = (UserRateThrottle,)
|
||||||
|
permission_classes = (IsProVersion, IsOrgAdminUser)
|
||||||
|
|
||||||
|
@check_parameter
|
||||||
|
def get(self, request, org_id, start_time, end_time):
|
||||||
|
"""
|
||||||
|
Get records of the specified time range.
|
||||||
|
param:
|
||||||
|
start: the start time of the query.
|
||||||
|
end: the end time of the query.
|
||||||
|
return:
|
||||||
|
the list of file operations record.
|
||||||
|
"""
|
||||||
|
data = get_org_file_ops_stats_by_day(org_id, start_time, end_time, get_time_offset())
|
||||||
|
ops_added_dict = get_init_data(start_time, end_time)
|
||||||
|
ops_visited_dict = get_init_data(start_time, end_time)
|
||||||
|
ops_deleted_dict = get_init_data(start_time, end_time)
|
||||||
|
ops_modified_dict = get_init_data(start_time, end_time)
|
||||||
|
|
||||||
|
# [{'number': 2,
|
||||||
|
# 'op_type': 'Added',
|
||||||
|
# 'timestamp': datetime.datetime(2022, 10, 27, 0, 0)}]
|
||||||
|
for item in data:
|
||||||
|
if item.get('op_type') == 'Added':
|
||||||
|
ops_added_dict[item.get('timestamp')] = item.get('number')
|
||||||
|
if item.get('op_type') == 'Visited':
|
||||||
|
ops_visited_dict[item.get('timestamp')] = item.get('number')
|
||||||
|
if item.get('op_type') == 'Deleted':
|
||||||
|
ops_deleted_dict[item.get('timestamp')] = item.get('number')
|
||||||
|
if item.get('op_type') == 'Modified':
|
||||||
|
ops_modified_dict[item.get('timestamp')] = item.get('number')
|
||||||
|
|
||||||
|
res_data = []
|
||||||
|
for k, v in list(ops_added_dict.items()):
|
||||||
|
res_data.append({'datetime': datetime_to_isoformat_timestr(k),
|
||||||
|
'added': v,
|
||||||
|
'visited': ops_visited_dict[k],
|
||||||
|
'deleted': ops_deleted_dict[k],
|
||||||
|
'modified': ops_modified_dict[k]})
|
||||||
|
|
||||||
|
return Response(sorted(res_data, key=lambda x: x['datetime']))
|
||||||
|
|
||||||
|
|
||||||
|
class OrgTotalStorageView(APIView):
|
||||||
|
|
||||||
|
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||||
|
throttle_classes = (UserRateThrottle,)
|
||||||
|
permission_classes = (IsProVersion, IsOrgAdminUser)
|
||||||
|
|
||||||
|
@check_parameter
|
||||||
|
def get(self, request, org_id, start_time, end_time):
|
||||||
|
|
||||||
|
data = get_org_total_storage_stats_by_day(org_id, start_time, end_time, get_time_offset())
|
||||||
|
|
||||||
|
# [{'number': Decimal('2558796'),
|
||||||
|
# 'timestamp': datetime.datetime(2022, 11, 1, 0, 0)},
|
||||||
|
# {'number': Decimal('2558796'),
|
||||||
|
# 'timestamp': datetime.datetime(2022, 11, 2, 0, 0)}]
|
||||||
|
init_data = get_init_data(start_time, end_time)
|
||||||
|
for e in data:
|
||||||
|
init_data[e.get("timestamp")] = e.get("number")
|
||||||
|
|
||||||
|
res_data = []
|
||||||
|
for k, v in list(init_data.items()):
|
||||||
|
res_data.append({'datetime': datetime_to_isoformat_timestr(k), 'total_storage': v})
|
||||||
|
|
||||||
|
return Response(sorted(res_data, key=lambda x: x['datetime']))
|
||||||
|
|
||||||
|
|
||||||
|
class OrgActiveUsersView(APIView):
|
||||||
|
|
||||||
|
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||||
|
throttle_classes = (UserRateThrottle,)
|
||||||
|
permission_classes = (IsProVersion, IsOrgAdminUser)
|
||||||
|
|
||||||
|
@check_parameter
|
||||||
|
def get(self, request, org_id, start_time, end_time):
|
||||||
|
|
||||||
|
data = get_org_user_activity_stats_by_day(org_id, start_time, end_time)
|
||||||
|
|
||||||
|
# [{'number': 1, 'timestamp': datetime.datetime(2022, 10, 27, 0, 0)},
|
||||||
|
# {'number': 2, 'timestamp': datetime.datetime(2022, 10, 31, 0, 0)},
|
||||||
|
# {'number': 2, 'timestamp': datetime.datetime(2022, 11, 1, 0, 0)},
|
||||||
|
# {'number': 1, 'timestamp': datetime.datetime(2022, 11, 2, 0, 0)}]
|
||||||
|
init_data = get_init_data(start_time, end_time)
|
||||||
|
for e in data:
|
||||||
|
init_data[e.get("timestamp")] = e.get("number")
|
||||||
|
|
||||||
|
res_data = []
|
||||||
|
for k, v in list(init_data.items()):
|
||||||
|
res_data.append({'datetime': datetime_to_isoformat_timestr(k), 'count': v})
|
||||||
|
|
||||||
|
return Response(sorted(res_data, key=lambda x: x['datetime']))
|
||||||
|
|
||||||
|
|
||||||
|
class OrgSystemTrafficView(APIView):
|
||||||
|
|
||||||
|
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||||
|
throttle_classes = (UserRateThrottle,)
|
||||||
|
permission_classes = (IsProVersion, IsOrgAdminUser)
|
||||||
|
|
||||||
|
@check_parameter
|
||||||
|
def get(self, request, org_id, start_time, end_time):
|
||||||
|
|
||||||
|
op_type_list = ['web-file-upload', 'web-file-download',
|
||||||
|
'sync-file-download', 'sync-file-upload',
|
||||||
|
'link-file-upload', 'link-file-download']
|
||||||
|
init_count = [0] * 6
|
||||||
|
init_data = get_init_data(start_time, end_time,
|
||||||
|
dict(list(zip(op_type_list, init_count))))
|
||||||
|
|
||||||
|
data = get_org_traffic_by_day(org_id, start_time, end_time, get_time_offset())
|
||||||
|
# [(datetime.datetime(2022, 11, 1, 0, 0), 'web-file-upload', 2558798),
|
||||||
|
# (datetime.datetime(2022, 11, 2, 0, 0), 'web-file-upload', 48659279),
|
||||||
|
# (datetime.datetime(2022, 11, 3, 0, 0), 'link-file-download', 48658882),
|
||||||
|
# (datetime.datetime(2022, 11, 3, 0, 0), 'web-file-upload', 24329441)]
|
||||||
|
for e in data:
|
||||||
|
dt, op_type, count = e
|
||||||
|
init_data[dt].update({op_type: count})
|
||||||
|
|
||||||
|
res_data = []
|
||||||
|
for k, v in list(init_data.items()):
|
||||||
|
res = {'datetime': datetime_to_isoformat_timestr(k)}
|
||||||
|
res.update(v)
|
||||||
|
res_data.append(res)
|
||||||
|
|
||||||
|
return Response(sorted(res_data, key=lambda x: x['datetime']))
|
||||||
|
|
||||||
|
|
||||||
|
class OrgUserTrafficView(APIView):
|
||||||
|
|
||||||
|
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||||
|
throttle_classes = (UserRateThrottle,)
|
||||||
|
permission_classes = (IsProVersion, IsOrgAdminUser)
|
||||||
|
|
||||||
|
def get(self, request, org_id):
|
||||||
|
|
||||||
|
month = request.GET.get("month", "")
|
||||||
|
if not month:
|
||||||
|
error_msg = "month invalid."
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
month_obj = datetime.datetime.strptime(month, "%Y%m")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
error_msg = "month %s invalid" % month
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
page = int(request.GET.get('page', '1'))
|
||||||
|
per_page = int(request.GET.get('per_page', '25'))
|
||||||
|
except ValueError:
|
||||||
|
page = 1
|
||||||
|
per_page = 25
|
||||||
|
start = (page - 1) * per_page
|
||||||
|
|
||||||
|
order_by = request.GET.get('order_by', '')
|
||||||
|
filters = [
|
||||||
|
'sync_file_upload', 'sync_file_download',
|
||||||
|
'web_file_upload', 'web_file_download',
|
||||||
|
'link_file_upload', 'link_file_download',
|
||||||
|
]
|
||||||
|
if order_by not in filters and \
|
||||||
|
order_by not in map(lambda x: x + '_desc', filters):
|
||||||
|
order_by = 'link_file_download_desc'
|
||||||
|
|
||||||
|
# get one more item than per_page, to judge has_next_page
|
||||||
|
try:
|
||||||
|
traffics = seafevents_api.get_all_users_traffic_by_month(month_obj,
|
||||||
|
start,
|
||||||
|
start + per_page + 1,
|
||||||
|
order_by,
|
||||||
|
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)
|
||||||
|
|
||||||
|
if len(traffics) == per_page + 1:
|
||||||
|
has_next_page = True
|
||||||
|
traffics = traffics[:per_page]
|
||||||
|
else:
|
||||||
|
has_next_page = False
|
||||||
|
|
||||||
|
user_monthly_traffic_list = []
|
||||||
|
for traffic in traffics:
|
||||||
|
info = {}
|
||||||
|
info['email'] = traffic['user']
|
||||||
|
info['name'] = email2nickname(traffic['user'])
|
||||||
|
info['sync_file_upload'] = traffic['sync_file_upload']
|
||||||
|
info['sync_file_download'] = traffic['sync_file_download']
|
||||||
|
info['web_file_upload'] = traffic['web_file_upload']
|
||||||
|
info['web_file_download'] = traffic['web_file_download']
|
||||||
|
info['link_file_upload'] = traffic['link_file_upload']
|
||||||
|
info['link_file_download'] = traffic['link_file_download']
|
||||||
|
user_monthly_traffic_list.append(info)
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'user_monthly_traffic_list': user_monthly_traffic_list,
|
||||||
|
'has_next_page': has_next_page
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class OrgUserTrafficExcelView(APIView):
|
||||||
|
|
||||||
|
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||||
|
throttle_classes = (UserRateThrottle,)
|
||||||
|
permission_classes = (IsProVersion, IsOrgAdminUser)
|
||||||
|
|
||||||
|
def get(self, request, org_id):
|
||||||
|
|
||||||
|
month = request.GET.get("month", "")
|
||||||
|
if not month:
|
||||||
|
error_msg = "month invalid."
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
month_obj = datetime.datetime.strptime(month, "%Y%m")
|
||||||
|
except Exception:
|
||||||
|
error_msg = "Month %s invalid" % month
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
res_data = seafevents_api.get_all_users_traffic_by_month(month_obj,
|
||||||
|
-1, -1,
|
||||||
|
org_id=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)
|
||||||
|
|
||||||
|
data_list = []
|
||||||
|
head = [_("Time"), _("User"), _("Web Download") + ('(MB)'),
|
||||||
|
_("Sync Download") + ('(MB)'), _("Link Download") + ('(MB)'),
|
||||||
|
_("Web Upload") + ('(MB)'), _("Sync Upload") + ('(MB)'),
|
||||||
|
_("Link Upload") + ('(MB)')]
|
||||||
|
|
||||||
|
for data in res_data:
|
||||||
|
web_download = byte_to_mb(data['web_file_download'])
|
||||||
|
sync_download = byte_to_mb(data['sync_file_download'])
|
||||||
|
link_download = byte_to_mb(data['link_file_download'])
|
||||||
|
web_upload = byte_to_mb(data['web_file_upload'])
|
||||||
|
sync_upload = byte_to_mb(data['sync_file_upload'])
|
||||||
|
link_upload = byte_to_mb(data['link_file_upload'])
|
||||||
|
|
||||||
|
row = [month, data['user'], web_download, sync_download,
|
||||||
|
link_download, web_upload, sync_upload, link_upload]
|
||||||
|
|
||||||
|
data_list.append(row)
|
||||||
|
|
||||||
|
excel_name = "User Traffic %s" % month
|
||||||
|
|
||||||
|
try:
|
||||||
|
wb = write_xls(excel_name, head, data_list)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
error_msg = 'Internal Server Error'
|
||||||
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||||
|
|
||||||
|
response = HttpResponse(content_type='application/ms-excel')
|
||||||
|
response['Content-Disposition'] = 'attachment; filename="%s.xlsx"' % excel_name
|
||||||
|
wb.save(response)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class OrgUserStorageExcelView(APIView):
|
||||||
|
|
||||||
|
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||||
|
throttle_classes = (UserRateThrottle,)
|
||||||
|
permission_classes = (IsProVersion, IsOrgAdminUser)
|
||||||
|
|
||||||
|
def get(self, request, org_id):
|
||||||
|
|
||||||
|
org_id = int(org_id)
|
||||||
|
org = ccnet_api.get_org_by_id(org_id)
|
||||||
|
all_users = ccnet_api.get_org_users_by_url_prefix(org.url_prefix, -1, -1)
|
||||||
|
|
||||||
|
head = [_("Email"), _("Name"), _("Contact Email"),
|
||||||
|
_("Space Usage") + "(MB)", _("Space Quota") + "(MB)"]
|
||||||
|
|
||||||
|
data_list = []
|
||||||
|
for user in all_users:
|
||||||
|
|
||||||
|
user_email = user.email
|
||||||
|
user_name = email2nickname(user_email)
|
||||||
|
user_contact_email = email2contact_email(user_email)
|
||||||
|
|
||||||
|
_populate_user_quota_usage(user)
|
||||||
|
space_usage_MB = byte_to_mb(user.space_usage)
|
||||||
|
space_quota_MB = byte_to_mb(user.space_quota)
|
||||||
|
|
||||||
|
row = [user_email, user_name, user_contact_email,
|
||||||
|
space_usage_MB, space_quota_MB]
|
||||||
|
|
||||||
|
data_list.append(row)
|
||||||
|
|
||||||
|
excel_name = 'User Storage'
|
||||||
|
try:
|
||||||
|
wb = write_xls('users', head, data_list)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
error_msg = 'Internal Server Error'
|
||||||
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||||
|
|
||||||
|
response = HttpResponse(content_type='application/ms-excel')
|
||||||
|
response['Content-Disposition'] = 'attachment; filename="%s.xlsx"' % excel_name
|
||||||
|
wb.save(response)
|
||||||
|
|
||||||
|
return response
|
@@ -1,5 +1,7 @@
|
|||||||
# Copyright (c) 2012-2016 Seafile Ltd.
|
# Copyright (c) 2012-2016 Seafile Ltd.
|
||||||
import logging
|
import logging
|
||||||
|
from io import BytesIO
|
||||||
|
from openpyxl import load_workbook
|
||||||
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
@@ -19,8 +21,10 @@ from seahub.base.models import UserLastLogin
|
|||||||
from seahub.base.templatetags.seahub_tags import email2nickname, email2contact_email
|
from seahub.base.templatetags.seahub_tags import email2nickname, email2contact_email
|
||||||
from seahub.profile.models import Profile
|
from seahub.profile.models import Profile
|
||||||
from seahub.settings import SEND_EMAIL_ON_ADDING_SYSTEM_MEMBER
|
from seahub.settings import SEND_EMAIL_ON_ADDING_SYSTEM_MEMBER
|
||||||
from seahub.utils import is_valid_email, IS_EMAIL_CONFIGURED
|
from seahub.utils import is_valid_email, IS_EMAIL_CONFIGURED, \
|
||||||
|
get_file_type_and_ext
|
||||||
from seahub.utils.file_size import get_file_size_unit
|
from seahub.utils.file_size import get_file_size_unit
|
||||||
|
from seahub.utils.error_msg import file_type_error_msg
|
||||||
from seahub.utils.timeutils import timestamp_to_isoformat_timestr, datetime_to_isoformat_timestr
|
from seahub.utils.timeutils import timestamp_to_isoformat_timestr, datetime_to_isoformat_timestr
|
||||||
from seahub.utils.licenseparse import user_number_over_limit
|
from seahub.utils.licenseparse import user_number_over_limit
|
||||||
from seahub.views.sysadmin import send_user_add_mail
|
from seahub.views.sysadmin import send_user_add_mail
|
||||||
@@ -44,8 +48,8 @@ class OrgAdminUsers(APIView):
|
|||||||
throttle_classes = (UserRateThrottle,)
|
throttle_classes = (UserRateThrottle,)
|
||||||
permission_classes = (IsProVersion, IsOrgAdminUser)
|
permission_classes = (IsProVersion, IsOrgAdminUser)
|
||||||
|
|
||||||
def get_info_of_users_order_by_quota_usage(self, org, all_users, direction,
|
def get_info_of_users_order_by_quota_usage(self, org, all_users,
|
||||||
page, per_page):
|
direction, page, per_page):
|
||||||
|
|
||||||
# get user's quota usage info
|
# get user's quota usage info
|
||||||
user_usage_dict = {}
|
user_usage_dict = {}
|
||||||
@@ -145,8 +149,11 @@ class OrgAdminUsers(APIView):
|
|||||||
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = self.get_info_of_users_order_by_quota_usage(org, all_users, direction,
|
data = self.get_info_of_users_order_by_quota_usage(org,
|
||||||
current_page, per_page)
|
all_users,
|
||||||
|
direction,
|
||||||
|
current_page,
|
||||||
|
per_page)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
error_msg = 'Internal Server Error'
|
error_msg = 'Internal Server Error'
|
||||||
@@ -201,7 +208,6 @@ class OrgAdminUsers(APIView):
|
|||||||
'page_next': page_next
|
'page_next': page_next
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
def post(self, request, org_id):
|
def post(self, request, org_id):
|
||||||
"""Added an organization user, check member quota before adding.
|
"""Added an organization user, check member quota before adding.
|
||||||
"""
|
"""
|
||||||
@@ -291,6 +297,7 @@ class OrgAdminUsers(APIView):
|
|||||||
|
|
||||||
user_info['quota_usage'] = user_info['self_usage']
|
user_info['quota_usage'] = user_info['self_usage']
|
||||||
user_info['quota_total'] = user_info['quota']
|
user_info['quota_total'] = user_info['quota']
|
||||||
|
user_info['create_time'] = user_info['ctime']
|
||||||
|
|
||||||
return Response(user_info)
|
return Response(user_info)
|
||||||
|
|
||||||
@@ -589,3 +596,161 @@ class OrgAdminSearchUser(APIView):
|
|||||||
user_list.append(user_info)
|
user_list.append(user_info)
|
||||||
|
|
||||||
return Response({'user_list': user_list})
|
return Response({'user_list': user_list})
|
||||||
|
|
||||||
|
|
||||||
|
class OrgAdminImportUsers(APIView):
|
||||||
|
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||||
|
throttle_classes = (UserRateThrottle,)
|
||||||
|
permission_classes = (IsProVersion, IsOrgAdminUser)
|
||||||
|
|
||||||
|
def post(self, request, org_id):
|
||||||
|
""" Import users from xlsx file
|
||||||
|
|
||||||
|
Permission checking:
|
||||||
|
1. admin user.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# resource check
|
||||||
|
org_id = int(org_id)
|
||||||
|
if not ccnet_api.get_org_by_id(org_id):
|
||||||
|
error_msg = 'Organization %s not found.' % org_id
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
|
# check plan
|
||||||
|
url_prefix = request.user.org.url_prefix
|
||||||
|
org_members = len(ccnet_api.get_org_users_by_url_prefix(url_prefix, -1, -1))
|
||||||
|
|
||||||
|
if ORG_MEMBER_QUOTA_ENABLED:
|
||||||
|
from seahub.organizations.models import OrgMemberQuota
|
||||||
|
org_members_quota = OrgMemberQuota.objects.get_quota(request.user.org.org_id)
|
||||||
|
if org_members_quota is not None and org_members >= org_members_quota:
|
||||||
|
err_msg = 'Failed. You can only invite %d members.' % org_members_quota
|
||||||
|
return api_error(status.HTTP_403_FORBIDDEN, err_msg)
|
||||||
|
|
||||||
|
xlsx_file = request.FILES.get('file', None)
|
||||||
|
if not xlsx_file:
|
||||||
|
error_msg = 'file can not be found.'
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
|
file_type, ext = get_file_type_and_ext(xlsx_file.name)
|
||||||
|
if ext != 'xlsx':
|
||||||
|
error_msg = file_type_error_msg(ext, 'xlsx')
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
|
content = xlsx_file.read()
|
||||||
|
|
||||||
|
try:
|
||||||
|
fs = BytesIO(content)
|
||||||
|
wb = load_workbook(filename=fs, read_only=True)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
|
||||||
|
# example file is like:
|
||||||
|
# Email Password Name(Optional) Space Quota(MB, Optional)
|
||||||
|
# a@a.com a a 1024
|
||||||
|
# b@b.com b b 2048
|
||||||
|
|
||||||
|
rows = wb.worksheets[0].rows
|
||||||
|
records = []
|
||||||
|
|
||||||
|
# skip first row(head field).
|
||||||
|
next(rows)
|
||||||
|
for row in rows:
|
||||||
|
if not all(col.value is None for col in row):
|
||||||
|
records.append([col.value for col in row])
|
||||||
|
|
||||||
|
if user_number_over_limit(new_users=len(records)):
|
||||||
|
error_msg = 'The number of users exceeds the limit.'
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
result['failed'] = []
|
||||||
|
result['success'] = []
|
||||||
|
for record in records:
|
||||||
|
if record[0]:
|
||||||
|
email = record[0].strip()
|
||||||
|
if not is_valid_email(email):
|
||||||
|
result['failed'].append({
|
||||||
|
'email': email,
|
||||||
|
'error_msg': 'email %s invalid.' % email
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
result['failed'].append({
|
||||||
|
'email': '',
|
||||||
|
'error_msg': 'email invalid.'
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not record[1] or not record[1].strip():
|
||||||
|
result['failed'].append({
|
||||||
|
'email': email,
|
||||||
|
'error_msg': 'password invalid.'
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
password = record[1].strip()
|
||||||
|
|
||||||
|
try:
|
||||||
|
User.objects.get(email=email)
|
||||||
|
result['failed'].append({
|
||||||
|
'email': email,
|
||||||
|
'error_msg': 'user %s exists.' % email
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
except User.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
User.objects.create_user(email, password, is_staff=False, is_active=True)
|
||||||
|
set_org_user(org_id, email)
|
||||||
|
|
||||||
|
if IS_EMAIL_CONFIGURED:
|
||||||
|
if SEND_EMAIL_ON_ADDING_SYSTEM_MEMBER:
|
||||||
|
try:
|
||||||
|
send_user_add_mail(request, email, password)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(str(e))
|
||||||
|
|
||||||
|
# update the user's optional info
|
||||||
|
# update nikename
|
||||||
|
if record[2]:
|
||||||
|
try:
|
||||||
|
nickname = record[2].strip()
|
||||||
|
if len(nickname) <= 64 and '/' not in nickname:
|
||||||
|
Profile.objects.add_or_update(email, nickname, '')
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
|
||||||
|
# update quota
|
||||||
|
if record[3]:
|
||||||
|
try:
|
||||||
|
space_quota_mb = int(record[3])
|
||||||
|
if space_quota_mb >= 0:
|
||||||
|
space_quota = int(space_quota_mb) * get_file_size_unit('MB')
|
||||||
|
seafile_api.set_org_user_quota(org_id, email, space_quota)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
|
||||||
|
user = User.objects.get(email=email)
|
||||||
|
|
||||||
|
info = {}
|
||||||
|
info['email'] = email
|
||||||
|
info['name'] = email2nickname(email)
|
||||||
|
info['contact_email'] = email2contact_email(email)
|
||||||
|
|
||||||
|
info['is_staff'] = user.is_staff
|
||||||
|
info['is_active'] = user.is_active
|
||||||
|
|
||||||
|
info['quota_usage'] = 0
|
||||||
|
try:
|
||||||
|
info['quota_total'] = get_org_user_quota(org_id, email)
|
||||||
|
except SearpcError as e:
|
||||||
|
logger.error(e)
|
||||||
|
info['quota_total'] = -1
|
||||||
|
|
||||||
|
info['create_time'] = timestamp_to_isoformat_timestr(user.ctime)
|
||||||
|
info['last_login'] = None
|
||||||
|
|
||||||
|
result['success'].append(info)
|
||||||
|
|
||||||
|
return Response(result)
|
||||||
|
@@ -10,7 +10,8 @@ from .api.group_owned_libraries import (
|
|||||||
AdminGroupOwnedLibraries, AdminGroupOwnedLibrary
|
AdminGroupOwnedLibraries, AdminGroupOwnedLibrary
|
||||||
)
|
)
|
||||||
from .api.group_members import AdminGroupMembers, AdminGroupMember
|
from .api.group_members import AdminGroupMembers, AdminGroupMember
|
||||||
from .api.admin.users import OrgAdminUser, OrgAdminUsers, OrgAdminSearchUser
|
from .api.admin.users import OrgAdminUser, OrgAdminUsers, OrgAdminSearchUser, \
|
||||||
|
OrgAdminImportUsers
|
||||||
from .api.admin.user_set_password import OrgAdminUserSetPassword
|
from .api.admin.user_set_password import OrgAdminUserSetPassword
|
||||||
from .api.admin.groups import OrgAdminGroups, OrgAdminGroup, OrgAdminSearchGroup
|
from .api.admin.groups import OrgAdminGroups, OrgAdminGroup, OrgAdminSearchGroup
|
||||||
from .api.admin.repos import OrgAdminRepos, OrgAdminRepo
|
from .api.admin.repos import OrgAdminRepos, OrgAdminRepo
|
||||||
@@ -19,7 +20,38 @@ from .api.admin.links import OrgAdminLinks, OrgAdminLink
|
|||||||
from .api.admin.logs import OrgAdminLogsFileAccess, OrgAdminLogsFileUpdate, OrgAdminLogsPermAudit
|
from .api.admin.logs import OrgAdminLogsFileAccess, OrgAdminLogsFileUpdate, OrgAdminLogsPermAudit
|
||||||
from .api.admin.user_repos import OrgAdminUserRepos, OrgAdminUserBesharedRepos
|
from .api.admin.user_repos import OrgAdminUserRepos, OrgAdminUserBesharedRepos
|
||||||
|
|
||||||
|
from .api.admin.devices import OrgAdminDevices, OrgAdminDevicesErrors
|
||||||
|
|
||||||
|
from .api.admin.statistics import OrgFileOperationsView, OrgTotalStorageView, \
|
||||||
|
OrgActiveUsersView, OrgSystemTrafficView, OrgUserTrafficView, \
|
||||||
|
OrgUserTrafficExcelView, OrgUserStorageExcelView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
url(r'^(?P<org_id>\d+)/admin/statistics/file-operations/$',
|
||||||
|
OrgFileOperationsView.as_view(),
|
||||||
|
name='api-v2.1-org-admin-statistics-file-operations'),
|
||||||
|
url(r'^(?P<org_id>\d+)/admin/statistics/total-storage/$',
|
||||||
|
OrgTotalStorageView.as_view(),
|
||||||
|
name='api-v2.1-org-admin-statistics-total-storage'),
|
||||||
|
url(r'^(?P<org_id>\d+)/admin/statistics/active-users/$',
|
||||||
|
OrgActiveUsersView.as_view(),
|
||||||
|
name='api-v2.1-org-admin-statistics-active-users'),
|
||||||
|
url(r'^(?P<org_id>\d+)/admin/statistics/system-traffic/$',
|
||||||
|
OrgSystemTrafficView.as_view(),
|
||||||
|
name='api-v2.1-org-admin-statistics-system-traffic'),
|
||||||
|
url(r'^(?P<org_id>\d+)/admin/statistics/user-traffic/$',
|
||||||
|
OrgUserTrafficView.as_view(),
|
||||||
|
name='api-v2.1-org-admin-statistics-user-traffic'),
|
||||||
|
url(r'^(?P<org_id>\d+)/admin/statistics/user-traffic/excel/$',
|
||||||
|
OrgUserTrafficExcelView.as_view(),
|
||||||
|
name='api-v2.1-org-admin-statistics-user-traffic-excel'),
|
||||||
|
url(r'^(?P<org_id>\d+)/admin/statistics/user-storage/excel/$',
|
||||||
|
OrgUserStorageExcelView.as_view(),
|
||||||
|
name='api-v2.1-org-admin-statistics-user-storage-excel'),
|
||||||
|
|
||||||
|
url(r'^(?P<org_id>\d+)/admin/devices/$', OrgAdminDevices.as_view(), name='api-v2.1-org-admin-devices'),
|
||||||
|
url(r'^(?P<org_id>\d+)/admin/devices-errors/$', OrgAdminDevicesErrors.as_view(), name='api-v2.1-org-admin-devices-errors'),
|
||||||
|
|
||||||
url(r'^(?P<org_id>\d+)/admin/address-book/groups/$', AdminAddressBookGroups.as_view(), name='api-admin-address-book-groups'),
|
url(r'^(?P<org_id>\d+)/admin/address-book/groups/$', AdminAddressBookGroups.as_view(), name='api-admin-address-book-groups'),
|
||||||
url(r'^(?P<org_id>\d+)/admin/address-book/groups/(?P<group_id>\d+)/$', AdminAddressBookGroup.as_view(), name='api-admin-address-book-group'),
|
url(r'^(?P<org_id>\d+)/admin/address-book/groups/(?P<group_id>\d+)/$', AdminAddressBookGroup.as_view(), name='api-admin-address-book-group'),
|
||||||
|
|
||||||
@@ -35,6 +67,7 @@ urlpatterns = [
|
|||||||
url(r'^(?P<org_id>\d+)/admin/groups/(?P<group_id>\d+)/members/$', AdminGroupMembers.as_view(), name='api-admin-group-members'),
|
url(r'^(?P<org_id>\d+)/admin/groups/(?P<group_id>\d+)/members/$', AdminGroupMembers.as_view(), name='api-admin-group-members'),
|
||||||
url(r'^(?P<org_id>\d+)/admin/groups/(?P<group_id>\d+)/members/(?P<email>[^/]+)/$', AdminGroupMember.as_view(), name='api-admin-group-member'),
|
url(r'^(?P<org_id>\d+)/admin/groups/(?P<group_id>\d+)/members/(?P<email>[^/]+)/$', AdminGroupMember.as_view(), name='api-admin-group-member'),
|
||||||
url(r'^(?P<org_id>\d+)/admin/users/$', OrgAdminUsers.as_view(), name='api-v2.1-org-admin-users'),
|
url(r'^(?P<org_id>\d+)/admin/users/$', OrgAdminUsers.as_view(), name='api-v2.1-org-admin-users'),
|
||||||
|
url(r'^(?P<org_id>\d+)/admin/import-users/$', OrgAdminImportUsers.as_view(), name='api-v2.1-org-admin-import-users'),
|
||||||
url(r'^(?P<org_id>\d+)/admin/search-user/$', OrgAdminSearchUser.as_view(), name='api-v2.1-org-admin-search-user'),
|
url(r'^(?P<org_id>\d+)/admin/search-user/$', OrgAdminSearchUser.as_view(), name='api-v2.1-org-admin-search-user'),
|
||||||
url(r'^(?P<org_id>\d+)/admin/users/(?P<email>[^/]+)/$', OrgAdminUser.as_view(), name='api-v2.1-org-admin-user'),
|
url(r'^(?P<org_id>\d+)/admin/users/(?P<email>[^/]+)/$', OrgAdminUser.as_view(), name='api-v2.1-org-admin-user'),
|
||||||
url(r'^(?P<org_id>\d+)/admin/users/(?P<email>[^/]+)/set-password/', OrgAdminUserSetPassword.as_view(), name='api-v2.1-org-admin-user-reset-password'),
|
url(r'^(?P<org_id>\d+)/admin/users/(?P<email>[^/]+)/set-password/', OrgAdminUserSetPassword.as_view(), name='api-v2.1-org-admin-user-reset-password'),
|
||||||
|
@@ -7,6 +7,15 @@ urlpatterns = [
|
|||||||
url(r'^add/$', org_add, name='org_add'),
|
url(r'^add/$', org_add, name='org_add'),
|
||||||
url(r'^register/$', org_register, name='org_register'),
|
url(r'^register/$', org_register, name='org_register'),
|
||||||
|
|
||||||
|
url(r'^statistics-admin/file/$', react_fake_view, name='org_statistics_admin_file'),
|
||||||
|
url(r'^statistics-admin/total-storage/$', react_fake_view, name='org_statistics_admin_total_storage'),
|
||||||
|
url(r'^statistics-admin/active-users/$', react_fake_view, name='org_statistics_admin_active_users'),
|
||||||
|
url(r'^statistics-admin/traffic/$', react_fake_view, name='org_statistics_admin_traffic'),
|
||||||
|
|
||||||
|
url(r'^deviceadmin/desktop-devices/$', react_fake_view, name='org_device_admin'),
|
||||||
|
url(r'^deviceadmin/mobile-devices/$', react_fake_view, name='org_device_admin_mobile_devices'),
|
||||||
|
url(r'^deviceadmin/devices-errors/$', react_fake_view, name='org_device_admin_devices_errors'),
|
||||||
|
|
||||||
url(r'^useradmin/$', react_fake_view, name='org_user_admin'),
|
url(r'^useradmin/$', react_fake_view, name='org_user_admin'),
|
||||||
url(r'^useradmin/search-users/$', react_fake_view, name='org_user_admin_search_users'),
|
url(r'^useradmin/search-users/$', react_fake_view, name='org_user_admin_search_users'),
|
||||||
url(r'^useradmin/admins/$', react_fake_view, name='org_useradmin_admins'),
|
url(r'^useradmin/admins/$', react_fake_view, name='org_useradmin_admins'),
|
||||||
@@ -25,7 +34,7 @@ urlpatterns = [
|
|||||||
url(r'^logadmin/file-update/$', react_fake_view, name='org_log_file_update'),
|
url(r'^logadmin/file-update/$', react_fake_view, name='org_log_file_update'),
|
||||||
url(r'^logadmin/perm-audit/$', react_fake_view, name='org_log_perm_audit'),
|
url(r'^logadmin/perm-audit/$', react_fake_view, name='org_log_perm_audit'),
|
||||||
|
|
||||||
url(r'^orgmanage/$', react_fake_view, name='org_manage'),
|
url(r'^info/$', react_fake_view, name='org_info'),
|
||||||
url(r'^departmentadmin/$', react_fake_view, name='org_department_admin'),
|
url(r'^departmentadmin/$', react_fake_view, name='org_department_admin'),
|
||||||
url(r'^departmentadmin/groups/(?P<group_id>\d+)/', react_fake_view, name='org_department_admin'),
|
url(r'^departmentadmin/groups/(?P<group_id>\d+)/', react_fake_view, name='org_department_admin'),
|
||||||
url(r'^associate/(?P<token>.+)/$', org_associate, name='org_associate'),
|
url(r'^associate/(?P<token>.+)/$', org_associate, name='org_associate'),
|
||||||
|
@@ -703,6 +703,12 @@ if EVENTS_CONFIG_FILE:
|
|||||||
res = seafevents.get_user_activity_stats_by_day(session, start, end, offset)
|
res = seafevents.get_user_activity_stats_by_day(session, start, end, offset)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
def get_org_user_activity_stats_by_day(org_id, start, end):
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
res = seafevents_api.get_org_user_activity_stats_by_day(org_id, start, end)
|
||||||
|
return res
|
||||||
|
|
||||||
def get_org_user_events(org_id, username, start, count):
|
def get_org_user_events(org_id, username, start, count):
|
||||||
return _get_events(username, start, count, org_id=org_id)
|
return _get_events(username, start, count, org_id=org_id)
|
||||||
|
|
||||||
@@ -773,6 +779,12 @@ if EVENTS_CONFIG_FILE:
|
|||||||
res = seafevents.get_file_ops_stats_by_day(session, start, end, offset)
|
res = seafevents.get_file_ops_stats_by_day(session, start, end, offset)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
def get_org_file_ops_stats_by_day(org_id, start, end, offset):
|
||||||
|
""" return file audit record of sepcifiy time group by day.
|
||||||
|
"""
|
||||||
|
res = seafevents_api.get_org_file_ops_stats_by_day(org_id, start, end, offset)
|
||||||
|
return res
|
||||||
|
|
||||||
def get_total_storage_stats_by_day(start, end, offset):
|
def get_total_storage_stats_by_day(start, end, offset):
|
||||||
"""
|
"""
|
||||||
"""
|
"""
|
||||||
@@ -780,6 +792,12 @@ if EVENTS_CONFIG_FILE:
|
|||||||
res = seafevents.get_total_storage_stats_by_day(session, start, end, offset)
|
res = seafevents.get_total_storage_stats_by_day(session, start, end, offset)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
def get_org_total_storage_stats_by_day(org_id, start, end, offset):
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
res = seafevents_api.get_org_storage_stats_by_day(org_id, start, end, offset)
|
||||||
|
return res
|
||||||
|
|
||||||
def get_system_traffic_by_day(start, end, offset, op_type='all'):
|
def get_system_traffic_by_day(start, end, offset, op_type='all'):
|
||||||
with _get_seafevents_session() as session:
|
with _get_seafevents_session() as session:
|
||||||
res = seafevents.get_system_traffic_by_day(session, start, end, offset, op_type)
|
res = seafevents.get_system_traffic_by_day(session, start, end, offset, op_type)
|
||||||
@@ -848,6 +866,8 @@ else:
|
|||||||
pass
|
pass
|
||||||
def get_user_activity_stats_by_day():
|
def get_user_activity_stats_by_day():
|
||||||
pass
|
pass
|
||||||
|
def get_org_user_activity_stats_by_day():
|
||||||
|
pass
|
||||||
def get_log_events_by_time():
|
def get_log_events_by_time():
|
||||||
pass
|
pass
|
||||||
def get_org_user_events():
|
def get_org_user_events():
|
||||||
@@ -864,10 +884,16 @@ else:
|
|||||||
pass
|
pass
|
||||||
def get_file_ops_stats_by_day():
|
def get_file_ops_stats_by_day():
|
||||||
pass
|
pass
|
||||||
|
def get_org_file_ops_stats_by_day():
|
||||||
|
pass
|
||||||
def get_total_storage_stats_by_day():
|
def get_total_storage_stats_by_day():
|
||||||
pass
|
pass
|
||||||
|
def get_org_total_storage_stats_by_day():
|
||||||
|
pass
|
||||||
def get_system_traffic_by_day():
|
def get_system_traffic_by_day():
|
||||||
pass
|
pass
|
||||||
|
def get_org_system_traffic_by_day():
|
||||||
|
pass
|
||||||
def get_org_traffic_by_day():
|
def get_org_traffic_by_day():
|
||||||
pass
|
pass
|
||||||
def get_file_update_events():
|
def get_file_update_events():
|
||||||
|
@@ -50,7 +50,7 @@ from seahub.utils import IS_EMAIL_CONFIGURED, string2list, is_valid_username, \
|
|||||||
is_pro_version, send_html_email, \
|
is_pro_version, send_html_email, \
|
||||||
get_server_id, delete_virus_file, get_virus_file_by_vid, \
|
get_server_id, delete_virus_file, get_virus_file_by_vid, \
|
||||||
get_virus_files, FILE_AUDIT_ENABLED, get_max_upload_file_size, \
|
get_virus_files, FILE_AUDIT_ENABLED, get_max_upload_file_size, \
|
||||||
get_site_name, seafevents_api
|
get_site_name, seafevents_api, is_org_context
|
||||||
from seahub.utils.ip import get_remote_ip
|
from seahub.utils.ip import get_remote_ip
|
||||||
from seahub.utils.file_size import get_file_size_unit
|
from seahub.utils.file_size import get_file_size_unit
|
||||||
from seahub.utils.ldap import get_ldap_info
|
from seahub.utils.ldap import get_ldap_info
|
||||||
@@ -779,7 +779,6 @@ def batch_user_make_admin(request):
|
|||||||
return HttpResponse(json.dumps({'success': True,}), content_type=content_type)
|
return HttpResponse(json.dumps({'success': True,}), content_type=content_type)
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@sys_staff_required
|
|
||||||
def batch_add_user_example(request):
|
def batch_add_user_example(request):
|
||||||
""" get example file.
|
""" get example file.
|
||||||
"""
|
"""
|
||||||
@@ -787,6 +786,7 @@ def batch_add_user_example(request):
|
|||||||
if not next_page:
|
if not next_page:
|
||||||
next_page = SITE_ROOT
|
next_page = SITE_ROOT
|
||||||
data_list = []
|
data_list = []
|
||||||
|
if not is_org_context(request):
|
||||||
head = [_('Email'),
|
head = [_('Email'),
|
||||||
_('Password'),
|
_('Password'),
|
||||||
_('Name') + '(' + _('Optional') + ')',
|
_('Name') + '(' + _('Optional') + ')',
|
||||||
@@ -801,6 +801,17 @@ def batch_add_user_example(request):
|
|||||||
quota = "1000"
|
quota = "1000"
|
||||||
login_id = "login id " + str(i)
|
login_id = "login id " + str(i)
|
||||||
data_list.append([username, password, name, role, quota, login_id])
|
data_list.append([username, password, name, role, quota, login_id])
|
||||||
|
else:
|
||||||
|
head = [_('Email'),
|
||||||
|
_('Password'),
|
||||||
|
_('Name') + '(' + _('Optional') + ')',
|
||||||
|
_('Space Quota') + '(MB, ' + _('Optional') + ')']
|
||||||
|
for i in range(5):
|
||||||
|
username = "test" + str(i) + "@example.com"
|
||||||
|
password = "123456"
|
||||||
|
name = "test" + str(i)
|
||||||
|
quota = "1000"
|
||||||
|
data_list.append([username, password, name, quota])
|
||||||
|
|
||||||
wb = write_xls('sample', head, data_list)
|
wb = write_xls('sample', head, data_list)
|
||||||
if not wb:
|
if not wb:
|
||||||
|
Reference in New Issue
Block a user