diff --git a/client/src/Components/Inputs/Select/index.jsx b/client/src/Components/Inputs/Select/index.jsx
index 2aa979434..927247d77 100644
--- a/client/src/Components/Inputs/Select/index.jsx
+++ b/client/src/Components/Inputs/Select/index.jsx
@@ -156,7 +156,7 @@ const Select = ({
};
Select.propTypes = {
- id: PropTypes.string.isRequired,
+ id: PropTypes.string,
name: PropTypes.string,
label: PropTypes.string,
placeholder: PropTypes.string,
diff --git a/client/src/Components/Inputs/TextInput/index.jsx b/client/src/Components/Inputs/TextInput/index.jsx
index 58e0e17eb..bb2df5d21 100644
--- a/client/src/Components/Inputs/TextInput/index.jsx
+++ b/client/src/Components/Inputs/TextInput/index.jsx
@@ -141,9 +141,9 @@ TextInput.displayName = "TextInput";
TextInput.propTypes = {
type: PropTypes.string,
- id: PropTypes.string.isRequired,
+ id: PropTypes.string,
name: PropTypes.string,
- value: PropTypes.string,
+ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
placeholder: PropTypes.string,
isRequired: PropTypes.bool,
isOptional: PropTypes.bool,
diff --git a/client/src/Hooks/useDeleteMonitorStats.js b/client/src/Hooks/useDeleteMonitorStats.js
new file mode 100644
index 000000000..64138929f
--- /dev/null
+++ b/client/src/Hooks/useDeleteMonitorStats.js
@@ -0,0 +1,24 @@
+import { useState } from "react";
+import { networkService } from "../main";
+import { createToast } from "../Utils/toastUtils";
+import { useTranslation } from "react-i18next";
+
+const UseDeleteMonitorStats = () => {
+ const { t } = useTranslation();
+ const [isLoading, setIsLoading] = useState(false);
+ const deleteMonitorStats = async ({ teamId }) => {
+ setIsLoading(true);
+ try {
+ const res = await networkService.deleteChecksByTeamId({ teamId });
+ createToast({ body: t("settingsStatsCleared") });
+ } catch (error) {
+ createToast({ body: t("settingsFailedToClearStats") });
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return [deleteMonitorStats, isLoading];
+};
+
+export { UseDeleteMonitorStats };
diff --git a/client/src/Hooks/useFetchSettings.js b/client/src/Hooks/useFetchSettings.js
new file mode 100644
index 000000000..f7651e789
--- /dev/null
+++ b/client/src/Hooks/useFetchSettings.js
@@ -0,0 +1,55 @@
+import { useState, useEffect } from "react";
+import { networkService } from "../main";
+import { createToast } from "../Utils/toastUtils";
+import { useTranslation } from "react-i18next";
+
+const useFetchSettings = ({ setSettingsData }) => {
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(undefined);
+ useEffect(() => {
+ const fetchSettings = async () => {
+ setIsLoading(true);
+ try {
+ const response = await networkService.getAppSettings();
+ setSettingsData(response?.data?.data);
+ } catch (error) {
+ createToast({ body: "Failed to fetch settings" });
+ setError(error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+ fetchSettings();
+ }, []);
+
+ return [isLoading, error];
+};
+
+const useSaveSettings = () => {
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(undefined);
+ const { t } = useTranslation();
+
+ const saveSettings = async (settings) => {
+ setIsLoading(true);
+ try {
+ await networkService.updateAppSettings({ settings });
+ if (settings.checkTTL) {
+ await networkService.updateChecksTTL({
+ ttl: settings.checkTTL,
+ });
+ }
+ createToast({ body: t("settingsSuccessSaved") });
+ } catch (error) {
+ createToast({ body: t("settingsFailedToSave") });
+
+ setError(error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return [isLoading, error, saveSettings];
+};
+
+export { useFetchSettings, useSaveSettings };
diff --git a/client/src/Pages/Settings/SettingsAbout.jsx b/client/src/Pages/Settings/SettingsAbout.jsx
new file mode 100644
index 000000000..b2b5df308
--- /dev/null
+++ b/client/src/Pages/Settings/SettingsAbout.jsx
@@ -0,0 +1,32 @@
+import Box from "@mui/material/Box";
+import Typography from "@mui/material/Typography";
+import ConfigBox from "../../Components/ConfigBox";
+// Utils
+import { useTheme } from "@emotion/react";
+import { useTranslation } from "react-i18next";
+import Link from "../../Components/Link";
+
+const SettingsAbout = () => {
+ const theme = useTheme();
+ const { t } = useTranslation();
+ return (
+
+
+ {t("settingsAbout")}
+
+
+ Checkmate {2.0}
+
+ {t("settingsDevelopedBy")}
+
+
+
+
+ );
+};
+
+export default SettingsAbout;
diff --git a/client/src/Pages/Settings/SettingsDemoMonitors.jsx b/client/src/Pages/Settings/SettingsDemoMonitors.jsx
new file mode 100644
index 000000000..1a592e776
--- /dev/null
+++ b/client/src/Pages/Settings/SettingsDemoMonitors.jsx
@@ -0,0 +1,90 @@
+import Box from "@mui/material/Box";
+import Typography from "@mui/material/Typography";
+import Button from "@mui/material/Button";
+import ConfigBox from "../../Components/ConfigBox";
+// Utils
+import { useTheme } from "@emotion/react";
+import { PropTypes } from "prop-types";
+import { useTranslation } from "react-i18next";
+import Dialog from "../../Components/Dialog";
+import { useState } from "react";
+
+const SettingsDemoMonitors = ({ HEADER_SX, handleChange, isLoading }) => {
+ const { t } = useTranslation();
+ const theme = useTheme();
+ // Local state
+ const [isOpen, setIsOpen] = useState(false);
+ return (
+ <>
+
+
+ {t("settingsDemoMonitors")}
+ {t("settingsDemoMonitorsDescription")}
+
+
+ {t("settingsAddDemoMonitors")}
+
+
+
+
+
+ {t("settingsSystemReset")}
+
+ {t("settingsSystemResetDescription")}
+
+
+
+ {t("settingsRemoveAllMonitors")}
+
+
+
+ >
+ );
+};
+
+SettingsDemoMonitors.propTypes = {
+ handleChange: PropTypes.func,
+ HEADER_SX: PropTypes.object,
+};
+
+export default SettingsDemoMonitors;
diff --git a/client/src/Pages/Settings/SettingsEmail.jsx b/client/src/Pages/Settings/SettingsEmail.jsx
new file mode 100644
index 000000000..ecc2a51f2
--- /dev/null
+++ b/client/src/Pages/Settings/SettingsEmail.jsx
@@ -0,0 +1,126 @@
+import Box from "@mui/material/Box";
+import Typography from "@mui/material/Typography";
+import ConfigBox from "../../Components/ConfigBox";
+import TextInput from "../../Components/Inputs/TextInput";
+import Button from "@mui/material/Button";
+import Stack from "@mui/material/Stack";
+// Utils
+import { useTheme } from "@emotion/react";
+import { PropTypes } from "prop-types";
+import { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments";
+const SettingsEmail = ({
+ HEADER_SX,
+ handleChange,
+ settingsData,
+ setSettingsData,
+ isPasswordSet,
+}) => {
+ const { t } = useTranslation();
+ const theme = useTheme();
+
+ const [password, setPassword] = useState("");
+ const [hasBeenReset, setHasBeenReset] = useState(false);
+
+ const handlePasswordChange = (e) => {
+ setPassword(e.target.value);
+ setSettingsData({
+ ...settingsData,
+ settings: { ...settingsData.settings, systemEmailPassword: e.target.value },
+ });
+ };
+
+ return (
+
+
+ {t("settingsEmail")}
+ {t("settingsEmailDescription")}
+
+
+
+
+ {t("settingsEmailHost")}
+
+
+
+ {t("settingsEmailPort")}
+
+
+
+ {t("settingsEmailUser")}
+
+
+
+ {t("settingsEmailAddress")}
+
+
+ {(isPasswordSet === false || hasBeenReset === true) && (
+
+ {t("settingsEmailPassword")}
+ }
+ />
+
+ )}
+ {isPasswordSet === true && hasBeenReset === false && (
+
+ {t("settingsEmailFieldResetLabel")}
+
+
+ )}
+
+
+
+ );
+};
+
+SettingsEmail.propTypes = {
+ settingsData: PropTypes.object,
+ setSettingsData: PropTypes.func,
+ handleChange: PropTypes.func,
+ HEADER_SX: PropTypes.object,
+ isPasswordSet: PropTypes.bool,
+};
+
+export default SettingsEmail;
diff --git a/client/src/Pages/Settings/SettingsPagespeed.jsx b/client/src/Pages/Settings/SettingsPagespeed.jsx
new file mode 100644
index 000000000..95b159fa5
--- /dev/null
+++ b/client/src/Pages/Settings/SettingsPagespeed.jsx
@@ -0,0 +1,87 @@
+import Box from "@mui/material/Box";
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import Button from "@mui/material/Button";
+import ConfigBox from "../../Components/ConfigBox";
+import TextInput from "../../Components/Inputs/TextInput";
+import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments";
+// Utils
+import { useTheme } from "@emotion/react";
+import { PropTypes } from "prop-types";
+import { useState, useEffect } from "react";
+import { useTranslation } from "react-i18next";
+
+const SettingsPagespeed = ({
+ HEADING_SX,
+ settingsData,
+ setSettingsData,
+ isApiKeySet,
+}) => {
+ const { t } = useTranslation();
+ const theme = useTheme();
+
+ // Local state
+ const [apiKey, setApiKey] = useState("");
+ const [hasBeenReset, setHasBeenReset] = useState(false);
+
+ // Handler
+ const handleChange = (e) => {
+ setApiKey(e.target.value);
+ setSettingsData({
+ ...settingsData,
+ settings: { ...settingsData.settings, pagespeedApiKey: e.target.value },
+ });
+ };
+
+ return (
+
+
+ {t("pageSpeedApiKeyFieldTitle")}
+ {t("pageSpeedApiKeyFieldDescription")}
+
+
+ {(isApiKeySet === false || hasBeenReset === true) && (
+ }
+ />
+ )}
+
+ {isApiKeySet === true && hasBeenReset === false && (
+
+ {t("pageSpeedApiKeyFieldResetLabel")}
+
+
+ )}
+
+
+ );
+};
+
+SettingsPagespeed.propTypes = {
+ HEADING_SX: PropTypes.object,
+ settingsData: PropTypes.object,
+ setSettingsData: PropTypes.func,
+ isApiKeySet: PropTypes.bool,
+};
+
+export default SettingsPagespeed;
diff --git a/client/src/Pages/Settings/SettingsStats.jsx b/client/src/Pages/Settings/SettingsStats.jsx
new file mode 100644
index 000000000..961f13665
--- /dev/null
+++ b/client/src/Pages/Settings/SettingsStats.jsx
@@ -0,0 +1,84 @@
+import Box from "@mui/material/Box";
+import Typography from "@mui/material/Typography";
+import Button from "@mui/material/Button";
+import Stack from "@mui/material/Stack";
+import ConfigBox from "../../Components/ConfigBox";
+import TextInput from "../../Components/Inputs/TextInput";
+import Dialog from "../../Components/Dialog";
+
+// Utils
+import { useTheme } from "@emotion/react";
+import { PropTypes } from "prop-types";
+import { useTranslation } from "react-i18next";
+import { useState } from "react";
+
+const SettingsStats = ({ HEADING_SX, handleChange, settingsData, errors }) => {
+ const theme = useTheme();
+ const { t } = useTranslation();
+ const [isOpen, setIsOpen] = useState(false);
+ return (
+
+
+
+ {t("settingsHistoryAndMonitoring")}
+
+
+ {t("settingsHistoryAndMonitoringDescription")}
+
+
+
+
+
+ {t("settingsClearAllStats")}
+
+
+
+
+ );
+};
+
+SettingsStats.propTypes = {
+ HEADING_SX: PropTypes.object,
+ handleChange: PropTypes.func,
+ settingsData: PropTypes.object,
+ errors: PropTypes.object,
+};
+
+export default SettingsStats;
diff --git a/client/src/Pages/Settings/SettingsTimeZone.jsx b/client/src/Pages/Settings/SettingsTimeZone.jsx
new file mode 100644
index 000000000..e6ba1a77b
--- /dev/null
+++ b/client/src/Pages/Settings/SettingsTimeZone.jsx
@@ -0,0 +1,43 @@
+import Box from "@mui/material/Box";
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import ConfigBox from "../../Components/ConfigBox";
+import Select from "../../Components/Inputs/Select";
+import timezones from "../../Utils/timezones.json";
+
+// Utils
+import { useTheme } from "@emotion/react";
+import { PropTypes } from "prop-types";
+import { useTranslation } from "react-i18next";
+const SettingsTimeZone = ({ HEADING_SX, handleChange, timezone }) => {
+ const theme = useTheme();
+ const { t } = useTranslation();
+ return (
+
+
+ {t("settingsGeneralSettings")}
+
+ {t("settingsDisplayTimezone")}-{" "}
+ {t("settingsDisplayTimezoneDescription")}
+
+
+
+
+
+
+ );
+};
+
+SettingsTimeZone.propTypes = {
+ HEADING_SX: PropTypes.object,
+ handleChange: PropTypes.func,
+ timezone: PropTypes.string,
+};
+
+export default SettingsTimeZone;
diff --git a/client/src/Pages/Settings/SettingsUI.jsx b/client/src/Pages/Settings/SettingsUI.jsx
new file mode 100644
index 000000000..24588d79e
--- /dev/null
+++ b/client/src/Pages/Settings/SettingsUI.jsx
@@ -0,0 +1,52 @@
+import Box from "@mui/material/Box";
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import ConfigBox from "../../Components/ConfigBox";
+import Select from "../../Components/Inputs/Select";
+
+// Utils
+import { useTheme } from "@emotion/react";
+import { PropTypes } from "prop-types";
+import { useTranslation } from "react-i18next";
+
+const SettingsUI = ({ HEADING_SX, handleChange, mode, language }) => {
+ const { t, i18n } = useTranslation();
+ const theme = useTheme();
+ const languages = Object.keys(i18n.options.resources || {});
+ return (
+
+
+ {t("settingsAppearance")}
+ {t("settingsAppearanceDescription")}
+
+
+
+
+
+
+ );
+};
+
+SettingsUI.propTypes = {
+ HEADING_SX: PropTypes.object,
+ handleChange: PropTypes.func,
+ mode: PropTypes.string,
+ language: PropTypes.string,
+};
+
+export default SettingsUI;
diff --git a/client/src/Pages/Settings/index.jsx b/client/src/Pages/Settings/index.jsx
index 79e5ea657..eb516ba01 100644
--- a/client/src/Pages/Settings/index.jsx
+++ b/client/src/Pages/Settings/index.jsx
@@ -1,116 +1,69 @@
-// Components
-import { Box, Stack, Typography, Button } from "@mui/material";
-import TextInput from "../../Components/Inputs/TextInput";
-import Link from "../../Components/Link";
-import Select from "../../Components/Inputs/Select";
-import { useIsAdmin } from "../../Hooks/useIsAdmin";
-import Dialog from "../../Components/Dialog";
-import ConfigBox from "../../Components/ConfigBox";
-import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments";
-import { getAppSettings } from "../../Features/Settings/settingsSlice";
-// import {
-// WalletMultiButton,
-// WalletDisconnectButton,
-// } from "@solana/wallet-adapter-react-ui";
-
-//Utils
-import { useTheme } from "@emotion/react";
-import { logger } from "../../Utils/Logger";
-import { useDispatch, useSelector } from "react-redux";
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import Breadcrumbs from "../../Components/Breadcrumbs";
+import SettingsTimeZone from "./SettingsTimeZone";
+import SettingsUI from "./SettingsUI";
+import SettingsPagespeed from "./SettingsPagespeed";
+import SettingsDemoMonitors from "./SettingsDemoMonitors";
+import SettingsAbout from "./SettingsAbout";
+import SettingsEmail from "./SettingsEmail";
+import Button from "@mui/material/Button";
+// Utils
+import { settingsValidation } from "../../Validation/validation";
import { createToast } from "../../Utils/toastUtils";
+import { useState } from "react";
+import { useTheme } from "@emotion/react";
+import { useTranslation } from "react-i18next";
+import { useSelector, useDispatch } from "react-redux";
+import { setTimezone, setMode, setLanguage } from "../../Features/UI/uiSlice";
+import SettingsStats from "./SettingsStats";
import {
deleteMonitorChecksByTeamId,
addDemoMonitors,
deleteAllMonitors,
} from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
-import { update } from "../../Features/Auth/authSlice";
-import PropTypes from "prop-types";
-import { setTimezone, setMode, setLanguage } from "../../Features/UI/uiSlice";
-import timezones from "../../Utils/timezones.json";
-import { useState, useEffect } from "react";
-import { networkService } from "../../main";
-import { settingsValidation } from "../../Validation/validation";
-import { updateAppSettings } from "../../Features/Settings/settingsSlice";
-import { useTranslation } from "react-i18next";
+import { useFetchSettings, useSaveSettings } from "../../Hooks/useFetchSettings";
+import { UseDeleteMonitorStats } from "../../Hooks/useDeleteMonitorStats";
// Constants
-const SECONDS_PER_DAY = 86400;
+const BREADCRUMBS = [{ name: `Settings`, path: "/settings" }];
const Settings = () => {
- const theme = useTheme();
- const { t, i18n } = useTranslation();
- const isAdmin = useIsAdmin();
+ // Redux state
+ const { mode, language, timezone } = useSelector((state) => state.ui);
const { user } = useSelector((state) => state.auth);
- const { language } = useSelector((state) => state.ui);
- const { checkTTL } = user;
- const { isLoading } = useSelector((state) => state.uptimeMonitors);
- const { isLoading: authIsLoading } = useSelector((state) => state.auth);
- const { timezone, distributedUptimeEnabled } = useSelector((state) => state.ui);
- const { mode } = useSelector((state) => state.ui);
- const { pagespeedApiKey } = useSelector((state) => state.settings);
- const [checksIsLoading, setChecksIsLoading] = useState(false);
- const [form, setForm] = useState({
- enableDistributedUptime: distributedUptimeEnabled,
- ttl: checkTTL ? (checkTTL / SECONDS_PER_DAY).toString() : 0,
- pagespeedApiKey: pagespeedApiKey,
+
+ // Local state
+ const [settingsData, setSettingsData] = useState({});
+ const [errors, setErrors] = useState({});
+ // Network
+ const [isSettingsLoading, settingsError] = useFetchSettings({
+ setSettingsData,
});
- const [version, setVersion] = useState("unknown");
- const [apiKeyFieldType, setApiKeyFieldType] = useState("password");
- const [isApiKeySet, setIsApiKeySet] = useState(pagespeedApiKey ? true : false);
- const [tempPSKey, setTempPSKey] = useState("");
- const [errors, setErrors] = useState({});
- const deleteStatsMonitorsInitState = { deleteMonitors: false, deleteStats: false };
- const [isOpen, setIsOpen] = useState(deleteStatsMonitorsInitState);
+ const [isSaving, saveError, saveSettings] = useSaveSettings();
+ const [deleteMonitorStats, isDeletingMonitorStats] = UseDeleteMonitorStats();
+
+ // Setup
+ const theme = useTheme();
+ const HEADING_SX = { mt: theme.spacing(2), mb: theme.spacing(2) };
+ const { t, i18n } = useTranslation();
const dispatch = useDispatch();
- //Fetching latest release version from github
- useEffect(() => {
- const fetchLatestVersion = async () => {
- let version = "unknown";
- try {
- const response = await networkService.fetchGithubLatestRelease();
- if (!response.status === 200) {
- throw new Error("Failed to fetch latest version");
- }
- version = response.data.tag_name;
- } catch (error) {
- createToast({ body: error.message || "Error fetching latest version" }); // Set error message
- } finally {
- setVersion(version);
- }
+ // Handlers
+ const handleChange = async (e) => {
+ const { name, value } = e.target;
+
+ // Build next state early
+ const newSettingsData = {
+ ...settingsData,
+ settings: { ...settingsData.settings, [name]: value },
};
- fetchLatestVersion();
- }, []);
-
- useEffect(() => {
- dispatch(getAppSettings());
- }, [dispatch]);
-
- const handleChange = (event) => {
- const { type, checked, value, id } = event.target;
-
- if (type === "checkbox") {
- setForm((prev) => ({
- ...prev,
- [id]: checked,
- }));
- return;
- }
-
- if (id === "pagespeedApiKey") {
- setTempPSKey(value);
- return;
- }
-
- let inputValue = value;
- if (id === "ttl") {
- inputValue = value.replace(/[^0-9]/g, "");
- }
-
- const updatedForm = { ...form, [id]: inputValue };
- const { error } = settingsValidation.validate(updatedForm, { abortEarly: false });
+ // Validate
+ const { error } = settingsValidation.validate(newSettingsData.settings, {
+ abortEarly: false,
+ });
if (!error || error.details.length === 0) {
setErrors({});
} else {
@@ -119,397 +72,130 @@ const Settings = () => {
newErrors[err.path[0]] = err.message;
});
setErrors(newErrors);
- logger.error("Validation errors:", error.details);
}
- setForm(updatedForm);
- };
-
- // TODO Handle saving
- const handleSave = async () => {
- try {
- setChecksIsLoading(true);
- await networkService.updateChecksTTL({
- ttl: form.ttl,
- });
- const updatedUser = { ...user, checkTTL: form.ttl };
- const [userAction, settingsAction] = await Promise.all([
- dispatch(update({ localData: updatedUser })),
- dispatch(
- updateAppSettings({
- settings: {
- language: language,
- pagespeedApiKey: tempPSKey ? tempPSKey : form.pagespeedApiKey,
- },
- })
- ),
- ]);
-
- if (userAction.payload.success && settingsAction.payload.success) {
- createToast({ body: t("settingsSuccessSaved") });
- } else {
- throw new Error("Failed to save settings");
- }
- } catch (error) {
- createToast({ body: t("settingsFailedToSave") });
- } finally {
- setChecksIsLoading(false);
+ if (name === "timezone") {
+ dispatch(setTimezone({ timezone: value }));
}
- };
- const handleClearStats = async () => {
- try {
- const action = await dispatch(deleteMonitorChecksByTeamId({ teamId: user.teamId }));
-
- if (deleteMonitorChecksByTeamId.fulfilled.match(action)) {
- createToast({ body: t("settingsStatsCleared") });
- } else {
- createToast({ body: t("settingsFailedToClearStats") });
- }
- } catch (error) {
- logger.error(error);
- createToast({ body: t("settingsFailedToClearStats") });
- } finally {
- setIsOpen(deleteStatsMonitorsInitState);
+ if (name === "mode") {
+ dispatch(setMode(value));
}
- };
- const handleInsertDemoMonitors = async () => {
- try {
- const action = await dispatch(addDemoMonitors());
- if (addDemoMonitors.fulfilled.match(action)) {
- createToast({ body: t("settingsDemoMonitorsAdded") });
- } else {
+ if (name === "language") {
+ dispatch(setLanguage(value));
+ i18n.changeLanguage(value);
+ }
+
+ if (name === "deleteStats") {
+ await deleteMonitorStats({ teamId: user.teamId });
+ return;
+ }
+
+ if (name === "demo") {
+ try {
+ const action = await dispatch(addDemoMonitors());
+ if (addDemoMonitors.fulfilled.match(action)) {
+ createToast({ body: t("settingsDemoMonitorsAdded") });
+ } else {
+ createToast({ body: t("settingsFailedToAddDemoMonitors") });
+ }
+ } catch (error) {
createToast({ body: t("settingsFailedToAddDemoMonitors") });
}
- } catch (error) {
- logger.error(error);
- createToast({ Body: t("settingsFailedToAddDemoMonitors") });
+ return;
}
- };
- const handleDeleteAllMonitors = async () => {
- try {
- const action = await dispatch(deleteAllMonitors());
- if (deleteAllMonitors.fulfilled.match(action)) {
- createToast({ body: t("settingsMonitorsDeleted") });
- } else {
+ if (name === "deleteMonitors") {
+ try {
+ const action = await dispatch(deleteAllMonitors());
+ if (deleteAllMonitors.fulfilled.match(action)) {
+ createToast({ body: t("settingsMonitorsDeleted") });
+ } else {
+ createToast({ body: t("settingsFailedToDeleteMonitors") });
+ }
+ } catch (error) {
createToast({ body: t("settingsFailedToDeleteMonitors") });
}
- } catch (error) {
- logger.error(error);
- createToast({ Body: t("settingsFailedToDeleteMonitors") });
- } finally {
- setIsOpen(deleteStatsMonitorsInitState);
+ return;
}
+
+ setSettingsData(newSettingsData);
};
- const handleResetApiKey = () => {
- setIsApiKeySet(false);
- setForm((prev) => ({
- ...prev,
- pagespeedApiKey: "",
- }));
+ const handleSave = () => {
+ const { error } = settingsValidation.validate(settingsData.settings, {
+ abortEarly: false,
+ });
+ if (!error || error.details.length === 0) {
+ setErrors({});
+ } else {
+ const newErrors = {};
+ error.details.forEach((err) => {
+ newErrors[err.path[0]] = err.message;
+ });
+ setErrors(newErrors);
+ }
+ saveSettings(settingsData?.settings);
};
- const languages = Object.keys(i18n.options.resources || {});
-
return (
-
+
+
+ Settings
+
+
+
+
+
+
+
-
-
- {t("settingsGeneralSettings")}
-
- {t("settingsDisplayTimezone")}-{" "}
- {t("settingsDisplayTimezoneDescription")}
-
-
-
-
-
-
-
- {t("settingsAppearance")}
-
- {t("settingsAppearanceDescription")}
-
-
-
-
-
-
-
- {/* {isAdmin && (
-
-
- {t("settingsDistributedUptime")}
-
- {t("settingsDistributedUptimeDescription")}
-
-
-
- {
- dispatch(setDistributedUptimeEnabled(e.target.checked));
- }}
- />
- {distributedUptimeEnabled === true
- ? t("settingsEnabled")
- : t("settingsDisabled")}
-
-
- )} */}
-
-
- {t("pageSpeedApiKeyFieldTitle")}
-
- {t("pageSpeedApiKeyFieldDescription")}
-
-
-
-
- )
- }
- />
- {isApiKeySet && (
-
- {t("pageSpeedApiKeyFieldResetLabel")}
-
-
- )}
-
-
- {/* {isAdmin && (
-
-
- {t("settingsWallet")}
-
- {t("settingsWalletDescription")}
-
-
-
-
-
-
-
-
-
- )} */}
- {isAdmin && (
-
-
- {t("settingsHistoryAndMonitoring")}
-
- {t("settingsHistoryAndMonitoringDescription")}
-
-
-
-
-
- {t("settingsClearAllStats")}
-
-
-
-
- )}
- {isAdmin && (
- <>
- {/* Demo Monitors Section */}
-
-
- {t("settingsDemoMonitors")}
-
- {t("settingsDemoMonitorsDescription")}
-
-
-
- {t("settingsAddDemoMonitors")}
-
-
-
-
- {/* System Reset Section */}
-
-
- {t("settingsSystemReset")}
-
- {t("settingsSystemResetDescription")}
-
-
-
- {t("settingsRemoveAllMonitors")}
-
-
-
- >
- )}
-
-
-
- {t("settingsAbout")}
-
-
- Checkmate {version}
-
- {t("settingsDevelopedBy")}
-
-
-
-
- 0}
+ variant="contained"
+ color="accent"
+ sx={{ px: theme.spacing(12), mt: theme.spacing(20) }}
+ onClick={handleSave}
>
-
-
+ {t("settingsSave")}
+
-
+
);
};
-Settings.propTypes = {
- isAdmin: PropTypes.bool,
-};
export default Settings;
diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js
index 0ace7b02c..81359d903 100644
--- a/client/src/Validation/validation.js
+++ b/client/src/Validation/validation.js
@@ -11,7 +11,8 @@ const nameSchema = joi
.messages({
"string.empty": "Name is required",
"string.max": "Name must be less than 50 characters",
- "string.pattern.base": "Name must contain only letters, spaces, apostrophes, or hyphens"
+ "string.pattern.base":
+ "Name must contain only letters, spaces, apostrophes, or hyphens",
});
const passwordSchema = joi
@@ -152,20 +153,21 @@ const monitorValidation = joi.object({
"string.invalidUrl": "Please enter a valid URL with optional port",
"string.pattern.base": "Please enter a valid container ID.",
}),
- port: joi.number()
- .integer()
- .min(1)
- .max(65535)
- .when("type", {
- is: "port",
- then: joi.required().messages({
- "number.base": "Port must be a number.",
- "number.min": "Port must be at least 1.",
- "number.max": "Port must be at most 65535.",
- "any.required": "Port is required for port monitors.",
+ port: joi
+ .number()
+ .integer()
+ .min(1)
+ .max(65535)
+ .when("type", {
+ is: "port",
+ then: joi.required().messages({
+ "number.base": "Port must be a number.",
+ "number.min": "Port must be at least 1.",
+ "number.max": "Port must be at most 65535.",
+ "any.required": "Port is required for port monitors.",
+ }),
+ otherwise: joi.optional(),
}),
- otherwise: joi.optional(),
- }),
name: joi.string().trim().max(50).allow("").messages({
"string.max": "This field should not exceed the 50 characters limit.",
}),
@@ -253,14 +255,19 @@ const statusPageValidation = joi.object({
showCharts: joi.boolean(),
});
const settingsValidation = joi.object({
- ttl: joi.number().required().messages({
+ checkTTL: joi.number().required().messages({
"string.empty": "Please enter a value",
"number.base": "Please enter a valid number",
- "any.required": "Please enter a value"
+ "any.required": "Please enter a value",
}),
pagespeedApiKey: joi.string().allow("").optional(),
-})
-.unknown(true);
+ language: joi.string().required(),
+ systemEmailHost: joi.string().allow(""),
+ systemEmailPort: joi.number().allow(null, ""),
+ systemEmailAddress: joi.string().allow(""),
+ systemEmailPassword: joi.string().allow(""),
+ systemEmailUser: joi.string().allow(""),
+});
const dayjsValidator = (value, helpers) => {
if (!dayjs(value).isValid()) {
@@ -363,9 +370,12 @@ const infrastructureMonitorValidation = joi.object({
notifications: joi.array().items(
joi.object({
type: joi.string().valid("email").required(),
- address: joi.string().email({ tlds: { allow: false } }).required(),
+ address: joi
+ .string()
+ .email({ tlds: { allow: false } })
+ .required(),
})
- )
+ ),
});
export {
@@ -377,5 +387,5 @@ export {
advancedSettingsValidation,
infrastructureMonitorValidation,
statusPageValidation,
- logoImageValidation
+ logoImageValidation,
};
diff --git a/client/src/locales/gb.json b/client/src/locales/gb.json
index 873229835..222d68a7f 100644
--- a/client/src/locales/gb.json
+++ b/client/src/locales/gb.json
@@ -108,6 +108,14 @@
"settingsFailedToAddDemoMonitors": "Failed to add demo monitors",
"settingsMonitorsDeleted": "Successfully deleted all monitors",
"settingsFailedToDeleteMonitors": "Failed to delete all monitors",
+ "settingsEmail": "Email settings",
+ "settingsEmailDescription": "Configure email settings",
+ "settingsEmailHost": "Email host",
+ "settingsEmailPort": "Email port",
+ "settingsEmailAddress": "Email address",
+ "settingsEmailPassword": "Email password",
+ "settingsEmailUser": "Email user",
+ "settingsEmailFieldResetLabel": "Password is set. Click Reset to change it.",
"backendUnreachable": "Server Connection Error",
"backendUnreachableMessage": "We're unable to connect to the server. Please check your internet connection or verify your deployment configuration if the problem persists.",
"backendUnreachableError": "Cannot connect to the server. Please try again later.",
@@ -439,6 +447,7 @@
"pageSpeedApiKeyFieldLabel": "PageSpeed API key",
"pageSpeedApiKeyFieldDescription": "Enter your Google PageSpeed API key to enable pagespeed monitoring. Click Reset to update the key.",
"pageSpeedApiKeyFieldResetLabel": "API key is set. Click Reset to change it.",
+
"reset": "Reset",
"createNew": "Create new",
"greeting": {
@@ -502,5 +511,4 @@
"profile": "Profile",
"password": "Password",
"team": "Team"
- }
}
diff --git a/server/controllers/monitorController.js b/server/controllers/monitorController.js
index 5ab654375..9a42ea4e5 100755
--- a/server/controllers/monitorController.js
+++ b/server/controllers/monitorController.js
@@ -649,9 +649,8 @@ class MonitorController {
sendTestEmail = async (req, res, next) => {
try {
const { to } = req.body;
-
if (!to || typeof to !== "string") {
- return res.error({ msg: this.stringService.errorForValidEmailAddress });
+ throw new Error(this.stringService.errorForValidEmailAddress);
}
const subject = this.stringService.testEmailSubject;
diff --git a/server/controllers/settingsController.js b/server/controllers/settingsController.js
index 4d561216e..b557e8dd6 100755
--- a/server/controllers/settingsController.js
+++ b/server/controllers/settingsController.js
@@ -13,12 +13,25 @@ class SettingsController {
getAppSettings = async (req, res, next) => {
const dbSettings = await this.settingsService.getDBSettings();
const sanitizedSettings = { ...dbSettings };
+
+ const returnSettings = {
+ pagespeedKeySet: false,
+ emailPasswordSet: false,
+ };
+
if (typeof sanitizedSettings.pagespeedApiKey !== "undefined") {
- sanitizedSettings.pagespeedApiKey = "********";
+ returnSettings.pagespeedKeySet = true;
+ delete sanitizedSettings.pagespeedApiKey;
}
+ if (typeof sanitizedSettings.systemEmailPassword !== "undefined") {
+ returnSettings.emailPasswordSet = true;
+ delete sanitizedSettings.systemEmailPassword;
+ }
+
+ returnSettings.settings = sanitizedSettings;
return res.success({
msg: this.stringService.getAppSettings,
- data: sanitizedSettings,
+ data: returnSettings,
});
};
diff --git a/server/db/models/AppSettings.js b/server/db/models/AppSettings.js
index 3942305e8..06ba8c1df 100755
--- a/server/db/models/AppSettings.js
+++ b/server/db/models/AppSettings.js
@@ -2,13 +2,16 @@ import mongoose from "mongoose";
const AppSettingsSchema = mongoose.Schema(
{
+ checkTTL: {
+ type: Number,
+ default: 30,
+ },
language: {
type: String,
default: "gb",
},
pagespeedApiKey: {
type: String,
- default: "",
},
systemEmailHost: {
type: String,
@@ -22,6 +25,9 @@ const AppSettingsSchema = mongoose.Schema(
systemEmailPassword: {
type: String,
},
+ systemEmailUser: {
+ type: String,
+ },
singleton: {
type: Boolean,
required: true,
diff --git a/server/db/mongo/modules/settingsModule.js b/server/db/mongo/modules/settingsModule.js
index b817d2a32..d3568ad32 100755
--- a/server/db/mongo/modules/settingsModule.js
+++ b/server/db/mongo/modules/settingsModule.js
@@ -14,12 +14,22 @@ const getAppSettings = async () => {
const updateAppSettings = async (newSettings) => {
try {
- console.log(newSettings);
- const settings = await AppSettings.findOneAndUpdate(
- {},
- { $set: newSettings },
- { new: true, upsert: true }
- );
+ const update = { $set: { ...newSettings } };
+
+ if (newSettings.pagespeedApiKey === "") {
+ update.$unset = { pagespeedApiKey: "" };
+ delete update.$set.pagespeedApiKey;
+ }
+
+ if (newSettings.systemEmailPassword === "") {
+ update.$unset = { systemEmailPassword: "" };
+ delete update.$set.systemEmailPassword;
+ }
+
+ const settings = await AppSettings.findOneAndUpdate({}, update, {
+ new: true,
+ upsert: true,
+ });
return settings;
} catch (error) {
error.service = SERVICE_NAME;
diff --git a/server/service/emailService.js b/server/service/emailService.js
index c9e34eeda..57f8c78e6 100755
--- a/server/service/emailService.js
+++ b/server/service/emailService.js
@@ -29,7 +29,10 @@ class EmailService {
this.mjml2html = mjml2html;
this.nodemailer = nodemailer;
this.logger = logger;
+ this.init();
+ }
+ init = async () => {
/**
* Loads an email template from the filesystem.
*
@@ -67,34 +70,14 @@ class EmailService {
serverIsUpTemplate: this.loadTemplate("serverIsUp"),
passwordResetTemplate: this.loadTemplate("passwordReset"),
hardwareIncidentTemplate: this.loadTemplate("hardwareIncident"),
- testEmailTemplate: this.loadTemplate("testEmailTemplate")
+ testEmailTemplate: this.loadTemplate("testEmailTemplate"),
};
/**
* The email transporter used to send emails.
* @type {Object}
*/
-
- const {
- systemEmailHost,
- systemEmailPort,
- systemEmailUser,
- systemEmailAddress,
- systemEmailPassword,
- } = this.settingsService.getSettings();
-
- const emailConfig = {
- host: systemEmailHost,
- port: systemEmailPort,
- secure: true,
- auth: {
- user: systemEmailUser || systemEmailAddress,
- pass: systemEmailPassword,
- },
- };
-
- this.transporter = this.nodemailer.createTransport(emailConfig);
- }
+ };
/**
* Asynchronously builds and sends an email using a specified template and context.
@@ -106,6 +89,28 @@ class EmailService {
* @returns {Promise} A promise that resolves to the messageId of the sent email.
*/
buildAndSendEmail = async (template, context, to, subject) => {
+ // TODO - Consider an update transporter method so this only needs to be recreated when smtp settings change
+ const {
+ systemEmailHost,
+ systemEmailPort,
+ systemEmailUser,
+ systemEmailAddress,
+ systemEmailPassword,
+ } = await this.settingsService.getDBSettings();
+
+ const emailConfig = {
+ host: systemEmailHost,
+ port: systemEmailPort,
+ secure: true,
+ auth: {
+ user: systemEmailUser || systemEmailAddress,
+ pass: systemEmailPassword,
+ },
+ connectionTimeout: 5000,
+ };
+
+ this.transporter = this.nodemailer.createTransport(emailConfig);
+
const buildHtml = async (template, context) => {
try {
const mjml = this.templateLookup[template](context);
diff --git a/server/service/settingsService.js b/server/service/settingsService.js
index 548d76eee..290e645e9 100755
--- a/server/service/settingsService.js
+++ b/server/service/settingsService.js
@@ -59,7 +59,17 @@ class SettingsService {
}
async getDBSettings() {
- const settings = await this.appSettings.findOne({ singleton: true }).lean();
+ let settings = await this.appSettings
+ .findOne({ singleton: true })
+ .select("-__v -_id -createdAt -updatedAt -singleton")
+ .lean();
+ if (settings === null) {
+ await this.appSettings.create({});
+ settings = await this.appSettings
+ .findOne({ singleton: true })
+ .select("-__v -_id -createdAt -updatedAt -singleton")
+ .lean();
+ }
return settings;
}
}
diff --git a/server/validation/joi.js b/server/validation/joi.js
index bd402fb8a..997d35d65 100755
--- a/server/validation/joi.js
+++ b/server/validation/joi.js
@@ -424,21 +424,14 @@ const editMaintenanceByIdWindowBodyValidation = joi.object({
// SettingsValidation
//****************************************
const updateAppSettingsBodyValidation = joi.object({
- apiBaseUrl: joi.string().allow(""),
- logLevel: joi.string().valid("debug", "none", "error", "warn").allow(""),
- clientHost: joi.string().allow(""),
- dbType: joi.string().allow(""),
- dbConnectionString: joi.string().allow(""),
- redisHost: joi.string().allow(""),
- redisPort: joi.number().allow(null, ""),
- redisUrl: joi.string().allow(""),
- jwtTTL: joi.string().allow(""),
+ checkTTL: joi.number().allow(""),
pagespeedApiKey: joi.string().allow(""),
language: joi.string().allow(""),
systemEmailHost: joi.string().allow(""),
systemEmailPort: joi.number().allow(""),
systemEmailAddress: joi.string().allow(""),
systemEmailPassword: joi.string().allow(""),
+ systemEmailUser: joi.string().allow(""),
});
//****************************************