1
0
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:
Aries
2025-07-07 15:27:21 +08:00
committed by GitHub
parent 2a0ab1be57
commit 7d04125d73
60 changed files with 1602 additions and 1204 deletions

View File

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

View File

@@ -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',
}; };

View File

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

View File

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

View File

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

View File

@@ -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} />
</> </>
); );
}; };

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -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,26 +161,22 @@ class AdminLoginLogs extends Component {
render() { render() {
let { logList, currentPage, perPage, hasNextPage } = this.state; let { logList, currentPage, perPage, hasNextPage } = this.state;
return ( return (
<Fragment> <div className="main-panel-center flex-row">
<MainPanelTopbar {...this.props} /> <div className="cur-view-container">
<div className="main-panel-center flex-row"> <div className="cur-view-content">
<div className="cur-view-container"> <Content
<LogsNav currentItem="adminLoginLogs" /> loading={this.state.loading}
<div className="cur-view-content"> errorMsg={this.state.errorMsg}
<Content items={logList}
loading={this.state.loading} currentPage={currentPage}
errorMsg={this.state.errorMsg} perPage={perPage}
items={logList} hasNextPage={hasNextPage}
currentPage={currentPage} getLogsByPage={this.getLogsByPage}
perPage={perPage} resetPerPage={this.resetPerPage}
hasNextPage={hasNextPage} />
getLogsByPage={this.getLogsByPage}
resetPerPage={this.resetPerPage}
/>
</div>
</div> </div>
</div> </div>
</Fragment> </div>
); );
} }
} }

View File

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

View File

@@ -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,26 +280,22 @@ class AdminOperationLogs extends Component {
render() { render() {
let { logList, currentPage, perPage, hasNextPage } = this.state; let { logList, currentPage, perPage, hasNextPage } = this.state;
return ( return (
<Fragment> <div className="main-panel-center flex-row">
<MainPanelTopbar {...this.props} /> <div className="cur-view-container">
<div className="main-panel-center flex-row"> <div className="cur-view-content">
<div className="cur-view-container"> <Content
<LogsNav currentItem="adminOperationLogs" /> loading={this.state.loading}
<div className="cur-view-content"> errorMsg={this.state.errorMsg}
<Content items={logList}
loading={this.state.loading} currentPage={currentPage}
errorMsg={this.state.errorMsg} perPage={perPage}
items={logList} hasNextPage={hasNextPage}
currentPage={currentPage} getLogsByPage={this.getLogsByPage}
perPage={perPage} resetPerPage={this.resetPerPage}
hasNextPage={hasNextPage} />
getLogsByPage={this.getLogsByPage}
resetPerPage={this.resetPerPage}
/>
</div>
</div> </div>
</div> </div>
</Fragment> </div>
); );
} }
} }

View File

@@ -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> <div className="main-panel-center flex-row">
<MainPanelTopbar {...this.props} /> <div className="cur-view-container">
<div className="main-panel-center flex-row"> <DevicesByPlatform
<div className="cur-view-container"> devicesPlatform={'desktop'}
<DevicesNav currentItem="desktop" /> />
<DevicesByPlatform
devicesPlatform={'desktop'}
/>
</div>
</div> </div>
</Fragment> </div>
); );
} }
} }

View File

@@ -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,31 +183,21 @@ class DeviceErrors extends Component {
}; };
render() { render() {
return ( return (
<Fragment> <div className="main-panel-center flex-row">
{this.state.isCleanBtnShown ? ( <div className="cur-view-container">
<MainPanelTopbar {...this.props}> <div className="cur-view-content">
<Button className="operation-item" onClick={this.clean}>{gettext('Clean')}</Button> <Content
</MainPanelTopbar> loading={this.state.loading}
) : ( errorMsg={this.state.errorMsg}
<MainPanelTopbar {...this.props} /> items={this.state.deviceErrors}
)} getDeviceErrorsListByPage={this.getDeviceErrorsListByPage}
<div className="main-panel-center flex-row"> curPerPage={this.state.perPage}
<div className="cur-view-container"> resetPerPage={this.resetPerPage}
<DevicesNav currentItem="errors" /> pageInfo={this.state.pageInfo}
<div className="cur-view-content"> />
<Content
loading={this.state.loading}
errorMsg={this.state.errorMsg}
items={this.state.devicesErrors}
getDeviceErrorsListByPage={this.getDeviceErrorsListByPage}
curPerPage={this.state.perPage}
resetPerPage={this.resetPerPage}
pageInfo={this.state.pageInfo}
/>
</div>
</div> </div>
</div> </div>
</Fragment> </div>
); );
} }
} }

View File

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

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

View File

@@ -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> <div className="main-panel-center flex-row">
<MainPanelTopbar {...this.props} /> <div className="cur-view-container">
<div className="main-panel-center flex-row"> <DevicesByPlatform
<div className="cur-view-container"> devicesPlatform={'mobile'}
<DevicesNav currentItem="mobile" /> />
<DevicesByPlatform
devicesPlatform={'mobile'}
/>
</div>
</div> </div>
</Fragment> </div>
); );
} }
} }

View File

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

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

View File

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

View File

@@ -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> </>
); );
} }
} }

View File

@@ -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> </>
); );
} }
} }

View File

@@ -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,58 +319,42 @@ class FileAccessLogs extends Component {
openSelector openSelector
} = this.state; } = this.state;
return ( return (
<Fragment> <div className="main-panel-center flex-row">
<MainPanelTopbar {...this.props}> <div className="cur-view-container">
<Button className="btn btn-secondary operation-item" onClick={this.toggleExportExcelDialog}>{gettext('Export Excel')}</Button> <div className="cur-view-content">
</MainPanelTopbar> <div className="d-flex align-items-center mb-2">
<div className="main-panel-center flex-row"> <LogUserSelector
<div className="cur-view-container"> componentName={gettext('Users')}
<LogsNav currentItem="fileAccessLogs" /> items={availableUsers}
<div className="cur-view-content"> selectedItems={selectedUsers}
<Fragment> onSelect={this.handleUserFilter}
<div className="d-flex align-items-center mb-2"> isOpen={openSelector === 'user'}
<LogUserSelector onToggle={() => this.handleSelectorToggle('user')}
componentName={gettext('Users')} searchUsersFunc={this.searchUsers}
items={availableUsers} />
selectedItems={selectedUsers} <div className="mx-3"></div>
onSelect={this.handleUserFilter} <LogRepoSelector
isOpen={openSelector === 'user'} items={availableRepos}
onToggle={() => this.handleSelectorToggle('user')} selectedItems={selectedRepos}
searchUsersFunc={this.searchUsers} onSelect={this.handleRepoFilter}
/> isOpen={openSelector === 'repo'}
<div className="mx-3"></div> onToggle={() => this.handleSelectorToggle('repo')}
<LogRepoSelector searchReposFunc={this.searchRepos}
items={availableRepos} />
selectedItems={selectedRepos}
onSelect={this.handleRepoFilter}
isOpen={openSelector === 'repo'}
onToggle={() => this.handleSelectorToggle('repo')}
searchReposFunc={this.searchRepos}
/>
</div>
<Content
loading={this.state.loading}
errorMsg={this.state.errorMsg}
items={logList}
currentPage={currentPage}
perPage={perPage}
hasNextPage={hasNextPage}
getLogsByPage={this.getLogsByPage}
resetPerPage={this.resetPerPage}
/>
</Fragment>
</div> </div>
<Content
loading={this.state.loading}
errorMsg={this.state.errorMsg}
items={logList}
currentPage={currentPage}
perPage={perPage}
hasNextPage={hasNextPage}
getLogsByPage={this.getLogsByPage}
resetPerPage={this.resetPerPage}
/>
</div> </div>
</div> </div>
{isExportExcelDialogOpen && </div>
<ModalPortal>
<LogsExportExcelDialog
logType={'fileAccess'}
toggle={this.toggleExportExcelDialog}
/>
</ModalPortal>
}
</Fragment>
); );
} }
} }

View File

@@ -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,68 +420,62 @@ class FIleTransferLogs extends Component {
]; ];
return ( return (
<Fragment> <div className="main-panel-center flex-row">
<MainPanelTopbar {...this.props} /> <div className="cur-view-container">
<div className="main-panel-center flex-row"> <div className="cur-view-content">
<div className="cur-view-container"> <div className="d-flex align-items-center mb-2">
<LogsNav currentItem="fileTransfer" /> <LogUserSelector
<div className="cur-view-content"> componentName={gettext('Transfer From')}
<Fragment> items={availableUsers}
<div className="d-flex align-items-center mb-2"> selectedItems={selectedFromUsers}
<LogUserSelector onSelect={this.handleFromUserFilter}
componentName={gettext('Transfer From')} isOpen={openSelector === 'fromUser'}
items={availableUsers} onToggle={() => this.handleSelectorToggle('fromUser')}
selectedItems={selectedFromUsers} searchUsersFunc={this.searchUsers}
onSelect={this.handleFromUserFilter} searchGroupsFunc={this.searchGroups}
isOpen={openSelector === 'fromUser'} />
onToggle={() => this.handleSelectorToggle('fromUser')} <LogUserSelector
searchUsersFunc={this.searchUsers} componentName={gettext('Transfer To')}
searchGroupsFunc={this.searchGroups} items={availableUsers}
/> selectedItems={selectedToItems}
<LogUserSelector onSelect={this.handleToUserFilter}
componentName={gettext('Transfer To')} isOpen={openSelector === 'toUser'}
items={availableUsers} onToggle={() => this.handleSelectorToggle('toUser')}
selectedItems={selectedToItems} searchUsersFunc={this.searchUsers}
onSelect={this.handleToUserFilter} searchGroupsFunc={this.searchGroups}
isOpen={openSelector === 'toUser'} />
onToggle={() => this.handleSelectorToggle('toUser')} <LogUserSelector
searchUsersFunc={this.searchUsers} componentName={gettext('Operator')}
searchGroupsFunc={this.searchGroups} items={availableUsers}
/> selectedItems={selectedOperators}
<LogUserSelector onSelect={this.handleOperatorFilter}
componentName={gettext('Operator')} isOpen={openSelector === 'operator'}
items={availableUsers} onToggle={() => this.handleSelectorToggle('operator')}
selectedItems={selectedOperators} searchUsersFunc={this.searchUsers}
onSelect={this.handleOperatorFilter} />
isOpen={openSelector === 'operator'} <div className="mx-3"></div>
onToggle={() => this.handleSelectorToggle('operator')} <LogRepoSelector
searchUsersFunc={this.searchUsers} items={availableRepos}
/> selectedItems={selectedRepos}
<div className="mx-3"></div> onSelect={this.handleRepoFilter}
<LogRepoSelector isOpen={openSelector === 'repo'}
items={availableRepos} onToggle={() => this.handleSelectorToggle('repo')}
selectedItems={selectedRepos} searchReposFunc={this.searchRepos}
onSelect={this.handleRepoFilter} />
isOpen={openSelector === 'repo'}
onToggle={() => this.handleSelectorToggle('repo')}
searchReposFunc={this.searchRepos}
/>
</div>
<Content
loading={this.state.loading}
errorMsg={this.state.errorMsg}
items={logList}
currentPage={currentPage}
perPage={perPage}
hasNextPage={hasNextPage}
getLogsByPage={this.getLogsByPage}
resetPerPage={this.resetPerPage}
/>
</Fragment>
</div> </div>
<Content
loading={this.state.loading}
errorMsg={this.state.errorMsg}
items={logList}
currentPage={currentPage}
perPage={perPage}
hasNextPage={hasNextPage}
getLogsByPage={this.getLogsByPage}
resetPerPage={this.resetPerPage}
/>
</div> </div>
</div> </div>
</Fragment> </div>
); );
} }
} }

View File

@@ -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,60 +293,44 @@ 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> <div className="main-panel-center flex-row">
<MainPanelTopbar {...this.props}> <div className="cur-view-container">
<Button className="btn btn-secondary operation-item" onClick={this.toggleExportExcelDialog}>{gettext('Export Excel')}</Button> <div className="cur-view-content">
</MainPanelTopbar> <div className="d-flex align-items-center mb-2">
<div className="main-panel-center flex-row"> <LogUserSelector
<div className="cur-view-container"> componentName={gettext('Users')}
<LogsNav currentItem="fileUpdateLogs" /> items={availableUsers}
<div className="cur-view-content"> selectedItems={selectedUsers}
<Fragment> onSelect={this.handleUserFilter}
<div className="d-flex align-items-center mb-2"> isOpen={this.state.openSelector === 'user'}
<LogUserSelector onToggle={() => this.handleSelectorToggle('user')}
componentName={gettext('Users')} searchUsersFunc={this.searchUsers}
items={availableUsers} />
selectedItems={selectedUsers} <div className="mx-3"></div>
onSelect={this.handleUserFilter} <LogRepoSelector
isOpen={this.state.openSelector === 'user'} items={availableRepos}
onToggle={() => this.handleSelectorToggle('user')} selectedItems={selectedRepos}
searchUsersFunc={this.searchUsers} onSelect={this.handleRepoFilter}
/> isOpen={this.state.openSelector === 'repo'}
<div className="mx-3"></div> onToggle={() => this.handleSelectorToggle('repo')}
<LogRepoSelector searchReposFunc={this.searchRepos}
items={availableRepos} />
selectedItems={selectedRepos}
onSelect={this.handleRepoFilter}
isOpen={this.state.openSelector === 'repo'}
onToggle={() => this.handleSelectorToggle('repo')}
searchReposFunc={this.searchRepos}
/>
</div>
<Content
loading={this.state.loading}
errorMsg={this.state.errorMsg}
items={logList}
currentPage={currentPage}
perPage={perPage}
hasNextPage={hasNextPage}
getLogsByPage={this.getLogsByPage}
resetPerPage={this.resetPerPage}
/>
</Fragment>
</div> </div>
<Content
loading={this.state.loading}
errorMsg={this.state.errorMsg}
items={logList}
currentPage={currentPage}
perPage={perPage}
hasNextPage={hasNextPage}
getLogsByPage={this.getLogsByPage}
resetPerPage={this.resetPerPage}
/>
</div> </div>
</div> </div>
{isExportExcelDialogOpen && </div>
<ModalPortal>
<LogsExportExcelDialog
logType={'fileUpdate'}
toggle={this.toggleExportExcelDialog}
/>
</ModalPortal>
}
</Fragment>
); );
} }
} }

View File

@@ -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,57 +307,51 @@ class GroupMemberAuditLogs extends Component {
} = this.state; } = this.state;
return ( return (
<Fragment> <div className="main-panel-center flex-row">
<MainPanelTopbar {...this.props} /> <div className="cur-view-container">
<div className="main-panel-center flex-row"> <div className="cur-view-content">
<div className="cur-view-container"> <div className="d-flex align-items-center mb-2">
<LogsNav currentItem="groupMember" /> <LogUserSelector
<div className="cur-view-content"> componentName={gettext('Member')}
<Fragment> items={availableUsers}
<div className="d-flex align-items-center mb-2"> selectedItems={selectedUsers}
<LogUserSelector onSelect={this.handleUserFilter}
componentName={gettext('Member')} isOpen={openSelector === 'user'}
items={availableUsers} onToggle={() => this.handleSelectorToggle('user')}
selectedItems={selectedUsers} searchUsersFunc={this.searchUsers}
onSelect={this.handleUserFilter} />
isOpen={openSelector === 'user'} <LogUserSelector
onToggle={() => this.handleSelectorToggle('user')} componentName={gettext('Group')}
searchUsersFunc={this.searchUsers} items={availableUsers}
/> selectedItems={selectedGroups}
<LogUserSelector onSelect={this.handleGroupFilter}
componentName={gettext('Group')} isOpen={openSelector === 'group'}
items={availableUsers} onToggle={() => this.handleSelectorToggle('group')}
selectedItems={selectedGroups} searchGroupsFunc={this.searchGroups}
onSelect={this.handleGroupFilter} />
isOpen={openSelector === 'group'} <LogUserSelector
onToggle={() => this.handleSelectorToggle('group')} componentName={gettext('Operator')}
searchGroupsFunc={this.searchGroups} items={availableUsers}
/> selectedItems={selectedOperators}
<LogUserSelector onSelect={this.handleOperatorFilter}
componentName={gettext('Operator')} isOpen={openSelector === 'operator'}
items={availableUsers} onToggle={() => this.handleSelectorToggle('operator')}
selectedItems={selectedOperators} searchUsersFunc={this.searchUsers}
onSelect={this.handleOperatorFilter} />
isOpen={openSelector === 'operator'}
onToggle={() => this.handleSelectorToggle('operator')}
searchUsersFunc={this.searchUsers}
/>
</div>
<Content
loading={this.state.loading}
errorMsg={this.state.errorMsg}
items={logList}
currentPage={currentPage}
perPage={perPage}
hasNextPage={hasNextPage}
getLogsByPage={this.getLogsByPage}
resetPerPage={this.resetPerPage}
/>
</Fragment>
</div> </div>
<Content
loading={this.state.loading}
errorMsg={this.state.errorMsg}
items={logList}
currentPage={currentPage}
perPage={perPage}
hasNextPage={hasNextPage}
getLogsByPage={this.getLogsByPage}
resetPerPage={this.resetPerPage}
/>
</div> </div>
</div> </div>
</Fragment> </div>
); );
} }
} }

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

View File

@@ -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,49 +214,33 @@ 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> <div className="main-panel-center flex-row">
<MainPanelTopbar {...this.props}> <div className="cur-view-container">
<Button className="btn btn-secondary operation-item" onClick={this.toggleExportExcelDialog}>{gettext('Export Excel')}</Button> <div className="cur-view-content">
</MainPanelTopbar> <LogUserSelector
<div className="main-panel-center flex-row"> componentName={gettext('Users')}
<div className="cur-view-container"> items={availableUsers}
<LogsNav currentItem="loginLogs" /> selectedItems={selectedUsers}
<div className="cur-view-content"> onSelect={this.handleUserFilter}
<Fragment> isOpen={this.state.isUserSelectorOpen}
<LogUserSelector onToggle={this.toggleUserSelector}
componentName={gettext('Users')} searchUsersFunc={this.searchUsers}
items={availableUsers} />
selectedItems={selectedUsers} <Content
onSelect={this.handleUserFilter} loading={this.state.loading}
isOpen={this.state.isUserSelectorOpen} errorMsg={this.state.errorMsg}
onToggle={this.toggleUserSelector} items={logList}
searchUsersFunc={this.searchUsers} currentPage={currentPage}
/> perPage={perPage}
<Content hasNextPage={hasNextPage}
loading={this.state.loading} getLogsByPage={this.getLogsByPage}
errorMsg={this.state.errorMsg} resetPerPage={this.resetPerPage}
items={logList} />
currentPage={currentPage}
perPage={perPage}
hasNextPage={hasNextPage}
getLogsByPage={this.getLogsByPage}
resetPerPage={this.resetPerPage}
/>
</Fragment>
</div>
</div> </div>
</div> </div>
{isExportExcelDialogOpen && </div>
<ModalPortal>
<LogsExportExcelDialog
logType={'login'}
toggle={this.toggleExportExcelDialog}
/>
</ModalPortal>
}
</Fragment>
); );
} }
} }

View File

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

View File

@@ -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,73 +328,57 @@ 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> <div className="main-panel-center flex-row">
<MainPanelTopbar {...this.props}> <div className="cur-view-container">
<Button className="btn btn-secondary operation-item" onClick={this.toggleExportExcelDialog}>{gettext('Export Excel')}</Button> <div className="cur-view-content">
</MainPanelTopbar> <div className="d-flex align-items-center mb-2">
<div className="main-panel-center flex-row"> <LogUserSelector
<div className="cur-view-container"> componentName={gettext('Share From')}
<LogsNav currentItem="sharePermissionLogs" /> items={availableUsers}
<div className="cur-view-content"> selectedItems={selectedFromUsers}
<Fragment> onSelect={this.handleFromUserFilter}
<div className="d-flex align-items-center mb-2"> isOpen={openSelector === 'fromUser'}
<LogUserSelector onToggle={() => this.handleSelectorToggle('fromUser')}
componentName={gettext('Share From')} searchUsersFunc={this.searchUsers}
items={availableUsers} />
selectedItems={selectedFromUsers} <LogUserSelector
onSelect={this.handleFromUserFilter} componentName={gettext('Share To')}
isOpen={openSelector === 'fromUser'} items={availableUsers}
onToggle={() => this.handleSelectorToggle('fromUser')} selectedItems={[...selectedToUsers, ...selectedToGroups]}
searchUsersFunc={this.searchUsers} onSelect={this.handleToUserFilter}
/> isOpen={openSelector === 'toUser'}
<LogUserSelector onToggle={() => this.handleSelectorToggle('toUser')}
componentName={gettext('Share To')} searchUsersFunc={this.searchUsers}
items={availableUsers} searchGroupsFunc={this.searchGroups}
selectedItems={[...selectedToUsers, ...selectedToGroups]} />
onSelect={this.handleToUserFilter} <div className="mx-3"></div>
isOpen={openSelector === 'toUser'} <LogRepoSelector
onToggle={() => this.handleSelectorToggle('toUser')} items={availableRepos}
searchUsersFunc={this.searchUsers} selectedItems={selectedRepos}
searchGroupsFunc={this.searchGroups} onSelect={this.handleRepoFilter}
/> isOpen={openSelector === 'repo'}
<div className="mx-3"></div> onToggle={() => this.handleSelectorToggle('repo')}
<LogRepoSelector searchReposFunc={this.searchRepos}
items={availableRepos} />
selectedItems={selectedRepos}
onSelect={this.handleRepoFilter}
isOpen={openSelector === 'repo'}
onToggle={() => this.handleSelectorToggle('repo')}
searchReposFunc={this.searchRepos}
/>
</div>
<Content
loading={this.state.loading}
errorMsg={this.state.errorMsg}
items={logList}
currentPage={currentPage}
perPage={perPage}
hasNextPage={hasNextPage}
getLogsByPage={this.getLogsByPage}
resetPerPage={this.resetPerPage}
/>
</Fragment>
</div> </div>
<Content
loading={this.state.loading}
errorMsg={this.state.errorMsg}
items={logList}
currentPage={currentPage}
perPage={perPage}
hasNextPage={hasNextPage}
getLogsByPage={this.getLogsByPage}
resetPerPage={this.resetPerPage}
/>
</div> </div>
</div> </div>
{isExportExcelDialogOpen && </div>
<ModalPortal>
<LogsExportExcelDialog
logType={'sharePermission'}
toggle={this.toggleExportExcelDialog}
/>
</ModalPortal>
}
</Fragment>
); );
} }
} }

View File

@@ -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> </>
); );
} }
} }

View File

@@ -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,32 +66,24 @@ class AllWikis extends Component {
render() { render() {
return ( return (
<Fragment> <div className="main-panel-center flex-row">
<MainPanelTopbar {...this.props} /> <div className="cur-view-container">
<div className="main-panel-center flex-row"> <div className="cur-view-content">
<div className="cur-view-container"> <Content
<ReposNav loading={this.state.loading}
currentItem="wikis" errorMsg={this.state.errorMsg}
sortBy={this.state.sortBy} items={this.state.wikis}
sortItems={this.sortItems} pageInfo={this.state.pageInfo}
curPerPage={this.props.perPage}
getListByPage={this.getWikisByPage}
resetPerPage={this.resetPerPage}
onDeleteRepo={this.onDeleteWiki}
onTransferRepo={this.onTransferWiki}
isWiki={true}
/> />
<div className="cur-view-content">
<Content
loading={this.state.loading}
errorMsg={this.state.errorMsg}
items={this.state.wikis}
pageInfo={this.state.pageInfo}
curPerPage={this.state.perPage}
getListByPage={this.getWikisByPage}
resetPerPage={this.resetPerPage}
onDeleteRepo={this.onDeleteWiki}
onTransferRepo={this.onTransferWiki}
isWiki={true}
/>
</div>
</div> </div>
</div> </div>
</Fragment> </div>
); );
} }
} }

View File

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

View File

@@ -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,21 +93,17 @@ class SystemRepo extends Component {
render() { render() {
return ( return (
<Fragment> <div className="main-panel-center flex-row">
<MainPanelTopbar {...this.props} /> <div className="cur-view-container">
<div className="main-panel-center flex-row"> <div className="cur-view-content">
<div className="cur-view-container"> <Content
<ReposNav currentItem="system" /> loading={this.state.loading}
<div className="cur-view-content"> errorMsg={this.state.errorMsg}
<Content items={this.state.items}
loading={this.state.loading} />
errorMsg={this.state.errorMsg}
items={this.state.items}
/>
</div>
</div> </div>
</div> </div>
</Fragment> </div>
); );
} }
} }

View File

@@ -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> </>
); );
} }
} }

View File

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

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

View File

@@ -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,19 +43,15 @@ const StatisticFile = (props) => {
}, []); }, []);
return ( return (
<Fragment> <div className="cur-view-container">
<MainPanelTopbar {...props} /> <div className="cur-view-content">
<div className="cur-view-container"> <StatisticCommonTool getActivesFiles={getActivesFiles} />
<StatisticNav currentItem="fileStatistic" /> {isLoading && <Loading />}
<div className="cur-view-content"> {!isLoading && data.length > 0 &&
<StatisticCommonTool getActivesFiles={getActivesFiles} /> <Chart title={gettext('File Operations')} data={data} legends={legends} />
{isLoading && <Loading />} }
{!isLoading && data.length > 0 &&
<Chart title={gettext('File Operations')} data={data} legends={legends} />
}
</div>
</div> </div>
</Fragment> </div>
); );
}; };

View File

@@ -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,45 +156,39 @@ class StatisticMetrics extends Component {
const { groupedMetrics, loading, error } = this.state; const { groupedMetrics, loading, error } = this.state;
return ( return (
<> <div className="cur-metrics-content">
<MainPanelTopbar {...this.props} /> {loading ? (
<div className=""> <div className="loading-icon loading-tip"></div>
<StatisticNav currentItem="metricsStatistic" /> ) : error ? (
<div className="cur-metrics-content"> <div className="error text-danger">{error}</div>
{loading ? ( ) : (
<div className="loading-icon loading-tip"></div> <div className="metrics-container">
) : error ? ( <div className="card">
<div className="error text-danger">{error}</div> <div className="card-body">
) : ( <table className="table table-striped mb-0">
<div className="metrics-container"> <thead>
<div className="card"> <tr>
<div className="card-body"> <th width="40%">{gettext('Metrics')}</th>
<table className="table table-striped mb-0"> <th width="20%">{gettext('Node')}</th>
<thead> <th width="15%">{gettext('Value')}</th>
<tr> <th width="25%">{gettext('Collected time')}</th>
<th width="40%">{gettext('Metrics')}</th> </tr>
<th width="20%">{gettext('Node')}</th> </thead>
<th width="15%">{gettext('Value')}</th> <tbody>
<th width="25%">{gettext('Collected time')}</th> {Object.entries(groupedMetrics).map(([component, metrics]) => (
</tr> <ComponentMetricsTable
</thead> key={component}
<tbody> componentName={component}
{Object.entries(groupedMetrics).map(([component, metrics]) => ( metrics={metrics}
<ComponentMetricsTable />
key={component} ))}
componentName={component} </tbody>
metrics={metrics} </table>
/>
))}
</tbody>
</table>
</div>
</div>
</div> </div>
)} </div>
</div> </div>
</div> )}
</> </div>
); );
} }
} }

View File

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

View File

@@ -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,27 +58,23 @@ class StatisticReports extends React.Component {
let { errorMessage } = this.state; let { errorMessage } = this.state;
return ( return (
<Fragment> <div className="cur-view-container">
<MainPanelTopbar {...this.props} /> <div className="cur-view-content">
<div className="cur-view-container"> <div className="statistic-reports">
<StatisticNav currentItem="reportsStatistic" /> <div className="statistic-reports-title">{gettext('Monthly User Traffic')}</div>
<div className="cur-view-content"> <div className="d-flex align-items-center mt-4">
<div className="statistic-reports"> <span className="statistic-reports-tip">{gettext('Month:')}</span>
<div className="statistic-reports-title">{gettext('Monthly User Traffic')}</div> <Input className="statistic-reports-input" defaultValue={dayjs().format('YYYYMM')} onChange={this.handleChange} />
<div className="d-flex align-items-center mt-4"> <Button className="statistic-reports-submit operation-item" onClick={this.onGenerateReports.bind(this, 'month')}>{gettext('Create Report')}</Button>
<span className="statistic-reports-tip">{gettext('Month:')}</span>
<Input className="statistic-reports-input" defaultValue={dayjs().format('YYYYMM')} onChange={this.handleChange} />
<Button className="statistic-reports-submit operation-item" onClick={this.onGenerateReports.bind(this, 'month')}>{gettext('Create Report')}</Button>
</div>
{errorMessage && <div className="error">{errorMessage}</div>}
</div>
<div className="statistic-reports">
<div className="statistic-reports-title">{gettext('User Storage')}</div>
<Button className="mt-4 operation-item" onClick={this.onGenerateReports.bind(this, 'storage')}>{gettext('Create Report')}</Button>
</div> </div>
{errorMessage && <div className="error">{errorMessage}</div>}
</div>
<div className="statistic-reports">
<div className="statistic-reports-title">{gettext('User Storage')}</div>
<Button className="mt-4 operation-item" onClick={this.onGenerateReports.bind(this, 'storage')}>{gettext('Create Report')}</Button>
</div> </div>
</div> </div>
</Fragment> </div>
); );
} }
} }

View File

@@ -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,26 +42,22 @@ const StatisticStorage = (props) => {
}, []); }, []);
return ( return (
<> <div className="cur-view-container">
<MainPanelTopbar {...props} /> <div className="cur-view-content">
<div className="cur-view-container"> <StatisticCommonTool getActivesFiles={getActivesFiles} />
<StatisticNav currentItem="storageStatistic" /> {isLoading && <Loading />}
<div className="cur-view-content"> {!isLoading && data.length > 0 && (
<StatisticCommonTool getActivesFiles={getActivesFiles} /> <Chart
{isLoading && <Loading />} title={gettext('Total Storage')}
{!isLoading && data.length > 0 && ( legends={legends}
<Chart data={data}
title={gettext('Total Storage')} margin={{ top: 60, right: 30, bottom: 30, left: 60 }}
legends={legends} ySuggestedMax={yMax}
data={data} getDisplayValue={getDisplayValue}
margin={{ top: 60, right: 30, bottom: 30, left: 60 }} />
ySuggestedMax={yMax} )}
getDisplayValue={getDisplayValue}
/>
)}
</div>
</div> </div>
</> </div>
); );
}; };

View File

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

View File

@@ -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,19 +38,15 @@ const StatisticUsers = (props) => {
}, []); }, []);
return ( return (
<Fragment> <div className="cur-view-container">
<MainPanelTopbar {...props} /> <div className="cur-view-content">
<div className="cur-view-container"> <StatisticCommonTool getActivesFiles={getActivesFiles} />
<StatisticNav currentItem="usersStatistic" /> {isLoading && <Loading />}
<div className="cur-view-content"> {!isLoading && data.length > 0 && (
<StatisticCommonTool getActivesFiles={getActivesFiles} /> <Chart title={gettext('Active Users')} legends={legends} data={data} ySuggestedMax={yMax} />
{isLoading && <Loading />} )}
{!isLoading && data.length > 0 && (
<Chart title={gettext('Active Users')} legends={legends} data={data} ySuggestedMax={yMax} />
)}
</div>
</div> </div>
</Fragment> </div>
); );
}; };

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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> </>
); );
} }
} }

View File

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

View File

@@ -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();
const {
currentPage, perPage,
sortBy = '',
sortOrder = 'asc',
is_active,
role
} = this.state;
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,
role: urlParams.get('role') || role
}, () => {
this.getUsersListByPage(this.state.currentPage);
});
} }
} }
toggleImportUserDialog = () => { componentDidUpdate(prevProps) {
this.setState({ isImportUserDialogOpen: !this.state.isImportUserDialogOpen }); const { isAdmin, sortBy, sortOrder, currentPage, perPage } = this.props;
}; if (prevProps.isAdmin !== isAdmin) {
this.setState({ loading: true }, () => {
if (isAdmin) {
this.getUserList();
} else {
this.initUserListFromURL();
}
});
}
toggleAddUserDialog = () => { if (prevProps.sortBy !== sortBy ||
this.setState({ isAddUserDialogOpen: !this.state.isAddUserDialogOpen }); prevProps.sortOrder !== sortOrder ||
}; prevProps.currentPage !== currentPage) {
this.updateURLSearchParams({
'page': currentPage,
'per_page': perPage,
'order_by': sortBy,
'direction': sortOrder
});
this.getUsersListByPage(currentPage);
}
}
toggleBatchSetQuotaDialog = () => { initUserListFromURL = () => {
this.setState({ isBatchSetQuotaDialogOpen: !this.state.isBatchSetQuotaDialogOpen }); const urlParams = new URL(window.location).searchParams;
}; const {
is_active,
toggleBatchDeleteUserDialog = () => { role
this.setState({ isBatchDeleteUserDialogOpen: !this.state.isBatchDeleteUserDialogOpen }); } = this.state;
this.setState({
is_active: urlParams.get('is_active') || is_active,
role: urlParams.get('role') || role
}, () => {
this.getUsersListByPage(this.props.currentPage);
});
}; };
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,87 +415,56 @@ 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}> <div className="cur-view-content">
{hasUserSelected ? {curTab == 'database' &&
<Fragment> <UsersFilterBar
<Button className="btn btn-secondary operation-item" onClick={this.toggleBatchSetQuotaDialog}>{gettext('Set Quota')}</Button> isActive={is_active}
<Button className="btn btn-secondary operation-item" onClick={this.toggleBatchDeleteUserDialog}>{gettext('Delete Users')}</Button> role={role}
</Fragment> onStatusChange={this.onStatusChange}
: this.getOperationsForAll() onRoleChange={this.onRoleChange}
/>
} }
</MainPanelTopbar> <Content
<div className="main-panel-center flex-row"> isAdmin={isAdmin}
<div className="cur-view-container"> isLDAPImported={isLDAPImported}
<UsersNav loading={this.state.loading}
currentItem={curTab} errorMsg={this.state.errorMsg}
sortBy={this.state.sortBy} items={this.state.userList}
sortOrder={this.state.sortOrder} sortBy={this.props.sortBy}
sortItems={this.sortByQuotaUsage} sortOrder={this.props.sortOrder}
/> sortByQuotaUsage={this.sortByQuotaUsage}
<div className="cur-view-content"> currentPage={this.state.currentPage}
{curTab == 'database' && hasNextPage={this.state.hasNextPage}
<UsersFilterBar curPerPage={this.props.perPage}
isActive={is_active} resetPerPage={this.resetPerPage}
role={role} getListByPage={this.getUsersListByPage}
onStatusChange={this.onStatusChange} updateUser={this.updateUser}
onRoleChange={this.onRoleChange} deleteUser={this.deleteUser}
/> updateAdminRole={this.updateAdminRole}
} revokeAdmin={this.revokeAdmin}
<Content onUserSelected={this.onUserSelected}
isAdmin={isAdmin} isAllUsersSelected={this.isAllUsersSelected}
isLDAPImported={isLDAPImported} toggleSelectAllUsers={this.toggleSelectAllUsers}
loading={this.state.loading} />
errorMsg={this.state.errorMsg}
items={this.state.userList}
currentPage={this.state.currentPage}
hasNextPage={this.state.hasNextPage}
curPerPage={this.state.perPage}
resetPerPage={this.resetPerPage}
getListByPage={this.getUsersListByPage}
updateUser={this.updateUser}
deleteUser={this.deleteUser}
updateAdminRole={this.updateAdminRole}
revokeAdmin={this.revokeAdmin}
onUserSelected={this.onUserSelected}
isAllUsersSelected={this.isAllUsersSelected}
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> </>
); );
} }
} }

View File

@@ -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,27 +290,23 @@ class AllVirusFiles extends Component {
render() { render() {
return ( return (
<Fragment> <div className="main-panel-center">
<MainPanelTopbar {...this.props} /> <div className="cur-view-container">
<div className="main-panel-center"> <div className="cur-view-content">
<div className="cur-view-container"> <Content
<Nav currentItem="all" /> loading={this.state.loading}
<div className="cur-view-content"> errorMsg={this.state.errorMsg}
<Content virusFiles={this.state.virusFiles}
loading={this.state.loading} currentPage={this.state.currentPage}
errorMsg={this.state.errorMsg} hasNextPage={this.state.hasNextPage}
virusFiles={this.state.virusFiles} curPerPage={this.state.perPage}
currentPage={this.state.currentPage} resetPerPage={this.resetPerPage}
hasNextPage={this.state.hasNextPage} getListByPage={this.getListByPage}
curPerPage={this.state.perPage} handleFile={this.handleFile}
resetPerPage={this.resetPerPage} />
getListByPage={this.getListByPage}
handleFile={this.handleFile}
/>
</div>
</div> </div>
</div> </div>
</Fragment> </div>
); );
} }
} }

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

View File

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

View File

@@ -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,50 +396,28 @@ 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> <div className="main-panel-center">
{this.state.virusFiles.some(item => item.isSelected) ? ( <div className="cur-view-container">
<MainPanelTopbar {...this.props}> <div className="cur-view-content">
<Fragment> <Content
<Button onClick={this.deleteSelectedItems} className="operation-item">{gettext('Delete')}</Button> loading={this.state.loading}
<Button onClick={this.ignoreSelectedItems} className="operation-item">{gettext('Ignore')}</Button> errorMsg={this.state.errorMsg}
</Fragment> virusFiles={this.state.virusFiles}
</MainPanelTopbar> currentPage={this.state.currentPage}
) : <MainPanelTopbar {...this.props} /> hasNextPage={this.state.hasNextPage}
} curPerPage={this.state.perPage}
<div className="main-panel-center"> resetPerPage={this.resetPerPage}
<div className="cur-view-container"> getListByPage={this.getListByPage}
<Nav currentItem="unhandled" /> handleFile={this.handleFile}
<div className="cur-view-content"> isAllItemsSelected={this.state.isAllItemsSelected}
<Content toggleAllSelected={this.toggleAllSelected}
loading={this.state.loading} toggleItemSelected={this.toggleItemSelected}
errorMsg={this.state.errorMsg} />
virusFiles={this.state.virusFiles}
currentPage={this.state.currentPage}
hasNextPage={this.state.hasNextPage}
curPerPage={this.state.perPage}
resetPerPage={this.resetPerPage}
getListByPage={this.getListByPage}
handleFile={this.handleFile}
isAllItemsSelected={this.state.isAllItemsSelected}
toggleAllSelected={this.toggleAllSelected}
toggleItemSelected={this.toggleItemSelected}
/>
</div>
</div> </div>
</div> </div>
</Fragment> </div>
); );
} }
} }