diff --git a/Client/package-lock.json b/Client/package-lock.json
index 72bd51752..5b7c891c9 100644
--- a/Client/package-lock.json
+++ b/Client/package-lock.json
@@ -20,9 +20,12 @@
"@reduxjs/toolkit": "2.5.1",
"axios": "^1.7.4",
"dayjs": "1.11.13",
+ "immutability-helper": "^3.1.1",
"joi": "17.13.3",
"jwt-decode": "^4.0.0",
"react": "^18.2.0",
+ "react-dnd": "^16.0.1",
+ "react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.2.0",
"react-redux": "9.2.0",
"react-router": "^6.23.0",
@@ -1618,6 +1621,21 @@
"url": "https://opencollective.com/popperjs"
}
},
+ "node_modules/@react-dnd/asap": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz",
+ "integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A=="
+ },
+ "node_modules/@react-dnd/invariant": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz",
+ "integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw=="
+ },
+ "node_modules/@react-dnd/shallowequal": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz",
+ "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA=="
+ },
"node_modules/@react-spring/animated": {
"version": "9.7.5",
"resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.5.tgz",
@@ -3215,6 +3233,24 @@
"node": ">=0.4.0"
}
},
+ "node_modules/dnd-core": {
+ "version": "16.0.1",
+ "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz",
+ "integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==",
+ "dependencies": {
+ "@react-dnd/asap": "^5.0.1",
+ "@react-dnd/invariant": "^4.0.1",
+ "redux": "^4.2.0"
+ }
+ },
+ "node_modules/dnd-core/node_modules/redux": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
+ "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
+ "dependencies": {
+ "@babel/runtime": "^7.9.2"
+ }
+ },
"node_modules/doctrine": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
@@ -4242,6 +4278,11 @@
"url": "https://opencollective.com/immer"
}
},
+ "node_modules/immutability-helper": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/immutability-helper/-/immutability-helper-3.1.1.tgz",
+ "integrity": "sha512-Q0QaXjPjwIju/28TsugCHNEASwoCcJSyJV3uO1sOIQGI0jKgm9f41Lvz0DZj3n46cNCyAZTsEYoY4C2bVRUzyQ=="
+ },
"node_modules/import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -5394,6 +5435,43 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-dnd": {
+ "version": "16.0.1",
+ "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz",
+ "integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==",
+ "dependencies": {
+ "@react-dnd/invariant": "^4.0.1",
+ "@react-dnd/shallowequal": "^4.0.1",
+ "dnd-core": "^16.0.1",
+ "fast-deep-equal": "^3.1.3",
+ "hoist-non-react-statics": "^3.3.2"
+ },
+ "peerDependencies": {
+ "@types/hoist-non-react-statics": ">= 3.3.1",
+ "@types/node": ">= 12",
+ "@types/react": ">= 16",
+ "react": ">= 16.14"
+ },
+ "peerDependenciesMeta": {
+ "@types/hoist-non-react-statics": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-dnd-html5-backend": {
+ "version": "16.0.1",
+ "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz",
+ "integrity": "sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==",
+ "dependencies": {
+ "dnd-core": "^16.0.1"
+ }
+ },
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
diff --git a/Client/package.json b/Client/package.json
index 441b79824..d27740361 100644
--- a/Client/package.json
+++ b/Client/package.json
@@ -23,9 +23,12 @@
"@reduxjs/toolkit": "2.5.1",
"axios": "^1.7.4",
"dayjs": "1.11.13",
+ "immutability-helper": "^3.1.1",
"joi": "17.13.3",
"jwt-decode": "^4.0.0",
"react": "^18.2.0",
+ "react-dnd": "^16.0.1",
+ "react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.2.0",
"react-redux": "9.2.0",
"react-router": "^6.23.0",
diff --git a/Client/src/Components/Inputs/Checkbox/index.jsx b/Client/src/Components/Inputs/Checkbox/index.jsx
index b3a036d57..9aac8534a 100644
--- a/Client/src/Components/Inputs/Checkbox/index.jsx
+++ b/Client/src/Components/Inputs/Checkbox/index.jsx
@@ -21,6 +21,7 @@ import "./index.css";
* @param {string} [props.value] - Optional value associated with the checkbox
* @param {Function} [props.onChange] - Callback function triggered when checkbox state changes
* @param {boolean} [props.isDisabled] - Determines if the checkbox is disabled
+ * @param {boolean} [props.alignSelf] - Whether the checkbox label should be positioned on flex-start.
*
* @returns {React.ReactElement} Rendered Checkbox component
*
@@ -42,6 +43,7 @@ import "./index.css";
* isChecked={isAdvanced}
* isDisabled={!canModify}
* onChange={handleAdvancedToggle}
+ * alignSelf = {alignSelf}
* />
*/
const Checkbox = ({
@@ -53,10 +55,12 @@ const Checkbox = ({
value,
onChange,
isDisabled,
+ alignSelf
}) => {
/* TODO move sizes to theme */
const sizes = { small: "14px", medium: "16px", large: "18px" };
const theme = useTheme();
+ const override = alignSelf? { alignSelf: "flex-start" } : {}
return (
}
@@ -114,6 +118,7 @@ Checkbox.propTypes = {
value: PropTypes.string,
onChange: PropTypes.func,
isDisabled: PropTypes.bool,
+ alignSelf: PropTypes.bool,
};
export default Checkbox;
diff --git a/Client/src/Components/Inputs/Image/index.jsx b/Client/src/Components/Inputs/Image/index.jsx
index a955a8ad6..5c26d2314 100644
--- a/Client/src/Components/Inputs/Image/index.jsx
+++ b/Client/src/Components/Inputs/Image/index.jsx
@@ -11,11 +11,15 @@ import { checkImage } from "../../../Utils/fileUtils";
* @param {string} props.id - The unique identifier for the input field.
* @param {string} props.src - The URL of the image to display.
* @param {function} props.onChange - The function to handle file input change.
+ * @param {string} props.isRound - The shape of the image to display.
* @returns {JSX.Element} The rendered component.
*/
-const ImageField = ({ id, src, loading, onChange }) => {
+const ImageField = ({ id, src, loading, onChange, error, isRound = true }) => {
const theme = useTheme();
+ const error_border_style = error ? { borderColor: theme.palette.error.main } : {};
+
+ const roundShape = isRound ? { borderRadius: "50%" } : {};
const [isDragging, setIsDragging] = useState(false);
const handleDragEnter = () => {
@@ -28,7 +32,7 @@ const ImageField = ({ id, src, loading, onChange }) => {
return (
<>
{!checkImage(src) || loading ? (
- <>
+ <>
{
borderColor: theme.palette.primary.main,
backgroundColor: "hsl(215, 87%, 51%, 0.05)",
},
+ ...error_border_style
}}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
@@ -62,6 +67,7 @@ const ImageField = ({ id, src, loading, onChange }) => {
cursor: "pointer",
maxWidth: "500px",
minHeight: "175px",
+ zIndex: 1,
},
"& fieldset": {
padding: 0,
@@ -78,7 +84,7 @@ const ImageField = ({ id, src, loading, onChange }) => {
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
- zIndex: "-1",
+ zIndex: 0,
width: "100%",
}}
>
@@ -113,7 +119,7 @@ const ImageField = ({ id, src, loading, onChange }) => {
>
(maximum size: 3MB)
-
+
{
>
Supported formats: JPG, PNG
+ {error && (
+
+ {error}
+
+ )}
>
) : (
{
sx={{
width: "250px",
height: "250px",
- borderRadius: "50%",
overflow: "hidden",
backgroundImage: `url(${src})`,
backgroundSize: "cover",
+ ...roundShape,
}}
>
@@ -148,6 +167,7 @@ ImageField.propTypes = {
id: PropTypes.string.isRequired,
src: PropTypes.string,
onChange: PropTypes.func.isRequired,
+ isRound: PropTypes.bool,
};
export default ImageField;
diff --git a/Client/src/Components/Inputs/TextInput/Adornments/index.jsx b/Client/src/Components/Inputs/TextInput/Adornments/index.jsx
index 2b3717085..2dcc42b93 100644
--- a/Client/src/Components/Inputs/TextInput/Adornments/index.jsx
+++ b/Client/src/Components/Inputs/TextInput/Adornments/index.jsx
@@ -1,10 +1,12 @@
import { Stack, Typography, InputAdornment, IconButton } from "@mui/material";
import { useTheme } from "@mui/material/styles";
-import { useState } from "react";
import PropTypes from "prop-types";
import VisibilityOff from "@mui/icons-material/VisibilityOff";
import Visibility from "@mui/icons-material/Visibility";
-export const HttpAdornment = ({ https }) => {
+import ReorderRoundedIcon from '@mui/icons-material/ReorderRounded';
+import DeleteIcon from "../../../../assets/icons/trash-bin.svg?react";
+
+export const HttpAdornment = ({ https, prefix }) => {
const theme = useTheme();
return (
{
color={theme.palette.primary.contrastTextSecondary}
sx={{ lineHeight: 1, opacity: 0.8 }}
>
- {https ? "https" : "http"}://
+ {prefix !== undefined ? prefix : https ? "https://" : "http://"}
);
@@ -31,6 +33,7 @@ export const HttpAdornment = ({ https }) => {
HttpAdornment.propTypes = {
https: PropTypes.bool.isRequired,
+ prefix: PropTypes.string,
};
export const PasswordEndAdornment = ({ fieldType, setFieldType }) => {
@@ -63,3 +66,37 @@ PasswordEndAdornment.propTypes = {
fieldType: PropTypes.string,
setFieldType: PropTypes.func,
};
+
+export const ServerStartAdornment = () => {
+ return (
+
+
+
+ );
+};
+
+export const ServerEndAdornment = ({ id, removeItem }) => {
+ const theme = useTheme();
+ return (
+
+ removeItem(id)}
+ sx={{
+ color: theme.palette.border.dark,
+ padding: theme.spacing(1),
+ "&:focus-visible": {
+ outline: `2px solid ${theme.palette.primary.main}`,
+ outlineOffset: `2px`,
+ },
+ "& .MuiTouchRipple-root": {
+ pointerEvents: "none",
+ display: "none",
+ },
+ }}
+ >
+
+
+
+ );
+};
diff --git a/Client/src/Components/ProgressBars/index.jsx b/Client/src/Components/ProgressBars/index.jsx
index abb896844..42ae652c7 100644
--- a/Client/src/Components/ProgressBars/index.jsx
+++ b/Client/src/Components/ProgressBars/index.jsx
@@ -168,7 +168,7 @@ const ProgressUpload = ({ icon, label, size, progress = 0, onClick, error }) =>
ProgressUpload.propTypes = {
icon: PropTypes.element, // JSX element for the icon (optional)
- label: PropTypes.string.isRequired, // Label text for the progress item
+ label: PropTypes.string, // Label text for the progress item
size: PropTypes.string.isRequired, // Size information for the progress item
progress: PropTypes.number.isRequired, // Current progress value (0-100)
onClick: PropTypes.func.isRequired, // Function to handle click events on the remove button
diff --git a/Client/src/Utils/NetworkService.js b/Client/src/Utils/NetworkService.js
index fb85c37b5..176c6d093 100644
--- a/Client/src/Utils/NetworkService.js
+++ b/Client/src/Utils/NetworkService.js
@@ -861,6 +861,26 @@ class NetworkService {
"https://api.github.com/repos/bluewave-labs/bluewave-uptime/releases/latest"
);
}
+
+ async getStatusPageByUrl(config) {
+ const { url, authToken } = config;
+ return this.axiosInstance.get(`/status-page/${url}`, {
+ headers: {
+ Authorization: `Bearer ${authToken}`,
+ "Content-Type": "application/json",
+ },
+ });
+ }
+
+ async createStatusPage(config) {
+ const { url, authToken, data } = config;
+ return this.axiosInstance.post(`/status-page/${url}`, data, {
+ headers: {
+ Authorization: `Bearer ${authToken}`,
+ "Content-Type": "application/json",
+ },
+ });
+ }
}
export default NetworkService;
diff --git a/Client/src/Validation/error.js b/Client/src/Validation/error.js
index 017fa9a46..8f043f997 100644
--- a/Client/src/Validation/error.js
+++ b/Client/src/Validation/error.js
@@ -1,6 +1,16 @@
+/**
+ * Update errors if passed id matches the error.details[0].path, otherwise remove
+ * the error for the id
+ * @param {*} prev Previous errors *
+ * @param {*} id ID of the field whose error is to be either updated or removed
+ * @param {*} error the error object
+ * @returns the Update Errors with the specific field with id being either removed or updated
+ */
+
+
const buildErrors = (prev, id, error) => {
const updatedErrors = { ...prev };
- if (error) {
+ if (error && id == error.details[0].path) {
updatedErrors[id] = error.details[0].message ?? "Validation error";
} else {
delete updatedErrors[id];
@@ -32,7 +42,15 @@ const getTouchedFieldErrors = (validation, touchedErrors) => {
return newErrors;
};
-
+/**
+ *
+ * @param {*} form The form object of the submitted form data
+ * @param {*} validation The Joi validation rules
+ * @param {*} setErrors The function used to set the local errors
+ * @returns true if there is no error or false if there is error after validating the form
+ * the error will be reset to {} if returns false; otherwise the errors object will be set with
+ * the new value
+ */
const hasValidationErrors = (form, validation, setErrors) => {
const { error } = validation.validate(form, {
abortEarly: false,
@@ -48,6 +66,10 @@ const hasValidationErrors = (form, validation, setErrors) => {
"refreshTokenTTL",
"jwtTTL",
"notify-email-list",
+ "_id",
+ "__v",
+ "createdAt",
+ "updatedAt"
].includes(err.path[0])
) {
newErrors[err.path[0]] = err.message ?? "Validation error";
@@ -66,8 +88,6 @@ const hasValidationErrors = (form, validation, setErrors) => {
newErrors["usage_temperature"] = null;
}
});
-
- console.log("newErrors", newErrors);
if (Object.values(newErrors).some((v) => v)) {
setErrors(newErrors);
return true;
@@ -76,6 +96,7 @@ const hasValidationErrors = (form, validation, setErrors) => {
return false;
}
}
+ setErrors({});
return false;
};
export { buildErrors, hasValidationErrors, getTouchedFieldErrors };
diff --git a/Client/src/Validation/validation.js b/Client/src/Validation/validation.js
index ec9245cdb..5e14de16a 100644
--- a/Client/src/Validation/validation.js
+++ b/Client/src/Validation/validation.js
@@ -175,6 +175,39 @@ const imageValidation = joi.object({
}),
});
+const logoImageValidation = joi.object({
+ type: joi.string().valid("image/jpeg", "image/png").messages({
+ "any.only": "Invalid file format.",
+ "string.empty": "File type required.",
+ }),
+ size: joi
+ .number()
+ .max(800*800)
+ .messages({
+ "number.base": "File size must be a number.",
+ "number.max": "File size must be less than 640000 pixels.",
+ "number.empty": "File size required.",
+ }),
+});
+
+const publicPageGeneralSettingsValidation = joi.object({
+ publish: joi.bool(),
+ companyName: joi
+ .string()
+ .trim()
+ .messages({ "string.empty": "Company name is required." }),
+ url: joi.string().trim().messages({ "string.empty": "URL is required." }),
+ timezone: joi.string().trim().messages({ "string.empty": "Timezone is required." }),
+ color: joi.string().trim().messages({ "string.empty": "Color is required." }),
+
+ theme: joi.string(),
+ monitors: joi.array().min(1).items(joi.string().required()).messages({
+ "string.pattern.base": "Must be a valid monitor ID",
+ "array.base": "Monitors must be an array",
+ "array.empty": "At least one monitor is required",
+ "any.required": "Monitors are required",
+ }),
+});
const settingsValidation = joi.object({
ttl: joi.number().required().messages({
"string.empty": "TTL is required",