From 136ed04b3ae0e188374d903e784a12956314d58f Mon Sep 17 00:00:00 2001 From: Shemy Gan Date: Tue, 26 Nov 2024 16:10:55 -0500 Subject: [PATCH 01/42] - Update status home page to connect to create status page - Create CreateStatus page -WIP --- Client/src/App.jsx | 5 ++ Client/src/Components/Sidebar/index.jsx | 3 +- .../TabPanels/Status/GeneralSettingsPanel.jsx | 66 +++++++++++++++ .../src/Pages/Status/CreateStatus/index.jsx | 82 +++++++++++++++++++ Client/src/Pages/Status/index.css | 0 Client/src/Pages/Status/index.jsx | 20 ++++- 6 files changed, 173 insertions(+), 3 deletions(-) create mode 100644 Client/src/Components/TabPanels/Status/GeneralSettingsPanel.jsx create mode 100644 Client/src/Pages/Status/CreateStatus/index.jsx delete mode 100644 Client/src/Pages/Status/index.css diff --git a/Client/src/App.jsx b/Client/src/App.jsx index 82c8203a8..efab418ec 100644 --- a/Client/src/App.jsx +++ b/Client/src/App.jsx @@ -41,6 +41,7 @@ import { logger } from "./Utils/Logger"; // Import the logger import { networkService } from "./main"; import { Infrastructure } from "./Pages/Infrastructure"; import InfrastructureDetails from "./Pages/Infrastructure/Details"; +import CreateStatus from "./Pages/Status/CreateStatus"; function App() { const AdminCheckedRegister = withAdminCheck(Register); const MonitorsWithAdminProp = withAdminProp(Monitors); @@ -148,6 +149,10 @@ function App() { path="status" element={} /> + } + /> } diff --git a/Client/src/Components/Sidebar/index.jsx b/Client/src/Components/Sidebar/index.jsx index 4c274409b..6b8321695 100644 --- a/Client/src/Components/Sidebar/index.jsx +++ b/Client/src/Components/Sidebar/index.jsx @@ -43,6 +43,7 @@ import DotsVertical from "../../assets/icons/dots-vertical.svg?react"; import ChangeLog from "../../assets/icons/changeLog.svg?react"; import Docs from "../../assets/icons/docs.svg?react"; import Folder from "../../assets/icons/folder.svg?react"; +import StatusPages from "../../assets/icons/status-pages.svg?react"; import "./index.css"; @@ -51,7 +52,7 @@ const menu = [ { name: "Pagespeed", path: "pagespeed", icon: }, { name: "Infrastructure", path: "infrastructure", icon: }, { name: "Incidents", path: "incidents", icon: }, - // { name: "Status pages", path: "status", icon: }, + { name: "Status pages", path: "status", icon: }, { name: "Maintenance", path: "maintenance", icon: }, // { name: "Integrations", path: "integrations", icon: }, { diff --git a/Client/src/Components/TabPanels/Status/GeneralSettingsPanel.jsx b/Client/src/Components/TabPanels/Status/GeneralSettingsPanel.jsx new file mode 100644 index 000000000..a14fba7ee --- /dev/null +++ b/Client/src/Components/TabPanels/Status/GeneralSettingsPanel.jsx @@ -0,0 +1,66 @@ +import { useState, useRef } from "react"; +import { Box, Stack, Typography } from "@mui/material"; +import { ConfigBox } from "../../../Pages/Monitors/styled"; +import Checkbox from "../../Inputs/Checkbox"; +import { useTheme } from "@emotion/react"; +import { useDispatch, useSelector } from "react-redux"; +import TabPanel from "@mui/lab/TabPanel"; + +const GeneralSettingsPanel = () => { + const theme = useTheme(); + const dispatch = useDispatch(); + + //redux state + const { user, authToken, isLoading } = useSelector((state) => state.auth); + + // Local state for form data, errors, and file handling + const [localData, setLocalData] = useState({ + }); + const [errors, setErrors] = useState({}); + const [file, setFile] = useState(); + + const handleSubmit = () => {}; + + const handleChange = () => {}; + + const handleBlur = () => {}; + return ( + + + + + + Access + + If your status page is ready, you can mark it as published. + + + + handleChange(e)} + onBlur={handleBlur} + /> + + + + ); +}; + +export default GeneralSettingsPanel; diff --git a/Client/src/Pages/Status/CreateStatus/index.jsx b/Client/src/Pages/Status/CreateStatus/index.jsx new file mode 100644 index 000000000..84512833d --- /dev/null +++ b/Client/src/Pages/Status/CreateStatus/index.jsx @@ -0,0 +1,82 @@ +import PropTypes from "prop-types"; +import { useNavigate } from "react-router"; +import { useSelector } from "react-redux"; +import { Box, Tab, useTheme } from "@mui/material"; +import TabContext from "@mui/lab/TabContext"; +import TabList from "@mui/lab/TabList"; +import GeneralSettingsPanel from "../../../Components/TabPanels/Status/GeneralSettingsPanel"; + +/** + * CreateStatus page renders a page with tabs for general settings and contents. + * @param {string} [props.open] - Specifies the initially open tab: 'general settings' or 'content'. + * @returns {JSX.Element} + */ + +const CreateStatus = ({ open = "general settings" }) => { + const theme = useTheme(); + const navigate = useNavigate(); + const tab = open; + const handleTabChange = (event, newTab) => { + navigate(`/status/${newTab}`); + }; + const { user } = useSelector((state) => state.auth); + + const requiredRoles = ["superadmin", "admin"]; + let tabList = ["General Settings", "Contents"]; + + return ( + + + + + {tabList.map((label, index) => ( + + ))} + + + + + + ); +}; + +CreateStatus.propTypes = { + open: PropTypes.oneOf(["general settings", "contents"]), +}; + +export default CreateStatus; diff --git a/Client/src/Pages/Status/index.css b/Client/src/Pages/Status/index.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/Client/src/Pages/Status/index.jsx b/Client/src/Pages/Status/index.jsx index 6827834ac..4ac66c3f5 100644 --- a/Client/src/Pages/Status/index.jsx +++ b/Client/src/Pages/Status/index.jsx @@ -1,10 +1,11 @@ -import { Box } from "@mui/material"; import { useTheme } from "@emotion/react"; import Fallback from "../../Components/Fallback"; +import { Box, Button, Stack } from "@mui/material"; +import { useNavigate } from "react-router-dom"; const Status = () => { const theme = useTheme(); - + const navigate = useNavigate(); return ( { "Build trust with your customers", ]} /> + + + ); }; From 4345d6988b3363a0b7bf3f94db0ee9eb36b83bae Mon Sep 17 00:00:00 2001 From: Shemy Gan Date: Wed, 27 Nov 2024 21:20:03 -0500 Subject: [PATCH 02/42] - Fix the checkbox position to be conditional on non string label --- Client/src/Components/Inputs/Checkbox/index.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Client/src/Components/Inputs/Checkbox/index.jsx b/Client/src/Components/Inputs/Checkbox/index.jsx index 2f89d86df..f19d9670e 100644 --- a/Client/src/Components/Inputs/Checkbox/index.jsx +++ b/Client/src/Components/Inputs/Checkbox/index.jsx @@ -40,6 +40,7 @@ const Checkbox = ({ /* TODO move sizes to theme */ const sizes = { small: "14px", medium: "16px", large: "18px" }; const theme = useTheme(); + const override = typeof label == "string" ? {} : { alignSelf: "flex-start" }; return ( } From 5adffd8c1ee2738d87b50e3539a7b1e3e0d18197 Mon Sep 17 00:00:00 2001 From: Shemy Gan Date: Wed, 27 Nov 2024 21:24:52 -0500 Subject: [PATCH 03/42] - WIP general setting panel --- .../TabPanels/Status/GeneralSettingsPanel.jsx | 162 ++++++++++++++---- .../src/Pages/Status/CreateStatus/index.jsx | 14 +- 2 files changed, 138 insertions(+), 38 deletions(-) diff --git a/Client/src/Components/TabPanels/Status/GeneralSettingsPanel.jsx b/Client/src/Components/TabPanels/Status/GeneralSettingsPanel.jsx index a14fba7ee..e510237cf 100644 --- a/Client/src/Components/TabPanels/Status/GeneralSettingsPanel.jsx +++ b/Client/src/Components/TabPanels/Status/GeneralSettingsPanel.jsx @@ -1,29 +1,46 @@ import { useState, useRef } from "react"; import { Box, Stack, Typography } from "@mui/material"; -import { ConfigBox } from "../../../Pages/Monitors/styled"; +import { ConfigBox } from "../../../Pages/Settings/styled"; import Checkbox from "../../Inputs/Checkbox"; import { useTheme } from "@emotion/react"; import { useDispatch, useSelector } from "react-redux"; import TabPanel from "@mui/lab/TabPanel"; +import Field from "../../Inputs/Field"; +import ImageField from "../../Inputs/Image"; +import { setMode, setTimezone } from "../../../Features/UI/uiSlice"; +import timezones from "../../../Utils/timezones.json"; +import Select from "../../Inputs/Select" const GeneralSettingsPanel = () => { const theme = useTheme(); const dispatch = useDispatch(); - - //redux state - const { user, authToken, isLoading } = useSelector((state) => state.auth); - + const [error, setError] = useState(""); // Local state for form data, errors, and file handling - const [localData, setLocalData] = useState({ - }); + const [localData, setLocalData] = useState({}); const [errors, setErrors] = useState({}); const [file, setFile] = useState(); + const { timezone } = useSelector((state) => state.ui); + const { mode } = useSelector((state) => state.ui); + + const handleChange = (event, appendedId) => { + event.preventDefault(); + const { value, id } = event.target; + let name = appendedId ?? idMap[id] ?? id; + setLocalData((prev) => ({ + ...prev, + [name]: value, + })); + } + const handleSubmit = () => {}; - const handleChange = () => {}; - const handleBlur = () => {}; + + const handlePicture = (event) => { + const pic = event.target.files[0]; + }; + return ( { }, }} > - - - + + + + + Access + + If your status page is ready, you can mark it as published. + + + - Access - - If your status page is ready, you can mark it as published. - + - - handleChange(e)} - onBlur={handleBlur} - /> - - - + + + + + + Basic Information + + Define company name and the subdomain that your status page points to. + + + + + + + + + + + + + Appearance + + Define the default look and feel of your public status page. + + + + + { + dispatch(setMode(e.target.value)); + }} + items={[ + { _id: "light", name: "Light" }, + { _id: "dark", name: "Dark" }, + ]} + /> + + + + ); }; diff --git a/Client/src/Pages/Status/CreateStatus/index.jsx b/Client/src/Pages/Status/CreateStatus/index.jsx index 84512833d..cf3733da9 100644 --- a/Client/src/Pages/Status/CreateStatus/index.jsx +++ b/Client/src/Pages/Status/CreateStatus/index.jsx @@ -1,10 +1,12 @@ import PropTypes from "prop-types"; import { useNavigate } from "react-router"; -import { useSelector } from "react-redux"; + import { Box, Tab, useTheme } from "@mui/material"; import TabContext from "@mui/lab/TabContext"; import TabList from "@mui/lab/TabList"; import GeneralSettingsPanel from "../../../Components/TabPanels/Status/GeneralSettingsPanel"; +import ImageField from "../../../Components/Inputs/Image"; +import Checkbox from "../../../Components/Inputs/Checkbox"; /** * CreateStatus page renders a page with tabs for general settings and contents. @@ -19,10 +21,14 @@ const CreateStatus = ({ open = "general settings" }) => { const handleTabChange = (event, newTab) => { navigate(`/status/${newTab}`); }; - const { user } = useSelector((state) => state.auth); - - const requiredRoles = ["superadmin", "admin"]; let tabList = ["General Settings", "Contents"]; + const handlePicture = (event) => {} + + const handleSubmit = () => {}; + + const handleChange = () => {}; + + const handleBlur = () => {}; return ( Date: Thu, 28 Nov 2024 10:43:45 -0500 Subject: [PATCH 04/42] - update to match BE --- .../TabPanels/Status/GeneralSettingsPanel.jsx | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/Client/src/Components/TabPanels/Status/GeneralSettingsPanel.jsx b/Client/src/Components/TabPanels/Status/GeneralSettingsPanel.jsx index e510237cf..49a7059eb 100644 --- a/Client/src/Components/TabPanels/Status/GeneralSettingsPanel.jsx +++ b/Client/src/Components/TabPanels/Status/GeneralSettingsPanel.jsx @@ -14,13 +14,19 @@ import Select from "../../Inputs/Select" const GeneralSettingsPanel = () => { const theme = useTheme(); const dispatch = useDispatch(); - const [error, setError] = useState(""); - // Local state for form data, errors, and file handling - const [localData, setLocalData] = useState({}); const [errors, setErrors] = useState({}); - const [file, setFile] = useState(); - const { timezone } = useSelector((state) => state.ui); - const { mode } = useSelector((state) => state.ui); + const [file, setFile] = useState(); + // Local state for form data, errors, and file handling + const [localData, setLocalData] = useState({ + companyName: "", + url: "", + timezone: "America/Toronto", + color: "#4169E1", + theme: "light", + //which fields matching below? + publish: false, + file: null + }); const handleChange = (event, appendedId) => { event.preventDefault(); @@ -72,8 +78,8 @@ const GeneralSettingsPanel = () => { @@ -94,7 +100,7 @@ const GeneralSettingsPanel = () => { id="company-name" type="text" label="Company name" - value={localData.name} + value={localData.companyName} /> { { - dispatch(setMode(e.target.value)); - }} + onChange={handleChange} + onBlur={handleBlur} items={[ { _id: "light", name: "Light" }, { _id: "dark", name: "Dark" }, diff --git a/Client/src/Validation/validation.js b/Client/src/Validation/validation.js index 52ed3aa10..ed934a309 100644 --- a/Client/src/Validation/validation.js +++ b/Client/src/Validation/validation.js @@ -120,6 +120,30 @@ 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({ + 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().trim().messages({ "string.empty": "Theme is required." }) +} + +) const settingsValidation = joi.object({ ttl: joi.number().required().messages({ "string.empty": "TTL is required", @@ -220,4 +244,6 @@ export { maintenanceWindowValidation, advancedSettingsValidation, infrastructureMonitorValidation, + publicPageGeneralSettingsValidation, + logoImageValidation }; From 5df7ddbe0f2b99a1d0d743f59018041cd41a18c3 Mon Sep 17 00:00:00 2001 From: Shemy Gan Date: Mon, 2 Dec 2024 16:04:28 -0500 Subject: [PATCH 07/42] - WIP Replace Field with TextInput , HttpAdornment seems not working --- .../TabPanels/Status/GeneralSettingsPanel.jsx | 76 +++++++++---------- 1 file changed, 37 insertions(+), 39 deletions(-) diff --git a/Client/src/Components/TabPanels/Status/GeneralSettingsPanel.jsx b/Client/src/Components/TabPanels/Status/GeneralSettingsPanel.jsx index 3a91f9e25..f2a37229f 100644 --- a/Client/src/Components/TabPanels/Status/GeneralSettingsPanel.jsx +++ b/Client/src/Components/TabPanels/Status/GeneralSettingsPanel.jsx @@ -3,11 +3,9 @@ import { Button, Box, Stack, Typography } from "@mui/material"; import { ConfigBox } from "../../../Pages/Settings/styled"; import Checkbox from "../../Inputs/Checkbox"; import { useTheme } from "@emotion/react"; -import { useDispatch, useSelector } from "react-redux"; import TabPanel from "@mui/lab/TabPanel"; -import Field from "../../Inputs/Field"; +import TextInput from "../../Inputs/TextInput"; import ImageField from "../../Inputs/Image"; -import { setMode, setTimezone } from "../../../Features/UI/uiSlice"; import timezones from "../../../Utils/timezones.json"; import Select from "../../Inputs/Select" import { logoImageValidation, publicPageGeneralSettingsValidation } from "../../../Validation/validation" @@ -15,10 +13,10 @@ import { buildErrors } from "../../../Validation/error"; import { formatBytes } from "../../../Utils/fileUtils"; import ProgressUpload from "../../ProgressBars"; import ImageIcon from "@mui/icons-material/Image"; +import { HttpAdornment } from "../../Inputs/TextInput/Adornments"; const GeneralSettingsPanel = () => { const theme = useTheme(); - const dispatch = useDispatch(); const [errors, setErrors] = useState({}); const [logo, setLogo] = useState(); const [progress, setProgress] = useState({ value: 0, isLoading: false }); @@ -57,7 +55,7 @@ const GeneralSettingsPanel = () => { setLocalData((prev) => ({ ...prev, logo: logo.src, - })); + })); }; const handleChange = (event) => { @@ -170,7 +168,7 @@ const GeneralSettingsPanel = () => { - { onBlur={handleBlur} error={errors["companyName"]} /> - } + //prefix={"http://uptimegenie.com/"} onChange={handleChange} onBlur={handleBlur} error={errors["url"]} /> - @@ -212,10 +209,9 @@ const GeneralSettingsPanel = () => { items={timezones} error={errors["display-timezone"]} /> - + )} + Date: Wed, 4 Dec 2024 13:33:42 -0500 Subject: [PATCH 08/42] - add lower case first letter string util func --- Client/src/Utils/stringUtils.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Client/src/Utils/stringUtils.js b/Client/src/Utils/stringUtils.js index 43ae5ed54..0bf7aaf11 100644 --- a/Client/src/Utils/stringUtils.js +++ b/Client/src/Utils/stringUtils.js @@ -15,3 +15,22 @@ export const capitalizeFirstLetter = (str) => { } return str.charAt(0).toUpperCase() + str.slice(1); }; + +/** + * Helper function to get first letter as a lower case string + * @param {string} str String whose first letter is to be lower cased + * @returns A string with first letter lower cased + */ + +export const toLowerCaseFirstLetter = (str) => { + if (str === null || str === undefined) { + return ""; + } + if (typeof str !== "string") { + throw new TypeError("Input must be a string"); + } + if (str.length === 0) { + return ""; + } + return str.charAt(0).toLowerCase() + str.slice(1); + }; \ No newline at end of file From cc2187d51ef6533aea6482b2f50dd23ed32e287c Mon Sep 17 00:00:00 2001 From: Shemy Gan Date: Wed, 4 Dec 2024 13:34:38 -0500 Subject: [PATCH 09/42] - add routes to status page nested pages --- Client/src/App.jsx | 23 ++++++++++++++++------- Client/src/Components/Sidebar/index.jsx | 9 ++++++++- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/Client/src/App.jsx b/Client/src/App.jsx index efab418ec..a9739b33a 100644 --- a/Client/src/App.jsx +++ b/Client/src/App.jsx @@ -139,20 +139,29 @@ function App() { path="infrastructure/:monitorId" element={} /> - } /> - } + path="status/general-settings" + element={ + + } /> } - /> + path="status/contents" + element={ + + } + /> + } diff --git a/Client/src/Components/Sidebar/index.jsx b/Client/src/Components/Sidebar/index.jsx index 6b8321695..ee7ef444e 100644 --- a/Client/src/Components/Sidebar/index.jsx +++ b/Client/src/Components/Sidebar/index.jsx @@ -52,7 +52,14 @@ const menu = [ { name: "Pagespeed", path: "pagespeed", icon: }, { name: "Infrastructure", path: "infrastructure", icon: }, { name: "Incidents", path: "incidents", icon: }, - { name: "Status pages", path: "status", icon: }, + { + name: "Status pages", + icon: , + nested: [ + { name: "General Settings", path: "status/general-settings", icon: }, + { name: "Contents", path: "status/contents", icon: }, + ], + }, { name: "Maintenance", path: "maintenance", icon: }, // { name: "Integrations", path: "integrations", icon: }, { From b275bdc8efe3b007b84520cd195af8e13169273f Mon Sep 17 00:00:00 2001 From: Shemy Gan Date: Wed, 4 Dec 2024 13:41:37 -0500 Subject: [PATCH 10/42] - Link general settings and contens to status page in navigation --- Client/src/Components/ProgressBars/index.jsx | 2 +- .../TabPanels/Status/GeneralSettingsPanel.jsx | 104 +++++++++--------- .../src/Pages/Status/CreateStatus/index.jsx | 29 +++-- 3 files changed, 65 insertions(+), 70 deletions(-) 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/Components/TabPanels/Status/GeneralSettingsPanel.jsx b/Client/src/Components/TabPanels/Status/GeneralSettingsPanel.jsx index f2a37229f..54caff137 100644 --- a/Client/src/Components/TabPanels/Status/GeneralSettingsPanel.jsx +++ b/Client/src/Components/TabPanels/Status/GeneralSettingsPanel.jsx @@ -7,13 +7,17 @@ import TabPanel from "@mui/lab/TabPanel"; import TextInput from "../../Inputs/TextInput"; import ImageField from "../../Inputs/Image"; import timezones from "../../../Utils/timezones.json"; -import Select from "../../Inputs/Select" -import { logoImageValidation, publicPageGeneralSettingsValidation } from "../../../Validation/validation" +import Select from "../../Inputs/Select"; +import { + logoImageValidation, + publicPageGeneralSettingsValidation, +} from "../../../Validation/validation"; import { buildErrors } from "../../../Validation/error"; import { formatBytes } from "../../../Utils/fileUtils"; import ProgressUpload from "../../ProgressBars"; import ImageIcon from "@mui/icons-material/Image"; import { HttpAdornment } from "../../Inputs/TextInput/Adornments"; +import { hasValidationErrors } from "../../../Validation/error"; const GeneralSettingsPanel = () => { const theme = useTheme(); @@ -32,7 +36,6 @@ const GeneralSettingsPanel = () => { publish: false, logo: null, }); - // Clears specific error from errors state const clearError = (err) => { setErrors((prev) => { @@ -40,22 +43,17 @@ const GeneralSettingsPanel = () => { if (updatedErrors[err]) delete updatedErrors[err]; return updatedErrors; }); - }; + }; const removeLogo = () => { errors["logo"] && clearError("logo"); setLogo({}); - // interrupt interval if image upload is canceled prior to completing the process - clearInterval(intervalRef.current); - setProgress({ value: 0, isLoading: false }); - }; - - // Updates profile image displayed on UI - const handleUpdateLogo = () => { - setProgress({ value: 0, isLoading: false }); setLocalData((prev) => ({ ...prev, - logo: logo.src, - })); + logo: logo?.src, + })); + // interrupt interval if image upload is canceled prior to completing the process + clearInterval(intervalRef.current); + setProgress({ value: 0, isLoading: false }); }; const handleChange = (event) => { @@ -67,7 +65,21 @@ const GeneralSettingsPanel = () => { })); }; - const handleSubmit = () => {}; + const handleSubmit = () => { + //validate rest of the form + delete localData.logo; + delete localData.publish; + if (hasValidationErrors(localData, publicPageGeneralSettingsValidation, setErrors)) { + return; + } + //validate image field + let error = validateField( + { type: logo?.type ?? null, size: logo?.size ?? null }, + logoImageValidation + ); + if (error) return; + localData.logo = { ...logo, size: formatBytes(logo?.size) }; + }; const handleBlur = (event) => { event.preventDefault(); @@ -92,11 +104,11 @@ const GeneralSettingsPanel = () => { else delete prevErrors[name]; return prevErrors; }); - if (error) return true; + if (error) return true; }; const handleLogo = (event) => { - const pic = event.target.files[0]; + const pic = event.target?.files?.[0]; let error = validateField({ type: pic?.type, size: pic?.size }, logoImageValidation); if (error) return; @@ -104,9 +116,9 @@ const GeneralSettingsPanel = () => { setLogo({ src: URL.createObjectURL(pic), name: pic.name, - size: formatBytes(pic.size), + type: pic.type, + size: pic.size, }); - intervalRef.current = setInterval(() => { const buffer = 12; setProgress((prev) => { @@ -121,7 +133,7 @@ const GeneralSettingsPanel = () => { return ( { { - + { value={localData.companyName} onChange={handleChange} onBlur={handleBlur} - error={errors["companyName"]} + helperText={errors["companyName"]} + error={errors["companyName"] ? true : false} /> { //prefix={"http://uptimegenie.com/"} onChange={handleChange} onBlur={handleBlur} - error={errors["url"]} + helperText={errors["url"]} + error={errors["url"] ? true : false} /> @@ -219,7 +232,7 @@ const GeneralSettingsPanel = () => { } label={logo?.name} - size={logo?.size} + size={formatBytes(logo?.size)} progress={progress.value} onClick={removeLogo} error={errors["logo"]} @@ -227,35 +240,6 @@ const GeneralSettingsPanel = () => { ) : ( "" )} - {logo?.src && ( - - - - - )} { /> + + + ); diff --git a/Client/src/Pages/Status/CreateStatus/index.jsx b/Client/src/Pages/Status/CreateStatus/index.jsx index cf3733da9..127bbb3b8 100644 --- a/Client/src/Pages/Status/CreateStatus/index.jsx +++ b/Client/src/Pages/Status/CreateStatus/index.jsx @@ -5,8 +5,10 @@ import { Box, Tab, useTheme } from "@mui/material"; import TabContext from "@mui/lab/TabContext"; import TabList from "@mui/lab/TabList"; import GeneralSettingsPanel from "../../../Components/TabPanels/Status/GeneralSettingsPanel"; -import ImageField from "../../../Components/Inputs/Image"; -import Checkbox from "../../../Components/Inputs/Checkbox"; +import { + capitalizeFirstLetter, + toLowerCaseFirstLetter, +} from "../../../Utils/stringUtils"; /** * CreateStatus page renders a page with tabs for general settings and contents. @@ -14,22 +16,19 @@ import Checkbox from "../../../Components/Inputs/Checkbox"; * @returns {JSX.Element} */ -const CreateStatus = ({ open = "general settings" }) => { +const CreateStatus = ({ open = "general-settings" }) => { const theme = useTheme(); const navigate = useNavigate(); - const tab = open; + const tab = open + .split("-") + .map((a) => capitalizeFirstLetter(a)) + .join(" "); const handleTabChange = (event, newTab) => { - navigate(`/status/${newTab}`); + let toNavString = newTab.split(" ").map((a) => toLowerCaseFirstLetter(a)); + toNavString = toNavString.join("-"); + navigate(`/status/${toNavString}`); }; let tabList = ["General Settings", "Contents"]; - const handlePicture = (event) => {} - - const handleSubmit = () => {}; - - const handleChange = () => {}; - - const handleBlur = () => {}; - return ( { { }; CreateStatus.propTypes = { - open: PropTypes.oneOf(["general settings", "contents"]), + open: PropTypes.oneOf(["general-settings", "contents"]), }; export default CreateStatus; From 35a981abb079afe745efdbd0c17c40249c8c0ba9 Mon Sep 17 00:00:00 2001 From: Shemy Gan Date: Wed, 4 Dec 2024 15:10:13 -0500 Subject: [PATCH 11/42] - Update icons etc --- Client/src/Components/Sidebar/index.jsx | 4 ++-- Client/src/Pages/Status/CreateStatus/index.jsx | 16 +++++++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/Client/src/Components/Sidebar/index.jsx b/Client/src/Components/Sidebar/index.jsx index ee7ef444e..3e506527a 100644 --- a/Client/src/Components/Sidebar/index.jsx +++ b/Client/src/Components/Sidebar/index.jsx @@ -56,8 +56,8 @@ const menu = [ name: "Status pages", icon: , nested: [ - { name: "General Settings", path: "status/general-settings", icon: }, - { name: "Contents", path: "status/contents", icon: }, + { name: "General Settings", path: "status/general-settings", icon: }, + { name: "Contents", path: "status/contents", icon: }, ], }, { name: "Maintenance", path: "maintenance", icon: }, diff --git a/Client/src/Pages/Status/CreateStatus/index.jsx b/Client/src/Pages/Status/CreateStatus/index.jsx index 127bbb3b8..b1f5a8c0b 100644 --- a/Client/src/Pages/Status/CreateStatus/index.jsx +++ b/Client/src/Pages/Status/CreateStatus/index.jsx @@ -1,6 +1,6 @@ import PropTypes from "prop-types"; import { useNavigate } from "react-router"; - +import { useEffect, useState } from "react"; import { Box, Tab, useTheme } from "@mui/material"; import TabContext from "@mui/lab/TabContext"; import TabList from "@mui/lab/TabList"; @@ -9,6 +9,7 @@ import { capitalizeFirstLetter, toLowerCaseFirstLetter, } from "../../../Utils/stringUtils"; +import ContentPanel from "../../../Components/TabPanels/Status/ContentPanel"; /** * CreateStatus page renders a page with tabs for general settings and contents. @@ -19,16 +20,25 @@ import { const CreateStatus = ({ open = "general-settings" }) => { const theme = useTheme(); const navigate = useNavigate(); + let tabList = ["General Settings", "Contents"]; const tab = open .split("-") .map((a) => capitalizeFirstLetter(a)) .join(" "); + + const [tabIdx, setTabIdx] = useState(tabList.indexOf(tab)); + const handleTabChange = (event, newTab) => { let toNavString = newTab.split(" ").map((a) => toLowerCaseFirstLetter(a)); + setTabIdx(tabList.indexOf(newTab)); toNavString = toNavString.join("-"); navigate(`/status/${toNavString}`); }; - let tabList = ["General Settings", "Contents"]; + + useEffect(() => { + setTabIdx(tabList.indexOf(tab)); + }, [tab]); + return ( { ))} - + {tabIdx == 0 ? : } ); From b651369dc538e2d7d4ed87ff3aaddf42e70ef0a0 Mon Sep 17 00:00:00 2001 From: Shemy Gan Date: Thu, 5 Dec 2024 10:34:41 -0500 Subject: [PATCH 12/42] - WIP Server list tab --- .../Inputs/TextInput/Adornments/index.jsx | 38 +++++ .../TabPanels/Status/ContentPanel.jsx | 133 ++++++++++++++++++ .../TabPanels/Status/Server/index.jsx | 21 +++ 3 files changed, 192 insertions(+) create mode 100644 Client/src/Components/TabPanels/Status/ContentPanel.jsx create mode 100644 Client/src/Components/TabPanels/Status/Server/index.jsx diff --git a/Client/src/Components/Inputs/TextInput/Adornments/index.jsx b/Client/src/Components/Inputs/TextInput/Adornments/index.jsx index 686db183b..a89500bda 100644 --- a/Client/src/Components/Inputs/TextInput/Adornments/index.jsx +++ b/Client/src/Components/Inputs/TextInput/Adornments/index.jsx @@ -4,6 +4,9 @@ import { useState } from "react"; import PropTypes from "prop-types"; import VisibilityOff from "@mui/icons-material/VisibilityOff"; import Visibility from "@mui/icons-material/Visibility"; +import Docs from "../../../../assets/icons/docs.svg?react"; +import DeleteTwoToneIcon from '@mui/icons-material/DeleteTwoTone'; + export const HttpAdornment = ({ https }) => { const theme = useTheme(); return ( @@ -63,3 +66,38 @@ 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/TabPanels/Status/ContentPanel.jsx b/Client/src/Components/TabPanels/Status/ContentPanel.jsx new file mode 100644 index 000000000..73fcc968f --- /dev/null +++ b/Client/src/Components/TabPanels/Status/ContentPanel.jsx @@ -0,0 +1,133 @@ +import { useState } from "react"; +import { Button, Box, Stack, Typography } from "@mui/material"; +import { ConfigBox } from "../../../Pages/Settings/styled"; +import { useTheme } from "@emotion/react"; +import TabPanel from "@mui/lab/TabPanel"; +import { + publicPageGeneralSettingsValidation, +} from "../../../Validation/validation"; +import { buildErrors } from "../../../Validation/error"; +import { hasValidationErrors } from "../../../Validation/error"; +import Server from "./Server" +const ContentPanel = () => { + const theme = useTheme(); + const [errors, setErrors] = useState({}); + // Local state for form data, errors, and file handling + const [localData, setLocalData] = useState({ + monitors: [] + }); + // Clears specific error from errors state + const clearError = (err) => { + setErrors((prev) => { + const updatedErrors = { ...prev }; + if (updatedErrors[err]) delete updatedErrors[err]; + return updatedErrors; + }); + }; + + const handleChange = (event) => { + event.preventDefault(); + const { value, id } = event.target; + setLocalData((prev) => ({ + ...prev, + [id]: value, + })); + }; + + const handleSubmit = () => { + if (hasValidationErrors(localData, publicPageGeneralSettingsValidation, setErrors)) { + return; + } + }; + + const handleBlur = (event) => { + event.preventDefault(); + const { value, id } = event.target; + const { error } = publicPageGeneralSettingsValidation.validate( + { [id]: value }, + { + abortEarly: false, + } + ); + + setErrors((prev) => { + return buildErrors(prev, id, error); + }); + }; + + return ( + + + + + + Status page servers + + You can add any number of servers that you monitor to your status page. + You can also reorder them for the best viewing experience. + + + + + + + {" "} + Servers list{" "} + + + + + + + + + + + + + ); +}; + +export default ContentPanel; diff --git a/Client/src/Components/TabPanels/Status/Server/index.jsx b/Client/src/Components/TabPanels/Status/Server/index.jsx new file mode 100644 index 000000000..2c6d6fc75 --- /dev/null +++ b/Client/src/Components/TabPanels/Status/Server/index.jsx @@ -0,0 +1,21 @@ +import TextInput from "../../../Inputs/TextInput"; +import { ServerStartAdornment, ServerEndAdornment } from "../../../Inputs/TextInput/Adornments"; + + +const Server = ({ id, removeItem }) => { + return ( + } + endAdornment={ + + } + id={id} + > + ); +}; + +export default Server; \ No newline at end of file From cbec5fd25c79b8ed2557a411008d964c9a2c4f1c Mon Sep 17 00:00:00 2001 From: Shemy Gan Date: Fri, 6 Dec 2024 10:28:42 -0500 Subject: [PATCH 13/42] - Change "Next" to "Save" --- Client/src/Components/TabPanels/Status/ContentPanel.jsx | 2 +- Client/src/Components/TabPanels/Status/GeneralSettingsPanel.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Client/src/Components/TabPanels/Status/ContentPanel.jsx b/Client/src/Components/TabPanels/Status/ContentPanel.jsx index 73fcc968f..4209b3fdc 100644 --- a/Client/src/Components/TabPanels/Status/ContentPanel.jsx +++ b/Client/src/Components/TabPanels/Status/ContentPanel.jsx @@ -122,7 +122,7 @@ const ContentPanel = () => { color="primary" onClick={handleSubmit} > - Next + Save diff --git a/Client/src/Components/TabPanels/Status/GeneralSettingsPanel.jsx b/Client/src/Components/TabPanels/Status/GeneralSettingsPanel.jsx index 54caff137..a8954b0f9 100644 --- a/Client/src/Components/TabPanels/Status/GeneralSettingsPanel.jsx +++ b/Client/src/Components/TabPanels/Status/GeneralSettingsPanel.jsx @@ -270,7 +270,7 @@ const GeneralSettingsPanel = () => { color="primary" onClick={handleSubmit} > - Next + Save From 2825ff4019d1cbd637f814358ee95a780ce55b7d Mon Sep 17 00:00:00 2001 From: Shemy Gan Date: Thu, 5 Dec 2024 15:57:45 -0500 Subject: [PATCH 14/42] - WIP contents tab adding servers list --- .../Inputs/TextInput/Adornments/index.jsx | 5 +- .../TabPanels/Status/ContentPanel.jsx | 69 ++++++++++++++----- .../TabPanels/Status/Server/index.jsx | 3 +- 3 files changed, 55 insertions(+), 22 deletions(-) diff --git a/Client/src/Components/Inputs/TextInput/Adornments/index.jsx b/Client/src/Components/Inputs/TextInput/Adornments/index.jsx index a89500bda..1faf784c9 100644 --- a/Client/src/Components/Inputs/TextInput/Adornments/index.jsx +++ b/Client/src/Components/Inputs/TextInput/Adornments/index.jsx @@ -1,11 +1,10 @@ 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"; import Docs from "../../../../assets/icons/docs.svg?react"; -import DeleteTwoToneIcon from '@mui/icons-material/DeleteTwoTone'; +import DeleteIcon from "../../../../assets/icons/trash-bin.svg?react"; export const HttpAdornment = ({ https }) => { const theme = useTheme(); @@ -96,7 +95,7 @@ export const ServerEndAdornment = ({ id, removeItem }) => { }, }} > - + ); diff --git a/Client/src/Components/TabPanels/Status/ContentPanel.jsx b/Client/src/Components/TabPanels/Status/ContentPanel.jsx index 4209b3fdc..b0bc80610 100644 --- a/Client/src/Components/TabPanels/Status/ContentPanel.jsx +++ b/Client/src/Components/TabPanels/Status/ContentPanel.jsx @@ -40,6 +40,17 @@ const ContentPanel = () => { } }; + const handleAddNew = () => { + let arr = localData.monitors; + arr.push({ id: "" + Math.random() }); + setLocalData({ ...localData, monitors: arr }); + } + + const removeItem = (id) =>{ + const currentMonitors = localData.monitors.filter(m => m.id !=id) + setLocalData({...localData, monitors: currentMonitors}) + } + const handleBlur = (event) => { event.preventDefault(); const { value, id } = event.target; @@ -82,9 +93,9 @@ const ContentPanel = () => { - { "&:hover": { borderColor: theme.palette.primary.main, backgroundColor: "hsl(215, 87%, 51%, 0.05)", - } - }}> - - + + - {" "} - Servers list{" "} - - - - + + {" "} + Servers list{" "} + + + + {localData.monitors.length > 0 && ( + + {localData.monitors.map((m, idx) => ( + + ))} + + )} + diff --git a/Client/src/Components/TabPanels/Status/Server/index.jsx b/Client/src/Components/TabPanels/Status/Server/index.jsx index 2c6d6fc75..4bc5155b5 100644 --- a/Client/src/Components/TabPanels/Status/Server/index.jsx +++ b/Client/src/Components/TabPanels/Status/Server/index.jsx @@ -2,7 +2,7 @@ import TextInput from "../../../Inputs/TextInput"; import { ServerStartAdornment, ServerEndAdornment } from "../../../Inputs/TextInput/Adornments"; -const Server = ({ id, removeItem }) => { +const Server = ({ id, removeItem, value }) => { return ( { /> } id={id} + value= {value} > ); }; From 03ce805e9fa13298dbc10b355101282afbb1cfe7 Mon Sep 17 00:00:00 2001 From: Shemy Gan Date: Mon, 9 Dec 2024 11:21:02 -0500 Subject: [PATCH 15/42] - Add React DND related libraries --- Client/package-lock.json | 79 +++++++++++++++++++++++++++++++++++++++- Client/package.json | 3 ++ Client/src/main.jsx | 6 ++- 3 files changed, 86 insertions(+), 2 deletions(-) diff --git a/Client/package-lock.json b/Client/package-lock.json index b24ef8cad..314d4991d 100644 --- a/Client/package-lock.json +++ b/Client/package-lock.json @@ -20,9 +20,12 @@ "@reduxjs/toolkit": "2.4.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.1.2", "react-router": "^6.23.0", @@ -1606,6 +1609,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", @@ -3174,6 +3192,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", @@ -3730,7 +3766,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-equals": { @@ -4177,6 +4212,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", @@ -5313,6 +5353,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 1f4480fa2..4582a7222 100644 --- a/Client/package.json +++ b/Client/package.json @@ -23,9 +23,12 @@ "@reduxjs/toolkit": "2.4.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.1.2", "react-router": "^6.23.0", diff --git a/Client/src/main.jsx b/Client/src/main.jsx index c07cad40b..d6226c393 100644 --- a/Client/src/main.jsx +++ b/Client/src/main.jsx @@ -6,6 +6,8 @@ import { Provider } from "react-redux"; import { persistor, store } from "./store"; import { PersistGate } from "redux-persist/integration/react"; import NetworkServiceProvider from "./Utils/NetworkServiceProvider.jsx"; +import { DndProvider } from 'react-dnd' +import { HTML5Backend } from 'react-dnd-html5-backend' import { networkService } from "./Utils/NetworkService"; export { networkService }; @@ -17,7 +19,9 @@ ReactDOM.createRoot(document.getElementById("root")).render( > - + + + From b1f056809f4dee83d103fbab461137172fd300fc Mon Sep 17 00:00:00 2001 From: Shemy Gan Date: Mon, 9 Dec 2024 11:33:30 -0500 Subject: [PATCH 16/42] - Created Card type - Implemented drag and drop for the server list section --- .../TabPanels/Status/Card/ItemTypes.js | 4 + .../Status/{ => Card}/Server/index.jsx | 4 +- .../TabPanels/Status/Card/index.jsx | 90 +++++++++++++++++++ .../TabPanels/Status/ContentPanel.jsx | 65 +++++++++----- 4 files changed, 137 insertions(+), 26 deletions(-) create mode 100644 Client/src/Components/TabPanels/Status/Card/ItemTypes.js rename Client/src/Components/TabPanels/Status/{ => Card}/Server/index.jsx (80%) create mode 100644 Client/src/Components/TabPanels/Status/Card/index.jsx diff --git a/Client/src/Components/TabPanels/Status/Card/ItemTypes.js b/Client/src/Components/TabPanels/Status/Card/ItemTypes.js new file mode 100644 index 000000000..a7cde4907 --- /dev/null +++ b/Client/src/Components/TabPanels/Status/Card/ItemTypes.js @@ -0,0 +1,4 @@ +export default { + CARD: 'card', + } + \ No newline at end of file diff --git a/Client/src/Components/TabPanels/Status/Server/index.jsx b/Client/src/Components/TabPanels/Status/Card/Server/index.jsx similarity index 80% rename from Client/src/Components/TabPanels/Status/Server/index.jsx rename to Client/src/Components/TabPanels/Status/Card/Server/index.jsx index 4bc5155b5..ee2934d7f 100644 --- a/Client/src/Components/TabPanels/Status/Server/index.jsx +++ b/Client/src/Components/TabPanels/Status/Card/Server/index.jsx @@ -1,5 +1,5 @@ -import TextInput from "../../../Inputs/TextInput"; -import { ServerStartAdornment, ServerEndAdornment } from "../../../Inputs/TextInput/Adornments"; +import TextInput from "../../../../Inputs/TextInput"; +import { ServerStartAdornment, ServerEndAdornment } from "../../../../Inputs/TextInput/Adornments"; const Server = ({ id, removeItem, value }) => { diff --git a/Client/src/Components/TabPanels/Status/Card/index.jsx b/Client/src/Components/TabPanels/Status/Card/index.jsx new file mode 100644 index 000000000..6a89618ff --- /dev/null +++ b/Client/src/Components/TabPanels/Status/Card/index.jsx @@ -0,0 +1,90 @@ +import React, { useRef } from "react"; +import { useDrag, useDrop } from "react-dnd"; +import ItemTypes from "./ItemTypes"; +import Card from "@mui/material/Card"; +import CardActionArea from "@mui/material/CardActionArea"; +import CardContent from "@mui/material/CardContent"; +import Server from "./Server" + +const CustomCard = React.forwardRef((props, ref) => ( + + + + + + + +)); + +const MyCard = ({ id, index, moveCard, removeCard, value }) => { + const ref = useRef(null); + const [, drop] = useDrop({ + accept: ItemTypes.CARD, + hover(item, monitor) { + if (!ref.current) { + return; + } + const dragIndex = item.index; + const hoverIndex = index; + + // Don't replace items with themselves + if (dragIndex === hoverIndex) { + return; + } + + // Determine rectangle on screen + const hoverBoundingRect = ref.current.getBoundingClientRect(); + // Get vertical middle + const hoverMiddleY = + (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2; + // Determine mouse position + const clientOffset = monitor.getClientOffset(); + // Get pixels to the top + const hoverClientY = clientOffset.y - hoverBoundingRect.top; + // Only perform the move when the mouse has crossed half of the items height + // When dragging downwards, only move when the cursor is below 50% + // When dragging upwards, only move when the cursor is above 50% + // Dragging downwards + + if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) { + return; + } + // Dragging upwards + if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) { + return; + } + + // Time to actually perform the action + moveCard(dragIndex, hoverIndex); + // Note: we're mutating the monitor item here! + // Generally it's better to avoid mutations, + // but it's good here for the sake of performance + // to avoid expensive index searches. + item.index = hoverIndex; + } + }); + const [ {isDragging}, drag] = useDrag({ + type: ItemTypes.CARD, + item: {id: id, index:index}, + collect: monitor => ({ + isDragging: monitor.isDragging() + }) + }); + drag(drop(ref)); + return ( + + ); +}; + +export default MyCard; + + diff --git a/Client/src/Components/TabPanels/Status/ContentPanel.jsx b/Client/src/Components/TabPanels/Status/ContentPanel.jsx index b0bc80610..98005e22e 100644 --- a/Client/src/Components/TabPanels/Status/ContentPanel.jsx +++ b/Client/src/Components/TabPanels/Status/ContentPanel.jsx @@ -1,21 +1,24 @@ -import { useState } from "react"; +import { useState, useCallback } from "react"; import { Button, Box, Stack, Typography } from "@mui/material"; import { ConfigBox } from "../../../Pages/Settings/styled"; import { useTheme } from "@emotion/react"; import TabPanel from "@mui/lab/TabPanel"; -import { - publicPageGeneralSettingsValidation, -} from "../../../Validation/validation"; +import { publicPageGeneralSettingsValidation } from "../../../Validation/validation"; import { buildErrors } from "../../../Validation/error"; import { hasValidationErrors } from "../../../Validation/error"; -import Server from "./Server" +import Card from "./Card"; +import update from "immutability-helper"; + const ContentPanel = () => { const theme = useTheme(); const [errors, setErrors] = useState({}); // Local state for form data, errors, and file handling const [localData, setLocalData] = useState({ - monitors: [] + monitors: [], }); + + const [cards, setCards] = useState(localData.monitors); + // Clears specific error from errors state const clearError = (err) => { setErrors((prev) => { @@ -25,6 +28,21 @@ const ContentPanel = () => { }); }; + const moveCard = useCallback( + (dragIndex, hoverIndex) => { + const dragCard = cards[dragIndex]; + setCards( + update(cards, { + $splice: [ + [dragIndex, 1], + [hoverIndex, 0, dragCard], + ], + }) + ); + }, + [cards] + ); + const handleChange = (event) => { event.preventDefault(); const { value, id } = event.target; @@ -41,16 +59,11 @@ const ContentPanel = () => { }; const handleAddNew = () => { - let arr = localData.monitors; - arr.push({ id: "" + Math.random() }); - setLocalData({ ...localData, monitors: arr }); - } - - const removeItem = (id) =>{ - const currentMonitors = localData.monitors.filter(m => m.id !=id) - setLocalData({...localData, monitors: currentMonitors}) - } - + setCards([...cards, { id: "" + Math.random() }]); + }; + const removeCard = (id) => { + setCards(cards.filter((c) => c?.id != id)); + }; const handleBlur = (event) => { event.preventDefault(); const { value, id } = event.target; @@ -122,22 +135,26 @@ const ContentPanel = () => { - {localData.monitors.length > 0 && ( + {cards.length > 0 && ( - {localData.monitors.map((m, idx) => ( - ( + ))} From 9370db62cc059f5b8cfa0b698ccefc61b35f5030 Mon Sep 17 00:00:00 2001 From: Shemy Gan Date: Tue, 10 Dec 2024 16:07:42 -0500 Subject: [PATCH 17/42] - Fix the suburl prefix via extending existing HttpAdorment - Add the rest of content panel UI layouts - Update the card value with onChange --- .../Inputs/TextInput/Adornments/index.jsx | 10 ++--- .../TabPanels/Status/Card/Server/index.jsx | 3 +- .../TabPanels/Status/Card/index.jsx | 4 +- .../TabPanels/Status/ContentPanel.jsx | 45 ++++++++++++++++++- .../TabPanels/Status/GeneralSettingsPanel.jsx | 3 +- 5 files changed, 55 insertions(+), 10 deletions(-) diff --git a/Client/src/Components/Inputs/TextInput/Adornments/index.jsx b/Client/src/Components/Inputs/TextInput/Adornments/index.jsx index 1faf784c9..5c309b3a6 100644 --- a/Client/src/Components/Inputs/TextInput/Adornments/index.jsx +++ b/Client/src/Components/Inputs/TextInput/Adornments/index.jsx @@ -6,7 +6,7 @@ import Visibility from "@mui/icons-material/Visibility"; import Docs from "../../../../assets/icons/docs.svg?react"; import DeleteIcon from "../../../../assets/icons/trash-bin.svg?react"; -export const HttpAdornment = ({ https }) => { +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://"} ); @@ -33,6 +33,7 @@ export const HttpAdornment = ({ https }) => { HttpAdornment.propTypes = { https: PropTypes.bool.isRequired, + prefix: PropTypes.string, }; export const PasswordEndAdornment = ({ fieldType, setFieldType }) => { @@ -69,12 +70,11 @@ PasswordEndAdornment.propTypes = { export const ServerStartAdornment = () => { return ( - + ); }; - export const ServerEndAdornment = ({ id, removeItem }) => { const theme = useTheme(); return ( @@ -95,7 +95,7 @@ export const ServerEndAdornment = ({ id, removeItem }) => { }, }} > - + ); diff --git a/Client/src/Components/TabPanels/Status/Card/Server/index.jsx b/Client/src/Components/TabPanels/Status/Card/Server/index.jsx index ee2934d7f..ed874ec01 100644 --- a/Client/src/Components/TabPanels/Status/Card/Server/index.jsx +++ b/Client/src/Components/TabPanels/Status/Card/Server/index.jsx @@ -2,7 +2,7 @@ import TextInput from "../../../../Inputs/TextInput"; import { ServerStartAdornment, ServerEndAdornment } from "../../../../Inputs/TextInput/Adornments"; -const Server = ({ id, removeItem, value }) => { +const Server = ({ id, removeItem, value, onChange }) => { return ( { } id={id} value= {value} + onChange ={ onChange } > ); }; diff --git a/Client/src/Components/TabPanels/Status/Card/index.jsx b/Client/src/Components/TabPanels/Status/Card/index.jsx index 6a89618ff..456b0ab22 100644 --- a/Client/src/Components/TabPanels/Status/Card/index.jsx +++ b/Client/src/Components/TabPanels/Status/Card/index.jsx @@ -14,13 +14,14 @@ const CustomCard = React.forwardRef((props, ref) => ( id={props.id} removeItem={props.removeCard} value={props.value} + onChange={props.onChange} /> )); -const MyCard = ({ id, index, moveCard, removeCard, value }) => { +const MyCard = ({ id, index, moveCard, removeCard, value, onChange }) => { const ref = useRef(null); const [, drop] = useDrop({ accept: ItemTypes.CARD, @@ -81,6 +82,7 @@ const MyCard = ({ id, index, moveCard, removeCard, value }) => { id={id} removeCard={removeCard} value={value} + onChange={onChange} /> ); }; diff --git a/Client/src/Components/TabPanels/Status/ContentPanel.jsx b/Client/src/Components/TabPanels/Status/ContentPanel.jsx index 98005e22e..88eb3b912 100644 --- a/Client/src/Components/TabPanels/Status/ContentPanel.jsx +++ b/Client/src/Components/TabPanels/Status/ContentPanel.jsx @@ -8,6 +8,7 @@ import { buildErrors } from "../../../Validation/error"; import { hasValidationErrors } from "../../../Validation/error"; import Card from "./Card"; import update from "immutability-helper"; +import Checkbox from "../../Inputs/Checkbox"; const ContentPanel = () => { const theme = useTheme(); @@ -15,6 +16,9 @@ const ContentPanel = () => { // Local state for form data, errors, and file handling const [localData, setLocalData] = useState({ monitors: [], + showUptimePercentage: false, + showBarcode: false + }); const [cards, setCards] = useState(localData.monitors); @@ -52,6 +56,17 @@ const ContentPanel = () => { })); }; + const handleCardChange = (event) => { + event.preventDefault(); + const { value, id } = event.target; + let idx = cards.findIndex((a) => a.id == id); + if (idx >= 0) { + let x = JSON.parse(JSON.stringify(cards[idx])); + x.url = value; + setCards(update(cards, { $splice: [[idx, 1, x]] })); + } else setCards(update(cards, { $push: [{ id: id, url: value }] })); + } + const handleSubmit = () => { if (hasValidationErrors(localData, publicPageGeneralSettingsValidation, setErrors)) { return; @@ -154,7 +169,8 @@ const ContentPanel = () => { text={"" + idx} moveCard={moveCard} removeCard={removeCard} - value={card?.url ?? card?.id} + value={card?.url} + onChange = {handleCardChange} /> ))} @@ -162,6 +178,33 @@ const ContentPanel = () => { + + + + Features + Show more details on the status page + + + + + + + { type="url" label="SubURL" value={localData.url} - startAdornment={} - //prefix={"http://uptimegenie.com/"} + startAdornment={} onChange={handleChange} onBlur={handleBlur} helperText={errors["url"]} From 00be6c219a1a47d192c3695f323d78605577b757 Mon Sep 17 00:00:00 2001 From: Shemy Gan Date: Thu, 12 Dec 2024 09:58:11 -0500 Subject: [PATCH 18/42] - Fix the warning button can not be descendant of button - Fix the warning possible change from uncontrolled to controlled component --- .../TabPanels/Status/Card/index.jsx | 31 +++++++++---------- .../TabPanels/Status/ContentPanel.jsx | 2 +- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/Client/src/Components/TabPanels/Status/Card/index.jsx b/Client/src/Components/TabPanels/Status/Card/index.jsx index 456b0ab22..a16c3d1ee 100644 --- a/Client/src/Components/TabPanels/Status/Card/index.jsx +++ b/Client/src/Components/TabPanels/Status/Card/index.jsx @@ -2,22 +2,19 @@ import React, { useRef } from "react"; import { useDrag, useDrop } from "react-dnd"; import ItemTypes from "./ItemTypes"; import Card from "@mui/material/Card"; -import CardActionArea from "@mui/material/CardActionArea"; -import CardContent from "@mui/material/CardContent"; import Server from "./Server" +import { CardContent } from "@mui/material"; const CustomCard = React.forwardRef((props, ref) => ( - - - - - + + + )); @@ -68,7 +65,7 @@ const MyCard = ({ id, index, moveCard, removeCard, value, onChange }) => { item.index = hoverIndex; } }); - const [ {isDragging}, drag] = useDrag({ + const [ _, drag] = useDrag({ type: ItemTypes.CARD, item: {id: id, index:index}, collect: monitor => ({ @@ -76,14 +73,14 @@ const MyCard = ({ id, index, moveCard, removeCard, value, onChange }) => { }) }); drag(drop(ref)); - return ( + return ( + onChange={onChange} + /> ); }; diff --git a/Client/src/Components/TabPanels/Status/ContentPanel.jsx b/Client/src/Components/TabPanels/Status/ContentPanel.jsx index 88eb3b912..9607f7c33 100644 --- a/Client/src/Components/TabPanels/Status/ContentPanel.jsx +++ b/Client/src/Components/TabPanels/Status/ContentPanel.jsx @@ -169,7 +169,7 @@ const ContentPanel = () => { text={"" + idx} moveCard={moveCard} removeCard={removeCard} - value={card?.url} + value={card?.url??""} onChange = {handleCardChange} /> ))} From 406fa6ed9f4e368c7607d0312393e3cb2f5870a9 Mon Sep 17 00:00:00 2001 From: Shemy Gan Date: Fri, 13 Dec 2024 11:29:50 -0500 Subject: [PATCH 19/42] - Merge feedback works from status page 1 and status page 2 branches --- Client/src/Components/Inputs/Checkbox/index.jsx | 8 ++++++-- Client/src/Components/Inputs/Image/index.jsx | 10 +++++++--- .../TabPanels/Status/GeneralSettingsPanel.jsx | 15 +-------------- Client/src/Validation/validation.js | 3 ++- 4 files changed, 16 insertions(+), 20 deletions(-) diff --git a/Client/src/Components/Inputs/Checkbox/index.jsx b/Client/src/Components/Inputs/Checkbox/index.jsx index 4685d8d44..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,11 +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 = typeof label == "string" ? {} : { alignSelf: "flex-start" }; + const override = alignSelf? { alignSelf: "flex-start" } : {} return ( } @@ -115,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 d1404e94c..e7c41b9e9 100644 --- a/Client/src/Components/Inputs/Image/index.jsx +++ b/Client/src/Components/Inputs/Image/index.jsx @@ -11,12 +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, error }) => { +const ImageField = ({ id, src, loading, onChange, error, isRound = true }) => { const theme = useTheme(); - const error_border_style = error? {borderColor: theme.palette.error.main}: {} + const error_border_style = error ? { borderColor: theme.palette.error.main } : {}; + + const roundShape = isRound ? { borderRadius: "50%" } : {}; const [isDragging, setIsDragging] = useState(false); const handleDragEnter = () => { @@ -148,10 +151,10 @@ const ImageField = ({ id, src, loading, onChange, error }) => { sx={{ width: "250px", height: "250px", - borderRadius: "50%", overflow: "hidden", backgroundImage: `url(${src})`, backgroundSize: "cover", + ...roundShape, }} > @@ -164,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/TabPanels/Status/GeneralSettingsPanel.jsx b/Client/src/Components/TabPanels/Status/GeneralSettingsPanel.jsx index b1e21a2db..230822447 100644 --- a/Client/src/Components/TabPanels/Status/GeneralSettingsPanel.jsx +++ b/Client/src/Components/TabPanels/Status/GeneralSettingsPanel.jsx @@ -31,7 +31,6 @@ const GeneralSettingsPanel = () => { url: "", timezone: "America/Toronto", color: "#4169E1", - theme: "light", //which fields matching below? publish: false, logo: null, @@ -68,7 +67,6 @@ const GeneralSettingsPanel = () => { const handleSubmit = () => { //validate rest of the form delete localData.logo; - delete localData.publish; if (hasValidationErrors(localData, publicPageGeneralSettingsValidation, setErrors)) { return; } @@ -162,7 +160,6 @@ const GeneralSettingsPanel = () => { id="published-to-public" label={`Published and visible to the public`} isChecked={localData.publish} - value={"localData.publish"} onChange={handleChange} onBlur={handleBlur} /> @@ -226,6 +223,7 @@ const GeneralSettingsPanel = () => { src={localData.logo?.src ?? logo?.src} loading={progress.isLoading && progress.value !== 100} onChange={handleLogo} + isRound={false} /> {progress.isLoading || progress.value !== 0 || errors["logo"] ? ( { onChange={handleChange} onBlur={handleBlur} /> - { />