mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-14 14:21:23 +00:00
Virus scan frontend (#4574)
* ignore virus scan record * get virus record by has_handle * add has_next_page * change has_handle to has_deleted * change url and view name * delete or ignore virus files in batch * rebase master * add cancel-ignore * optimize code * add translate * [virus scan] added 'All' and 'Unhandled' tabs * rewrote 'all' page (previous 'virus-scan-records') * added 'unhandled' page * [virus scan] 'unhandled' tab: added 'delete & ignore selected items' * [virus scan] 'unhandled' tab: added 'handle selected items' Co-authored-by: 王健辉 <40563566+jianhw@users.noreply.github.com>
This commit is contained in:
@@ -7,6 +7,12 @@ import MainPanel from './main-panel';
|
||||
|
||||
import Info from './info';
|
||||
|
||||
import StatisticFile from './statistic/statistic-file';
|
||||
import StatisticStorage from './statistic/statistic-storage';
|
||||
import StatisticTraffic from './statistic/statistic-traffic';
|
||||
import StatisticUsers from './statistic/statistic-users';
|
||||
import StatisticReport from './statistic/statistic-reports';
|
||||
|
||||
import DesktopDevices from './devices/desktop-devices';
|
||||
import MobileDevices from './devices/mobile-devices';
|
||||
import DeviceErrors from './devices/devices-errors';
|
||||
@@ -58,24 +64,19 @@ 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 TermsAndConditions from './terms-and-conditions/terms-and-conditions';
|
||||
|
||||
import WebSettings from './web-settings/web-settings';
|
||||
import Notifications from './notifications/notifications';
|
||||
import FileScanRecords from './file-scan-records';
|
||||
import VirusScanRecords from './virus-scan-records';
|
||||
import WorkWeixinDepartments from './work-weixin-departments';
|
||||
import DingtalkDepartments from './dingtalk-departments';
|
||||
import Invitations from './invitations/invitations';
|
||||
import TermsAndConditions from './terms-and-conditions/terms-and-conditions';
|
||||
|
||||
import StatisticFile from './statistic/statistic-file';
|
||||
import StatisticStorage from './statistic/statistic-storage';
|
||||
import StatisticTraffic from './statistic/statistic-traffic';
|
||||
import StatisticUsers from './statistic/statistic-users';
|
||||
import StatisticReport from './statistic/statistic-reports';
|
||||
import AllVirusFiles from './virus-scan/all-virus-files';
|
||||
import UnhandledVirusFiles from './virus-scan/unhandled-virus-files';
|
||||
|
||||
import AdminOperationLogs from './admin-logs/operation-logs';
|
||||
import AdminLoginLogs from './admin-logs/login-logs';
|
||||
|
||||
import '../../assets/css/fa-solid.css';
|
||||
import '../../assets/css/fa-regular.css';
|
||||
@@ -141,6 +142,10 @@ class SysAdmin extends React.Component {
|
||||
tab: 'logs',
|
||||
urlPartList: ['logs/']
|
||||
},
|
||||
{
|
||||
tab: 'virus-files',
|
||||
urlPartList: ['virus-files/']
|
||||
},
|
||||
{
|
||||
tab: 'adminLogs',
|
||||
urlPartList: ['admin-logs/']
|
||||
@@ -244,16 +249,15 @@ class SysAdmin extends React.Component {
|
||||
<Invitations path={siteRoot + 'sys/invitations'} />
|
||||
<TermsAndConditions path={siteRoot + 'sys/terms-and-conditions/'} />
|
||||
|
||||
<AllVirusFiles path={siteRoot + 'sys/virus-files/all'} />
|
||||
<UnhandledVirusFiles path={siteRoot + 'sys/virus-files/unhandled'} />
|
||||
|
||||
<FileScanRecords
|
||||
path={siteRoot + 'sys/file-scan-records'}
|
||||
currentTab={currentTab}
|
||||
tabItemClick={this.tabItemClick}
|
||||
/>
|
||||
<VirusScanRecords
|
||||
path={siteRoot + 'sys/virus-scan-records'}
|
||||
currentTab={currentTab}
|
||||
tabItemClick={this.tabItemClick}
|
||||
/>
|
||||
|
||||
<WorkWeixinDepartments
|
||||
path={siteRoot + 'sys/work-weixin'}
|
||||
currentTab={currentTab}
|
||||
|
@@ -202,9 +202,9 @@ class SidePanel extends React.Component {
|
||||
{isPro && otherPermission &&
|
||||
<li className="nav-item">
|
||||
<Link
|
||||
className={`nav-link ellipsis ${this.getActiveClass('virus-scan-records')}`}
|
||||
to={siteRoot + 'sys/virus-scan-records/'}
|
||||
onClick={() => this.props.tabItemClick('virus-scan-records')}
|
||||
className={`nav-link ellipsis ${this.getActiveClass('virus-files')}`}
|
||||
to={siteRoot + 'sys/virus-files/all/'}
|
||||
onClick={() => this.props.tabItemClick('virus-files')}
|
||||
>
|
||||
<span className="sf2-icon-security" aria-hidden="true"></span>
|
||||
<span className="nav-text">{gettext('Virus Scan')}</span>
|
||||
|
@@ -1,206 +0,0 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import toaster from '../../components/toast';
|
||||
import Account from '../../components/common/account';
|
||||
|
||||
|
||||
const recordItemPropTypes = {
|
||||
record: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
class RecordItem extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
handleStatus: this.props.record.has_handle,
|
||||
errorMsg: '',
|
||||
};
|
||||
}
|
||||
|
||||
deleteVirusScanRecord = () => {
|
||||
seafileAPI.deleteVirusScanRecord(this.props.record.virus_id).then(() => {
|
||||
this.setState({
|
||||
handleStatus: !this.state.handleStatus,
|
||||
});
|
||||
}).catch((error) => {
|
||||
if (error.response) {
|
||||
this.setState({
|
||||
errorMsg: error.response.data.error_msg,
|
||||
});
|
||||
toaster.danger(this.state.errorMsg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
let record = this.props.record;
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td>{record.repo_name}</td>
|
||||
<td>{record.repo_owner}</td>
|
||||
<td>{record.file_path}</td>
|
||||
<td>
|
||||
{
|
||||
this.state.handleStatus ?
|
||||
<span style={{color: 'green'}}>{gettext('Handled')}</span> :
|
||||
<a style={{color: 'red', cursor: 'pointer'}} onClick={this.deleteVirusScanRecord}>{gettext('Delete')}</a>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RecordItem.propTypes = recordItemPropTypes;
|
||||
|
||||
|
||||
const recordListPropTypes = {
|
||||
loading: PropTypes.bool.isRequired,
|
||||
isLoadingMore: PropTypes.bool.isRequired,
|
||||
errorMsg: PropTypes.string.isRequired,
|
||||
records: PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
class RecordList extends Component {
|
||||
|
||||
render() {
|
||||
let { loading, isLoadingMore, errorMsg, records } = this.props;
|
||||
|
||||
if (loading) {
|
||||
return <span className="loading-icon loading-tip"></span>;
|
||||
} else if (errorMsg) {
|
||||
return <p className="error text-center">{errorMsg}</p>;
|
||||
} else {
|
||||
return (
|
||||
<Fragment>
|
||||
<table width="100%" className="table table-hover table-vcenter">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="28%">{gettext('Library')}</th>
|
||||
<th width="28%">{gettext('Owner')}</th>
|
||||
<th width="29%">{gettext('Virus File')}</th>
|
||||
<th width="15%">{gettext('Operations')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{records.map((record, index) => {
|
||||
return (
|
||||
<RecordItem key={index} record={record} />
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
{isLoadingMore ? <span className="loading-icon loading-tip"></span> : ''}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RecordList.propTypes = recordListPropTypes;
|
||||
|
||||
|
||||
class VirusScanRecords extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
loading: true,
|
||||
isLoadingMore: false,
|
||||
currentPage: 1,
|
||||
hasMore: true,
|
||||
errorMsg: '',
|
||||
records: [],
|
||||
};
|
||||
}
|
||||
|
||||
getMore() {
|
||||
let currentPage = this.state.currentPage;
|
||||
seafileAPI.listVirusScanRecords(currentPage).then((res) => {
|
||||
this.setState({
|
||||
isLoadingMore: false,
|
||||
records: [...this.state.records, ...res.data.record_list],
|
||||
currentPage: currentPage + 1,
|
||||
hasMore: res.data.record_list.length === 0 ? false : true,
|
||||
});
|
||||
}).catch((error) => {
|
||||
if (error.response) {
|
||||
this.setState({
|
||||
isLoadingMore: false,
|
||||
errorMsg: error.response.data.error_msg,
|
||||
});
|
||||
toaster.danger(this.state.errorMsg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleScroll = (event) => {
|
||||
if (!this.state.isLoadingMore && this.state.hasMore) {
|
||||
const clientHeight = event.target.clientHeight;
|
||||
const scrollHeight = event.target.scrollHeight;
|
||||
const scrollTop = event.target.scrollTop;
|
||||
const isBottom = (clientHeight + scrollTop + 1 >= scrollHeight);
|
||||
if (isBottom) {
|
||||
this.setState({isLoadingMore: true}, () => {
|
||||
this.getMore();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
let currentPage = this.state.currentPage;
|
||||
seafileAPI.listVirusScanRecords(currentPage).then((res) => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
records: res.data.record_list,
|
||||
currentPage: currentPage + 1,
|
||||
hasMore: true,
|
||||
});
|
||||
}).catch((error) => {
|
||||
if (error.response) {
|
||||
this.setState({
|
||||
loading: false,
|
||||
errorMsg: error.response.data.error_msg,
|
||||
});
|
||||
toaster.danger(this.state.errorMsg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="main-panel-north border-left-show">
|
||||
<div className="cur-view-toolbar">
|
||||
<span className="sf2-icon-menu side-nav-toggle hidden-md-up d-md-none" title="Side Nav Menu"></span>
|
||||
</div>
|
||||
<div className="common-toolbar">
|
||||
<Account isAdminPanel={true} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="main-panel-center">
|
||||
<div className="cur-view-container" id="content-scan-records">
|
||||
<div className="cur-view-path">
|
||||
<h3 className="sf-heading">{gettext('Virus Scan Records')}</h3>
|
||||
</div>
|
||||
<div className="cur-view-content" onScroll={this.handleScroll}>
|
||||
<RecordList
|
||||
loading={this.state.loading}
|
||||
isLoadingMore={this.state.isLoadingMore}
|
||||
errorMsg={this.state.errorMsg}
|
||||
records={this.state.records}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default VirusScanRecords;
|
313
frontend/src/pages/sys-admin/virus-scan/all-virus-files.js
Normal file
313
frontend/src/pages/sys-admin/virus-scan/all-virus-files.js
Normal file
@@ -0,0 +1,313 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { seafileAPI } from '../../../utils/seafile-api';
|
||||
import { gettext } from '../../../utils/constants';
|
||||
import { Utils } from '../../../utils/utils';
|
||||
import toaster from '../../../components/toast';
|
||||
import OpMenu from '../../../components/dialog/op-menu';
|
||||
import Loading from '../../../components/loading';
|
||||
import Paginator from '../../../components/paginator';
|
||||
import MainPanelTopbar from '../main-panel-topbar';
|
||||
import Nav from './nav';
|
||||
|
||||
const virusFileItemPropTypes = {
|
||||
virusFile: PropTypes.object.isRequired,
|
||||
isItemFreezed: PropTypes.bool.isRequired,
|
||||
onFreezedItem: PropTypes.func.isRequired,
|
||||
onUnfreezedItem: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
class VirusFileItem extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
highlight: false,
|
||||
isOpIconShown: false
|
||||
};
|
||||
}
|
||||
|
||||
handleMouseEnter = () => {
|
||||
if (!this.props.isItemFreezed) {
|
||||
this.setState({
|
||||
isOpIconShown: true,
|
||||
highlight: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseLeave = () => {
|
||||
if (!this.props.isItemFreezed) {
|
||||
this.setState({
|
||||
isOpIconShown: false,
|
||||
highlight: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onUnfreezedItem = () => {
|
||||
this.setState({
|
||||
highlight: false,
|
||||
isOpIconShow: false
|
||||
});
|
||||
this.props.onUnfreezedItem();
|
||||
}
|
||||
|
||||
onMenuItemClick = (operation) => {
|
||||
this.props.handleFile(this.props.virusFile.virus_id, operation);
|
||||
}
|
||||
|
||||
translateOperations = (item) => {
|
||||
let translateResult = '';
|
||||
switch(item) {
|
||||
case 'delete':
|
||||
translateResult = gettext('Delete');
|
||||
break;
|
||||
case 'ignore':
|
||||
translateResult = gettext('Ignore');
|
||||
break;
|
||||
case 'do-not-ignore':
|
||||
translateResult = gettext('Don\'t ignore');
|
||||
break;
|
||||
}
|
||||
return translateResult;
|
||||
}
|
||||
|
||||
render() {
|
||||
const virusFile = this.props.virusFile;
|
||||
let fileStatus = '',
|
||||
fileOpList = [];
|
||||
if (virusFile.has_deleted) {
|
||||
fileStatus = <span className="text-green">{gettext('Deleted')}</span>;
|
||||
} else if (virusFile.has_ignored) {
|
||||
fileStatus = <span className="text-orange">{gettext('Ignored')}</span>;
|
||||
fileOpList = ['do-not-ignore'];
|
||||
} else {
|
||||
fileStatus = <span className="text-red">{gettext('Unhandled')}</span>;
|
||||
fileOpList = ['delete', 'ignore'];
|
||||
}
|
||||
|
||||
return (
|
||||
<tr className={this.state.highlight ? 'tr-highlight' : ''} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
|
||||
<td>{virusFile.repo_name}</td>
|
||||
<td>{virusFile.repo_owner}</td>
|
||||
<td>{virusFile.file_path}</td>
|
||||
<td>{fileStatus}</td>
|
||||
<td>
|
||||
{fileOpList.length > 0 && this.state.isOpIconShown &&
|
||||
<OpMenu
|
||||
operations={fileOpList}
|
||||
translateOperations={this.translateOperations}
|
||||
onMenuItemClick={this.onMenuItemClick}
|
||||
onFreezedItem={this.props.onFreezedItem}
|
||||
onUnfreezedItem={this.onUnfreezedItem}
|
||||
/>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
VirusFileItem.propTypes = virusFileItemPropTypes;
|
||||
|
||||
|
||||
const virusFileListPropTypes = {
|
||||
loading: PropTypes.bool.isRequired,
|
||||
errorMsg: PropTypes.string.isRequired,
|
||||
virusFiles: PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
class Content extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isItemFreezed: false
|
||||
};
|
||||
}
|
||||
|
||||
onFreezedItem = () => {
|
||||
this.setState({isItemFreezed: true});
|
||||
}
|
||||
|
||||
onUnfreezedItem = () => {
|
||||
this.setState({isItemFreezed: false});
|
||||
}
|
||||
|
||||
getPreviousPage = () => {
|
||||
this.props.getListByPage(this.props.currentPage - 1);
|
||||
}
|
||||
|
||||
getNextPage = () => {
|
||||
this.props.getListByPage(this.props.currentPage + 1);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
loading, errorMsg, virusFiles,
|
||||
curPerPage, hasNextPage, currentPage
|
||||
} = this.props;
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
} else if (errorMsg) {
|
||||
return <p className="error text-center mt-4">{errorMsg}</p>;
|
||||
} else {
|
||||
return (
|
||||
<Fragment>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="27%">{gettext('Library')}</th>
|
||||
<th width="25%">{gettext('Owner')}</th>
|
||||
<th width="28%">{gettext('Virus File')}</th>
|
||||
<th width="15%">{gettext('Status')}</th>
|
||||
<th width="5%">{/* Operations */}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{virusFiles.map((virusFile, index) => {
|
||||
return (
|
||||
<VirusFileItem
|
||||
key={index}
|
||||
virusFile={virusFile}
|
||||
isItemFreezed={this.state.isItemFreezed}
|
||||
onFreezedItem={this.onFreezedItem}
|
||||
onUnfreezedItem={this.onUnfreezedItem}
|
||||
handleFile={this.props.handleFile}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
{virusFiles.length > 0 &&
|
||||
<Paginator
|
||||
gotoPreviousPage={this.getPreviousPage}
|
||||
gotoNextPage={this.getNextPage}
|
||||
currentPage={currentPage}
|
||||
hasNextPage={hasNextPage}
|
||||
curPerPage={curPerPage}
|
||||
resetPerPage={this.props.resetPerPage}
|
||||
/>
|
||||
}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Content.propTypes = virusFileListPropTypes;
|
||||
|
||||
|
||||
class AllVirusFiles extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
loading: true,
|
||||
errorMsg: '',
|
||||
virusFiles: [],
|
||||
currentPage: 1,
|
||||
perPage: 25,
|
||||
hasNextPage: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
let urlParams = (new URL(window.location)).searchParams;
|
||||
const {
|
||||
currentPage, perPage
|
||||
} = this.state;
|
||||
this.setState({
|
||||
perPage: parseInt(urlParams.get('per_page') || perPage),
|
||||
currentPage: parseInt(urlParams.get('page') || currentPage)
|
||||
}, () => {
|
||||
this.getListByPage(this.state.currentPage);
|
||||
});
|
||||
}
|
||||
|
||||
getListByPage = (page) => {
|
||||
const { perPage } = this.state;
|
||||
seafileAPI.listVirusFiles(page, perPage).then((res) => {
|
||||
const data = res.data;
|
||||
this.setState({
|
||||
loading: false,
|
||||
virusFiles: data.virus_file_list,
|
||||
hasNextPage: data.has_next_page
|
||||
});
|
||||
}).catch((error) => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
resetPerPage = (perPage) => {
|
||||
this.setState({
|
||||
perPage: perPage
|
||||
}, () => {
|
||||
this.getListByPage(1);
|
||||
});
|
||||
}
|
||||
|
||||
handleFile = (virusID, op) => {
|
||||
let request;
|
||||
switch(op) {
|
||||
case 'delete':
|
||||
request = seafileAPI.deleteVirusFile(virusID);
|
||||
break;
|
||||
case 'ignore':
|
||||
request = seafileAPI.toggleIgnoreVirusFile(virusID, true);
|
||||
break;
|
||||
case 'do-not-ignore':
|
||||
request = seafileAPI.toggleIgnoreVirusFile(virusID, false);
|
||||
break;
|
||||
}
|
||||
request.then((res) => {
|
||||
this.setState({
|
||||
virusFiles: this.state.virusFiles.map((item) => {
|
||||
if (item.virus_id == virusID) {
|
||||
if (op == 'delete') {
|
||||
item.has_deleted = true;
|
||||
} else {
|
||||
item = res.data.virus_file;
|
||||
}
|
||||
}
|
||||
return item;
|
||||
})
|
||||
});
|
||||
}).catch((error) => {
|
||||
toaster.danger(Utils.getErrorMsg(error));
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Fragment>
|
||||
<MainPanelTopbar />
|
||||
<div className="main-panel-center">
|
||||
<div className="cur-view-container">
|
||||
<Nav currentItem="all" />
|
||||
<div className="cur-view-content">
|
||||
<Content
|
||||
loading={this.state.loading}
|
||||
errorMsg={this.state.errorMsg}
|
||||
virusFiles={this.state.virusFiles}
|
||||
currentPage={this.state.currentPage}
|
||||
hasNextPage={this.state.hasNextPage}
|
||||
curPerPage={this.state.perPage}
|
||||
resetPerPage={this.resetPerPage}
|
||||
getListByPage={this.getListByPage}
|
||||
handleFile={this.handleFile}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default AllVirusFiles;
|
40
frontend/src/pages/sys-admin/virus-scan/nav.js
Normal file
40
frontend/src/pages/sys-admin/virus-scan/nav.js
Normal file
@@ -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 Nav extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.navItems = [
|
||||
{name: 'all', urlPart: 'all', text: gettext('All')},
|
||||
{name: 'unhandled', urlPart: 'unhandled', text: gettext('Unhandled')}
|
||||
];
|
||||
}
|
||||
|
||||
render() {
|
||||
const { currentItem } = this.props;
|
||||
return (
|
||||
<div className="cur-view-path tab-nav-container">
|
||||
<ul className="nav">
|
||||
{this.navItems.map((item, index) => {
|
||||
return (
|
||||
<li className="nav-item" key={index}>
|
||||
<Link to={`${siteRoot}sys/virus-files/${item.urlPart}/`} className={`nav-link${currentItem == item.name ? ' active' : ''}`}>{item.text}</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Nav.propTypes = propTypes;
|
||||
|
||||
export default Nav;
|
421
frontend/src/pages/sys-admin/virus-scan/unhandled-virus-files.js
Normal file
421
frontend/src/pages/sys-admin/virus-scan/unhandled-virus-files.js
Normal file
@@ -0,0 +1,421 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button } from 'reactstrap';
|
||||
import { seafileAPI } from '../../../utils/seafile-api';
|
||||
import { gettext } from '../../../utils/constants';
|
||||
import { Utils } from '../../../utils/utils';
|
||||
import toaster from '../../../components/toast';
|
||||
import OpMenu from '../../../components/dialog/op-menu';
|
||||
import Loading from '../../../components/loading';
|
||||
import Paginator from '../../../components/paginator';
|
||||
import MainPanelTopbar from '../main-panel-topbar';
|
||||
import Nav from './nav';
|
||||
|
||||
const virusFileItemPropTypes = {
|
||||
virusFile: PropTypes.object.isRequired,
|
||||
isItemFreezed: PropTypes.bool.isRequired,
|
||||
onFreezedItem: PropTypes.func.isRequired,
|
||||
onUnfreezedItem: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
class VirusFileItem extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
highlight: false,
|
||||
isOpIconShown: false
|
||||
};
|
||||
}
|
||||
|
||||
handleMouseEnter = () => {
|
||||
if (!this.props.isItemFreezed) {
|
||||
this.setState({
|
||||
isOpIconShown: true,
|
||||
highlight: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseLeave = () => {
|
||||
if (!this.props.isItemFreezed) {
|
||||
this.setState({
|
||||
isOpIconShown: false,
|
||||
highlight: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onUnfreezedItem = () => {
|
||||
this.setState({
|
||||
highlight: false,
|
||||
isOpIconShow: false
|
||||
});
|
||||
this.props.onUnfreezedItem();
|
||||
}
|
||||
|
||||
onMenuItemClick = (operation) => {
|
||||
this.props.handleFile(this.props.virusFile.virus_id, operation);
|
||||
}
|
||||
|
||||
translateOperations = (item) => {
|
||||
let translateResult = '';
|
||||
switch(item) {
|
||||
case 'delete':
|
||||
translateResult = gettext('Delete');
|
||||
break;
|
||||
case 'ignore':
|
||||
translateResult = gettext('Ignore');
|
||||
break;
|
||||
case 'do-not-ignore':
|
||||
translateResult = gettext('Don\'t ignore');
|
||||
break;
|
||||
}
|
||||
return translateResult;
|
||||
}
|
||||
|
||||
toggleItemSelected = (e) => {
|
||||
this.props.toggleItemSelected(this.props.virusFile, e.target.checked);
|
||||
}
|
||||
|
||||
render() {
|
||||
const virusFile = this.props.virusFile;
|
||||
let fileStatus = '',
|
||||
fileOpList = [];
|
||||
if (virusFile.has_deleted) {
|
||||
fileStatus = <span className="text-green">{gettext('Deleted')}</span>;
|
||||
} else if (virusFile.has_ignored) {
|
||||
fileStatus = <span className="text-orange">{gettext('Ignored')}</span>;
|
||||
fileOpList = ['do-not-ignore'];
|
||||
} else {
|
||||
fileStatus = <span className="text-red">{gettext('Unhandled')}</span>;
|
||||
fileOpList = ['delete', 'ignore'];
|
||||
}
|
||||
|
||||
return (
|
||||
<tr className={this.state.highlight ? 'tr-highlight' : ''} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
|
||||
<td className="text-center">
|
||||
<input type="checkbox" checked={virusFile.isSelected} onChange={this.toggleItemSelected} />
|
||||
</td>
|
||||
<td>{virusFile.repo_name}</td>
|
||||
<td>{virusFile.repo_owner}</td>
|
||||
<td>{virusFile.file_path}</td>
|
||||
<td>{fileStatus}</td>
|
||||
<td>
|
||||
{fileOpList.length > 0 && this.state.isOpIconShown &&
|
||||
<OpMenu
|
||||
operations={fileOpList}
|
||||
translateOperations={this.translateOperations}
|
||||
onMenuItemClick={this.onMenuItemClick}
|
||||
onFreezedItem={this.props.onFreezedItem}
|
||||
onUnfreezedItem={this.onUnfreezedItem}
|
||||
/>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
VirusFileItem.propTypes = virusFileItemPropTypes;
|
||||
|
||||
|
||||
const virusFileListPropTypes = {
|
||||
loading: PropTypes.bool.isRequired,
|
||||
errorMsg: PropTypes.string.isRequired,
|
||||
virusFiles: PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
class Content extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isItemFreezed: false
|
||||
};
|
||||
}
|
||||
|
||||
onFreezedItem = () => {
|
||||
this.setState({isItemFreezed: true});
|
||||
}
|
||||
|
||||
onUnfreezedItem = () => {
|
||||
this.setState({isItemFreezed: false});
|
||||
}
|
||||
|
||||
getPreviousPage = () => {
|
||||
this.props.getListByPage(this.props.currentPage - 1);
|
||||
}
|
||||
|
||||
getNextPage = () => {
|
||||
this.props.getListByPage(this.props.currentPage + 1);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
loading, errorMsg, virusFiles,
|
||||
curPerPage, hasNextPage, currentPage,
|
||||
isAllItemsSelected
|
||||
} = this.props;
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
} else if (errorMsg) {
|
||||
return <p className="error text-center mt-4">{errorMsg}</p>;
|
||||
} else {
|
||||
return (
|
||||
<Fragment>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="3%" className="text-center">
|
||||
<input type="checkbox" checked={isAllItemsSelected} onChange={this.props.toggleAllSelected} />
|
||||
</th>
|
||||
<th width="24%">{gettext('Library')}</th>
|
||||
<th width="25%">{gettext('Owner')}</th>
|
||||
<th width="28%">{gettext('Virus File')}</th>
|
||||
<th width="15%">{gettext('Status')}</th>
|
||||
<th width="5%">{/* Operations */}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{virusFiles.map((virusFile, index) => {
|
||||
return (
|
||||
<VirusFileItem
|
||||
key={index}
|
||||
virusFile={virusFile}
|
||||
isItemFreezed={this.state.isItemFreezed}
|
||||
onFreezedItem={this.onFreezedItem}
|
||||
onUnfreezedItem={this.onUnfreezedItem}
|
||||
handleFile={this.props.handleFile}
|
||||
toggleItemSelected={this.props.toggleItemSelected}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
{virusFiles.length > 0 &&
|
||||
<Paginator
|
||||
gotoPreviousPage={this.getPreviousPage}
|
||||
gotoNextPage={this.getNextPage}
|
||||
currentPage={currentPage}
|
||||
hasNextPage={hasNextPage}
|
||||
curPerPage={curPerPage}
|
||||
resetPerPage={this.props.resetPerPage}
|
||||
/>
|
||||
}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Content.propTypes = virusFileListPropTypes;
|
||||
|
||||
|
||||
class UnhandledVirusFiles extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
loading: true,
|
||||
errorMsg: '',
|
||||
virusFiles: [],
|
||||
|
||||
isAllItemsSelected: false,
|
||||
selectedItems: [],
|
||||
|
||||
currentPage: 1,
|
||||
perPage: 25,
|
||||
hasNextPage: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
let urlParams = (new URL(window.location)).searchParams;
|
||||
const {
|
||||
currentPage, perPage
|
||||
} = this.state;
|
||||
this.setState({
|
||||
perPage: parseInt(urlParams.get('per_page') || perPage),
|
||||
currentPage: parseInt(urlParams.get('page') || currentPage)
|
||||
}, () => {
|
||||
this.getListByPage(this.state.currentPage);
|
||||
});
|
||||
}
|
||||
|
||||
getListByPage = (page) => {
|
||||
const { perPage } = this.state;
|
||||
const hasHandled = false;
|
||||
seafileAPI.listVirusFiles(page, perPage, hasHandled).then((res) => {
|
||||
const data = res.data;
|
||||
const items = data.virus_file_list.map(item => {
|
||||
item.isSelected = false;
|
||||
return item;
|
||||
});
|
||||
this.setState({
|
||||
loading: false,
|
||||
virusFiles: items,
|
||||
hasNextPage: data.has_next_page
|
||||
});
|
||||
}).catch((error) => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
resetPerPage = (perPage) => {
|
||||
this.setState({
|
||||
perPage: perPage
|
||||
}, () => {
|
||||
this.getListByPage(1);
|
||||
});
|
||||
}
|
||||
|
||||
handleFile = (virusID, op) => {
|
||||
let request;
|
||||
switch(op) {
|
||||
case 'delete':
|
||||
request = seafileAPI.deleteVirusFile(virusID);
|
||||
break;
|
||||
case 'ignore':
|
||||
request = seafileAPI.toggleIgnoreVirusFile(virusID, true);
|
||||
break;
|
||||
case 'do-not-ignore':
|
||||
request = seafileAPI.toggleIgnoreVirusFile(virusID, false);
|
||||
break;
|
||||
}
|
||||
request.then((res) => {
|
||||
this.setState({
|
||||
virusFiles: this.state.virusFiles.map((item) => {
|
||||
if (item.virus_id == virusID) {
|
||||
if (op == 'delete') {
|
||||
item.has_deleted = true;
|
||||
} else {
|
||||
item = res.data.virus_file;
|
||||
}
|
||||
}
|
||||
return item;
|
||||
})
|
||||
});
|
||||
}).catch((error) => {
|
||||
toaster.danger(Utils.getErrorMsg(error));
|
||||
});
|
||||
}
|
||||
|
||||
toggleAllSelected = () => {
|
||||
this.setState((prevState) => ({
|
||||
isAllItemsSelected: !prevState.isAllItemsSelected,
|
||||
virusFiles: this.state.virusFiles.map((item) => {
|
||||
item.isSelected = !prevState.isAllItemsSelected;
|
||||
return item;
|
||||
})
|
||||
}));
|
||||
}
|
||||
|
||||
toggleItemSelected = (targetItem, isSelected) => {
|
||||
this.setState({
|
||||
virusFiles: this.state.virusFiles.map((item) => {
|
||||
if (item === targetItem) {
|
||||
item.isSelected = isSelected;
|
||||
}
|
||||
return item;
|
||||
})
|
||||
}, () => {
|
||||
this.setState({
|
||||
isAllItemsSelected: !this.state.virusFiles.some(item => !item.isSelected)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
handleSelectedItems = (op) => {
|
||||
// op: 'delete-virus', 'ignore-virus'
|
||||
const virusIDs = this.state.virusFiles
|
||||
.filter(item => {
|
||||
if (op == 'delete-virus') {
|
||||
return item.isSelected && !item.has_deleted;
|
||||
} else {
|
||||
return item.isSelected && !item.has_ignored;
|
||||
}
|
||||
})
|
||||
.map(item => item.virus_id);
|
||||
seafileAPI.batchProcessVirusFiles(virusIDs, op).then((res) => {
|
||||
let fileList = this.state.virusFiles;
|
||||
res.data.success.forEach(item => {
|
||||
let file = fileList.find(file => file.virus_id == item.virus_id);
|
||||
if (op == 'delete-virus') {
|
||||
file.has_deleted = true;
|
||||
} else {
|
||||
file.has_ignored = true;
|
||||
}
|
||||
});
|
||||
this.setState({
|
||||
virusFiles: fileList
|
||||
});
|
||||
|
||||
res.data.failed.forEach(item => {
|
||||
const file = fileList.find(file => file.virus_id == item.virus_id);
|
||||
let errMsg = op == 'delete-virus' ?
|
||||
gettext('Failed to delete %(virus_file) from library %(library): %(error_msg)') :
|
||||
gettext('Failed to ignore %(virus_file) from library %(library): %(error_msg)');
|
||||
errMsg = errMsg.replace('%(virus_file)', file.file_path)
|
||||
.replace('%(library)', file.repo_name)
|
||||
.replace('%(error_msg)', item.error_msg);
|
||||
toaster.danger(errMsg);
|
||||
});
|
||||
}).catch((error) => {
|
||||
toaster.danger(Utils.getErrorMsg(error));
|
||||
});
|
||||
}
|
||||
|
||||
deleteSelectedItems = () => {
|
||||
const op = 'delete-virus';
|
||||
this.handleSelectedItems(op);
|
||||
}
|
||||
|
||||
ignoreSelectedItems = () => {
|
||||
const op = 'ignore-virus';
|
||||
this.handleSelectedItems(op);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Fragment>
|
||||
{this.state.virusFiles.some(item => item.isSelected) ? (
|
||||
<MainPanelTopbar>
|
||||
<Fragment>
|
||||
<Button onClick={this.deleteSelectedItems} className="operation-item">{gettext('Delete')}</Button>
|
||||
<Button onClick={this.ignoreSelectedItems} className="operation-item">{gettext('Ignore')}</Button>
|
||||
</Fragment>
|
||||
</MainPanelTopbar>
|
||||
) : <MainPanelTopbar />
|
||||
}
|
||||
<div className="main-panel-center">
|
||||
<div className="cur-view-container">
|
||||
<Nav currentItem="unhandled" />
|
||||
<div className="cur-view-content">
|
||||
<Content
|
||||
loading={this.state.loading}
|
||||
errorMsg={this.state.errorMsg}
|
||||
virusFiles={this.state.virusFiles}
|
||||
currentPage={this.state.currentPage}
|
||||
hasNextPage={this.state.hasNextPage}
|
||||
curPerPage={this.state.perPage}
|
||||
resetPerPage={this.resetPerPage}
|
||||
getListByPage={this.getListByPage}
|
||||
handleFile={this.handleFile}
|
||||
isAllItemsSelected={this.state.isAllItemsSelected}
|
||||
toggleAllSelected={this.toggleAllSelected}
|
||||
toggleItemSelected={this.toggleItemSelected}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default UnhandledVirusFiles;
|
Reference in New Issue
Block a user