mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-05 08:53:14 +00:00
"['dir view'] display file access log in a dialog instead of an independent page (#6673)
- click 'Access Log' in the menu for files in list/grid mode, or single-selected file
This commit is contained in:
143
frontend/src/components/dialog/file-access-log.js
Normal file
143
frontend/src/components/dialog/file-access-log.js
Normal file
@@ -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}', '<span class="op-target text-truncate mx-1">' + Utils.HTMLescape(fileName) + '</span>');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={true} toggle={this.props.toggleDialog} className="file-access-log-container">
|
||||||
|
<ModalHeader toggle={this.props.toggleDialog}>
|
||||||
|
<span dangerouslySetInnerHTML={{ __html: title }} className="d-flex mw-100"></span>
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalBody className="file-access-log-content-container" onScroll={this.handleScroll}>
|
||||||
|
{isLoading ? <Loading /> : (
|
||||||
|
<>
|
||||||
|
{items.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<table className="table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th width="25%" className="pl10">{gettext('User')}</th>
|
||||||
|
<th width="15%">{gettext('Type')}</th>
|
||||||
|
<th width="40%">{gettext('IP')} / {gettext('Device Name')}</th>
|
||||||
|
<th width="20%">{gettext('Date')}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{items.map((item, index) => {
|
||||||
|
return (
|
||||||
|
<tr key={index}>
|
||||||
|
<td className="pl10">
|
||||||
|
<img src={item.avatar_url} alt='' width="24" className="rounded-circle mr-2" />
|
||||||
|
{item.email ? <a href={`${siteRoot}profile/${encodeURIComponent(item.email)}/`} target="_blank" rel="noreferrer">{item.name}</a> : <span>{gettext('Anonymous User')}</span>}
|
||||||
|
</td>
|
||||||
|
<td>{item.etype}</td>
|
||||||
|
<td className="pr-4">
|
||||||
|
{`${item.ip}${item.device ? '/' + item.device : ''}`}
|
||||||
|
</td>
|
||||||
|
<td>{moment(item.time).format('YYYY-MM-DD HH:mm:ss')}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{hasNextPage && <Loading />}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<EmptyTip text={gettext('This file has (apparently) not been accessed yet')} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ModalBody>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileAccessLog.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default FileAccessLog;
|
@@ -21,6 +21,7 @@ import CreateFile from '../dialog/create-file-dialog';
|
|||||||
import CreateFolder from '../dialog/create-folder-dialog';
|
import CreateFolder from '../dialog/create-folder-dialog';
|
||||||
import LibSubFolderPermissionDialog from '../dialog/lib-sub-folder-permission-dialog';
|
import LibSubFolderPermissionDialog from '../dialog/lib-sub-folder-permission-dialog';
|
||||||
import toaster from '../toast';
|
import toaster from '../toast';
|
||||||
|
import FileAccessLog from '../dialog/file-access-log';
|
||||||
|
|
||||||
import '../../css/grid-view.css';
|
import '../../css/grid-view.css';
|
||||||
|
|
||||||
@@ -72,6 +73,7 @@ class DirentGridView extends React.Component {
|
|||||||
imageItems: [],
|
imageItems: [],
|
||||||
imageIndex: 0,
|
imageIndex: 0,
|
||||||
// onmenuClick
|
// onmenuClick
|
||||||
|
isFileAccessLogDialogOpen: false,
|
||||||
isShareDialogShow: false,
|
isShareDialogShow: false,
|
||||||
isMoveDialogShow: false,
|
isMoveDialogShow: false,
|
||||||
isCopyDialogShow: false,
|
isCopyDialogShow: false,
|
||||||
@@ -377,7 +379,7 @@ class DirentGridView extends React.Component {
|
|||||||
this.onCreateFileToggle('.sdoc');
|
this.onCreateFileToggle('.sdoc');
|
||||||
break;
|
break;
|
||||||
case 'Access Log':
|
case 'Access Log':
|
||||||
this.onAccessLog(currentObject);
|
this.toggleFileAccessLogDialog();
|
||||||
break;
|
break;
|
||||||
case 'Properties':
|
case 'Properties':
|
||||||
this.props.showDirentDetail('info');
|
this.props.showDirentDetail('info');
|
||||||
@@ -526,10 +528,10 @@ class DirentGridView extends React.Component {
|
|||||||
location.href = url;
|
location.href = url;
|
||||||
};
|
};
|
||||||
|
|
||||||
onAccessLog = (currentObject) => {
|
toggleFileAccessLogDialog = () => {
|
||||||
let filePath = this.getDirentPath(currentObject);
|
this.setState({
|
||||||
let path = siteRoot + 'repo/file-access/' + this.props.repoID + '/?p=' + encodeURIComponent(filePath) ;
|
isFileAccessLogDialogOpen: !this.state.isFileAccessLogDialogOpen
|
||||||
window.open(path);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onOpenViaClient = (currentObject) => {
|
onOpenViaClient = (currentObject) => {
|
||||||
@@ -905,6 +907,16 @@ class DirentGridView extends React.Component {
|
|||||||
/>
|
/>
|
||||||
</ModalPortal>
|
</ModalPortal>
|
||||||
)}
|
)}
|
||||||
|
{this.state.isFileAccessLogDialogOpen &&
|
||||||
|
<ModalPortal>
|
||||||
|
<FileAccessLog
|
||||||
|
repoID={this.props.repoID}
|
||||||
|
filePath={direntPath}
|
||||||
|
fileName={dirent.name}
|
||||||
|
toggleDialog={this.toggleFileAccessLogDialog}
|
||||||
|
/>
|
||||||
|
</ModalPortal>
|
||||||
|
}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -18,6 +18,7 @@ import ZipDownloadDialog from '../dialog/zip-download-dialog';
|
|||||||
import EditFileTagDialog from '../dialog/edit-filetag-dialog';
|
import EditFileTagDialog from '../dialog/edit-filetag-dialog';
|
||||||
import EditFileTagPopover from '../popover/edit-filetag-popover';
|
import EditFileTagPopover from '../popover/edit-filetag-popover';
|
||||||
import LibSubFolderPermissionDialog from '../dialog/lib-sub-folder-permission-dialog';
|
import LibSubFolderPermissionDialog from '../dialog/lib-sub-folder-permission-dialog';
|
||||||
|
import FileAccessLog from '../dialog/file-access-log';
|
||||||
import toaster from '../toast';
|
import toaster from '../toast';
|
||||||
import FileTag from './file-tag';
|
import FileTag from './file-tag';
|
||||||
|
|
||||||
@@ -81,6 +82,7 @@ class DirentListItem extends React.Component {
|
|||||||
isOperationShow: false,
|
isOperationShow: false,
|
||||||
highlight: false,
|
highlight: false,
|
||||||
isZipDialogOpen: false,
|
isZipDialogOpen: false,
|
||||||
|
isFileAccessLogDialogOpen: false,
|
||||||
isMoveDialogShow: false,
|
isMoveDialogShow: false,
|
||||||
isCopyDialogShow: false,
|
isCopyDialogShow: false,
|
||||||
isShareDialogShow: false,
|
isShareDialogShow: false,
|
||||||
@@ -298,7 +300,7 @@ class DirentListItem extends React.Component {
|
|||||||
this.onHistory();
|
this.onHistory();
|
||||||
break;
|
break;
|
||||||
case 'Access Log':
|
case 'Access Log':
|
||||||
this.onAccessLog();
|
this.toggleFileAccessLogDialog();
|
||||||
break;
|
break;
|
||||||
case 'Properties':
|
case 'Properties':
|
||||||
this.props.onDirentClick(this.props.dirent);
|
this.props.onDirentClick(this.props.dirent);
|
||||||
@@ -415,10 +417,10 @@ class DirentListItem extends React.Component {
|
|||||||
location.href = url;
|
location.href = url;
|
||||||
};
|
};
|
||||||
|
|
||||||
onAccessLog = () => {
|
toggleFileAccessLogDialog = () => {
|
||||||
let filePath = this.getDirentPath(this.props.dirent);
|
this.setState({
|
||||||
let path = siteRoot + 'repo/file-access/' + this.props.repoID + '/?p=' + encodeURIComponent(filePath) ;
|
isFileAccessLogDialogOpen: !this.state.isFileAccessLogDialogOpen
|
||||||
window.open(path);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onOpenViaClient = () => {
|
onOpenViaClient = () => {
|
||||||
@@ -935,6 +937,16 @@ class DirentListItem extends React.Component {
|
|||||||
/>
|
/>
|
||||||
</ModalPortal>
|
</ModalPortal>
|
||||||
}
|
}
|
||||||
|
{this.state.isFileAccessLogDialogOpen &&
|
||||||
|
<ModalPortal>
|
||||||
|
<FileAccessLog
|
||||||
|
repoID={this.props.repoID}
|
||||||
|
filePath={direntPath}
|
||||||
|
fileName={dirent.name}
|
||||||
|
toggleDialog={this.toggleFileAccessLogDialog}
|
||||||
|
/>
|
||||||
|
</ModalPortal>
|
||||||
|
}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -14,6 +14,8 @@ import LibSubFolderPermissionDialog from '../dialog/lib-sub-folder-permission-di
|
|||||||
import ModalPortal from '../modal-portal';
|
import ModalPortal from '../modal-portal';
|
||||||
import ItemDropdownMenu from '../dropdown-menu/item-dropdown-menu';
|
import ItemDropdownMenu from '../dropdown-menu/item-dropdown-menu';
|
||||||
import toaster from '../toast';
|
import toaster from '../toast';
|
||||||
|
import FileAccessLog from '../dialog/file-access-log';
|
||||||
|
|
||||||
import '../../css/selected-dirents-toolbar.css';
|
import '../../css/selected-dirents-toolbar.css';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
@@ -46,6 +48,7 @@ class MultipleDirOperationToolbar extends React.Component {
|
|||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
isZipDialogOpen: false,
|
isZipDialogOpen: false,
|
||||||
|
isFileAccessLogDialogOpen: false,
|
||||||
isMoveDialogShow: false,
|
isMoveDialogShow: false,
|
||||||
isCopyDialogShow: false,
|
isCopyDialogShow: false,
|
||||||
isMultipleOperation: true,
|
isMultipleOperation: true,
|
||||||
@@ -184,7 +187,7 @@ class MultipleDirOperationToolbar extends React.Component {
|
|||||||
this.onHistory(dirent);
|
this.onHistory(dirent);
|
||||||
break;
|
break;
|
||||||
case 'Access Log':
|
case 'Access Log':
|
||||||
this.onAccessLog(dirent);
|
this.toggleFileAccessLogDialog();
|
||||||
break;
|
break;
|
||||||
case 'Properties':
|
case 'Properties':
|
||||||
this.props.showDirentDetail('info');
|
this.props.showDirentDetail('info');
|
||||||
@@ -247,10 +250,11 @@ class MultipleDirOperationToolbar extends React.Component {
|
|||||||
location.href = url;
|
location.href = url;
|
||||||
};
|
};
|
||||||
|
|
||||||
onAccessLog = (dirent) => {
|
toggleFileAccessLogDialog = () => {
|
||||||
let filePath = this.getDirentPath(dirent);
|
this.setState({
|
||||||
let path = siteRoot + 'repo/file-access/' + this.props.repoID + '/?p=' + encodeURIComponent(filePath) ;
|
isFileAccessLogDialogOpen: !this.state.isFileAccessLogDialogOpen,
|
||||||
window.open(path);
|
showLibContentViewDialogs: !this.state.isFileAccessLogDialogOpen
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
toggleCancel = () => {
|
toggleCancel = () => {
|
||||||
@@ -450,6 +454,16 @@ class MultipleDirOperationToolbar extends React.Component {
|
|||||||
/>
|
/>
|
||||||
</ModalPortal>
|
</ModalPortal>
|
||||||
}
|
}
|
||||||
|
{this.state.isFileAccessLogDialogOpen &&
|
||||||
|
<ModalPortal>
|
||||||
|
<FileAccessLog
|
||||||
|
repoID={this.props.repoID}
|
||||||
|
filePath={direntPath}
|
||||||
|
fileName={dirent.name}
|
||||||
|
toggleDialog={this.toggleFileAccessLogDialog}
|
||||||
|
/>
|
||||||
|
</ModalPortal>
|
||||||
|
}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)}
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
10
frontend/src/css/file-access-log.css
Normal file
10
frontend/src/css/file-access-log.css
Normal file
@@ -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;
|
||||||
|
}
|
51
frontend/src/utils/file-access-log-api.js
Normal file
51
frontend/src/utils/file-access-log-api.js
Normal file
@@ -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 };
|
@@ -973,9 +973,9 @@ table th {
|
|||||||
border-bottom: 1px solid #e8e8e8;
|
border-bottom: 1px solid #e8e8e8;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-size: 0.8125rem;
|
font-size: 0.875rem;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
color: #9c9c9c;
|
color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
table td {
|
table td {
|
||||||
|
Reference in New Issue
Block a user