1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-24 12:58:34 +00:00

list-org-staff (#8067)

* list-org-staff

* Update org-staff.js

* code-optimize

* update

* Update org-admins.js

---------

Co-authored-by: r350178982 <32759763+r350178982@users.noreply.github.com>
This commit is contained in:
kaichen999
2025-07-24 18:07:43 +08:00
committed by GitHub
parent f5013dadb1
commit 29b1820066
7 changed files with 469 additions and 14 deletions

View File

@@ -37,6 +37,7 @@ import OrgsTrafficExceeded from './orgs/orgs-traffic-exceeded';
import SearchOrgs from './orgs/search-orgs';
import OrgInfo from './orgs/org-info';
import OrgUsers from './orgs/org-users';
import OrgAdmins from './orgs/org-admins';
import OrgGroups from './orgs/org-groups';
import OrgRepos from './orgs/org-repos';
@@ -228,6 +229,7 @@ class SysAdmin extends React.Component {
<SearchOrgs path={siteRoot + 'sys/search-organizations'} {...commonProps} />
<OrgInfo path={siteRoot + 'sys/organizations/:orgID/info'} {...commonProps} />
<OrgUsers path={siteRoot + 'sys/organizations/:orgID/users'} {...commonProps} />
<OrgAdmins path={siteRoot + 'sys/organizations/:orgID/admin-users'} {...commonProps} />
<OrgGroups path={siteRoot + 'sys/organizations/:orgID/groups'} {...commonProps} />
<OrgRepos path={siteRoot + 'sys/organizations/:orgID/libraries'} {...commonProps} />
<Institutions path={siteRoot + 'sys/institutions'} {...commonProps} />

View File

@@ -0,0 +1,420 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import { Utils } from '../../../utils/utils';
import { systemAdminAPI } from '../../../utils/system-admin-api';
import { gettext, username } from '../../../utils/constants';
import toaster from '../../../components/toast';
import EmptyTip from '../../../components/empty-tip';
import Loading from '../../../components/loading';
import Selector from '../../../components/single-selector';
import CommonOperationConfirmationDialog from '../../../components/dialog/common-operation-confirmation-dialog';
import OpMenu from '../../../components/dialog/op-menu';
import MainPanelTopbar from '../main-panel-topbar';
import UserLink from '../user-link';
import OrgNav from './org-nav';
import SysAdminUserDeactivateDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-user-deactivate-dialog';
dayjs.extend(relativeTime);
class Content extends React.Component {
constructor(props) {
super(props);
this.state = {
isItemFreezed: false
};
}
toggleItemFreezed = (isFreezed) => {
this.setState({ isItemFreezed: isFreezed });
};
onFreezedItem = () => {
this.setState({ isItemFreezed: true });
};
onUnfreezedItem = () => {
this.setState({ isItemFreezed: false });
};
render() {
const { loading, errorMsg, items } = this.props;
if (loading) {
return <Loading />;
} else if (errorMsg) {
return <p className="error text-center mt-4">{errorMsg}</p>;
} else {
const emptyTip = (
<EmptyTip text={gettext('No admins')}>
</EmptyTip>
);
const table = (
<Fragment>
<table>
<thead>
<tr>
<th width="25%">{gettext('Name')}</th>
<th width="15%">{gettext('Status')}</th>
<th width="25%">{gettext('Space Used')}</th>
<th width="30%">{gettext('Created At')}{' / '}{gettext('Last Login')}</th>
<th width="5%">{/* Operations */}</th>
</tr>
</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}
updateStatus={this.props.updateStatus}
updateMembership={this.props.updateMembership}
deleteUser={this.props.deleteUser}
/>);
})}
</tbody>
</table>
</Fragment>
);
return items.length ? table : emptyTip;
}
}
}
Content.propTypes = {
loading: PropTypes.bool.isRequired,
errorMsg: PropTypes.string.isRequired,
items: PropTypes.array.isRequired,
updateStatus: PropTypes.func.isRequired,
updateMembership: PropTypes.func.isRequired,
deleteUser: PropTypes.func.isRequired,
};
class Item extends React.Component {
constructor(props) {
super(props);
this.state = {
isOpIconShown: false,
highlight: false,
isDeleteDialogOpen: false,
isResetPasswordDialogOpen: false
};
}
handleMouseEnter = () => {
if (!this.props.isItemFreezed) {
this.setState({
isOpIconShown: true,
highlight: true
});
}
};
handleMouseLeave = () => {
if (!this.props.isItemFreezed) {
this.setState({
isOpIconShown: false,
highlight: false
});
}
};
onUnfreezedItem = () => {
this.setState({
highlight: false,
isOpIconShow: false
});
this.props.onUnfreezedItem();
};
onMenuItemClick = (operation) => {
switch (operation) {
case 'Delete':
this.toggleDeleteDialog();
break;
case 'Reset Password':
this.toggleResetPasswordDialog();
break;
case 'Revoke Admin':
this.props.updateMembership(this.props.item.email, 'Member');
break;
default:
break;
}
};
toggleDeleteDialog = (e) => {
if (e) {
e.preventDefault();
}
this.setState({ isDeleteDialogOpen: !this.state.isDeleteDialogOpen });
};
toggleResetPasswordDialog = (e) => {
if (e) {
e.preventDefault();
}
this.setState({ isResetPasswordDialogOpen: !this.state.isResetPasswordDialogOpen });
};
toggleConfirmInactiveDialog = (targetItem) => {
if (targetItem?.value === 'active') {
return;
}
this.setState({ isConfirmInactiveDialogOpen: !this.state.isConfirmInactiveDialogOpen });
};
updateStatus = (statusOption) => {
this.props.updateStatus(this.props.item.email, statusOption.value);
};
setUserInactive = (keepSharing) => {
this.props.updateStatus(this.props.item.email, 'inactive', { keepSharing: keepSharing });
};
updateMembership = (membershipOption) => {
this.props.updateMembership(this.props.item.email, membershipOption.value);
};
deleteUser = () => {
const { item } = this.props;
this.props.deleteUser(item.org_id, item.email);
};
resetPassword = () => {
systemAdminAPI.sysAdminResetUserPassword(this.props.item.email).then(res => {
toaster.success(res.data.reset_tip);
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
};
translateOperations = (item) => {
let translateResult = '';
switch (item) {
case 'Delete':
translateResult = gettext('Delete');
break;
case 'Reset Password':
translateResult = gettext('Reset Password');
break;
case 'Revoke Admin':
translateResult = gettext('Revoke Admin');
break;
}
return translateResult;
};
translateStatus = (status) => {
switch (status) {
case 'active':
return gettext('Active');
case 'inactive':
return gettext('Inactive');
}
};
render() {
const { item } = this.props;
const { highlight, isOpIconShown, isDeleteDialogOpen, isResetPasswordDialogOpen, isConfirmInactiveDialogOpen } = this.state;
const itemName = '<span class="op-target">' + Utils.HTMLescape(item.name) + '</span>';
let deleteDialogMsg = gettext('Are you sure you want to delete {placeholder} ?').replace('{placeholder}', itemName);
let resetPasswordDialogMsg = gettext('Are you sure you want to reset the password of {placeholder} ?').replace('{placeholder}', itemName);
// for 'user status'
const curStatus = item.active ? 'active' : 'inactive';
this.statusOptions = ['active', 'inactive'].map(item => {
return {
value: item,
text: this.translateStatus(item),
isSelected: item === curStatus
};
});
const currentSelectedStatusOption = this.statusOptions.filter(item => item.isSelected)[0];
return (
<Fragment>
<tr className={this.state.highlight ? 'tr-highlight' : ''} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<td><UserLink email={item.email} name={item.name} /></td>
<td>
<Selector
isDropdownToggleShown={highlight}
currentSelectedOption={currentSelectedStatusOption}
options={this.statusOptions}
selectOption={this.updateStatus}
toggleItemFreezed={this.props.toggleItemFreezed}
operationBeforeSelect={item.active ? this.toggleConfirmInactiveDialog : undefined}
/>
</td>
<td>{`${Utils.bytesToSize(item.quota_usage)} / ${item.quota_total > 0 ? Utils.bytesToSize(item.quota_total) : '--'}`}</td>
<td>
{dayjs(item.create_time).format('YYYY-MM-DD HH:mm:ss')}{' / '}{item.last_login ? dayjs(item.last_login).fromNow() : '--'}
</td>
<td>
{(isOpIconShown && item.email !== username) &&
<OpMenu
operations={['Delete', 'Reset Password', 'Revoke Admin']}
translateOperations={this.translateOperations}
onMenuItemClick={this.onMenuItemClick}
onFreezedItem={this.props.onFreezedItem}
onUnfreezedItem={this.onUnfreezedItem}
/>
}
</td>
</tr>
{isDeleteDialogOpen &&
<CommonOperationConfirmationDialog
title={gettext('Delete Member')}
message={deleteDialogMsg}
executeOperation={this.deleteUser}
confirmBtnText={gettext('Delete')}
toggleDialog={this.toggleDeleteDialog}
/>
}
{isResetPasswordDialogOpen &&
<CommonOperationConfirmationDialog
title={gettext('Reset Password')}
message={resetPasswordDialogMsg}
executeOperation={this.resetPassword}
confirmBtnText={gettext('Reset')}
toggleDialog={this.toggleResetPasswordDialog}
/>
}
{isConfirmInactiveDialogOpen &&
<SysAdminUserDeactivateDialog
toggleDialog={this.toggleConfirmInactiveDialog}
onSubmit={this.setUserInactive}
/>
}
</Fragment>
);
}
}
Item.propTypes = {
item: PropTypes.object.isRequired,
isItemFreezed: PropTypes.bool.isRequired,
onFreezedItem: PropTypes.func.isRequired,
onUnfreezedItem: PropTypes.func.isRequired,
toggleItemFreezed: PropTypes.func.isRequired,
updateStatus: PropTypes.func.isRequired,
updateMembership: PropTypes.func.isRequired,
deleteUser: PropTypes.func.isRequired,
};
class OrgAdmins extends React.Component {
constructor(props) {
super(props);
this.state = {
loading: true,
errorMsg: '',
orgName: '',
userList: [],
};
}
componentDidMount() {
systemAdminAPI.sysAdminGetOrg(this.props.orgID).then((res) => {
this.setState({
orgName: res.data.org_name
});
});
systemAdminAPI.sysAdminListOrgAdminUsers(this.props.orgID).then((res) => {
this.setState({
loading: false,
userList: res.data.users
});
}).catch((error) => {
this.setState({
loading: false,
errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403
});
});
}
deleteUser = (orgID, email) => {
systemAdminAPI.sysAdminDeleteOrgUser(orgID, email).then(res => {
let newUserList = this.state.userList.filter(item => {
return item.email !== email;
});
this.setState({ userList: newUserList });
toaster.success(gettext('Successfully deleted 1 item.'));
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
};
updateStatus = (email, statusValue, options = {}) => {
const isActive = statusValue === 'active';
systemAdminAPI.sysAdminUpdateOrgUser(this.props.orgID, email, 'active', isActive, options).then(res => {
let newUserList = this.state.userList.map(item => {
if (item.email === email) {
item.active = res.data.active;
}
return item;
});
this.setState({ userList: newUserList });
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
};
updateMembership = (email, membershipValue) => {
const isOrgStaff = membershipValue === 'Admin';
systemAdminAPI.sysAdminUpdateOrgUser(this.props.orgID, email, 'is_org_staff', isOrgStaff).then(res => {
let newUserList = this.state.userList.filter(item => {
return item.email !== email;
});
this.setState({ userList: newUserList });
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
};
render() {
const { orgName } = this.state;
return (
<Fragment>
<MainPanelTopbar {...this.props}>
</MainPanelTopbar>
<div className="main-panel-center flex-row">
<div className="cur-view-container">
<OrgNav
currentItem="admin-users"
orgID={this.props.orgID}
orgName={orgName}
/>
<div className="cur-view-content">
<Content
loading={this.state.loading}
errorMsg={this.state.errorMsg}
items={this.state.userList}
updateStatus={this.updateStatus}
updateMembership={this.updateMembership}
deleteUser={this.deleteUser}
/>
</div>
</div>
</div>
</Fragment>
);
}
}
OrgAdmins.propTypes = {
orgID: PropTypes.string,
};
export default OrgAdmins;

View File

@@ -16,6 +16,7 @@ class Nav extends React.Component {
this.navItems = [
{ name: 'info', urlPart: 'info', text: gettext('Info') },
{ name: 'users', urlPart: 'users', text: gettext('Members') },
{ name: 'admin-users', urlPart: 'admin-users', text: gettext('Admin') },
{ name: 'groups', urlPart: 'groups', text: gettext('Groups') },
{ name: 'repos', urlPart: 'libraries', text: gettext('Libraries') },
// {name: 'traffic', urlPart: 'traffic', text: gettext('traffic')},

View File

@@ -386,17 +386,6 @@ class Users extends Component {
});
};
getCurrentNavItem = () => {
const { isAdmin, isLDAPImported } = this.props;
let item = 'database';
if (isAdmin) {
item = 'admin';
} else if (isLDAPImported) {
item = 'ldap-imported';
}
return item;
};
addAdminInBatch = (emails) => {
systemAdminAPI.sysAdminAddAdminInBatch(emails).then(res => {
let users = res.data.success.map(user => {
@@ -424,7 +413,7 @@ class Users extends Component {
isAddUserDialogOpen,
isBatchDeleteUserDialogOpen,
isBatchSetQuotaDialogOpen,
isBatchAddAdminDialogOpen
isBatchAddAdminDialogOpen,
} = this.props;
const { is_active, role } = this.state;
@@ -495,7 +484,7 @@ class Users extends Component {
{isBatchAddAdminDialogOpen &&
<SysAdminBatchAddAdminDialog
addAdminInBatch={this.addAdminInBatch}
toggle={this.toggleBatchAddAdminDialog}
toggle={this.props.toggleBatchAddAdminDialog}
/>
}
</>

View File

@@ -649,6 +649,11 @@ class SystemAdminAPI {
return this.req.get(url);
}
sysAdminListOrgAdminUsers(orgID) {
const url = this.server + '/api/v2.1/admin/organizations/' + orgID + '/admin-users/';
return this.req.get(url);
}
sysAdminAddOrgUser(orgID, email, name, password) {
const url = this.server + '/api/v2.1/admin/organizations/' + orgID + '/users/';
let formData = new FormData();

View File

@@ -3,6 +3,7 @@ import logging
from types import FunctionType
from django.utils.translation import gettext as _
from seahub.utils.ccnet_db import CcnetDB
from rest_framework.authentication import SessionAuthentication
from rest_framework.permissions import IsAdminUser
from rest_framework.response import Response
@@ -423,3 +424,38 @@ class AdminOrgUser(APIView):
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
return Response({'success': True})
class AdminOrgAdminUsers(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
throttle_classes = (UserRateThrottle,)
permission_classes = (IsAdminUser, IsProVersion)
def get(self, request, org_id):
if not request.user.admin_permissions.other_permission():
return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.')
# argument check
org_id = int(org_id)
if org_id == 0:
error_msg = 'org_id invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
org = ccnet_api.get_org_by_id(org_id)
if not org:
error_msg = 'Organization %d not found.' % org_id
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
ccnet_db = CcnetDB()
staff = ccnet_db.get_org_staffs(int(org_id))
result = []
for email in staff:
org_user = User.objects.get(email=email)
user_info = get_org_user_info(org_id, org_user)
user_info['active'] = org_user.is_active
result.append(user_info)
return Response({'users': result})

View File

@@ -179,7 +179,7 @@ from seahub.api2.endpoints.admin.organizations import AdminOrganizations, \
AdminOrganization, AdminSearchOrganization, AdminOrganizationsBaseInfo, TrafficExceededOrganizations
from seahub.api2.endpoints.admin.institutions import AdminInstitutions, AdminInstitution
from seahub.api2.endpoints.admin.institution_users import AdminInstitutionUsers, AdminInstitutionUser
from seahub.api2.endpoints.admin.org_users import AdminOrgUsers, AdminOrgUser
from seahub.api2.endpoints.admin.org_users import AdminOrgAdminUsers, AdminOrgUsers, AdminOrgUser
from seahub.api2.endpoints.admin.org_groups import AdminOrgGroups
from seahub.api2.endpoints.admin.org_repos import AdminOrgRepos
from seahub.api2.endpoints.admin.org_stats import AdminOrgStatsTraffic
@@ -776,6 +776,7 @@ urlpatterns = [
re_path(r'^api/v2.1/admin/organizations/(?P<org_id>\d+)/$', AdminOrganization.as_view(), name='api-v2.1-admin-organization'),
re_path(r'^api/v2.1/admin/organizations/(?P<org_id>\d+)/users/$', AdminOrgUsers.as_view(), name='api-v2.1-admin-org-users'),
re_path(r'^api/v2.1/admin/organizations/(?P<org_id>\d+)/users/(?P<email>[^/]+)/$', AdminOrgUser.as_view(), name='api-v2.1-admin-org-user'),
re_path(r'^api/v2.1/admin/organizations/(?P<org_id>\d+)/admin-users/$', AdminOrgAdminUsers.as_view(), name='api-v2.1-admin-org-staff'),
re_path(r'^api/v2.1/admin/organizations/(?P<org_id>\d+)/groups/$', AdminOrgGroups.as_view(),name='api-v2.1-admin-org-groups'),
re_path(r'^api/v2.1/admin/organizations/(?P<org_id>\d+)/repos/$', AdminOrgRepos.as_view(),name='api-v2.1-admin-org-repos'),
re_path(r'^api/v2.1/admin/organizations/(?P<org_id>\d+)/statistics/traffic/$', AdminOrgStatsTraffic.as_view(), name='api-v2.1-admin-org-stats-traffic'),
@@ -930,6 +931,7 @@ urlpatterns = [
path('sys/search-organizations/', sysadmin_react_fake_view, name="sys_search_organizations"),
path('sys/organizations/<int:org_id>/info/', sysadmin_react_fake_view, name="sys_organization_info"),
path('sys/organizations/<int:org_id>/users/', sysadmin_react_fake_view, name="sys_organization_users"),
path('sys/organizations/<int:org_id>/admin-users/', sysadmin_react_fake_view, name="sys_organization_staff"),
path('sys/organizations/<int:org_id>/groups/', sysadmin_react_fake_view, name="sys_organization_groups"),
path('sys/organizations/<int:org_id>/libraries/', sysadmin_react_fake_view, name="sys_organization_repos"),
path('sys/institutions/', sysadmin_react_fake_view, name="sys_institutions"),