diff --git a/frontend/src/css/log-filter.css b/frontend/src/css/log-filter.css new file mode 100644 index 0000000000..7edae4d80b --- /dev/null +++ b/frontend/src/css/log-filter.css @@ -0,0 +1,74 @@ + .activity-details { + text-decoration: underline; + cursor: pointer; + } + .activity-details:hover { + color: #212529; + } + .mobile-activity-time { + display: inline-block; + margin-bottom: .2em; + } + + .cur-activity-modifiers { + margin-left: -0.5rem; + } + + .cur-activity-modifiers:hover { + background: #f5f5f5; + cursor: pointer; + } + + .cur-activity-modifiers .toggle-icon { + color: #999; + } + + .activity-modifier-selector-container { + width: 320px; + background: #fff; + border: 1px solid #e8e8e8; + margin-top: 2px; + z-index: 2; + } + + .activity-selected-modifiers { + min-height: 2rem; + background: #f6f6f6; + border-bottom: 1px solid #dde2ea; + line-height: 1; + } + + .activity-selected-modifier { + display: inline-flex; + align-items: center; + margin-right: 10px; + padding: 0 8px 0 2px; + border-radius: 10px; + background: #eaeaea; + } + + .unselect-activity-user { + color: #909090; + cursor: pointer; + } + + .unselect-activity-user:hover { + color: #5a5a5a; + } + + .activity-user-list { + min-height: 4rem; + max-height: 200px; + } + + .activity-user-item { + cursor: pointer; + } + + .activity-user-item:hover { + background: #f5f5f5; + } + + .activity-user-name { + font-size: 14px; + } diff --git a/frontend/src/pages/dashboard/files-activities.js b/frontend/src/pages/dashboard/files-activities.js index 192a0aad4c..f501a9e504 100644 --- a/frontend/src/pages/dashboard/files-activities.js +++ b/frontend/src/pages/dashboard/files-activities.js @@ -235,7 +235,7 @@ class FilesActivities extends Component { const { onlyMine } = this.props; const { targetUsers, availableUsers } = this.state; return ( -
+
    diff --git a/frontend/src/pages/dashboard/log-repo-selector.js b/frontend/src/pages/dashboard/log-repo-selector.js new file mode 100644 index 0000000000..158d22f9e6 --- /dev/null +++ b/frontend/src/pages/dashboard/log-repo-selector.js @@ -0,0 +1,154 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { Input } from 'reactstrap'; +import { gettext } from '../../utils/constants'; +import '../../css/log-filter.css'; +import { Utils } from '../../utils/utils'; +import toaster from '../../components/toast'; + +const propTypes = { + items: PropTypes.array.isRequired, + selectedItems: PropTypes.array.isRequired, + onSelect: PropTypes.func.isRequired, + isOpen: PropTypes.bool.isRequired, + onToggle: PropTypes.func.isRequired, + searchReposFunc: PropTypes.func.isRequired, +}; + +class LogRepoSelector extends Component { + + constructor(props) { + super(props); + this.state = { + query: '', + isLoading: false, + searchResults: [] + }; + this.dropdownRef = React.createRef(); + this.finalValue = ''; + } + + componentDidMount() { + document.addEventListener('click', this.handleClickOutside); + } + + componentWillUnmount() { + document.removeEventListener('click', this.handleClickOutside); + } + + handleClickOutside = (e) => { + if (this.props.isOpen && !this.repoSelector.contains(e.target)) { + this.props.onToggle(); + } + }; + + onToggleClick = (e) => { + e.stopPropagation(); + this.props.onToggle(); + }; + + onQueryChange = (e) => { + const value = e.target.value; + this.setState({ query: value }); + this.searchRepos(value); + }; + + searchRepos = (value) => { + this.finalValue = value; + if (value.length > 0) { + this.setState({ isLoading: true }); + setTimeout(() => { + if (this.finalValue === value) { + this.props.searchReposFunc(value).then((res) => { + const repos = res.data.repo_list || res.data.repos || []; + this.setState({ + searchResults: repos, + isLoading: false + }); + }).catch(error => { + this.setState({ isLoading: false }); + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + }, 500); + } else { + this.setState({ searchResults: [] }); + } + }; + + toggleSelectItem = (e, item) => { + e.stopPropagation(); + this.props.onSelect(item, false); + }; + + render() { + const { query, isLoading, searchResults } = this.state; + const { selectedItems, isOpen } = this.props; + const displayItems = query.trim() ? searchResults : this.props.items; + + return ( +
    + + {selectedItems.length > 0 ? ( + <> + {gettext('Libraries:')} + {selectedItems.map(item => item.name).join(', ')} + + ) : gettext('Libraries')} + + + {isOpen && ( +
    this.repoSelector = ref}> +
      + {selectedItems.map((item, index) => ( +
    • + + {item.name} + {this.toggleSelectItem(e, item);}}> +
    • + ))} +
    +
    + +
    +
      + {isLoading ? ( +
    • {gettext('Loading...')}
    • + ) : displayItems.length === 0 ? ( +
    • + {query ? gettext('Library not found') : gettext('Enter characters to start searching')} +
    • + ) : ( + displayItems.map((item, index) => { + const isSelected = selectedItems.some(selected => selected.id === item.id); + return ( +
    • {this.toggleSelectItem(e, item);}} + > +
      + + {item.name} +
      + {isSelected && } +
    • + ); + }) + )} +
    +
    + )} +
    + ); + } +} + +LogRepoSelector.propTypes = propTypes; + +export default LogRepoSelector; diff --git a/frontend/src/pages/dashboard/log-user-selector.js b/frontend/src/pages/dashboard/log-user-selector.js new file mode 100644 index 0000000000..7f22e51bd5 --- /dev/null +++ b/frontend/src/pages/dashboard/log-user-selector.js @@ -0,0 +1,190 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { Input } from 'reactstrap'; +import { gettext } from '../../utils/constants'; +import '../../css/log-filter.css'; +import { Utils } from '../../utils/utils'; +import toaster from '../../components/toast'; + +const propTypes = { + componentName: PropTypes.string.isRequired, + items: PropTypes.array.isRequired, + selectedItems: PropTypes.array.isRequired, + onSelect: PropTypes.func.isRequired, + isOpen: PropTypes.bool.isRequired, + onToggle: PropTypes.func.isRequired, + searchUsersFunc: PropTypes.func, + searchGroupsFunc: PropTypes.func +}; + +class LogUserSelector extends Component { + + constructor(props) { + super(props); + this.state = { + query: '', + searchResults: [], + isLoading: false + }; + this.dropdownRef = React.createRef(); + this.finalValue = ''; + } + + componentDidMount() { + document.addEventListener('click', this.handleClickOutside); + } + + componentWillUnmount() { + document.removeEventListener('click', this.handleClickOutside); + } + + handleClickOutside = (e) => { + if (this.props.isOpen && !this.userSelector.contains(e.target)) { + this.props.onToggle(); + } + }; + + onToggleClick = (e) => { + e.stopPropagation(); + this.props.onToggle(); + }; + + onQueryChange = (e) => { + const value = e.target.value; + this.setState({ query: value }); + this.handleSearchUser(value); + }; + + handleSearchUser = (value) => { + if (!value.trim()) { + this.setState({ + searchResults: [] + }); + return; + } + + this.setState({ + isLoading: true + }); + + this.finalValue = value; + + setTimeout(() => { + if (this.finalValue === value) { + if (this.props.searchUsersFunc) { + this.props.searchUsersFunc(value).then((res) => { + const users = res.data.user_list || res.data.users || []; + this.setState({ + searchResults: users, + isLoading: false + }, () => { + if (this.props.searchGroupsFunc) { + this.props.searchGroupsFunc(value).then((res) => { + const groups = res.data.group_list || res.data.groups || []; + this.setState({ + searchResults: [...users, ...groups] + }); + }); + } + }); + }).catch((error) => { + this.setState({ + isLoading: false + }); + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + if (this.props.searchGroupsFunc && !this.props.searchUsersFunc) { + this.props.searchGroupsFunc(value).then((res) => { + const groups = res.data.group_list || res.data.groups || []; + this.setState({ + searchResults: groups, + isLoading: false + }); + }); + } + } + }, 500); + }; + + toggleSelectItem = (e, item) => { + e.stopPropagation(); + this.props.onSelect(item, false); + }; + + render() { + const { query, isLoading, searchResults } = this.state; + const { selectedItems, isOpen } = this.props; + const displayItems = query.trim() ? searchResults : this.props.items; + + return ( +
    + + {selectedItems.length > 0 ? ( + <> + {gettext(this.props.componentName + ':')} + {selectedItems.map(item => item.name).join(', ')} + + ) : gettext(this.props.componentName)} + + + {isOpen && ( +
    this.userSelector = ref}> +
      + {selectedItems.map((item, index) => { + return ( +
    • + + {item.name} + {this.toggleSelectItem(e, item);}}> +
    • + ); + })} +
    +
    + +
    +
      + {isLoading ? ( +
    • {gettext('Loading...')}
    • + ) : displayItems.length === 0 ? ( +
    • + {query ? gettext('User not found') : gettext('Enter characters to start searching')} +
    • + ) : ( + displayItems.map((item, index) => { + const isSelected = selectedItems.some(selected => + (item.email && selected.email === item.email) || + (item.id && selected.id === item.id) + ); + return ( +
    • {this.toggleSelectItem(e, item);}} + > +
      + + {item.name} +
      + {isSelected && } +
    • + ); + }) + )} +
    +
    + )} +
    + ); + } +} + +LogUserSelector.propTypes = propTypes; + +export default LogUserSelector; diff --git a/frontend/src/pages/sys-admin/logs-page/file-access-logs.js b/frontend/src/pages/sys-admin/logs-page/file-access-logs.js index fb568f7c32..2492e41a2b 100644 --- a/frontend/src/pages/sys-admin/logs-page/file-access-logs.js +++ b/frontend/src/pages/sys-admin/logs-page/file-access-logs.js @@ -13,10 +13,10 @@ import Paginator from '../../../components/paginator'; import LogsExportExcelDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-logs-export-excel-dialog'; import ModalPortal from '../../../components/modal-portal'; import LogsNav from './logs-nav'; -import FilterMenu from './file-access-item-menu'; -import ToggleFilter from './file-access-toggle-filter'; import MainPanelTopbar from '../main-panel-topbar'; import UserLink from '../user-link'; +import LogUserSelector from '../../dashboard/log-user-selector'; +import LogRepoSelector from '../../dashboard/log-repo-selector'; dayjs.extend(relativeTime); @@ -37,14 +37,6 @@ class Content extends Component { this.props.getLogsByPage(this.props.currentPage + 1); }; - toggleFilterByUser = () => { - this.props.filterByUser(null); - }; - - toggleFilterByRepo = () => { - this.props.filterByRepo(null); - }; - toggleFreezeItem = (freezed) => { this.setState({ isItemFreezed: freezed @@ -54,7 +46,6 @@ class Content extends Component { render() { const { loading, errorMsg, items, - userFilteredBy, repoFilteredBy, perPage, currentPage, hasNextPage } = this.props; if (loading) { @@ -68,20 +59,6 @@ class Content extends Component { ); const table = ( -
    - {userFilteredBy && ( - - )} - {repoFilteredBy && ( - - )} -
    @@ -101,10 +78,6 @@ class Content extends Component { item={item} isFreezed={this.state.isItemFreezed} toggleFreezeItem={this.toggleFreezeItem} - userFilteredBy={userFilteredBy} - repoFilteredBy={repoFilteredBy} - filterByUser={this.props.filterByUser} - filterByRepo={this.props.filterByRepo} />); })} @@ -135,11 +108,7 @@ Content.propTypes = { perPage: PropTypes.number, pageInfo: PropTypes.object, hasNextPage: PropTypes.bool, - toggleFreezeItem: PropTypes.func, - userFilteredBy: PropTypes.string, - repoFilteredBy: PropTypes.string, - filterByUser: PropTypes.func, - filterByRepo: PropTypes.func, + toggleFreezeItem: PropTypes.func }; @@ -149,7 +118,6 @@ class Item extends Component { super(props); this.state = { isHighlighted: false, - isOpIconShown: false }; } @@ -157,7 +125,6 @@ class Item extends Component { if (!this.props.isFreezed) { this.setState({ isHighlighted: true, - isOpIconShown: true }); } }; @@ -166,58 +133,32 @@ class Item extends Component { if (!this.props.isFreezed) { this.setState({ isHighlighted: false, - isOpIconShown: false }); } }; - filterByUser = () => { - const { item } = this.props; - this.props.filterByUser(item.email); - }; - - filterByRepo = () => { - const { item } = this.props; - this.props.filterByRepo(item.repo_id); - }; - toggleFreezeItem = (freezed) => { this.props.toggleFreezeItem(freezed); if (!freezed) { this.setState({ isHighlighted: false, - isOpIconShown: false }); } }; render() { - const { isHighlighted, isOpIconShown } = this.state; - const { item, userFilteredBy, repoFilteredBy } = this.props; + const { isHighlighted } = this.state; + const { item } = this.props; return ( @@ -230,10 +171,6 @@ Item.propTypes = { item: PropTypes.object, isFreezed: PropTypes.bool, toggleFreezeItem: PropTypes.func, - userFilteredBy: PropTypes.string, - repoFilteredBy: PropTypes.string, - filterByUser: PropTypes.func, - filterByRepo: PropTypes.func, }; class FileAccessLogs extends Component { @@ -248,6 +185,11 @@ class FileAccessLogs extends Component { currentPage: 1, hasNextPage: false, isExportExcelDialogOpen: false, + availableUsers: [], + selectedUsers: [], + availableRepos: [], + selectedRepos: [], + openSelector: null, }; this.initPage = 1; } @@ -262,16 +204,16 @@ class FileAccessLogs extends Component { this.setState({ perPage: parseInt(urlParams.get('per_page') || perPage), currentPage: parseInt(urlParams.get('page') || currentPage), - userFilteredBy: urlParams.get('email'), - repoFilteredBy: urlParams.get('repo_id') }, () => { this.getLogsByPage(this.state.currentPage); }); } getLogsByPage = (page) => { - const { perPage, userFilteredBy, repoFilteredBy } = this.state; - systemAdminAPI.sysAdminListFileAccessLogs(page, perPage, userFilteredBy, repoFilteredBy).then((res) => { + const { perPage, selectedUsers, selectedRepos } = this.state; + let emails = selectedUsers.map(user => user.email); + let repos = selectedRepos.map(repo => repo.id); + systemAdminAPI.sysAdminListFileAccessLogs(page, perPage, { 'email': emails, 'repo': repos }).then((res) => { this.setState({ logList: res.data.file_access_log_list, loading: false, @@ -304,30 +246,87 @@ class FileAccessLogs extends Component { navigate(url.toString()); }; - filterByUser = (email) => { + handleUserFilter = (user, shouldFetchData = true) => { + const { selectedUsers } = this.state; + let newSelectedUsers; + + if (user === null) { + newSelectedUsers = selectedUsers; + } else { + const isSelected = selectedUsers.find(item => item.email === user.email); + if (isSelected) { + newSelectedUsers = selectedUsers.filter(item => item.email !== user.email); + } else { + newSelectedUsers = [...selectedUsers, user]; + } + } + this.setState({ - userFilteredBy: email + selectedUsers: newSelectedUsers, + currentPage: 1 }, () => { - this.getLogsByPage(this.initPage); - this.updateURL({ 'email': email }); + if (shouldFetchData) { + this.getLogsByPage(1); + } }); }; - filterByRepo = (repoID) => { + handleRepoFilter = (repo, shouldFetchData = true) => { + const { selectedRepos } = this.state; + let newSelectedRepos; + + if (repo === null) { + newSelectedRepos = []; + } else { + const isSelected = selectedRepos.find(item => item.id === repo.id); + if (isSelected) { + newSelectedRepos = selectedRepos.filter(item => item.id !== repo.id); + } else { + newSelectedRepos = [...selectedRepos, repo]; + } + } + this.setState({ - repoFilteredBy: repoID + selectedRepos: newSelectedRepos, + currentPage: 1 }, () => { - this.getLogsByPage(this.initPage); - this.updateURL({ 'repo_id': repoID }); + if (shouldFetchData) { + this.getLogsByPage(1); + } }); }; + handleSelectorToggle = (selectorType) => { + const { openSelector } = this.state; + const wasOpen = openSelector === selectorType; + + this.setState({ + openSelector: wasOpen ? null : selectorType + }, () => { + if (wasOpen) { + this.getLogsByPage(1); + } + }); + }; + + searchUsers = (value) => { + return systemAdminAPI.sysAdminSearchUsers(value); + }; + + searchRepos = (value) => { + return systemAdminAPI.sysAdminSearchRepos(value); + }; + render() { const { logList, - userFilteredBy, repoFilteredBy, currentPage, perPage, hasNextPage, - isExportExcelDialogOpen + isExportExcelDialogOpen, + availableUsers, + selectedUsers, + availableRepos, + selectedRepos, + openSelector } = this.state; return ( @@ -338,20 +337,38 @@ class FileAccessLogs extends Component {
    - + +
    + this.handleSelectorToggle('user')} + searchUsersFunc={this.searchUsers} + /> +
    + this.handleSelectorToggle('repo')} + searchReposFunc={this.searchRepos} + /> +
    + +
    diff --git a/frontend/src/pages/sys-admin/logs-page/file-transfer-log.js b/frontend/src/pages/sys-admin/logs-page/file-transfer-log.js index 2ecc60394c..0f6758504b 100644 --- a/frontend/src/pages/sys-admin/logs-page/file-transfer-log.js +++ b/frontend/src/pages/sys-admin/logs-page/file-transfer-log.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { Link } from '@gatsbyjs/reach-router'; import dayjs from 'dayjs'; import relativeTime from 'dayjs/plugin/relativeTime'; -import { seafileAPI } from '../../../utils/seafile-api'; +import { systemAdminAPI } from '../../../utils/system-admin-api'; import { gettext, siteRoot } from '../../../utils/constants'; import { Utils } from '../../../utils/utils'; import EmptyTip from '../../../components/empty-tip'; @@ -12,6 +12,8 @@ import Paginator from '../../../components/paginator'; import MainPanelTopbar from '../main-panel-topbar'; import UserLink from '../user-link'; import LogsNav from './logs-nav'; +import LogUserSelector from '../../dashboard/log-user-selector'; +import LogRepoSelector from '../../dashboard/log-repo-selector'; dayjs.extend(relativeTime); @@ -162,6 +164,14 @@ class FIleTransferLogs extends Component { perPage: 100, currentPage: 1, hasNextPage: false, + availableUsers: [], + selectedFromUsers: [], + selectedToUsers: [], + selectedToGroups: [], + selectedOperators: [], + openSelector: null, + availableRepos: [], + selectedRepos: [], }; this.initPage = 1; } @@ -178,8 +188,28 @@ class FIleTransferLogs extends Component { } getLogsByPage = (page) => { - let { perPage } = this.state; - seafileAPI.sysAdminListFileTransferLogs(page, perPage).then((res) => { + let { + perPage, + selectedFromUsers, + selectedToUsers, + selectedToGroups, + selectedOperators, + selectedRepos + } = this.state; + + const options = { + 'from_email': selectedFromUsers.filter(item => item.email).map(user => user.email), + 'from_group': selectedFromUsers.filter(item => !item.email).map(group => group.id), + 'to_email': selectedToUsers.map(user => user.email), + 'to_group': selectedToGroups.map(group => group.to_group_id || group.id), + 'operator_email': selectedOperators.map(user => user.email), + 'repo': selectedRepos.map(repo => repo.id) + }; + systemAdminAPI.sysAdminListFileTransferLogs( + page, + perPage, + options + ).then((res) => { this.setState({ logList: res.data.repo_transfer_log_list, loading: false, @@ -189,7 +219,8 @@ class FIleTransferLogs extends Component { }).catch((error) => { this.setState({ loading: false, - errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403 + currentPage: page, + errorMsg: Utils.getErrorMsg(error, true) }); }); }; @@ -200,8 +231,196 @@ class FIleTransferLogs extends Component { }, () => this.getLogsByPage(this.initPage)); }; + handleFromUserFilter = (item, shouldFetchData = true) => { + const { selectedFromUsers } = this.state; + let newSelectedUsers; + + if (item === null) { + newSelectedUsers = selectedFromUsers; + } else { + if (item.email) { + const isSelected = selectedFromUsers.find(user => user.email === item.email); + if (isSelected) { + newSelectedUsers = selectedFromUsers.filter(user => user.email !== item.email); + } else { + newSelectedUsers = [...selectedFromUsers, item]; + } + } else { + const groupId = item.id; + const isSelected = selectedFromUsers.find(group => group.id === groupId); + if (isSelected) { + newSelectedUsers = selectedFromUsers.filter(group => group.id !== groupId); + } else { + const groupItem = { + id: groupId, + name: item.name, + from_group_id: groupId, + from_group_name: item.name + }; + newSelectedUsers = [...selectedFromUsers, groupItem]; + } + } + } + + this.setState({ + selectedFromUsers: newSelectedUsers, + currentPage: 1 + }, () => { + if (shouldFetchData) { + this.getLogsByPage(1); + } + }); + }; + + handleToUserFilter = (item, shouldFetchData = true) => { + const { selectedToUsers, selectedToGroups } = this.state; + let newSelectedUsers = selectedToUsers; + let newSelectedGroups = selectedToGroups; + + if (item === null) { + newSelectedUsers = selectedToUsers; + newSelectedGroups = selectedToGroups; + } else { + if (item.email) { + const isSelected = selectedToUsers.find(user => user.email === item.email); + if (isSelected) { + newSelectedUsers = selectedToUsers.filter(user => user.email !== item.email); + } else { + newSelectedUsers = [...selectedToUsers, item]; + } + } else { + const groupId = item.to_group_id || item.id; + const groupName = item.to_group_name || item.name; + + const isSelected = selectedToGroups.find(group => { + const selectedGroupId = group.to_group_id || group.id; + return selectedGroupId === groupId; + }); + + if (isSelected) { + newSelectedGroups = selectedToGroups.filter(group => { + const selectedGroupId = group.to_group_id || group.id; + return selectedGroupId !== groupId; + }); + } else { + const groupItem = { + id: groupId, + name: groupName, + to_group_id: groupId, + to_group_name: groupName + }; + newSelectedGroups = [...selectedToGroups, groupItem]; + } + } + } + + this.setState({ + selectedToUsers: newSelectedUsers, + selectedToGroups: newSelectedGroups, + currentPage: 1 + }, () => { + if (shouldFetchData) { + this.getLogsByPage(1); + } + }); + }; + + handleOperatorFilter = (user, shouldFetchData = true) => { + const { selectedOperators } = this.state; + let newSelectedUsers; + + if (user === null) { + newSelectedUsers = selectedOperators; + } else { + const isSelected = selectedOperators.find(item => item.email === user.email); + if (isSelected) { + newSelectedUsers = selectedOperators.filter(item => item.email !== user.email); + } else { + newSelectedUsers = [...selectedOperators, user]; + } + } + + this.setState({ + selectedOperators: newSelectedUsers, + currentPage: 1 + }, () => { + if (shouldFetchData) { + this.getLogsByPage(1); + } + }); + }; + + handleSelectorToggle = (selectorType) => { + const { openSelector } = this.state; + const wasOpen = openSelector === selectorType; + + this.setState({ + openSelector: wasOpen ? null : selectorType + }, () => { + if (wasOpen) { + this.getLogsByPage(1); + } + }); + }; + + + handleRepoFilter = (repo, shouldFetchData = true) => { + const { selectedRepos } = this.state; + let newSelectedRepos; + + if (repo === null) { + newSelectedRepos = []; + } else { + const isSelected = selectedRepos.find(item => item.id === repo.id); + if (isSelected) { + newSelectedRepos = selectedRepos.filter(item => item.id !== repo.id); + } else { + newSelectedRepos = [...selectedRepos, repo]; + } + } + + this.setState({ + selectedRepos: newSelectedRepos, + currentPage: 1 + }, () => { + if (shouldFetchData) { + this.getLogsByPage(1); + } + }); + }; + + searchUsers = (value) => { + return systemAdminAPI.sysAdminSearchUsers(value); + }; + + searchGroups = (value) => { + return systemAdminAPI.sysAdminSearchGroups(value); + }; + + searchRepos = (value) => { + return systemAdminAPI.sysAdminSearchRepos(value); + }; + render() { - let { logList, currentPage, perPage, hasNextPage } = this.state; + let { + logList, currentPage, perPage, hasNextPage, + availableUsers, selectedFromUsers, + selectedToUsers, selectedToGroups, + selectedOperators, + availableRepos, selectedRepos, + openSelector + } = this.state; + + const selectedToItems = [ + ...selectedToUsers, + ...selectedToGroups.map(group => ({ + id: group.to_group_id || group.id, + name: group.to_group_name || group.name, + to_group_id: group.to_group_id || group.id, + to_group_name: group.to_group_name || group.name + })) + ]; + return ( @@ -209,16 +428,58 @@ class FIleTransferLogs extends Component {
    - + +
    + this.handleSelectorToggle('fromUser')} + searchUsersFunc={this.searchUsers} + searchGroupsFunc={this.searchGroups} + /> + this.handleSelectorToggle('toUser')} + searchUsersFunc={this.searchUsers} + searchGroupsFunc={this.searchGroups} + /> + this.handleSelectorToggle('operator')} + searchUsersFunc={this.searchUsers} + /> +
    + this.handleSelectorToggle('repo')} + searchReposFunc={this.searchRepos} + /> +
    + +
    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 df7d22aeba..caeb7470a6 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 @@ -15,6 +15,8 @@ import UserLink from '../user-link'; import ModalPortal from '../../../components/modal-portal'; import CommitDetails from '../../../components/dialog/commit-details'; import LogsExportExcelDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-logs-export-excel-dialog'; +import LogUserSelector from '../../dashboard/log-user-selector'; +import LogRepoSelector from '../../dashboard/log-repo-selector'; dayjs.extend(relativeTime); @@ -173,6 +175,11 @@ class FileUpdateLogs extends Component { currentPage: 1, hasNextPage: false, isExportExcelDialogOpen: false, + availableUsers: [], + selectedUsers: [], + availableRepos: [], + selectedRepos: [], + openSelector: null, }; this.initPage = 1; } @@ -193,8 +200,10 @@ class FileUpdateLogs extends Component { } getLogsByPage = (page) => { - let { perPage } = this.state; - systemAdminAPI.sysAdminListFileUpdateLogs(page, perPage).then((res) => { + let { perPage, selectedUsers, selectedRepos } = this.state; + let emails = selectedUsers.map(user => user.email); + let repos = selectedRepos.map(repo => repo.id); + systemAdminAPI.sysAdminListFileUpdateLogs(page, perPage, { 'email': emails, 'repo': repos }).then((res) => { this.setState({ logList: res.data.file_update_log_list, loading: false, @@ -215,8 +224,80 @@ class FileUpdateLogs extends Component { }, () => this.getLogsByPage(this.initPage)); }; + + handleUserFilter = (user, shouldFetchData = true) => { + const { selectedUsers } = this.state; + let newSelectedUsers; + + if (user === null) { + newSelectedUsers = selectedUsers; + } else { + const isSelected = selectedUsers.find(item => item.email === user.email); + if (isSelected) { + newSelectedUsers = selectedUsers.filter(item => item.email !== user.email); + } else { + newSelectedUsers = [...selectedUsers, user]; + } + } + + this.setState({ + selectedUsers: newSelectedUsers, + currentPage: 1 + }, () => { + if (shouldFetchData) { + this.getLogsByPage(1); + } + }); + }; + + handleSelectorToggle = (selectorType) => { + const { openSelector } = this.state; + const wasOpen = openSelector === selectorType; + + this.setState({ + openSelector: wasOpen ? null : selectorType + }, () => { + if (wasOpen) { + this.getLogsByPage(1); + } + }); + }; + + handleRepoFilter = (repo, shouldFetchData = true) => { + const { selectedRepos } = this.state; + let newSelectedRepos; + + if (repo === null) { + newSelectedRepos = selectedRepos; + } else { + const isSelected = selectedRepos.find(item => item.id === repo.id); + if (isSelected) { + newSelectedRepos = selectedRepos.filter(item => item.id !== repo.id); + } else { + newSelectedRepos = [...selectedRepos, repo]; + } + } + + this.setState({ + selectedRepos: newSelectedRepos, + currentPage: 1 + }, () => { + if (shouldFetchData) { + this.getLogsByPage(1); + } + }); + }; + + searchUsers = (value) => { + return systemAdminAPI.sysAdminSearchUsers(value); + }; + + searchRepos = (value) => { + return systemAdminAPI.sysAdminSearchRepos(value); + }; + render() { - let { logList, currentPage, perPage, hasNextPage, isExportExcelDialogOpen } = this.state; + let { logList, currentPage, perPage, hasNextPage, isExportExcelDialogOpen, availableUsers, selectedUsers, availableRepos, selectedRepos } = this.state; return ( @@ -226,16 +307,38 @@ class FileUpdateLogs extends Component {
    - + +
    + this.handleSelectorToggle('user')} + searchUsersFunc={this.searchUsers} + /> +
    + this.handleSelectorToggle('repo')} + searchReposFunc={this.searchRepos} + /> +
    + +
    diff --git a/frontend/src/pages/sys-admin/logs-page/group-member-audit-logs.js b/frontend/src/pages/sys-admin/logs-page/group-member-audit-logs.js index bccb786ee8..efd0da5fff 100644 --- a/frontend/src/pages/sys-admin/logs-page/group-member-audit-logs.js +++ b/frontend/src/pages/sys-admin/logs-page/group-member-audit-logs.js @@ -3,15 +3,16 @@ import PropTypes from 'prop-types'; import { Link } from '@gatsbyjs/reach-router'; import dayjs from 'dayjs'; import relativeTime from 'dayjs/plugin/relativeTime'; -import { seafileAPI } from '../../../utils/seafile-api'; import { gettext, siteRoot } from '../../../utils/constants'; import { Utils } from '../../../utils/utils'; +import { systemAdminAPI } from '../../../utils/system-admin-api'; import EmptyTip from '../../../components/empty-tip'; import Loading from '../../../components/loading'; import Paginator from '../../../components/paginator'; import MainPanelTopbar from '../main-panel-topbar'; import UserLink from '../user-link'; import LogsNav from './logs-nav'; +import LogUserSelector from '../../dashboard/log-user-selector'; dayjs.extend(relativeTime); @@ -154,6 +155,11 @@ class GroupMemberAuditLogs extends Component { perPage: 100, currentPage: 1, hasNextPage: false, + availableUsers: [], + selectedUsers: [], + selectedOperators: [], + selectedGroups: [], + openSelector: null, }; this.initPage = 1; } @@ -170,8 +176,15 @@ class GroupMemberAuditLogs extends Component { } getLogsByPage = (page) => { - let { perPage } = this.state; - seafileAPI.sysAdminListGroupInviteLogs(page, perPage).then((res) => { + let { perPage, selectedUsers, selectedOperators, selectedGroups } = this.state; + + const emails = { + 'user_email': selectedUsers.map(user => user.email), + 'operator_email': selectedOperators.map(user => user.email), + 'group_id': selectedGroups.map(group => group.id) + }; + + systemAdminAPI.sysAdminListGroupInviteLogs(page, perPage, emails).then((res) => { this.setState({ logList: res.data.group_invite_log_list, loading: false, @@ -186,6 +199,102 @@ class GroupMemberAuditLogs extends Component { }); }; + handleUserFilter = (user, shouldFetchData = true) => { + const { selectedUsers } = this.state; + let newSelectedUsers; + + if (user === null) { + newSelectedUsers = selectedUsers; + } else { + const isSelected = selectedUsers.find(item => item.email === user.email); + if (isSelected) { + newSelectedUsers = selectedUsers.filter(item => item.email !== user.email); + } else { + newSelectedUsers = [...selectedUsers, user]; + } + } + + this.setState({ + selectedUsers: newSelectedUsers, + currentPage: 1 + }, () => { + if (shouldFetchData) { + this.getLogsByPage(1); + } + }); + }; + + handleOperatorFilter = (user, shouldFetchData = true) => { + const { selectedOperators } = this.state; + let newSelectedUsers; + + if (user === null) { + newSelectedUsers = selectedOperators; + } else { + const isSelected = selectedOperators.find(item => item.email === user.email); + if (isSelected) { + newSelectedUsers = selectedOperators.filter(item => item.email !== user.email); + } else { + newSelectedUsers = [...selectedOperators, user]; + } + } + + this.setState({ + selectedOperators: newSelectedUsers, + currentPage: 1 + }, () => { + if (shouldFetchData) { + this.getLogsByPage(1); + } + }); + }; + + handleGroupFilter = (group, shouldFetchData = true) => { + const { selectedGroups } = this.state; + let newSelectedGroups; + + if (group === null) { + newSelectedGroups = selectedGroups; + } else { + const isSelected = selectedGroups.find(item => item.id === group.id); + if (isSelected) { + newSelectedGroups = selectedGroups.filter(item => item.id !== group.id); + } else { + newSelectedGroups = [...selectedGroups, group]; + } + } + + this.setState({ + selectedGroups: newSelectedGroups, + currentPage: 1 + }, () => { + if (shouldFetchData) { + this.getLogsByPage(1); + } + }); + }; + + handleSelectorToggle = (selectorType) => { + const { openSelector } = this.state; + const wasOpen = openSelector === selectorType; + + this.setState({ + openSelector: wasOpen ? null : selectorType + }, () => { + if (wasOpen) { + this.getLogsByPage(1); + } + }); + }; + + searchUsers = (value) => { + return systemAdminAPI.sysAdminSearchUsers(value); + }; + + searchGroups = (value) => { + return systemAdminAPI.sysAdminSearchGroups(value); + }; + resetPerPage = (newPerPage) => { this.setState({ perPage: newPerPage, @@ -193,7 +302,12 @@ class GroupMemberAuditLogs extends Component { }; render() { - let { logList, currentPage, perPage, hasNextPage } = this.state; + let { + logList, currentPage, perPage, hasNextPage, + availableUsers, selectedUsers, selectedOperators, selectedGroups, + openSelector + } = this.state; + return ( @@ -201,16 +315,47 @@ class GroupMemberAuditLogs extends Component {
    - + +
    + this.handleSelectorToggle('user')} + searchUsersFunc={this.searchUsers} + /> + this.handleSelectorToggle('group')} + searchGroupsFunc={this.searchGroups} + /> + this.handleSelectorToggle('operator')} + searchUsersFunc={this.searchUsers} + /> +
    + +
    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 28083cb867..4bcead7b5f 100644 --- a/frontend/src/pages/sys-admin/logs-page/login-logs.js +++ b/frontend/src/pages/sys-admin/logs-page/login-logs.js @@ -14,6 +14,7 @@ import MainPanelTopbar from '../main-panel-topbar'; import UserLink from '../user-link'; import LogsExportExcelDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-logs-export-excel-dialog'; import ModalPortal from '../../../components/modal-portal'; +import LogUserSelector from '../../dashboard/log-user-selector'; dayjs.extend(relativeTime); @@ -138,6 +139,9 @@ class LoginLogs extends Component { currentPage: 1, hasNextPage: false, isExportExcelDialogOpen: false, + availableUsers: [], + selectedUsers: [], + isUserSelectorOpen: false, }; this.initPage = 1; } @@ -158,8 +162,10 @@ class LoginLogs extends Component { } getLogsByPage = (page) => { - let { perPage } = this.state; - systemAdminAPI.sysAdminListLoginLogs(page, perPage).then((res) => { + let { perPage, selectedUsers } = this.state; + let emails = selectedUsers.map(user => user.email); + + systemAdminAPI.sysAdminListLoginLogs(page, perPage, { 'email': emails }).then((res) => { this.setState({ logList: res.data.login_log_list, loading: false, @@ -180,8 +186,44 @@ class LoginLogs extends Component { }, () => this.getLogsByPage(this.initPage)); }; + handleUserFilter = (user) => { + const { selectedUsers } = this.state; + let newSelectedUsers; + + if (user === null) { + newSelectedUsers = selectedUsers; + } else { + const isSelected = selectedUsers.find(item => item.email === user.email); + if (isSelected) { + newSelectedUsers = selectedUsers.filter(item => item.email !== user.email); + } else { + newSelectedUsers = [...selectedUsers, user]; + } + } + + this.setState({ + selectedUsers: newSelectedUsers, + currentPage: 1 + }); + }; + + toggleUserSelector = () => { + const { isUserSelectorOpen } = this.state; + this.setState({ + isUserSelectorOpen: !isUserSelectorOpen + }, () => { + if (!this.state.isUserSelectorOpen) { + this.getLogsByPage(1); + } + }); + }; + + searchUsers = (value) => { + return systemAdminAPI.sysAdminSearchUsers(value); + }; + render() { - let { logList, currentPage, perPage, hasNextPage, isExportExcelDialogOpen } = this.state; + let { logList, currentPage, perPage, hasNextPage, isExportExcelDialogOpen, availableUsers, selectedUsers } = this.state; return ( @@ -191,16 +233,27 @@ class LoginLogs extends Component {
    - + + + +
    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 a6be3a3f33..2ceadfac1d 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 @@ -15,6 +15,8 @@ import Paginator from '../../../components/paginator'; import MainPanelTopbar from '../main-panel-topbar'; import UserLink from '../user-link'; import LogsNav from './logs-nav'; +import LogUserSelector from '../../dashboard/log-user-selector'; +import LogRepoSelector from '../../dashboard/log-repo-selector'; dayjs.extend(relativeTime); @@ -151,6 +153,13 @@ class SharePermissionLogs extends Component { currentPage: 1, hasNextPage: false, isExportExcelDialogOpen: false, + availableUsers: [], + selectedFromUsers: [], + selectedToUsers: [], + selectedToGroups: [], + availableRepos: [], + selectedRepos: [], + openSelector: null, }; this.initPage = 1; } @@ -171,8 +180,25 @@ class SharePermissionLogs extends Component { } getLogsByPage = (page) => { - let { perPage } = this.state; - systemAdminAPI.sysAdminListSharePermissionLogs(page, perPage).then((res) => { + let { + perPage, + selectedFromUsers, + selectedToUsers, + selectedToGroups, + selectedRepos + } = this.state; + + const options = { + 'from_email': selectedFromUsers.map(user => user.email), + 'to_email': selectedToUsers.map(user => user.email), + 'to_group': selectedToGroups.map(group => group.id), + 'repo': selectedRepos.map(repo => repo.id) + }; + systemAdminAPI.sysAdminListSharePermissionLogs( + page, + perPage, + options + ).then((res) => { this.setState({ logList: res.data.share_permission_log_list, loading: false, @@ -182,7 +208,7 @@ class SharePermissionLogs extends Component { }).catch((error) => { this.setState({ loading: false, - errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403 + errorMsg: Utils.getErrorMsg(error, true) }); }); }; @@ -193,8 +219,124 @@ class SharePermissionLogs extends Component { }, () => this.getLogsByPage(this.initPage)); }; + handleFromUserFilter = (user, shouldFetchData = true) => { + const { selectedFromUsers } = this.state; + let newSelectedUsers; + + if (user === null) { + newSelectedUsers = selectedFromUsers; + } else { + const isSelected = selectedFromUsers.find(item => item.email === user.email); + if (isSelected) { + newSelectedUsers = selectedFromUsers.filter(item => item.email !== user.email); + } else { + newSelectedUsers = [...selectedFromUsers, user]; + } + } + + this.setState({ + selectedFromUsers: newSelectedUsers, + currentPage: 1 + }, () => { + if (shouldFetchData) { + this.getLogsByPage(1); + } + }); + }; + + handleToUserFilter = (item, shouldFetchData = true) => { + const { selectedToUsers, selectedToGroups } = this.state; + let newSelectedUsers = selectedToUsers; + let newSelectedGroups = selectedToGroups; + + if (item === null) { + newSelectedUsers = selectedToUsers; + newSelectedGroups = selectedToGroups; + } else { + if (item.email) { + const isSelected = selectedToUsers.find(user => user.email === item.email); + if (isSelected) { + newSelectedUsers = selectedToUsers.filter(user => user.email !== item.email); + } else { + newSelectedUsers = [...selectedToUsers, item]; + } + } else { + const isSelected = selectedToGroups.find(group => group.id === item.id); + if (isSelected) { + newSelectedGroups = selectedToGroups.filter(group => group.id !== item.id); + } else { + newSelectedGroups = [...selectedToGroups, item]; + } + } + } + + this.setState({ + selectedToUsers: newSelectedUsers, + selectedToGroups: newSelectedGroups, + currentPage: 1 + }, () => { + if (shouldFetchData) { + this.getLogsByPage(1); + } + }); + }; + + handleSelectorToggle = (selectorType) => { + const { openSelector } = this.state; + const wasOpen = openSelector === selectorType; + + this.setState({ + openSelector: wasOpen ? null : selectorType + }, () => { + if (wasOpen) { + this.getLogsByPage(1); + } + }); + }; + + handleRepoFilter = (repo, shouldFetchData = true) => { + const { selectedRepos } = this.state; + let newSelectedRepos; + + if (repo === null) { + newSelectedRepos = selectedRepos; + } else { + const isSelected = selectedRepos.find(item => item.id === repo.id); + if (isSelected) { + newSelectedRepos = selectedRepos.filter(item => item.id !== repo.id); + } else { + newSelectedRepos = [...selectedRepos, repo]; + } + } + + this.setState({ + selectedRepos: newSelectedRepos, + currentPage: 1 + }, () => { + if (shouldFetchData) { + this.getLogsByPage(1); + } + }); + }; + + searchUsers = (value) => { + return systemAdminAPI.sysAdminSearchUsers(value); + }; + + searchRepos = (value) => { + return systemAdminAPI.sysAdminSearchRepos(value); + }; + + searchGroups = (value) => { + return systemAdminAPI.sysAdminSearchGroups(value); + }; + render() { - let { logList, currentPage, perPage, hasNextPage, isExportExcelDialogOpen } = this.state; + let { + logList, currentPage, perPage, hasNextPage, isExportExcelDialogOpen, + availableUsers, selectedFromUsers, selectedToUsers, + selectedToGroups, availableRepos, selectedRepos, openSelector + } = this.state; return ( @@ -204,16 +346,48 @@ class SharePermissionLogs extends Component {
    - + +
    + this.handleSelectorToggle('fromUser')} + searchUsersFunc={this.searchUsers} + /> + this.handleSelectorToggle('toUser')} + searchUsersFunc={this.searchUsers} + searchGroupsFunc={this.searchGroups} + /> +
    + this.handleSelectorToggle('repo')} + searchReposFunc={this.searchRepos} + /> +
    + +
    diff --git a/frontend/src/utils/seafile-api.js b/frontend/src/utils/seafile-api.js index 9cc437c4f5..3803f94348 100644 --- a/frontend/src/utils/seafile-api.js +++ b/frontend/src/utils/seafile-api.js @@ -2207,24 +2207,6 @@ class SeafileAPI { return this._sendPostRequest(url, formData); } - sysAdminListFileTransferLogs(page, perPage) { - const url = this.server + '/api/v2.1/admin/logs/repo-transfer-logs/'; - let params = { - page: page, - per_page: perPage - }; - return this.req.get(url, { params: params }); - } - - sysAdminListGroupInviteLogs(page, perPage) { - const url = this.server + '/api/v2.1/admin/logs/group-member-audit/'; - let params = { - page: page, - per_page: perPage - }; - return this.req.get(url, { params: params }); - } - } let seafileAPI = new SeafileAPI(); diff --git a/frontend/src/utils/system-admin-api.js b/frontend/src/utils/system-admin-api.js index 47579f8038..31fa1cf76b 100644 --- a/frontend/src/utils/system-admin-api.js +++ b/frontend/src/utils/system-admin-api.js @@ -680,46 +680,166 @@ class SystemAdminAPI { return this.req.get(url); } - sysAdminListLoginLogs(page, perPage) { + sysAdminListLoginLogs(page, perPage, options = {}) { const url = this.server + '/api/v2.1/admin/logs/login-logs/'; let params = { page: page, - per_page: perPage + per_page: perPage, + ...options }; - return this.req.get(url, { params: params }); + return this.req.get(url, { + params: params, + paramsSerializer: { + serialize: function (params) { + let list = []; + for (let key in params) { + if (Array.isArray(params[key])) { + for (let i = 0, len = params[key].length; i < len; i++) { + list.push(key + '=' + encodeURIComponent(params[key][i])); + } + } else { + list.push(key + '=' + encodeURIComponent(params[key])); + } + } + return list.join('&'); + } + } + }); } - sysAdminListFileAccessLogs(page, perPage, email, repoID) { + sysAdminListFileAccessLogs(page, perPage, options = {}) { const url = this.server + '/api/v2.1/admin/logs/file-access-logs/'; let params = { page: page, - per_page: perPage + per_page: perPage, + ...options }; - if (email != undefined) { - params.email = email; - } - if (repoID != undefined) { - params.repo_id = repoID; - } - return this.req.get(url, { params: params }); + return this.req.get(url, { + params: params, + paramsSerializer: { + serialize: function (params) { + let list = []; + for (let key in params) { + if (Array.isArray(params[key])) { + for (let i = 0, len = params[key].length; i < len; i++) { + list.push(key + '=' + encodeURIComponent(params[key][i])); + } + } else { + list.push(key + '=' + encodeURIComponent(params[key])); + } + } + return list.join('&'); + } + } + }); } - sysAdminListFileUpdateLogs(page, perPage) { + sysAdminListFileUpdateLogs(page, perPage, options = {}) { const url = this.server + '/api/v2.1/admin/logs/file-update-logs/'; let params = { page: page, - per_page: perPage + per_page: perPage, + ...options }; - return this.req.get(url, { params: params }); + return this.req.get(url, { + params: params, + paramsSerializer: { + serialize: function (params) { + let list = []; + for (let key in params) { + if (Array.isArray(params[key])) { + for (let i = 0, len = params[key].length; i < len; i++) { + list.push(key + '=' + encodeURIComponent(params[key][i])); + } + } else { + list.push(key + '=' + encodeURIComponent(params[key])); + } + } + return list.join('&'); + } + } + }); } - sysAdminListSharePermissionLogs(page, perPage) { + sysAdminListSharePermissionLogs(page, perPage, options = {}) { const url = this.server + '/api/v2.1/admin/logs/share-permission-logs/'; let params = { page: page, - per_page: perPage + per_page: perPage, + ...options }; - return this.req.get(url, { params: params }); + return this.req.get(url, { + params: params, + paramsSerializer: { + serialize: function (params) { + let list = []; + for (let key in params) { + if (Array.isArray(params[key])) { + for (let i = 0, len = params[key].length; i < len; i++) { + list.push(key + '=' + encodeURIComponent(params[key][i])); + } + } else { + list.push(key + '=' + encodeURIComponent(params[key])); + } + } + return list.join('&'); + } + } + }); + } + + sysAdminListFileTransferLogs(page, perPage, options = {}) { + const url = this.server + '/api/v2.1/admin/logs/repo-transfer-logs/'; + let params = { + page: page, + per_page: perPage, + ...options + }; + return this.req.get(url, { + params: params, + paramsSerializer: { + serialize: function (params) { + let list = []; + for (let key in params) { + if (Array.isArray(params[key])) { + for (let i = 0, len = params[key].length; i < len; i++) { + list.push(key + '=' + encodeURIComponent(params[key][i])); + } + } else { + list.push(key + '=' + encodeURIComponent(params[key])); + } + } + return list.join('&'); + } + } + }); + } + + sysAdminListGroupInviteLogs(page, perPage, options = {}) { + const url = this.server + '/api/v2.1/admin/logs/group-member-audit/'; + let params = { + page: page, + per_page: perPage, + ...options + }; + return this.req.get(url, { + params: params, + paramsSerializer: { + serialize: function (params) { + let list = []; + for (let key in params) { + if (Array.isArray(params[key])) { + for (let i = 0, len = params[key].length; i < len; i++) { + list.push(key + '=' + encodeURIComponent(params[key][i])); + } + } else { + list.push(key + '=' + encodeURIComponent(params[key])); + } + } + return list.join('&'); + } + } + }); } sysAdminExportLogsExcel(start, end, logType) { diff --git a/seahub/api2/endpoints/admin/logs.py b/seahub/api2/endpoints/admin/logs.py index e5172ea85a..24a871f547 100644 --- a/seahub/api2/endpoints/admin/logs.py +++ b/seahub/api2/endpoints/admin/logs.py @@ -6,7 +6,6 @@ from rest_framework.permissions import IsAdminUser from rest_framework.response import Response from rest_framework.views import APIView from rest_framework import status - from seaserv import ccnet_api, seafile_api from seahub.api2.authentication import TokenAuthentication @@ -16,8 +15,8 @@ from seahub.api2.utils import api_error from seahub.base.templatetags.seahub_tags import email2nickname, \ email2contact_email, translate_commit_desc -from seahub.utils import get_file_audit_events, generate_file_audit_event_type, \ - get_file_update_events, get_perm_audit_events, is_valid_email +from seahub.utils import get_log_events_by_users_and_repos, generate_file_audit_event_type, \ + is_valid_email from seahub.utils.timeutils import datetime_to_isoformat_timestr, utc_datetime_to_isoformat_timestr from seahub.utils.repo import is_valid_repo_id_format from seahub.base.models import RepoTransfer, GroupMemberAudit @@ -48,13 +47,17 @@ class AdminLogsLoginLogs(APIView): except ValueError: current_page = 1 per_page = 100 - + emails = request.GET.getlist('email') start = (current_page - 1) * per_page end = start + per_page from seahub.sysadmin_extra.models import UserLoginLog - logs = UserLoginLog.objects.all().order_by('-login_date')[start:end] - count = UserLoginLog.objects.all().count() + if emails: + logs = UserLoginLog.objects.filter(username__in=emails).order_by('-login_date')[start:end] + count = UserLoginLog.objects.filter(username__in=emails).count() + else: + logs = UserLoginLog.objects.all().order_by('-login_date')[start:end] + count = UserLoginLog.objects.all().count() # Use dict to reduce memcache fetch cost in large for-loop. nickname_dict = {} @@ -109,32 +112,22 @@ class AdminLogsFileAccessLogs(APIView): current_page = 1 per_page = 100 - user_selected = request.GET.get('email', None) - if user_selected and not is_valid_email(user_selected): - error_msg = 'email %s invalid.' % user_selected - return api_error(status.HTTP_400_BAD_REQUEST, error_msg) - - repo_id_selected = request.GET.get('repo_id', None) - if repo_id_selected and not is_valid_repo_id_format(repo_id_selected): - error_msg = 'repo_id %s invalid.' % repo_id_selected - return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + emails = request.GET.getlist('email') + for user_selected in emails: + if not is_valid_email(user_selected): + error_msg = 'email %s invalid.' % user_selected + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + repos = request.GET.getlist('repo') + if repos: + for repo_selected in repos: + if not is_valid_repo_id_format(repo_selected): + error_msg = 'repo_id %s invalid.' % repo_selected + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) start = per_page * (current_page - 1) limit = per_page + 1 - if user_selected: - org_id = -1 - orgs = ccnet_api.get_orgs_by_user(user_selected) - if orgs: - org_id = orgs[0].org_id - elif repo_id_selected: - org_id = seafile_api.get_org_id_by_repo_id(repo_id_selected) - else: - org_id = 0 - - # org_id = 0, show all file audit - events = get_file_audit_events(user_selected, org_id, repo_id_selected, start, limit) or [] - + events = get_log_events_by_users_and_repos('file_audit', emails, repos, start, limit) or [] if len(events) > per_page: events = events[:per_page] has_next_page = True @@ -215,22 +208,23 @@ class AdminLogsFileUpdateLogs(APIView): current_page = 1 per_page = 100 - user_selected = request.GET.get('email', None) - if user_selected and not is_valid_email(user_selected): - error_msg = 'email %s invalid.' % user_selected - return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + emails = request.GET.getlist('email') + for user_selected in emails: + if not is_valid_email(user_selected): + error_msg = 'email %s invalid.' % user_selected + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) - repo_id_selected = request.GET.get('repo_id', None) - if repo_id_selected and not is_valid_repo_id_format(repo_id_selected): - error_msg = 'repo_id %s invalid.' % repo_id_selected - return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + repos = request.GET.getlist('repo') + if repos: + for repo_selected in repos: + if not is_valid_repo_id_format(repo_selected): + error_msg = 'repo_id %s invalid.' % repo_selected + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) start = per_page * (current_page - 1) limit = per_page - # org_id = 0, show all file audit - events = get_file_update_events(user_selected, 0, repo_id_selected, start, limit) or [] - + events = get_log_events_by_users_and_repos('file_update', emails, repos, start, limit) or [] has_next_page = True if len(events) == per_page else False # Use dict to reduce memcache fetch cost in large for-loop. @@ -301,22 +295,38 @@ class AdminLogsSharePermissionLogs(APIView): current_page = 1 per_page = 100 - user_selected = request.GET.get('email', None) - if user_selected and not is_valid_email(user_selected): - error_msg = 'email %s invalid.' % user_selected - return api_error(status.HTTP_400_BAD_REQUEST, error_msg) - repo_id_selected = request.GET.get('repo_id', None) - if repo_id_selected and not is_valid_repo_id_format(repo_id_selected): - error_msg = 'repo_id %s invalid.' % repo_id_selected - return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + from_emails = request.GET.getlist('from_email') + for user_selected in from_emails: + if not is_valid_email(user_selected): + error_msg = 'email %s invalid.' % user_selected + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + to_emails = request.GET.getlist('to_email') + for user_selected in to_emails: + if not is_valid_email(user_selected): + error_msg = 'email %s invalid.' % user_selected + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + to_groups = request.GET.getlist('to_group') + emails = { + 'from_emails': from_emails, + 'to_emails': to_emails, + 'to_groups': to_groups + } + + repos = request.GET.getlist('repo') + if repos: + for repo_selected in repos: + if not is_valid_repo_id_format(repo_selected): + error_msg = 'repo_id %s invalid.' % repo_selected + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) start = per_page * (current_page - 1) limit = per_page - # org_id = 0, show all file audit - events = get_perm_audit_events(user_selected, 0, repo_id_selected, start, limit) or [] + events = get_log_events_by_users_and_repos('perm_audit', emails, repos, start, limit) or [] has_next_page = True if len(events) == per_page else False # Use dict to reduce memcache fetch cost in large for-loop. @@ -456,8 +466,45 @@ class AdminLogsFileTransferLogs(APIView): error_msg = 'limit invalid' return api_error(status.HTTP_400_BAD_REQUEST, error_msg) - events = RepoTransfer.objects.all().order_by('-timestamp')[start:start+limit+1] - if len(events) > limit: + from_emails = request.GET.getlist('from_email') + for user_selected in from_emails: + if not is_valid_email(user_selected): + error_msg = 'email %s invalid.' % user_selected + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + to_emails = request.GET.getlist('to_email') + for user_selected in to_emails: + if not is_valid_email(user_selected): + error_msg = 'email %s invalid.' % user_selected + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + operator_emails = request.GET.getlist('operator_email') + for user_selected in operator_emails: + if not is_valid_email(user_selected): + error_msg = 'email %s invalid.' % user_selected + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + from_groups = request.GET.getlist('from_group') + from_groups = [group_id + '@seafile_group' for group_id in from_groups] + to_groups = request.GET.getlist('to_group') + to_groups = [group_id + '@seafile_group' for group_id in to_groups] + repos = request.GET.getlist('repo') + if repos: + for repo_selected in repos: + if not is_valid_repo_id_format(repo_selected): + error_msg = 'repo_id %s invalid.' % repo_selected + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + queryset = RepoTransfer.objects.all() + if from_emails or from_groups: + queryset = queryset.by_from_user(from_emails + from_groups) + if to_emails or to_groups: + queryset = queryset.by_to_user(to_emails + to_groups) + if operator_emails: + queryset = queryset.by_operator(operator_emails) + if repos: + queryset = queryset.by_repo_ids(repos) + events = queryset.order_by('-timestamp')[start:start+limit+1] + + if events and len(events) > limit: has_next_page = True events = events[:limit] else: @@ -598,7 +645,27 @@ class AdminLogGroupMemberAuditLogs(APIView): error_msg = 'limit invalid' return api_error(status.HTTP_400_BAD_REQUEST, error_msg) - events = GroupMemberAudit.objects.all().order_by('-timestamp')[start:start+limit+1] + user_emails = request.GET.getlist('user_email') + for user_selected in user_emails: + if not is_valid_email(user_selected): + error_msg = 'email %s invalid.' % user_selected + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + operator_emails = request.GET.getlist('operator_email') + for user_selected in operator_emails: + if not is_valid_email(user_selected): + error_msg = 'email %s invalid.' % user_selected + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + group_ids = request.GET.getlist('group_id') + + queryset = GroupMemberAudit.objects.all() + if user_emails: + queryset = queryset.by_users(user_emails) + if operator_emails: + queryset = queryset.by_operators(operator_emails) + if group_ids: + queryset = queryset.by_group_ids(group_ids) + + events = queryset.order_by('-timestamp')[start:start+limit+1] if len(events) > limit: has_next_page = True events = events[:limit] diff --git a/seahub/api2/endpoints/admin/users.py b/seahub/api2/endpoints/admin/users.py index e12b81dc43..a818649fbb 100644 --- a/seahub/api2/endpoints/admin/users.py +++ b/seahub/api2/endpoints/admin/users.py @@ -721,9 +721,11 @@ class AdminUsers(APIView): social_auth_user_dict[item.username] = [item] for user in users: + url, _, _ = api_avatar_url(user.email) profile = Profile.objects.get_profile_by_user(user.email) info = {} + info['avatar_url'] = url info['email'] = user.email info['name'] = email2nickname(user.email) info['contact_email'] = email2contact_email(user.email) @@ -1161,6 +1163,8 @@ class AdminSearchUser(APIView): has_appended.append(user.email) info = {} + url, is_default, date_uploaded = api_avatar_url(user.email) + info['avatar_url'] = url info['email'] = user.email info['name'] = email2nickname(user.email) info['contact_email'] = email2contact_email(user.email) diff --git a/seahub/base/models.py b/seahub/base/models.py index 5db21ae748..daae0a381c 100644 --- a/seahub/base/models.py +++ b/seahub/base/models.py @@ -466,6 +466,20 @@ class ClientSSOToken(models.Model): return super(ClientSSOToken, self).save(*args, **kwargs) +class RepoTransferQuerySet(models.QuerySet): + def by_from_user(self, from_users): + return self.filter(from_user__in=from_users) + + def by_to_user(self, to_users): + return self.filter(to__in=to_users) + + def by_operator(self, operators): + return self.filter(operator__in=operators) + + def by_repo_ids(self, repo_ids): + return self.filter(repo_id__in=repo_ids) + + class RepoTransfer(models.Model): repo_id = models.CharField(max_length=36) org_id = models.IntegerField(db_index=True) @@ -473,6 +487,7 @@ class RepoTransfer(models.Model): to = models.CharField(max_length=255) operator = models.CharField(max_length=255) timestamp = models.DateTimeField(default=timezone.now, db_index=True) + objects = RepoTransferQuerySet.as_manager() class Meta: db_table = 'RepoTransfer' @@ -482,6 +497,16 @@ class RepoTransfer(models.Model): GROUP_MEMBER_ADD = 'group_member_add' GROUP_MEMBER_DELETE = 'group_member_delete' +class GroupMemberAuditQuerySet(models.QuerySet): + def by_users(self, users): + return self.filter(user__in=users) + + def by_operators(self, operators): + return self.filter(operator__in=operators) + + def by_group_ids(self, group_ids): + return self.filter(group_id__in=group_ids) + class GroupMemberAudit(models.Model): org_id = models.IntegerField(db_index=True) group_id = models.IntegerField(db_index=True) @@ -489,6 +514,7 @@ class GroupMemberAudit(models.Model): operator = models.CharField(max_length=255, db_index=True) operation = models.CharField(max_length=128) timestamp = models.DateTimeField(default=timezone.now, db_index=True) + objects = GroupMemberAuditQuerySet.as_manager() class Meta: db_table = 'group_member_audit' diff --git a/seahub/utils/__init__.py b/seahub/utils/__init__.py index 5673c65bf4..f72f135f3b 100644 --- a/seahub/utils/__init__.py +++ b/seahub/utils/__init__.py @@ -684,7 +684,7 @@ if EVENTS_CONFIG_FILE: """Return log events list by start/end timestamp. (If no logs, return 'None') """ with _get_seafevents_session() as session: - events = seafevents_api.get_event_log_by_time(session, log_type, tstart, tend) + events = seafevents_api.get_events_by_time(session, log_type, tstart, tend) return events if events else None @@ -732,6 +732,12 @@ if EVENTS_CONFIG_FILE: events = seafevents_api.get_file_audit_events(session, email, org_id, repo_id, start, limit) return events if events else None + + def get_log_events_by_users_and_repos(type, emails, repo_ids, start, limit): + with _get_seafevents_session() as session: + events = seafevents_api.get_events_by_users_and_repos(session, type, emails, repo_ids, start, limit) + + return events if events else None def get_file_ops_stats_by_day(start, end, offset): """ return file audit record of sepcifiy time group by day. @@ -886,6 +892,8 @@ else: pass def get_file_audit_events(): pass + def get_log_events_by_users_and_repos(): + pass def get_file_ops_stats_by_day(): pass def get_org_file_ops_stats_by_day():
    - {isOpIconShown && !userFilteredBy && ( - - )} {item.event_type} {item.ip}{' / '}{item.device || '--'} {dayjs(item.time).fromNow()} {item.repo_name ? item.repo_name : gettext('Deleted')} - {isOpIconShown && item.repo_name && !repoFilteredBy && ( - - )} {item.file_or_dir_name}