1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-03 07:55:36 +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

@@ -123,9 +123,9 @@
"integrity": "sha512-DJPhjBRLENONdDNaaKRckWWtwXvoqfJcRdSk01FbjmZ3DWpbIwebd/vfuB4qJwm0R2j2qzdEVhgxe6wDYS/n9A==" "integrity": "sha512-DJPhjBRLENONdDNaaKRckWWtwXvoqfJcRdSk01FbjmZ3DWpbIwebd/vfuB4qJwm0R2j2qzdEVhgxe6wDYS/n9A=="
}, },
"@seafile/seafile-calendar": { "@seafile/seafile-calendar": {
"version": "0.0.6", "version": "0.0.8",
"resolved": "https://registry.npmjs.org/@seafile/seafile-calendar/-/seafile-calendar-0.0.6.tgz", "resolved": "https://registry.npmjs.org/@seafile/seafile-calendar/-/seafile-calendar-0.0.8.tgz",
"integrity": "sha512-lN/KwrmbQGtTdojdry12Z0ovcqqQwTHKBRrZrRuKaT8qEKX8i27y8R/wcV96dwUAOsXTKH2L3/kPgXL8N+G+eg==", "integrity": "sha512-xbFk79mfwOnTd+LGQ42EBFV/uSRNTXa/nm68fcTis6+BmNHcZfmrCEwaq8bPWJTCMm9hw7PP1+J6qQziQMohFg==",
"requires": { "requires": {
"babel-runtime": "6.x", "babel-runtime": "6.x",
"classnames": "2.x", "classnames": "2.x",
@@ -216,6 +216,20 @@
"xtend": "^4.0.1" "xtend": "^4.0.1"
}, },
"dependencies": { "dependencies": {
"@seafile/seafile-calendar": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@seafile/seafile-calendar/-/seafile-calendar-0.0.6.tgz",
"integrity": "sha512-lN/KwrmbQGtTdojdry12Z0ovcqqQwTHKBRrZrRuKaT8qEKX8i27y8R/wcV96dwUAOsXTKH2L3/kPgXL8N+G+eg==",
"requires": {
"babel-runtime": "6.x",
"classnames": "2.x",
"moment": "2.x",
"prop-types": "^15.5.8",
"rc-trigger": "^2.2.0",
"rc-util": "^4.1.1",
"react-lifecycles-compat": "^3.0.4"
}
},
"memoize-one": { "memoize-one": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz",
@@ -2505,6 +2519,42 @@
"resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
"integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=" "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc="
}, },
"chart.js": {
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.9.1.tgz",
"integrity": "sha512-DA5dFt0Bz79oz56ezmrwmZqj0hXGs+i9VbCFOcHqbwrHIGv7RI4YqninJKNIAC0qa29WBI9qYTN7LzULlOeunA==",
"requires": {
"chartjs-color": "^2.1.0",
"moment": "^2.10.2"
}
},
"chartjs-color": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.4.1.tgz",
"integrity": "sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==",
"requires": {
"chartjs-color-string": "^0.6.0",
"color-convert": "^1.9.3"
},
"dependencies": {
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"requires": {
"color-name": "1.1.3"
}
}
}
},
"chartjs-color-string": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz",
"integrity": "sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==",
"requires": {
"color-name": "^1.0.0"
}
},
"chokidar": { "chokidar": {
"version": "1.7.0", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz",
@@ -5470,7 +5520,7 @@
}, },
"git-up": { "git-up": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/git-up/-/git-up-1.2.1.tgz", "resolved": "http://registry.npmjs.org/git-up/-/git-up-1.2.1.tgz",
"integrity": "sha1-JkSAoAax2EJhrB/gmjpRacV+oZ0=", "integrity": "sha1-JkSAoAax2EJhrB/gmjpRacV+oZ0=",
"requires": { "requires": {
"is-ssh": "^1.0.0", "is-ssh": "^1.0.0",
@@ -5479,7 +5529,7 @@
}, },
"git-url-parse": { "git-url-parse": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-5.0.1.tgz", "resolved": "http://registry.npmjs.org/git-url-parse/-/git-url-parse-5.0.1.tgz",
"integrity": "sha1-/j15xnRq4FBIz6UIyB553du6OEM=", "integrity": "sha1-/j15xnRq4FBIz6UIyB553du6OEM=",
"requires": { "requires": {
"git-up": "^1.0.0" "git-up": "^1.0.0"
@@ -8192,7 +8242,7 @@
}, },
"node-status-codes": { "node-status-codes": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-status-codes/-/node-status-codes-1.0.0.tgz", "resolved": "http://registry.npmjs.org/node-status-codes/-/node-status-codes-1.0.0.tgz",
"integrity": "sha1-WuVUHQJGRdMqWPzdyc7s6nrjrC8=" "integrity": "sha1-WuVUHQJGRdMqWPzdyc7s6nrjrC8="
}, },
"noop6": { "noop6": {
@@ -8499,7 +8549,7 @@
}, },
"package.json": { "package.json": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/package.json/-/package.json-2.0.1.tgz", "resolved": "http://registry.npmjs.org/package.json/-/package.json-2.0.1.tgz",
"integrity": "sha1-+IYFnSpJ7QduZIg2ldc7K0bSHW0=", "integrity": "sha1-+IYFnSpJ7QduZIg2ldc7K0bSHW0=",
"requires": { "requires": {
"git-package-json": "^1.4.0", "git-package-json": "^1.4.0",
@@ -8509,7 +8559,7 @@
"dependencies": { "dependencies": {
"got": { "got": {
"version": "5.7.1", "version": "5.7.1",
"resolved": "https://registry.npmjs.org/got/-/got-5.7.1.tgz", "resolved": "http://registry.npmjs.org/got/-/got-5.7.1.tgz",
"integrity": "sha1-X4FjWmHkplifGAVp6k44FoClHzU=", "integrity": "sha1-X4FjWmHkplifGAVp6k44FoClHzU=",
"requires": { "requires": {
"create-error-class": "^3.0.1", "create-error-class": "^3.0.1",
@@ -8531,7 +8581,7 @@
}, },
"package-json": { "package-json": {
"version": "2.4.0", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/package-json/-/package-json-2.4.0.tgz", "resolved": "http://registry.npmjs.org/package-json/-/package-json-2.4.0.tgz",
"integrity": "sha1-DRW9Z9HLvduyyiIv8u24a8sxqLs=", "integrity": "sha1-DRW9Z9HLvduyyiIv8u24a8sxqLs=",
"requires": { "requires": {
"got": "^5.0.0", "got": "^5.0.0",
@@ -10306,6 +10356,15 @@
} }
} }
}, },
"react-chartjs-2": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-2.8.0.tgz",
"integrity": "sha512-BPpC+qfnh37DkcXvxRwA1rdD9rX/0AQrwru4VZTLofCCuZBwRsc7PbfxjilvoB6YlHhorwZu40YDWEQkoz7xfQ==",
"requires": {
"lodash": "^4.17.4",
"prop-types": "^15.5.8"
}
},
"react-codemirror": { "react-codemirror": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/react-codemirror/-/react-codemirror-1.0.0.tgz", "resolved": "https://registry.npmjs.org/react-codemirror/-/react-codemirror-1.0.0.tgz",
@@ -11361,9 +11420,9 @@
} }
}, },
"seafile-js": { "seafile-js": {
"version": "0.2.139", "version": "0.2.140",
"resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.139.tgz", "resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.140.tgz",
"integrity": "sha512-/KNI7N59iOh6zyEW4lMvSiHE0uHgEmm9hdnthp0hVvEfIVKX8K4dxQBj1zqDxuGIQnF8FXxiR/6Pzs21jlkhZg==", "integrity": "sha512-7WMvQMShWBorBGxygDFCIUt9XPBxHnbobnObSw3eOHflSWfdXrCu0G4ZEj32cDbeece/mEEaD5p/YNn7O7zgKw==",
"requires": { "requires": {
"axios": "^0.18.0", "axios": "^0.18.0",
"form-data": "^2.3.2", "form-data": "^2.3.2",

View File

@@ -5,9 +5,11 @@
"dependencies": { "dependencies": {
"@reach/router": "^1.2.0", "@reach/router": "^1.2.0",
"@seafile/resumablejs": "^1.1.15", "@seafile/resumablejs": "^1.1.15",
"@seafile/seafile-calendar": "0.0.8",
"@seafile/seafile-editor": "^0.2.76", "@seafile/seafile-editor": "^0.2.76",
"MD5": "^1.3.0", "MD5": "^1.3.0",
"autoprefixer": "7.1.6", "autoprefixer": "7.1.6",
"chart.js": "^2.9.1",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"copy-to-clipboard": "^3.0.8", "copy-to-clipboard": "^3.0.8",
"css-loader": "0.28.7", "css-loader": "0.28.7",
@@ -31,6 +33,7 @@
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"raf": "3.4.0", "raf": "3.4.0",
"react": "^16.8.6", "react": "^16.8.6",
"react-chartjs-2": "^2.8.0",
"react-codemirror": "^1.0.0", "react-codemirror": "^1.0.0",
"react-cookies": "^0.1.0", "react-cookies": "^0.1.0",
"react-dom": "^16.8.6", "react-dom": "^16.8.6",
@@ -41,7 +44,7 @@
"react-responsive": "^6.1.2", "react-responsive": "^6.1.2",
"react-select": "^2.4.1", "react-select": "^2.4.1",
"reactstrap": "^6.4.0", "reactstrap": "^6.4.0",
"seafile-js": "^0.2.139", "seafile-js": "^0.2.140",
"socket.io-client": "^2.2.0", "socket.io-client": "^2.2.0",
"sw-precache-webpack-plugin": "0.11.4", "sw-precache-webpack-plugin": "0.11.4",
"unified": "^7.0.0", "unified": "^7.0.0",

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 WorkWeixinDepartments from './work-weixin-departments';
import Invitations from './invitations/invitations'; 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-solid.css';
import '../../assets/css/fa-regular.css'; import '../../assets/css/fa-regular.css';
@@ -97,6 +102,10 @@ class SysAdmin extends React.Component {
tab: 'libraries', tab: 'libraries',
urlPartList: ['all-libraries', 'search-libraries', 'system-library', 'trash-libraries', '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', tab: 'users',
urlPartList: ['users/'] urlPartList: ['users/']
@@ -152,6 +161,11 @@ class SysAdmin extends React.Component {
<MainPanel> <MainPanel>
<Router className="reach-router"> <Router className="reach-router">
<Info path={siteRoot + 'sys/info'} /> <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'} /> <DesktopDevices path={siteRoot + 'sys/desktop-devices'} />
<MobileDevices path={siteRoot + 'sys/mobile-devices'} /> <MobileDevices path={siteRoot + 'sys/mobile-devices'} />
<DeviceErrors path={siteRoot + 'sys/device-errors'} /> <DeviceErrors path={siteRoot + 'sys/device-errors'} />

View File

@@ -46,10 +46,13 @@ class SidePanel extends React.Component {
} }
{isPro && canViewStatistic && {isPro && canViewStatistic &&
<li className="nav-item"> <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="sf2-icon-histogram" aria-hidden="true"></span>
<span className="nav-text">{gettext('Statistic')}</span> <span className="nav-text">{gettext('Statistic')}</span>
</a> </Link>
</li> </li>
} }
{isDefaultAdmin && {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; 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 * only used in the 'catch' part of a seafileAPI request
*/ */

View File

@@ -692,6 +692,11 @@ urlpatterns = [
url(r'^useradmin/batchadduser/example/$', batch_add_user_example, name='batch_add_user_example'), url(r'^useradmin/batchadduser/example/$', batch_add_user_example, name='batch_add_user_example'),
url(r'^sys/info/$', sysadmin_react_fake_view, name="sys_info"), url(r'^sys/info/$', sysadmin_react_fake_view, name="sys_info"),
url(r'^sys/statistics/file/$', sysadmin_react_fake_view, name="sys_statistics_file"),
url(r'^sys/statistics/storage/$', sysadmin_react_fake_view, name="sys_statistics_storage"),
url(r'^sys/statistics/user/$', sysadmin_react_fake_view, name="sys_statistics_user"),
url(r'^sys/statistics/traffic/$', sysadmin_react_fake_view, name="sys_statistics_traffic"),
url(r'^sys/statistics/reports/$', sysadmin_react_fake_view, name="sys_statistics_reports"),
url(r'^sys/desktop-devices/$', sysadmin_react_fake_view, name="sys_desktop_devices"), url(r'^sys/desktop-devices/$', sysadmin_react_fake_view, name="sys_desktop_devices"),
url(r'^sys/mobile-devices/$', sysadmin_react_fake_view, name="sys_mobile_devices"), url(r'^sys/mobile-devices/$', sysadmin_react_fake_view, name="sys_mobile_devices"),
url(r'^sys/device-errors/$', sysadmin_react_fake_view, name="sys_device_errors"), url(r'^sys/device-errors/$', sysadmin_react_fake_view, name="sys_device_errors"),