1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-08 10:22:46 +00:00

admin add ilter api for users (#6127)

* admin add ilter api for users

* change filter users UI

* change code format

---------

Co-authored-by: 孙永强 <11704063+s-yongqiang@user.noreply.gitee.com>
Co-authored-by: Michael An <2331806369@qq.com>
This commit is contained in:
Ranjiwei
2024-06-03 12:03:41 +08:00
committed by GitHub
parent 80637c506c
commit 82b4dabb5e
7 changed files with 438 additions and 35 deletions

View File

@@ -14,6 +14,7 @@ import OpMenu from '../../../components/dialog/op-menu';
import SysAdminUserSetQuotaDialog from '../../../components/dialog/sysadmin-dialog/set-quota';
import CommonOperationConfirmationDialog from '../../../components/dialog/common-operation-confirmation-dialog';
import UserLink from '../user-link';
import UsersFilterBar from './users-filter-bar';
const { availableRoles, availableAdminRoles, institutions } = window.sysadmin.pageOptions;
@@ -39,11 +40,11 @@ class Content extends Component {
};
getPreviousPage = () => {
this.props.getListByPage(this.props.currentPage - 1);
this.props.getListByPage(this.props.currentPage - 1, this.props.is_active, this.props.role);
};
getNextPage = () => {
this.props.getListByPage(this.props.currentPage + 1);
this.props.getListByPage(this.props.currentPage + 1, this.props.is_active, this.props.role);
};
sortByQuotaUsage = (e) => {
@@ -57,6 +58,7 @@ class Content extends Component {
curPerPage, hasNextPage, currentPage,
sortBy, sortOrder
} = this.props;
if (loading) {
return <Loading />;
} else if (errorMsg) {
@@ -129,21 +131,26 @@ class Content extends Component {
</thead>
<tbody>
{items.map((item, index) => {
return (<Item
key={index}
item={item}
isItemFreezed={this.state.isItemFreezed}
onFreezedItem={this.onFreezedItem}
onUnfreezedItem={this.onUnfreezedItem}
toggleItemFreezed={this.toggleItemFreezed}
updateUser={this.props.updateUser}
deleteUser={this.props.deleteUser}
updateAdminRole={this.props.updateAdminRole}
revokeAdmin={this.props.revokeAdmin}
onUserSelected={this.props.onUserSelected}
isAdmin={this.props.isAdmin}
isLDAPImported={this.props.isLDAPImported}
/>);
if (index < this.props.curPerPage) {
return (
<Item
key={index}
item={item}
isItemFreezed={this.state.isItemFreezed}
onFreezedItem={this.onFreezedItem}
onUnfreezedItem={this.onUnfreezedItem}
toggleItemFreezed={this.toggleItemFreezed}
updateUser={this.props.updateUser}
deleteUser={this.props.deleteUser}
updateAdminRole={this.props.updateAdminRole}
revokeAdmin={this.props.revokeAdmin}
onUserSelected={this.props.onUserSelected}
isAdmin={this.props.isAdmin}
isLDAPImported={this.props.isLDAPImported}
/>
);
}
return null;
})}
</tbody>
</table>
@@ -160,7 +167,19 @@ class Content extends Component {
</Fragment>
);
return items.length ? table : emptyTip;
return (
<div>
{this.props.currentItem === 'database' &&
<UsersFilterBar
isActive={this.props.is_active}
role={this.props.role}
onStatusChange={this.props.onStatusChange}
onRoleChange={this.props.onRoleChange}
/>
}
{items.length ? table : emptyTip}
</div>
);
}
}
}
@@ -188,6 +207,11 @@ Content.propTypes = {
curPerPage: PropTypes.number,
hasNextPage: PropTypes.bool,
sortOrder: PropTypes.string,
is_active: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
role: PropTypes.string,
currentItem: PropTypes.string,
onStatusChange: PropTypes.func,
onRoleChange: PropTypes.func
};
class Item extends Component {

View File

@@ -0,0 +1,35 @@
.users-filter-bar .users-filter-bar-dropdown-toggle {
height: 30px;
line-height: 30px;
padding-left: 8px;
font-size: 14px;
border-radius: 5px;
}
.users-filter-bar .users-filter-bar-dropdown-toggle .sf3-font-drop-down {
color: #999;
margin-left: auto;
display: inline-flex;
justify-content: center;
align-items: center;
font-size: 12px;
width: 24px;
height: 24px;
transform: scale(.8);
}
.users-filter-bar .users-filter-bar-dropdown-toggle:hover {
background-color: #f5f5f5;
cursor: pointer;
}
.users-filter-bar .dropdown-menu .dropdown-item {
padding: 0.25rem 0.5rem;
display: flex;
justify-content: space-between;
}
.users-filter-bar .dropdown-menu .dropdown-item:hover .sf2-icon-tick,
.users-filter-bar .dropdown-menu .dropdown-item:focus .sf2-icon-tick {
color: #fff !important;
}

View File

@@ -0,0 +1,121 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Dropdown, DropdownToggle, DropdownItem, DropdownMenu } from 'reactstrap';
import { gettext } from '../../../utils/constants';
import './users-filter-bar.css';
const { availableRoles } = window.sysadmin.pageOptions;
class UsersFilterBar extends Component {
constructor(props) {
super(props);
this.state = {
isStatusOpen: false,
isRoleOpen: false,
};
}
translateStatus = (status) => {
switch (status) {
case '0':
return gettext('Inactive');
case '1':
return gettext('Active');
default:
return gettext('All');
}
};
translateRole = (role) => {
switch (role) {
case 'default':
return gettext('Default');
case 'guest':
return gettext('Guest');
default:
return gettext('All');
}
};
toggleStatusDropdown = () => {
this.setState({ isStatusOpen: !this.state.isStatusOpen });
};
toggleRoleDropdown = () => {
this.setState({ isRoleOpen: !this.state.isRoleOpen });
};
renderCheck = () => {
return <span className="sf2-icon-tick text-gray font-weight-bold"></span>;
};
render() {
const { onStatusChange, onRoleChange } = this.props;
return (
<div className="users-filter-bar mt-4 mb-2 d-flex">
<Dropdown isOpen={this.state.isStatusOpen} toggle={this.toggleStatusDropdown}>
<DropdownToggle
tag="div"
data-toggle="dropdown"
aria-expanded={this.state.isStatusOpen}
className="users-filter-bar-dropdown-toggle"
>
<span>{gettext('Status')}{': '}{this.translateStatus(this.props.isActive)}</span>
<span className='sf3-font-drop-down sf3-font'></span>
</DropdownToggle>
<DropdownMenu>
<DropdownItem key={0} onClick={() => { onStatusChange(''); }}>
<span>{gettext('All')}</span>{this.props.isActive === '' && this.renderCheck()}
</DropdownItem>
<DropdownItem key={1} onClick={() => { onStatusChange('1'); }}>
<span>{gettext('Active')}</span>{this.props.isActive === '1' && this.renderCheck()}
</DropdownItem>
<DropdownItem key={2} onClick={() => { onStatusChange('0'); }}>
<span>{gettext('Inactive')}</span>{this.props.isActive === '0' && this.renderCheck()}
</DropdownItem>
</DropdownMenu>
</Dropdown>
<Dropdown isOpen={this.state.isRoleOpen} toggle={this.toggleRoleDropdown}>
<DropdownToggle
tag="div"
data-toggle="dropdown"
aria-expanded={this.state.isRoleOpen}
className="users-filter-bar-dropdown-toggle"
>
<span>{gettext('Role')}{': '}{this.translateRole(this.props.role)}</span>
<span className='sf3-font-drop-down sf3-font'></span>
</DropdownToggle>
<DropdownMenu>
<DropdownItem key={0} onClick={() => { onRoleChange(''); }}>
<span>{gettext('All')}</span>
{this.props.role === '' && this.renderCheck()}
</DropdownItem>
{availableRoles.map((item, index) => {
return (
<DropdownItem key={index} onClick={() => { onRoleChange(item); }}>
<span>{this.translateRole(item)}</span>
{this.props.role === item && this.renderCheck()}
</DropdownItem>
);
})}
</DropdownMenu>
</Dropdown>
</div>
);
}
}
UsersFilterBar.propTypes = {
loading: PropTypes.bool,
curPerPage: PropTypes.number,
sortBy: PropTypes.string,
currentPage: PropTypes.number,
sortOrder: PropTypes.string,
onStatusChange: PropTypes.func,
onRoleChange: PropTypes.func,
role: PropTypes.string,
isActive: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};
export default UsersFilterBar;

View File

@@ -4,6 +4,7 @@ import { Button } from 'reactstrap';
import PropTypes from 'prop-types';
import { Utils } from '../../../utils/utils';
import { seafileAPI } from '../../../utils/seafile-api';
import { systemAdminAPI } from '../../../utils/system-admin-api';
import { isPro, gettext, siteRoot } from '../../../utils/constants';
import toaster from '../../../components/toast';
import SysAdminUserSetQuotaDialog from '../../../components/dialog/sysadmin-dialog/set-quota';
@@ -43,7 +44,9 @@ class Users extends Component {
isAddUserDialogOpen: false,
isBatchSetQuotaDialogOpen: false,
isBatchDeleteUserDialogOpen: false,
isBatchAddAdminDialogOpen: false
isBatchAddAdminDialogOpen: false,
is_active: null,
role: null,
};
}
@@ -55,15 +58,17 @@ class Users extends Component {
const {
currentPage, perPage,
sortBy = '',
sortOrder = 'asc'
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
sortOrder: urlParams.get('direction') || sortOrder,
}, () => {
this.getUsersListByPage(this.state.currentPage);
this.getUsersListByPage(this.state.currentPage, is_active, role);
});
}
}
@@ -160,10 +165,10 @@ class Users extends Component {
});
};
getUsersListByPage = (page) => {
getUsersListByPage = (page, is_active, role) => {
const { perPage, sortBy, sortOrder } = this.state;
const { isLDAPImported } = this.props;
seafileAPI.sysAdminListUsers(page, perPage, isLDAPImported, sortBy, sortOrder).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);});
this.setState({
userList: users,
@@ -179,6 +184,39 @@ class Users extends Component {
});
};
updateURL = (page, perPage) => {
let url = new URL(location.href);
let searchParams = new URLSearchParams(url.search);
searchParams.set('page', page);
searchParams.set('per_page', perPage);
url.search = searchParams.toString();
navigate(url.toString());
};
// is_active: '1', '0', '' (active, inactive, all)
onStatusChange = (is_active) => {
this.setState({
is_active: is_active,
currentPage: 1
}, () => {
const { currentPage, perPage, is_active, role } = this.state;
this.updateURL(currentPage, perPage);
this.getUsersListByPage(currentPage, is_active, role);
});
};
// role: 'default', 'guest', ''
onRoleChange = (role) => {
this.setState({
role: role,
currentPage: 1
}, () => {
const { currentPage,perPage, is_active, role } = this.state;
this.updateURL(currentPage, perPage);
this.getUsersListByPage(currentPage, is_active, role);
});
};
sortByQuotaUsage = () => {
this.setState({
sortBy: 'quota_usage',
@@ -187,13 +225,13 @@ class Users extends Component {
}, () => {
let url = new URL(location.href);
let searchParams = new URLSearchParams(url.search);
const { currentPage, sortBy, sortOrder } = this.state;
const { currentPage, sortBy, sortOrder, is_active, role } = this.state;
searchParams.set('page', currentPage);
searchParams.set('order_by', sortBy);
searchParams.set('direction', sortOrder);
url.search = searchParams.toString();
navigate(url.toString());
this.getUsersListByPage(currentPage);
this.getUsersListByPage(currentPage, is_active, role);
});
};
@@ -309,7 +347,7 @@ class Users extends Component {
this.setState({
perPage: perPage
}, () => {
this.getUsersListByPage(1);
this.getUsersListByPage(1, this.state.is_active, this.state.role);
});
};
@@ -468,6 +506,8 @@ class Users extends Component {
currentPage={this.state.currentPage}
hasNextPage={this.state.hasNextPage}
curPerPage={this.state.perPage}
is_active={this.state.is_active}
role={this.state.role}
resetPerPage={this.resetPerPage}
getListByPage={this.getUsersListByPage}
updateUser={this.updateUser}
@@ -477,6 +517,9 @@ class Users extends Component {
onUserSelected={this.onUserSelected}
isAllUsersSelected={this.isAllUsersSelected}
toggleSelectAllUsers={this.toggleSelectAllUsers}
onRoleChange={this.onRoleChange}
onStatusChange={this.onStatusChange}
currentItem={this.getCurrentNavItem()}
/>
</div>
</div>

View File

@@ -0,0 +1,75 @@
import axios from 'axios';
import cookie from 'react-cookies';
import { siteRoot } from './constants';
class SystemAdminAPI {
init({ server, username, password, token }) {
this.server = server;
this.username = username;
this.password = password;
this.token = token; //none
if (this.token && this.server) {
this.req = axios.create({
baseURL: this.server,
headers: { 'Authorization': 'Token ' + this.token },
});
}
return this;
}
initForSeahubUsage({ siteRoot, xcsrfHeaders }) {
if (siteRoot && siteRoot.charAt(siteRoot.length-1) === '/') {
var server = siteRoot.substring(0, siteRoot.length-1);
this.server = server;
} else {
this.server = siteRoot;
}
this.req = axios.create({
headers: {
'X-CSRFToken': xcsrfHeaders,
}
});
return this;
}
_sendPostRequest(url, form) {
if (form.getHeaders) {
return this.req.post(url, form, {
headers:form.getHeaders()
});
} else {
return this.req.post(url, form);
}
}
sysAdminListUsers(page, perPage, isLDAPImported, sortBy, sortOrder, is_active, role) {
let url = this.server + '/api/v2.1/admin/users/';
let params = {
page: page,
per_page: perPage,
};
if (is_active) {
params.is_active = is_active;
}
if (role) {
params.role = role;
}
if (isLDAPImported) {
params.source = 'LDAPImport';
}
if (sortBy) {
params.order_by = sortBy;
params.direction = sortOrder;
}
return this.req.get(url, {params: params});
}
}
let systemAdminAPI = new SystemAdminAPI();
let xcsrfHeaders = cookie.load('sfcsrftoken');
systemAdminAPI.initForSeahubUsage({ siteRoot, xcsrfHeaders });
export { systemAdminAPI };

View File

@@ -46,6 +46,7 @@ from seahub.utils.timeutils import timestamp_to_isoformat_timestr, \
datetime_to_isoformat_timestr
from seahub.utils.user_permissions import get_user_role
from seahub.utils.repo import normalize_repo_status_code
from seahub.utils.ccnet_db import CcnetDB
from seahub.constants import DEFAULT_ADMIN
from seahub.role_permissions.models import AdminRole
from seahub.role_permissions.utils import get_available_roles
@@ -549,7 +550,7 @@ class AdminUsers(APIView):
throttle_classes = (UserRateThrottle, )
def get_info_of_users_order_by_quota_usage(self, source, direction,
page, per_page):
page, per_page, is_active=None, role=None):
# get user's quota usage info
user_usage_dict = {}
@@ -581,6 +582,13 @@ class AdminUsers(APIView):
# sort
users.sort(key=lambda item: item.quota_usage,
reverse=direction == 'desc')
if is_active == '1':
users = [u for u in users if u.is_active]
elif is_active == '0':
users = [u for u in users if not u.is_active]
if role:
users = [u for u in users if get_user_role(u) == role]
data = []
MULTI_INSTITUTION = getattr(settings, 'MULTI_INSTITUTION', False)
@@ -633,12 +641,15 @@ class AdminUsers(APIView):
try:
page = int(request.GET.get('page', '1'))
per_page = int(request.GET.get('per_page', '25'))
is_active = request.GET.get('is_active', None)
role = request.GET.get('role', None)
except ValueError:
page = 1
per_page = 25
is_active, role = None, None
start = (page - 1) * per_page
limit = per_page + 1
source = request.GET.get('source', 'DB').lower().strip()
if source not in ['db', 'ldapimport']:
# source: 'DB' or 'LDAPImport', default is 'DB'
@@ -671,7 +682,10 @@ class AdminUsers(APIView):
data = self.get_info_of_users_order_by_quota_usage(source,
direction,
page,
per_page)
per_page,
is_active,
role,
)
except Exception as e:
logger.error(e)
error_msg = 'Internal Server Error'
@@ -680,7 +694,11 @@ class AdminUsers(APIView):
result = {'data': data, 'total_count': total_count}
return Response(result)
else:
users = ccnet_api.get_emailusers('DB', start, per_page)
try:
ccnet_db = CcnetDB()
users, total_count = ccnet_db.list_eligible_users(start, limit, is_active, role)
except Exception:
users = ccnet_api.get_emailusers('DB', start, per_page)
elif source == 'ldapimport':
ldap_users_count = multi_ldap_users_count = 0

View File

@@ -28,6 +28,7 @@ import os
import configparser
from django.db import connection
class CcnetGroup(object):
def __init__(self, **kwargs):
@@ -37,13 +38,25 @@ class CcnetGroup(object):
self.timestamp = kwargs.get('timestamp')
self.parent_group_id = kwargs.get('parent_group_id')
class CcnetUsers(object):
def __init__(self, **kwargs):
self.user_id = kwargs.get('user_id')
self.email = kwargs.get('email')
self.is_staff = kwargs.get('is_staff')
self.is_active = kwargs.get('is_active')
self.ctime = kwargs.get('ctime')
self.role = kwargs.get('role')
self.passwd = kwargs.get('passwd')
class CcnetDB:
def __init__(self):
self.db_name = get_ccnet_db_name()[0]
def list_org_departments(self, org_id):
sql = f"""
SELECT
@@ -63,10 +76,10 @@ class CcnetDB:
group_id = item[0]
group_name = item[1]
creator_name = item[2]
timestamp=item[3]
timestamp = item[3]
parent_group_id = item[5]
params = {
'group_id':group_id,
'group_id': group_id,
'group_name': group_name,
'creator_name': creator_name,
'timestamp': timestamp,
@@ -75,3 +88,77 @@ class CcnetDB:
group_obj = CcnetGroup(**params)
groups.append(group_obj)
return groups
def list_eligible_users(self, start, limit, is_active=None, role=None):
def status(is_active):
return 'AND t1.is_active=%s ' % is_active
def is_role(role):
if role == 'default':
return 'AND (t2.role is null or t2.role = "default")'
else:
return 'AND t2.role = "%s"' % role
search_clause = ''
if is_active:
search_clause += status(is_active)
if role:
search_clause += is_role(role)
count_sql = f"""
SELECT count(1)
FROM
`{self.db_name}`.`EmailUser` t1
LEFT JOIN
`{self.db_name}`.`UserRole` t2
ON
t1.email = t2.email
WHERE
t1.email NOT LIKE '%%@seafile_group' %s
ORDER BY t1.id
""" % search_clause
sql = f"""
SELECT t1.id, t1.email, t1.is_staff, t1.is_active, t1.ctime, t2.role, t1.passwd
FROM
`{self.db_name}`.`EmailUser` t1
LEFT JOIN
`{self.db_name}`.`UserRole` t2
ON
t1.email = t2.email
WHERE
t1.email NOT LIKE '%%@seafile_group' %s
ORDER BY t1.id
LIMIT {limit} OFFSET {start}
""" % search_clause
users = []
total = 0
with connection.cursor() as cursor:
cursor.execute(count_sql)
cursor.execute(count_sql)
total_count = int(cursor.fetchone()[0])
cursor.execute(sql)
for item in cursor.fetchall():
user_id = item[0]
email = item[1]
is_staff = item[2]
is_active = item[3]
ctime = item[4]
role = item[5]
passwd = item[6]
params = {
'user_id': user_id,
'email': email,
'is_staff': is_staff,
'is_active': is_active,
'ctime': ctime,
'role': role,
'passwd': passwd
}
users_obj = CcnetUsers(**params)
users.append(users_obj)
return users, total_count