diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 000000000..d4b59627d
Binary files /dev/null and b/.DS_Store differ
diff --git a/Client/package-lock.json b/Client/package-lock.json
index e21e712ef..194c41ea6 100644
--- a/Client/package-lock.json
+++ b/Client/package-lock.json
@@ -11,10 +11,10 @@
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@fontsource/roboto": "^5.0.13",
- "@hello-pangea/dnd": "^17.0.0",
- "@mui/icons-material": "6.4.3",
- "@mui/lab": "6.0.0-beta.26",
- "@mui/material": "6.4.3",
+ "@hello-pangea/dnd": "^18.0.0",
+ "@mui/icons-material": "6.4.4",
+ "@mui/lab": "6.0.0-beta.27",
+ "@mui/material": "6.4.4",
"@mui/x-charts": "^7.5.1",
"@mui/x-data-grid": "7.26.0",
"@mui/x-date-pickers": "7.26.0",
@@ -450,6 +450,10 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz",
"integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==",
+ "node_modules/@babel/runtime": {
+ "version": "7.26.7",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.7.tgz",
+ "integrity": "sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==",
"license": "MIT",
"peer": true,
"dependencies": {
@@ -1161,6 +1165,21 @@
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
+ "node_modules/@hello-pangea/dnd": {
+ "version": "18.0.1",
+ "resolved": "https://registry.npmjs.org/@hello-pangea/dnd/-/dnd-18.0.1.tgz",
+ "integrity": "sha512-xojVWG8s/TGrKT1fC8K2tIWeejJYTAeJuj36zM//yEm/ZrnZUSFGS15BpO+jGZT1ybWvyXmeDJwPYb4dhWlbZQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@babel/runtime": "^7.26.7",
+ "css-box-model": "^1.2.1",
+ "raf-schd": "^4.0.3",
+ "react-redux": "^9.2.0",
+ "redux": "^5.0.1"
+ },
+ "peerDependencies": {
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
}
},
"node_modules/@babel/plugin-transform-json-strings": {
@@ -1349,6 +1368,10 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz",
"integrity": "sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==",
+ "node_modules/@mui/core-downloads-tracker": {
+ "version": "6.4.4",
+ "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.4.4.tgz",
+ "integrity": "sha512-r+J0EditrekkTtO2CnCBCOGpNaDYwJqz8lH4rj6o/anDcskZFJodBlG8aCJkS8DL/CF/9EHS+Gz53EbmYEnQbw==",
"license": "MIT",
"peer": true,
"dependencies": {
@@ -1365,6 +1388,10 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz",
"integrity": "sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==",
+ "node_modules/@mui/icons-material": {
+ "version": "6.4.4",
+ "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.4.4.tgz",
+ "integrity": "sha512-uF1chGaoFmYdRUomK6f8kgJfWosk9A3HXWiVD0vQm+2mE7f25eTQ1E8RRO11LXpnUBqu8Rbv/uGlpnjT/u1Ksg==",
"license": "MIT",
"peer": true,
"dependencies": {
@@ -1383,6 +1410,20 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz",
"integrity": "sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==",
+ "@mui/material": "^6.4.4",
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/lab": {
+ "version": "6.0.0-beta.27",
+ "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-6.0.0-beta.27.tgz",
+ "integrity": "sha512-weLxPsCs2wJKgWKf46shXHE+x7qlf5VxMK3P+4HsWasMakV/uTmxsoT7PG3QCvakGQ2TdpZtQLE2umJKC0mvKQ==",
"license": "MIT",
"peer": true,
"dependencies": {
@@ -1400,10 +1441,49 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz",
"integrity": "sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==",
+ "@emotion/react": "^11.5.0",
+ "@emotion/styled": "^11.3.0",
+ "@mui/material": "^6.4.4",
+ "@mui/material-pigment-css": "^6.4.3",
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/react": {
+ "optional": true
+ },
+ "@emotion/styled": {
+ "optional": true
+ },
+ "@mui/material-pigment-css": {
+ "optional": true
+ },
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/material": {
+ "version": "6.4.4",
+ "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.4.4.tgz",
+ "integrity": "sha512-ISVPrIsPQsxnwvS40C4u03AuNSPigFeS2+n1qpuEZ94hDsdMi19dQM2JcC9CHEhXecSIQjP1RTyY0mPiSpSrFQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
+ "@babel/runtime": "^7.26.0",
+ "@mui/core-downloads-tracker": "^6.4.4",
+ "@mui/system": "^6.4.3",
+ "@mui/types": "^7.2.21",
+ "@mui/utils": "^6.4.3",
+ "@popperjs/core": "^2.11.8",
+ "@types/react-transition-group": "^4.4.12",
+ "clsx": "^2.1.1",
+ "csstype": "^3.1.3",
+ "prop-types": "^15.8.1",
+ "react-is": "^19.0.0",
+ "react-transition-group": "^4.4.5"
},
"engines": {
"node": ">=6.9.0"
@@ -16881,6 +16961,10 @@
"version": "10.0.6",
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.6.tgz",
"integrity": "sha512-yYjp+omCDf9lhZcrZHKbSq7YMuK0zcYkDFTzfRFgTXkTFHZ1ToxwAonzA4JI5CxA91JpjFLmwEsZEgfYfOqI1A==",
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"dependencies": {
"clsx": "^2.1.0"
@@ -17517,6 +17601,10 @@
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
"integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==",
+ "node_modules/prettier": {
+ "version": "3.5.1",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.1.tgz",
+ "integrity": "sha512-hPpFQvHwL3Qv5AdRvBFMhnKo4tYxp0ReXiPn2bxkiohEX6mBeBwEpBSQTkD458RaaDKQMYSp4hX4UtfUTA5wDw==",
"dev": true,
"license": "MIT",
"dependencies": {
diff --git a/Client/package.json b/Client/package.json
index 3d9bbe755..d97fa9408 100644
--- a/Client/package.json
+++ b/Client/package.json
@@ -14,10 +14,10 @@
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@fontsource/roboto": "^5.0.13",
- "@hello-pangea/dnd": "^17.0.0",
- "@mui/icons-material": "6.4.3",
- "@mui/lab": "6.0.0-beta.26",
- "@mui/material": "6.4.3",
+ "@hello-pangea/dnd": "^18.0.0",
+ "@mui/icons-material": "6.4.4",
+ "@mui/lab": "6.0.0-beta.27",
+ "@mui/material": "6.4.4",
"@mui/x-charts": "^7.5.1",
"@mui/x-data-grid": "7.26.0",
"@mui/x-date-pickers": "7.26.0",
diff --git a/Client/src/Components/LanguageSelector.jsx b/Client/src/Components/LanguageSelector.jsx
index 7a809065f..ca8e9207c 100644
--- a/Client/src/Components/LanguageSelector.jsx
+++ b/Client/src/Components/LanguageSelector.jsx
@@ -1,17 +1,16 @@
-import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Box, MenuItem, Select, Stack } from "@mui/material";
import { useTheme } from "@emotion/react";
import "flag-icons/css/flag-icons.min.css";
+import { useSelector } from "react-redux";
const LanguageSelector = () => {
const { i18n } = useTranslation();
const theme = useTheme();
- const [language, setLanguage] = useState(i18n.language || "gb");
+ const { language } = useSelector((state) => state.ui);
const handleChange = (event) => {
const newLang = event.target.value;
- setLanguage(newLang);
i18n.changeLanguage(newLang);
};
diff --git a/Client/src/Components/Skeletons/FullPage/index.jsx b/Client/src/Components/Skeletons/FullPage/index.jsx
new file mode 100644
index 000000000..233fc3c06
--- /dev/null
+++ b/Client/src/Components/Skeletons/FullPage/index.jsx
@@ -0,0 +1,14 @@
+import { Stack, Skeleton } from "@mui/material";
+
+export const SkeletonLayout = () => {
+ return (
+
+
+
+ );
+};
+
+export default SkeletonLayout;
diff --git a/Client/src/Features/UI/uiSlice.js b/Client/src/Features/UI/uiSlice.js
index 4565c6c5d..5731307ec 100644
--- a/Client/src/Features/UI/uiSlice.js
+++ b/Client/src/Features/UI/uiSlice.js
@@ -23,6 +23,7 @@ const initialState = {
greeting: { index: 0, lastUpdate: null },
timezone: "America/Toronto",
distributedUptimeEnabled: false,
+ language: "gb",
};
const uiSlice = createSlice({
@@ -51,6 +52,9 @@ const uiSlice = createSlice({
setTimezone(state, action) {
state.timezone = action.payload.timezone;
},
+ setLanguage(state, action) {
+ state.language = action.payload;
+ },
},
});
@@ -62,4 +66,5 @@ export const {
setGreeting,
setTimezone,
setDistributedUptimeEnabled,
+ setLanguage,
} = uiSlice.actions;
diff --git a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js
index 7ea34b9ea..76f56a2ca 100644
--- a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js
+++ b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js
@@ -92,7 +92,7 @@ export const updateUptimeMonitor = createAsyncThunk(
const res = await networkService.updateMonitor({
authToken: authToken,
monitorId: monitor._id,
- updatedFields: updatedFields,
+ monitor,
});
return res.data;
} catch (error) {
diff --git a/Client/src/Pages/DistributedUptime/Create/Hooks/useCreateDistributedUptimeMonitor.jsx b/Client/src/Pages/DistributedUptime/Create/Hooks/useCreateDistributedUptimeMonitor.jsx
new file mode 100644
index 000000000..2351fb0e0
--- /dev/null
+++ b/Client/src/Pages/DistributedUptime/Create/Hooks/useCreateDistributedUptimeMonitor.jsx
@@ -0,0 +1,33 @@
+import { useState } from "react";
+import { networkService } from "../../../../main";
+import { useSelector } from "react-redux";
+import { createToast } from "../../../../Utils/toastUtils";
+
+const useCreateDistributedUptimeMonitor = ({ isCreate, monitorId }) => {
+ const { authToken, user } = useSelector((state) => state.auth);
+
+ const [isLoading, setIsLoading] = useState(false);
+ const [networkError, setNetworkError] = useState(false);
+ const createDistributedUptimeMonitor = async ({ form }) => {
+ setIsLoading(true);
+ try {
+ if (isCreate) {
+ await networkService.createMonitor({ authToken, monitor: form });
+ } else {
+ await networkService.updateMonitor({ authToken, monitor: form, monitorId });
+ }
+
+ return true;
+ } catch (error) {
+ setNetworkError(true);
+ createToast({ body: error?.response?.data?.msg ?? error.message });
+ return false;
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return [createDistributedUptimeMonitor, isLoading, networkError];
+};
+
+export { useCreateDistributedUptimeMonitor };
diff --git a/Client/src/Pages/DistributedUptime/Create/Hooks/useMonitorFetch.jsx b/Client/src/Pages/DistributedUptime/Create/Hooks/useMonitorFetch.jsx
new file mode 100644
index 000000000..cf7f3c377
--- /dev/null
+++ b/Client/src/Pages/DistributedUptime/Create/Hooks/useMonitorFetch.jsx
@@ -0,0 +1,32 @@
+import { useEffect, useState } from "react";
+import { networkService } from "../../../../main";
+import { createToast } from "../../../../Utils/toastUtils";
+
+export const useMonitorFetch = ({ authToken, monitorId, isCreate }) => {
+ const [networkError, setNetworkError] = useState(false);
+ const [isLoading, setIsLoading] = useState(true);
+ const [monitor, setMonitor] = useState(undefined);
+
+ useEffect(() => {
+ const fetchMonitors = async () => {
+ try {
+ if (isCreate) return;
+ const res = await networkService.getUptimeDetailsById({
+ authToken: authToken,
+ monitorId: monitorId,
+ normalize: true,
+ });
+ setMonitor(res?.data?.data ?? {});
+ } catch (error) {
+ setNetworkError(true);
+ createToast({ body: error.message });
+ } finally {
+ setIsLoading(false);
+ }
+ };
+ fetchMonitors();
+ }, [authToken, monitorId, isCreate]);
+ return [monitor, isLoading, networkError];
+};
+
+export default useMonitorFetch;
diff --git a/Client/src/Pages/DistributedUptime/Create/index.jsx b/Client/src/Pages/DistributedUptime/Create/index.jsx
index a0efca428..0afefb207 100644
--- a/Client/src/Pages/DistributedUptime/Create/index.jsx
+++ b/Client/src/Pages/DistributedUptime/Create/index.jsx
@@ -13,18 +13,15 @@ import { createToast } from "../../../Utils/toastUtils";
// Utility
import { useTheme } from "@emotion/react";
-import { useState } from "react";
+import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
-import { useSelector, useDispatch } from "react-redux";
+import { useSelector } from "react-redux";
import { monitorValidation } from "../../../Validation/validation";
-import { createUptimeMonitor } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
-import { useLocation } from "react-router-dom";
+import { useParams } from "react-router-dom";
+import { useCreateDistributedUptimeMonitor } from "./Hooks/useCreateDistributedUptimeMonitor";
+import { useMonitorFetch } from "./Hooks/useMonitorFetch";
// Constants
-const BREADCRUMBS = [
- { name: `distributed uptime`, path: "/distributed-uptime" },
- { name: "create", path: `/distributed-uptime/create` },
-];
const MS_PER_MINUTE = 60000;
const SELECT_VALUES = [
{ _id: 1, name: "1 minute" },
@@ -34,18 +31,30 @@ const SELECT_VALUES = [
{ _id: 5, name: "5 minutes" },
];
+const parseUrl = (url) => {
+ try {
+ return new URL(url);
+ } catch (error) {
+ return null;
+ }
+};
+
const CreateDistributedUptime = () => {
- const location = useLocation();
- const isCreate = location.pathname.startsWith("/distributed-uptime/create");
+ const { monitorId } = useParams();
+ const isCreate = typeof monitorId === "undefined";
+
+ const BREADCRUMBS = [
+ { name: `distributed uptime`, path: "/distributed-uptime" },
+ { name: isCreate ? "create" : "configure", path: `` },
+ ];
// Redux state
const { user, authToken } = useSelector((state) => state.auth);
- const isLoading = useSelector((state) => state.uptimeMonitors.isLoading);
// Local state
const [https, setHttps] = useState(true);
const [notifications, setNotifications] = useState([]);
- const [monitor, setMonitor] = useState({
+ const [form, setForm] = useState({
type: "distributed_http",
name: "",
url: "",
@@ -55,12 +64,37 @@ const CreateDistributedUptime = () => {
//utils
const theme = useTheme();
- const dispatch = useDispatch();
const navigate = useNavigate();
+ const [createDistributedUptimeMonitor, isLoading, networkError] =
+ useCreateDistributedUptimeMonitor({ isCreate, monitorId });
+
+ const [monitor, monitorIsLoading, monitorNetworkError] = useMonitorFetch({
+ authToken,
+ monitorId,
+ isCreate,
+ });
+
+ // Effect to set monitor to fetched monitor
+ useEffect(() => {
+ if (typeof monitor !== "undefined") {
+ const parsedUrl = parseUrl(monitor?.url);
+ const protocol = parsedUrl?.protocol?.replace(":", "") || "";
+ setHttps(protocol === "https");
+
+ const newForm = {
+ name: monitor.name,
+ interval: monitor.interval / MS_PER_MINUTE,
+ url: parsedUrl.host,
+ type: monitor.type,
+ };
+
+ setForm(newForm);
+ }
+ }, [monitor]);
// Handlers
- const handleCreateMonitor = async (event) => {
- const monitorToSubmit = { ...monitor };
+ const handleCreateMonitor = async () => {
+ const monitorToSubmit = { ...form };
// Prepend protocol to url
monitorToSubmit.url = `http${https ? "s" : ""}://` + monitorToSubmit.url;
@@ -68,7 +102,6 @@ const CreateDistributedUptime = () => {
const { error } = monitorValidation.validate(monitorToSubmit, {
abortEarly: false,
});
-
if (error) {
const newErrors = {};
error.details.forEach((err) => {
@@ -80,16 +113,14 @@ const CreateDistributedUptime = () => {
}
// Append needed fields
- monitorToSubmit.description = monitor.name;
- monitorToSubmit.interval = monitor.interval * MS_PER_MINUTE;
+ monitorToSubmit.description = form.name;
+ monitorToSubmit.interval = form.interval * MS_PER_MINUTE;
monitorToSubmit.teamId = user.teamId;
monitorToSubmit.userId = user._id;
monitorToSubmit.notifications = notifications;
- const action = await dispatch(
- createUptimeMonitor({ authToken, monitor: monitorToSubmit })
- );
- if (action.meta.requestStatus === "fulfilled") {
+ const success = await createDistributedUptimeMonitor({ form: monitorToSubmit });
+ if (success) {
createToast({ body: "Monitor created successfully!" });
navigate("/distributed-uptime");
} else {
@@ -98,9 +129,10 @@ const CreateDistributedUptime = () => {
};
const handleChange = (event) => {
- const { name, value } = event.target;
- setMonitor({
- ...monitor,
+ let { name, value } = event.target;
+
+ setForm({
+ ...form,
[name]: value,
});
const { error } = monitorValidation.validate(
@@ -178,7 +210,8 @@ const CreateDistributedUptime = () => {
label="URL to monitor"
https={https}
placeholder={"www.google.com"}
- value={monitor.url}
+ disabled={!isCreate}
+ value={form.url}
name="url"
onChange={handleChange}
error={errors["url"] ? true : false}
@@ -190,7 +223,7 @@ const CreateDistributedUptime = () => {
label="Display name"
isOptional={true}
placeholder={"Google"}
- value={monitor.name}
+ value={form.name}
name="name"
onChange={handleChange}
error={errors["name"] ? true : false}
@@ -217,8 +250,11 @@ const CreateDistributedUptime = () => {
checked={true}
onChange={handleChange}
/>
- {monitor.type === "http" || monitor.type === "distributed_http" ? (
-
+ {form.type === "http" || form.type === "distributed_http" ? (
+