mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-05-20 16:39:07 -05:00
Merge pull request #3231 from bluewave-labs/feat/v2-status-page
feat: v2 status page
This commit is contained in:
@@ -91,7 +91,7 @@ export const BasePage = ({
|
||||
spacing={theme.spacing(10)}
|
||||
{...props}
|
||||
>
|
||||
<Breadcrumb />
|
||||
<Breadcrumb breadcrumbOverride={breadcrumbOverride} />
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Link, useLocation } from "react-router-dom";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
const isId = (segment: string): boolean => {
|
||||
return segment.length === 24 || /^[a-f0-9-]{36}$/.test(segment);
|
||||
@@ -11,28 +12,8 @@ const isId = (segment: string): boolean => {
|
||||
|
||||
const actionSegments = ["create", "configure"];
|
||||
|
||||
export const Breadcrumb = () => {
|
||||
const { t } = useTranslation();
|
||||
const BreadcrumbWrapper = ({ children }: { children: ReactNode }) => {
|
||||
const theme = useTheme();
|
||||
const location = useLocation();
|
||||
|
||||
const segments = location.pathname.split("/").filter((x) => x);
|
||||
|
||||
// Build simplified breadcrumb: "uptime" or "uptime / details" or "uptime / create"
|
||||
const basePage = segments[0] || t("common.breadcrumbs.home");
|
||||
|
||||
const secondSegment = segments[1];
|
||||
const isActionPage = secondSegment && actionSegments.includes(secondSegment); // create/config
|
||||
const isDetailsPage = secondSegment && isId(secondSegment); // details
|
||||
const hasSubPage = isActionPage || isDetailsPage;
|
||||
|
||||
const getSubPageLabel = (): string => {
|
||||
if (isActionPage) {
|
||||
return secondSegment.charAt(0).toUpperCase() + secondSegment.slice(1);
|
||||
}
|
||||
return t("common.breadcrumbs.details");
|
||||
};
|
||||
|
||||
return (
|
||||
<MuiBreadcrumbs
|
||||
separator={
|
||||
@@ -49,6 +30,72 @@ export const Breadcrumb = () => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</MuiBreadcrumbs>
|
||||
);
|
||||
};
|
||||
|
||||
export const Breadcrumb = ({
|
||||
breadcrumbOverride,
|
||||
}: {
|
||||
breadcrumbOverride?: string[] | undefined;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const location = useLocation();
|
||||
|
||||
// If override is an empty array, hide entirely
|
||||
if (breadcrumbOverride !== undefined && breadcrumbOverride.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If override has items, render them directly
|
||||
if (breadcrumbOverride !== undefined && breadcrumbOverride.length > 0) {
|
||||
return (
|
||||
<BreadcrumbWrapper>
|
||||
{breadcrumbOverride.map((item, index) => {
|
||||
const isLast = index === breadcrumbOverride.length - 1;
|
||||
return (
|
||||
<Typography
|
||||
key={index}
|
||||
sx={{
|
||||
fontSize: "14px",
|
||||
fontWeight: isLast ? 600 : 400,
|
||||
color: isLast ? theme.palette.primary.main : theme.palette.text.secondary,
|
||||
}}
|
||||
>
|
||||
{item}
|
||||
</Typography>
|
||||
);
|
||||
})}
|
||||
</BreadcrumbWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
// Default behavior: use location pathname
|
||||
const segments = location.pathname.split("/").filter((x) => x);
|
||||
|
||||
if (segments.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Build simplified breadcrumb: "uptime" or "uptime / details" or "uptime / create"
|
||||
const basePage = segments[0] || t("common.breadcrumbs.home");
|
||||
|
||||
const secondSegment = segments[1];
|
||||
const isActionPage = secondSegment && actionSegments.includes(secondSegment);
|
||||
const isDetailsPage = secondSegment && isId(secondSegment);
|
||||
const hasSubPage = isActionPage || isDetailsPage;
|
||||
|
||||
const getSubPageLabel = (): string => {
|
||||
if (isActionPage) {
|
||||
return secondSegment.charAt(0).toUpperCase() + secondSegment.slice(1);
|
||||
}
|
||||
return t("common.breadcrumbs.details");
|
||||
};
|
||||
|
||||
return (
|
||||
<BreadcrumbWrapper>
|
||||
{hasSubPage ? (
|
||||
<Link
|
||||
to={`/${basePage}`}
|
||||
@@ -88,6 +135,6 @@ export const Breadcrumb = () => {
|
||||
{getSubPageLabel()}
|
||||
</Typography>
|
||||
)}
|
||||
</MuiBreadcrumbs>
|
||||
</BreadcrumbWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,8 +3,12 @@ import Switch from "@mui/material/Switch";
|
||||
import type { SwitchProps } from "@mui/material/Switch";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
|
||||
export const SwitchComponent = forwardRef<HTMLInputElement, SwitchProps>(
|
||||
function SwitchComponent({ sx, ...props }, ref) {
|
||||
interface SwitchComponentProps extends SwitchProps {
|
||||
dualOption?: boolean;
|
||||
}
|
||||
|
||||
export const SwitchComponent = forwardRef<HTMLInputElement, SwitchComponentProps>(
|
||||
function SwitchComponent({ sx, dualOption = false, ...props }, ref) {
|
||||
const theme = useTheme();
|
||||
const additionalSx = Array.isArray(sx) ? sx : sx ? [sx] : [];
|
||||
|
||||
@@ -28,6 +32,12 @@ export const SwitchComponent = forwardRef<HTMLInputElement, SwitchProps>(
|
||||
},
|
||||
},
|
||||
},
|
||||
...(dualOption && {
|
||||
"& .MuiSwitch-track": {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
opacity: 1,
|
||||
},
|
||||
}),
|
||||
},
|
||||
...additionalSx,
|
||||
]}
|
||||
|
||||
@@ -95,7 +95,7 @@ export const HeaderMonitorControls = ({
|
||||
await refetch();
|
||||
}}
|
||||
>
|
||||
{monitor?.isActive ? t("pause") : t("resume")}
|
||||
{monitor?.isActive ? t("common.buttons.pause") : t("common.buttons.resume")}
|
||||
</Button>
|
||||
)}
|
||||
{isAdmin && (
|
||||
@@ -105,7 +105,7 @@ export const HeaderMonitorControls = ({
|
||||
startIcon={<Icon icon={Settings} />}
|
||||
onClick={() => navigate(`/${path}/configure/${monitor.id}`)}
|
||||
>
|
||||
Configure
|
||||
{t("common.buttons.configure")}
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
@@ -155,7 +155,7 @@ export const HeaderDeleteControls = ({
|
||||
await refetch();
|
||||
}}
|
||||
>
|
||||
{monitor?.isActive ? t("pause") : t("resume")}
|
||||
{monitor?.isActive ? t("common.buttons.pause") : t("common.buttons.resume")}
|
||||
</Button>
|
||||
)}
|
||||
{isAdmin && (
|
||||
|
||||
@@ -17,7 +17,6 @@ import { useNavigate } from "react-router-dom";
|
||||
import { useStatusPageFetch } from "../Status/Hooks/useStatusPageFetch.jsx";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useStatusPageDelete } from "../Status/Hooks/useStatusPageDelete.jsx";
|
||||
//Constants
|
||||
const ERROR_TAB_MAPPING = [
|
||||
["companyName", "url", "timezone", "color", "isPublished", "logo"],
|
||||
@@ -60,7 +59,7 @@ const CreateStatusPage = () => {
|
||||
const [createStatusPage] = useCreateStatusPage(isCreate);
|
||||
const [statusPage, statusPageMonitors, statusPageIsLoading, , fetchStatusPage] =
|
||||
useStatusPageFetch(isCreate, url);
|
||||
const [deleteStatusPage, isDeleting] = useStatusPageDelete(fetchStatusPage, url);
|
||||
// const [deleteStatusPage, isDeleting] = useStatusPageDelete(fetchStatusPage, url);
|
||||
|
||||
// Handlers
|
||||
const handleFormChange = (e) => {
|
||||
@@ -133,7 +132,7 @@ const CreateStatusPage = () => {
|
||||
const handleDelete = async () => {
|
||||
setIsDeleteOpen(false);
|
||||
// Start deletion process but don't wait for it
|
||||
deleteStatusPage();
|
||||
// deleteStatusPage();
|
||||
// Immediately navigate away to prevent additional fetches for the deleted page
|
||||
navigate("/status");
|
||||
};
|
||||
@@ -252,7 +251,7 @@ const CreateStatusPage = () => {
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Button
|
||||
loading={isDeleting}
|
||||
// loading={isDeleting}
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={() => setIsDeleteOpen(true)}
|
||||
@@ -266,7 +265,7 @@ const CreateStatusPage = () => {
|
||||
open={isDeleteOpen}
|
||||
confirmationButtonLabel={t("deleteStatusPageConfirm")}
|
||||
description={t("deleteStatusPageDescription")}
|
||||
isLoading={isDeleting || statusPageIsLoading}
|
||||
// isLoading={isDeleting || statusPageIsLoading}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
@@ -290,7 +289,7 @@ const CreateStatusPage = () => {
|
||||
handleDelete={handleDelete}
|
||||
isDeleteOpen={isDeleteOpen}
|
||||
setIsDeleteOpen={setIsDeleteOpen}
|
||||
isDeleting={isDeleting}
|
||||
// isDeleting={isDeleting}
|
||||
isLoading={statusPageIsLoading}
|
||||
/>
|
||||
<Stack
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
// Components
|
||||
import { Box, Typography } from "@mui/material";
|
||||
|
||||
// Utils
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const AdminLink = () => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography
|
||||
className="forgot-p"
|
||||
display="inline-block"
|
||||
color={theme.palette.primary.contrastText}
|
||||
>
|
||||
{t("administrator")}
|
||||
</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
color={theme.palette.accent.main}
|
||||
ml={theme.spacing(2)}
|
||||
sx={{ cursor: "pointer" }}
|
||||
onClick={() => navigate("/login")}
|
||||
>
|
||||
{t("loginHere")}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminLink;
|
||||
@@ -1,141 +0,0 @@
|
||||
// Components
|
||||
import { Box, Stack, Typography, Button } from "@mui/material";
|
||||
import Image from "@/Components/v1/Image/index.jsx";
|
||||
import Icon from "@/Components/v1/Icon";
|
||||
import ThemeSwitch from "@/Components/v1/ThemeSwitch/index.jsx";
|
||||
//Utils
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import PropTypes from "prop-types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const Controls = ({ url, type }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
const currentPath = location.pathname;
|
||||
const navigate = useNavigate();
|
||||
|
||||
if (currentPath.startsWith("/status/uptime/public")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={() => {
|
||||
if (type === "uptime") {
|
||||
navigate(`/status/uptime/configure/${url}`);
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
px: theme.spacing(5),
|
||||
"& svg": {
|
||||
mr: theme.spacing(3),
|
||||
"& path": {
|
||||
stroke: theme.palette.secondary.contrastText,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="Settings"
|
||||
size={18}
|
||||
style={{ marginRight: "8px" }}
|
||||
/>{" "}
|
||||
{t("configure")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
Controls.propTypes = {
|
||||
type: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
};
|
||||
|
||||
const ControlsHeader = ({ statusPage, isPublic, url, type = "uptime" }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const publicUrl = `/status/uptime/public/${url}`;
|
||||
|
||||
return (
|
||||
<Stack
|
||||
alignSelf="flex-start"
|
||||
direction="row"
|
||||
width="100%"
|
||||
gap={theme.spacing(2)}
|
||||
justifyContent="space-between"
|
||||
alignItems="flex-end"
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(8)}
|
||||
alignItems="flex-end"
|
||||
>
|
||||
<Image
|
||||
shouldRender={statusPage?.logo?.data ? true : false}
|
||||
alt={"Company logo"}
|
||||
maxWidth={"300px"}
|
||||
maxHeight={"50px"}
|
||||
base64={statusPage?.logo?.data}
|
||||
/>
|
||||
<Typography
|
||||
variant="h1"
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
sx={{
|
||||
maxWidth: { xs: "200px", sm: "100%" },
|
||||
}}
|
||||
>
|
||||
{statusPage?.companyName}
|
||||
</Typography>
|
||||
{statusPage?.isPublished && !isPublic && (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
onClick={() => {
|
||||
window.open(publicUrl, "_blank", "noopener,noreferrer");
|
||||
}}
|
||||
sx={{
|
||||
display: "inline-flex",
|
||||
":hover": {
|
||||
cursor: "pointer",
|
||||
borderBottom: 1,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography>{t("publicLink")}</Typography>
|
||||
<Icon
|
||||
name="ExternalLink"
|
||||
size={18}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
<Controls
|
||||
url={url}
|
||||
type={type}
|
||||
/>
|
||||
{isPublic && <ThemeSwitch />}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
ControlsHeader.propTypes = {
|
||||
url: PropTypes.string,
|
||||
statusPage: PropTypes.object,
|
||||
isPublic: PropTypes.bool,
|
||||
type: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ControlsHeader;
|
||||
@@ -0,0 +1,87 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { Icon } from "@/Components/v2/design-elements";
|
||||
import { Button } from "@/Components/v2/inputs";
|
||||
import { Settings, ExternalLink } from "lucide-react";
|
||||
|
||||
import { useTheme } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import type { StatusPage } from "@/Types/StatusPage";
|
||||
|
||||
interface HeaderStatusPageControlsProps {
|
||||
isAdmin: boolean;
|
||||
statusPage: StatusPage;
|
||||
isPublic?: boolean;
|
||||
}
|
||||
export const HeaderStatusPageControls = ({
|
||||
isAdmin,
|
||||
statusPage,
|
||||
isPublic = false,
|
||||
}: HeaderStatusPageControlsProps) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Stack
|
||||
direction={"row"}
|
||||
alignItems={"center"}
|
||||
justifyContent={"space-between"}
|
||||
mb={4}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(4)}
|
||||
alignItems="baseline"
|
||||
>
|
||||
<Typography
|
||||
variant="h1"
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
sx={{
|
||||
maxWidth: { xs: "200px", sm: "100%" },
|
||||
}}
|
||||
>
|
||||
{statusPage?.companyName}
|
||||
</Typography>
|
||||
{statusPage?.isPublished && !isPublic && (
|
||||
<>
|
||||
<Typography
|
||||
onClick={() => {
|
||||
window.open(
|
||||
`/status/uptime/public/${statusPage.url}`,
|
||||
"_blank",
|
||||
"noopener,noreferrer"
|
||||
);
|
||||
}}
|
||||
sx={{
|
||||
borderBottom: 1,
|
||||
borderColor: "transparent",
|
||||
":hover": {
|
||||
cursor: "pointer",
|
||||
borderBottom: 1,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{t("publicLink")}
|
||||
</Typography>
|
||||
<Box>
|
||||
<ExternalLink size={14} />
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
{isAdmin && !isPublic && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
startIcon={<Icon icon={Settings} />}
|
||||
onClick={() => navigate(`/status/uptime/configure/${statusPage.url}`)}
|
||||
>
|
||||
{t("common.buttons.configure")}
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,113 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { HistogramResponseTime, HeatmapResponseTime } from "@/Components/v2/common";
|
||||
import { StatusLabel, BaseBox } from "@/Components/v2/design-elements";
|
||||
import { SwitchComponent } from "@/Components/v2/inputs";
|
||||
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { determineState } from "@/Utils/MonitorUtils";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useState } from "react";
|
||||
import type { Monitor } from "@/Types/Monitor";
|
||||
import type { StatusPage } from "@/Types/StatusPage";
|
||||
import type { RootState } from "@/Types/state";
|
||||
|
||||
interface StatusPageMonitor extends Monitor {
|
||||
checks?: Monitor["recentChecks"];
|
||||
}
|
||||
|
||||
interface MonitorsListProps {
|
||||
statusPage: StatusPage;
|
||||
monitors: StatusPageMonitor[];
|
||||
}
|
||||
|
||||
export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const showURL = useSelector((state: RootState) => state.ui?.showURL);
|
||||
|
||||
const [chartType, setChartType] = useState<"histogram" | "heatmap">("histogram");
|
||||
|
||||
return (
|
||||
<Stack gap={theme.spacing(8)}>
|
||||
{statusPage.showCharts !== false && (
|
||||
<Stack
|
||||
direction={"row"}
|
||||
alignItems={"center"}
|
||||
>
|
||||
<Typography>{t("pages.statusPages.monitorsList.chartTypeHeatmap")}</Typography>
|
||||
<SwitchComponent
|
||||
dualOption
|
||||
value={chartType}
|
||||
checked={chartType === "histogram"}
|
||||
onChange={(e) => {
|
||||
setChartType(e.target.checked ? "histogram" : "heatmap");
|
||||
}}
|
||||
/>
|
||||
<Typography>
|
||||
{t("pages.statusPages.monitorsList.chartTypeHistogram")}
|
||||
</Typography>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{monitors?.map((monitor) => {
|
||||
const status = determineState(monitor);
|
||||
return (
|
||||
<BaseBox
|
||||
key={monitor.id}
|
||||
padding={theme.spacing(4)}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
gap={theme.spacing(4)}
|
||||
mb={statusPage.showCharts !== false ? theme.spacing(4) : 0}
|
||||
>
|
||||
<Box sx={{ overflow: "hidden", minWidth: 0, flex: 1 }}>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{monitor.name}
|
||||
</Typography>
|
||||
{showURL && (
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
>
|
||||
{monitor.url}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<StatusLabel
|
||||
status={status === "up"}
|
||||
isActive={monitor.isActive}
|
||||
/>
|
||||
</Stack>
|
||||
{statusPage.showCharts !== false && (
|
||||
<Box sx={{ overflow: "hidden", minWidth: 0, flex: 1 }}>
|
||||
{chartType === "histogram" ? (
|
||||
<HistogramResponseTime
|
||||
checks={monitor?.checks?.slice().reverse() ?? []}
|
||||
/>
|
||||
) : (
|
||||
<HeatmapResponseTime
|
||||
checks={monitor?.checks?.slice().reverse() ?? []}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</BaseBox>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -1,69 +0,0 @@
|
||||
// Components
|
||||
import { Stack, Box } from "@mui/material";
|
||||
import Host from "@/Components/v1/Host/index.jsx";
|
||||
import StatusPageBarChart from "@/Components/v1/Charts/StatusPageBarChart/index.jsx";
|
||||
import { StatusLabel } from "@/Components/v1/Label/index.jsx";
|
||||
|
||||
//Utils
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils.js";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
const MonitorsList = ({
|
||||
isLoading = false,
|
||||
shouldRender = true,
|
||||
monitors = [],
|
||||
statusPage = {},
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { determineState } = useMonitorUtils();
|
||||
|
||||
const { showURL } = useSelector((state) => state.ui);
|
||||
|
||||
return (
|
||||
<>
|
||||
{monitors?.map((monitor) => {
|
||||
const status = determineState(monitor);
|
||||
return (
|
||||
<Stack
|
||||
key={monitor.id}
|
||||
width="100%"
|
||||
gap={theme.spacing(10)}
|
||||
margin="0 auto"
|
||||
maxWidth="95%"
|
||||
>
|
||||
<Host
|
||||
key={monitor.id}
|
||||
url={monitor.url}
|
||||
title={monitor.name}
|
||||
percentageColor={monitor.percentageColor}
|
||||
percentage={monitor.percentage}
|
||||
showURL={showURL}
|
||||
status={status}
|
||||
/>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(1)}
|
||||
>
|
||||
{statusPage.showCharts !== false && (
|
||||
<Box flex={9}>
|
||||
<StatusPageBarChart checks={monitor?.checks?.slice().reverse()} />
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
MonitorsList.propTypes = {
|
||||
monitors: PropTypes.array.isRequired,
|
||||
statusPage: PropTypes.object,
|
||||
};
|
||||
|
||||
export default MonitorsList;
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Stack, Skeleton } from "@mui/material";
|
||||
|
||||
const SkeletonLayout = () => {
|
||||
return (
|
||||
<Stack>
|
||||
<Skeleton
|
||||
variant="rectangular"
|
||||
height={"90vh"}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkeletonLayout;
|
||||
@@ -0,0 +1,62 @@
|
||||
import { AlertTriangle, CircleCheck } from "lucide-react";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { Theme } from "@mui/material";
|
||||
import { useTheme } from "@mui/material";
|
||||
import type { Monitor } from "@/Types/Monitor";
|
||||
|
||||
const getMonitorStatus = (monitors: Monitor[], theme: Theme, t: Function) => {
|
||||
const monitorsStatus: Record<string, any> = {
|
||||
icon: <AlertTriangle size={24} />,
|
||||
};
|
||||
|
||||
if (monitors.every((monitor) => monitor.status === true)) {
|
||||
monitorsStatus.msg = t("pages.statusPages.statusBar.allUp");
|
||||
monitorsStatus.color = theme.palette.success.main;
|
||||
monitorsStatus.icon = <CircleCheck size={24} />;
|
||||
}
|
||||
|
||||
if (monitors.every((monitor) => monitor.status === false)) {
|
||||
monitorsStatus.msg = t("pages.statusPages.statusBar.allDown");
|
||||
monitorsStatus.color = theme.palette.error.main;
|
||||
}
|
||||
|
||||
if (monitors.some((monitor) => monitor.status === false)) {
|
||||
monitorsStatus.msg = t("pages.statusPages.statusBar.degraded");
|
||||
monitorsStatus.color = theme.palette.warning.main;
|
||||
}
|
||||
|
||||
// Paused or unknown
|
||||
if (monitors.some((monitor) => typeof monitor.status === "undefined")) {
|
||||
monitorsStatus.msg = t("pages.statusPages.statusBar.unknown");
|
||||
monitorsStatus.color = theme.palette.warning.main;
|
||||
}
|
||||
return monitorsStatus;
|
||||
};
|
||||
|
||||
interface StatusBarProps {
|
||||
monitors: Monitor[];
|
||||
}
|
||||
|
||||
export const StatusBar = ({ monitors }: StatusBarProps) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const monitorsStatus = getMonitorStatus(monitors, theme, t);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
gap={theme.spacing(2)}
|
||||
height={theme.spacing(30)}
|
||||
bgcolor={monitorsStatus.color}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
>
|
||||
{monitorsStatus.icon}
|
||||
<Typography>{monitorsStatus.msg}</Typography>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -1,77 +0,0 @@
|
||||
// Components
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import Icon from "@/Components/v1/Icon";
|
||||
|
||||
// Utils
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const getMonitorStatus = (monitors, theme) => {
|
||||
const monitorsStatus = {
|
||||
icon: (
|
||||
<Icon
|
||||
name="AlertTriangle"
|
||||
size={24}
|
||||
color="primary.contrastTextSecondaryDarkBg"
|
||||
/>
|
||||
),
|
||||
};
|
||||
if (monitors.every((monitor) => monitor.status === true)) {
|
||||
monitorsStatus.msg = "All systems operational";
|
||||
monitorsStatus.color = theme.palette.success.lowContrast;
|
||||
monitorsStatus.icon = (
|
||||
<Icon
|
||||
name="CheckCircle2"
|
||||
size={24}
|
||||
color="primary.contrastTextSecondaryDarkBg"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (monitors.every((monitor) => monitor.status === false)) {
|
||||
monitorsStatus.msg = "All systems down";
|
||||
monitorsStatus.color = theme.palette.error.lowContrast;
|
||||
}
|
||||
|
||||
if (monitors.some((monitor) => monitor.status === false)) {
|
||||
monitorsStatus.msg = "Degraded performance";
|
||||
monitorsStatus.color = theme.palette.warning.lowContrast;
|
||||
}
|
||||
|
||||
// Paused or unknown
|
||||
if (monitors.some((monitor) => typeof monitor.status === "undefined")) {
|
||||
monitorsStatus.msg = "Unknown status";
|
||||
monitorsStatus.color = theme.palette.warning.lowContrast;
|
||||
}
|
||||
return monitorsStatus;
|
||||
};
|
||||
|
||||
const StatusBar = ({ monitors }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
if (typeof monitors === "undefined") return;
|
||||
|
||||
const monitorsStatus = getMonitorStatus(monitors, theme);
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
gap={theme.spacing(2)}
|
||||
height={theme.spacing(30)}
|
||||
width={"100%"}
|
||||
backgroundColor={monitorsStatus.color}
|
||||
borderRadius={theme.spacing(2)}
|
||||
>
|
||||
{monitorsStatus.icon}
|
||||
{/* CAIO_REVIEW */}
|
||||
<Typography variant="h2DarkBg">{monitorsStatus.msg}</Typography>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusBar;
|
||||
|
||||
StatusBar.propTypes = {
|
||||
status: PropTypes.object,
|
||||
};
|
||||
@@ -1,166 +0,0 @@
|
||||
// Components
|
||||
import { Typography, Stack } from "@mui/material";
|
||||
import GenericFallback from "@/Components/v1/GenericFallback/index.jsx";
|
||||
import AdminLink from "./Components/AdminLink/index.jsx";
|
||||
import ControlsHeader from "./Components/ControlsHeader/index.jsx";
|
||||
import SkeletonLayout from "./Components/Skeleton/index.jsx";
|
||||
import StatusBar from "./Components/StatusBar/index.jsx";
|
||||
import MonitorsList from "./Components/MonitorsList/index.jsx";
|
||||
import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
|
||||
import TextLink from "@/Components/v1/TextLink/index.jsx";
|
||||
|
||||
// Utils
|
||||
import { useStatusPageFetch } from "./Hooks/useStatusPageFetch.jsx";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useIsAdmin } from "@/Hooks/useIsAdmin.js";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const PublicStatus = () => {
|
||||
const { url } = useParams();
|
||||
|
||||
// Utils
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
const isAdmin = useIsAdmin();
|
||||
|
||||
const [statusPage, monitors, isLoading, networkError] = useStatusPageFetch(false, url);
|
||||
|
||||
// Breadcrumbs
|
||||
const crumbs = [
|
||||
{ name: t("statusBreadCrumbsStatusPages"), path: "/status" },
|
||||
{ name: t("statusBreadCrumbsDetails"), path: `/status/uptime/${statusPage?.url}` },
|
||||
];
|
||||
|
||||
// Setup
|
||||
let sx = { paddingLeft: theme.spacing(20), paddingRight: theme.spacing(20) };
|
||||
let link = undefined;
|
||||
const isPublic = location.pathname.startsWith("/status/uptime/public");
|
||||
// Public status page
|
||||
if (isPublic && statusPage && statusPage.showAdminLoginLink === true) {
|
||||
sx = {
|
||||
paddingTop: theme.spacing(20),
|
||||
paddingLeft: "20vw",
|
||||
paddingRight: "20vw",
|
||||
};
|
||||
link = <AdminLink />;
|
||||
}
|
||||
|
||||
// Loading
|
||||
if (isLoading) {
|
||||
return <SkeletonLayout />;
|
||||
}
|
||||
|
||||
if (monitors?.length === 0) {
|
||||
return (
|
||||
<GenericFallback>
|
||||
<Typography
|
||||
variant="h1"
|
||||
marginY={theme.spacing(4)}
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
>
|
||||
{"Theres nothing here yet"}
|
||||
</Typography>
|
||||
{isAdmin && (
|
||||
<TextLink
|
||||
linkText={"Add a monitor to get started"}
|
||||
href={`/status/uptime/configure/${url}`}
|
||||
/>
|
||||
)}
|
||||
</GenericFallback>
|
||||
);
|
||||
}
|
||||
|
||||
// Error fetching data
|
||||
if (networkError === true) {
|
||||
return (
|
||||
<GenericFallback>
|
||||
<Typography
|
||||
variant="h1"
|
||||
marginY={theme.spacing(4)}
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
>
|
||||
{t("common.toasts.networkError")}
|
||||
</Typography>
|
||||
<Typography>{t("common.toasts.checkConnection")}</Typography>
|
||||
</GenericFallback>
|
||||
);
|
||||
}
|
||||
|
||||
// Public status page fallback
|
||||
if (!isLoading && typeof statusPage === "undefined" && isPublic) {
|
||||
return (
|
||||
<Stack sx={sx}>
|
||||
<GenericFallback>
|
||||
<Typography
|
||||
variant="h1"
|
||||
marginY={theme.spacing(4)}
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
>
|
||||
{t("statusPageStatus")}
|
||||
</Typography>
|
||||
<Typography>{t("statusPageStatusContactAdmin")}</Typography>
|
||||
</GenericFallback>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
// Finished loading, but status page is not public
|
||||
if (!isLoading && isPublic && statusPage.isPublished === false) {
|
||||
return (
|
||||
<Stack sx={sx}>
|
||||
<GenericFallback>
|
||||
<Typography
|
||||
variant="h1"
|
||||
marginY={theme.spacing(4)}
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
>
|
||||
{t("statusPageStatusNotPublic")}
|
||||
</Typography>
|
||||
<Typography>{t("statusPageStatusContactAdmin")}</Typography>
|
||||
</GenericFallback>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
// Status page doesn't exist
|
||||
if (!isLoading && typeof statusPage === "undefined") {
|
||||
return (
|
||||
<GenericFallback>
|
||||
<Typography
|
||||
variant="h1"
|
||||
marginY={theme.spacing(4)}
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
>
|
||||
{t("statusPageStatusNoPage")}
|
||||
</Typography>
|
||||
<Typography>{t("statusPageStatusContactAdmin")}</Typography>
|
||||
</GenericFallback>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
gap={theme.spacing(10)}
|
||||
sx={{ ...sx, position: "relative" }}
|
||||
>
|
||||
{!isPublic && <Breadcrumbs list={crumbs} />}
|
||||
<ControlsHeader
|
||||
statusPage={statusPage}
|
||||
url={url}
|
||||
isPublic={isPublic}
|
||||
/>
|
||||
<Typography variant="h2">{t("statusPageStatusServiceStatus")}</Typography>
|
||||
<StatusBar monitors={monitors} />
|
||||
<MonitorsList
|
||||
monitors={monitors}
|
||||
statusPage={statusPage}
|
||||
/>
|
||||
{link}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default PublicStatus;
|
||||
@@ -0,0 +1,60 @@
|
||||
import { BasePage } from "@/Components/v2/design-elements";
|
||||
import { StatusBar } from "@/Pages/StatusPage/Status/Components/StatusBar";
|
||||
import { MonitorsList } from "@/Pages/StatusPage/Status/Components/MonitorsList";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
import { useTheme } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useIsAdmin } from "@/Hooks/useIsAdmin";
|
||||
import { useLocation, useParams } from "react-router-dom";
|
||||
import { useGet } from "@/Hooks/UseApi";
|
||||
import type { StatusPageResponse } from "@/Types/StatusPage";
|
||||
import { HeaderStatusPageControls } from "./Components/HeaderStatusPageControls";
|
||||
|
||||
const StatusPageView = () => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const { url } = useParams();
|
||||
const isAdmin = useIsAdmin();
|
||||
const location = useLocation();
|
||||
const isPublic = location.pathname.startsWith("/status/uptime/public");
|
||||
|
||||
const apiUrl = url ? `/status-page/${url}?type=uptime` : null;
|
||||
|
||||
const { data, isLoading, error } = useGet<StatusPageResponse>(apiUrl);
|
||||
|
||||
const statusPage = data?.statusPage;
|
||||
const monitors = data?.monitors ?? [];
|
||||
|
||||
if (!statusPage) return null;
|
||||
|
||||
let sx: React.CSSProperties = {};
|
||||
if (isPublic) {
|
||||
sx.paddingTop = theme.spacing(20);
|
||||
sx.paddingLeft = "20vw";
|
||||
sx.paddingRight = "20vw";
|
||||
}
|
||||
|
||||
return (
|
||||
<BasePage
|
||||
loading={isLoading}
|
||||
error={error}
|
||||
sx={sx}
|
||||
breadcrumbOverride={isPublic ? [] : undefined}
|
||||
>
|
||||
<HeaderStatusPageControls
|
||||
isAdmin={isAdmin}
|
||||
statusPage={statusPage}
|
||||
isPublic={isPublic}
|
||||
/>
|
||||
<Typography variant="h2">{t("statusPageStatusServiceStatus")}</Typography>
|
||||
<StatusBar monitors={monitors} />
|
||||
<MonitorsList
|
||||
statusPage={statusPage}
|
||||
monitors={monitors}
|
||||
/>
|
||||
</BasePage>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusPageView;
|
||||
@@ -0,0 +1,109 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { Table, type Header, ValueLabel } from "@/Components/v2/design-elements";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import type { StatusPage } from "@/Types/StatusPage";
|
||||
|
||||
interface StatusPagesTableProps {
|
||||
data: StatusPage[];
|
||||
}
|
||||
|
||||
export const StatusPagesTable = ({ data }: StatusPagesTableProps) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleUrlClick = (e: React.MouseEvent, row: StatusPage) => {
|
||||
if (row.isPublished) {
|
||||
e.stopPropagation();
|
||||
const url = `/status/uptime/public/${row.url}`;
|
||||
window.open(url, "_blank", "noopener,noreferrer");
|
||||
}
|
||||
};
|
||||
|
||||
const getHeaders = (): Header<StatusPage>[] => {
|
||||
return [
|
||||
{
|
||||
id: "name",
|
||||
content: t("pages.statusPages.table.headers.name"),
|
||||
render: (row) => row.companyName,
|
||||
},
|
||||
{
|
||||
id: "url",
|
||||
content: t("pages.statusPages.table.headers.url"),
|
||||
render: (row) => {
|
||||
const content = row.isPublished
|
||||
? `/${row.url}`
|
||||
: t("pages.statusPages.table.unpublished");
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
gap={theme.spacing(2)}
|
||||
paddingLeft={theme.spacing(2)}
|
||||
paddingRight={theme.spacing(2)}
|
||||
onClick={(e) => handleUrlClick(e, row)}
|
||||
sx={{
|
||||
...(row.isPublished && {
|
||||
display: "inline-flex",
|
||||
":hover": {
|
||||
cursor: "pointer",
|
||||
borderBottom: 1,
|
||||
},
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<Typography>{content}</Typography>
|
||||
{row.isPublished && <ExternalLink size={18} />}
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "type",
|
||||
content: t("common.table.headers.type"),
|
||||
render: (row) => row.type,
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
content: t("common.table.headers.status"),
|
||||
render: (row) => {
|
||||
return (
|
||||
<ValueLabel
|
||||
value={row.isPublished ? "positive" : "neutral"}
|
||||
text={
|
||||
row.isPublished
|
||||
? t("pages.statusPages.table.published")
|
||||
: t("pages.statusPages.table.unpublished")
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const handleRowClick = (statusPage: StatusPage) => {
|
||||
navigate(`/status/uptime/${statusPage.url}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Table
|
||||
headers={getHeaders()}
|
||||
data={data}
|
||||
onRowClick={handleRowClick}
|
||||
emptyViewText={t("common.table.empty")}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusPagesTable;
|
||||
@@ -1,107 +0,0 @@
|
||||
import DataTable from "@/Components/v1/Table/index.jsx";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { StatusLabel } from "@/Components/v1/Label/index.jsx";
|
||||
import Icon from "@/Components/v1/Icon";
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
const StatusPagesTable = ({ data }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const headers = [
|
||||
{
|
||||
id: "name",
|
||||
content: t("statusPageName"),
|
||||
render: (row) => {
|
||||
return row.companyName;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "url",
|
||||
content: t("publicURL"),
|
||||
onClick: (e, row) => {
|
||||
if (row.isPublished) {
|
||||
e.stopPropagation();
|
||||
const url = `/status/uptime/public/${row.url}`;
|
||||
window.open(url, "_blank", "noopener,noreferrer");
|
||||
}
|
||||
},
|
||||
render: (row) => {
|
||||
const content = row.isPublished ? `/${row.url}` : "Unpublished";
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
gap={theme.spacing(2)}
|
||||
paddingLeft={theme.spacing(2)}
|
||||
paddingRight={theme.spacing(2)}
|
||||
sx={{
|
||||
...(row.isPublished && {
|
||||
display: "inline-flex",
|
||||
":hover": {
|
||||
cursor: "pointer",
|
||||
borderBottom: 1,
|
||||
},
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<Typography>{content}</Typography>
|
||||
{row.isPublished && (
|
||||
<Icon
|
||||
name="ExternalLink"
|
||||
size={18}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "type",
|
||||
content: t("type"),
|
||||
render: (row) => {
|
||||
return row.type;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
content: t("status"),
|
||||
render: (row) => {
|
||||
const status = row.isPublished ? "published" : "unpublished";
|
||||
return (
|
||||
<StatusLabel
|
||||
status={status}
|
||||
text={row.isPublished ? "Published" : "Unpublished"}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const handleRowClick = (statusPage) => {
|
||||
navigate(`/status/uptime/${statusPage.url}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
config={{
|
||||
rowSX: {
|
||||
cursor: "pointer",
|
||||
"&:hover td": {
|
||||
backgroundColor: theme.palette.tertiary.main,
|
||||
transition: "background-color .3s ease",
|
||||
},
|
||||
},
|
||||
onRowClick: (row) => {
|
||||
handleRowClick(row);
|
||||
},
|
||||
}}
|
||||
headers={headers}
|
||||
data={data}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusPagesTable;
|
||||
@@ -1,32 +0,0 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { networkService } from "../../../../main.jsx";
|
||||
import { useSelector } from "react-redux";
|
||||
import { createToast } from "../../../../Utils/toastUtils.jsx";
|
||||
|
||||
const useStatusPagesFetch = () => {
|
||||
const { user } = useSelector((state) => state.auth);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [networkError, setNetworkError] = useState(false);
|
||||
const [statusPages, setStatusPages] = useState(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchStatusPages = async () => {
|
||||
try {
|
||||
const res = await networkService.getStatusPagesByTeamId();
|
||||
setStatusPages(res?.data?.data || []);
|
||||
} catch (error) {
|
||||
setNetworkError(true);
|
||||
createToast({
|
||||
body: error.message,
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
fetchStatusPages();
|
||||
}, [user]);
|
||||
return [isLoading, networkError, statusPages];
|
||||
};
|
||||
|
||||
export { useStatusPagesFetch };
|
||||
@@ -1,43 +0,0 @@
|
||||
// Components
|
||||
import { Stack } from "@mui/material";
|
||||
import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
|
||||
import MonitorCreateHeader from "@/Components/v1/MonitorCreateHeader/index.jsx";
|
||||
import StatusPagesTable from "./Components/StatusPagesTable/index.jsx";
|
||||
import PageStateWrapper from "@/Components/v1/PageStateWrapper/index.jsx";
|
||||
// Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useStatusPagesFetch } from "./Hooks/useStatusPagesFetch.jsx";
|
||||
import { useIsAdmin } from "@/Hooks/useIsAdmin.js";
|
||||
const BREADCRUMBS = [{ name: `Status Pages`, path: "" }];
|
||||
|
||||
const StatusPages = () => {
|
||||
// Utils
|
||||
const theme = useTheme();
|
||||
const isAdmin = useIsAdmin();
|
||||
const [isLoading, networkError, statusPages] = useStatusPagesFetch();
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageStateWrapper
|
||||
networkError={networkError}
|
||||
isLoading={isLoading}
|
||||
items={statusPages}
|
||||
type="statusPage"
|
||||
fallbackLink="/status/uptime/create"
|
||||
>
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
<MonitorCreateHeader
|
||||
label="Create status page"
|
||||
isAdmin={isAdmin}
|
||||
path="/status/uptime/create"
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<StatusPagesTable data={statusPages} />
|
||||
</Stack>
|
||||
</PageStateWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusPages;
|
||||
@@ -0,0 +1,41 @@
|
||||
import { BasePageWithStates } from "@/Components/v2/design-elements";
|
||||
import { StatusPagesTable } from "./Components/StatusPagesTable";
|
||||
import { useGet } from "@/Hooks/UseApi";
|
||||
import type { StatusPage } from "@/Types/StatusPage";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { HeaderCreate } from "@/Components/v2/common";
|
||||
import { useIsAdmin } from "@/Hooks/useIsAdmin";
|
||||
|
||||
const StatusPages = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const {
|
||||
data: statusPages,
|
||||
isLoading,
|
||||
error,
|
||||
} = useGet<StatusPage[]>("/status-page/team");
|
||||
|
||||
const isAdmin = useIsAdmin();
|
||||
|
||||
return (
|
||||
<BasePageWithStates
|
||||
page={t("pages.statusPages.title")}
|
||||
loading={isLoading}
|
||||
bullets={
|
||||
t("pages.statusPages.fallback.checks", { returnObjects: true }) as string[]
|
||||
}
|
||||
error={!!error}
|
||||
items={statusPages ?? []}
|
||||
actionButtonText={t("pages.statusPages.fallback.actionButton")}
|
||||
actionLink="/status/uptime/create"
|
||||
>
|
||||
<HeaderCreate
|
||||
path="/status/uptime/create"
|
||||
isAdmin={isAdmin}
|
||||
/>
|
||||
<StatusPagesTable data={statusPages ?? []} />
|
||||
</BasePageWithStates>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusPages;
|
||||
@@ -37,8 +37,8 @@ import Incidents from "../Pages/Incidents/";
|
||||
|
||||
// Status pages
|
||||
import CreateStatus from "../Pages/StatusPage/Create/index.jsx";
|
||||
import StatusPages from "../Pages/StatusPage/StatusPages/index.jsx";
|
||||
import Status from "../Pages/StatusPage/Status/index.jsx";
|
||||
import StatusPages from "../Pages/StatusPage/StatusPages";
|
||||
import Status from "../Pages/StatusPage/Status";
|
||||
|
||||
import Notifications from "../Pages/Notifications";
|
||||
import CreateNotifications from "../Pages/Notifications/create";
|
||||
@@ -227,12 +227,24 @@ const Routes = () => {
|
||||
|
||||
<Route
|
||||
path="status"
|
||||
element={<StatusPages />}
|
||||
element={
|
||||
<>
|
||||
<ThemeProvider theme={v2theme}>
|
||||
<StatusPages />
|
||||
</ThemeProvider>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="status/uptime/:url"
|
||||
element={<Status />}
|
||||
element={
|
||||
<>
|
||||
<ThemeProvider theme={v2theme}>
|
||||
<Status />
|
||||
</ThemeProvider>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
@@ -354,7 +366,13 @@ const Routes = () => {
|
||||
/>
|
||||
<Route
|
||||
path="/status/uptime/public/:url"
|
||||
element={<Status />}
|
||||
element={
|
||||
<>
|
||||
<ThemeProvider theme={v2theme}>
|
||||
<Status />
|
||||
</ThemeProvider>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { Monitor } from "@/Types/Monitor";
|
||||
|
||||
export interface StatusPage {
|
||||
id: string;
|
||||
userId: string;
|
||||
teamId: string;
|
||||
type: string;
|
||||
companyName: string;
|
||||
url: string;
|
||||
timezone?: string;
|
||||
color: string;
|
||||
monitors: string[];
|
||||
subMonitors: string[];
|
||||
originalMonitors?: string[];
|
||||
logo?: {
|
||||
data: string;
|
||||
contentType: string;
|
||||
} | null;
|
||||
isPublished: boolean;
|
||||
showCharts: boolean;
|
||||
showUptimePercentage: boolean;
|
||||
showAdminLoginLink: boolean;
|
||||
customCSS: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface StatusPageResponse {
|
||||
statusPage: StatusPage;
|
||||
monitors: Monitor[];
|
||||
}
|
||||
@@ -2,6 +2,13 @@ import type { Monitor, MonitorStatus, MonitorType } from "@/Types/Monitor";
|
||||
import type { PaletteKey } from "@/Utils/Theme/v2Theme";
|
||||
import type { ValueType } from "@/Components/v2/design-elements/StatusLabel";
|
||||
|
||||
export const determineState = (monitor: Monitor) => {
|
||||
if (typeof monitor === "undefined") return "pending";
|
||||
if (monitor?.isActive === false) return "paused";
|
||||
if (monitor?.status === undefined) return "pending";
|
||||
return monitor?.status == true ? "up" : "down";
|
||||
};
|
||||
|
||||
export const getMonitorPath = (type: MonitorType): string => {
|
||||
const pathMap: Record<MonitorType, string> = {
|
||||
http: "uptime",
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm",
|
||||
"save": "Save",
|
||||
"test": "Test"
|
||||
"test": "Test",
|
||||
"configure": "Configure"
|
||||
},
|
||||
|
||||
"alerts": {
|
||||
@@ -452,6 +453,37 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"statusPages": {
|
||||
"title": "Status Pages",
|
||||
"table": {
|
||||
"headers": {
|
||||
"name": "Status page name",
|
||||
"url": "Public URL"
|
||||
},
|
||||
"published": "Published",
|
||||
"unpublished": "Unpublished"
|
||||
},
|
||||
"fallback": {
|
||||
"actionButton": "Create a 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": "A status page is used to:"
|
||||
},
|
||||
"serviceStauts": "Service stauts",
|
||||
"statusBar": {
|
||||
"allUp": "All systems operational",
|
||||
"allDown": "All systems down",
|
||||
"degraded": "Degraded performance",
|
||||
"unknown": "Unknown status"
|
||||
},
|
||||
"monitorsList": {
|
||||
"chartTypeHeatmap": "Heatmap",
|
||||
"chartTypeHistogram": "Histogram"
|
||||
}
|
||||
}
|
||||
},
|
||||
"used": "Used",
|
||||
@@ -490,8 +522,6 @@
|
||||
"cancel": "Cancel",
|
||||
"close": "Close",
|
||||
"type": "Type",
|
||||
"statusPageName": "Status page name",
|
||||
"publicURL": "Public URL",
|
||||
"repeat": "Repeat",
|
||||
"edit": "Edit",
|
||||
"createA": "Create a",
|
||||
@@ -516,10 +546,6 @@
|
||||
"showCharts": "Show charts",
|
||||
"showUptimePercentage": "Show uptime percentage",
|
||||
"removeLogo": "Remove Logo",
|
||||
"statusPageStatus": "A public status page is not set up.",
|
||||
"statusPageStatusContactAdmin": "Please contact to your administrator",
|
||||
"statusPageStatusNotPublic": "This status page is not public.",
|
||||
"statusPageStatusNoPage": "There's no status page here.",
|
||||
"statusPageStatusServiceStatus": "Service status",
|
||||
"deleteStatusPage": "Do you want to delete this status page?",
|
||||
"deleteStatusPageConfirm": "Yes, delete status page",
|
||||
@@ -589,6 +615,10 @@
|
||||
"getToken": "Get token",
|
||||
"emailToken": "E-mail token",
|
||||
"table": {
|
||||
"headers": {
|
||||
"name": "Status page name",
|
||||
"url": "Public URL"
|
||||
},
|
||||
"name": "Name",
|
||||
"email": "Email",
|
||||
"role": "Role",
|
||||
@@ -793,6 +823,10 @@
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"headers": {
|
||||
"name": "Status page name",
|
||||
"url": "Public URL"
|
||||
},
|
||||
"timestamp": "Timestamp",
|
||||
"level": "Level",
|
||||
"service": "Service",
|
||||
@@ -973,6 +1007,10 @@
|
||||
"save": "Save"
|
||||
},
|
||||
"table": {
|
||||
"headers": {
|
||||
"name": "Status page name",
|
||||
"url": "Public URL"
|
||||
},
|
||||
"actionHeader": "Action",
|
||||
"roleHeader": "Role"
|
||||
},
|
||||
|
||||
@@ -81,7 +81,7 @@ class StatusPageController {
|
||||
const settings = await this.settingsService.getDBSettings();
|
||||
const showURL = settings.showURL;
|
||||
|
||||
const monitors = await this.monitorsRepository.findByIdsWithChecks(statusPage.monitors);
|
||||
const monitors = await this.monitorsRepository.findByIds(statusPage.monitors);
|
||||
const normalizedMonitors = monitors.map((monitor) => {
|
||||
const normalizedChecks = NormalizeData(monitor.recentChecks, 10, 100);
|
||||
if (!showURL) {
|
||||
|
||||
Reference in New Issue
Block a user