Merge remote-tracking branch 'upstream/develop' into feat/be/webhook-integrations
This commit is contained in:
commit
07882e045b
63 files changed
+13694
-1342
No files matched your search
Generated
+12458
-670
File diff suppressed because it is too large.
Load diff
+7
-1
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,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;
|
||||
+95
@@ -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;
|
||||
+95
@@ -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;
|
||||
+31
@@ -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;
|
||||
+86
@@ -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;
|
||||
+36
-211
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
<Typography component="a" href="https://jmespath.org/" target="_blank" color="info">
|
||||
jmespath.org
|
||||
</Typography>
|
||||
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>
|
||||
</>
|
||||
|
||||
@@ -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
|
||||
<Typography component="a" href="https://jmespath.org/" target="_blank" color="info">
|
||||
jmespath.org
|
||||
</Typography>
|
||||
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}
|
||||
/>
|
||||
|
||||
@@ -107,6 +107,14 @@ const Routes = () => {
|
||||
</ProtectedDistributedUptimeRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/distributed-uptime/configure/:monitorId"
|
||||
element={
|
||||
<ProtectedDistributedUptimeRoute>
|
||||
<CreateDistributedUptime />
|
||||
</ProtectedDistributedUptimeRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/distributed-uptime/:monitorId"
|
||||
element={
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
Vendored
+12
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
@@ -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;"]
|
||||
@@ -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");
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
FROM redis
|
||||
EXPOSE 6379
|
||||
@@ -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"]
|
||||
@@ -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:
|
||||
|
||||

|
||||
|
||||
You can see the memory footprint of MongoDB and Redis on the same server (398Mb and 15Mb) for the same amount of servers:
|
||||
|
||||

|
||||
|
||||
|
||||
## 💚 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
|
||||
|
||||

|
||||
|
||||
-->
|
||||
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
|
||||
|
||||
[](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
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
@@ -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,
|
||||
};
|
||||
Generated
+14
-14
@@ -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
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user