diff --git a/client/src/Components/inputs/forms/FormAutoCompleteField.tsx b/client/src/Components/inputs/forms/FormAutoCompleteField.tsx index 104141b87..1dbc65118 100644 --- a/client/src/Components/inputs/forms/FormAutoCompleteField.tsx +++ b/client/src/Components/inputs/forms/FormAutoCompleteField.tsx @@ -18,7 +18,10 @@ interface FormAutocompleteProps({ +export const FormAutocompleteField = < + T extends FieldValues, + O extends AutocompleteOption, +>({ name, options, fieldLabel, diff --git a/client/src/Components/inputs/forms/FormSwitchField.tsx b/client/src/Components/inputs/forms/FormSwitchField.tsx index 8d9357ce1..ed65d99f2 100644 --- a/client/src/Components/inputs/forms/FormSwitchField.tsx +++ b/client/src/Components/inputs/forms/FormSwitchField.tsx @@ -13,11 +13,13 @@ import { SPACING } from "@/Utils/Theme/constants"; interface FormSwitchProps { name: FieldPath; label: string; + labelPlacement?: "start" | "end"; // left | right } export const FormSwitchField = ({ name, label, + labelPlacement = "end", }: FormSwitchProps) => { const { control } = useFormContext(); const theme = useTheme(); @@ -29,14 +31,27 @@ export const FormSwitchField = ({ render={({ field }) => ( - field.onChange(e.target.checked)} - /> - {label} + {labelPlacement === "start" ? ( + <> + {label} + field.onChange(e.target.checked)} + /> + + ) : ( + <> + field.onChange(e.target.checked)} + /> + {label} + + )} )} /> diff --git a/client/src/Pages/Settings/index.tsx b/client/src/Pages/Settings/index.tsx index e14366e0f..2ec402759 100644 --- a/client/src/Pages/Settings/index.tsx +++ b/client/src/Pages/Settings/index.tsx @@ -1,5 +1,5 @@ import { BasePage, ConfigBox, TextLink } from "@/Components/design-elements"; -import { Autocomplete, Select, Dialog, SwitchComponent } from "@/Components/inputs"; +import { Autocomplete, Select, Dialog } from "@/Components/inputs"; import { logger } from "@/Utils/logger"; import { LAYOUT } from "@/Utils/Theme/constants"; import { @@ -11,7 +11,7 @@ import { type SelectChangeEvent, } from "@mui/material"; import { useEffect } from "react"; -import { useForm } from "react-hook-form"; +import { FormProvider, useForm, type FieldPath } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { useTranslation } from "react-i18next"; import { useSelector, useDispatch } from "react-redux"; @@ -22,8 +22,7 @@ import { useSettingsForm } from "@/Hooks/useSettingsForm"; import { useIsAdmin } from "@/Hooks/useIsAdmin.js"; import type { SettingsFormData, SettingsFormInput } from "@/Validation/settings"; import { useState } from "react"; -import { Controller } from "react-hook-form"; -import { TextField, Button, FieldLabel, SliderWithLabel } from "@/Components/inputs"; +import { Button, FieldLabel } from "@/Components/inputs"; import { languageNames } from "@/Components/inputs/LanguageSelector"; import { Box, Typography } from "@mui/material"; import { useDelete } from "@/Hooks/UseApi"; @@ -39,6 +38,9 @@ import { import timezones from "@/Utils/timezones.json"; import type { RootState } from "@/Types/state"; import { CHECK_TTL_SENTINEL } from "@/Types/Check"; +import { FormTextField } from "@/Components/inputs/forms/FormTextField"; +import { FormSliderField } from "@/Components/inputs/forms/FormSliderField"; +import { FormSwitchField } from "@/Components/inputs/forms/FormSwitchField"; interface Timezone { id: string; @@ -90,6 +92,26 @@ export const SettingsPage = () => { // Initialize form with schema and defaults const { schema, defaults } = useSettingsForm({ data: fetchedSettings?.settings }); + const emailSwitches: { name: FieldPath; label: string }[] = [ + { + name: "systemEmailSecure", + label: t("pages.settings.form.email.option.secure.label"), + }, + { name: "systemEmailPool", label: t("pages.settings.form.email.option.pool.label") }, + { + name: "systemEmailIgnoreTLS", + label: t("pages.settings.form.email.option.ignoreTLS.label"), + }, + { + name: "systemEmailRequireTLS", + label: t("pages.settings.form.email.option.requireTLS.label"), + }, + { + name: "systemEmailRejectUnauthorized", + label: t("pages.settings.form.email.option.rejectUnauthorized.label"), + }, + ]; + const form = useForm({ resolver: zodResolver(schema), defaultValues: defaults, @@ -280,347 +302,233 @@ export const SettingsPage = () => { const languages = Object.keys(i18n.options.resources || {}); return ( - - - option.name} - isOptionEqualToValue={(option: Timezone, value: Timezone) => - option.id === value.id - } - onChange={(_, newValue) => { - handleTimezoneChange(newValue as Timezone | null); - }} - fieldLabel={t("pages.settings.form.timezone.option.timezone.label")} - /> - } - /> - - - - - - - } - /> - {isAdmin && ( + + + - {(isApiKeySet === false || apiKeyHasBeenReset === true) && ( - ( - - )} - /> - )} - - {isApiKeySet === true && apiKeyHasBeenReset === false && ( - - - {t("pages.settings.form.pagespeed.option.apiKey.labelSet")} - - - - )} - - } - /> - )} - - {/* URL Settings */} - ( - - )} - /> - } - /> - - {/* Clear All Stats */} - {isAdmin && ( - setIsStatsDialogOpen(true)} - > - {t("common.buttons.clear")} - - } - /> - )} - - {/* Check Retention */} - {isAdmin && ( - ( - field.onChange(value)} - valueLabelDisplay="auto" - valueLabelFormat={(value: number) => - value >= CHECK_TTL_SENTINEL - ? t("pages.settings.form.retention.option.days.unlimited") - : `${value}` - } - formatDisplayValue={(value: number) => - value >= CHECK_TTL_SENTINEL - ? t("pages.settings.form.retention.option.days.unlimited") - : `${value}` - } - /> - )} + option.name} + isOptionEqualToValue={(option: Timezone, value: Timezone) => + option.id === value.id + } + onChange={(_, newValue) => { + handleTimezoneChange(newValue as Timezone | null); + }} + fieldLabel={t("pages.settings.form.timezone.option.timezone.label")} /> } /> - )} - - {/* Global Thresholds */} - {isAdmin && ( - ( - field.onChange(value)} - valueLabelDisplay="auto" - /> - )} - /> - ( - field.onChange(value)} - valueLabelDisplay="auto" - /> - )} - /> - ( - field.onChange(value)} - valueLabelDisplay="auto" - /> - )} - /> - ( - field.onChange(value)} - valueLabelDisplay="auto" - /> - )} - /> + + + + + } /> - )} - + {isAdmin && ( + + {(isApiKeySet === false || apiKeyHasBeenReset === true) && ( + + )} - {/* Email Settings - Admin Only */} - {isAdmin && ( - - - {(() => { - const address = form.watch("systemEmailAddress") || ""; - const displayName = form.watch("systemEmailDisplayName")?.trim(); - return ( - <> - - - {JSON.stringify( - { - host: form.watch("systemEmailHost") || "", - port: form.watch("systemEmailPort") || "", - secure: form.watch("systemEmailSecure") ?? false, - auth: { - user: form.watch("systemEmailUser") || address, - pass: "", - }, - name: form.watch("systemEmailConnectionHost") || "localhost", - pool: form.watch("systemEmailPool") ?? false, - tls: { - rejectUnauthorized: - form.watch("systemEmailRejectUnauthorized") ?? true, - ignoreTLS: form.watch("systemEmailIgnoreTLS") ?? false, - requireTLS: form.watch("systemEmailRequireTLS") ?? false, - servername: form.watch("systemEmailTLSServername") || "", - }, - }, - null, - 2 - )} - + {isApiKeySet === true && apiKeyHasBeenReset === false && ( + + + {t("pages.settings.form.pagespeed.option.apiKey.labelSet")} + + - {address && ( + )} + + } + /> + )} + + {/* URL Settings */} + + } + /> + + {/* Clear All Stats */} + {isAdmin && ( + setIsStatsDialogOpen(true)} + > + {t("common.buttons.clear")} + + } + /> + )} + + {/* Check Retention */} + {isAdmin && ( + + value >= CHECK_TTL_SENTINEL + ? t("pages.settings.form.retention.option.days.unlimited") + : `${value}` + } + formatDisplayValue={(value: number) => + value >= CHECK_TTL_SENTINEL + ? t("pages.settings.form.retention.option.days.unlimited") + : `${value}` + } + /> + } + /> + )} + + {/* Global Thresholds */} + {isAdmin && ( + + + + + + + + } + /> + )} + + + {/* Email Settings - Admin Only */} + {isAdmin && ( + + + {(() => { + const address = form.watch("systemEmailAddress") || ""; + const displayName = form.watch("systemEmailDisplayName")?.trim(); + return ( + <> { }} > - {`From: ${displayName ? `"${displayName}" <${address}>` : address}`} + {JSON.stringify( + { + host: form.watch("systemEmailHost") || "", + port: form.watch("systemEmailPort") || "", + secure: form.watch("systemEmailSecure") ?? false, + auth: { + user: form.watch("systemEmailUser") || address, + pass: "", + }, + name: + form.watch("systemEmailConnectionHost") || "localhost", + pool: form.watch("systemEmailPool") ?? false, + tls: { + rejectUnauthorized: + form.watch("systemEmailRejectUnauthorized") ?? true, + ignoreTLS: form.watch("systemEmailIgnoreTLS") ?? false, + requireTLS: form.watch("systemEmailRequireTLS") ?? false, + servername: form.watch("systemEmailTLSServername") || "", + }, + }, + null, + 2 + )} - )} - - ); - })()} - - } - rightContent={ - - {/* Email Host */} - ( - - )} - /> - - {/* Email Port */} - ( - { - const val = e.target.value; - field.onChange(val === "" ? 0 : Number(val)); - }} - fieldLabel={t("pages.settings.form.email.option.port.label")} - type="number" - inputProps={{ min: 0 }} - placeholder={t("pages.settings.form.email.option.port.placeholder")} - error={!!fieldState.error} - helperText={fieldState.error?.message} - /> - )} - /> - - {/* Email Address */} - ( - - )} - /> - - {/* Email Display Name (Optional) */} - ( - - )} - /> - - {/* Email User (Optional) */} - ( - - )} - /> - - {/* Email Password with Reset Pattern */} - {isEmailPasswordSet && !emailPasswordHasBeenReset ? ( - - - {t("pages.settings.form.email.option.password.labelSet")} - - - - - - ) : ( - ( - + + {`From: ${displayName ? `"${displayName}" <${address}>` : address}`} + + )} - error={!!fieldState.error} - helperText={fieldState.error?.message} - /> + + ); + })()} + + } + rightContent={ + + {/* Email Host */} + + + {/* Email Port */} + + + {/* Email Address */} + + + {/* Email Display Name (Optional) */} + - )} - {/* TLS Servername (Optional) */} - ( - - )} - /> + {/* Email User (Optional) */} + - {/* Connection Host (Optional) */} - ( - - )} - /> - - {/* Boolean Switches */} - - {[ - { - name: "systemEmailSecure", - label: t("pages.settings.form.email.option.secure.label"), - }, - { - name: "systemEmailPool", - label: t("pages.settings.form.email.option.pool.label"), - }, - { - name: "systemEmailIgnoreTLS", - label: t("pages.settings.form.email.option.ignoreTLS.label"), - }, - { - name: "systemEmailRequireTLS", - label: t("pages.settings.form.email.option.requireTLS.label"), - }, - { - name: "systemEmailRejectUnauthorized", - label: t("pages.settings.form.email.option.rejectUnauthorized.label"), - }, - ].map(({ name, label }) => ( - ( - + + {t("pages.settings.form.email.option.password.labelSet")} + + + + + + ) : ( + - ))} - + )} - {/* Test Email Button */} + {/* TLS Servername (Optional) */} + + + {/* Connection Host (Optional) */} + + + {/* Boolean Switches */} + + {emailSwitches.map(({ name, label }) => ( + + ))} + + + {/* Test Email Button */} + + + + + } + /> + )} + + {/* Demo Monitors - Admin Only */} + {isAdmin && ( + - - } - /> - )} - - {/* Demo Monitors - Admin Only */} - {isAdmin && ( - - - - } - /> - )} - - {/* Remove All Monitors - Admin Only */} - {isAdmin && ( - - - - } - /> - )} - - {/* Export Monitors - Admin Only */} - {isAdmin && ( - - - - - - } - /> - )} - - {/* About */} - - - {t("common.appName")} {__APP_VERSION__} - - - {t("pages.settings.form.about.developedBy")} - - - https://github.com/bluewave-labs/checkmate - - - } - /> - - {/* Clear Stats Confirmation Dialog */} - setIsStatsDialogOpen(false)} - onConfirm={handleClearStats} - loading={isDeletingStats} - /> - - {/* Delete All Monitors Confirmation Dialog */} - setIsDemoMonitorsDialogOpen(false)} - onConfirm={async () => { - await deleteAllMonitors("/monitors/"); - setIsDemoMonitorsDialogOpen(false); - }} - loading={isDeletingAllMonitors} - confirmColor="error" - confirmText={t("common.buttons.removeMonitors")} - > - - {t("pages.settings.form.removeMonitors.dialog.paragraph")} - - - - - {/* Validation Error Display */} - {Object.keys(form.formState.errors).length > 0 && ( - - - {t("pages.settings.form.validation.errorMessage")} - - - {Object.entries(form.formState.errors).map(([field, error]) => { - const message = - typeof error === "object" && error?.message - ? error.message - : "Invalid value"; - return ( -
  • - - {field}: {message} - -
  • - ); - })} -
    -
    + } + /> )} - + + } + /> + )} + + {/* Export Monitors - Admin Only */} + {isAdmin && ( + + + + +
    + } + /> + )} + + {/* About */} + + + {t("common.appName")} {__APP_VERSION__} + + + {t("pages.settings.form.about.developedBy")} + + + https://github.com/bluewave-labs/checkmate + + + } + /> + + {/* Clear Stats Confirmation Dialog */} + setIsStatsDialogOpen(false)} + onConfirm={handleClearStats} + loading={isDeletingStats} + /> + + {/* Delete All Monitors Confirmation Dialog */} + setIsDemoMonitorsDialogOpen(false)} + onConfirm={async () => { + await deleteAllMonitors("/monitors/"); + setIsDemoMonitorsDialogOpen(false); + }} + loading={isDeletingAllMonitors} + confirmColor="error" + confirmText={t("common.buttons.removeMonitors")} > - {t("common.buttons.save")} - - -
    + + {t("pages.settings.form.removeMonitors.dialog.paragraph")} + + + + + {/* Validation Error Display */} + {Object.keys(form.formState.errors).length > 0 && ( + + + {t("pages.settings.form.validation.errorMessage")} + + + {Object.entries(form.formState.errors).map(([field, error]) => { + const message = + typeof error === "object" && error?.message + ? error.message + : "Invalid value"; + return ( +
  • + + {field}: {message} + +
  • + ); + })} +
    +
    + )} + + +
    +
    + ); };