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(),