add fallbacks

This commit is contained in:
Alex Holliday
2025-10-09 12:44:27 -07:00
parent 1bacab89bb
commit fb90f193b1
5 changed files with 275 additions and 12 deletions
@@ -1,7 +1,9 @@
import Stack from "@mui/material/Stack";
import { ErrorFallback, EmptyFallback } from "./Fallback";
import type { StackProps } from "@mui/material/Stack";
import { useTheme } from "@mui/material/styles";
import { useTranslation } from "react-i18next";
interface BasePageProps extends StackProps {
children: React.ReactNode;
}
@@ -22,3 +24,57 @@ export const BasePage: React.FC<BasePageProps> = ({
</Stack>
);
};
interface BasePageWithStatesProps extends StackProps {
loading: boolean;
error: any;
items: any[];
page: string;
actionLink?: string;
children: React.ReactNode;
}
const isEmpty = (items: any[]) => {
if (!items) return true;
if (Array.isArray(items) && items.length === 0) return true;
return false;
};
export const BasePageWithStates: React.FC<BasePageWithStatesProps> = ({
loading,
error,
items,
page,
actionLink,
children,
...props
}: BasePageWithStatesProps) => {
const { t } = useTranslation();
if (loading) {
return null;
}
if (error) {
return (
<ErrorFallback
title="Something went wrong..."
subtitle="Please try again later"
/>
);
}
if (isEmpty(items)) {
return (
<EmptyFallback
page={page}
title={t(`${page}Monitor.fallback.title`)}
bullets={t(`${page}Monitor.fallback.checks`, { returnObjects: true })}
actionButtonText={t(`${page}Monitor.fallback.actionButton`)}
actionLink={actionLink || ""}
/>
);
}
return <BasePage {...props}>{children}</BasePage>;
};
@@ -0,0 +1,45 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import CheckOutlined from "@/assets/icons/check-outlined.svg?react";
import { useTheme } from "@mui/material/styles";
export const BulletPointCheck = ({
text,
variant = "info",
}: {
text: string;
noHighlightText?: string;
variant?: "success" | "error" | "info";
}) => {
const theme = useTheme();
const colors: Record<string, string | undefined> = {
success: theme.palette.success.main,
error: theme.palette.error.main,
info: theme.palette.primary.contrastTextSecondary,
};
return (
<Stack
direction="row"
className="check"
gap={theme.spacing(6)}
alignItems="center"
>
<CheckOutlined />
<Typography
component="span"
color={
variant === "info"
? theme.palette.primary.contrastTextSecondary
: colors[variant]
}
fontWeight={450}
sx={{
opacity: 0.9,
}}
>
{text}
</Typography>
</Stack>
);
};
@@ -0,0 +1,158 @@
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import OutputAnimation from "@/assets/Animations/output.gif";
import DarkmodeOutput from "@/assets/Animations/darkmodeOutput.gif";
import Typography from "@mui/material/Typography";
import { BulletPointCheck } from "@/Components/v2/DesignElements";
import { Button } from "@/Components/v2/Inputs";
import { useNavigate } from "react-router";
import { useMediaQuery } from "@mui/material";
import { useTheme } from "@mui/material/styles";
import { useSelector } from "react-redux";
import type { BoxProps } from "@mui/material";
interface BaseFallbackProps extends BoxProps {
children: React.ReactNode;
}
export const BaseFallback: React.FC<BaseFallbackProps> = ({ children, ...props }) => {
const theme = useTheme();
const mode = useSelector((state: any) => state.ui.mode);
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
return (
<Box
margin={isSmall ? "inherit" : "auto"}
marginTop={isSmall ? "33%" : "auto"}
width={{
sm: "90%",
md: "70%",
lg: "50%",
xl: "40%",
}}
padding={theme.spacing(16)}
bgcolor={theme.palette.primary.main}
position="relative"
border={1}
borderColor={theme.palette.primary.lowContrast}
borderRadius={theme.shape.borderRadius}
overflow="hidden"
sx={{
borderStyle: "dashed",
}}
{...props}
>
<Stack
alignItems="center"
gap={theme.spacing(20)}
sx={{
width: "fit-content",
margin: "auto",
marginTop: "100px",
}}
>
<Box
component="img"
src={mode === "light" ? OutputAnimation : DarkmodeOutput}
bgcolor="transparent"
alt="Loading animation"
width="100%"
sx={{
zIndex: 1,
border: "none",
borderRadius: theme.spacing(8),
}}
/>
<Stack
gap={theme.spacing(4)}
alignItems="center"
maxWidth={"300px"}
zIndex={1}
>
{children}
</Stack>
</Stack>
</Box>
);
};
export const ErrorFallback = ({
title,
subtitle,
}: {
title: string;
subtitle: string;
}) => {
const theme = useTheme();
return (
<BaseFallback>
<Typography
variant="h1"
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
{title}
</Typography>
<Typography>{subtitle}</Typography>
</BaseFallback>
);
};
export const EmptyFallback = ({
page,
title,
bullets,
actionButtonText,
actionLink,
}: {
page: string;
title: string;
bullets: any;
actionButtonText: string;
actionLink: string;
}) => {
const theme = useTheme();
const navigate = useNavigate();
return (
<BaseFallback>
<Stack
gap={theme.spacing(10)}
zIndex={1}
alignItems="center"
>
<Typography
component="h1"
color={theme.palette.primary.contrastText}
>
{title}
</Typography>
<Stack
sx={{
flexWrap: "wrap",
gap: theme.spacing(2),
maxWidth: { xs: "90%", md: "80%", lg: "75%" },
}}
>
{bullets?.map((bullet: string, index: number) => (
<BulletPointCheck
text={bullet}
key={`${(page + "Monitors").trim().split(" ")[0]}-${index}`}
/>
))}
</Stack>
<Stack>
<Button
variant="contained"
color="accent"
onClick={() => navigate(actionLink)}
>
{actionButtonText}
</Button>
</Stack>
</Stack>
</BaseFallback>
);
};
@@ -1,7 +1,9 @@
export { SplitBox as HorizontalSplitBox, ConfigBox } from "./SplitBox";
export { BasePage } from "./BasePage";
export { BasePage, BasePageWithStates } from "./BasePage";
export { BGBox, UpStatusBox, DownStatusBox, PausedStatusBox } from "./StatusBox";
export { DataTable as Table, Pagination } from "./Table";
export { GradientBox, StatBox } from "./StatBox";
export { BaseBox } from "./BaseBox";
export { StatusLabel } from "./StatusLabel";
export { BaseFallback, ErrorFallback, EmptyFallback } from "./Fallback";
export { BulletPointCheck } from "./BulletPointCheck";
+12 -10
View File
@@ -1,5 +1,5 @@
import {
BasePage,
BasePageWithStates,
UpStatusBox,
DownStatusBox,
PausedStatusBox,
@@ -18,10 +18,10 @@ const UptimeMonitors = () => {
const theme = useTheme();
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
const { response, loading, refetch } = useGet<ApiResponse>(
const { response, isValidating, error, refetch } = useGet<ApiResponse>(
"/monitors?embedChecks=true",
{},
{ refreshInterval: 30000 }
{ refreshInterval: 30000, keepPreviousData: true }
);
const monitors: IMonitor[] = response?.data ?? ([] as IMonitor[]);
@@ -43,14 +43,16 @@ const UptimeMonitors = () => {
}
);
if (monitors.length === 0 && !loading) {
return "No monitors found";
}
return (
<BasePage>
<BasePageWithStates
loading={isValidating}
error={error}
items={monitors}
page="uptime"
actionLink="create"
>
<HeaderCreate
isLoading={loading}
isLoading={isValidating}
path="/v2/uptime/create"
/>
<Stack
@@ -65,7 +67,7 @@ const UptimeMonitors = () => {
monitors={monitors}
refetch={refetch}
/>
</BasePage>
</BasePageWithStates>
);
};