1
0
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:
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;