Merge remote-tracking branch 'upstream/develop' into feat/be/webhook-integrations

This commit is contained in:
Skorpios committed 2025-02-16 18:48:00 -08:00
commit 07882e045b
63 files changed
+13694 -1342

No files matched your search

+12458 -670
View File
File diff suppressed because it is too large. Load diff
+7 -1
View File
@@ -22,6 +22,12 @@
"@mui/x-data-grid": "7.26.0",
"@mui/x-date-pickers": "7.26.0",
"@reduxjs/toolkit": "2.5.1",
"@solana/wallet-adapter-base": "0.9.23",
"@solana/wallet-adapter-material-ui": "0.16.34",
"@solana/wallet-adapter-react": "0.15.35",
"@solana/wallet-adapter-react-ui": "0.9.35",
"@solana/wallet-adapter-wallets": "0.19.32",
"@solana/web3.js": "1.98.0",
"axios": "^1.7.4",
"dayjs": "1.11.13",
"flag-icons": "7.3.2",
@@ -31,7 +37,7 @@
"jwt-decode": "^4.0.0",
"maplibre-gl": "5.1.0",
"mui-color-input": "^5.0.1",
"react": "^18.2.0",
"react": "18.3.1",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.2.0",
+16 -13
View File
@@ -11,6 +11,7 @@ import { getAppSettings } from "./Features/Settings/settingsSlice";
import { logger } from "./Utils/Logger"; // Import the logger
import { networkService } from "./main";
import { Routes } from "./Routes";
import WalletProvider from "./Components/WalletProvider";
function App() {
const mode = useSelector((state) => state.ui.mode);
@@ -34,19 +35,21 @@ function App() {
return (
/* Extract Themeprovider, baseline and global styles to Styles */
<ThemeProvider theme={mode === "light" ? lightTheme : darkTheme}>
<CssBaseline />
<GlobalStyles
styles={({ palette }) => {
return {
body: {
backgroundImage: `radial-gradient(circle, ${palette.gradient.color1}, ${palette.gradient.color2}, ${palette.gradient.color3}, ${palette.gradient.color4}, ${palette.gradient.color5})`,
color: palette.primary.contrastText,
},
};
}}
/>
<Routes />
<ToastContainer />
<WalletProvider>
<CssBaseline />
<GlobalStyles
styles={({ palette }) => {
return {
body: {
backgroundImage: `radial-gradient(circle, ${palette.gradient.color1}, ${palette.gradient.color2}, ${palette.gradient.color3}, ${palette.gradient.color4}, ${palette.gradient.color5})`,
color: palette.primary.contrastText,
},
};
}}
/>
<Routes />
<ToastContainer />
</WalletProvider>
</ThemeProvider>
);
}
+18 -1
View File
@@ -183,6 +183,7 @@ function Sidebar() {
This is the top lever for styles
*/
sx={{
position: 'relative',
border: 1,
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.shape.borderRadius,
@@ -302,6 +303,20 @@ function Sidebar() {
</Typography>
</Stack>
</Stack>
<Box
sx={{
flexGrow: 1,
overflow: 'auto',
overflowX: 'hidden',
'&::-webkit-scrollbar': {
width: theme.spacing(2),
},
'&::-webkit-scrollbar-thumb': {
backgroundColor: theme.palette.primary.lowContrast,
borderRadius: theme.spacing(4),
},
}}
>
<List
component="nav"
aria-labelledby="nested-menu-subheader"
@@ -315,6 +330,7 @@ function Sidebar() {
/* TODO px should be centralized in container */
px: collapsed ? theme.spacing(2) : theme.spacing(4),
backgroundColor: "transparent",
position: "static",
}}
>
Menu
@@ -566,7 +582,8 @@ function Sidebar() {
);
})}
</List>
<Divider sx={{ mt: "auto" }} />
</Box>
<Divider sx={{ mt: "auto", borderColor: theme.palette.primary.lowContrast }} />
<Stack
direction="row"
@@ -1,8 +1,7 @@
import TabPanel from "@mui/lab/TabPanel";
import { useState } from "react";
import { useTheme } from "@emotion/react";
import { Box, Stack, Typography } from "@mui/material";
import LoadingButton from "@mui/lab/LoadingButton";
import { Box, Stack, Typography, Button } from "@mui/material";
import { PasswordEndAdornment } from "../../Inputs/TextInput/Adornments";
import TextInput from "../../Inputs/TextInput";
import { credentials } from "../../../Validation/validation";
@@ -226,7 +225,7 @@ const PasswordPanel = () => {
direction="row"
justifyContent="flex-end"
>
<LoadingButton
<Button
variant="contained"
color="accent"
type="submit"
@@ -242,7 +241,7 @@ const PasswordPanel = () => {
}}
>
Save
</LoadingButton>
</Button>
</Stack>
</Stack>
</TabPanel>
@@ -14,7 +14,6 @@ import { formatBytes } from "../../../Utils/fileUtils";
import { clearUptimeMonitorState } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
import { createToast } from "../../../Utils/toastUtils";
import { logger } from "../../../Utils/Logger";
import LoadingButton from "@mui/lab/LoadingButton";
import { GenericDialog } from "../../Dialog/genericDialog";
import Dialog from "../../Dialog";
@@ -344,7 +343,7 @@ const ProfilePanel = () => {
justifyContent="flex-end"
>
<Box width="fit-content">
<LoadingButton
<Button
variant="contained"
color="accent"
onClick={handleSaveProfile}
@@ -354,7 +353,7 @@ const ProfilePanel = () => {
sx={{ px: theme.spacing(12) }}
>
Save
</LoadingButton>
</Button>
</Box>
</Stack>
</Stack>
@@ -8,7 +8,6 @@ import { networkService } from "../../../main";
import { createToast } from "../../../Utils/toastUtils";
import { useSelector } from "react-redux";
import Select from "../../Inputs/Select";
import LoadingButton from "@mui/lab/LoadingButton";
import { GenericDialog } from "../../Dialog/genericDialog";
import DataTable from "../../Table/";
/**
@@ -229,14 +228,13 @@ const TeamPanel = () => {
</Button>
</ButtonGroup>
</Stack>
<LoadingButton
loading={isSendingInvite}
<Button
variant="contained"
color="accent"
onClick={() => setIsOpen(true)}
>
Invite a team member
</LoadingButton>
</Button>
</Stack>
<DataTable
@@ -287,15 +285,15 @@ const TeamPanel = () => {
mt={theme.spacing(8)}
justifyContent="flex-end"
>
<LoadingButton
<Button
loading={isSendingInvite}
variant="contained" // CAIO_REVIEW
color="error"
onClick={closeInviteModal}
>
Cancel
</LoadingButton>
<LoadingButton
</Button>
<Button
variant="contained"
color="accent"
onClick={handleInviteMember}
@@ -303,7 +301,7 @@ const TeamPanel = () => {
disabled={isDisabled}
>
Send invite
</LoadingButton>
</Button>
</Stack>
</GenericDialog>
</TabPanel>
@@ -0,0 +1,45 @@
import { useMemo } from "react";
import { ConnectionProvider, WalletProvider } from "@solana/wallet-adapter-react";
import { WalletAdapterNetwork } from "@solana/wallet-adapter-base";
import {
UnsafeBurnerWalletAdapter,
PhantomWalletAdapter,
} from "@solana/wallet-adapter-wallets";
import { WalletModalProvider } from "@solana/wallet-adapter-react-ui";
import { clusterApiUrl } from "@solana/web3.js";
import PropTypes from "prop-types";
// Default styles that can be overridden by your app
import "@solana/wallet-adapter-react-ui/styles.css";
export const Wallet = ({ children }) => {
// The network can be set to 'devnet', 'testnet', or 'mainnet-beta'.
const network = WalletAdapterNetwork.Mainnet;
// You can also provide a custom RPC endpoint.
const endpoint = useMemo(() => clusterApiUrl(network), [network]);
const wallets = useMemo(
() => [new PhantomWalletAdapter()],
// eslint-disable-next-line react-hooks/exhaustive-deps
[network]
);
return (
<ConnectionProvider endpoint={endpoint}>
<WalletProvider
wallets={wallets}
autoConnect
>
<WalletModalProvider>{children}</WalletModalProvider>
</WalletProvider>
</ConnectionProvider>
);
};
Wallet.propTypes = {
children: PropTypes.node,
};
export default Wallet;
@@ -88,11 +88,14 @@ export const updateUptimeMonitor = createAsyncThunk(
description: monitor.description,
interval: monitor.interval,
notifications: monitor.notifications,
matchMethod: monitor.matchMethod,
expectedValue: monitor.expectedValue,
jsonPath: monitor.jsonPath,
};
const res = await networkService.updateMonitor({
authToken: authToken,
monitorId: monitor._id,
updatedFields: updatedFields,
monitor,
});
return res.data;
} catch (error) {
+3 -4
View File
@@ -1,4 +1,4 @@
import { Box, Stack, Typography } from "@mui/material";
import { Box, Stack, Typography, Button } from "@mui/material";
import { useTheme } from "@emotion/react";
import { createToast } from "../../Utils/toastUtils";
import { useDispatch, useSelector } from "react-redux";
@@ -10,7 +10,6 @@ import TextInput from "../../Components/Inputs/TextInput";
import Logo from "../../assets/icons/checkmate-icon.svg?react";
import Key from "../../assets/icons/key.svg?react";
import Background from "../../assets/Images/background-grid.svg?react";
import LoadingButton from "@mui/lab/LoadingButton";
import IconBox from "../../Components/IconBox";
import "./index.css";
@@ -185,7 +184,7 @@ const ForgotPassword = () => {
error={errors.email ? true : false}
helperText={errors.email}
/>
<LoadingButton
<Button
variant="contained"
color="accent"
loading={isLoading}
@@ -197,7 +196,7 @@ const ForgotPassword = () => {
}}
>
Send instructions
</LoadingButton>
</Button>
</Box>
</Stack>
</Stack>
@@ -1,7 +1,6 @@
import { useRef, useEffect } from "react";
import { Box, Button, Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import LoadingButton from "@mui/lab/LoadingButton";
import { useSelector } from "react-redux";
import TextInput from "../../../../Components/Inputs/TextInput";
import { PasswordEndAdornment } from "../../../../Components/Inputs/TextInput/Adornments";
@@ -90,7 +89,7 @@ const PasswordStep = ({ form, errors, onSubmit, onChange, onBack }) => {
<ArrowBackRoundedIcon />
{t("commonBack")}{" "}
</Button>
<LoadingButton
<Button
variant="contained"
color="accent"
type="submit"
@@ -106,7 +105,7 @@ const PasswordStep = ({ form, errors, onSubmit, onChange, onBack }) => {
}}
>
{t("continue")}
</LoadingButton>
</Button>
</Stack>
</Box>
</Stack>
+3 -4
View File
@@ -3,8 +3,7 @@ import { useDispatch, useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import { useParams } from "react-router-dom";
import { useTheme } from "@emotion/react";
import { Box, Stack, Typography } from "@mui/material";
import LoadingButton from "@mui/lab/LoadingButton";
import { Box, Stack, Typography, Button } from "@mui/material";
import { setNewPassword } from "../../Features/Auth/authSlice";
import { createToast } from "../../Utils/toastUtils";
import { credentials } from "../../Validation/validation";
@@ -232,7 +231,7 @@ const SetNewPassword = () => {
/>
</Stack>
</Box>
<LoadingButton
<Button
variant="contained"
color="accent"
loading={isLoading}
@@ -245,7 +244,7 @@ const SetNewPassword = () => {
sx={{ width: "100%", maxWidth: 400 }}
>
{t("authSetNewPasswordResetPassword")}
</LoadingButton>
</Button>
</Stack>
</Stack>
<Box
@@ -0,0 +1,33 @@
import { useState } from "react";
import { networkService } from "../../../../main";
import { useSelector } from "react-redux";
import { createToast } from "../../../../Utils/toastUtils";
const useCreateDistributedUptimeMonitor = ({ isCreate, monitorId }) => {
const { authToken, user } = useSelector((state) => state.auth);
const [isLoading, setIsLoading] = useState(false);
const [networkError, setNetworkError] = useState(false);
const createDistributedUptimeMonitor = async ({ form }) => {
setIsLoading(true);
try {
if (isCreate) {
await networkService.createMonitor({ authToken, monitor: form });
} else {
await networkService.updateMonitor({ authToken, monitor: form, monitorId });
}
return true;
} catch (error) {
setNetworkError(true);
createToast({ body: error?.response?.data?.msg ?? error.message });
return false;
} finally {
setIsLoading(false);
}
};
return [createDistributedUptimeMonitor, isLoading, networkError];
};
export { useCreateDistributedUptimeMonitor };
@@ -0,0 +1,32 @@
import { useEffect, useState } from "react";
import { networkService } from "../../../../main";
import { createToast } from "../../../../Utils/toastUtils";
export const useMonitorFetch = ({ authToken, monitorId, isCreate }) => {
const [networkError, setNetworkError] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [monitor, setMonitor] = useState(undefined);
useEffect(() => {
const fetchMonitors = async () => {
try {
if (isCreate) return;
const res = await networkService.getUptimeDetailsById({
authToken: authToken,
monitorId: monitorId,
normalize: true,
});
setMonitor(res?.data?.data ?? {});
} catch (error) {
setNetworkError(true);
createToast({ body: error.message });
} finally {
setIsLoading(false);
}
};
fetchMonitors();
}, [authToken, monitorId, isCreate]);
return [monitor, isLoading, networkError];
};
export default useMonitorFetch;
@@ -1,6 +1,5 @@
// Components
import { Box, Stack, Typography, Button, ButtonGroup } from "@mui/material";
import LoadingButton from "@mui/lab/LoadingButton";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import ConfigBox from "../../../Components/ConfigBox";
@@ -13,18 +12,15 @@ import { createToast } from "../../../Utils/toastUtils";
// Utility
import { useTheme } from "@emotion/react";
import { useState } from "react";
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useSelector, useDispatch } from "react-redux";
import { useSelector } from "react-redux";
import { monitorValidation } from "../../../Validation/validation";
import { createUptimeMonitor } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
import { useLocation } from "react-router-dom";
import { useParams } from "react-router-dom";
import { useCreateDistributedUptimeMonitor } from "./Hooks/useCreateDistributedUptimeMonitor";
import { useMonitorFetch } from "./Hooks/useMonitorFetch";
// Constants
const BREADCRUMBS = [
{ name: `distributed uptime`, path: "/distributed-uptime" },
{ name: "create", path: `/distributed-uptime/create` },
];
const MS_PER_MINUTE = 60000;
const SELECT_VALUES = [
{ _id: 1, name: "1 minute" },
@@ -34,18 +30,30 @@ const SELECT_VALUES = [
{ _id: 5, name: "5 minutes" },
];
const parseUrl = (url) => {
try {
return new URL(url);
} catch (error) {
return null;
}
};
const CreateDistributedUptime = () => {
const location = useLocation();
const isCreate = location.pathname.startsWith("/distributed-uptime/create");
const { monitorId } = useParams();
const isCreate = typeof monitorId === "undefined";
const BREADCRUMBS = [
{ name: `distributed uptime`, path: "/distributed-uptime" },
{ name: isCreate ? "create" : "configure", path: `` },
];
// Redux state
const { user, authToken } = useSelector((state) => state.auth);
const isLoading = useSelector((state) => state.uptimeMonitors.isLoading);
// Local state
const [https, setHttps] = useState(true);
const [notifications, setNotifications] = useState([]);
const [monitor, setMonitor] = useState({
const [form, setForm] = useState({
type: "distributed_http",
name: "",
url: "",
@@ -55,12 +63,37 @@ const CreateDistributedUptime = () => {
//utils
const theme = useTheme();
const dispatch = useDispatch();
const navigate = useNavigate();
const [createDistributedUptimeMonitor, isLoading, networkError] =
useCreateDistributedUptimeMonitor({ isCreate, monitorId });
const [monitor, monitorIsLoading, monitorNetworkError] = useMonitorFetch({
authToken,
monitorId,
isCreate,
});
// Effect to set monitor to fetched monitor
useEffect(() => {
if (typeof monitor !== "undefined") {
const parsedUrl = parseUrl(monitor?.url);
const protocol = parsedUrl?.protocol?.replace(":", "") || "";
setHttps(protocol === "https");
const newForm = {
name: monitor.name,
interval: monitor.interval / MS_PER_MINUTE,
url: parsedUrl.host,
type: monitor.type,
};
setForm(newForm);
}
}, [monitor]);
// Handlers
const handleCreateMonitor = async (event) => {
const monitorToSubmit = { ...monitor };
const handleCreateMonitor = async () => {
const monitorToSubmit = { ...form };
// Prepend protocol to url
monitorToSubmit.url = `http${https ? "s" : ""}://` + monitorToSubmit.url;
@@ -68,7 +101,6 @@ const CreateDistributedUptime = () => {
const { error } = monitorValidation.validate(monitorToSubmit, {
abortEarly: false,
});
if (error) {
const newErrors = {};
error.details.forEach((err) => {
@@ -80,16 +112,14 @@ const CreateDistributedUptime = () => {
}
// Append needed fields
monitorToSubmit.description = monitor.name;
monitorToSubmit.interval = monitor.interval * MS_PER_MINUTE;
monitorToSubmit.description = form.name;
monitorToSubmit.interval = form.interval * MS_PER_MINUTE;
monitorToSubmit.teamId = user.teamId;
monitorToSubmit.userId = user._id;
monitorToSubmit.notifications = notifications;
const action = await dispatch(
createUptimeMonitor({ authToken, monitor: monitorToSubmit })
);
if (action.meta.requestStatus === "fulfilled") {
const success = await createDistributedUptimeMonitor({ form: monitorToSubmit });
if (success) {
createToast({ body: "Monitor created successfully!" });
navigate("/distributed-uptime");
} else {
@@ -98,9 +128,10 @@ const CreateDistributedUptime = () => {
};
const handleChange = (event) => {
const { name, value } = event.target;
setMonitor({
...monitor,
let { name, value } = event.target;
setForm({
...form,
[name]: value,
});
const { error } = monitorValidation.validate(
@@ -178,7 +209,8 @@ const CreateDistributedUptime = () => {
label="URL to monitor"
https={https}
placeholder={"www.google.com"}
value={monitor.url}
disabled={!isCreate}
value={form.url}
name="url"
onChange={handleChange}
error={errors["url"] ? true : false}
@@ -190,7 +222,7 @@ const CreateDistributedUptime = () => {
label="Display name"
isOptional={true}
placeholder={"Google"}
value={monitor.name}
value={form.name}
name="name"
onChange={handleChange}
error={errors["name"] ? true : false}
@@ -217,8 +249,11 @@ const CreateDistributedUptime = () => {
checked={true}
onChange={handleChange}
/>
{monitor.type === "http" || monitor.type === "distributed_http" ? (
<ButtonGroup sx={{ ml: theme.spacing(16) }}>
{form.type === "http" || form.type === "distributed_http" ? (
<ButtonGroup
disabled={!isCreate}
sx={{ ml: theme.spacing(16) }}
>
<Button
variant="group"
filled={https.toString()}
@@ -282,7 +317,7 @@ const CreateDistributedUptime = () => {
id="monitor-interval"
label="Check frequency"
name="interval"
value={monitor.interval || 1}
value={form.interval}
onChange={handleChange}
items={SELECT_VALUES}
/>
@@ -292,15 +327,15 @@ const CreateDistributedUptime = () => {
direction="row"
justifyContent="flex-end"
>
<LoadingButton
<Button
variant="contained"
color="primary"
color="accent"
onClick={() => handleCreateMonitor()}
disabled={!Object.values(errors).every((value) => value === undefined)}
loading={isLoading}
>
Create monitor
</LoadingButton>
{isCreate ? "Create monitor" : "Configure monitor"}
</Button>
</Stack>
</Stack>
</Box>
@@ -0,0 +1,91 @@
// Components
import { Box, Stack, Typography, Button } from "@mui/material";
import Image from "../../../../../Components/Image";
import SettingsIcon from "../../../../../assets/icons/settings-bold.svg?react";
//Utils
import { useTheme } from "@mui/material/styles";
import { useNavigate } from "react-router-dom";
import { useLocation } from "react-router-dom";
import PropTypes from "prop-types";
const Controls = ({ isDeleteOpen, setIsDeleteOpen, isDeleting, monitorId }) => {
const theme = useTheme();
const navigate = useNavigate();
return (
<Stack
direction="row"
gap={theme.spacing(2)}
>
<Box>
<Button
variant="contained"
color="error"
onClick={() => setIsDeleteOpen(!isDeleteOpen)}
loading={isDeleting}
>
Delete
</Button>
</Box>
<Box>
<Button
variant="contained"
color="secondary"
onClick={() => {
navigate(`/distributed-uptime/configure/${monitorId}`);
}}
sx={{
px: theme.spacing(5),
"& svg": {
mr: theme.spacing(3),
"& path": {
stroke: theme.palette.secondary.contrastText,
},
},
}}
>
<SettingsIcon /> Configure
</Button>
</Box>
</Stack>
);
};
Controls.propTypes = {
isDeleting: PropTypes.bool,
monitorId: PropTypes.string,
isDeleteOpen: PropTypes.bool.isRequired,
setIsDeleteOpen: PropTypes.func.isRequired,
};
const ControlsHeader = ({ isDeleting, isDeleteOpen, setIsDeleteOpen, monitorId }) => {
const theme = useTheme();
return (
<Stack
alignSelf="flex-start"
direction="row"
width="100%"
gap={theme.spacing(2)}
justifyContent="flex-end"
alignItems="flex-end"
>
<Controls
isDeleting={isDeleting}
isDeleteOpen={isDeleteOpen}
setIsDeleteOpen={setIsDeleteOpen}
monitorId={monitorId}
/>
</Stack>
);
};
ControlsHeader.propTypes = {
monitorId: PropTypes.string,
isDeleting: PropTypes.bool,
isDeleteOpen: PropTypes.bool.isRequired,
setIsDeleteOpen: PropTypes.func.isRequired,
};
export default ControlsHeader;
@@ -0,0 +1,95 @@
import { useState } from "react";
import { useTheme } from "@emotion/react";
import {
AreaChart,
Area,
XAxis,
Tooltip,
CartesianGrid,
ResponsiveContainer,
} from "recharts";
import CustomTick from "../Helpers/Tick";
import CustomToolTip from "../Helpers/ToolTip";
import PropTypes from "prop-types";
const DistributedUptimeResponseAreaChart = ({ checks }) => {
const theme = useTheme();
const [isHovered, setIsHovered] = useState(false);
return (
<ResponsiveContainer
width="100%"
minWidth={25}
height={220}
>
<AreaChart
width="100%"
height="100%"
data={checks}
margin={{
top: 10,
right: 0,
left: 0,
bottom: 0,
}}
onMouseMove={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<CartesianGrid
stroke={theme.palette.primary.lowContrast}
strokeWidth={1}
strokeOpacity={1}
fill="transparent"
vertical={false}
/>
<defs>
<linearGradient
id="colorUv"
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset="0%"
stopColor={theme.palette.accent.darker}
stopOpacity={0.8}
/>
<stop
offset="100%"
stopColor={theme.palette.accent.main}
stopOpacity={0}
/>
</linearGradient>
</defs>
<XAxis
stroke={theme.palette.primary.lowContrast}
dataKey="_id"
tick={<CustomTick />}
minTickGap={0}
axisLine={false}
tickLine={false}
height={20}
/>
<Tooltip
cursor={{ stroke: theme.palette.primary.lowContrast }}
content={<CustomToolTip />}
wrapperStyle={{ pointerEvents: "none" }}
/>
<Area
type="monotone"
dataKey="avgResponseTime"
stroke={theme.palette.primary.accent}
fill="url(#colorUv)"
strokeWidth={isHovered ? 2.5 : 1.5}
activeDot={{ stroke: theme.palette.background.main, r: 5 }}
/>
</AreaChart>
</ResponsiveContainer>
);
};
DistributedUptimeResponseAreaChart.propTypes = {
checks: PropTypes.array,
};
export default DistributedUptimeResponseAreaChart;
@@ -0,0 +1,95 @@
import {
BarChart,
Bar,
CartesianGrid,
ResponsiveContainer,
Tooltip,
XAxis,
} from "recharts";
import { useTheme } from "@emotion/react";
import PropTypes from "prop-types";
import CustomToolTip from "../Helpers/ToolTip";
import CustomTick from "../Helpers/Tick";
const DistributedUptimeResponseBarChart = ({ checks }) => {
const theme = useTheme();
return (
<ResponsiveContainer
width="100%"
minWidth={25}
height={220}
>
<BarChart
width="100%"
height="100%"
data={checks}
margin={{
top: 10,
right: 0,
left: 0,
bottom: 0,
}}
>
<defs>
<linearGradient
id="colorUv"
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset="0%"
stopColor={theme.palette.accent.darker}
stopOpacity={0.8}
/>
<stop
offset="100%"
stopColor={theme.palette.accent.main}
stopOpacity={0}
/>
</linearGradient>
</defs>
<CartesianGrid
stroke={theme.palette.primary.lowContrast}
strokeWidth={1}
strokeOpacity={1}
fill="transparent"
vertical={false}
/>
<Tooltip
cursor={{
stroke: theme.palette.primary.lowContrast,
fill: "transparent",
}}
content={<CustomToolTip />}
wrapperStyle={{ pointerEvents: "none" }}
/>
<XAxis
stroke={theme.palette.primary.lowContrast}
dataKey="_id"
tick={<CustomTick />}
minTickGap={0}
axisLine={false}
tickLine={false}
height={20}
/>
<Bar
maxBarSize={25}
dataKey="avgResponseTime"
fill="url(#colorUv)"
activeBar={{
stroke: theme.palette.accent.main,
r: 5,
}}
/>
</BarChart>
</ResponsiveContainer>
);
};
DistributedUptimeResponseBarChart.propTypes = {
checks: PropTypes.array,
};
export default DistributedUptimeResponseBarChart;
@@ -0,0 +1,31 @@
import { useTheme } from "@emotion/react";
import { useSelector } from "react-redux";
import { formatDateWithTz } from "../../../../../../Utils/timeUtils";
import PropTypes from "prop-types";
import { Text } from "recharts";
const CustomTick = ({ x, y, payload, index }) => {
const theme = useTheme();
const uiTimezone = useSelector((state) => state.ui.timezone);
return (
<Text
x={x}
y={y + 10}
textAnchor="middle"
fill={theme.palette.text.tertiary}
fontSize={11}
fontWeight={400}
>
{formatDateWithTz(payload?.value, "h:mm a", uiTimezone)}
</Text>
);
};
CustomTick.propTypes = {
x: PropTypes.number,
y: PropTypes.number,
payload: PropTypes.object,
index: PropTypes.number,
};
export default CustomTick;
@@ -0,0 +1,86 @@
import { Box, Stack, Typography } from "@mui/material";
import { useSelector } from "react-redux";
import { formatDateWithTz } from "../../../../../../Utils/timeUtils";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import AccessTimeIcon from "@mui/icons-material/AccessTime";
const CustomToolTip = ({ active, payload, label }) => {
const uiTimezone = useSelector((state) => state.ui.timezone);
const theme = useTheme();
if (active && payload && payload.length) {
const responseTime = payload[0]?.payload?.originalAvgResponseTime
? payload[0]?.payload?.originalAvgResponseTime
: (payload[0]?.payload?.avgResponseTime ?? 0);
return (
<Stack
className="area-tooltip"
sx={{
backgroundColor: theme.palette.primary.main,
border: 1,
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.shape.borderRadius,
py: theme.spacing(2),
px: theme.spacing(4),
}}
>
<Typography
sx={{
color: theme.palette.text.tertiary,
fontSize: 12,
fontWeight: 500,
}}
>
{formatDateWithTz(label, "ddd, MMMM D, YYYY, h:mm A", uiTimezone)}
</Typography>
<Stack
direction="row"
alignItems="end"
gap={theme.spacing(2)}
sx={{
"& span": {
color: theme.palette.text.tertiary,
fontSize: 11,
fontWeight: 500,
},
}}
>
<AccessTimeIcon fontSize="small" />
<Typography
component="span"
sx={{ opacity: 0.8 }}
>
Response time:
</Typography>
<Typography component="span">
{Math.floor(responseTime)}
<Typography
component="span"
sx={{ opacity: 0.8 }}
>
ms
</Typography>
</Typography>
</Stack>
</Stack>
);
}
return null;
};
CustomToolTip.propTypes = {
active: PropTypes.bool,
payload: PropTypes.arrayOf(
PropTypes.shape({
value: PropTypes.number,
payload: PropTypes.shape({
_id: PropTypes.string,
avgResponseTime: PropTypes.number,
originalAvgResponseTime: PropTypes.number,
}),
})
),
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};
export default CustomToolTip;
@@ -1,225 +1,50 @@
import PropTypes from "prop-types";
import {
AreaChart,
Area,
XAxis,
Tooltip,
CartesianGrid,
ResponsiveContainer,
Text,
} from "recharts";
//Components
import { Stack, Switch, Typography } from "@mui/material";
import { useState } from "react";
import DistributedUptimeResponseAreaChart from "./Area";
import DistributedUptimeResponseBarChart from "./Bar";
import ChartBox from "../../../../../Components/Charts/ChartBox";
import ResponseTimeIcon from "../../../../../assets/icons/response-time-icon.svg?react";
import { Box, Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import { useState } from "react";
import { useSelector } from "react-redux";
import { formatDateWithTz } from "../../../../../Utils/timeUtils";
const CustomToolTip = ({ active, payload, label }) => {
const uiTimezone = useSelector((state) => state.ui.timezone);
const theme = useTheme();
if (active && payload && payload.length) {
const responseTime = payload[0]?.payload?.originalAvgResponseTime
? payload[0]?.payload?.originalAvgResponseTime
: (payload[0]?.payload?.avgResponseTime ?? 0);
return (
<Box
className="area-tooltip"
sx={{
backgroundColor: theme.palette.primary.main,
border: 1,
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.shape.borderRadius,
py: theme.spacing(2),
px: theme.spacing(4),
}}
>
<Typography
sx={{
color: theme.palette.text.tertiary,
fontSize: 12,
fontWeight: 500,
}}
>
{formatDateWithTz(label, "ddd, MMMM D, YYYY, h:mm A", uiTimezone)}
</Typography>
<Box mt={theme.spacing(1)}>
<Box
display="inline-block"
width={theme.spacing(4)}
height={theme.spacing(4)}
backgroundColor={theme.palette.primary.main}
sx={{ borderRadius: "50%" }}
/>
<Stack
display="inline-flex"
direction="row"
justifyContent="space-between"
ml={theme.spacing(3)}
sx={{
"& span": {
color: theme.palette.text.tertiary,
fontSize: 11,
fontWeight: 500,
},
}}
>
<Typography
component="span"
sx={{ opacity: 0.8 }}
>
Response time
</Typography>
<Typography
ml={theme.spacing(4)}
component="span"
>
{Math.floor(responseTime)}
<Typography
component="span"
sx={{ opacity: 0.8 }}
>
{" "}
ms
</Typography>
</Typography>
</Stack>
</Box>
{/* Display original value */}
</Box>
);
}
return null;
};
CustomToolTip.propTypes = {
active: PropTypes.bool,
payload: PropTypes.arrayOf(
PropTypes.shape({
value: PropTypes.number,
payload: PropTypes.shape({
_id: PropTypes.string,
avgResponseTime: PropTypes.number,
originalAvgResponseTime: PropTypes.number,
}),
})
),
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};
const CustomTick = ({ x, y, payload, index }) => {
const theme = useTheme();
const uiTimezone = useSelector((state) => state.ui.timezone);
// Render nothing for the first tick
if (index === 0) return null;
return (
<Text
x={x}
y={y + 10}
textAnchor="middle"
fill={theme.palette.text.tertiary}
fontSize={11}
fontWeight={400}
>
{formatDateWithTz(payload?.value, "h:mm a", uiTimezone)}
</Text>
);
};
CustomTick.propTypes = {
x: PropTypes.number,
y: PropTypes.number,
payload: PropTypes.object,
index: PropTypes.number,
};
// Utils
import PropTypes from "prop-types";
const DistributedUptimeResponseChart = ({ checks }) => {
const theme = useTheme();
const [isHovered, setIsHovered] = useState(false);
if (checks.length === 0) return null;
const [chartType, setChartType] = useState("bar");
let Chart = null;
if (chartType === "area") {
Chart = DistributedUptimeResponseAreaChart;
}
if (chartType === "bar") {
Chart = DistributedUptimeResponseBarChart;
}
return (
<ChartBox
icon={<ResponseTimeIcon />}
header="Response Times"
sx={{ padding: 0 }}
>
<ResponsiveContainer
width="100%"
minWidth={25}
height={220}
<Stack>
<Stack
direction="row"
alignItems="center"
>
<AreaChart
width="100%"
height="100%"
data={checks}
margin={{
top: 10,
right: 0,
left: 0,
bottom: 0,
}}
onMouseMove={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<CartesianGrid
stroke={theme.palette.primary.lowContrast}
strokeWidth={1}
strokeOpacity={1}
fill="transparent"
vertical={false}
/>
<defs>
<linearGradient
id="colorUv"
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset="0%"
stopColor={theme.palette.accent.darker}
stopOpacity={0.8}
/>
<stop
offset="100%"
stopColor={theme.palette.accent.main}
stopOpacity={0}
/>
</linearGradient>
</defs>
<XAxis
stroke={theme.palette.primary.lowContrast}
dataKey="_id"
tick={<CustomTick />}
minTickGap={0}
axisLine={false}
tickLine={false}
height={20}
/>
<Tooltip
cursor={{ stroke: theme.palette.primary.lowContrast }}
content={<CustomToolTip />}
wrapperStyle={{ pointerEvents: "none" }}
/>
<Area
type="monotone"
dataKey="avgResponseTime"
stroke={theme.palette.primary.accent}
fill="url(#colorUv)"
strokeWidth={isHovered ? 2.5 : 1.5}
activeDot={{ stroke: theme.palette.background.main, r: 5 }}
/>
</AreaChart>
</ResponsiveContainer>
</ChartBox>
<Typography>Bar</Typography>
<Switch
color="main"
value={chartType}
onChange={(e) => setChartType(e.target.checked ? "area" : "bar")}
/>
<Typography>Area</Typography>
</Stack>
<ChartBox
icon={<ResponseTimeIcon />}
header="Response Times"
sx={{ padding: 0 }}
>
<Chart checks={checks} />
</ChartBox>
</Stack>
);
};
DistributedUptimeResponseChart.propTypes = {
checks: PropTypes.array,
type: PropTypes.string,
};
export default DistributedUptimeResponseChart;
@@ -0,0 +1,27 @@
import { useSelector } from "react-redux";
import { useState } from "react";
import { networkService } from "../../../../main";
import { createToast } from "../../../../Utils/toastUtils";
const useDeleteMonitor = ({ monitorId }) => {
const [isLoading, setIsLoading] = useState(false);
const { authToken } = useSelector((state) => state.auth);
const deleteMonitor = async () => {
try {
setIsLoading(true);
await networkService.deleteMonitorById({ authToken, monitorId });
return true;
} catch (error) {
createToast({
body: error.message,
});
return false;
} finally {
setIsLoading(false);
}
};
return [deleteMonitor, isLoading];
};
export { useDeleteMonitor };
@@ -3,7 +3,7 @@ import DistributedUptimeMap from "./Components/DistributedUptimeMap";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import { Stack, Typography } from "@mui/material";
import DeviceTicker from "./Components/DeviceTicker";
import DistributedUptimeResponseChart from "./Components/DistributedUptimeResponseChart";
import ResponseTimeChart from "./Components/DistributedUptimeResponseChart";
import NextExpectedCheck from "./Components/NextExpectedCheck";
import Footer from "./Components/Footer";
import StatBoxes from "./Components/StatBoxes";
@@ -12,23 +12,31 @@ import MonitorTimeFrameHeader from "../../../Components/MonitorTimeFrameHeader";
import GenericFallback from "../../../Components/GenericFallback";
import MonitorCreateHeader from "../../../Components/MonitorCreateHeader";
import SkeletonLayout from "./Components/Skeleton";
import ControlsHeader from "./Components/ControlsHeader";
import Dialog from "../../../Components/Dialog";
//Utils
import { useTheme } from "@mui/material/styles";
import { useState } from "react";
import { useParams } from "react-router-dom";
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
import { useSubscribeToDetails } from "./Hooks/useSubscribeToDetails";
import { useDeleteMonitor } from "./Hooks/useDeleteMonitor";
import { useNavigate } from "react-router-dom";
const DistributedUptimeDetails = () => {
const { monitorId } = useParams();
// Local State
const [dateRange, setDateRange] = useState("day");
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
// Utils
const theme = useTheme();
const isAdmin = useIsAdmin();
const navigate = useNavigate();
const [isLoading, networkError, connectionStatus, monitor, lastUpdateTrigger] =
useSubscribeToDetails({ monitorId, dateRange });
const [deleteMonitor, isDeleting] = useDeleteMonitor({ monitorId });
// Constants
const BREADCRUMBS = [
{ name: "Distributed Uptime", path: "/distributed-uptime" },
@@ -53,8 +61,11 @@ const DistributedUptimeDetails = () => {
</GenericFallback>
);
}
if (typeof monitor === "undefined" || monitor.totalChecks === 0) {
if (
typeof monitor === "undefined" ||
typeof monitor?.totalChecks === "undefined" ||
monitor?.totalChecks === 0
) {
return (
<Stack gap={theme.spacing(10)}>
<Breadcrumbs list={BREADCRUMBS} />
@@ -76,6 +87,12 @@ const DistributedUptimeDetails = () => {
isAdmin={isAdmin}
path={`/status/distributed/create/${monitorId}`}
/>
<ControlsHeader
isDeleting={isDeleting}
isDeleteOpen={isDeleteOpen}
setIsDeleteOpen={setIsDeleteOpen}
monitorId={monitorId}
/>
<MonitorHeader monitor={monitor} />
<StatBoxes
monitor={monitor}
@@ -90,7 +107,8 @@ const DistributedUptimeDetails = () => {
dateRange={dateRange}
setDateRange={setDateRange}
/>
<DistributedUptimeResponseChart checks={monitor?.groupedChecks ?? []} />
<ResponseTimeChart checks={monitor?.groupedChecks ?? []} />
<Stack
direction="row"
gap={theme.spacing(8)}
@@ -107,6 +125,21 @@ const DistributedUptimeDetails = () => {
/>
</Stack>
<Footer />
<Dialog
title="Do you want to delete this monitor?"
onConfirm={() => {
deleteMonitor();
setIsDeleteOpen(false);
navigate("/distributed-uptime");
}}
onCancel={() => {
setIsDeleteOpen(false);
}}
open={isDeleteOpen}
confirmationButtonLabel="Yes, delete monitor"
description="Once deleted, your monitor cannot be retrieved."
isLoading={isDeleting || isLoading}
/>
</Stack>
);
};
@@ -29,7 +29,7 @@ const MonitorTable = ({ isLoading, monitors }) => {
<Host
key={row._id}
url={row.url}
title={row.title}
title={row.name}
percentageColor={row.percentageColor}
percentage={row.percentage}
/>
@@ -41,7 +41,7 @@ const DistributedUptimeMonitors = () => {
);
}
if (typeof monitorsSummary !== "undefined" && monitorsSummary.totalMonitors === 0) {
if (typeof monitorsSummary === "undefined" || monitorsSummary.totalMonitors === 0) {
return (
<Fallback
vowelStart={false}
@@ -13,7 +13,6 @@ import { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
import { useStatusPageFetch } from "../../StatusPage/Status/Hooks/useStatusPageFetch";
import { useCreateStatusPage } from "../../StatusPage/Create/Hooks/useCreateStatusPage";
import { useLocation } from "react-router-dom";
import { statusPageValidation } from "../../../Validation/validation";
import { buildErrors } from "../../../Validation/error";
import { createToast } from "../../../Utils/toastUtils";
@@ -23,8 +22,7 @@ const CreateStatus = () => {
const theme = useTheme();
const { monitorId, url } = useParams();
const navigate = useNavigate();
const location = useLocation();
const isCreate = location.pathname.startsWith("/status/distributed/create");
const isCreate = typeof url === "undefined";
const [createStatusPage, isLoading, networkError] = useCreateStatusPage(isCreate);
const [statusPage, statusPageMonitors, statusPageIsLoading, statusPageNetworkError] =
@@ -11,7 +11,6 @@ import { capitalizeFirstLetter } from "../../../Utils/stringUtils";
// MUI
import { Box, Stack, Typography, Button, ButtonGroup } from "@mui/material";
import LoadingButton from "@mui/lab/LoadingButton";
//Components
import Breadcrumbs from "../../../Components/Breadcrumbs";
@@ -405,14 +404,14 @@ const CreateInfrastructureMonitor = () => {
direction="row"
justifyContent="flex-end"
>
<LoadingButton
<Button
variant="contained"
color="accent"
onClick={handleCreateInfrastructureMonitor}
loading={monitorState?.isLoading}
>
Create infrastructure monitor
</LoadingButton>
</Button>
</Stack>
</Stack>
</Box>
@@ -9,7 +9,6 @@ import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import { MobileTimePicker } from "@mui/x-date-pickers/MobileTimePicker";
import { maintenanceWindowValidation } from "../../../Validation/validation";
import { createToast } from "../../../Utils/toastUtils";
import LoadingButton from "@mui/lab/LoadingButton";
import dayjs from "dayjs";
import Select from "../../../Components/Inputs/Select";
@@ -552,7 +551,7 @@ const CreateMaintenance = () => {
>
Cancel
</Button>
<LoadingButton
<Button
loading={isLoading}
variant="contained"
color="accent"
@@ -564,7 +563,7 @@ const CreateMaintenance = () => {
? "Create maintenance"
: "Edit maintenance"
}`}
</LoadingButton>
</Button>
</Box>
</Stack>
</Box>
@@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import { useTheme } from "@emotion/react";
import { Box, Stack, Tooltip, Typography } from "@mui/material";
import { Box, Stack, Tooltip, Typography, Button } from "@mui/material";
import { useDispatch, useSelector } from "react-redux";
import { useNavigate, useParams } from "react-router";
import {
@@ -20,7 +20,6 @@ import Checkbox from "../../../Components/Inputs/Checkbox";
import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import PulseDot from "../../../Components/Animated/PulseDot";
import LoadingButton from "@mui/lab/LoadingButton";
import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded";
import SkeletonLayout from "./skeleton";
import useUtils from "../../Uptime/Monitors/Hooks/useUtils";
@@ -265,7 +264,7 @@ const PageSpeedConfigure = () => {
alignSelf="flex-end"
ml="auto"
>
<LoadingButton
<Button
onClick={handlePause}
loading={isLoading}
variant="contained"
@@ -293,8 +292,8 @@ const PageSpeedConfigure = () => {
Resume
</>
)}
</LoadingButton>
<LoadingButton
</Button>
<Button
loading={isLoading}
variant="contained"
color="error"
@@ -304,7 +303,7 @@ const PageSpeedConfigure = () => {
}}
>
Remove
</LoadingButton>
</Button>
</Box>
</Stack>
<ConfigBox>
@@ -422,7 +421,7 @@ const PageSpeedConfigure = () => {
justifyContent="flex-end"
mt="auto"
>
<LoadingButton
<Button
loading={isLoading}
type="submit"
variant="contained"
@@ -431,7 +430,7 @@ const PageSpeedConfigure = () => {
sx={{ px: theme.spacing(12) }}
>
Save
</LoadingButton>
</Button>
</Stack>
</Stack>
</>
+2 -3
View File
@@ -14,7 +14,6 @@ import { parseDomainName } from "../../../Utils/monitorUtils";
// MUI
import { useTheme } from "@emotion/react";
import { Box, Stack, Typography, Button, ButtonGroup } from "@mui/material";
import LoadingButton from "@mui/lab/LoadingButton";
//Components
import Breadcrumbs from "../../../Components/Breadcrumbs";
@@ -335,7 +334,7 @@ const CreatePageSpeed = () => {
direction="row"
justifyContent="flex-end"
>
<LoadingButton
<Button
variant="contained"
color="accent"
onClick={handleCreateMonitor}
@@ -343,7 +342,7 @@ const CreatePageSpeed = () => {
loading={isLoading}
>
Create monitor
</LoadingButton>
</Button>
</Stack>
</Stack>
</Box>
+29 -7
View File
@@ -3,10 +3,13 @@ import { Box, Stack, Typography, Button, Switch } from "@mui/material";
import TextInput from "../../Components/Inputs/TextInput";
import Link from "../../Components/Link";
import Select from "../../Components/Inputs/Select";
import LoadingButton from "@mui/lab/LoadingButton";
import { useIsAdmin } from "../../Hooks/useIsAdmin";
import Dialog from "../../Components/Dialog";
import ConfigBox from "../../Components/ConfigBox";
import {
WalletMultiButton,
WalletDisconnectButton,
} from "@solana/wallet-adapter-react-ui";
//Utils
import { useTheme } from "@emotion/react";
@@ -324,7 +327,7 @@ const Settings = () => {
<Stack gap={theme.spacing(20)}>
<Box>
<Typography>Add demo monitors</Typography>
<LoadingButton
<Button
variant="contained"
color="accent"
loading={isLoading || authIsLoading || checksIsLoading}
@@ -332,11 +335,11 @@ const Settings = () => {
sx={{ mt: theme.spacing(4) }}
>
Add demo monitors
</LoadingButton>
</Button>
</Box>
<Box>
<Typography>Remove all monitors</Typography>
<LoadingButton
<Button
variant="contained"
color="error"
loading={isLoading || authIsLoading || checksIsLoading}
@@ -346,7 +349,7 @@ const Settings = () => {
sx={{ mt: theme.spacing(4) }}
>
Remove all monitors
</LoadingButton>
</Button>
</Box>
</Stack>
<Dialog
@@ -360,6 +363,25 @@ const Settings = () => {
/>
</ConfigBox>
)}
{isAdmin && (
<ConfigBox>
<Box>
<Typography component="h1">Wallet</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
Connect your wallet here. This is required for the Distributed Uptime monitor to connect to multiple nodes globally.
</Typography>
</Box>
<Box>
<Stack
direction="row"
spacing={2}
>
<WalletMultiButton />
<WalletDisconnectButton />
</Stack>
</Box>
</ConfigBox>
)}
<ConfigBox>
<Box>
@@ -381,7 +403,7 @@ const Settings = () => {
direction="row"
justifyContent="flex-end"
>
<LoadingButton
<Button
loading={isLoading || authIsLoading || checksIsLoading}
disabled={Object.keys(errors).length > 0}
variant="contained"
@@ -390,7 +412,7 @@ const Settings = () => {
onClick={handleSave}
>
Save
</LoadingButton>
</Button>
</Stack>
</Stack>
</Box>
+1 -3
View File
@@ -12,7 +12,6 @@ import { useMonitorsFetch } from "./Hooks/useMonitorsFetch";
import { useCreateStatusPage } from "./Hooks/useCreateStatusPage";
import { createToast } from "../../../Utils/toastUtils";
import { useNavigate } from "react-router-dom";
import { useLocation } from "react-router-dom";
import { useStatusPageFetch } from "../Status/Hooks/useStatusPageFetch";
import { useParams } from "react-router-dom";
@@ -47,8 +46,7 @@ const CreateStatusPage = () => {
const intervalRef = useRef(null);
// Setup
const location = useLocation();
const isCreate = location.pathname === "/status/uptime/create";
const isCreate = typeof url === "undefined";
//Utils
const theme = useTheme();
+79 -9
View File
@@ -2,7 +2,7 @@ import { useNavigate, useParams } from "react-router";
import { useTheme } from "@emotion/react";
import { useDispatch, useSelector } from "react-redux";
import { useEffect, useState } from "react";
import { Box, Stack, Tooltip, Typography } from "@mui/material";
import { Box, Stack, Tooltip, Typography, Button } from "@mui/material";
import { monitorValidation } from "../../../Validation/validation";
import { createToast } from "../../../Utils/toastUtils";
import { logger } from "../../../Utils/Logger";
@@ -22,7 +22,6 @@ import Checkbox from "../../../Components/Inputs/Checkbox";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import PulseDot from "../../../Components/Animated/PulseDot";
import SkeletonLayout from "./skeleton";
import LoadingButton from "@mui/lab/LoadingButton";
import "./index.css";
import Dialog from "../../../Components/Dialog";
@@ -62,6 +61,18 @@ const Configure = () => {
"notify-email-default": "notification-email",
};
const matchMethodOptions = [
{ _id: "equal", name: "Equal" },
{ _id: "include", name: "Include" },
{ _id: "regex", name: "Regex" },
];
const expectedValuePlaceholders = {
regex: "^[\w.-]+@gmail.com$",
equal: "janet@gmail.com",
include: "@gmail.com"
};
useEffect(() => {
const fetchMonitor = async () => {
try {
@@ -292,7 +303,7 @@ const Configure = () => {
ml: "auto",
}}
>
<LoadingButton
<Button
variant="contained"
color="secondary"
loading={isLoading}
@@ -323,8 +334,8 @@ const Configure = () => {
Resume
</>
)}
</LoadingButton>
<LoadingButton
</Button>
<Button
loading={isLoading}
variant="contained"
color="error"
@@ -332,7 +343,7 @@ const Configure = () => {
onClick={() => setIsOpen(true)}
>
Remove
</LoadingButton>
</Button>
</Box>
</Stack>
<ConfigBox>
@@ -347,7 +358,11 @@ const Configure = () => {
<TextInput
type={monitor?.type === "http" ? "url" : "text"}
https={protocol === "https"}
startAdornment={monitor?.type === "http" && <HttpAdornment https={protocol === "https"} />}
startAdornment={
monitor?.type === "http" && (
<HttpAdornment https={protocol === "https"} />
)
}
id="monitor-url"
label="URL to monitor"
placeholder="google.com"
@@ -436,6 +451,61 @@ const Configure = () => {
onChange={(event) => handleChange(event, "interval")}
items={frequencies}
/>
{
monitor.type === "http" && <>
<Select
id="match-method"
label="Match Method"
value={monitor.matchMethod || "equal"}
onChange={(event) => handleChange(event, "matchMethod")}
items={matchMethodOptions}
/>
<Stack>
<TextInput
type="text"
id="expected-value"
label="Expected value"
isOptional={true}
placeholder={expectedValuePlaceholders[monitor.matchMethod || "equal"]}
value={monitor.expectedValue}
onChange={(event) => handleChange(event, "expectedValue")}
error={errors["expectedValue"] ? true : false}
helperText={errors["expectedValue"]}
/>
<Typography
component="span"
color={theme.palette.primary.contrastTextTertiary}
opacity={0.8}
>
The expected value is used to match against response result, and the match determines the status.
</Typography>
</Stack>
<Stack>
<TextInput
type="text"
id="json-path"
label="JSON Path"
isOptional={true}
placeholder="data.email"
value={monitor.jsonPath}
onChange={(event) => handleChange(event, "jsonPath")}
error={errors["jsonPath"] ? true : false}
helperText={errors["jsonPath"]}
/>
<Typography
component="span"
color={theme.palette.primary.contrastTextTertiary}
opacity={0.8}
>
This expression will be evaluated against the reponse JSON data and the result will be used to match against the expected value. See&nbsp;
<Typography component="a" href="https://jmespath.org/" target="_blank" color="info">
jmespath.org
</Typography>
&nbsp;for query language documentation.
</Typography>
</Stack>
</>
}
</Stack>
</ConfigBox>
<Stack
@@ -443,7 +513,7 @@ const Configure = () => {
justifyContent="flex-end"
mt="auto"
>
<LoadingButton
<Button
variant="contained"
color="accent"
loading={isLoading}
@@ -451,7 +521,7 @@ const Configure = () => {
onClick={handleSubmit}
>
Save
</LoadingButton>
</Button>
</Stack>
</Stack>
</>
+77 -5
View File
@@ -13,7 +13,6 @@ import { createUptimeMonitor } from "../../../Features/UptimeMonitors/uptimeMoni
// MUI
import { Box, Stack, Typography, Button, ButtonGroup } from "@mui/material";
import LoadingButton from "@mui/lab/LoadingButton";
//Components
import Breadcrumbs from "../../../Components/Breadcrumbs";
@@ -34,6 +33,18 @@ const CreateMonitor = () => {
{ _id: 5, name: "5 minutes" },
];
const matchMethodOptions = [
{ _id: "equal", name: "Equal" },
{ _id: "include", name: "Include" },
{ _id: "regex", name: "Regex" },
];
const expectedValuePlaceholders = {
regex: "^[\w.-]+@gmail.com$",
equal: "janet@gmail.com",
include: "@gmail.com"
};
const monitorTypeMaps = {
http: {
label: "URL to monitor",
@@ -93,6 +104,12 @@ const CreateMonitor = () => {
interval: monitor.interval * MS_PER_MINUTE,
};
if (monitor.type === "http") {
form.expectedValue = monitor.expectedValue;
form.jsonPath = monitor.jsonPath;
form.matchMethod = monitor.matchMethod;
}
const { error } = monitorValidation.validate(form, {
abortEarly: false,
});
@@ -143,8 +160,8 @@ const CreateMonitor = () => {
...monitor,
[formName]: value,
};
if (formName === 'type') {
newMonitor.url = '';
if (formName === "type") {
newMonitor.url = "";
}
setMonitor(newMonitor);
@@ -399,13 +416,68 @@ const CreateMonitor = () => {
onChange={(event) => handleChange(event, "interval")}
items={SELECT_VALUES}
/>
{
monitor.type === "http" && <>
<Select
id="match-method"
label="Match Method"
value={monitor.matchMethod || "equal"}
onChange={(event) => handleChange(event, "matchMethod")}
items={matchMethodOptions}
/>
<Stack>
<TextInput
type="text"
id="expected-value"
label="Expected value"
isOptional={true}
placeholder={expectedValuePlaceholders[monitor.matchMethod || "equal"]}
value={monitor.expectedValue}
onChange={(event) => handleChange(event, "expectedValue")}
error={errors["expectedValue"] ? true : false}
helperText={errors["expectedValue"]}
/>
<Typography
component="span"
color={theme.palette.primary.contrastTextTertiary}
opacity={0.8}
>
The expected value is used to match against response result, and the match determines the status.
</Typography>
</Stack>
<Stack>
<TextInput
type="text"
id="json-path"
label="JSON Path"
isOptional={true}
placeholder="data.email"
value={monitor.jsonPath}
onChange={(event) => handleChange(event, "jsonPath")}
error={errors["jsonPath"] ? true : false}
helperText={errors["jsonPath"]}
/>
<Typography
component="span"
color={theme.palette.primary.contrastTextTertiary}
opacity={0.8}
>
This expression will be evaluated against the reponse JSON data and the result will be used to match against the expected value. See&nbsp;
<Typography component="a" href="https://jmespath.org/" target="_blank" color="info">
jmespath.org
</Typography>
&nbsp;for query language documentation.
</Typography>
</Stack>
</>
}
</Stack>
</ConfigBox>
<Stack
direction="row"
justifyContent="flex-end"
>
<LoadingButton
<Button
variant="contained"
color="accent"
onClick={handleCreateMonitor}
@@ -413,7 +485,7 @@ const CreateMonitor = () => {
loading={isLoading}
>
Create monitor
</LoadingButton>
</Button>
</Stack>
</Stack>
</Box>
@@ -30,6 +30,12 @@ const ChartBoxes = ({
return <SkeletonLayout />;
}
const totalUpChecks = monitor?.upChecks?.totalChecks ?? 0;
const totalDownChecks = monitor?.downChecks?.totalChecks ?? 0;
const denominator =
totalUpChecks + totalDownChecks > 0 ? totalUpChecks + totalDownChecks : 1;
const groupedUptimePercentage = (totalUpChecks / denominator) * 100;
return (
<Stack
direction="row"
@@ -73,11 +79,7 @@ const ChartBoxes = ({
<Typography component="span">
{hoveredUptimeData !== null
? Math.floor(hoveredUptimeData?.avgResponseTime ?? 0)
: Math.floor(
((monitor?.upChecks?.totalChecks ?? 0) /
(monitor?.totalChecks ?? 1)) *
100
)}
: Math.floor(groupedUptimePercentage)}
<Typography component="span">
{hoveredUptimeData !== null ? " ms" : " %"}
</Typography>
@@ -101,7 +101,7 @@ const UptimeDataTable = ({
<Host
key={row._id}
url={row.url}
title={row.title}
title={row.name}
percentageColor={row.percentageColor}
percentage={row.percentage}
/>
+8
View File
@@ -107,6 +107,14 @@ const Routes = () => {
</ProtectedDistributedUptimeRoute>
}
/>
<Route
path="/distributed-uptime/configure/:monitorId"
element={
<ProtectedDistributedUptimeRoute>
<CreateDistributedUptime />
</ProtectedDistributedUptimeRoute>
}
/>
<Route
path="/distributed-uptime/:monitorId"
element={
+13 -4
View File
@@ -99,9 +99,11 @@ class NetworkService {
* @returns {Promise<AxiosResponse>} The response from the axios POST request.
*/
async createMonitor(config) {
return this.axiosInstance.post(`/monitors`, config.monitor, {
const { authToken, monitor } = config;
return this.axiosInstance.post(`/monitors`, monitor, {
headers: {
Authorization: `Bearer ${config.authToken}`,
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
},
});
@@ -284,9 +286,16 @@ class NetworkService {
* @returns {Promise<AxiosResponse>} The response from the axios PUT request.
*/
async updateMonitor(config) {
return this.axiosInstance.put(`/monitors/${config.monitorId}`, config.updatedFields, {
const { authToken, monitorId, monitor } = config;
const updatedFields = {
name: monitor.name,
description: monitor.description,
interval: monitor.interval,
notifications: monitor.notifications,
};
return this.axiosInstance.put(`/monitors/${monitorId}`, updatedFields, {
headers: {
Authorization: `Bearer ${config.authToken}`,
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
},
});
+7 -6
View File
@@ -109,7 +109,7 @@ const baseTheme = (palette) => ({
return (
props.variant === "contained" &&
props.disabled &&
props.classes.loadingIndicator === undefined // Do not apply to loading button
props?.classes?.loadingIndicator === undefined // Do not apply to loading button
);
},
style: {
@@ -132,7 +132,7 @@ const baseTheme = (palette) => ({
backgroundColor: theme.palette.secondary.main,
color: theme.palette.primary.contrastText,
},
"&.MuiLoadingButton-root": {
"&.MuiButton-root": {
"&:disabled": {
backgroundColor: theme.palette.secondary.main,
color: theme.palette.primary.contrastText,
@@ -141,12 +141,13 @@ const baseTheme = (palette) => ({
boxShadow: `0 0 0 1px ${theme.palette.accent.main}`, // CAIO_REVIEW, this should really have a solid BG color
},
},
"&.MuiLoadingButton-loading": {
"& .MuiLoadingButton-label": {
"&.MuiButton-loading": {
"&:disabled": {
color: "transparent",
},
"& .MuiLoadingButton-loadingIndicator": {
color: "inherit",
"& .MuiButton-loadingIndicator": {
color: theme.palette.primary.contrastText,
},
},
}),
+3
View File
@@ -161,6 +161,9 @@ const monitorValidation = joi.object({
"number.base": "Frequency must be a number.",
"any.required": "Frequency is required.",
}),
expectedValue: joi.string().allow(""),
jsonPath: joi.string().allow(""),
matchMethod: joi.string(),
});
const imageValidation = joi.object({
+12
View File
@@ -1,7 +1,19 @@
FROM node:20-alpine AS build
ENV NODE_OPTIONS="--max-old-space-size=4096"
WORKDIR /app
RUN apk add --no-cache \
python3 \
make g++ \
gcc \
libc-dev \
linux-headers \
libusb-dev \
eudev-dev
COPY ../../Client/package*.json ./
RUN npm install
+12
View File
@@ -1,7 +1,19 @@
FROM node:20-alpine AS build
ENV NODE_OPTIONS="--max-old-space-size=4096"
WORKDIR /app
RUN apk add --no-cache \
python3 \
make g++ \
gcc \
libc-dev \
linux-headers \
libusb-dev \
eudev-dev
COPY ../../Client/package*.json ./
RUN npm install
+11
View File
@@ -1,7 +1,18 @@
FROM node:20-alpine AS build
ENV NODE_OPTIONS="--max-old-space-size=4096"
WORKDIR /app
RUN apk add --no-cache \
python3 \
make g++ \
gcc \
libc-dev \
linux-headers \
libusb-dev \
eudev-dev
COPY ../../Client/package*.json ./
RUN npm install
+12
View File
@@ -1,7 +1,19 @@
FROM node:20-alpine AS build
ENV NODE_OPTIONS="--max-old-space-size=4096"
WORKDIR /app
RUN apk add --no-cache \
python3 \
make g++ \
gcc \
libc-dev \
linux-headers \
libusb-dev \
eudev-dev
COPY ../../Client/package*.json ./
RUN npm install
-26
View File
@@ -1,26 +0,0 @@
#!/bin/bash
# Change directory to root directory for correct Docker Context
cd "$(dirname "$0")"
cd ../..
# Define an array of services and their Dockerfiles
declare -A services=(
["uptime_client"]="./Docker/prod/client.Dockerfile"
["uptime_database_mongo"]="./Docker/prod/mongoDB.Dockerfile"
["uptime_redis"]="./Docker/prod/redis.Dockerfile"
["uptime_server"]="./Docker/prod/server.Dockerfile"
)
# Loop through each service and build the corresponding image
for service in "${!services[@]}"; do
docker build -f "${services[$service]}" -t "$service" .
# Check if the build succeeded
if [ $? -ne 0 ]; then
echo "Error building $service image. Exiting..."
exit 1
fi
done
echo "All images built successfully"
-18
View File
@@ -1,18 +0,0 @@
FROM node:20-alpine AS build
WORKDIR /app
COPY ../../Client/package*.json ./
RUN npm install
COPY ../../Client .
RUN npm run build
FROM nginx:1.27.1-alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY --from=build /app/env.sh /docker-entrypoint.d/env.sh
RUN chmod +x /docker-entrypoint.d/env.sh
CMD ["nginx", "-g", "daemon off;"]
-46
View File
@@ -1,46 +0,0 @@
services:
client:
image: uptime_client:latest
environment:
UPTIME_APP_API_BASE_URL: "https://checkmate-demo.bluewavelabs.ca/api/v1"
UPTIME_STATUS_PAGE_SUBDOMAIN_PREFIX: "http://uptimegenie.com/"
ports:
- "80:80"
- "443:443"
depends_on:
- server
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d/:ro
- ./certbot/www:/var/www/certbot/:ro
- ./certbot/conf/:/etc/nginx/ssl/:ro
certbot:
image: certbot/certbot:latest
volumes:
- ./certbot/www/:/var/www/certbot/:rw
- ./certbot/conf/:/etc/letsencrypt/:rw
server:
image: uptime_server:latest
ports:
- "5000:5000"
env_file:
- server.env
depends_on:
- redis
- mongodb
redis:
image: uptime_redis:latest
ports:
- "6379:6379"
volumes:
- ./redis/data:/data
mongodb:
image: uptime_database_mongo:latest
command: ["mongod", "--quiet", "--auth"]
ports:
- "27017:27017"
volumes:
- ./mongo/data:/data/db
- ./mongo/init/create_users.js:/docker-entrypoint-initdb.d/create_users.js
env_file:
- mongo.env
@@ -1,16 +0,0 @@
var username = process.env.USERNAME_ENV_VAR;
var password = process.env.PASSWORD_ENV_VAR;
db = db.getSiblingDB("uptime_db");
db.createUser({
user: username,
pwd: password,
roles: [
{
role: "readWrite",
db: "uptime_db",
},
],
});
print("User uptime_user created successfully");
-4
View File
@@ -1,4 +0,0 @@
FROM mongo
COPY ./Docker/prod/mongo/init/create_users.js /docker-entrypoint-initdb.d/
EXPOSE 27017
CMD ["mongod"]
@@ -1,69 +0,0 @@
server {
listen 80;
listen [::]:80;
server_name checkmate-demo.bluewavelabs.ca;
server_tokens off;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://server:5000/api/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /api-docs/ {
proxy_pass http://server:5000/api-docs/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
listen 443 default_server ssl http2;
listen [::]:443 ssl http2;
server_name checkmate-demo.bluewavelabs.ca;
ssl_certificate /etc/nginx/ssl/live/checkmate-demo.bluewavelabs.ca/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/live/checkmate-demo.bluewavelabs.ca/privkey.pem;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://server:5000/api/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /api-docs/ {
proxy_pass http://server:5000/api-docs/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
-2
View File
@@ -1,2 +0,0 @@
FROM redis
EXPOSE 6379
-13
View File
@@ -1,13 +0,0 @@
FROM node:20-alpine
WORKDIR /app
COPY ./Server/package*.json ./
RUN npm install
COPY ./Server/ ./
EXPOSE 5000
CMD ["node", "index.js"]
+16 -7
View File
@@ -36,7 +36,17 @@ Usage instructions can be found [here](https://docs.checkmate.so/). It's still W
## 🛠️ Installation
See installation instructions in [Checkmate documentation portal](https://docs.checkmate.so/quickstart). Alternatively, you can also use [Coolify](https://coolify.io/) 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/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.
## 🚀 Performance
Thanks to extensive optimizations, Checkmate operates with an exceptionally small memory footprint, requiring minimal memory and CPU resources. Here’s the memory usage of a Node.js instance running on a server that monitors 323 servers every minute:
![image](https://github.com/user-attachments/assets/37e04a75-d83a-488f-b25c-025511b492c9)
You can see the memory footprint of MongoDB and Redis on the same server (398Mb and 15Mb) for the same amount of servers:
![image](https://github.com/user-attachments/assets/3b469e85-e675-4040-a162-3f24c1afc751)
## 💚 Questions & ideas
@@ -97,7 +107,7 @@ If you have any questions, suggestions or comments, please use our [Discord chan
## 🤝 Contributing
We pride ourselves on building strong connections with contributors at every level. Despite being a young project, Checkmate has already earned 3300 stars and attracted 40+ contributors from around the globe. So, don’t hold back — jump in, contribute and learn with us!
We pride ourselves on building strong connections with contributors at every level. Despite being a young project, Checkmate has already earned 3700+ stars and attracted 45+ contributors from around the globe. So, don’t hold back — jump in, contribute and learn with us!
Here's how you can contribute:
@@ -113,18 +123,17 @@ Here's how you can contribute:
<img src="https://contrib.rocks/image?repo=bluewave-labs/checkmate" />
</a>
<!--
## 💰 Our sponsors
![Alt](https://repobeats.axiom.co/api/embed/c35d999c82dbb31e967427ea4166c14da4172e73.svg "Repobeats analytics image")
-->
Thanks to [Gitbook](https://gitbook.io/) for giving us a free tier for their documentation platform, and [Poeditor](https://poeditor.com/) providing us a free account to use their i18n services. If you would like to sponsor Checkmate, please send an email to hello@bluewavelabs.ca
[![Star History Chart](https://api.star-history.com/svg?repos=bluewave-labs/checkmate&type=Date)](https://star-history.com/#bluewave-labs/bluewave-uptime&Date)
Also check other developer and contributor-friendly projects of BlueWave:
- [LangRoute](https://github.com/bluewave-labs/langroute), an LLM proxy and gateway
- [DataRoom](https://github.com/bluewave-labs/bluewave-dataroom), an secure file sharing application, aka dataroom.
- [BlueWave HRM](https://github.com/bluewave-labs/bluewave-hrm), a complete Human Resource Management platform.
- [Headcount](https://github.com/bluewave-labs/bluewave-hrm), a complete Human Resource Management platform.
- [Guidefox](https://github.com/bluewave-labs/guidefox), an application that helps new users learn how to use your product via hints, tours, popups and banners.
- [VerifyWise](https://github.com/bluewave-labs/verifywise), the first open source AI governance platform.
+2 -1
View File
@@ -3,4 +3,5 @@ node_modules
*.log
*.sh
.nyc_output
coverage
coverage
.clinic
@@ -30,14 +30,7 @@ class DistributedUptimeController {
} = result;
// Calculate response time
const responseTime =
(first_byte_took +
body_read_took +
dns_took +
conn_took +
connect_took +
tls_took) /
1_000_000;
const responseTime = first_byte_took / 1_000_000;
// Calculate if server is up or down
const isErrorStatus = status_code >= 400;
+46 -9
View File
@@ -295,12 +295,49 @@ class MonitorController {
try {
const monitor = await this.db.deleteMonitor(req, res, next);
// Delete associated checks,alerts,and notifications
try {
await this.jobQueue.deleteJob(monitor);
await this.db.deleteChecks(monitor._id);
await this.db.deletePageSpeedChecksByMonitorId(monitor._id);
await this.db.deleteNotificationsByMonitorId(monitor._id);
await this.db.deleteHardwareChecksByMonitorId(monitor._id);
const operations = [
{ name: "deleteJob", fn: () => this.jobQueue.deleteJob(monitor) },
{ name: "deleteChecks", fn: () => this.db.deleteChecks(monitor._id) },
{
name: "deletePageSpeedChecks",
fn: () => this.db.deletePageSpeedChecksByMonitorId(monitor._id),
},
{
name: "deleteNotifications",
fn: () => this.db.deleteNotificationsByMonitorId(monitor._id),
},
{
name: "deleteHardwareChecks",
fn: () => this.db.deleteHardwareChecksByMonitorId(monitor._id),
},
{
name: "deleteDistributedUptimeChecks",
fn: () => this.db.deleteDistributedChecksByMonitorId(monitor._id),
},
// TODO We don't actually want to delete the status page if there are other monitors in it
// We actually just want to remove the monitor being deleted from the status page.
// Only delete he status page if there are no other monitors in it.
{
name: "deleteStatusPages",
fn: () => this.db.deleteStatusPagesByMonitorId(monitor._id),
},
];
const results = await Promise.allSettled(operations.map((op) => op.fn()));
results.forEach((result, index) => {
if (result.status === "rejected") {
const operationName = operations[index].name;
logger.error({
message: `Failed to ${operationName} for monitor ${monitor._id}`,
service: SERVICE_NAME,
method: "deleteMonitor",
stack: result.reason.stack,
});
}
});
} catch (error) {
logger.error({
message: `Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`,
@@ -390,10 +427,10 @@ class MonitorController {
await Promise.all(
notifications &&
notifications.map(async (notification) => {
notification.monitorId = editedMonitor._id;
await this.db.createNotification(notification);
})
notifications.map(async (notification) => {
notification.monitorId = editedMonitor._id;
await this.db.createNotification(notification);
})
);
// Delete the old job(editedMonitor has the same ID as the old monitor)
@@ -12,4 +12,15 @@ const createDistributedCheck = async (checkData) => {
}
};
export { createDistributedCheck };
const deleteDistributedChecksByMonitorId = async (monitorId) => {
try {
const result = await DistributedUptimeCheck.deleteMany({ monitorId });
return result.deletedCount;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteDistributedChecksByMonitorId";
throw error;
}
};
export { createDistributedCheck, deleteDistributedChecksByMonitorId };
+73 -70
View File
@@ -313,7 +313,7 @@ const calculateGroupStats = (group) => {
avgResponseTime:
checksWithResponseTime.length > 0
? checksWithResponseTime.reduce((sum, check) => sum + check.responseTime, 0) /
checksWithResponseTime.length
checksWithResponseTime.length
: 0,
};
};
@@ -372,7 +372,10 @@ const getUptimeDetailsById = async (req) => {
const getDistributedUptimeDetailsById = async (req) => {
try {
const { monitorId } = req.params;
const { monitorId } = req?.params ?? {};
if (typeof monitorId === "undefined") {
throw new Error();
}
const monitor = await Monitor.findById(monitorId);
if (monitor === null || monitor === undefined) {
throw new Error(this.stringService.dbFindMonitorById(monitorId));
@@ -605,98 +608,98 @@ const getMonitorsByTeamId = async (req) => {
filteredMonitors: [
...(filter !== undefined
? [
{
$match: {
$or: [
{ name: { $regex: filter, $options: "i" } },
{ url: { $regex: filter, $options: "i" } },
],
{
$match: {
$or: [
{ name: { $regex: filter, $options: "i" } },
{ url: { $regex: filter, $options: "i" } },
],
},
},
},
]
]
: []),
{ $sort: sort },
{ $skip: skip },
...(rowsPerPage ? [{ $limit: rowsPerPage }] : []),
...(limit
? [
{
$lookup: {
from: "checks",
let: { monitorId: "$_id" },
pipeline: [
{
$match: {
$expr: { $eq: ["$monitorId", "$$monitorId"] },
{
$lookup: {
from: "checks",
let: { monitorId: "$_id" },
pipeline: [
{
$match: {
$expr: { $eq: ["$monitorId", "$$monitorId"] },
},
},
},
{ $sort: { createdAt: -1 } },
...(limit ? [{ $limit: limit }] : []),
],
as: "standardchecks",
{ $sort: { createdAt: -1 } },
...(limit ? [{ $limit: limit }] : []),
],
as: "standardchecks",
},
},
},
]
]
: []),
...(limit
? [
{
$lookup: {
from: "pagespeedchecks",
let: { monitorId: "$_id" },
pipeline: [
{
$match: {
$expr: { $eq: ["$monitorId", "$$monitorId"] },
{
$lookup: {
from: "pagespeedchecks",
let: { monitorId: "$_id" },
pipeline: [
{
$match: {
$expr: { $eq: ["$monitorId", "$$monitorId"] },
},
},
},
{ $sort: { createdAt: -1 } },
...(limit ? [{ $limit: limit }] : []),
],
as: "pagespeedchecks",
{ $sort: { createdAt: -1 } },
...(limit ? [{ $limit: limit }] : []),
],
as: "pagespeedchecks",
},
},
},
]
]
: []),
...(limit
? [
{
$lookup: {
from: "hardwarechecks",
let: { monitorId: "$_id" },
pipeline: [
{
$match: {
$expr: { $eq: ["$monitorId", "$$monitorId"] },
{
$lookup: {
from: "hardwarechecks",
let: { monitorId: "$_id" },
pipeline: [
{
$match: {
$expr: { $eq: ["$monitorId", "$$monitorId"] },
},
},
},
{ $sort: { createdAt: -1 } },
...(limit ? [{ $limit: limit }] : []),
],
as: "hardwarechecks",
{ $sort: { createdAt: -1 } },
...(limit ? [{ $limit: limit }] : []),
],
as: "hardwarechecks",
},
},
},
]
]
: []),
...(limit
? [
{
$lookup: {
from: "distributeduptimechecks",
let: { monitorId: "$_id" },
pipeline: [
{
$match: {
$expr: { $eq: ["$monitorId", "$$monitorId"] },
{
$lookup: {
from: "distributeduptimechecks",
let: { monitorId: "$_id" },
pipeline: [
{
$match: {
$expr: { $eq: ["$monitorId", "$$monitorId"] },
},
},
},
{ $sort: { createdAt: -1 } },
...(limit ? [{ $limit: limit }] : []),
],
as: "distributeduptimechecks",
{ $sort: { createdAt: -1 } },
...(limit ? [{ $limit: limit }] : []),
],
as: "distributeduptimechecks",
},
},
},
]
]
: []),
{
@@ -190,6 +190,16 @@ const deleteStatusPage = async (url) => {
}
};
const deleteStatusPagesByMonitorId = async (monitorId) => {
try {
await StatusPage.deleteMany({ monitors: { $in: [monitorId] } });
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteStatusPageByMonitorId";
throw error;
}
};
export {
createStatusPage,
updateStatusPage,
@@ -197,4 +207,5 @@ export {
getStatusPage,
getStatusPageByUrl,
deleteStatusPage,
deleteStatusPagesByMonitorId,
};
+14 -14
View File
@@ -11,7 +11,7 @@
"dependencies": {
"axios": "^1.7.2",
"bcrypt": "5.1.1",
"bullmq": "5.40.3",
"bullmq": "5.41.0",
"cors": "^2.8.5",
"dockerode": "4.0.4",
"dotenv": "^16.4.5",
@@ -36,7 +36,7 @@
"devDependencies": {
"@eslint/js": "^9.17.0",
"c8": "10.1.3",
"chai": "5.1.2",
"chai": "5.2.0",
"eslint": "^9.17.0",
"eslint-plugin-mocha": "^10.5.0",
"esm": "3.2.25",
@@ -1749,9 +1749,9 @@
}
},
"node_modules/bullmq": {
"version": "5.40.3",
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.40.3.tgz",
"integrity": "sha512-ZHGHilK/A8wo5HHlPbwyOWMCEMRsyEmwFubEu98Xb7IyELyJCDn2hXYQVemG8dqFovkgIxXmerf4y3OHcmwmOQ==",
"version": "5.41.0",
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.41.0.tgz",
"integrity": "sha512-GGfKu2DHGIvbnMtQjR/82wvWsdCaGxN5JGR3pvKd1mkDI9DsWn8r0+pAzZ6Y4ImWXFaetaAqywOhv2Ik0R2m3g==",
"license": "MIT",
"dependencies": {
"cron-parser": "^4.9.0",
@@ -1930,9 +1930,9 @@
"license": "CC-BY-4.0"
},
"node_modules/chai": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz",
"integrity": "sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==",
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz",
"integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5596,9 +5596,9 @@
}
},
"node_modules/mongoose": {
"version": "8.10.0",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.10.0.tgz",
"integrity": "sha512-nLhk3Qrv6q/HpD2k1O7kbBqsq+/kmKpdv5KJ+LLhQlII3e1p/SSLoLP6jMuSiU6+iLK7zFw4T1niAk3mA3QVug==",
"version": "8.10.1",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.10.1.tgz",
"integrity": "sha512-5beTeBZnJNndRXU9rxPol0JmTWZMAtgkPbooROkGilswvrZALDERY4cJrGZmgGwDS9dl0mxiB7si+Mv9Yms2fg==",
"license": "MIT",
"dependencies": {
"bson": "^6.10.1",
@@ -6893,9 +6893,9 @@
}
},
"node_modules/prettier": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.0.tgz",
"integrity": "sha512-quyMrVt6svPS7CjQ9gKb3GLEX/rl3BCL2oa/QkNcXv4YNVBC9olt3s+H7ukto06q7B1Qz46PbrKLO34PR6vXcA==",
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.1.tgz",
"integrity": "sha512-hPpFQvHwL3Qv5AdRvBFMhnKo4tYxp0ReXiPn2bxkiohEX6mBeBwEpBSQTkD458RaaDKQMYSp4hX4UtfUTA5wDw==",
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
+2 -2
View File
@@ -18,7 +18,7 @@
"dependencies": {
"axios": "^1.7.2",
"bcrypt": "5.1.1",
"bullmq": "5.40.3",
"bullmq": "5.41.0",
"cors": "^2.8.5",
"dockerode": "4.0.4",
"dotenv": "^16.4.5",
@@ -43,7 +43,7 @@
"devDependencies": {
"@eslint/js": "^9.17.0",
"c8": "10.1.3",
"chai": "5.1.2",
"chai": "5.2.0",
"eslint": "^9.17.0",
"eslint-plugin-mocha": "^10.5.0",
"esm": "3.2.25",
+25 -8
View File
@@ -1,4 +1,4 @@
import jmespath from 'jmespath';
import jmespath from "jmespath";
const SERVICE_NAME = "NetworkService";
const UPROCK_ENDPOINT = "https://api.uprock.com/checkmate/push";
@@ -123,7 +123,17 @@ class NetworkService {
*/
async requestHttp(job) {
try {
const { url, secret, _id, name, teamId, type, jsonPath, matchMethod, expectedValue } = job.data;
const {
url,
secret,
_id,
name,
teamId,
type,
jsonPath,
matchMethod,
expectedValue,
} = job.data;
const config = {};
secret !== undefined && (config.headers = { Authorization: `Bearer ${secret}` });
@@ -144,7 +154,8 @@ class NetworkService {
const code = error.response?.status || this.NETWORK_ERROR;
httpResponse.code = code;
httpResponse.status = false;
httpResponse.message = this.http.STATUS_CODES[code] || this.stringService.httpNetworkError;
httpResponse.message =
this.http.STATUS_CODES[code] || this.stringService.httpNetworkError;
return httpResponse;
}
@@ -164,13 +175,13 @@ class NetworkService {
service: this.SERVICE_NAME,
method: "requestHttp",
message: `Job: [${name}](${_id}) match result with expected value`,
details: { expectedValue, result, jsonPath, matchMethod }
details: { expectedValue, result, jsonPath, matchMethod },
});
if (jsonPath) {
const contentType = response.headers['content-type'];
const contentType = response.headers["content-type"];
const isJson = contentType?.includes('application/json');
const isJson = contentType?.includes("application/json");
if (!isJson) {
httpResponse.status = false;
httpResponse.message = this.stringService.httpNotJson;
@@ -199,7 +210,9 @@ class NetworkService {
else match = result === expectedValue;
httpResponse.status = match;
httpResponse.message = match ? this.stringService.httpMatchSuccess : this.stringService.httpMatchFail;
httpResponse.message = match
? this.stringService.httpMatchSuccess
: this.stringService.httpMatchFail;
return httpResponse;
} catch (error) {
error.service = this.SERVICE_NAME;
@@ -308,7 +321,8 @@ class NetworkService {
if (error) {
dockerResponse.status = false;
dockerResponse.code = error.statusCode || this.NETWORK_ERROR;
dockerResponse.message = error.reason || "Failed to fetch Docker container information";
dockerResponse.message =
error.reason || "Failed to fetch Docker container information";
return dockerResponse;
}
dockerResponse.status = response?.State?.Status === "running" ? true : false;
@@ -397,6 +411,9 @@ class NetworkService {
},
}
);
if (response.data.success === false) {
throw new Error(response.data.message);
}
} catch (error) {
console.log(error.message);
error.service = this.SERVICE_NAME;
+4 -4
View File
@@ -194,8 +194,8 @@ const createMonitorBodyValidation = joi.object({
}),
notifications: joi.array().items(joi.object()),
secret: joi.string(),
jsonPath: joi.string(),
expectedValue: joi.string(),
jsonPath: joi.string().allow(""),
expectedValue: joi.string().allow(""),
matchMethod: joi.string(),
});
@@ -205,8 +205,8 @@ const editMonitorBodyValidation = joi.object({
interval: joi.number(),
notifications: joi.array().items(joi.object()),
secret: joi.string(),
jsonPath: joi.string(),
expectedValue: joi.string(),
jsonPath: joi.string().allow(""),
expectedValue: joi.string().allow(""),
matchMethod: joi.string(),
});