parent
ce2fad66c7
commit
c623170334
7 changed files with 542 additions and 0 deletions
@ -0,0 +1,270 @@ |
|||||||
|
import { Grid } from '@mui/material'; |
||||||
|
import { FC, ReactNode, useCallback, useMemo, useRef } from 'react'; |
||||||
|
import { v4 as uuidv4 } from 'uuid'; |
||||||
|
|
||||||
|
import ActionGroup from '../ActionGroup'; |
||||||
|
import api from '../../lib/api'; |
||||||
|
import handleAPIError from '../../lib/handleAPIError'; |
||||||
|
import MessageGroup, { MessageGroupForwardedRefContent } from '../MessageGroup'; |
||||||
|
import OutlinedInputWithLabel from '../OutlinedInputWithLabel'; |
||||||
|
import mailServerListSchema from './schema'; |
||||||
|
import SelectWithLabel from '../SelectWithLabel'; |
||||||
|
import useFormikUtils from '../../hooks/useFormikUtils'; |
||||||
|
import UncontrolledInput from '../UncontrolledInput'; |
||||||
|
|
||||||
|
const AddMailServerForm: FC<AddMailServerFormProps> = (props) => { |
||||||
|
const { |
||||||
|
localhostDomain = '', |
||||||
|
mailServerUuid, |
||||||
|
onSubmit, |
||||||
|
previousFormikValues, |
||||||
|
} = props; |
||||||
|
|
||||||
|
const msUuid = useMemo<string>( |
||||||
|
() => mailServerUuid ?? uuidv4(), |
||||||
|
[mailServerUuid], |
||||||
|
); |
||||||
|
|
||||||
|
const messageGroupRef = useRef<MessageGroupForwardedRefContent>({}); |
||||||
|
|
||||||
|
const setApiMessage = useCallback( |
||||||
|
(message?: Message) => |
||||||
|
messageGroupRef.current.setMessage?.call(null, 'api', message), |
||||||
|
[], |
||||||
|
); |
||||||
|
|
||||||
|
const { |
||||||
|
disableAutocomplete, |
||||||
|
disabledSubmit, |
||||||
|
formik, |
||||||
|
formikErrors, |
||||||
|
handleChange, |
||||||
|
} = useFormikUtils<MailServerFormikValues>({ |
||||||
|
initialValues: previousFormikValues ?? { |
||||||
|
[msUuid]: { |
||||||
|
address: '', |
||||||
|
authentication: 'none', |
||||||
|
heloDomain: localhostDomain, |
||||||
|
port: 587, |
||||||
|
security: 'none', |
||||||
|
uuid: msUuid, |
||||||
|
}, |
||||||
|
}, |
||||||
|
onSubmit: (...args) => { |
||||||
|
onSubmit( |
||||||
|
{ |
||||||
|
mailServer: args[0][msUuid], |
||||||
|
onConfirmCancel: (values, { setSubmitting }) => setSubmitting(false), |
||||||
|
onConfirmProceed: (values, { setSubmitting }) => { |
||||||
|
let errorMessage: ReactNode = <>Failed to add mail server.</>; |
||||||
|
let method: 'post' | 'put' = 'post'; |
||||||
|
let successMessage = <>Mail server added.</>; |
||||||
|
let url = '/mail-server'; |
||||||
|
|
||||||
|
if (previousFormikValues) { |
||||||
|
errorMessage = <>Failed to update mail server.</>; |
||||||
|
method = 'put'; |
||||||
|
successMessage = <>Mail server updated.</>; |
||||||
|
url += `/${msUuid}`; |
||||||
|
} |
||||||
|
|
||||||
|
api[method](url, values[msUuid]) |
||||||
|
.then(() => { |
||||||
|
setApiMessage({ children: successMessage }); |
||||||
|
}) |
||||||
|
.catch((error) => { |
||||||
|
const emsg = handleAPIError(error); |
||||||
|
|
||||||
|
emsg.children = ( |
||||||
|
<> |
||||||
|
{errorMessage} {emsg.children} |
||||||
|
</> |
||||||
|
); |
||||||
|
|
||||||
|
setApiMessage(emsg); |
||||||
|
}) |
||||||
|
.finally(() => { |
||||||
|
setSubmitting(false); |
||||||
|
}); |
||||||
|
}, |
||||||
|
}, |
||||||
|
...args, |
||||||
|
); |
||||||
|
}, |
||||||
|
validationSchema: mailServerListSchema, |
||||||
|
}); |
||||||
|
|
||||||
|
const addressChain = useMemo<string>(() => `${msUuid}.address`, [msUuid]); |
||||||
|
const authenticationChain = useMemo<string>( |
||||||
|
() => `${msUuid}.authentication`, |
||||||
|
[msUuid], |
||||||
|
); |
||||||
|
const confirmPasswordChain = useMemo<string>( |
||||||
|
() => `${msUuid}.confirmPassword`, |
||||||
|
[msUuid], |
||||||
|
); |
||||||
|
const heloDomainChain = useMemo<string>( |
||||||
|
() => `${msUuid}.heloDomain`, |
||||||
|
[msUuid], |
||||||
|
); |
||||||
|
const passwordChain = useMemo<string>(() => `${msUuid}.password`, [msUuid]); |
||||||
|
const portChain = useMemo<string>(() => `${msUuid}.port`, [msUuid]); |
||||||
|
const securityChain = useMemo<string>(() => `${msUuid}.security`, [msUuid]); |
||||||
|
const usernameChain = useMemo<string>(() => `${msUuid}.username`, [msUuid]); |
||||||
|
|
||||||
|
return ( |
||||||
|
<Grid |
||||||
|
component="form" |
||||||
|
onSubmit={(event) => { |
||||||
|
event.preventDefault(); |
||||||
|
|
||||||
|
formik.submitForm(); |
||||||
|
}} |
||||||
|
container |
||||||
|
columns={{ xs: 1, sm: 2 }} |
||||||
|
spacing="1em" |
||||||
|
> |
||||||
|
<Grid item xs={1}> |
||||||
|
<UncontrolledInput |
||||||
|
input={ |
||||||
|
<OutlinedInputWithLabel |
||||||
|
id={addressChain} |
||||||
|
label="Server address" |
||||||
|
name={addressChain} |
||||||
|
onBlur={formik.handleBlur} |
||||||
|
onChange={handleChange} |
||||||
|
required |
||||||
|
value={formik.values[msUuid].address} |
||||||
|
/> |
||||||
|
} |
||||||
|
/> |
||||||
|
</Grid> |
||||||
|
<Grid item xs={1}> |
||||||
|
<UncontrolledInput |
||||||
|
input={ |
||||||
|
<OutlinedInputWithLabel |
||||||
|
id={portChain} |
||||||
|
label="Server port" |
||||||
|
name={portChain} |
||||||
|
onBlur={formik.handleBlur} |
||||||
|
onChange={handleChange} |
||||||
|
required |
||||||
|
type="number" |
||||||
|
value={formik.values[msUuid].port} |
||||||
|
/> |
||||||
|
} |
||||||
|
/> |
||||||
|
</Grid> |
||||||
|
<Grid item sm={2} xs={1}> |
||||||
|
<UncontrolledInput |
||||||
|
input={ |
||||||
|
<SelectWithLabel |
||||||
|
id={securityChain} |
||||||
|
label="Server security type" |
||||||
|
name={securityChain} |
||||||
|
onBlur={formik.handleBlur} |
||||||
|
onChange={handleChange} |
||||||
|
required |
||||||
|
selectItems={['none', 'starttls', 'tls-ssl']} |
||||||
|
value={formik.values[msUuid].security} |
||||||
|
/> |
||||||
|
} |
||||||
|
/> |
||||||
|
</Grid> |
||||||
|
<Grid item sm={2} xs={1}> |
||||||
|
<UncontrolledInput |
||||||
|
input={ |
||||||
|
<SelectWithLabel |
||||||
|
id={authenticationChain} |
||||||
|
label="Server authentication method" |
||||||
|
name={authenticationChain} |
||||||
|
onBlur={formik.handleBlur} |
||||||
|
onChange={handleChange} |
||||||
|
required |
||||||
|
selectItems={['none', 'plain-text', 'encrypted']} |
||||||
|
value={formik.values[msUuid].authentication} |
||||||
|
/> |
||||||
|
} |
||||||
|
/> |
||||||
|
</Grid> |
||||||
|
<Grid item sm={2} xs={1}> |
||||||
|
<UncontrolledInput |
||||||
|
input={ |
||||||
|
<OutlinedInputWithLabel |
||||||
|
id={heloDomainChain} |
||||||
|
label="HELO domain" |
||||||
|
name={heloDomainChain} |
||||||
|
onBlur={formik.handleBlur} |
||||||
|
onChange={handleChange} |
||||||
|
required |
||||||
|
value={formik.values[msUuid].heloDomain} |
||||||
|
/> |
||||||
|
} |
||||||
|
/> |
||||||
|
</Grid> |
||||||
|
<Grid item xs={1}> |
||||||
|
<UncontrolledInput |
||||||
|
input={ |
||||||
|
<OutlinedInputWithLabel |
||||||
|
id={usernameChain} |
||||||
|
inputProps={disableAutocomplete()} |
||||||
|
label="Server username" |
||||||
|
name={usernameChain} |
||||||
|
onBlur={formik.handleBlur} |
||||||
|
onChange={handleChange} |
||||||
|
value={formik.values[msUuid].username} |
||||||
|
/> |
||||||
|
} |
||||||
|
/> |
||||||
|
</Grid> |
||||||
|
<Grid item xs={1}> |
||||||
|
<UncontrolledInput |
||||||
|
input={ |
||||||
|
<OutlinedInputWithLabel |
||||||
|
id={passwordChain} |
||||||
|
label="Server password" |
||||||
|
name={passwordChain} |
||||||
|
onBlur={formik.handleBlur} |
||||||
|
onChange={handleChange} |
||||||
|
type="password" |
||||||
|
value={formik.values[msUuid].password} |
||||||
|
/> |
||||||
|
} |
||||||
|
/> |
||||||
|
</Grid> |
||||||
|
<Grid item xs={1} /> |
||||||
|
<Grid item xs={1}> |
||||||
|
<UncontrolledInput |
||||||
|
input={ |
||||||
|
<OutlinedInputWithLabel |
||||||
|
id={confirmPasswordChain} |
||||||
|
label="Confirm password" |
||||||
|
name={confirmPasswordChain} |
||||||
|
onBlur={formik.handleBlur} |
||||||
|
onChange={handleChange} |
||||||
|
type="password" |
||||||
|
value={formik.values[msUuid].confirmPassword} |
||||||
|
/> |
||||||
|
} |
||||||
|
/> |
||||||
|
</Grid> |
||||||
|
<Grid item width="100%"> |
||||||
|
<MessageGroup count={1} messages={formikErrors} ref={messageGroupRef} /> |
||||||
|
</Grid> |
||||||
|
<Grid item width="100%"> |
||||||
|
<ActionGroup |
||||||
|
actions={[ |
||||||
|
{ |
||||||
|
background: 'blue', |
||||||
|
children: previousFormikValues ? 'Update' : 'Add', |
||||||
|
disabled: disabledSubmit, |
||||||
|
type: 'submit', |
||||||
|
}, |
||||||
|
]} |
||||||
|
/> |
||||||
|
</Grid> |
||||||
|
</Grid> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default AddMailServerForm; |
@ -0,0 +1,9 @@ |
|||||||
|
import { FC } from 'react'; |
||||||
|
|
||||||
|
import AddMailServerForm from './AddMailServerForm'; |
||||||
|
|
||||||
|
const EditMailServerForm: FC<EditMailServerFormProps> = (props) => ( |
||||||
|
<AddMailServerForm {...props} /> |
||||||
|
); |
||||||
|
|
||||||
|
export default EditMailServerForm; |
@ -0,0 +1,179 @@ |
|||||||
|
import { FC, useRef, useState } from 'react'; |
||||||
|
|
||||||
|
import API_BASE_URL from '../../lib/consts/API_BASE_URL'; |
||||||
|
|
||||||
|
import AddMailServerForm from './AddMailServerForm'; |
||||||
|
import api from '../../lib/api'; |
||||||
|
import { DialogWithHeader } from '../Dialog'; |
||||||
|
import EditMailServerForm from './EditMailServerForm'; |
||||||
|
import FormSummary from '../FormSummary'; |
||||||
|
import List from '../List'; |
||||||
|
import { ExpandablePanel } from '../Panels'; |
||||||
|
import periodicFetch from '../../lib/fetchers/periodicFetch'; |
||||||
|
import { BodyText } from '../Text'; |
||||||
|
import useActiveFetch from '../../hooks/useActiveFetch'; |
||||||
|
import useChecklist from '../../hooks/useChecklist'; |
||||||
|
import useConfirmDialog from '../../hooks/useConfirmDialog'; |
||||||
|
import useFetch from '../../hooks/useFetch'; |
||||||
|
|
||||||
|
const ManageMailServer: FC = () => { |
||||||
|
const addDialogRef = useRef<DialogForwardedRefContent>(null); |
||||||
|
const editDialogRef = useRef<DialogForwardedRefContent>(null); |
||||||
|
|
||||||
|
const { confirmDialog, setConfirmDialogOpen, setConfirmDialogProps } = |
||||||
|
useConfirmDialog({ initial: { closeOnProceed: true } }); |
||||||
|
|
||||||
|
const [edit, setEdit] = useState<boolean>(false); |
||||||
|
const [mailServer, setMailServer] = useState< |
||||||
|
APIMailServerDetail | undefined |
||||||
|
>(); |
||||||
|
const [mailServers, setMailServers] = useState< |
||||||
|
APIMailServerOverviewList | undefined |
||||||
|
>(); |
||||||
|
|
||||||
|
const { isLoading: loadingMailServersPeriodic } = |
||||||
|
periodicFetch<APIMailServerOverviewList>(`${API_BASE_URL}/mail-server`, { |
||||||
|
onSuccess: (data) => setMailServers(data), |
||||||
|
}); |
||||||
|
|
||||||
|
const { fetch: getMailServers, loading: loadingMailServersActive } = |
||||||
|
useActiveFetch<APIMailServerOverviewList>({ |
||||||
|
onData: (data) => setMailServers(data), |
||||||
|
url: '/mail-server', |
||||||
|
}); |
||||||
|
|
||||||
|
const { fetch: getMailServer, loading: loadingMailServer } = |
||||||
|
useActiveFetch<APIMailServerDetail>({ |
||||||
|
onData: (data) => setMailServer(data), |
||||||
|
url: '/mail-server', |
||||||
|
}); |
||||||
|
|
||||||
|
const { data: host, loading: loadingHost } = |
||||||
|
useFetch<APIHostDetail>('/host/local'); |
||||||
|
|
||||||
|
const { |
||||||
|
buildDeleteDialogProps, |
||||||
|
checks, |
||||||
|
getCheck, |
||||||
|
hasAllChecks, |
||||||
|
hasChecks, |
||||||
|
multipleItems, |
||||||
|
resetChecks, |
||||||
|
setAllChecks, |
||||||
|
setCheck, |
||||||
|
} = useChecklist({ list: mailServers }); |
||||||
|
|
||||||
|
return ( |
||||||
|
<> |
||||||
|
<ExpandablePanel expandInitially header="Manage mail servers"> |
||||||
|
<List |
||||||
|
allowCheckAll={multipleItems} |
||||||
|
allowEdit |
||||||
|
allowItemButton={edit} |
||||||
|
disableDelete={!hasChecks} |
||||||
|
edit={edit} |
||||||
|
getListCheckboxProps={() => ({ |
||||||
|
checked: hasAllChecks, |
||||||
|
onChange: (event, checked) => setAllChecks(checked), |
||||||
|
})} |
||||||
|
getListItemCheckboxProps={(uuid) => ({ |
||||||
|
checked: getCheck(uuid), |
||||||
|
onChange: (event, checked) => setCheck(uuid, checked), |
||||||
|
})} |
||||||
|
header |
||||||
|
listEmpty="No mail server(s) found." |
||||||
|
listItems={mailServers} |
||||||
|
loading={loadingMailServersPeriodic || loadingMailServersActive} |
||||||
|
onAdd={() => addDialogRef?.current?.setOpen(true)} |
||||||
|
onDelete={() => { |
||||||
|
setConfirmDialogProps({ |
||||||
|
...buildDeleteDialogProps({ |
||||||
|
onProceedAppend: () => { |
||||||
|
Promise.all( |
||||||
|
checks.map((uuid) => api.delete(`/mail-server/${uuid}`)), |
||||||
|
).then(() => getMailServers()); |
||||||
|
|
||||||
|
resetChecks(); |
||||||
|
}, |
||||||
|
getConfirmDialogTitle: (count) => |
||||||
|
`Delete the following ${count} mail server(s)?`, |
||||||
|
renderEntry: ({ key }) => { |
||||||
|
const ms = mailServers?.[key]; |
||||||
|
|
||||||
|
return ( |
||||||
|
<BodyText> |
||||||
|
{ms?.address}:{ms?.port} |
||||||
|
</BodyText> |
||||||
|
); |
||||||
|
}, |
||||||
|
}), |
||||||
|
}); |
||||||
|
|
||||||
|
setConfirmDialogOpen(true); |
||||||
|
}} |
||||||
|
onEdit={() => setEdit((previous) => !previous)} |
||||||
|
onItemClick={(value, uuid) => { |
||||||
|
editDialogRef?.current?.setOpen(true); |
||||||
|
|
||||||
|
getMailServer(`/${uuid}`); |
||||||
|
}} |
||||||
|
renderListItem={(uuid, { address, port }) => ( |
||||||
|
<BodyText> |
||||||
|
{address}:{port} |
||||||
|
</BodyText> |
||||||
|
)} |
||||||
|
/> |
||||||
|
</ExpandablePanel> |
||||||
|
<DialogWithHeader |
||||||
|
header="Add mail server" |
||||||
|
loading={loadingMailServersPeriodic || loadingHost} |
||||||
|
ref={addDialogRef} |
||||||
|
showClose |
||||||
|
> |
||||||
|
{host && ( |
||||||
|
<AddMailServerForm |
||||||
|
localhostDomain={host.domain} |
||||||
|
onSubmit={(tools, ...args) => { |
||||||
|
setConfirmDialogProps({ |
||||||
|
actionProceedText: 'Add', |
||||||
|
content: <FormSummary entries={tools.mailServer} />, |
||||||
|
onCancelAppend: () => tools.onConfirmCancel(...args), |
||||||
|
onProceedAppend: () => tools.onConfirmProceed(...args), |
||||||
|
titleText: 'Add mail server with the following?', |
||||||
|
}); |
||||||
|
|
||||||
|
setConfirmDialogOpen(true); |
||||||
|
}} |
||||||
|
/> |
||||||
|
)} |
||||||
|
</DialogWithHeader> |
||||||
|
<DialogWithHeader |
||||||
|
header="Update mail server" |
||||||
|
loading={loadingMailServersPeriodic || loadingMailServer} |
||||||
|
ref={editDialogRef} |
||||||
|
showClose |
||||||
|
> |
||||||
|
{mailServer && ( |
||||||
|
<EditMailServerForm |
||||||
|
mailServerUuid={mailServer.uuid} |
||||||
|
onSubmit={(tools, ...args) => { |
||||||
|
setConfirmDialogProps({ |
||||||
|
actionProceedText: 'Update', |
||||||
|
content: <FormSummary entries={tools.mailServer} />, |
||||||
|
onCancelAppend: () => tools.onConfirmCancel(...args), |
||||||
|
onProceedAppend: () => tools.onConfirmProceed(...args), |
||||||
|
titleText: 'Update mail server with the following?', |
||||||
|
}); |
||||||
|
|
||||||
|
setConfirmDialogOpen(true); |
||||||
|
}} |
||||||
|
previousFormikValues={{ [mailServer.uuid]: mailServer }} |
||||||
|
/> |
||||||
|
)} |
||||||
|
</DialogWithHeader> |
||||||
|
{confirmDialog} |
||||||
|
</> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default ManageMailServer; |
@ -0,0 +1,4 @@ |
|||||||
|
import AddMailServerForm from './AddMailServerForm'; |
||||||
|
import ManageMailServer from './ManageMailServer'; |
||||||
|
|
||||||
|
export { AddMailServerForm, ManageMailServer }; |
@ -0,0 +1,27 @@ |
|||||||
|
import * as yup from 'yup'; |
||||||
|
|
||||||
|
import buildYupDynamicObject from '../../lib/buildYupDynamicObject'; |
||||||
|
|
||||||
|
const mailServerSchema = yup.object({ |
||||||
|
address: yup.string().required(), |
||||||
|
authentication: yup.string().oneOf(['none', 'plain-text', 'encrypted']), |
||||||
|
confirmPassword: yup |
||||||
|
.string() |
||||||
|
.when('password', (password, field) => |
||||||
|
String(password).length > 0 |
||||||
|
? field.required().oneOf([yup.ref('password')]) |
||||||
|
: field.optional(), |
||||||
|
), |
||||||
|
heloDomain: yup.string().required(), |
||||||
|
password: yup.string().optional(), |
||||||
|
port: yup.number().required(), |
||||||
|
security: yup.string().oneOf(['none', 'starttls', 'tls-ssl']), |
||||||
|
username: yup.string().optional(), |
||||||
|
uuid: yup.string().uuid().required(), |
||||||
|
}); |
||||||
|
|
||||||
|
const mailServerListSchema = yup.lazy((mailServers) => |
||||||
|
yup.object(buildYupDynamicObject(mailServers, mailServerSchema)), |
||||||
|
); |
||||||
|
|
||||||
|
export default mailServerListSchema; |
@ -0,0 +1,10 @@ |
|||||||
|
type APIMailServerOverview = Pick< |
||||||
|
MailServerFormikMailServer, |
||||||
|
'address' | 'port' | 'uuid' |
||||||
|
>; |
||||||
|
|
||||||
|
type APIMailServerOverviewList = { |
||||||
|
[uuid: string]: APIMailServerOverview; |
||||||
|
}; |
||||||
|
|
||||||
|
type APIMailServerDetail = MailServerFormikMailServer; |
@ -0,0 +1,43 @@ |
|||||||
|
type MailServerFormikMailServer = { |
||||||
|
address: string; |
||||||
|
authentication: 'none' | 'plain-text' | 'encrypted'; |
||||||
|
confirmPassword?: string; |
||||||
|
heloDomain: string; |
||||||
|
password?: string; |
||||||
|
port: number; |
||||||
|
security: 'none' | 'starttls' | 'tls-ssl'; |
||||||
|
username?: string; |
||||||
|
uuid: string; |
||||||
|
}; |
||||||
|
|
||||||
|
type MailServerFormikValues = { |
||||||
|
[mailServerUuid: string]: MailServerFormikMailServer; |
||||||
|
}; |
||||||
|
|
||||||
|
/** AddMailServerForm */ |
||||||
|
|
||||||
|
type FormikSubmitHandler = |
||||||
|
import('formik').FormikConfig<MailServerFormikValues>['onSubmit']; |
||||||
|
|
||||||
|
type AddMailServerFormOptionalProps = { |
||||||
|
localhostDomain?: string; |
||||||
|
mailServerUuid?: string; |
||||||
|
previousFormikValues?: MailServerFormikValues; |
||||||
|
}; |
||||||
|
|
||||||
|
type AddMailServerFormProps = AddMailServerFormOptionalProps & { |
||||||
|
onSubmit: ( |
||||||
|
tools: { |
||||||
|
mailServer: MailServerFormikMailServer; |
||||||
|
onConfirmCancel: FormikSubmitHandler; |
||||||
|
onConfirmProceed: FormikSubmitHandler; |
||||||
|
}, |
||||||
|
...args: Parameters<FormikSubmitHandler> |
||||||
|
) => ReturnType<FormikSubmitHandler>; |
||||||
|
}; |
||||||
|
|
||||||
|
/** EditMailServerForm */ |
||||||
|
|
||||||
|
type EditMailServerFormProps = Required< |
||||||
|
Omit<AddMailServerFormProps, 'localhostDomain'> |
||||||
|
>; |
Loading…
Reference in new issue