mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-01-24 02:29:35 -06:00
Merge branch 'develop' into fix/2471-infrastructure-action-menu
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
116
client/src/Pages/Incidents/Components/StatusBoxes/StatusBox.jsx
Normal file
116
client/src/Pages/Incidents/Components/StatusBoxes/StatusBox.jsx
Normal file
@@ -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;
|
||||
46
client/src/Pages/Incidents/Components/StatusBoxes/index.jsx
Normal file
46
client/src/Pages/Incidents/Components/StatusBoxes/index.jsx
Normal file
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,13 +54,13 @@ 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
|
||||
@@ -124,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,
|
||||
@@ -137,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}`);
|
||||
}
|
||||
@@ -162,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;
|
||||
}
|
||||
|
||||
@@ -223,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}
|
||||
@@ -235,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 {
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -690,6 +690,7 @@
|
||||
},
|
||||
"advancedMatching": "",
|
||||
"sendTestNotifications": "",
|
||||
"testNotificationsDisabled": "",
|
||||
"selectAll": "",
|
||||
"showAdminLoginLink": "",
|
||||
"logsPage": {
|
||||
|
||||
@@ -690,6 +690,7 @@
|
||||
},
|
||||
"advancedMatching": "",
|
||||
"sendTestNotifications": "",
|
||||
"testNotificationsDisabled": "",
|
||||
"selectAll": "",
|
||||
"showAdminLoginLink": "",
|
||||
"logsPage": {
|
||||
|
||||
@@ -690,6 +690,7 @@
|
||||
},
|
||||
"advancedMatching": "",
|
||||
"sendTestNotifications": "",
|
||||
"testNotificationsDisabled": "",
|
||||
"selectAll": "",
|
||||
"showAdminLoginLink": "",
|
||||
"logsPage": {
|
||||
|
||||
@@ -197,12 +197,14 @@
|
||||
"basicInformation": "Basic Information",
|
||||
"bulkImport": {
|
||||
"fallbackPage": "Import a file to upload a list of servers in bulk",
|
||||
"invalidFileType": "Invalid file type",
|
||||
"noFileSelected": "No file selected",
|
||||
"parsingFailed": "Parsing failed",
|
||||
"selectFile": "Select File",
|
||||
"selectFileDescription": "You can download our <template>template</template> or <sample>sample</sample>",
|
||||
"selectFileTips": "Select CSV file to upload",
|
||||
"title": "Bulk Import",
|
||||
"uploadFailed": "Upload failed",
|
||||
"uploadSuccess": "Monitors created successfully!",
|
||||
"validationFailed": "Validation failed"
|
||||
},
|
||||
@@ -226,6 +228,7 @@
|
||||
"commonSaving": "Saving...",
|
||||
"companyName": "Company name",
|
||||
"configure": "Configure",
|
||||
"confirmPassword": "Confirm password",
|
||||
"cores": "Cores",
|
||||
"country": "COUNTRY",
|
||||
"cpu": "CPU",
|
||||
@@ -385,8 +388,12 @@
|
||||
"incidentsOptionsHeader": "Incidents for:",
|
||||
"incidentsOptionsHeaderFilterAll": "All",
|
||||
"incidentsOptionsHeaderFilterBy": "Filter by:",
|
||||
"incidentsOptionsHeaderFilterCannotResolve": "Cannot resolve",
|
||||
"incidentsOptionsHeaderTotalIncidents": "Total Incidents",
|
||||
"incidentsOptionsHeaderFilterCannotResolve": "Cannot Resolve",
|
||||
"incidentsTableResolvedAt": "Resolved at",
|
||||
"incidentsOptionsHeaderFilterDown": "Down",
|
||||
"incidentsTableActionResolve": "Resolve",
|
||||
"incidentsPageActionResolve": "Resolve all incidents",
|
||||
"incidentsOptionsHeaderFilterResolved": "Resolved",
|
||||
"incidentsOptionsHeaderLastDay": "Last day",
|
||||
"incidentsOptionsHeaderLastHour": "Last hour",
|
||||
@@ -491,6 +498,11 @@
|
||||
"title": "Export/Import"
|
||||
},
|
||||
"monitorDisplayName": "Monitor display name",
|
||||
"monitorHooks": {
|
||||
"failureAddDemoMonitors": "Failed to add demo monitors",
|
||||
"successAddDemoMonitors": "Successfully added demo monitors"
|
||||
},
|
||||
|
||||
"monitorState": {
|
||||
"active": "Active",
|
||||
"paused": "Paused",
|
||||
@@ -507,6 +519,10 @@
|
||||
"monitorStatusUp": "Monitor {name} ({url}) is now UP and responding",
|
||||
"monitors": "monitors",
|
||||
"monitorsToApply": "Monitors to apply maintenance window to",
|
||||
"checkHooks": {
|
||||
"failureResolveOne": "Failed to resolve incident.",
|
||||
"failureResolveAll": "Failed to resolve all incidents."
|
||||
},
|
||||
"ms": "ms",
|
||||
"navControls": "Controls",
|
||||
"nextWindow": "Next window",
|
||||
@@ -664,12 +680,14 @@
|
||||
"selectAll": "Select all",
|
||||
"sendTestNotifications": "Send test notifications",
|
||||
"seperateEmails": "You can separate multiple emails with a comma",
|
||||
"settingsAppearance": "Appearance",
|
||||
"settingsDisabled": "Disabled",
|
||||
"settingsDisplayTimezone": "Display timezone",
|
||||
"settingsEmailUser": "Email user - Username for authentication, overrides email address if specified",
|
||||
"settingsFailedToAddDemoMonitors": "Failed to add demo monitors",
|
||||
"settingsFailedToClearStats": "Failed to clear stats",
|
||||
"settingsFailedToDeleteMonitors": "Failed to delete all monitors",
|
||||
"settingsFailedToSave": "Failed to save settings",
|
||||
"settingsGeneralSettings": "General settings",
|
||||
"settingsMonitorsDeleted": "Successfully deleted all monitors",
|
||||
"settingsPage": {
|
||||
"aboutSettings": {
|
||||
@@ -701,7 +719,8 @@
|
||||
"labelUser": "Email user - Username for authentication, overrides email address if specified",
|
||||
"linkTransport": "See specifications here",
|
||||
"placeholderUser": "Leave empty if not required",
|
||||
"title": "Email"
|
||||
"title": "Email",
|
||||
"toastEmailRequiredFieldsError": "Email address, host, port and password are required"
|
||||
},
|
||||
"pageSpeedSettings": {
|
||||
"description": "Enter your Google PageSpeed API key to enable Google PageSpeed monitoring. Click Reset to update the key.",
|
||||
@@ -793,6 +812,14 @@
|
||||
"statusPageStatusNoPage": "There's no status page here.",
|
||||
"statusPageStatusNotPublic": "This status page is not public.",
|
||||
"statusPageStatusServiceStatus": "Service status",
|
||||
"statusPage": {
|
||||
"deleteSuccess": "Status page deleted successfully",
|
||||
"deleteFailed": "Failed to delete status page",
|
||||
"createSuccess": "Status page created successfully",
|
||||
"updateSuccess": "Status page updated successfully",
|
||||
"generalSettings": "General settings",
|
||||
"contents": "Contents"
|
||||
},
|
||||
"submit": "Submit",
|
||||
"teamPanel": {
|
||||
"cancel": "Cancel",
|
||||
@@ -818,6 +845,7 @@
|
||||
"teamMembers": "Team members"
|
||||
},
|
||||
"testLocale": "testLocale",
|
||||
"testNotificationsDisabled": "There are no notifications setup for this monitor. You need to add one by clicking 'Configure' button",
|
||||
"timeZoneInfo": "All dates and times are in GMT+0 time zone.",
|
||||
"timezone": "Timezone",
|
||||
"title": "Title",
|
||||
|
||||
@@ -690,6 +690,7 @@
|
||||
},
|
||||
"advancedMatching": "",
|
||||
"sendTestNotifications": "",
|
||||
"testNotificationsDisabled": "",
|
||||
"selectAll": "",
|
||||
"showAdminLoginLink": "",
|
||||
"logsPage": {
|
||||
|
||||
@@ -690,6 +690,7 @@
|
||||
},
|
||||
"advancedMatching": "",
|
||||
"sendTestNotifications": "",
|
||||
"testNotificationsDisabled": "",
|
||||
"selectAll": "",
|
||||
"showAdminLoginLink": "",
|
||||
"logsPage": {
|
||||
|
||||
@@ -690,6 +690,7 @@
|
||||
},
|
||||
"advancedMatching": "",
|
||||
"sendTestNotifications": "",
|
||||
"testNotificationsDisabled": "",
|
||||
"selectAll": "",
|
||||
"showAdminLoginLink": "",
|
||||
"logsPage": {
|
||||
|
||||
@@ -690,6 +690,7 @@
|
||||
},
|
||||
"advancedMatching": "",
|
||||
"sendTestNotifications": "",
|
||||
"testNotificationsDisabled": "",
|
||||
"selectAll": "",
|
||||
"showAdminLoginLink": "",
|
||||
"logsPage": {
|
||||
|
||||
@@ -690,6 +690,7 @@
|
||||
},
|
||||
"advancedMatching": "",
|
||||
"sendTestNotifications": "",
|
||||
"testNotificationsDisabled": "",
|
||||
"selectAll": "",
|
||||
"showAdminLoginLink": "",
|
||||
"logsPage": {
|
||||
|
||||
@@ -690,6 +690,7 @@
|
||||
},
|
||||
"advancedMatching": "",
|
||||
"sendTestNotifications": "Bildirimleri dene",
|
||||
"testNotificationsDisabled": "",
|
||||
"selectAll": "Tümünü seç",
|
||||
"showAdminLoginLink": "",
|
||||
"logsPage": {
|
||||
|
||||
@@ -690,6 +690,7 @@
|
||||
},
|
||||
"advancedMatching": "",
|
||||
"sendTestNotifications": "",
|
||||
"testNotificationsDisabled": "",
|
||||
"selectAll": "",
|
||||
"showAdminLoginLink": "",
|
||||
"logsPage": {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
52
server/db/mongo/modules/checkModuleQueries.js
Normal file
52
server/db/mongo/modules/checkModuleQueries.js
Normal file
@@ -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 };
|
||||
@@ -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"]),
|
||||
|
||||
Reference in New Issue
Block a user