mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-06 17:33:18 +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) {
|
||||
data = {
|
||||
url: `${siteRoot}org/useradmin/`,
|
||||
url: `${siteRoot}org/info/`,
|
||||
text: gettext('Organization Admin')
|
||||
};
|
||||
} 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 { siteRoot } from '../../utils/constants';
|
||||
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 OrgUsersSearchUsers from './org-users-search-users';
|
||||
import OrgAdmins from './org-users-admins';
|
||||
@@ -44,6 +53,12 @@ class Org extends React.Component {
|
||||
if (location.href.indexOf(`${siteRoot}org/useradmin`) != -1) {
|
||||
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) {
|
||||
currentTab = 'groupadmin';
|
||||
}
|
||||
@@ -68,7 +83,15 @@ class Org extends React.Component {
|
||||
<SidePanel isSidePanelClosed={isSidePanelClosed} onCloseSidePanel={this.onCloseSidePanel} currentTab={currentTab} tabItemClick={this.tabItemClick}/>
|
||||
<div className="main-panel o-hidden">
|
||||
<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'} />
|
||||
<OrgUsersSearchUsers path={siteRoot + 'org/useradmin/search-users'} />
|
||||
<OrgAdmins path={siteRoot + 'org/useradmin/admins/'} />
|
||||
|
@@ -4,6 +4,7 @@ import Nav from './org-users-nav';
|
||||
import OrgUsersList from './org-users-list';
|
||||
import MainPanelTopbar from './main-panel-topbar';
|
||||
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 InviteUserDialog from '../../components/dialog/org-admin-invite-user-dialog';
|
||||
import toaster from '../../components/toast';
|
||||
@@ -72,6 +73,7 @@ class OrgUsers extends Component {
|
||||
sortBy: '',
|
||||
sortOrder: 'asc',
|
||||
isShowAddOrgUserDialog: false,
|
||||
isImportOrgUsersDialogOpen: false,
|
||||
isInviteUserDialogOpen: false
|
||||
};
|
||||
}
|
||||
@@ -110,6 +112,10 @@ class OrgUsers extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
toggleImportOrgUsersDialog = () => {
|
||||
this.setState({isImportOrgUsersDialogOpen: !this.state.isImportOrgUsersDialogOpen});
|
||||
}
|
||||
|
||||
toggleAddOrgUser = () => {
|
||||
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) => {
|
||||
seafileAPI.orgAdminAddOrgUser(orgID, email, name, password).then(res => {
|
||||
let userInfo = new OrgUserInfo(res.data);
|
||||
@@ -198,12 +228,18 @@ class OrgUsers extends Component {
|
||||
let topbarChildren;
|
||||
topbarChildren = (
|
||||
<Fragment>
|
||||
<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>
|
||||
<button className="btn btn-secondary operation-item" onClick={this.toggleImportOrgUsersDialog}>{gettext('Import Users')}</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 &&
|
||||
<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>
|
||||
}
|
||||
{this.state.isImportOrgUsersDialogOpen &&
|
||||
<ModalPortal>
|
||||
<ImportOrgUsersDialog importUsersInBatch={this.importOrgUsers} toggle={this.toggleImportOrgUsersDialog}/>
|
||||
</ModalPortal>
|
||||
}
|
||||
{this.state.isShowAddOrgUserDialog &&
|
||||
<ModalPortal>
|
||||
<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>
|
||||
<ul className="nav nav-pills flex-column nav-container">
|
||||
<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="nav-text">{gettext('Info')}</span>
|
||||
</Link>
|
||||
</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">
|
||||
<Link className={`nav-link ellipsis ${this.getActiveClass('repoadmin')}`} to={siteRoot + 'org/repoadmin/'} onClick={() => this.tabItemClick('repoadmin')} >
|
||||
<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.
|
||||
import logging
|
||||
from io import BytesIO
|
||||
from openpyxl import load_workbook
|
||||
|
||||
from rest_framework import status
|
||||
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.profile.models import Profile
|
||||
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.error_msg import file_type_error_msg
|
||||
from seahub.utils.timeutils import timestamp_to_isoformat_timestr, datetime_to_isoformat_timestr
|
||||
from seahub.utils.licenseparse import user_number_over_limit
|
||||
from seahub.views.sysadmin import send_user_add_mail
|
||||
@@ -44,8 +48,8 @@ class OrgAdminUsers(APIView):
|
||||
throttle_classes = (UserRateThrottle,)
|
||||
permission_classes = (IsProVersion, IsOrgAdminUser)
|
||||
|
||||
def get_info_of_users_order_by_quota_usage(self, org, all_users, direction,
|
||||
page, per_page):
|
||||
def get_info_of_users_order_by_quota_usage(self, org, all_users,
|
||||
direction, page, per_page):
|
||||
|
||||
# get user's quota usage info
|
||||
user_usage_dict = {}
|
||||
@@ -145,8 +149,11 @@ class OrgAdminUsers(APIView):
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||
|
||||
try:
|
||||
data = self.get_info_of_users_order_by_quota_usage(org, all_users, direction,
|
||||
current_page, per_page)
|
||||
data = self.get_info_of_users_order_by_quota_usage(org,
|
||||
all_users,
|
||||
direction,
|
||||
current_page,
|
||||
per_page)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
error_msg = 'Internal Server Error'
|
||||
@@ -201,7 +208,6 @@ class OrgAdminUsers(APIView):
|
||||
'page_next': page_next
|
||||
})
|
||||
|
||||
|
||||
def post(self, request, org_id):
|
||||
"""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_total'] = user_info['quota']
|
||||
user_info['create_time'] = user_info['ctime']
|
||||
|
||||
return Response(user_info)
|
||||
|
||||
@@ -589,3 +596,161 @@ class OrgAdminSearchUser(APIView):
|
||||
user_list.append(user_info)
|
||||
|
||||
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
|
||||
)
|
||||
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.groups import OrgAdminGroups, OrgAdminGroup, OrgAdminSearchGroup
|
||||
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.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 = [
|
||||
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/(?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/(?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/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/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'),
|
||||
|
@@ -7,6 +7,15 @@ urlpatterns = [
|
||||
url(r'^add/$', org_add, name='org_add'),
|
||||
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/search-users/$', react_fake_view, name='org_user_admin_search_users'),
|
||||
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/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/groups/(?P<group_id>\d+)/', react_fake_view, name='org_department_admin'),
|
||||
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)
|
||||
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):
|
||||
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)
|
||||
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):
|
||||
"""
|
||||
"""
|
||||
@@ -780,6 +792,12 @@ if EVENTS_CONFIG_FILE:
|
||||
res = seafevents.get_total_storage_stats_by_day(session, start, end, offset)
|
||||
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'):
|
||||
with _get_seafevents_session() as session:
|
||||
res = seafevents.get_system_traffic_by_day(session, start, end, offset, op_type)
|
||||
@@ -848,6 +866,8 @@ else:
|
||||
pass
|
||||
def get_user_activity_stats_by_day():
|
||||
pass
|
||||
def get_org_user_activity_stats_by_day():
|
||||
pass
|
||||
def get_log_events_by_time():
|
||||
pass
|
||||
def get_org_user_events():
|
||||
@@ -864,10 +884,16 @@ else:
|
||||
pass
|
||||
def get_file_ops_stats_by_day():
|
||||
pass
|
||||
def get_org_file_ops_stats_by_day():
|
||||
pass
|
||||
def get_total_storage_stats_by_day():
|
||||
pass
|
||||
def get_org_total_storage_stats_by_day():
|
||||
pass
|
||||
def get_system_traffic_by_day():
|
||||
pass
|
||||
def get_org_system_traffic_by_day():
|
||||
pass
|
||||
def get_org_traffic_by_day():
|
||||
pass
|
||||
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, \
|
||||
get_server_id, delete_virus_file, get_virus_file_by_vid, \
|
||||
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.file_size import get_file_size_unit
|
||||
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)
|
||||
|
||||
@login_required
|
||||
@sys_staff_required
|
||||
def batch_add_user_example(request):
|
||||
""" get example file.
|
||||
"""
|
||||
@@ -787,6 +786,7 @@ def batch_add_user_example(request):
|
||||
if not next_page:
|
||||
next_page = SITE_ROOT
|
||||
data_list = []
|
||||
if not is_org_context(request):
|
||||
head = [_('Email'),
|
||||
_('Password'),
|
||||
_('Name') + '(' + _('Optional') + ')',
|
||||
@@ -801,6 +801,17 @@ def batch_add_user_example(request):
|
||||
quota = "1000"
|
||||
login_id = "login id " + str(i)
|
||||
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)
|
||||
if not wb:
|
||||
|
Reference in New Issue
Block a user