diff --git a/Client/src/App.jsx b/Client/src/App.jsx index cad8a4ebd..12db9dab9 100644 --- a/Client/src/App.jsx +++ b/Client/src/App.jsx @@ -26,6 +26,7 @@ import PageSpeed from "./Pages/PageSpeed"; import CreatePageSpeed from "./Pages/PageSpeed/CreatePageSpeed"; import CreateNewMaintenanceWindow from "./Pages/Maintenance/CreateMaintenanceWindow"; import PageSpeedDetails from "./Pages/PageSpeed/Details"; +import PageSpeedConfigure from "./Pages/PageSpeed/Configure"; function App() { const AdminCheckedRegister = withAdminCheck(Register); @@ -103,6 +104,10 @@ function App() { path="pagespeed/:monitorId" element={} /> + } + /> } /> diff --git a/Client/src/Components/TabPanels/Account/ProfilePanel.jsx b/Client/src/Components/TabPanels/Account/ProfilePanel.jsx index 05f3d8412..355b09ca6 100644 --- a/Client/src/Components/TabPanels/Account/ProfilePanel.jsx +++ b/Client/src/Components/TabPanels/Account/ProfilePanel.jsx @@ -352,7 +352,7 @@ const ProfilePanel = () => { top: "50%", left: "50%", transform: "translate(-50%, -50%)", - width: 400, + width: 500, bgcolor: "white", border: "solid 1px #f2f2f2", borderRadius: `${theme.shape.borderRadius}px`, diff --git a/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js b/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js index 6524a3417..4377edb35 100644 --- a/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js +++ b/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js @@ -68,6 +68,58 @@ export const getPageSpeedByUserId = createAsyncThunk( } ); +export const updatePageSpeed = createAsyncThunk( + "pageSpeedMonitors/updatePageSpeed", + async (data, thunkApi) => { + try { + const { authToken, monitor } = data; + const updatedFields = { + name: monitor.name, + description: monitor.description, + interval: monitor.interval, + // notifications: monitor.notifications, + }; + const res = await axiosInstance.put( + `/monitors/${monitor._id}`, + updatedFields, + { + headers: { + Authorization: `Bearer ${authToken}`, + "Content-Type": "application/json", + }, + } + ); + return res.data; + } catch (error) { + if (error.response && error.response.data) { + return thunkApi.rejectWithValue(error.response.data); + } + return thunkApi.rejectWithValue(error.message); + } + } +); + +export const deletePageSpeed = createAsyncThunk( + "pageSpeedMonitors/deletePageSpeed", + async (data, thunkApi) => { + try { + const { authToken, monitor } = data; + const res = await axiosInstance.delete(`/monitors/${monitor._id}`, { + headers: { + Authorization: `Bearer ${authToken}`, + "Content-Type": "application/json", + }, + }); + return res.data; + } catch (error) { + if (error.response && error.response.data) { + return thunkApi.rejectWithValue(error.response.data); + } + return thunkApi.rejectWithValue(error.message); + } + } +); + const pageSpeedMonitorSlice = createSlice({ name: "pageSpeedMonitor", initialState, @@ -137,6 +189,44 @@ const pageSpeedMonitorSlice = createSlice({ state.msg = action.payload ? action.payload.msg : "Failed to create page speed monitor"; + }) + + // ***************************************************** + // Create Monitor + // ***************************************************** + .addCase(updatePageSpeed.pending, (state) => { + state.isLoading = true; + }) + .addCase(updatePageSpeed.fulfilled, (state, action) => { + state.isLoading = false; + state.success = action.payload.success; + state.msg = action.payload.msg; + }) + .addCase(updatePageSpeed.rejected, (state, action) => { + state.isLoading = false; + state.success = false; + state.msg = action.payload + ? action.payload.msg + : "Failed to update page speed monitor"; + }) + + // ***************************************************** + // Delete Monitor + // ***************************************************** + .addCase(deletePageSpeed.pending, (state) => { + state.isLoading = true; + }) + .addCase(deletePageSpeed.fulfilled, (state, action) => { + state.isLoading = false; + state.success = action.payload.success; + state.msg = action.payload.msg; + }) + .addCase(deletePageSpeed.rejected, (state, action) => { + state.isLoading = false; + state.success = false; + state.msg = action.payload + ? action.payload.msg + : "Failed to delete page speed monitor"; }); }, }); diff --git a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js index 4c2acd3ec..e69e6d41d 100644 --- a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js +++ b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js @@ -80,7 +80,7 @@ export const updateUptimeMonitor = createAsyncThunk( notifications: monitor.notifications, }; const res = await axiosInstance.put( - `/monitors/edit/${monitor._id}`, + `/monitors/${monitor._id}`, updatedFields, { headers: { diff --git a/Client/src/Pages/Monitors/Configure/index.jsx b/Client/src/Pages/Monitors/Configure/index.jsx index 0df88078f..08c8cbe39 100644 --- a/Client/src/Pages/Monitors/Configure/index.jsx +++ b/Client/src/Pages/Monitors/Configure/index.jsx @@ -460,10 +460,10 @@ const Configure = () => { }} > - Really delete this monitor? + Do you really want to delete this monitor? - Once deleted, this monitor cannot be retrieved back. + Once deleted, this monitor cannot be retrieved. { const { authToken } = useSelector((state) => state.auth); @@ -53,63 +58,76 @@ const PaginationTable = ({ monitorId, dateRange }) => { const handlePageChange = (_, newPage) => { setPaginationController({ ...paginationController, - page: newPage, - }); - }; - - const handleChangeRowsPerPage = (event) => { - setPaginationController({ - ...paginationController, - rowsPerPage: parseInt(event.target.value, 10), - page: 0, + page: newPage - 1, }); }; + + let paginationComponent = <>; + if (checksCount > paginationController.rowsPerPage) { + paginationComponent = ( + ( + + )} + /> + ); + } return ( - - - - - Status - Date & Time - Message - - - - {checks.map((check) => { - const status = check.status === true ? "up" : "down"; + <> + +
+ + + Status + Date & Time + Message + + + + {checks.map((check) => { + const status = check.status === true ? "up" : "down"; - return ( - - - - - - {new Date(check.createdAt).toLocaleString()} - - {check.statusCode} - - ); - })} - - - - - - -
-
+ return ( + + + + + + {new Date(check.createdAt).toLocaleString()} + + {check.statusCode} + + ); + })} + + + + {paginationComponent} + ); }; diff --git a/Client/src/Pages/Monitors/index.css b/Client/src/Pages/Monitors/index.css index 67da49c1f..7b40f7464 100644 --- a/Client/src/Pages/Monitors/index.css +++ b/Client/src/Pages/Monitors/index.css @@ -12,7 +12,6 @@ .monitors p.MuiTypography-root { font-size: var(--env-var-font-size-medium); } -.monitors h2:has(+ p), .monitors p.MuiTypography-root { color: var(--env-var-color-5); } diff --git a/Client/src/Pages/PageSpeed/Configure/index.css b/Client/src/Pages/PageSpeed/Configure/index.css new file mode 100644 index 000000000..577d18389 --- /dev/null +++ b/Client/src/Pages/PageSpeed/Configure/index.css @@ -0,0 +1,68 @@ +.configure-pagespeed form > .MuiStack-root:nth-child(2) { + background-color: var(--env-var-color-8); + border: solid 1px var(--env-var-color-6); + border-radius: var(--env-var-radius-1); + padding: var(--env-var-default-2); +} +.configure-pagespeed h1.MuiTypography-root { + font-size: var(--env-var-font-size-large-plus); +} +.configure-pagespeed h1.MuiTypography-root, +.configure-pagespeed h2.MuiTypography-root { + color: var(--env-var-color-1); +} +.configure-pagespeed span.MuiTypography-root { + font-size: var(--env-var-font-size-small-plus); +} +.configure-pagespeed p.MuiTypography-root, +.configure-pagespeed h3.MuiTypography-root { + color: var(--env-var-color-5); +} +.configure-pagespeed h1.MuiTypography-root, +.configure-pagespeed h3.MuiTypography-root, +.configure-pagespeed h2.MuiTypography-root { + font-weight: 600; +} +.configure-pagespeed p.MuiTypography-root, +.configure-pagespeed h3.MuiTypography-root, +.configure-pagespeed h3.MuiTypography-root > span.MuiTypography-root { + font-size: var(--env-var-font-size-medium); +} +.configure-pagespeed .MuiBox-root > .field + p.MuiTypography-root { + font-size: var(--env-var-font-size-small-plus); +} +.configure-pagespeed .MuiBox-root > .field + p.MuiTypography-root, +.configure-pagespeed h3.MuiTypography-root > span.MuiTypography-root { + opacity: 0.6; +} +.configure-pagespeed .checkbox-wrapper { + margin-bottom: 1px; +} +.configure-pagespeed .MuiInputBase-root:has(> .Mui-disabled) { + background-color: var(--env-var-color-13); +} +/* to be removed */ +.configure-pagespeed .section-disabled { + opacity: 0.4; + background-color: #f1f1f2; + padding: 10px; + border-radius: 8px; + user-select: none; + pointer-events: none; +} + +.configure-pagespeed button { + height: var(--env-var-height-2); +} + +body:has(.configure-pagespeed) .select-dropdown .MuiMenuItem-root, +.configure-pagespeed .select-wrapper .select-component > .MuiSelect-select { + text-transform: lowercase; +} + +.configure-pagespeed .field, +.configure-pagespeed .section-disabled, +.configure-pagespeed .select-wrapper, +.configure-pagespeed h3.MuiTypography-root { + flex: 1; +} diff --git a/Client/src/Pages/PageSpeed/Configure/index.jsx b/Client/src/Pages/PageSpeed/Configure/index.jsx new file mode 100644 index 000000000..91d250dd9 --- /dev/null +++ b/Client/src/Pages/PageSpeed/Configure/index.jsx @@ -0,0 +1,365 @@ +import { useEffect, useState } from "react"; +import { useTheme } from "@emotion/react"; +import { Box, Modal, Skeleton, Stack, Typography } from "@mui/material"; +import { useDispatch, useSelector } from "react-redux"; +import { useNavigate, useParams } from "react-router"; +import { + deletePageSpeed, + getPageSpeedByUserId, + updatePageSpeed, +} from "../../../Features/PageSpeedMonitor/pageSpeedMonitorSlice"; +import { monitorValidation } from "../../../Validation/validation"; +import { createToast } from "../../../Utils/toastUtils"; +import Button from "../../../Components/Button"; +import Field from "../../../Components/Inputs/Field"; +import Select from "../../../Components/Inputs/Select"; +import Checkbox from "../../../Components/Inputs/Checkbox"; +import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline"; +import WestRoundedIcon from "@mui/icons-material/WestRounded"; +import GreenCheck from "../../../assets/icons/checkbox-green.svg?react"; +import RedCheck from "../../../assets/icons/checkbox-red.svg?react"; + +import "./index.css"; + +/** + * Renders a skeleton layout. + * + * @returns {JSX.Element} + */ +const SkeletonLayout = () => { + const theme = useTheme(); + + return ( + <> + + + + + + + + + + + + + + + + + + + + ); +}; + +const PageSpeedConfigure = () => { + const theme = useTheme(); + const navigate = useNavigate(); + const dispatch = useDispatch(); + const MS_PER_MINUTE = 60000; + const { authToken } = useSelector((state) => state.auth); + const { monitors } = useSelector((state) => state.pageSpeedMonitors); + const { monitorId } = useParams(); + const [monitor, setMonitor] = useState({}); + const [errors, setErrors] = useState({}); + + const frequencies = [ + { _id: 3, name: "3 minutes" }, + { _id: 5, name: "5 minutes" }, + { _id: 10, name: "10 minutes" }, + { _id: 20, name: "20 minutes" }, + { _id: 60, name: "1 hour" }, + { _id: 1440, name: "1 day" }, + { _id: 10080, name: "1 week" }, + ]; + + useEffect(() => { + const data = monitors.find((monitor) => monitor._id === monitorId); + if (!data) { + console.error("Error fetching pagespeed monitor of id: " + monitorId); + navigate("/not-found"); + } + setMonitor({ + ...data, + }); + }, [monitorId]); + + const handleChange = (event, id) => { + let { value } = event.target; + if (id === "interval") { + value = value * MS_PER_MINUTE; + } + setMonitor((prev) => ({ ...prev, [id]: value })); + + const { error } = monitorValidation.validate( + { [id]: value }, + { abortEarly: false } + ); + + setErrors((prev) => { + const newErrors = { ...prev }; + if (error) newErrors[id] = error.details[0].message; + else delete newErrors[id]; + return newErrors; + }); + }; + + const handleSave = async (event) => { + event.preventDefault(); + const action = await dispatch( + updatePageSpeed({ authToken, monitor: monitor }) + ); + if (action.meta.requestStatus === "fulfilled") { + createToast({ body: "Monitor updated successfully!" }); + dispatch(getPageSpeedByUserId(authToken)); + } else { + createToast({ body: "Failed to update monitor." }); + } + }; + + const [isOpen, setIsOpen] = useState(false); + const handleRemove = async (event) => { + event.preventDefault(); + const action = await dispatch(deletePageSpeed({ authToken, monitor })); + if (action.meta.requestStatus === "fulfilled") { + navigate("/pagespeed"); + } else { + createToast({ body: "Failed to delete monitor." }); + } + }; + + let loading = Object.keys(monitor).length === 0; + + return ( + + {loading ? ( + + ) : ( + <> +