// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-FileCopyrightText: 2020-2022 grommunio GmbH import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { withStyles } from '@mui/styles'; import { withTranslation } from 'react-i18next'; import { Typography, Paper, Grid, TextField, FormControl, MenuItem, Button, Dialog, DialogTitle, DialogContent, DialogActions, Tabs, Tab, FormControlLabel, Checkbox, } from '@mui/material'; import { connect } from 'react-redux'; import { editDomainData, fetchDomainDetails } from '../actions/domains'; import { changeDomainPassword } from '../api'; import { getStringAfterLastSlash, getPolicyDiff } from '../utils'; import { fetchOrgsData } from '../actions/orgs'; import SlimSyncPolicies from '../components/SlimSyncPolicies'; import { SYSTEM_ADMIN_READ, SYSTEM_ADMIN_WRITE } from '../constants'; import { CapabilityContext } from '../CapabilityContext'; import ViewWrapper from '../components/ViewWrapper'; import { fetchServersData } from '../actions/servers'; import MagnitudeAutocomplete from '../components/MagnitudeAutocomplete'; const styles = theme => ({ paper: { margin: theme.spacing(3, 2, 3, 2), padding: theme.spacing(2, 2, 2, 2), borderRadius: 6, }, form: { width: '100%', marginTop: theme.spacing(4), }, input: { marginBottom: theme.spacing(3), }, select: { minWidth: 60, }, tabs: { marginTop: 16, }, }); class DomainListDetails extends PureComponent { state = { domainname: '', domainStatus: 0, orgID: '', maxUser: 0, title: '', address: '', adminName: '', tel: '', homeserver: '', syncPolicy: {}, defaultPolicy: {}, changingPw: false, newPw: '', checkPw: '', tab: 0, chat: false, autocompleteInput: '', } statuses = [ { name: 'Activated', ID: 0 }, { name: 'Deactivated', ID: 3 }, ] async componentDidMount() { const { fetch, fetchOrgs, fetchServers, capabilities } = this.props; if(capabilities.includes(SYSTEM_ADMIN_READ)) { await fetchOrgs() .catch(message => this.setState({ snackbar: message || 'Unknown error' })); await fetchServers() .catch(message => this.setState({ snackbar: message || 'Unknown error' })); } const domain = await fetch(getStringAfterLastSlash()) .catch(message => this.setState({ snackbar: message || 'Unknown error' })); const defaultPolicy = domain.defaultPolicy; domain.syncPolicy = domain.syncPolicy || {}; const domainOrg = this.props.orgs.find(o => o.ID === domain.orgID); this.setState({ ...(domain || {}), autocompleteInput: domainOrg?.name || '', orgID: domainOrg, syncPolicy: { ...defaultPolicy, ...domain.syncPolicy, maxattsize: (domain.syncPolicy.maxattsize || defaultPolicy.maxattsize) / 1048576 || '', }, defaultPolicy, }); } handleInput = field => event => { this.setState({ [field]: event.target.value, }); } handleCheckbox = field => event => this.setState({ [field]: event.target.checked, unsaved: true, }); handleEdit = () => { const { ID, domainname, domainStatus, orgID, chat, homeserver, maxUser, title, address, adminName, tel, defaultPolicy, syncPolicy } = this.state; this.props.edit({ ID, domainname, domainStatus, orgID: Number.isInteger(orgID) ? orgID : 0, maxUser: parseInt(maxUser) || null, title, address, adminName, tel, homeserver: homeserver?.ID || null, syncPolicy: getPolicyDiff(defaultPolicy, syncPolicy), chat, }) .then(() => this.setState({ snackbar: 'Success!' })) .catch(message => this.setState({ snackbar: message || 'Unknown error' })); } handlePasswordChange = async () => { const { domain, newPw } = this.state; await changeDomainPassword(domain.ID, newPw); this.setState({ changingPw: false }); } handleKeyPress = event => { const { newPw, checkPw } = this.state; if(event.key === 'Enter' && newPw === checkPw) this.handlePasswordChange(); } handleBack = () => { const { capabilities } = this.props; if(capabilities.includes(SYSTEM_ADMIN_READ)) this.props.history.push('/domains'); else this.props.history.push('/' + getStringAfterLastSlash()); } handleTab = (e, tab) => this.setState({ tab }) handleSyncChange = field => event => { const { syncPolicy } = this.state; this.setState({ syncPolicy: { ...syncPolicy, [field]: event.target.value, }, }); } handleRadio = field => event => { const { syncPolicy } = this.state; this.setState({ syncPolicy: { ...syncPolicy, [field]: parseInt(event.target.value), }, }); } handleSyncCheckboxChange = field => (event, newVal) => { const { syncPolicy } = this.state; this.setState({ syncPolicy: { ...syncPolicy, [field]: newVal ? 1 : 0, }, }); } handleSlider = field => (event, newVal) => { const { syncPolicy } = this.state; this.setState({ syncPolicy: { ...syncPolicy, [field]: newVal, }, }); } handleAutocomplete = (field) => (e, newVal) => { this.setState({ [field]: newVal?.ID || '', autocompleteInput: newVal?.name || '', }); } handleServer =(e, newVal) => { this.setState({ homeserver: newVal || '', }); } render() { const { classes, t, orgs, capabilities, servers } = this.props; const writable = this.context.includes(SYSTEM_ADMIN_WRITE); const { domainname, domainStatus, orgID, maxUser, title, address, adminName, tel, syncPolicy, checkPw, newPw, changingPw, snackbar, tab, defaultPolicy, chat, homeserver, autocompleteInput } = this.state; return ( <ViewWrapper topbarTitle={t('Domains')} snackbar={snackbar} onSnackbarClose={() => this.setState({ snackbar: '' })} > <Paper className={classes.paper} elevation={1}> <Grid container> <Typography color="primary" variant="h5" > {t('editHeadline', { item: 'Domain' })} </Typography> </Grid> <Tabs className={classes.tabs} indicatorColor="primary" onChange={this.handleTab} value={tab}> <Tab value={0} label={t('Domain')} /> <Tab value={1} label={t('Sync policies')} /> </Tabs> {tab === 0 && <FormControl className={classes.form}> <Grid container className={classes.input}> <TextField label={t("Domain")} style={{ flex: 1, marginRight: 8 }} value={domainname || ''} autoFocus disabled /> </Grid> <TextField select className={classes.input} label={t("Status")} fullWidth value={domainStatus || 0} onChange={this.handleInput('domainStatus')} > {this.statuses.map((status, key) => ( <MenuItem key={key} value={status.ID}> {status.name} </MenuItem> ))} </TextField> {capabilities.includes(SYSTEM_ADMIN_READ) && <MagnitudeAutocomplete value={orgID} filterAttribute={'name'} onChange={this.handleAutocomplete('orgID')} className={classes.input} options={orgs} inputValue={autocompleteInput} onInputChange={this.handleInput('autocompleteInput')} label={t('Organization')} />} <TextField className={classes.input} label={t("Maximum users")} fullWidth value={maxUser || ''} onChange={this.handleInput('maxUser')} /> <TextField className={classes.input} label={t("Title")} fullWidth value={title || ''} onChange={this.handleInput('title')} /> <TextField className={classes.input} label={t("Address")} fullWidth value={address || ''} onChange={this.handleInput('address')} /> <TextField className={classes.input} label={t("Administrator")} fullWidth value={adminName || ''} onChange={this.handleInput('adminName')} /> <TextField className={classes.input} label={t("Telephone")} fullWidth value={tel || ''} onChange={this.handleInput('tel')} /> <MagnitudeAutocomplete value={homeserver} filterAttribute={'hostname'} onChange={this.handleServer} className={classes.input} options={servers} label={t('Homeserver')} /> <FormControlLabel control={ <Checkbox checked={chat || false} onChange={this.handleCheckbox('chat')} color="primary" /> } className={classes.input} label={t('grommunio-chat Team')} /> </FormControl>} {tab === 1 && <SlimSyncPolicies syncPolicy={syncPolicy} defaultPolicy={defaultPolicy} handleChange={this.handleSyncChange} handleCheckbox={this.handleSyncCheckboxChange} handleSlider={this.handleSlider} />} <Button color="secondary" onClick={this.handleBack} style={{ marginRight: 8 }} > {t('Back')} </Button> <Button variant="contained" color="primary" onClick={this.handleEdit} disabled={!writable} > {t('Save')} </Button> </Paper> <Dialog open={!!changingPw} onClose={() => this.setState({ changingPw: false })}> <DialogTitle>{t('Change password')}</DialogTitle> <DialogContent> <TextField className={classes.input} label={t("New password")} fullWidth type="password" value={newPw} onChange={event => this.setState({ newPw: event.target.value })} autoFocus onKeyPress={this.handleKeyPress} /> <TextField className={classes.input} label={t("Repeat new password")} fullWidth type="password" value={checkPw} onChange={event => this.setState({ checkPw: event.target.value })} onKeyPress={this.handleKeyPress} /> </DialogContent> <DialogActions> <Button color="secondary" onClick={() => this.setState({ changingPw: false })}> {t('Cancel')} </Button> <Button variant="contained" color="primary" onClick={this.handlePasswordChange} disabled={checkPw !== newPw} > {t('Save')} </Button> </DialogActions> </Dialog> </ViewWrapper> ); } } DomainListDetails.contextType = CapabilityContext; DomainListDetails.propTypes = { classes: PropTypes.object.isRequired, t: PropTypes.func.isRequired, history: PropTypes.object.isRequired, location: PropTypes.object.isRequired, fetch: PropTypes.func.isRequired, fetchOrgs: PropTypes.func.isRequired, fetchServers: PropTypes.func.isRequired, edit: PropTypes.func.isRequired, orgs: PropTypes.array.isRequired, capabilities: PropTypes.array.isRequired, servers: PropTypes.array.isRequired, }; const mapStateToProps = state => { return { orgs: state.orgs.Orgs, capabilities: state.auth.capabilities, servers: state.servers.Servers, }; }; const mapDispatchToProps = dispatch => { return { edit: async domain => { await dispatch(editDomainData(domain)).catch(message => Promise.reject(message)); }, fetch: async id => await dispatch(fetchDomainDetails(id)) .then(domain => domain) .catch(message => Promise.reject(message)), fetchOrgs: async () => await dispatch(fetchOrgsData({ sort: 'name,asc', limit: 1000000, level: 0 })) .catch(message => Promise.reject(message)), fetchServers: async () => await dispatch(fetchServersData({ sort: 'hostname,asc', limit: 1000000, level: 0 })) .catch(message => Promise.reject(message)), }; }; export default connect(mapStateToProps, mapDispatchToProps)( withTranslation()(withStyles(styles)(DomainListDetails)));