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:
83
frontend/package-lock.json
generated
83
frontend/package-lock.json
generated
@@ -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",
|
||||||
|
@@ -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",
|
||||||
|
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 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'} />
|
||||||
|
@@ -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 &&
|
||||||
|
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;
|
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
|
||||||
*/
|
*/
|
||||||
|
@@ -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"),
|
||||||
|
Reference in New Issue
Block a user