diff --git a/frontend/src/components/dialog/file-access-log.js b/frontend/src/components/dialog/file-access-log.js new file mode 100644 index 0000000000..d706a739af --- /dev/null +++ b/frontend/src/components/dialog/file-access-log.js @@ -0,0 +1,143 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import moment from 'moment'; +import { Modal, ModalHeader, ModalBody } from 'reactstrap'; +import { Utils } from '../../utils/utils'; +import { gettext, siteRoot } from '../../utils/constants'; +import { fileAccessLogAPI } from '../../utils/file-access-log-api'; +import toaster from '../toast'; +import Loading from '../loading'; +import EmptyTip from '../empty-tip'; + +import '../../css/file-access-log.css'; + +moment.locale(window.app.config.lang); + +const propTypes = { + repoID: PropTypes.string.isRequired, + filePath: PropTypes.string.isRequired, + fileName: PropTypes.string.isRequired, + toggleDialog: PropTypes.func.isRequired +}; + +class FileAccessLog extends React.Component { + + constructor(props) { + super(props); + this.state = { + isLoading: true, // first loading + isLoadingMore: false, + items: [], + page: 1, + perPage: 100, + hasNextPage: false + }; + } + + componentDidMount() { + this.listFileAccessLog(this.state.page); + } + + listFileAccessLog = (page) => { + const { repoID, filePath } = this.props; + const { perPage, items } = this.state; + const avatarSize = 24 * 2; + fileAccessLogAPI.listFileAccessLog(repoID, filePath, page, perPage, avatarSize).then((res) => { + const { data: newItems } = res.data; + console.log(newItems); + this.setState({ + isLoading: false, + isLoadingMore: false, + page: page, + hasNextPage: newItems.length < perPage ? false : true, + items: items.concat(newItems) + }); + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + this.setState({ + isLoading: false, + isLoadingMore: false, + hasNextPage: false + }); + }); + }; + + handleScroll = (event) => { + // isLoadingMore: to avoid repeated request + const { page, hasNextPage, isLoadingMore } = this.state; + if (hasNextPage && !isLoadingMore) { + const clientHeight = event.target.clientHeight; + const scrollHeight = event.target.scrollHeight; + const scrollTop = event.target.scrollTop; + const isBottom = (clientHeight + scrollTop + 1 >= scrollHeight); + if (isBottom) { // scroll to the bottom + this.setState({ isLoadingMore: true }, () => { + this.listFileAccessLog(page + 1); + }); + } + } + }; + + render() { + const { + isLoading, hasNextPage, items + } = this.state; + + const { fileName } = this.props; + let title = gettext('{placeholder} Access Log'); + title = title.replace('{placeholder}', '' + Utils.HTMLescape(fileName) + ''); + + return ( + + + + + + {isLoading ? : ( + <> + {items.length > 0 ? ( + <> + + + + + + + + + + + {items.map((item, index) => { + return ( + + + + + + + ); + })} + +
{gettext('User')}{gettext('Type')}{gettext('IP')} / {gettext('Device Name')}{gettext('Date')}
+ + {item.email ? {item.name} : {gettext('Anonymous User')}} + {item.etype} + {`${item.ip}${item.device ? '/' + item.device : ''}`} + {moment(item.time).format('YYYY-MM-DD HH:mm:ss')}
+ {hasNextPage && } + + ) : ( + + )} + + )} +
+
+ ); + } +} + +FileAccessLog.propTypes = propTypes; + +export default FileAccessLog; diff --git a/frontend/src/components/dirent-grid-view/dirent-grid-view.js b/frontend/src/components/dirent-grid-view/dirent-grid-view.js index a255e19e59..f6a668579a 100644 --- a/frontend/src/components/dirent-grid-view/dirent-grid-view.js +++ b/frontend/src/components/dirent-grid-view/dirent-grid-view.js @@ -21,6 +21,7 @@ import CreateFile from '../dialog/create-file-dialog'; import CreateFolder from '../dialog/create-folder-dialog'; import LibSubFolderPermissionDialog from '../dialog/lib-sub-folder-permission-dialog'; import toaster from '../toast'; +import FileAccessLog from '../dialog/file-access-log'; import '../../css/grid-view.css'; @@ -72,6 +73,7 @@ class DirentGridView extends React.Component { imageItems: [], imageIndex: 0, // onmenuClick + isFileAccessLogDialogOpen: false, isShareDialogShow: false, isMoveDialogShow: false, isCopyDialogShow: false, @@ -377,7 +379,7 @@ class DirentGridView extends React.Component { this.onCreateFileToggle('.sdoc'); break; case 'Access Log': - this.onAccessLog(currentObject); + this.toggleFileAccessLogDialog(); break; case 'Properties': this.props.showDirentDetail('info'); @@ -526,10 +528,10 @@ class DirentGridView extends React.Component { location.href = url; }; - onAccessLog = (currentObject) => { - let filePath = this.getDirentPath(currentObject); - let path = siteRoot + 'repo/file-access/' + this.props.repoID + '/?p=' + encodeURIComponent(filePath) ; - window.open(path); + toggleFileAccessLogDialog = () => { + this.setState({ + isFileAccessLogDialogOpen: !this.state.isFileAccessLogDialogOpen + }); }; onOpenViaClient = (currentObject) => { @@ -905,6 +907,16 @@ class DirentGridView extends React.Component { /> )} + {this.state.isFileAccessLogDialogOpen && + + + + } ); } diff --git a/frontend/src/components/dirent-list-view/dirent-list-item.js b/frontend/src/components/dirent-list-view/dirent-list-item.js index 063316786b..300a6e135e 100644 --- a/frontend/src/components/dirent-list-view/dirent-list-item.js +++ b/frontend/src/components/dirent-list-view/dirent-list-item.js @@ -18,6 +18,7 @@ import ZipDownloadDialog from '../dialog/zip-download-dialog'; import EditFileTagDialog from '../dialog/edit-filetag-dialog'; import EditFileTagPopover from '../popover/edit-filetag-popover'; import LibSubFolderPermissionDialog from '../dialog/lib-sub-folder-permission-dialog'; +import FileAccessLog from '../dialog/file-access-log'; import toaster from '../toast'; import FileTag from './file-tag'; @@ -81,6 +82,7 @@ class DirentListItem extends React.Component { isOperationShow: false, highlight: false, isZipDialogOpen: false, + isFileAccessLogDialogOpen: false, isMoveDialogShow: false, isCopyDialogShow: false, isShareDialogShow: false, @@ -298,7 +300,7 @@ class DirentListItem extends React.Component { this.onHistory(); break; case 'Access Log': - this.onAccessLog(); + this.toggleFileAccessLogDialog(); break; case 'Properties': this.props.onDirentClick(this.props.dirent); @@ -415,10 +417,10 @@ class DirentListItem extends React.Component { location.href = url; }; - onAccessLog = () => { - let filePath = this.getDirentPath(this.props.dirent); - let path = siteRoot + 'repo/file-access/' + this.props.repoID + '/?p=' + encodeURIComponent(filePath) ; - window.open(path); + toggleFileAccessLogDialog = () => { + this.setState({ + isFileAccessLogDialogOpen: !this.state.isFileAccessLogDialogOpen + }); }; onOpenViaClient = () => { @@ -935,6 +937,16 @@ class DirentListItem extends React.Component { /> } + {this.state.isFileAccessLogDialogOpen && + + + + } ); } diff --git a/frontend/src/components/toolbar/selected-dirents-toolbar.js b/frontend/src/components/toolbar/selected-dirents-toolbar.js index 0de7f9eb5c..c86f2a3577 100644 --- a/frontend/src/components/toolbar/selected-dirents-toolbar.js +++ b/frontend/src/components/toolbar/selected-dirents-toolbar.js @@ -14,6 +14,8 @@ import LibSubFolderPermissionDialog from '../dialog/lib-sub-folder-permission-di import ModalPortal from '../modal-portal'; import ItemDropdownMenu from '../dropdown-menu/item-dropdown-menu'; import toaster from '../toast'; +import FileAccessLog from '../dialog/file-access-log'; + import '../../css/selected-dirents-toolbar.css'; const propTypes = { @@ -46,6 +48,7 @@ class MultipleDirOperationToolbar extends React.Component { super(props); this.state = { isZipDialogOpen: false, + isFileAccessLogDialogOpen: false, isMoveDialogShow: false, isCopyDialogShow: false, isMultipleOperation: true, @@ -184,7 +187,7 @@ class MultipleDirOperationToolbar extends React.Component { this.onHistory(dirent); break; case 'Access Log': - this.onAccessLog(dirent); + this.toggleFileAccessLogDialog(); break; case 'Properties': this.props.showDirentDetail('info'); @@ -247,10 +250,11 @@ class MultipleDirOperationToolbar extends React.Component { location.href = url; }; - onAccessLog = (dirent) => { - let filePath = this.getDirentPath(dirent); - let path = siteRoot + 'repo/file-access/' + this.props.repoID + '/?p=' + encodeURIComponent(filePath) ; - window.open(path); + toggleFileAccessLogDialog = () => { + this.setState({ + isFileAccessLogDialogOpen: !this.state.isFileAccessLogDialogOpen, + showLibContentViewDialogs: !this.state.isFileAccessLogDialogOpen + }); }; toggleCancel = () => { @@ -450,6 +454,16 @@ class MultipleDirOperationToolbar extends React.Component { /> } + {this.state.isFileAccessLogDialogOpen && + + + + } )} diff --git a/frontend/src/css/file-access-log.css b/frontend/src/css/file-access-log.css new file mode 100644 index 0000000000..cf51142224 --- /dev/null +++ b/frontend/src/css/file-access-log.css @@ -0,0 +1,10 @@ +@media(min-width:768px) { + .file-access-log-container { + max-width: 1100px; + } +} + +.file-access-log-content-container { + max-height: 500px; + overflow: auto; +} diff --git a/frontend/src/utils/file-access-log-api.js b/frontend/src/utils/file-access-log-api.js new file mode 100644 index 0000000000..5085c3031a --- /dev/null +++ b/frontend/src/utils/file-access-log-api.js @@ -0,0 +1,51 @@ +import cookie from 'react-cookies'; +import { siteRoot } from './constants'; +import axios from 'axios'; + +class FileAccessLogAPI { + init({ server, username, password, token }) { + this.server = server; + this.username = username; + this.password = password; + this.token = token; // none + if (this.token && this.server) { + this.req = axios.create({ + baseURL: this.server, + headers: { 'Authorization': 'Token ' + this.token }, + }); + } + return this; + } + + initForSeahubUsage({ siteRoot, xcsrfHeaders }) { + if (siteRoot && siteRoot.charAt(siteRoot.length - 1) === '/') { + let server = siteRoot.substring(0, siteRoot.length - 1); + this.server = server; + } else { + this.server = siteRoot; + } + this.req = axios.create({ + headers: { + 'X-CSRFToken': xcsrfHeaders, + } + }); + return this; + } + + listFileAccessLog(repoID, filePath, page, perPage, avatarSize) { + const url = this.server + '/api/v2.1/repos/' + repoID + '/file/access-log/'; + const params = { + path: filePath, + page: page || 1, + per_page: perPage || 100, + avatar_size: avatarSize || 64 + }; + return this.req.get(url, { params: params }); + } + +} + +let fileAccessLogAPI = new FileAccessLogAPI(); +let xcsrfHeaders = cookie.load('sfcsrftoken'); +fileAccessLogAPI.initForSeahubUsage({ siteRoot, xcsrfHeaders }); +export { fileAccessLogAPI }; diff --git a/media/css/seahub_react.css b/media/css/seahub_react.css index bcf7a9e817..0d4244a025 100644 --- a/media/css/seahub_react.css +++ b/media/css/seahub_react.css @@ -973,9 +973,9 @@ table th { border-bottom: 1px solid #e8e8e8; text-align: left; font-weight: normal; - font-size: 0.8125rem; + font-size: 0.875rem; line-height: 1.6; - color: #9c9c9c; + color: #666; } table td {