diff --git a/client/src/Components/Fallback/FallBackActionButtons.jsx b/client/src/Components/Fallback/FallBackActionButtons.jsx new file mode 100644 index 000000000..fbe8fe807 --- /dev/null +++ b/client/src/Components/Fallback/FallBackActionButtons.jsx @@ -0,0 +1,43 @@ +import { Button, Stack } from "@mui/material"; +import { useTheme } from "@mui/material/styles"; +import { useNavigate } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import PropTypes from "prop-types"; + +const FallbackActionButtons = ({ link, type }) => { + const theme = useTheme(); + const navigate = useNavigate(); + const { t } = useTranslation(); + + return ( + + + {type === "uptimeMonitor" && ( + + )} + + ); +}; +FallbackActionButtons.propTypes = { + title: PropTypes.string.isRequired, + link: PropTypes.string.isRequired, + type: PropTypes.string.isRequired, +}; + +export default FallbackActionButtons; diff --git a/client/src/Components/Fallback/FallbackBackground.jsx b/client/src/Components/Fallback/FallbackBackground.jsx new file mode 100644 index 000000000..e01659f49 --- /dev/null +++ b/client/src/Components/Fallback/FallbackBackground.jsx @@ -0,0 +1,31 @@ +import { useTheme } from "@emotion/react"; +import Box from "@mui/material/Box"; +import Skeleton from "../../assets/Images/create-placeholder.svg?react"; +import Background from "../../assets/Images/background-grid.svg?react"; +import SkeletonDark from "../../assets/Images/create-placeholder-dark.svg?react"; +import { useSelector } from "react-redux"; +const FallbackBackground = () => { + const theme = useTheme(); + const mode = useSelector((state) => state.ui.mode); + return ( + <> + {mode === "light" ? ( + + ) : ( + + )} + + + + + ); +}; + +export default FallbackBackground; diff --git a/client/src/Components/Fallback/FallbackCheckList.jsx b/client/src/Components/Fallback/FallbackCheckList.jsx new file mode 100644 index 000000000..4d13dead4 --- /dev/null +++ b/client/src/Components/Fallback/FallbackCheckList.jsx @@ -0,0 +1,35 @@ +import PropTypes from "prop-types"; +import { useTheme } from "@mui/material/styles"; +import Box from "@mui/material/Box"; +import Stack from "@mui/material/Stack"; +import Check from "../Check/Check"; +const FallbackCheckList = ({ checks, type }) => { + const theme = useTheme(); + return ( + + {checks?.map((check, index) => ( + + ))} + + ); +}; + +FallbackCheckList.propTypes = { + checks: PropTypes.arrayOf(PropTypes.string).isRequired, + title: PropTypes.string.isRequired, + type: PropTypes.string.isRequired, +}; + +export default FallbackCheckList; diff --git a/client/src/Components/Fallback/FallbackContainer.jsx b/client/src/Components/Fallback/FallbackContainer.jsx new file mode 100644 index 000000000..65d927190 --- /dev/null +++ b/client/src/Components/Fallback/FallbackContainer.jsx @@ -0,0 +1,44 @@ +import { useTheme } from "@emotion/react"; +import { Box, Stack } from "@mui/material"; +import PropTypes from "prop-types"; + +const FallbackContainer = ({ children, type }) => { + const theme = useTheme(); + return ( + + + {children} + + + ); +}; + +FallbackContainer.propTypes = { + children: PropTypes.node, + type: PropTypes.string, +}; + +export default FallbackContainer; diff --git a/client/src/Components/Fallback/FallbackPageSpeedWarning.jsx b/client/src/Components/Fallback/FallbackPageSpeedWarning.jsx new file mode 100644 index 000000000..bcec7300d --- /dev/null +++ b/client/src/Components/Fallback/FallbackPageSpeedWarning.jsx @@ -0,0 +1,69 @@ +import Box from "@mui/material/Box"; +import { useTheme } from "@mui/material/styles"; +import Link from "@mui/material/Link"; +import { Link as RouterLink } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import Alert from "../Alert"; +import PropTypes from "prop-types"; + +const renderWarningMessage = (t) => { + return ( + <> + {t("pageSpeedWarning")}{" "} + + {t("pageSpeedLearnMoreLink")} + {" "} + {t("pageSpeedAddApiKey")} + + ); +}; + +const FallbackPageSpeedWarning = ({ settingsData }) => { + const theme = useTheme(); + const { t } = useTranslation(); + return ( + + svg": { + color: theme.palette.warningSecondary.contrastText, + }, + }, + }} + > + {settingsData?.pagespeedKeySet === false && ( + + )} + + + ); +}; + +FallbackPageSpeedWarning.propTypes = { + settingsData: PropTypes.shape({ + pagespeedKeySet: PropTypes.bool, + }), +}; + +export default FallbackPageSpeedWarning; diff --git a/client/src/Components/Fallback/FallbackTitle.jsx b/client/src/Components/Fallback/FallbackTitle.jsx new file mode 100644 index 000000000..f7f42f2ca --- /dev/null +++ b/client/src/Components/Fallback/FallbackTitle.jsx @@ -0,0 +1,21 @@ +import { useTheme } from "@mui/material/styles"; +import { Typography } from "@mui/material"; +import PropTypes from "prop-types"; + +const FallbackTitle = ({ title }) => { + const theme = useTheme(); + return ( + + {title} + + ); +}; +FallbackTitle.propTypes = { + title: PropTypes.string.isRequired, +}; +export default FallbackTitle; diff --git a/client/src/Components/Fallback/index.css b/client/src/Components/Fallback/index.css index 357988b07..6988fa5bf 100644 --- a/client/src/Components/Fallback/index.css +++ b/client/src/Components/Fallback/index.css @@ -38,7 +38,3 @@ background-size: cover; background-repeat: no-repeat; } - -.fallback__status > .MuiStack-root { - margin-left: var(--env-var-spacing-2); -} diff --git a/client/src/Components/Fallback/index.jsx b/client/src/Components/Fallback/index.jsx index dd6043697..5fbb958e7 100644 --- a/client/src/Components/Fallback/index.jsx +++ b/client/src/Components/Fallback/index.jsx @@ -1,72 +1,26 @@ import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; -import { Box, Button, Stack, Typography, Link } from "@mui/material"; -import { Link as RouterLink } from "react-router-dom"; -import Skeleton from "../../assets/Images/create-placeholder.svg?react"; -import SkeletonDark from "../../assets/Images/create-placeholder-dark.svg?react"; -import Background from "../../assets/Images/background-grid.svg?react"; -import Check from "../Check/Check"; -import { useNavigate } from "react-router-dom"; -import { useSelector } from "react-redux"; -import Alert from "../Alert"; -import { useTranslation } from "react-i18next"; +import { Box, Stack } from "@mui/material"; import "./index.css"; -import { useFetchSettings } from "../../Hooks/settingsHooks"; -import { useState } from "react"; +import FallbackTitle from "./FallbackTitle"; +import FallbackCheckList from "./FallbackCheckList"; +import FallbackActionButtons from "./FallBackActionButtons"; +import FallbackBackground from "./FallbackBackground"; +import FallbackContainer from "./FallbackContainer"; /** * Fallback component to display a fallback UI with a title, a list of checks, and a navigation button. * * @param {Object} props - The component props. * @param {string} props.title - The title to be displayed in the fallback UI. + * @param {string} props.type - The type of the fallback (e.g., "pageSpeed", "notifications"). * @param {Array} props.checks - An array of strings representing the checks to display. * @param {string} [props.link="/"] - The link to navigate to. - * @param {boolean} [props.vowelStart=false] - Whether the title starts with a vowel. * @param {boolean} [props.showPageSpeedWarning=false] - Whether to show the PageSpeed API warning. * @returns {JSX.Element} The rendered fallback UI. */ -const Fallback = ({ - title, - checks, - link = "/", - isAdmin, - vowelStart = false, - showPageSpeedWarning = false, -}) => { +const Fallback = ({ title, checks, link = "/", isAdmin, type, children }) => { const theme = useTheme(); - const navigate = useNavigate(); - const mode = useSelector((state) => state.ui.mode); - const { t } = useTranslation(); - const [settingsData, setSettingsData] = useState(undefined); - - const [isLoading, error] = useFetchSettings({ - setSettingsData, - setIsApiKeySet: () => {}, - setIsEmailPasswordSet: () => {}, - }); - // Custom warning message with clickable link - const renderWarningMessage = () => { - return ( - <> - {t("pageSpeedWarning")}{" "} - - {t("pageSpeedLearnMoreLink")} - {" "} - {t("pageSpeedAddApiKey")} - - ); - }; return ( - + + - {mode === "light" ? ( - - ) : ( - - )} - - - - - - {vowelStart ? "An" : "A"} {title} is used to: - - {checks?.map((check, index) => ( - - ))} - - {/* TODO - display a different fallback if user is not an admin*/} - {isAdmin && ( - - - {/* Bulk create of uptime monitors */} - {title === "uptime monitor" && ( - - )} - - {/* Warning box for PageSpeed monitor */} - {title === "pagespeed monitor" && showPageSpeedWarning && ( - - svg": { - color: theme.palette.warningSecondary.contrastText, - }, - }, - }} - > - {settingsData?.pagespeedKeySet === false && ( - - )} - - - )} - - )} + + - + {isAdmin && ( + + + {children} + + )} + ); }; - Fallback.propTypes = { title: PropTypes.string.isRequired, checks: PropTypes.arrayOf(PropTypes.string).isRequired, link: PropTypes.string, isAdmin: PropTypes.bool, - vowelStart: PropTypes.bool, showPageSpeedWarning: PropTypes.bool, + type: PropTypes.string.isRequired, + children: PropTypes.node, }; - export default Fallback; diff --git a/client/src/Components/LanguageSelector.jsx b/client/src/Components/LanguageSelector.jsx index 57d102a9e..56a9a898d 100644 --- a/client/src/Components/LanguageSelector.jsx +++ b/client/src/Components/LanguageSelector.jsx @@ -6,7 +6,6 @@ import { useSelector } from "react-redux"; import { useDispatch } from "react-redux"; import { setLanguage } from "../Features/UI/uiSlice"; - const langMap = { cs: "cz", ja: "jp", diff --git a/client/src/Pages/Infrastructure/Monitors/index.jsx b/client/src/Pages/Infrastructure/Monitors/index.jsx index 6feb933db..f7ca892c2 100644 --- a/client/src/Pages/Infrastructure/Monitors/index.jsx +++ b/client/src/Pages/Infrastructure/Monitors/index.jsx @@ -99,13 +99,9 @@ const InfrastructureMonitors = () => { if (!isLoading && typeof summary?.totalMonitors === "undefined") { return ( diff --git a/client/src/Pages/Maintenance/index.jsx b/client/src/Pages/Maintenance/index.jsx index 66b80f87d..9f9528a67 100644 --- a/client/src/Pages/Maintenance/index.jsx +++ b/client/src/Pages/Maintenance/index.jsx @@ -68,12 +68,9 @@ const Maintenance = () => { if (isDataFetched && maintenanceWindows.length === 0) { return ( diff --git a/client/src/Pages/Notifications/index.jsx b/client/src/Pages/Notifications/index.jsx index 28feb99ea..f9fc2e674 100644 --- a/client/src/Pages/Notifications/index.jsx +++ b/client/src/Pages/Notifications/index.jsx @@ -79,7 +79,7 @@ const Notifications = () => { if (notifications?.length === 0) { return ( { order: null, }); + const [settingsData, setSettingsData] = useState(undefined); + const [isSettingsLoading, settingsError] = useFetchSettings({ + setSettingsData, + setIsApiKeySet: () => {}, + setIsEmailPasswordSet: () => {}, + }); + if (networkError === true) { return ( @@ -48,16 +58,16 @@ const PageSpeed = () => { if (!isLoading && monitors?.length === 0) { return ( + > + {isAdmin && settingsData && !settingsData.pagespeedApiKey && ( + + )} + ); } diff --git a/client/src/Pages/StatusPage/StatusPages/index.jsx b/client/src/Pages/StatusPage/StatusPages/index.jsx index 650307bdb..7057eec0c 100644 --- a/client/src/Pages/StatusPage/StatusPages/index.jsx +++ b/client/src/Pages/StatusPage/StatusPages/index.jsx @@ -42,12 +42,9 @@ const StatusPages = () => { if (!isLoading && typeof statusPages !== "undefined" && statusPages.length === 0) { return ( diff --git a/client/src/Pages/Uptime/Monitors/index.jsx b/client/src/Pages/Uptime/Monitors/index.jsx index 6e6f6caba..4ebac94d3 100644 --- a/client/src/Pages/Uptime/Monitors/index.jsx +++ b/client/src/Pages/Uptime/Monitors/index.jsx @@ -174,14 +174,9 @@ const UptimeMonitors = () => { ) { return ( diff --git a/client/src/locales/en.json b/client/src/locales/en.json index c36014fb3..654a29c83 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -428,6 +428,17 @@ "infrastructureDisplayNameLabel": "Display name", "infrastructureEditMonitor": "Save Infrastructure Monitor", "infrastructureEditYour": "Edit your", + "infrastructureMonitor": { + "fallback": { + "checks": [ + "Track the performance of your servers", + "Identify bottlenecks and optimize usage", + "Ensure reliability with real-time monitoring" + ], + "title": "An infrastructure monitor is used to:", + "actionButton": "Let's create your first infrastructure monitor!" + } + }, "infrastructureMonitorCreated": "Infrastructure monitor created successfully!", "infrastructureMonitorUpdated": "Infrastructure monitor updated successfully!", "infrastructureProtocol": "Protocol", @@ -492,6 +503,17 @@ "maintenance": "maintenance", "maintenanceRepeat": "Maintenance Repeat", "maintenanceTableActionMenuDialogTitle": "Do you really want to remove this maintenance window?", + "maintenanceWindow": { + "fallback": { + "checks": [ + "Mark your maintenance periods", + "Eliminate any misunderstandings", + "Stop sending alerts in maintenance windows" + ], + "title": "A maintenance window is used to:", + "actionButton": "Let's create your first maintenance window!" + } + }, "maintenanceWindowDescription": "Your pings won't be sent during this time frame", "maintenanceWindowName": "Maintenance Window Name", "maskedPageSpeedKeyPlaceholder": "*************************************", @@ -589,7 +611,8 @@ "Let engineers know when incidents happen", "Keep administrators informed of system changes" ], - "title": "notification channel" + "title": "A notification channel is used to:", + "actionButton": "Let's create your first notification channel!" }, "fetch": { "failed": "Failed to fetch notifications", @@ -634,6 +657,17 @@ "notifySMS": "Notify via SMS (coming soon)", "now": "Now", "os": "OS", + "pageSpeed": { + "fallback": { + "checks": [ + "Report on the user experience of a page", + "Help analyze webpage speed", + "Give suggestions on how the page can be improved" + ], + "title": "A PageSpeed monitor is used to:", + "actionButton": "Let's create your first PageSpeed monitor!" + } + }, "pageSpeedAddApiKey": "to add your API key.", "pageSpeedConfigureSettingsDescription": "Here you can select the URL of the host, together with the type of monitor.", "pageSpeedDetailsPerformanceReport": "Values are estimated and may vary.", @@ -847,7 +881,16 @@ "createSuccess": "Status page created successfully", "updateSuccess": "Status page updated successfully", "generalSettings": "General settings", - "contents": "Contents" + "contents": "Contents", + "fallback": { + "checks": [ + "Monitor and display the health of your services in real time", + "Track multiple services and share their status", + "Keep users informed about outages and performance" + ], + "title": "A status page is used to:", + "actionButton": "Let's create your first status page!" + } }, "submit": "Submit", "teamPanel": { @@ -901,6 +944,18 @@ "ping": "Enter the IP address or hostname to ping (e.g., 192.168.1.100 or example.com) and add a clear display name that appears on the dashboard.", "port": "Enter the URL or IP of the server, the port number and a clear display name that appears on the dashboard." }, + "uptimeMonitor": { + "fallback": { + "checks": [ + "Check if websites or servers are online & responsive", + "Alert teams about downtime or performance issues", + "Monitor HTTP endpoints, pings, containers & ports", + "Track historical uptime and reliability trends" + ], + "title": "An uptime monitor is used to:", + "actionButton": "Let's create your first uptime monitor!" + } + }, "url": "URL", "urlMonitor": "URL to monitor", "used": "Used", diff --git a/server/validation/joi.js b/server/validation/joi.js index 57c8b7350..b33283d98 100755 --- a/server/validation/joi.js +++ b/server/validation/joi.js @@ -164,11 +164,7 @@ const createMonitorBodyValidation = joi.object({ .custom((value, helpers) => { // 1. Standard URLs: must have protocol and pass canParse() if (/^(https?:\/\/)/.test(value)) { - if ( - typeof URL !== "undefined" && - typeof URL.canParse === "function" && - URL.canParse(value) - ) { + if (typeof URL !== "undefined" && typeof URL.canParse === "function" && URL.canParse(value)) { return value; } // else, it's a malformed URL with protocol @@ -197,8 +193,7 @@ const createMonitorBodyValidation = joi.object({ .messages({ "string.empty": "This field is required.", "string.uri": "The URL you provided is not valid.", - "string.invalidUrl": - "Please enter a valid URL, hostname, or container name (with optional port).", + "string.invalidUrl": "Please enter a valid URL, hostname, or container name (with optional port).", }), ignoreTlsErrors: joi.boolean().default(false), port: joi.number(),