diff --git a/Client/src/Components/Inputs/Search/index.jsx b/Client/src/Components/Inputs/Search/index.jsx index a71732e3f..3de2cbe07 100644 --- a/Client/src/Components/Inputs/Search/index.jsx +++ b/Client/src/Components/Inputs/Search/index.jsx @@ -38,6 +38,7 @@ const SearchAdornment = () => { ); }; +//TODO keep search state inside of component const Search = ({ id, options, 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/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 1558084a4..da533c7ba 100644 --- a/Client/src/Pages/Incidents/IncidentTable/index.jsx +++ b/Client/src/Pages/Incidents/IncidentTable/index.jsx @@ -24,6 +24,8 @@ 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); @@ -37,6 +39,7 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { page: 0, rowsPerPage: 14, }); + const [isLoading, setIsLoading] = useState(true); useEffect(() => { setPaginationController((prevPaginationController) => ({ @@ -51,6 +54,7 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { return; } try { + setIsLoading(true); let res; if (selectedMonitor === "0") { res = await networkService.getChecksByTeam({ @@ -79,6 +83,8 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { setChecksCount(res.data.data.checksCount); } catch (error) { logger.error(error); + } finally { + setIsLoading(false); } }; fetchPage(); @@ -129,24 +135,20 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { p: theme.spacing(30), }; + const hasChecks = checks?.length === 0; + const noIncidentsRecordedYet = hasChecks && selectedMonitor === "0"; + const noIncidentsForThatMonitor = hasChecks && selectedMonitor !== "0"; + return ( <> - {checks?.length === 0 && selectedMonitor === "0" ? ( - - - {mode === "light" ? : } - - - No incidents recorded yet. - - - ) : checks?.length === 0 ? ( + {isLoading ? ( + + ) : noIncidentsRecordedYet ? ( + + ) : noIncidentsForThatMonitor ? ( { const [monitors, setMonitors] = useState({}); const [selectedMonitor, setSelectedMonitor] = useState("0"); - const [loading, setLoading] = useState(false); + const [isLoading, setIsLoading] = useState(true); // 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); + 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); } - setLoading(false); }; - fetchMonitors(); }, [authState]); - useEffect(() => {}, []); + useEffect(() => {}, [monitors]); const handleSelect = (event) => { setSelectedMonitor(event.target.value); }; + const isActuallyLoading = isLoading && Object.keys(monitors)?.length === 0; + return ( - {loading ? ( + {isActuallyLoading ? ( ) : ( <> 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/Pages/Monitors/Home/CurrentMonitoring/index.jsx b/Client/src/Pages/Monitors/Home/CurrentMonitoring/index.jsx new file mode 100644 index 000000000..613c280a4 --- /dev/null +++ b/Client/src/Pages/Monitors/Home/CurrentMonitoring/index.jsx @@ -0,0 +1,79 @@ +import { useTheme } from "@emotion/react"; +import { Box, Stack, Typography } from "@mui/material"; +import Search from "../../../../Components/Inputs/Search"; +import MemoizedMonitorTable from "../MonitorTable"; +import { useState } from "react"; +import useDebounce from "../../../../Utils/debounce"; +import PropTypes from "prop-types"; + +const CurrentMonitoring = ({ totalMonitors, monitors, isAdmin }) => { + const theme = useTheme(); + const [search, setSearch] = useState(""); + const [isSearching, setIsSearching] = useState(false); + const debouncedFilter = useDebounce(search, 500); + const handleSearch = (value) => { + setIsSearching(true); + setSearch(value); + }; + return ( + + + + Actively monitoring + + + {totalMonitors} + + + + + + + + ); +}; + +CurrentMonitoring.propTypes = { + totalMonitors: PropTypes.number, + monitors: PropTypes.array, + isAdmin: PropTypes.bool, +}; + +export { CurrentMonitoring }; 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..e30e0a255 --- /dev/null +++ b/Client/src/Pages/Monitors/Home/MonitorTable/Skeleton/index.jsx @@ -0,0 +1,31 @@ +import { Skeleton, 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 4b0765b76..d9fbc34b2 100644 --- a/Client/src/Pages/Monitors/Home/MonitorTable/index.jsx +++ b/Client/src/Pages/Monitors/Home/MonitorTable/index.jsx @@ -12,6 +12,7 @@ import { Stack, Typography, Button, + CircularProgress, } from "@mui/material"; import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded"; import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded"; @@ -34,6 +35,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). @@ -107,17 +109,17 @@ TablePaginationActions.propTypes = { onPageChange: PropTypes.func.isRequired, }; -const MonitorTable = ({ isAdmin, filter, setLoading }) => { +const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching }) => { const theme = useTheme(); const navigate = useNavigate(); const dispatch = useDispatch(); const { determineState } = useUtils(); 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 authState = useSelector((state) => state.auth); const [updateTrigger, setUpdateTrigger] = useState(false); const [sort, setSort] = useState({}); const prevFilter = useRef(filter); @@ -160,15 +162,25 @@ const MonitorTable = ({ isAdmin, filter, setLoading }) => { }); setMonitors(res?.data?.data?.monitors ?? []); setMonitorCount(res?.data?.data?.monitorCount ?? 0); - setLoading(false); } catch (error) { logger.error(error); + } finally { + setIsSearching(false); } - }, [authState, page, rowsPerPage, filter, sort, setLoading]); + }, [authState, page, rowsPerPage, filter, sort, setIsSearching]); useEffect(() => { fetchPage(); - }, [updateTrigger, authState, page, rowsPerPage, filter, sort, setLoading, fetchPage]); + }, [ + updateTrigger, + authState, + page, + rowsPerPage, + filter, + sort, + setIsSearching, + fetchPage, + ]); // Listen for changes in filter, if new value reset the page useEffect(() => { @@ -220,7 +232,37 @@ const MonitorTable = ({ isAdmin, filter, setLoading }) => { }; return ( - <> + + {isSearching && ( + <> + + + + + + )} @@ -271,77 +313,82 @@ const MonitorTable = ({ isAdmin, filter, setLoading }) => { - {monitors.map((monitor) => { - let uptimePercentage = ""; - let percentageColor = theme.palette.percentage.uptimeExcellent; + {/* TODO add empty state. Check if is searching, and empty => skeleton. Is empty, not searching => skeleton */} + {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} - - - - - - ); - })} + return ( + { + navigate(`/monitors/${monitor._id}`); + }} + > + + + + + + + + + + + {monitor.type} + + + + + + ); + }) + ) : ( + + )}
@@ -415,14 +462,15 @@ const MonitorTable = ({ isAdmin, filter, setLoading }) => { }} />
- +
); }; MonitorTable.propTypes = { isAdmin: PropTypes.bool, filter: PropTypes.string, - setLoading: PropTypes.func, + setIsSearching: PropTypes.func, + isSearching: PropTypes.bool, }; const MemoizedMonitorTable = memo(MonitorTable); diff --git a/Client/src/Pages/Monitors/Home/index.jsx b/Client/src/Pages/Monitors/Home/index.jsx index 0a3c1c912..345a0a88e 100644 --- a/Client/src/Pages/Monitors/Home/index.jsx +++ b/Client/src/Pages/Monitors/Home/index.jsx @@ -1,79 +1,74 @@ import "./index.css"; -import { useEffect, useState } from "react"; +import { useEffect } from "react"; 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, Stack } from "@mui/material"; import PropTypes from "prop-types"; import SkeletonLayout from "./skeleton"; import Fallback from "./fallback"; import StatusBox from "./StatusBox"; import Breadcrumbs from "../../../Components/Breadcrumbs"; import Greeting from "../../../Utils/greeting"; -import MonitorTable from "./MonitorTable"; -import Search from "../../../Components/Inputs/Search"; -import useDebounce from "../../../Utils/debounce"; +import { CurrentMonitoring } from "./CurrentMonitoring"; 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 handleSearch = (value) => { - setIsSearching(true); - setSearch(value); - }; useEffect(() => { dispatch(getUptimeMonitorsByTeamId(authState.authToken)); }, [authState.authToken, dispatch]); - let loading = - monitorState?.isLoading && monitorState?.monitorsSummary?.monitors?.length === 0; + //TODO bring fetching to this component, like on pageSpeed + + const loading = monitorState?.isLoading; + + const totalMonitors = monitorState?.monitorsSummary?.monitorCounts?.total; + + const hasMonitors = totalMonitors > 0; + const noMonitors = !hasMonitors; + const canAddMonitor = isAdmin && hasMonitors; return ( + + + + + {canAddMonitor && ( + + )} + + + {noMonitors && } {loading ? ( ) : ( <> - - - - - {isAdmin && monitorState?.monitorsSummary?.monitors?.length !== 0 && ( - - )} - - - {isAdmin && monitorState?.monitorsSummary?.monitors?.length === 0 && ( - - )} - - {monitorState?.monitorsSummary?.monitors?.length !== 0 && ( + {hasMonitors && ( <> { value={monitorState?.monitorsSummary?.monitorCounts?.paused ?? 0} /> - - - - Actively monitoring - - - {monitorState?.monitorsSummary?.monitorCounts?.total || 0} - - - - - - - {isSearching && ( - <> - - - - - - )} - - - + )} diff --git a/Client/src/Pages/PageSpeed/index.jsx b/Client/src/Pages/PageSpeed/index.jsx index 8ecc4392f..30e11b2d7 100644 --- a/Client/src/Pages/PageSpeed/index.jsx +++ b/Client/src/Pages/PageSpeed/index.jsx @@ -19,7 +19,7 @@ const PageSpeed = ({ isAdmin }) => { const navigate = useNavigate(); const { user, authToken } = useSelector((state) => state.auth); - const [isLoading, setIsLoading] = useState(false); + const [isLoading, setIsLoading] = useState(true); const [monitors, setMonitors] = useState([]); useEffect(() => { dispatch(getPageSpeedByTeamId(authToken)); diff --git a/Client/src/Validation/error.js b/Client/src/Validation/error.js new file mode 100644 index 000000000..e205ce7d1 --- /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?? "Validation error"; + } 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?? "Validation error"; + }); + 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..a6ec7688c 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().valid("debug", "none", "error", "warn").allow(""), + 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, }; 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 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 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, 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": {} +} 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" + ] +}