1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-03 16:10:26 +00:00

sysadmin reconstrcut orgs frontend (#4074)

* sysadmin reconstruct orgs page

* [system admin] orgs: refactored it
This commit is contained in:
Leo
2019-10-21 09:45:00 +08:00
committed by Daniel Pan
parent e9f511bcd9
commit 07059239d1
21 changed files with 2010 additions and 35 deletions

View File

@@ -0,0 +1,127 @@
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';
const propTypes = {
toggleDialog: PropTypes.func.isRequired,
addOrg: PropTypes.func.isRequired
};
class SysAdminAddOrgDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
name: '',
email: '',
password: '',
passwordAgain: '',
errorMsg: '',
isSubmitBtnActive: false
};
}
checkSubmitBtnActive = () => {
const { name, email, password, passwordAgain } = this.state;
let btnActive = true;
if (name !='' &&
email != '' &&
password != '' &&
passwordAgain != '') {
btnActive = true;
} else {
btnActive = false;
}
this.setState({
isSubmitBtnActive: btnActive
});
}
toggle = () => {
this.props.toggleDialog();
}
inputPassword = (e) => {
let passwd = e.target.value.trim();
this.setState({
password: passwd
}, this.checkSubmitBtnActive);
}
inputPasswordAgain = (e) => {
let passwd = e.target.value.trim();
this.setState({
passwordAgain: passwd
}, this.checkSubmitBtnActive);
}
inputEmail = (e) => {
let email = e.target.value.trim();
this.setState({
email: email
}, this.checkSubmitBtnActive);
}
inputName = (e) => {
let name = e.target.value.trim();
this.setState({
name: name
}, this.checkSubmitBtnActive);
}
handleSubmit = () => {
let { name, email, password, passwordAgain } = this.state;
if (password != passwordAgain) {
this.setState({errorMsg: gettext('Passwords do not match.')});
return;
}
const data = {
orgName: name,
ownerEmail: email,
password: password
};
this.props.addOrg(data);
this.toggle();
}
render() {
const { errorMsg, password, passwordAgain, email, name, isSubmitBtnActive } = this.state;
return (
<Modal isOpen={true} toggle={this.toggle}>
<ModalHeader toggle={this.toggle}>{gettext('Add Organization')}</ModalHeader>
<ModalBody>
<Form autoComplete="off">
<FormGroup>
<Label>{gettext('Name')}</Label>
<Input value={name} onChange={this.inputName} />
</FormGroup>
<FormGroup>
<Label>
{gettext('Owner')}
<span className="small text-secondary ml-1 fas fa-question-circle" title={gettext('Owner can use admin panel in an organization, must be a new account.')}></span>
</Label>
<Input value={email} onChange={this.inputEmail} />
</FormGroup>
<FormGroup>
<Label>{gettext('Password')}</Label>
<Input type="password" value={password} onChange={this.inputPassword} />
</FormGroup>
<FormGroup>
<Label>{gettext('Password again')}</Label>
<Input type="password" value={passwordAgain} onChange={this.inputPasswordAgain} />
</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} disabled={!isSubmitBtnActive}>{gettext('Submit')}</Button>
</ModalFooter>
</Modal>
);
}
}
SysAdminAddOrgDialog.propTypes = propTypes;
export default SysAdminAddOrgDialog;

View File

@@ -0,0 +1,149 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Alert, Modal, ModalHeader, ModalBody, ModalFooter, Button, Form, FormGroup, Label, Input, InputGroup, InputGroupAddon } from 'reactstrap';
import { gettext } from '../../../utils/constants';
import { Utils } from '../../../utils/utils';
import SysAdminUserRoleEditor from '../../../components/select-editor/sysadmin-user-role-editor';
const propTypes = {
toggleDialog: PropTypes.func.isRequired,
addUser: PropTypes.func.isRequired
};
class SysAdminAddUserDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
errorMsg: '',
isPasswordVisible: false,
password: '',
passwordAgain: '',
email: '',
name: '',
isSubmitBtnActive: false
};
}
checkSubmitBtnActive = () => {
const { email, password, passwordAgain } = this.state;
let btnActive = true;
if (email != '' &&
password != '' &&
passwordAgain != '') {
btnActive = true;
} else {
btnActive = false;
}
this.setState({
isSubmitBtnActive: btnActive
});
}
toggle = () => {
this.props.toggleDialog();
}
togglePasswordVisible = () => {
this.setState({isPasswordVisible: !this.state.isPasswordVisible});
}
inputPassword = (e) => {
let passwd = e.target.value.trim();
this.setState({
password: passwd,
errorMsg: ''
}, this.checkSubmitBtnActive);
}
inputPasswordAgain = (e) => {
let passwd = e.target.value.trim();
this.setState({
passwordAgain: passwd,
errorMsg: ''
}, this.checkSubmitBtnActive);
}
generatePassword = () => {
let val = Utils.generatePassword(8);
this.setState({
password: val,
passwordAgain: val
}, this.checkSubmitBtnActive);
}
inputEmail = (e) => {
let email = e.target.value.trim();
this.setState({
email: email
}, this.checkSubmitBtnActive);
}
inputName = (e) => {
let name = e.target.value.trim();
this.setState({
name: name
});
}
handleSubmit = () => {
const { email, password, passwordAgain, name } = this.state;
if (password != passwordAgain) {
this.setState({errorMsg: gettext('Passwords do not match.')});
return;
}
const data = {
email: email,
name: name,
password: password
};
this.props.addUser(data);
this.toggle();
}
render() {
const { errorMsg, isPasswordVisible, password, passwordAgain, email, name,
isSubmitBtnActive
} = this.state;
return (
<Modal isOpen={true} toggle={this.toggle}>
<ModalHeader toggle={this.toggle}>{gettext('Add Member')}</ModalHeader>
<ModalBody>
<Form autoComplete="off">
<FormGroup>
<Label>{gettext('Email')}</Label>
<Input value={email} onChange={this.inputEmail} />
</FormGroup>
<FormGroup>
<Label>{gettext('Name(optional)')}</Label>
<Input autoComplete="new-password" value={name} onChange={this.inputName} />
</FormGroup>
<FormGroup>
<Label>{gettext('Password')}</Label>
<InputGroup>
<Input autoComplete="new-password" type={isPasswordVisible ? 'text' : 'password'} value={password || ''} onChange={this.inputPassword} />
<InputGroupAddon addonType="append">
<Button className="mt-0" onClick={this.togglePasswordVisible}><i className={`link-operation-icon fas ${this.state.isPasswordVisible ? 'fa-eye': 'fa-eye-slash'}`}></i></Button>
<Button className="mt-0" onClick={this.generatePassword}><i className="link-operation-icon fas fa-magic"></i></Button>
</InputGroupAddon>
</InputGroup>
</FormGroup>
<FormGroup>
<Label>{gettext('Password again')}</Label>
<Input type={isPasswordVisible ? 'text' : 'password'} value={passwordAgain || ''} onChange={this.inputPasswordAgain} />
</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} disabled={!isSubmitBtnActive}>{gettext('Submit')}</Button>
</ModalFooter>
</Modal>
);
}
}
SysAdminAddUserDialog.propTypes = propTypes;
export default SysAdminAddUserDialog;

View File

@@ -0,0 +1,75 @@
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 = {
toggle: PropTypes.func.isRequired,
updateValue: PropTypes.func.isRequired
};
class SysAdminSetOrgMaxUserNumberDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
value: this.props.value,
isSubmitBtnActive: false
};
}
toggle = () => {
this.props.toggle();
}
handleInputChange = (e) => {
const value = e.target.value.trim();
this.setState({
value: value,
isSubmitBtnActive: value != ''
});
}
handleKeyPress = (e) => {
if (e.key == 'Enter') {
this.handleSubmit();
e.preventDefault();
}
}
handleSubmit = () => {
this.props.updateValue(this.state.value);
this.toggle();
}
render() {
const { value, isSubmitBtnActive } = this.state;
return (
<Modal isOpen={true} toggle={this.toggle}>
<ModalHeader toggle={this.toggle}>{gettext('Set max number of members')}</ModalHeader>
<ModalBody>
<Form>
<FormGroup>
<Input
type="text"
className="form-control"
value={value}
onKeyPress={this.handleKeyPress}
onChange={this.handleInputChange}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={this.toggle}>{gettext('Cancel')}</Button>
<Button color="primary" onClick={this.handleSubmit} disabled={!isSubmitBtnActive}>{gettext('Submit')}</Button>
</ModalFooter>
</Modal>
);
}
}
SysAdminSetOrgMaxUserNumberDialog.propTypes = propTypes;
export default SysAdminSetOrgMaxUserNumberDialog;

View File

@@ -0,0 +1,75 @@
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 = {
toggle: PropTypes.func.isRequired,
updateName: PropTypes.func.isRequired
};
class SysAdminSetOrgNameDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
name: this.props.name,
isSubmitBtnActive: false
};
}
toggle = () => {
this.props.toggle();
}
handleInputChange = (e) => {
const value = e.target.value.trim();
this.setState({
name: value,
isSubmitBtnActive: value != ''
});
}
handleKeyPress = (e) => {
if (e.key == 'Enter') {
this.handleSubmit();
e.preventDefault();
}
}
handleSubmit = () => {
this.props.updateName(this.state.name);
this.toggle();
}
render() {
const { name, isSubmitBtnActive } = this.state;
return (
<Modal isOpen={true} toggle={this.toggle}>
<ModalHeader toggle={this.toggle}>{gettext('Set Name')}</ModalHeader>
<ModalBody>
<Form>
<FormGroup>
<Input
type="text"
className="form-control"
value={name}
onKeyPress={this.handleKeyPress}
onChange={this.handleInputChange}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={this.toggle}>{gettext('Cancel')}</Button>
<Button color="primary" onClick={this.handleSubmit} disabled={!isSubmitBtnActive}>{gettext('Submit')}</Button>
</ModalFooter>
</Modal>
);
}
}
SysAdminSetOrgNameDialog.propTypes = propTypes;
export default SysAdminSetOrgNameDialog;

View File

@@ -0,0 +1,85 @@
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 = {
toggle: PropTypes.func.isRequired,
updateQuota: PropTypes.func.isRequired
};
class SysAdminOrgSetQuotaDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
quota: '',
isSubmitBtnActive: false
};
}
toggle = () => {
this.props.toggle();
}
handleQuotaChange = (e) => {
const value = e.target.value.trim();
this.setState({
quota: value,
isSubmitBtnActive: value != ''
});
}
handleKeyPress = (e) => {
if (e.key == 'Enter') {
this.handleSubmit();
e.preventDefault();
}
}
handleSubmit = () => {
this.props.updateQuota(this.state.quota);
this.toggle();
}
render() {
const { quota, isSubmitBtnActive } = this.state;
return (
<Modal isOpen={true} toggle={this.toggle}>
<ModalHeader toggle={this.toggle}>{gettext('Set Quota')}</ModalHeader>
<ModalBody>
<Form>
<FormGroup>
<InputGroup>
<Input
type="text"
className="form-control"
value={quota}
onKeyPress={this.handleKeyPress}
onChange={this.handleQuotaChange}
/>
<InputGroupAddon addonType="append">
<InputGroupText>MB</InputGroupText>
</InputGroupAddon>
</InputGroup>
<p className="small text-secondary mt-2 mb-2">
{gettext('An integer that is greater than or equal to 0.')}
<br />
{gettext('Tip: 0 means default limit')}
</p>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={this.toggle}>{gettext('Cancel')}</Button>
<Button color="primary" onClick={this.handleSubmit} disabled={!isSubmitBtnActive}>{gettext('Submit')}</Button>
</ModalFooter>
</Modal>
);
}
}
SysAdminOrgSetQuotaDialog.propTypes = propTypes;
export default SysAdminOrgSetQuotaDialog;

View File

@@ -0,0 +1,43 @@
import React from 'react';
import PropTypes from 'prop-types';
import { gettext } from '../../utils/constants';
import SelectEditor from './select-editor';
const propTypes = {
isTextMode: PropTypes.bool.isRequired,
isEditIconShow: PropTypes.bool.isRequired,
roleOptions: PropTypes.array.isRequired,
currentRole: PropTypes.string.isRequired,
onRoleChanged: PropTypes.func.isRequired
};
class SysAdminUserRoleEditor extends React.Component {
translateRoles = (role) => {
switch (role) {
case 'default':
return gettext('Default');
case 'guest':
return gettext('Guest');
default:
return role;
}
}
render() {
return (
<SelectEditor
isTextMode={this.props.isTextMode}
isEditIconShow={this.props.isEditIconShow}
options={this.props.roleOptions}
currentOption={this.props.currentRole}
onOptionChanged={this.props.onRoleChanged}
translateOption={this.translateRoles}
/>
);
}
}
SysAdminUserRoleEditor.propTypes = propTypes;
export default SysAdminUserRoleEditor;

View File

@@ -0,0 +1,41 @@
import React from 'react';
import PropTypes from 'prop-types';
import { gettext } from '../../utils/constants';
import SelectEditor from './select-editor';
const propTypes = {
isTextMode: PropTypes.bool.isRequired,
isEditIconShow: PropTypes.bool.isRequired,
statusOptions: PropTypes.array.isRequired,
currentStatus: PropTypes.string.isRequired,
onStatusChanged: PropTypes.func.isRequired
};
class SysAdminUserStatusEditor extends React.Component {
translateStatus = (status) => {
switch (status) {
case 'active':
return gettext('Active');
case 'inactive':
return gettext('Inactive');
}
}
render() {
return (
<SelectEditor
isTextMode={this.props.isTextMode}
isEditIconShow={this.props.isEditIconShow}
options={this.props.statusOptions}
currentOption={this.props.currentStatus}
onOptionChanged={this.props.onStatusChanged}
translateOption={this.translateStatus}
/>
);
}
}
SysAdminUserStatusEditor.propTypes = propTypes;
export default SysAdminUserStatusEditor;

View File

@@ -27,6 +27,12 @@ import DepartmentDetail from './departments/department-detail';
import ShareLinks from './links/share-links'; import ShareLinks from './links/share-links';
import UploadLinks from './links/upload-links'; import UploadLinks from './links/upload-links';
import Orgs from './orgs/orgs';
import OrgInfo from './orgs/org-info';
import OrgUsers from './orgs/org-users';
import OrgGroups from './orgs/org-groups';
import OrgRepos from './orgs/org-repos';
import WebSettings from './web-settings/web-settings'; import WebSettings from './web-settings/web-settings';
import Notifications from './notifications/notifications'; import Notifications from './notifications/notifications';
import FileScanRecords from './file-scan-records'; import FileScanRecords from './file-scan-records';
@@ -64,6 +70,10 @@ class SysAdmin extends React.Component {
tab: 'groups', tab: 'groups',
urlPartList: ['groups/'] urlPartList: ['groups/']
}, },
{
tab: 'organizations',
urlPartList: ['organizations/']
},
]; ];
const tmpTab = this.getCurrentTabForPageList(pageList); const tmpTab = this.getCurrentTabForPageList(pageList);
currentTab = tmpTab ? tmpTab : currentTab; currentTab = tmpTab ? tmpTab : currentTab;
@@ -125,6 +135,11 @@ class SysAdmin extends React.Component {
</Departments> </Departments>
<ShareLinks path={siteRoot + 'sys/share-links'} /> <ShareLinks path={siteRoot + 'sys/share-links'} />
<UploadLinks path={siteRoot + 'sys/upload-links'} /> <UploadLinks path={siteRoot + 'sys/upload-links'} />
<Orgs path={siteRoot + 'sys/organizations'} />
<OrgInfo path={siteRoot + 'sys/organizations/:orgID/info'} />
<OrgUsers path={siteRoot + 'sys/organizations/:orgID/users'} />
<OrgGroups path={siteRoot + 'sys/organizations/:orgID/groups'} />
<OrgRepos path={siteRoot + 'sys/organizations/:orgID/libraries'} />
<FileScanRecords <FileScanRecords
path={siteRoot + 'sys/file-scan-records'} path={siteRoot + 'sys/file-scan-records'}
currentTab={currentTab} currentTab={currentTab}

View File

@@ -0,0 +1,209 @@
import React, { Component, Fragment } from 'react';
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 OrgNav from './org-nav';
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 groups')}</h2>
</EmptyTip>
);
const table = (
<Fragment>
<table className="table-hover">
<thead>
<tr>
<th width="30%">{gettext('Name')}</th>
<th width="30%">{gettext('Creator')}</th>
<th width="30%">{gettext('Created At')}</th>
<th width="10%">{/* Operations */}</th>
</tr>
</thead>
<tbody>
{items.map((item, index) => {
return (<Item
key={index}
item={item}
deleteGroup={this.props.deleteGroup}
/>);
})}
</tbody>
</table>
</Fragment>
);
return items.length ? table : emptyTip;
}
}
}
class Item extends Component {
constructor(props) {
super(props);
this.state = {
isOpIconShown: false,
isDeleteDialogOpen: false
};
}
handleMouseEnter = () => {
this.setState({isOpIconShown: true});
}
handleMouseLeave = () => {
this.setState({isOpIconShown: false});
}
toggleDeleteDialog = (e) => {
if (e) {
e.preventDefault();
}
this.setState({isDeleteDialogOpen: !this.state.isDeleteDialogOpen});
}
deleteGroup = () => {
this.toggleDeleteDialog();
this.props.deleteGroup(this.props.item.group_id);
}
render() {
const { item } = this.props;
const { isOpIconShown, isDeleteDialogOpen } = this.state;
const itemName = '<span class="op-target">' + Utils.HTMLescape(item.group_name) + '</span>';
const deleteDialogMsg = gettext('Are you sure you want to delete {placeholder} ?').replace('{placeholder}', itemName);
const groupUrl = item.parent_group_id == 0 ?
`${siteRoot}sys/groups/${item.group_id}/libraries/` :
`${siteRoot}sysadmin/#address-book/groups/${item.group_id}/`;
return (
<Fragment>
<tr onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<td><a href={groupUrl}>{item.group_name}</a></td>
<td><a href={`${siteRoot}useradmin/info/${encodeURIComponent(item.creator_email)}/`}>{item.creator_name}</a></td>
<td>{moment(item.created_at).format('YYYY-MM-DD hh:mm:ss')}</td>
<td>
<a href="#" className={`action-icon sf2-icon-delete ${isOpIconShown ? '' : 'invisible'}`} title={gettext('Delete')} onClick={this.toggleDeleteDialog}></a>
</td>
</tr>
{isDeleteDialogOpen &&
<CommonOperationConfirmationDialog
title={gettext('Delete Group')}
message={deleteDialogMsg}
executeOperation={this.deleteGroup}
confirmBtnText={gettext('Delete')}
toggleDialog={this.toggleDeleteDialog}
/>
}
</Fragment>
);
}
}
class OrgGroups extends Component {
constructor(props) {
super(props);
this.state = {
loading: true,
errorMsg: '',
orgName: '',
groupList: []
};
}
componentDidMount () {
seafileAPI.sysAdminGetOrgInfo(this.props.orgID).then((res) => {
this.setState({
orgName: res.data.org_name
});
});
seafileAPI.sysAdminListAllOrgGroups(this.props.orgID).then((res) => {
this.setState({
loading: false,
groupList: res.data.group_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.')
});
}
});
}
deleteGroup = (groupID) => {
seafileAPI.sysAdminDismissGroupByID(groupID).then(res => {
let newGroupList = this.state.groupList.filter(item => {
return item.group_id != groupID;
});
this.setState({groupList: newGroupList});
toaster.success(gettext('Successfully deleted 1 item.'));
}).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">
<OrgNav
currentItem="groups"
orgID={this.props.orgID}
orgName={this.state.orgName}
/>
<div className="cur-view-content">
<Content
loading={this.state.loading}
errorMsg={this.state.errorMsg}
items={this.state.groupList}
deleteGroup={this.deleteGroup}
/>
</div>
</div>
</div>
</Fragment>
);
}
}
export default OrgGroups;

View File

@@ -0,0 +1,220 @@
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 Loading from '../../../components/loading';
import SysAdminSetOrgQuotaDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-set-org-quota-dialog';
import SysAdminSetOrgNameDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-set-org-name-dialog';
import SysAdminSetOrgMaxUserNumberDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-set-org-max-user-number-dialog';
import MainPanelTopbar from '../main-panel-topbar';
import OrgNav from './org-nav';
class Content extends Component {
constructor(props) {
super(props);
this.state = {
isSetQuotaDialogOpen: false,
isSetNameDialogOpen: false,
isSetMaxUserNumberDialogOpen: false
};
}
toggleSetQuotaDialog = () => {
this.setState({isSetQuotaDialogOpen: !this.state.isSetQuotaDialogOpen});
}
toggleSetNameDialog = () => {
this.setState({isSetNameDialogOpen: !this.state.isSetNameDialogOpen});
}
toggleSetMaxUserNumberDialog = () => {
this.setState({isSetMaxUserNumberDialogOpen: !this.state.isSetMaxUserNumberDialogOpen});
}
showEditIcon = (action) => {
return (
<span
title={gettext('Edit')}
className="fa fa-pencil-alt attr-action-icon"
onClick={action}>
</span>
);
}
render() {
const { loading, errorMsg, orgInfo } = this.props;
if (loading) {
return <Loading />;
} else if (errorMsg) {
return <p className="error text-center">{errorMsg}</p>;
} else {
const { org_name, users_count, max_user_number, groups_count, quota, quota_usage } = this.props.orgInfo;
const { isSetQuotaDialogOpen, isSetNameDialogOpen, isSetMaxUserNumberDialogOpen } = this.state;
return (
<Fragment>
<dl className="m-0">
<dt className="info-item-heading">{gettext('Name')}</dt>
<dd className="info-item-content">
{org_name}
{this.showEditIcon(this.toggleSetNameDialog)}
</dd>
<dt className="info-item-heading">{gettext('Number of members')}</dt>
<dd className="info-item-content">{users_count}</dd>
{max_user_number &&
<Fragment>
<dt className="info-item-heading">{gettext('Max number of members')}</dt>
<dd className="info-item-content">
{max_user_number}
{this.showEditIcon(this.toggleSetMaxUserNumberDialog)}
</dd>
</Fragment>
}
<dt className="info-item-heading">{gettext('Number of groups')}</dt>
<dd className="info-item-content">{groups_count}</dd>
<dt className="info-item-heading">{gettext('Space Used')}</dt>
<dd className="info-item-content">
{`${Utils.bytesToSize(quota_usage)} / ${quota > 0 ? Utils.bytesToSize(quota) : '--'}`}
{this.showEditIcon(this.toggleSetQuotaDialog)}
</dd>
</dl>
{isSetQuotaDialogOpen &&
<SysAdminSetOrgQuotaDialog
updateQuota={this.props.updateQuota}
toggle={this.toggleSetQuotaDialog}
/>
}
{isSetNameDialogOpen &&
<SysAdminSetOrgNameDialog
name={org_name}
updateName={this.props.updateName}
toggle={this.toggleSetNameDialog}
/>
}
{isSetMaxUserNumberDialogOpen &&
<SysAdminSetOrgMaxUserNumberDialog
value={max_user_number}
updateValue={this.props.updateMaxUserNumber}
toggle={this.toggleSetMaxUserNumberDialog}
/>
}
</Fragment>
);
}
}
}
class OrgInfo extends Component {
constructor(props) {
super(props);
this.state = {
loading: true,
errorMsg: '',
orgInfo: {}
};
}
componentDidMount () {
seafileAPI.sysAdminGetOrgInfo(this.props.orgID).then((res) => {
this.setState({
loading: false,
orgInfo: res.data
});
}).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.')
});
}
});
}
updateQuota = (quota) => {
const data = {quota: quota};
seafileAPI.sysAdminUpdateOrgInfo(this.props.orgID, data).then(res => {
const newOrgInfo = Object.assign(this.state.orgInfo, {
quota: res.data.quota
});
this.setState({orgInfo: newOrgInfo});
toaster.success(gettext('Successfully set quota.'));
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
updateName = (orgName) => {
const data = {orgName: orgName};
seafileAPI.sysAdminUpdateOrgInfo(this.props.orgID, data).then(res => {
const newOrgInfo = Object.assign(this.state.orgInfo, {
org_name: res.data.org_name
});
this.setState({orgInfo: newOrgInfo});
toaster.success(gettext('Successfully set name.'));
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
updateMaxUserNumber = (newValue) => {
const data = {maxUserNumber: newValue};
seafileAPI.sysAdminUpdateOrgInfo(this.props.orgID, data).then(res => {
const newOrgInfo = Object.assign(this.state.orgInfo, {
max_user_number: res.data.max_user_number
});
this.setState({orgInfo: newOrgInfo});
toaster.success(gettext('Successfully set max number of members.'));
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
render() {
const { orgInfo } = this.state;
return (
<Fragment>
<MainPanelTopbar />
<div className="main-panel-center flex-row">
<div className="cur-view-container">
<OrgNav currentItem="info" orgID={this.props.orgID} orgName={orgInfo.org_name} />
<div className="cur-view-content">
<Content
orgID={this.props.orgID}
loading={this.state.loading}
errorMsg={this.state.errorMsg}
orgInfo={this.state.orgInfo}
updateQuota={this.updateQuota}
updateName={this.updateName}
updateMaxUserNumber={this.updateMaxUserNumber}
/>
</div>
</div>
</div>
</Fragment>
);
}
}
export default OrgInfo;

View File

@@ -0,0 +1,47 @@
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: 'info', text: gettext('Info')},
{name: 'users', urlPart: 'users', text: gettext('Members')},
{name: 'groups', urlPart: 'groups', text: gettext('Groups')},
{name: 'repos', urlPart: 'libraries', text: gettext('Libraries')},
//{name: 'traffic', urlPart: 'traffic', text: gettext('traffic')},
//{name: 'settings', urlPart: 'settings', text: gettext('Settings')}
];
}
render() {
const { currentItem, orgID, orgName } = this.props;
return (
<div>
<div className="cur-view-path">
<h3 className="sf-heading"><Link to={`${siteRoot}sys/organizations/`}>{gettext('Organizations')}</Link> / {orgName}</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/organizations/${orgID}/${item.urlPart}/`} className={`nav-link ${currentItem == item.name ? ' active' : ''}`}>{item.text}</Link>
</li>
);
})}
</ul>
</div>
);
}
}
Nav.propTypes = propTypes;
export default Nav;

View File

@@ -0,0 +1,213 @@
import React, { Component, Fragment } from 'react';
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 OrgNav from './org-nav';
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="30%">{gettext('Name')}</th>
<th width="35%">{gettext('ID')}</th>
<th width="20%">{gettext('Owner')}</th>
<th width="10%">{/* Operations */}</th>
</tr>
</thead>
<tbody>
{items.map((item, index) => {
return (<Item
key={index}
item={item}
deleteRepo={this.props.deleteRepo}
/>);
})}
</tbody>
</table>
</Fragment>
);
return items.length ? table : emptyTip;
}
}
}
class Item extends Component {
constructor(props) {
super(props);
this.state = {
isOpIconShown: false,
isDeleteDialogOpen: false
};
}
handleMouseEnter = () => {
this.setState({isOpIconShown: true});
}
handleMouseLeave = () => {
this.setState({isOpIconShown: false});
}
toggleDeleteDialog = (e) => {
if (e) {
e.preventDefault();
}
this.setState({isDeleteDialogOpen: !this.state.isDeleteDialogOpen});
}
deleteRepo = () => {
this.props.deleteRepo(this.props.item.repo_id);
}
render() {
const { item } = this.props;
const { isOpIconShown, isDeleteDialogOpen } = this.state;
const iconUrl = Utils.getLibIconUrl(item);
const iconTitle = Utils.getLibIconTitle(item);
const itemName = '<span class="op-target">' + Utils.HTMLescape(item.repo_name) + '</span>';
const deleteDialogMsg = gettext('Are you sure you want to delete {placeholder} ?').replace('{placeholder}', itemName);
return (
<Fragment>
<tr onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<td><img src={iconUrl} title={iconTitle} alt={iconTitle} width="24" /></td>
<td>{item.repo_name}</td>
<td>{item.repo_id}</td>
<td>
{item.owner_email ?
<a href={`${siteRoot}useradmin/info/${encodeURIComponent(item.owner_email)}/`}>{item.owner_name}</a> :
'--'
}
</td>
<td>
<a href="#" className={`action-icon sf2-icon-delete ${isOpIconShown ? '' : 'invisible'}`} title={gettext('Delete')} onClick={this.toggleDeleteDialog}></a>
</td>
</tr>
{isDeleteDialogOpen &&
<CommonOperationConfirmationDialog
title={gettext('Delete Library')}
message={deleteDialogMsg}
executeOperation={this.deleteRepo}
confirmBtnText={gettext('Delete')}
toggleDialog={this.toggleDeleteDialog}
/>
}
</Fragment>
);
}
}
class OrgRepos extends Component {
constructor(props) {
super(props);
this.state = {
loading: true,
errorMsg: '',
orgName: '',
repoList: []
};
}
componentDidMount () {
seafileAPI.sysAdminGetOrgInfo(this.props.orgID).then((res) => {
this.setState({
orgName: res.data.org_name
});
});
seafileAPI.sysAdminListAllOrgRepos(this.props.orgID).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.')
});
}
});
}
deleteRepo = (repoID) => {
seafileAPI.sysAdminDeleteRepo(repoID).then(res => {
let newRepoList = this.state.repoList.filter(item => {
return item.repo_id != repoID;
});
this.setState({repoList: newRepoList});
toaster.success(gettext('Successfully deleted 1 item.'));
}).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">
<OrgNav
currentItem="repos"
orgID={this.props.orgID}
orgName={this.state.orgName}
/>
<div className="cur-view-content">
<Content
loading={this.state.loading}
errorMsg={this.state.errorMsg}
items={this.state.repoList}
deleteRepo={this.deleteRepo}
/>
</div>
</div>
</div>
</Fragment>
);
}
}
export default OrgRepos;

View File

@@ -0,0 +1,348 @@
import React, { Component, Fragment } from 'react';
import { Button } from 'reactstrap';
import moment from 'moment';
import { Utils } from '../../../utils/utils';
import { seafileAPI } from '../../../utils/seafile-api';
import { siteRoot, loginUrl, gettext, username } from '../../../utils/constants';
import toaster from '../../../components/toast';
import EmptyTip from '../../../components/empty-tip';
import Loading from '../../../components/loading';
import SysAdminUserStatusEditor from '../../../components/select-editor/sysadmin-user-status-editor';
import SysAdminAddUserDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-add-user-dialog';
import CommonOperationConfirmationDialog from '../../../components/dialog/common-operation-confirmation-dialog';
import MainPanelTopbar from '../main-panel-topbar';
import OrgNav from './org-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 members')}</h2>
</EmptyTip>
);
const table = (
<Fragment>
<table>
<thead>
<tr>
<th width="25%">{gettext('Name')}</th>
<th width="20%">{gettext('Status')}</th>
<th width="20%">{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}
updateStatus={this.props.updateStatus}
deleteUser={this.props.deleteUser}
/>);
})}
</tbody>
</table>
</Fragment>
);
return items.length ? table : emptyTip;
}
}
}
class Item extends 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;
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});
}
updateStatus= (statusValue) => {
this.props.updateStatus(this.props.item.email, statusValue);
}
deleteUser = () => {
const { item } = this.props;
this.props.deleteUser(item.org_id, item.email);
}
resetPassword = () => {
seafileAPI.sysAdminResetUserPassword(this.props.item.email).then(res => {
toaster.success(res.data.reset_tip);
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
render() {
const { item } = this.props;
const { isOpIconShown, isDeleteDialogOpen, isResetPasswordDialogOpen } = 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);
return (
<Fragment>
<tr className={this.state.highlight ? 'tr-highlight' : ''} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<td><a href={`${siteRoot}useradmin/info/${encodeURIComponent(item.email)}/`}>{item.name}</a></td>
<td>
<SysAdminUserStatusEditor
isTextMode={true}
isEditIconShow={isOpIconShown}
currentStatus={item.active ? 'active' : 'inactive'}
statusOptions={['active', 'inactive']}
onStatusChanged={this.updateStatus}
/>
</td>
<td>{`${Utils.bytesToSize(item.quota_usage)} / ${item.quota_total > 0 ? Utils.bytesToSize(item.quota_total) : '--'}`}</td>
<td>
{moment(item.ctime).format('YYYY-MM-DD hh:mm:ss')}{' / '}{item.last_login ? moment(item.last_login).fromNow() : '--'}
</td>
<td>
{(isOpIconShown && item.email != username) &&
<OpMenu
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}
/>
}
</Fragment>
);
}
}
class OrgUsers extends Component {
constructor(props) {
super(props);
this.state = {
loading: true,
errorMsg: '',
orgName: '',
userList: [],
isAddUserDialogOpen: false
};
}
componentDidMount () {
seafileAPI.sysAdminGetOrgInfo(this.props.orgID).then((res) => {
this.setState({
orgName: res.data.org_name
});
});
seafileAPI.sysAdminListAllOrgUsers(this.props.orgID).then((res) => {
this.setState({
loading: false,
userList: res.data.users
});
}).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.')
});
}
});
}
toggleAddUserDialog = () => {
this.setState({isAddUserDialogOpen: !this.state.isAddUserDialogOpen});
}
addUser = (newUserInfo) => {
const { email, name, password } = newUserInfo;
seafileAPI.sysAdminAddOrgUser(this.props.orgID, email, name, password).then(res => {
let userList = this.state.userList;
userList.unshift(res.data);
this.setState({userList: userList});
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
deleteUser = (orgID, email) => {
seafileAPI.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) => {
const isActive = statusValue == 'active';
seafileAPI.sysAdminUpdateOrgUserInfo(this.props.orgID, email, 'active', isActive).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);
});
}
render() {
const { isAddUserDialogOpen, orgName } = this.state;
return (
<Fragment>
<MainPanelTopbar>
<Button className="btn btn-secondary operation-item" onClick={this.toggleAddUserDialog}>{gettext('Add Member')}</Button>
</MainPanelTopbar>
<div className="main-panel-center flex-row">
<div className="cur-view-container">
<OrgNav
currentItem="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}
deleteUser={this.deleteUser}
/>
</div>
</div>
</div>
{isAddUserDialogOpen &&
<SysAdminAddUserDialog
addUser={this.addUser}
toggleDialog={this.toggleAddUserDialog}
/>
}
</Fragment>
);
}
}
export default OrgUsers;

View File

@@ -0,0 +1,261 @@
import React, { Component, Fragment } from 'react';
import { Button } from 'reactstrap';
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 SysAdminUserRoleEditor from '../../../components/select-editor/sysadmin-user-role-editor';
import SysAdminAddOrgDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-add-org-dialog';
import CommonOperationConfirmationDialog from '../../../components/dialog/common-operation-confirmation-dialog';
import MainPanelTopbar from '../main-panel-topbar';
const { availableRoles } = 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 organizations')}</h2>
</EmptyTip>
);
const table = (
<Fragment>
<table className="table-hover">
<thead>
<tr>
<th width="20%">{gettext('Name')}</th>
<th width="20%">{gettext('Creator')}</th>
<th width="20%">{gettext('Role')}</th>
<th width="15%">{gettext('Space Used')}</th>
<th width="20%">{gettext('Created At')}</th>
<th width="5%">{/* Operations */}</th>
</tr>
</thead>
<tbody>
{items.map((item, index) => {
return (<Item
key={index}
item={item}
updateRole={this.props.updateRole}
deleteOrg={this.props.deleteOrg}
/>);
})}
</tbody>
</table>
</Fragment>
);
return items.length ? table : emptyTip;
}
}
}
class Item extends Component {
constructor(props) {
super(props);
this.state = {
isOpIconShown: false,
isDeleteDialogOpen: false
};
}
handleMouseEnter = () => {
this.setState({isOpIconShown: true});
}
handleMouseLeave = () => {
this.setState({isOpIconShown: false});
}
toggleDeleteDialog = (e) => {
if (e) {
e.preventDefault();
}
this.setState({isDeleteDialogOpen: !this.state.isDeleteDialogOpen});
}
updateRole = (role) => {
this.props.updateRole(this.props.item.org_id, role);
}
deleteOrg = () => {
this.props.deleteOrg(this.props.item.org_id);
}
render() {
const { item } = this.props;
const { isOpIconShown, isDeleteDialogOpen } = this.state;
const orgName = '<span class="op-target">' + Utils.HTMLescape(item.org_name) + '</span>';
const deleteDialogMsg = gettext('Are you sure you want to delete {placeholder} ?').replace('{placeholder}', orgName);
return (
<Fragment>
<tr onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<td><a href={`${siteRoot}sys/organizations/${item.org_id}/info/`}>{item.org_name}</a></td>
<td><a href={`${siteRoot}useradmin/info/${encodeURIComponent(item.creator_email)}/`}>{item.creator_name}</a></td>
<td>
<SysAdminUserRoleEditor
isTextMode={true}
isEditIconShow={isOpIconShown}
currentRole={item.role}
roleOptions={availableRoles}
onRoleChanged={this.updateRole}
/>
</td>
<td>{`${Utils.bytesToSize(item.quota_usage)} / ${item.quota > 0 ? Utils.bytesToSize(item.quota) : '--'}`}</td>
<td>{moment(item.ctime).format('YYYY-MM-DD hh:mm:ss')}</td>
<td>
<a href="#" className={`action-icon sf2-icon-delete ${isOpIconShown ? '' : 'invisible'}`} title={gettext('Delete')} onClick={this.toggleDeleteDialog}></a>
</td>
</tr>
{isDeleteDialogOpen &&
<CommonOperationConfirmationDialog
title={gettext('Delete Organization')}
message={deleteDialogMsg}
executeOperation={this.deleteOrg}
confirmBtnText={gettext('Delete')}
toggleDialog={this.toggleDeleteDialog}
/>
}
</Fragment>
);
}
}
class Orgs extends Component {
constructor(props) {
super(props);
this.state = {
loading: true,
errorMsg: '',
orgList: [],
isAddOrgDialogOpen: false
};
}
componentDidMount () {
seafileAPI.sysAdminListAllOrgs().then((res) => {
this.setState({
loading: false,
orgList: res.data.organizations
});
}).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.')
});
}
});
}
toggleAddOrgDialog = () => {
this.setState({isAddOrgDialogOpen: !this.state.isAddOrgDialogOpen});
}
updateRole = (orgID, role) => {
let orgInfo = {};
orgInfo.role = role;
seafileAPI.sysAdminUpdateOrgInfo(orgID, orgInfo).then(res => {
let newOrgList = this.state.orgList.map(org => {
if (org.org_id == orgID) {
org.role = role;
}
return org;
});
this.setState({orgList: newOrgList});
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
addOrg = (data) => {
const { orgName, ownerEmail, password } = data;
seafileAPI.sysAdminAddOrg(orgName, ownerEmail, password).then(res => {
let orgList = this.state.orgList;
orgList.unshift(res.data);
this.setState({orgList: orgList});
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
deleteOrg = (orgID) => {
seafileAPI.sysAdminDeleteOrg(orgID).then(res => {
let orgList = this.state.orgList.filter(org => {
return org.org_id != orgID;
});
this.setState({orgList: orgList});
toaster.success(gettext('Successfully deleted 1 item.'));
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
render() {
const { isAddOrgDialogOpen } = this.state;
return (
<Fragment>
<MainPanelTopbar>
<Button className="btn btn-secondary operation-item" onClick={this.toggleAddOrgDialog}>{gettext('Add Organization')}</Button>
</MainPanelTopbar>
<div className="main-panel-center flex-row">
<div className="cur-view-container">
<div className="cur-view-path">
<h3 className="sf-heading">{gettext('Organizations')}</h3>
</div>
<div className="cur-view-content">
<Content
loading={this.state.loading}
errorMsg={this.state.errorMsg}
items={this.state.orgList}
updateRole={this.updateRole}
deleteOrg={this.deleteOrg}
/>
</div>
</div>
</div>
{isAddOrgDialogOpen &&
<SysAdminAddOrgDialog
addOrg={this.addOrg}
toggleDialog={this.toggleAddOrgDialog}
/>
}
</Fragment>
);
}
}
export default Orgs;

View File

@@ -0,0 +1,81 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Dropdown, DropdownMenu, DropdownToggle, DropdownItem } from 'reactstrap';
import { gettext } from '../../../utils/constants';
import { Utils } from '../../../utils/utils';
const propTypes = {
onFreezedItem: PropTypes.func.isRequired,
onUnfreezedItem: PropTypes.func.isRequired,
onMenuItemClick: PropTypes.func.isRequired,
};
class OpMenu extends React.Component {
constructor(props) {
super(props);
this.state = {
isItemMenuShow: false
};
}
onMenuItemClick = (e) => {
let operation = Utils.getEventData(e, 'op');
this.props.onMenuItemClick(operation);
}
onDropdownToggleClick = (e) => {
this.toggleOperationMenu(e);
}
toggleOperationMenu = (e) => {
this.setState(
{isItemMenuShow: !this.state.isItemMenuShow},
() => {
if (this.state.isItemMenuShow) {
this.props.onFreezedItem();
} else {
this.props.onUnfreezedItem();
}
}
);
}
translateOperations = (item) => {
let translateResult = '';
switch(item) {
case 'Delete':
translateResult = gettext('Delete');
break;
case 'Reset Password':
translateResult = gettext('Reset Password');
break;
}
return translateResult;
}
render() {
const operations = ['Delete', 'Reset Password'];
return (
<Dropdown isOpen={this.state.isItemMenuShow} toggle={this.toggleOperationMenu}>
<DropdownToggle
tag="i"
className="sf-dropdown-toggle fa fa-ellipsis-v"
title={gettext('More Operations')}
data-toggle="dropdown"
aria-expanded={this.state.isItemMenuShow}
/>
<DropdownMenu className="mt-2 mr-2">
{operations.map((item, index )=> {
return (<DropdownItem key={index} data-op={item} onClick={this.onMenuItemClick}>{this.translateOperations(item)}</DropdownItem>);
})}
</DropdownMenu>
</Dropdown>
);
}
}
OpMenu.propTypes = propTypes;
export default OpMenu;

View File

@@ -121,10 +121,14 @@ class SidePanel extends React.Component {
} }
{multiTenancy && isDefaultAdmin && {multiTenancy && isDefaultAdmin &&
<li className="nav-item"> <li className="nav-item">
<a className='nav-link ellipsis' href={siteRoot + 'sys/orgadmin/'}> <Link
className={`nav-link ellipsis ${this.getActiveClass('organizations')}`}
to={siteRoot + 'sys/organizations/'}
onClick={() => this.props.tabItemClick('organizations')}
>
<span className="sf2-icon-organization" aria-hidden="true"></span> <span className="sf2-icon-organization" aria-hidden="true"></span>
<span className="nav-text">{gettext('Organizations')}</span> <span className="nav-text">{gettext('Organizations')}</span>
</a> </Link>
</li> </li>
} }
{multiInstitution && isDefaultAdmin && {multiInstitution && isDefaultAdmin &&

View File

@@ -125,4 +125,3 @@ export const canManageGroup = window.sysadmin ? window.sysadmin.pageOptions.admi
export const canViewUserLog = window.sysadmin ? window.sysadmin.pageOptions.admin_permissions.can_view_user_log : ''; export const canViewUserLog = window.sysadmin ? window.sysadmin.pageOptions.admin_permissions.can_view_user_log : '';
export const canViewAdminLog = window.sysadmin ? window.sysadmin.pageOptions.admin_permissions.can_view_admin_log : ''; 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 enableWorkWeixin = window.sysadmin ? window.sysadmin.pageOptions.enable_work_weixin : '';

View File

@@ -44,11 +44,6 @@ try:
except ImportError: except ImportError:
MULTI_TENANCY = False MULTI_TENANCY = False
try:
from seahub_extra.organizations.settings import ORG_TRIAL_DAYS
except ImportError:
ORG_TRIAL_DAYS = 0
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def get_org_info(org): def get_org_info(org):
@@ -86,11 +81,6 @@ def get_org_detailed_info(org):
groups = ccnet_api.get_org_groups(org_id, -1, -1) groups = ccnet_api.get_org_groups(org_id, -1, -1)
org_info['groups_count'] = len(groups) org_info['groups_count'] = len(groups)
if ORG_TRIAL_DAYS > 0:
org_info['expiration'] = datetime.datetime.fromtimestamp(org.ctime / 1e6) + timedelta(days=ORG_TRIAL_DAYS)
else:
org_info['expiration'] = ''
return org_info return org_info

View File

@@ -18,12 +18,16 @@
enable_work_weixin: {% if enable_work_weixin %} true {% else %} false {% endif %}, enable_work_weixin: {% if enable_work_weixin %} true {% else %} false {% endif %},
enableSysAdminViewRepo: {% if enable_sys_admin_view_repo %} true {% else %} false {% endif %}, enableSysAdminViewRepo: {% if enable_sys_admin_view_repo %} true {% else %} false {% endif %},
trashReposExpireDays: {{ trash_repos_expire_days }}, trashReposExpireDays: {{ trash_repos_expire_days }},
is_email_configured: {% if is_email_configured %} true {% else %} false {% endif %}, send_email_on_adding_system_member: {% if send_email_on_adding_system_member %} true {% else %} false {% endif %},
send_email_on_resetting_user_passwd: {% if send_email_on_resetting_user_passwd %} true {% else %} false {% endif %}, enable_two_factor_auth: {% if enable_two_factor_auth %} true {% else %} false {% endif %},
send_email_on_adding_system_member: {% if send_email_on_adding_system_member %} true {% else %} false {% endif %}, availableRoles: (function() {
enable_two_factor_auth: {% if enable_two_factor_auth %} true {% else %} false {% endif %}, var list = [];
available_roles: '{{ available_roles | escapejs }}' , {% for role in available_roles %}
available_admin_roles: '{{ available_admin_roles | escapejs }}' , list.push('{{role|escapejs}}');
{% endfor %}
return list;
})(),
available_admin_roles: '{{ available_admin_roles | escapejs }}',
admin_permissions: { admin_permissions: {
"can_view_system_info": {% if user.admin_permissions.can_view_system_info %} true {% else %} false {% endif %}, "can_view_system_info": {% if user.admin_permissions.can_view_system_info %} true {% else %} false {% endif %},
"can_view_statistic": {% if user.admin_permissions.can_view_statistic %} true {% else %} false {% endif %}, "can_view_statistic": {% if user.admin_permissions.can_view_statistic %} true {% else %} false {% endif %},

View File

@@ -672,11 +672,11 @@ urlpatterns = [
url(r'^sys/users-all/$', sysadmin_react_fake_view, name="sys_users_all"), url(r'^sys/users-all/$', sysadmin_react_fake_view, name="sys_users_all"),
url(r'^sys/users-admin/$', sysadmin_react_fake_view, name="sys_users_admin"), url(r'^sys/users-admin/$', sysadmin_react_fake_view, name="sys_users_admin"),
url(r'^sys/user-info/(?P<email>[^/]+)/$', sysadmin_react_fake_view, name="sys_users"), url(r'^sys/user-info/(?P<email>[^/]+)/$', sysadmin_react_fake_view, name="sys_users"),
url(r'^sys/organizations/$', sysadmin_react_fake_view, name="sys_organizations_all"), url(r'^sys/organizations/$', sysadmin_react_fake_view, name="sys_organizations"),
url(r'^sys/organizations/(?P<org_id>\d+)/users/$', sysadmin_org_react_fake_view, name="sys_organization_users"), url(r'^sys/organizations/(?P<org_id>\d+)/info/$', sysadmin_react_fake_view, name="sys_organization_info"),
url(r'^sys/organizations/(?P<org_id>\d+)/groups/$', sysadmin_org_react_fake_view, name="sys_organization_groups"), url(r'^sys/organizations/(?P<org_id>\d+)/users/$', sysadmin_react_fake_view, name="sys_organization_users"),
url(r'^sys/organizations/(?P<org_id>\d+)/libraries/$', sysadmin_org_react_fake_view, name="sys_organization_repos"), url(r'^sys/organizations/(?P<org_id>\d+)/groups/$', sysadmin_react_fake_view, name="sys_organization_groups"),
url(r'^sys/organizations/(?P<org_id>\d+)/settings/$', sysadmin_org_react_fake_view, name="sys_organization_settings"), url(r'^sys/organizations/(?P<org_id>\d+)/libraries/$', sysadmin_react_fake_view, name="sys_organization_repos"),
url(r'^sys/share-links/$', sysadmin_react_fake_view, name="sys_share_links"), url(r'^sys/share-links/$', sysadmin_react_fake_view, name="sys_share_links"),
url(r'^sys/upload-links/$', sysadmin_react_fake_view, name="sys_upload_links"), url(r'^sys/upload-links/$', sysadmin_react_fake_view, name="sys_upload_links"),
url(r'^sys/work-weixin/$', sysadmin_react_fake_view, name="sys_work_weixin"), url(r'^sys/work-weixin/$', sysadmin_react_fake_view, name="sys_work_weixin"),

View File

@@ -150,8 +150,6 @@ def sysadmin_react_fake_view(request, **kwargs):
'constance_enabled': dj_settings.CONSTANCE_ENABLED, 'constance_enabled': dj_settings.CONSTANCE_ENABLED,
'multi_tenancy': MULTI_TENANCY, 'multi_tenancy': MULTI_TENANCY,
'multi_institution': getattr(dj_settings, 'MULTI_INSTITUTION', False), 'multi_institution': getattr(dj_settings, 'MULTI_INSTITUTION', False),
'is_email_configured': IS_EMAIL_CONFIGURED,
'send_email_on_resetting_user_passwd': SEND_EMAIL_ON_RESETTING_USER_PASSWD,
'send_email_on_adding_system_member': SEND_EMAIL_ON_ADDING_SYSTEM_MEMBER, 'send_email_on_adding_system_member': SEND_EMAIL_ON_ADDING_SYSTEM_MEMBER,
'sysadmin_extra_enabled': ENABLE_SYSADMIN_EXTRA, 'sysadmin_extra_enabled': ENABLE_SYSADMIN_EXTRA,
'enable_guest_invitation': ENABLE_GUEST_INVITATION, 'enable_guest_invitation': ENABLE_GUEST_INVITATION,
@@ -165,15 +163,6 @@ def sysadmin_react_fake_view(request, **kwargs):
'available_admin_roles': get_available_admin_roles() 'available_admin_roles': get_available_admin_roles()
}) })
@login_required
@sys_staff_required
def sysadmin_org_react_fake_view(request, **kwargs):
return render(request, 'sysadmin/sysadmin_org_react_app.html', {
'org_id': kwargs['org_id'],
'is_email_configured': IS_EMAIL_CONFIGURED,
'send_email_on_resetting_user_passwd': SEND_EMAIL_ON_RESETTING_USER_PASSWD,
})
@login_required @login_required
@sys_staff_required @sys_staff_required
def sys_statistic_file(request): def sys_statistic_file(request):