From 031508ef9b1eaccf413238400e2d4205b742ce24 Mon Sep 17 00:00:00 2001 From: Shemy Gan Date: Mon, 30 Dec 2024 14:07:05 -0500 Subject: [PATCH] - Updated existing components for the status page usage - Add libraries for drag and drop --- Client/package-lock.json | 78 +++++++++++++++++++ Client/package.json | 3 + .../src/Components/Inputs/Checkbox/index.jsx | 7 +- Client/src/Components/Inputs/Image/index.jsx | 30 +++++-- .../Inputs/TextInput/Adornments/index.jsx | 43 +++++++++- Client/src/Components/ProgressBars/index.jsx | 2 +- Client/src/Utils/NetworkService.js | 20 +++++ Client/src/Validation/error.js | 29 ++++++- Client/src/Validation/validation.js | 33 ++++++++ 9 files changed, 231 insertions(+), 14 deletions(-) diff --git a/Client/package-lock.json b/Client/package-lock.json index c36b48c12..13e8c5dae 100644 --- a/Client/package-lock.json +++ b/Client/package-lock.json @@ -20,9 +20,12 @@ "@reduxjs/toolkit": "2.5.0", "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", @@ -1638,6 +1641,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", @@ -3235,6 +3253,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", @@ -4262,6 +4298,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", @@ -5414,6 +5455,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 75044ea9b..671c93b64 100644 --- a/Client/package.json +++ b/Client/package.json @@ -23,9 +23,12 @@ "@reduxjs/toolkit": "2.5.0", "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 054f43b92..b5b5b8981 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 26c48eb12..e7c41b9e9 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 686db183b..4d7b09d0f 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.text.secondary} 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 198f740aa..c8fd9b607 100644 --- a/Client/src/Components/ProgressBars/index.jsx +++ b/Client/src/Components/ProgressBars/index.jsx @@ -169,7 +169,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 71cbe9670..011c4faed 100644 --- a/Client/src/Utils/NetworkService.js +++ b/Client/src/Utils/NetworkService.js @@ -846,6 +846,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 ee4379368..d11c88c7f 100644 --- a/Client/src/Validation/validation.js +++ b/Client/src/Validation/validation.js @@ -135,6 +135,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",