From 32b0c2039ec1fff91b90add0555be525e9d049ae Mon Sep 17 00:00:00 2001 From: Caio Cabral Date: Wed, 16 Oct 2024 21:32:04 -0400 Subject: [PATCH 01/16] chore: changing const for let since it 's not being redeclared --- Client/src/Pages/Monitors/Home/index.jsx | 2 +- package-lock.json | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 package-lock.json diff --git a/Client/src/Pages/Monitors/Home/index.jsx b/Client/src/Pages/Monitors/Home/index.jsx index 151a04bbd..6cfb97265 100644 --- a/Client/src/Pages/Monitors/Home/index.jsx +++ b/Client/src/Pages/Monitors/Home/index.jsx @@ -40,7 +40,7 @@ const Monitors = ({ isAdmin }) => { dispatch(getUptimeMonitorsByTeamId(authState.authToken)); }, [authState.authToken, dispatch]); - let loading = + const loading = monitorState?.isLoading && monitorState?.monitorsSummary?.monitors?.length === 0; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..f71a78c1f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "bluewave-uptime", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} From 63a4223d9c6cc0739faa333b57a86c42b5f22b0f Mon Sep 17 00:00:00 2001 From: Caio Cabral Date: Wed, 16 Oct 2024 21:48:13 -0400 Subject: [PATCH 02/16] refactor: extracting condition for showing create monitor button --- Client/src/Pages/Monitors/Home/index.jsx | 333 +++++++++++------------ 1 file changed, 166 insertions(+), 167 deletions(-) diff --git a/Client/src/Pages/Monitors/Home/index.jsx b/Client/src/Pages/Monitors/Home/index.jsx index 6cfb97265..0d673b29b 100644 --- a/Client/src/Pages/Monitors/Home/index.jsx +++ b/Client/src/Pages/Monitors/Home/index.jsx @@ -4,13 +4,7 @@ import { useSelector, useDispatch } from "react-redux"; import { getUptimeMonitorsByTeamId } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice"; import { useNavigate } from "react-router-dom"; import { useTheme } from "@emotion/react"; -import { - Box, - Button, - CircularProgress, - Stack, - Typography, -} from "@mui/material"; +import { Box, Button, CircularProgress, Stack, Typography } from "@mui/material"; import PropTypes from "prop-types"; import SkeletonLayout from "./skeleton"; import Fallback from "./fallback"; @@ -22,173 +16,178 @@ import Search from "../../../Components/Inputs/Search"; import useDebounce from "../../../Utils/debounce"; const Monitors = ({ isAdmin }) => { - const theme = useTheme(); - const navigate = useNavigate(); - const monitorState = useSelector((state) => state.uptimeMonitors); - const authState = useSelector((state) => state.auth); - const [search, setSearch] = useState(""); - const [isSearching, setIsSearching] = useState(false); - const dispatch = useDispatch({}); - const debouncedFilter = useDebounce(search, 500); + const theme = useTheme(); + const navigate = useNavigate(); + const monitorState = useSelector((state) => state.uptimeMonitors); + const authState = useSelector((state) => state.auth); + const [search, setSearch] = useState(""); + const [isSearching, setIsSearching] = useState(false); + const dispatch = useDispatch({}); + const debouncedFilter = useDebounce(search, 500); - const handleSearch = (value) => { - setIsSearching(true); - setSearch(value); - }; + const handleSearch = (value) => { + setIsSearching(true); + setSearch(value); + }; - useEffect(() => { - dispatch(getUptimeMonitorsByTeamId(authState.authToken)); - }, [authState.authToken, dispatch]); + useEffect(() => { + dispatch(getUptimeMonitorsByTeamId(authState.authToken)); + }, [authState.authToken, dispatch]); - const loading = - monitorState?.isLoading && - monitorState?.monitorsSummary?.monitors?.length === 0; + console.log(monitorState); + const loading = true; + /* monitorState?.isLoading && + monitorState?.monitorsSummary?.monitors?.length === 0; */ + const canCreateMonitor = + isAdmin && monitorState?.monitorsSummary?.monitors?.length !== 0; - return ( - - {loading ? ( - - ) : ( - <> - - - - - {isAdmin && - monitorState?.monitorsSummary?.monitors?.length !== 0 && ( - - )} - - - {isAdmin && monitorState?.monitorsSummary?.monitors?.length === 0 && ( - - )} + return ( + + + + + + {canCreateMonitor && ( + + )} + + + {loading ? ( + + ) : ( + <> + {isAdmin && monitorState?.monitorsSummary?.monitors?.length === 0 && ( + + )} - {monitorState?.monitorsSummary?.monitors?.length !== 0 && ( - <> - - - - - - - - - Actively monitoring - - - {monitorState?.monitorsSummary?.monitorCounts?.total || 0} - - - - - - - {isSearching && ( - <> - - - - - - )} - - - - - )} - - )} - - ); + {monitorState?.monitorsSummary?.monitors?.length !== 0 && ( + <> + + + + + + + + + Actively monitoring + + + {monitorState?.monitorsSummary?.monitorCounts?.total || 0} + + + + + + + {isSearching && ( + <> + + + + + + )} + + + + + )} + + )} + + ); }; Monitors.propTypes = { - isAdmin: PropTypes.bool, + isAdmin: PropTypes.bool, }; export default Monitors; From 478a8b7f13bedc0e76c4a5dcaa072bd4c5efb541 Mon Sep 17 00:00:00 2001 From: Caio Cabral Date: Wed, 16 Oct 2024 21:57:41 -0400 Subject: [PATCH 03/16] feat: adding spacing to stack --- Client/src/Pages/Monitors/Home/index.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/Client/src/Pages/Monitors/Home/index.jsx b/Client/src/Pages/Monitors/Home/index.jsx index 0d673b29b..d02d046ab 100644 --- a/Client/src/Pages/Monitors/Home/index.jsx +++ b/Client/src/Pages/Monitors/Home/index.jsx @@ -53,6 +53,7 @@ const Monitors = ({ isAdmin }) => { justifyContent="space-between" alignItems="center" mt={theme.spacing(5)} + gap={theme.spacing(6)} > {canCreateMonitor && ( From 8c57f3990b661e5cd4396e23e76322b8c5db3a8b Mon Sep 17 00:00:00 2001 From: Shemy Gan Date: Fri, 18 Oct 2024 11:20:20 -0400 Subject: [PATCH 04/16] - Extracted functions to error.js - Add validation to advanced settings page --- Client/src/Pages/AdvancedSettings/index.jsx | 455 +++++++++--------- .../Maintenance/CreateMaintenance/index.jsx | 105 ++-- Client/src/Validation/error.js | 25 + Client/src/Validation/validation.js | 33 ++ 4 files changed, 339 insertions(+), 279 deletions(-) create mode 100644 Client/src/Validation/error.js diff --git a/Client/src/Pages/AdvancedSettings/index.jsx b/Client/src/Pages/AdvancedSettings/index.jsx index ce1c89250..12016889f 100644 --- a/Client/src/Pages/AdvancedSettings/index.jsx +++ b/Client/src/Pages/AdvancedSettings/index.jsx @@ -12,45 +12,47 @@ import { useNavigate } from "react-router"; import { getAppSettings, updateAppSettings } from "../../Features/Settings/settingsSlice"; import { useState, useEffect } from "react"; import Select from "../../Components/Inputs/Select"; +import { advancedSettingsValidation } from "../../Validation/validation"; +import { buildErrors, hasValidationErrors } from "../../Validation/error"; const AdvancedSettings = ({ isAdmin }) => { - const navigate = useNavigate(); + const navigate = useNavigate(); + useEffect(() => { + if (!isAdmin) { + navigate("/"); + } + }, [navigate, isAdmin]); - useEffect(() => { - if (!isAdmin) { - navigate("/"); - } - }, [navigate, isAdmin]); + const theme = useTheme(); + const { authToken } = useSelector((state) => state.auth); + const dispatch = useDispatch(); + const settings = useSelector((state) => state.settings); + const [localSettings, setLocalSettings] = useState({ + apiBaseUrl: "", + logLevel: "debug", + systemEmailHost: "", + systemEmailPort: "", + systemEmailAddress: "", + systemEmailPassword: "", + jwtTTL: "", + dbType: "", + redisHost: "", + redisPort: "", + pagespeedApiKey: "", + }); + const [errors, setErrors] = useState({}); - const theme = useTheme(); - const { authToken } = useSelector((state) => state.auth); - const dispatch = useDispatch(); - const settings = useSelector((state) => state.settings); - const [localSettings, setLocalSettings] = useState({ - apiBaseUrl: "", - logLevel: "debug", - systemEmailHost: "", - systemEmailPort: "", - systemEmailAddress: "", - systemEmailPassword: "", - jwtTTL: "", - dbType: "", - redisHost: "", - redisPort: "", - pagespeedApiKey: "", - }); - - useEffect(() => { - const getSettings = async () => { - const action = await dispatch(getAppSettings({ authToken })); - if (action.payload.success) { - setLocalSettings(action.payload.data); - } else { - createToast({ body: "Failed to get settings" }); - } - }; - getSettings(); - }, [authToken, dispatch]); + useEffect(() => { + const getSettings = async () => { + const action = await dispatch(getAppSettings({ authToken })); + if (action.payload?.success) { + setLocalSettings(action.payload.data); + } else { + createToast({ body: "Failed to get settings" }); + } + }; + getSettings(); + }, [authToken, dispatch]); const logItems = [ { _id: 1, name: "none" }, @@ -72,189 +74,210 @@ const AdvancedSettings = ({ isAdmin }) => { setLocalSettings({ ...localSettings, logLevel: newLogLevel }); }; - const handleChange = (event) => { - const { value, id } = event.target; - setLocalSettings({ ...localSettings, [id]: value }); - }; + const handleChange = (event) => { + const { value, id } = event.target; + setLocalSettings({ ...localSettings, [id]: value }); + const { error } = advancedSettingsValidation.validate( + { [id]: value }, + { + abortEarly: false, + } + ); + setErrors((prev) => { + return buildErrors(prev, id, error); + }); + }; - const handleSave = async () => { - const action = await dispatch( - updateAppSettings({ settings: localSettings, authToken }) - ); - let body = ""; - if (action.payload.success) { - console.log(action.payload.data); - setLocalSettings(action.payload.data); - body = "Settings saved successfully"; - } else { - body = "Failed to save settings"; - } - createToast({ body }); - }; + const handleSave = async () => { + if (hasValidationErrors(localSettings, advancedSettingsValidation, setErrors)) + return; + const action = await dispatch( + updateAppSettings({ settings: localSettings, authToken }) + ); + let body = ""; + if (action.payload.success) { + console.log(action.payload.data); + setLocalSettings(action.payload.data); + body = "Settings saved successfully"; + } else { + body = "Failed to save settings"; + } + createToast({ body }); + }; - return ( - - - - - Client settings - - Modify client settings here. - - - - - + + + + + Email settings + + Set your host email settings here. These settings are used for + sending system emails. + + + + + + + + + + + + Server settings + + Modify server settings here. + + + + + + + + + + + + + About + + + BlueWave Uptime v1.0.0 + + Developed by Bluewave Labs. + + + + + + + Save + + + + + ); }; AdvancedSettings.propTypes = { diff --git a/Client/src/Pages/Maintenance/CreateMaintenance/index.jsx b/Client/src/Pages/Maintenance/CreateMaintenance/index.jsx index 4dbf8f747..d165523f9 100644 --- a/Client/src/Pages/Maintenance/CreateMaintenance/index.jsx +++ b/Client/src/Pages/Maintenance/CreateMaintenance/index.jsx @@ -28,6 +28,7 @@ import { MS_PER_WEEK, } from "../../../Utils/timeUtils"; import { useNavigate, useParams } from "react-router-dom"; +import { buildErrors, hasValidationErrors } from "../../../Validation/error"; const getDurationAndUnit = (durationInMs) => { if (durationInMs % MS_PER_DAY === 0) { @@ -145,46 +146,36 @@ const CreateMaintenance = () => { return; } - const res = await networkService.getMaintenanceWindowById({ - authToken: authToken, - maintenanceWindowId: maintenanceWindowId, - }); - const maintenanceWindow = res.data.data; - const { name, start, end, repeat, monitorId } = maintenanceWindow; - const startTime = dayjs(start); - const endTime = dayjs(end); - const durationInMs = endTime.diff(startTime, "milliseconds").toString(); - const { duration, durationUnit } = getDurationAndUnit(durationInMs); - const monitor = monitors.find((monitor) => monitor._id === monitorId); - setForm({ - ...form, - name, - repeat: REVERSE_REPEAT_LOOKUP[repeat], - startDate: startTime, - startTime, - duration, - durationUnit, - monitors: monitor ? [monitor] : [], - }); - } catch (error) { - createToast({ body: "Failed to fetch data" }); - logger.error("Failed to fetch monitors", error); - } finally { - setIsLoading(false); - } - }; - fetchMonitors(); - }, [authToken, user]); - - const buildErrors = (prev, id, error) => { - const updatedErrors = { ...prev }; - if (error) { - updatedErrors[id] = error.details[0].message; - } else { - delete updatedErrors[id]; - } - return updatedErrors; - }; + const res = await networkService.getMaintenanceWindowById({ + authToken: authToken, + maintenanceWindowId: maintenanceWindowId, + }); + const maintenanceWindow = res.data.data; + const { name, start, end, repeat, monitorId } = maintenanceWindow; + const startTime = dayjs(start); + const endTime = dayjs(end); + const durationInMs = endTime.diff(startTime, "milliseconds").toString(); + const { duration, durationUnit } = getDurationAndUnit(durationInMs); + const monitor = monitors.find((monitor) => monitor._id === monitorId); + setForm({ + ...form, + name, + repeat: REVERSE_REPEAT_LOOKUP[repeat], + startDate: startTime, + startTime, + duration, + durationUnit, + monitors: monitor ? [monitor] : [], + }); + } catch (error) { + createToast({ body: "Failed to fetch data" }); + logger.error("Failed to fetch monitors", error); + } finally { + setIsLoading(false); + } + }; + fetchMonitors(); + }, [authToken, user]); const handleSearch = (value) => { setSearch(value); @@ -223,29 +214,17 @@ const CreateMaintenance = () => { }); }; - const handleSubmit = async () => { - const { error } = maintenanceWindowValidation.validate(form, { - abortEarly: false, - }); - - // If errors, return early - if (error) { - const newErrors = {}; - error.details.forEach((err) => { - newErrors[err.path[0]] = err.message; - }); - setErrors(newErrors); - logger.error(error); - return; - } - // Build timestamp for maintenance window from startDate and startTime - const start = dayjs(form.startDate) - .set("hour", form.startTime.hour()) - .set("minute", form.startTime.minute()); - // Build end timestamp for maintenance window - const MS_MULTIPLIER = MS_LOOKUP[form.durationUnit]; - const durationInMs = form.duration * MS_MULTIPLIER; - const end = start.add(durationInMs); + const handleSubmit = async () => { + if(hasValidationErrors(form, maintenanceWindowValidation, setErrors)) + return; + // Build timestamp for maintenance window from startDate and startTime + const start = dayjs(form.startDate) + .set("hour", form.startTime.hour()) + .set("minute", form.startTime.minute()); + // Build end timestamp for maintenance window + const MS_MULTIPLIER = MS_LOOKUP[form.durationUnit]; + const durationInMs = form.duration * MS_MULTIPLIER; + const end = start.add(durationInMs); // Get repeat value in milliseconds const repeat = REPEAT_LOOKUP[form.repeat]; diff --git a/Client/src/Validation/error.js b/Client/src/Validation/error.js new file mode 100644 index 000000000..0e675f413 --- /dev/null +++ b/Client/src/Validation/error.js @@ -0,0 +1,25 @@ +const buildErrors = (prev, id, error) => { + const updatedErrors = { ...prev }; + if (error) { + updatedErrors[id] = error.details[0].message; + } else { + delete updatedErrors[id]; + } + return updatedErrors; +}; + +const hasValidationErrors = (form, validation, setErrors) => { + const { error } = validation.validate(form, { + abortEarly: false, + }); + if (error) { + const newErrors = {}; + error.details.forEach((err) => { + newErrors[err.path[0]] = err.message; + }); + setErrors(newErrors); + return true; + } + return false; +}; +export { buildErrors, hasValidationErrors }; diff --git a/Client/src/Validation/validation.js b/Client/src/Validation/validation.js index c620bdf43..1a9742e21 100644 --- a/Client/src/Validation/validation.js +++ b/Client/src/Validation/validation.js @@ -128,10 +128,43 @@ const maintenanceWindowValidation = joi.object({ monitors: joi.array().min(1), }); +const advancedSettingsValidation = joi.object({ + apiBaseUrl: joi.string().uri({ allowRelative: true }).trim().messages({ + "string.empty": "API base url is required.", + "string.uri": "The URL you provided is not valid.", + }), + logLevel: joi.string(), + systemEmailHost: joi.string().allow(''), + systemEmailPort: joi.string().allow(''), + systemEmailAddress: joi + .string().allow(''), + systemEmailPassword: joi.string().allow(''), + jwtTTL: joi + .string() + .trim() + .messages({ + "string.empty": "JWT TTL is required." + }), + dbType: joi.string().trim().messages({ + "string.empty": "DB type is required", + }), + redisHost: joi.string().trim().messages({ + "string.empty": "Redis host is required", + }), + redisPort: joi.string().allow('').custom((value, helpers) => { + if(value && isNaN(parseInt(value))){ + return helpers.message("Redit port must be a number") + } + return value + }), + pagespeedApiKey: joi.string().allow('') +}); + export { credentials, imageValidation, monitorValidation, settingsValidation, maintenanceWindowValidation, + advancedSettingsValidation, }; From a517edfea344d457baea0f7ae8b8b33f0e6ae061 Mon Sep 17 00:00:00 2001 From: Caio Cabral Date: Fri, 18 Oct 2024 14:49:48 -0400 Subject: [PATCH 05/16] feat: adding skeleton to table body --- .../Home/MonitorTable/Skeleton/index.jsx | 31 + .../Monitors/Home/MonitorTable/index.jsx | 732 +++++++++--------- Client/src/Pages/Monitors/Home/index.jsx | 31 +- 3 files changed, 415 insertions(+), 379 deletions(-) create mode 100644 Client/src/Pages/Monitors/Home/MonitorTable/Skeleton/index.jsx diff --git a/Client/src/Pages/Monitors/Home/MonitorTable/Skeleton/index.jsx b/Client/src/Pages/Monitors/Home/MonitorTable/Skeleton/index.jsx new file mode 100644 index 000000000..186b89725 --- /dev/null +++ b/Client/src/Pages/Monitors/Home/MonitorTable/Skeleton/index.jsx @@ -0,0 +1,31 @@ +import { Skeleton, TableBody, TableCell, TableRow } from "@mui/material"; +const ROWS_NUMBER = 7; +const ROWS_ARRAY = Array.from({ length: ROWS_NUMBER }, (_, i) => i); + +const TableBodySkeleton = () => { + return ( + + {ROWS_ARRAY.map((row) => ( + + + + + + + + + + + + + + + + + + ))} + + ); +}; + +export { TableBodySkeleton }; diff --git a/Client/src/Pages/Monitors/Home/MonitorTable/index.jsx b/Client/src/Pages/Monitors/Home/MonitorTable/index.jsx index b86a739e0..4e8f5100b 100644 --- a/Client/src/Pages/Monitors/Home/MonitorTable/index.jsx +++ b/Client/src/Pages/Monitors/Home/MonitorTable/index.jsx @@ -1,17 +1,17 @@ import PropTypes from "prop-types"; import { - TableContainer, - Table, - TableHead, - TableRow, - TableCell, - TableBody, - Paper, - Box, - TablePagination, - Stack, - Typography, - Button, + TableContainer, + Table, + TableHead, + TableRow, + TableCell, + TableBody, + Paper, + Box, + TablePagination, + Stack, + Typography, + Button, } from "@mui/material"; import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded"; import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded"; @@ -34,6 +34,7 @@ import RightArrow from "../../../../assets/icons/right-arrow.svg?react"; import SelectorVertical from "../../../../assets/icons/selector-vertical.svg?react"; import ActionsMenu from "../actionsMenu"; import useUtils from "../../utils"; +import { TableBodySkeleton } from "./Skeleton"; /** * Component for pagination actions (first, previous, next, last). @@ -48,386 +49,385 @@ import useUtils from "../../utils"; * @returns {JSX.Element} Pagination actions component. */ const TablePaginationActions = (props) => { - const { count, page, rowsPerPage, onPageChange } = props; - const handleFirstPageButtonClick = (event) => { - onPageChange(event, 0); - }; - const handleBackButtonClick = (event) => { - onPageChange(event, page - 1); - }; - const handleNextButtonClick = (event) => { - onPageChange(event, page + 1); - }; - const handleLastPageButtonClick = (event) => { - onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1)); - }; + const { count, page, rowsPerPage, onPageChange } = props; + const handleFirstPageButtonClick = (event) => { + onPageChange(event, 0); + }; + const handleBackButtonClick = (event) => { + onPageChange(event, page - 1); + }; + const handleNextButtonClick = (event) => { + onPageChange(event, page + 1); + }; + const handleLastPageButtonClick = (event) => { + onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1)); + }; - return ( - - - - - - - ); + return ( + + + + + + + ); }; TablePaginationActions.propTypes = { - count: PropTypes.number.isRequired, - page: PropTypes.number.isRequired, - rowsPerPage: PropTypes.number.isRequired, - onPageChange: PropTypes.func.isRequired, + count: PropTypes.number.isRequired, + page: PropTypes.number.isRequired, + rowsPerPage: PropTypes.number.isRequired, + onPageChange: PropTypes.func.isRequired, }; const MonitorTable = ({ isAdmin, filter, setLoading }) => { - const theme = useTheme(); - const navigate = useNavigate(); - const dispatch = useDispatch(); - const { determineState } = useUtils(); + const theme = useTheme(); + const navigate = useNavigate(); + const dispatch = useDispatch(); + const { determineState } = useUtils(); - const { rowsPerPage } = useSelector((state) => state.ui.monitors); - const [page, setPage] = useState(0); - const [monitors, setMonitors] = useState([]); - const [monitorCount, setMonitorCount] = useState(0); - const authState = useSelector((state) => state.auth); - const [updateTrigger, setUpdateTrigger] = useState(false); - const [sort, setSort] = useState({}); - const prevFilter = useRef(filter); + const { rowsPerPage } = useSelector((state) => state.ui.monitors); + const authState = useSelector((state) => state.auth); + const [page, setPage] = useState(0); + const [monitors, setMonitors] = useState([]); + const [monitorCount, setMonitorCount] = useState(0); + const [updateTrigger, setUpdateTrigger] = useState(false); + const [sort, setSort] = useState({}); + const prevFilter = useRef(filter); - const handleActionMenuDelete = () => { - setUpdateTrigger((prev) => !prev); - }; + const handleActionMenuDelete = () => { + setUpdateTrigger((prev) => !prev); + }; - const handleChangePage = (event, newPage) => { - setPage(newPage); - }; + const handleChangePage = (event, newPage) => { + setPage(newPage); + }; - const handleChangeRowsPerPage = (event) => { - dispatch( - setRowsPerPage({ - value: parseInt(event.target.value, 10), - table: "monitors", - }) - ); - setPage(0); - }; + const handleChangeRowsPerPage = (event) => { + dispatch( + setRowsPerPage({ + value: parseInt(event.target.value, 10), + table: "monitors", + }) + ); + setPage(0); + }; - const fetchPage = useCallback(async () => { - try { - const { authToken } = authState; - const user = jwtDecode(authToken); - const res = await networkService.getMonitorsByTeamId({ - authToken, - teamId: user.teamId, - limit: 25, - types: ["http", "ping"], - status: null, - checkOrder: "desc", - normalize: true, - page: page, - rowsPerPage: rowsPerPage, - filter: filter, - field: sort.field, - order: sort.order, - }); - setMonitors(res?.data?.data?.monitors ?? []); - setMonitorCount(res?.data?.data?.monitorCount ?? 0); - setLoading(false); - } catch (error) { - logger.error(error); - } - }, [authState, page, rowsPerPage, filter, sort, setLoading]); + const fetchPage = useCallback(async () => { + try { + const { authToken } = authState; + const user = jwtDecode(authToken); + const res = await networkService.getMonitorsByTeamId({ + authToken, + teamId: user.teamId, + limit: 25, + types: ["http", "ping"], + status: null, + checkOrder: "desc", + normalize: true, + page: page, + rowsPerPage: rowsPerPage, + filter: filter, + field: sort.field, + order: sort.order, + }); + setMonitors(res?.data?.data?.monitors ?? []); + setMonitorCount(res?.data?.data?.monitorCount ?? 0); + setLoading(false); + } catch (error) { + logger.error(error); + } + }, [authState, page, rowsPerPage, filter, sort, setLoading]); - useEffect(() => { - fetchPage(); - }, [ - updateTrigger, - authState, - page, - rowsPerPage, - filter, - sort, - setLoading, - fetchPage, - ]); + useEffect(() => { + fetchPage(); + }, [updateTrigger, authState, page, rowsPerPage, filter, sort, setLoading, fetchPage]); - // Listen for changes in filter, if new value reset the page - useEffect(() => { - if (prevFilter.current !== filter) { - setPage(0); - fetchPage(); - } - prevFilter.current = filter; - }, [filter, fetchPage]); + // Listen for changes in filter, if new value reset the page + useEffect(() => { + if (prevFilter.current !== filter) { + setPage(0); + fetchPage(); + } + prevFilter.current = filter; + }, [filter, fetchPage]); - /** - * Helper function to calculate the range of displayed rows. - * @returns {string} - */ - const getRange = () => { - let start = page * rowsPerPage + 1; - let end = Math.min(page * rowsPerPage + rowsPerPage, monitorCount); - return `${start} - ${end}`; - }; + /** + * Helper function to calculate the range of displayed rows. + * @returns {string} + */ + const getRange = () => { + let start = page * rowsPerPage + 1; + let end = Math.min(page * rowsPerPage + rowsPerPage, monitorCount); + return `${start} - ${end}`; + }; - const handleSort = async (field) => { - let order = ""; - if (sort.field !== field) { - order = "desc"; - } else { - order = sort.order === "asc" ? "desc" : "asc"; - } - setSort({ field, order }); + const handleSort = async (field) => { + let order = ""; + if (sort.field !== field) { + order = "desc"; + } else { + order = sort.order === "asc" ? "desc" : "asc"; + } + setSort({ field, order }); - const { authToken } = authState; - const user = jwtDecode(authToken); + const { authToken } = authState; + const user = jwtDecode(authToken); - const res = await networkService.getMonitorsByTeamId({ - authToken, - teamId: user.teamId, - limit: 25, - types: ["http", "ping"], - status: null, - checkOrder: "desc", - normalize: true, - page: page, - rowsPerPage: rowsPerPage, - filter: null, - field: field, - order: order, - }); - setMonitors(res?.data?.data?.monitors ?? []); - setMonitorCount(res?.data?.data?.monitorCount ?? 0); - }; + const res = await networkService.getMonitorsByTeamId({ + authToken, + teamId: user.teamId, + limit: 25, + types: ["http", "ping"], + status: null, + checkOrder: "desc", + normalize: true, + page: page, + rowsPerPage: rowsPerPage, + filter: null, + field: field, + order: order, + }); + setMonitors(res?.data?.data?.monitors ?? []); + setMonitorCount(res?.data?.data?.monitorCount ?? 0); + }; - return ( - <> - - - - - handleSort("name")} - > - - Host - - {sort.order === "asc" ? ( - - ) : ( - - )} - - - - handleSort("status")} - > - {" "} - - {" "} - Status - - {sort.order === "asc" ? ( - - ) : ( - - )} - - - - Response Time - Type - Actions - - - - {monitors.map((monitor) => { - let uptimePercentage = ""; - let percentageColor = theme.palette.percentage.uptimeExcellent; + return ( + <> + +
+ + + handleSort("name")} + > + + Host + + {sort.order === "asc" ? ( + + ) : ( + + )} + + + + handleSort("status")} + > + {" "} + + {" "} + Status + + {sort.order === "asc" ? ( + + ) : ( + + )} + + + + Response Time + Type + Actions + + + {monitors.length > 0 ? ( + + {monitors.map((monitor) => { + let uptimePercentage = ""; + let percentageColor = theme.palette.percentage.uptimeExcellent; - // Determine uptime percentage and color based on the monitor's uptimePercentage value - if (monitor.uptimePercentage !== undefined) { - uptimePercentage = - monitor.uptimePercentage === 0 - ? "0" - : (monitor.uptimePercentage * 100).toFixed(2); + // Determine uptime percentage and color based on the monitor's uptimePercentage value + if (monitor.uptimePercentage !== undefined) { + uptimePercentage = + monitor.uptimePercentage === 0 + ? "0" + : (monitor.uptimePercentage * 100).toFixed(2); - percentageColor = - monitor.uptimePercentage < 0.25 - ? theme.palette.percentage.uptimePoor - : monitor.uptimePercentage < 0.5 - ? theme.palette.percentage.uptimeFair - : monitor.uptimePercentage < 0.75 - ? theme.palette.percentage.uptimeGood - : theme.palette.percentage.uptimeExcellent; - } + percentageColor = + monitor.uptimePercentage < 0.25 + ? theme.palette.percentage.uptimePoor + : monitor.uptimePercentage < 0.5 + ? theme.palette.percentage.uptimeFair + : monitor.uptimePercentage < 0.75 + ? theme.palette.percentage.uptimeGood + : theme.palette.percentage.uptimeExcellent; + } - const params = { - url: monitor.url, - title: monitor.name, - percentage: uptimePercentage, - percentageColor, - status: determineState(monitor), - }; + const params = { + url: monitor.url, + title: monitor.name, + percentage: uptimePercentage, + percentageColor, + status: determineState(monitor), + }; - return ( - { - navigate(`/monitors/${monitor._id}`); - }} - > - - - - - - - - - - - - {monitor.type} - - - - - - - ); - })} - -
-
- - - Showing {getRange()} of {monitorCount} monitor(s) - - - `Page ${page + 1} of ${Math.max(0, Math.ceil(count / rowsPerPage))}` - } - slotProps={{ - select: { - MenuProps: { - keepMounted: true, - disableScrollLock: true, - PaperProps: { - className: "pagination-dropdown", - sx: { - mt: 0, - mb: theme.spacing(2), - }, - }, - transformOrigin: { vertical: "bottom", horizontal: "left" }, - anchorOrigin: { vertical: "top", horizontal: "left" }, - sx: { mt: theme.spacing(-2) }, - }, - inputProps: { id: "pagination-dropdown" }, - IconComponent: SelectorVertical, - sx: { - ml: theme.spacing(4), - mr: theme.spacing(12), - minWidth: theme.spacing(20), - textAlign: "left", - "&.Mui-focused > div": { - backgroundColor: theme.palette.background.main, - }, - }, - }, - }} - sx={{ - mt: theme.spacing(6), - color: theme.palette.text.secondary, - "& svg path": { - stroke: theme.palette.text.tertiary, - strokeWidth: 1.3, - }, - "& .MuiSelect-select": { - border: 1, - borderColor: theme.palette.border.light, - borderRadius: theme.shape.borderRadius, - }, - }} - /> - - - ); + return ( + { + navigate(`/monitors/${monitor._id}`); + }} + > + + + + + + + + + + + {monitor.type} + + + + + + ); + })} + + ) : ( + + )} + + + + + Showing {getRange()} of {monitorCount} monitor(s) + + + `Page ${page + 1} of ${Math.max(0, Math.ceil(count / rowsPerPage))}` + } + slotProps={{ + select: { + MenuProps: { + keepMounted: true, + disableScrollLock: true, + PaperProps: { + className: "pagination-dropdown", + sx: { + mt: 0, + mb: theme.spacing(2), + }, + }, + transformOrigin: { vertical: "bottom", horizontal: "left" }, + anchorOrigin: { vertical: "top", horizontal: "left" }, + sx: { mt: theme.spacing(-2) }, + }, + inputProps: { id: "pagination-dropdown" }, + IconComponent: SelectorVertical, + sx: { + ml: theme.spacing(4), + mr: theme.spacing(12), + minWidth: theme.spacing(20), + textAlign: "left", + "&.Mui-focused > div": { + backgroundColor: theme.palette.background.main, + }, + }, + }, + }} + sx={{ + mt: theme.spacing(6), + color: theme.palette.text.secondary, + "& svg path": { + stroke: theme.palette.text.tertiary, + strokeWidth: 1.3, + }, + "& .MuiSelect-select": { + border: 1, + borderColor: theme.palette.border.light, + borderRadius: theme.shape.borderRadius, + }, + }} + /> + + + ); }; MonitorTable.propTypes = { - isAdmin: PropTypes.bool, - filter: PropTypes.string, - setLoading: PropTypes.func, + isAdmin: PropTypes.bool, + filter: PropTypes.string, + setLoading: PropTypes.func, }; const MemoizedMonitorTable = memo(MonitorTable); diff --git a/Client/src/Pages/Monitors/Home/index.jsx b/Client/src/Pages/Monitors/Home/index.jsx index d02d046ab..e52556bb9 100644 --- a/Client/src/Pages/Monitors/Home/index.jsx +++ b/Client/src/Pages/Monitors/Home/index.jsx @@ -20,9 +20,11 @@ const Monitors = ({ isAdmin }) => { const navigate = useNavigate(); const monitorState = useSelector((state) => state.uptimeMonitors); const authState = useSelector((state) => state.auth); + const dispatch = useDispatch({}); + + //TODO create components, and lower these states. const [search, setSearch] = useState(""); const [isSearching, setIsSearching] = useState(false); - const dispatch = useDispatch({}); const debouncedFilter = useDebounce(search, 500); const handleSearch = (value) => { @@ -34,13 +36,17 @@ const Monitors = ({ isAdmin }) => { dispatch(getUptimeMonitorsByTeamId(authState.authToken)); }, [authState.authToken, dispatch]); - console.log(monitorState); - const loading = true; - /* monitorState?.isLoading && - monitorState?.monitorsSummary?.monitors?.length === 0; */ - const canCreateMonitor = - isAdmin && monitorState?.monitorsSummary?.monitors?.length !== 0; + //Why are we tying loading to monitors length? + const loading = + monitorState?.isLoading; /* && monitorState?.monitorsSummary?.monitors?.length === 0 */ + const hasMonitors = monitorState?.monitorsSummary?.monitors?.length !== 0; + const noMonitors = monitorState?.monitorsSummary?.monitors?.length === 0; + const canAddMonitor = isAdmin && hasMonitors; + const needsAdmin = !isAdmin && noMonitors; + + /* console.log({ loading }); + console.log({ monitorState }); */ return ( { gap={theme.spacing(6)} > - {canCreateMonitor && ( + {canAddMonitor && ( - )} - - - - {monitors?.map((monitor) => ( - - ))} - - - ) : ( - - )} - - ); + // will show skeletons only on initial load + // since monitor state is being added to redux persist, there's no reason to display skeletons on every render + let isActuallyLoading = isLoading && monitors?.length === 0; + console.log({ isActuallyLoading }); + return ( + [class*="fallback__"])': { + position: "relative", + border: 1, + borderColor: theme.palette.border.light, + borderRadius: theme.shape.borderRadius, + borderStyle: "dashed", + backgroundColor: theme.palette.background.main, + overflow: "hidden", + }, + }} + > + + + + + {isAdmin && ( + + )} + + + {isActuallyLoading ? ( + + ) : monitors?.length !== 0 ? ( + + + {monitors?.map((monitor) => ( + + ))} + + + ) : ( + + )} + + ); }; PageSpeed.propTypes = { - isAdmin: PropTypes.bool, + isAdmin: PropTypes.bool, }; export default PageSpeed; From a20db6df51968541413e565873f652d72ea541ec Mon Sep 17 00:00:00 2001 From: Caio Cabral Date: Fri, 18 Oct 2024 17:35:49 -0400 Subject: [PATCH 08/16] fix: adding gap to page speed header --- Client/src/Pages/PageSpeed/index.jsx | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/Client/src/Pages/PageSpeed/index.jsx b/Client/src/Pages/PageSpeed/index.jsx index 2b5e60aad..5c751337a 100644 --- a/Client/src/Pages/PageSpeed/index.jsx +++ b/Client/src/Pages/PageSpeed/index.jsx @@ -82,6 +82,7 @@ const PageSpeed = ({ isAdmin }) => { justifyContent="space-between" alignItems="center" mt={theme.spacing(5)} + gap={theme.spacing(6)} > {isAdmin && ( @@ -99,19 +100,17 @@ const PageSpeed = ({ isAdmin }) => { {isActuallyLoading ? ( ) : monitors?.length !== 0 ? ( - - - {monitors?.map((monitor) => ( - - ))} - - + + {monitors?.map((monitor) => ( + + ))} + ) : ( Date: Fri, 18 Oct 2024 18:38:08 -0400 Subject: [PATCH 09/16] feat: incident skeleton adjustement --- .../Incidents/IncidentTable/Empty/Empty.jsx | 32 ++ .../IncidentTable/Skeleton/Skeleton.jsx | 21 + .../Pages/Incidents/IncidentTable/index.jsx | 366 +++++++++--------- Client/src/Pages/Incidents/index.jsx | 226 ++++++----- Client/src/Pages/Maintenance/index.jsx | 188 ++++----- 5 files changed, 457 insertions(+), 376 deletions(-) create mode 100644 Client/src/Pages/Incidents/IncidentTable/Empty/Empty.jsx create mode 100644 Client/src/Pages/Incidents/IncidentTable/Skeleton/Skeleton.jsx diff --git a/Client/src/Pages/Incidents/IncidentTable/Empty/Empty.jsx b/Client/src/Pages/Incidents/IncidentTable/Empty/Empty.jsx new file mode 100644 index 000000000..b8f6a0986 --- /dev/null +++ b/Client/src/Pages/Incidents/IncidentTable/Empty/Empty.jsx @@ -0,0 +1,32 @@ +import { useTheme } from "@emotion/react"; +import PlaceholderLight from "../../../../assets/Images/data_placeholder.svg?react"; +import PlaceholderDark from "../../../../assets/Images/data_placeholder_dark.svg?react"; +import { Box, Typography } from "@mui/material"; +import PropTypes from "prop-types"; + +const Empty = ({ styles, mode }) => { + const theme = useTheme(); + return ( + + + {mode === "light" ? : } + + + No incidents recorded yet. + + + ); +}; + +Empty.propTypes = { + styles: PropTypes.object, + mode: PropTypes.string, +}; + +export { Empty }; diff --git a/Client/src/Pages/Incidents/IncidentTable/Skeleton/Skeleton.jsx b/Client/src/Pages/Incidents/IncidentTable/Skeleton/Skeleton.jsx new file mode 100644 index 000000000..a3a23c14c --- /dev/null +++ b/Client/src/Pages/Incidents/IncidentTable/Skeleton/Skeleton.jsx @@ -0,0 +1,21 @@ +import { Skeleton /* , Stack */ } from "@mui/material"; +const IncidentSkeleton = () => { + return ( + <> + + + + ); +}; + +export { IncidentSkeleton }; diff --git a/Client/src/Pages/Incidents/IncidentTable/index.jsx b/Client/src/Pages/Incidents/IncidentTable/index.jsx index ef8356031..da533c7ba 100644 --- a/Client/src/Pages/Incidents/IncidentTable/index.jsx +++ b/Client/src/Pages/Incidents/IncidentTable/index.jsx @@ -1,16 +1,16 @@ import PropTypes from "prop-types"; import { - TableContainer, - Table, - TableHead, - TableRow, - TableCell, - TableBody, - Pagination, - PaginationItem, - Paper, - Typography, - Box, + TableContainer, + Table, + TableHead, + TableRow, + TableCell, + TableBody, + Pagination, + PaginationItem, + Paper, + Typography, + Box, } from "@mui/material"; import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded"; @@ -24,185 +24,197 @@ import { useTheme } from "@emotion/react"; import { formatDateWithTz } from "../../../Utils/timeUtils"; import PlaceholderLight from "../../../assets/Images/data_placeholder.svg?react"; import PlaceholderDark from "../../../assets/Images/data_placeholder_dark.svg?react"; +import { Empty } from "./Empty/Empty"; +import { IncidentSkeleton } from "./Skeleton/Skeleton"; const IncidentTable = ({ monitors, selectedMonitor, filter }) => { - const uiTimezone = useSelector((state) => state.ui.timezone); + const uiTimezone = useSelector((state) => state.ui.timezone); - const theme = useTheme(); - const { authToken, user } = useSelector((state) => state.auth); - const mode = useSelector((state) => state.ui.mode); - const [checks, setChecks] = useState([]); - const [checksCount, setChecksCount] = useState(0); - const [paginationController, setPaginationController] = useState({ - page: 0, - rowsPerPage: 14, - }); + const theme = useTheme(); + const { authToken, user } = useSelector((state) => state.auth); + const mode = useSelector((state) => state.ui.mode); + const [checks, setChecks] = useState([]); + const [checksCount, setChecksCount] = useState(0); + const [paginationController, setPaginationController] = useState({ + page: 0, + rowsPerPage: 14, + }); + const [isLoading, setIsLoading] = useState(true); - useEffect(() => { - setPaginationController((prevPaginationController) => ({ - ...prevPaginationController, - page: 0, - })); - }, [filter, selectedMonitor]); + useEffect(() => { + setPaginationController((prevPaginationController) => ({ + ...prevPaginationController, + page: 0, + })); + }, [filter, selectedMonitor]); - useEffect(() => { - const fetchPage = async () => { - if (!monitors || Object.keys(monitors).length === 0) { - return; - } - try { - let res; - if (selectedMonitor === "0") { - res = await networkService.getChecksByTeam({ - authToken: authToken, - teamId: user.teamId, - sortOrder: "desc", - limit: null, - dateRange: null, - filter: filter, - page: paginationController.page, - rowsPerPage: paginationController.rowsPerPage, - }); - } else { - res = await networkService.getChecksByMonitor({ - authToken: authToken, - monitorId: selectedMonitor, - sortOrder: "desc", - limit: null, - dateRange: null, - sitler: filter, - page: paginationController.page, - rowsPerPage: paginationController.rowsPerPage, - }); - } - setChecks(res.data.data.checks); - setChecksCount(res.data.data.checksCount); - } catch (error) { - logger.error(error); - } - }; - fetchPage(); - }, [ - authToken, - user, - monitors, - selectedMonitor, - filter, - paginationController.page, - paginationController.rowsPerPage, - ]); + useEffect(() => { + const fetchPage = async () => { + if (!monitors || Object.keys(monitors).length === 0) { + return; + } + try { + setIsLoading(true); + let res; + if (selectedMonitor === "0") { + res = await networkService.getChecksByTeam({ + authToken: authToken, + teamId: user.teamId, + sortOrder: "desc", + limit: null, + dateRange: null, + filter: filter, + page: paginationController.page, + rowsPerPage: paginationController.rowsPerPage, + }); + } else { + res = await networkService.getChecksByMonitor({ + authToken: authToken, + monitorId: selectedMonitor, + sortOrder: "desc", + limit: null, + dateRange: null, + sitler: filter, + page: paginationController.page, + rowsPerPage: paginationController.rowsPerPage, + }); + } + setChecks(res.data.data.checks); + setChecksCount(res.data.data.checksCount); + } catch (error) { + logger.error(error); + } finally { + setIsLoading(false); + } + }; + fetchPage(); + }, [ + authToken, + user, + monitors, + selectedMonitor, + filter, + paginationController.page, + paginationController.rowsPerPage, + ]); - const handlePageChange = (_, newPage) => { - setPaginationController({ - ...paginationController, - page: newPage - 1, // 0-indexed - }); - }; + const handlePageChange = (_, newPage) => { + setPaginationController({ + ...paginationController, + page: newPage - 1, // 0-indexed + }); + }; - let paginationComponent = <>; - if (checksCount > paginationController.rowsPerPage) { - paginationComponent = ( - ( - - )} - sx={{ mt: "auto" }} - /> - ); - } + let paginationComponent = <>; + if (checksCount > paginationController.rowsPerPage) { + paginationComponent = ( + ( + + )} + sx={{ mt: "auto" }} + /> + ); + } - let sharedStyles = { - border: 1, - borderColor: theme.palette.border.light, - borderRadius: theme.shape.borderRadius, - backgroundColor: theme.palette.background.main, - p: theme.spacing(30), - }; + let sharedStyles = { + border: 1, + borderColor: theme.palette.border.light, + borderRadius: theme.shape.borderRadius, + backgroundColor: theme.palette.background.main, + p: theme.spacing(30), + }; - return ( - <> - {checks?.length === 0 && selectedMonitor === "0" ? ( - - - {mode === "light" ? : } - - - No incidents recorded yet. - - - ) : checks?.length === 0 ? ( - - - {mode === "light" ? : } - - - The monitor you have selected has no recorded incidents yet. - - - ) : ( - <> - - - - - Monitor Name - Status - Date & Time - Status Code - Message - - - - {checks.map((check) => { - const status = check.status === true ? "up" : "down"; - const formattedDate = formatDateWithTz( - check.createdAt, - "YYYY-MM-DD HH:mm:ss A", - uiTimezone - ); + const hasChecks = checks?.length === 0; + const noIncidentsRecordedYet = hasChecks && selectedMonitor === "0"; + const noIncidentsForThatMonitor = hasChecks && selectedMonitor !== "0"; - return ( - - {monitors[check.monitorId]?.name} - - - - {formattedDate} - - {check.statusCode ? check.statusCode : "N/A"} - - {check.message} - - ); - })} - -
-
- {paginationComponent} - - )} - - ); + return ( + <> + {isLoading ? ( + + ) : noIncidentsRecordedYet ? ( + + ) : noIncidentsForThatMonitor ? ( + + + {mode === "light" ? : } + + + The monitor you have selected has no recorded incidents yet. + + + ) : ( + <> + + + + + Monitor Name + Status + Date & Time + Status Code + Message + + + + {checks.map((check) => { + const status = check.status === true ? "up" : "down"; + const formattedDate = formatDateWithTz( + check.createdAt, + "YYYY-MM-DD HH:mm:ss A", + uiTimezone + ); + + return ( + + {monitors[check.monitorId]?.name} + + + + {formattedDate} + {check.statusCode ? check.statusCode : "N/A"} + {check.message} + + ); + })} + +
+
+ {paginationComponent} + + )} + + ); }; IncidentTable.propTypes = { - monitors: PropTypes.object.isRequired, - selectedMonitor: PropTypes.string.isRequired, - filter: PropTypes.string.isRequired, + monitors: PropTypes.object.isRequired, + selectedMonitor: PropTypes.string.isRequired, + filter: PropTypes.string.isRequired, }; export default IncidentTable; diff --git a/Client/src/Pages/Incidents/index.jsx b/Client/src/Pages/Incidents/index.jsx index 10ab30ad5..31f853e34 100644 --- a/Client/src/Pages/Incidents/index.jsx +++ b/Client/src/Pages/Incidents/index.jsx @@ -11,119 +11,133 @@ import SkeletonLayout from "./skeleton"; import "./index.css"; const Incidents = () => { - const theme = useTheme(); - const authState = useSelector((state) => state.auth); - const { monitorId } = useParams(); + const theme = useTheme(); + const authState = useSelector((state) => state.auth); + const { monitorId } = useParams(); - const [monitors, setMonitors] = useState({}); - const [selectedMonitor, setSelectedMonitor] = useState("0"); - const [loading, setLoading] = useState(false); + const [monitors, setMonitors] = useState({}); + const [selectedMonitor, setSelectedMonitor] = useState("0"); + const [isLoading, setIsLoading] = useState(true); - // TODO do something with these filters - const [filter, setFilter] = useState("all"); + // TODO do something with these filters + const [filter, setFilter] = useState("all"); - useEffect(() => { - const fetchMonitors = async () => { - setLoading(true); - const res = await networkService.getMonitorsByTeamId({ - authToken: authState.authToken, - teamId: authState.user.teamId, - limit: -1, - types: null, - status: null, - checkOrder: null, - normalize: null, - page: null, - rowsPerPage: null, - filter: null, - field: null, - order: null, - }); - // Reduce to a lookup object for 0(1) lookup - if (res?.data?.data?.monitors?.length > 0) { - const monitorLookup = res.data.data.monitors.reduce((acc, monitor) => { - acc[monitor._id] = monitor; - return acc; - }, {}); - setMonitors(monitorLookup); - monitorId !== undefined && setSelectedMonitor(monitorId); - } - setLoading(false); - }; + useEffect(() => { + const fetchMonitors = async () => { + try { + setIsLoading(true); + const res = await networkService.getMonitorsByTeamId({ + authToken: authState.authToken, + teamId: authState.user.teamId, + limit: -1, + types: null, + status: null, + checkOrder: null, + normalize: null, + page: null, + rowsPerPage: null, + filter: null, + field: null, + order: null, + }); + // Reduce to a lookup object for 0(1) lookup + if (res?.data?.data?.monitors?.length > 0) { + const monitorLookup = res.data.data.monitors.reduce((acc, monitor) => { + acc[monitor._id] = monitor; + return acc; + }, {}); + setMonitors(monitorLookup); + monitorId !== undefined && setSelectedMonitor(monitorId); + } + } catch (error) { + console.info(error); + } finally { + setIsLoading(false); + } + }; + fetchMonitors(); + }, [authState]); - fetchMonitors(); - }, [authState]); + useEffect(() => {}, [monitors]); - useEffect(() => {}, []); + const handleSelect = (event) => { + setSelectedMonitor(event.target.value); + }; - const handleSelect = (event) => { - setSelectedMonitor(event.target.value); - }; + const isActuallyLoading = isLoading && Object.keys(monitors)?.length === 0; - return ( - - {loading ? ( - - ) : ( - <> - - - Incidents for - - + + + + + + + + + )} + + ); }; export default Incidents; diff --git a/Client/src/Pages/Maintenance/index.jsx b/Client/src/Pages/Maintenance/index.jsx index e69711eec..2fc688bb2 100644 --- a/Client/src/Pages/Maintenance/index.jsx +++ b/Client/src/Pages/Maintenance/index.jsx @@ -10,103 +10,105 @@ import Breadcrumbs from "../../Components/Breadcrumbs"; import { useNavigate } from "react-router-dom"; const Maintenance = ({ isAdmin }) => { - const theme = useTheme(); - const navigate = useNavigate(); - const { authToken } = useSelector((state) => state.auth); - const { rowsPerPage } = useSelector((state) => state.ui.maintenance); + const theme = useTheme(); + const navigate = useNavigate(); + const { authToken } = useSelector((state) => state.auth); + const { rowsPerPage } = useSelector((state) => state.ui.maintenance); - const [maintenanceWindows, setMaintenanceWindows] = useState([]); - const [maintenanceWindowCount, setMaintenanceWindowCount] = useState(0); - const [page, setPage] = useState(0); - const [sort, setSort] = useState({}); - const [updateTrigger, setUpdateTrigger] = useState(false); + const [maintenanceWindows, setMaintenanceWindows] = useState([]); + const [maintenanceWindowCount, setMaintenanceWindowCount] = useState(0); + const [page, setPage] = useState(0); + const [sort, setSort] = useState({}); + const [updateTrigger, setUpdateTrigger] = useState(false); + const [isLoading, setIsLoading] = useState(true); - const handleActionMenuDelete = () => { - setUpdateTrigger((prev) => !prev); - }; + const handleActionMenuDelete = () => { + setUpdateTrigger((prev) => !prev); + }; - useEffect(() => { - const fetchMaintenanceWindows = async () => { - try { - const response = await networkService.getMaintenanceWindowsByTeamId({ - authToken: authToken, - page: page, - rowsPerPage: rowsPerPage, - }); - const { maintenanceWindows, maintenanceWindowCount } = - response.data.data; - setMaintenanceWindows(maintenanceWindows); - setMaintenanceWindowCount(maintenanceWindowCount); - } catch (error) { - console.log(error); - } - }; - fetchMaintenanceWindows(); - }, [authToken, page, rowsPerPage, updateTrigger]); + useEffect(() => { + const fetchMaintenanceWindows = async () => { + try { + setIsLoading(true); + const response = await networkService.getMaintenanceWindowsByTeamId({ + authToken: authToken, + page: page, + rowsPerPage: rowsPerPage, + }); + const { maintenanceWindows, maintenanceWindowCount } = response.data.data; + setMaintenanceWindows(maintenanceWindows); + setMaintenanceWindowCount(maintenanceWindowCount); + } catch (error) { + console.log(error); + } finally { + setIsLoading(false); + } + }; + fetchMaintenanceWindows(); + }, [authToken, page, rowsPerPage, updateTrigger]); - return ( - [class*="fallback__"])': { - position: "relative", - border: 1, - borderColor: theme.palette.border.light, - borderRadius: theme.shape.borderRadius, - borderStyle: "dashed", - backgroundColor: theme.palette.background.main, - overflow: "hidden", - }, - }} - > - {maintenanceWindows.length > 0 && ( - - - - - - - - )} - {maintenanceWindows.length === 0 && ( - - )} - - ); + const isActuallyLoading = isLoading && maintenanceWindows?.length === 0; + return ( + [class*="fallback__"])': { + position: "relative", + border: 1, + borderColor: theme.palette.border.light, + borderRadius: theme.shape.borderRadius, + borderStyle: "dashed", + backgroundColor: theme.palette.background.main, + overflow: "hidden", + }, + }} + > + {maintenanceWindows.length > 0 && ( + + + + + + + + )} + {maintenanceWindows.length === 0 && ( + + )} + + ); }; export default Maintenance; From 74b63d05530c96fdac9cdb79cf8c0bcfd0146ac4 Mon Sep 17 00:00:00 2001 From: Caio Cabral Date: Fri, 18 Oct 2024 18:39:46 -0400 Subject: [PATCH 10/16] chore: cleaning unused code from maintenance --- Client/src/Pages/Maintenance/index.jsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Client/src/Pages/Maintenance/index.jsx b/Client/src/Pages/Maintenance/index.jsx index 2fc688bb2..399308116 100644 --- a/Client/src/Pages/Maintenance/index.jsx +++ b/Client/src/Pages/Maintenance/index.jsx @@ -20,7 +20,6 @@ const Maintenance = ({ isAdmin }) => { const [page, setPage] = useState(0); const [sort, setSort] = useState({}); const [updateTrigger, setUpdateTrigger] = useState(false); - const [isLoading, setIsLoading] = useState(true); const handleActionMenuDelete = () => { setUpdateTrigger((prev) => !prev); @@ -29,7 +28,6 @@ const Maintenance = ({ isAdmin }) => { useEffect(() => { const fetchMaintenanceWindows = async () => { try { - setIsLoading(true); const response = await networkService.getMaintenanceWindowsByTeamId({ authToken: authToken, page: page, @@ -40,14 +38,11 @@ const Maintenance = ({ isAdmin }) => { setMaintenanceWindowCount(maintenanceWindowCount); } catch (error) { console.log(error); - } finally { - setIsLoading(false); } }; fetchMaintenanceWindows(); }, [authToken, page, rowsPerPage, updateTrigger]); - const isActuallyLoading = isLoading && maintenanceWindows?.length === 0; return ( Date: Sun, 20 Oct 2024 02:22:01 +0000 Subject: [PATCH 11/16] Add renovate.json --- renovate.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 renovate.json diff --git a/renovate.json b/renovate.json new file mode 100644 index 000000000..5db72dd6a --- /dev/null +++ b/renovate.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:recommended" + ] +} From 5dda7cdde71308ea3c0bd2fb492ffb3a60b33932 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 21 Oct 2024 10:33:13 +0800 Subject: [PATCH 12/16] Update dist build script --- Docker/dist/build_images.sh | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/Docker/dist/build_images.sh b/Docker/dist/build_images.sh index 1df514e7e..a5ec3eec9 100755 --- a/Docker/dist/build_images.sh +++ b/Docker/dist/build_images.sh @@ -1,24 +1,26 @@ - #!/bin/bash # Change directory to root Server directory for correct Docker Context +cd "$(dirname "$0")" cd ../.. -#Client -client="./Docker/dist/client.Dockerfile" +# Define an array of services and their Dockerfiles +declare -A services=( + ["bluewave/uptime_client"]="./Docker/dist/client.Dockerfile" + ["bluewave/database_mongo"]="./Docker/dist/mongoDB.Dockerfile" + ["bluewave/uptime_redis"]="./Docker/dist/redis.Dockerfile" + ["bluewave/uptime_server"]="./Docker/dist/server.Dockerfile" +) -# MongoDB -mongoDB="./Docker/dist/mongoDB.Dockerfile" +# Loop through each service and build the corresponding image +for service in "${!services[@]}"; do + docker build -f "${services[$service]}" -t "$service" . + + # Check if the build succeeded + if [ $? -ne 0 ]; then + echo "Error building $service image. Exiting..." + exit 1 + fi +done -# Redis -redis="./Docker/dist/redis.Dockerfile" - -# Server -server="./Docker/dist/server.Dockerfile" - -docker build -f $client -t dist_uptime_client . -docker build -f $mongoDB -t dist_uptime_database_mongo . -docker build -f $redis -t dist_uptime_redis . -docker build -f $server -t dist_uptime_server . - -echo "All images built" \ No newline at end of file +echo "All images built successfully" \ No newline at end of file From dd235f0d8109520b89fd849e1ea3f23d8d741eef Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 21 Oct 2024 10:35:33 +0800 Subject: [PATCH 13/16] Update production build script --- Docker/prod/build_images.sh | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/Docker/prod/build_images.sh b/Docker/prod/build_images.sh index 98d77e660..c879190bc 100755 --- a/Docker/prod/build_images.sh +++ b/Docker/prod/build_images.sh @@ -1,24 +1,26 @@ - #!/bin/bash # Change directory to root directory for correct Docker Context +cd "$(dirname "$0")" cd ../.. -#Client -client="./Docker/prod/client.Dockerfile" +# Define an array of services and their Dockerfiles +declare -A services=( + ["uptime_client"]="./Docker/prod/client.Dockerfile" + ["uptime_database_mongo"]="./Docker/prod/mongoDB.Dockerfile" + ["uptime_redis"]="./Docker/prod/redis.Dockerfile" + ["uptime_server"]="./Docker/prod/server.Dockerfile" +) -# MongoDB -mongoDB="./Docker/prod/mongoDB.Dockerfile" +# Loop through each service and build the corresponding image +for service in "${!services[@]}"; do + docker build -f "${services[$service]}" -t "$service" . + + # Check if the build succeeded + if [ $? -ne 0 ]; then + echo "Error building $service image. Exiting..." + exit 1 + fi +done -# Redis -redis="./Docker/prod/redis.Dockerfile" - -# Server -server="./Docker/prod/server.Dockerfile" - -docker build -f $client -t uptime_client . -docker build -f $mongoDB -t uptime_database_mongo . -docker build -f $redis -t uptime_redis . -docker build -f $server -t uptime_server . - -echo "All images built" \ No newline at end of file +echo "All images built successfully" \ No newline at end of file From 8ec2f2f16b916eb15cd5d092da768e29b55e6da2 Mon Sep 17 00:00:00 2001 From: Shemy Gan Date: Mon, 21 Oct 2024 10:02:36 -0400 Subject: [PATCH 14/16] - Update based on review --- Client/src/Validation/error.js | 4 ++-- Client/src/Validation/validation.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Client/src/Validation/error.js b/Client/src/Validation/error.js index 0e675f413..e205ce7d1 100644 --- a/Client/src/Validation/error.js +++ b/Client/src/Validation/error.js @@ -1,7 +1,7 @@ const buildErrors = (prev, id, error) => { const updatedErrors = { ...prev }; if (error) { - updatedErrors[id] = error.details[0].message; + updatedErrors[id] = error.details[0].message?? "Validation error"; } else { delete updatedErrors[id]; } @@ -15,7 +15,7 @@ const hasValidationErrors = (form, validation, setErrors) => { if (error) { const newErrors = {}; error.details.forEach((err) => { - newErrors[err.path[0]] = err.message; + newErrors[err.path[0]] = err.message?? "Validation error"; }); setErrors(newErrors); return true; diff --git a/Client/src/Validation/validation.js b/Client/src/Validation/validation.js index 1a9742e21..a6ec7688c 100644 --- a/Client/src/Validation/validation.js +++ b/Client/src/Validation/validation.js @@ -133,7 +133,7 @@ const advancedSettingsValidation = joi.object({ "string.empty": "API base url is required.", "string.uri": "The URL you provided is not valid.", }), - logLevel: joi.string(), + logLevel: joi.string().valid("debug", "none", "error", "warn").allow(""), systemEmailHost: joi.string().allow(''), systemEmailPort: joi.string().allow(''), systemEmailAddress: joi From d28c406c883717a225f132785bb16dc99ad7d402 Mon Sep 17 00:00:00 2001 From: Caio Cabral Date: Mon, 21 Oct 2024 19:40:13 -0400 Subject: [PATCH 15/16] feat: simplifying states --- Client/src/Pages/Monitors/Home/index.jsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Client/src/Pages/Monitors/Home/index.jsx b/Client/src/Pages/Monitors/Home/index.jsx index 8bd1d7275..345a0a88e 100644 --- a/Client/src/Pages/Monitors/Home/index.jsx +++ b/Client/src/Pages/Monitors/Home/index.jsx @@ -29,11 +29,10 @@ const Monitors = ({ isAdmin }) => { const loading = monitorState?.isLoading; const totalMonitors = monitorState?.monitorsSummary?.monitorCounts?.total; - const isTotalMonitorsUndefined = totalMonitors === undefined; - const hasMonitors = !isTotalMonitorsUndefined && totalMonitors !== 0; - const noMonitors = isTotalMonitorsUndefined || totalMonitors === 0; + + const hasMonitors = totalMonitors > 0; + const noMonitors = !hasMonitors; const canAddMonitor = isAdmin && hasMonitors; - const needsAdmin = !isAdmin && noMonitors; return ( { {noMonitors && } - {needsAdmin &&

Wait for an admin to add some monitors!

} {loading ? ( ) : ( From 433dbf19073aa317dd5ca2284e0ad5f67edfd95e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 22 Oct 2024 10:02:14 +0800 Subject: [PATCH 16/16] Revise disk field from diskSchema to [diskSchema] --- Server/db/models/HardwareCheck.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Server/db/models/HardwareCheck.js b/Server/db/models/HardwareCheck.js index a9e8485f1..b710087bf 100644 --- a/Server/db/models/HardwareCheck.js +++ b/Server/db/models/HardwareCheck.js @@ -46,8 +46,8 @@ const HardwareCheckSchema = mongoose.Schema( default: () => ({}), }, disk: { - type: discSchema, - default: () => ({}), + type: [discSchema], + default: () => [], }, host: { type: hostSchema,