diff --git a/frontend/src/pages/sys-admin/admin-logs/login-logs.js b/frontend/src/pages/sys-admin/admin-logs/login-logs.js
new file mode 100644
index 0000000000..edd7a0d376
--- /dev/null
+++ b/frontend/src/pages/sys-admin/admin-logs/login-logs.js
@@ -0,0 +1,180 @@
+import React, { Component, Fragment } from 'react';
+import { seafileAPI } from '../../../utils/seafile-api';
+import { gettext, loginUrl, siteRoot } from '../../../utils/constants';
+import { Utils } from '../../../utils/utils';
+import EmptyTip from '../../../components/empty-tip';
+import moment from 'moment';
+import Loading from '../../../components/loading';
+import Paginator from '../../../components/paginator';
+import LogsNav from './logs-nav';
+import MainPanelTopbar from '../main-panel-topbar';
+
+
+class Content extends Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ getPreviousPage = () => {
+ this.props.getLogsByPage(this.props.currentPage - 1);
+ }
+
+ getNextPage = () => {
+ this.props.getLogsByPage(this.props.currentPage + 1);
+ }
+
+ render() {
+ const { loading, errorMsg, items, perPage, currentPage, hasNextPage } = this.props;
+ if (loading) {
+ return ;
+ } else if (errorMsg) {
+ return
{errorMsg}
;
+ } else {
+ const emptyTip = (
+
+ {gettext('No Admin Login Logs.')}
+
+ );
+ const table = (
+
+
+
+
+ {gettext('Name')} |
+ {gettext('IP')} |
+ {gettext('Status')} |
+ {gettext('Time')} |
+
+
+ {items &&
+
+ {items.map((item, index) => {
+ return ( );
+ })}
+
+ }
+
+
+
+ );
+ return items.length ? table : emptyTip;
+ }
+ }
+}
+
+class Item extends Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ render() {
+ let { item } = this.props;
+ return (
+
+ {item.name} |
+ {item.login_ip} |
+ {item.login_success ? gettext('Success') : gettext('Failed')} |
+ {moment(item.login_time).fromNow()} |
+
+ );
+ }
+}
+
+class AdminLoginLogs extends Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ loading: true,
+ errorMsg: '',
+ logList: [],
+ perPage: 100,
+ currentPage: 1,
+ hasNextPage: false,
+ };
+ this.initPage = 1;
+ }
+
+ componentDidMount () {
+ this.getLogsByPage(this.initPage);
+ }
+
+ getLogsByPage = (page) => {
+ let { perPage } = this.state;
+ seafileAPI.sysAdminListAdminLoginLogs(page, perPage).then((res) => {
+ this.setState({
+ logList: res.data.data,
+ loading: false,
+ currentPage: page,
+ hasNextPage: Utils.hasNextPage(page, perPage, res.data.total_count),
+ });
+ }).catch((error) => {
+ if (error.response) {
+ if (error.response.status == 403) {
+ this.setState({
+ loading: false,
+ errorMsg: gettext('Permission denied')
+ });
+ location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`;
+ } else {
+ this.setState({
+ loading: false,
+ errorMsg: gettext('Error')
+ });
+ }
+ } else {
+ this.setState({
+ loading: false,
+ errorMsg: gettext('Please check the network.')
+ });
+ }
+ });
+ }
+
+ resetPerPage = (newPerPage) => {
+ this.setState({
+ perPage: newPerPage,
+ }, () => this.getLogsByPage(this.initPage));
+ }
+
+ render() {
+ let { logList, currentPage, perPage, hasNextPage } = this.state;
+ return (
+
+
+
+
+ );
+ }
+}
+
+export default AdminLoginLogs;
diff --git a/frontend/src/pages/sys-admin/admin-logs/logs-nav.js b/frontend/src/pages/sys-admin/admin-logs/logs-nav.js
new file mode 100644
index 0000000000..d113e27a6a
--- /dev/null
+++ b/frontend/src/pages/sys-admin/admin-logs/logs-nav.js
@@ -0,0 +1,40 @@
+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 LogsNav extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.navItems = [
+ {name: 'adminOperationLogs', urlPart:'admin-logs/operation', text: gettext('Admin Operation Logs')},
+ {name: 'adminLoginLogs', urlPart:'admin-logs/login', text: gettext('Admin Login Logs')},
+ ];
+ }
+
+ render() {
+ const { currentItem } = this.props;
+ return (
+
+
+ {this.navItems.map((item, index) => {
+ return (
+ -
+ {item.text}
+
+ );
+ })}
+
+
+ );
+ }
+}
+
+LogsNav.propTypes = propTypes;
+
+export default LogsNav;
diff --git a/frontend/src/pages/sys-admin/admin-logs/operation-logs.js b/frontend/src/pages/sys-admin/admin-logs/operation-logs.js
new file mode 100644
index 0000000000..ac30e7ffea
--- /dev/null
+++ b/frontend/src/pages/sys-admin/admin-logs/operation-logs.js
@@ -0,0 +1,278 @@
+import React, { Component, Fragment } from 'react';
+import { seafileAPI } from '../../../utils/seafile-api';
+import { gettext, loginUrl, siteRoot, enableSysAdminViewRepo, isPro } from '../../../utils/constants';
+import { Utils } from '../../../utils/utils';
+import EmptyTip from '../../../components/empty-tip';
+import moment from 'moment';
+import Loading from '../../../components/loading';
+import Paginator from '../../../components/paginator';
+import LogsNav from './logs-nav';
+import MainPanelTopbar from '../main-panel-topbar';
+
+
+class Content extends Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ getPreviousPage = () => {
+ this.props.getLogsByPage(this.props.currentPage - 1);
+ }
+
+ getNextPage = () => {
+ this.props.getLogsByPage(this.props.currentPage + 1);
+ }
+
+ render() {
+ const { loading, errorMsg, items, perPage, currentPage, hasNextPage } = this.props;
+ if (loading) {
+ return ;
+ } else if (errorMsg) {
+ return {errorMsg}
;
+ } else {
+ const emptyTip = (
+
+ {gettext('No Admin Operation Logs.')}
+
+ );
+ const table = (
+
+
+
+
+ {gettext('Name')} |
+ {gettext('Operation')} |
+ {gettext('Detail')} |
+ {gettext('Time')} |
+
+
+ {items &&
+
+ {items.map((item, index) => {
+ return ( );
+ })}
+
+ }
+
+
+
+ );
+ return items.length ? table : emptyTip;
+ }
+ }
+}
+
+class Item extends Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ isOpIconShown: false,
+ };
+ }
+
+ getOperationText = (operationType) => {
+ switch (operationType) {
+ case 'repo_create': return gettext('Create Library');
+ case 'repo_delete': return gettext('Delete Library');
+ case 'repo_transfer': return gettext('Transfer Library');
+ case 'group_create': return gettext('Create Group');
+ case 'group_transfer': return gettext('Transfer Group');
+ case 'group_delete': return gettext('Delete Group');
+ case 'user_add': return gettext('Add User');
+ case 'user_delete': return gettext('Delete User');
+ default: return '';
+ }
+ }
+
+ getOperationDetail = (item) => {
+ let detail = item.detail;
+
+ let ownerPageUrl = '';
+ if (detail.owner) {
+ ownerPageUrl = siteRoot + 'useradmin/info/' + detail.owner + '/';
+ }
+ let userPageUrl = '';
+ if (detail.email) {
+ userPageUrl = siteRoot + 'useradmin/info/' + detail.email + '/';
+ }
+ let detailText = '';
+ let repoPageUrl = '';
+ let groupPageUrl = '';
+ if (item.operation == 'repo_create' || item.operation == 'repo_delete' || item.operation == 'repo_transfer') {
+ repoPageUrl = siteRoot + 'sys/libraries/' + detail.id + '/' + detail.name + '/';
+ }
+ if (item.operation == 'group_create' || item.operation == 'group_delete' || item.operation == 'group_transfer') {
+ groupPageUrl = siteRoot + 'sys/groups/' + detail.id + '/libraries/';
+ }
+
+ switch (item.operation) {
+ case 'repo_create':
+ detailText = gettext('Created library {library_name} with {owner} as its owner')
+ .replace('{owner}', '' + detail.owner + '');
+ if (isPro && enableSysAdminViewRepo) {
+ detailText.replace('{library_name}', '' + detail.name + '');
+ } else {
+ detailText.replace('{library_name}', '' + detail.name + '');
+ }
+ return detailText;
+
+ case 'repo_delete':
+ detailText = gettext('Deleted library {library_name}')
+ .replace('{library_name}', '' + detail.name + '');
+ return detailText;
+
+ case 'repo_transfer':
+ detailText = gettext('Transferred library {library_name} from {user_from} to {user_to}')
+ .replace('{user_from}', '' + detail.from + '')
+ .replace('{user_to}', '' + detail.to+ '');
+ if (isPro && enableSysAdminViewRepo) {
+ detailText.replace('{library_name}', '' + detail.name + '');
+ } else {
+ detailText.replace('{library_name}', '' + detail.name + '');
+ }
+ return detailText;
+
+ case 'group_create':
+ detailText = gettext('Created group {group_name}')
+ .replace('{group_name}', '' + detail.name+ '');
+ return detailText;
+
+ case 'group_transfer':
+ detailText = gettext('Transferred group {group_name} from {user_from} to {user_to}')
+ .replace('{user_from}', '' + detail.from + '')
+ .replace('{user_to}', '' + detail.to+ '')
+ .replace('{group_name}', '' + detail.name+ '');
+ return detailText;
+
+ case 'group_delete':
+ detailText = gettext('Deleted group {group_name}')
+ .replace('{group_name}', '' + detail.name + '');
+ return detailText;
+
+ case 'user_add':
+ detailText = gettext('Added user {user}')
+ .replace('{user}', '' + detail.email+ '');
+ return detailText;
+
+ case 'user_delete':
+ detailText = gettext('Deleted user {user}')
+ .replace('{user}', '' + detail.email+ '');
+ return detailText;
+
+ default: return '';
+ }
+ }
+
+ render() {
+ let { item } = this.props;
+ return (
+
+ {item.name} |
+ {this.getOperationText(item.operation)} |
+
+
+ |
+ {moment(item.datetime).fromNow()} |
+
+ );
+ }
+}
+
+class AdminOperationLogs extends Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ loading: true,
+ errorMsg: '',
+ logList: [],
+ perPage: 100,
+ currentPage: 1,
+ hasNextPage: false,
+ };
+ this.initPage = 1;
+ }
+
+ componentDidMount () {
+ this.getLogsByPage(this.initPage);
+ }
+
+ getLogsByPage = (page) => {
+ let { perPage } = this.state;
+ seafileAPI.sysAdminListAdminLogs(page, perPage).then((res) => {
+ this.setState({
+ logList: res.data.data,
+ loading: false,
+ currentPage: page,
+ hasNextPage: Utils.hasNextPage(page, perPage, res.data.total_count),
+ });
+ }).catch((error) => {
+ if (error.response) {
+ if (error.response.status == 403) {
+ this.setState({
+ loading: false,
+ errorMsg: gettext('Permission denied')
+ });
+ location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`;
+ } else {
+ this.setState({
+ loading: false,
+ errorMsg: gettext('Error')
+ });
+ }
+ } else {
+ this.setState({
+ loading: false,
+ errorMsg: gettext('Please check the network.')
+ });
+ }
+ });
+ }
+
+ resetPerPage = (newPerPage) => {
+ this.setState({
+ perPage: newPerPage,
+ }, () => this.getLogsByPage(this.initPage));
+ }
+
+ render() {
+ let { logList, currentPage, perPage, hasNextPage } = this.state;
+ return (
+
+
+
+
+ );
+ }
+}
+
+export default AdminOperationLogs;
diff --git a/frontend/src/pages/sys-admin/index.js b/frontend/src/pages/sys-admin/index.js
index ba19c25faa..da85818b6b 100644
--- a/frontend/src/pages/sys-admin/index.js
+++ b/frontend/src/pages/sys-admin/index.js
@@ -38,6 +38,9 @@ import FileAccessLogs from './logs-page/file-access-logs';
import FileUpdateLogs from './logs-page/file-update-logs';
import SharePermissionLogs from './logs-page/share-permission-logs';
+import AdminOperationLogs from './admin-logs/operation-logs';
+import AdminLoginLogs from './admin-logs/login-logs';
+
import WebSettings from './web-settings/web-settings';
import Notifications from './notifications/notifications';
import FileScanRecords from './file-scan-records';
@@ -149,6 +152,8 @@ class SysAdmin extends React.Component {
+
+
- {gettext('No Share Links.')}
+ {gettext('No File Access Logs.')}
);
const table = (
diff --git a/frontend/src/pages/sys-admin/logs-page/file-update-logs.js b/frontend/src/pages/sys-admin/logs-page/file-update-logs.js
index 1478d90909..7e112b2081 100644
--- a/frontend/src/pages/sys-admin/logs-page/file-update-logs.js
+++ b/frontend/src/pages/sys-admin/logs-page/file-update-logs.js
@@ -35,7 +35,7 @@ class Content extends Component {
} else {
const emptyTip = (
- {gettext('No Share Links.')}
+ {gettext('No File Update Logs.')}
);
const table = (
diff --git a/frontend/src/pages/sys-admin/logs-page/login-logs.js b/frontend/src/pages/sys-admin/logs-page/login-logs.js
index fc903a7edc..671d76ae22 100644
--- a/frontend/src/pages/sys-admin/logs-page/login-logs.js
+++ b/frontend/src/pages/sys-admin/logs-page/login-logs.js
@@ -36,7 +36,7 @@ class Content extends Component {
} else {
const emptyTip = (
- {gettext('No Share Links.')}
+ {gettext('No Login Logs.')}
);
const table = (
diff --git a/frontend/src/pages/sys-admin/logs-page/share-permission-logs.js b/frontend/src/pages/sys-admin/logs-page/share-permission-logs.js
index 9c10e08442..602bfe1d9d 100644
--- a/frontend/src/pages/sys-admin/logs-page/share-permission-logs.js
+++ b/frontend/src/pages/sys-admin/logs-page/share-permission-logs.js
@@ -35,7 +35,7 @@ class Content extends Component {
} else {
const emptyTip = (
- {gettext('No Share Links.')}
+ {gettext('No Permission Logs.')}
);
const table = (
diff --git a/frontend/src/pages/sys-admin/side-panel.js b/frontend/src/pages/sys-admin/side-panel.js
index 90c4e8f236..2c3b619fcd 100644
--- a/frontend/src/pages/sys-admin/side-panel.js
+++ b/frontend/src/pages/sys-admin/side-panel.js
@@ -213,10 +213,14 @@ class SidePanel extends React.Component {
}
{isPro && canViewAdminLog &&
-
-
+ this.props.tabItemClick('adminLogs')}
+ >
+
{gettext('Admin Logs')}
-
+
}
{isDefaultAdmin && enableWorkWeixin &&
diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js
index 15aa19a71d..5499e4c249 100644
--- a/frontend/src/utils/constants.js
+++ b/frontend/src/utils/constants.js
@@ -124,3 +124,4 @@ export const canManageGroup = window.sysadmin ? window.sysadmin.pageOptions.admi
export const canViewUserLog = window.sysadmin ? window.sysadmin.pageOptions.admin_permissions.can_view_user_log : '';
export const canViewAdminLog = window.sysadmin ? window.sysadmin.pageOptions.admin_permissions.can_view_admin_log : '';
export const enableWorkWeixin = window.sysadmin ? window.sysadmin.pageOptions.enable_work_weixin : '';
+export const enableSysAdminViewRepo = window.sysadmin ? window.sysadmin.pageOptions.enableSysAdminViewRepo : '';
diff --git a/seahub/urls.py b/seahub/urls.py
index dc96c86492..5956279700 100644
--- a/seahub/urls.py
+++ b/seahub/urls.py
@@ -682,6 +682,8 @@ urlpatterns = [
url(r'^sys/logs/file-access/$', sysadmin_react_fake_view, name="sys_logs_file_access"),
url(r'^sys/logs/file-update/$', sysadmin_react_fake_view, name="sys_logs_file_update"),
url(r'^sys/logs/share-permission/$', sysadmin_react_fake_view, name="sys_logs_share_permission"),
+ url(r'^sys/admin-logs/operation/$', sysadmin_react_fake_view, name="sys_admin_logs_operation"),
+ url(r'^sys/admin-logs/login/$', sysadmin_react_fake_view, name="sys_admin_logs_login"),
url(r'^sys/organizations/$', sysadmin_react_fake_view, name="sys_organizations"),
url(r'^sys/organizations/(?P\d+)/info/$', sysadmin_react_fake_view, name="sys_organization_info"),
url(r'^sys/organizations/(?P\d+)/users/$', sysadmin_react_fake_view, name="sys_organization_users"),