1
0
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:
lian
2022-11-10 13:27:55 +08:00
committed by GitHub
parent 53e2e70d8c
commit b0d0874013
30 changed files with 2775 additions and 30 deletions

View File

@@ -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) {

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

View File

@@ -0,0 +1,29 @@
import React, { Component, Fragment } from 'react';
import DevicesNav from './devices-nav';
import DevicesByPlatform from './devices-by-platform';
import MainPanelTopbar from '../main-panel-topbar';
class 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;

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

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

View File

@@ -0,0 +1,41 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Link } from '@reach/router';
import { siteRoot, gettext } from '../../../utils/constants';
const propTypes = {
currentItem: PropTypes.string.isRequired
};
class Nav extends React.Component {
constructor(props) {
super(props);
this.navItems = [
{name: 'desktop', urlPart:'desktop-devices', text: gettext('Desktop')},
{name: 'mobile', urlPart:'mobile-devices', text: gettext('Mobile')},
{name: 'errors', urlPart:'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;

View File

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

View File

@@ -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/'} />

View File

@@ -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}/>

View File

@@ -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>

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

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

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

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

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

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

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

View File

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

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

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

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

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

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

View 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})

View 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

View File

@@ -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)

View File

@@ -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'),

View File

@@ -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'),

View File

@@ -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():

View File

@@ -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: