Merge branch 'develop' into feat/super-simple-queue

This commit is contained in:
Alex Holliday committed 2025-07-08 11:02:14 -07:00
commit 9040a99853
45 files changed
+1630 -956

No files matched your search

+1 -1
View File
@@ -50,7 +50,7 @@ Usage instructions can be found [here](https://docs.checkmate.so/checkmate-2.1).
## 🛠️ Installation
See installation instructions in [Checkmate documentation portal](https://docs.checkmate.so/checkmate-2.1/users-guide/quickstart). Alternatively, you can also use [Coolify](https://coolify.io/) or [Elestio](https://elest.io/open-source/checkmate) for a one-click Docker deployment. If you would like to monitor your server infrastructure, you'll need [Capture agent](https://github.com/bluewave-labs/capture). Capture repository also contains the installation instructions.
See installation instructions in [Checkmate documentation portal](https://docs.checkmate.so/checkmate-2.1/users-guide/quickstart). Alternatively, you can also use [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate) or [Pikapods](https://www.pikapods.com/) to quickly spin off a Checkmate instance. If you would like to monitor your server infrastructure, you'll need [Capture agent](https://github.com/bluewave-labs/capture). Capture repository also contains the installation instructions.
## 🏁 Translations
+5 -1
View File
@@ -1,19 +1,22 @@
[class*="fallback__"] {
width: fit-content;
margin: auto;
margin-top: 100px;
}
[class*="fallback__"] h1.MuiTypography-root {
font-size: var(--env-var-font-size-large);
font-weight: 600;
}
[class*="fallback__"] button.MuiButtonBase-root,
[class*="fallback__"] .check {
width: max-content;
}
[class*="fallback__"] button.MuiButtonBase-root {
min-height: 34px;
}
[class*="fallback__"] .check span.MuiTypography-root,
[class*="fallback__"] button.MuiButtonBase-root {
font-size: var(--env-var-font-size-medium);
@@ -35,6 +38,7 @@
background-size: cover;
background-repeat: no-repeat;
}
.fallback__status > .MuiStack-root {
margin-left: var(--env-var-spacing-2);
}
+110 -88
View File
@@ -67,113 +67,135 @@ const Fallback = ({
</>
);
};
return (
<Box
position="relative"
border={1}
borderColor={theme.palette.primary.lowContrast}
borderRadius={theme.shape.borderRadius}
backgroundColor={theme.palette.primary.main}
overflow="hidden"
sx={{
borderStyle: "dashed",
minHeight: "calc(100vh - var(--env-var-spacing-2) * 2)",
height: "100%",
width: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<Stack
className={`fallback__${title?.trim().split(" ")[0]}`}
alignItems="center"
gap={theme.spacing(20)}
<Box
border={1}
borderColor={theme.palette.primary.lowContrast}
borderRadius={theme.shape.borderRadius}
backgroundColor={theme.palette.primary.main}
overflow="hidden"
sx={{
display: "flex",
borderStyle: "dashed",
height: {
sm: "50vh",
md: "70vh",
lg: "60vh",
xl: "55vh",
},
width: {
sm: "90%",
md: "50%",
lg: "40%",
},
padding: theme.spacing(10),
}}
>
{mode === "light" ? (
<Skeleton style={{ zIndex: 1 }} />
) : (
<SkeletonDark style={{ zIndex: 1 }} />
)}
<Box
className="background-pattern-svg"
sx={{
"& svg g g:last-of-type path": {
stroke: theme.palette.primary.lowContrast,
},
}}
>
<Background style={{ width: "100%" }} />
</Box>
<Stack
gap={theme.spacing(4)}
maxWidth={"300px"}
zIndex={1}
className={`fallback__${title?.trim().split(" ")[0]}`}
alignItems="center"
gap={theme.spacing(20)}
>
<Typography
component="h1"
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
{mode === "light" ? (
<Skeleton style={{ zIndex: 1 }} />
) : (
<SkeletonDark style={{ zIndex: 1 }} />
)}
<Box
className="background-pattern-svg"
sx={{
"& svg g g:last-of-type path": {
stroke: theme.palette.primary.lowContrast,
},
}}
>
{vowelStart ? "An" : "A"} {title} is used to:
</Typography>
{checks?.map((check, index) => (
<Check
text={check}
key={`${title.trim().split(" ")[0]}-${index}`}
outlined={true}
/>
))}
</Stack>
{/* TODO - display a different fallback if user is not an admin*/}
{isAdmin && (
<>
<Button
variant="contained"
color="accent"
sx={{ alignSelf: "center" }}
onClick={() => navigate(link)}
<Background style={{ width: "100%" }} />
</Box>
<Stack
gap={theme.spacing(4)}
maxWidth={theme.spacing(180)}
zIndex={1}
>
<Typography
alignSelf="center"
component="h1"
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
Let's create your first {title}
</Button>
{/* Bulk create of uptime monitors */}
{title === "uptime monitor" && (
{vowelStart ? "An" : "A"} {title} is used to:
</Typography>
{checks?.map((check, index) => (
<Check
text={check}
key={`${title.trim().split(" ")[0]}-${index}`}
outlined={true}
/>
))}
</Stack>
{/* TODO - display a different fallback if user is not an admin*/}
{isAdmin && (
<>
<Button
variant="contained"
color="accent"
sx={{ alignSelf: "center" }}
onClick={() => navigate("/uptime/bulk-import")}
onClick={() => navigate(link)}
>
{t("bulkImport.fallbackPage")}
Let's create your first {title}
</Button>
)}
{/* Warning box for PageSpeed monitor */}
{title === "pagespeed monitor" && showPageSpeedWarning && (
<Box sx={{ width: "80%", maxWidth: "600px", zIndex: 1 }}>
<Box
sx={{
"& .alert.row-stack": {
backgroundColor: theme.palette.warningSecondary.main,
borderColor: theme.palette.warningSecondary.lowContrast,
"& .MuiTypography-root": {
color: theme.palette.warningSecondary.contrastText,
},
"& .MuiBox-root > svg": {
color: theme.palette.warningSecondary.contrastText,
},
},
}}
{/* Bulk create of uptime monitors */}
{title === "uptime monitor" && (
<Button
variant="contained"
color="accent"
sx={{ alignSelf: "center" }}
onClick={() => navigate("/uptime/bulk-import")}
>
{settingsData?.pagespeedKeySet === false && (
<Alert
variant="warning"
hasIcon={true}
body={renderWarningMessage()}
/>
)}
{t("bulkImport.fallbackPage")}
</Button>
)}
{/* Warning box for PageSpeed monitor */}
{title === "pagespeed monitor" && showPageSpeedWarning && (
<Box sx={{ width: "80%", maxWidth: "600px", zIndex: 1 }}>
<Box
sx={{
"& .alert.row-stack": {
backgroundColor: theme.palette.warningSecondary.main,
borderColor: theme.palette.warningSecondary.lowContrast,
"& .MuiTypography-root": {
color: theme.palette.warningSecondary.contrastText,
},
"& .MuiBox-root > svg": {
color: theme.palette.warningSecondary.contrastText,
},
},
}}
>
{settingsData?.pagespeedKeySet === false && (
<Alert
variant="warning"
hasIcon={true}
body={renderWarningMessage()}
/>
)}
</Box>
</Box>
</Box>
)}
</>
)}
</Stack>
)}
</>
)}
</Stack>
</Box>
</Box>
);
};
@@ -2,6 +2,7 @@ import Stack from "@mui/material/Stack";
import Status from "./status";
import Skeleton from "./skeleton";
import Button from "@mui/material/Button";
import { Tooltip } from "@mui/material";
import SettingsOutlinedIcon from "@mui/icons-material/SettingsOutlined";
import PauseOutlinedIcon from "@mui/icons-material/PauseOutlined";
import PlayArrowOutlinedIcon from "@mui/icons-material/PlayArrowOutlined";
@@ -40,6 +41,10 @@ const MonitorDetailsControlHeader = ({
const { t } = useTranslation();
const [pauseMonitor, isPausing, error] = usePauseMonitor();
const isTestNotificationsDisabled = monitor?.notifications?.length === 0;
const tooltipTitle = isTestNotificationsDisabled ? t("testNotificationsDisabled") : "";
// const [isSending, emailError, sendTestEmail] = useSendTestEmail();
const [testAllNotifications, isSending, errorAllNotifications] =
@@ -60,20 +65,29 @@ const MonitorDetailsControlHeader = ({
direction="row"
gap={theme.spacing(2)}
>
<Button
variant="contained"
color="secondary"
loading={isSending}
startIcon={<EmailIcon />}
onClick={() => {
testAllNotifications({ monitorId: monitor?._id });
}}
sx={{
whiteSpace: "nowrap",
}}
<Tooltip
key={monitor?._id}
placement="bottom"
title={tooltipTitle}
>
{t("sendTestNotifications")}
</Button>
<span>
<Button
variant="contained"
color="secondary"
loading={isSending}
startIcon={<EmailIcon />}
disabled={isTestNotificationsDisabled}
onClick={() => {
testAllNotifications({ monitorId: monitor?._id });
}}
sx={{
whiteSpace: "nowrap",
}}
>
{t("sendTestNotifications")}
</Button>
</span>
</Tooltip>
<Button
variant="contained"
color="secondary"
@@ -214,7 +214,7 @@ const PasswordPanel = () => {
<TextInput
type="password"
id="edit-confirm-password"
placeholder={t("confirmPassword", "Confirm password")}
placeholder={t("confirmPassword")}
autoComplete="new-password"
value={localData.confirm}
onChange={handleChange}
+90 -2
View File
@@ -1,6 +1,7 @@
import { useState, useEffect } from "react";
import { networkService } from "../main";
import { createToast } from "../Utils/toastUtils";
import { useTranslation } from "react-i18next";
const useFetchChecksTeam = ({
status,
@@ -12,6 +13,7 @@ const useFetchChecksTeam = ({
page,
rowsPerPage,
enabled = true,
updateTrigger,
}) => {
const [checks, setChecks] = useState(undefined);
const [checksCount, setChecksCount] = useState(undefined);
@@ -49,7 +51,18 @@ const useFetchChecksTeam = ({
};
fetchChecks();
}, [status, sortOrder, limit, dateRange, filter, ack, page, rowsPerPage, enabled]);
}, [
status,
sortOrder,
limit,
dateRange,
filter,
ack,
page,
rowsPerPage,
enabled,
updateTrigger,
]);
return [checks, checksCount, isLoading, networkError];
};
@@ -66,6 +79,7 @@ const useFetchChecksByMonitor = ({
page,
rowsPerPage,
enabled = true,
updateTrigger,
}) => {
const [checks, setChecks] = useState(undefined);
const [checksCount, setChecksCount] = useState(undefined);
@@ -117,9 +131,83 @@ const useFetchChecksByMonitor = ({
page,
rowsPerPage,
enabled,
updateTrigger,
]);
return [checks, checksCount, isLoading, networkError];
};
export { useFetchChecksByMonitor, useFetchChecksTeam };
const useFetchChecksSummaryByTeamId = ({ updateTrigger } = {}) => {
const [summary, setSummary] = useState(undefined);
const [isLoading, setIsLoading] = useState(false);
const [networkError, setNetworkError] = useState(false);
useEffect(() => {
const fetchSummary = async () => {
try {
setIsLoading(true);
const res = await networkService.getChecksAndSummaryByTeamId();
setSummary(res.data.data);
} catch (error) {
setNetworkError(true);
createToast({ body: error.message });
} finally {
setIsLoading(false);
}
};
fetchSummary();
}, [updateTrigger]);
return [summary, isLoading, networkError];
};
const useResolveIncident = () => {
const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation();
const resolveIncident = async (checkId, setUpdateTrigger) => {
try {
setIsLoading(true);
await networkService.updateCheckStatus({
checkId,
ack: true,
});
setUpdateTrigger((prev) => !prev);
} catch (error) {
createToast({ body: t("checkHooks.failureResolveOne") });
} finally {
setIsLoading(false);
}
};
return [resolveIncident, isLoading];
};
const useAckAllChecks = () => {
const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation();
const ackAllChecks = async (setUpdateTrigger) => {
try {
setIsLoading(true);
await networkService.updateAllChecksStatus({ ack: true });
setUpdateTrigger((prev) => !prev);
} catch (error) {
createToast({ body: t("checkHooks.failureResolveAll") });
} finally {
setIsLoading(false);
}
};
return [ackAllChecks, isLoading];
};
export {
useFetchChecksByMonitor,
useFetchChecksTeam,
useFetchChecksSummaryByTeamId,
useResolveIncident,
useAckAllChecks,
};
+2 -2
View File
@@ -385,9 +385,9 @@ const useAddDemoMonitors = () => {
try {
setIsLoading(true);
await networkService.addDemoMonitors();
createToast({ body: t("settingsDemoMonitorsAdded") });
createToast({ body: t("monitorHooks.successAddDemoMonitors") });
} catch (error) {
createToast({ body: t("settingsFailedToAddDemoMonitors") });
createToast({ body: t("monitorHooks.failureAddDemoMonitors") });
} finally {
setIsLoading(false);
}
+4 -2
View File
@@ -65,14 +65,16 @@ const useDeleteNotification = () => {
const [error, setError] = useState(null);
const { t } = useTranslation();
const deleteNotification = async (id, triggerUpdate) => {
const deleteNotification = async (id, callback) => {
try {
setIsLoading(true);
await networkService.deleteNotificationById({ id });
createToast({
body: t("notifications.delete.success"),
});
triggerUpdate();
if (callback) {
callback();
}
} catch (error) {
setError(error);
createToast({
@@ -15,13 +15,17 @@ import PropTypes from "prop-types";
import { useTranslation } from "react-i18next";
import { useFetchChecksTeam } from "../../../../Hooks/checkHooks";
import { useFetchChecksByMonitor } from "../../../../Hooks/checkHooks";
import { useResolveIncident } from "../../../../Hooks/checkHooks";
import { Button, Typography } from "@mui/material";
const IncidentTable = ({
shouldRender,
isLoading,
monitors,
selectedMonitor,
filter,
dateRange,
updateTrigger,
setUpdateTrigger,
}) => {
//Redux state
const uiTimezone = useSelector((state) => state.ui.timezone);
@@ -32,6 +36,9 @@ const IncidentTable = ({
const selectedMonitorDetails = monitors?.[selectedMonitor];
const selectedMonitorType = selectedMonitorDetails?.type;
//Hooks
const [resolveIncident, resolveLoading] = useResolveIncident();
const [checksMonitor, checksCountMonitor, isLoadingMonitor, networkErrorMonitor] =
useFetchChecksByMonitor({
monitorId: selectedMonitor === "0" ? undefined : selectedMonitor,
@@ -45,6 +52,7 @@ const IncidentTable = ({
page: page,
rowsPerPage: rowsPerPage,
enabled: selectedMonitor !== "0",
updateTrigger,
});
const [checksTeam, checksCountTeam, isLoadingTeam, networkErrorTeam] =
@@ -58,11 +66,12 @@ const IncidentTable = ({
page: page,
rowsPerPage: rowsPerPage,
enabled: selectedMonitor === "0",
updateTrigger,
});
const checks = selectedMonitor === "0" ? checksTeam : checksMonitor;
const checksCount = selectedMonitor === "0" ? checksCountTeam : checksCountMonitor;
const isLoading = isLoadingTeam || isLoadingMonitor;
isLoading = isLoadingTeam || isLoadingMonitor;
const networkError = selectedMonitor === "0" ? networkErrorTeam : networkErrorMonitor;
const { t } = useTranslation();
@@ -76,6 +85,10 @@ const IncidentTable = ({
setRowsPerPage(event.target.value);
};
const handleResolveIncident = (checkId) => {
resolveIncident(checkId, setUpdateTrigger);
};
const headers = [
{
id: "monitorName",
@@ -114,9 +127,31 @@ const IncidentTable = ({
render: (row) => <HttpStatusLabel status={row.statusCode} />,
},
{ id: "message", content: t("incidentsTableMessage"), render: (row) => row.message },
{
id: "action",
content: t("actions"),
render: (row) => {
return row.ack === false ? (
<Button
variant="contained"
color="accent"
onClick={() => {
handleResolveIncident(row._id);
}}
>
{t("incidentsTableActionResolve")}
</Button>
) : (
<Typography>
{t("incidentsTableResolvedAt")}{" "}
{formatDateWithTz(row.ackAt, "YYYY-MM-DD HH:mm:ss A", uiTimezone)}
</Typography>
);
},
},
];
if (!shouldRender || isLoading) return <TableSkeleton />;
if (isLoading || resolveLoading) return <TableSkeleton />;
if (networkError) {
return (
@@ -149,10 +184,12 @@ const IncidentTable = ({
};
IncidentTable.propTypes = {
shouldRender: PropTypes.bool,
isLoading: PropTypes.bool,
monitors: PropTypes.object,
selectedMonitor: PropTypes.string,
filter: PropTypes.string,
dateRange: PropTypes.string,
updateTrigger: PropTypes.bool,
setUpdateTrigger: PropTypes.func,
};
export default IncidentTable;
@@ -21,6 +21,12 @@ const OptionsHeader = ({
const theme = useTheme();
const { t } = useTranslation();
const monitorNames = typeof monitors !== "undefined" ? Object.values(monitors) : [];
const filterOptions = [
{ _id: "all", name: t("incidentsOptionsHeaderFilterAll") },
{ _id: "down", name: t("incidentsOptionsHeaderFilterDown") },
{ _id: "resolve", name: t("incidentsOptionsHeaderFilterCannotResolve") },
{ _id: "resolved", name: t("incidentsOptionsHeaderFilterResolved") },
];
// The stacks below which are three in number have the same style so
const stackStyles = {
@@ -65,36 +71,16 @@ const OptionsHeader = ({
>
{t("incidentsOptionsHeaderFilterBy")}
</Typography>
<ButtonGroup>
<Button
variant="group"
filled={(filter === "all").toString()}
onClick={() => setFilter("all")}
>
{t("incidentsOptionsHeaderFilterAll")}
</Button>
<Button
variant="group"
filled={(filter === "down").toString()}
onClick={() => setFilter("down")}
>
{t("incidentsOptionsHeaderFilterDown")}
</Button>
<Button
variant="group"
filled={(filter === "resolve").toString()}
onClick={() => setFilter("resolve")}
>
{t("incidentsOptionsHeaderFilterCannotResolve")}
</Button>
{/* <Button
variant="group"
filled={(filter === "resolved").toString()}
onClick={() => setFilter("resolved")}
>
{t("incidentsOptionsHeaderFilterResolved")}
</Button> */}
</ButtonGroup>
<Select
id="incidents-select-filter"
value={filter}
onChange={(e) => setFilter(e.target.value)}
items={filterOptions}
sx={{
backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastTextSecondary,
}}
/>
</Stack>
<Stack {...stackStyles}>
<Typography
@@ -0,0 +1,116 @@
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { Box, Stack, Typography } from "@mui/material";
import Background from "../../../../assets/Images/background-grid.svg?react";
import MonitorHeartOutlinedIcon from "@mui/icons-material/MonitorHeartOutlined";
import TaskAltOutlinedIcon from "@mui/icons-material/TaskAltOutlined";
import CancelOutlinedIcon from "@mui/icons-material/CancelOutlined";
import WarningAmberRoundedIcon from "@mui/icons-material/WarningAmberRounded";
const StatusBox = ({ title, value, status }) => {
const theme = useTheme();
let sharedStyles = {
position: "absolute",
right: 8,
opacity: 0.5,
"& svg path": { stroke: theme.palette.primary.contrastTextTertiary },
};
let color;
let icon;
if (status === "up") {
color = theme.palette.success.lowContrast;
icon = (
<Box sx={{ ...sharedStyles, top: theme.spacing(6), right: theme.spacing(6) }}>
<TaskAltOutlinedIcon fontSize="small" />
</Box>
);
} else if (status === "down") {
color = theme.palette.error.lowContrast;
icon = (
<Box sx={{ ...sharedStyles, top: theme.spacing(6), right: theme.spacing(6) }}>
<CancelOutlinedIcon fontSize="small" />
</Box>
);
} else if (status === "paused") {
color = theme.palette.warning.lowContrast;
icon = (
<Box sx={{ ...sharedStyles, top: theme.spacing(6), right: theme.spacing(6) }}>
<WarningAmberRoundedIcon fontSize="small" />
</Box>
);
} else {
color = theme.palette.accent.main;
icon = (
<Box sx={{ ...sharedStyles, top: theme.spacing(6), right: theme.spacing(6) }}>
<MonitorHeartOutlinedIcon fontSize="small" />
</Box>
);
}
return (
<Box
position="relative"
flex={1}
border={1}
backgroundColor={theme.palette.primary.main}
borderColor={theme.palette.primary.lowContrast}
borderRadius={theme.shape.borderRadius}
p={theme.spacing(8)}
overflow="hidden"
>
<Box
position="absolute"
top="-10%"
left="5%"
>
<Background />
</Box>
<Stack direction="column">
<Stack
direction="row"
alignItems="center"
mb={theme.spacing(8)}
>
<Typography
variant={"h2"}
textTransform="uppercase"
color={theme.palette.primary.contrastTextTertiary}
>
{title}
</Typography>
{icon}
</Stack>
<Stack
direction="row"
alignItems="flex-start"
fontSize={theme.typography.h1.fontSize}
fontWeight={600}
color={color}
gap={theme.spacing(1)}
>
{value}
<Typography
fontSize={theme.typography.label.fontSize}
fontWeight={300}
color={theme.palette.primary.contrastTextSecondary}
sx={{
opacity: 0.3,
}}
>
#
</Typography>
</Stack>
</Stack>
</Box>
);
};
StatusBox.propTypes = {
title: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
status: PropTypes.string,
};
export default StatusBox;
@@ -0,0 +1,46 @@
import PropTypes from "prop-types";
import { Stack } from "@mui/material";
import StatusBox from "./StatusBox";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import SkeletonLayout from "./skeleton";
const StatusBoxes = ({ isLoading, summary }) => {
const theme = useTheme();
const { t } = useTranslation();
if (isLoading) return <SkeletonLayout shouldRender={isLoading} />;
return (
<Stack
gap={theme.spacing(12)}
direction="row"
justifyContent="space-between"
>
<StatusBox
title={t("incidentsOptionsHeaderTotalIncidents")}
value={summary?.totalChecks || 0}
/>
<StatusBox
title={t("incidentsOptionsHeaderFilterResolved")}
status="up"
value={summary?.resolvedChecks || 0}
/>
<StatusBox
title={t("incidentsOptionsHeaderFilterCannotResolve")}
status="paused"
value={summary?.cannotResolveChecks || 0}
/>
<StatusBox
title={t("incidentsOptionsHeaderFilterDown")}
status="down"
value={summary?.downChecks || 0}
/>
</Stack>
);
};
StatusBoxes.propTypes = {
isLoading: PropTypes.bool,
summary: PropTypes.object,
};
export default StatusBoxes;
@@ -0,0 +1,36 @@
import { Skeleton, Stack } from "@mui/material";
import { useTheme } from "@emotion/react";
const SkeletonLayout = () => {
const theme = useTheme();
return (
<Stack
gap={theme.spacing(12)}
direction="row"
justifyContent="space-between"
>
<Skeleton
variant="rounded"
width="100%"
height={100}
/>
<Skeleton
variant="rounded"
width="100%"
height={100}
/>
<Skeleton
variant="rounded"
width="100%"
height={100}
/>
<Skeleton
variant="rounded"
width="100%"
height={100}
/>
</Stack>
);
};
export default SkeletonLayout;
+33 -2
View File
@@ -4,10 +4,14 @@ import Breadcrumbs from "../../Components/Breadcrumbs";
import GenericFallback from "../../Components/GenericFallback";
import IncidentTable from "./Components/IncidentTable";
import OptionsHeader from "./Components/OptionsHeader";
import StatusBoxes from "./Components/StatusBoxes";
import { Box, Button } from "@mui/material";
//Utils
import { useTheme } from "@emotion/react";
import { useFetchMonitorsByTeamId } from "../../Hooks/monitorHooks";
import { useFetchChecksSummaryByTeamId } from "../../Hooks/checkHooks";
import { useAckAllChecks } from "../../Hooks/checkHooks";
import { useState, useEffect } from "react";
import NetworkError from "../../Components/GenericFallback/NetworkError";
import { useTranslation } from "react-i18next";
@@ -27,10 +31,17 @@ const Incidents = () => {
const [filter, setFilter] = useState(undefined);
const [dateRange, setDateRange] = useState(undefined);
const [monitorLookup, setMonitorLookup] = useState(undefined);
const [updateTrigger, setUpdateTrigger] = useState(false);
//Hooks
const [ackAllChecks, ackAllLoading] = useAckAllChecks();
//Utils
const theme = useTheme();
const [monitors, , isLoading, networkError] = useFetchMonitorsByTeamId({});
const [summary, isLoadingSummary, networkErrorSummary] = useFetchChecksSummaryByTeamId({
updateTrigger,
});
const { monitorId } = useParams();
useEffect(() => {
@@ -51,7 +62,11 @@ const Incidents = () => {
setMonitorLookup(monitorLookup);
}, [monitors]);
if (networkError) {
const handleAckAllChecks = () => {
ackAllChecks(setUpdateTrigger);
};
if (networkError || networkErrorSummary) {
return (
<GenericFallback>
<NetworkError />
@@ -62,6 +77,20 @@ const Incidents = () => {
return (
<Stack gap={theme.spacing(10)}>
<Breadcrumbs list={BREADCRUMBS} />
<Box alignSelf="flex-end">
<Button
variant="contained"
color="accent"
onClick={handleAckAllChecks}
disabled={ackAllLoading}
>
{t("incidentsPageActionResolve")}
</Button>
</Box>
<StatusBoxes
isLoading={isLoadingSummary}
summary={summary}
/>
<OptionsHeader
shouldRender={!isLoading}
monitors={monitorLookup}
@@ -73,11 +102,13 @@ const Incidents = () => {
setDateRange={setDateRange}
/>
<IncidentTable
shouldRender={!isLoading}
isLoading={isLoading}
monitors={monitorLookup ? monitorLookup : {}}
selectedMonitor={selectedMonitor}
filter={filter}
dateRange={dateRange}
updateTrigger={updateTrigger}
setUpdateTrigger={setUpdateTrigger}
/>
</Stack>
);
@@ -9,6 +9,8 @@ import Settings from "../../../../../assets/icons/settings-bold.svg?react";
import PropTypes from "prop-types";
import Dialog from "../../../../../Components/Dialog";
import { networkService } from "../../../../../Utils/NetworkService.js";
import { usePauseMonitor } from "../../../../../Hooks/monitorHooks";
import { useTranslation } from "react-i18next";
/**
* InfrastructureMenu Component
@@ -19,7 +21,7 @@ import { networkService } from "../../../../../Utils/NetworkService.js";
* @param {string} props.monitor.id - Unique ID of the monitor.
* @param {string} [props.monitor.url] - URL associated with the monitor.
* @param {string} props.monitor.type - Type of monitor (e.g., uptime, infrastructure).
* @param {boolean} props.monitor.isActive - Indicates if the monitor is currently active.
* @param {boolean} props.monitor.isActive - Indicates if the monitor is currently active (true) or paused (false).
* @param {boolean} props.isAdmin - Whether the user has admin privileges.
* @param {Function} props.updateCallback - Callback to trigger when the monitor data is updated.
* @returns {JSX.Element} The rendered component.
@@ -29,6 +31,8 @@ const InfrastructureMenu = ({ monitor, isAdmin, updateCallback }) => {
const [isOpen, setIsOpen] = useState(false);
const [isDialogOpen, setIsDialogOpen] = useState(false);
const theme = useTheme();
const [pauseMonitor] = usePauseMonitor();
const { t } = useTranslation();
const openMenu = (e) => {
e.stopPropagation();
@@ -54,14 +58,26 @@ const InfrastructureMenu = ({ monitor, isAdmin, updateCallback }) => {
navigate(`/infrastructure/${id}`);
}
const openConfigure = (id) => {
navigate(`/infrastructure/configure/${id}`);
};
const handlePause = async () => {
// Pass updateCallback as triggerUpdate to the hook
await pauseMonitor({ monitorId: monitor.id, triggerUpdate: updateCallback });
// Toast is already displayed in the hook, no need to display it again
};
const handleRemove = async () => {
try {
await networkService.deleteMonitorById({
monitorId: monitor.id,
});
createToast({ body: "Monitor deleted successfully." });
createToast({
body: t("monitorActions.deleteSuccess"),
});
} catch (error) {
createToast({ body: "Failed to delete monitor." });
createToast({ body: t("monitorActions.deleteFailed") });
} finally {
setIsDialogOpen(false);
updateCallback();
@@ -105,15 +121,53 @@ const InfrastructureMenu = ({ monitor, isAdmin, updateCallback }) => {
},
}}
>
{isAdmin && <MenuItem onClick={openRemove}>Remove</MenuItem>}
<MenuItem
onClick={(e) => {
e.stopPropagation();
openDetails(monitor.id);
closeMenu(e);
}}
>
{t("monitorActions.details")}
</MenuItem>
{isAdmin && (
<MenuItem
onClick={(e) => {
e.stopPropagation();
openConfigure(monitor.id);
closeMenu(e);
}}
>
{t("configure")}
</MenuItem>
)}
{isAdmin && (
<MenuItem
onClick={async (e) => {
e.stopPropagation();
await handlePause();
closeMenu(e);
}}
>
{!monitor.isActive ? t("resume") : t("pause")}
</MenuItem>
)}
{isAdmin && (
<MenuItem
onClick={openRemove}
sx={{ color: theme.palette.error.main }}
>
{t("remove")}
</MenuItem>
)}
</Menu>
<Dialog
open={isDialogOpen}
theme={theme}
title="Do you really want to delete this monitor?"
description="Once deleted, this monitor cannot be retrieved."
title={t("deleteDialogTitle")}
description={t("deleteDialogDescription")}
onCancel={cancelRemove}
confirmationButtonLabel="Delete"
confirmationButtonLabel={t("delete")}
onConfirm={handleRemove}
modelTitle="modal-delete-monitor"
modelDescription="delete-monitor-confirmation"
@@ -124,13 +178,16 @@ const InfrastructureMenu = ({ monitor, isAdmin, updateCallback }) => {
InfrastructureMenu.propTypes = {
monitor: PropTypes.shape({
id: PropTypes.string,
id: PropTypes.string.isRequired,
url: PropTypes.string,
// Note: type must remain optional. Making it required (type: PropTypes.string.isRequired)
// causes runtime errors as some monitors don't have a defined type property
type: PropTypes.string,
isActive: PropTypes.bool,
isActive: PropTypes.bool, // Determines whether the monitor is paused (false) or active (true)
status: PropTypes.string, // Represents the monitor's operational status (e.g., 'up', 'down', etc.)
}).isRequired,
isAdmin: PropTypes.bool,
updateCallback: PropTypes.func,
isAdmin: PropTypes.bool.isRequired,
updateCallback: PropTypes.func.isRequired,
};
export { InfrastructureMenu };
@@ -9,29 +9,35 @@ import { useState } from "react";
import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router-dom";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next";
const ActionMenu = ({ notification, onDelete }) => {
const theme = useTheme();
const navigate = useNavigate();
const [anchorEl, setAnchorEl] = useState(null);
const open = Boolean(anchorEl);
const { t } = useTranslation();
// Handlers
const handleClick = (event) => {
event.stopPropagation();
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
const handleClose = (event) => {
if (event) {
event.stopPropagation();
}
setAnchorEl(null);
};
const handleRemove = () => {
const handleRemove = (e) => {
e.stopPropagation();
onDelete(notification._id);
handleClose();
};
const handleConfigure = () => {
const handleConfigure = (e) => {
e.stopPropagation();
navigate(`/notifications/${notification._id}`);
handleClose();
};
@@ -39,8 +45,9 @@ const ActionMenu = ({ notification, onDelete }) => {
return (
<>
<IconButton
aria-label="monitor actions"
aria-label="notification actions"
onClick={handleClick}
onMouseDown={(e) => e.stopPropagation()}
>
<SettingsOutlinedIcon />
</IconButton>
@@ -50,13 +57,15 @@ const ActionMenu = ({ notification, onDelete }) => {
open={open}
onClose={handleClose}
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
>
<MenuItem onClick={handleConfigure}>Configure</MenuItem>
<MenuItem onClick={handleConfigure}>{t("configure")}</MenuItem>
<MenuItem
onClick={handleRemove}
sx={{ "&.MuiButtonBase-root": { color: theme.palette.error.main } }}
>
Remove
{t("delete")}
</MenuItem>
</Menu>
</>
+42 -12
View File
@@ -7,6 +7,7 @@ import ConfigBox from "../../../Components/ConfigBox";
import Box from "@mui/material/Box";
import Select from "../../../Components/Inputs/Select";
import TextInput from "../../../Components/Inputs/TextInput";
import Dialog from "../../../Components/Dialog";
// Utils
import { useState } from "react";
@@ -16,11 +17,12 @@ import {
useGetNotificationById,
useEditNotification,
useTestNotification,
useDeleteNotification,
} from "../../../Hooks/useNotifications";
import { notificationValidation } from "../../../Validation/validation";
import { createToast } from "../../../Utils/toastUtils";
import { useTranslation } from "react-i18next";
import { useParams } from "react-router-dom";
import { useParams, useNavigate } from "react-router-dom";
import {
NOTIFICATION_TYPES,
TITLE_MAP,
@@ -33,15 +35,19 @@ import {
const CreateNotifications = () => {
const { notificationId } = useParams();
const navigate = useNavigate();
const theme = useTheme();
const [createNotification, isCreating, createNotificationError] =
useCreateNotification();
const [editNotification, isEditing, editNotificationError] = useEditNotification();
const [testNotification, isTesting, testNotificationError] = useTestNotification();
const [createNotification, isCreating] = useCreateNotification();
const [editNotification, isEditing] = useEditNotification();
const [testNotification, isTesting] = useTestNotification();
const [deleteNotification, isDeleting] = useDeleteNotification();
const BREADCRUMBS = [
{ name: "notifications", path: "/notifications" },
{ name: "create", path: "/notifications/create" },
{
name: notificationId ? "edit" : "create",
path: notificationId ? `/notifications/${notificationId}` : "/notifications/create",
},
];
// Redux state
@@ -55,10 +61,9 @@ const CreateNotifications = () => {
const [errors, setErrors] = useState({});
const { t } = useTranslation();
const [notificationIsLoading, getNotificationError] = useGetNotificationById(
notificationId,
setNotification
);
const [notificationIsLoading] = useGetNotificationById(notificationId, setNotification);
const [isOpen, setIsOpen] = useState(false);
// handlers
const onSubmit = (e) => {
@@ -128,6 +133,12 @@ const CreateNotifications = () => {
testNotification(form);
};
const onDelete = () => {
if (notificationId) {
deleteNotification(notificationId, () => navigate("/notifications"));
}
};
const type = NOTIFICATION_TYPES.find((type) => type._id === notification.type).value;
return (
<Stack gap={theme.spacing(10)}>
@@ -209,18 +220,37 @@ const CreateNotifications = () => {
color="secondary"
onClick={onTestNotification}
>
Test notification
{t("createNotifications.testNotification")}
</Button>
{notificationId && (
<Button
loading={isDeleting}
variant="contained"
color="error"
onClick={() => setIsOpen(true)}
>
{t("delete")}
</Button>
)}
<Button
loading={isCreating || isEditing || notificationIsLoading}
type="submit"
variant="contained"
color="accent"
>
Submit
{t("submit")}
</Button>
</Stack>
</Stack>
<Dialog
open={isOpen}
onClose={() => setIsOpen(false)}
onCancel={() => setIsOpen(false)}
title={t("createNotifications.dialogDeleteTitle")}
confirmationButtonLabel={t("createNotifications.dialogDeleteConfirm")}
onConfirm={onDelete}
isLoading={isDeleting}
/>
</Stack>
);
};
+3
View File
@@ -62,6 +62,9 @@ const Notifications = () => {
{
id: "actions",
content: "Actions",
onClick: (e) => {
e.stopPropagation();
},
render: (row) => {
return (
<ActionMenu
@@ -221,7 +221,15 @@ const PieChart = ({ audits }) => {
const colorMap = getColors(performance);
return (
<Box onMouseLeave={() => setExpand(false)}>
<Box
onMouseLeave={() => setExpand(false)}
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
width: "100%",
}}
>
{expand ? (
<MuiPieChart
series={[
@@ -26,6 +26,9 @@ const PerformanceReport = ({ shouldRender, audits }) => {
<Typography
variant="body1"
mt="auto"
sx={{
textAlign: "center",
}}
>
{t("pageSpeedDetailsPerformanceReport")}{" "}
<Typography
+1 -4
View File
@@ -87,10 +87,7 @@ const SettingsEmail = ({
!emailConfig.systemEmailPassword
) {
createToast({
body: t(
"settingsEmailRequiredFields",
"Email address, host, port and password are required"
),
body: t("settingsPage.emailSettings.toastEmailRequiredFieldsError"),
variant: "error",
});
return;
@@ -117,7 +117,7 @@ const TabSettings = ({
component="h2"
variant="h2"
>
{t("settingsAppearance")}
{t("statusPageCreateAppearanceTitle")}
</Typography>
<Typography component="p">
{t("statusPageCreateAppearanceDescription")}
@@ -1,6 +1,6 @@
// Components
import { TabContext } from "@mui/lab";
import { Tab, useTheme } from "@mui/material";
import { Tab } from "@mui/material";
import Settings from "./Settings";
import Content from "./Content";
@@ -22,8 +22,12 @@ const Tabs = ({
tab,
setTab,
TAB_LIST,
handleDelete,
isDeleteOpen,
setIsDeleteOpen,
isDeleting,
isLoading,
}) => {
const theme = useTheme();
return (
<TabContext value={TAB_LIST[tab]}>
<CustomTabList
@@ -32,7 +36,7 @@ const Tabs = ({
}}
aria-label="status page tabs"
>
{TAB_LIST.map((tabLabel, idx) => (
{TAB_LIST.map((tabLabel) => (
<Tab
key={tabLabel}
label={tabLabel}
@@ -50,6 +54,11 @@ const Tabs = ({
removeLogo={removeLogo}
errors={errors}
isCreate={isCreate}
handleDelete={handleDelete}
isDeleteOpen={isDeleteOpen}
setIsDeleteOpen={setIsDeleteOpen}
isDeleting={isDeleting}
isLoading={isLoading}
/>
) : (
<Content
@@ -67,6 +76,7 @@ const Tabs = ({
};
Tabs.propTypes = {
isCreate: PropTypes.bool,
form: PropTypes.object,
errors: PropTypes.object,
monitors: PropTypes.array,
@@ -79,6 +89,11 @@ Tabs.propTypes = {
tab: PropTypes.number,
setTab: PropTypes.func,
TAB_LIST: PropTypes.array,
handleDelete: PropTypes.func,
isDeleteOpen: PropTypes.bool,
setIsDeleteOpen: PropTypes.func,
isDeleting: PropTypes.bool,
isLoading: PropTypes.bool,
};
export default Tabs;
+64 -11
View File
@@ -3,6 +3,8 @@ import { Stack, Button, Typography } from "@mui/material";
import Tabs from "./Components/Tabs";
import GenericFallback from "../../../Components/GenericFallback";
import SkeletonLayout from "./Components/Skeleton";
import Dialog from "../../../Components/Dialog";
import Breadcrumbs from "../../../Components/Breadcrumbs";
//Utils
import { useTheme } from "@emotion/react";
import { useState, useEffect, useRef, useCallback } from "react";
@@ -15,9 +17,8 @@ import { useNavigate } from "react-router-dom";
import { useStatusPageFetch } from "../Status/Hooks/useStatusPageFetch";
import { useParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { useStatusPageDelete } from "../Status/Hooks/useStatusPageDelete";
//Constants
const TAB_LIST = ["General settings", "Contents"];
const ERROR_TAB_MAPPING = [
["companyName", "url", "timezone", "color", "isPublished", "logo"],
["monitors", "showUptimePercentage", "showCharts", "showAdminLoginLink"],
@@ -28,6 +29,7 @@ const CreateStatusPage = () => {
//Local state
const [tab, setTab] = useState(0);
const [progress, setProgress] = useState({ value: 0, isLoading: false });
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
const [form, setForm] = useState({
isPublished: false,
companyName: "",
@@ -52,14 +54,15 @@ const CreateStatusPage = () => {
//Utils
const theme = useTheme();
const [monitors, isLoading, networkError] = useMonitorsFetch();
const [createStatusPage, createStatusIsLoading, createStatusPageNetworkError] =
useCreateStatusPage(isCreate);
const [createStatusPage] = useCreateStatusPage(isCreate);
const navigate = useNavigate();
const { t } = useTranslation();
const [statusPage, statusPageMonitors, statusPageIsLoading, statusPageNetworkError] =
const [statusPage, statusPageMonitors, statusPageIsLoading, , fetchStatusPage] =
useStatusPageFetch(isCreate, url);
const [deleteStatusPage, isDeleting] = useStatusPageDelete(fetchStatusPage, url);
console.log(JSON.stringify(form, null, 2));
// Handlers
const handleFormChange = (e) => {
let { type, name, value, checked } = e.target;
@@ -123,6 +126,19 @@ const CreateStatusPage = () => {
setProgress({ value: 0, isLoading: false });
};
/**
* Handle status page deletion with optimistic UI update
* Immediately navigates away without waiting for the deletion to complete
* to prevent unnecessary network requests for the deleted page
*/
const handleDelete = async () => {
setIsDeleteOpen(false);
// Start deletion process but don't wait for it
deleteStatusPage();
// Immediately navigate away to prevent additional fetches for the deleted page
navigate("/status");
};
const handleSubmit = async () => {
let toSubmit = {
...form,
@@ -136,9 +152,7 @@ const CreateStatusPage = () => {
const success = await createStatusPage({ form });
if (success) {
createToast({
body: isCreate
? "Status page created successfully"
: "Status page updated successfully",
body: isCreate ? t("statusPage.createSuccess") : t("statusPage.updateSuccess"),
});
navigate(`/status/uptime/${form.url}`);
}
@@ -161,7 +175,7 @@ const CreateStatusPage = () => {
// If we get -1, there's an unknown error
if (errorTabs[0] === -1) {
createToast({ body: "Unknown error" });
createToast({ body: t("common.toasts.unknownError") });
return;
}
@@ -177,7 +191,7 @@ const CreateStatusPage = () => {
}
let newLogo = undefined;
if (statusPage.logo) {
if (statusPage.logo && Object.keys(statusPage.logo).length > 0) {
newLogo = {
src: `data:${statusPage.logo.contentType};base64,${statusPage.logo.data}`,
name: "logo",
@@ -222,6 +236,37 @@ const CreateStatusPage = () => {
// Load fields
return (
<Stack gap={theme.spacing(10)}>
<Breadcrumbs
list={[
{ name: t("statusBreadCrumbsStatusPages", "Status"), path: "/status" },
{ name: t("statusBreadCrumbsDetails", "Details"), path: `/status/${url}` },
{ name: t("configure", "Configure"), path: `/status/create/${url}` },
]}
/>
{!isCreate && (
<Stack
direction="row"
justifyContent="flex-end"
>
<Button
loading={isDeleting}
variant="contained"
color="error"
onClick={() => setIsDeleteOpen(true)}
>
{t("remove")}
</Button>
<Dialog
title={t("deleteStatusPage")}
onConfirm={handleDelete}
onCancel={() => setIsDeleteOpen(false)}
open={isDeleteOpen}
confirmationButtonLabel={t("deleteStatusPageConfirm")}
description={t("deleteStatusPageDescription")}
isLoading={isDeleting || statusPageIsLoading}
/>
</Stack>
)}
<Tabs
form={form}
errors={errors}
@@ -234,8 +279,16 @@ const CreateStatusPage = () => {
removeLogo={removeLogo}
tab={tab}
setTab={setTab}
TAB_LIST={TAB_LIST}
TAB_LIST={[
t("statusPage.generalSettings", "General settings"),
t("statusPage.contents", "Contents"),
]}
isCreate={isCreate}
handleDelete={handleDelete}
isDeleteOpen={isDeleteOpen}
setIsDeleteOpen={setIsDeleteOpen}
isDeleting={isDeleting}
isLoading={statusPageIsLoading}
/>
<Stack
direction="row"
@@ -11,7 +11,7 @@ import { useLocation } from "react-router-dom";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next";
const Controls = ({ isDeleteOpen, setIsDeleteOpen, isDeleting, url, type }) => {
const Controls = ({ url, type }) => {
const theme = useTheme();
const { t } = useTranslation();
const location = useLocation();
@@ -27,16 +27,6 @@ const Controls = ({ isDeleteOpen, setIsDeleteOpen, isDeleting, url, type }) => {
direction="row"
gap={theme.spacing(2)}
>
<Box>
<Button
variant="contained"
color="error"
onClick={() => setIsDeleteOpen(!isDeleteOpen)}
loading={isDeleting}
>
{t("delete")}
</Button>
</Box>
<Box>
<Button
variant="contained"
@@ -65,21 +55,10 @@ const Controls = ({ isDeleteOpen, setIsDeleteOpen, isDeleting, url, type }) => {
Controls.propTypes = {
type: PropTypes.string,
isDeleting: PropTypes.bool,
url: PropTypes.string,
isDeleteOpen: PropTypes.bool.isRequired,
setIsDeleteOpen: PropTypes.func.isRequired,
};
const ControlsHeader = ({
statusPage,
isPublic,
isDeleting,
isDeleteOpen,
setIsDeleteOpen,
url,
type = "uptime",
}) => {
const ControlsHeader = ({ statusPage, isPublic, url, type = "uptime" }) => {
const theme = useTheme();
const { t } = useTranslation();
const publicUrl = `/status/uptime/public/${url}`;
@@ -137,9 +116,6 @@ const ControlsHeader = ({
)}
</Stack>
<Controls
isDeleting={isDeleting}
isDeleteOpen={isDeleteOpen}
setIsDeleteOpen={setIsDeleteOpen}
url={url}
type={type}
/>
@@ -152,9 +128,6 @@ ControlsHeader.propTypes = {
url: PropTypes.string,
statusPage: PropTypes.object,
isPublic: PropTypes.bool,
isDeleting: PropTypes.bool,
isDeleteOpen: PropTypes.bool.isRequired,
setIsDeleteOpen: PropTypes.func.isRequired,
type: PropTypes.string,
};
@@ -1,19 +1,35 @@
import { useSelector } from "react-redux";
import { useState } from "react";
import { networkService } from "../../../../main";
import { networkService } from "../../../../Utils/NetworkService";
import { createToast } from "../../../../Utils/toastUtils";
import { useTranslation } from "react-i18next";
/**
* Hook for deleting a status page with optimistic UI update
* @param {Function} fetchStatusPage - Function to fetch status page data
* @param {string} url - URL of the status page
* @returns {Array} - [deleteStatusPage function, isLoading state]
*/
const useStatusPageDelete = (fetchStatusPage, url) => {
const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation();
/**
* Delete a status page with optimistic UI update
* @returns {Promise<boolean>} - Success status
*/
const deleteStatusPage = async () => {
// We don't need to call fetchStatusPage after deletion
// This prevents the 404 error when trying to fetch a deleted status page
try {
setIsLoading(true);
await networkService.deleteStatusPage({ url });
fetchStatusPage?.();
createToast({
body: t("statusPage.deleteSuccess", "Status page deleted successfully"),
});
return true;
} catch (error) {
createToast({
body: error.message,
body: t("statusPage.deleteFailed", "Failed to delete status page"),
});
return false;
} finally {
+1 -28
View File
@@ -6,7 +6,6 @@ import ControlsHeader from "./Components/ControlsHeader";
import SkeletonLayout from "./Components/Skeleton";
import StatusBar from "./Components/StatusBar";
import MonitorsList from "./Components/MonitorsList";
import Dialog from "../../../Components/Dialog";
import Breadcrumbs from "../../../Components/Breadcrumbs/index.jsx";
import TextLink from "../../../Components/TextLink";
@@ -15,27 +14,19 @@ import { useStatusPageFetch } from "./Hooks/useStatusPageFetch";
import { useTheme } from "@emotion/react";
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
import { useLocation } from "react-router-dom";
import { useStatusPageDelete } from "./Hooks/useStatusPageDelete";
import { useState } from "react";
import { useParams } from "react-router-dom";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
const PublicStatus = () => {
const { url } = useParams();
// Local state
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
// Utils
const theme = useTheme();
const { t } = useTranslation();
const location = useLocation();
const navigate = useNavigate();
const isAdmin = useIsAdmin();
const [statusPage, monitors, isLoading, networkError, fetchStatusPage] =
useStatusPageFetch(false, url);
const [deleteStatusPage, isDeleting] = useStatusPageDelete(fetchStatusPage, url);
const [statusPage, monitors, isLoading, networkError] = useStatusPageFetch(false, url);
// Breadcrumbs
const crumbs = [
@@ -158,9 +149,6 @@ const PublicStatus = () => {
{!isPublic && <Breadcrumbs list={crumbs} />}
<ControlsHeader
statusPage={statusPage}
isDeleting={isDeleting}
isDeleteOpen={isDeleteOpen}
setIsDeleteOpen={setIsDeleteOpen}
url={url}
isPublic={isPublic}
/>
@@ -168,21 +156,6 @@ const PublicStatus = () => {
<StatusBar monitors={monitors} />
<MonitorsList monitors={monitors} />
{link}
<Dialog
title={t("deleteStatusPage")}
onConfirm={() => {
deleteStatusPage();
setIsDeleteOpen(false);
navigate("/status");
}}
onCancel={() => {
setIsDeleteOpen(false);
}}
open={isDeleteOpen}
confirmationButtonLabel={t("deleteStatusPageConfirm")}
description={t("deleteStatusPageDescription")}
isLoading={isDeleting || isLoading}
/>
</Stack>
);
};
@@ -19,21 +19,21 @@ const StatusBox = ({ title, value, status }) => {
if (status === "up") {
color = theme.palette.success.lowContrast;
icon = (
<Box sx={{ ...sharedStyles, top: 8 }}>
<Box sx={{ ...sharedStyles, top: theme.spacing(4) }}>
<Arrow />
</Box>
);
} else if (status === "down") {
color = theme.palette.error.lowContrast;
icon = (
<Box sx={{ ...sharedStyles, transform: "rotate(180deg)", top: 5 }}>
<Box sx={{ ...sharedStyles, transform: "rotate(180deg)", top: theme.spacing(2) }}>
<Arrow />
</Box>
);
} else if (status === "paused") {
color = theme.palette.warning.lowContrast;
icon = (
<Box sx={{ ...sharedStyles, top: 12, right: 12 }}>
<Box sx={{ ...sharedStyles, top: theme.spacing(6), right: theme.spacing(6) }}>
<ClockSnooze />
</Box>
);
@@ -75,15 +75,15 @@ const StatusBox = ({ title, value, status }) => {
<Stack
direction="row"
alignItems="flex-start"
fontSize={36}
fontSize={theme.typography.h1.fontSize}
fontWeight={600}
color={color}
gap="2px"
gap={theme.spacing(1)}
>
{value}
<Typography
fontSize={20}
fontSize={theme.typography.label.fontSize}
fontWeight={300}
color={theme.palette.primary.contrastTextSecondary}
sx={{
+18
View File
@@ -601,6 +601,18 @@ class NetworkService {
return this.axiosInstance.get(`/checks/team?${params.toString()}`);
};
/**
* ************************************
* Get summary of checks by team ID
* ************************************
*
* @async
* @returns {Promise<AxiosResponse>} The response from the axios GET request.
*/
getChecksAndSummaryByTeamId = async () => {
return this.axiosInstance.get(`/checks/team/summary`);
};
/**
* ************************************
* Update the status of a check
@@ -619,6 +631,12 @@ class NetworkService {
});
}
async updateAllChecksStatus(config) {
return this.axiosInstance.put(`/checks/team/`, {
ack: config.ack,
});
}
/**
* ************************************
* Get all checks for a given user
+1
View File
@@ -690,6 +690,7 @@
},
"advancedMatching": "",
"sendTestNotifications": "",
"testNotificationsDisabled": "",
"selectAll": "",
"showAdminLoginLink": "",
"logsPage": {
+1
View File
@@ -690,6 +690,7 @@
},
"advancedMatching": "",
"sendTestNotifications": "",
"testNotificationsDisabled": "",
"selectAll": "",
"showAdminLoginLink": "",
"logsPage": {
+1
View File
@@ -690,6 +690,7 @@
},
"advancedMatching": "",
"sendTestNotifications": "",
"testNotificationsDisabled": "",
"selectAll": "",
"showAdminLoginLink": "",
"logsPage": {
+728 -692
View File
File diff suppressed because it is too large. Load diff
+1
View File
@@ -690,6 +690,7 @@
},
"advancedMatching": "",
"sendTestNotifications": "",
"testNotificationsDisabled": "",
"selectAll": "",
"showAdminLoginLink": "",
"logsPage": {
+1
View File
@@ -690,6 +690,7 @@
},
"advancedMatching": "",
"sendTestNotifications": "",
"testNotificationsDisabled": "",
"selectAll": "",
"showAdminLoginLink": "",
"logsPage": {
+1
View File
@@ -690,6 +690,7 @@
},
"advancedMatching": "",
"sendTestNotifications": "",
"testNotificationsDisabled": "",
"selectAll": "",
"showAdminLoginLink": "",
"logsPage": {
+1
View File
@@ -690,6 +690,7 @@
},
"advancedMatching": "",
"sendTestNotifications": "",
"testNotificationsDisabled": "",
"selectAll": "",
"showAdminLoginLink": "",
"logsPage": {
+1
View File
@@ -690,6 +690,7 @@
},
"advancedMatching": "",
"sendTestNotifications": "",
"testNotificationsDisabled": "",
"selectAll": "",
"showAdminLoginLink": "",
"logsPage": {
+1
View File
@@ -690,6 +690,7 @@
},
"advancedMatching": "",
"sendTestNotifications": "Bildirimleri dene",
"testNotificationsDisabled": "",
"selectAll": "Tümünü seç",
"showAdminLoginLink": "",
"logsPage": {
+1
View File
@@ -690,6 +690,7 @@
},
"advancedMatching": "",
"sendTestNotifications": "",
"testNotificationsDisabled": "",
"selectAll": "",
"showAdminLoginLink": "",
"logsPage": {
+1 -1
View File
@@ -5,7 +5,7 @@ import { execSync } from "child_process";
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), "");
let version = 2.2;
let version = 2.3;
return {
base: "/",
+13
View File
@@ -111,6 +111,19 @@ class CheckController {
}
};
getChecksSummaryByTeamId = async (req, res, next) => {
try {
const { teamId } = req.user;
const summary = await this.db.getChecksSummaryByTeamId({ teamId });
return res.success({
msg: this.stringService.checkGetSummary,
data: summary,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getChecksSummaryByTeamId"));
}
};
ackCheck = async (req, res, next) => {
try {
await ackCheckBodyValidation.validateAsync(req.body);
+25
View File
@@ -5,6 +5,7 @@ import PageSpeedCheck from "../../models/PageSpeedCheck.js";
import User from "../../models/User.js";
import logger from "../../../utils/logger.js";
import { ObjectId } from "mongodb";
import { buildChecksSummaryByTeamIdPipeline } from "./checkModuleQueries.js";
const SERVICE_NAME = "checkModule";
const dateRangeLookup = {
@@ -304,6 +305,29 @@ const ackAllChecks = async (monitorId, teamId, ack, path) => {
}
};
/**
* Get checks and summary by team ID
* @async
* @param {string} teamId
* @returns {Promise<Object>}
* @throws {Error}
*/
const getChecksSummaryByTeamId = async ({ teamId }) => {
try {
const matchStage = {
teamId: new ObjectId(teamId),
};
const checks = await Check.aggregate(
buildChecksSummaryByTeamIdPipeline({ matchStage })
);
return checks[0].summary;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getChecksSummaryByTeamId";
throw error;
}
};
/**
* Delete all checks for a monitor
* @async
@@ -387,6 +411,7 @@ export {
getChecksByTeam,
ackCheck,
ackAllChecks,
getChecksSummaryByTeamId,
deleteChecks,
deleteChecksByTeamId,
updateChecksTTL,
@@ -0,0 +1,52 @@
const buildChecksSummaryByTeamIdPipeline = ({ matchStage }) => {
return [
{ $match: matchStage },
{
$facet: {
summary: [
{
$group: {
_id: null,
totalChecks: { $sum: { $cond: [{ $eq: ["$status", false] }, 1, 0] } },
resolvedChecks: {
$sum: {
$cond: [
{ $and: [{ $eq: ["$ack", true] }, { $eq: ["$status", false] }] },
1,
0,
],
},
},
downChecks: {
$sum: {
$cond: [
{ $and: [{ $eq: ["$ack", false] }, { $eq: ["$status", false] }] },
1,
0,
],
},
},
cannotResolveChecks: {
$sum: {
$cond: [{ $eq: ["$statusCode", 5000] }, 1, 0],
},
},
},
},
{
$project: {
_id: 0,
},
},
],
},
},
{
$project: {
summary: { $arrayElemAt: ["$summary", 0] },
},
},
];
};
export { buildChecksSummaryByTeamIdPipeline };
+1
View File
@@ -14,6 +14,7 @@ class CheckRoutes {
initRoutes() {
this.router.get("/team", this.checkController.getChecksByTeam);
this.router.get("/team/summary", this.checkController.getChecksSummaryByTeamId);
this.router.delete(
"/team",
isAllowed(["admin", "superadmin"]),