1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-16 23:29:49 +00:00

[system admin] users: rewrote user pages & 'ldap users' page (#4216)

This commit is contained in:
llj
2019-11-02 17:02:26 +08:00
committed by Daniel Pan
parent 5dae23e52b
commit ad5b070c9f
30 changed files with 1471 additions and 1570 deletions

View File

@@ -116,7 +116,7 @@ class Account extends Component {
} else {
if (isStaff) {
data = {
url: `${siteRoot}sys/useradmin/`,
url: `${siteRoot}sys/info/`,
text: gettext('System Admin')
};
} else if (isOrgStaff) {

View File

@@ -41,6 +41,7 @@ class SysAdminImportUserDialog extends React.Component {
}
const file = this.fileInputRef.current.files[0];
this.props.importUserInBatch(file);
this.toggle();
}
render() {
@@ -49,8 +50,7 @@ class SysAdminImportUserDialog extends React.Component {
<Modal isOpen={true} toggle={this.toggle}>
<ModalHeader toggle={this.toggle}>{gettext('Import users from a .xlsx file')}</ModalHeader>
<ModalBody>
<a href={`${siteRoot}useradmin/batchadduser/example/`}>{gettext('Download an example file')}</a>
<br/>
<p><a className="text-secondary small" href={`${siteRoot}useradmin/batchadduser/example/`}>{gettext('Download an example file')}</a></p>
<button className="btn btn-outline-primary" onClick={this.openFileInput}>{gettext('Upload file')}</button>
<input className="d-none" type="file" onChange={this.uploadFile} ref={this.fileInputRef} />
{errorMsg && <Alert color="danger">{errorMsg}</Alert>}

View File

@@ -53,7 +53,6 @@ class SysAdminSetOrgNameDialog extends React.Component {
<FormGroup>
<Input
type="text"
className="form-control"
value={name}
onKeyPress={this.handleKeyPress}
onChange={this.handleInputChange}

View File

@@ -1,79 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Alert, Modal, ModalHeader, ModalBody, ModalFooter, Button, Form, FormGroup, Label, Input } from 'reactstrap';
import { gettext } from '../../../utils/constants';
import { Utils } from '../../../utils/utils';
const propTypes = {
toggle: PropTypes.func.isRequired,
onContactEmailChanged: PropTypes.func.isRequired
};
class SysAdminUserSetContactEmailDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
contactEmail: '',
isSubmitBtnActive: false,
errorMsg: '',
};
}
toggle = () => {
this.props.toggle();
}
handleContactEmailChange = (e) => {
this.setState({contactEmail: e.target.value.trim()});
}
handleKeyPress = (e) => {
if (e.key === 'Enter') {
this.handleSubmit();
e.preventDefault();
}
}
handleSubmit = () => {
let { contactEmail } = this.state;
if(Utils.isValidEmail(contactEmail) || contactEmail === '') {
this.props.onContactEmailChanged(contactEmail);
} else {
this.setState({
errorMsg: gettext('Contact email invalid.')
});
}
}
render() {
let { contactEmail, errorMsg } = this.state;
return (
<Modal isOpen={true} toggle={this.toggle}>
<ModalHeader toggle={this.toggle}>{gettext('Set user contact email')}</ModalHeader>
<ModalBody>
<Form>
<FormGroup>
<Label for="repoName">{gettext('Name')}</Label>
<Input
id="repoName"
onKeyPress={this.handleKeyPress}
value={contactEmail}
onChange={this.handleContactEmailChange}
/>
</FormGroup>
</Form>
{errorMsg && <Alert color="danger">{errorMsg}</Alert>}
</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={this.toggle}>{gettext('Cancel')}</Button>
<Button color="primary" onClick={this.handleSubmit}>{gettext('Submit')}</Button>
</ModalFooter>
</Modal>
);
}
}
SysAdminUserSetContactEmailDialog.propTypes = propTypes;
export default SysAdminUserSetContactEmailDialog;

View File

@@ -1,68 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Modal, ModalHeader, ModalBody, ModalFooter, Button, Form, FormGroup, Input } from 'reactstrap';
import { gettext } from '../../../utils/constants';
const propTypes = {
toggle: PropTypes.func.isRequired,
onLoginIDChanged: PropTypes.func.isRequired
};
class SysAdminUserSetLoginIDDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
loginID: '',
};
}
toggle = () => {
this.props.toggle();
}
handleLoginIDChange = (e) => {
this.setState({loginID: e.target.value.trim()});
}
handleKeyPress = (e) => {
if (e.key === 'Enter') {
this.handleSubmit();
e.preventDefault();
}
}
handleSubmit = () => {
let { loginID } = this.state;
this.props.onLoginIDChanged(loginID);
}
render() {
let { loginID } = this.state;
return (
<Modal isOpen={true} toggle={this.toggle}>
<ModalHeader toggle={this.toggle}>{gettext('Set user Login ID')}</ModalHeader>
<ModalBody>
<Form>
<FormGroup>
<Input
id="repoName"
onKeyPress={this.handleKeyPress}
value={loginID}
onChange={this.handleLoginIDChange}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={this.toggle}>{gettext('Cancel')}</Button>
<Button color="primary" onClick={this.handleSubmit}>{gettext('Submit')}</Button>
</ModalFooter>
</Modal>
);
}
}
SysAdminUserSetLoginIDDialog.propTypes = propTypes;
export default SysAdminUserSetLoginIDDialog;

View File

@@ -1,68 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Modal, ModalHeader, ModalBody, ModalFooter, Button, Form, FormGroup, Label, Input } from 'reactstrap';
import { gettext } from '../../../utils/constants';
const propTypes = {
toggle: PropTypes.func.isRequired,
onNameChanged: PropTypes.func.isRequired
};
class SysAdminUserSetNameDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
name: '',
};
}
toggle = () => {
this.props.toggle();
}
handleNameChange = (e) => {
this.setState({name: e.target.value.trim()});
}
handleKeyPress = (e) => {
if (e.key === 'Enter') {
this.handleSubmit();
e.preventDefault();
}
}
handleSubmit = () => {
let { name } = this.state;
this.props.onNameChanged(name);
}
render() {
let { name } = this.state;
return (
<Modal isOpen={true} toggle={this.toggle}>
<ModalHeader toggle={this.toggle}>{gettext('Set user name')}</ModalHeader>
<ModalBody>
<Form>
<FormGroup>
<Input
id="repoName"
onKeyPress={this.handleKeyPress}
value={name}
onChange={this.handleNameChange}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={this.toggle}>{gettext('Cancel')}</Button>
<Button color="primary" onClick={this.handleSubmit}>{gettext('Submit')}</Button>
</ModalFooter>
</Modal>
);
}
}
SysAdminUserSetNameDialog.propTypes = propTypes;
export default SysAdminUserSetNameDialog;

View File

@@ -1,88 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Alert, Modal, ModalHeader, ModalBody, ModalFooter, Button, Form, FormGroup, Label, Input } from 'reactstrap';
import { gettext } from '../../../utils/constants';
import { Utils } from '../../../utils/utils';
const propTypes = {
toggle: PropTypes.func.isRequired,
onQuotaChanged: PropTypes.func.isRequired
};
class SysAdminUserSetQuotaDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
quota: '',
isSubmitBtnActive: false,
errorMsg: '',
};
}
toggle = () => {
this.props.toggle();
}
handleQuotaChange = (e) => {
if (!e.target.value.trim()) {
this.setState({isSubmitBtnActive: false});
} else {
this.setState({
isSubmitBtnActive: true,
errorMsg: ''
});
}
this.setState({quota: e.target.value});
}
handleKeyPress = (e) => {
if (e.key === 'Enter') {
this.handleSubmit();
e.preventDefault();
}
}
handleSubmit = () => {
let { quota } = this.state;
if(Utils.isInteger(quota) && quota >= 0) {
this.props.onQuotaChanged(quota);
} else {
this.setState({
errorMsg: gettext('Invalid quota.')
});
}
}
render() {
let { quota, isSubmitBtnActive, errorMsg } = this.state;
return (
<Modal isOpen={true} toggle={this.toggle}>
<ModalHeader toggle={this.toggle}>{gettext('Set quota')}</ModalHeader>
<ModalBody>
<Form>
<FormGroup>
<Label for="repoName">{gettext('Name')}</Label>
<Input
id="repoName"
onKeyPress={this.handleKeyPress}
value={quota}
onChange={this.handleQuotaChange}
/>
</FormGroup>
</Form>
<Alert color="light">{gettext('An integer that is greater than or equal to 0.')}{gettext('Tip: 0 means default limit')}</Alert>
{errorMsg && <Alert color="danger">{errorMsg}</Alert>}
</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={this.toggle}>{gettext('Cancel')}</Button>
<Button color="primary" onClick={this.handleSubmit} disabled={!isSubmitBtnActive}>{gettext('Submit')}</Button>
</ModalFooter>
</Modal>
);
}
}
SysAdminUserSetQuotaDialog.propTypes = propTypes;
export default SysAdminUserSetQuotaDialog;

View File

@@ -1,71 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Modal, ModalHeader, ModalBody, ModalFooter, Button, Form, FormGroup, Label, Input } from 'reactstrap';
import { gettext } from '../../../utils/constants';
const propTypes = {
toggle: PropTypes.func.isRequired,
onReferenceIDChanged: PropTypes.func.isRequired
};
class SysAdminUserSetReferenceIDDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
referenceID: '',
isSubmitBtnActive: false,
errorMsg: '',
};
}
toggle = () => {
this.props.toggle();
}
handleReferenceIDChange = (e) => {
this.setState({referenceID: e.target.value.trim()});
}
handleKeyPress = (e) => {
if (e.key === 'Enter') {
this.handleSubmit();
e.preventDefault();
}
}
handleSubmit = () => {
let { referenceID } = this.state;
this.props.onReferenceIDChanged(referenceID);
}
render() {
let { referenceID } = this.state;
return (
<Modal isOpen={true} toggle={this.toggle}>
<ModalHeader toggle={this.toggle}>{gettext('Set user Reference ID')}</ModalHeader>
<ModalBody>
<Form>
<FormGroup>
<Label for="repoName">{gettext('Name')}</Label>
<Input
id="repoName"
onKeyPress={this.handleKeyPress}
value={referenceID}
onChange={this.handleReferenceIDChange}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={this.toggle}>{gettext('Cancel')}</Button>
<Button color="primary" onClick={this.handleSubmit}>{gettext('Submit')}</Button>
</ModalFooter>
</Modal>
);
}
}
SysAdminUserSetReferenceIDDialog.propTypes = propTypes;
export default SysAdminUserSetReferenceIDDialog;

View File

@@ -0,0 +1,70 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Alert, Modal, ModalHeader, ModalBody, ModalFooter, Button, Form, FormGroup, Input, InputGroup, InputGroupAddon, InputGroupText } from 'reactstrap';
import { gettext } from '../../../utils/constants';
import { Utils } from '../../../utils/utils';
const propTypes = {
dialogTitle: PropTypes.string.isRequired,
updateValue: PropTypes.func.isRequired,
toggleDialog: PropTypes.func.isRequired
};
class UpdateUser extends React.Component {
constructor(props) {
super(props);
this.state = {
value: this.props.value,
isSubmitBtnActive: false
};
}
handleInputChange = (e) => {
const value = e.target.value.trim();
this.setState({
value: value
});
}
handleKeyPress = (e) => {
if (e.key == 'Enter') {
this.handleSubmit();
e.preventDefault();
}
}
handleSubmit = () => {
this.props.updateValue(this.state.value);
this.props.toggleDialog();
}
render() {
const { dialogTitle, toggleDialog } = this.props;
return (
<Modal isOpen={true} toggle={toggleDialog}>
<ModalHeader toggle={toggleDialog}>{this.props.dialogTitle}</ModalHeader>
<ModalBody>
<Form>
<FormGroup>
<Input
type="text"
value={this.state.value}
onKeyPress={this.handleKeyPress}
onChange={this.handleInputChange}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={toggleDialog}>{gettext('Cancel')}</Button>
<Button color="primary" onClick={this.handleSubmit}>{gettext('Submit')}</Button>
</ModalFooter>
</Modal>
);
}
}
UpdateUser.propTypes = propTypes;
export default UpdateUser;

View File

@@ -37,7 +37,11 @@ class SelectEditor extends React.Component {
for (let i = 0, length = options.length; i < length; i++) {
let option = {};
option.value = options[i];
option.label = <div>{this.props.translateOption(options[i])}{ this.props.translateExplanation && <div className="permission-editor-explanation">{this.props.translateExplanation(options[i])}</div>}</div>;
if (!options[i].length) { // it's ''. for example, intitution option in 'system admin - users' page can be ''.
option.label = <div style={{minHeight: '1em'}}></div>;
} else {
option.label = <div>{this.props.translateOption(options[i])}{ this.props.translateExplanation && <div className="permission-editor-explanation">{this.props.translateExplanation(options[i])}</div>}</div>;
}
this.options.push(option);
}

View File

@@ -1,4 +1,5 @@
import React, { Component, Fragment } from 'react';
import { Link } from '@reach/router';
import { Button } from 'reactstrap';
import moment from 'moment';
import { Utils } from '../../../utils/utils';
@@ -170,18 +171,18 @@ class Item extends Component {
let groupName = '<span class="op-target">' + Utils.HTMLescape(item.name) + '</span>';
let deleteDialogMsg = gettext('Are you sure you want to delete {placeholder} ?').replace('{placeholder}', groupName);
const libUrl = item.parent_group_id == 0 ?
const groupUrl = item.parent_group_id == 0 ?
`${siteRoot}sys/groups/${item.id}/libraries/` :
`${siteRoot}sysadmin/#address-book/groups/${item.id}/`;
`${siteRoot}sys/departments/${item.id}/`;
return (
<Fragment>
<tr className={this.state.highlight ? 'tr-highlight' : ''} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<td><a href={libUrl}>{item.name}</a></td>
<td><Link to={groupUrl}>{item.name}</Link></td>
<td>
{item.owner == 'system admin' ?
'--' :
<a href={`${siteRoot}useradmin/info/${encodeURIComponent(item.owner)}/`}>{item.owner_name}</a>
<Link to={`${siteRoot}sys/users/${encodeURIComponent(item.owner)}/`}>{item.owner_name}</Link>
}
</td>
<td>

View File

@@ -14,8 +14,12 @@ 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 UsersLDAP from './users/users-ldap';
import LDAPUsers from './users/ldap-users';
import User from './users/user-info';
import UserOwnedRepos from './users/user-repos';
import UserSharedRepos from './users/user-shared-repos';
import UserLinks from './users/user-links';
import UserGroups from './users/user-groups';
import AllRepos from './repos/all-repos';
import SystemRepo from './repos/system-repo';
@@ -80,6 +84,10 @@ class SysAdmin extends React.Component {
tab: 'libraries',
urlPartList: ['all-libraries', 'system-library', 'trash-libraries', 'libraries/']
},
{
tab: 'users',
urlPartList: ['users/']
},
{
tab: 'groups',
urlPartList: ['groups/']
@@ -164,8 +172,12 @@ class SysAdmin extends React.Component {
<Users path={siteRoot + 'sys/users'} />
<AdminUsers path={siteRoot + 'sys/users/admins'} />
<LDAPImportedUsers path={siteRoot + 'sys/users/ldap-imported'} />
<UsersLDAP path={siteRoot + 'sys/users/ldap'} />
<LDAPUsers path={siteRoot + 'sys/users/ldap'} />
<User path={siteRoot + 'sys/users/:email'} />
<UserOwnedRepos path={siteRoot + 'sys/users/:email/owned-libraries'} />
<UserSharedRepos path={siteRoot + 'sys/users/:email/shared-libraries'} />
<UserLinks path={siteRoot + 'sys/users/:email/shared-links'} />
<UserGroups path={siteRoot + 'sys/users/:email/groups'} />
<FileScanRecords
path={siteRoot + 'sys/file-scan-records'}

View File

@@ -1,4 +1,5 @@
import React, { Component, Fragment } from 'react';
import { Link } from '@reach/router';
import { Button } from 'reactstrap';
import { Utils } from '../../../utils/utils';
import { seafileAPI } from '../../../utils/seafile-api';
@@ -201,7 +202,7 @@ class Item extends Component {
const { repo } = this.props;
if (repo.name) {
if (isPro && enableSysAdminViewRepo && !repo.encrypted) {
return <a href={`${siteRoot}sys/libraries/${repo.id}/`}>{repo.name}</a>;
return <Link to={`${siteRoot}sys/libraries/${repo.id}/`}>{repo.name}</Link>;
} else {
return repo.name;
}
@@ -229,8 +230,8 @@ class Item extends Component {
<td>{repo.id}</td>
<td>
{isGroupOwnedRepo ?
<a href={`${siteRoot}sysadmin/#address-book/groups/${repo.owner_name}/`}>{repo.group_name}</a> :
<a href={`${siteRoot}useradmin/info/${encodeURIComponent(repo.owner_email)}/`}>{repo.owner_name}</a>
<Link to={`${siteRoot}sys/departments/${repo.owner_name}/`}>{repo.group_name}</Link> :
<Link to={`${siteRoot}sys/users/${encodeURIComponent(repo.owner_email)}/`}>{repo.owner_name}</Link>
}
</td>
<td>

View File

@@ -1,9 +1,10 @@
import React, { Component, Fragment } from 'react';
import { seafileAPI } from '../../../utils/seafile-api';
import { gettext, siteRoot } from '../../../utils/constants';
import { Utils } from '../../../utils/utils';
import EmptyTip from '../../../components/empty-tip';
import { Link } from '@reach/router';
import moment from 'moment';
import { Utils } from '../../../utils/utils';
import { seafileAPI } from '../../../utils/seafile-api';
import { gettext, siteRoot, loginUrl } from '../../../utils/constants';
import EmptyTip from '../../../components/empty-tip';
import Loading from '../../../components/loading';
import Paginator from '../../../components/paginator';
import UsersNav from './users-nav';
@@ -13,8 +14,6 @@ class Content extends Component {
constructor(props) {
super(props);
this.state = {
};
}
getPreviousPage = () => {
@@ -30,7 +29,7 @@ class Content extends Component {
if (loading) {
return <Loading />;
} else if (errorMsg) {
return <p className="error text-center">{errorMsg}</p>;
return <p className="error text-center mt-4">{errorMsg}</p>;
} else {
const emptyTip = (
<EmptyTip>
@@ -42,23 +41,19 @@ class Content extends Component {
<table className="table-hover">
<thead>
<tr>
<th width="34%">{gettext('Email')}</th>
<th width="33%">{gettext('Space Used')}{' / '}{gettext('Quota')}</th>
<th width="33%">{gettext('Create At')}{' / '}{gettext('Last Login')}</th>
<th width="40%">{gettext('Email')}</th>
<th width="30%">{gettext('Space Used')}{' / '}{gettext('Quota')}</th>
<th width="30%">{gettext('Last Login')}</th>
</tr>
</thead>
{items &&
<tbody>
{items.map((item, index) => {
return (<Item
key={index}
item={item}
deleteUser={this.props.deleteUser}
onUserSelected={this.props.onUserSelected}
/>);
})}
</tbody>
}
<tbody>
{items.map((item, index) => {
return (<Item
key={index}
item={item}
/>);
})}
</tbody>
</table>
<Paginator
gotoPreviousPage={this.getPreviousPage}
@@ -81,39 +76,22 @@ class Item extends Component {
constructor(props) {
super(props);
this.state = {
quota_total: this.props.item.quota_total,
};
}
handleMouseOver = () => {
this.setState({isOpIconShown: true});
}
handleMouseOut = () => {
this.setState({isOpIconShown: false});
}
render() {
let { status, role, quota_total, isOpIconShown } = this.state;
let {item} = this.props;
let iconVisibility = this.state.isOpIconShown ? '' : ' invisible';
let pencilIconClassName = 'fa fa-pencil-alt attr-action-icon' + iconVisibility;
const { item } = this.props;
let email = '<span class="op-target">' + Utils.HTMLescape(item.email) + '</span>';
return (
<Fragment>
<tr onMouseEnter={this.handleMouseOver} onMouseLeave={this.handleMouseOut}>
<tr>
<td>
<div><a href={siteRoot + 'sys/user-info/' + item.email + '/'}>{item.email}</a></div>
</td>
<td>{Utils.bytesToSize(item.quota_usage)}{' / '}
{quota_total >= 0 ? Utils.bytesToSize(quota_total) : '--'}
<Link to={`${siteRoot}sys/users/${encodeURIComponent(item.email)}/`}>{item.email}</Link>
</td>
<td>
<div>{moment(item.create_time).format('YYYY-MM-DD HH:mm') }{' /'}</div>
<div>{item.last_login == '' ? '--' : moment(item.last_login).fromNow()}</div>
{`${Utils.bytesToSize(item.quota_usage)} / ${item.quota_total > 0 ? Utils.bytesToSize(item.quota_total) : '--'}`}
</td>
<td>
{item.last_login ? moment(item.last_login).fromNow() : '--'}
</td>
</tr>
</Fragment>
@@ -121,7 +99,7 @@ class Item extends Component {
}
}
class UsersLDAP extends Component {
class Users extends Component {
constructor(props) {
super(props);
@@ -131,22 +109,22 @@ class UsersLDAP extends Component {
userList: {},
hasNextPage: false,
currentPage: 1,
perPage: 25,
perPage: 25
};
}
componentDidMount () {
this.getUsersListByPage(1); // init enter the first page
this.getUsersListByPage(1);
}
getUsersListByPage = (page) => {
let { perPage } = this.state;
seafileAPI.sysAdminListAllLDAPUsers(page, perPage).then(res => {
seafileAPI.sysAdminListLDAPUsers(page, perPage).then(res => {
this.setState({
loading: false,
userList: res.data.ldap_user_list,
hasNextPage: res.data.has_next_page,
loading: false,
currentPage: page,
currentPage: page
});
}).catch((error) => {
if (error.response) {
@@ -155,6 +133,7 @@ class UsersLDAP extends Component {
loading: false,
errorMsg: gettext('Permission denied')
});
location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`;
} else {
this.setState({
loading: false,
@@ -179,11 +158,9 @@ class UsersLDAP extends Component {
}
render() {
//let { } = this.state;
return (
<Fragment>
<MainPanelTopbar>
</MainPanelTopbar>
<MainPanelTopbar />
<div className="main-panel-center flex-row">
<div className="cur-view-container">
<UsersNav currentItem="ldap" />
@@ -206,4 +183,4 @@ class UsersLDAP extends Component {
}
}
export default UsersLDAP;
export default Users;

View File

@@ -1,17 +1,32 @@
import React, { Component, Fragment } from 'react';
import { seafileAPI } from '../../../utils/seafile-api';
import { gettext, siteRoot } from '../../../utils/constants';
import toaster from '../../../components/toast';
import { Utils } from '../../../utils/utils';
import EmptyTip from '../../../components/empty-tip';
import { Link } from '@reach/router';
import moment from 'moment';
import { Utils } from '../../../utils/utils';
import { seafileAPI } from '../../../utils/seafile-api';
import { siteRoot, loginUrl, gettext } from '../../../utils/constants';
import toaster from '../../../components/toast';
import EmptyTip from '../../../components/empty-tip';
import Loading from '../../../components/loading';
import CommonOperationConfirmationDialog from '../../../components/dialog/common-operation-confirmation-dialog';
import MainPanelTopbar from '../main-panel-topbar';
import Nav from './user-nav';
import OpMenu from './user-op-menu';
class Content extends Component {
constructor(props) {
super(props);
this.state = {
isItemFreezed: false
};
}
onFreezedItem = () => {
this.setState({isItemFreezed: true});
}
onUnfreezedItem = () => {
this.setState({isItemFreezed: false});
}
render() {
@@ -19,38 +34,39 @@ class Content extends Component {
if (loading) {
return <Loading />;
} else if (errorMsg) {
return <p className="error text-center">{errorMsg}</p>;
return <p className="error text-center mt-4">{errorMsg}</p>;
} else {
const emptyTip = (
<EmptyTip>
<h2>{gettext('This user has not created or joined any groups')}</h2>
<h2>{gettext('No groups')}</h2>
</EmptyTip>
);
const table = (
<Fragment>
<table className="table-hover">
<table>
<thead>
<tr>
<th width="30%">{gettext('Name')}</th>
<th width="35%">{gettext('Name')}</th>
<th width="30%">{gettext('Role')}</th>
<th width="30%">{gettext('Create At')}</th>
<th width="10%">{gettext('Operations')}</th>
<th width="30%">{gettext('Created At')}</th>
<th width="5%">{/* Operations */}</th>
</tr>
</thead>
<tbody>
{items.map((item, index) => {
return (<Item
return (<Item
key={index}
item={item}
deleteGroup={this.props.deleteGroup}
isItemFreezed={this.state.isItemFreezed}
onFreezedItem={this.onFreezedItem}
onUnfreezedItem={this.onUnfreezedItem}
deleteItem={this.props.deleteItem}
/>);
})}
</tbody>
</table>
</Fragment>
);
return items.length ? table : emptyTip;
}
}
@@ -61,60 +77,116 @@ class Item extends Component {
constructor(props) {
super(props);
this.state = {
showOpIcon: false,
isDeleteDialogOpen: false,
isOpIconShown: false,
highlight: false,
isDeleteDialogOpen: false
};
}
handleMouseOver = () => {
this.setState({showOpIcon: true});
handleMouseEnter = () => {
if (!this.props.isItemFreezed) {
this.setState({
isOpIconShown: true,
highlight: true
});
}
}
handleMouseOut = () => {
this.setState({showOpIcon: false});
handleMouseLeave = () => {
if (!this.props.isItemFreezed) {
this.setState({
isOpIconShown: false,
highlight: false
});
}
}
onUnfreezedItem = () => {
this.setState({
highlight: false,
isOpIconShow: false
});
this.props.onUnfreezedItem();
}
toggleDeleteDialog = () => {
this.setState({isDeleteDialogOpen: !this.state.isDeleteDialogOpen});
}
deleteGroup = () => {
this.props.deleteGroup(this.props.item.id);
this.toggleDeleteDialog();
deleteItem = () => {
this.props.deleteItem(this.props.item.id);
}
translateOperations = (item) => {
let translateResult = '';
switch (item) {
case 'Delete':
translateResult = gettext('Delete');
break;
}
return translateResult;
}
onMenuItemClick = (operation) => {
switch(operation) {
case 'Delete':
this.toggleDeleteDialog();
break;
}
}
getRoleText = () => {
let roleText;
const { item } = this.props;
switch(item.role) {
case 'Owner':
roleText = gettext('Owner');
break;
case 'Admin':
roleText = gettext('Admin');
break;
case 'Member':
roleText = gettext('Member');
break;
}
return roleText;
}
render() {
let { showOpIcon, isDeleteDialogOpen } = this.state;
let { item } = this.props;
let roleText;
if (item.role == 'owner') {
roleText = gettext('Owner');
} else if (item.role == 'admin') {
roleText = gettext('Admin');
} else if (item.role == 'member') {
roleText = gettext('Member');
}
const { item } = this.props;
const { isOpIconShown, isDeleteDialogOpen } = this.state;
let groupName = '<span class="op-target">' + Utils.HTMLescape(item.name) + '</span>';
let deleteDialogMsg = gettext('Are you sure you want to delete {placeholder} ?'.replace('{placeholder}', groupName))
const itemName = '<span class="op-target">' + Utils.HTMLescape(item.name) + '</span>';
const deleteDialogMsg = gettext('Are you sure you want to delete {placeholder} ?').replace('{placeholder}', itemName);
const url = item.parent_group_id == 0 ?
`${siteRoot}sys/groups/${item.id}/libraries/` :
`${siteRoot}sys/departments/${item.id}/`;
let iconVisibility = showOpIcon ? '' : ' invisible';
let deleteIconClassName = 'op-icon sf2-icon-delete' + iconVisibility;
return (
<Fragment>
<tr onMouseEnter={this.handleMouseOver} onMouseLeave={this.handleMouseOut}>
<td><a href={siteRoot + 'sysadmin/#groups/' + item.id + '/libs/'}>{item.name}</a></td>
<td>{roleText}</td>
<td>{moment(item.created_at).fromNow()}</td>
<tr className={this.state.highlight ? 'tr-highlight' : ''} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<td><Link to={url}>{item.name}</Link></td>
<td>{this.getRoleText()}</td>
<td>{moment(item.created_at).format('YYYY-MM-DD HH:mm')}</td>
<td>
<a href="#" className={deleteIconClassName} title={gettext('Delete')} onClick={this.toggleDeleteDialog}></a>
{(isOpIconShown && item.parent_group_id == 0) &&
<OpMenu
operations={['Delete']}
translateOperations={this.translateOperations}
onMenuItemClick={this.onMenuItemClick}
onFreezedItem={this.props.onFreezedItem}
onUnfreezedItem={this.onUnfreezedItem}
/>
}
</td>
</tr>
{isDeleteDialogOpen &&
<CommonOperationConfirmationDialog
title={gettext('Delete Library')}
<CommonOperationConfirmationDialog
title={gettext('Delete Group')}
message={deleteDialogMsg}
executeOperation={this.deleteGroup}
executeOperation={this.deleteItem}
confirmBtnText={gettext('Delete')}
toggleDialog={this.toggleDeleteDialog}
/>
@@ -124,22 +196,28 @@ class Item extends Component {
}
}
class UserGroups extends Component {
class Groups extends Component {
constructor(props) {
super(props);
this.state = {
loading: true,
errorMsg: '',
groupList: [],
userInfo: {},
items: []
};
}
componentDidMount () {
seafileAPI.sysAdminListAllGroupsJoinedByUser(this.props.email).then(res => {
seafileAPI.sysAdminGetUser(this.props.email).then((res) => {
this.setState({
groupList: res.data.group_list,
loading: false
userInfo: res.data
});
});
seafileAPI.sysAdminListGroupsJoinedByUser(this.props.email).then(res => {
this.setState({
loading: false,
items: res.data.group_list
});
}).catch((error) => {
if (error.response) {
@@ -147,12 +225,13 @@ class UserGroups extends Component {
this.setState({
loading: false,
errorMsg: gettext('Permission denied')
});
});
location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`;
} else {
this.setState({
loading: false,
errorMsg: gettext('Error')
});
});
}
} else {
this.setState({
@@ -163,14 +242,13 @@ class UserGroups extends Component {
});
}
deleteGroup = (groupID) => {
deleteItem = (groupID) => {
seafileAPI.sysAdminDismissGroupByID(groupID).then(res => {
let newGroupList = this.state.groupList.filter(item=> {
let items = this.state.items.filter(item => {
return item.id != groupID;
});
this.setState({
groupList: newGroupList
});
this.setState({items: items});
toaster.success(gettext('Successfully deleted 1 item.'));
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
@@ -179,16 +257,24 @@ class UserGroups extends Component {
render() {
return (
<div className="cur-view-content">
<Content
loading={this.state.loading}
errorMsg={this.state.errorMsg}
items={this.state.groupList}
deleteGroup={this.deleteGroup}
/>
</div>
<Fragment>
<MainPanelTopbar />
<div className="main-panel-center flex-row">
<div className="cur-view-container">
<Nav currentItem="groups" email={this.props.email} userName={this.state.userInfo.name} />
<div className="cur-view-content">
<Content
loading={this.state.loading}
errorMsg={this.state.errorMsg}
items={this.state.items}
deleteItem={this.deleteItem}
/>
</div>
</div>
</div>
</Fragment>
);
}
}
export default UserGroups;
export default Groups;

View File

@@ -1,32 +1,199 @@
import React, { Component, Fragment } from 'react';
import { Nav, NavItem, NavLink, TabContent, TabPane, Label } from 'reactstrap';
import { gettext, siteRoot } from '../../../utils/constants';
import { seafileAPI } from '../../../utils/seafile-api';
import classnames from 'classnames';
import toaster from '../../../components/toast';
import { FormGroup, Label, Input, Button } from 'reactstrap';
import { Utils } from '../../../utils/utils';
import MainPanelTopbar from '../../org-admin/main-panel-topbar';
import UserProfile from './user-profile';
import UserOwnedRepos from './user-owned-repos';
import UserSharedInRepos from './user-share-in-repos';
import UserShareLinks from './user-share-links';
import UserGroups from './user-groups';
import { seafileAPI } from '../../../utils/seafile-api';
import { loginUrl, gettext } from '../../../utils/constants';
import toaster from '../../../components/toast';
import Loading from '../../../components/loading';
import SysAdminSetQuotaDialog from '../../../components/dialog/sysadmin-dialog/set-quota';
import SysAdminUpdateUserDialog from '../../../components/dialog/sysadmin-dialog/update-user';
import MainPanelTopbar from '../main-panel-topbar';
import Nav from './user-nav';
class UserInfo extends Component {
const { twoFactorAuthEnabled } = window.sysadmin.pageOptions;
class Content extends Component {
constructor(props) {
super(props);
this.state = {
activeTab: 'profile',
currentKey: '',
dialogTitle: '',
isSetQuotaDialogOpen: false,
isUpdateUserDialogOpen: false
};
}
toggleSetQuotaDialog = () => {
this.setState({isSetQuotaDialogOpen: !this.state.isSetQuotaDialogOpen});
}
updateQuota = (value) => {
this.props.updateUser('quota_total', value);
}
toggleDialog = (key, dialogTitle) => {
this.setState({
currentKey: key,
dialogTitle: dialogTitle,
isUpdateUserDialogOpen: !this.state.isUpdateUserDialogOpen
});
}
toggleSetNameDialog = () => {
this.toggleDialog('name', gettext('Set Name'));
}
toggleSetUserLoginIDDialog = () => {
this.toggleDialog('login_id', gettext('Set Login ID'));
}
toggleSetUserComtactEmailDialog = () => {
this.toggleDialog('contact_email', gettext('Set Contact Email'));
}
toggleSetUserReferenceIDDialog = () => {
this.toggleDialog('reference_id', gettext('Set Reference ID'));
}
updateValue = (value) => {
this.props.updateUser(this.state.currentKey, value);
}
toggleUpdateUserDialog = () => {
this.toggleDialog('', '');
}
showEditIcon = (action) => {
return (
<span
title={gettext('Edit')}
className="fa fa-pencil-alt attr-action-icon"
onClick={action}>
</span>
);
}
render() {
const { loading, errorMsg, userInfo } = this.props;
if (loading) {
return <Loading />;
} else if (errorMsg) {
return <p className="error text-center mt-4">{errorMsg}</p>;
} else {
const user = this.props.userInfo;
const {
currentKey, dialogTitle,
isSetQuotaDialogOpen, isUpdateUserDialogOpen
} = this.state;
return (
<Fragment>
<dl className="m-0">
<dt className="info-item-heading">{gettext('Avatar')}</dt>
<dd className="info-item-content">
<img src={user.avatar_url} alt={user.name} width="80" className="rounded" />
</dd>
<dt className="info-item-heading">{gettext('Email')}</dt>
<dd className="info-item-content">{user.email}</dd>
{user.org_name &&
<Fragment>
<dt className="info-item-heading">{gettext('Organization')}</dt>
<dd className="info-item-content">{user.org_name}</dd>
</Fragment>
}
<dt className="info-item-heading">{gettext('Name')}</dt>
<dd className="info-item-content">
{user.name || '--'}
{this.showEditIcon(this.toggleSetNameDialog)}
</dd>
<dt className="info-item-heading">{gettext('Login ID')}</dt>
<dd className="info-item-content">
{user.login_id || '--'}
{this.showEditIcon(this.toggleSetUserLoginIDDialog)}
</dd>
<dt className="info-item-heading">{gettext('Contact Email')}</dt>
<dd className="info-item-content">
{user.contact_email || '--'}
{this.showEditIcon(this.toggleSetUserComtactEmailDialog)}
</dd>
<dt className="info-item-heading">{gettext('Reference ID')}</dt>
<dd className="info-item-content">
{user.reference_id|| '--'}
{this.showEditIcon(this.toggleSetUserReferenceIDDialog)}
</dd>
<dt className="info-item-heading">{gettext('Space Used / Quota')}</dt>
<dd className="info-item-content">
{`${Utils.bytesToSize(user.quota_usage)} / ${user.quota_total > 0 ? Utils.bytesToSize(user.quota_total) : '--'}`}
{this.showEditIcon(this.toggleSetQuotaDialog)}
</dd>
{twoFactorAuthEnabled &&
<Fragment>
<dt className="info-item-heading">{gettext('Two-Factor Authentication')}</dt>
<dd className="info-item-content">
{user.has_default_device ?
<FormGroup>
<p className="mb-1">{gettext('Status: enabled')}</p>
<Button onClick={this.props.disable2FA}>{gettext('Disable Two-Factor Authentication')}</Button>
</FormGroup> :
<FormGroup>
<Button disabled={true}>{gettext('Disable Two-Factor Authentication')}</Button>
</FormGroup>
}
<FormGroup check>
<Label check>
<Input type="checkbox" checked={user.is_force_2fa} onChange={this.props.toggleForce2fa} />
<span>{gettext('Force Two-Factor Authentication')}</span>
</Label>
</FormGroup>
</dd>
</Fragment>
}
</dl>
{isSetQuotaDialogOpen &&
<SysAdminSetQuotaDialog
updateQuota={this.updateQuota}
toggle={this.toggleSetQuotaDialog}
/>
}
{isUpdateUserDialogOpen &&
<SysAdminUpdateUserDialog
dialogTitle={dialogTitle}
value={user[currentKey]}
updateValue={this.updateValue}
toggleDialog={this.toggleUpdateUserDialog}
/>
}
</Fragment>
);
}
}
}
class User extends Component {
constructor(props) {
super(props);
this.state = {
loading: true,
errorMsg: '',
userInfo: {}
};
}
componentDidMount() {
seafileAPI.sysAdminGetUserInfo(this.props.email).then(res => {
componentDidMount () {
// avatar size: 160
seafileAPI.sysAdminGetUser(this.props.email, 160).then((res) => {
this.setState({
userInfo: res.data,
loading: false
loading: false,
userInfo: res.data
});
}).catch((error) => {
if (error.response) {
@@ -34,13 +201,14 @@ class UserInfo extends Component {
this.setState({
loading: false,
errorMsg: gettext('Permission denied')
});
});
location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`;
} else {
this.setState({
loading: false,
errorMsg: gettext('Error')
});
}
});
}
} else {
this.setState({
loading: false,
@@ -50,87 +218,44 @@ class UserInfo extends Component {
});
}
toggle(tab) {
if (this.state.activeTab !== tab) {
this.setState({
activeTab: tab
});
}
}
onNameChanged = (name) => {
seafileAPI.sysAdminUpdateUserInfo('name', name, this.props.email).then(res => {
this.setState({
userInfo: res.data,
});
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
onLoginIDChanged = (loginID) => {
seafileAPI.sysAdminUpdateUserInfo('login_id', loginID, this.props.email).then(res => {
this.setState({
userInfo: res.data,
});
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
onContactEmailChanged = (contactEmail) => {
seafileAPI.sysAdminUpdateUserInfo('contact_email', contactEmail, this.props.email).then(res => {
this.setState({
userInfo: res.data,
});
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
onReferenceIDChanged = (referenceID) => {
seafileAPI.sysAdminUpdateUserInfo('reference_id', referenceID, this.props.email).then(res => {
this.setState({
userInfo: res.data,
});
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
onQuotaChanged = (quota) => {
seafileAPI.sysAdminUpdateUserInfo('quota_total', quota, this.props.email).then(res => {
this.setState({
userInfo: res.data,
});
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
toggleForce2FA = (isForce2FA) => {
seafileAPI.sysAdminToggleForceTwoFactorAuth(isForce2FA, this.props.email).then(res => {
updateUser = (key, value) => {
const email = this.state.userInfo.email;
seafileAPI.sysAdminUpdateUser(email, key, value).then(res => {
let userInfo = this.state.userInfo;
userInfo.is_force_2fa = isForce2FA;
this.setState({userInfo: userInfo});
userInfo[key]= res.data[key];
this.setState({
userInfo: userInfo
});
toaster.success(gettext('Edit succeeded'));
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
});
}
deleteVerified2FADevices = () => {
seafileAPI.sysAdminDeleteVerifiedTwoFactorAuth(this.props.email).then(res => {
disable2FA = () => {
const email = this.state.userInfo.email;
seafileAPI.sysAdminDeleteTwoFactorAuth(email).then(res => {
let userInfo = this.state.userInfo;
userInfo.has_default_device = false;
this.setState({userInfo: userInfo});
toaster.success(gettext('success'));
this.setState({
userInfo: userInfo
});
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
toggleForce2fa = (e) => {
const email = this.state.userInfo.email;
const checked = e.target.checked;
seafileAPI.sysAdminSetForceTwoFactorAuth(email, checked).then(res => {
let userInfo = this.state.userInfo;
userInfo.is_force_2fa = checked;
this.setState({
userInfo: userInfo
});
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
@@ -138,102 +263,22 @@ class UserInfo extends Component {
}
render() {
const { userInfo } = this.state;
return (
<Fragment>
<MainPanelTopbar>
<Fragment>
<a href={siteRoot + 'sys/users-all/'}>{gettext('Users')}</a>
{' / '}
<Label>{this.state.userInfo.name}</Label>
</Fragment>
</MainPanelTopbar>
<MainPanelTopbar />
<div className="main-panel-center flex-row">
<div className="cur-view-container">
<div className="cur-view-path align-items-center">
<Nav>
<NavItem>
<NavLink
className={classnames({ active: this.state.activeTab === 'profile' })}
onClick={() => { this.toggle('profile'); }}>
{gettext('Profile')}
</NavLink>
</NavItem>
<NavItem>
<NavLink
className={classnames({ active: this.state.activeTab === 'ownedLibs' })}
onClick={() => { this.toggle('ownedLibs'); }}>
{gettext('Owned Libs')}
</NavLink>
</NavItem>
<NavItem>
<NavLink
className={classnames({ active: this.state.activeTab === 'sharedLibs' })}
onClick={() => { this.toggle('sharedLibs'); }}>
{gettext('Shared Libs')}
</NavLink>
</NavItem>
<NavItem>
<NavLink
className={classnames({ active: this.state.activeTab === 'sharedLinks' })}
onClick={() => { this.toggle('sharedLinks'); }}>
{gettext('Shared Links')}
</NavLink>
</NavItem>
<NavItem>
<NavLink
className={classnames({ active: this.state.activeTab === 'groups' })}
onClick={() => { this.toggle('groups'); }}>
{gettext('Groups')}
</NavLink>
</NavItem>
</Nav>
</div>
<Nav currentItem="info" email={this.props.email} userName={userInfo.name} />
<div className="cur-view-content">
<TabContent activeTab={this.state.activeTab}>
<TabPane tabId="profile">
{this.state.activeTab === 'profile' &&
<UserProfile
email={this.props.email}
userInfo={this.state.userInfo}
onNameChanged={this.onNameChanged}
onContactEmailChanged={this.onContactEmailChanged}
onLoginIDChanged={this.onLoginIDChanged}
onReferenceIDChanged={this.onReferenceIDChanged}
onQuotaChanged={this.onQuotaChanged}
toggleForce2FA={this.toggleForce2FA}
deleteVerified2FADevices={this.deleteVerified2FADevices}
/>
}
</TabPane>
<TabPane tabId="ownedLibs">
{this.state.activeTab === 'ownedLibs' &&
<UserOwnedRepos
email={this.props.email}
/>
}
</TabPane>
<TabPane tabId="sharedLibs">
{this.state.activeTab === 'sharedLibs' &&
<UserSharedInRepos
email={this.props.email}
/>
}
</TabPane>
<TabPane tabId="sharedLinks">
{this.state.activeTab === 'sharedLinks' &&
<UserShareLinks
email={this.props.email}
/>
}
</TabPane>
<TabPane tabId="groups">
{this.state.activeTab === 'groups' &&
<UserGroups
email={this.props.email}
/>
}
</TabPane>
</TabContent>
<Content
loading={this.state.loading}
errorMsg={this.state.errorMsg}
userInfo={this.state.userInfo}
updateUser={this.updateUser}
disable2FA={this.disable2FA}
toggleForce2fa={this.toggleForce2fa}
/>
</div>
</div>
</div>
@@ -242,4 +287,4 @@ class UserInfo extends Component {
}
}
export default UserInfo;
export default User;

View File

@@ -0,0 +1,340 @@
import React, { Component, Fragment } from 'react';
import { Utils } from '../../../utils/utils';
import { seafileAPI } from '../../../utils/seafile-api';
import { loginUrl, gettext } from '../../../utils/constants';
import toaster from '../../../components/toast';
import EmptyTip from '../../../components/empty-tip';
import Loading from '../../../components/loading';
import LinkDialog from '../../../components/dialog/share-admin-link';
import MainPanelTopbar from '../main-panel-topbar';
import Nav from './user-nav';
import OpMenu from './user-op-menu';
class Content extends Component {
constructor(props) {
super(props);
this.state = {
isItemFreezed: false
};
}
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>
<h2>{gettext('No shared links')}</h2>
</EmptyTip>
);
const table = (
<Fragment>
<table>
<thead>
<tr>
<th width="5%">{/* icon */}</th>
<th width="30%">{gettext('Name')}</th>
<th width="20%">{gettext('Size')}</th>
<th width="20%">{gettext('Type')}</th>
<th width="20%">{gettext('Visits')}</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}
deleteItem={this.props.deleteItem}
/>);
})}
</tbody>
</table>
</Fragment>
);
return items.length ? table : emptyTip;
}
}
}
class Item extends Component {
constructor(props) {
super(props);
this.state = {
isOpIconShown: false,
highlight: false,
isLinkDialogOpen: 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();
}
toggleLinkDialog = () => {
this.setState({isLinkDialogOpen: !this.state.isLinkDialogOpen});
}
deleteItem = () => {
this.props.deleteItem(this.props.item);
}
translateOperations = (item) => {
let translateResult = '';
switch (item) {
case 'View':
translateResult = gettext('View');
break;
case 'Delete':
translateResult = gettext('Delete');
break;
}
return translateResult;
}
onMenuItemClick = (operation) => {
switch(operation) {
case 'View':
this.toggleLinkDialog();
break;
case 'Delete':
this.deleteItem();
break;
}
}
getRoleText = () => {
let roleText;
const { item } = this.props;
switch(item.role) {
case 'Owner':
roleText = gettext('Owner');
break;
case 'Admin':
roleText = gettext('Admin');
break;
case 'Member':
roleText = gettext('Member');
break;
}
return roleText;
}
getIconUrl = () => {
const { item } = this.props;
let url;
if (item.type == 'upload') {
url = Utils.getFolderIconUrl();
} else { // share link
if (item.is_dir) {
url = Utils.getFolderIconUrl();
} else {
url = Utils.getFileIconUrl(item.obj_name);
}
}
return url;
}
render() {
const { item } = this.props;
const { isOpIconShown, isLinkDialogOpen } = this.state;
return (
<Fragment>
<tr className={this.state.highlight ? 'tr-highlight' : ''} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<td><img src={this.getIconUrl()} alt="" width="24" /></td>
<td>{item.obj_name == '/' ? item.repo_name : item.obj_name}</td>
{item.type == 'upload' ?
<Fragment>
<td></td>
<td>{gettext('Upload')}</td>
</Fragment> :
<Fragment>
<td>{item.is_dir ? null : Utils.bytesToSize(item.size)}</td>
<td>{gettext('Download')}</td>
</Fragment>
}
<td>{item.view_cnt}</td>
<td>
{isOpIconShown &&
<OpMenu
operations={['View', 'Delete']}
translateOperations={this.translateOperations}
onMenuItemClick={this.onMenuItemClick}
onFreezedItem={this.props.onFreezedItem}
onUnfreezedItem={this.onUnfreezedItem}
/>
}
</td>
</tr>
{isLinkDialogOpen &&
<LinkDialog
link={item.link}
toggleDialog={this.toggleLinkDialog}
/>
}
</Fragment>
);
}
}
class Links extends Component {
constructor(props) {
super(props);
this.state = {
loading: true,
errorMsg: '',
userInfo: {},
uploadLinkItems: [],
shareLinkItems: []
};
}
componentDidMount () {
seafileAPI.sysAdminGetUser(this.props.email).then((res) => {
this.setState({
userInfo: res.data
});
});
seafileAPI.sysAdminListShareLinksByUser(this.props.email).then(res => {
const items = res.data.share_link_list.map(item => {
item.type = 'download';
return item;
});
items.sort((a, b) => {
return a.is_dir ? -1 : 1;
});
this.setState({
loading: false,
shareLinkItems: items
});
});
seafileAPI.sysAdminListUploadLinksByUser(this.props.email).then(res => {
const items = res.data.upload_link_list.map(item => {
item.type = 'upload';
return item;
});
this.setState({
loading: false,
uploadLinkItems: items
});
}).catch((error) => {
if (error.response) {
if (error.response.status == 403) {
this.setState({
loading: false,
errorMsg: gettext('Permission denied')
});
location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`;
} else {
this.setState({
loading: false,
errorMsg: gettext('Error')
});
}
} else {
this.setState({
loading: false,
errorMsg: gettext('Please check the network.')
});
}
});
}
deleteItem = (item) => {
const type = item.type;
const token = item.token;
if (type == 'download') {
seafileAPI.sysAdminDeleteShareLink(token).then(res => {
let items = this.state.shareLinkItems.filter(item=> {
return item.token != token;
});
this.setState({
shareLinkItems: items
});
toaster.success(gettext('Successfully deleted 1 item.'));
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
} else {
seafileAPI.sysAdminDeleteUploadLink(token).then(res => {
let items = this.state.uploadLinkItems.filter(item=> {
return item.token != token;
});
this.setState({
uploadLinkItems: items
});
toaster.success(gettext('Successfully deleted 1 item.'));
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
}
render() {
const { shareLinkItems, uploadLinkItems } = this.state;
return (
<Fragment>
<MainPanelTopbar />
<div className="main-panel-center flex-row">
<div className="cur-view-container">
<Nav currentItem="links" email={this.props.email} userName={this.state.userInfo.name} />
<div className="cur-view-content">
<Content
loading={this.state.loading}
errorMsg={this.state.errorMsg}
items={[].concat(uploadLinkItems, shareLinkItems)}
deleteItem={this.deleteItem}
/>
</div>
</div>
</div>
</Fragment>
);
}
}
export default Links;

View File

@@ -0,0 +1,46 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Link } from '@reach/router';
import { siteRoot, gettext } from '../../../utils/constants';
const propTypes = {
currentItem: PropTypes.string.isRequired
};
class Nav extends React.Component {
constructor(props) {
super(props);
this.navItems = [
{name: 'info', urlPart: '', text: gettext('Info')},
{name: 'owned-repos', urlPart: 'owned-libraries', text: gettext('Owned Libraries')},
{name: 'shared-repos', urlPart: 'shared-libraries', text: gettext('Shared Libraries')},
{name: 'links', urlPart: 'shared-links', text: gettext('Shared Links')},
{name: 'groups', urlPart: 'groups', text: gettext('Groups')}
];
}
render() {
const { currentItem, email, userName } = this.props;
return (
<div>
<div className="cur-view-path">
<h3 className="sf-heading"><Link to={`${siteRoot}sys/users/`}>{gettext('Users')}</Link> / {userName}</h3>
</div>
<ul className="nav border-bottom mx-4">
{this.navItems.map((item, index) => {
return (
<li className="nav-item mr-2" key={index}>
<Link to={`${siteRoot}sys/users/${encodeURIComponent(email)}/${item.urlPart}`} className={`nav-link ${currentItem == item.name ? ' active' : ''}`}>{item.text}</Link>
</li>
);
})}
</ul>
</div>
);
}
}
Nav.propTypes = propTypes;
export default Nav;

View File

@@ -1,236 +0,0 @@
import React, { Component, Fragment } from 'react';
import { seafileAPI } from '../../../utils/seafile-api';
import { gettext } from '../../../utils/constants';
import toaster from '../../../components/toast';
import { Utils } from '../../../utils/utils';
import { username } from '../../../utils/constants';
import EmptyTip from '../../../components/empty-tip';
import moment from 'moment';
import Loading from '../../../components/loading';
import CommonOperationConfirmationDialog from '../../../components/dialog/common-operation-confirmation-dialog';
import SysAdminRepoTransferDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-repo-transfer-dialog';
class Content extends Component {
constructor(props) {
super(props);
this.state = {
};
}
render() {
const { loading, errorMsg, items } = this.props;
if (loading) {
return <Loading />;
} else if (errorMsg) {
return <p className="error text-center">{errorMsg}</p>;
} else {
const emptyTip = (
<EmptyTip>
<h2>{gettext('This user has not created any libraries')}</h2>
</EmptyTip>
);
const table = (
<Fragment>
<table className="table-hover">
<thead>
<tr>
<th width="5%">{/*icon*/}</th>
<th width="20%">{gettext('Name')}</th>
<th width="25%">{gettext('Size')}</th>
<th width="19%">{gettext('Last Update')}</th>
<th width="10%">{/*Operations*/}</th>
</tr>
</thead>
<tbody>
{items.map((item, index) => {
return (<Item
key={index}
item={item}
deleteRepo={this.props.deleteRepo}
transferRepo={this.props.transferRepo}
/>);
})}
</tbody>
</table>
</Fragment>
);
return items.length ? table : emptyTip;
}
}
}
class Item extends Component {
constructor(props) {
super(props);
this.state = {
showOpIcon: false,
role: this.props.item.role,
quota_total: this.props.item.quota_total,
isDeleteDialogOpen: false,
isTransferDialogOpen: false,
};
}
handleMouseOver = () => {
this.setState({showOpIcon: true});
}
handleMouseOut = () => {
this.setState({showOpIcon: false});
}
toggleDeleteDialog = () => {
this.setState({isDeleteDialogOpen: !this.state.isDeleteDialogOpen});
}
toggleTransferDialog = () => {
this.setState({isTransferDialogOpen: !this.state.isTransferDialogOpen});
}
deleteRepo = () => {
this.props.deleteRepo(this.props.item.id);
this.toggleDeleteDialog();
}
transferRepo = (receiver) => {
this.props.transferRepo(receiver.email, this.props.item.id);
this.toggleTransferDialog();
}
render() {
let { showOpIcon, isDeleteDialogOpen, isTransferDialogOpen } = this.state;
let { item } = this.props;
let iconUrl = Utils.getLibIconUrl(item);
let iconTitle = Utils.getLibIconTitle(item);
let repoName = '<span class="op-target">' + Utils.HTMLescape(item.name) + '</span>';
let deleteDialogMsg = gettext('Are you sure you want to delete {placeholder} ?'.replace('{placeholder}', repoName))
let iconVisibility = this.state.showOpIcon ? '' : ' invisible';
let deleteIconClassName = 'op-icon sf2-icon-delete' + iconVisibility;
let transferIconClassName = 'op-icon sf2-icon-move' + iconVisibility;
return (
<Fragment>
<tr onMouseEnter={this.handleMouseOver} onMouseLeave={this.handleMouseOut}>
<td><img src={iconUrl} title={iconTitle} alt={iconTitle} width="24" /></td>
<td>{item.name}</td>
<td>{Utils.bytesToSize(item.size)}</td>
<td>{moment(item.last_modify).fromNow()}</td>
<td>
{item.email != username && showOpIcon &&
<Fragment>
<a href="#" className={deleteIconClassName} title={gettext('Delete')} onClick={this.toggleDeleteDialog}></a>
<a href="#" className={transferIconClassName} title={gettext('Transfer')} onClick={this.toggleTransferDialog}></a>
</Fragment>
}
</td>
</tr>
{isDeleteDialogOpen &&
<CommonOperationConfirmationDialog
title={gettext('Delete Library')}
message={deleteDialogMsg}
executeOperation={this.deleteRepo}
confirmBtnText={gettext('Delete')}
toggleDialog={this.toggleDeleteDialog}
/>
}
{isTransferDialogOpen &&
<SysAdminRepoTransferDialog
repoName={item.name}
toggle={this.toggleTransferDialog}
submit={this.transferRepo}
/>
}
</Fragment>
);
}
}
class UserOwnedRepos extends Component {
constructor(props) {
super(props);
this.state = {
loading: true,
errorMsg: '',
repoList: [],
isShowImportWaitingDialog: false,
isShowAddUserWaitingDialog: false
};
}
componentDidMount () {
seafileAPI.sysAdminListAllRepoInfoByOwner(this.props.email).then(res => {
this.setState({
repoList: res.data.repos,
loading: false
});
}).catch((error) => {
if (error.response) {
if (error.response.status == 403) {
this.setState({
loading: false,
errorMsg: gettext('Permission denied')
});
} else {
this.setState({
loading: false,
errorMsg: gettext('Error')
});
}
} else {
this.setState({
loading: false,
errorMsg: gettext('Please check the network.')
});
}
});
}
deleteRepo = (repoID) => {
seafileAPI.sysAdminDeleteRepo(repoID).then(res => {
let newRepoList = this.state.repoList.filter(repo => {
return repo.id != repoID;
});
this.setState({
repoList: newRepoList
});
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
transferRepo = (receiverEmail, repoID) => {
seafileAPI.sysAdminTransferRepo(repoID, receiverEmail).then(res => {
let newRepoList = this.state.repoList.filter(repo => {
return repo.id != repoID;
});
this.setState({
repoList: newRepoList
});
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
render() {
return (
<div className="cur-view-content">
<Content
loading={this.state.loading}
errorMsg={this.state.errorMsg}
items={this.state.repoList}
deleteRepo={this.deleteRepo}
transferRepo={this.transferRepo}
/>
</div>
);
}
}
export default UserOwnedRepos;

View File

@@ -1,188 +0,0 @@
import React, { Component, Fragment } from 'react';
import { Input, Button } from 'reactstrap';
import { gettext, enableTwoFactorAuth } from '../../../utils/constants';
import { Utils } from '../../../utils/utils';
import PropTypes from 'prop-types';
import Loading from '../../../components/loading';
import SysAdminUserSetNameDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-user-set-name-dialog';
import SysAdminUserSetContactEmailDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-user-set-contact-email-dialog';
import SysAdminUserSetLoginIDDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-user-set-loginid-dialog';
import SysAdminUserSetReferenceIDDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-user-set-referenceid-dialog';
import SysAdminUserSetQuotaDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-user-set-quota-dialog';
import '../../../css/system-info.css';
const { avatarURL } = window.app.config;
const propTypes = {
email: PropTypes.string.isRequired,
userInfo: PropTypes.object.isRequired,
onNameChanged: PropTypes.func.isRequired,
onContactEmailChanged: PropTypes.func.isRequired,
onLoginIDChanged: PropTypes.func.isRequired,
onReferenceIDChanged: PropTypes.func.isRequired,
onQuotaChanged: PropTypes.func.isRequired
};
class UserProfile extends Component {
constructor(props) {
super(props);
this.state = {
loading: true,
errorMsg: '',
userInfo: {},
isSetUserNameDialogOpen: false,
isSetUserLoginIDDialogOpen: false,
isSetUserContactEmailDialogOpen: false,
isSetUserReferenceIDDialogOpen: false,
isSetUserQuotaDialogOpen: false
};
}
toggleSetUserNameDialog = () => {
this.setState({isSetUserNameDialogOpen: !this.state.isSetUserNameDialogOpen});
}
toggleSetUserLoginIDDialog = () => {
this.setState({isSetUserLoginIDDialogOpen: !this.state.isSetUserLoginIDDialogOpen});
}
toggleSetUserContactEmailDialog = () => {
this.setState({isSetUserContactEmailDialogOpen: !this.state.isSetUserContactEmailDialogOpen});
}
toggleSetUserReferenceIDDialog = () => {
this.setState({isSetUserReferenceIDDialogOpen: !this.state.isSetUserReferenceIDDialogOpen});
}
toggleSetUserQuotaDialog = () => {
this.setState({isSetUserQuotaDialogOpen: !this.state.isSetUserQuotaDialogOpen});
}
onNameChanged = (name) => {
this.props.onNameChanged(name);
this.toggleSetUserNameDialog();
}
onLoginIDChanged = (loginID) => {
this.props.onLoginIDChanged(loginID);
this.toggleSetUserLoginIDDialog();
}
onContactEmailChanged = (contactEmail) => {
this.props.onContactEmailChanged(contactEmail);
this.toggleSetUserContactEmailDialog();
}
onReferenceIDChanged = (referenceID) => {
this.props.onReferenceIDChanged(referenceID);
this.toggleSetUserReferenceIDDialog();
}
onQuotaChanged = (quota) => {
this.props.onQuotaChanged(quota);
this.toggleSetUserQuotaDialog();
}
toggleForce2FA = (is_force_2fa) => {
this.props.toggleForce2FA(is_force_2fa);
}
deleteVerified2FADevices = () => {
this.props.deleteVerified2FADevices();
}
render() {
let { errorMsg, isSetUserContactEmailDialogOpen, isSetUserLoginIDDialogOpen,
isSetUserNameDialogOpen, isSetUserQuotaDialogOpen, isSetUserReferenceIDDialogOpen } = this.state;
let { email, name, login_id, contact_email, reference_id, quota_usage, quota_total, is_force_2fa,
has_default_device } = this.props.userInfo;
if (!this.props.userInfo) {
return <Loading />;
} else if (errorMsg) {
return <p className="error text-center">{errorMsg}</p>;
} else {
return (
<Fragment>
<div className="info-item">
<h4 className="info-item-heading">{gettext('Avatar')}</h4>
<img src={avatarURL} width="80" height="80" alt="" className="user-avatar" />
</div>
<div className="info-item">
<h4 className="info-item-heading">{gettext('Email')}</h4>
<span>{email}</span>
</div>
<div className="info-item">
<h4 className="info-item-heading">{gettext('Name')}</h4>
<span>{name ? name : '--'}</span>
<span onClick={this.toggleSetUserNameDialog} title={gettext('Edit')} style={{wdith:'14px', height:'14px'}} className="fa fa-pencil-alt attr-action-icon"></span>
</div>
<div className="info-item">
<h4 className="info-item-heading">{gettext('Login ID')}</h4>
<span>{login_id ? login_id : '--'}</span>
<span onClick={this.toggleSetUserLoginIDDialog} title={gettext('Edit')} style={{wdith:'14px', height:'14px'}} className="fa fa-pencil-alt attr-action-icon"></span>
</div>
<div className="info-item">
<h4 className="info-item-heading">{gettext('Contact Email')}</h4>
<span>{contact_email ? contact_email : '--'}</span>
<span onClick={this.toggleSetUserContactEmailDialog} title={gettext('Edit')} style={{wdith:'14px', height:'14px'}} className="fa fa-pencil-alt attr-action-icon"></span>
</div>
<div className="info-item">
<h4 className="info-item-heading">{gettext('Reference ID')}</h4>
<span>{reference_id ? reference_id : '--'}</span>
<span onClick={this.toggleSetUserReferenceIDDialog} title={gettext('Edit')} style={{wdith:'14px', height:'14px'}} className="fa fa-pencil-alt attr-action-icon"></span>
</div>
<div className="info-item">
<h4 className="info-item-heading">{gettext('Space Used')}{' / '}{gettext('Quota')}</h4>
{Utils.bytesToSize(quota_usage)}{' / '}
{quota_total >= 0 ? Utils.bytesToSize(quota_total) : '--'}
<span onClick={this.toggleSetUserQuotaDialog} title={gettext('Edit Quota')} style={{wdith:'14px', height:'14px'}} className="fa fa-pencil-alt attr-action-icon"></span>
</div>
{enableTwoFactorAuth &&
<div className="info-item">
<h4 className="info-item-heading">{gettext('Two-Factor Authentication')}</h4>
<Button color="primary" disabled={!has_default_device} onClick={this.deleteVerified2FADevices}>{gettext('Disable Two-Factor Authentication')}</Button>
<br/>
<label><Input className="ml-0" type="checkbox" onChange={() => {this.toggleForce2FA(!is_force_2fa)}} checked={is_force_2fa ? true : false} />{gettext('Force Two-Factor Authentication')}</label>
</div>
}
{isSetUserNameDialogOpen &&
<SysAdminUserSetNameDialog
toggle={this.toggleSetUserNameDialog}
onNameChanged={this.onNameChanged}
/>
}
{isSetUserContactEmailDialogOpen &&
<SysAdminUserSetContactEmailDialog
toggle={this.toggleSetUserContactEmailDialog}
onContactEmailChanged={this.onContactEmailChanged}
/>
}
{isSetUserLoginIDDialogOpen &&
<SysAdminUserSetLoginIDDialog
toggle={this.toggleSetUserLoginIDDialog}
onLoginIDChanged={this.onLoginIDChanged}
/>
}
{isSetUserReferenceIDDialogOpen &&
<SysAdminUserSetReferenceIDDialog
toggle={this.toggleSetUserReferenceIDDialog}
onReferenceIDChanged={this.onReferenceIDChanged}
/>
}
{isSetUserQuotaDialogOpen &&
<SysAdminUserSetQuotaDialog
toggle={this.toggleSetUserQuotaDialog}
onQuotaChanged={this.onQuotaChanged}
/>
}
</Fragment>
);
}
}
}
UserProfile.propTypes = propTypes;
export default UserProfile;

View File

@@ -0,0 +1,322 @@
import React, { Component, Fragment } from 'react';
import { Link } from '@reach/router';
import moment from 'moment';
import { Utils } from '../../../utils/utils';
import { seafileAPI } from '../../../utils/seafile-api';
import { isPro, siteRoot, loginUrl, gettext } from '../../../utils/constants';
import toaster from '../../../components/toast';
import EmptyTip from '../../../components/empty-tip';
import Loading from '../../../components/loading';
import CommonOperationConfirmationDialog from '../../../components/dialog/common-operation-confirmation-dialog';
import TransferDialog from '../../../components/dialog/transfer-dialog';
import MainPanelTopbar from '../main-panel-topbar';
import Nav from './user-nav';
import OpMenu from './user-op-menu';
const { enableSysAdminViewRepo } = window.sysadmin.pageOptions;
class Content extends Component {
constructor(props) {
super(props);
this.state = {
isItemFreezed: false
};
}
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>
<h2>{gettext('No libraries')}</h2>
</EmptyTip>
);
const table = (
<Fragment>
<table>
<thead>
<tr>
<th width="5%"></th>
<th width="35%">{gettext('Name')}</th>
<th width="30%">{gettext('Size')}</th>
<th width="25%">{gettext('Last Update')}</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}
deleteRepo={this.props.deleteRepo}
transferRepo={this.props.transferRepo}
/>);
})}
</tbody>
</table>
</Fragment>
);
return items.length ? table : emptyTip;
}
}
}
class Item extends Component {
constructor(props) {
super(props);
this.state = {
isOpIconShown: false,
highlight: false,
isDeleteDialogOpen: false,
isTransferDialogOpen: 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();
}
toggleDeleteDialog = () => {
this.setState({isDeleteDialogOpen: !this.state.isDeleteDialogOpen});
}
deleteRepo = () => {
this.props.deleteRepo(this.props.item.id);
}
toggleTransferDialog = () => {
this.setState({isTransferDialogOpen: !this.state.isTransferDialogOpen});
}
transferRepo = (owner) => {
this.props.transferRepo(this.props.item.id, owner.email);
this.toggleTransferDialog();
}
renderRepoName = () => {
const { item } = this.props;
const repo = item;
if (repo.name) {
if (isPro && enableSysAdminViewRepo && !repo.encrypted) {
return <Link to={`${siteRoot}sys/libraries/${repo.id}/`}>{repo.name}</Link>;
} else {
return repo.name;
}
} else {
return gettext('Broken ({repo_id_placeholder})')
.replace('{repo_id_placeholder}', repo.id);
}
}
translateOperations = (item) => {
let translateResult = '';
switch (item) {
case 'Delete':
translateResult = gettext('Delete');
break;
case 'Transfer':
translateResult = gettext('Transfer');
break;
}
return translateResult;
}
onMenuItemClick = (operation) => {
switch(operation) {
case 'Delete':
this.toggleDeleteDialog();
break;
case 'Transfer':
this.toggleTransferDialog();
break;
}
}
render() {
const { item } = this.props;
const { isOpIconShown, isDeleteDialogOpen, isTransferDialogOpen } = this.state;
const iconUrl = Utils.getLibIconUrl(item);
const iconTitle = Utils.getLibIconTitle(item);
const itemName = '<span class="op-target">' + Utils.HTMLescape(item.name) + '</span>';
const deleteDialogMsg = gettext('Are you sure you want to delete {placeholder} ?').replace('{placeholder}', itemName);
return (
<Fragment>
<tr className={this.state.highlight ? 'tr-highlight' : ''} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<td><img src={iconUrl} title={iconTitle} alt={iconTitle} width="24" /></td>
<td>{this.renderRepoName()}</td>
<td>{Utils.bytesToSize(item.size)}</td>
<td>{moment(item.last_modify).fromNow()}</td>
<td>
{isOpIconShown &&
<OpMenu
operations={['Delete', 'Transfer']}
translateOperations={this.translateOperations}
onMenuItemClick={this.onMenuItemClick}
onFreezedItem={this.props.onFreezedItem}
onUnfreezedItem={this.onUnfreezedItem}
/>
}
</td>
</tr>
{isDeleteDialogOpen &&
<CommonOperationConfirmationDialog
title={gettext('Delete Library')}
message={deleteDialogMsg}
executeOperation={this.deleteRepo}
confirmBtnText={gettext('Delete')}
toggleDialog={this.toggleDeleteDialog}
/>
}
{isTransferDialogOpen &&
<TransferDialog
itemName={item.name}
submit={this.transferRepo}
canTransferToDept={false}
toggleDialog={this.toggleTransferDialog}
/>
}
</Fragment>
);
}
}
class Repos extends Component {
constructor(props) {
super(props);
this.state = {
loading: true,
errorMsg: '',
userInfo: {},
repoList: []
};
}
componentDidMount () {
seafileAPI.sysAdminGetUser(this.props.email).then((res) => {
this.setState({
userInfo: res.data
});
});
seafileAPI.sysAdminListReposByOwner(this.props.email).then(res => {
this.setState({
loading: false,
repoList: res.data.repos
});
}).catch((error) => {
if (error.response) {
if (error.response.status == 403) {
this.setState({
loading: false,
errorMsg: gettext('Permission denied')
});
location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`;
} else {
this.setState({
loading: false,
errorMsg: gettext('Error')
});
}
} else {
this.setState({
loading: false,
errorMsg: gettext('Please check the network.')
});
}
});
}
deleteRepo = (repoID) => {
seafileAPI.sysAdminDeleteRepo(repoID).then(res => {
let newRepoList = this.state.repoList.filter(item => {
return item.id != repoID;
});
this.setState({repoList: newRepoList});
toaster.success(gettext('Successfully deleted 1 item.'));
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
transferRepo = (repoID, email) => {
seafileAPI.sysAdminTransferRepo(repoID, email).then((res) => {
let newRepoList = this.state.repoList.filter(item => {
return item.id != repoID;
});
this.setState({repoList: newRepoList});
let message = gettext('Successfully transferred the library.');
toaster.success(message);
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
render() {
return (
<Fragment>
<MainPanelTopbar />
<div className="main-panel-center flex-row">
<div className="cur-view-container">
<Nav currentItem="owned-repos" email={this.props.email} userName={this.state.userInfo.name} />
<div className="cur-view-content">
<Content
loading={this.state.loading}
errorMsg={this.state.errorMsg}
items={this.state.repoList}
deleteRepo={this.deleteRepo}
transferRepo={this.transferRepo}
/>
</div>
</div>
</div>
</Fragment>
);
}
}
export default Repos;

View File

@@ -1,154 +0,0 @@
import React, { Component, Fragment } from 'react';
import { seafileAPI } from '../../../utils/seafile-api';
import { gettext } from '../../../utils/constants';
import { Utils } from '../../../utils/utils';
import EmptyTip from '../../../components/empty-tip';
import moment from 'moment';
import Loading from '../../../components/loading';
class Content extends Component {
constructor(props) {
super(props);
this.state = {
};
}
render() {
const { loading, errorMsg, items } = this.props;
if (loading) {
return <Loading />;
} else if (errorMsg) {
return <p className="error text-center">{errorMsg}</p>;
} else {
const emptyTip = (
<EmptyTip>
<h2>{gettext('This user has no shared libraries')}</h2>
</EmptyTip>
);
const table = (
<Fragment>
<table className="table-hover">
<thead>
<tr>
<th width="5%">{/*icon*/}</th>
<th width="20%">{gettext('Name')}</th>
<th width="10%">{gettext('Share From')}</th>
<th width="25%">{gettext('Size')}</th>
<th width="19%">{gettext('Last Update')}</th>
</tr>
</thead>
<tbody>
{items.map((item, index) => {
return (<Item
key={index}
item={item}
deleteRepo={this.props.deleteRepo}
transferRepo={this.props.transferRepo}
/>);
})}
</tbody>
</table>
</Fragment>
);
return items.length ? table : emptyTip;
}
}
}
class Item extends Component {
constructor(props) {
super(props);
this.state = {
showOpIcon: false,
role: this.props.item.role,
quota_total: this.props.item.quota_total
};
}
handleMouseOver = () => {
this.setState({showOpIcon: true});
}
handleMouseOut = () => {
this.setState({showOpIcon: false});
}
render() {
let { item } = this.props;
let iconUrl = Utils.getLibIconUrl(item);
let iconTitle = Utils.getLibIconTitle(item);
return (
<Fragment>
<tr onMouseEnter={this.handleMouseOver} onMouseLeave={this.handleMouseOut}>
<td><img src={iconUrl} title={iconTitle} alt={iconTitle} width="24" /></td>
<td>{item.name}</td>
<td>{item.owner}</td>
<td>{Utils.bytesToSize(item.size)}</td>
<td>{moment(item.last_modify).fromNow()}</td>
</tr>
</Fragment>
);
}
}
class UserShareInRepos extends Component {
constructor(props) {
super(props);
this.state = {
loading: true,
errorMsg: '',
repoList: [],
isShowImportWaitingDialog: false,
isShowAddUserWaitingDialog: false
};
}
componentDidMount () {
seafileAPI.sysAdminListShareInRepo(this.props.email).then(res => {
this.setState({
repoList: res.data.repo_list,
loading: false
});
}).catch((error) => {
if (error.response) {
if (error.response.status == 403) {
this.setState({
loading: false,
errorMsg: gettext('Permission denied')
});
} else {
this.setState({
loading: false,
errorMsg: gettext('Error')
});
}
} else {
this.setState({
loading: false,
errorMsg: gettext('Please check the network.')
});
}
});
}
render() {
return (
<div className="cur-view-content">
<Content
loading={this.state.loading}
errorMsg={this.state.errorMsg}
items={this.state.repoList}
/>
</div>
);
}
}
export default UserShareInRepos;

View File

@@ -1,255 +0,0 @@
import React, { Component, Fragment } from 'react';
import { seafileAPI } from '../../../utils/seafile-api';
import { gettext } from '../../../utils/constants';
import toaster from '../../../components/toast';
import { Utils } from '../../../utils/utils';
import EmptyTip from '../../../components/empty-tip';
import Loading from '../../../components/loading';
import CommonOperationConfirmationDialog from '../../../components/dialog/common-operation-confirmation-dialog';
class Content extends Component {
constructor(props) {
super(props);
}
render() {
const { loading, errorMsg, shareLinkItems, uploadLinkItems } = this.props;
if (loading) {
return <Loading />;
} else if (errorMsg) {
return <p className="error text-center">{errorMsg}</p>;
} else {
const emptyTip = (
<EmptyTip>
<h2>{gettext('This user has not created any shared links')}</h2>
</EmptyTip>
);
const table = (
<Fragment>
<table className="table-hover">
<thead>
<tr>
<th width="5%">{/*icon*/}</th>
<th width="20%">{gettext('Name')}</th>
<th width="25%">{gettext('Size')}</th>
<th width="10%">{gettext('Type')}</th>
<th width="10%">{gettext('Visits')}</th>
<th width="10%">{/*Operations*/}</th>
</tr>
</thead>
<tbody>
{shareLinkItems.length > 0 && shareLinkItems.map((item, index) => {
return (<Item
key={index}
item={item}
type={'Download'}
deleteLink={this.props.deleteLink}
/>);
})}
{uploadLinkItems.length > 0 && uploadLinkItems.map((item, index) => {
return (<Item
key={index}
item={item}
type={'Upload'}
deleteLink={this.props.deleteLink}
/>);
})}
</tbody>
</table>
</Fragment>
);
return shareLinkItems.length || uploadLinkItems.length ? table : emptyTip;
}
}
}
class Item extends Component {
constructor(props) {
super(props);
this.state = {
showOpIcon: false,
isDeleteDialogOpen: false
};
}
handleMouseOver = () => {
this.setState({showOpIcon: true});
}
handleMouseOut = () => {
this.setState({showOpIcon: false});
}
toggleDeleteDialog = () => {
this.setState({isDeleteDialogOpen: !this.state.isDeleteDialogOpen});
}
deleteLink = () => {
this.props.deleteLink(this.props.item.token, this.props.type);
this.toggleDeleteDialog();
}
render() {
let { showOpIcon, isDeleteDialogOpen } = this.state;
let { item, type } = this.props;
let iconUrl;
if (type == 'Upload' || (type== 'Download' && item.is_dir)) {
iconUrl = Utils.getFolderIconUrl();
} else {
iconUrl = Utils.getFileIconUrl(item.obj_name);
}
let itemName = '<span class="op-target">' + Utils.HTMLescape(item.obj_name == '/' ? item.repo_name : item.obj_name) + '</span>';
let deleteDialogMsg = gettext('Are you sure you want to delete link {placeholder} ?'.replace('{placeholder}', itemName))
let iconVisibility = showOpIcon ? '' : ' invisible';
let deleteIconClassName = 'op-icon sf2-icon-delete' + iconVisibility;
return (
<Fragment>
<tr onMouseEnter={this.handleMouseOver} onMouseLeave={this.handleMouseOut}>
<td><img src={iconUrl} alt="" width="24" /></td>
<td>{item.obj_name == '/' ? item.repo_name : item.obj_name}</td>
<td>{Utils.bytesToSize(item.size)}</td>
<td>{type}</td>
<td>{item.view_cnt}</td>
<td>
<a href="#" className={deleteIconClassName} title={gettext('Delete')} onClick={this.toggleDeleteDialog}></a>
</td>
</tr>
{isDeleteDialogOpen &&
<CommonOperationConfirmationDialog
title={gettext('Delete Link')}
message={deleteDialogMsg}
executeOperation={this.deleteLink}
confirmBtnText={gettext('Delete')}
toggleDialog={this.toggleDeleteDialog}
/>
}
</Fragment>
);
}
}
class UserShareLinks extends Component {
constructor(props) {
super(props);
this.state = {
loading: true,
errorMsg: '',
repoList: {},
shareLinkList: {},
uploadLinkList: {},
isShowImportWaitingDialog: false
};
}
componentDidMount () {
this.getShareLinkList();
this.getUploadLinkList();
}
getShareLinkList = () => {
seafileAPI.sysAdminListShareLinksByUser(this.props.email).then(res => {
this.setState({
shareLinkList: res.data.share_link_list,
loading: false
});
}).catch((error) => {
if (error.response) {
if (error.response.status == 403) {
this.setState({
loading: false,
errorMsg: gettext('Permission denied')
});
} else {
this.setState({
loading: false,
errorMsg: gettext('Error')
});
}
} else {
this.setState({
loading: false,
errorMsg: gettext('Please check the network.')
});
}
});
}
getUploadLinkList = () => {
seafileAPI.sysAdminListUploadLinksByUser(this.props.email).then(res => {
this.setState({
uploadLinkList: res.data.upload_link_list,
loading: false
});
}).catch((error) => {
if (error.response) {
if (error.response.status == 403) {
this.setState({
loading: false,
errorMsg: gettext('Permission denied')
});
} else {
this.setState({
loading: false,
errorMsg: gettext('Error')
});
}
} else {
this.setState({
loading: false,
errorMsg: gettext('Please check the network.')
});
}
});
}
deleteLink = (token, type) => {
if (type == 'Download') {
seafileAPI.sysAdminRemoveShareLink(token).then(res => {
let newShareLinkList = this.state.shareLinkList.filter(item=> {
return item.token != token;
});
this.setState({
shareLinkList: newShareLinkList
});
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
} else if (type == 'Upload') {
seafileAPI.sysAdminRemoveShareLink(token).then(res => {
let newUploadLinkList = this.state.uploadLinkList.filter(item=> {
return item.token != token;
});
this.setState({
uploadLinkList: newUploadLinkList
});
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
}
render() {
return (
<div className="cur-view-content">
<Content
loading={this.state.loading}
errorMsg={this.state.errorMsg}
shareLinkItems={this.state.shareLinkList}
uploadLinkItems={this.state.uploadLinkList}
deleteLink={this.deleteLink}
/>
</div>
);
}
}
export default UserShareLinks;

View File

@@ -0,0 +1,183 @@
import React, { Component, Fragment } from 'react';
import { Link } from '@reach/router';
import moment from 'moment';
import { Utils } from '../../../utils/utils';
import { seafileAPI } from '../../../utils/seafile-api';
import { isPro, siteRoot, loginUrl, gettext } from '../../../utils/constants';
import toaster from '../../../components/toast';
import EmptyTip from '../../../components/empty-tip';
import Loading from '../../../components/loading';
import CommonOperationConfirmationDialog from '../../../components/dialog/common-operation-confirmation-dialog';
import TransferDialog from '../../../components/dialog/transfer-dialog';
import MainPanelTopbar from '../main-panel-topbar';
import Nav from './user-nav';
import OpMenu from './user-op-menu';
const { enableSysAdminViewRepo } = window.sysadmin.pageOptions;
class Content extends Component {
constructor(props) {
super(props);
}
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>
<h2>{gettext('No libraries')}</h2>
</EmptyTip>
);
const table = (
<Fragment>
<table className="table-hover">
<thead>
<tr>
<th width="5%"></th>
<th width="35%">{gettext('Name')}</th>
<th width="20%">{gettext('Share From')}</th>
<th width="20%">{gettext('Size')}</th>
<th width="20%">{gettext('Last Update')}</th>
</tr>
</thead>
<tbody>
{items.map((item, index) => {
return (<Item
key={index}
item={item}
/>);
})}
</tbody>
</table>
</Fragment>
);
return items.length ? table : emptyTip;
}
}
}
class Item extends Component {
constructor(props) {
super(props);
}
renderRepoName = () => {
const { item } = this.props;
const repo = item;
if (repo.name) {
if (isPro && enableSysAdminViewRepo && !repo.encrypted) {
return <Link to={`${siteRoot}sys/libraries/${repo.id}/`}>{repo.name}</Link>;
} else {
return repo.name;
}
} else {
return gettext('Broken ({repo_id_placeholder})')
.replace('{repo_id_placeholder}', repo.id);
}
}
getOwnerUrl = () => {
let url;
const { item } = this.props;
const index = item.owner_email.indexOf('@seafile_group');
if (index == -1) {
url = `${siteRoot}sys/users/${encodeURIComponent(item.owner_email)}/`;
} else {
const groupID = item.owner_email.substring(0, index);
url = `${siteRoot}sys/departments/${groupID}/`;
}
return url;
}
render() {
const { item } = this.props;
const iconUrl = Utils.getLibIconUrl(item);
const iconTitle = Utils.getLibIconTitle(item);
return (
<Fragment>
<tr>
<td><img src={iconUrl} title={iconTitle} alt={iconTitle} width="24" /></td>
<td>{this.renderRepoName()}</td>
<td><Link to={this.getOwnerUrl()}>{item.owner_name}</Link></td>
<td>{Utils.bytesToSize(item.size)}</td>
<td>{moment(item.last_modify).fromNow()}</td>
</tr>
</Fragment>
);
}
}
class Repos extends Component {
constructor(props) {
super(props);
this.state = {
loading: true,
errorMsg: '',
userInfo: {},
repoList: []
};
}
componentDidMount () {
seafileAPI.sysAdminGetUser(this.props.email).then((res) => {
this.setState({
userInfo: res.data
});
});
seafileAPI.sysAdminListShareInRepos(this.props.email).then(res => {
this.setState({
loading: false,
repoList: res.data.repo_list
});
}).catch((error) => {
if (error.response) {
if (error.response.status == 403) {
this.setState({
loading: false,
errorMsg: gettext('Permission denied')
});
location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`;
} else {
this.setState({
loading: false,
errorMsg: gettext('Error')
});
}
} else {
this.setState({
loading: false,
errorMsg: gettext('Please check the network.')
});
}
});
}
render() {
return (
<Fragment>
<MainPanelTopbar />
<div className="main-panel-center flex-row">
<div className="cur-view-container">
<Nav currentItem="shared-repos" email={this.props.email} userName={this.state.userInfo.name} />
<div className="cur-view-content">
<Content
loading={this.state.loading}
errorMsg={this.state.errorMsg}
items={this.state.repoList}
/>
</div>
</div>
</div>
</Fragment>
);
}
}
export default Repos;

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Link } from '@reach/router';
import { siteRoot, gettext, haveLDAP } from '../../../utils/constants';
import { siteRoot, gettext, haveLDAP, isDefaultAdmin } from '../../../utils/constants';
const propTypes = {
currentItem: PropTypes.string.isRequired
@@ -12,19 +12,19 @@ class Nav extends React.Component {
constructor(props) {
super(props);
this.navItems = [
{name: 'database', urlPart: 'users', text: gettext('Database')},
{name: 'admin', urlPart: 'users/admins', text: gettext('Admin')}
{name: 'database', urlPart: 'users', text: gettext('Database')}
];
/*
if (haveLDAP) {
this.navItems.splice(1, 0,
this.navItems.push(
{name: 'ldap', urlPart: 'users/ldap', text: gettext('LDAP')},
{name: 'ldap-imported', urlPart: 'users/ldap-imported', text: gettext('LDAP(imported)')}
);
this.navItems.splice(1, 0,
{name: 'ldap', urlPart: 'users/ldap', text: gettext('LDAP')}
}
if (isDefaultAdmin) {
this.navItems.push(
{name: 'admin', urlPart: 'users/admins', text: gettext('Admin')}
);
}
*/
}
render() {

View File

@@ -4,7 +4,7 @@ import { Button } from 'reactstrap';
import moment from 'moment';
import { Utils } from '../../../utils/utils';
import { seafileAPI } from '../../../utils/seafile-api';
import { isPro, username, gettext, multiInstitution, siteRoot } from '../../../utils/constants';
import { isPro, username, gettext, multiInstitution, siteRoot, loginUrl } from '../../../utils/constants';
import toaster from '../../../components/toast';
import EmptyTip from '../../../components/empty-tip';
import Loading from '../../../components/loading';
@@ -386,14 +386,14 @@ class Item extends Component {
</td>
{(multiInstitution && !isAdmin) &&
<td>
<SelectEditor
isTextMode={true}
isEditIconShow={isOpIconShown && institutions.length > 0}
options={institutions}
currentOption={item.institution}
onOptionChanged={this.updateInstitution}
translateOption={this.translateInstitution}
/>
<SelectEditor
isTextMode={true}
isEditIconShow={isOpIconShown && institutions.length > 0}
options={institutions}
currentOption={item.institution}
onOptionChanged={this.updateInstitution}
translateOption={this.translateInstitution}
/>
</td>
}
<td>
@@ -574,6 +574,7 @@ class UsersAll extends Component {
loading: false,
errorMsg: gettext('Permission denied')
});
location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`;
} else {
this.setState({
loading: false,
@@ -597,7 +598,7 @@ class UsersAll extends Component {
userList: users,
loading: false,
hasNextPage: Utils.hasNextPage(page, perPage, res.data.total_count),
currentPage: page,
currentPage: page
});
}).catch((error) => {
if (error.response) {
@@ -659,17 +660,28 @@ class UsersAll extends Component {
return user.email;
});
seafileAPI.sysAdminDeleteUserInBatch(emails).then(res => {
let oldUserList = this.state.userList;
let newUserList = oldUserList.filter(oldUser => {
return !res.data.success.some(deletedUser =>{
return deletedUser.email == oldUser.email;
if (res.data.success.length) {
let oldUserList = this.state.userList;
let newUserList = oldUserList.filter(oldUser => {
return !res.data.success.some(deletedUser =>{
return deletedUser.email == oldUser.email;
});
});
this.setState({
userList: newUserList,
hasUserSelected: emails.length != res.data.success.length
});
const length = res.data.success.length;
const msg = length == 1 ?
gettext('Successfully deleted 1 user.') :
gettext('Successfully deleted {user_number_placeholder} users.')
.replace('{user_number_placeholder}', length);
toaster.success(msg);
}
res.data.failed.map(item => {
const msg = `${item.email}: ${item.error_msg}`;
toaster.danger(msg);
});
this.setState({
userList: newUserList,
hasUserSelected: emails.length != res .data.success.length
});
// todo: msg
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
@@ -678,13 +690,22 @@ class UsersAll extends Component {
importUserInBatch = (file) => {
toaster.notify(gettext('It may take some time, please wait.'));
// TODO: the url needs to be changed
seafileAPI.sysAdminImportUserViaFile(file).then((res) => {
// currently using old view in python, no return newUserList,
// so after import new users, just send a get user list again.
this.toggleImportUserDialog();
this.getUsersListByPage(1);
toaster.success(gettext('Import succeeded.'));
if (res.data.success.length) {
const users = res.data.success.map(item => {
if (item.institution == undefined) {
item.institution = '';
}
return new SysAdminUser(item);
});
this.setState({
userList: users.concat(this.state.userList)
});
}
res.data.failed.map(item => {
const msg = `${item.email}: ${item.error_msg}`;
toaster.danger(msg);
});
}).catch((error) => {
let errMsg = Utils.getErrorMsg(error);
toaster.danger(errMsg);
@@ -853,6 +874,7 @@ class UsersAll extends Component {
hasNextPage={this.state.hasNextPage}
curPerPage={this.state.perPage}
resetPerPage={this.resetPerPage}
getListByPage={this.getUsersListByPage}
updateUser={this.updateUser}
deleteUser={this.deleteUser}
updateAdminRole={this.updateAdminRole}
@@ -860,7 +882,6 @@ class UsersAll extends Component {
onUserSelected={this.onUserSelected}
isAllUsersSelected={this.isAllUsersSelected}
toggleSelectAllUsers={this.toggleSelectAllUsers}
getListByPage={this.getUsersListByPage}
/>
</div>
</div>

View File

@@ -125,7 +125,4 @@ export const canViewUserLog = window.sysadmin ? window.sysadmin.pageOptions.admi
export const canViewAdminLog = window.sysadmin ? window.sysadmin.pageOptions.admin_permissions.can_view_admin_log : '';
export const enableWorkWeixin = window.sysadmin ? window.sysadmin.pageOptions.enable_work_weixin : '';
export const enableSysAdminViewRepo = window.sysadmin ? window.sysadmin.pageOptions.enableSysAdminViewRepo : '';
export const enableTwoFactorAuth = window.sysadmin ? window.sysadmin.pageOptions.enable_two_factor_auth : '';
export const sendEmailOnResettingUserPasswd = window.sysadmin ? window.sysadmin.pageOptions.send_email_on_resetting_user_passwd : '';
export const isEmailConfiguredInSysAdmin = window.sysadmin ? window.sysadmin.pageOptions.is_email_configured : '';
export const haveLDAP = window.sysadmin ? window.sysadmin.pageOptions.haveLDAP : '';