diff --git a/client/src/Components/v2/design-elements/BasePage.tsx b/client/src/Components/v2/design-elements/BasePage.tsx new file mode 100644 index 000000000..e3785936b --- /dev/null +++ b/client/src/Components/v2/design-elements/BasePage.tsx @@ -0,0 +1,159 @@ +import Stack from "@mui/material/Stack"; +import { + ErrorFallback, + EmptyFallback, + EmptyMonitorFallback, +} from "@/Components/v2/design-elements/Fallback"; +import { Breadcrumb } from "@/Components/v2/design-elements/Breadcrumb"; +import CircularProgress from "@mui/material/CircularProgress"; + +import type { StackProps } from "@mui/material/Stack"; +import { useTheme } from "@mui/material/styles"; +import { useTranslation } from "react-i18next"; +interface BasePageProps extends StackProps { + loading?: boolean; + error?: boolean; + children: React.ReactNode; + breadcrumbOverride?: string[]; +} + +export const BasePage = ({ + loading, + error, + children, + breadcrumbOverride, + ...props +}: BasePageProps) => { + const theme = useTheme(); + + if (loading) { + return ( + + + + ); + } + + if (error) { + return ( + + ); + } + + return ( + + + {children} + + ); +}; + +interface BasePageWithStatesProps extends StackProps { + loading: boolean; + error: any; + items: any[]; + bullets: string[] | unknown; + page: string; + actionButtonText: string; + actionLink: string; + children: React.ReactNode; +} + +export const BasePageWithStates = ({ + loading, + error, + items, + page, + bullets, + actionButtonText, + actionLink, + children, + ...props +}: BasePageWithStatesProps) => { + const showLoading = loading && (!items || items.length === 0); + + if (isEmpty(items)) { + return ( + + ); + } + + return ( + + {children} + + ); +}; + +interface MonitorBasePageWithStatesProps 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 MonitorBasePageWithStates = ({ + loading, + error, + items, + page, + actionLink, + children, + ...props +}: MonitorBasePageWithStatesProps) => { + const { t } = useTranslation(); + + const showLoading = loading && (!items || items.length === 0); + + if (!loading && isEmpty(items)) { + return ( + + ); + } + + return ( + + {children} + + ); +}; diff --git a/client/src/Components/v2/design-elements/Breadcrumb.tsx b/client/src/Components/v2/design-elements/Breadcrumb.tsx new file mode 100644 index 000000000..4489f992d --- /dev/null +++ b/client/src/Components/v2/design-elements/Breadcrumb.tsx @@ -0,0 +1,117 @@ +import MuiBreadcrumbs from "@mui/material/Breadcrumbs"; +import Typography from "@mui/material/Typography"; +import { Link, useLocation } from "react-router-dom"; +import { useTheme } from "@mui/material/styles"; +import { ChevronRight, Home } from "lucide-react"; +import { useTranslation } from "react-i18next"; + +export const Breadcrumb = ({ breadcrumbOverride }: { breadcrumbOverride?: string[] }) => { + const { t } = useTranslation(); + const theme = useTheme(); + const location = useLocation(); + + // Don't show breadcrumb on dashboard + if (location.pathname === "/" || location.pathname === "/uptime") { + return null; + } + + const pathnames = breadcrumbOverride || location.pathname.split("/").filter((x) => x); + + // Don't show if only one level (e.g., /uptime) + if (pathnames.length === 0) { + return null; + } + + return ( + + } + sx={{ + fontSize: "14px", + marginBottom: theme.spacing(6), + "& .MuiBreadcrumbs-separator": { + color: theme.palette.text.secondary, + }, + }} + > + + + + Home + + + {pathnames.map((value, index) => { + const last = index === pathnames.length - 1; + const to = `/${pathnames.slice(0, index + 1).join("/")}`; + + let displayName = ""; + + // Paths that are IDs should be detail pages + if (value.length === 24 || value.match(/^[a-f0-9-]{36}$/)) { + displayName = "Details"; + } else { + displayName = + t(`common.breadcrumbs.${value}`) || + value.charAt(0).toUpperCase() + value.slice(1); + } + + return last ? ( + + {displayName} + + ) : ( + + + {displayName} + + + ); + })} + + ); +}; diff --git a/client/src/Components/v2/design-elements/BulletPointCheck.tsx b/client/src/Components/v2/design-elements/BulletPointCheck.tsx new file mode 100644 index 000000000..2893ee4c2 --- /dev/null +++ b/client/src/Components/v2/design-elements/BulletPointCheck.tsx @@ -0,0 +1,46 @@ +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { Check } from "lucide-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 = { + success: theme.palette.success.main, + error: theme.palette.error.main, + info: theme.palette.text.secondary, + }; + + return ( + + + + {text} + + + ); +}; diff --git a/client/src/Components/v2/design-elements/Fallback.tsx b/client/src/Components/v2/design-elements/Fallback.tsx new file mode 100644 index 000000000..9dfeb5122 --- /dev/null +++ b/client/src/Components/v2/design-elements/Fallback.tsx @@ -0,0 +1,171 @@ +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { BulletPointCheck, SkeletonCard } from "@/Components/v2/design-elements"; +import { Button } from "@/Components/v2/inputs"; + +import { useNavigate } from "react-router"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import { useTheme } from "@mui/material/styles"; +import type { StackProps } from "@mui/material/Stack"; + +interface BaseFallbackProps extends StackProps { + children: React.ReactNode; +} + +export const BaseFallback = ({ children, ...props }: BaseFallbackProps) => { + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + + return ( + + + {children} + + ); +}; + +export const ErrorFallback = ({ + title, + subtitle, +}: { + title: string; + subtitle: string; +}) => { + const theme = useTheme(); + return ( + + + {title} + + {subtitle} + + ); +}; + +export const EmptyFallback = ({ + title, + bullets, + actionButtonText, + actionLink, +}: { + title: string; + bullets: any; + actionButtonText: string; + actionLink: string; +}) => { + const theme = useTheme(); + const navigate = useNavigate(); + return ( + + + + {title} + + + {Array.isArray(bullets) && + bullets?.map((bullet: string) => ( + + ))} + + + + + + + ); +}; + +export const EmptyMonitorFallback = ({ + page, + title, + bullets, + actionButtonText, + actionLink, +}: { + page: string; + title: string; + bullets: any; + actionButtonText: string; + actionLink: string; +}) => { + const theme = useTheme(); + const navigate = useNavigate(); + return ( + + + + {title} + + + {Array.isArray(bullets) && + bullets?.map((bullet: string, index: number) => ( + + ))} + + + + ); +}; diff --git a/client/src/Components/v2/design-elements/SkeletonCard.tsx b/client/src/Components/v2/design-elements/SkeletonCard.tsx new file mode 100644 index 000000000..05f0c462a --- /dev/null +++ b/client/src/Components/v2/design-elements/SkeletonCard.tsx @@ -0,0 +1,147 @@ +import { Box, Stack, useTheme } from "@mui/material"; + +interface SkeletonCardProps { + width?: number | string | { xs?: number | string; md?: number | string }; + showHalo?: boolean; +} + +export const SkeletonCard = ({ + width = { xs: 130, md: 216 }, + showHalo = true, +}: SkeletonCardProps) => { + const theme = useTheme(); + const pulseAnimation = { + "@keyframes pulse": { + "0%, 100%": { opacity: 1 }, + "50%": { opacity: 0.72 }, + }, + }; + + const floatAnimation = { + "@keyframes float": { + "0%, 100%": { transform: "rotate(5deg) translate(12px, -20px)" }, + "50%": { transform: "rotate(5deg) translate(12px, -24px)" }, + }, + }; + + const cardStyle = { + width: width, + height: 57, + borderRadius: theme.shape.borderRadius, + background: + theme.palette.mode === "dark" + ? theme.palette.secondary.dark + : theme.palette.background.paper, + boxShadow: + theme.palette.mode === "dark" + ? "0 8px 30px rgba(0,0,0,.3), 0 0 0 1px rgba(255,255,255,.05) inset" + : "0 8px 30px rgba(0,0,0,.08), 0 0 0 1px rgba(0,0,0,.05) inset", + display: "flex", + gap: theme.spacing(5), + alignItems: "center", + padding: theme.spacing(5), + }; + + const blockStyle = { + width: 55, + height: 34, + borderRadius: theme.shape.borderRadius, + background: theme.palette.secondary.main, + border: `1px solid ${theme.palette.divider}`, + animation: "pulse 1.6s ease-in-out infinite", + ...pulseAnimation, + }; + + const lineStyle = { + height: 7, + borderRadius: theme.shape.borderRadius, + background: theme.palette.secondary.main, + border: `1px solid ${theme.palette.divider}`, + animation: "pulse 1.6s ease-in-out infinite", + ...pulseAnimation, + }; + + return ( + + {showHalo && ( +