1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-17 07:41:26 +00:00

System management statistics function reconstruction (#4243)

* System management statistics function reconstruction

* translation update

* optimizated code

* Replace date component

* Modify component language

* add sort function

* optimizated code

* optimizated code
This commit is contained in:
zxj96
2019-11-13 17:13:48 +08:00
committed by Daniel Pan
parent 3791a4653e
commit fe0940e4c4
21 changed files with 1636 additions and 15 deletions

View File

@@ -0,0 +1,111 @@
.system-statistic-time-range {
margin: 15px 0 25px;
display: flex;
align-items: center;
font-size: 13px;
}
.sys-stat-tool {
display: flex;
font-size: 13px;
color: #333;
}
.system-statistic-item {
border: 1px solid #c5c5c5;
padding: 5px 10px;
cursor: pointer;
}
.system-statistic-item:hover {
background: #efefef;
}
.sys-stat-tool .item-active {
background: #efefef;
}
.system-statistic-input-container {
display: flex;
margin-left: 1rem;
align-items: center;
}
.system-statistic-input {
height: 31px;
width: 120px;
}
.error-tip {
color: red;
}
.statistic-traffic-tab {
display: flex;
font-size: 13px;
color: #333;
margin-top: 10px;
}
.statistic-traffic-tab .statistic-traffic-tab-item {
margin-right: 10px;
padding: 3px 0;
border-bottom: 2px solid transparent;
cursor: pointer;
color: #8a948f;
}
.statistic-traffic-tab .statistic-traffic-tab-item:hover {
color: #eb8025;
border-bottom: 2px solid #eb8025;
}
.statistic-traffic-tab .statistic-traffic-tab-item.active {
color: #eb8025;
border-bottom: 2px solid #eb8025;
}
.statistic-reports-title {
background: #f7f7f7;
margin-top: 15px;
color: #222222;
padding: 3px 10px;
}
.statistic-reports-submit {
margin-left: 15px;
}
.statistic-reports-wrapper {
display: flex;
align-items: center;
margin-top: 15px;
}
.statistic-reports-input {
width: 80px;
height: 30px;
}
.statistic-reports-tip {
padding: 0 10px;
font-size: 13px;
}
.system-statistic-connect {
padding: 0 5px;
line-height: 1;
}
.system-statistic-button {
height: 31px;
margin-left: 1rem;
}
.rc-calendar table {
table-layout: initial;
}
.rc-calendar tbody tr {
height: 1.75rem;
}

View File

@@ -68,6 +68,11 @@ import VirusScanRecords from './virus-scan-records';
import WorkWeixinDepartments from './work-weixin-departments';
import Invitations from './invitations/invitations';
import StatisticFile from './statistic/statistic-file';
import StatisticStorage from './statistic/statistic-storage';
import StatisticTraffic from './statistic/statistic-traffic';
import StatisticUsers from './statistic/statistic-users';
import StatisticReport from './statistic/statistic-reports';
import '../../assets/css/fa-solid.css';
import '../../assets/css/fa-regular.css';
@@ -97,6 +102,10 @@ class SysAdmin extends React.Component {
tab: 'libraries',
urlPartList: ['all-libraries', 'search-libraries', 'system-library', 'trash-libraries', 'libraries/']
},
{
tab: 'statistic',
urlPartList: ['statistics/file', 'statistics/storage', 'statistics/user', 'statistics/traffic', 'statistics/reports']
},
{
tab: 'users',
urlPartList: ['users/']
@@ -152,6 +161,11 @@ class SysAdmin extends React.Component {
<MainPanel>
<Router className="reach-router">
<Info path={siteRoot + 'sys/info'} />
<StatisticFile path={siteRoot + 'sys/statistics/file'} />
<StatisticStorage path={siteRoot + 'sys/statistics/storage'} />
<StatisticUsers path={siteRoot + 'sys/statistics/user'} />
<StatisticTraffic path={siteRoot + 'sys/statistics/traffic'} />
<StatisticReport path={siteRoot + 'sys/statistics/reports'} />
<DesktopDevices path={siteRoot + 'sys/desktop-devices'} />
<MobileDevices path={siteRoot + 'sys/mobile-devices'} />
<DeviceErrors path={siteRoot + 'sys/device-errors'} />

View File

@@ -46,10 +46,13 @@ class SidePanel extends React.Component {
}
{isPro && canViewStatistic &&
<li className="nav-item">
<a className='nav-link ellipsis' href={siteRoot + 'sys/statistic/file/'}>
<Link className={`nav-link ellipsis ${this.getActiveClass('statistic')}`}
to={siteRoot + 'sys/statistics/file/'}
onClick={() => this.props.tabItemClick('statistic')}
>
<span className="sf2-icon-histogram" aria-hidden="true"></span>
<span className="nav-text">{gettext('Statistic')}</span>
</a>
</Link>
</li>
}
{isDefaultAdmin &&

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,103 @@
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 } from '../../../utils/constants';
import { Utils } from '../../../utils/utils';
import toaster from '../../../components/toast';
import '../../../css/system-stat.css';
class StatisticFile extends React.Component {
constructor(props) {
super(props);
this.state = {
filesData: [],
labels: [],
isLoading: true
};
}
getActiviesFiles = (startTime, endTime, groupBy) => {
let { filesData } = this.state;
seafileAPI.sysAdminStatisticFiles(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 StatisticFile;

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/file', text: gettext('File')},
{name: 'storageStatistic', urlPart: 'statistics/storage', text: gettext('Storage')},
{name: 'usersStatistic', urlPart: 'statistics/user', text: gettext('Users')},
{name: 'trafficStatistic', urlPart: 'statistics/traffic', text: gettext('Traffic')},
{name: 'reportsStatistic', urlPart: 'statistics/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}sys/${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 } from '../../../utils/constants';
class StatisticReports 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/admin/statistics/';
let { month } = this.state;
if (!month) {
let errorMessage = gettext('Required field');
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 += 'system-user-traffic/excel/?month=' + month;
break;
case 'storage':
url += 'system-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 StatisticReports;

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 } from '../../../utils/constants';
import { Utils } from '../../../utils/utils';
import toaster from '../../../components/toast';
class StatisticStorage extends React.Component {
constructor(props) {
super(props);
this.state = {
filesData: [],
labels: [],
isLoading: true
};
}
getActiviesFiles = (startTime, endTime, groupBy) => {
let { filesData } = this.state;
seafileAPI.sysAdminStatisticStorages(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 space'),
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 StatisticStorage;

View File

@@ -0,0 +1,219 @@
import React, { Fragment } from 'react';
import moment from 'moment';
import { gettext } 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 TrafficOrganizationsTable from './traffic-organizations-table';
import TrafficUserTable from './traffic-user-table';
import StatisticChart from './statistic-chart';
import { Utils } from '../../../utils/utils';
import toaster from '../../../components/toast';
class StatisticTraffic 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.sysAdminStatisticTraffic(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 className={`statistic-traffic-tab-item ${tabActive === 'organizations' ? 'active' : ''}`} onClick={this.changeTabActive.bind(this, 'organizations')}>{gettext('Organizations')}</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 className={`statistic-traffic-tab-item ${tabActive === 'organizations' ? 'active' : ''}`} onClick={this.changeTabActive.bind(this, 'organizations')}>{gettext('Organizations')}</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' &&
<TrafficUserTable />
}
{!isLoading && tabActive === 'organizations' &&
<TrafficOrganizationsTable />
}
</div>
</div>
</Fragment>
);
}
}
export default StatisticTraffic;

View File

@@ -0,0 +1,79 @@
import React, { Fragment } from 'react';
import moment from 'moment';
import { gettext } 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 StatisticUsers extends React.Component {
constructor(props) {
super(props);
this.state = {
filesData: [],
labels: [],
isLoading: true,
};
}
getActiviesFiles = (startTime, endTime, groupBy) => {
let { filesData } = this.state;
seafileAPI.sysAdminStatisticActiveUsers(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 StatisticUsers;

View File

@@ -0,0 +1,140 @@
import React, { Fragment } from 'react';
import { Input } from 'reactstrap';
import moment from 'moment';
import { gettext } from '../../../utils/constants';
import TrafficTable from './traffic-table';
import TrafficTableBody from './traffic-table-body';
import { seafileAPI } from '../../../utils/seafile-api';
import Paginator from '../../../components/paginator';
import Loading from '../../../components/loading';
import { Utils } from '../../../utils/utils';
import toaster from '../../../components/toast';
class TrafficOrganizationsTable extends React.Component {
constructor(props) {
super(props);
this.state = {
userTrafficList: [],
perPage: 100,
currentPage: 1,
hasNextPage: false,
month: moment().format('YYYYMM'),
isLoading: false,
errorMessage: '',
sortOrder: 'asc'
};
this.initPage = 1;
this.initMonth = moment().format('YYYYMM');
}
componentDidMount() {
this.onGenerateReports(this.initMonth, this.initPage);
}
getPreviousPage = () => {
this.onGenerateReports(this.state.currentPage - 1);
}
getNextPage = () => {
this.onGenerateReports(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.onGenerateReports(month, this.initPage);
e.target.blur();
e.preventDefault();
}
}
sortBySize = (sortByType, sortOrder) => {
let { userTrafficList } = this.state;
let newUserTrafficList = Utils.sortTraffic(userTrafficList, sortByType, sortOrder);
this.setState({
userTrafficList: newUserTrafficList,
sortOrder: sortOrder
})
}
onGenerateReports = (month, page) => {
let { perPage } = this.state;
this.setState({isLoading: true, errorMessage: ''});
seafileAPI.sysAdminListOrgTraffic(month, page, perPage).then(res => {
let userTrafficList = res.data.org_monthly_traffic_list.slice(0);
this.setState({
userTrafficList: userTrafficList,
hasNextPage: res.data.has_next_page,
isLoading: false
});
}).catch(err => {
let errMessage = Utils.getErrorMsg(err);
toaster.danger(errMessage);
});
}
resetPerPage = (newPerPage) => {
this.setState({
perPage: newPerPage,
}, () => this.onGenerateReports(this.initPage, this.initMonth));
}
render() {
let { userTrafficList, currentPage, hasNextPage, perPage, isLoading, errorMessage, 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={'org'} sortOrder={sortOrder} sortBySize={this.sortBySize} >
{userTrafficList.length > 0 && userTrafficList.map((item, index) => {
return(
<TrafficTableBody
key={index}
userTrafficItem={item}
type={'org'}
/>
);
})}
</TrafficTable>
}
<Paginator
gotoPreviousPage={this.getPreviousPage}
gotoNextPage={this.getNextPage}
currentPage={currentPage}
hasNextPage={hasNextPage}
canResetPerPage={true}
curPerPage={perPage}
resetPerPage={this.resetPerPage}
/>
</Fragment>
);
}
}
export default TrafficOrganizationsTable;

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,80 @@
import React from 'react';
import PropTypes from 'prop-types';
import { gettext } from '../../../utils/constants';
const propTypes = {
type: PropTypes.string.isRequired,
children: PropTypes.oneOfType([PropTypes.bool, PropTypes.array]),
};
class TrafficTable extends React.Component {
constructor(props) {
super(props);
this.state = {
showIconName: 'link_file_download'
}
}
componentDidMount() {
let { showIconName } = this.state;
let { sortOrder } = this.props;
this.props.sortBySize(showIconName, sortOrder);
}
getTrafficTypeName = () => {
let { type } = this.props;
let trafficTypeName;
switch(type) {
case 'user':
trafficTypeName = 'User';
break;
case 'org':
trafficTypeName = 'Org';
break;
}
return trafficTypeName;
}
sortBySize = (sortByType) => {
let { sortOrder } = this.props;
let newSortOrder = sortOrder === 'asc' ? 'desc' : 'asc';
this.setState({
showIconName: sortByType
})
this.props.sortBySize(sortByType, newSortOrder);
}
render() {
let { sortOrder } = this.props;
let { showIconName } = this.state;
let trafficTypeName = this.getTrafficTypeName();
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%">{gettext('{trafficTypeName}').replace('{trafficTypeName}', trafficTypeName)}</th>
<th width="11%"><div className="d-block table-sort-op cursor-pointer" onClick={this.sortBySize.bind(this, 'sync_file_upload')}>{gettext('Sync Upload')} {showIconName === 'sync_file_upload' && sortIcon}</div></th>
<th width="14%"><div className="d-block table-sort-op cursor-pointer" onClick={this.sortBySize.bind(this, 'sync_file_donwload')}>{gettext('Sync Download')} {showIconName === 'sync_file_donwload' && sortIcon}</div></th>
<th width="11%"><div className="d-block table-sort-op cursor-pointer" onClick={this.sortBySize.bind(this, 'web_file_upload')}>{gettext('Web Upload')} {showIconName === 'web_file_upload' && sortIcon}</div></th>
<th width="14%"><div className="d-block table-sort-op cursor-pointer" onClick={this.sortBySize.bind(this, 'web_file_download')}>{gettext('Web Download')} {showIconName === 'web_file_download' && sortIcon}</div></th>
<th width="17%"><div className="d-block table-sort-op cursor-pointer" onClick={this.sortBySize.bind(this, 'link_file_upload')}>{gettext('Link Upload')} {showIconName === 'link_file_upload' && sortIcon}</div></th>
<th width="17%"><div className="d-block table-sort-op cursor-pointer" onClick={this.sortBySize.bind(this, 'link_file_download')}>{gettext('Link Download')} {showIconName === '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,143 @@
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 } from '../../../utils/constants';
import { Utils } from '../../../utils/utils';
import toaster from '../../../components/toast';
class TrafficOrganizationsTable extends React.Component {
constructor(props) {
super(props);
this.state = {
userTrafficList: [],
hasNextPage: false,
perPage: 100,
currentPage: 1,
month: moment().format('YYYYMM'),
isLoading: false,
errorMessage: '',
sortOrder: 'asc'
};
this.initPage = 1;
this.initMonth = moment().format('YYYYMM');
}
componentDidMount() {
this.onGenerateReports(this.initMonth, this.initPage);
}
getPreviousPage = () => {
this.onGenerateReports(this.state.currentPage - 1);
}
getNextPage = () => {
this.onGenerateReports(this.state.currentPage + 1);
}
handleChange = (e) => {
let month = e.target.value;
this.setState({
month: month
});
}
sortBySize = (sortByType, sortOrder) => {
let { userTrafficList } = this.state;
let newUserTrafficList = Utils.sortTraffic(userTrafficList, sortByType, sortOrder);
this.setState({
userTrafficList: newUserTrafficList,
sortOrder: sortOrder
})
}
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.onGenerateReports(month, this.initPage);
e.target.blur();
e.preventDefault();
}
}
onGenerateReports = (month, page) => {
let { perPage } = this.state;
this.setState({
isLoading: true,
errorMessage: ''
});
seafileAPI.sysAdminListUserTraffic(month, page, perPage).then(res => {
let userTrafficList = res.data.user_monthly_traffic_list.slice(0);
this.setState({
userTrafficList: userTrafficList,
hasNextPage: res.data.has_next_page,
isLoading: false
});
}).catch(err => {
let errMessage = Utils.getErrorMsg(err);
toaster.danger(errMessage);
});
}
resetPerPage = (newPerPage) => {
this.setState({
perPage: newPerPage,
}, () => this.onGenerateReports(this.initMonth, this.initPage));
}
render() {
let { userTrafficList, currentPage, hasNextPage, perPage, isLoading, errorMessage, 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'} sortBySize={this.sortBySize} 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}
canResetPerPage={true}
curPerPage={perPage}
resetPerPage={this.resetPerPage}
/>
</Fragment>
);
}
}
export default TrafficOrganizationsTable;

View File

@@ -0,0 +1,51 @@
const zhCN = require('@seafile/seafile-calendar/lib/locale/zh_CN');
const zhTW = require('@seafile/seafile-calendar/lib/locale/zh_TW');
const enUS = require('@seafile/seafile-calendar/lib/locale/en_US');
const frFR = require('@seafile/seafile-calendar/lib/locale/fr_FR');
const deDE = require('@seafile/seafile-calendar/lib/locale/de_DE');
const esES = require('@seafile/seafile-calendar/lib/locale/es_ES');
const plPL = require('@seafile/seafile-calendar/lib/locale/pl_PL');
const csCZ = require('@seafile/seafile-calendar/lib/locale/cs_CZ');
function translateCalendar() {
const locale = window.app.config ? window.app.config.lang : 'zh-CH';
let language;
switch (locale) {
case 'zh-CH':
language = zhCN;
break;
case 'zh-tw':
language = zhTW;
break;
case 'en':
language = enUS;
break;
case 'fr':
language = frFR;
break;
case 'de':
language = deDE;
break;
case 'es':
language = esES;
break;
case 'es-ar':
language = esES;
break;
case 'es-mx':
language = esES;
break;
case 'pl':
language = plPL;
break;
case 'cs':
language = csCZ;
break;
default:
language = zhCN;
}
return language;
}
export { translateCalendar };

View File

@@ -962,6 +962,24 @@ export const Utils = {
return items;
},
sortTraffic(items, sortBy, sortOrder) {
let comparator;
switch(sortOrder) {
case 'asc':
comparator = function(a, b) {
return a[sortBy] < b[sortBy] ? -1 : 1;
};
break;
case 'desc':
comparator = function(a, b) {
return a[sortBy] < b[sortBy] ? 1 : -1;
};
break;
}
items.sort(comparator);
return items;
},
/*
* only used in the 'catch' part of a seafileAPI request
*/