From 0941f3ce1aa9056c8b7b8dbb320672c3842e9587 Mon Sep 17 00:00:00 2001 From: karenvicent Date: Thu, 12 Feb 2026 12:13:36 -0500 Subject: [PATCH 1/5] zod schema and hook --- client/src/Hooks/useSettingsForm.ts | 35 +++++++++++++++++++++++++++ client/src/Validation/settings.ts | 37 +++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 client/src/Hooks/useSettingsForm.ts create mode 100644 client/src/Validation/settings.ts diff --git a/client/src/Hooks/useSettingsForm.ts b/client/src/Hooks/useSettingsForm.ts new file mode 100644 index 000000000..6abf386be --- /dev/null +++ b/client/src/Hooks/useSettingsForm.ts @@ -0,0 +1,35 @@ +import { useMemo } from "react"; +import { settingsSchema } from "@/Validation/settings"; +import type { Settings } from "@/Types/Settings"; +import type { SettingsFormData } from "@/Validation/settings"; + +interface UseSettingsFormOptions { + data?: Settings | null; +} +export const useSettingsForm = ({ data = null }: UseSettingsFormOptions = {}) => { + return useMemo(() => { + const defaults: SettingsFormData = { + systemEmailIgnoreTLS: data?.systemEmailIgnoreTLS || false, + systemEmailRequireTLS: data?.systemEmailRequireTLS || false, + systemEmailRejectUnauthorized: data?.systemEmailRejectUnauthorized || true, + systemEmailConnectionHost: data?.systemEmailConnectionHost || "", + systemEmailSecure: data?.systemEmailSecure || true, + systemEmailPool: data?.systemEmailPool || false, + showURL: data?.showURL || false, + //timezone: data?.timezone || "UTC", + language: data?.language || "en", + //chartType: data?.chartType || "histogram", + checkTTL: data?.checkTTL || 30, + pagespeedApiKey: data?.pagespeedApiKey || "", + systemEmailHost: data?.systemEmailHost || "", + systemEmailPort: data?.systemEmailPort || "", + systemEmailAddress: data?.systemEmailAddress || "", + systemEmailUser: data?.systemEmailUser || "", + systemEmailPassword: data?.systemEmailPassword || "", + + } + + + + } +} \ No newline at end of file diff --git a/client/src/Validation/settings.ts b/client/src/Validation/settings.ts new file mode 100644 index 000000000..99d0d826d --- /dev/null +++ b/client/src/Validation/settings.ts @@ -0,0 +1,37 @@ +import { z } from "zod"; + +export const settingsSchema = z.object({ + systemEmailIgnoreTLS: z.boolean(), + systemEmailRequireTLS: z.boolean(), + systemEmailRejectUnauthorized: z.boolean(), + systemEmailConnectionHost: z.string().optional().or(z.literal("")), + systemEmailSecure: z.boolean().optional(), + systemEmailPool: z.boolean().optional(), + showURL: z.boolean().optional(), + timezone: z.string().min(1, "Timezone is required").optional(), + mode: z.enum(["light", "dark"]).optional(), + language: z.string().min(2, "Language is required"), + chartType: z.enum(["histogram", "heatmap"]).optional(), + checkTTL: z.coerce.number().int().min(1, "Please enter a value"), + pagespeedApiKey: z.string().optional().or(z.literal("")), + systemEmailHost: z.string().optional().or(z.literal("")), + systemEmailPort: z.coerce.number().nullable().optional().or(z.literal("")), + systemEmailAddress: z + .email("Please enter a valid email address") + .transform((val) => val.toLowerCase().trim()) + .optional() + .or(z.literal("")), + systemEmailUser: z.string().optional().or(z.literal("")), + systemEmailPassword: z.string().optional().or(z.literal("")), + systemEmailTLSServername: z.string().optional().or(z.literal("")), + globalThresholds: z + .object({ + cpu: z.coerce.number().min(1).max(100).optional(), + memory: z.coerce.number().min(1).max(100).optional(), + disk: z.coerce.number().min(1).max(100).optional(), + temperature: z.coerce.number().min(1).max(150).optional(), + }) + .optional(), +}); + +export type SettingsFormData = z.infer; From 93c15588a4f46b7551bdd3f5eb6980a26f848e14 Mon Sep 17 00:00:00 2001 From: karenvicent Date: Mon, 16 Feb 2026 00:09:08 -0500 Subject: [PATCH 2/5] implement schema zod and validation on settings --- client/src/Hooks/useSettingsForm.ts | 54 ++-- .../Pages/Settings/SettingsDemoMonitors.jsx | 25 +- client/src/Pages/Settings/SettingsEmail.jsx | 194 ++++++++------ client/src/Pages/Settings/SettingsExport.jsx | 36 ++- .../Settings/SettingsGlobalThresholds.jsx | 52 ++-- .../src/Pages/Settings/SettingsPagespeed.jsx | 44 ++-- client/src/Pages/Settings/SettingsStats.jsx | 16 +- .../src/Pages/Settings/SettingsTimeZone.jsx | 25 +- client/src/Pages/Settings/SettingsUI.jsx | 20 +- client/src/Pages/Settings/SettingsURL.jsx | 30 ++- client/src/Pages/Settings/index.jsx | 237 +++++------------- client/src/Validation/settings.ts | 32 ++- 12 files changed, 343 insertions(+), 422 deletions(-) diff --git a/client/src/Hooks/useSettingsForm.ts b/client/src/Hooks/useSettingsForm.ts index 6abf386be..17a67af25 100644 --- a/client/src/Hooks/useSettingsForm.ts +++ b/client/src/Hooks/useSettingsForm.ts @@ -4,32 +4,34 @@ import type { Settings } from "@/Types/Settings"; import type { SettingsFormData } from "@/Validation/settings"; interface UseSettingsFormOptions { - data?: Settings | null; + data?: Settings | null; } export const useSettingsForm = ({ data = null }: UseSettingsFormOptions = {}) => { - return useMemo(() => { - const defaults: SettingsFormData = { - systemEmailIgnoreTLS: data?.systemEmailIgnoreTLS || false, - systemEmailRequireTLS: data?.systemEmailRequireTLS || false, - systemEmailRejectUnauthorized: data?.systemEmailRejectUnauthorized || true, - systemEmailConnectionHost: data?.systemEmailConnectionHost || "", - systemEmailSecure: data?.systemEmailSecure || true, - systemEmailPool: data?.systemEmailPool || false, - showURL: data?.showURL || false, - //timezone: data?.timezone || "UTC", - language: data?.language || "en", - //chartType: data?.chartType || "histogram", - checkTTL: data?.checkTTL || 30, - pagespeedApiKey: data?.pagespeedApiKey || "", - systemEmailHost: data?.systemEmailHost || "", - systemEmailPort: data?.systemEmailPort || "", - systemEmailAddress: data?.systemEmailAddress || "", - systemEmailUser: data?.systemEmailUser || "", - systemEmailPassword: data?.systemEmailPassword || "", - - } + return useMemo(() => { + const defaults: SettingsFormData = { + systemEmailIgnoreTLS: data?.systemEmailIgnoreTLS || false, + systemEmailRequireTLS: data?.systemEmailRequireTLS || false, + systemEmailRejectUnauthorized: data?.systemEmailRejectUnauthorized || true, + systemEmailSecure: data?.systemEmailSecure || false, + systemEmailPool: data?.systemEmailPool || false, + showURL: data?.showURL || false, + systemEmailHost: data?.systemEmailHost || "", + systemEmailUser: data?.systemEmailUser || "", + systemEmailAddress: data?.systemEmailAddress || "", + systemEmailConnectionHost: data?.systemEmailConnectionHost || "localhost", + systemEmailTLSServername: data?.systemEmailTLSServername || "", + systemEmailPort: data?.systemEmailPort || "", + globalThresholds: { + cpu: data?.globalThresholds?.cpu || "", + memory: data?.globalThresholds?.memory || "", + disk: data?.globalThresholds?.disk || "", + temperature: data?.globalThresholds?.temperature || "", + }, + checkTTL: data?.checkTTL || 30, + pagespeedApiKey: "", + systemEmailPassword: "", + }; - - - } -} \ No newline at end of file + return { schema: settingsSchema, defaults }; + }, [data]); +}; diff --git a/client/src/Pages/Settings/SettingsDemoMonitors.jsx b/client/src/Pages/Settings/SettingsDemoMonitors.jsx index 47e699851..eb7f86c7a 100644 --- a/client/src/Pages/Settings/SettingsDemoMonitors.jsx +++ b/client/src/Pages/Settings/SettingsDemoMonitors.jsx @@ -8,8 +8,9 @@ import { PropTypes } from "prop-types"; import { useTranslation } from "react-i18next"; import Dialog from "@/Components/v1/Dialog/index.jsx"; import { useState } from "react"; +import { useDelete, usePost } from "@/Hooks/UseApi"; -const SettingsDemoMonitors = ({ isAdmin, HEADER_SX, handleChange, isLoading }) => { +const SettingsDemoMonitors = ({ isAdmin, HEADER_SX, isLoading }) => { const { t } = useTranslation(); const theme = useTheme(); // Local state @@ -18,7 +19,8 @@ const SettingsDemoMonitors = ({ isAdmin, HEADER_SX, handleChange, isLoading }) = if (!isAdmin) { return null; } - + const { post: postDemoMonitors } = usePost(); + const { deleteFn: deleteAllMonitorsFn } = useDelete(); return ( <> @@ -38,13 +40,8 @@ const SettingsDemoMonitors = ({ isAdmin, HEADER_SX, handleChange, isLoading }) = variant="contained" color="accent" loading={isLoading} - onClick={() => { - const syntheticEvent = { - target: { - name: "demo", - }, - }; - handleChange(syntheticEvent); + onClick={async () => { + await postDemoMonitors("/monitors/demo", {}); }} sx={{ mt: theme.spacing(4) }} > @@ -81,13 +78,8 @@ const SettingsDemoMonitors = ({ isAdmin, HEADER_SX, handleChange, isLoading }) = title={t("settingsPage.systemResetSettings.dialogTitle")} onCancel={() => setIsOpen(false)} confirmationButtonLabel={t("settingsPage.systemResetSettings.dialogConfirm")} - onConfirm={() => { - const syntheticEvent = { - target: { - name: "deleteMonitors", - }, - }; - handleChange(syntheticEvent); + onConfirm={async () => { + await deleteAllMonitorsFn("/monitors/"); setIsOpen(false); }} isLoading={isLoading} @@ -99,7 +91,6 @@ const SettingsDemoMonitors = ({ isAdmin, HEADER_SX, handleChange, isLoading }) = SettingsDemoMonitors.propTypes = { isAdmin: PropTypes.bool, - handleChange: PropTypes.func, HEADER_SX: PropTypes.object, }; diff --git a/client/src/Pages/Settings/SettingsEmail.jsx b/client/src/Pages/Settings/SettingsEmail.jsx index f31373ed9..024fd5854 100644 --- a/client/src/Pages/Settings/SettingsEmail.jsx +++ b/client/src/Pages/Settings/SettingsEmail.jsx @@ -15,16 +15,18 @@ import { PasswordEndAdornment } from "@/Components/v1/Inputs/TextInput/Adornment import { usePost } from "@/Hooks/UseApi"; import { useSelector } from "react-redux"; import { createToast } from "@/Utils/toastUtils.jsx"; +import { Controller } from "react-hook-form"; const SettingsEmail = ({ isAdmin, HEADER_SX, - handleChange, - settingsData, - setSettingsData, isEmailPasswordSet, emailPasswordHasBeenReset, setEmailPasswordHasBeenReset, + control, + defaults, + formValues, + setValue, }) => { // Setup const { t } = useTranslation(); @@ -44,22 +46,13 @@ const SettingsEmail = ({ systemEmailIgnoreTLS = false, systemEmailRequireTLS = false, systemEmailRejectUnauthorized = true, - } = settingsData?.settings || {}; - // Local state - const [password, setPassword] = useState(""); + } = formValues || {}; // Network const { post: sendTestEmailFn, loading: isSending } = usePost(); const user = useSelector((state) => state.auth.user); // Handlers - const handlePasswordChange = (e) => { - setPassword(e.target.value); - setSettingsData({ - ...settingsData, - settings: { ...settingsData.settings, systemEmailPassword: e.target.value }, - }); - }; /** * Handle sending test email with current form values @@ -70,7 +63,7 @@ const SettingsEmail = ({ !systemEmailHost || !systemEmailPort || !systemEmailAddress || - !(password || systemEmailPassword) + !systemEmailPassword ) { createToast({ body: t("settingsPage.emailSettings.toastEmailRequiredFieldsError"), @@ -117,52 +110,87 @@ const SettingsEmail = ({ - ( + + )} /> - ( + + )} /> - ( + + )} /> - ( + + )} /> {(isEmailPasswordSet === false || emailPasswordHasBeenReset === true) && ( - } + control={control} + defaultValue={defaults.systemEmailPassword} + render={({ field, fieldState }) => ( + } + error={!!fieldState.error} + helperText={fieldState.error?.message} + /> + )} /> )} @@ -172,11 +200,6 @@ const SettingsEmail = ({ {t("settingsPage.emailSettings.labelPasswordSet")} diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 8e18d8e2a..94d5b5642 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -901,6 +901,93 @@ "title": "A PageSpeed monitor is used to:" } }, + "settings": { + "aboutSettings": { + "labelDevelopedBy": "Developed by Bluewave Labs", + "labelVersion": "Version", + "title": "About" + }, + "demoMonitorsSettings": { + "buttonAddMonitors": "Add demo monitors", + "description": "Add sample monitors for demonstration purposes.", + "title": "Demo monitors" + }, + "emailSettings": { + "buttonSendTestEmail": "Send test e-mail", + "description": "Configure the email settings for your system. This is used to send notifications and alerts.", + "descriptionTransport": "This builds an SMTP transport for NodeMailer", + "labelAddress": "Email address - Used for authentication", + "labelConnectionHost": "Email connection host - Hostname to use in the HELO/EHLO greeting", + "labelHost": "Email host - Hostname or IP address to connect to", + "labelIgnoreTLS": "Disable STARTTLS: Don't use TLS even if the server supports it", + "labelPassword": "Email password - Password for authentication", + "labelPasswordSet": "Password is set. Click Reset to change it.", + "labelPool": "Enable connection pooling: Reuse existing connections to improve performance", + "labelPort": "Email port - Port to connect to", + "labelRejectUnauthorized": "Reject invalid certificates: Reject connections with self-signed or untrusted certificates", + "labelRequireTLS": "Force STARTTLS: Require TLS upgrade, fail if not supported", + "labelSecure": "Use SSL (recommended): Encrypt the connection using SSL/TLS", + "labelTLSServername": "TLS Servername - Optional Hostname for TLS Validation when host is an IP", + "labelUser": "Email user - Username for authentication, overrides email address if specified", + "linkTransport": "See specifications here", + "placeholderUser": "Leave empty if not required", + "title": "Email", + "toastEmailRequiredFieldsError": "Email address, host, port and password are required" + }, + "globalThresholds": { + "title": "Global Thresholds", + "description": "Configure global CPU, Memory, Disk, and Temperature thresholds. If a value is provided, it will automatically be enabled for monitoring." + }, + "pageSpeedSettings": { + "description": "Enter your Google PageSpeed API key to enable Google PageSpeed monitoring. Click Reset to update the key.", + "labelApiKeySet": "API key is set. Click Reset to change it.", + "labelApiKey": "PageSpeed API key", + "title": "Google PageSpeed API key" + }, + "saveButtonLabel": "Save", + "statsSettings": { + "clearAllStatsButton": "Clear all stats", + "clearAllStatsDescription": "Clear all stats. This is irreversible.", + "clearAllStatsDialogConfirm": "Yes, clear all stats", + "clearAllStatsDialogDescription": "Once removed, the monitoring history and stats cannot be retrieved.", + "clearAllStatsDialogTitle": "Do you want to clear all stats?", + "description": "Define how long you want to retain historical data. You can also clear all existing data.", + "labelTTL": "The days you want to keep monitoring history.", + "labelTTLOptional": "0 for infinite", + "title": "Monitor history" + }, + "systemResetSettings": { + "buttonRemoveAllMonitors": "Remove all monitors", + "description": "Remove all monitors from your system.", + "dialogConfirm": "Yes, remove all monitors", + "dialogDescription": "Once removed, the monitors cannot be retrieved.", + "dialogTitle": "Do you want to remove all monitors?", + "title": "System reset" + }, + "timezoneSettings": { + "description": "Select the timezone used to display dates and times throughout the application.", + "label": "Display timezone", + "title": "Display timezone" + }, + "title": "Settings", + "uiSettings": { + "description": "Switch between light and dark mode, or change user interface language.", + "labelLanguage": "Language", + "labelTheme": "Theme mode", + "labelChartType": "Chart type", + "chartTypeHistogram": "Histogram", + "chartTypeHeatmap": "Heatmap", + "title": "Appearance" + }, + "urlSettings": { + "description": "Display the IP address or URL of monitor on the public Status page. If it's disabled, only the monitor name will be shown to protect sensitive information.", + "label": "Display IP/URL on status page", + "selectDisabled": "Disabled", + "selectEnabled": "Enabled", + "title": "Monitor IP/URL on Status Page" + } + }, + "statusPages": { "deleteSuccess": "Status page deleted successfully", "fallback": { From e921b20fe38f3835e48517db21e49aefb5210360 Mon Sep 17 00:00:00 2001 From: karenvicent Date: Tue, 17 Feb 2026 22:21:56 -0500 Subject: [PATCH 4/5] fix zod patterns --- client/src/Pages/Settings/SettingsEmail.jsx | 2 +- client/src/Validation/settings.ts | 19 ++++++++++++------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/client/src/Pages/Settings/SettingsEmail.jsx b/client/src/Pages/Settings/SettingsEmail.jsx index 5757df98e..0ab8d8b2a 100644 --- a/client/src/Pages/Settings/SettingsEmail.jsx +++ b/client/src/Pages/Settings/SettingsEmail.jsx @@ -78,7 +78,7 @@ const SettingsEmail = ({ systemEmailHost, systemEmailPort, systemEmailAddress, - systemEmailPassword: password || systemEmailPassword, + systemEmailPassword, systemEmailSecure, systemEmailPool, systemEmailIgnoreTLS, diff --git a/client/src/Validation/settings.ts b/client/src/Validation/settings.ts index 71f059e17..84c6b6ee6 100644 --- a/client/src/Validation/settings.ts +++ b/client/src/Validation/settings.ts @@ -4,22 +4,27 @@ export const settingsSchema = z.object({ systemEmailIgnoreTLS: z.boolean(), systemEmailRequireTLS: z.boolean(), systemEmailRejectUnauthorized: z.boolean(), - systemEmailConnectionHost: z.string().optional().or(z.literal("")), + systemEmailConnectionHost: z.string().optional(), systemEmailSecure: z.boolean().optional(), systemEmailPool: z.boolean().optional(), showURL: z.boolean().optional(), checkTTL: z.coerce.number().int().min(1, "Please enter a value"), - pagespeedApiKey: z.string().optional().or(z.literal("")), - systemEmailHost: z.string().optional().or(z.literal("")), - systemEmailPort: z.coerce.number().nullable().optional().or(z.literal("")), + pagespeedApiKey: z.string().optional(), + systemEmailHost: z.string().optional(), + systemEmailPort: z.coerce + .number() + .int() + .min(1, "Port must be at least 1") + .optional() + .or(z.literal("")), systemEmailAddress: z .email("Please enter a valid email address") .transform((val) => val.toLowerCase().trim()) .optional() .or(z.literal("")), - systemEmailUser: z.string().optional().or(z.literal("")), - systemEmailPassword: z.string().optional().or(z.literal("")), - systemEmailTLSServername: z.string().optional().or(z.literal("")), + systemEmailUser: z.string().optional(), + systemEmailPassword: z.string().optional(), + systemEmailTLSServername: z.string().optional(), globalThresholds: z .object({ cpu: z.coerce From 59d245b42451232131adf436abc4f1f320164a6f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 18 Feb 2026 17:27:59 +0000 Subject: [PATCH 5/5] remove pre tag --- client/src/Pages/Settings/SettingsEmail.jsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/client/src/Pages/Settings/SettingsEmail.jsx b/client/src/Pages/Settings/SettingsEmail.jsx index 0ab8d8b2a..180b7f2d9 100644 --- a/client/src/Pages/Settings/SettingsEmail.jsx +++ b/client/src/Pages/Settings/SettingsEmail.jsx @@ -340,14 +340,6 @@ const SettingsEmail = ({ -
-					{JSON.stringify({
-						systemEmailHost: systemEmailHost,
-						systemEmailAddress: systemEmailAddress,
-						systemEmailPassword: systemEmailPassword,
-					})}
-				
- {systemEmailHost && systemEmailPort &&