diff --git a/client/src/Pages/Infrastructure/Create/index.jsx b/client/src/Pages/Infrastructure/Create/index.jsx index 0800bb500..fa6ac4fcb 100644 --- a/client/src/Pages/Infrastructure/Create/index.jsx +++ b/client/src/Pages/Infrastructure/Create/index.jsx @@ -3,7 +3,7 @@ import { useTheme } from "@emotion/react"; import { useParams } from "react-router-dom"; import { useState, useEffect } from "react"; import { useSelector } from "react-redux"; - +import axios from "axios"; // Utility and Network import { infrastructureMonitorValidation } from "../../../Validation/validation"; import { useFetchHardwareMonitorById } from "../../../Hooks/monitorHooks"; @@ -87,35 +87,59 @@ const CreateInfrastructureMonitor = () => { }); // Populate form fields if editing + // Populate global thresholds from /settings if creating + const { authToken } = useSelector((state) => state.auth); + useEffect(() => { - if (isCreate || !monitor) return; + const fetchGlobalThresholds = async () => { + if (!isCreate) return; - setInfrastructureMonitor({ - url: monitor.url.replace(/^https?:\/\//, ""), - name: monitor.name || "", - notifications: monitor.notifications, - interval: monitor.interval / MS_PER_MINUTE, - cpu: monitor.thresholds?.usage_cpu !== undefined, - usage_cpu: monitor.thresholds?.usage_cpu ? monitor.thresholds.usage_cpu * 100 : "", + try { + const token = authToken; + if (!token) { + console.warn("No token found for fetching global thresholds."); + return; + } - memory: monitor.thresholds?.usage_memory !== undefined, - usage_memory: monitor.thresholds?.usage_memory - ? monitor.thresholds.usage_memory * 100 - : "", + const response = await axios.get(`${import.meta.env.VITE_APP_API_BASE_URL}/settings`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); - disk: monitor.thresholds?.usage_disk !== undefined, - usage_disk: monitor.thresholds?.usage_disk - ? monitor.thresholds.usage_disk * 100 - : "", + const globalThresholds = response.data?.data?.settings?.globalThresholds; - temperature: monitor.thresholds?.usage_temperature !== undefined, - usage_temperature: monitor.thresholds?.usage_temperature - ? monitor.thresholds.usage_temperature * 100 - : "", - secret: monitor.secret || "", - }); - setHttps(monitor.url.startsWith("https")); - }, [isCreate, monitor]); + if (!globalThresholds) { + console.warn("No global thresholds found in /settings response."); + return; + } + + setInfrastructureMonitor((prev) => ({ + ...prev, + cpu: globalThresholds.cpu != null, + usage_cpu: globalThresholds.cpu != null ? globalThresholds.cpu.toString() : "", + + memory: globalThresholds.memory != null, + usage_memory: + globalThresholds.memory != null ? globalThresholds.memory.toString() : "", + + disk: globalThresholds.disk != null, + usage_disk: + globalThresholds.disk != null ? globalThresholds.disk.toString() : "", + + temperature: globalThresholds.temperature != null, + usage_temperature: + globalThresholds.temperature != null + ? globalThresholds.temperature.toString() + : "", + })); + } catch (error) { + console.error("Failed to fetch global thresholds:", error); + } + }; + + fetchGlobalThresholds(); + }, [isCreate, user]); // Handlers const onSubmit = async (event) => { diff --git a/client/src/Pages/Settings/SettingsGlobalThresholds.jsx b/client/src/Pages/Settings/SettingsGlobalThresholds.jsx new file mode 100644 index 000000000..eca752688 --- /dev/null +++ b/client/src/Pages/Settings/SettingsGlobalThresholds.jsx @@ -0,0 +1,117 @@ +import Box from "@mui/material/Box"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import ConfigBox from "../../Components/ConfigBox"; +import TextInput from "../../Components/Inputs/TextInput"; + +import { useTheme } from "@emotion/react"; +import { PropTypes } from "prop-types"; +import { useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; + +const SettingsGlobalThresholds = ({ + isAdmin, + HEADING_SX, + settingsData, + setSettingsData, +}) => { + const { t } = useTranslation(); // For language translation + const theme = useTheme(); // MUI theme access + + // Local state for thresholds + const [thresholds, setThresholds] = useState({ + cpu: "", + memory: "", + disk: "", + temperature: "", + }); + + // Load existing thresholds from settingsData on first render or when settingsData changes + useEffect(() => { + if (settingsData?.settings?.globalThresholds) { + setThresholds({ + cpu: settingsData.settings.globalThresholds.cpu || "", + memory: settingsData.settings.globalThresholds.memory || "", + disk: settingsData.settings.globalThresholds.disk || "", + temperature: settingsData.settings.globalThresholds.temperature || "", + }); + } + }, [settingsData]); + + // Handles input change and updates state & parent data + const handleChange = (e, min, max) => { + const { name, value } = e.target; + + // Allow empty or numeric value within range + if ( + value === "" || + (/^\d+$/.test(value) && Number(value) >= min && Number(value) <= max) + ) { + const updatedThresholds = { + ...thresholds, + [name]: value, + }; + + setThresholds(updatedThresholds); + + // Update settingsData without enabling/disabling metrics + setSettingsData({ + ...settingsData, + settings: { + ...settingsData.settings, + globalThresholds: updatedThresholds, + }, + }); + } + }; + + // Only render this section for admins + if (!isAdmin) return null; + + return ( + + {/* Header and description */} + + + {t("settingsPage.globalThresholds.title", "Global Thresholds")} + + + {t( + "settingsPage.globalThresholds.description", + "Configure global CPU, Memory, Disk, and Temperature thresholds." + )} + + + + {/* Threshold inputs */} + + {[ + ["CPU Threshold (%)", "cpu", 1, 100], + ["Memory Threshold (%)", "memory", 1, 100], + ["Disk Threshold (%)", "disk", 1, 100], + ["Temperature Threshold (°C)", "temperature", 1, 150], + ].map(([label, name, min, max]) => ( + handleChange(e, min, max)} + /> + ))} + + + ); +}; + +// Prop types +SettingsGlobalThresholds.propTypes = { + isAdmin: PropTypes.bool, + HEADING_SX: PropTypes.object, + settingsData: PropTypes.object, + setSettingsData: PropTypes.func, +}; + +export default SettingsGlobalThresholds; diff --git a/client/src/Pages/Settings/index.jsx b/client/src/Pages/Settings/index.jsx index dae6bd680..ab529eb68 100644 --- a/client/src/Pages/Settings/index.jsx +++ b/client/src/Pages/Settings/index.jsx @@ -8,6 +8,7 @@ import SettingsPagespeed from "./SettingsPagespeed"; import SettingsDemoMonitors from "./SettingsDemoMonitors"; import SettingsAbout from "./SettingsAbout"; import SettingsEmail from "./SettingsEmail"; +import SettingsGlobalThresholds from "./SettingsGlobalThresholds"; import Button from "@mui/material/Button"; // Utils import { settingsValidation } from "../../Validation/validation"; @@ -48,6 +49,7 @@ const Settings = () => { setIsApiKeySet, setIsEmailPasswordSet, }); + const [addDemoMonitors, isAddingDemoMonitors] = useAddDemoMonitors(); const [isSaving, saveError, saveSettings] = useSaveSettings({ @@ -60,6 +62,7 @@ const Settings = () => { const [deleteAllMonitors, isDeletingMonitors] = useDeleteAllMonitors(); const [deleteMonitorStats, isDeletingMonitorStats] = useDeleteMonitorStats(); + // Setup const isAdmin = useIsAdmin(); const theme = useTheme(); @@ -149,6 +152,7 @@ const Settings = () => { error.details.forEach((err) => { newErrors[err.path[0]] = err.message; }); + setErrors(newErrors); } saveSettings(settingsData?.settings); @@ -190,6 +194,13 @@ const Settings = () => { handleChange={handleChange} errors={errors} /> + + { if (!dayjs(value).isValid()) { return helpers.error("any.invalid"); diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 78d0f2aed..5c69ecd81 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -837,6 +837,10 @@ "title": "Display timezone" }, "title": "Settings", + "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." + }, "uiSettings": { "description": "Switch between light and dark mode, or change user interface language.", "labelLanguage": "Language", diff --git a/server/src/db/models/AppSettings.js b/server/src/db/models/AppSettings.js index cd63118e9..be7c856a4 100755 --- a/server/src/db/models/AppSettings.js +++ b/server/src/db/models/AppSettings.js @@ -65,7 +65,13 @@ const AppSettingsSchema = mongoose.Schema( type: Number, default: 1, }, - }, + globalThresholds: { + cpu: { type: Number }, + memory: { type: Number}, + disk: { type: Number}, + temperature: { type: Number}, + } +}, { timestamps: true, } diff --git a/server/src/validation/joi.js b/server/src/validation/joi.js index 744b0ec74..51d8a94a2 100755 --- a/server/src/validation/joi.js +++ b/server/src/validation/joi.js @@ -416,8 +416,16 @@ const updateAppSettingsBodyValidation = joi.object({ systemEmailIgnoreTLS: joi.boolean(), systemEmailRequireTLS: joi.boolean(), systemEmailRejectUnauthorized: joi.boolean(), + + globalThresholds: joi.object({ + cpu: joi.number().min(1).max(100).allow("").optional(), + memory: joi.number().min(1).max(100).allow("").optional(), + disk: joi.number().min(1).max(100).allow("").optional(), + temperature: joi.number().min(1).max(150).allow("").optional(), + }).optional(), }); + //**************************************** // Status Page Validation //****************************************