Skip to content
Snippets Groups Projects
Unverified Commit c4134211 authored by Nick Fitzpatrick's avatar Nick Fitzpatrick Committed by GitHub
Browse files

Migrate SMTPConnectionForm to Form Provider (#41703)

* Migrate SMTPConnectionForm to Form Provider

* e2e test adjustment

* updaing setting mock

* PR feedback
parent 48b63228
No related branches found
No related tags found
No related merge requests found
...@@ -156,6 +156,11 @@ export const createMockSettings = ( ...@@ -156,6 +156,11 @@ export const createMockSettings = (
"ee-openai-model": "", "ee-openai-model": "",
"ee-openai-api-key": "", "ee-openai-api-key": "",
"email-configured?": false, "email-configured?": false,
"email-smtp-host": null,
"email-smtp-port": null,
"email-smtp-security": "None",
"email-smtp-username": null,
"email-smtp-password": null,
"embedding-app-origin": "", "embedding-app-origin": "",
"enable-embedding": false, "enable-embedding": false,
"enable-enhancements?": false, "enable-enhancements?": false,
......
...@@ -192,6 +192,11 @@ export type HelpLinkSetting = "metabase" | "hidden" | "custom"; ...@@ -192,6 +192,11 @@ export type HelpLinkSetting = "metabase" | "hidden" | "custom";
interface InstanceSettings { interface InstanceSettings {
"admin-email": string; "admin-email": string;
"email-smtp-host": string | null;
"email-smtp-port": number | null;
"email-smtp-security": "None" | "SSL" | "TLS" | "STARTTLS";
"email-smtp-username": string | null;
"email-smtp-password": string | null;
"enable-embedding": boolean; "enable-embedding": boolean;
"enable-nested-queries": boolean; "enable-nested-queries": boolean;
"enable-query-caching"?: boolean; "enable-query-caching"?: boolean;
......
import { useCallback, useEffect, useRef, useState } from "react"; import cx from "classnames";
import { useCallback, useEffect, useState, useMemo } from "react";
import { push } from "react-router-redux"; import { push } from "react-router-redux";
import { t } from "ttag"; import { t } from "ttag";
import _ from "underscore";
import * as Yup from "yup";
import MarginHostingCTA from "metabase/admin/settings/components/widgets/MarginHostingCTA";
import type { SettingElement } from "metabase/admin/settings/types"; import type { SettingElement } from "metabase/admin/settings/types";
import Button from "metabase/core/components/Button"; import Breadcrumbs from "metabase/components/Breadcrumbs";
import CS from "metabase/css/core/index.css"; import CS from "metabase/css/core/index.css";
import {
FormProvider,
Form,
FormTextInput,
FormRadioGroup,
FormSubmitButton,
} from "metabase/forms";
import * as MetabaseAnalytics from "metabase/lib/analytics"; import * as MetabaseAnalytics from "metabase/lib/analytics";
import { color } from "metabase/lib/colors";
import * as Errors from "metabase/lib/errors";
import { useDispatch, useSelector } from "metabase/lib/redux"; import { useDispatch, useSelector } from "metabase/lib/redux";
import { getIsPaidPlan } from "metabase/selectors/settings"; import { getIsPaidPlan } from "metabase/selectors/settings";
import { getIsEmailConfigured, getIsHosted } from "metabase/setup/selectors"; import { getIsEmailConfigured, getIsHosted } from "metabase/setup/selectors";
import { Flex, Stack } from "metabase/ui"; import { Group, Radio, Stack, Button, Text, Flex } from "metabase/ui";
import type { Settings } from "metabase-types/api"; import type { Settings } from "metabase-types/api";
import { import {
...@@ -18,7 +29,7 @@ import { ...@@ -18,7 +29,7 @@ import {
updateEmailSettings, updateEmailSettings,
clearEmailSettings, clearEmailSettings,
} from "../../settings"; } from "../../settings";
import SettingsBatchForm from "../SettingsBatchForm"; import MarginHostingCTA from "../widgets/MarginHostingCTA";
const BREADCRUMBS = [[t`Email`, "/admin/settings/email"], [t`SMTP`]]; const BREADCRUMBS = [[t`Email`, "/admin/settings/email"], [t`SMTP`]];
...@@ -29,35 +40,59 @@ const SEND_TEST_BUTTON_STATES = { ...@@ -29,35 +40,59 @@ const SEND_TEST_BUTTON_STATES = {
}; };
type ButtonStateType = keyof typeof SEND_TEST_BUTTON_STATES; type ButtonStateType = keyof typeof SEND_TEST_BUTTON_STATES;
interface FormRefType {
handleFormErrors: (error: Error) => void;
setFormErrors: (formErrors: any) => void;
setState: ({ formData, dirty }: { formData: object; dirty: boolean }) => void;
}
interface SMTPConnectionFormProps { interface SMTPConnectionFormProps {
elements: SettingElement[]; elements: SettingElement[];
settingValues: Settings; settingValues: Settings;
} }
type FormValueProps = Pick<
Settings,
| "email-smtp-host"
| "email-smtp-port"
| "email-smtp-security"
| "email-smtp-username"
| "email-smtp-password"
>;
const FORM_VALUE_SCHEMA = Yup.object({
"email-smtp-host": Yup.string().required(Errors.required).default(""),
"email-smtp-port": Yup.number()
.positive()
.nullable()
.required(Errors.required)
.default(null),
"email-smtp-security": Yup.string(),
"email-smtp-username": Yup.string().default(""),
"email-smtp-password": Yup.string().default(""),
});
export const SMTPConnectionForm = ({ export const SMTPConnectionForm = ({
elements, elements,
settingValues, settingValues,
}: SMTPConnectionFormProps) => { }: SMTPConnectionFormProps) => {
const [sendingEmail, setSendingEmail] = useState<ButtonStateType>("default"); const [sendingEmail, setSendingEmail] = useState<ButtonStateType>("default");
const [testEmailError, setTestEmailError] = useState<string | null>(null);
const formRef = useRef<FormRefType>();
const isHosted = useSelector(getIsHosted); const isHosted = useSelector(getIsHosted);
const isPaidPlan = useSelector(getIsPaidPlan); const isPaidPlan = useSelector(getIsPaidPlan);
const isEmailConfigured = useSelector(getIsEmailConfigured); const isEmailConfigured = useSelector(getIsEmailConfigured);
const dispatch = useDispatch(); const dispatch = useDispatch();
const elementMap = useMemo(() => _.indexBy(elements, "key"), [elements]);
const initialValues = useMemo<FormValueProps>(
() => ({
"email-smtp-host": settingValues["email-smtp-host"] || "",
"email-smtp-port": settingValues["email-smtp-port"],
"email-smtp-security": settingValues["email-smtp-security"] || "none",
"email-smtp-username": settingValues["email-smtp-username"] || "",
"email-smtp-password": settingValues["email-smtp-password"] || "",
}),
[settingValues],
);
const handleClearEmailSettings = useCallback(async () => { const handleClearEmailSettings = useCallback(async () => {
await dispatch(clearEmailSettings()); await dispatch(clearEmailSettings());
// NOTE: reaching into form component is not ideal
formRef.current?.setState({ formData: {}, dirty: false });
}, [dispatch]); }, [dispatch]);
const handleUpdateEmailSettings = useCallback( const handleUpdateEmailSettings = useCallback(
...@@ -71,40 +106,31 @@ export const SMTPConnectionForm = ({ ...@@ -71,40 +106,31 @@ export const SMTPConnectionForm = ({
[dispatch, isEmailConfigured], [dispatch, isEmailConfigured],
); );
const handleSendTestEmail = useCallback( const handleSendTestEmail = useCallback(async () => {
async (e: React.MouseEvent) => { setSendingEmail("working");
e.preventDefault(); setTestEmailError(null);
setSendingEmail("working"); try {
// NOTE: reaching into form component is not ideal await dispatch(sendTestEmail());
formRef.current?.setFormErrors(null); setSendingEmail("success");
MetabaseAnalytics.trackStructEvent(
try { "Email Settings",
await dispatch(sendTestEmail()); "Test Email",
setSendingEmail("success"); "success",
MetabaseAnalytics.trackStructEvent( );
"Email Settings",
"Test Email", // show a confirmation for 3 seconds, then return to normal
"success", setTimeout(() => setSendingEmail("default"), 3000);
); } catch (error: any) {
MetabaseAnalytics.trackStructEvent(
// show a confirmation for 3 seconds, then return to normal "Email Settings",
setTimeout(() => setSendingEmail("default"), 3000); "Test Email",
} catch (error: any) { "error",
MetabaseAnalytics.trackStructEvent( );
"Email Settings", setSendingEmail("default");
"Test Email", setTestEmailError(error?.data?.message);
"error", }
); }, [dispatch]);
setSendingEmail("default");
// NOTE: reaching into form component is not ideal
formRef.current?.setFormErrors(
formRef.current?.handleFormErrors(error),
);
}
},
[dispatch],
);
useEffect(() => { useEffect(() => {
if (isHosted) { if (isHosted) {
...@@ -113,50 +139,136 @@ export const SMTPConnectionForm = ({ ...@@ -113,50 +139,136 @@ export const SMTPConnectionForm = ({
}, [dispatch, isHosted]); }, [dispatch, isHosted]);
return ( return (
<Stack spacing="sm"> <Flex justify="space-between">
<Flex justify="space-between"> <Stack spacing="sm" maw={400} style={{ paddingInlineStart: "0.5rem" }}>
<SettingsBatchForm {isEmailConfigured && (
ref={formRef} <Breadcrumbs crumbs={BREADCRUMBS} className={cx(CS.mb3)} />
breadcrumbs={isEmailConfigured ? BREADCRUMBS : null} )}
elements={elements} <FormProvider
settingValues={settingValues} initialValues={initialValues}
updateSettings={handleUpdateEmailSettings} validationSchema={FORM_VALUE_SCHEMA}
renderExtraButtons={({ onSubmit={handleUpdateEmailSettings}
disabled, enableReinitialize
valid, >
pristine, {({ dirty, isValid, isSubmitting, values }) => (
submitting, <Form>
}: { <FormTextInput
disabled: boolean; name="email-smtp-host"
valid: boolean; label={elementMap["email-smtp-host"]["display_name"]}
pristine: boolean; description={elementMap["email-smtp-host"]["description"]}
submitting: ButtonStateType; placeholder={elementMap["email-smtp-host"]["placeholder"]}
}) => ( mb="1.5rem"
<> labelProps={{
{valid && pristine && submitting === "default" ? ( tt: "uppercase",
<Button mb: "0.5rem",
className={CS.mr1} }}
success={sendingEmail === "success"} descriptionProps={{
disabled={disabled} fz: "0.75rem",
onClick={handleSendTestEmail} mb: "0.5rem",
> }}
{SEND_TEST_BUTTON_STATES[sendingEmail]} />
</Button> <FormTextInput
) : null} name="email-smtp-port"
<Button label={elementMap["email-smtp-port"]["display_name"]}
className={CS.mr1} description={elementMap["email-smtp-port"]["description"]}
disabled={disabled} placeholder={elementMap["email-smtp-port"]["placeholder"]}
onClick={handleClearEmailSettings} mb="1.5rem"
labelProps={{
tt: "uppercase",
mb: "0.5rem",
}}
descriptionProps={{
fz: "0.75rem",
mb: "0.5rem",
}}
/>
<FormRadioGroup
name="email-smtp-security"
label={elementMap["email-smtp-security"]["display_name"]}
description={elementMap["email-smtp-security"]["description"]}
mb="1.5rem"
labelProps={{
tt: "uppercase",
fz: "0.875rem",
c: "text-medium",
mb: "0.5rem",
}}
> >
{t`Clear`} <Group>
</Button> {Object.entries(
</> elementMap["email-smtp-security"].options || {},
).map(([value, name]) => (
<Radio
value={value}
label={name}
key={value}
styles={{
inner: { display: "none" },
label: {
paddingLeft: 0,
color:
values["email-smtp-security"] === value
? color("brand")
: color("text-dark"),
},
}}
/>
))}
</Group>
</FormRadioGroup>
<FormTextInput
name="email-smtp-username"
label={elementMap["email-smtp-username"]["display_name"]}
description={elementMap["email-smtp-username"]["description"]}
placeholder={elementMap["email-smtp-username"]["placeholder"]}
mb="1.5rem"
labelProps={{
tt: "uppercase",
mb: "0.5rem",
}}
/>
<FormTextInput
name="email-smtp-password"
type="password"
label={elementMap["email-smtp-password"]["display_name"]}
description={elementMap["email-smtp-password"]["description"]}
placeholder={elementMap["email-smtp-password"]["placeholder"]}
mb="1.5rem"
labelProps={{
tt: "uppercase",
mb: "0.5rem",
}}
/>
{testEmailError && (
<Text
role="alert"
aria-label={testEmailError}
color="error"
mb="1rem"
>
{testEmailError}
</Text>
)}
<Flex mt="1rem" gap="1.5rem">
<FormSubmitButton
label={t`Save changes`}
disabled={!dirty}
variant="filled"
/>
{!dirty && isValid && !isSubmitting && (
<Button onClick={handleSendTestEmail}>
{SEND_TEST_BUTTON_STATES[sendingEmail]}
</Button>
)}
<Button onClick={handleClearEmailSettings}>{t`Clear`}</Button>
</Flex>
</Form>
)} )}
/> </FormProvider>
{!isPaidPlan && ( </Stack>
<MarginHostingCTA tagline={t`Have your email configured for you.`} /> {!isPaidPlan && (
)} <MarginHostingCTA tagline={t`Have your email configured for you.`} />
</Flex> )}
</Stack> </Flex>
); );
}; };
...@@ -116,8 +116,11 @@ export const sendTestEmail = createThunkAction(SEND_TEST_EMAIL, function () { ...@@ -116,8 +116,11 @@ export const sendTestEmail = createThunkAction(SEND_TEST_EMAIL, function () {
export const CLEAR_EMAIL_SETTINGS = export const CLEAR_EMAIL_SETTINGS =
"metabase/admin/settings/CLEAR_EMAIL_SETTINGS"; "metabase/admin/settings/CLEAR_EMAIL_SETTINGS";
export const clearEmailSettings = createAction(CLEAR_EMAIL_SETTINGS, () => export const clearEmailSettings = createThunkAction(
EmailApi.clear(), CLEAR_EMAIL_SETTINGS,
() => async dispatch => {
await EmailApi.clear(), await dispatch(reloadSettings());
},
); );
export const UPDATE_SLACK_SETTINGS = export const UPDATE_SLACK_SETTINGS =
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment