Merge remote-tracking branch 'origin/develop' into feat/pagespeed-mobile-strat

This commit is contained in:
Alex committed 2026-05-11 11:11:13 -07:00
commit 78fdc548bb
395 files changed
+60597 -29063

No files matched your search

@@ -5,7 +5,7 @@ import { getResponseColor } from "@/Utils/DataUtils";
import { HeatmapResponseTimeTooltip } from "@/Components/common/charts/HeatmapResponseTimeTooltip";
import type { SxProps } from "@mui/material/styles";
import type { ResponsiveStyleValue } from "@mui/system";
import { MAX_RECENT_CHECKS } from "@/Types/Monitor";
interface PlaceholderCheck {
status: "placeholder";
responseTime: 0;
@@ -32,11 +32,11 @@ export const HeatmapResponseTime = ({
if (!checks || checks.length === 0) return null;
const latestChecks = checks.slice(-25);
const latestChecks = checks.slice(-MAX_RECENT_CHECKS);
let data: Array<CheckSnapshot | PlaceholderCheck>;
if (latestChecks.length !== 25) {
const placeholders = Array(25 - latestChecks.length).fill({
if (latestChecks.length !== MAX_RECENT_CHECKS) {
const placeholders = Array(MAX_RECENT_CHECKS - latestChecks.length).fill({
status: "placeholder" as const,
});
data = [...latestChecks, ...placeholders];
@@ -44,13 +44,13 @@ export const HeatmapResponseTime = ({
data = latestChecks;
}
return (
<Box sx={{ width: "100%" }}>
<Box width={"100%"}>
<Box
width={"100%"}
display={"grid"}
gap={gap}
sx={{
width: "100%",
display: "grid",
gridTemplateColumns: "repeat(25, 1fr)",
gap: gap,
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
alignItems: "stretch",
}}
>
@@ -82,50 +82,50 @@ export const HeatmapResponseTime = ({
check={check}
>
<Box
display={"grid"}
sx={{
display: "grid",
gridTemplateRows: "auto auto",
}}
>
<Box
display={"flex"}
gap={theme.spacing(2)}
sx={{
display: "flex",
flexDirection: "column",
gap: theme.spacing(2),
}}
>
<Box
width={"100%"}
sx={{
position: "relative",
width: "100%",
aspectRatio: "10",
}}
>
<Box
bgcolor={statusBg}
borderRadius={theme.shape.borderRadius}
sx={{
position: "absolute",
inset: 0,
bgcolor: statusBg,
borderRadius: theme.spacing(0.5),
...availabilityCellSx,
}}
/>
<Box
borderRadius={theme.shape.borderRadius}
sx={{
position: "absolute",
inset: 0,
pointerEvents: "none",
borderRadius: theme.spacing(0.5),
boxShadow: `inset 0 0 0 2px ${statusBorder}`,
}}
/>
</Box>
<Box
width={"100%"}
bgcolor={respBg}
borderRadius={theme.shape.borderRadius}
sx={{
width: "100%",
aspectRatio: "1 / 1",
bgcolor: respBg,
borderRadius: theme.spacing(0.5),
...responseCellSx,
}}
/>
@@ -7,6 +7,7 @@ import type { CheckSnapshot } from "@/Types/Check";
import { HeatmapResponseTimeTooltip } from "./HeatmapResponseTimeTooltip";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { MAX_RECENT_CHECKS } from "@/Types/Monitor";
interface HistogramResponseTimeProps {
checks: CheckSnapshot[];
@@ -57,8 +58,11 @@ export const HistogramResponseTime = ({
if (!Array.isArray(checks) || checks.length === 0) return null;
const data =
checks.length !== 25
? [...checks, ...Array(25 - checks.length).fill({ status: "placeholder" })]
checks.length !== MAX_RECENT_CHECKS
? [
...checks,
...Array(MAX_RECENT_CHECKS - checks.length).fill({ status: "placeholder" }),
]
: checks;
const chartHeight = typeof height === "number" ? `${height}px` : height;
@@ -86,17 +90,17 @@ export const HistogramResponseTime = ({
<Stack
direction={statsPosition === "left" ? "row-reverse" : "row"}
alignItems="center"
sx={{ width: "100%" }}
width={"100%"}
>
<Box sx={{ flex: 1 }}>
<Box
width={"100%"}
display={"grid"}
gap={gridGap}
height={chartHeight}
sx={{
width: "100%",
display: "grid",
gridTemplateColumns: "repeat(25, 1fr)",
gap: gridGap,
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
alignItems: "end",
height: chartHeight,
}}
>
{data.map((check, index) => {
@@ -108,23 +112,23 @@ export const HistogramResponseTime = ({
: theme.palette.error.light;
const bar = (
<Box
width={"100%"}
height={"100%"}
borderRadius={theme.shape.borderRadius}
bgcolor={theme.palette.action.hover}
sx={{
position: "relative",
width: "100%",
height: "100%",
borderRadius: theme.spacing(1),
bgcolor: theme.palette.action.hover,
overflow: "hidden",
}}
>
<Box
width={"100%"}
height={isPlaceholder ? 0 : heightPct}
bgcolor={barColor}
sx={{
position: "absolute",
bottom: 0,
left: 0,
width: "100%",
height: isPlaceholder ? 0 : heightPct,
bgcolor: barColor,
transition: "height 500ms cubic-bezier(0.4, 0, 0.2, 1)",
}}
/>
@@ -133,8 +137,8 @@ export const HistogramResponseTime = ({
return isPlaceholder ? (
<Box
height={"100%"}
key={index}
sx={{ height: "100%" }}
>
{bar}
</Box>
@@ -8,7 +8,7 @@ import type { ResponsiveStyleValue } from "@mui/system";
import { useTheme } from "@mui/material/styles";
type BaseChartProps = React.PropsWithChildren<{
icon: React.ReactNode;
icon?: React.ReactNode;
title: string;
width?: number | string;
maxWidth?: number | string;
@@ -17,7 +17,6 @@ type BaseChartProps = React.PropsWithChildren<{
export const BaseChart = ({
children,
icon,
title,
width = "100%",
maxWidth = "100%",
@@ -38,34 +37,12 @@ export const BaseChart = ({
gap={theme.spacing(LAYOUT.MD)}
flex={1}
>
<Stack
direction="row"
alignItems={"center"}
gap={theme.spacing(LAYOUT.XS)}
<Typography
variant="eyebrow"
color="text.secondary"
>
{icon && (
<BaseBox
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 34,
height: 34,
backgroundColor: theme.palette.action.hover,
"& svg": {
width: 20,
height: 20,
"& path": {
stroke: theme.palette.text.secondary,
},
},
}}
>
{icon}
</BaseBox>
)}
<Typography variant="h2">{title}</Typography>
</Stack>
{title}
</Typography>
<Box flex={1}>{children}</Box>
</Stack>
</BaseBox>
@@ -1,18 +1,19 @@
import Logo from "@/assets/icons/checkmate-icon.svg?react";
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Alert from "@mui/material/Alert";
import Link from "@mui/material/Link";
import { LAYOUT, SPACING } from "@/Utils/Theme/constants";
import { LAYOUT } from "@/Utils/Theme/constants";
import {
ErrorFallback,
EmptyFallback,
EmptyMonitorFallback,
BaseFallback,
} from "@/Components/design-elements/Fallback";
import { EmptyState } from "@/Components/design-elements/EmptyState";
import { NoticeBanner } from "@/Components/design-elements/NoticeBanner";
import { Breadcrumb } from "@/Components/design-elements/Breadcrumb";
import { PageHeader } from "@/Components/design-elements/PageHeader";
import CircularProgress from "@mui/material/CircularProgress";
import { HeaderAuthControls } from "@/Pages/Auth/components/HeaderAuthControls";
import { LanguageSelector, SwitchTheme } from "@/Components/inputs";
import type { StackProps } from "@mui/material/Stack";
import { useTheme } from "@mui/material/styles";
@@ -21,16 +22,15 @@ import { Link as RouterLink } from "react-router-dom";
import { Typography } from "@mui/material";
export const PageSpeedKeyPriorityFallback = () => {
const theme = useTheme();
const { t } = useTranslation();
return (
<BaseFallback>
<Alert
severity="warning"
sx={{
width: "100%",
maxWidth: 600,
}}
>
<Typography>
<EmptyState
fullscreen
title={t("pages.pageSpeed.fallback.title")}
description={t("pages.pageSpeed.fallback.description")}
alert={
<NoticeBanner severity="warning">
<Trans
i18nKey="common.alerts.pageSpeedApiKey.content"
components={{
@@ -38,15 +38,18 @@ export const PageSpeedKeyPriorityFallback = () => {
<Link
component={RouterLink}
to="/settings"
color="inherit"
fontWeight="inherit"
color={theme.palette.primary.main}
fontWeight={600}
sx={{
textDecoration: "underline",
}}
/>
),
}}
/>
</Typography>
</Alert>
</BaseFallback>
</NoticeBanner>
}
/>
);
};
@@ -55,6 +58,9 @@ interface BasePageProps extends StackProps {
error?: boolean;
children: React.ReactNode;
breadcrumbOverride?: string[];
headerKey?: string;
backTo?: string;
backLabel?: string;
}
export const BasePage = ({
@@ -62,9 +68,13 @@ export const BasePage = ({
error,
children,
breadcrumbOverride,
headerKey,
backTo,
backLabel,
...props
}: BasePageProps) => {
const theme = useTheme();
const { t } = useTranslation();
if (loading) {
return (
@@ -95,7 +105,16 @@ export const BasePage = ({
spacing={theme.spacing(LAYOUT.LG)}
{...props}
>
<Breadcrumb breadcrumbOverride={breadcrumbOverride} />
{headerKey ? (
<PageHeader
title={t(`pages.${headerKey}.header.title`)}
description={t(`pages.${headerKey}.header.description`)}
backTo={backTo}
backLabel={backLabel}
/>
) : (
<Breadcrumb breadcrumbOverride={breadcrumbOverride} />
)}
{children}
</Stack>
);
@@ -105,11 +124,12 @@ interface BasePageWithStatesProps extends StackProps {
loading: boolean;
error: any;
totalCount: number;
bullets: string[] | unknown;
description?: string;
page: string;
actionButtonText: string;
actionLink: string;
children: React.ReactNode;
headerKey?: string;
}
export const BasePageWithStates = ({
@@ -117,10 +137,11 @@ export const BasePageWithStates = ({
error,
totalCount,
page,
bullets,
description,
actionButtonText,
actionLink,
children,
headerKey,
...props
}: BasePageWithStatesProps) => {
const showLoading = loading && totalCount === 0;
@@ -128,7 +149,7 @@ export const BasePageWithStates = ({
if (!loading && totalCount === 0) {
return (
<EmptyFallback
bullets={bullets}
description={description}
title={page}
actionButtonText={actionButtonText}
actionLink={actionLink}
@@ -140,6 +161,7 @@ export const BasePageWithStates = ({
<BasePage
loading={showLoading}
error={error}
headerKey={headerKey}
{...props}
>
{children}
@@ -155,6 +177,7 @@ interface MonitorBasePageWithStatesProps extends StackProps {
actionLink?: string;
children: React.ReactNode;
priorityFallback?: React.ReactNode;
headerKey?: string;
}
export const MonitorBasePageWithStates = ({
@@ -165,6 +188,7 @@ export const MonitorBasePageWithStates = ({
actionLink,
children,
priorityFallback,
headerKey,
...props
}: MonitorBasePageWithStatesProps) => {
const { t } = useTranslation();
@@ -172,15 +196,7 @@ export const MonitorBasePageWithStates = ({
const showLoading = loading && totalCount === 0;
if (priorityFallback) {
return (
<BasePage
loading={loading}
error={error}
{...props}
>
<Stack height={"100%"}>{priorityFallback}</Stack>
</BasePage>
);
return <>{priorityFallback}</>;
}
if (!loading && totalCount === 0) {
@@ -188,7 +204,7 @@ export const MonitorBasePageWithStates = ({
<EmptyMonitorFallback
page={page}
title={t(`pages.${page}.fallback.title`)}
bullets={t(`pages.${page}.fallback.checks`, { returnObjects: true })}
description={t(`pages.${page}.fallback.description`)}
actionButtonText={t(`pages.${page}.fallback.actionButton`)}
actionLink={actionLink || ""}
/>
@@ -199,6 +215,7 @@ export const MonitorBasePageWithStates = ({
<BasePage
loading={showLoading}
error={error}
headerKey={headerKey}
{...props}
>
{children}
@@ -215,51 +232,163 @@ export const BaseAuthPage = ({
title,
subtitle,
children,
...props
component,
onSubmit,
}: BaseAuthPageProps) => {
const theme = useTheme();
const { t } = useTranslation();
return (
<BasePage
breadcrumbOverride={[]}
gap={theme.spacing(LAYOUT.MD)}
alignItems={"center"}
justifyContent={"center"}
<Stack
direction={{ xs: "column", md: "row" }}
minHeight="100vh"
position={"relative"}
{...props}
width="100%"
position="relative"
sx={{ backgroundColor: theme.palette.background.default }}
>
<HeaderAuthControls
hideLogo
py={theme.spacing(LAYOUT.XS)}
position={"absolute"}
top={0}
left={0}
/>
<Box width={{ xs: 60, sm: 70, md: 80 }}>
<Logo
width={"100%"}
height={"100%"}
/>
</Box>
<Stack alignItems={"center"}>
<Typography
variant="h1"
mb={theme.spacing(SPACING.LG)}
<Stack
flex={1}
alignItems="center"
justifyContent="center"
position="relative"
px={theme.spacing(LAYOUT.MD)}
py={{ xs: theme.spacing(20), md: theme.spacing(12) }}
>
<Stack
direction="row"
spacing={theme.spacing(2)}
alignItems="center"
sx={{
position: "absolute",
top: theme.spacing(LAYOUT.MD),
right: theme.spacing(LAYOUT.MD),
zIndex: 3,
}}
>
{title}
</Typography>
<Typography variant="h2">{subtitle}</Typography>
<LanguageSelector />
<SwitchTheme />
</Stack>
<Box
component={component as React.ElementType}
onSubmit={onSubmit}
sx={{
display: "flex",
flexDirection: "column",
gap: theme.spacing(LAYOUT.MD),
width: "100%",
maxWidth: 360,
}}
>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(3)}
mb={theme.spacing(2)}
>
<Box width={28}>
<Logo width="100%" />
</Box>
<Typography sx={{ fontWeight: 500, letterSpacing: "-0.01em", fontSize: 16 }}>
{t("common.appName")}
</Typography>
</Stack>
<Stack gap={theme.spacing(2)}>
<Typography
sx={{
fontSize: 26,
fontWeight: 400,
lineHeight: 1.15,
letterSpacing: "-0.02em",
color: theme.palette.text.primary,
}}
>
{title}
</Typography>
<Typography
sx={{
fontSize: 14,
color: theme.palette.text.secondary,
}}
>
{subtitle}
</Typography>
</Stack>
{children}
</Box>
</Stack>
<Stack
gap={theme.spacing(LAYOUT.MD)}
width={{
xs: "80%",
md: "25%",
lg: "15%",
flex={1}
justifyContent="space-between"
display={{ xs: "none", md: "flex" }}
sx={{
background: "linear-gradient(135deg, #0c4d3d 0%, #155a48 60%, #0f5d4a 100%)",
color: "#fff",
p: theme.spacing(28),
position: "relative",
overflow: "hidden",
"&::after": {
content: '""',
position: "absolute",
inset: 0,
backgroundImage: `
linear-gradient(45deg, rgba(255,255,255,0.04) 25%, transparent 25%),
linear-gradient(-45deg, rgba(255,255,255,0.04) 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, rgba(255,255,255,0.04) 75%),
linear-gradient(-45deg, transparent 75%, rgba(255,255,255,0.04) 75%)
`,
backgroundSize: "80px 80px",
backgroundPosition: "0 0, 0 40px, 40px -40px, -40px 0px",
maskImage: "linear-gradient(135deg, transparent 0%, black 70%)",
WebkitMaskImage: "linear-gradient(135deg, transparent 0%, black 70%)",
pointerEvents: "none",
},
}}
>
{children}
<div />
<Stack sx={{ position: "relative", zIndex: 1, gap: theme.spacing(4) }}>
<Typography
sx={{
fontSize: 13,
fontWeight: 600,
textTransform: "uppercase",
letterSpacing: "0.12em",
color: "rgba(255,255,255,0.75)",
}}
>
{t("pages.auth.brandPanel.eyebrow")}
</Typography>
<Typography
sx={{
fontSize: 44,
fontWeight: 400,
lineHeight: 1.1,
letterSpacing: "-0.02em",
maxWidth: 460,
}}
>
{t("pages.auth.brandPanel.tagline")}
</Typography>
<Typography
sx={{
fontSize: 17,
lineHeight: 1.5,
color: "rgba(255,255,255,0.75)",
maxWidth: 460,
}}
>
{t("pages.auth.brandPanel.description")}
</Typography>
</Stack>
<Typography
sx={{
position: "relative",
zIndex: 1,
color: "rgba(255,255,255,0.5)",
fontSize: 12,
}}
>
v{__APP_VERSION__}
</Typography>
</Stack>
</BasePage>
</Stack>
);
};
@@ -0,0 +1,158 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { useTheme } from "@mui/material/styles";
import { Link as RouterLink } from "react-router-dom";
import { Button } from "@/Components/inputs";
import { LAYOUT } from "@/Utils/Theme/constants";
import { EmptyStateIllustration } from "./EmptyStateIllustration";
import type { ReactNode } from "react";
interface EmptyStateProps {
title: string;
description?: ReactNode;
actionText?: string;
actionTo?: string;
onAction?: () => void;
secondaryText?: string;
secondaryHref?: string;
compact?: boolean;
fullscreen?: boolean;
alert?: ReactNode;
children?: ReactNode;
}
export const EmptyState = ({
title,
description,
actionText,
actionTo,
onAction,
secondaryText,
secondaryHref,
compact = false,
fullscreen = false,
alert,
children,
}: EmptyStateProps) => {
const theme = useTheme();
if (compact) {
return (
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(LAYOUT.MD)}
sx={{ py: theme.spacing(LAYOUT.MD), px: theme.spacing(LAYOUT.SM) }}
>
<EmptyStateIllustration
width={80}
height={60}
/>
<Stack sx={{ flex: 1 }}>
<Typography
sx={{
fontSize: 14,
fontWeight: 400,
color: theme.palette.text.secondary,
}}
>
{title}
</Typography>
{description && (
<Typography
sx={{
fontSize: 13,
color: theme.palette.text.secondary,
lineHeight: 1.55,
}}
>
{description}
</Typography>
)}
</Stack>
</Stack>
);
}
return (
<Stack
alignItems="center"
justifyContent="center"
sx={{
...(fullscreen && { minHeight: "65vh" }),
py: theme.spacing(LAYOUT.XXL),
px: theme.spacing(LAYOUT.MD),
textAlign: "center",
}}
>
<EmptyStateIllustration />
<Typography
sx={{
mt: theme.spacing(LAYOUT.MD),
mb: theme.spacing(LAYOUT.XS),
fontSize: 14,
fontWeight: 400,
color: theme.palette.text.secondary,
}}
>
{title}
</Typography>
{description && (
<Typography
sx={{
maxWidth: 360,
fontSize: 13,
color: theme.palette.text.secondary,
lineHeight: 1.55,
mb: theme.spacing(LAYOUT.LG),
}}
>
{description}
</Typography>
)}
{alert && (
<Stack sx={{ width: "100%", maxWidth: 520, mb: theme.spacing(LAYOUT.MD) }}>
{alert}
</Stack>
)}
{(actionText || secondaryText) && (
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(LAYOUT.MD)}
>
{actionText && (actionTo || onAction) && (
<Button
variant="contained"
color="primary"
{...(actionTo
? { component: RouterLink, to: actionTo }
: { onClick: onAction })}
>
{actionText}
</Button>
)}
{secondaryText && secondaryHref && (
<Typography
component="a"
href={secondaryHref}
target="_blank"
rel="noopener"
sx={{
fontSize: 13,
fontWeight: 500,
color: theme.palette.primary.main,
textDecoration: "none",
"&:hover": { textDecoration: "underline" },
}}
>
{secondaryText} →
</Typography>
)}
</Stack>
)}
{children}
</Stack>
);
};
@@ -0,0 +1,197 @@
import { useTheme } from "@mui/material/styles";
interface EmptyStateIllustrationProps {
width?: number | string;
height?: number | string;
}
const PALETTE = {
light: {
grid: "#CBD5E1",
backFill: "#F5F7FA",
backStroke: "#D5DBE3",
backInner: "#B6C2CF",
frontFill: "#EAF5F0",
frontStroke: "#7DBFAA",
frontInner: "#4DAF94",
},
dark: {
grid: "#3A4452",
backFill: "#475569",
backStroke: "#64748B",
backInner: "#CBD5E1",
frontFill: "#0F5D4A",
frontStroke: "#4DAF94",
frontInner: "#B5E2D2",
},
};
export const EmptyStateIllustration = ({
width = 160,
height = 120,
}: EmptyStateIllustrationProps) => {
const theme = useTheme();
const p = theme.palette.mode === "dark" ? PALETTE.dark : PALETTE.light;
return (
<svg
viewBox="0 0 160 120"
width={width}
height={height}
aria-hidden="true"
>
<g
stroke={p.grid}
strokeWidth="0.75"
>
<line
x1="16"
y1="0"
x2="16"
y2="120"
/>
<line
x1="56"
y1="0"
x2="56"
y2="120"
strokeDasharray="3 3"
/>
<line
x1="104"
y1="0"
x2="104"
y2="120"
strokeDasharray="3 3"
/>
<line
x1="144"
y1="0"
x2="144"
y2="120"
/>
<line
x1="0"
y1="12"
x2="160"
y2="12"
/>
<line
x1="0"
y1="44"
x2="160"
y2="44"
strokeDasharray="3 3"
/>
<line
x1="0"
y1="76"
x2="160"
y2="76"
strokeDasharray="3 3"
/>
<line
x1="0"
y1="108"
x2="160"
y2="108"
/>
</g>
<g>
<rect
x="38"
y="22"
width="92"
height="42"
rx="6"
fill={p.backFill}
stroke={p.backStroke}
strokeWidth="0.5"
/>
<rect
x="48"
y="32"
width="14"
height="22"
rx="2"
fill={p.backInner}
opacity="0.5"
/>
<rect
x="68"
y="34"
width="50"
height="2.5"
rx="1.25"
fill={p.backInner}
opacity="0.7"
/>
<rect
x="68"
y="42"
width="38"
height="2.5"
rx="1.25"
fill={p.backInner}
opacity="0.55"
/>
<rect
x="68"
y="50"
width="44"
height="2.5"
rx="1.25"
fill={p.backInner}
opacity="0.55"
/>
<rect
x="22"
y="50"
width="92"
height="42"
rx="6"
fill={p.frontFill}
stroke={p.frontStroke}
strokeWidth="0.5"
strokeOpacity="0.4"
/>
<rect
x="32"
y="60"
width="14"
height="22"
rx="2"
fill={p.frontInner}
opacity="0.35"
/>
<rect
x="52"
y="62"
width="50"
height="2.5"
rx="1.25"
fill={p.frontInner}
opacity="0.55"
/>
<rect
x="52"
y="70"
width="38"
height="2.5"
rx="1.25"
fill={p.frontInner}
opacity="0.4"
/>
<rect
x="52"
y="78"
width="44"
height="2.5"
rx="1.25"
fill={p.frontInner}
opacity="0.4"
/>
</g>
</svg>
);
};
@@ -1,12 +1,8 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { BulletPointCheck, SkeletonCard } from "@/Components/design-elements";
import { Button } from "@/Components/inputs";
import { SPACING, LAYOUT } from "@/Utils/Theme/constants";
import { useNavigate } from "react-router";
import useMediaQuery from "@mui/material/useMediaQuery";
import { useTheme } from "@mui/material/styles";
import { LAYOUT } from "@/Utils/Theme/constants";
import { EmptyState } from "./EmptyState";
import type { StackProps } from "@mui/material/Stack";
interface BaseFallbackProps extends StackProps {
@@ -15,30 +11,13 @@ interface BaseFallbackProps extends StackProps {
export const BaseFallback = ({ children, ...props }: BaseFallbackProps) => {
const theme = useTheme();
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
return (
<Stack
alignItems={"center"}
margin={isSmall ? "inherit" : "auto"}
marginTop={isSmall ? "33%" : "10%"}
width={{
sm: "90%",
md: "70%",
lg: "50%",
xl: "40%",
}}
padding={{ xs: theme.spacing(LAYOUT.MD), md: theme.spacing(LAYOUT.XXL) }}
bgcolor={theme.palette.background.paper}
border={1}
borderColor={theme.palette.divider}
borderRadius={theme.shape.borderRadius}
sx={{
borderStyle: "dashed",
}}
alignItems="center"
justifyContent="center"
sx={{ width: "100%", py: theme.spacing(LAYOUT.LG) }}
{...props}
>
<SkeletonCard showHalo={true} />
{children}
</Stack>
);
@@ -51,126 +30,56 @@ export const ErrorFallback = ({
title: string;
subtitle: string;
}) => {
const theme = useTheme();
return (
<BaseFallback>
<Typography
variant="h1"
marginY={theme.spacing(LAYOUT.XS)}
color={theme.palette.text.secondary}
>
{title}
</Typography>
<Typography>{subtitle}</Typography>
</BaseFallback>
<EmptyState
fullscreen
title={title}
description={subtitle}
/>
);
};
export const EmptyFallback = ({
title,
bullets,
description,
actionButtonText,
actionLink,
}: {
title: string;
bullets: any;
description?: string;
actionButtonText: string;
actionLink: string;
}) => {
const theme = useTheme();
const navigate = useNavigate();
return (
<BaseFallback>
<Stack
gap={theme.spacing(LAYOUT.MD)}
zIndex={1}
alignItems="center"
>
<Typography component="h1">{title}</Typography>
<Stack
sx={{
flexWrap: "wrap",
gap: theme.spacing(SPACING.MD),
maxWidth: "1100px",
width: "100%",
}}
>
{Array.isArray(bullets) &&
bullets?.map((bullet: string) => (
<BulletPointCheck
text={bullet}
key={`${bullet}-${Math.random()}`}
/>
))}
</Stack>
<Stack>
<Button
variant="contained"
color="primary"
onClick={() => navigate(actionLink)}
>
{actionButtonText}
</Button>
</Stack>
</Stack>
</BaseFallback>
<EmptyState
fullscreen
title={title}
description={description}
actionText={actionButtonText}
actionTo={actionLink}
/>
);
};
export const EmptyMonitorFallback = ({
page,
title,
bullets,
description,
actionButtonText,
actionLink,
}: {
page: string;
title: string;
bullets: any;
description?: string;
actionButtonText: string;
actionLink: string;
}) => {
const theme = useTheme();
const navigate = useNavigate();
return (
<BaseFallback>
<Stack
gap={theme.spacing(LAYOUT.MD)}
zIndex={1}
alignItems="center"
>
<Typography
component="h1"
color={theme.palette.primary.contrastText}
>
{title}
</Typography>
<Stack
sx={{
flexWrap: "wrap",
gap: theme.spacing(SPACING.MD),
maxWidth: "1100px",
width: "100%",
}}
>
{Array.isArray(bullets) &&
bullets?.map((bullet: string, index: number) => (
<BulletPointCheck
text={bullet}
key={`${(page + "Monitors").trim().split(" ")[0]}-${index}`}
/>
))}
</Stack>
<Stack>
<Button
variant="contained"
color="primary"
onClick={() => navigate(actionLink)}
>
{actionButtonText}
</Button>
</Stack>
</Stack>
</BaseFallback>
<EmptyState
fullscreen
title={title}
description={description}
actionText={actionButtonText}
actionTo={actionLink}
/>
);
};
@@ -114,6 +114,7 @@ export const DetailGauge = ({
upperValue,
lowerLabel,
lowerValue,
maxWidth = 225,
}: {
title: string;
progress: number;
@@ -121,13 +122,14 @@ export const DetailGauge = ({
upperValue?: string | number;
lowerLabel?: string;
lowerValue?: string | number;
maxWidth?: number;
}) => {
const theme = useTheme();
return (
<BaseChart
icon={null}
title={title}
maxWidth={225}
maxWidth={maxWidth}
>
<Stack
alignItems={"center"}
@@ -18,7 +18,7 @@ export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => {
return (
<Stack>
<Typography
fontSize={typographyLevels.xl}
fontSize={typographyLevels.xxl}
fontWeight={500}
overflow={"hidden"}
textOverflow={"ellipsis"}
@@ -35,7 +35,7 @@ export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => {
<Typography
fontSize={typographyLevels.l}
fontWeight={"bolder"}
fontFamily={"monospace"}
fontFamily={theme.typography.fontFamilyMonospace}
overflow={"hidden"}
textOverflow={"ellipsis"}
whiteSpace={"nowrap"}
@@ -0,0 +1,55 @@
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import { useTheme, alpha } from "@mui/material/styles";
import { LAYOUT } from "@/Utils/Theme/constants";
import { typographyLevels } from "@/Utils/Theme/Palette";
export const Severities = ["info", "warning", "error"] as const;
export type Severity = (typeof Severities)[number];
const SEVERITY_GLYPH: Record<Severity, string> = {
info: "ⓘ",
warning: "⚠",
error: "✕",
};
interface NoticeBannerProps {
severity?: Severity;
children: React.ReactNode;
}
export const NoticeBanner = ({ severity = "info", children }: NoticeBannerProps) => {
const theme = useTheme();
const tone = theme.palette[severity].main;
return (
<Stack
direction="row"
alignItems="flex-start"
gap={theme.spacing(LAYOUT.SM)}
width={"100%"}
p={theme.spacing(LAYOUT.MD)}
borderRadius={theme.shape.borderRadius}
border={`1px solid ${alpha(tone, 0.45)}`}
bgcolor={alpha(tone, 0.08)}
textAlign={"left"}
>
<Box
component="span"
color={tone}
fontSize={typographyLevels.xl}
lineHeight={1}
mt={LAYOUT.XXS}
aria-hidden
>
{SEVERITY_GLYPH[severity]}
</Box>
<Typography
color={theme.palette.text.primary}
lineHeight={1.55}
>
{children}
</Typography>
</Stack>
);
};
@@ -0,0 +1,164 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import Box from "@mui/material/Box";
import Tooltip from "@mui/material/Tooltip";
import { Link as RouterLink } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { useTheme } from "@mui/material/styles";
import { ChevronLeft, HelpCircle, FileText, Code } from "lucide-react";
import type { ReactNode } from "react";
const EXTERNAL_LINKS = {
support: "https://discord.com/invite/NAb6H3UTjK",
docs: "https://checkmate.so/docs",
changelog: "https://github.com/bluewave-labs/checkmate/releases",
};
interface PageHeaderProps {
title: string;
description?: ReactNode;
backTo?: string;
backLabel?: string;
}
export const PageHeader = ({
title,
description,
backTo,
backLabel,
}: PageHeaderProps) => {
const theme = useTheme();
const { t } = useTranslation();
const linkItems: { key: keyof typeof EXTERNAL_LINKS; icon: ReactNode }[] = [
{
key: "support",
icon: (
<HelpCircle
size={16}
strokeWidth={1.6}
/>
),
},
{
key: "docs",
icon: (
<FileText
size={16}
strokeWidth={1.6}
/>
),
},
{
key: "changelog",
icon: (
<Code
size={16}
strokeWidth={1.6}
/>
),
},
];
return (
<Stack
direction="row"
alignItems="flex-start"
justifyContent="space-between"
gap={6}
sx={{ mb: theme.spacing(8) }}
>
<Stack gap={theme.spacing(1)}>
{backTo && backLabel && (
<Box
component={RouterLink}
to={backTo}
sx={{
display: "inline-flex",
alignItems: "center",
gap: 0.5,
textDecoration: "none",
color: theme.palette.text.secondary,
fontSize: 13,
mb: theme.spacing(1),
"&:hover": { color: theme.palette.text.primary },
}}
>
<ChevronLeft
size={14}
strokeWidth={1.8}
/>
{backLabel}
</Box>
)}
<Typography
sx={{
fontSize: 26,
fontWeight: 400,
color: theme.palette.text.primary,
lineHeight: 1.15,
letterSpacing: "-0.02em",
}}
>
{title}
</Typography>
{description && (
<Typography
sx={{
fontSize: 14,
color: theme.palette.text.secondary,
lineHeight: 1.55,
maxWidth: 640,
}}
>
{description}
</Typography>
)}
</Stack>
<Box
sx={{
display: "inline-flex",
alignItems: "center",
border: `1px solid ${theme.palette.divider}`,
borderRadius: 1,
overflow: "hidden",
backgroundColor: theme.palette.background.paper,
flexShrink: 0,
}}
>
{linkItems.map((item, idx) => (
<Tooltip
key={item.key}
title={t(`components.pageHeader.links.${item.key}`)}
>
<Box
component="a"
href={EXTERNAL_LINKS[item.key]}
target="_blank"
rel="noopener noreferrer"
sx={{
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
width: 36,
height: 32,
color: theme.palette.text.secondary,
borderRight:
idx < linkItems.length - 1
? `1px solid ${theme.palette.divider}`
: "none",
"&:hover": {
backgroundColor: theme.palette.action.hover,
color: theme.palette.text.primary,
},
}}
>
{item.icon}
</Box>
</Tooltip>
))}
</Box>
</Stack>
);
};
@@ -53,7 +53,7 @@ export const ConfigBox = ({
rightContent,
}: {
title: string;
subtitle: string;
subtitle: React.ReactNode;
leftContent?: React.ReactNode;
rightContent: React.ReactNode;
}) => {
@@ -63,9 +63,9 @@ export const ConfigBox = ({
left={
<Stack spacing={theme.spacing(LAYOUT.XS)}>
<Typography
textTransform={"capitalize"}
component="h2"
variant="h2"
variant="eyebrow"
color="text.secondary"
>
{title}
</Typography>
@@ -69,7 +69,12 @@ export const StatBox = ({
>
<Stack onClick={onClick}>
<Box sx={{ display: "flex", alignItems: "center", gap: theme.spacing(2) }}>
<Typography color={textColor}>{title}</Typography>
<Typography
variant="eyebrow"
color={palette ? textColor : "text.secondary"}
>
{title}
</Typography>
{tooltip && (
<TooltipWithInfo
title={tooltip}
@@ -21,7 +21,7 @@ export const BGBox = ({ children, sx }: StatusBoxProps) => {
overflow: "hidden",
position: "relative",
flex: 1,
padding: theme.spacing(4),
padding: theme.spacing(6),
...sx,
}}
>
@@ -65,9 +65,13 @@ const StatusBox = ({
<BGBox sx={sx}>
<Stack spacing={theme.spacing(4)}>
<Typography
variant={"h2"}
textTransform="uppercase"
color={theme.palette.text.secondary}
sx={{
fontSize: 11,
fontWeight: 500,
letterSpacing: "0.08em",
}}
>
{label}
</Typography>
@@ -0,0 +1,40 @@
import { useTranslation } from "react-i18next";
import {
formatStatusCode,
getStatusCodeTooltip,
getStatusCodeValueType,
} from "@/Utils/statusCode";
import { ValueLabel } from "./StatusLabel";
import { Tooltip } from "./Tooltip";
export type StatusCodeLabelProps = {
statusCode: number | null | undefined;
message?: string | null;
fallback?: string;
};
export const StatusCodeLabel = ({
statusCode,
message,
fallback = "N/A",
}: StatusCodeLabelProps) => {
const { t } = useTranslation();
if (!statusCode) {
return <>{fallback}</>;
}
const text = formatStatusCode(statusCode, t);
const tooltip = getStatusCodeTooltip(statusCode, message, t);
const body = (
<ValueLabel
value={getStatusCodeValueType(statusCode)}
text={text}
/>
);
if (!tooltip) return body;
return (
<Tooltip title={tooltip}>
<span>{body}</span>
</Tooltip>
);
};
+39 -45
View File
@@ -18,10 +18,9 @@ import {
ChevronsRight,
ChevronLeft,
ChevronRight,
Coffee,
Ellipsis,
PartyPopper,
} from "lucide-react";
import { EmptyState } from "./EmptyState";
import TablePagination from "@mui/material/TablePagination";
import type { TablePaginationOwnProps } from "@mui/material/TablePagination";
@@ -36,6 +35,9 @@ import useMediaQuery from "@mui/material/useMediaQuery";
export type Header<T> = {
id: number | string;
content: React.ReactNode;
mobileLabel?: React.ReactNode;
hideMobileLabel?: boolean;
align?: "left" | "center" | "right" | "justify" | "inherit";
onClick?: (event: React.MouseEvent<HTMLTableCellElement | null>, row: T) => void;
render: (row: T) => React.ReactNode;
};
@@ -110,6 +112,24 @@ export function DataTable<
key={key}
>
{headers.map((header) => {
if (header.hideMobileLabel) {
return (
<Grid2
container
key={header.id}
>
<Grid2
size={12}
display="flex"
alignItems="center"
justifyContent="flex-end"
>
{header.render(row)}
</Grid2>
</Grid2>
);
}
return (
<Grid2
container
@@ -124,7 +144,7 @@ export function DataTable<
component="div"
color={theme.palette.text.primary}
>
{header.content}
{header.mobileLabel ?? header.content}
</Typography>
</Grid2>
<Grid2
@@ -163,14 +183,19 @@ export function DataTable<
},
"& .MuiTableHead-root .MuiTableRow-root": {
height: "28px",
borderBottom: `1px solid ${theme.palette.divider}`,
},
"& .MuiTableHead-root .MuiTableCell-root": {
borderBottom: "none",
},
"& :is(th)": {
backgroundColor: theme.palette.background.paper,
color: theme.palette.text.secondary,
fontWeight: 500,
textTransform: "uppercase",
letterSpacing: "0.08em",
padding: `${theme.spacing(SPACING.LG)} ${theme.spacing(LAYOUT.MD)}`,
fontSize: theme.typography.fontSize,
fontSize: 11,
},
"& :is(td)": {
backgroundColor: theme.palette.background.paper,
@@ -189,7 +214,7 @@ export function DataTable<
{headers.map((header, idx) => {
return (
<TableCell
align={idx === 0 ? "left" : "center"}
align={header.align ?? (idx === 0 ? "left" : "center")}
key={header.id}
>
{header.content}
@@ -218,7 +243,7 @@ export function DataTable<
{headers.map((header, index) => {
return (
<TableCell
align={index === 0 ? "left" : "center"}
align={header.align ?? (index === 0 ? "left" : "center")}
key={header.id}
onClick={
header.onClick ? (e) => header.onClick!(e, row) : undefined
@@ -548,49 +573,18 @@ export const Pagination = ({ ...props }: PaginationProps) => {
);
};
const EmptyView = ({ text, positive }: { text?: string; positive?: boolean }) => {
const theme = useTheme();
const EmptyView = ({ text }: { text?: string; positive?: boolean }) => {
const { t } = useTranslation();
const Icon = positive ? PartyPopper : Coffee;
const theme = useTheme();
return (
<Stack
alignItems={"center"}
justifyContent={"center"}
<Box
sx={{
py: theme.spacing(LAYOUT.XL),
px: theme.spacing(LAYOUT.XS),
borderWidth: 1,
borderStyle: "solid",
borderColor: theme.palette.divider,
borderRadius: theme.shape.borderRadius,
textAlign: "center",
border: `1px solid ${theme.palette.divider}`,
borderRadius: 1,
backgroundColor: theme.palette.background.paper,
}}
>
<Box
sx={{
width: 64,
height: 64,
borderRadius: "50%",
backgroundColor: theme.palette.action.hover,
display: "flex",
alignItems: "center",
justifyContent: "center",
mb: theme.spacing(SPACING.LG),
}}
>
<Icon
size={28}
strokeWidth={1}
color={theme.palette.text.secondary}
/>
</Box>
<Typography
variant="subtitle1"
color={theme.palette.text.primary}
sx={{ fontWeight: 500 }}
>
{text ?? t("common.table.empty")}
</Typography>
</Stack>
<EmptyState title={text ?? t("common.table.empty")} />
</Box>
);
};
@@ -16,16 +16,15 @@ export const Tooltip = ({ placement = "top", ...props }: StyledTooltipProps) =>
slotProps={{
tooltip: {
sx: {
background:
backgroundColor:
theme.palette.mode === "dark"
? "linear-gradient(135deg, #1a1a2e 0%, #16213e 100%)"
: "linear-gradient(135deg, #2c3e50 0%, #1a252f 100%)",
backgroundColor: "transparent",
color: "#ffffff",
? theme.palette.grey[900]
: theme.palette.grey[800],
color: theme.palette.common.white,
fontSize: "13px",
padding: `${theme.spacing(4)} ${theme.spacing(5)}`,
borderRadius: `${theme.shape.borderRadius}px`,
boxShadow: "0 8px 24px rgba(0, 0, 0, 0.4)",
boxShadow: theme.shadows[6],
},
},
}}
@@ -1,5 +1,8 @@
export * from "./Breadcrumb";
export * from "./PageHeader";
export * from "./BasePage";
export * from "./EmptyState";
export * from "./EmptyStateIllustration";
export * from "./Fallback";
export * from "./SkeletonCard";
export * from "./BulletPointCheck";
@@ -19,6 +22,8 @@ export * from "./Gauge";
export * from "./Tabs";
export * from "./SplitBox";
export * from "./TextLink";
export * from "./NoticeBanner";
export * from "./OfflineBanner";
export * from "./Avatar";
export * from "./StatusCodeLabel";
export * from "./StrategyBadge";
+31 -9
View File
@@ -1,19 +1,43 @@
import Button from "@mui/material/Button";
import type { ButtonProps } from "@mui/material/Button";
import { useTheme } from "@mui/material/styles";
import { useTheme, darken } from "@mui/material/styles";
import { HOVER } from "@/Utils/Theme/constants";
const PALETTE_COLORS = [
"primary",
"secondary",
"error",
"warning",
"info",
"success",
] as const;
type PaletteColor = (typeof PALETTE_COLORS)[number];
export const ButtonInput = ({ sx, ...props }: ButtonProps) => {
const theme = useTheme();
const outlinedSx =
const hoverOverrides: Record<string, unknown> = { boxShadow: "none" };
if (props.variant === "outlined") {
hoverOverrides.borderColor = theme.palette.text.secondary;
}
if (props.variant === "contained") {
const colorKey = (props.color ?? "primary") as PaletteColor;
const palette = theme.palette[colorKey];
if (palette && typeof palette === "object" && "main" in palette) {
hoverOverrides.backgroundColor = darken(palette.main, HOVER.DARKEN);
}
}
const variantSx =
props.variant === "outlined"
? {
color: theme.palette.text.primary,
borderColor: theme.palette.divider,
"&:hover": {
borderColor: theme.palette.text.secondary,
},
}
: {};
return (
<Button
{...props}
@@ -26,10 +50,8 @@ export const ButtonInput = ({ sx, ...props }: ButtonProps) => {
textOverflow: "ellipsis",
overflow: "hidden",
boxShadow: "none",
"&:hover": {
boxShadow: "none",
},
...outlinedSx,
"&:hover": hoverOverrides,
...variantSx,
...sx,
}}
/>
+48 -12
View File
@@ -2,10 +2,12 @@ import Dialog from "@mui/material/Dialog";
import type { DialogProps } from "@mui/material/Dialog";
import DialogActions from "@mui/material/DialogActions";
import DialogContent from "@mui/material/DialogContent";
import DialogContentText from "@mui/material/DialogContentText";
import DialogTitle from "@mui/material/DialogTitle";
import Typography from "@mui/material/Typography";
import Box from "@mui/material/Box";
import { Button } from "@/Components/inputs";
import { typographyLevels } from "@/Utils/Theme/Palette";
import { LAYOUT } from "@/Utils/Theme/constants";
import { useTheme } from "@mui/material/styles";
import { useTranslation } from "react-i18next";
import type { ReactNode } from "react";
@@ -16,7 +18,6 @@ export const DialogInput = ({
onConfirm,
onCancel,
confirmColor = "primary",
cancelColor = "error",
loading = false,
cancelText,
confirmText,
@@ -31,7 +32,6 @@ export const DialogInput = ({
onConfirm?(item: any): any;
onCancel?(item: any): any;
confirmColor?: "error" | "primary";
cancelColor?: "error" | "primary";
loading?: boolean;
cancelText?: string;
confirmText?: string;
@@ -41,22 +41,58 @@ export const DialogInput = ({
additionalButtons?: ReactNode;
}) => {
const { t } = useTranslation();
const theme = useTheme();
return (
<Dialog
open={open}
maxWidth={maxWidth}
fullWidth={fullWidth}
onClose={(_event, reason) => {
if (reason !== "backdropClick" && onCancel) onCancel(undefined);
}}
>
{title && <DialogTitle sx={{ fontSize: typographyLevels.l }}>{title}</DialogTitle>}
<DialogContent>
{content && <DialogContentText>{content}</DialogContentText>}
{children}
</DialogContent>
<DialogActions>
{title && (
<DialogTitle
pb={theme.spacing(LAYOUT.XS)}
bgcolor={theme.palette.action.hover}
borderBottom={`1px solid ${theme.palette.divider}`}
>
<Typography
component="span"
variant="h2"
fontWeight={600}
display={"block"}
>
{title}
</Typography>
{content && (
<Typography
variant="body1"
color={theme.palette.text.secondary}
mt={theme.spacing(LAYOUT.XS)}
display={"block"}
>
{content}
</Typography>
)}
</DialogTitle>
)}
{children && (
<DialogContent sx={{ pt: theme.spacing(LAYOUT.MD) }}>
<Box pt={theme.spacing(LAYOUT.XS)}>{children}</Box>
</DialogContent>
)}
<DialogActions
sx={{
p: theme.spacing(LAYOUT.MD),
pt: theme.spacing(LAYOUT.SM),
backgroundColor: theme.palette.action.hover,
borderTop: `1px solid ${theme.palette.divider}`,
}}
>
<Button
loading={loading}
variant="contained"
color={cancelColor}
variant="outlined"
onClick={onCancel}
>
{cancelText ?? t("common.buttons.cancel")}
+1 -1
View File
@@ -15,9 +15,9 @@ export const FieldLabel = ({ children, required = false, htmlFor }: FieldLabelPr
return (
<Typography
component="label"
variant="body1"
htmlFor={htmlFor}
sx={{
fontSize: "14px",
fontWeight: 500,
color: theme.palette.text.secondary,
marginBottom: theme.spacing(2),
+6 -1
View File
@@ -126,7 +126,12 @@ export const ImageUpload = ({
onChange={(e) => handleFile(e.target.files?.[0])}
style={{ position: "absolute", inset: 0, opacity: 0, cursor: "pointer" }}
/>
<Upload size={24} />
<Upload
size={20}
strokeWidth={1.5}
color="currentColor"
style={{ opacity: 0.7 }}
/>
<Typography
variant="body2"
color="text.secondary"
@@ -1,26 +1,34 @@
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import MenuItem from "@mui/material/MenuItem";
import { Select } from "@/Components/inputs";
import "flag-icons/css/flag-icons.min.css";
import { useTranslation } from "react-i18next";
import { useTheme } from "@mui/material";
import { useSelector } from "react-redux";
import { useDispatch } from "react-redux";
import { setLanguage } from "@/Features/UI/uiSlice";
import type { RootState } from "@/Types/state";
const langMap: Record<string, string> = {
cs: "cz",
ja: "jp",
uk: "ua",
vi: "vn",
export const languageNames: Record<string, string> = {
en: "English",
de: "Deutsch",
ca: "Català",
es: "Español",
fr: "Français",
"pt-BR": "Português (BR)",
cs: "Čeština",
fi: "Suomi",
tr: "Türkçe",
ru: "Русский",
uk: "Українська",
ar: "العربية",
th: "ไทย",
vi: "Tiếng Việt",
ja: "日本語",
"zh-CN": "简体中文",
"zh-TW": "繁體中文",
};
export const LanguageSelector = () => {
const { i18n } = useTranslation();
const theme = useTheme();
const { language = "en" } = useSelector((state: RootState) => state.ui);
const dispatch = useDispatch();
const handleChange = (event: any) => {
@@ -35,64 +43,15 @@ export const LanguageSelector = () => {
value={language}
onChange={handleChange}
size="small"
// sx={{
// minWidth: 80,
// "& .MuiSelect-select": {
// display: "flex",
// alignItems: "center",
// justifyContent: "center",
// },
// "& .MuiSelect-icon": {
// alignSelf: "center",
// },
// }}
>
{languages.map((lang) => {
let parsedLang = lang === "en" ? "gb" : lang;
if (parsedLang.includes("-")) {
parsedLang = parsedLang.split("-")[1].toLowerCase();
}
parsedLang = langMap[parsedLang] || parsedLang;
const flag = parsedLang ? `fi fi-${parsedLang}` : null;
return (
<MenuItem
key={lang}
value={lang}
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<Stack
direction="row"
spacing={theme.spacing(2)}
alignItems="center"
justifyContent="center"
>
<Box
component="span"
sx={{
display: "flex",
alignItems: "center",
}}
>
{flag && <span className={flag} />}
</Box>
<Box
component="span"
sx={{ textTransform: "uppercase" }}
>
{lang}
</Box>
</Stack>
</MenuItem>
);
})}
{languages.map((lang) => (
<MenuItem
key={lang}
value={lang}
>
{languageNames[lang] ?? lang}
</MenuItem>
))}
</Select>
);
};
+1 -41
View File
@@ -1,52 +1,13 @@
import Radio from "@mui/material/Radio";
import type { RadioProps } from "@mui/material/Radio";
import { useTheme } from "@mui/material/styles";
import { Circle, CircleDot } from "lucide-react";
import FormControlLabel from "@mui/material/FormControlLabel";
import Typography from "@mui/material/Typography";
interface RadioInputProps extends RadioProps {}
export const RadioInput = ({ ...props }: RadioInputProps) => {
const theme = useTheme();
return (
<Radio
{...props}
icon={
<Circle
size={16}
strokeWidth={1.5}
/>
}
checkedIcon={
<CircleDot
size={14}
strokeWidth={1.5}
/>
}
sx={{
padding: 0,
mt: theme.spacing(0.5),
color: theme.palette.text.secondary,
"&.Mui-checked": {
color: theme.palette.primary.main,
"& svg circle": {
fill: theme.palette.primary.main,
},
},
"& .MuiSvgIcon-root": {
fontSize: 16,
},
"& svg": {
stroke: "currentColor",
},
"& svg path, & svg line, & svg polyline, & svg rect, & svg circle": {
stroke: "currentColor",
fill: "none",
},
}}
/>
);
return <Radio {...props} />;
};
export const RadioWithDescription = ({
@@ -79,7 +40,6 @@ export const RadioWithDescription = ({
backgroundColor: theme.palette.background.paper,
},
"& .MuiButtonBase-root": {
p: 0,
mr: theme.spacing(6),
},
}}
+2 -3
View File
@@ -11,15 +11,14 @@ interface SelectInputProps<T> extends Omit<SelectProps<T>, "label"> {
fieldLabel?: string;
required?: boolean;
placeholder?: string;
placeholderColor?: string;
}
const SelectInputInner = <T,>(
{ fieldLabel, required, placeholder, placeholderColor, ...props }: SelectInputProps<T>,
{ fieldLabel, required, placeholder, ...props }: SelectInputProps<T>,
ref: React.ForwardedRef<HTMLDivElement>
) => {
const theme = useTheme();
const emptyPlaceholderColor = placeholderColor || theme.palette.text.disabled;
const emptyPlaceholderColor = theme.palette.text.secondary;
const renderValue = (selected: unknown) => {
const isMultiple = Boolean((props as { multiple?: boolean }).multiple);
+9 -17
View File
@@ -21,24 +21,16 @@ export const SwitchComponent = forwardRef<HTMLInputElement, SwitchComponentProps
},
}}
sx={[
{
"& .MuiSwitch-switchBase": {
"&.Mui-checked": {
color: "#E0E0E0",
"& + .MuiSwitch-track": {
backgroundColor: theme.palette.primary.main,
opacity: 1,
border: 0,
...(dualOption
? [
{
"& .MuiSwitch-track": {
backgroundColor: theme.palette.primary.main,
opacity: 1,
},
},
},
},
...(dualOption && {
"& .MuiSwitch-track": {
backgroundColor: theme.palette.primary.main,
opacity: 1,
},
}),
},
]
: []),
...additionalSx,
]}
/>
+35 -20
View File
@@ -5,6 +5,7 @@ import { typographyLevels } from "@/Utils/Theme/Palette";
import { useTheme } from "@mui/material/styles";
import Stack from "@mui/material/Stack";
import { FieldLabel } from "./FieldLabel";
import { LAYOUT } from "@/Utils/Theme/constants";
interface TextInputProps extends Omit<TextFieldProps, "label"> {
fieldLabel?: string;
@@ -12,39 +13,53 @@ interface TextInputProps extends Omit<TextFieldProps, "label"> {
}
export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(function TextInput(
{ fieldLabel, required, ...props },
{ fieldLabel, required, sx, ...props },
ref
) {
const theme = useTheme();
const innerSx = {
width: "100%",
"& .MuiOutlinedInput-root": {
borderRadius: theme.shape.borderRadius,
height: 34,
fontSize: typographyLevels.base,
overflow: "hidden",
},
"& .MuiOutlinedInput-notchedOutline": {
borderColor: theme.palette.divider,
},
"&:hover .MuiOutlinedInput-notchedOutline": {
borderColor: theme.palette.divider,
},
"& .MuiFormHelperText-root": {
marginLeft: 0,
marginRight: 0,
marginTop: theme.spacing(1),
},
"& input:-webkit-autofill, & input:-webkit-autofill:hover, & input:-webkit-autofill:focus, & input:-webkit-autofill:active":
{
WebkitBoxShadow: `0 0 0 100px ${theme.palette.background.default} inset !important`,
WebkitTextFillColor: `${theme.palette.text.primary} !important`,
caretColor: theme.palette.text.primary,
transition: "background-color 5000s ease-in-out 0s",
},
};
const input = (
<TextField
{...props}
inputRef={ref}
sx={{
"& .MuiOutlinedInput-root": {
borderRadius: theme.shape.borderRadius,
height: 34,
fontSize: typographyLevels.base,
},
"& .MuiOutlinedInput-notchedOutline": {
borderColor: theme.palette.divider,
},
"&:hover .MuiOutlinedInput-notchedOutline": {
borderColor: theme.palette.divider,
},
"& .MuiFormHelperText-root": {
marginLeft: 0,
marginRight: 0,
marginTop: theme.spacing(1),
},
}}
sx={fieldLabel ? innerSx : { ...innerSx, ...sx }}
/>
);
if (fieldLabel) {
return (
<Stack spacing={theme.spacing(2)}>
<Stack
spacing={theme.spacing(LAYOUT.XXS)}
sx={sx}
>
<FieldLabel required={required}>{fieldLabel}</FieldLabel>
{input}
</Stack>
+19 -1
View File
@@ -3,8 +3,17 @@ import ToggleButtonGroup from "@mui/material/ToggleButtonGroup";
import type { ToggleButtonProps } from "@mui/material/ToggleButton";
import type { ToggleButtonGroupProps } from "@mui/material/ToggleButtonGroup";
import { useTheme } from "@mui/material/styles";
import { alpha } from "@mui/material/styles";
export const ToggleButtonInput = ({ sx, ...props }: ToggleButtonProps) => {
const theme = useTheme();
const isDark = theme.palette.mode === "dark";
const selectedBg = isDark
? alpha(theme.palette.primary.main, 0.22)
: alpha(theme.palette.primary.main, 0.06);
const selectedHoverBg = isDark
? alpha(theme.palette.primary.main, 0.3)
: alpha(theme.palette.primary.main, 0.1);
return (
<ToggleButton
{...props}
@@ -17,8 +26,18 @@ export const ToggleButtonInput = ({ sx, ...props }: ToggleButtonProps) => {
textOverflow: "ellipsis",
overflow: "hidden",
boxShadow: "none",
color: theme.palette.text.secondary,
"&:hover": {
boxShadow: "none",
backgroundColor: theme.palette.action.hover,
},
"&.Mui-selected": {
backgroundColor: selectedBg,
color: theme.palette.text.primary,
fontWeight: 500,
"&:hover": {
backgroundColor: selectedHoverBg,
},
},
...sx,
}}
@@ -34,7 +53,6 @@ export const ToggleButtonGroupInput = ({ sx, ...props }: ToggleButtonGroupProps)
sx={{
"& .MuiToggleButtonGroup-grouped": {
borderColor: theme.palette.divider,
borderRadius: 0,
"&:first-of-type": {
borderTopLeftRadius: 2,
@@ -0,0 +1,102 @@
import React from "react";
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import Paper from "@mui/material/Paper";
import Slide from "@mui/material/Slide";
import IconButton from "@mui/material/IconButton";
import { X } from "lucide-react";
import { useTranslation } from "react-i18next";
import { useTheme } from "@mui/material/styles";
import useMediaQuery from "@mui/material/useMediaQuery";
import { LAYOUT } from "@/Utils/Theme/constants";
import { useSidebar } from "@/Hooks/useSidebar";
interface BulkActionsBarProps {
selectedCount: number;
onCancel: () => void;
children?: React.ReactNode;
}
export const BulkActionsBar = ({
selectedCount,
onCancel,
children,
}: BulkActionsBarProps) => {
const { t } = useTranslation();
const theme = useTheme();
const isOpen = selectedCount > 0;
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
const { width, collapsedWidth, transition } = useSidebar();
return (
<Box
sx={{
position: "fixed",
bottom: theme.spacing(LAYOUT.XS),
left: isSmall ? collapsedWidth : width,
right: 0,
display: "flex",
justifyContent: "center",
pointerEvents: "none",
zIndex: theme.zIndex.snackbar,
transition: transition.replace("width", "left"),
}}
>
<Slide
direction="up"
in={isOpen}
mountOnEnter
unmountOnExit
>
<Paper
elevation={6}
sx={{
pointerEvents: "auto",
px: LAYOUT.MD,
py: LAYOUT.XS,
borderRadius: theme.shape.borderRadius,
backgroundColor: theme.palette.background.paper,
border: `1px solid ${theme.palette.divider}`,
display: "flex",
alignItems: "center",
gap: LAYOUT.XS,
boxShadow: theme.shadows[8],
}}
>
<Stack
direction="row"
alignItems="center"
gap={LAYOUT.XXS}
>
<Typography
variant="body1"
fontWeight={600}
color={theme.palette.text.primary}
>
{t("pages.common.monitors.actions.bulkSelected", {
count: selectedCount,
})}
</Typography>
<IconButton
size="small"
onClick={onCancel}
aria-label={t("common.buttons.cancel")}
>
<X size={18} />
</IconButton>
</Stack>
<Stack
direction="row"
alignItems="center"
gap={LAYOUT.XXS}
>
{children}
</Stack>
</Paper>
</Slide>
</Box>
);
};
@@ -3,6 +3,8 @@ import { Box, Typography, Stack, useTheme, GlobalStyles } from "@mui/material";
import Map, { Marker, Popup } from "react-map-gl/maplibre";
import type { MapRef } from "react-map-gl/maplibre";
import type { FlatGeoCheck } from "@/Types/GeoCheck";
import { useTranslation } from "react-i18next";
import { formatStatusCode } from "@/Utils/statusCode";
import "maplibre-gl/dist/maplibre-gl.css";
interface GeoChecksMapProps {
@@ -14,6 +16,7 @@ export const GeoChecksMap = ({ geoChecks }: GeoChecksMapProps) => {
const [selectedCheck, setSelectedCheck] = useState<FlatGeoCheck | null>(null);
const theme = useTheme();
const isDarkMode = theme.palette.mode === "dark";
const { t } = useTranslation();
const mapPopupStyles = (
<GlobalStyles
@@ -174,7 +177,7 @@ export const GeoChecksMap = ({ geoChecks }: GeoChecksMapProps) => {
>
Status Code:
</Typography>{" "}
{selectedCheck.statusCode}
{formatStatusCode(selectedCheck.statusCode, t)}
</Typography>
<Typography variant="body2">
<Typography
@@ -25,10 +25,7 @@ export const MonitorStatBoxes = ({
}
const timeOfLastFailure = monitorStats?.timeOfLastFailure || 0;
const timeSinceLastFailure =
timeOfLastFailure > 0
? Date.now() - timeOfLastFailure
: Date.now() - new Date(monitorStats?.createdAt || 0).getTime();
const timeSinceLastFailure = timeOfLastFailure > 0 ? Date.now() - timeOfLastFailure : 0;
// Determine time since last check
const timeOfLastCheck = monitorStats?.lastCheckTimestamp || 0;
@@ -42,6 +39,12 @@ export const MonitorStatBoxes = ({
const streakTime = prettyMilliseconds(timeSinceLastFailure, options);
const lastCheckTime = prettyMilliseconds(timeSinceLastCheck, options);
const isActive =
monitor?.status === "up" ||
monitor?.status === "paused" ||
monitor?.status === "maintenance" ||
monitor?.status === "initializing" ||
monitor?.status === "breached";
const palette = getStatusPalette(monitor?.status);
return (
@@ -51,8 +54,12 @@ export const MonitorStatBoxes = ({
>
<StatBox
palette={palette}
title={t("pages.common.monitors.statBoxes.activeFor")}
subtitle={streakTime}
title={
isActive
? t("pages.common.monitors.statBoxes.activeFor")
: t("pages.common.monitors.statBoxes.serviceIsDown")
}
subtitle={isActive ? streakTime : ""}
/>
<StatBox
title={t("pages.common.monitors.statBoxes.lastCheck")}
@@ -52,7 +52,7 @@ export const RadialAvgResponse = ({ avg, max }: { avg: number; max: number }) =>
>
<RadialBar
dataKey="value"
background={{ fill: theme.palette[palette].light }}
background={{ fill: theme.palette.action.disabledBackground }}
>
<Cell visibility={"hidden"} />
<Cell fill={theme.palette[palette].main} />
@@ -75,13 +75,14 @@ export const RadialAvgResponse = ({ avg, max }: { avg: number; max: number }) =>
}}
>
<Typography
variant="h6"
variant="eyebrow"
color={theme.palette[palette].main}
textAlign={"center"}
>
{msg[palette]}
</Typography>
<Typography
variant="h6"
variant="h1"
textAlign={"center"}
>{`${avg?.toFixed()}ms`}</Typography>
</Stack>
+1
View File
@@ -14,3 +14,4 @@ export * from "./charts/HistogramPageSpeedDetails";
export * from "./charts/HistogramPageSpeedDetailsTooltip";
export * from "./charts/HistogramInfrastructure";
export * from "./HeaderMonitorsSummary";
export * from "./BulkActionsBar";
+6 -1
View File
@@ -68,7 +68,12 @@ export const AuthFooter = ({ collapsed, accountMenuItems }: AuthFooterProps) =>
gap: theme.spacing(LAYOUT.MD),
borderRadius: theme.shape.borderRadius,
pl: theme.spacing(LAYOUT.XS),
"& svg": { stroke: theme.palette.text.secondary },
"& svg": {
height: 16,
width: 16,
opacity: 0.81,
stroke: theme.palette.text.secondary,
},
};
return (
+15 -12
View File
@@ -6,6 +6,7 @@ import { toggleSidebar } from "@/Features/UI/uiSlice.js";
import { useDispatch } from "react-redux";
import { useTranslation } from "react-i18next";
import useSidebar from "@/Hooks/useSidebar";
import KingIcon from "@/assets/icons/checkmate-icon.svg?react";
export const Logo = (props: StackProps) => {
const { t } = useTranslation();
@@ -23,19 +24,21 @@ export const Logo = (props: StackProps) => {
sx={{ cursor: "pointer" }}
{...props}
>
<Typography
minWidth={theme.spacing(16)}
minHeight={theme.spacing(16)}
display={"flex"}
justifyContent={"center"}
alignItems={"center"}
bgcolor={theme.palette.primary.main}
borderRadius={theme.shape.borderRadius}
color={theme.palette.primary.contrastText}
fontSize={18}
<Box
sx={{
width: theme.spacing(16),
height: theme.spacing(16),
display: "flex",
justifyContent: "center",
alignItems: "center",
"& svg": {
width: "100%",
height: "100%",
},
}}
>
C
</Typography>
<KingIcon />
</Box>
<Box
overflow={"hidden"}
sx={{
+2 -2
View File
@@ -9,7 +9,7 @@ import {
AlertTriangle,
Wifi,
Wrench,
Database,
ScrollText,
Settings,
HelpCircle,
MessageCircle,
@@ -64,7 +64,7 @@ export const getMenu = (t: Function) => {
{
name: t("components.sidebar.menu.logs"),
path: "logs",
icon: <Icon icon={Database} />,
icon: <Icon icon={ScrollText} />,
},
{
name: t("components.sidebar.menu.settings"),
+4 -2
View File
@@ -22,7 +22,9 @@ export const NavItem = ({
}) => {
const { collapsed } = useSidebar();
const theme = useTheme();
const iconStroke = selected ? theme.palette.primary.main : theme.palette.text.secondary;
const iconStroke = selected
? theme.palette.sidebar.accent
: theme.palette.text.secondary;
const buttonBgColor = selected ? theme.palette.action.selected : "transparent";
const buttonBgHoverColor = selected
@@ -79,7 +81,7 @@ export const NavItem = ({
},
".MuiListItemButton-root:hover &": {
"& svg path, & svg line, & svg polyline, & svg rect, & svg circle": {
stroke: theme.palette.primary.main,
stroke: theme.palette.sidebar.accent,
},
},
}}
+2 -20
View File
@@ -6,7 +6,7 @@ import Divider from "@mui/material/Divider";
import { LAYOUT } from "@/Utils/Theme/constants";
import { useSidebar } from "@/Hooks/useSidebar.js";
import { Logo } from "@/Components/sidebar/Logo";
import { getMenu, getBottomMenu, getAccountMenu } from "@/Components/sidebar/Menu";
import { getMenu, getAccountMenu } from "@/Components/sidebar/Menu";
import { NavItem } from "@/Components/sidebar/NavItem";
import { StarPrompt } from "@/Components/sidebar/StarPrompt";
import { AuthFooter } from "@/Components/sidebar/Authfooter";
@@ -20,7 +20,7 @@ import { setCollapsed } from "@/Features/UI/uiSlice";
const URL_MAP: Record<string, string> = {
support: "https://discord.com/invite/NAb6H3UTjK",
discussions: "https://github.com/bluewave-labs/checkmate/discussions",
docs: "https://bluewavelabs.gitbook.io/checkmate",
docs: "https://checkmate.so/docs",
changelog: "https://github.com/bluewave-labs/checkmate/releases",
};
@@ -33,7 +33,6 @@ export const Sidebar = () => {
const theme = useTheme();
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
const menu = getMenu(t);
const bottomMenu = getBottomMenu(t);
const accountMenu = getAccountMenu(t);
useEffect(() => {
@@ -103,23 +102,6 @@ export const Sidebar = () => {
})}
</List>
<StarPrompt />
<List
component="nav"
disablePadding
sx={{ px: theme.spacing(LAYOUT.SM) }}
>
{bottomMenu.map((item) => {
const selected = location.pathname.startsWith(`/${item.path}`);
return (
<NavItem
key={item.path}
item={item}
selected={selected}
onClick={() => handleNavClick(item.path)}
/>
);
})}
</List>
<Divider sx={{ borderColor: theme.palette.divider }} />
<AuthFooter