1
0
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:
llj
2020-07-09 14:15:17 +08:00
committed by GitHub
parent 72b1edd832
commit fa3964332f
10 changed files with 994 additions and 265 deletions

View File

@@ -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}

View File

@@ -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>

View File

@@ -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;

View 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;

View 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;

View 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;