mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-02 23:48:47 +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:
83
frontend/package-lock.json
generated
83
frontend/package-lock.json
generated
@@ -123,9 +123,9 @@
|
||||
"integrity": "sha512-DJPhjBRLENONdDNaaKRckWWtwXvoqfJcRdSk01FbjmZ3DWpbIwebd/vfuB4qJwm0R2j2qzdEVhgxe6wDYS/n9A=="
|
||||
},
|
||||
"@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==",
|
||||
"version": "0.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@seafile/seafile-calendar/-/seafile-calendar-0.0.8.tgz",
|
||||
"integrity": "sha512-xbFk79mfwOnTd+LGQ42EBFV/uSRNTXa/nm68fcTis6+BmNHcZfmrCEwaq8bPWJTCMm9hw7PP1+J6qQziQMohFg==",
|
||||
"requires": {
|
||||
"babel-runtime": "6.x",
|
||||
"classnames": "2.x",
|
||||
@@ -216,6 +216,20 @@
|
||||
"xtend": "^4.0.1"
|
||||
},
|
||||
"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": {
|
||||
"version": "5.1.1",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz",
|
||||
@@ -5470,7 +5520,7 @@
|
||||
},
|
||||
"git-up": {
|
||||
"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=",
|
||||
"requires": {
|
||||
"is-ssh": "^1.0.0",
|
||||
@@ -5479,7 +5529,7 @@
|
||||
},
|
||||
"git-url-parse": {
|
||||
"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=",
|
||||
"requires": {
|
||||
"git-up": "^1.0.0"
|
||||
@@ -8192,7 +8242,7 @@
|
||||
},
|
||||
"node-status-codes": {
|
||||
"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="
|
||||
},
|
||||
"noop6": {
|
||||
@@ -8499,7 +8549,7 @@
|
||||
},
|
||||
"package.json": {
|
||||
"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=",
|
||||
"requires": {
|
||||
"git-package-json": "^1.4.0",
|
||||
@@ -8509,7 +8559,7 @@
|
||||
"dependencies": {
|
||||
"got": {
|
||||
"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=",
|
||||
"requires": {
|
||||
"create-error-class": "^3.0.1",
|
||||
@@ -8531,7 +8581,7 @@
|
||||
},
|
||||
"package-json": {
|
||||
"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=",
|
||||
"requires": {
|
||||
"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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-codemirror/-/react-codemirror-1.0.0.tgz",
|
||||
@@ -11361,9 +11420,9 @@
|
||||
}
|
||||
},
|
||||
"seafile-js": {
|
||||
"version": "0.2.139",
|
||||
"resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.139.tgz",
|
||||
"integrity": "sha512-/KNI7N59iOh6zyEW4lMvSiHE0uHgEmm9hdnthp0hVvEfIVKX8K4dxQBj1zqDxuGIQnF8FXxiR/6Pzs21jlkhZg==",
|
||||
"version": "0.2.140",
|
||||
"resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.140.tgz",
|
||||
"integrity": "sha512-7WMvQMShWBorBGxygDFCIUt9XPBxHnbobnObSw3eOHflSWfdXrCu0G4ZEj32cDbeece/mEEaD5p/YNn7O7zgKw==",
|
||||
"requires": {
|
||||
"axios": "^0.18.0",
|
||||
"form-data": "^2.3.2",
|
||||
|
@@ -5,9 +5,11 @@
|
||||
"dependencies": {
|
||||
"@reach/router": "^1.2.0",
|
||||
"@seafile/resumablejs": "^1.1.15",
|
||||
"@seafile/seafile-calendar": "0.0.8",
|
||||
"@seafile/seafile-editor": "^0.2.76",
|
||||
"MD5": "^1.3.0",
|
||||
"autoprefixer": "7.1.6",
|
||||
"chart.js": "^2.9.1",
|
||||
"classnames": "^2.2.6",
|
||||
"copy-to-clipboard": "^3.0.8",
|
||||
"css-loader": "0.28.7",
|
||||
@@ -31,6 +33,7 @@
|
||||
"prop-types": "^15.6.2",
|
||||
"raf": "3.4.0",
|
||||
"react": "^16.8.6",
|
||||
"react-chartjs-2": "^2.8.0",
|
||||
"react-codemirror": "^1.0.0",
|
||||
"react-cookies": "^0.1.0",
|
||||
"react-dom": "^16.8.6",
|
||||
@@ -41,7 +44,7 @@
|
||||
"react-responsive": "^6.1.2",
|
||||
"react-select": "^2.4.1",
|
||||
"reactstrap": "^6.4.0",
|
||||
"seafile-js": "^0.2.139",
|
||||
"seafile-js": "^0.2.140",
|
||||
"socket.io-client": "^2.2.0",
|
||||
"sw-precache-webpack-plugin": "0.11.4",
|
||||
"unified": "^7.0.0",
|
||||
|
111
frontend/src/css/system-stat.css
Normal file
111
frontend/src/css/system-stat.css
Normal 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;
|
||||
}
|
@@ -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'} />
|
||||
|
@@ -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 &&
|
||||
|
66
frontend/src/pages/sys-admin/statistic/picker.js
Normal file
66
frontend/src/pages/sys-admin/statistic/picker.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import moment from 'moment';
|
||||
import Calendar from '@seafile/seafile-calendar';
|
||||
import DatePicker from '@seafile/seafile-calendar/lib/Picker';
|
||||
import { translateCalendar } from '../../../utils/date-format-utils';
|
||||
|
||||
import '@seafile/seafile-calendar/assets/index.css';
|
||||
|
||||
const propsTypes = {
|
||||
disabledDate: PropTypes.func.isRequired,
|
||||
value: PropTypes.object,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
const FORMAT = 'YYYY-MM-DD';
|
||||
|
||||
class Picker extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.defaultCalendarValue = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
let lang = window.app.config.lang;
|
||||
this.defaultCalendarValue = moment().locale(lang).clone();
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
const calendar = (<Calendar
|
||||
defaultValue={this.defaultCalendarValue}
|
||||
disabledDate={props.disabledDate}
|
||||
format={FORMAT}
|
||||
locale={translateCalendar()}
|
||||
/>);
|
||||
return (
|
||||
<DatePicker
|
||||
calendar={calendar}
|
||||
value={props.value}
|
||||
onChange={props.onChange}
|
||||
>
|
||||
{
|
||||
({value}) => {
|
||||
return (
|
||||
<span>
|
||||
<input
|
||||
placeholder="yyyy-mm-dd"
|
||||
tabIndex="-1"
|
||||
readOnly
|
||||
value={value && value.format(FORMAT) || ''}
|
||||
className="form-control system-statistic-input"
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
</DatePicker>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Picker.propsTypes = propsTypes;
|
||||
|
||||
export default Picker;
|
123
frontend/src/pages/sys-admin/statistic/statistic-chart.js
Normal file
123
frontend/src/pages/sys-admin/statistic/statistic-chart.js
Normal file
@@ -0,0 +1,123 @@
|
||||
import React from 'react';
|
||||
import { Line } from 'react-chartjs-2';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Utils } from '../../../utils/utils';
|
||||
|
||||
const propTypes = {
|
||||
labels: PropTypes.array.isRequired,
|
||||
filesData: PropTypes.array.isRequired,
|
||||
suggestedMaxNumbers: PropTypes.number.isRequired,
|
||||
isLegendStatus: PropTypes.bool.isRequired,
|
||||
chartTitle: PropTypes.string.isRequired,
|
||||
isTitleCallback: PropTypes.bool,
|
||||
isTicksCallback: PropTypes.bool,
|
||||
};
|
||||
|
||||
class StatisticChart extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
data: {},
|
||||
opations: {}
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
let { labels, filesData, isTitleCallback, isTicksCallback, suggestedMaxNumbers, isLegendStatus, chartTitle } = this.props;
|
||||
let _this = this;
|
||||
let data = {
|
||||
labels: labels,
|
||||
datasets: filesData
|
||||
};
|
||||
let options = {
|
||||
title: {
|
||||
display: true,
|
||||
fontSize: 14,
|
||||
text: chartTitle,
|
||||
},
|
||||
elements: {
|
||||
line: {
|
||||
fill: false,
|
||||
tension: 0, // disable bezier curves, i.e, draw straight lines
|
||||
borderWidth: 2
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
display: isLegendStatus,
|
||||
labels: {
|
||||
usePointStyle: true
|
||||
}
|
||||
},
|
||||
tooltips: {
|
||||
callbacks: {
|
||||
label: function(tooltipItem, data) {
|
||||
if (isTitleCallback) {
|
||||
return _this.titleCallback(tooltipItem, data);
|
||||
}
|
||||
return data.datasets[tooltipItem.datasetIndex].label + ': ' + tooltipItem.yLabel;
|
||||
}
|
||||
}
|
||||
},
|
||||
layout: {
|
||||
padding: {
|
||||
right: 100,
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
yAxes: [{
|
||||
ticks: {
|
||||
beginAtZero: true,
|
||||
suggestedMax: suggestedMaxNumbers,
|
||||
callback: function(value, index, values) {
|
||||
if (isTicksCallback) {
|
||||
return _this.ticksCallback(value, index, values);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}],
|
||||
xAxes: [{
|
||||
ticks: {
|
||||
maxTicksLimit: 20
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
this.setState({
|
||||
data: data,
|
||||
options: options
|
||||
});
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
let data = {
|
||||
labels: nextProps.labels,
|
||||
datasets: nextProps.filesData
|
||||
};
|
||||
this.setState({data: data});
|
||||
}
|
||||
|
||||
titleCallback = (tooltipItem, data) => {
|
||||
return data.datasets[tooltipItem.datasetIndex].label + ': ' + Utils.bytesToSize(tooltipItem.yLabel);
|
||||
}
|
||||
|
||||
ticksCallback = (value, index, values) => {
|
||||
return Utils.bytesToSize(value);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
let { data, options } = this.state;
|
||||
return (
|
||||
<Line
|
||||
data={data}
|
||||
options={options}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
StatisticChart.propTypes = propTypes;
|
||||
|
||||
export default StatisticChart;
|
138
frontend/src/pages/sys-admin/statistic/statistic-common-tool.js
Normal file
138
frontend/src/pages/sys-admin/statistic/statistic-common-tool.js
Normal file
@@ -0,0 +1,138 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button } from 'reactstrap';
|
||||
import moment from 'moment';
|
||||
import { gettext } from '../../../utils/constants';
|
||||
import Picker from './picker';
|
||||
|
||||
const propTypes = {
|
||||
getActiviesFiles: PropTypes.func.isRequired,
|
||||
children: PropTypes.object,
|
||||
};
|
||||
|
||||
class StatisticCommonTool extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
statisticType: 'oneWeek',
|
||||
startValue: null,
|
||||
endValue: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
let today = moment().format('YYYY-MM-DD 00:00:00');
|
||||
let endTime = today;
|
||||
let startTime = moment().subtract(6,'d').format('YYYY-MM-DD 00:00:00');
|
||||
let group_by = 'day';
|
||||
this.props.getActiviesFiles(startTime, endTime, group_by);
|
||||
}
|
||||
|
||||
changeActive = (statisticTypeName) => {
|
||||
let { statisticType } = this.state;
|
||||
if (statisticType === statisticTypeName) {
|
||||
return;
|
||||
}
|
||||
let today = moment().format('YYYY-MM-DD 00:00:00');
|
||||
let endTime = today;
|
||||
let startTime;
|
||||
switch(statisticTypeName) {
|
||||
case 'oneWeek' :
|
||||
startTime = moment().subtract(6,'d').format('YYYY-MM-DD 00:00:00');
|
||||
break;
|
||||
case 'oneMonth' :
|
||||
startTime = moment().subtract(29,'d').format('YYYY-MM-DD 00:00:00');
|
||||
break;
|
||||
case 'oneYear' :
|
||||
startTime = moment().subtract(364,'d').format('YYYY-MM-DD 00:00:00');
|
||||
break;
|
||||
}
|
||||
this.setState({
|
||||
statisticType: statisticTypeName,
|
||||
});
|
||||
let group_by = 'day';
|
||||
this.props.getActiviesFiles(startTime, endTime, group_by);
|
||||
}
|
||||
|
||||
disabledStartDate = (startValue) => {
|
||||
if (!startValue) {
|
||||
return false;
|
||||
}
|
||||
let today = moment().format();
|
||||
|
||||
const endValue = this.state.endValue;
|
||||
if (!endValue) {
|
||||
let startTime = moment(startValue).format();
|
||||
return today < startTime
|
||||
}
|
||||
return endValue.isBefore(startValue) || moment(startValue).format() > today;
|
||||
}
|
||||
|
||||
disabledEndDate = (endValue) => {
|
||||
if (!endValue) {
|
||||
return false;
|
||||
}
|
||||
let today = moment().format();
|
||||
const startValue = this.state.startValue;
|
||||
if (!startValue) {
|
||||
let endTime = moment(endValue).format();
|
||||
return today < endTime;
|
||||
}
|
||||
return endValue.isBefore(startValue) || moment(endValue).format() > today;
|
||||
}
|
||||
|
||||
onChange = (field, value) => {
|
||||
this.setState({
|
||||
[field]: value,
|
||||
});
|
||||
}
|
||||
|
||||
onSubmit = () => {
|
||||
let { startValue, endValue } = this.state;
|
||||
if(!startValue || !endValue) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
statisticType: 'itemButton',
|
||||
});
|
||||
let startTime = moment(startValue).format('YYYY-MM-DD 00:00:00');
|
||||
let endTime = moment(endValue).format('YYYY-MM-DD 00:00:00');
|
||||
let group_by = 'day';
|
||||
this.props.getActiviesFiles(startTime, endTime, group_by);
|
||||
}
|
||||
|
||||
render() {
|
||||
let { statisticType, endValue, startValue } = this.state;
|
||||
return(
|
||||
<Fragment>
|
||||
{this.props.children}
|
||||
<div className="system-statistic-time-range">
|
||||
<div className="sys-stat-tool">
|
||||
<div className={`system-statistic-item border-right-0 rounded-left ${statisticType === 'oneWeek' ? 'item-active' : ''}`} onClick={this.changeActive.bind(this, 'oneWeek')}>{gettext('7 Days')}</div>
|
||||
<div className={`system-statistic-item border-right-0 ${statisticType === 'oneMonth' ? 'item-active' : ''}`} onClick={this.changeActive.bind(this, 'oneMonth')}>{gettext('30 Days')}</div>
|
||||
<div className={`system-statistic-item rounded-right ${statisticType === 'oneYear' ? 'item-active' : ''}`} onClick={this.changeActive.bind(this, 'oneYear')}>{gettext('1 Year')}</div>
|
||||
</div>
|
||||
<div className="system-statistic-input-container">
|
||||
<Picker
|
||||
disabledDate={this.disabledStartDate}
|
||||
value={startValue}
|
||||
onChange={this.onChange.bind(this, 'startValue')}
|
||||
/>
|
||||
<span className="system-statistic-connect">-</span>
|
||||
<Picker
|
||||
disabledDate={this.disabledEndDate}
|
||||
value={endValue}
|
||||
onChange={this.onChange.bind(this, 'endValue')}
|
||||
/>
|
||||
<Button className="operation-item system-statistic-button" onClick={this.onSubmit}>{gettext('Submit')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
StatisticCommonTool.propTypes = propTypes;
|
||||
|
||||
export default StatisticCommonTool;
|
103
frontend/src/pages/sys-admin/statistic/statistic-file.js
Normal file
103
frontend/src/pages/sys-admin/statistic/statistic-file.js
Normal 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;
|
43
frontend/src/pages/sys-admin/statistic/statistic-nav.js
Normal file
43
frontend/src/pages/sys-admin/statistic/statistic-nav.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link } from '@reach/router';
|
||||
import { siteRoot, gettext } from '../../../utils/constants';
|
||||
|
||||
const propTypes = {
|
||||
currentItem: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
class Nav extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.navItems = [
|
||||
{name: 'fileStatistic', urlPart: 'statistics/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;
|
88
frontend/src/pages/sys-admin/statistic/statistic-reports.js
Normal file
88
frontend/src/pages/sys-admin/statistic/statistic-reports.js
Normal file
@@ -0,0 +1,88 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import MainPanelTopbar from '../main-panel-topbar';
|
||||
import moment from 'moment';
|
||||
import StatisticNav from './statistic-nav';
|
||||
import { Button, Input } from 'reactstrap';
|
||||
import { siteRoot, gettext } 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;
|
81
frontend/src/pages/sys-admin/statistic/statistic-storage.js
Normal file
81
frontend/src/pages/sys-admin/statistic/statistic-storage.js
Normal file
@@ -0,0 +1,81 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import moment from 'moment';
|
||||
import MainPanelTopbar from '../main-panel-topbar';
|
||||
import StatisticNav from './statistic-nav';
|
||||
import StatisticCommonTool from './statistic-common-tool';
|
||||
import { seafileAPI } from '../../../utils/seafile-api';
|
||||
import StatisticChart from './statistic-chart';
|
||||
import Loading from '../../../components/loading';
|
||||
import { gettext } 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;
|
219
frontend/src/pages/sys-admin/statistic/statistic-traffic.js
Normal file
219
frontend/src/pages/sys-admin/statistic/statistic-traffic.js
Normal 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;
|
79
frontend/src/pages/sys-admin/statistic/statistic-users.js
Normal file
79
frontend/src/pages/sys-admin/statistic/statistic-users.js
Normal 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;
|
@@ -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;
|
54
frontend/src/pages/sys-admin/statistic/traffic-table-body.js
Normal file
54
frontend/src/pages/sys-admin/statistic/traffic-table-body.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Utils } from '../../../utils/utils';
|
||||
import { siteRoot } from '../../../utils/constants';
|
||||
|
||||
const propTypes = {
|
||||
type: PropTypes.string.isRequired,
|
||||
userTrafficItem: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
class TrafficTableBody extends React.Component {
|
||||
|
||||
trafficName = () => {
|
||||
let { userTrafficItem, type } = this.props;
|
||||
switch(type) {
|
||||
case 'user':
|
||||
if (userTrafficItem.name) {
|
||||
return (
|
||||
<a href={siteRoot + 'useradmin/info/' + userTrafficItem.email + '/'}>{userTrafficItem.name}</a>
|
||||
);
|
||||
}
|
||||
return(<span>{'--'}</span>);
|
||||
case 'org':
|
||||
return(<span>{userTrafficItem.org_name}</span>);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let { userTrafficItem } = this.props;
|
||||
|
||||
let syncUploadSize = Utils.bytesToSize(userTrafficItem.sync_file_upload);
|
||||
let syncDownloadSize = Utils.bytesToSize(userTrafficItem.sync_file_download);
|
||||
let webUploadSize = Utils.bytesToSize(userTrafficItem.web_file_upload);
|
||||
let webDownloadSize = Utils.bytesToSize(userTrafficItem.web_file_download);
|
||||
let linkUploadSize = Utils.bytesToSize(userTrafficItem.link_file_upload);
|
||||
let linkDownloadSize = Utils.bytesToSize(userTrafficItem.link_file_download);
|
||||
|
||||
return(
|
||||
<tr>
|
||||
<td>{this.trafficName()}</td>
|
||||
<td>{syncUploadSize}</td>
|
||||
<td>{syncDownloadSize}</td>
|
||||
<td>{webUploadSize}</td>
|
||||
<td>{webDownloadSize}</td>
|
||||
<td>{linkUploadSize}</td>
|
||||
<td>{linkDownloadSize}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TrafficTableBody.propTypes = propTypes;
|
||||
|
||||
export default TrafficTableBody;
|
80
frontend/src/pages/sys-admin/statistic/traffic-table.js
Normal file
80
frontend/src/pages/sys-admin/statistic/traffic-table.js
Normal 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;
|
143
frontend/src/pages/sys-admin/statistic/traffic-user-table.js
Normal file
143
frontend/src/pages/sys-admin/statistic/traffic-user-table.js
Normal 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;
|
51
frontend/src/utils/date-format-utils.js
Normal file
51
frontend/src/utils/date-format-utils.js
Normal 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 };
|
@@ -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
|
||||
*/
|
||||
|
@@ -692,6 +692,11 @@ urlpatterns = [
|
||||
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/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/mobile-devices/$', sysadmin_react_fake_view, name="sys_mobile_devices"),
|
||||
url(r'^sys/device-errors/$', sysadmin_react_fake_view, name="sys_device_errors"),
|
||||
|
Reference in New Issue
Block a user