mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-20 02:48:51 +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;
|
Reference in New Issue
Block a user