feat(striker-ui): add manager mail server components

main
Tsu-ba-me 11 months ago
parent ce2fad66c7
commit c623170334
  1. 270
      striker-ui/components/ManageMailServer/AddMailServerForm.tsx
  2. 9
      striker-ui/components/ManageMailServer/EditMailServerForm.tsx
  3. 179
      striker-ui/components/ManageMailServer/ManageMailServer.tsx
  4. 4
      striker-ui/components/ManageMailServer/index.ts
  5. 27
      striker-ui/components/ManageMailServer/schema.ts
  6. 10
      striker-ui/types/APIMailServer.d.ts
  7. 43
      striker-ui/types/ManageMailServer.d.ts

@ -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…
Cancel
Save