diff --git a/Client/src/App.jsx b/Client/src/App.jsx index a5b649e8b..cad8a4ebd 100644 --- a/Client/src/App.jsx +++ b/Client/src/App.jsx @@ -24,6 +24,8 @@ import withAdminCheck from "./HOC/withAdminCheck"; import Configure from "./Pages/Monitors/Configure"; import PageSpeed from "./Pages/PageSpeed"; import CreatePageSpeed from "./Pages/PageSpeed/CreatePageSpeed"; +import CreateNewMaintenanceWindow from "./Pages/Maintenance/CreateMaintenanceWindow"; +import PageSpeedDetails from "./Pages/PageSpeed/Details"; function App() { const AdminCheckedRegister = withAdminCheck(Register); @@ -69,6 +71,10 @@ function App() { path="maintenance" element={} /> + } + /> } @@ -86,13 +92,17 @@ function App() { element={} /> } /> } /> + } + /> } /> diff --git a/Client/src/Components/BasicTable/index.css b/Client/src/Components/BasicTable/index.css index 9d8b31217..fd1f75b53 100644 --- a/Client/src/Components/BasicTable/index.css +++ b/Client/src/Components/BasicTable/index.css @@ -3,23 +3,22 @@ height: var(--env-var-img-width-2); color: var(--env-var-color-5); margin-right: 10px; - margin-bottom: 2px; } .MuiTable-root .host a svg { width: var(--env-var-font-size-large); height: var(--env-var-font-size-large); } -.MuiTable-root .host div:nth-child(3) { - font-size: var(--env-var-font-size-small); -} .MuiTable-root .host div:nth-child(2) { width: fit-content; - margin-right: 10px; font-weight: 700; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; } +.MuiTable-root .host div:nth-child(2) span { + font-size: var(--env-var-font-size-small); + margin-left: 10px; +} .MuiTable-root .label { line-height: 1; diff --git a/Client/src/Components/BasicTable/index.jsx b/Client/src/Components/BasicTable/index.jsx index 2a580d512..3f89de700 100644 --- a/Client/src/Components/BasicTable/index.jsx +++ b/Client/src/Components/BasicTable/index.jsx @@ -135,7 +135,9 @@ const BasicTable = ({ data, paginated, reversed, rowsPerPage = 5 }) => { {displayData.map((row) => { return ( diff --git a/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx b/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx index 02729a709..985b5b846 100644 --- a/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx +++ b/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx @@ -60,8 +60,6 @@ const MonitorDetailsAreaChart = ({ checks, filter }) => { const checkTime = new Date(checks[i].createdAt).getTime(); if (now - checkTime < limits[filter]) { result.push(checks[i]); - } else { - break; } } return result; diff --git a/Client/src/Components/Charts/PagespeedLineChart/index.css b/Client/src/Components/Charts/PagespeedLineChart/index.css new file mode 100644 index 000000000..e69de29bb diff --git a/Client/src/Components/Charts/PagespeedLineChart/index.jsx b/Client/src/Components/Charts/PagespeedLineChart/index.jsx new file mode 100644 index 000000000..270cbaca0 --- /dev/null +++ b/Client/src/Components/Charts/PagespeedLineChart/index.jsx @@ -0,0 +1,71 @@ +import { LineChart } from "@mui/x-charts/LineChart"; +import PropTypes from "prop-types"; + +const PageSpeedLineChart = ({ pageSpeedChecks = [] }) => { + const keyToLabel = { + performance: "Performance", + seo: "SEO", + bestPractices: "Best practices", + accessibility: "Accessibility", + }; + + const colors = { + performance: "#2aa02b", + seo: "#9467bd", + bestPractices: "#ff7f0e", + accessibility: "#1f76b3", + }; + + const customize = { + legend: { position: { vertical: "bottom", horizontal: "middle" } }, + margin: { bottom: 75 }, + }; + + const xLabels = pageSpeedChecks.map((check) => { + return check.createdAt; + }); + + return ( + ({ + dataKey: key, + label: keyToLabel[key], + color: colors[key], + showMark: false, + }))} + yAxis={[{ min: 0, max: 100 }]} + xAxis={[ + { + scaleType: "point", + data: xLabels, + valueFormatter: (val) => new Date(val).toLocaleDateString(), + }, + ]} + dataset={pageSpeedChecks} + {...customize} + grid={{ vertical: true, horizontal: true }} + tooltip={{ trigger: "none" }} + slotProps={{ + legend: { + direction: "row", + position: { vertical: "bottom", horizontal: "middle" }, + padding: 2, + itemMarkWidth: 8, + itemMarkHeight: 8, + markGap: 5, + itemGap: 15, + labelStyle: { + fontSize: 13, + color: "#344054" + } + }, + }} + /> + ); +}; + +PageSpeedLineChart.propTypes = { + pageSpeedChecks: PropTypes.array, +}; + +export default PageSpeedLineChart; diff --git a/Client/src/Components/Charts/ResponseTimeChart/index.jsx b/Client/src/Components/Charts/ResponseTimeChart/index.jsx index d76abc196..063ce3b16 100644 --- a/Client/src/Components/Charts/ResponseTimeChart/index.jsx +++ b/Client/src/Components/Charts/ResponseTimeChart/index.jsx @@ -8,7 +8,12 @@ const ResponseTimeChart = ({ checks = [] }) => { return (
- + {normalizedChecks.map((check, index) => ( {setting} diff --git a/Client/src/Components/Sidebar/index.jsx b/Client/src/Components/Sidebar/index.jsx index 020831313..3b2a32394 100644 --- a/Client/src/Components/Sidebar/index.jsx +++ b/Client/src/Components/Sidebar/index.jsx @@ -26,7 +26,7 @@ const menu = [ { name: "Incidents", path: "incidents", icon: }, { name: "Status pages", path: "status", icon: }, { name: "Maintenance", path: "maintenance", icon: }, - { name: "Page speed", path: "page-speed", icon: }, + { name: "Page speed", path: "pagespeed", icon: }, { name: "Integrations", path: "integrations", icon: }, { name: "Support", path: "support", icon: }, { name: "Settings", path: "settings", icon: }, @@ -50,7 +50,15 @@ function Sidebar() { alignItems="center" gap={theme.gap.small} borderRadius={`${theme.shape.borderRadius}px`} - onClick={() => navigate(`/${item.path}`)} + onClick={() => + item.path === "support" + ? window.open( + "https://github.com/bluewave-labs/bluewave-uptime", + "_blank", + "noreferrer" + ) + : navigate(`/${item.path}`) + } sx={{ p: `${theme.gap.small} ${theme.gap.medium}` }} > {item.icon} diff --git a/Client/src/Features/Auth/authSlice.js b/Client/src/Features/Auth/authSlice.js index 2b75c8d33..a30c295d9 100644 --- a/Client/src/Features/Auth/authSlice.js +++ b/Client/src/Features/Auth/authSlice.js @@ -61,7 +61,7 @@ export const update = createAsyncThunk( form.deleteProfileImage && fd.append("deleteProfileImage", form.deleteProfileImage); - const res = await axiosInstance.post(`/auth/user/${user._id}`, fd, { + const res = await axiosInstance.put(`/auth/user/${user._id}`, fd, { headers: { Authorization: `Bearer ${token}`, "Content-Type": "multipart/form-data", @@ -185,9 +185,7 @@ const handleForgotRejected = (state, action) => { const handleNewPasswordRejected = (state, action) => { state.isLoading = false; state.success = false; - state.msg = action.payload - ? action.payload.msg - : "Failed to reset password."; + state.msg = action.payload ? action.payload.msg : "Failed to reset password."; }; const authSlice = createSlice({ diff --git a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js index 05c182f5f..52a02cd55 100644 --- a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js +++ b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js @@ -77,9 +77,9 @@ export const updateUptimeMonitor = createAsyncThunk( name: monitor.name, description: monitor.description, interval: monitor.interval, - notifications: monitor.notifications + notifications: monitor.notifications, }; - const res = await axiosInstance.post( + const res = await axiosInstance.put( `/monitors/edit/${monitor._id}`, updatedFields, { @@ -104,16 +104,12 @@ export const deleteUptimeMonitor = createAsyncThunk( async (data, thunkApi) => { try { const { authToken, monitor } = data; - const res = await axiosInstance.post( - `/monitors/delete/${monitor._id}`, - {}, - { - headers: { - Authorization: `Bearer ${authToken}`, - "Content-Type": "application/json", - }, - } - ); + 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) { diff --git a/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.css b/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.css new file mode 100644 index 000000000..a799bf0af --- /dev/null +++ b/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.css @@ -0,0 +1,21 @@ +.maintenance-options > .MuiStack-root > .MuiStack-root.select-wrapper { + width: 380px; +} + +.date-picker .MuiOutlinedInput-root, +.time-picker .MuiOutlinedInput-root { + height: 34px; + font-size: var(--env-var-font-size-medium); +} + +.date-picker .MuiOutlinedInput-root { + width: 140px; +} + +.time-picker .MuiOutlinedInput-root { + width: 90px; +} + +.MuiInputAdornment-outlined .MuiIconButton-root svg { + width: 20px; +} diff --git a/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.jsx b/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.jsx new file mode 100644 index 000000000..aea012e2c --- /dev/null +++ b/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.jsx @@ -0,0 +1,151 @@ +import { Box, Stack, Typography } from "@mui/material"; +import "./index.css"; +import React, { useState } from "react"; +import Button from "../../../Components/Button"; +import Back from "../../../assets/icons/left-arrow.svg?react"; +import Select from "../../../Components/Inputs/Select"; +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; +import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; +import dayjs from "dayjs"; +import { DatePicker } from "@mui/x-date-pickers/DatePicker"; +import { MobileTimePicker } from "@mui/x-date-pickers/MobileTimePicker"; + +const directory = { + title: "Create a maintenance window", + description: "Your pings won’t be sent in this time frame.", +}; + +const repeatOptions = [ + { _id: 1, name: "Don't repeat" }, + { _id: 2, name: "Repeat daily" }, + { _id: 3, name: "Repeat weekly" }, +]; + +const configOptionTitle = (title, description) => { + return ( + + + {title} + + {description && ( + + {description} + + )} + + ); +}; + +const CreateNewMaintenanceWindow = () => { + const [values, setValues] = useState({ + repeat: 1, + }); + + const handleChange = (event, name) => { + const { value } = event.target; + setValues((prev) => ({ + ...prev, + [name]: value, + })); + }; + + const configOptions = [ + { + title: "Repeat", + component: ( + handleChange(event, "interval")} - /> - - - - Incidents notifications{" "} - (coming soon) - - - - When there is a new incident, + Create a page speed monitor + + + Monitor friendly name + handleChange(event, "name")} + error={errors.name} + /> + + + URL + handleChange(event, "url")} + error={errors.url} + /> + + + Check frequency +