mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-26 07:22:34 +00:00
Optimize/tabs animation (#7998)
* activities tabs * radio group * optimize statistic router * statistic tabs * devices tabs * libraries tabs * users tabs * optimize users * optimize libraries related routers * logs tab * virus scan & admin logs * optimize * revert debug code * optimize --------- Co-authored-by: zhouwenxuan <aries@Mac.local>
This commit is contained in:
@@ -18,7 +18,6 @@ import {
|
|||||||
MIN_SIDE_PANEL_RATE
|
MIN_SIDE_PANEL_RATE
|
||||||
} from './components/resize-bar/constants';
|
} from './components/resize-bar/constants';
|
||||||
import FilesActivities from './pages/dashboard/files-activities';
|
import FilesActivities from './pages/dashboard/files-activities';
|
||||||
import MyFileActivities from './pages/dashboard/my-file-activities';
|
|
||||||
import Starred from './pages/starred/starred';
|
import Starred from './pages/starred/starred';
|
||||||
import LinkedDevices from './pages/linked-devices/linked-devices';
|
import LinkedDevices from './pages/linked-devices/linked-devices';
|
||||||
import ShareAdminLibraries from './pages/share-admin/libraries';
|
import ShareAdminLibraries from './pages/share-admin/libraries';
|
||||||
@@ -330,8 +329,10 @@ class App extends Component {
|
|||||||
/>
|
/>
|
||||||
<Starred path={siteRoot + 'starred'} />
|
<Starred path={siteRoot + 'starred'} />
|
||||||
<InvitationsView path={siteRoot + 'invitations/'} />
|
<InvitationsView path={siteRoot + 'invitations/'} />
|
||||||
<FilesActivities path={siteRoot + 'dashboard'} />
|
<FilesActivities
|
||||||
<MyFileActivities path={siteRoot + 'my-activities'} />
|
path={`${siteRoot}(dashboard|my-activities)/*`}
|
||||||
|
default
|
||||||
|
/>
|
||||||
<GroupView path={siteRoot + 'group/:groupID'} />
|
<GroupView path={siteRoot + 'group/:groupID'} />
|
||||||
<LinkedDevices path={siteRoot + 'linked-devices'} />
|
<LinkedDevices path={siteRoot + 'linked-devices'} />
|
||||||
<ShareAdminLibraries path={siteRoot + 'share-admin-libs'} />
|
<ShareAdminLibraries path={siteRoot + 'share-admin-libs'} />
|
||||||
|
@@ -32,4 +32,13 @@ export const EVENT_BUS_TYPE = {
|
|||||||
PERMISSION: 'permission',
|
PERMISSION: 'permission',
|
||||||
ACCESS_LOG: 'access_log',
|
ACCESS_LOG: 'access_log',
|
||||||
PREVIEW_IMAGE: 'preview_image',
|
PREVIEW_IMAGE: 'preview_image',
|
||||||
|
|
||||||
|
// sys-admin pages
|
||||||
|
CLEAR_DEVICE_ERRORS: 'clear_device_errors',
|
||||||
|
SHOW_CLEAN_BTN: 'show_clean_btn',
|
||||||
|
SYNC_USERNAME: 'sync_username',
|
||||||
|
OPEN_CREATE_REPO_DIALOG: 'open_create_repo_dialog',
|
||||||
|
OPEN_CLEAN_TRASH_DIALOG: 'open_clean_trash_dialog',
|
||||||
|
HANDLE_SELECTED_OPERATIONS: 'handle_selected_operations',
|
||||||
|
RESET_PER_PAGE: 'reset_per_page',
|
||||||
};
|
};
|
||||||
|
@@ -32,13 +32,13 @@ class LogsExportExcelDialog extends React.Component {
|
|||||||
case 'login':
|
case 'login':
|
||||||
this.sysExportLogs('loginadmin');
|
this.sysExportLogs('loginadmin');
|
||||||
break;
|
break;
|
||||||
case 'fileAccess':
|
case 'file-access':
|
||||||
this.sysExportLogs('fileaudit');
|
this.sysExportLogs('fileaudit');
|
||||||
break;
|
break;
|
||||||
case 'fileUpdate':
|
case 'file-update':
|
||||||
this.sysExportLogs('fileupdate');
|
this.sysExportLogs('fileupdate');
|
||||||
break;
|
break;
|
||||||
case 'sharePermission':
|
case 'share-permission':
|
||||||
this.sysExportLogs('permaudit');
|
this.sysExportLogs('permaudit');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@@ -72,3 +72,26 @@
|
|||||||
.activity-user-name {
|
.activity-user-name {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav.activities-nav-indicator-container .nav-item .nav-link {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activities-nav-indicator-container::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
height: 2px;
|
||||||
|
width: 80px;
|
||||||
|
background: #ED7109;
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activities-nav-indicator-container[data-active="all"]::before {
|
||||||
|
transform: translateX(0%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activities-nav-indicator-container[data-active="mine"]::before {
|
||||||
|
transform: translateX(108px);
|
||||||
|
}
|
||||||
|
@@ -355,3 +355,19 @@ img[src=""],img:not([src]) { /* for first loading img*/
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #666;
|
color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-indicator-container .nav-item .nav-link {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-indicator-container::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
height: 2px;
|
||||||
|
width: var(--indicator-width);
|
||||||
|
background: #ED7109;
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
transform: translateX(var(--indicator-offset));
|
||||||
|
}
|
||||||
|
@@ -40,7 +40,7 @@ const GalleryGroupBySetter = ({ viewID }) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{currentMode === GALLERY_DATE_MODE.ALL && <GallerySliderSetter viewID={viewID} />}
|
{currentMode === GALLERY_DATE_MODE.ALL && <GallerySliderSetter viewID={viewID} />}
|
||||||
<RadioGroup value={currentMode} options={DATE_MODES} onChange={handleGroupByChange} />
|
<RadioGroup className="sf-metadata-gallery-groupby-setter" value={currentMode} options={DATE_MODES} onChange={handleGroupByChange} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -23,7 +23,7 @@ const MapTypeSetter = ({ viewID }) => {
|
|||||||
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.MODIFY_MAP_TYPE, type);
|
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.MODIFY_MAP_TYPE, type);
|
||||||
}, [currentType]);
|
}, [currentType]);
|
||||||
|
|
||||||
return (<RadioGroup options={MAP_TYPES} value={currentType} onChange={onChange} />);
|
return (<RadioGroup className="sf-metadata-map-type-setter" options={MAP_TYPES} value={currentType} onChange={onChange} />);
|
||||||
};
|
};
|
||||||
|
|
||||||
MapTypeSetter.propTypes = {
|
MapTypeSetter.propTypes = {
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
.sf-metadata-radio-group {
|
.sf-metadata-radio-group {
|
||||||
|
position: relative;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -15,7 +16,6 @@
|
|||||||
width: fit-content;
|
width: fit-content;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
color: #212529;
|
color: #212529;
|
||||||
background-color: #fff;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -30,10 +30,6 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sf-metadata-radio-group .sf-metadata-radio-group-option.active {
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-metadata-radio-group .sf-metadata-radio-group-option:not(:first-child)::before {
|
.sf-metadata-radio-group .sf-metadata-radio-group-option:not(:first-child)::before {
|
||||||
content: '';
|
content: '';
|
||||||
width: 1px;
|
width: 1px;
|
||||||
@@ -52,3 +48,39 @@
|
|||||||
.sf-metadata-radio-group .sf-metadata-radio-group-option.active + .sf-metadata-radio-group-option::before {
|
.sf-metadata-radio-group .sf-metadata-radio-group-option.active + .sf-metadata-radio-group-option::before {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sf-metadata-radio-group::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 4px;
|
||||||
|
width: 66px;
|
||||||
|
height: 28px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-radio-group.sf-metadata-gallery-groupby-setter[data-active="year"]::before {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-radio-group.sf-metadata-gallery-groupby-setter[data-active="month"]::before {
|
||||||
|
transform: translateX(65px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-radio-group.sf-metadata-gallery-groupby-setter[data-active="day"]::before {
|
||||||
|
transform: translateX(131px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-radio-group.sf-metadata-gallery-groupby-setter[data-active="all"]::before {
|
||||||
|
transform: translateX(197px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-radio-group.sf-metadata-map-type-setter[data-active="map"]::before {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-radio-group.sf-metadata-map-type-setter[data-active="satellite"]::before {
|
||||||
|
transform: translateX(65px);
|
||||||
|
}
|
||||||
|
@@ -17,7 +17,7 @@ const RadioGroup = ({ value, options, className, onChange: onChangeAPI }) => {
|
|||||||
}, [selected, onChangeAPI]);
|
}, [selected, onChangeAPI]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classnames('sf-metadata-radio-group', className)}>
|
<div className={classnames('sf-metadata-radio-group', className)} data-active={value}>
|
||||||
{options.map(option => {
|
{options.map(option => {
|
||||||
const { value, label } = option;
|
const { value, label } = option;
|
||||||
return (
|
return (
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { Link } from '@gatsbyjs/reach-router';
|
import { Link, globalHistory } from '@gatsbyjs/reach-router';
|
||||||
import { seafileAPI } from '../../utils/seafile-api';
|
import { seafileAPI } from '../../utils/seafile-api';
|
||||||
import { gettext, siteRoot, username } from '../../utils/constants';
|
import { gettext, siteRoot, username } from '../../utils/constants';
|
||||||
import { Utils } from '../../utils/utils';
|
import { Utils } from '../../utils/utils';
|
||||||
@@ -14,14 +13,11 @@ import '../../css/files-activities.css';
|
|||||||
|
|
||||||
dayjs.locale(window.app.config.lang);
|
dayjs.locale(window.app.config.lang);
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
onlyMine: PropTypes.bool
|
|
||||||
};
|
|
||||||
|
|
||||||
class FilesActivities extends Component {
|
class FilesActivities extends Component {
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
const isMyActivities = props.uri.includes('my-activities');
|
||||||
this.state = {
|
this.state = {
|
||||||
errorMsg: '',
|
errorMsg: '',
|
||||||
isFirstLoading: true,
|
isFirstLoading: true,
|
||||||
@@ -31,7 +27,8 @@ class FilesActivities extends Component {
|
|||||||
allItems: [],
|
allItems: [],
|
||||||
items: [],
|
items: [],
|
||||||
availableUsers: [],
|
availableUsers: [],
|
||||||
targetUsers: []
|
targetUsers: [],
|
||||||
|
onlyMine: isMyActivities
|
||||||
};
|
};
|
||||||
this.curPathList = [];
|
this.curPathList = [];
|
||||||
this.oldPathList = [];
|
this.oldPathList = [];
|
||||||
@@ -75,6 +72,15 @@ class FilesActivities extends Component {
|
|||||||
errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403
|
errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.unlisten = globalHistory.listen(({ location }) => {
|
||||||
|
const isMyActivities = location.pathname.includes('my-activities');
|
||||||
|
this.setState({ onlyMine: isMyActivities });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.unlisten && this.unlisten();
|
||||||
}
|
}
|
||||||
|
|
||||||
mergePublishEvents = (events) => {
|
mergePublishEvents = (events) => {
|
||||||
@@ -232,13 +238,12 @@ class FilesActivities extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { onlyMine } = this.props;
|
const { targetUsers, availableUsers, onlyMine } = this.state;
|
||||||
const { targetUsers, availableUsers } = this.state;
|
|
||||||
return (
|
return (
|
||||||
<div className="main-panel-center">
|
<div className="main-panel-center">
|
||||||
<div className="cur-view-container" id="activities">
|
<div className="cur-view-container" id="activities">
|
||||||
<div className="cur-view-path">
|
<div className="cur-view-path">
|
||||||
<ul className="nav">
|
<ul className="nav activities-nav-indicator-container position-relative" data-active={onlyMine ? 'mine' : 'all'}>
|
||||||
<li className="nav-item">
|
<li className="nav-item">
|
||||||
<Link to={`${siteRoot}dashboard/`} className={`nav-link${onlyMine ? '' : ' active'}`}>{gettext('All Activities')}</Link>
|
<Link to={`${siteRoot}dashboard/`} className={`nav-link${onlyMine ? '' : ' active'}`}>{gettext('All Activities')}</Link>
|
||||||
</li>
|
</li>
|
||||||
@@ -273,6 +278,4 @@ class FilesActivities extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FilesActivities.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default FilesActivities;
|
export default FilesActivities;
|
||||||
|
18
frontend/src/pages/sys-admin/admin-logs/index.js
Normal file
18
frontend/src/pages/sys-admin/admin-logs/index.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import MainPanelTopbar from '../main-panel-topbar';
|
||||||
|
import LogsNav from './logs-nav';
|
||||||
|
import { useLocation } from '@gatsbyjs/reach-router';
|
||||||
|
|
||||||
|
const AdminLogs = ({ children, ...commonProps }) => {
|
||||||
|
const location = useLocation();
|
||||||
|
const path = location.pathname.split('/').filter(Boolean).pop();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MainPanelTopbar {...commonProps} />
|
||||||
|
<LogsNav currentItem={path} />
|
||||||
|
<div className="h-100 d-flex overflow-auto">{children}</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdminLogs;
|
@@ -1,4 +1,4 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
@@ -8,8 +8,6 @@ import { Utils } from '../../../utils/utils';
|
|||||||
import EmptyTip from '../../../components/empty-tip';
|
import EmptyTip from '../../../components/empty-tip';
|
||||||
import Loading from '../../../components/loading';
|
import Loading from '../../../components/loading';
|
||||||
import Paginator from '../../../components/paginator';
|
import Paginator from '../../../components/paginator';
|
||||||
import LogsNav from './logs-nav';
|
|
||||||
import MainPanelTopbar from '../main-panel-topbar';
|
|
||||||
import UserLink from '../user-link';
|
import UserLink from '../user-link';
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
@@ -40,7 +38,7 @@ class Content extends Component {
|
|||||||
</EmptyTip>
|
</EmptyTip>
|
||||||
);
|
);
|
||||||
const table = (
|
const table = (
|
||||||
<Fragment>
|
<>
|
||||||
<table className="table-hover">
|
<table className="table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -69,7 +67,7 @@ class Content extends Component {
|
|||||||
curPerPage={perPage}
|
curPerPage={perPage}
|
||||||
resetPerPage={this.props.resetPerPage}
|
resetPerPage={this.props.resetPerPage}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</>
|
||||||
);
|
);
|
||||||
return items.length ? table : emptyTip;
|
return items.length ? table : emptyTip;
|
||||||
}
|
}
|
||||||
@@ -163,11 +161,8 @@ class AdminLoginLogs extends Component {
|
|||||||
render() {
|
render() {
|
||||||
let { logList, currentPage, perPage, hasNextPage } = this.state;
|
let { logList, currentPage, perPage, hasNextPage } = this.state;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
|
||||||
<MainPanelTopbar {...this.props} />
|
|
||||||
<div className="main-panel-center flex-row">
|
<div className="main-panel-center flex-row">
|
||||||
<div className="cur-view-container">
|
<div className="cur-view-container">
|
||||||
<LogsNav currentItem="adminLoginLogs" />
|
|
||||||
<div className="cur-view-content">
|
<div className="cur-view-content">
|
||||||
<Content
|
<Content
|
||||||
loading={this.state.loading}
|
loading={this.state.loading}
|
||||||
@@ -182,7 +177,6 @@ class AdminLoginLogs extends Component {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -12,19 +12,38 @@ class LogsNav extends React.Component {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.navItems = [
|
this.navItems = [
|
||||||
{ name: 'adminOperationLogs', urlPart: 'admin-logs/operation', text: gettext('Admin Operation Logs') },
|
{ name: 'operation', urlPart: 'admin-logs/operation', text: gettext('Admin Operation Logs') },
|
||||||
{ name: 'adminLoginLogs', urlPart: 'admin-logs/login', text: gettext('Admin Login Logs') },
|
{ name: 'login', urlPart: 'admin-logs/login', text: gettext('Admin Login Logs') },
|
||||||
];
|
];
|
||||||
|
this.itemRefs = [];
|
||||||
|
this.itemWidths = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.itemWidths = this.itemRefs.map(ref => ref?.offsetWidth || 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { currentItem } = this.props;
|
const { currentItem } = this.props;
|
||||||
|
const activeIndex = this.navItems.findIndex(item => item.name === currentItem) || 0;
|
||||||
|
const indicatorWidth = this.itemWidths[activeIndex] || 167;
|
||||||
|
const indicatorOffset = this.itemWidths.slice(0, activeIndex).reduce((prev, cur) => prev + cur, 0);
|
||||||
return (
|
return (
|
||||||
<div className="cur-view-path tab-nav-container">
|
<div className="cur-view-path tab-nav-container">
|
||||||
<ul className="nav">
|
<ul
|
||||||
|
className="nav nav-indicator-container position-relative"
|
||||||
|
style={{
|
||||||
|
'--indicator-width': `${indicatorWidth}px`,
|
||||||
|
'--indicator-offset': `${indicatorOffset}px`
|
||||||
|
}}
|
||||||
|
>
|
||||||
{this.navItems.map((item, index) => {
|
{this.navItems.map((item, index) => {
|
||||||
return (
|
return (
|
||||||
<li className="nav-item" key={index}>
|
<li
|
||||||
|
className="nav-item"
|
||||||
|
key={index}
|
||||||
|
ref={el => this.itemRefs[index] = el}
|
||||||
|
>
|
||||||
<Link to={`${siteRoot}sys/${item.urlPart}/`} className={`nav-link${currentItem == item.name ? ' active' : ''}`}>{item.text}</Link>
|
<Link to={`${siteRoot}sys/${item.urlPart}/`} className={`nav-link${currentItem == item.name ? ' active' : ''}`}>{item.text}</Link>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
@@ -8,8 +8,6 @@ import { Utils } from '../../../utils/utils';
|
|||||||
import EmptyTip from '../../../components/empty-tip';
|
import EmptyTip from '../../../components/empty-tip';
|
||||||
import Loading from '../../../components/loading';
|
import Loading from '../../../components/loading';
|
||||||
import Paginator from '../../../components/paginator';
|
import Paginator from '../../../components/paginator';
|
||||||
import LogsNav from './logs-nav';
|
|
||||||
import MainPanelTopbar from '../main-panel-topbar';
|
|
||||||
import UserLink from '../user-link';
|
import UserLink from '../user-link';
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
@@ -40,7 +38,7 @@ class Content extends Component {
|
|||||||
</EmptyTip>
|
</EmptyTip>
|
||||||
);
|
);
|
||||||
const table = (
|
const table = (
|
||||||
<Fragment>
|
<>
|
||||||
<table className="table-hover">
|
<table className="table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -69,7 +67,7 @@ class Content extends Component {
|
|||||||
curPerPage={perPage}
|
curPerPage={perPage}
|
||||||
resetPerPage={this.props.resetPerPage}
|
resetPerPage={this.props.resetPerPage}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</>
|
||||||
);
|
);
|
||||||
return items.length ? table : emptyTip;
|
return items.length ? table : emptyTip;
|
||||||
}
|
}
|
||||||
@@ -282,11 +280,8 @@ class AdminOperationLogs extends Component {
|
|||||||
render() {
|
render() {
|
||||||
let { logList, currentPage, perPage, hasNextPage } = this.state;
|
let { logList, currentPage, perPage, hasNextPage } = this.state;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
|
||||||
<MainPanelTopbar {...this.props} />
|
|
||||||
<div className="main-panel-center flex-row">
|
<div className="main-panel-center flex-row">
|
||||||
<div className="cur-view-container">
|
<div className="cur-view-container">
|
||||||
<LogsNav currentItem="adminOperationLogs" />
|
|
||||||
<div className="cur-view-content">
|
<div className="cur-view-content">
|
||||||
<Content
|
<Content
|
||||||
loading={this.state.loading}
|
loading={this.state.loading}
|
||||||
@@ -301,7 +296,6 @@ class AdminOperationLogs extends Component {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,5 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component } from 'react';
|
||||||
import DevicesNav from './devices-nav';
|
|
||||||
import DevicesByPlatform from './devices-by-platform';
|
import DevicesByPlatform from './devices-by-platform';
|
||||||
import MainPanelTopbar from '../main-panel-topbar';
|
|
||||||
|
|
||||||
class DesktopDevices extends Component {
|
class DesktopDevices extends Component {
|
||||||
|
|
||||||
@@ -11,17 +9,13 @@ class DesktopDevices extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
|
||||||
<MainPanelTopbar {...this.props} />
|
|
||||||
<div className="main-panel-center flex-row">
|
<div className="main-panel-center flex-row">
|
||||||
<div className="cur-view-container">
|
<div className="cur-view-container">
|
||||||
<DevicesNav currentItem="desktop" />
|
|
||||||
<DevicesByPlatform
|
<DevicesByPlatform
|
||||||
devicesPlatform={'desktop'}
|
devicesPlatform={'desktop'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,19 +1,17 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Button } from 'reactstrap';
|
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
import { systemAdminAPI } from '../../../utils/system-admin-api';
|
import { systemAdminAPI } from '../../../utils/system-admin-api';
|
||||||
import { siteRoot, gettext } from '../../../utils/constants';
|
import { siteRoot, gettext } from '../../../utils/constants';
|
||||||
import toaster from '../../../components/toast';
|
|
||||||
import { Utils } from '../../../utils/utils';
|
import { Utils } from '../../../utils/utils';
|
||||||
import EmptyTip from '../../../components/empty-tip';
|
import EmptyTip from '../../../components/empty-tip';
|
||||||
import Loading from '../../../components/loading';
|
import Loading from '../../../components/loading';
|
||||||
import { Link } from '@gatsbyjs/reach-router';
|
import { Link } from '@gatsbyjs/reach-router';
|
||||||
import DevicesNav from './devices-nav';
|
|
||||||
import MainPanelTopbar from '../main-panel-topbar';
|
|
||||||
import UserLink from '../user-link';
|
import UserLink from '../user-link';
|
||||||
import Paginator from '../../../components/paginator';
|
import Paginator from '../../../components/paginator';
|
||||||
|
import { eventBus } from '../../../components/common/event-bus';
|
||||||
|
import { EVENT_BUS_TYPE } from '../../../components/common/event-bus-type';
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
@@ -135,10 +133,9 @@ class DeviceErrors extends Component {
|
|||||||
this.state = {
|
this.state = {
|
||||||
loading: true,
|
loading: true,
|
||||||
errorMsg: '',
|
errorMsg: '',
|
||||||
devicesErrors: [],
|
|
||||||
isCleanBtnShown: false,
|
|
||||||
pageInfo: {},
|
pageInfo: {},
|
||||||
perPage: 100
|
perPage: 100,
|
||||||
|
deviceErrors: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,6 +148,12 @@ class DeviceErrors extends Component {
|
|||||||
}, () => {
|
}, () => {
|
||||||
this.getDeviceErrorsListByPage(this.state.currentPage);
|
this.getDeviceErrorsListByPage(this.state.currentPage);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.unsubscribeClearDeviceErrors = eventBus.subscribe(EVENT_BUS_TYPE.CLEAR_DEVICE_ERRORS, () => this.setState({ deviceErrors: [] }));
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.unsubscribeClearDeviceErrors();
|
||||||
}
|
}
|
||||||
|
|
||||||
getDeviceErrorsListByPage = (page) => {
|
getDeviceErrorsListByPage = (page) => {
|
||||||
@@ -158,10 +161,11 @@ class DeviceErrors extends Component {
|
|||||||
systemAdminAPI.sysAdminListDeviceErrors(page, per_page).then((res) => {
|
systemAdminAPI.sysAdminListDeviceErrors(page, per_page).then((res) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: false,
|
loading: false,
|
||||||
devicesErrors: res.data.device_errors,
|
|
||||||
pageInfo: res.data.page_info,
|
pageInfo: res.data.page_info,
|
||||||
isCleanBtnShown: res.data.device_errors.length > 0
|
|
||||||
});
|
});
|
||||||
|
if (res.data.device_errors.length > 0) {
|
||||||
|
eventBus.dispatch(EVENT_BUS_TYPE.SHOW_CLEAN_BTN);
|
||||||
|
}
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: false,
|
loading: false,
|
||||||
@@ -170,20 +174,6 @@ class DeviceErrors extends Component {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
clean = () => {
|
|
||||||
systemAdminAPI.sysAdminClearDeviceErrors().then((res) => {
|
|
||||||
this.setState({
|
|
||||||
devicesErrors: [],
|
|
||||||
isCleanBtnShown: false
|
|
||||||
});
|
|
||||||
let message = gettext('Successfully cleaned all errors.');
|
|
||||||
toaster.success(message);
|
|
||||||
}).catch((error) => {
|
|
||||||
let errMessage = Utils.getErrorMsg(error);
|
|
||||||
toaster.danger(errMessage);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
resetPerPage = (perPage) => {
|
resetPerPage = (perPage) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
perPage: perPage
|
perPage: perPage
|
||||||
@@ -193,22 +183,13 @@ class DeviceErrors extends Component {
|
|||||||
};
|
};
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
|
||||||
{this.state.isCleanBtnShown ? (
|
|
||||||
<MainPanelTopbar {...this.props}>
|
|
||||||
<Button className="operation-item" onClick={this.clean}>{gettext('Clean')}</Button>
|
|
||||||
</MainPanelTopbar>
|
|
||||||
) : (
|
|
||||||
<MainPanelTopbar {...this.props} />
|
|
||||||
)}
|
|
||||||
<div className="main-panel-center flex-row">
|
<div className="main-panel-center flex-row">
|
||||||
<div className="cur-view-container">
|
<div className="cur-view-container">
|
||||||
<DevicesNav currentItem="errors" />
|
|
||||||
<div className="cur-view-content">
|
<div className="cur-view-content">
|
||||||
<Content
|
<Content
|
||||||
loading={this.state.loading}
|
loading={this.state.loading}
|
||||||
errorMsg={this.state.errorMsg}
|
errorMsg={this.state.errorMsg}
|
||||||
items={this.state.devicesErrors}
|
items={this.state.deviceErrors}
|
||||||
getDeviceErrorsListByPage={this.getDeviceErrorsListByPage}
|
getDeviceErrorsListByPage={this.getDeviceErrorsListByPage}
|
||||||
curPerPage={this.state.perPage}
|
curPerPage={this.state.perPage}
|
||||||
resetPerPage={this.resetPerPage}
|
resetPerPage={this.resetPerPage}
|
||||||
@@ -217,7 +198,6 @@ class DeviceErrors extends Component {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -12,23 +12,47 @@ class Nav extends React.Component {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.navItems = [
|
this.navItems = [
|
||||||
{ name: 'desktop', urlPart: 'desktop-devices', text: gettext('Desktop') },
|
{ name: 'desktop', urlPart: 'desktop', text: gettext('Desktop') },
|
||||||
{ name: 'mobile', urlPart: 'mobile-devices', text: gettext('Mobile') }
|
{ name: 'mobile', urlPart: 'mobile', text: gettext('Mobile') }
|
||||||
];
|
];
|
||||||
if (isPro) {
|
if (isPro) {
|
||||||
this.navItems.push({ name: 'errors', urlPart: 'device-errors', text: gettext('Errors') });
|
this.navItems.push({ name: 'errors', urlPart: 'errors', text: gettext('Errors') });
|
||||||
}
|
}
|
||||||
|
this.itemRefs = [];
|
||||||
|
this.itemWidths = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.measureItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
measureItems = () => {
|
||||||
|
this.itemWidths = this.itemRefs.map(ref => ref?.offsetWidth || 77);
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { currentItem } = this.props;
|
const { currentItem } = this.props;
|
||||||
|
const activeIndex = this.navItems.findIndex(item => item.name === currentItem) || 0;
|
||||||
|
const indicatorWidth = this.itemWidths[activeIndex] || 77;
|
||||||
|
const indicatorOffset = this.itemWidths.slice(0, activeIndex).reduce((a, b) => a + b, 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="cur-view-path tab-nav-container">
|
<div className="cur-view-path tab-nav-container">
|
||||||
<ul className="nav">
|
<ul
|
||||||
|
className="nav nav-indicator-container position-relative"
|
||||||
|
style={{
|
||||||
|
'--indicator-width': `${indicatorWidth}px`,
|
||||||
|
'--indicator-offset': `${indicatorOffset}px`
|
||||||
|
}}
|
||||||
|
>
|
||||||
{this.navItems.map((item, index) => {
|
{this.navItems.map((item, index) => {
|
||||||
return (
|
return (
|
||||||
<li className="nav-item" key={index}>
|
<li
|
||||||
<Link to={`${siteRoot}sys/${item.urlPart}/`} className={`nav-link${currentItem == item.name ? ' active' : ''}`}>{item.text}</Link>
|
className="nav-item"
|
||||||
|
key={index}
|
||||||
|
ref={el => this.itemRefs[index] = el}
|
||||||
|
>
|
||||||
|
<Link to={`${siteRoot}sys/devices/${item.urlPart}/`} className={`nav-link${currentItem == item.name ? ' active' : ''}`}>{item.text}</Link>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
56
frontend/src/pages/sys-admin/devices/index.js
Normal file
56
frontend/src/pages/sys-admin/devices/index.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import MainPanelTopbar from '../main-panel-topbar';
|
||||||
|
import DevicesNav from './devices-nav';
|
||||||
|
import { useLocation } from '@gatsbyjs/reach-router';
|
||||||
|
import { Button } from 'reactstrap';
|
||||||
|
import { gettext } from '../../../utils/constants';
|
||||||
|
import { systemAdminAPI } from '../../../utils/system-admin-api';
|
||||||
|
import toaster from '../../../components/toast';
|
||||||
|
import { Utils } from '../../../utils/utils';
|
||||||
|
import { EVENT_BUS_TYPE } from '../../../components/common/event-bus-type';
|
||||||
|
import { eventBus } from '../../../components/common/event-bus';
|
||||||
|
|
||||||
|
const Devices = ({ children, ...commonProps }) => {
|
||||||
|
const [isCleanBtnShown, setIsCleanBtnShown] = useState(false);
|
||||||
|
|
||||||
|
const location = useLocation();
|
||||||
|
const path = location.pathname.split('/').filter(Boolean).pop();
|
||||||
|
|
||||||
|
const onClean = useCallback(() => {
|
||||||
|
systemAdminAPI.sysAdminClearDeviceErrors().then((res) => {
|
||||||
|
eventBus.dispatch(EVENT_BUS_TYPE.CLEAR_DEVICE_ERRORS);
|
||||||
|
setIsCleanBtnShown(false);
|
||||||
|
let message = gettext('Successfully cleaned all errors.');
|
||||||
|
toaster.success(message);
|
||||||
|
}).catch((error) => {
|
||||||
|
let errMessage = Utils.getErrorMsg(error);
|
||||||
|
toaster.danger(errMessage);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribeShowCleanBtn = eventBus.subscribe(EVENT_BUS_TYPE.SHOW_CLEAN_BTN, () => {
|
||||||
|
setIsCleanBtnShown(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribeShowCleanBtn();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{path === 'errors' && isCleanBtnShown ? (
|
||||||
|
<MainPanelTopbar {...commonProps}>
|
||||||
|
<Button className="operation-item" onClick={onClean}>{gettext('Clean')}</Button>
|
||||||
|
</MainPanelTopbar>
|
||||||
|
) : (
|
||||||
|
<MainPanelTopbar { ...commonProps } />
|
||||||
|
)}
|
||||||
|
<DevicesNav currentItem={path} />
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Devices;
|
@@ -1,7 +1,5 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component } from 'react';
|
||||||
import DevicesNav from './devices-nav';
|
|
||||||
import DevicesByPlatform from './devices-by-platform';
|
import DevicesByPlatform from './devices-by-platform';
|
||||||
import MainPanelTopbar from '../main-panel-topbar';
|
|
||||||
|
|
||||||
class MobileDevices extends Component {
|
class MobileDevices extends Component {
|
||||||
|
|
||||||
@@ -11,17 +9,13 @@ class MobileDevices extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
|
||||||
<MainPanelTopbar {...this.props} />
|
|
||||||
<div className="main-panel-center flex-row">
|
<div className="main-panel-center flex-row">
|
||||||
<div className="cur-view-container">
|
<div className="cur-view-container">
|
||||||
<DevicesNav currentItem="mobile" />
|
|
||||||
<DevicesByPlatform
|
<DevicesByPlatform
|
||||||
devicesPlatform={'mobile'}
|
devicesPlatform={'mobile'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -19,26 +19,16 @@ import StatisticTraffic from './statistic/statistic-traffic';
|
|||||||
import StatisticUsers from './statistic/statistic-users';
|
import StatisticUsers from './statistic/statistic-users';
|
||||||
import StatisticReport from './statistic/statistic-reports';
|
import StatisticReport from './statistic/statistic-reports';
|
||||||
import StatisticMetrics from './statistic/statistic-metrics';
|
import StatisticMetrics from './statistic/statistic-metrics';
|
||||||
|
import StatisticLayout from './statistic/layout';
|
||||||
|
|
||||||
import DesktopDevices from './devices/desktop-devices';
|
|
||||||
import MobileDevices from './devices/mobile-devices';
|
|
||||||
import DeviceErrors from './devices/devices-errors';
|
|
||||||
|
|
||||||
import Users from './users/users';
|
|
||||||
import AdminUsers from './users/admin-users';
|
|
||||||
import LDAPImportedUsers from './users/ldap-imported-users';
|
|
||||||
import LDAPUsers from './users/ldap-users';
|
|
||||||
import SearchUsers from './users/search-users';
|
import SearchUsers from './users/search-users';
|
||||||
import User from './users/user-info';
|
import User from './users/user-info';
|
||||||
import UserOwnedRepos from './users/user-repos';
|
import UserOwnedRepos from './users/user-repos';
|
||||||
import UserSharedRepos from './users/user-shared-repos';
|
import UserSharedRepos from './users/user-shared-repos';
|
||||||
import UserLinks from './users/user-links';
|
import UserLinks from './users/user-links';
|
||||||
import UserGroups from './users/user-groups';
|
import UserGroups from './users/user-groups';
|
||||||
|
import { UsersLayout, UserLayout } from './users';
|
||||||
|
|
||||||
import AllRepos from './repos/all-repos';
|
|
||||||
import AllWikis from './repos/all-wikis';
|
|
||||||
import SystemRepo from './repos/system-repo';
|
|
||||||
import TrashRepos from './repos/trash-repos';
|
|
||||||
import SearchRepos from './repos/search-repos';
|
import SearchRepos from './repos/search-repos';
|
||||||
import DirView from './repos/dir-view';
|
import DirView from './repos/dir-view';
|
||||||
|
|
||||||
@@ -49,9 +39,6 @@ import GroupMembers from './groups/group-members';
|
|||||||
|
|
||||||
import Departments from './departments/departments';
|
import Departments from './departments/departments';
|
||||||
|
|
||||||
import ShareLinks from './links/share-links';
|
|
||||||
import UploadLinks from './links/upload-links';
|
|
||||||
|
|
||||||
import Orgs from './orgs/orgs';
|
import Orgs from './orgs/orgs';
|
||||||
import SearchOrgs from './orgs/search-orgs';
|
import SearchOrgs from './orgs/search-orgs';
|
||||||
import OrgInfo from './orgs/org-info';
|
import OrgInfo from './orgs/org-info';
|
||||||
@@ -85,6 +72,14 @@ import AdminOperationLogs from './admin-logs/operation-logs';
|
|||||||
import AdminLoginLogs from './admin-logs/login-logs';
|
import AdminLoginLogs from './admin-logs/login-logs';
|
||||||
|
|
||||||
import AbuseReports from './abuse-reports';
|
import AbuseReports from './abuse-reports';
|
||||||
|
import Devices from './devices';
|
||||||
|
import DesktopDevices from './devices/desktop-devices';
|
||||||
|
import MobileDevices from './devices/mobile-devices';
|
||||||
|
import DeviceErrors from './devices/devices-errors';
|
||||||
|
import LibrariesAndLinks from './libraries-and-links';
|
||||||
|
import Logs from './logs-page';
|
||||||
|
import VirusScan from './virus-scan';
|
||||||
|
import AdminLogs from './admin-logs';
|
||||||
|
|
||||||
import '../../css/layout.css';
|
import '../../css/layout.css';
|
||||||
import '../../css/toolbar.css';
|
import '../../css/toolbar.css';
|
||||||
@@ -218,21 +213,22 @@ class SysAdmin extends React.Component {
|
|||||||
<MainPanel>
|
<MainPanel>
|
||||||
<Router className="reach-router">
|
<Router className="reach-router">
|
||||||
<Info path={siteRoot + 'sys/info'} {...commonProps} />
|
<Info path={siteRoot + 'sys/info'} {...commonProps} />
|
||||||
<StatisticFile path={siteRoot + 'sys/statistics/file'} {...commonProps} />
|
<StatisticLayout path={`${siteRoot}sys/statistics/`} {...commonProps}>
|
||||||
<StatisticStorage path={siteRoot + 'sys/statistics/storage'} {...commonProps} />
|
<StatisticFile path="file" />
|
||||||
<StatisticUsers path={siteRoot + 'sys/statistics/user'} {...commonProps} />
|
<StatisticStorage path="storage" />
|
||||||
<StatisticTraffic path={siteRoot + 'sys/statistics/traffic'} {...commonProps} />
|
<StatisticUsers path="user" />
|
||||||
<StatisticReport path={siteRoot + 'sys/statistics/reports'} {...commonProps} />
|
<StatisticTraffic path="traffic" />
|
||||||
<StatisticMetrics path={siteRoot + 'sys/statistics/metrics'} {...commonProps} />
|
<StatisticReport path="reports" />
|
||||||
<DesktopDevices path={siteRoot + 'sys/desktop-devices'} {...commonProps} />
|
<StatisticMetrics path="metrics" />
|
||||||
<MobileDevices path={siteRoot + 'sys/mobile-devices'} {...commonProps} />
|
</StatisticLayout>
|
||||||
<DeviceErrors path={siteRoot + 'sys/device-errors'} {...commonProps} />
|
<LibrariesAndLinks path={`${siteRoot}sys/*`} {...commonProps} />
|
||||||
<AllRepos path={siteRoot + 'sys/all-libraries'} {...commonProps} />
|
<DirView path={`${siteRoot}sys/libraries/:repoID`} {...commonProps} />
|
||||||
<AllWikis path={siteRoot + 'sys/all-wikis'} {...commonProps} />
|
<Devices path={`${siteRoot}sys/devices/`} {...commonProps}>
|
||||||
<SystemRepo path={siteRoot + 'sys/system-library'} {...commonProps} />
|
<DesktopDevices path="desktop" />
|
||||||
<TrashRepos path={siteRoot + 'sys/trash-libraries'} {...commonProps} />
|
<MobileDevices path="mobile" />
|
||||||
|
<DeviceErrors path="errors" />
|
||||||
|
</Devices>
|
||||||
<SearchRepos path={siteRoot + 'sys/search-libraries'} {...commonProps} />
|
<SearchRepos path={siteRoot + 'sys/search-libraries'} {...commonProps} />
|
||||||
<DirView path={siteRoot + 'sys/libraries/:repoID/*'} {...commonProps} />
|
|
||||||
<WebSettings path={siteRoot + 'sys/web-settings'} {...commonProps} />
|
<WebSettings path={siteRoot + 'sys/web-settings'} {...commonProps} />
|
||||||
<Notifications path={siteRoot + 'sys/notifications'} {...commonProps} />
|
<Notifications path={siteRoot + 'sys/notifications'} {...commonProps} />
|
||||||
<Groups path={siteRoot + 'sys/groups'} {...commonProps} />
|
<Groups path={siteRoot + 'sys/groups'} {...commonProps} />
|
||||||
@@ -240,8 +236,6 @@ class SysAdmin extends React.Component {
|
|||||||
<GroupRepos path={siteRoot + 'sys/groups/:groupID/libraries'} {...commonProps} />
|
<GroupRepos path={siteRoot + 'sys/groups/:groupID/libraries'} {...commonProps} />
|
||||||
<GroupMembers path={siteRoot + 'sys/groups/:groupID/members'} {...commonProps} />
|
<GroupMembers path={siteRoot + 'sys/groups/:groupID/members'} {...commonProps} />
|
||||||
<Departments path={siteRoot + 'sys/departments/'} {...commonProps} />
|
<Departments path={siteRoot + 'sys/departments/'} {...commonProps} />
|
||||||
<ShareLinks path={siteRoot + 'sys/share-links'} {...commonProps} />
|
|
||||||
<UploadLinks path={siteRoot + 'sys/upload-links'} {...commonProps} />
|
|
||||||
<Orgs path={siteRoot + 'sys/organizations'} {...commonProps} />
|
<Orgs path={siteRoot + 'sys/organizations'} {...commonProps} />
|
||||||
<SearchOrgs path={siteRoot + 'sys/search-organizations'} {...commonProps} />
|
<SearchOrgs path={siteRoot + 'sys/search-organizations'} {...commonProps} />
|
||||||
<OrgInfo path={siteRoot + 'sys/organizations/:orgID/info'} {...commonProps} />
|
<OrgInfo path={siteRoot + 'sys/organizations/:orgID/info'} {...commonProps} />
|
||||||
@@ -252,32 +246,33 @@ class SysAdmin extends React.Component {
|
|||||||
<InstitutionInfo path={siteRoot + 'sys/institutions/:institutionID/info'} {...commonProps} />
|
<InstitutionInfo path={siteRoot + 'sys/institutions/:institutionID/info'} {...commonProps} />
|
||||||
<InstitutionUsers path={siteRoot + 'sys/institutions/:institutionID/members'} {...commonProps} />
|
<InstitutionUsers path={siteRoot + 'sys/institutions/:institutionID/members'} {...commonProps} />
|
||||||
<InstitutionAdmins path={siteRoot + 'sys/institutions/:institutionID/admins'} {...commonProps} />
|
<InstitutionAdmins path={siteRoot + 'sys/institutions/:institutionID/admins'} {...commonProps} />
|
||||||
<LoginLogs path={siteRoot + 'sys/logs/login'} {...commonProps} />
|
<Logs path={`${siteRoot}sys/logs/`} {...commonProps}>
|
||||||
<FileAccessLogs path={siteRoot + 'sys/logs/file-access'} {...commonProps} />
|
<LoginLogs path="login" {...commonProps} />
|
||||||
<FIleTransferLogs path={siteRoot + 'sys/logs/repo-transfer'} {...commonProps} />
|
<FileAccessLogs path="file-access" {...commonProps} />
|
||||||
<GroupMemberAuditLogs path={siteRoot + 'sys/logs/group-member-audit'} {...commonProps} />
|
<FIleTransferLogs path="repo-transfer" {...commonProps} />
|
||||||
<FileUpdateLogs path={siteRoot + 'sys/logs/file-update'} {...commonProps} />
|
<GroupMemberAuditLogs path="group-member-audit" {...commonProps} />
|
||||||
<SharePermissionLogs path={siteRoot + 'sys/logs/share-permission'} {...commonProps} />
|
<FileUpdateLogs path="file-update" {...commonProps} />
|
||||||
<AdminOperationLogs path={siteRoot + 'sys/admin-logs/operation'} {...commonProps} />
|
<SharePermissionLogs path="share-permission" {...commonProps} />
|
||||||
<AdminLoginLogs path={siteRoot + 'sys/admin-logs/login'} {...commonProps} />
|
</Logs>
|
||||||
|
<AdminLogs path={`${siteRoot}sys/admin-logs/`} {...commonProps}>
|
||||||
<Users path={siteRoot + 'sys/users'} {...commonProps} />
|
<AdminOperationLogs path="operation" />
|
||||||
<AdminUsers path={siteRoot + 'sys/users/admins'} {...commonProps} />
|
<AdminLoginLogs path="login" />
|
||||||
<LDAPImportedUsers path={siteRoot + 'sys/users/ldap-imported'} {...commonProps} />
|
</AdminLogs>
|
||||||
<LDAPUsers path={siteRoot + 'sys/users/ldap'} {...commonProps} />
|
<UsersLayout path={`${siteRoot}sys/users/*`} {...commonProps} />
|
||||||
<SearchUsers path={siteRoot + 'sys/search-users'} {...commonProps} />
|
<SearchUsers path={siteRoot + 'sys/search-users'} {...commonProps} />
|
||||||
<User path={siteRoot + 'sys/users/:email'} {...commonProps} />
|
<UserLayout path={`${siteRoot}sys/user/:email/`} {...commonProps} >
|
||||||
<UserOwnedRepos path={siteRoot + 'sys/users/:email/owned-libraries'} {...commonProps} />
|
<User path="/" />
|
||||||
<UserSharedRepos path={siteRoot + 'sys/users/:email/shared-libraries'} {...commonProps} />
|
<UserOwnedRepos path="owned-libraries" />
|
||||||
<UserLinks path={siteRoot + 'sys/users/:email/shared-links'} {...commonProps} />
|
<UserSharedRepos path="shared-libraries" />
|
||||||
<UserGroups path={siteRoot + 'sys/users/:email/groups'} {...commonProps} />
|
<UserLinks path="shared-links" />
|
||||||
|
<UserGroups path="groups" />
|
||||||
|
</UserLayout>
|
||||||
<Invitations path={siteRoot + 'sys/invitations'} {...commonProps} />
|
<Invitations path={siteRoot + 'sys/invitations'} {...commonProps} />
|
||||||
<TermsAndConditions path={siteRoot + 'sys/terms-and-conditions/'} {...commonProps} />
|
<TermsAndConditions path={siteRoot + 'sys/terms-and-conditions/'} {...commonProps} />
|
||||||
|
<VirusScan path={`${siteRoot}sys/virus-files/`} {...commonProps}>
|
||||||
<AllVirusFiles path={siteRoot + 'sys/virus-files/all'} {...commonProps} />
|
<AllVirusFiles path="all" />
|
||||||
<UnhandledVirusFiles path={siteRoot + 'sys/virus-files/unhandled'} {...commonProps} />
|
<UnhandledVirusFiles path="unhandled" />
|
||||||
|
</VirusScan>
|
||||||
<FileScanRecords path={siteRoot + 'sys/file-scan-records'} {...commonProps} />
|
<FileScanRecords path={siteRoot + 'sys/file-scan-records'} {...commonProps} />
|
||||||
<WorkWeixinDepartments path={siteRoot + 'sys/work-weixin'} {...commonProps} />
|
<WorkWeixinDepartments path={siteRoot + 'sys/work-weixin'} {...commonProps} />
|
||||||
<DingtalkDepartments path={siteRoot + 'sys/dingtalk'} {...commonProps} />
|
<DingtalkDepartments path={siteRoot + 'sys/dingtalk'} {...commonProps} />
|
||||||
|
195
frontend/src/pages/sys-admin/libraries-and-links.js
Normal file
195
frontend/src/pages/sys-admin/libraries-and-links.js
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { Button } from 'reactstrap';
|
||||||
|
import { useLocation, navigate, Router } from '@gatsbyjs/reach-router';
|
||||||
|
import MainPanelTopbar from './main-panel-topbar';
|
||||||
|
import { gettext, siteRoot } from '../../utils/constants';
|
||||||
|
import ReposNav from './repos/repos-nav';
|
||||||
|
import Search from './search';
|
||||||
|
import toaster from '../../components/toast';
|
||||||
|
import AllRepos from './repos/all-repos';
|
||||||
|
import AllWikis from './repos/all-wikis';
|
||||||
|
import SystemRepo from './repos/system-repo';
|
||||||
|
import TrashRepos from './repos/trash-repos';
|
||||||
|
import ShareLinks from './links/share-links';
|
||||||
|
import UploadLinks from './links/upload-links';
|
||||||
|
import LinksNav from './links/links-nav';
|
||||||
|
|
||||||
|
const LINKS_PATH_NAME_MAP = {
|
||||||
|
'share-links': 'shareLinks',
|
||||||
|
'upload-links': 'uploadLinks'
|
||||||
|
};
|
||||||
|
|
||||||
|
const LibrariesAndLinks = ({ ...commonProps }) => {
|
||||||
|
const [sortBy, setSortBy] = useState('');
|
||||||
|
const [sortOrder, setSortOrder] = useState('asc');
|
||||||
|
const [perPage, setPerPage] = useState(100);
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [isCreateRepoDialogOpen, setIsCreateRepoDialogOpen] = useState(false);
|
||||||
|
const [isCleanTrashDialogOpen, setIsCleanTrashDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
const location = useLocation();
|
||||||
|
const path = location.pathname.split('/').filter(Boolean).pop();
|
||||||
|
let curTab = '';
|
||||||
|
const isLibraries = useMemo(() => path === 'all-libraries' || path === 'trash-libraries' || path === 'all-wikis' || path === 'system-library', [path]);
|
||||||
|
if (isLibraries) {
|
||||||
|
curTab = path;
|
||||||
|
} else {
|
||||||
|
curTab = LINKS_PATH_NAME_MAP[path];
|
||||||
|
}
|
||||||
|
|
||||||
|
const getValueLength = (str) => {
|
||||||
|
let code; let len = 0;
|
||||||
|
for (let i = 0, length = str.length; i < length; i++) {
|
||||||
|
code = str.charCodeAt(i);
|
||||||
|
if (code === 10) { // solve enter problem
|
||||||
|
len += 2;
|
||||||
|
} else if (code < 0x007f) {
|
||||||
|
len += 1;
|
||||||
|
} else if (code >= 0x0080 && code <= 0x07ff) {
|
||||||
|
len += 2;
|
||||||
|
} else if (code >= 0x0800 && code <= 0xffff) {
|
||||||
|
len += 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return len;
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchRepos = (repoNameOrID) => {
|
||||||
|
if (getValueLength(repoNameOrID) < 3) {
|
||||||
|
toaster.notify(gettext('Required at least three letters.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigate(`${siteRoot}sys/search-libraries/?name_or_id=${encodeURIComponent(repoNameOrID)}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSearch = () => {
|
||||||
|
return (
|
||||||
|
<Search
|
||||||
|
placeholder={gettext('Search libraries by name or ID')}
|
||||||
|
submit={searchRepos}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleCreateRepoDialog = useCallback(() => {
|
||||||
|
setIsCreateRepoDialogOpen(!isCreateRepoDialogOpen);
|
||||||
|
}, [isCreateRepoDialogOpen]);
|
||||||
|
|
||||||
|
const toggleCleanTrashDialog = useCallback(() => {
|
||||||
|
setIsCleanTrashDialogOpen(!isCleanTrashDialogOpen);
|
||||||
|
}, [isCleanTrashDialogOpen]);
|
||||||
|
|
||||||
|
const sortItems = (sortBy, sortOrder) => {
|
||||||
|
setSortBy(sortBy);
|
||||||
|
setSortOrder(sortOrder);
|
||||||
|
setCurrentPage(1);
|
||||||
|
const url = new URL(location.href);
|
||||||
|
const searchParams = new URLSearchParams(url.search);
|
||||||
|
searchParams.set('page', 1);
|
||||||
|
searchParams.set('order_by', sortBy);
|
||||||
|
sortOrder && searchParams.set('direction', sortOrder);
|
||||||
|
url.search = searchParams.toString();
|
||||||
|
navigate(url.toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
const onResetPerPage = (perPage) => {
|
||||||
|
setPerPage(perPage);
|
||||||
|
setCurrentPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetAllStates = useCallback(() => {
|
||||||
|
setSortBy('');
|
||||||
|
setSortOrder('asc');
|
||||||
|
setPerPage(100);
|
||||||
|
setCurrentPage(1);
|
||||||
|
setIsCreateRepoDialogOpen(false);
|
||||||
|
setIsCleanTrashDialogOpen(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const urlParams = (new URL(window.location)).searchParams;
|
||||||
|
setSortBy(urlParams.get('order_by') || sortBy);
|
||||||
|
setSortOrder(urlParams.get('direction') || sortOrder);
|
||||||
|
setPerPage(parseInt(urlParams.get('per_page') || perPage));
|
||||||
|
setCurrentPage(parseInt(urlParams.get('page') || currentPage));
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentPathType = isLibraries ? 'library' : 'link';
|
||||||
|
resetAllStates();
|
||||||
|
|
||||||
|
const cleanUrlParams = () => {
|
||||||
|
const url = new URL(location.href);
|
||||||
|
const paramsToKeep = currentPathType === 'library' ? ['page', 'per_page'] : [];
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
|
||||||
|
Array.from(url.searchParams.entries()).forEach(([key, value]) => {
|
||||||
|
if (paramsToKeep.includes(key)) {
|
||||||
|
searchParams.set(key, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (url.search !== searchParams.toString()) {
|
||||||
|
navigate(url.pathname + (searchParams.toString() ? `?${searchParams}` : ''));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
cleanUrlParams();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [isLibraries]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{path === 'all-libraries' && (
|
||||||
|
<MainPanelTopbar search={getSearch()} { ...commonProps }>
|
||||||
|
<Button className="btn btn-secondary operation-item" onClick={toggleCreateRepoDialog}>
|
||||||
|
<i className="sf3-font sf3-font-enlarge text-secondary mr-1"></i>{gettext('New Library')}
|
||||||
|
</Button>
|
||||||
|
</MainPanelTopbar>
|
||||||
|
)}
|
||||||
|
{path === 'trash-libraries' && (
|
||||||
|
<MainPanelTopbar {...commonProps}>
|
||||||
|
<Button className="operation-item" onClick={toggleCleanTrashDialog}>{gettext('Clean')}</Button>
|
||||||
|
</MainPanelTopbar>
|
||||||
|
)}
|
||||||
|
{(path === 'all-wikis' || path === 'system-library') && (
|
||||||
|
<MainPanelTopbar {...commonProps} />
|
||||||
|
)}
|
||||||
|
{!isLibraries && <MainPanelTopbar {...commonProps} />}
|
||||||
|
{isLibraries ? (
|
||||||
|
<ReposNav currentItem={curTab} sortBy={sortBy} sortItems={sortItems} />
|
||||||
|
) : (
|
||||||
|
<LinksNav currentItem={curTab} sortBy={sortBy} sortOrder={sortOrder} sortItems={sortItems} />
|
||||||
|
)}
|
||||||
|
<Router className="d-flex overflow-hidden">
|
||||||
|
<AllRepos
|
||||||
|
path="all-libraries"
|
||||||
|
sortBy={sortBy}
|
||||||
|
perPage={perPage}
|
||||||
|
currentPage={currentPage}
|
||||||
|
onResetPerPage={onResetPerPage}
|
||||||
|
isCreateRepoDialogOpen={isCreateRepoDialogOpen}
|
||||||
|
toggleCreateRepoDialog={toggleCreateRepoDialog}
|
||||||
|
/>
|
||||||
|
<AllWikis
|
||||||
|
path="all-wikis"
|
||||||
|
sortBy={sortBy}
|
||||||
|
perPage={perPage}
|
||||||
|
currentPage={currentPage}
|
||||||
|
onResetPerPage={onResetPerPage}
|
||||||
|
/>
|
||||||
|
<SystemRepo path="system-library" />
|
||||||
|
<TrashRepos
|
||||||
|
path="trash-libraries"
|
||||||
|
isCleanTrashDialogOpen={isCleanTrashDialogOpen}
|
||||||
|
toggleCleanTrashDialog={toggleCleanTrashDialog}
|
||||||
|
/>
|
||||||
|
<ShareLinks path="share-links" onResetPerPage={onResetPerPage} />
|
||||||
|
<UploadLinks path="upload-links" />
|
||||||
|
</Router>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LibrariesAndLinks;
|
@@ -26,6 +26,12 @@ class Nav extends React.Component {
|
|||||||
{ value: 'view_cnt-asc', text: gettext('Ascending by visit count') },
|
{ value: 'view_cnt-asc', text: gettext('Ascending by visit count') },
|
||||||
{ value: 'view_cnt-desc', text: gettext('Descending by visit count') }
|
{ value: 'view_cnt-desc', text: gettext('Descending by visit count') }
|
||||||
];
|
];
|
||||||
|
this.itemRefs = [];
|
||||||
|
this.itemWidths = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.itemWidths = this.itemRefs.map(ref => ref?.offsetWidth || 98);
|
||||||
}
|
}
|
||||||
|
|
||||||
onSelectSortOption = (item) => {
|
onSelectSortOption = (item) => {
|
||||||
@@ -36,12 +42,25 @@ class Nav extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
const { currentItem, sortBy, sortOrder } = this.props;
|
const { currentItem, sortBy, sortOrder } = this.props;
|
||||||
const showSortIcon = currentItem == 'shareLinks';
|
const showSortIcon = currentItem == 'shareLinks';
|
||||||
|
const activeIndex = this.navItems.findIndex(item => item.name === currentItem) || 0;
|
||||||
|
const indicatorWidth = this.itemWidths[activeIndex] || 98;
|
||||||
|
const indicatorOffset = this.itemWidths.slice(0, activeIndex).reduce((a, b) => a + b, 0);
|
||||||
return (
|
return (
|
||||||
<div className="cur-view-path tab-nav-container">
|
<div className="cur-view-path tab-nav-container">
|
||||||
<ul className="nav">
|
<ul
|
||||||
|
className="nav nav-indicator-container position-relative"
|
||||||
|
style={{
|
||||||
|
'--indicator-width': `${indicatorWidth}px`,
|
||||||
|
'--indicator-offset': `${indicatorOffset}px`
|
||||||
|
}}
|
||||||
|
>
|
||||||
{this.navItems.map((item, index) => {
|
{this.navItems.map((item, index) => {
|
||||||
return (
|
return (
|
||||||
<li className="nav-item" key={index}>
|
<li
|
||||||
|
className="nav-item"
|
||||||
|
key={index}
|
||||||
|
ref={el => this.itemRefs[index] = el}
|
||||||
|
>
|
||||||
<Link to={`${siteRoot}sys/${item.urlPart}/`} className={`nav-link${currentItem == item.name ? ' active' : ''}`}>{item.text}</Link>
|
<Link to={`${siteRoot}sys/${item.urlPart}/`} className={`nav-link${currentItem == item.name ? ' active' : ''}`}>{item.text}</Link>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { navigate } from '@gatsbyjs/reach-router';
|
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
@@ -11,8 +10,6 @@ import { Utils } from '../../../utils/utils';
|
|||||||
import EmptyTip from '../../../components/empty-tip';
|
import EmptyTip from '../../../components/empty-tip';
|
||||||
import Loading from '../../../components/loading';
|
import Loading from '../../../components/loading';
|
||||||
import Paginator from '../../../components/paginator';
|
import Paginator from '../../../components/paginator';
|
||||||
import LinksNav from './links-nav';
|
|
||||||
import MainPanelTopbar from '../main-panel-topbar';
|
|
||||||
import UserLink from '../user-link';
|
import UserLink from '../user-link';
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
@@ -47,7 +44,7 @@ class Content extends Component {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const table = (
|
const table = (
|
||||||
<Fragment>
|
<>
|
||||||
<table className="table-hover">
|
<table className="table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -80,7 +77,7 @@ class Content extends Component {
|
|||||||
curPerPage={perPage}
|
curPerPage={perPage}
|
||||||
resetPerPage={this.props.resetPerPage}
|
resetPerPage={this.props.resetPerPage}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</>
|
||||||
);
|
);
|
||||||
return items.length ? table : emptyTip;
|
return items.length ? table : emptyTip;
|
||||||
}
|
}
|
||||||
@@ -184,34 +181,30 @@ class ShareLinks extends Component {
|
|||||||
errorMsg: '',
|
errorMsg: '',
|
||||||
shareLinkList: [],
|
shareLinkList: [],
|
||||||
perPage: 100,
|
perPage: 100,
|
||||||
currentPage: 1,
|
|
||||||
hasNextPage: false,
|
hasNextPage: false,
|
||||||
sortBy: '',
|
|
||||||
sortOrder: 'asc'
|
|
||||||
};
|
};
|
||||||
this.initPage = 1;
|
this.initPage = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
let urlParams = (new URL(window.location)).searchParams;
|
this.getShareLinksByPage(this.props.currentPage);
|
||||||
const { currentPage, perPage, sortBy, sortOrder } = this.state;
|
}
|
||||||
this.setState({
|
|
||||||
perPage: parseInt(urlParams.get('per_page') || perPage),
|
componentDidUpdate(prevProps, prevState) {
|
||||||
currentPage: parseInt(urlParams.get('page') || currentPage),
|
const { currentPage, sortBy, sortOrder } = this.props;
|
||||||
sortBy: urlParams.get('order_by') || sortBy,
|
if (currentPage !== prevProps.currentPage ||
|
||||||
sortOrder: urlParams.get('direction') || sortOrder
|
sortBy !== prevProps.sortBy ||
|
||||||
}, () => {
|
sortOrder !== prevProps.sortOrder) {
|
||||||
this.getShareLinksByPage(this.state.currentPage);
|
this.getShareLinksByPage(currentPage);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getShareLinksByPage = (page) => {
|
getShareLinksByPage = (page) => {
|
||||||
const { perPage, sortBy, sortOrder } = this.state;
|
const { perPage, sortBy, sortOrder } = this.props;
|
||||||
systemAdminAPI.sysAdminListShareLinks(page, perPage, sortBy, sortOrder).then((res) => {
|
systemAdminAPI.sysAdminListShareLinks(page, perPage, sortBy, sortOrder).then((res) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
shareLinkList: res.data.share_link_list,
|
shareLinkList: res.data.share_link_list,
|
||||||
loading: false,
|
loading: false,
|
||||||
currentPage: page,
|
|
||||||
hasNextPage: Utils.hasNextPage(page, perPage, res.data.count),
|
hasNextPage: Utils.hasNextPage(page, perPage, res.data.count),
|
||||||
});
|
});
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
@@ -222,24 +215,6 @@ class ShareLinks extends Component {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
sortItems = (sortBy, sortOrder) => {
|
|
||||||
this.setState({
|
|
||||||
currentPage: 1,
|
|
||||||
sortBy: sortBy,
|
|
||||||
sortOrder: sortOrder
|
|
||||||
}, () => {
|
|
||||||
let url = new URL(location.href);
|
|
||||||
let searchParams = new URLSearchParams(url.search);
|
|
||||||
const { currentPage, sortBy, sortOrder } = this.state;
|
|
||||||
searchParams.set('page', currentPage);
|
|
||||||
searchParams.set('order_by', sortBy);
|
|
||||||
searchParams.set('direction', sortOrder);
|
|
||||||
url.search = searchParams.toString();
|
|
||||||
navigate(url.toString());
|
|
||||||
this.getShareLinksByPage(currentPage);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
deleteShareLink = (linkToken) => {
|
deleteShareLink = (linkToken) => {
|
||||||
systemAdminAPI.sysAdminDeleteShareLink(linkToken).then(res => {
|
systemAdminAPI.sysAdminDeleteShareLink(linkToken).then(res => {
|
||||||
let newShareLinkList = this.state.shareLinkList.filter(item =>
|
let newShareLinkList = this.state.shareLinkList.filter(item =>
|
||||||
@@ -253,24 +228,16 @@ class ShareLinks extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
resetPerPage = (newPerPage) => {
|
resetPerPage = (newPerPage) => {
|
||||||
this.setState({
|
this.props.onResetPerPage(newPerPage);
|
||||||
perPage: newPerPage,
|
this.getShareLinksByPage(this.initPage);
|
||||||
}, () => this.getShareLinksByPage(this.initPage));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let { shareLinkList, currentPage, perPage, hasNextPage } = this.state;
|
let { shareLinkList, currentPage, perPage, hasNextPage } = this.state;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<>
|
||||||
<MainPanelTopbar {...this.props} />
|
|
||||||
<div className="main-panel-center flex-row">
|
<div className="main-panel-center flex-row">
|
||||||
<div className="cur-view-container">
|
<div className="cur-view-container">
|
||||||
<LinksNav
|
|
||||||
currentItem="shareLinks"
|
|
||||||
sortBy={this.state.sortBy}
|
|
||||||
sortOrder={this.state.sortOrder}
|
|
||||||
sortItems={this.sortItems}
|
|
||||||
/>
|
|
||||||
<div className="cur-view-content">
|
<div className="cur-view-content">
|
||||||
<Content
|
<Content
|
||||||
loading={this.state.loading}
|
loading={this.state.loading}
|
||||||
@@ -286,7 +253,7 @@ class ShareLinks extends Component {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -10,8 +10,6 @@ import { Utils } from '../../../utils/utils';
|
|||||||
import EmptyTip from '../../../components/empty-tip';
|
import EmptyTip from '../../../components/empty-tip';
|
||||||
import Loading from '../../../components/loading';
|
import Loading from '../../../components/loading';
|
||||||
import Paginator from '../../../components/paginator';
|
import Paginator from '../../../components/paginator';
|
||||||
import LinksNav from './links-nav';
|
|
||||||
import MainPanelTopbar from '../main-panel-topbar';
|
|
||||||
import UserLink from '../user-link';
|
import UserLink from '../user-link';
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
@@ -238,11 +236,9 @@ class UploadLinks extends Component {
|
|||||||
render() {
|
render() {
|
||||||
let { uploadLinkList, currentPage, perPage, hasNextPage } = this.state;
|
let { uploadLinkList, currentPage, perPage, hasNextPage } = this.state;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<>
|
||||||
<MainPanelTopbar {...this.props} />
|
|
||||||
<div className="main-panel-center flex-row">
|
<div className="main-panel-center flex-row">
|
||||||
<div className="cur-view-container">
|
<div className="cur-view-container">
|
||||||
<LinksNav currentItem="uploadLinks" />
|
|
||||||
<div className="cur-view-content">
|
<div className="cur-view-content">
|
||||||
<Content
|
<Content
|
||||||
loading={this.state.loading}
|
loading={this.state.loading}
|
||||||
@@ -258,7 +254,7 @@ class UploadLinks extends Component {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Button } from 'reactstrap';
|
|
||||||
import { navigate } from '@gatsbyjs/reach-router';
|
import { navigate } from '@gatsbyjs/reach-router';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
@@ -10,10 +9,6 @@ import { Utils } from '../../../utils/utils';
|
|||||||
import EmptyTip from '../../../components/empty-tip';
|
import EmptyTip from '../../../components/empty-tip';
|
||||||
import Loading from '../../../components/loading';
|
import Loading from '../../../components/loading';
|
||||||
import Paginator from '../../../components/paginator';
|
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 MainPanelTopbar from '../main-panel-topbar';
|
|
||||||
import UserLink from '../user-link';
|
import UserLink from '../user-link';
|
||||||
import LogUserSelector from '../../dashboard/log-user-selector';
|
import LogUserSelector from '../../dashboard/log-user-selector';
|
||||||
import LogRepoSelector from '../../dashboard/log-repo-selector';
|
import LogRepoSelector from '../../dashboard/log-repo-selector';
|
||||||
@@ -194,10 +189,6 @@ class FileAccessLogs extends Component {
|
|||||||
this.initPage = 1;
|
this.initPage = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleExportExcelDialog = () => {
|
|
||||||
this.setState({ isExportExcelDialogOpen: !this.state.isExportExcelDialogOpen });
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
let urlParams = (new URL(window.location)).searchParams;
|
let urlParams = (new URL(window.location)).searchParams;
|
||||||
const { currentPage, perPage } = this.state;
|
const { currentPage, perPage } = this.state;
|
||||||
@@ -321,7 +312,6 @@ class FileAccessLogs extends Component {
|
|||||||
const {
|
const {
|
||||||
logList,
|
logList,
|
||||||
currentPage, perPage, hasNextPage,
|
currentPage, perPage, hasNextPage,
|
||||||
isExportExcelDialogOpen,
|
|
||||||
availableUsers,
|
availableUsers,
|
||||||
selectedUsers,
|
selectedUsers,
|
||||||
availableRepos,
|
availableRepos,
|
||||||
@@ -329,15 +319,9 @@ class FileAccessLogs extends Component {
|
|||||||
openSelector
|
openSelector
|
||||||
} = this.state;
|
} = this.state;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
|
||||||
<MainPanelTopbar {...this.props}>
|
|
||||||
<Button className="btn btn-secondary operation-item" onClick={this.toggleExportExcelDialog}>{gettext('Export Excel')}</Button>
|
|
||||||
</MainPanelTopbar>
|
|
||||||
<div className="main-panel-center flex-row">
|
<div className="main-panel-center flex-row">
|
||||||
<div className="cur-view-container">
|
<div className="cur-view-container">
|
||||||
<LogsNav currentItem="fileAccessLogs" />
|
|
||||||
<div className="cur-view-content">
|
<div className="cur-view-content">
|
||||||
<Fragment>
|
|
||||||
<div className="d-flex align-items-center mb-2">
|
<div className="d-flex align-items-center mb-2">
|
||||||
<LogUserSelector
|
<LogUserSelector
|
||||||
componentName={gettext('Users')}
|
componentName={gettext('Users')}
|
||||||
@@ -368,19 +352,9 @@ class FileAccessLogs extends Component {
|
|||||||
getLogsByPage={this.getLogsByPage}
|
getLogsByPage={this.getLogsByPage}
|
||||||
resetPerPage={this.resetPerPage}
|
resetPerPage={this.resetPerPage}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isExportExcelDialogOpen &&
|
|
||||||
<ModalPortal>
|
|
||||||
<LogsExportExcelDialog
|
|
||||||
logType={'fileAccess'}
|
|
||||||
toggle={this.toggleExportExcelDialog}
|
|
||||||
/>
|
|
||||||
</ModalPortal>
|
|
||||||
}
|
|
||||||
</Fragment>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -9,9 +9,7 @@ import { Utils } from '../../../utils/utils';
|
|||||||
import EmptyTip from '../../../components/empty-tip';
|
import EmptyTip from '../../../components/empty-tip';
|
||||||
import Loading from '../../../components/loading';
|
import Loading from '../../../components/loading';
|
||||||
import Paginator from '../../../components/paginator';
|
import Paginator from '../../../components/paginator';
|
||||||
import MainPanelTopbar from '../main-panel-topbar';
|
|
||||||
import UserLink from '../user-link';
|
import UserLink from '../user-link';
|
||||||
import LogsNav from './logs-nav';
|
|
||||||
import LogUserSelector from '../../dashboard/log-user-selector';
|
import LogUserSelector from '../../dashboard/log-user-selector';
|
||||||
import LogRepoSelector from '../../dashboard/log-repo-selector';
|
import LogRepoSelector from '../../dashboard/log-repo-selector';
|
||||||
|
|
||||||
@@ -422,13 +420,9 @@ class FIleTransferLogs extends Component {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
|
||||||
<MainPanelTopbar {...this.props} />
|
|
||||||
<div className="main-panel-center flex-row">
|
<div className="main-panel-center flex-row">
|
||||||
<div className="cur-view-container">
|
<div className="cur-view-container">
|
||||||
<LogsNav currentItem="fileTransfer" />
|
|
||||||
<div className="cur-view-content">
|
<div className="cur-view-content">
|
||||||
<Fragment>
|
|
||||||
<div className="d-flex align-items-center mb-2">
|
<div className="d-flex align-items-center mb-2">
|
||||||
<LogUserSelector
|
<LogUserSelector
|
||||||
componentName={gettext('Transfer From')}
|
componentName={gettext('Transfer From')}
|
||||||
@@ -479,11 +473,9 @@ class FIleTransferLogs extends Component {
|
|||||||
getLogsByPage={this.getLogsByPage}
|
getLogsByPage={this.getLogsByPage}
|
||||||
resetPerPage={this.resetPerPage}
|
resetPerPage={this.resetPerPage}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,20 +1,16 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
import { Button } from 'reactstrap';
|
|
||||||
import { systemAdminAPI } from '../../../utils/system-admin-api';
|
import { systemAdminAPI } from '../../../utils/system-admin-api';
|
||||||
import { gettext } from '../../../utils/constants';
|
import { gettext } from '../../../utils/constants';
|
||||||
import { Utils } from '../../../utils/utils';
|
import { Utils } from '../../../utils/utils';
|
||||||
import EmptyTip from '../../../components/empty-tip';
|
import EmptyTip from '../../../components/empty-tip';
|
||||||
import Loading from '../../../components/loading';
|
import Loading from '../../../components/loading';
|
||||||
import Paginator from '../../../components/paginator';
|
import Paginator from '../../../components/paginator';
|
||||||
import LogsNav from './logs-nav';
|
|
||||||
import MainPanelTopbar from '../main-panel-topbar';
|
|
||||||
import UserLink from '../user-link';
|
import UserLink from '../user-link';
|
||||||
import ModalPortal from '../../../components/modal-portal';
|
import ModalPortal from '../../../components/modal-portal';
|
||||||
import CommitDetails from '../../../components/dialog/commit-details';
|
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 LogUserSelector from '../../dashboard/log-user-selector';
|
||||||
import LogRepoSelector from '../../dashboard/log-repo-selector';
|
import LogRepoSelector from '../../dashboard/log-repo-selector';
|
||||||
|
|
||||||
@@ -42,7 +38,7 @@ class Content extends Component {
|
|||||||
</EmptyTip>
|
</EmptyTip>
|
||||||
);
|
);
|
||||||
const table = (
|
const table = (
|
||||||
<Fragment>
|
<>
|
||||||
<table className="table-hover">
|
<table className="table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -71,7 +67,7 @@ class Content extends Component {
|
|||||||
curPerPage={perPage}
|
curPerPage={perPage}
|
||||||
resetPerPage={this.props.resetPerPage}
|
resetPerPage={this.props.resetPerPage}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</>
|
||||||
);
|
);
|
||||||
return items.length ? table : emptyTip;
|
return items.length ? table : emptyTip;
|
||||||
}
|
}
|
||||||
@@ -130,7 +126,7 @@ class Item extends Component {
|
|||||||
render() {
|
render() {
|
||||||
let { item } = this.props;
|
let { item } = this.props;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<>
|
||||||
<tr onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut}>
|
<tr onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut}>
|
||||||
<td><UserLink email={item.email} name={item.name} /></td>
|
<td><UserLink email={item.email} name={item.name} /></td>
|
||||||
<td>{dayjs(item.time).fromNow()}</td>
|
<td>{dayjs(item.time).fromNow()}</td>
|
||||||
@@ -152,7 +148,7 @@ class Item extends Component {
|
|||||||
/>
|
/>
|
||||||
</ModalPortal>
|
</ModalPortal>
|
||||||
}
|
}
|
||||||
</Fragment>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -297,17 +293,11 @@ class FileUpdateLogs extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let { logList, currentPage, perPage, hasNextPage, isExportExcelDialogOpen, availableUsers, selectedUsers, availableRepos, selectedRepos } = this.state;
|
let { logList, currentPage, perPage, hasNextPage, availableUsers, selectedUsers, availableRepos, selectedRepos } = this.state;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
|
||||||
<MainPanelTopbar {...this.props}>
|
|
||||||
<Button className="btn btn-secondary operation-item" onClick={this.toggleExportExcelDialog}>{gettext('Export Excel')}</Button>
|
|
||||||
</MainPanelTopbar>
|
|
||||||
<div className="main-panel-center flex-row">
|
<div className="main-panel-center flex-row">
|
||||||
<div className="cur-view-container">
|
<div className="cur-view-container">
|
||||||
<LogsNav currentItem="fileUpdateLogs" />
|
|
||||||
<div className="cur-view-content">
|
<div className="cur-view-content">
|
||||||
<Fragment>
|
|
||||||
<div className="d-flex align-items-center mb-2">
|
<div className="d-flex align-items-center mb-2">
|
||||||
<LogUserSelector
|
<LogUserSelector
|
||||||
componentName={gettext('Users')}
|
componentName={gettext('Users')}
|
||||||
@@ -338,19 +328,9 @@ class FileUpdateLogs extends Component {
|
|||||||
getLogsByPage={this.getLogsByPage}
|
getLogsByPage={this.getLogsByPage}
|
||||||
resetPerPage={this.resetPerPage}
|
resetPerPage={this.resetPerPage}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isExportExcelDialogOpen &&
|
|
||||||
<ModalPortal>
|
|
||||||
<LogsExportExcelDialog
|
|
||||||
logType={'fileUpdate'}
|
|
||||||
toggle={this.toggleExportExcelDialog}
|
|
||||||
/>
|
|
||||||
</ModalPortal>
|
|
||||||
}
|
|
||||||
</Fragment>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -9,9 +9,7 @@ import { systemAdminAPI } from '../../../utils/system-admin-api';
|
|||||||
import EmptyTip from '../../../components/empty-tip';
|
import EmptyTip from '../../../components/empty-tip';
|
||||||
import Loading from '../../../components/loading';
|
import Loading from '../../../components/loading';
|
||||||
import Paginator from '../../../components/paginator';
|
import Paginator from '../../../components/paginator';
|
||||||
import MainPanelTopbar from '../main-panel-topbar';
|
|
||||||
import UserLink from '../user-link';
|
import UserLink from '../user-link';
|
||||||
import LogsNav from './logs-nav';
|
|
||||||
import LogUserSelector from '../../dashboard/log-user-selector';
|
import LogUserSelector from '../../dashboard/log-user-selector';
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
@@ -309,13 +307,9 @@ class GroupMemberAuditLogs extends Component {
|
|||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
|
||||||
<MainPanelTopbar {...this.props} />
|
|
||||||
<div className="main-panel-center flex-row">
|
<div className="main-panel-center flex-row">
|
||||||
<div className="cur-view-container">
|
<div className="cur-view-container">
|
||||||
<LogsNav currentItem="groupMember" />
|
|
||||||
<div className="cur-view-content">
|
<div className="cur-view-content">
|
||||||
<Fragment>
|
|
||||||
<div className="d-flex align-items-center mb-2">
|
<div className="d-flex align-items-center mb-2">
|
||||||
<LogUserSelector
|
<LogUserSelector
|
||||||
componentName={gettext('Member')}
|
componentName={gettext('Member')}
|
||||||
@@ -355,11 +349,9 @@ class GroupMemberAuditLogs extends Component {
|
|||||||
getLogsByPage={this.getLogsByPage}
|
getLogsByPage={this.getLogsByPage}
|
||||||
resetPerPage={this.resetPerPage}
|
resetPerPage={this.resetPerPage}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
53
frontend/src/pages/sys-admin/logs-page/index.js
Normal file
53
frontend/src/pages/sys-admin/logs-page/index.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import React, { Fragment, useState } from 'react';
|
||||||
|
import MainPanelTopbar from '../main-panel-topbar';
|
||||||
|
import { Button } from 'reactstrap';
|
||||||
|
import { gettext } from '../../../utils/constants';
|
||||||
|
import LogsNav from '../logs-page/logs-nav';
|
||||||
|
import { useLocation } from '@gatsbyjs/reach-router';
|
||||||
|
import ModalPortal from '../../../components/modal-portal';
|
||||||
|
import LogsExportExcelDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-logs-export-excel-dialog';
|
||||||
|
|
||||||
|
const LOG_PATH_NAME_MAP = {
|
||||||
|
'login': 'loginLogs',
|
||||||
|
'file-access': 'fileAccessLogs',
|
||||||
|
'file-update': 'fileUpdateLogs',
|
||||||
|
'share-permission': 'sharePermissionLogs',
|
||||||
|
'repo-transfer': 'fileTransfer',
|
||||||
|
'group-member-audit': 'groupMember',
|
||||||
|
};
|
||||||
|
|
||||||
|
const Logs = ({ children, ...commonProps }) => {
|
||||||
|
const [isExportExcelDialogOpen, setIsExportExcelDialogOpen] = useState(false);
|
||||||
|
const location = useLocation();
|
||||||
|
const path = location.pathname.split('/').filter(Boolean).pop();
|
||||||
|
const curTab = LOG_PATH_NAME_MAP[path];
|
||||||
|
|
||||||
|
const toggleDialog = () => {
|
||||||
|
setIsExportExcelDialogOpen(!isExportExcelDialogOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
const showDefaultTopbar = curTab === 'fileTransfer' || curTab === 'groupMember';
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{showDefaultTopbar ? (
|
||||||
|
<MainPanelTopbar {...commonProps} />
|
||||||
|
) : (
|
||||||
|
<MainPanelTopbar {...commonProps}>
|
||||||
|
<Button className="btn btn-secondary operation-item" onClick={toggleDialog}>{gettext('Export Excel')}</Button>
|
||||||
|
</MainPanelTopbar>
|
||||||
|
)}
|
||||||
|
<LogsNav currentItem={curTab} {...commonProps} />
|
||||||
|
<div className="h-100 d-flex overflow-auto">{children}</div>
|
||||||
|
{isExportExcelDialogOpen &&
|
||||||
|
<ModalPortal>
|
||||||
|
<LogsExportExcelDialog
|
||||||
|
logType={curTab}
|
||||||
|
toggle={toggleDialog}
|
||||||
|
/>
|
||||||
|
</ModalPortal>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Logs;
|
@@ -5,15 +5,10 @@ import relativeTime from 'dayjs/plugin/relativeTime';
|
|||||||
import { systemAdminAPI } from '../../../utils/system-admin-api';
|
import { systemAdminAPI } from '../../../utils/system-admin-api';
|
||||||
import { gettext } from '../../../utils/constants';
|
import { gettext } from '../../../utils/constants';
|
||||||
import { Utils } from '../../../utils/utils';
|
import { Utils } from '../../../utils/utils';
|
||||||
import { Button } from 'reactstrap';
|
|
||||||
import EmptyTip from '../../../components/empty-tip';
|
import EmptyTip from '../../../components/empty-tip';
|
||||||
import Loading from '../../../components/loading';
|
import Loading from '../../../components/loading';
|
||||||
import Paginator from '../../../components/paginator';
|
import Paginator from '../../../components/paginator';
|
||||||
import LogsNav from './logs-nav';
|
|
||||||
import MainPanelTopbar from '../main-panel-topbar';
|
|
||||||
import UserLink from '../user-link';
|
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';
|
import LogUserSelector from '../../dashboard/log-user-selector';
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
@@ -146,10 +141,6 @@ class LoginLogs extends Component {
|
|||||||
this.initPage = 1;
|
this.initPage = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleExportExcelDialog = () => {
|
|
||||||
this.setState({ isExportExcelDialogOpen: !this.state.isExportExcelDialogOpen });
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
let urlParams = (new URL(window.location)).searchParams;
|
let urlParams = (new URL(window.location)).searchParams;
|
||||||
const { currentPage, perPage } = this.state;
|
const { currentPage, perPage } = this.state;
|
||||||
@@ -223,17 +214,11 @@ class LoginLogs extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let { logList, currentPage, perPage, hasNextPage, isExportExcelDialogOpen, availableUsers, selectedUsers } = this.state;
|
let { logList, currentPage, perPage, hasNextPage, availableUsers, selectedUsers } = this.state;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
|
||||||
<MainPanelTopbar {...this.props}>
|
|
||||||
<Button className="btn btn-secondary operation-item" onClick={this.toggleExportExcelDialog}>{gettext('Export Excel')}</Button>
|
|
||||||
</MainPanelTopbar>
|
|
||||||
<div className="main-panel-center flex-row">
|
<div className="main-panel-center flex-row">
|
||||||
<div className="cur-view-container">
|
<div className="cur-view-container">
|
||||||
<LogsNav currentItem="loginLogs" />
|
|
||||||
<div className="cur-view-content">
|
<div className="cur-view-content">
|
||||||
<Fragment>
|
|
||||||
<LogUserSelector
|
<LogUserSelector
|
||||||
componentName={gettext('Users')}
|
componentName={gettext('Users')}
|
||||||
items={availableUsers}
|
items={availableUsers}
|
||||||
@@ -253,19 +238,9 @@ class LoginLogs extends Component {
|
|||||||
getLogsByPage={this.getLogsByPage}
|
getLogsByPage={this.getLogsByPage}
|
||||||
resetPerPage={this.resetPerPage}
|
resetPerPage={this.resetPerPage}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isExportExcelDialogOpen &&
|
|
||||||
<ModalPortal>
|
|
||||||
<LogsExportExcelDialog
|
|
||||||
logType={'login'}
|
|
||||||
toggle={this.toggleExportExcelDialog}
|
|
||||||
/>
|
|
||||||
</ModalPortal>
|
|
||||||
}
|
|
||||||
</Fragment>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -19,16 +19,35 @@ class Nav extends React.Component {
|
|||||||
{ name: 'fileTransfer', urlPart: 'logs/repo-transfer', text: gettext('Repo Transfer') },
|
{ name: 'fileTransfer', urlPart: 'logs/repo-transfer', text: gettext('Repo Transfer') },
|
||||||
{ name: 'groupMember', urlPart: 'logs/group-member-audit', text: gettext('Group Member') },
|
{ name: 'groupMember', urlPart: 'logs/group-member-audit', text: gettext('Group Member') },
|
||||||
];
|
];
|
||||||
|
this.itemRefs = [];
|
||||||
|
this.itemWidths = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.itemWidths = this.itemRefs.map(ref => ref?.offsetWidth) || 59;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { currentItem } = this.props;
|
const { currentItem } = this.props;
|
||||||
|
const activeIndex = this.navItems.findIndex(item => item.name === currentItem) || 0;
|
||||||
|
const indicatorWidth = this.itemWidths[activeIndex] || 59;
|
||||||
|
const leftOffset = this.itemWidths.slice(0, activeIndex).reduce((prev, cur) => prev + cur, 0);
|
||||||
return (
|
return (
|
||||||
<div className="cur-view-path tab-nav-container">
|
<div className="cur-view-path tab-nav-container">
|
||||||
<ul className="nav">
|
<ul
|
||||||
|
className="nav nav-indicator-container position-relative"
|
||||||
|
style={{
|
||||||
|
'--indicator-width': `${indicatorWidth}px`,
|
||||||
|
'--indicator-offset': `${leftOffset}px`
|
||||||
|
}}
|
||||||
|
>
|
||||||
{this.navItems.map((item, index) => {
|
{this.navItems.map((item, index) => {
|
||||||
return (
|
return (
|
||||||
<li className="nav-item" key={index}>
|
<li
|
||||||
|
className="nav-item"
|
||||||
|
key={index}
|
||||||
|
ref={el => this.itemRefs[index] = el}
|
||||||
|
>
|
||||||
<Link to={`${siteRoot}sys/${item.urlPart}/`} className={`nav-link${currentItem == item.name ? ' active' : ''}`}>{item.text}</Link>
|
<Link to={`${siteRoot}sys/${item.urlPart}/`} className={`nav-link${currentItem == item.name ? ' active' : ''}`}>{item.text}</Link>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
@@ -1,20 +1,15 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Link } from '@gatsbyjs/reach-router';
|
import { Link } from '@gatsbyjs/reach-router';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
import { Button } from 'reactstrap';
|
|
||||||
import { systemAdminAPI } from '../../../utils/system-admin-api';
|
import { systemAdminAPI } from '../../../utils/system-admin-api';
|
||||||
import { gettext, siteRoot } from '../../../utils/constants';
|
import { gettext, siteRoot } from '../../../utils/constants';
|
||||||
import { Utils } from '../../../utils/utils';
|
import { Utils } from '../../../utils/utils';
|
||||||
import LogsExportExcelDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-logs-export-excel-dialog';
|
|
||||||
import ModalPortal from '../../../components/modal-portal';
|
|
||||||
import EmptyTip from '../../../components/empty-tip';
|
import EmptyTip from '../../../components/empty-tip';
|
||||||
import Loading from '../../../components/loading';
|
import Loading from '../../../components/loading';
|
||||||
import Paginator from '../../../components/paginator';
|
import Paginator from '../../../components/paginator';
|
||||||
import MainPanelTopbar from '../main-panel-topbar';
|
|
||||||
import UserLink from '../user-link';
|
import UserLink from '../user-link';
|
||||||
import LogsNav from './logs-nav';
|
|
||||||
import LogUserSelector from '../../dashboard/log-user-selector';
|
import LogUserSelector from '../../dashboard/log-user-selector';
|
||||||
import LogRepoSelector from '../../dashboard/log-repo-selector';
|
import LogRepoSelector from '../../dashboard/log-repo-selector';
|
||||||
|
|
||||||
@@ -42,7 +37,7 @@ class Content extends Component {
|
|||||||
</EmptyTip>
|
</EmptyTip>
|
||||||
);
|
);
|
||||||
const table = (
|
const table = (
|
||||||
<Fragment>
|
<>
|
||||||
<table className="table-hover">
|
<table className="table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -74,7 +69,7 @@ class Content extends Component {
|
|||||||
curPerPage={perPage}
|
curPerPage={perPage}
|
||||||
resetPerPage={this.props.resetPerPage}
|
resetPerPage={this.props.resetPerPage}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</>
|
||||||
);
|
);
|
||||||
return items.length ? table : emptyTip;
|
return items.length ? table : emptyTip;
|
||||||
}
|
}
|
||||||
@@ -333,20 +328,14 @@ class SharePermissionLogs extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
let {
|
let {
|
||||||
logList, currentPage, perPage, hasNextPage, isExportExcelDialogOpen,
|
logList, currentPage, perPage, hasNextPage,
|
||||||
availableUsers, selectedFromUsers, selectedToUsers,
|
availableUsers, selectedFromUsers, selectedToUsers,
|
||||||
selectedToGroups, availableRepos, selectedRepos, openSelector
|
selectedToGroups, availableRepos, selectedRepos, openSelector
|
||||||
} = this.state;
|
} = this.state;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
|
||||||
<MainPanelTopbar {...this.props}>
|
|
||||||
<Button className="btn btn-secondary operation-item" onClick={this.toggleExportExcelDialog}>{gettext('Export Excel')}</Button>
|
|
||||||
</MainPanelTopbar>
|
|
||||||
<div className="main-panel-center flex-row">
|
<div className="main-panel-center flex-row">
|
||||||
<div className="cur-view-container">
|
<div className="cur-view-container">
|
||||||
<LogsNav currentItem="sharePermissionLogs" />
|
|
||||||
<div className="cur-view-content">
|
<div className="cur-view-content">
|
||||||
<Fragment>
|
|
||||||
<div className="d-flex align-items-center mb-2">
|
<div className="d-flex align-items-center mb-2">
|
||||||
<LogUserSelector
|
<LogUserSelector
|
||||||
componentName={gettext('Share From')}
|
componentName={gettext('Share From')}
|
||||||
@@ -387,19 +376,9 @@ class SharePermissionLogs extends Component {
|
|||||||
getLogsByPage={this.getLogsByPage}
|
getLogsByPage={this.getLogsByPage}
|
||||||
resetPerPage={this.resetPerPage}
|
resetPerPage={this.resetPerPage}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isExportExcelDialogOpen &&
|
|
||||||
<ModalPortal>
|
|
||||||
<LogsExportExcelDialog
|
|
||||||
logType={'sharePermission'}
|
|
||||||
toggle={this.toggleExportExcelDialog}
|
|
||||||
/>
|
|
||||||
</ModalPortal>
|
|
||||||
}
|
|
||||||
</Fragment>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,14 +1,8 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { navigate } from '@gatsbyjs/reach-router';
|
|
||||||
import { Button } from 'reactstrap';
|
|
||||||
import { Utils } from '../../../utils/utils';
|
import { Utils } from '../../../utils/utils';
|
||||||
import { systemAdminAPI } from '../../../utils/system-admin-api';
|
import { systemAdminAPI } from '../../../utils/system-admin-api';
|
||||||
import { gettext, siteRoot } from '../../../utils/constants';
|
|
||||||
import toaster from '../../../components/toast';
|
import toaster from '../../../components/toast';
|
||||||
import SysAdminCreateRepoDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-create-repo-dialog';
|
import SysAdminCreateRepoDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-create-repo-dialog';
|
||||||
import MainPanelTopbar from '../main-panel-topbar';
|
|
||||||
import Search from '../search';
|
|
||||||
import ReposNav from './repos-nav';
|
|
||||||
import Content from './repos';
|
import Content from './repos';
|
||||||
|
|
||||||
class AllRepos extends Component {
|
class AllRepos extends Component {
|
||||||
@@ -21,29 +15,25 @@ class AllRepos extends Component {
|
|||||||
repos: [],
|
repos: [],
|
||||||
pageInfo: {},
|
pageInfo: {},
|
||||||
perPage: 100,
|
perPage: 100,
|
||||||
sortBy: '',
|
|
||||||
isCreateRepoDialogOpen: false
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
let urlParams = (new URL(window.location)).searchParams;
|
this.getReposByPage(this.props.currentPage);
|
||||||
const { currentPage = 1, perPage, sortBy } = this.state;
|
|
||||||
this.setState({
|
|
||||||
sortBy: urlParams.get('order_by') || sortBy,
|
|
||||||
perPage: parseInt(urlParams.get('per_page') || perPage),
|
|
||||||
currentPage: parseInt(urlParams.get('page') || currentPage)
|
|
||||||
}, () => {
|
|
||||||
this.getReposByPage(this.state.currentPage);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleCreateRepoDialog = () => {
|
componentDidUpdate(prevProps, prevState) {
|
||||||
this.setState({ isCreateRepoDialogOpen: !this.state.isCreateRepoDialogOpen });
|
if (prevProps.currentPage !== this.props.currentPage ||
|
||||||
};
|
prevProps.sortBy !== this.props.sortBy
|
||||||
|
) {
|
||||||
|
this.getReposByPage(this.props.currentPage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getReposByPage = (page) => {
|
getReposByPage = (page) => {
|
||||||
const { perPage, sortBy } = this.state;
|
const { perPage, sortBy } = this.props;
|
||||||
|
if (!this.isValidSortBy(sortBy)) return;
|
||||||
|
|
||||||
systemAdminAPI.sysAdminListAllRepos(page, perPage, sortBy).then((res) => {
|
systemAdminAPI.sysAdminListAllRepos(page, perPage, sortBy).then((res) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: false,
|
loading: false,
|
||||||
@@ -58,28 +48,13 @@ class AllRepos extends Component {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
sortItems = (sortBy) => {
|
isValidSortBy = (sortBy) => {
|
||||||
this.setState({
|
return ['file_count-desc', 'size-desc', ''].includes(sortBy);
|
||||||
currentPage: 1,
|
|
||||||
sortBy: sortBy
|
|
||||||
}, () => {
|
|
||||||
let url = new URL(location.href);
|
|
||||||
let searchParams = new URLSearchParams(url.search);
|
|
||||||
const { currentPage, sortBy } = this.state;
|
|
||||||
searchParams.set('page', currentPage);
|
|
||||||
searchParams.set('order_by', sortBy);
|
|
||||||
url.search = searchParams.toString();
|
|
||||||
navigate(url.toString());
|
|
||||||
this.getReposByPage(currentPage);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
resetPerPage = (perPage) => {
|
resetPerPage = (perPage) => {
|
||||||
this.setState({
|
this.props.onResetPerPage(perPage);
|
||||||
perPage: perPage
|
|
||||||
}, () => {
|
|
||||||
this.getReposByPage(1);
|
this.getReposByPage(1);
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
createRepo = (repoName, Owner) => {
|
createRepo = (repoName, Owner) => {
|
||||||
@@ -112,61 +87,19 @@ class AllRepos extends Component {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
getSearch = () => {
|
|
||||||
return <Search
|
|
||||||
placeholder={gettext('Search libraries by name or ID')}
|
|
||||||
submit={this.searchRepos}
|
|
||||||
/>;
|
|
||||||
};
|
|
||||||
|
|
||||||
searchRepos = (repoNameOrID) => {
|
|
||||||
if (this.getValueLength(repoNameOrID) < 3) {
|
|
||||||
toaster.notify(gettext('Required at least three letters.'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
navigate(`${siteRoot}sys/search-libraries/?name_or_id=${encodeURIComponent(repoNameOrID)}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
getValueLength(str) {
|
|
||||||
let code; let len = 0;
|
|
||||||
for (let i = 0, length = str.length; i < length; i++) {
|
|
||||||
code = str.charCodeAt(i);
|
|
||||||
if (code === 10) { // solve enter problem
|
|
||||||
len += 2;
|
|
||||||
} else if (code < 0x007f) {
|
|
||||||
len += 1;
|
|
||||||
} else if (code >= 0x0080 && code <= 0x07ff) {
|
|
||||||
len += 2;
|
|
||||||
} else if (code >= 0x0800 && code <= 0xffff) {
|
|
||||||
len += 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return len;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let { isCreateRepoDialogOpen } = this.state;
|
const { isCreateRepoDialogOpen } = this.props;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<>
|
||||||
<MainPanelTopbar search={this.getSearch()} {...this.props}>
|
|
||||||
<Button className="btn btn-secondary operation-item" onClick={this.toggleCreateRepoDialog}>
|
|
||||||
<i className="sf3-font sf3-font-enlarge text-secondary mr-1"></i>{gettext('New Library')}
|
|
||||||
</Button>
|
|
||||||
</MainPanelTopbar>
|
|
||||||
<div className="main-panel-center flex-row">
|
<div className="main-panel-center flex-row">
|
||||||
<div className="cur-view-container">
|
<div className="cur-view-container">
|
||||||
<ReposNav
|
|
||||||
currentItem="all"
|
|
||||||
sortBy={this.state.sortBy}
|
|
||||||
sortItems={this.sortItems}
|
|
||||||
/>
|
|
||||||
<div className="cur-view-content">
|
<div className="cur-view-content">
|
||||||
<Content
|
<Content
|
||||||
loading={this.state.loading}
|
loading={this.state.loading}
|
||||||
errorMsg={this.state.errorMsg}
|
errorMsg={this.state.errorMsg}
|
||||||
items={this.state.repos}
|
items={this.state.repos}
|
||||||
pageInfo={this.state.pageInfo}
|
pageInfo={this.state.pageInfo}
|
||||||
curPerPage={this.state.perPage}
|
curPerPage={this.props.perPage}
|
||||||
getListByPage={this.getReposByPage}
|
getListByPage={this.getReposByPage}
|
||||||
resetPerPage={this.resetPerPage}
|
resetPerPage={this.resetPerPage}
|
||||||
onDeleteRepo={this.onDeleteRepo}
|
onDeleteRepo={this.onDeleteRepo}
|
||||||
@@ -175,13 +108,13 @@ class AllRepos extends Component {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isCreateRepoDialogOpen &&
|
{isCreateRepoDialogOpen && (
|
||||||
<SysAdminCreateRepoDialog
|
<SysAdminCreateRepoDialog
|
||||||
createRepo={this.createRepo}
|
createRepo={this.createRepo}
|
||||||
toggleDialog={this.toggleCreateRepoDialog}
|
toggleDialog={this.props.toggleCreateRepoDialog}
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
</Fragment>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,9 +1,6 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { navigate } from '@gatsbyjs/reach-router';
|
|
||||||
import { Utils } from '../../../utils/utils';
|
import { Utils } from '../../../utils/utils';
|
||||||
import { systemAdminAPI } from '../../../utils/system-admin-api';
|
import { systemAdminAPI } from '../../../utils/system-admin-api';
|
||||||
import MainPanelTopbar from '../main-panel-topbar';
|
|
||||||
import ReposNav from './repos-nav';
|
|
||||||
import Content from './repos';
|
import Content from './repos';
|
||||||
|
|
||||||
class AllWikis extends Component {
|
class AllWikis extends Component {
|
||||||
@@ -15,25 +12,21 @@ class AllWikis extends Component {
|
|||||||
errorMsg: '',
|
errorMsg: '',
|
||||||
wikis: [],
|
wikis: [],
|
||||||
pageInfo: {},
|
pageInfo: {},
|
||||||
perPage: 100,
|
|
||||||
sortBy: '',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
let urlParams = (new URL(window.location)).searchParams;
|
this.getWikisByPage(this.props.currentPage);
|
||||||
const { currentPage = 1, perPage, sortBy } = this.state;
|
}
|
||||||
this.setState({
|
|
||||||
sortBy: urlParams.get('order_by') || sortBy,
|
componentDidUpdate(prevProps, prevState) {
|
||||||
perPage: parseInt(urlParams.get('per_page') || perPage),
|
if (prevProps.currentPage !== this.props.currentPage) {
|
||||||
currentPage: parseInt(urlParams.get('page') || currentPage)
|
this.getWikisByPage(this.props.currentPage);
|
||||||
}, () => {
|
}
|
||||||
this.getWikisByPage(this.state.currentPage);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getWikisByPage = (page) => {
|
getWikisByPage = (page) => {
|
||||||
const { perPage, sortBy } = this.state;
|
const { perPage, sortBy } = this.props;
|
||||||
systemAdminAPI.sysAdminListAllWikis(page, perPage, sortBy).then((res) => {
|
systemAdminAPI.sysAdminListAllWikis(page, perPage, sortBy).then((res) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: false,
|
loading: false,
|
||||||
@@ -48,28 +41,9 @@ class AllWikis extends Component {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
sortItems = (sortBy) => {
|
|
||||||
this.setState({
|
|
||||||
currentPage: 1,
|
|
||||||
sortBy: sortBy
|
|
||||||
}, () => {
|
|
||||||
let url = new URL(location.href);
|
|
||||||
let searchParams = new URLSearchParams(url.search);
|
|
||||||
const { currentPage, sortBy } = this.state;
|
|
||||||
searchParams.set('page', currentPage);
|
|
||||||
searchParams.set('order_by', sortBy);
|
|
||||||
url.search = searchParams.toString();
|
|
||||||
navigate(url.toString());
|
|
||||||
this.getWikisByPage(currentPage);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
resetPerPage = (perPage) => {
|
resetPerPage = (perPage) => {
|
||||||
this.setState({
|
this.props.onResetPerPage(perPage);
|
||||||
perPage: perPage
|
|
||||||
}, () => {
|
|
||||||
this.getWikisByPage(1);
|
this.getWikisByPage(1);
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onDeleteWiki = (targetRepo) => {
|
onDeleteWiki = (targetRepo) => {
|
||||||
@@ -92,22 +66,15 @@ class AllWikis extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
|
||||||
<MainPanelTopbar {...this.props} />
|
|
||||||
<div className="main-panel-center flex-row">
|
<div className="main-panel-center flex-row">
|
||||||
<div className="cur-view-container">
|
<div className="cur-view-container">
|
||||||
<ReposNav
|
|
||||||
currentItem="wikis"
|
|
||||||
sortBy={this.state.sortBy}
|
|
||||||
sortItems={this.sortItems}
|
|
||||||
/>
|
|
||||||
<div className="cur-view-content">
|
<div className="cur-view-content">
|
||||||
<Content
|
<Content
|
||||||
loading={this.state.loading}
|
loading={this.state.loading}
|
||||||
errorMsg={this.state.errorMsg}
|
errorMsg={this.state.errorMsg}
|
||||||
items={this.state.wikis}
|
items={this.state.wikis}
|
||||||
pageInfo={this.state.pageInfo}
|
pageInfo={this.state.pageInfo}
|
||||||
curPerPage={this.state.perPage}
|
curPerPage={this.props.perPage}
|
||||||
getListByPage={this.getWikisByPage}
|
getListByPage={this.getWikisByPage}
|
||||||
resetPerPage={this.resetPerPage}
|
resetPerPage={this.resetPerPage}
|
||||||
onDeleteRepo={this.onDeleteWiki}
|
onDeleteRepo={this.onDeleteWiki}
|
||||||
@@ -117,7 +84,6 @@ class AllWikis extends Component {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -15,15 +15,27 @@ class Nav extends React.Component {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.navItems = [
|
this.navItems = [
|
||||||
{ name: 'all', urlPart: 'all-libraries', text: gettext('All') },
|
{ name: 'all-libraries', urlPart: 'all-libraries', text: gettext('All') },
|
||||||
{ name: 'wikis', urlPart: 'all-wikis', text: gettext('Wikis') },
|
{ name: 'all-wikis', urlPart: 'all-wikis', text: gettext('Wikis') },
|
||||||
{ name: 'system', urlPart: 'system-library', text: gettext('System') },
|
{ name: 'system-library', urlPart: 'system-library', text: gettext('System') },
|
||||||
{ name: 'trash', urlPart: 'trash-libraries', text: gettext('Trash') }
|
{ name: 'trash-libraries', urlPart: 'trash-libraries', text: gettext('Trash') }
|
||||||
];
|
];
|
||||||
this.sortOptions = [
|
this.sortOptions = [
|
||||||
{ value: 'file_count-desc', text: gettext('Descending by files') },
|
{ value: 'file_count-desc', text: gettext('Descending by files') },
|
||||||
{ value: 'size-desc', text: gettext('Descending by size') }
|
{ value: 'size-desc', text: gettext('Descending by size') }
|
||||||
];
|
];
|
||||||
|
this.itemRefs = [];
|
||||||
|
this.itemWidths = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.measureItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (this.props.currentItem !== prevProps.currentItem) {
|
||||||
|
this.measureItems();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onSelectSortOption = (item) => {
|
onSelectSortOption = (item) => {
|
||||||
@@ -31,15 +43,34 @@ class Nav extends React.Component {
|
|||||||
this.props.sortItems(sortBy);
|
this.props.sortItems(sortBy);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
measureItems = () => {
|
||||||
|
this.itemWidths = this.itemRefs.map(ref => ref?.offsetWidth || 77);
|
||||||
|
this.forceUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { currentItem, sortBy, sortOrder = 'desc' } = this.props;
|
const { currentItem, sortBy, sortOrder = 'desc' } = this.props;
|
||||||
const showSortIcon = currentItem == 'all' || currentItem == 'wikis';
|
const showSortIcon = currentItem == 'all-libraries' || currentItem == 'all-wikis';
|
||||||
|
const activeIndex = this.navItems.findIndex(item => item.name === currentItem) || 0;
|
||||||
|
const indicatorWidth = this.itemWidths[activeIndex] || 56;
|
||||||
|
const indicatorOffset = this.itemWidths.slice(0, activeIndex).reduce((a, b) => a + b, 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="cur-view-path tab-nav-container">
|
<div className="cur-view-path tab-nav-container">
|
||||||
<ul className="nav">
|
<ul
|
||||||
|
className="nav nav-indicator-container position-relative"
|
||||||
|
style={{
|
||||||
|
'--indicator-width': `${indicatorWidth}px`,
|
||||||
|
'--indicator-offset': `${indicatorOffset}px`
|
||||||
|
}}
|
||||||
|
>
|
||||||
{this.navItems.map((item, index) => {
|
{this.navItems.map((item, index) => {
|
||||||
return (
|
return (
|
||||||
<li className="nav-item" key={index}>
|
<li
|
||||||
|
className="nav-item"
|
||||||
|
key={index}
|
||||||
|
ref={el => this.itemRefs[index] = el}
|
||||||
|
>
|
||||||
<Link to={`${siteRoot}sys/${item.urlPart}/`} className={`nav-link${currentItem == item.name ? ' active' : ''}`}>{item.text}</Link>
|
<Link to={`${siteRoot}sys/${item.urlPart}/`} className={`nav-link${currentItem == item.name ? ' active' : ''}`}>{item.text}</Link>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
@@ -1,12 +1,9 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Link } from '@gatsbyjs/reach-router';
|
|
||||||
import { Utils } from '../../../utils/utils';
|
import { Utils } from '../../../utils/utils';
|
||||||
import { systemAdminAPI } from '../../../utils/system-admin-api';
|
import { systemAdminAPI } from '../../../utils/system-admin-api';
|
||||||
import { gettext, siteRoot } from '../../../utils/constants';
|
import { gettext, siteRoot } from '../../../utils/constants';
|
||||||
import Loading from '../../../components/loading';
|
import Loading from '../../../components/loading';
|
||||||
import MainPanelTopbar from '../main-panel-topbar';
|
|
||||||
import ReposNav from './repos-nav';
|
|
||||||
|
|
||||||
class Content extends Component {
|
class Content extends Component {
|
||||||
render() {
|
render() {
|
||||||
@@ -55,7 +52,7 @@ class Item extends Component {
|
|||||||
const item = this.props.item;
|
const item = this.props.item;
|
||||||
return (
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
<td><Link to={`${siteRoot}sys/libraries/${item.id}/`}>{item.name}</Link></td>
|
<td><a href={`${siteRoot}sys/libraries/${item.id}/`}>{item.name}</a></td>
|
||||||
<td>{item.id}</td>
|
<td>{item.id}</td>
|
||||||
<td>{item.description}</td>
|
<td>{item.description}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -96,11 +93,8 @@ class SystemRepo extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
|
||||||
<MainPanelTopbar {...this.props} />
|
|
||||||
<div className="main-panel-center flex-row">
|
<div className="main-panel-center flex-row">
|
||||||
<div className="cur-view-container">
|
<div className="cur-view-container">
|
||||||
<ReposNav currentItem="system" />
|
|
||||||
<div className="cur-view-content">
|
<div className="cur-view-content">
|
||||||
<Content
|
<Content
|
||||||
loading={this.state.loading}
|
loading={this.state.loading}
|
||||||
@@ -110,7 +104,6 @@ class SystemRepo extends Component {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Button } from 'reactstrap';
|
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
@@ -14,10 +13,8 @@ import Paginator from '../../../components/paginator';
|
|||||||
import ModalPortal from '../../../components/modal-portal';
|
import ModalPortal from '../../../components/modal-portal';
|
||||||
import OpMenu from '../../../components/dialog/op-menu';
|
import OpMenu from '../../../components/dialog/op-menu';
|
||||||
import CommonOperationConfirmationDialog from '../../../components/dialog/common-operation-confirmation-dialog';
|
import CommonOperationConfirmationDialog from '../../../components/dialog/common-operation-confirmation-dialog';
|
||||||
import MainPanelTopbar from '../main-panel-topbar';
|
|
||||||
import Search from '../search';
|
import Search from '../search';
|
||||||
import UserLink from '../user-link';
|
import UserLink from '../user-link';
|
||||||
import ReposNav from './repos-nav';
|
|
||||||
|
|
||||||
const { trashReposExpireDays } = window.sysadmin.pageOptions;
|
const { trashReposExpireDays } = window.sysadmin.pageOptions;
|
||||||
|
|
||||||
@@ -304,7 +301,6 @@ class TrashRepos extends Component {
|
|||||||
repos: [],
|
repos: [],
|
||||||
pageInfo: {},
|
pageInfo: {},
|
||||||
perPage: 100,
|
perPage: 100,
|
||||||
isCleanTrashDialogOpen: false
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,10 +315,6 @@ class TrashRepos extends Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleCleanTrashDialog = () => {
|
|
||||||
this.setState({ isCleanTrashDialogOpen: !this.state.isCleanTrashDialogOpen });
|
|
||||||
};
|
|
||||||
|
|
||||||
getReposByPage = (page) => {
|
getReposByPage = (page) => {
|
||||||
let perPage = this.state.perPage;
|
let perPage = this.state.perPage;
|
||||||
systemAdminAPI.sysAdminListTrashRepos(page, perPage).then((res) => {
|
systemAdminAPI.sysAdminListTrashRepos(page, perPage).then((res) => {
|
||||||
@@ -399,20 +391,12 @@ class TrashRepos extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { isCleanTrashDialogOpen } = this.state;
|
const { isCleanTrashDialogOpen } = this.props;
|
||||||
|
|
||||||
// enable 'search': <MainPanelTopbar search={this.getSearch()}>
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<>
|
||||||
{this.state.repos.length ? (
|
|
||||||
<MainPanelTopbar {...this.props}>
|
|
||||||
<Button className="operation-item" onClick={this.toggleCleanTrashDialog}>{gettext('Clean')}</Button>
|
|
||||||
</MainPanelTopbar>
|
|
||||||
) : <MainPanelTopbar {...this.props} />
|
|
||||||
}
|
|
||||||
<div className="main-panel-center flex-row">
|
<div className="main-panel-center flex-row">
|
||||||
<div className="cur-view-container">
|
<div className="cur-view-container">
|
||||||
<ReposNav currentItem="trash" />
|
|
||||||
<div className="cur-view-content">
|
<div className="cur-view-content">
|
||||||
<Content
|
<Content
|
||||||
loading={this.state.loading}
|
loading={this.state.loading}
|
||||||
@@ -434,10 +418,10 @@ class TrashRepos extends Component {
|
|||||||
message={gettext('Are you sure you want to clear trash?')}
|
message={gettext('Are you sure you want to clear trash?')}
|
||||||
executeOperation={this.cleanTrash}
|
executeOperation={this.cleanTrash}
|
||||||
confirmBtnText={gettext('Clear')}
|
confirmBtnText={gettext('Clear')}
|
||||||
toggleDialog={this.toggleCleanTrashDialog}
|
toggleDialog={this.props.toggleCleanTrashDialog}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
</Fragment>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -59,7 +59,7 @@ class SidePanel extends React.Component {
|
|||||||
<li className={`nav-item ${this.getActiveClass('devices')}`}>
|
<li className={`nav-item ${this.getActiveClass('devices')}`}>
|
||||||
<Link
|
<Link
|
||||||
className={`nav-link ellipsis ${this.getActiveClass('devices')}`}
|
className={`nav-link ellipsis ${this.getActiveClass('devices')}`}
|
||||||
to={siteRoot + 'sys/desktop-devices/'}
|
to={siteRoot + 'sys/devices/desktop/'}
|
||||||
onClick={() => this.props.tabItemClick('devices')}
|
onClick={() => this.props.tabItemClick('devices')}
|
||||||
>
|
>
|
||||||
<span className="sf2-icon-monitor" aria-hidden="true"></span>
|
<span className="sf2-icon-monitor" aria-hidden="true"></span>
|
||||||
|
19
frontend/src/pages/sys-admin/statistic/layout.js
Normal file
19
frontend/src/pages/sys-admin/statistic/layout.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useLocation } from '@gatsbyjs/reach-router';
|
||||||
|
import StatisticNav from './statistic-nav';
|
||||||
|
import MainPanelTopbar from '../main-panel-topbar';
|
||||||
|
|
||||||
|
const StatisticLayout = ({ children, ...commonProps }) => {
|
||||||
|
const location = useLocation();
|
||||||
|
const pathSegment = location.pathname.split('/').filter(Boolean).pop();
|
||||||
|
const currentItem = `${pathSegment}Statistic`;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MainPanelTopbar {...commonProps} />
|
||||||
|
<StatisticNav currentItem={currentItem} />
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StatisticLayout;
|
@@ -1,7 +1,5 @@
|
|||||||
import React, { Fragment, useCallback, useMemo, useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import MainPanelTopbar from '../main-panel-topbar';
|
|
||||||
import StatisticNav from './statistic-nav';
|
|
||||||
import StatisticCommonTool from './statistic-common-tool';
|
import StatisticCommonTool from './statistic-common-tool';
|
||||||
import { systemAdminAPI } from '../../../utils/system-admin-api';
|
import { systemAdminAPI } from '../../../utils/system-admin-api';
|
||||||
import Loading from '../../../components/loading';
|
import Loading from '../../../components/loading';
|
||||||
@@ -45,10 +43,7 @@ const StatisticFile = (props) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
|
||||||
<MainPanelTopbar {...props} />
|
|
||||||
<div className="cur-view-container">
|
<div className="cur-view-container">
|
||||||
<StatisticNav currentItem="fileStatistic" />
|
|
||||||
<div className="cur-view-content">
|
<div className="cur-view-content">
|
||||||
<StatisticCommonTool getActivesFiles={getActivesFiles} />
|
<StatisticCommonTool getActivesFiles={getActivesFiles} />
|
||||||
{isLoading && <Loading />}
|
{isLoading && <Loading />}
|
||||||
@@ -57,7 +52,6 @@ const StatisticFile = (props) => {
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
@@ -1,8 +1,6 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { systemAdminAPI } from '../../../utils/system-admin-api';
|
import { systemAdminAPI } from '../../../utils/system-admin-api';
|
||||||
import MainPanelTopbar from '../main-panel-topbar';
|
|
||||||
import StatisticNav from './statistic-nav';
|
|
||||||
import { gettext } from '../../../utils/constants';
|
import { gettext } from '../../../utils/constants';
|
||||||
import { Tooltip } from 'reactstrap';
|
import { Tooltip } from 'reactstrap';
|
||||||
|
|
||||||
@@ -158,10 +156,6 @@ class StatisticMetrics extends Component {
|
|||||||
const { groupedMetrics, loading, error } = this.state;
|
const { groupedMetrics, loading, error } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<MainPanelTopbar {...this.props} />
|
|
||||||
<div className="">
|
|
||||||
<StatisticNav currentItem="metricsStatistic" />
|
|
||||||
<div className="cur-metrics-content">
|
<div className="cur-metrics-content">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="loading-icon loading-tip"></div>
|
<div className="loading-icon loading-tip"></div>
|
||||||
@@ -195,8 +189,6 @@ class StatisticMetrics extends Component {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -14,21 +14,45 @@ class Nav extends React.Component {
|
|||||||
this.navItems = [
|
this.navItems = [
|
||||||
{ name: 'fileStatistic', urlPart: 'statistics/file', text: gettext('File') },
|
{ name: 'fileStatistic', urlPart: 'statistics/file', text: gettext('File') },
|
||||||
{ name: 'storageStatistic', urlPart: 'statistics/storage', text: gettext('Storage') },
|
{ name: 'storageStatistic', urlPart: 'statistics/storage', text: gettext('Storage') },
|
||||||
{ name: 'usersStatistic', urlPart: 'statistics/user', text: gettext('Users') },
|
{ name: 'userStatistic', urlPart: 'statistics/user', text: gettext('Users') },
|
||||||
{ name: 'trafficStatistic', urlPart: 'statistics/traffic', text: gettext('Traffic') },
|
{ name: 'trafficStatistic', urlPart: 'statistics/traffic', text: gettext('Traffic') },
|
||||||
{ name: 'reportsStatistic', urlPart: 'statistics/reports', text: gettext('Reports') },
|
{ name: 'reportsStatistic', urlPart: 'statistics/reports', text: gettext('Reports') },
|
||||||
{ name: 'metricsStatistic', urlPart: 'statistics/metrics', text: gettext('Metrics') },
|
{ name: 'metricsStatistic', urlPart: 'statistics/metrics', text: gettext('Metrics') },
|
||||||
];
|
];
|
||||||
|
this.itemRefs = [];
|
||||||
|
this.itemWidths = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.measureItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
measureItems = () => {
|
||||||
|
this.itemWidths = this.itemRefs.map(ref => ref?.offsetWidth || 0);
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { currentItem } = this.props;
|
const { currentItem } = this.props;
|
||||||
|
const activeIndex = this.navItems.findIndex(item => item.name === currentItem);
|
||||||
|
const indicatorWidth = this.itemWidths[activeIndex] || 56;
|
||||||
|
const indicatorOffset = this.itemWidths.slice(0, activeIndex).reduce((a, b) => a + b, 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="cur-view-path tab-nav-container">
|
<div className="cur-view-path tab-nav-container">
|
||||||
<ul className="nav">
|
<ul
|
||||||
|
className="nav nav-indicator-container position-relative"
|
||||||
|
style={{
|
||||||
|
'--indicator-width': `${indicatorWidth}px`,
|
||||||
|
'--indicator-offset': `${indicatorOffset}px`
|
||||||
|
}}
|
||||||
|
>
|
||||||
{this.navItems.map((item, index) => {
|
{this.navItems.map((item, index) => {
|
||||||
return (
|
return (
|
||||||
<li className="nav-item" key={index}>
|
<li
|
||||||
|
className="nav-item"
|
||||||
|
key={index}
|
||||||
|
ref={el => this.itemRefs[index] = el}
|
||||||
|
>
|
||||||
<Link to={`${siteRoot}sys/${item.urlPart}/`} className={`nav-link${currentItem == item.name ? ' active' : ''}`}>{item.text}</Link>
|
<Link to={`${siteRoot}sys/${item.urlPart}/`} className={`nav-link${currentItem == item.name ? ' active' : ''}`}>{item.text}</Link>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
@@ -1,7 +1,5 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React from 'react';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import MainPanelTopbar from '../main-panel-topbar';
|
|
||||||
import StatisticNav from './statistic-nav';
|
|
||||||
import { Button, Input } from 'reactstrap';
|
import { Button, Input } from 'reactstrap';
|
||||||
import { siteRoot, gettext, serviceURL } from '../../../utils/constants';
|
import { siteRoot, gettext, serviceURL } from '../../../utils/constants';
|
||||||
|
|
||||||
@@ -60,10 +58,7 @@ class StatisticReports extends React.Component {
|
|||||||
|
|
||||||
let { errorMessage } = this.state;
|
let { errorMessage } = this.state;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
|
||||||
<MainPanelTopbar {...this.props} />
|
|
||||||
<div className="cur-view-container">
|
<div className="cur-view-container">
|
||||||
<StatisticNav currentItem="reportsStatistic" />
|
|
||||||
<div className="cur-view-content">
|
<div className="cur-view-content">
|
||||||
<div className="statistic-reports">
|
<div className="statistic-reports">
|
||||||
<div className="statistic-reports-title">{gettext('Monthly User Traffic')}</div>
|
<div className="statistic-reports-title">{gettext('Monthly User Traffic')}</div>
|
||||||
@@ -80,7 +75,6 @@ class StatisticReports extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,5 @@
|
|||||||
import React, { useState, useMemo, useCallback } from 'react';
|
import React, { useState, useMemo, useCallback } from 'react';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import MainPanelTopbar from '../main-panel-topbar';
|
|
||||||
import StatisticNav from './statistic-nav';
|
|
||||||
import StatisticCommonTool from './statistic-common-tool';
|
import StatisticCommonTool from './statistic-common-tool';
|
||||||
import { systemAdminAPI } from '../../../utils/system-admin-api';
|
import { systemAdminAPI } from '../../../utils/system-admin-api';
|
||||||
import Loading from '../../../components/loading';
|
import Loading from '../../../components/loading';
|
||||||
@@ -44,10 +42,7 @@ const StatisticStorage = (props) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<MainPanelTopbar {...props} />
|
|
||||||
<div className="cur-view-container">
|
<div className="cur-view-container">
|
||||||
<StatisticNav currentItem="storageStatistic" />
|
|
||||||
<div className="cur-view-content">
|
<div className="cur-view-content">
|
||||||
<StatisticCommonTool getActivesFiles={getActivesFiles} />
|
<StatisticCommonTool getActivesFiles={getActivesFiles} />
|
||||||
{isLoading && <Loading />}
|
{isLoading && <Loading />}
|
||||||
@@ -63,7 +58,6 @@ const StatisticStorage = (props) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
@@ -2,8 +2,6 @@ import React, { Fragment } from 'react';
|
|||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { gettext } from '../../../utils/constants';
|
import { gettext } from '../../../utils/constants';
|
||||||
import { systemAdminAPI } from '../../../utils/system-admin-api';
|
import { systemAdminAPI } from '../../../utils/system-admin-api';
|
||||||
import MainPanelTopbar from '../main-panel-topbar';
|
|
||||||
import StatisticNav from './statistic-nav';
|
|
||||||
import StatisticCommonTool from './statistic-common-tool';
|
import StatisticCommonTool from './statistic-common-tool';
|
||||||
import Loading from '../../../components/loading';
|
import Loading from '../../../components/loading';
|
||||||
import OrgsTraffic from './statistic-traffic-orgs';
|
import OrgsTraffic from './statistic-traffic-orgs';
|
||||||
@@ -110,9 +108,9 @@ class StatisticTraffic extends React.Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<MainPanelTopbar {...this.props} />
|
{/* <MainPanelTopbar {...this.props} /> */}
|
||||||
<div className="cur-view-container">
|
<div className="cur-view-container">
|
||||||
<StatisticNav currentItem="trafficStatistic" />
|
{/* <StatisticNav currentItem="trafficStatistic" /> */}
|
||||||
<div className="cur-view-content">
|
<div className="cur-view-content">
|
||||||
{this.renderCommonTool()}
|
{this.renderCommonTool()}
|
||||||
{isLoading && <Loading />}
|
{isLoading && <Loading />}
|
||||||
|
@@ -1,8 +1,6 @@
|
|||||||
import React, { Fragment, useCallback, useMemo, useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { gettext } from '../../../utils/constants';
|
import { gettext } from '../../../utils/constants';
|
||||||
import MainPanelTopbar from '../main-panel-topbar';
|
|
||||||
import StatisticNav from './statistic-nav';
|
|
||||||
import StatisticCommonTool from './statistic-common-tool';
|
import StatisticCommonTool from './statistic-common-tool';
|
||||||
import { systemAdminAPI } from '../../../utils/system-admin-api';
|
import { systemAdminAPI } from '../../../utils/system-admin-api';
|
||||||
import Loading from '../../../components/loading';
|
import Loading from '../../../components/loading';
|
||||||
@@ -40,10 +38,7 @@ const StatisticUsers = (props) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
|
||||||
<MainPanelTopbar {...props} />
|
|
||||||
<div className="cur-view-container">
|
<div className="cur-view-container">
|
||||||
<StatisticNav currentItem="usersStatistic" />
|
|
||||||
<div className="cur-view-content">
|
<div className="cur-view-content">
|
||||||
<StatisticCommonTool getActivesFiles={getActivesFiles} />
|
<StatisticCommonTool getActivesFiles={getActivesFiles} />
|
||||||
{isLoading && <Loading />}
|
{isLoading && <Loading />}
|
||||||
@@ -52,7 +47,6 @@ const StatisticUsers = (props) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
@@ -11,7 +11,7 @@ const propTypes = {
|
|||||||
class UserLink extends Component {
|
class UserLink extends Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <Link to={`${siteRoot}sys/users/${encodeURIComponent(this.props.email)}/`}>{this.props.name}</Link>;
|
return <Link to={`${siteRoot}sys/user/${encodeURIComponent(this.props.email)}/`}>{this.props.name}</Link>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
191
frontend/src/pages/sys-admin/users/index.js
Normal file
191
frontend/src/pages/sys-admin/users/index.js
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { Button } from 'reactstrap';
|
||||||
|
import { Router, useLocation, navigate } from '@gatsbyjs/reach-router';
|
||||||
|
import MainPanelTopbar from '../main-panel-topbar';
|
||||||
|
import { gettext, siteRoot } from '../../../utils/constants';
|
||||||
|
import UsersNav from './users-nav';
|
||||||
|
import Users from './users';
|
||||||
|
import Search from '../search';
|
||||||
|
import AdminUsers from './admin-users';
|
||||||
|
import LDAPImportedUsers from './ldap-imported-users';
|
||||||
|
import LDAPUsers from './ldap-users';
|
||||||
|
import UserNav from './user-nav';
|
||||||
|
import { eventBus } from '../../../components/common/event-bus';
|
||||||
|
import { EVENT_BUS_TYPE } from '../../../components/common/event-bus-type';
|
||||||
|
|
||||||
|
const UsersLayout = ({ ...commonProps }) => {
|
||||||
|
const [sortBy, setSortBy] = useState('');
|
||||||
|
const [sortOrder, setSortOrder] = useState('asc');
|
||||||
|
const [perPage, setPerPage] = useState(100);
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [hasUserSelected, setHasUserSelected] = useState(false);
|
||||||
|
const [isImportUserDialogOpen, setIsImportUserDialogOpen] = useState(false);
|
||||||
|
const [isAddUserDialogOpen, setIsAddUserDialogOpen] = useState(false);
|
||||||
|
const [isBatchSetQuotaDialogOpen, setIsBatchSetQuotaDialogOpen] = useState(false);
|
||||||
|
const [isBatchDeleteUserDialogOpen, setIsBatchDeleteUserDialogOpen] = useState(false);
|
||||||
|
const [isBatchAddAdminDialogOpen, setIsBatchAddAdminDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
const location = useLocation();
|
||||||
|
const { curTab, isAdmin, isLDAPImported } = useMemo(() => {
|
||||||
|
const path = location.pathname.split('/').filter(Boolean).pop();
|
||||||
|
let curTab = path;
|
||||||
|
if (path === 'users') {
|
||||||
|
curTab = 'database';
|
||||||
|
} else if (path === 'admins') {
|
||||||
|
curTab = 'admin';
|
||||||
|
}
|
||||||
|
const isAdmin = curTab === 'admin';
|
||||||
|
const isLDAPImported = curTab === 'ldap-imported';
|
||||||
|
return { curTab, isAdmin, isLDAPImported };
|
||||||
|
}, [location.pathname]);
|
||||||
|
|
||||||
|
const onHasUserSelected = (hasSelected) => {
|
||||||
|
setHasUserSelected(hasSelected);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleImportUserDialog = () => {
|
||||||
|
setIsImportUserDialogOpen(!isImportUserDialogOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleAddUserDialog = () => {
|
||||||
|
setIsAddUserDialogOpen(!isAddUserDialogOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleBatchSetQuotaDialog = () => {
|
||||||
|
setIsBatchSetQuotaDialogOpen(!isBatchSetQuotaDialogOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleBatchDeleteUserDialog = () => {
|
||||||
|
setIsBatchDeleteUserDialogOpen(!isBatchDeleteUserDialogOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleBatchAddAdminDialog = () => {
|
||||||
|
setIsBatchAddAdminDialogOpen(!isBatchAddAdminDialogOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortByQuotaUsage = (sortBy, sortOrder) => {
|
||||||
|
setSortBy(sortBy);
|
||||||
|
setSortOrder(sortOrder);
|
||||||
|
setCurrentPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOperationsForAll = () => {
|
||||||
|
if (isAdmin) {
|
||||||
|
return <Button className="btn btn-secondary operation-item" onClick={toggleBatchAddAdminDialog}>{gettext('Add Admin')}</Button>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLDAPImported) {
|
||||||
|
return <a className="btn btn-secondary operation-item" href={`${siteRoot}sys/useradmin/export-excel/`}>{gettext('Export Excel')}</a>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 'database'
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button className="btn btn-secondary operation-item" onClick={toggleImportUserDialog}>{gettext('Import Users')}</Button>
|
||||||
|
<Button className="btn btn-secondary operation-item" onClick={toggleAddUserDialog}>{gettext('Add User')}</Button>
|
||||||
|
<a className="btn btn-secondary operation-item" href={`${siteRoot}sys/useradmin/export-excel/`}>{gettext('Export Excel')}</a>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSearch = () => {
|
||||||
|
if (isAdmin) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// offer 'Search' for 'DB' & 'LDAPImported' users
|
||||||
|
return <Search
|
||||||
|
placeholder={gettext('Search users')}
|
||||||
|
submit={(keyword) => navigate(`${siteRoot}sys/search-users/?query=${encodeURIComponent(keyword)}`)}
|
||||||
|
/>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const usersProps = {
|
||||||
|
curTab,
|
||||||
|
isAdmin,
|
||||||
|
isLDAPImported,
|
||||||
|
isAddUserDialogOpen,
|
||||||
|
isImportUserDialogOpen,
|
||||||
|
isBatchAddAdminDialogOpen,
|
||||||
|
isBatchDeleteUserDialogOpen,
|
||||||
|
isBatchSetQuotaDialogOpen,
|
||||||
|
onHasUserSelected,
|
||||||
|
toggleAddUserDialog,
|
||||||
|
toggleImportUserDialog,
|
||||||
|
toggleBatchAddAdminDialog,
|
||||||
|
toggleBatchDeleteUserDialog,
|
||||||
|
toggleBatchSetQuotaDialog
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const urlParams = new URL(window.location).searchParams;
|
||||||
|
setSortBy(urlParams.get('order_by') || sortBy);
|
||||||
|
setSortOrder(urlParams.get('direction') || sortOrder);
|
||||||
|
setCurrentPage(parseInt(urlParams.get('page') || currentPage));
|
||||||
|
setPerPage(parseInt(urlParams.get('per_page') || perPage));
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MainPanelTopbar search={getSearch()} {...commonProps}>
|
||||||
|
{hasUserSelected ?
|
||||||
|
<>
|
||||||
|
<Button className="btn btn-secondary operation-item" onClick={toggleBatchSetQuotaDialog}>{gettext('Set Quota')}</Button>
|
||||||
|
<Button className="btn btn-secondary operation-item" onClick={toggleBatchDeleteUserDialog}>{gettext('Delete Users')}</Button>
|
||||||
|
</>
|
||||||
|
: getOperationsForAll()
|
||||||
|
}
|
||||||
|
</MainPanelTopbar>
|
||||||
|
<UsersNav currentItem={curTab} sortBy={sortBy} sortOrder={sortOrder} sortItems={sortByQuotaUsage} />
|
||||||
|
<Router className="d-flex overflow-hidden">
|
||||||
|
<Users
|
||||||
|
default
|
||||||
|
sortBy={sortBy}
|
||||||
|
sortOrder={sortOrder}
|
||||||
|
perPage={perPage}
|
||||||
|
currentPage={currentPage}
|
||||||
|
{...usersProps}
|
||||||
|
/>
|
||||||
|
<AdminUsers path="admins" {...usersProps} />
|
||||||
|
<LDAPImportedUsers path="ldap-imported" {...usersProps} />
|
||||||
|
<LDAPUsers path="ldap" {...usersProps} />
|
||||||
|
</Router>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const UserLayout = ({ email, children, ...commonProps }) => {
|
||||||
|
const [username, setUsername] = useState('');
|
||||||
|
const location = useLocation();
|
||||||
|
const path = location.pathname.split('/').filter(Boolean).pop();
|
||||||
|
let curTab = 'info';
|
||||||
|
if (path === 'owned-libraries') {
|
||||||
|
curTab = 'owned-repos';
|
||||||
|
} else if (path === 'shared-libraries') {
|
||||||
|
curTab = 'shared-repos';
|
||||||
|
} else if (path === 'shared-links') {
|
||||||
|
curTab = 'links';
|
||||||
|
} else if (path === 'groups') {
|
||||||
|
curTab = 'groups';
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribeUsername = eventBus.subscribe(EVENT_BUS_TYPE.SYNC_USERNAME, (username) => {
|
||||||
|
setUsername(username);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribeUsername();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MainPanelTopbar {...commonProps} />
|
||||||
|
<UserNav currentItem={curTab} email={email} userName={username} />
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { UsersLayout, UserLayout };
|
@@ -7,8 +7,6 @@ import { systemAdminAPI } from '../../../utils/system-admin-api';
|
|||||||
import { siteRoot, gettext } from '../../../utils/constants';
|
import { siteRoot, gettext } from '../../../utils/constants';
|
||||||
import EmptyTip from '../../../components/empty-tip';
|
import EmptyTip from '../../../components/empty-tip';
|
||||||
import Loading from '../../../components/loading';
|
import Loading from '../../../components/loading';
|
||||||
import MainPanelTopbar from '../main-panel-topbar';
|
|
||||||
import Nav from './user-nav';
|
|
||||||
|
|
||||||
class Content extends Component {
|
class Content extends Component {
|
||||||
|
|
||||||
@@ -182,10 +180,10 @@ class Groups extends Component {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<MainPanelTopbar {...this.props} />
|
{/* <MainPanelTopbar {...this.props} /> */}
|
||||||
<div className="main-panel-center flex-row">
|
<div className="main-panel-center flex-row">
|
||||||
<div className="cur-view-container">
|
<div className="cur-view-container">
|
||||||
<Nav currentItem="groups" email={this.props.email} userName={this.state.userInfo.name} />
|
{/* <Nav currentItem="groups" email={this.props.email} userName={this.state.userInfo.name} /> */}
|
||||||
<div className="cur-view-content">
|
<div className="cur-view-content">
|
||||||
<Content
|
<Content
|
||||||
loading={this.state.loading}
|
loading={this.state.loading}
|
||||||
|
@@ -10,9 +10,9 @@ import EditIcon from '../../../components/edit-icon';
|
|||||||
import SysAdminSetQuotaDialog from '../../../components/dialog/sysadmin-dialog/set-quota';
|
import SysAdminSetQuotaDialog from '../../../components/dialog/sysadmin-dialog/set-quota';
|
||||||
import SysAdminSetUploadDownloadRateLimitDialog from '../../../components/dialog/sysadmin-dialog/set-upload-download-rate-limit';
|
import SysAdminSetUploadDownloadRateLimitDialog from '../../../components/dialog/sysadmin-dialog/set-upload-download-rate-limit';
|
||||||
import SysAdminUpdateUserDialog from '../../../components/dialog/sysadmin-dialog/update-user';
|
import SysAdminUpdateUserDialog from '../../../components/dialog/sysadmin-dialog/update-user';
|
||||||
import MainPanelTopbar from '../main-panel-topbar';
|
|
||||||
import Nav from './user-nav';
|
|
||||||
import Selector from '../../../components/single-selector';
|
import Selector from '../../../components/single-selector';
|
||||||
|
import { eventBus } from '../../../components/common/event-bus';
|
||||||
|
import { EVENT_BUS_TYPE } from '../../../components/common/event-bus-type';
|
||||||
|
|
||||||
const { twoFactorAuthEnabled, availableRoles } = window.sysadmin.pageOptions;
|
const { twoFactorAuthEnabled, availableRoles } = window.sysadmin.pageOptions;
|
||||||
|
|
||||||
@@ -224,8 +224,6 @@ class Content extends Component {
|
|||||||
Content.propTypes = {
|
Content.propTypes = {
|
||||||
loading: PropTypes.bool.isRequired,
|
loading: PropTypes.bool.isRequired,
|
||||||
errorMsg: PropTypes.string.isRequired,
|
errorMsg: PropTypes.string.isRequired,
|
||||||
items: PropTypes.array.isRequired,
|
|
||||||
deleteItem: PropTypes.func,
|
|
||||||
updateUser: PropTypes.func.isRequired,
|
updateUser: PropTypes.func.isRequired,
|
||||||
userInfo: PropTypes.object.isRequired,
|
userInfo: PropTypes.object.isRequired,
|
||||||
disable2FA: PropTypes.func.isRequired,
|
disable2FA: PropTypes.func.isRequired,
|
||||||
@@ -252,6 +250,7 @@ class User extends Component {
|
|||||||
loading: false,
|
loading: false,
|
||||||
userInfo: res.data
|
userInfo: res.data
|
||||||
});
|
});
|
||||||
|
eventBus.dispatch(EVENT_BUS_TYPE.SYNC_USERNAME, res.data.name);
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: false,
|
loading: false,
|
||||||
@@ -268,6 +267,7 @@ class User extends Component {
|
|||||||
this.setState({
|
this.setState({
|
||||||
userInfo: userInfo
|
userInfo: userInfo
|
||||||
});
|
});
|
||||||
|
eventBus.dispatch(EVENT_BUS_TYPE.SYNC_USERNAME, res.data.name);
|
||||||
toaster.success(gettext('Edit succeeded'));
|
toaster.success(gettext('Edit succeeded'));
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
let errMessage = Utils.getErrorMsg(error);
|
let errMessage = Utils.getErrorMsg(error);
|
||||||
@@ -339,10 +339,8 @@ class User extends Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MainPanelTopbar {...this.props} />
|
|
||||||
<div className="main-panel-center flex-row">
|
<div className="main-panel-center flex-row">
|
||||||
<div className="cur-view-container">
|
<div className="cur-view-container">
|
||||||
<Nav currentItem="info" email={this.props.email} userName={userInfo.name} />
|
|
||||||
<div className="cur-view-content">
|
<div className="cur-view-content">
|
||||||
<Content
|
<Content
|
||||||
loading={this.state.loading}
|
loading={this.state.loading}
|
||||||
|
@@ -8,8 +8,6 @@ import EmptyTip from '../../../components/empty-tip';
|
|||||||
import Loading from '../../../components/loading';
|
import Loading from '../../../components/loading';
|
||||||
import OpMenu from '../../../components/dialog/op-menu';
|
import OpMenu from '../../../components/dialog/op-menu';
|
||||||
import LinkDialog from '../../../components/dialog/share-admin-link';
|
import LinkDialog from '../../../components/dialog/share-admin-link';
|
||||||
import MainPanelTopbar from '../main-panel-topbar';
|
|
||||||
import Nav from './user-nav';
|
|
||||||
|
|
||||||
class Content extends Component {
|
class Content extends Component {
|
||||||
|
|
||||||
@@ -321,10 +319,8 @@ class Links extends Component {
|
|||||||
const { shareLinkItems, uploadLinkItems } = this.state;
|
const { shareLinkItems, uploadLinkItems } = this.state;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<MainPanelTopbar {...this.props} />
|
|
||||||
<div className="main-panel-center flex-row">
|
<div className="main-panel-center flex-row">
|
||||||
<div className="cur-view-container">
|
<div className="cur-view-container">
|
||||||
<Nav currentItem="links" email={this.props.email} userName={this.state.userInfo.name} />
|
|
||||||
<div className="cur-view-content">
|
<div className="cur-view-content">
|
||||||
<Content
|
<Content
|
||||||
loading={this.state.loading}
|
loading={this.state.loading}
|
||||||
|
@@ -20,20 +20,43 @@ class Nav extends React.Component {
|
|||||||
{ name: 'links', urlPart: 'shared-links', text: gettext('Shared Links') },
|
{ name: 'links', urlPart: 'shared-links', text: gettext('Shared Links') },
|
||||||
{ name: 'groups', urlPart: 'groups', text: gettext('Groups') }
|
{ name: 'groups', urlPart: 'groups', text: gettext('Groups') }
|
||||||
];
|
];
|
||||||
|
this.itemRefs = [];
|
||||||
|
this.itemWidths = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.measureItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
measureItems = () => {
|
||||||
|
this.itemWidths = this.itemRefs.map(ref => ref?.offsetWidth || 77);
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { currentItem, email, userName } = this.props;
|
const { currentItem, email, userName } = this.props;
|
||||||
|
const activeIndex = this.navItems.findIndex(item => item.name === currentItem) || 0;
|
||||||
|
const indicatorWidth = this.itemWidths[activeIndex] || 56;
|
||||||
|
const indicatorOffset = this.itemWidths.slice(0, activeIndex).reduce((a, b) => a + b, 0);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="cur-view-path">
|
<div className="cur-view-path">
|
||||||
<h3 className="sf-heading"><Link to={`${siteRoot}sys/users/`}>{gettext('Users')}</Link> / {userName}</h3>
|
<h3 className="sf-heading"><Link to={`${siteRoot}sys/users/`}>{gettext('Users')}</Link> / {userName}</h3>
|
||||||
</div>
|
</div>
|
||||||
<ul className="nav border-bottom mx-4">
|
<ul
|
||||||
|
className="nav nav-indicator-container position-relative mx-4"
|
||||||
|
style={{
|
||||||
|
'--indicator-width': `${indicatorWidth}px`,
|
||||||
|
'--indicator-offset': `${indicatorOffset}px`
|
||||||
|
}}
|
||||||
|
>
|
||||||
{this.navItems.map((item, index) => {
|
{this.navItems.map((item, index) => {
|
||||||
return (
|
return (
|
||||||
<li className="nav-item mr-2" key={index}>
|
<li
|
||||||
<Link to={`${siteRoot}sys/users/${encodeURIComponent(email)}/${item.urlPart}`} className={`nav-link ${currentItem == item.name ? ' active' : ''}`}>{item.text}</Link>
|
className="nav-item"
|
||||||
|
key={index}
|
||||||
|
ref={el => this.itemRefs[index] = el}
|
||||||
|
>
|
||||||
|
<Link to={`${siteRoot}sys/user/${encodeURIComponent(email)}/${item.urlPart}`} className={`nav-link mx-3 ${currentItem == item.name ? ' active' : ''}`}>{item.text}</Link>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@@ -12,8 +12,6 @@ import Loading from '../../../components/loading';
|
|||||||
import CommonOperationConfirmationDialog from '../../../components/dialog/common-operation-confirmation-dialog';
|
import CommonOperationConfirmationDialog from '../../../components/dialog/common-operation-confirmation-dialog';
|
||||||
import TransferDialog from '../../../components/dialog/transfer-dialog';
|
import TransferDialog from '../../../components/dialog/transfer-dialog';
|
||||||
import OpMenu from '../../../components/dialog/op-menu';
|
import OpMenu from '../../../components/dialog/op-menu';
|
||||||
import MainPanelTopbar from '../main-panel-topbar';
|
|
||||||
import Nav from './user-nav';
|
|
||||||
|
|
||||||
const { enableSysAdminViewRepo } = window.sysadmin.pageOptions;
|
const { enableSysAdminViewRepo } = window.sysadmin.pageOptions;
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
@@ -303,10 +301,8 @@ class Repos extends Component {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<MainPanelTopbar {...this.props} />
|
|
||||||
<div className="main-panel-center flex-row">
|
<div className="main-panel-center flex-row">
|
||||||
<div className="cur-view-container">
|
<div className="cur-view-container">
|
||||||
<Nav currentItem="owned-repos" email={this.props.email} userName={this.state.userInfo.name} />
|
|
||||||
<div className="cur-view-content">
|
<div className="cur-view-content">
|
||||||
<Content
|
<Content
|
||||||
loading={this.state.loading}
|
loading={this.state.loading}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Link } from '@gatsbyjs/reach-router';
|
import { Link } from '@gatsbyjs/reach-router';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
@@ -9,9 +9,7 @@ import { systemAdminAPI } from '../../../utils/system-admin-api';
|
|||||||
import { isPro, siteRoot, gettext } from '../../../utils/constants';
|
import { isPro, siteRoot, gettext } from '../../../utils/constants';
|
||||||
import EmptyTip from '../../../components/empty-tip';
|
import EmptyTip from '../../../components/empty-tip';
|
||||||
import Loading from '../../../components/loading';
|
import Loading from '../../../components/loading';
|
||||||
import MainPanelTopbar from '../main-panel-topbar';
|
|
||||||
import UserLink from '../user-link';
|
import UserLink from '../user-link';
|
||||||
import Nav from './user-nav';
|
|
||||||
|
|
||||||
const { enableSysAdminViewRepo } = window.sysadmin.pageOptions;
|
const { enableSysAdminViewRepo } = window.sysadmin.pageOptions;
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
@@ -166,11 +164,9 @@ class Repos extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<>
|
||||||
<MainPanelTopbar {...this.props} />
|
|
||||||
<div className="main-panel-center flex-row">
|
<div className="main-panel-center flex-row">
|
||||||
<div className="cur-view-container">
|
<div className="cur-view-container">
|
||||||
<Nav currentItem="shared-repos" email={this.props.email} userName={this.state.userInfo.name} />
|
|
||||||
<div className="cur-view-content">
|
<div className="cur-view-content">
|
||||||
<Content
|
<Content
|
||||||
loading={this.state.loading}
|
loading={this.state.loading}
|
||||||
@@ -180,7 +176,7 @@ class Repos extends Component {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -29,11 +29,17 @@ class Nav extends React.Component {
|
|||||||
{ name: 'admin', urlPart: 'users/admins', text: gettext('Admin') }
|
{ name: 'admin', urlPart: 'users/admins', text: gettext('Admin') }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.sortOptions = [
|
this.sortOptions = [
|
||||||
{ value: 'quota_usage-asc', text: gettext('Ascending by space used') },
|
{ value: 'quota_usage-asc', text: gettext('Ascending by space used') },
|
||||||
{ value: 'quota_usage-desc', text: gettext('Descending by space used') }
|
{ value: 'quota_usage-desc', text: gettext('Descending by space used') }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
this.itemRefs = [];
|
||||||
|
this.itemWidths = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.measureItems();
|
||||||
}
|
}
|
||||||
|
|
||||||
onSelectSortOption = (item) => {
|
onSelectSortOption = (item) => {
|
||||||
@@ -41,15 +47,33 @@ class Nav extends React.Component {
|
|||||||
this.props.sortItems(sortBy, sortOrder);
|
this.props.sortItems(sortBy, sortOrder);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
measureItems = () => {
|
||||||
|
this.itemWidths = this.itemRefs.map(ref => ref?.offsetWidth || 77);
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { currentItem, sortBy, sortOrder } = this.props;
|
const { currentItem, sortBy, sortOrder } = this.props;
|
||||||
const showSortIcon = currentItem == 'database' || currentItem == 'ldap-imported';
|
const showSortIcon = currentItem == 'database' || currentItem == 'ldap-imported';
|
||||||
|
const activeIndex = this.navItems.findIndex(item => item.name === currentItem) || 0;
|
||||||
|
const indicatorWidth = this.itemWidths[activeIndex] || 85;
|
||||||
|
const indicatorOffset = this.itemWidths.slice(0, activeIndex).reduce((a, b) => a + b, 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="cur-view-path tab-nav-container">
|
<div className="cur-view-path tab-nav-container">
|
||||||
<ul className="nav">
|
<ul
|
||||||
|
className="nav nav-indicator-container position-relative"
|
||||||
|
style={{
|
||||||
|
'--indicator-width': `${indicatorWidth}px`,
|
||||||
|
'--indicator-offset': `${indicatorOffset}px`
|
||||||
|
}}
|
||||||
|
>
|
||||||
{this.navItems.map((item, index) => {
|
{this.navItems.map((item, index) => {
|
||||||
return (
|
return (
|
||||||
<li className="nav-item" key={index}>
|
<li
|
||||||
|
className="nav-item"
|
||||||
|
key={index}
|
||||||
|
ref={el => this.itemRefs[index] = el}
|
||||||
|
>
|
||||||
<Link to={`${siteRoot}sys/${item.urlPart}/`} className={`nav-link${currentItem == item.name ? ' active' : ''}`}>{item.text}</Link>
|
<Link to={`${siteRoot}sys/${item.urlPart}/`} className={`nav-link${currentItem == item.name ? ' active' : ''}`}>{item.text}</Link>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
@@ -1,10 +1,9 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
import { navigate } from '@gatsbyjs/reach-router';
|
import { navigate } from '@gatsbyjs/reach-router';
|
||||||
import { Button } from 'reactstrap';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Utils } from '../../../utils/utils';
|
import { Utils } from '../../../utils/utils';
|
||||||
import { systemAdminAPI } from '../../../utils/system-admin-api';
|
import { systemAdminAPI } from '../../../utils/system-admin-api';
|
||||||
import { isPro, gettext, siteRoot } from '../../../utils/constants';
|
import { isPro, gettext } from '../../../utils/constants';
|
||||||
import toaster from '../../../components/toast';
|
import toaster from '../../../components/toast';
|
||||||
import SysAdminUserSetQuotaDialog from '../../../components/dialog/sysadmin-dialog/set-quota';
|
import SysAdminUserSetQuotaDialog from '../../../components/dialog/sysadmin-dialog/set-quota';
|
||||||
import SysAdminImportUserDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-import-user-dialog';
|
import SysAdminImportUserDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-import-user-dialog';
|
||||||
@@ -13,9 +12,6 @@ import SysAdminBatchAddAdminDialog from '../../../components/dialog/sysadmin-dia
|
|||||||
import CommonOperationConfirmationDialog from '../../../components/dialog/common-operation-confirmation-dialog';
|
import CommonOperationConfirmationDialog from '../../../components/dialog/common-operation-confirmation-dialog';
|
||||||
import SysAdminUser from '../../../models/sysadmin-user';
|
import SysAdminUser from '../../../models/sysadmin-user';
|
||||||
import SysAdminAdminUser from '../../../models/sysadmin-admin-user';
|
import SysAdminAdminUser from '../../../models/sysadmin-admin-user';
|
||||||
import MainPanelTopbar from '../main-panel-topbar';
|
|
||||||
import Search from '../search';
|
|
||||||
import UsersNav from './users-nav';
|
|
||||||
import UsersFilterBar from './users-filter-bar';
|
import UsersFilterBar from './users-filter-bar';
|
||||||
import Content from './users-content';
|
import Content from './users-content';
|
||||||
|
|
||||||
@@ -35,60 +31,58 @@ class Users extends Component {
|
|||||||
errorMsg: '',
|
errorMsg: '',
|
||||||
userList: [],
|
userList: [],
|
||||||
hasNextPage: false,
|
hasNextPage: false,
|
||||||
currentPage: 1,
|
|
||||||
perPage: 100,
|
|
||||||
hasUserSelected: false,
|
|
||||||
selectedUserList: [],
|
selectedUserList: [],
|
||||||
isAllUsersSelected: false,
|
isAllUsersSelected: false,
|
||||||
isImportUserDialogOpen: false,
|
|
||||||
isAddUserDialogOpen: false,
|
|
||||||
isBatchSetQuotaDialogOpen: false,
|
|
||||||
isBatchDeleteUserDialogOpen: false,
|
|
||||||
isBatchAddAdminDialogOpen: false,
|
|
||||||
is_active: '',
|
is_active: '',
|
||||||
role: ''
|
role: ''
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (this.props.isAdmin) { // 'Admin' page
|
if (this.props.isAdmin) {
|
||||||
this.getUserList(); // no pagination
|
this.getUserList();
|
||||||
} else {
|
} else {
|
||||||
let urlParams = (new URL(window.location)).searchParams;
|
this.initUserListFromURL();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const { isAdmin, sortBy, sortOrder, currentPage, perPage } = this.props;
|
||||||
|
if (prevProps.isAdmin !== isAdmin) {
|
||||||
|
this.setState({ loading: true }, () => {
|
||||||
|
if (isAdmin) {
|
||||||
|
this.getUserList();
|
||||||
|
} else {
|
||||||
|
this.initUserListFromURL();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevProps.sortBy !== sortBy ||
|
||||||
|
prevProps.sortOrder !== sortOrder ||
|
||||||
|
prevProps.currentPage !== currentPage) {
|
||||||
|
this.updateURLSearchParams({
|
||||||
|
'page': currentPage,
|
||||||
|
'per_page': perPage,
|
||||||
|
'order_by': sortBy,
|
||||||
|
'direction': sortOrder
|
||||||
|
});
|
||||||
|
this.getUsersListByPage(currentPage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initUserListFromURL = () => {
|
||||||
|
const urlParams = new URL(window.location).searchParams;
|
||||||
const {
|
const {
|
||||||
currentPage, perPage,
|
|
||||||
sortBy = '',
|
|
||||||
sortOrder = 'asc',
|
|
||||||
is_active,
|
is_active,
|
||||||
role
|
role
|
||||||
} = this.state;
|
} = this.state;
|
||||||
this.setState({
|
this.setState({
|
||||||
perPage: parseInt(urlParams.get('per_page') || perPage),
|
|
||||||
currentPage: parseInt(urlParams.get('page') || currentPage),
|
|
||||||
sortBy: urlParams.get('order_by') || sortBy,
|
|
||||||
sortOrder: urlParams.get('direction') || sortOrder,
|
|
||||||
is_active: urlParams.get('is_active') || is_active,
|
is_active: urlParams.get('is_active') || is_active,
|
||||||
role: urlParams.get('role') || role
|
role: urlParams.get('role') || role
|
||||||
}, () => {
|
}, () => {
|
||||||
this.getUsersListByPage(this.state.currentPage);
|
this.getUsersListByPage(this.props.currentPage);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleImportUserDialog = () => {
|
|
||||||
this.setState({ isImportUserDialogOpen: !this.state.isImportUserDialogOpen });
|
|
||||||
};
|
|
||||||
|
|
||||||
toggleAddUserDialog = () => {
|
|
||||||
this.setState({ isAddUserDialogOpen: !this.state.isAddUserDialogOpen });
|
|
||||||
};
|
|
||||||
|
|
||||||
toggleBatchSetQuotaDialog = () => {
|
|
||||||
this.setState({ isBatchSetQuotaDialogOpen: !this.state.isBatchSetQuotaDialogOpen });
|
|
||||||
};
|
|
||||||
|
|
||||||
toggleBatchDeleteUserDialog = () => {
|
|
||||||
this.setState({ isBatchDeleteUserDialogOpen: !this.state.isBatchDeleteUserDialogOpen });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onUserSelected = (item) => {
|
onUserSelected = (item) => {
|
||||||
@@ -116,9 +110,9 @@ class Users extends Component {
|
|||||||
// finally update state
|
// finally update state
|
||||||
this.setState({
|
this.setState({
|
||||||
userList: users,
|
userList: users,
|
||||||
hasUserSelected: hasUserSelected,
|
|
||||||
selectedUserList: selectedUserList,
|
selectedUserList: selectedUserList,
|
||||||
});
|
});
|
||||||
|
this.props.onHasUserSelected(hasUserSelected);
|
||||||
};
|
};
|
||||||
|
|
||||||
toggleSelectAllUsers = () => {
|
toggleSelectAllUsers = () => {
|
||||||
@@ -130,10 +124,10 @@ class Users extends Component {
|
|||||||
});
|
});
|
||||||
this.setState({
|
this.setState({
|
||||||
userList: users,
|
userList: users,
|
||||||
hasUserSelected: false,
|
|
||||||
isAllUsersSelected: false,
|
isAllUsersSelected: false,
|
||||||
selectedUserList: [],
|
selectedUserList: [],
|
||||||
});
|
});
|
||||||
|
this.props.onHasUserSelected(false);
|
||||||
} else {
|
} else {
|
||||||
// if previous state is not allSelected, toggle to selectAll
|
// if previous state is not allSelected, toggle to selectAll
|
||||||
let users = this.state.userList.map(user => {
|
let users = this.state.userList.map(user => {
|
||||||
@@ -142,10 +136,10 @@ class Users extends Component {
|
|||||||
});
|
});
|
||||||
this.setState({
|
this.setState({
|
||||||
userList: users,
|
userList: users,
|
||||||
hasUserSelected: true,
|
|
||||||
isAllUsersSelected: true,
|
isAllUsersSelected: true,
|
||||||
selectedUserList: users
|
selectedUserList: users
|
||||||
});
|
});
|
||||||
|
this.props.onHasUserSelected(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -168,8 +162,8 @@ class Users extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
getUsersListByPage = (page) => {
|
getUsersListByPage = (page) => {
|
||||||
const { perPage, sortBy, sortOrder, is_active, role } = this.state;
|
const { is_active, role } = this.state;
|
||||||
const { isLDAPImported } = this.props;
|
const { perPage, sortBy, sortOrder, isLDAPImported } = this.props;
|
||||||
systemAdminAPI.sysAdminListUsers(page, perPage, isLDAPImported, sortBy, sortOrder, is_active, role).then(res => {
|
systemAdminAPI.sysAdminListUsers(page, perPage, isLDAPImported, sortBy, sortOrder, is_active, role).then(res => {
|
||||||
let users = res.data.data.map(user => {return new SysAdminUser(user);});
|
let users = res.data.data.map(user => {return new SysAdminUser(user);});
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -202,7 +196,7 @@ class Users extends Component {
|
|||||||
is_active: is_active,
|
is_active: is_active,
|
||||||
currentPage: 1
|
currentPage: 1
|
||||||
}, () => {
|
}, () => {
|
||||||
const { currentPage, perPage } = this.state;
|
const { currentPage, perPage } = this.props;
|
||||||
this.updateURLSearchParams({
|
this.updateURLSearchParams({
|
||||||
'page': currentPage,
|
'page': currentPage,
|
||||||
'per_page': perPage,
|
'per_page': perPage,
|
||||||
@@ -217,7 +211,7 @@ class Users extends Component {
|
|||||||
role: role,
|
role: role,
|
||||||
currentPage: 1
|
currentPage: 1
|
||||||
}, () => {
|
}, () => {
|
||||||
const { currentPage, perPage } = this.state;
|
const { currentPage, perPage } = this.props;
|
||||||
this.updateURLSearchParams({
|
this.updateURLSearchParams({
|
||||||
'page': currentPage,
|
'page': currentPage,
|
||||||
'per_page': perPage,
|
'per_page': perPage,
|
||||||
@@ -227,23 +221,6 @@ class Users extends Component {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
sortByQuotaUsage = (sortBy, sortOrder) => {
|
|
||||||
this.setState({
|
|
||||||
sortBy: sortBy,
|
|
||||||
sortOrder: sortOrder,
|
|
||||||
currentPage: 1
|
|
||||||
}, () => {
|
|
||||||
const { currentPage, perPage, sortBy, sortOrder } = this.state;
|
|
||||||
this.updateURLSearchParams({
|
|
||||||
'page': currentPage,
|
|
||||||
'per_page': perPage,
|
|
||||||
'order_by': sortBy,
|
|
||||||
'direction': sortOrder
|
|
||||||
});
|
|
||||||
this.getUsersListByPage(currentPage);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
deleteUser = (email, username) => {
|
deleteUser = (email, username) => {
|
||||||
systemAdminAPI.sysAdminDeleteUser(email).then(res => {
|
systemAdminAPI.sysAdminDeleteUser(email).then(res => {
|
||||||
let newUserList = this.state.userList.filter(item => {
|
let newUserList = this.state.userList.filter(item => {
|
||||||
@@ -292,9 +269,9 @@ class Users extends Component {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
this.setState({
|
this.setState({
|
||||||
userList: newUserList,
|
userList: newUserList
|
||||||
hasUserSelected: emails.length != res.data.success.length
|
|
||||||
});
|
});
|
||||||
|
this.props.onHasUserSelected(emails.length != res.data.success.length);
|
||||||
const length = res.data.success.length;
|
const length = res.data.success.length;
|
||||||
const msg = length == 1 ?
|
const msg = length == 1 ?
|
||||||
gettext('Successfully deleted 1 user.') :
|
gettext('Successfully deleted 1 user.') :
|
||||||
@@ -409,27 +386,6 @@ class Users extends Component {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
getOperationsForAll = () => {
|
|
||||||
const { isAdmin, isLDAPImported } = this.props;
|
|
||||||
|
|
||||||
if (isAdmin) {
|
|
||||||
return <Button className="btn btn-secondary operation-item" onClick={this.toggleBatchAddAdminDialog}>{gettext('Add Admin')}</Button>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLDAPImported) {
|
|
||||||
return <a className="btn btn-secondary operation-item" href={`${siteRoot}sys/useradmin/export-excel/`}>{gettext('Export Excel')}</a>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 'database'
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<Button className="btn btn-secondary operation-item" onClick={this.toggleImportUserDialog}>{gettext('Import Users')}</Button>
|
|
||||||
<Button className="btn btn-secondary operation-item" onClick={this.toggleAddUserDialog}>{gettext('Add User')}</Button>
|
|
||||||
<a className="btn btn-secondary operation-item" href={`${siteRoot}sys/useradmin/export-excel/`}>{gettext('Export Excel')}</a>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
getCurrentNavItem = () => {
|
getCurrentNavItem = () => {
|
||||||
const { isAdmin, isLDAPImported } = this.props;
|
const { isAdmin, isLDAPImported } = this.props;
|
||||||
let item = 'database';
|
let item = 'database';
|
||||||
@@ -441,10 +397,6 @@ class Users extends Component {
|
|||||||
return item;
|
return item;
|
||||||
};
|
};
|
||||||
|
|
||||||
toggleBatchAddAdminDialog = () => {
|
|
||||||
this.setState({ isBatchAddAdminDialogOpen: !this.state.isBatchAddAdminDialogOpen });
|
|
||||||
};
|
|
||||||
|
|
||||||
addAdminInBatch = (emails) => {
|
addAdminInBatch = (emails) => {
|
||||||
systemAdminAPI.sysAdminAddAdminInBatch(emails).then(res => {
|
systemAdminAPI.sysAdminAddAdminInBatch(emails).then(res => {
|
||||||
let users = res.data.success.map(user => {
|
let users = res.data.success.map(user => {
|
||||||
@@ -463,53 +415,21 @@ class Users extends Component {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
getSearch = () => {
|
|
||||||
if (this.props.isAdmin) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// offer 'Search' for 'DB' & 'LDAPImported' users
|
|
||||||
return <Search
|
|
||||||
placeholder={gettext('Search users')}
|
|
||||||
submit={this.searchItems}
|
|
||||||
/>;
|
|
||||||
};
|
|
||||||
|
|
||||||
searchItems = (keyword) => {
|
|
||||||
navigate(`${siteRoot}sys/search-users/?query=${encodeURIComponent(keyword)}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { isAdmin, isLDAPImported } = this.props;
|
|
||||||
const {
|
const {
|
||||||
is_active,
|
curTab,
|
||||||
role,
|
isAdmin,
|
||||||
hasUserSelected,
|
isLDAPImported,
|
||||||
isImportUserDialogOpen,
|
isImportUserDialogOpen,
|
||||||
isAddUserDialogOpen,
|
isAddUserDialogOpen,
|
||||||
isBatchDeleteUserDialogOpen,
|
isBatchDeleteUserDialogOpen,
|
||||||
isBatchSetQuotaDialogOpen,
|
isBatchSetQuotaDialogOpen,
|
||||||
isBatchAddAdminDialogOpen
|
isBatchAddAdminDialogOpen
|
||||||
} = this.state;
|
} = this.props;
|
||||||
const curTab = this.getCurrentNavItem();
|
const { is_active, role } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<>
|
||||||
<MainPanelTopbar search={this.getSearch()} {...this.props}>
|
|
||||||
{hasUserSelected ?
|
|
||||||
<Fragment>
|
|
||||||
<Button className="btn btn-secondary operation-item" onClick={this.toggleBatchSetQuotaDialog}>{gettext('Set Quota')}</Button>
|
|
||||||
<Button className="btn btn-secondary operation-item" onClick={this.toggleBatchDeleteUserDialog}>{gettext('Delete Users')}</Button>
|
|
||||||
</Fragment>
|
|
||||||
: this.getOperationsForAll()
|
|
||||||
}
|
|
||||||
</MainPanelTopbar>
|
|
||||||
<div className="main-panel-center flex-row">
|
|
||||||
<div className="cur-view-container">
|
|
||||||
<UsersNav
|
|
||||||
currentItem={curTab}
|
|
||||||
sortBy={this.state.sortBy}
|
|
||||||
sortOrder={this.state.sortOrder}
|
|
||||||
sortItems={this.sortByQuotaUsage}
|
|
||||||
/>
|
|
||||||
<div className="cur-view-content">
|
<div className="cur-view-content">
|
||||||
{curTab == 'database' &&
|
{curTab == 'database' &&
|
||||||
<UsersFilterBar
|
<UsersFilterBar
|
||||||
@@ -525,9 +445,12 @@ class Users extends Component {
|
|||||||
loading={this.state.loading}
|
loading={this.state.loading}
|
||||||
errorMsg={this.state.errorMsg}
|
errorMsg={this.state.errorMsg}
|
||||||
items={this.state.userList}
|
items={this.state.userList}
|
||||||
|
sortBy={this.props.sortBy}
|
||||||
|
sortOrder={this.props.sortOrder}
|
||||||
|
sortByQuotaUsage={this.sortByQuotaUsage}
|
||||||
currentPage={this.state.currentPage}
|
currentPage={this.state.currentPage}
|
||||||
hasNextPage={this.state.hasNextPage}
|
hasNextPage={this.state.hasNextPage}
|
||||||
curPerPage={this.state.perPage}
|
curPerPage={this.props.perPage}
|
||||||
resetPerPage={this.resetPerPage}
|
resetPerPage={this.resetPerPage}
|
||||||
getListByPage={this.getUsersListByPage}
|
getListByPage={this.getUsersListByPage}
|
||||||
updateUser={this.updateUser}
|
updateUser={this.updateUser}
|
||||||
@@ -539,11 +462,9 @@ class Users extends Component {
|
|||||||
toggleSelectAllUsers={this.toggleSelectAllUsers}
|
toggleSelectAllUsers={this.toggleSelectAllUsers}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{isImportUserDialogOpen &&
|
{isImportUserDialogOpen &&
|
||||||
<SysAdminImportUserDialog
|
<SysAdminImportUserDialog
|
||||||
toggle={this.toggleImportUserDialog}
|
toggle={this.props.toggleImportUserDialog}
|
||||||
importUserInBatch={this.importUserInBatch}
|
importUserInBatch={this.importUserInBatch}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -553,12 +474,12 @@ class Users extends Component {
|
|||||||
showRole={isPro}
|
showRole={isPro}
|
||||||
availableRoles={availableRoles}
|
availableRoles={availableRoles}
|
||||||
addUser={this.addUser}
|
addUser={this.addUser}
|
||||||
toggleDialog={this.toggleAddUserDialog}
|
toggleDialog={this.props.toggleAddUserDialog}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
{isBatchSetQuotaDialogOpen &&
|
{isBatchSetQuotaDialogOpen &&
|
||||||
<SysAdminUserSetQuotaDialog
|
<SysAdminUserSetQuotaDialog
|
||||||
toggle={this.toggleBatchSetQuotaDialog}
|
toggle={this.props.toggleBatchSetQuotaDialog}
|
||||||
updateQuota={this.setUserQuotaInBatch}
|
updateQuota={this.setUserQuotaInBatch}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -568,7 +489,7 @@ class Users extends Component {
|
|||||||
message={gettext('Are you sure you want to delete the selected user(s) ?')}
|
message={gettext('Are you sure you want to delete the selected user(s) ?')}
|
||||||
executeOperation={this.deleteUserInBatch}
|
executeOperation={this.deleteUserInBatch}
|
||||||
confirmBtnText={gettext('Delete')}
|
confirmBtnText={gettext('Delete')}
|
||||||
toggleDialog={this.toggleBatchDeleteUserDialog}
|
toggleDialog={this.props.toggleBatchDeleteUserDialog}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
{isBatchAddAdminDialogOpen &&
|
{isBatchAddAdminDialogOpen &&
|
||||||
@@ -577,7 +498,7 @@ class Users extends Component {
|
|||||||
toggle={this.toggleBatchAddAdminDialog}
|
toggle={this.toggleBatchAddAdminDialog}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
</Fragment>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { systemAdminAPI } from '../../../utils/system-admin-api';
|
import { systemAdminAPI } from '../../../utils/system-admin-api';
|
||||||
import { gettext } from '../../../utils/constants';
|
import { gettext } from '../../../utils/constants';
|
||||||
@@ -7,8 +7,6 @@ import toaster from '../../../components/toast';
|
|||||||
import OpMenu from '../../../components/dialog/op-menu';
|
import OpMenu from '../../../components/dialog/op-menu';
|
||||||
import Loading from '../../../components/loading';
|
import Loading from '../../../components/loading';
|
||||||
import Paginator from '../../../components/paginator';
|
import Paginator from '../../../components/paginator';
|
||||||
import MainPanelTopbar from '../main-panel-topbar';
|
|
||||||
import Nav from './nav';
|
|
||||||
|
|
||||||
const virusFileItemPropTypes = {
|
const virusFileItemPropTypes = {
|
||||||
virusFile: PropTypes.object.isRequired,
|
virusFile: PropTypes.object.isRequired,
|
||||||
@@ -162,7 +160,7 @@ class Content extends Component {
|
|||||||
return <p className="error text-center mt-4">{errorMsg}</p>;
|
return <p className="error text-center mt-4">{errorMsg}</p>;
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -198,7 +196,7 @@ class Content extends Component {
|
|||||||
resetPerPage={this.props.resetPerPage}
|
resetPerPage={this.props.resetPerPage}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
</Fragment>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -292,11 +290,8 @@ class AllVirusFiles extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
|
||||||
<MainPanelTopbar {...this.props} />
|
|
||||||
<div className="main-panel-center">
|
<div className="main-panel-center">
|
||||||
<div className="cur-view-container">
|
<div className="cur-view-container">
|
||||||
<Nav currentItem="all" />
|
|
||||||
<div className="cur-view-content">
|
<div className="cur-view-content">
|
||||||
<Content
|
<Content
|
||||||
loading={this.state.loading}
|
loading={this.state.loading}
|
||||||
@@ -312,7 +307,6 @@ class AllVirusFiles extends Component {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
42
frontend/src/pages/sys-admin/virus-scan/index.js
Normal file
42
frontend/src/pages/sys-admin/virus-scan/index.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import MainPanelTopbar from '../main-panel-topbar';
|
||||||
|
import Nav from './nav';
|
||||||
|
import { useLocation } from '@gatsbyjs/reach-router';
|
||||||
|
import { Button } from 'reactstrap';
|
||||||
|
import { gettext } from '../../../utils/constants';
|
||||||
|
import { eventBus } from '../../../components/common/event-bus';
|
||||||
|
import { EVENT_BUS_TYPE } from '../../../components/common/event-bus-type';
|
||||||
|
|
||||||
|
const VirusScan = ({ children, ...commonProps }) => {
|
||||||
|
const location = useLocation();
|
||||||
|
const path = location.pathname.split('/').filter(Boolean).pop();
|
||||||
|
|
||||||
|
|
||||||
|
const deleteSelectedItems = () => {
|
||||||
|
const op = 'delete-virus';
|
||||||
|
eventBus.dispatch(EVENT_BUS_TYPE.HANDLE_SELECTED_OPERATIONS, op);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ignoreSelectedItems = () => {
|
||||||
|
const op = 'ignore-virus';
|
||||||
|
eventBus.dispatch(EVENT_BUS_TYPE.HANDLE_SELECTED_OPERATIONS, op);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{path === 'unhandled' ? (
|
||||||
|
<MainPanelTopbar {...commonProps}>
|
||||||
|
<>
|
||||||
|
<Button onClick={deleteSelectedItems} className="operation-item">{gettext('Delete')}</Button>
|
||||||
|
<Button onClick={ignoreSelectedItems} className="operation-item">{gettext('Ignore')}</Button>
|
||||||
|
</>
|
||||||
|
</MainPanelTopbar>
|
||||||
|
) : (
|
||||||
|
<MainPanelTopbar {...commonProps} />
|
||||||
|
)}
|
||||||
|
<Nav currentItem={path} />
|
||||||
|
<div className="h-100 d-flex overflow-auto">{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VirusScan;
|
@@ -15,16 +15,35 @@ class Nav extends React.Component {
|
|||||||
{ name: 'all', urlPart: 'all', text: gettext('All') },
|
{ name: 'all', urlPart: 'all', text: gettext('All') },
|
||||||
{ name: 'unhandled', urlPart: 'unhandled', text: gettext('Unhandled') }
|
{ name: 'unhandled', urlPart: 'unhandled', text: gettext('Unhandled') }
|
||||||
];
|
];
|
||||||
|
this.itemRefs = [];
|
||||||
|
this.itemWidths = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.itemWidths = this.itemRefs.map(ref => ref?.offsetWidth) || 59;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { currentItem } = this.props;
|
const { currentItem } = this.props;
|
||||||
|
const activeIndex = this.navItems.findIndex(item => item.name === currentItem) || 0;
|
||||||
|
const indicatorWidth = this.itemWidths[activeIndex] || 59;
|
||||||
|
const indicatorOffset = this.itemWidths.slice(0, activeIndex).reduce((prev, cur) => prev + cur, 0);
|
||||||
return (
|
return (
|
||||||
<div className="cur-view-path tab-nav-container">
|
<div className="cur-view-path tab-nav-container">
|
||||||
<ul className="nav">
|
<ul
|
||||||
|
className="nav nav-indicator-container position-relative"
|
||||||
|
style={{
|
||||||
|
'--indicator-width': `${indicatorWidth}px`,
|
||||||
|
'--indicator-offset': `${indicatorOffset}px`
|
||||||
|
}}
|
||||||
|
>
|
||||||
{this.navItems.map((item, index) => {
|
{this.navItems.map((item, index) => {
|
||||||
return (
|
return (
|
||||||
<li className="nav-item" key={index}>
|
<li
|
||||||
|
className="nav-item"
|
||||||
|
key={index}
|
||||||
|
ref={el => this.itemRefs[index] = el}
|
||||||
|
>
|
||||||
<Link to={`${siteRoot}sys/virus-files/${item.urlPart}/`} className={`nav-link${currentItem == item.name ? ' active' : ''}`}>{item.text}</Link>
|
<Link to={`${siteRoot}sys/virus-files/${item.urlPart}/`} className={`nav-link${currentItem == item.name ? ' active' : ''}`}>{item.text}</Link>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Button } from 'reactstrap';
|
|
||||||
import { systemAdminAPI } from '../../../utils/system-admin-api';
|
import { systemAdminAPI } from '../../../utils/system-admin-api';
|
||||||
import { gettext } from '../../../utils/constants';
|
import { gettext } from '../../../utils/constants';
|
||||||
import { Utils } from '../../../utils/utils';
|
import { Utils } from '../../../utils/utils';
|
||||||
@@ -8,8 +7,8 @@ import toaster from '../../../components/toast';
|
|||||||
import OpMenu from '../../../components/dialog/op-menu';
|
import OpMenu from '../../../components/dialog/op-menu';
|
||||||
import Loading from '../../../components/loading';
|
import Loading from '../../../components/loading';
|
||||||
import Paginator from '../../../components/paginator';
|
import Paginator from '../../../components/paginator';
|
||||||
import MainPanelTopbar from '../main-panel-topbar';
|
import { eventBus } from '../../../components/common/event-bus';
|
||||||
import Nav from './nav';
|
import { EVENT_BUS_TYPE } from '../../../components/common/event-bus-type';
|
||||||
|
|
||||||
const virusFileItemPropTypes = {
|
const virusFileItemPropTypes = {
|
||||||
resetPerPage: PropTypes.func,
|
resetPerPage: PropTypes.func,
|
||||||
@@ -177,7 +176,7 @@ class Content extends Component {
|
|||||||
return <p className="error text-center mt-4">{errorMsg}</p>;
|
return <p className="error text-center mt-4">{errorMsg}</p>;
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -217,7 +216,7 @@ class Content extends Component {
|
|||||||
resetPerPage={this.props.resetPerPage}
|
resetPerPage={this.props.resetPerPage}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
</Fragment>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -255,6 +254,20 @@ class UnhandledVirusFiles extends Component {
|
|||||||
}, () => {
|
}, () => {
|
||||||
this.getListByPage(this.state.currentPage);
|
this.getListByPage(this.state.currentPage);
|
||||||
});
|
});
|
||||||
|
this.unsubscribeHandleSelectedOp = eventBus.subscribe(EVENT_BUS_TYPE.HANDLE_SELECTED_OPERATIONS, (op) => {
|
||||||
|
switch (op) {
|
||||||
|
case 'delete-virus':
|
||||||
|
this.deleteSelectedItems();
|
||||||
|
break;
|
||||||
|
case 'ignore-virus':
|
||||||
|
this.ignoreSelectedItems();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.unsubscribeHandleSelectedOp();
|
||||||
}
|
}
|
||||||
|
|
||||||
getListByPage = (page) => {
|
getListByPage = (page) => {
|
||||||
@@ -383,31 +396,10 @@ class UnhandledVirusFiles extends Component {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
deleteSelectedItems = () => {
|
|
||||||
const op = 'delete-virus';
|
|
||||||
this.handleSelectedItems(op);
|
|
||||||
};
|
|
||||||
|
|
||||||
ignoreSelectedItems = () => {
|
|
||||||
const op = 'ignore-virus';
|
|
||||||
this.handleSelectedItems(op);
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
|
||||||
{this.state.virusFiles.some(item => item.isSelected) ? (
|
|
||||||
<MainPanelTopbar {...this.props}>
|
|
||||||
<Fragment>
|
|
||||||
<Button onClick={this.deleteSelectedItems} className="operation-item">{gettext('Delete')}</Button>
|
|
||||||
<Button onClick={this.ignoreSelectedItems} className="operation-item">{gettext('Ignore')}</Button>
|
|
||||||
</Fragment>
|
|
||||||
</MainPanelTopbar>
|
|
||||||
) : <MainPanelTopbar {...this.props} />
|
|
||||||
}
|
|
||||||
<div className="main-panel-center">
|
<div className="main-panel-center">
|
||||||
<div className="cur-view-container">
|
<div className="cur-view-container">
|
||||||
<Nav currentItem="unhandled" />
|
|
||||||
<div className="cur-view-content">
|
<div className="cur-view-content">
|
||||||
<Content
|
<Content
|
||||||
loading={this.state.loading}
|
loading={this.state.loading}
|
||||||
@@ -426,7 +418,6 @@ class UnhandledVirusFiles extends Component {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user