Merge branch 'develop' into feat/edit-users

This commit is contained in:
Alexander Holliday
2025-07-23 09:37:04 -07:00
committed by GitHub
15 changed files with 372 additions and 215 deletions

View File

@@ -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 (
<Stack
gap={theme.spacing(10)}
alignItems="center"
>
<Button
variant="contained"
color="accent"
onClick={() => navigate(link)}
>
{t(`${type}.fallback.actionButton`)}
</Button>
{type === "uptimeMonitor" && (
<Button
variant="contained"
color="accent"
sx={{ alignSelf: "center" }}
onClick={() => navigate("/uptime/bulk-import")}
>
{t("bulkImport.fallbackPage")}
</Button>
)}
</Stack>
);
};
FallbackActionButtons.propTypes = {
title: PropTypes.string.isRequired,
link: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
};
export default FallbackActionButtons;

View File

@@ -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" ? (
<Skeleton style={{ zIndex: 1 }} />
) : (
<SkeletonDark style={{ zIndex: 1 }} />
)}
<Box
className="background-pattern-svg"
sx={{
"& svg g g:last-of-type path": {
stroke: theme.palette.primary.lowContrast,
},
}}
>
<Background style={{ width: "100%" }} />
</Box>
</>
);
};
export default FallbackBackground;

View File

@@ -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 (
<Box
sx={{
display: "flex",
flexWrap: "wrap",
gap: theme.spacing(2),
alignItems: "flex-start",
maxWidth: { xs: "90%", md: "80%", lg: "75%" },
}}
>
{checks?.map((check, index) => (
<Check
text={check}
key={`${type.trim().split(" ")[0]}-${index}`}
outlined={true}
/>
))}
</Box>
);
};
FallbackCheckList.propTypes = {
checks: PropTypes.arrayOf(PropTypes.string).isRequired,
title: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
};
export default FallbackCheckList;

View File

@@ -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 (
<Box
border={1}
borderColor={theme.palette.primary.lowContrast}
borderRadius={theme.shape.borderRadius}
backgroundColor={theme.palette.primary.main}
overflow="hidden"
sx={{
display: "flex",
borderStyle: "dashed",
height: "fit-content",
minHeight: "60vh",
width: {
sm: "90%",
md: "70%",
lg: "50%",
xl: "40%",
},
padding: `${theme.spacing(20)} ${theme.spacing(10)}`,
}}
>
<Stack
className={`fallback__${type?.trim().split(" ")[0]}`}
alignItems="center"
gap={theme.spacing(20)}
>
{children}
</Stack>
</Box>
);
};
FallbackContainer.propTypes = {
children: PropTypes.node,
type: PropTypes.string,
};
export default FallbackContainer;

View File

@@ -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")}{" "}
<Link
component={RouterLink}
to="/settings"
sx={{
textDecoration: "underline",
color: "inherit",
fontWeight: "inherit",
"&:hover": {
textDecoration: "underline",
},
}}
>
{t("pageSpeedLearnMoreLink")}
</Link>{" "}
{t("pageSpeedAddApiKey")}
</>
);
};
const FallbackPageSpeedWarning = ({ settingsData }) => {
const theme = useTheme();
const { t } = useTranslation();
return (
<Box sx={{ width: "80%", maxWidth: "600px", zIndex: 1 }}>
<Box
sx={{
"& .alert.row-stack": {
backgroundColor: theme.palette.warningSecondary.main,
borderColor: theme.palette.warningSecondary.lowContrast,
"& .MuiTypography-root": {
color: theme.palette.warningSecondary.contrastText,
},
"& .MuiBox-root > svg": {
color: theme.palette.warningSecondary.contrastText,
},
},
}}
>
{settingsData?.pagespeedKeySet === false && (
<Alert
variant="warning"
hasIcon={true}
body={renderWarningMessage(t)}
/>
)}
</Box>
</Box>
);
};
FallbackPageSpeedWarning.propTypes = {
settingsData: PropTypes.shape({
pagespeedKeySet: PropTypes.bool,
}),
};
export default FallbackPageSpeedWarning;

View File

@@ -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 (
<Typography
alignSelf="center"
component="h1"
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
{title}
</Typography>
);
};
FallbackTitle.propTypes = {
title: PropTypes.string.isRequired,
};
export default FallbackTitle;

View File

@@ -38,7 +38,3 @@
background-size: cover;
background-repeat: no-repeat;
}
.fallback__status > .MuiStack-root {
margin-left: var(--env-var-spacing-2);
}

View File

@@ -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<string>} 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")}{" "}
<Link
component={RouterLink}
to="/settings"
sx={{
textDecoration: "underline",
color: "inherit",
fontWeight: "inherit",
"&:hover": {
textDecoration: "underline",
},
}}
>
{t("pageSpeedLearnMoreLink")}
</Link>{" "}
{t("pageSpeedAddApiKey")}
</>
);
};
return (
<Box
position="relative"
@@ -78,138 +32,44 @@ const Fallback = ({
alignItems: "center",
}}
>
<Box
border={1}
borderColor={theme.palette.primary.lowContrast}
borderRadius={theme.shape.borderRadius}
backgroundColor={theme.palette.primary.main}
overflow="hidden"
sx={{
display: "flex",
borderStyle: "dashed",
height: {
sm: "55vh",
md: "50vh",
lg: "70vh",
xl: "58vh",
},
width: {
sm: "90%",
md: "70%",
lg: "42%",
},
minHeight: {
sm: theme.spacing(280),
},
padding: theme.spacing(10),
}}
>
<FallbackContainer type={type}>
<FallbackBackground />
<Stack
className={`fallback__${title?.trim().split(" ")[0]}`}
gap={theme.spacing(5)}
zIndex={1}
alignItems="center"
gap={theme.spacing(20)}
>
{mode === "light" ? (
<Skeleton style={{ zIndex: 1 }} />
) : (
<SkeletonDark style={{ zIndex: 1 }} />
)}
<Box
className="background-pattern-svg"
sx={{
"& svg g g:last-of-type path": {
stroke: theme.palette.primary.lowContrast,
},
}}
>
<Background style={{ width: "100%" }} />
</Box>
<Stack
gap={theme.spacing(4)}
maxWidth={theme.spacing(180)}
zIndex={1}
>
<Typography
alignSelf="center"
component="h1"
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
{vowelStart ? "An" : "A"} {title} is used to:
</Typography>
{checks?.map((check, index) => (
<Check
text={check}
key={`${title.trim().split(" ")[0]}-${index}`}
outlined={true}
/>
))}
</Stack>
{/* TODO - display a different fallback if user is not an admin*/}
{isAdmin && (
<Stack gap={theme.spacing(10)}>
<Button
variant="contained"
color="accent"
sx={{ alignSelf: "center", mb: "2px" }}
onClick={() => navigate(link)}
>
Let's create your first {title}
</Button>
{/* Bulk create of uptime monitors */}
{title === "uptime monitor" && (
<Button
variant="contained"
color="accent"
sx={{ alignSelf: "center" }}
onClick={() => navigate("/uptime/bulk-import")}
>
{t("bulkImport.fallbackPage")}
</Button>
)}
{/* Warning box for PageSpeed monitor */}
{title === "pagespeed monitor" && showPageSpeedWarning && (
<Box sx={{ width: "80%", maxWidth: "600px", zIndex: 1 }}>
<Box
sx={{
"& .alert.row-stack": {
backgroundColor: theme.palette.warningSecondary.main,
borderColor: theme.palette.warningSecondary.lowContrast,
"& .MuiTypography-root": {
color: theme.palette.warningSecondary.contrastText,
},
"& .MuiBox-root > svg": {
color: theme.palette.warningSecondary.contrastText,
},
},
}}
>
{settingsData?.pagespeedKeySet === false && (
<Alert
variant="warning"
hasIcon={true}
body={renderWarningMessage()}
/>
)}
</Box>
</Box>
)}
</Stack>
)}
<FallbackTitle title={title} />
<FallbackCheckList
checks={checks}
title={title}
type={type}
/>
</Stack>
</Box>
{isAdmin && (
<Stack
gap={theme.spacing(10)}
alignItems="center"
>
<FallbackActionButtons
title={title}
link={link}
type={type}
/>
{children}
</Stack>
)}
</FallbackContainer>
</Box>
);
};
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;

View File

@@ -99,13 +99,9 @@ const InfrastructureMonitors = () => {
if (!isLoading && typeof summary?.totalMonitors === "undefined") {
return (
<Fallback
vowelStart={true}
title="infrastructure monitor"
checks={[
"Track the performance of your servers",
"Identify bottlenecks and optimize usage",
"Ensure reliability with real-time monitoring",
]}
type="infrastructureMonitor"
title={t("infrastructureMonitor.fallback.title")}
checks={t("infrastructureMonitor.fallback.checks", { returnObjects: true })}
link="/infrastructure/create"
isAdmin={isAdmin}
/>

View File

@@ -68,12 +68,9 @@ const Maintenance = () => {
if (isDataFetched && maintenanceWindows.length === 0) {
return (
<Fallback
title="maintenance window"
checks={[
"Mark your maintenance periods",
"Eliminate any misunderstandings",
"Stop sending alerts in maintenance windows",
]}
type="maintenanceWindow"
title={t("maintenanceWindow.fallback.title")}
checks={t("maintenanceWindow.fallback.checks", { returnObjects: true })}
link="/maintenance/create"
isAdmin={isAdmin}
/>

View File

@@ -79,7 +79,7 @@ const Notifications = () => {
if (notifications?.length === 0) {
return (
<Fallback
vowelStart={false}
type="notifications"
title={t("notifications.fallback.title")}
checks={t("notifications.fallback.checks", { returnObjects: true })}
link="/notifications/create"

View File

@@ -1,4 +1,5 @@
// Components
import { useState } from "react";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import { Stack, Typography } from "@mui/material";
import CreateMonitorHeader from "../../../Components/MonitorCreateHeader";
@@ -6,12 +7,14 @@ import MonitorCountHeader from "../../../Components/MonitorCountHeader";
import MonitorGrid from "./Components/MonitorGrid";
import Fallback from "../../../Components/Fallback";
import GenericFallback from "../../../Components/GenericFallback";
import FallbackPageSpeedWarning from "../../../Components/Fallback/FallbackPageSpeedWarning";
// Utils
import { useTheme } from "@emotion/react";
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
import { useTranslation } from "react-i18next";
import { useFetchMonitorsByTeamId } from "../../../Hooks/monitorHooks";
import { useFetchSettings } from "../../../Hooks/settingsHooks";
// Constants
const BREADCRUMBS = [{ name: `pagespeed`, path: "/pagespeed" }];
const TYPES = ["pagespeed"];
@@ -30,6 +33,13 @@ const PageSpeed = () => {
order: null,
});
const [settingsData, setSettingsData] = useState(undefined);
const [isSettingsLoading, settingsError] = useFetchSettings({
setSettingsData,
setIsApiKeySet: () => {},
setIsEmailPasswordSet: () => {},
});
if (networkError === true) {
return (
<GenericFallback>
@@ -48,16 +58,16 @@ const PageSpeed = () => {
if (!isLoading && monitors?.length === 0) {
return (
<Fallback
title="pagespeed monitor"
checks={[
"Report on the user experience of a page",
"Help analyze webpage speed",
"Give suggestions on how the page can be improved",
]}
type="pageSpeed"
title={t("pageSpeed.fallback.title")}
checks={t("pageSpeed.fallback.checks", { returnObjects: true })}
link="/pagespeed/create"
isAdmin={isAdmin}
// showPageSpeedWarning={isAdmin && !pagespeedApiKey}
/>
>
{isAdmin && settingsData && !settingsData.pagespeedApiKey && (
<FallbackPageSpeedWarning settingsData={settingsData} />
)}
</Fallback>
);
}

View File

@@ -42,12 +42,9 @@ const StatusPages = () => {
if (!isLoading && typeof statusPages !== "undefined" && statusPages.length === 0) {
return (
<Fallback
title="status page"
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={t("statusPage.fallback.title")}
checks={t("statusPage.fallback.checks", { returnObjects: true })}
type="statusPage"
link="/status/uptime/create"
isAdmin={isAdmin}
/>

View File

@@ -174,14 +174,9 @@ const UptimeMonitors = () => {
) {
return (
<Fallback
vowelStart={true}
title="uptime monitor"
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",
]}
type="uptimeMonitor"
title={t("uptimeMonitor.fallback.title")}
checks={t("uptimeMonitor.fallback.checks", { returnObjects: true })}
link="/uptime/create"
isAdmin={isAdmin}
/>

View File

@@ -472,6 +472,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",
@@ -515,6 +526,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": "*************************************",
@@ -612,7 +634,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",
@@ -657,6 +680,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.",
@@ -872,6 +906,23 @@
"statusPageStatusNoPage": "There's no status page here.",
"statusPageStatusNotPublic": "This status page is not public.",
"statusPageStatusServiceStatus": "Service status",
"statusPage": {
"deleteSuccess": "Status page deleted successfully",
"deleteFailed": "Failed to delete status page",
"createSuccess": "Status page created successfully",
"updateSuccess": "Status page updated successfully",
"generalSettings": "General settings",
"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": {
"cancel": "Cancel",
@@ -924,6 +975,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",