Merge pull request #3288 from bluewave-labs/settings-zod-schema

Settings zod schema
This commit is contained in:
Alexander Holliday
2026-02-18 09:32:28 -08:00
committed by GitHub
14 changed files with 640 additions and 562 deletions
+37
View File
@@ -0,0 +1,37 @@
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,
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: "",
};
return { schema: settingsSchema, defaults };
}, [data]);
};
+2 -2
View File
@@ -16,7 +16,7 @@ const SettingsAbout = () => {
component="h1"
variant="h2"
>
{t("settingsPage.aboutSettings.title")}
{t("pages.settings.aboutSettings.title")}
</Typography>
</Box>
<Box>
@@ -24,7 +24,7 @@ const SettingsAbout = () => {
{t("common.appName")} {__APP_VERSION__}
</Typography>
<Typography sx={{ mt: theme.spacing(2), mb: theme.spacing(6), opacity: 0.6 }}>
{t("settingsPage.aboutSettings.labelDevelopedBy")}
{t("pages.settings.aboutSettings.labelDevelopedBy")}
</Typography>
<Link
level="secondary"
@@ -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 (
<>
<ConfigBox>
@@ -27,10 +29,10 @@ const SettingsDemoMonitors = ({ isAdmin, HEADER_SX, handleChange, isLoading }) =
component="h1"
variant="h2"
>
{t("settingsPage.demoMonitorsSettings.title")}
{t("pages.settings.demoMonitorsSettings.title")}
</Typography>
<Typography sx={HEADER_SX}>
{t("settingsPage.demoMonitorsSettings.description")}
{t("pages.settings.demoMonitorsSettings.description")}
</Typography>
</Box>
<Box>
@@ -38,17 +40,12 @@ 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) }}
>
{t("settingsPage.demoMonitorsSettings.buttonAddMonitors")}
{t("pages.settings.demoMonitorsSettings.buttonAddMonitors")}
</Button>
</Box>
</ConfigBox>
@@ -58,10 +55,10 @@ const SettingsDemoMonitors = ({ isAdmin, HEADER_SX, handleChange, isLoading }) =
component="h1"
variant="h2"
>
{t("settingsPage.systemResetSettings.title")}
{t("pages.settings.systemResetSettings.title")}
</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
{t("settingsPage.systemResetSettings.description")}
{t("pages.settings.systemResetSettings.description")}
</Typography>
</Box>
<Box>
@@ -72,22 +69,17 @@ const SettingsDemoMonitors = ({ isAdmin, HEADER_SX, handleChange, isLoading }) =
onClick={() => setIsOpen(true)}
sx={{ mt: theme.spacing(4) }}
>
{t("settingsPage.systemResetSettings.buttonRemoveAllMonitors")}
{t("pages.settings.systemResetSettings.buttonRemoveAllMonitors")}
</Button>
</Box>
<Dialog
open={isOpen}
theme={theme}
title={t("settingsPage.systemResetSettings.dialogTitle")}
title={t("pages.settings.systemResetSettings.dialogTitle")}
onCancel={() => setIsOpen(false)}
confirmationButtonLabel={t("settingsPage.systemResetSettings.dialogConfirm")}
onConfirm={() => {
const syntheticEvent = {
target: {
name: "deleteMonitors",
},
};
handleChange(syntheticEvent);
confirmationButtonLabel={t("pages.settings.systemResetSettings.dialogConfirm")}
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,
};
+247 -216
View File
@@ -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,10 +63,10 @@ const SettingsEmail = ({
!systemEmailHost ||
!systemEmailPort ||
!systemEmailAddress ||
!(password || systemEmailPassword)
!systemEmailPassword
) {
createToast({
body: t("settingsPage.emailSettings.toastEmailRequiredFieldsError"),
body: t("pages.settings.emailSettings.toastEmailRequiredFieldsError"),
variant: "error",
});
return;
@@ -85,7 +78,7 @@ const SettingsEmail = ({
systemEmailHost,
systemEmailPort,
systemEmailAddress,
systemEmailPassword: password || systemEmailPassword,
systemEmailPassword,
systemEmailSecure,
systemEmailPool,
systemEmailIgnoreTLS,
@@ -108,229 +101,267 @@ const SettingsEmail = ({
component="h1"
variant="h2"
>
{t("settingsPage.emailSettings.title")}
{t("pages.settings.emailSettings.title")}
</Typography>
<Typography sx={HEADER_SX}>
{t("settingsPage.emailSettings.description")}
{t("pages.settings.emailSettings.description")}
</Typography>
</Box>
<Box>
<Stack gap={theme.spacing(10)}>
<Box>
<TextInput
label={t("settingsPage.emailSettings.labelHost")}
name="systemEmailHost"
placeholder="smtp.gmail.com"
value={systemEmailHost}
onChange={handleChange}
/>
</Box>
<Box>
<TextInput
label={t("settingsPage.emailSettings.labelPort")}
name="systemEmailPort"
placeholder="425"
type="number"
value={systemEmailPort}
onChange={handleChange}
/>
</Box>
<Box>
<TextInput
label={t("settingsPage.emailSettings.labelUser")}
name="systemEmailUser"
placeholder={t("settingsPage.emailSettings.placeholderUser")}
value={systemEmailUser}
onChange={handleChange}
/>
</Box>
<Box>
<TextInput
label={t("settingsPage.emailSettings.labelAddress")}
name="systemEmailAddress"
placeholder="uptime@bluewavelabs.ca"
value={systemEmailAddress}
onChange={handleChange}
/>
</Box>
{(isEmailPasswordSet === false || emailPasswordHasBeenReset === true) && (
<Box>
<TextInput
label={t("settingsPage.emailSettings.labelPassword")}
name="systemEmailPassword"
type="password"
placeholder="123 456 789 101112"
value={password}
onChange={handlePasswordChange}
endAdornment={<PasswordEndAdornment />}
/>
</Box>
)}
{isEmailPasswordSet === true && emailPasswordHasBeenReset === false && (
<Box>
<Typography>{t("settingsPage.emailSettings.labelPasswordSet")}</Typography>
<Button
onClick={() => {
setPassword("");
setSettingsData({
...settingsData,
settings: { ...settingsData.settings, systemEmailPassword: "" },
});
setEmailPasswordHasBeenReset(true);
}}
variant="contained"
color="error"
sx={{ mt: theme.spacing(4) }}
>
{t("reset")}
</Button>
</Box>
)}
<Stack gap={theme.spacing(10)}>
<Box>
<Controller
name="systemEmailHost"
control={control}
defaultValue={defaults.systemEmailHost}
render={({ field, fieldState }) => (
<TextInput
{...field}
label={t("pages.settings.emailSettings.labelHost")}
placeholder="smtp.gmail.com"
error={!!fieldState.error}
helperText={fieldState.error?.message}
/>
)}
/>
</Box>
<Box>
<Controller
name="systemEmailPort"
control={control}
defaultValue={defaults.systemEmailPort}
render={({ field, fieldState }) => (
<TextInput
{...field}
label={t("pages.settings.emailSettings.labelPort")}
placeholder="425"
type="number"
error={!!fieldState.error}
helperText={fieldState.error?.message}
/>
)}
/>
</Box>
<Box>
<Controller
name="systemEmailUser"
control={control}
defaultValue={defaults.systemEmailUser}
render={({ field, fieldState }) => (
<TextInput
{...field}
label={t("pages.settings.emailSettings.labelUser")}
placeholder={t("pages.settings.emailSettings.placeholderUser")}
error={!!fieldState.error}
helperText={fieldState.error?.message}
/>
)}
/>
</Box>
<Box>
<Controller
name="systemEmailAddress"
control={control}
defaultValue={defaults.systemEmailAddress}
render={({ field, fieldState }) => (
<TextInput
{...field}
label={t("pages.settings.emailSettings.labelAddress")}
placeholder="uptime@bluewavelabs.ca"
error={!!fieldState.error}
helperText={fieldState.error?.message}
/>
)}
/>
</Box>
{(isEmailPasswordSet === false || emailPasswordHasBeenReset === true) && (
<Box>
<TextInput
label={t("settingsPage.emailSettings.labelTLSServername")}
name="systemEmailTLSServername"
placeholder="bluewavelabs.ca"
value={systemEmailTLSServername}
onChange={handleChange}
<Controller
name="systemEmailPassword"
control={control}
defaultValue={defaults.systemEmailPassword}
render={({ field, fieldState }) => (
<TextInput
{...field}
label={t("pages.settings.emailSettings.labelPassword")}
type="password"
placeholder="123 456 789 101112"
endAdornment={<PasswordEndAdornment />}
error={!!fieldState.error}
helperText={fieldState.error?.message}
/>
)}
/>
</Box>
)}
{isEmailPasswordSet === true && emailPasswordHasBeenReset === false && (
<Box>
<TextInput
label={t("settingsPage.emailSettings.labelConnectionHost")}
name="systemEmailConnectionHost"
placeholder="bluewavelabs.ca"
value={systemEmailConnectionHost}
onChange={handleChange}
/>
<Typography>{t("pages.settings.emailSettings.labelPasswordSet")}</Typography>
<Button
onClick={() => {
setEmailPasswordHasBeenReset(true);
}}
variant="contained"
color="error"
sx={{ mt: theme.spacing(4) }}
>
{t("reset")}
</Button>
</Box>
)}
<Box>
<Controller
name="systemEmailTLSServername"
control={control}
defaultValue={defaults.systemEmailTLSServername}
render={({ field, fieldState }) => (
<TextInput
{...field}
label={t("pages.settings.emailSettings.labelTLSServername")}
placeholder="bluewavelabs.ca"
/>
)}
/>
</Box>
<Box>
<Controller
name="systemEmailConnectionHost"
control={control}
defaultValue={defaults.systemEmailConnectionHost}
render={({ field, fieldState }) => (
<TextInput
{...field}
label={t("pages.settings.emailSettings.labelConnectionHost")}
placeholder="bluewavelabs.ca"
error={!!fieldState.error}
helperText={fieldState.error?.message}
/>
)}
/>
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: theme.spacing(4),
}}
>
{[
[
"pages.settings.emailSettings.labelSecure",
"systemEmailSecure",
systemEmailSecure,
],
[
"pages.settings.emailSettings.labelPool",
"systemEmailPool",
systemEmailPool,
],
[
"pages.settings.emailSettings.labelIgnoreTLS",
"systemEmailIgnoreTLS",
systemEmailIgnoreTLS,
],
[
"pages.settings.emailSettings.labelRequireTLS",
"systemEmailRequireTLS",
systemEmailRequireTLS,
],
[
"pages.settings.emailSettings.labelRejectUnauthorized",
"systemEmailRejectUnauthorized",
systemEmailRejectUnauthorized,
],
].map(([labelKey, name, value]) => (
<Controller
key={name}
name={name}
control={control}
defaultValue={defaults[name]}
render={({ field }) => (
<Box
key={name}
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<Typography>{t(labelKey)}</Typography>
<Switch
{...field}
checked={field.value}
/>
</Box>
)}
/>
))}
<TextLink
text={t("pages.settings.emailSettings.descriptionTransport")}
linkText={t("pages.settings.emailSettings.linkTransport")}
href="https://nodemailer.com/smtp"
target="_blank"
/>
<Box
component={"pre"}
sx={{
display: "flex",
flexDirection: "column",
gap: theme.spacing(4),
fontFamily: "monospace",
p: 2,
borderRadius: 1,
overflow: "auto",
}}
>
{[
[
"settingsPage.emailSettings.labelSecure",
"systemEmailSecure",
systemEmailSecure,
],
[
"settingsPage.emailSettings.labelPool",
"systemEmailPool",
systemEmailPool,
],
[
"settingsPage.emailSettings.labelIgnoreTLS",
"systemEmailIgnoreTLS",
systemEmailIgnoreTLS,
],
[
"settingsPage.emailSettings.labelRequireTLS",
"systemEmailRequireTLS",
systemEmailRequireTLS,
],
[
"settingsPage.emailSettings.labelRejectUnauthorized",
"systemEmailRejectUnauthorized",
systemEmailRejectUnauthorized,
],
].map(([labelKey, name, value]) => (
<Box
key={name}
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<Typography>{t(labelKey)}</Typography>
<Switch
name={name}
checked={value}
onChange={handleChange}
/>
</Box>
))}
<TextLink
text={t("settingsPage.emailSettings.descriptionTransport")}
linkText={t("settingsPage.emailSettings.linkTransport")}
href="https://nodemailer.com/smtp"
target="_blank"
/>
<Box
component={"pre"}
sx={{
fontFamily: "monospace",
p: 2,
borderRadius: 1,
overflow: "auto",
}}
>
<code>
{JSON.stringify(
{
host: systemEmailHost,
port: systemEmailPort,
secure: systemEmailSecure,
auth: {
user: systemEmailUser || systemEmailAddress,
pass: "<your_password>",
},
name: systemEmailConnectionHost || "localhost",
pool: systemEmailPool,
tls: {
rejectUnauthorized: systemEmailRejectUnauthorized,
ignoreTLS: systemEmailIgnoreTLS,
requireTLS: systemEmailRequireTLS,
servername: systemEmailTLSServername,
},
<code>
{JSON.stringify(
{
host: systemEmailHost,
port: systemEmailPort,
secure: systemEmailSecure,
auth: {
user: systemEmailUser || systemEmailAddress,
pass: "<your_password>",
},
null,
2
)}
</code>
</Box>
</Box>
<pre>
{JSON.stringify({
systemEmailHost,
systemEmailAddress,
systemEmailPassword,
})}
</pre>
<Box>
{systemEmailHost &&
systemEmailPort &&
systemEmailAddress &&
systemEmailPassword && (
<Button
variant="contained"
color="accent"
loading={isSending}
onClick={handleSendTestEmail}
>
{t("settingsPage.emailSettings.buttonSendTestEmail")}
</Button>
name: systemEmailConnectionHost || "localhost",
pool: systemEmailPool,
tls: {
rejectUnauthorized: systemEmailRejectUnauthorized,
ignoreTLS: systemEmailIgnoreTLS,
requireTLS: systemEmailRequireTLS,
servername: systemEmailTLSServername,
},
},
null,
2
)}
</code>
</Box>
</Stack>
</Box>
</Box>
<Box>
{systemEmailHost &&
systemEmailPort &&
systemEmailAddress &&
systemEmailPassword && (
<Button
variant="contained"
color="accent"
loading={isSending}
onClick={handleSendTestEmail}
>
{t("pages.settings.emailSettings.buttonSendTestEmail")}
</Button>
)}
</Box>
</Stack>
</ConfigBox>
);
};
SettingsEmail.propTypes = {
isAdmin: PropTypes.bool,
settingsData: PropTypes.object,
setSettingsData: PropTypes.func,
handleChange: PropTypes.func,
HEADER_SX: PropTypes.object,
isPasswordSet: PropTypes.bool,
};
+26 -10
View File
@@ -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 { useLazyGet } 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,6 +19,29 @@ const SettingsDemoMonitors = ({ isAdmin, HEADER_SX, handleChange, isLoading }) =
if (!isAdmin) {
return null;
}
const { get: fetchJson } = useLazyGet();
const handleExport = async () => {
const res = await fetchJson("/monitors/export/json");
const json = res?.data ?? [];
if (!json || json.length === 0) {
return;
}
const blob = new Blob([JSON.stringify(json, null, 2)], {
type: "application/json",
});
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "monitors.json";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
return;
};
return (
<>
@@ -38,14 +62,7 @@ const SettingsDemoMonitors = ({ isAdmin, HEADER_SX, handleChange, isLoading }) =
variant="contained"
color="accent"
loading={isLoading}
onClick={() => {
const syntheticEvent = {
target: {
name: "export",
},
};
handleChange(syntheticEvent);
}}
onClick={handleExport}
sx={{ mt: theme.spacing(4) }}
>
Export Monitors to JSON
@@ -58,7 +75,6 @@ const SettingsDemoMonitors = ({ isAdmin, HEADER_SX, handleChange, isLoading }) =
SettingsDemoMonitors.propTypes = {
isAdmin: PropTypes.bool,
handleChange: PropTypes.func,
HEADER_SX: PropTypes.object,
};
@@ -7,39 +7,18 @@ import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx";
import { useTheme } from "@emotion/react";
import { PropTypes } from "prop-types";
import { useTranslation } from "react-i18next";
import { Controller } from "react-hook-form";
const SettingsGlobalThresholds = ({
isAdmin,
HEADING_SX,
settingsData,
setSettingsData,
control,
defaults,
}) => {
const { t } = useTranslation(); // For language translation
const theme = useTheme(); // MUI theme access
// Handles input change and updates parent state
const handleChange = (e, min, max) => {
const { name, value } = e.target;
const numValue = parseFloat(value);
const isValidNumber =
value === "" ||
(!isNaN(numValue) && isFinite(numValue) && numValue >= min && numValue <= max);
if (isValidNumber) {
setSettingsData((prev) => ({
...prev,
settings: {
...prev.settings,
globalThresholds: {
...prev.settings?.globalThresholds,
[name]: value,
},
},
}));
}
};
// Only render this section for admins
if (!isAdmin) return null;
@@ -51,11 +30,11 @@ const SettingsGlobalThresholds = ({
component="h1"
variant="h2"
>
{t("settingsPage.globalThresholds.title", "Global Thresholds")}
{t("pages.settings.globalThresholds.title", "Global Thresholds")}
</Typography>
<Typography sx={HEADING_SX}>
{t(
"settingsPage.globalThresholds.description",
"pages.settings.globalThresholds.description",
"Configure global CPU, Memory, Disk, and Temperature thresholds."
)}
</Typography>
@@ -69,14 +48,21 @@ const SettingsGlobalThresholds = ({
["Disk Threshold (%)", "disk", 1, 100],
["Temperature Threshold (°C)", "temperature", 1, 150],
].map(([label, name, min, max]) => (
<TextInput
<Controller
key={name}
name={name}
label={label}
placeholder={`${min} - ${max}`}
type="number"
value={settingsData?.settings?.globalThresholds?.[name] || ""}
onChange={(e) => handleChange(e, min, max)}
name={`globalThresholds.${name}`}
control={control}
defaultValue={defaults.globalThresholds?.[name]}
render={({ field, fieldState }) => (
<TextInput
{...field}
label={label}
placeholder={`${min} - ${max}`}
type="number"
error={!!fieldState.error}
helperText={fieldState.error?.message}
/>
)}
/>
))}
</Stack>
@@ -88,8 +74,6 @@ const SettingsGlobalThresholds = ({
SettingsGlobalThresholds.propTypes = {
isAdmin: PropTypes.bool,
HEADING_SX: PropTypes.object,
settingsData: PropTypes.object,
setSettingsData: PropTypes.func,
};
export default SettingsGlobalThresholds;
+24 -28
View File
@@ -10,15 +10,17 @@ import { useTheme } from "@emotion/react";
import { PropTypes } from "prop-types";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Controller } from "react-hook-form";
const SettingsPagespeed = ({
isAdmin,
HEADING_SX,
settingsData,
setSettingsData,
isApiKeySet,
apiKeyHasBeenReset,
setApiKeyHasBeenReset,
defaults,
control,
setValue,
}) => {
const { t } = useTranslation();
const theme = useTheme();
@@ -26,15 +28,6 @@ const SettingsPagespeed = ({
// Local state
const [apiKey, setApiKey] = useState("");
// Handler
const handleChange = (e) => {
setApiKey(e.target.value);
setSettingsData({
...settingsData,
settings: { ...settingsData.settings, pagespeedApiKey: e.target.value },
});
};
if (!isAdmin) {
return null;
}
@@ -46,35 +39,40 @@ const SettingsPagespeed = ({
component="h1"
variant="h2"
>
{t("settingsPage.pageSpeedSettings.title")}
{t("pages.settings.pageSpeedSettings.title")}
</Typography>
<Typography sx={HEADING_SX}>
{t("settingsPage.pageSpeedSettings.description")}
{t("pages.settings.pageSpeedSettings.description")}
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
{(isApiKeySet === false || apiKeyHasBeenReset === true) && (
<TextInput
<Controller
name="pagespeedApiKey"
label={t("settingsPage.pageSpeedSettings.labelApiKey")}
value={apiKey}
type={"password"}
onChange={handleChange}
optionalLabel="(Optional)"
endAdornment={<PasswordEndAdornment />}
control={control}
defaultValue={defaults.pagespeedApiKey}
render={({ field, fieldState }) => (
<TextInput
{...field}
label={t("pages.settings.pageSpeedSettings.labelApiKey")}
type={"password"}
optionalLabel="(Optional)"
endAdornment={<PasswordEndAdornment />}
error={!!fieldState.error}
helperText={fieldState.error?.message}
/>
)}
/>
)}
{isApiKeySet === true && apiKeyHasBeenReset === false && (
<Box>
<Typography>{t("settingsPage.pageSpeedSettings.labelApiKeySet")}</Typography>
<Typography>
{t("pages.settings.pageSpeedSettings.labelApiKeySet")}
</Typography>
<Button
onClick={() => {
setApiKey("");
setSettingsData({
...settingsData,
settings: { ...settingsData.settings, pagespeedApiKey: "" },
});
setValue("pagespeedApiKey", "");
setApiKeyHasBeenReset(true);
}}
variant="contained"
@@ -93,8 +91,6 @@ const SettingsPagespeed = ({
SettingsPagespeed.propTypes = {
isAdmin: PropTypes.bool,
HEADING_SX: PropTypes.object,
settingsData: PropTypes.object,
setSettingsData: PropTypes.func,
isApiKeySet: PropTypes.bool,
setIsApiKeySet: PropTypes.func,
apiKeyHasBeenReset: PropTypes.bool,
+13 -17
View File
@@ -11,8 +11,9 @@ import { useTheme } from "@emotion/react";
import { PropTypes } from "prop-types";
import { useTranslation } from "react-i18next";
import { useState } from "react";
import { useDelete } from "@/Hooks/UseApi";
const SettingsStats = ({ isAdmin, HEADING_SX, handleChange, settingsData, errors }) => {
const SettingsStats = ({ isAdmin, HEADING_SX, errors }) => {
const theme = useTheme();
const { t } = useTranslation();
const [isOpen, setIsOpen] = useState(false);
@@ -20,6 +21,7 @@ const SettingsStats = ({ isAdmin, HEADING_SX, handleChange, settingsData, errors
if (!isAdmin) {
return null;
}
const { deleteFn: deleteMonitorStatsFn } = useDelete();
return (
<ConfigBox>
@@ -29,16 +31,16 @@ const SettingsStats = ({ isAdmin, HEADING_SX, handleChange, settingsData, errors
variant="h2"
sx={HEADING_SX}
>
{t("settingsPage.statsSettings.title")}
{t("pages.settings.statsSettings.title")}
</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
{t("settingsPage.statsSettings.description")}
{t("pages.settings.statsSettings.description")}
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<Box>
<Typography>
{t("settingsPage.statsSettings.clearAllStatsDescription")}
{t("pages.settings.statsSettings.clearAllStatsDescription")}
</Typography>
<Button
variant="contained"
@@ -46,26 +48,22 @@ const SettingsStats = ({ isAdmin, HEADING_SX, handleChange, settingsData, errors
onClick={() => setIsOpen(true)}
sx={{ mt: theme.spacing(4) }}
>
{t("settingsPage.statsSettings.clearAllStatsButton")}
{t("pages.settings.statsSettings.clearAllStatsButton")}
</Button>
</Box>
</Stack>
<Dialog
open={isOpen}
theme={theme}
title={t("settingsPage.statsSettings.clearAllStatsDialogTitle")}
description={t("settingsPage.statsSettings.clearAllStatsDialogDescription")}
title={t("pages.settings.statsSettings.clearAllStatsDialogTitle")}
description={t("pages.settings.statsSettings.clearAllStatsDialogDescription")}
onCancel={() => setIsOpen(false)}
confirmationButtonLabel={t(
"settingsPage.statsSettings.clearAllStatsDialogConfirm"
"pages.settings.statsSettings.clearAllStatsDialogConfirm"
)}
onConfirm={() => {
const syntheticEvent = {
target: {
name: "deleteStats",
},
};
handleChange(syntheticEvent);
onConfirm={async () => {
await deleteMonitorStatsFn("/checks/team");
setIsOpen(false);
}}
isLoading={false}
@@ -77,8 +75,6 @@ const SettingsStats = ({ isAdmin, HEADING_SX, handleChange, settingsData, errors
SettingsStats.propTypes = {
isAdmin: PropTypes.bool,
HEADING_SX: PropTypes.object,
handleChange: PropTypes.func,
settingsData: PropTypes.object,
errors: PropTypes.object,
};
+12 -17
View File
@@ -7,31 +7,28 @@ import timezones from "@/Utils/timezones.json";
// Utils
import { useTheme } from "@emotion/react";
import { useSelector, useDispatch } from "react-redux";
import { setTimezone } from "@/Features/UI/uiSlice.js";
import { PropTypes } from "prop-types";
import { useTranslation } from "react-i18next";
import { useCallback, useMemo, useState } from "react";
const SettingsTimeZone = ({ HEADING_SX, handleChange, timezone }) => {
const SettingsTimeZone = ({ HEADING_SX }) => {
const theme = useTheme();
const { t } = useTranslation();
const [rawInput, setRawInput] = useState("");
const dispatch = useDispatch();
const { timezone } = useSelector((state) => state.ui);
const selectedTimezone = useMemo(
() => timezones.find((tz) => tz._id === timezone) ?? null,
[timezone]
);
const handleTimezoneChange = useCallback(
(newValue) => {
setRawInput("");
handleChange({
target: {
name: "timezone",
value: newValue?._id ?? "",
},
});
},
[handleChange]
);
const handleTimezoneChange = (newValue) => {
setRawInput("");
const newId = newValue?._id ?? "";
dispatch(setTimezone({ timezone: newId }));
};
return (
<ConfigBox>
@@ -40,11 +37,11 @@ const SettingsTimeZone = ({ HEADING_SX, handleChange, timezone }) => {
component="h1"
variant="h2"
>
{t("settingsPage.timezoneSettings.title")}
{t("pages.settings.timezoneSettings.title")}
</Typography>
<Typography sx={HEADING_SX}>
<Typography component="span">
{t("settingsPage.timezoneSettings.description")}
{t("pages.settings.timezoneSettings.description")}
</Typography>
</Typography>
</Box>
@@ -68,8 +65,6 @@ const SettingsTimeZone = ({ HEADING_SX, handleChange, timezone }) => {
SettingsTimeZone.propTypes = {
HEADING_SX: PropTypes.object,
handleChange: PropTypes.func,
timezone: PropTypes.string,
};
export default SettingsTimeZone;
+19 -15
View File
@@ -11,11 +11,19 @@ import { lightTheme, darkTheme } from "@/Utils/Theme/v2Theme";
import { useTheme } from "@emotion/react";
import { PropTypes } from "prop-types";
import { useTranslation } from "react-i18next";
import { useSelector, useDispatch } from "react-redux";
import { setMode, setLanguage, setChartType } from "@/Features/UI/uiSlice.js";
const SettingsUI = ({ HEADING_SX, handleChange, mode, language, chartType }) => {
const SettingsUI = ({ HEADING_SX }) => {
const {
mode,
language = "en",
chartType = "histogram",
} = useSelector((state) => state.ui);
const { t, i18n } = useTranslation();
const theme = useTheme();
const languages = Object.keys(i18n.options.resources || {});
const dispatch = useDispatch();
const v2Theme = mode === "dark" ? darkTheme : lightTheme;
return (
<ConfigBox>
@@ -24,18 +32,18 @@ const SettingsUI = ({ HEADING_SX, handleChange, mode, language, chartType }) =>
component="h1"
variant="h2"
>
{t("settingsPage.uiSettings.title")}
{t("pages.settings.uiSettings.title")}
</Typography>
<Typography sx={HEADING_SX}>
{t("settingsPage.uiSettings.description")}
{t("pages.settings.uiSettings.description")}
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<Select
name="mode"
label={t("settingsPage.uiSettings.labelTheme")}
label={t("pages.settings.uiSettings.labelTheme")}
value={mode}
onChange={handleChange}
onChange={(e) => dispatch(setMode(e.target.value))}
items={[
{ _id: "light", name: "Light" },
{ _id: "dark", name: "Dark" },
@@ -43,19 +51,19 @@ const SettingsUI = ({ HEADING_SX, handleChange, mode, language, chartType }) =>
></Select>
<Select
name="language"
label={t("settingsPage.uiSettings.labelLanguage")}
label={t("pages.settings.uiSettings.labelLanguage")}
value={language}
onChange={handleChange}
onChange={(e) => dispatch(setLanguage(e.target.value))}
items={languages.map((lang) => ({ _id: lang, name: lang.toUpperCase() }))}
></Select>
<Select
name="chartType"
label={t("settingsPage.uiSettings.labelChartType")}
label={t("pages.settings.uiSettings.labelChartType")}
value={chartType}
onChange={handleChange}
onChange={(e) => dispatch(setChartType(e.target.value))}
items={[
{ _id: "histogram", name: t("settingsPage.uiSettings.chartTypeHistogram") },
{ _id: "heatmap", name: t("settingsPage.uiSettings.chartTypeHeatmap") },
{ _id: "histogram", name: t("pages.settings.uiSettings.chartTypeHistogram") },
{ _id: "heatmap", name: t("pages.settings.uiSettings.chartTypeHeatmap") },
]}
></Select>
<ThemeProvider theme={v2Theme}>
@@ -68,10 +76,6 @@ const SettingsUI = ({ HEADING_SX, handleChange, mode, language, chartType }) =>
SettingsUI.propTypes = {
HEADING_SX: PropTypes.object,
handleChange: PropTypes.func,
mode: PropTypes.string,
language: PropTypes.string,
chartType: PropTypes.string,
};
export default SettingsUI;
+20 -14
View File
@@ -3,13 +3,14 @@ import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import ConfigBox from "@/Components/v1/ConfigBox/index.jsx";
import Select from "@/Components/v1/Inputs/Select/index.jsx";
import { Controller } from "react-hook-form";
// Utils
import { useTheme } from "@emotion/react";
import { PropTypes } from "prop-types";
import { useTranslation } from "react-i18next";
const SettingsURL = ({ HEADING_SX, handleChange, showURL = false }) => {
const SettingsURL = ({ HEADING_SX, control, defaults }) => {
const { t } = useTranslation();
const theme = useTheme();
return (
@@ -19,23 +20,30 @@ const SettingsURL = ({ HEADING_SX, handleChange, showURL = false }) => {
component="h1"
variant="h2"
>
{t("settingsPage.urlSettings.title")}
{t("pages.settings.urlSettings.title")}
</Typography>
<Typography sx={HEADING_SX}>
{t("settingsPage.urlSettings.description")}
{t("pages.settings.urlSettings.description")}
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<Select
<Controller
name="showURL"
label={t("settingsPage.urlSettings.label")}
value={showURL === true}
onChange={handleChange}
items={[
{ _id: true, name: t("settingsPage.urlSettings.selectEnabled") },
{ _id: false, name: t("settingsPage.urlSettings.selectDisabled") },
]}
></Select>
control={control}
defaultValue={defaults.showURL}
render={({ field, fieldState }) => (
<Select
{...field}
error={!!fieldState.error}
helperText={fieldState.error?.message}
label={t("pages.settings.urlSettings.label")}
items={[
{ _id: true, name: t("pages.settings.urlSettings.selectEnabled") },
{ _id: false, name: t("pages.settings.urlSettings.selectDisabled") },
]}
/>
)}
/>
</Stack>
</ConfigBox>
);
@@ -43,8 +51,6 @@ const SettingsURL = ({ HEADING_SX, handleChange, showURL = false }) => {
SettingsURL.propTypes = {
HEADING_SX: PropTypes.object,
handleChange: PropTypes.func,
showURL: PropTypes.bool,
};
export default SettingsURL;
+59 -182
View File
@@ -13,17 +13,14 @@ import SettingsExport from "./SettingsExport.jsx";
import Button from "@mui/material/Button";
// Utils
import { settingsValidation } from "@/Validation/validation.js";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useSettingsForm } from "@/Hooks/useSettingsForm.js";
import { useState, useEffect } from "react";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import { useSelector, useDispatch } from "react-redux";
import {
setTimezone,
setMode,
setLanguage,
setShowURL,
setChartType,
} from "@/Features/UI/uiSlice.js";
import SettingsStats from "./SettingsStats.jsx";
import { useIsAdmin } from "@/Hooks/useIsAdmin.js";
@@ -32,31 +29,42 @@ import { useGet, usePost, useDelete, useLazyGet, usePatch } from "@/Hooks/UseApi
const BREADCRUMBS = [{ name: `Settings`, path: "/settings" }];
const Settings = () => {
// Redux state
const {
mode,
language = "en",
timezone,
showURL,
chartType = "histogram",
} = useSelector((state) => state.ui);
const { data: fetchedSettings, isLoading: isSettingsLoading } = useGet("/settings");
// Local state
const [settingsData, setSettingsData] = useState({});
const [errors, setErrors] = useState({});
const [isApiKeySet, setIsApiKeySet] = useState(settingsData?.pagespeedKeySet ?? false);
const [isApiKeySet, setIsApiKeySet] = useState(
fetchedSettings?.pagespeedKeySet ?? false
);
const [apiKeyHasBeenReset, setApiKeyHasBeenReset] = useState(false);
const [isEmailPasswordSet, setIsEmailPasswordSet] = useState(
settingsData?.emailPasswordSet ?? false
fetchedSettings?.emailPasswordSet ?? false
);
const [emailPasswordHasBeenReset, setEmailPasswordHasBeenReset] = useState(false);
// Network
const { data: fetchedSettings, isLoading: isSettingsLoading } = useGet("/settings");
const { schema, defaults } = useSettingsForm({ data: fetchedSettings?.settings });
const form = useForm({
resolver: zodResolver(schema),
defaultValues: defaults,
});
const {
control,
watch,
reset,
handleSubmit,
clearErrors,
trigger,
getValues,
setValue,
formState: { dirtyFields },
} = form;
useEffect(() => {
reset(defaults);
}, [defaults, reset]);
useEffect(() => {
if (fetchedSettings) {
setSettingsData(fetchedSettings);
setIsApiKeySet(fetchedSettings?.pagespeedKeySet);
setIsEmailPasswordSet(fetchedSettings?.emailPasswordSet);
}
@@ -76,14 +84,24 @@ const Settings = () => {
if (data.emailPasswordSet === true) {
setEmailPasswordHasBeenReset(false);
}
setSettingsData(data);
}
};
const onSubmit = async (data) => {
const toSubmit = { ...data };
if (!form.formState.dirtyFields.systemEmailPassword) {
delete toSubmit.systemEmailPassword;
}
if (!form.formState.dirtyFields.pagespeedApiKey) {
delete toSubmit.pagespeedApiKey;
}
saveSettings(toSubmit);
};
// New API hooks to replace monitorHooks
const { post: postDemoMonitors, loading: isAddingDemoMonitors } = usePost();
const { deleteFn: deleteAllMonitorsFn, loading: isDeletingMonitors } = useDelete();
const { deleteFn: deleteMonitorStatsFn, loading: isDeletingMonitorStats } = useDelete();
const { loading: isAddingDemoMonitors } = usePost();
const { loading: isDeletingMonitors } = useDelete();
const { loading: isDeletingMonitorStats } = useDelete();
const { get: fetchJson, loading: isFetchingJson } = useLazyGet();
// Setup
@@ -92,184 +110,43 @@ const Settings = () => {
const HEADING_SX = { mt: theme.spacing(2), mb: theme.spacing(2) };
const { t } = useTranslation();
const dispatch = useDispatch();
// Handlers
const handleChange = async (e) => {
const { name, value, checked } = e.target;
let newValue;
if (
name === "systemEmailIgnoreTLS" ||
name === "systemEmailRequireTLS" ||
name === "systemEmailRejectUnauthorized" ||
name === "systemEmailSecure" ||
name === "systemEmailPool"
) {
newValue = checked;
}
// Ensure showURL is a proper boolean
if (name === "showURL") {
newValue = value === true || value === "true";
}
// Build next state early
const newSettingsData = {
...settingsData,
settings: { ...settingsData.settings, [name]: newValue ?? value },
};
if (name === "timezone") {
dispatch(setTimezone({ timezone: value }));
return;
}
if (name === "mode") {
dispatch(setMode(value));
return;
}
if (name === "language") {
dispatch(setLanguage(value));
return;
}
if (name === "chartType") {
dispatch(setChartType(value));
return;
}
if (name === "deleteStats") {
await deleteMonitorStatsFn("/checks/team");
return;
}
if (name === "demo") {
await postDemoMonitors("/monitors/demo", {});
return;
}
if (name === "deleteMonitors") {
await deleteAllMonitorsFn("/monitors/");
return;
}
if (name === "export") {
const res = await fetchJson("/monitors/export/json");
const json = res?.data ?? [];
if (!json || json.length === 0) {
return;
}
const blob = new Blob([JSON.stringify(json, null, 2)], {
type: "application/json",
});
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "monitors.json";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
return;
}
setSettingsData(newSettingsData);
// Update Redux immediately for UI feedback
if (name === "showURL") {
dispatch(setShowURL(newValue));
}
};
const handleSave = () => {
// Validate]
const toSubmit = {
checkTTL: settingsData.settings.checkTTL,
pagespeedApiKey: settingsData.settings.pagespeedApiKey,
language: settingsData.settings.language,
timezone: settingsData.settings.timezone,
systemEmailHost: settingsData.settings.systemEmailHost,
systemEmailPort: settingsData.settings.systemEmailPort,
systemEmailSecure: settingsData.settings.systemEmailSecure,
systemEmailPool: settingsData.settings.systemEmailPool,
systemEmailAddress: settingsData.settings.systemEmailAddress,
systemEmailPassword: settingsData.settings.systemEmailPassword,
systemEmailUser: settingsData.settings.systemEmailUser,
systemEmailConnectionHost: settingsData.settings.systemEmailConnectionHost,
systemEmailTLSServername: settingsData.settings.systemEmailTLSServername,
systemEmailIgnoreTLS: settingsData.settings.systemEmailIgnoreTLS,
systemEmailRequireTLS: settingsData.settings.systemEmailRequireTLS,
systemEmailRejectUnauthorized: settingsData.settings.systemEmailRejectUnauthorized,
showURL: settingsData.settings.showURL,
globalThresholds: settingsData.settings.globalThresholds,
};
const { error } = settingsValidation.validate(toSubmit, {
abortEarly: false,
});
if (!error || error.details.length === 0) {
setErrors({});
saveSettings(toSubmit);
} else {
const newErrors = {};
error.details.forEach((err) => {
newErrors[err.path[0]] = err.message;
});
setErrors(newErrors);
}
};
return (
<Stack gap={theme.spacing(10)}>
<Breadcrumbs list={BREADCRUMBS} />
<Typography variant="h1">{t("settingsPage.title")}</Typography>
<SettingsTimeZone
HEADING_SX={HEADING_SX}
handleChange={handleChange}
timezone={timezone}
/>
<SettingsUI
HEADING_SX={HEADING_SX}
handleChange={handleChange}
mode={mode}
language={language}
chartType={chartType}
/>
<Typography variant="h1">{t("pages.settings.title")}</Typography>
<SettingsTimeZone HEADING_SX={HEADING_SX} />
<SettingsUI HEADING_SX={HEADING_SX} />
<SettingsPagespeed
isAdmin={isAdmin}
HEADING_SX={HEADING_SX}
settingsData={settingsData}
setSettingsData={setSettingsData}
isApiKeySet={isApiKeySet}
apiKeyHasBeenReset={apiKeyHasBeenReset}
setApiKeyHasBeenReset={setApiKeyHasBeenReset}
control={control}
defaults={defaults}
setValue={setValue}
/>
<SettingsURL
HEADING_SX={HEADING_SX}
handleChange={handleChange}
showURL={showURL}
control={control}
defaults={defaults}
/>
<SettingsStats
isAdmin={isAdmin}
HEADING_SX={HEADING_SX}
settingsData={settingsData}
handleChange={handleChange}
errors={errors}
/>
<SettingsGlobalThresholds
isAdmin={isAdmin}
HEADING_SX={HEADING_SX}
settingsData={settingsData}
setSettingsData={setSettingsData}
control={control}
defaults={defaults}
/>
<SettingsDemoMonitors
isAdmin={isAdmin}
HEADER_SX={HEADING_SX}
handleChange={handleChange}
isLoading={
isSettingsLoading || isSaving || isDeletingMonitorStats || isAddingDemoMonitors
}
@@ -277,18 +154,18 @@ const Settings = () => {
<SettingsEmail
isAdmin={isAdmin}
HEADER_SX={HEADING_SX}
handleChange={handleChange}
settingsData={settingsData}
setSettingsData={setSettingsData}
isEmailPasswordSet={isEmailPasswordSet}
emailPasswordHasBeenReset={emailPasswordHasBeenReset}
setEmailPasswordHasBeenReset={setEmailPasswordHasBeenReset}
control={control}
defaults={defaults}
formValues={watch()}
setValue={setValue}
/>
<SettingsExport
isAdmin={isAdmin}
HEADER_SX={HEADING_SX}
handleChange={handleChange}
isLoading={isSettingsLoading || isSaving || isFetchingJson}
/>
<SettingsAbout />
@@ -322,9 +199,9 @@ const Settings = () => {
variant="contained"
color="accent"
sx={{ px: theme.spacing(12), py: theme.spacing(8) }}
onClick={handleSave}
onClick={handleSubmit(onSubmit)}
>
{t("settingsPage.saveButtonLabel")}
{t("pages.settings.saveButtonLabel")}
</Button>
</Stack>
</Stack>
+58
View File
@@ -0,0 +1,58 @@
import { z } from "zod";
export const settingsSchema = z.object({
systemEmailIgnoreTLS: z.boolean(),
systemEmailRequireTLS: z.boolean(),
systemEmailRejectUnauthorized: z.boolean(),
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(),
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(),
systemEmailPassword: z.string().optional(),
systemEmailTLSServername: z.string().optional(),
globalThresholds: z
.object({
cpu: z.coerce
.number()
.min(1, "Min 1%")
.max(100, "Max 100%")
.optional()
.or(z.literal("")),
memory: z.coerce
.number()
.min(1, "Min 1%")
.max(100, "Max 100%")
.optional()
.or(z.literal("")),
disk: z.coerce
.number()
.min(1, "Min 1%")
.max(100, "Max 100%")
.optional()
.or(z.literal("")),
temperature: z.coerce
.number()
.min(1, "Min 1°C")
.max(150, "Max 150°C")
.optional()
.or(z.literal("")),
})
.optional(),
});
export type SettingsFormData = z.infer<typeof settingsSchema>;
+87
View File
@@ -907,6 +907,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": {