purge distributed uptime

This commit is contained in:
Alex Holliday
2025-06-09 10:17:09 +08:00
parent 5690d91d59
commit 274595a533
70 changed files with 17 additions and 4494 deletions

View File

@@ -23,7 +23,6 @@ import UserSvg from "../../assets/icons/user.svg?react";
import TeamSvg from "../../assets/icons/user-two.svg?react";
import LogoutSvg from "../../assets/icons/logout.svg?react";
import Support from "../../assets/icons/support.svg?react";
import Account from "../../assets/icons/user-edit.svg?react";
import Maintenance from "../../assets/icons/maintenance.svg?react";
import Monitors from "../../assets/icons/monitors.svg?react";
import Incidents from "../../assets/icons/incidents.svg?react";
@@ -37,10 +36,8 @@ import ArrowLeft from "../../assets/icons/left-arrow.svg?react";
import DotsVertical from "../../assets/icons/dots-vertical.svg?react";
import ChangeLog from "../../assets/icons/changeLog.svg?react";
import Docs from "../../assets/icons/docs.svg?react";
import Folder from "../../assets/icons/folder.svg?react";
import StatusPages from "../../assets/icons/status-pages.svg?react";
import Discussions from "../../assets/icons/discussions.svg?react";
import DistributedUptimeIcon from "../../assets/icons/distributed-uptime.svg?react";
import "./index.css";
// Utils
@@ -56,11 +53,7 @@ const getMenu = (t) => [
{ name: t("menu.uptime"), path: "uptime", icon: <Monitors /> },
{ name: t("menu.pagespeed"), path: "pagespeed", icon: <PageSpeed /> },
{ name: t("menu.infrastructure"), path: "infrastructure", icon: <Integrations /> },
{
name: t("menu.distributedUptime"),
path: "distributed-uptime",
icon: <DistributedUptimeIcon />,
},
{ name: t("menu.incidents"), path: "incidents", icon: <Incidents /> },
{ name: t("menu.statusPages"), path: "status", icon: <StatusPages /> },
@@ -95,14 +88,13 @@ const URL_MAP = {
support: "https://discord.com/invite/NAb6H3UTjK",
discussions: "https://github.com/bluewave-labs/checkmate/discussions",
docs: "https://bluewavelabs.gitbook.io/checkmate",
changelog: "https://github.com/bluewave-labs/bluewave-uptime/releases",
changelog: "https://github.com/bluewave-labs/checkmate/releases",
};
const PATH_MAP = {
monitors: "Dashboard",
pagespeed: "Dashboard",
infrastructure: "Dashboard",
["distributed-uptime"]: "Dashboard",
account: "Account",
settings: "Settings",
};
@@ -375,12 +367,6 @@ function Sidebar() {
}}
>
{menu.map((item) => {
if (
item.path === "distributed-uptime" &&
distributedUptimeEnabled === false
) {
return null;
}
return item.path ? (
/* If item has a path */
<Tooltip

View File

@@ -1,58 +0,0 @@
import { useState, useEffect } from "react";
import { networkService } from "../main";
import { createToast } from "../Utils/toastUtils";
import { useSelector } from "react-redux";
import { useTheme } from "@emotion/react";
import { useMonitorUtils } from "./useMonitorUtils";
const useFetchDepinStatusPage = ({ url, timeFrame, isCreate = false }) => {
const [isLoading, setIsLoading] = useState(true);
const [networkError, setNetworkError] = useState(false);
const [statusPage, setStatusPage] = useState(undefined);
const [monitorId, setMonitorId] = useState(undefined);
const [isPublished, setIsPublished] = useState(false);
const { authToken } = useSelector((state) => state.auth);
const theme = useTheme();
const { getMonitorWithPercentage } = useMonitorUtils();
useEffect(() => {
if (isCreate) return;
const fetchStatusPageByUrl = async () => {
try {
const response = await networkService.getDistributedStatusPageByUrl({
authToken,
url,
type: "distributed",
timeFrame,
});
if (!response?.data?.data) return;
const statusPage = response.data.data;
const monitorsWithPercentage = statusPage?.subMonitors.map((monitor) =>
getMonitorWithPercentage(monitor, theme)
);
const statusPageWithSubmonitorPercentages = {
...statusPage,
subMonitors: monitorsWithPercentage,
};
setStatusPage(statusPageWithSubmonitorPercentages);
setMonitorId(statusPage?.monitors[0]);
setIsPublished(statusPage?.isPublished);
} catch (error) {
setNetworkError(true);
createToast({
body: error.message,
});
} finally {
setIsLoading(false);
}
};
fetchStatusPageByUrl();
}, [authToken, url, getMonitorWithPercentage, theme, timeFrame, isCreate]);
return [isLoading, networkError, statusPage, monitorId, isPublished];
};
export { useFetchDepinStatusPage };

View File

@@ -1,96 +0,0 @@
import { useState, useEffect } from "react";
import { networkService } from "../main";
import { createToast } from "../Utils/toastUtils";
const useSubscribeToDepinDetails = ({ monitorId, isPublic, isPublished, dateRange }) => {
const [isLoading, setIsLoading] = useState(true);
const [connectionStatus, setConnectionStatus] = useState(undefined);
const [retryCount, setRetryCount] = useState(0);
const [networkError, setNetworkError] = useState(false);
const [monitor, setMonitor] = useState(undefined);
const [lastUpdateTrigger, setLastUpdateTrigger] = useState(0);
useEffect(() => {
if (typeof monitorId === "undefined") {
return;
}
// If this page is public and not published, don't subscribe to details
if (isPublic && isPublished === false) {
return;
}
// Get initial data
const fetchInitialData = async () => {
try {
const res = await networkService.getDistributedUptimeDetails({
monitorId,
dateRange: dateRange,
normalize: true,
isPublic,
});
const responseData = res?.data?.data;
if (typeof responseData === "undefined") {
throw new Error("No data");
}
setConnectionStatus("up");
setLastUpdateTrigger(Date.now());
setMonitor(responseData);
} catch (error) {
setNetworkError(true);
setConnectionStatus("down");
createToast({
body: error.message,
});
} finally {
setIsLoading(false);
}
};
fetchInitialData();
try {
const cleanup = networkService.subscribeToDistributedUptimeDetails({
monitorId,
dateRange: dateRange,
normalize: true,
onUpdate: (data) => {
if (isLoading === true) {
setIsLoading(false);
}
if (networkError === true) {
setNetworkError(false);
}
setLastUpdateTrigger(Date.now());
setMonitor(data.monitor);
},
onOpen: () => {
setConnectionStatus("up");
setRetryCount(0); // Reset retry count on successful connection
},
onError: () => {
setIsLoading(false);
setNetworkError(true);
setConnectionStatus("down");
},
});
return cleanup;
} catch (error) {
setNetworkError(true);
}
}, [
dateRange,
monitorId,
retryCount,
setConnectionStatus,
networkError,
isLoading,
isPublic,
isPublished,
]);
return [isLoading, networkError, connectionStatus, monitor, lastUpdateTrigger];
};
export { useSubscribeToDepinDetails };

View File

@@ -1,95 +0,0 @@
import { useEffect, useState } from "react";
import { networkService } from "../main";
import { useSelector } from "react-redux";
import { useTheme } from "@emotion/react";
import { useMonitorUtils } from "./useMonitorUtils";
import { createToast } from "../Utils/toastUtils";
const useSubscribeToDepinMonitors = (page, rowsPerPage) => {
// Redux
const { user } = useSelector((state) => state.auth);
// Local state
const [isLoading, setIsLoading] = useState(true);
const [networkError, setNetworkError] = useState(false);
const [count, setCount] = useState(undefined);
const [monitors, setMonitors] = useState(undefined);
const theme = useTheme();
const { getMonitorWithPercentage } = useMonitorUtils();
useEffect(() => {
const fetchInitialData = async () => {
try {
const initialDataRes = await networkService.getDistributedUptimeMonitors({
teamId: user.teamId,
limit: 25,
types: ["distributed_http"],
page,
rowsPerPage,
});
const { count, monitors } = initialDataRes?.data?.data ?? {};
const responseData = initialDataRes?.data?.data;
if (typeof responseData === "undefined") throw new Error("No data");
const mappedMonitors = monitors?.map((monitor) =>
getMonitorWithPercentage(monitor, theme)
);
setMonitors(mappedMonitors);
setCount(count?.monitorsCount ?? 0);
} catch (error) {
setNetworkError(true);
createToast({
body: error.message,
});
} finally {
setIsLoading(false);
}
};
fetchInitialData();
try {
const cleanup = networkService.subscribeToDistributedUptimeMonitors({
teamId: user.teamId,
limit: 25,
types: [
typeof import.meta.env.VITE_DEPIN_TESTING === "undefined"
? "distributed_http"
: "distributed_test",
],
page,
rowsPerPage,
filter: null,
field: null,
order: null,
onUpdate: (data) => {
if (isLoading === true) {
setIsLoading(false);
}
const { count, monitors } = data;
const mappedMonitors = monitors.map((monitor) =>
getMonitorWithPercentage(monitor, theme)
);
setMonitors(mappedMonitors);
setCount(count?.monitorsCount ?? 0);
},
onError: () => {
setIsLoading(false);
setNetworkError(true);
},
});
return cleanup;
} catch (error) {
createToast({
body: error.message,
});
setNetworkError(true);
}
}, [user, getMonitorWithPercentage, theme, isLoading, page, rowsPerPage]);
return [monitors, count, isLoading, networkError];
};
export { useSubscribeToDepinMonitors };

View File

@@ -1,31 +0,0 @@
import { useState } from "react";
import { networkService } from "../../../../main";
import { useSelector } from "react-redux";
import { createToast } from "../../../../Utils/toastUtils";
const useCreateDistributedUptimeMonitor = ({ isCreate, monitorId }) => {
const [isLoading, setIsLoading] = useState(false);
const [networkError, setNetworkError] = useState(false);
const createDistributedUptimeMonitor = async ({ form }) => {
setIsLoading(true);
try {
if (isCreate) {
await networkService.createMonitor({ monitor: form });
} else {
await networkService.updateMonitor({ 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 };

View File

@@ -1,31 +0,0 @@
import { useEffect, useState } from "react";
import { networkService } from "../../../../main";
import { createToast } from "../../../../Utils/toastUtils";
export const useMonitorFetch = ({ 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({
monitorId: monitorId,
normalize: true,
});
setMonitor(res?.data?.data ?? {});
} catch (error) {
setNetworkError(true);
createToast({ body: error.message });
} finally {
setIsLoading(false);
}
};
fetchMonitors();
}, [monitorId, isCreate]);
return [monitor, isLoading, networkError];
};
export default useMonitorFetch;

View File

@@ -1,349 +0,0 @@
// Components
import { Box, Stack, Typography, Button, ButtonGroup } from "@mui/material";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import ConfigBox from "../../../Components/ConfigBox";
import TextInput from "../../../Components/Inputs/TextInput";
import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments";
import Radio from "../../../Components/Inputs/Radio";
import Checkbox from "../../../Components/Inputs/Checkbox";
import Select from "../../../Components/Inputs/Select";
import { createToast } from "../../../Utils/toastUtils";
// Utility
import { useTheme } from "@emotion/react";
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useSelector } from "react-redux";
import { monitorValidation } from "../../../Validation/validation";
import { useParams } from "react-router-dom";
import { useCreateDistributedUptimeMonitor } from "./Hooks/useCreateDistributedUptimeMonitor";
import { useMonitorFetch } from "./Hooks/useMonitorFetch";
import { useTranslation } from "react-i18next";
// Constants
const MS_PER_MINUTE = 60000;
const SELECT_VALUES = [
{ _id: 1, name: "1 minute" },
{ _id: 2, name: "2 minutes" },
{ _id: 3, name: "3 minutes" },
{ _id: 4, name: "4 minutes" },
{ _id: 5, name: "5 minutes" },
];
const parseUrl = (url) => {
try {
return new URL(url);
} catch (error) {
return null;
}
};
const CreateDistributedUptime = () => {
const { monitorId } = useParams();
const isCreate = typeof monitorId === "undefined";
const { t } = useTranslation();
let BREADCRUMBS = [
{ name: `distributed uptime`, path: "/distributed-uptime" },
...(isCreate ? [] : [{ name: "details", path: `/distributed-uptime/${monitorId}` }]),
{ name: isCreate ? "create" : "configure", path: `` },
];
// Redux state
const { user } = useSelector((state) => state.auth);
// Local state
const [https, setHttps] = useState(true);
const [notifications, setNotifications] = useState([]);
const [form, setForm] = useState({
type: "distributed_http",
name: "",
url: "",
interval: 1,
});
const [errors, setErrors] = useState({});
//utils
const theme = useTheme();
const navigate = useNavigate();
const [createDistributedUptimeMonitor, isLoading, networkError] =
useCreateDistributedUptimeMonitor({ isCreate, monitorId });
const [monitor, monitorIsLoading, monitorNetworkError] = useMonitorFetch({
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 () => {
const monitorToSubmit = { ...form };
// Prepend protocol to url
monitorToSubmit.url = `http${https ? "s" : ""}://` + monitorToSubmit.url;
const { error } = monitorValidation.validate(monitorToSubmit, {
abortEarly: false,
});
if (error) {
const newErrors = {};
error.details.forEach((err) => {
newErrors[err.path[0]] = err.message;
});
setErrors(newErrors);
createToast({ body: "Please check the form for errors." });
return;
}
// Append needed fields
monitorToSubmit.description = form.name;
monitorToSubmit.interval = form.interval * MS_PER_MINUTE;
monitorToSubmit.teamId = user.teamId;
monitorToSubmit.userId = user._id;
monitorToSubmit.notifications = notifications;
const success = await createDistributedUptimeMonitor({ form: monitorToSubmit });
if (success) {
createToast({ body: "Monitor created successfully!" });
navigate("/distributed-uptime");
} else {
createToast({ body: "Failed to create monitor." });
}
};
const handleChange = (event) => {
let { name, value } = event.target;
setForm({
...form,
[name]: value,
});
const { error } = monitorValidation.validate(
{ [name]: value },
{ abortEarly: false }
);
setErrors((prev) => ({
...prev,
...(error ? { [name]: error.details[0].message } : { [name]: undefined }),
}));
};
const handleNotifications = (event, type) => {
const { value } = event.target;
let currentNotifications = [...notifications];
const notificationAlreadyExists = notifications.some((notification) => {
if (notification.type === type && notification.address === value) {
return true;
}
return false;
});
if (notificationAlreadyExists) {
currentNotifications = currentNotifications.filter((notification) => {
if (notification.type === type && notification.address === value) {
return false;
}
return true;
});
} else {
currentNotifications.push({ type, address: value });
}
setNotifications(currentNotifications);
};
return (
<Box>
<Breadcrumbs list={BREADCRUMBS} />
<Stack
component="form"
gap={theme.spacing(12)}
mt={theme.spacing(6)}
onSubmit={() => console.log("submit")}
>
<Typography
component="h1"
variant="h1"
>
<Typography
component="span"
fontSize="inherit"
>
{isCreate ? "Create your" : "Edit your"}{" "}
</Typography>
<Typography
component="span"
variant="h2"
fontSize="inherit"
fontWeight="inherit"
>
{t("monitor")}
</Typography>
</Typography>
<ConfigBox>
<Box>
<Typography component="h2">{t("settingsGeneralSettings")}</Typography>
<Typography component="p">{t("distributedUptimeCreateSelectURL")}</Typography>
</Box>
<Stack gap={theme.spacing(15)}>
<TextInput
type={"url"}
id="monitor-url"
startAdornment={<HttpAdornment https={https} />}
label="URL to monitor"
https={https}
placeholder={"www.google.com"}
disabled={!isCreate}
value={form.url}
name="url"
onChange={handleChange}
error={errors["url"] ? true : false}
helperText={errors["url"]}
/>
<TextInput
type="text"
id="monitor-name"
label="Display name"
isOptional={true}
placeholder={"Google"}
value={form.name}
name="name"
onChange={handleChange}
error={errors["name"] ? true : false}
helperText={errors["name"]}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2">{t("distributedUptimeCreateChecks")}</Typography>
<Typography component="p">
{t("distributedUptimeCreateChecksDescription")}
</Typography>
</Box>
<Stack gap={theme.spacing(12)}>
<Stack gap={theme.spacing(6)}>
<Radio
id="monitor-checks-http"
title="Website monitoring"
desc="Use HTTP(s) to monitor your website or API endpoint."
size="small"
value="http"
name="type"
checked={true}
onChange={handleChange}
/>
{form.type === "http" || form.type === "distributed_http" ? (
<ButtonGroup
disabled={!isCreate}
sx={{ ml: theme.spacing(16) }}
>
<Button
variant="group"
filled={https.toString()}
onClick={() => setHttps(true)}
>
{t("https")}
</Button>
<Button
variant="group"
filled={(!https).toString()}
onClick={() => setHttps(false)}
>
{t("http")}
</Button>
</ButtonGroup>
) : (
""
)}
</Stack>
{errors["type"] ? (
<Box className="error-container">
<Typography
component="p"
className="input-error"
color={theme.palette.error.contrastText}
>
{errors["type"]}
</Typography>
</Box>
) : (
""
)}
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2">
{t("distributedUptimeCreateIncidentNotification")}
</Typography>
<Typography component="p">
{t("distributedUptimeCreateIncidentDescription")}
</Typography>
</Box>
<Stack gap={theme.spacing(6)}>
<Checkbox
id="notify-email-default"
label={`Notify via email (to ${user.email})`}
isChecked={notifications.some(
(notification) => notification.type === "email"
)}
value={user?.email}
onChange={(event) => handleNotifications(event, "email")}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2">
{t("distributedUptimeCreateAdvancedSettings")}
</Typography>
</Box>
<Stack gap={theme.spacing(12)}>
<Select
id="monitor-interval"
label="Check frequency"
name="interval"
value={form.interval}
onChange={handleChange}
items={SELECT_VALUES}
/>
</Stack>
</ConfigBox>
<Stack
direction="row"
justifyContent="flex-end"
>
<Button
variant="contained"
color="accent"
onClick={() => handleCreateMonitor()}
disabled={!Object.values(errors).every((value) => value === undefined)}
loading={isLoading}
>
{isCreate ? "Create monitor" : "Configure monitor"}
</Button>
</Stack>
</Stack>
</Box>
);
};
export default CreateDistributedUptime;

View File

@@ -1,50 +0,0 @@
// Components
import { Stack, Typography } from "@mui/material";
import { ColContainer } from "../../../../../Components/StandardContainer";
import SmartToyIcon from "@mui/icons-material/SmartToy";
import Dot from "../../../../../Components/Dot";
// Utils
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
const MESSAGES = [
"I've checked the network status, and we're seeing excellent performance across all regions.",
"The network is stable and functioning optimally. All connections are active and stable.",
"I've reviewed the network status, and everything looks great. No issues detected.",
"The network is up and running smoothly. All connections are active and stable.",
"I've checked the network status, and everything is looking good. No issues detected.",
];
const ChatBot = ({ sx }) => {
const theme = useTheme();
const { t } = useTranslation();
return (
<ColContainer
backgroundColor={theme.palette.chatbot.background}
sx={{ ...sx }}
>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(4)}
>
<SmartToyIcon sx={{ color: theme.palette.chatbot.textAccent }} />
<Typography color={theme.palette.chatbot.textAccent}>Status Bot</Typography>
<Dot
color={theme.palette.chatbot.textAccent}
style={{ opacity: 0.4 }}
/>
<Typography
variant="body2"
color={theme.palette.chatbot.textAccent}
sx={{ opacity: 0.4 }}
>
{t("now")}
</Typography>
</Stack>
<Typography>{MESSAGES[Math.floor(Math.random() * MESSAGES.length)]}</Typography>
</ColContainer>
);
};
export default ChatBot;

View File

@@ -1,92 +0,0 @@
// Components
import { Box, Stack, 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 PropTypes from "prop-types";
import { useTranslation } from "react-i18next";
const Controls = ({ isDeleteOpen, setIsDeleteOpen, isDeleting, monitorId }) => {
const theme = useTheme();
const navigate = useNavigate();
const { t } = useTranslation();
return (
<Stack
direction="row"
gap={theme.spacing(2)}
>
<Box>
<Button
variant="contained"
color="error"
onClick={() => setIsDeleteOpen(!isDeleteOpen)}
loading={isDeleting}
>
{t("delete")}
</Button>
</Box>
<Box>
<Button
variant="contained"
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 /> {t("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;

View File

@@ -1,98 +0,0 @@
// Components
import { Stack, Typography } from "@mui/material";
import PulseDot from "../../../../../Components/Animated/PulseDot";
import "flag-icons/css/flag-icons.min.css";
import { ColContainer } from "../../../../../Components/StandardContainer";
// Utils
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
const DeviceTicker = ({ data, width = "100%", connectionStatus }) => {
const theme = useTheme();
const { t } = useTranslation();
const statusColor = {
up: theme.palette.success.main,
down: theme.palette.error.main,
undefined: theme.palette.warning.main,
};
return (
<ColContainer sx={{ height: "100%" }}>
<Stack
direction="row"
gap={theme.spacing(4)}
>
<PulseDot color={statusColor[connectionStatus]} />
<Typography
variant="h1"
mb={theme.spacing(8)}
sx={{ alignSelf: "center" }}
>
{connectionStatus === "up" ? "Connected" : "Connecting..."}
</Typography>
</Stack>
<div
style={{
overflowX: "auto",
maxWidth: "100%",
// Optional: add a max height if you want vertical scrolling too
// maxHeight: '400px',
// overflowY: 'auto'
}}
>
<table style={{ width: "100%" }}>
<thead>
<tr>
<th style={{ textAlign: "left", paddingLeft: theme.spacing(4) }}>
<Typography>{t("country")}</Typography>
</th>
<th style={{ textAlign: "left", paddingLeft: theme.spacing(4) }}>
<Typography>{t("city")}</Typography>
</th>
<th style={{ textAlign: "right", paddingLeft: theme.spacing(4) }}>
<Typography>{t("response")}</Typography>
</th>
<th style={{ textAlign: "right", paddingLeft: theme.spacing(4) }}>
<Typography sx={{ whiteSpace: "nowrap" }}>{"UPT BURNED"}</Typography>
</th>
</tr>
</thead>
<tbody>
{data.map((dataPoint) => {
const countryCode = dataPoint?.countryCode?.toLowerCase() ?? null;
const flag = countryCode ? `fi fi-${countryCode}` : null;
const city = dataPoint?.city !== "" ? dataPoint?.city : "Unknown";
return (
<tr key={Math.random()}>
<td style={{ paddingLeft: theme.spacing(4) }}>
<Typography>
{flag ? <span className={flag} /> : null}{" "}
{countryCode?.toUpperCase() ?? "N/A"}
</Typography>
</td>
<td style={{ paddingLeft: theme.spacing(4) }}>
<Typography>{city}</Typography>
</td>
<td style={{ textAlign: "right", paddingLeft: theme.spacing(4) }}>
<Typography>
{Math.floor(dataPoint.responseTime)} {t("ms")}
</Typography>
</td>
<td style={{ textAlign: "right", paddingLeft: theme.spacing(4) }}>
<Typography color={theme.palette.warning.main}>
+{dataPoint.uptBurnt}
</Typography>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</ColContainer>
);
};
export default DeviceTicker;

View File

@@ -1,169 +0,0 @@
{
"id": "43f36e14-e3f5-43c1-84c0-50a9c80dc5c7",
"name": "MapLibre",
"zoom": 0.861983335785597,
"pitch": 0,
"center": [17.6543171043124, 32.9541203267468],
"glyphs": "https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf",
"layers": [
{
"id": "background",
"type": "background",
"paint": {
"background-color": "#121217"
},
"filter": ["all"],
"layout": {
"visibility": "visible"
},
"maxzoom": 24
},
{
"id": "coastline",
"type": "line",
"paint": {
"line-blur": 0.5,
"line-color": "#000000",
"line-width": {
"stops": [
[0, 2],
[6, 6],
[14, 9],
[22, 18]
]
}
},
"filter": ["all"],
"layout": {
"line-cap": "round",
"line-join": "round",
"visibility": "visible"
},
"source": "maplibre",
"maxzoom": 24,
"minzoom": 0,
"source-layer": "countries"
},
{
"id": "countries-fill",
"type": "fill",
"paint": {
"fill-color": "#292929"
},
"filter": ["all"],
"layout": {
"visibility": "visible"
},
"source": "maplibre",
"maxzoom": 24,
"source-layer": "countries"
},
{
"id": "countries-boundary",
"type": "line",
"paint": {
"line-color": "#484848",
"line-width": {
"stops": [
[1, 1],
[6, 2],
[14, 6],
[22, 12]
]
},
"line-opacity": {
"stops": [
[3, 0.5],
[6, 1]
]
}
},
"layout": {
"line-cap": "round",
"line-join": "round",
"visibility": "visible"
},
"source": "maplibre",
"maxzoom": 24,
"source-layer": "countries"
},
{
"id": "countries-label",
"type": "symbol",
"paint": {
"text-color": "rgba(8, 37, 77, 1)",
"text-halo-blur": {
"stops": [
[2, 0.2],
[6, 0]
]
},
"text-halo-color": "rgba(255, 255, 255, 1)",
"text-halo-width": {
"stops": [
[2, 1],
[6, 1.6]
]
}
},
"filter": ["all"],
"layout": {
"text-font": ["Open Sans Semibold"],
"text-size": {
"stops": [
[2, 10],
[4, 12],
[6, 16]
]
},
"text-field": {
"stops": [
[2, "{ABBREV}"],
[4, "{NAME}"]
]
},
"visibility": "visible",
"text-max-width": 10,
"text-transform": {
"stops": [
[0, "uppercase"],
[2, "none"]
]
}
},
"source": "maplibre",
"maxzoom": 24,
"minzoom": 2,
"source-layer": "centroids"
},
{
"id": "data-dots",
"type": "circle",
"source": "data-dots",
"paint": {
"circle-radius": 3,
"circle-color": ["get", "color"],
"circle-opacity": 0.5
}
}
],
"bearing": 0,
"sources": {
"maplibre": {
"url": "https://demotiles.maplibre.org/tiles/tiles.json",
"type": "vector"
},
"data-dots": {
"type": "geojson",
"data": {
"type": "FeatureCollection",
"features": []
}
}
},
"version": 8,
"metadata": {
"maptiler:copyright": "This style was generated on MapTiler Cloud. Usage is governed by the license terms in https://github.com/maplibre/demotiles/blob/gh-pages/LICENSE",
"openmaptiles:version": "3.x"
}
}

View File

@@ -1,33 +0,0 @@
import baseStyle from "./DistributedUptimeMapStyle.json";
const buildStyle = (theme, mode) => {
const style = JSON.parse(JSON.stringify(baseStyle));
if (mode === "dark") {
return baseStyle;
}
if (style.layers) {
const newLayers = style.layers.map((layer) => {
if (layer.id === "background") {
layer.paint["background-color"] = theme.palette.map.main;
}
if (layer.id === "countries-fill") {
layer.paint["fill-color"] = theme.palette.map.lowContrast;
}
if (layer.id === "coastline") {
layer.paint["line-color"] = theme.palette.map.highContrast;
}
if (layer.id === "countries-boundary") {
layer.paint["line-color"] = theme.palette.map.highContrast;
}
return layer;
});
style.layers = newLayers;
}
return style;
};
export default buildStyle;

View File

@@ -1,118 +0,0 @@
import "maplibre-gl/dist/maplibre-gl.css";
import PropTypes from "prop-types";
import { useRef, useState, useEffect } from "react";
import { useTheme } from "@mui/material/styles";
import maplibregl from "maplibre-gl";
import { useSelector } from "react-redux";
import buildStyle from "./buildStyle";
const DistributedUptimeMap = ({
width = "100%",
checks,
height,
minHeight = "350px",
}) => {
const mapContainer = useRef(null);
const map = useRef(null);
const theme = useTheme();
const [mapLoaded, setMapLoaded] = useState(false);
const mode = useSelector((state) => state.ui.mode);
const initialTheme = useRef(theme);
const initialMode = useRef(mode);
const colorLookup = (avgResponseTime) => {
if (avgResponseTime <= 150) {
return "#00FF00"; // Green
} else if (avgResponseTime <= 250) {
return "#FFFF00"; // Yellow
} else {
return "#FF0000"; // Red
}
};
useEffect(() => {
if (mapContainer.current && !map.current) {
const initialStyle = buildStyle(initialTheme.current, initialMode.current);
map.current = new maplibregl.Map({
container: mapContainer.current,
style: initialStyle,
center: [0, 20],
zoom: 0.8,
attributionControl: false,
canvasContextAttributes: {
antialias: true,
preserveDrawingBuffer: true,
},
});
}
map.current.on("load", () => {
setMapLoaded(true);
});
return () => {
if (map.current) {
map.current.remove();
map.current = null;
}
};
}, []);
useEffect(() => {
const style = buildStyle(theme, mode);
if (map.current && mapLoaded) {
map.current.setStyle(style);
}
}, [theme, mode, mapLoaded]);
useEffect(() => {
if (map.current && checks?.length > 0) {
// Convert dots to GeoJSON
const geojson = {
type: "FeatureCollection",
features: checks.map((check) => {
return {
type: "Feature",
geometry: {
type: "Point",
coordinates: [check._id.lng, check._id.lat],
},
properties: {
color: theme.palette.accent.main,
// color: colorLookup(check.avgResponseTime) || "blue", // Default to blue if no color specified
},
};
}),
};
// Update the source with new dots
const source = map.current.getSource("data-dots");
if (source) {
source.setData(geojson);
}
}
}, [checks, theme, mapLoaded]);
return (
<div
ref={mapContainer}
style={{
width: width,
height: height,
minHeight: minHeight,
borderRadius: theme.spacing(4),
borderColor: theme.palette.primary.lowContrast,
borderStyle: "solid",
borderWidth: 1,
}}
/>
);
};
DistributedUptimeMap.propTypes = {
checks: PropTypes.array,
width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
minHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};
export default DistributedUptimeMap;

View File

@@ -1,95 +0,0 @@
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.date"
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;

View File

@@ -1,95 +0,0 @@
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.date"
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;

View File

@@ -1,31 +0,0 @@
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;

View File

@@ -1,89 +0,0 @@
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";
import { useTranslation } from "react-i18next";
const CustomToolTip = ({ active, payload, label }) => {
const uiTimezone = useSelector((state) => state.ui.timezone);
const theme = useTheme();
const { t } = useTranslation();
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 }}
>
{t("responseTime")}
</Typography>
<Typography component="span">
{Math.floor(responseTime)}
<Typography
component="span"
sx={{ opacity: 0.8 }}
>
{t("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.oneOfType([PropTypes.string, PropTypes.object]),
avgResponseTime: PropTypes.number,
originalAvgResponseTime: PropTypes.number,
}),
})
),
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};
export default CustomToolTip;

View File

@@ -1,53 +0,0 @@
//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";
// Utils
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next";
const DistributedUptimeResponseChart = ({ checks }) => {
const [chartType, setChartType] = useState("bar");
const { t } = useTranslation();
let Chart = null;
if (chartType === "area") {
Chart = DistributedUptimeResponseAreaChart;
}
if (chartType === "bar") {
Chart = DistributedUptimeResponseBarChart;
}
return (
<Stack>
<Stack
direction="row"
alignItems="center"
>
<Typography>{t("bar")}</Typography>
<Switch
color="main"
value={chartType}
onChange={(e) => setChartType(e.target.checked ? "area" : "bar")}
/>
<Typography>{t("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;

View File

@@ -1,34 +0,0 @@
import { Stack, Typography, Box } from "@mui/material";
import SolanaLogo from "../../../../../assets/icons/solana_logo.svg?react";
import { useTheme } from "@mui/material/styles";
import { useTranslation } from "react-i18next";
const Footer = () => {
const theme = useTheme();
const { t } = useTranslation();
return (
<Stack
justifyContent="space-between"
alignItems="center"
spacing={2}
>
<Typography variant="h2">{t("distributedUptimeDetailsFooterHeading")}</Typography>
<Stack
width="100%"
direction="row"
gap={theme.spacing(2)}
justifyContent="center"
alignItems="center"
>
<Typography variant="h2">{t("distributedUptimeDetailsFooterBuilt")}</Typography>
<SolanaLogo
width={15}
height={15}
/>
<Typography variant="h2">{t("distributedUptimeDetailsFooterSolana")}</Typography>
</Stack>
</Stack>
);
};
export default Footer;

View File

@@ -1,15 +0,0 @@
import { useState, useEffect } from "react";
const LastUpdate = ({ suffix, lastUpdateTime, trigger }) => {
const [elapsedMs, setElapsedMs] = useState(lastUpdateTime);
useEffect(() => {
setElapsedMs(lastUpdateTime);
const timer = setInterval(() => {
setElapsedMs((prev) => prev + 1000);
}, 1000);
return () => clearInterval(timer);
}, [lastUpdateTime, trigger]);
return `${Math.floor(elapsedMs / 1000)} ${suffix}`;
};
export default LastUpdate;

View File

@@ -1,23 +0,0 @@
import { Stack, Typography } from "@mui/material";
import { useTheme } from "@mui/material/styles";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next";
const MonitorHeader = ({ monitor }) => {
const theme = useTheme();
const { t } = useTranslation();
return (
<Stack direction="row">
<Stack gap={theme.spacing(2)}>
<Typography variant="h1">{monitor.name}</Typography>
<Typography variant="h2">{t("distributedUptimeDetailsMonitorHeader")}</Typography>
</Stack>
</Stack>
);
};
MonitorHeader.propTypes = {
monitor: PropTypes.object,
};
export default MonitorHeader;

View File

@@ -1,30 +0,0 @@
import { LinearProgress } from "@mui/material";
import { useState, useEffect } from "react";
const NextExpectedCheck = ({ lastUpdateTime, interval, trigger }) => {
const [elapsedMs, setElapsedMs] = useState(lastUpdateTime);
useEffect(() => {
setElapsedMs(lastUpdateTime);
const timer = setInterval(() => {
setElapsedMs((prev) => {
const newElapsedMs = prev + 100;
return newElapsedMs;
});
}, 100);
return () => clearInterval(timer);
}, [interval, trigger]);
return (
<LinearProgress
variant="determinate"
color="accent"
value={Math.min((elapsedMs / interval) * 100, 100)}
sx={{
transition: "width 1s linear", // Smooth transition over 1 second
}}
/>
);
};
export default NextExpectedCheck;

View File

@@ -1,14 +0,0 @@
import { Stack, Skeleton } from "@mui/material";
export const SkeletonLayout = () => {
return (
<Stack>
<Skeleton
variant="rectangular"
height={"90vh"}
/>
</Stack>
);
};
export default SkeletonLayout;

View File

@@ -1,60 +0,0 @@
// Components
import { Stack } from "@mui/material";
import InfoBox from "../../../../../Components/InfoBox";
import LastUpdate from "../LastUpdate";
// Utils
import { useTheme } from "@mui/material/styles";
import PropTypes from "prop-types";
const StatBoxes = ({ monitor, lastUpdateTrigger }) => {
const theme = useTheme();
return (
<Stack
direction="row"
justifyContent="space-between"
flexWrap="wrap"
gap={theme.spacing(8)}
>
<InfoBox
sx={{ flex: 1 }}
heading="Avg Response Time"
subHeading={`${Math.floor(monitor?.avgResponseTime ?? 0)} ms`}
/>
<InfoBox
sx={{ flex: 1 }}
heading="Checking every"
subHeading={`${(monitor?.interval ?? 0) / 1000} seconds`}
/>
<InfoBox
sx={{ flex: 1 }}
heading={"Last check"}
subHeading={
<LastUpdate
lastUpdateTime={monitor?.timeSinceLastCheck}
suffix={"seconds ago"}
/>
}
/>
<InfoBox
sx={{ flex: 1 }}
heading="Last server push"
subHeading={
<LastUpdate
suffix={"seconds ago"}
lastUpdateTime={0}
trigger={lastUpdateTrigger}
/>
}
/>
</Stack>
);
};
StatBoxes.propTypes = {
monitor: PropTypes.object,
lastUpdateTrigger: PropTypes.number,
};
export default StatBoxes;

View File

@@ -1,94 +0,0 @@
// Components
import { ColContainer } from "../../../../../Components/StandardContainer";
import { Stack, Typography } from "@mui/material";
import PulseDot from "../../../../../Components/Animated/PulseDot";
import LastUpdate from "../LastUpdate";
import ChatBot from "../Chatbot";
import ShareComponent from "../../../../../Components/ShareComponent";
// Utils
import { useTheme } from "@emotion/react";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next";
const StatusHeader = ({ monitor, connectionStatus, elementToCapture }) => {
const theme = useTheme();
const { t } = useTranslation();
const COLOR_MAP = {
up: theme.palette.successSecondary.main,
down: theme.palette.error.lowContrast,
};
const MSG_MAP = {
up: "All Systems Operational",
down: "Last Check Failed",
};
const PULSE_COLOR = {
up: theme.palette.success.main,
down: theme.palette.error.main,
};
let bgColor = COLOR_MAP[connectionStatus];
return (
<ColContainer backgroundColor={bgColor}>
<Stack
direction={{ s: "column", md: "row" }}
justifyContent="space-between"
gap={theme.spacing(8)}
>
<Stack
direction="row"
gap={theme.spacing(8)}
>
<PulseDot color={PULSE_COLOR[connectionStatus]} />
<Stack>
<Stack
direction="row"
gap={theme.spacing(8)}
>
<Typography
variant="h1"
color={theme.palette.success.lowContrast}
>
{MSG_MAP[connectionStatus]}
</Typography>
<Typography
variant="body2"
borderRadius={theme.spacing(8)}
padding={theme.spacing(4)}
backgroundColor={theme.palette.successSecondary.lowContrast}
color={theme.palette.success.lowContrast}
>
{t("distributedUptimeDetailsStatusHeaderUptime")}{" "}
{(monitor.uptimePercentage * 100).toFixed(2)}%
</Typography>
</Stack>
<Typography
variant="body2"
color={theme.palette.success.lowContrast}
>
{t("distributedUptimeDetailsStatusHeaderLastUpdate")}{" "}
<LastUpdate
suffix={"seconds ago"}
lastUpdateTime={monitor?.timeSinceLastCheck}
/>
</Typography>
</Stack>
</Stack>
<ShareComponent
elementToCapture={elementToCapture}
fileName={monitor.name}
/>
</Stack>
<ChatBot sx={{ marginTop: theme.spacing(10) }} />
</ColContainer>
);
};
StatusHeader.propTypes = {
monitor: PropTypes.object,
connectionStatus: PropTypes.string,
};
export default StatusHeader;

View File

@@ -1,25 +0,0 @@
import { useState } from "react";
import { networkService } from "../../../../main";
import { createToast } from "../../../../Utils/toastUtils";
const useDeleteMonitor = ({ monitorId }) => {
const [isLoading, setIsLoading] = useState(false);
const deleteMonitor = async () => {
try {
setIsLoading(true);
await networkService.deleteMonitorById({ monitorId });
return true;
} catch (error) {
createToast({
body: error.message,
});
return false;
} finally {
setIsLoading(false);
}
};
return [deleteMonitor, isLoading];
};
export { useDeleteMonitor };

View File

@@ -1,148 +0,0 @@
//Components
import DistributedUptimeMap from "./Components/DistributedUptimeMap";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import { Stack, Typography } from "@mui/material";
import DeviceTicker from "./Components/DeviceTicker";
import ResponseTimeChart from "./Components/DistributedUptimeResponseChart";
import NextExpectedCheck from "./Components/NextExpectedCheck";
import Footer from "./Components/Footer";
import StatBoxes from "./Components/StatBoxes";
import MonitorHeader from "./Components/MonitorHeader";
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 { useSubscribeToDepinDetails } from "../../../Hooks/useSubscribeToDepinDetails";
import { useDeleteMonitor } from "./Hooks/useDeleteMonitor";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
const DistributedUptimeDetails = () => {
const { monitorId } = useParams();
// Local State
const [dateRange, setDateRange] = useState("recent");
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
// Utils
const theme = useTheme();
const isAdmin = useIsAdmin();
const { t } = useTranslation();
const navigate = useNavigate();
const [isLoading, networkError, connectionStatus, monitor, lastUpdateTrigger] =
useSubscribeToDepinDetails({ monitorId, dateRange });
const [deleteMonitor, isDeleting] = useDeleteMonitor({ monitorId });
// Constants
const BREADCRUMBS = [
{ name: "Distributed Uptime", path: "/distributed-uptime" },
{ name: "Details", path: `/distributed-uptime/${monitorId}` },
];
if (isLoading) {
return <SkeletonLayout />;
}
if (networkError) {
return (
<GenericFallback>
<Typography
variant="h1"
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
{t("networkError")}
</Typography>
<Typography>{t("checkConnection")}</Typography>
</GenericFallback>
);
}
if (
typeof monitor === "undefined" ||
typeof monitor?.totalChecks === "undefined" ||
monitor?.totalChecks === 0
) {
return (
<Stack gap={theme.spacing(10)}>
<Breadcrumbs list={BREADCRUMBS} />
<GenericFallback>
<Typography>{t("distributedUptimeDetailsNoMonitorHistory")}</Typography>
</GenericFallback>
</Stack>
);
}
return (
<Stack
direction="column"
gap={theme.spacing(10)}
>
<Breadcrumbs list={BREADCRUMBS} />
<MonitorCreateHeader
label="Create status page"
isAdmin={isAdmin}
path={`/status/distributed/create/${monitorId}`}
/>
<ControlsHeader
isDeleting={isDeleting}
isDeleteOpen={isDeleteOpen}
setIsDeleteOpen={setIsDeleteOpen}
monitorId={monitorId}
/>
<MonitorHeader monitor={monitor} />
<StatBoxes
monitor={monitor}
lastUpdateTrigger={lastUpdateTrigger}
/>
<NextExpectedCheck
lastUpdateTime={monitor?.timeSinceLastCheck ?? 0}
interval={monitor?.interval ?? 0}
trigger={lastUpdateTrigger}
/>
<MonitorTimeFrameHeader
dateRange={dateRange}
setDateRange={setDateRange}
/>
<ResponseTimeChart checks={monitor?.groupedChecks ?? []} />
<Stack
direction="row"
gap={theme.spacing(8)}
>
<DistributedUptimeMap
checks={monitor?.groupedMapChecks ?? []}
height={"100%"}
width={"100%"}
/>
<DeviceTicker
width={"25vw"}
data={monitor?.latestChecks ?? []}
connectionStatus={connectionStatus}
/>
</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>
);
};
export default DistributedUptimeDetails;

View File

@@ -1,93 +0,0 @@
// Components
import DataTable from "../../../../../Components/Table";
import BarChart from "../../../../../Components/Charts/BarChart";
import { Box } from "@mui/material";
import { StatusLabel } from "../../../../../Components/Label";
import Host from "../../../../../Components/Host";
// Utils
import { useTheme } from "@mui/material/styles";
import { useNavigate } from "react-router-dom";
import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils";
import PropTypes from "prop-types";
// Constants
const TYPE_MAP = {
distributed_http: "Distributed HTTP",
};
const MonitorTable = ({ isLoading, monitors }) => {
const theme = useTheme();
const navigate = useNavigate();
const { determineState } = useMonitorUtils();
const headers = [
{
id: "name",
content: <Box>Host</Box>,
render: (row) => (
<Host
key={row._id}
url={row.url}
title={row.name}
percentageColor={row.percentageColor}
percentage={row.percentage}
/>
),
},
{
id: "status",
content: <Box width="max-content"> Status</Box>,
render: (row) => {
const status = determineState(row?.monitor);
return (
<StatusLabel
status={status}
text={status}
customStyles={{ textTransform: "capitalize" }}
/>
);
},
},
{
id: "responseTime",
content: "Response Time",
render: (row) => <BarChart checks={row?.monitor?.checks.slice().reverse()} />,
},
{
id: "type",
content: "Type",
render: (row) => <span>{TYPE_MAP[row.monitor.type]}</span>,
},
{
id: "actions",
content: "Actions",
render: (row) => <span>{"TODO"}</span>,
},
];
return (
<DataTable
shouldRender={!isLoading}
headers={headers}
data={monitors}
config={{
rowSX: {
cursor: "pointer",
"&:hover": {
backgroundColor: theme.palette.background.accent,
},
},
onRowClick: (row) => {
navigate(`/distributed-uptime/${row._id}`);
},
}}
/>
);
};
MonitorTable.propTypes = {
isLoading: PropTypes.bool,
monitors: PropTypes.array,
};
export default MonitorTable;

View File

@@ -1,14 +0,0 @@
import { Stack, Skeleton } from "@mui/material";
export const SkeletonLayout = () => {
return (
<Stack>
<Skeleton
variant="rectangular"
height={"90vh"}
/>
</Stack>
);
};
export default SkeletonLayout;

View File

@@ -1,102 +0,0 @@
// Components
import { Stack, Typography } from "@mui/material";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import CreateMonitorHeader from "../../../Components/MonitorCreateHeader";
import MonitorTable from "./Components/MonitorTable";
import Pagination from "../../../Components/Table/TablePagination";
import Fallback from "../../../Components/Fallback";
import GenericFallback from "../../../Components/GenericFallback";
// Utils
import { useState } from "react";
import { useTheme } from "@mui/material/styles";
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
import { useSubscribeToDepinMonitors } from "../../../Hooks/useSubscribeToDepinMonitors";
import SkeletonLayout from "./Components/Skeleton";
import { useTranslation } from "react-i18next";
// Constants
const BREADCRUMBS = [{ name: `Distributed Uptime`, path: "/distributed-uptime" }];
const DistributedUptimeMonitors = () => {
// Local state
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10);
// Utils
const theme = useTheme();
const isAdmin = useIsAdmin();
const { t } = useTranslation();
const [monitors, count, isLoading, networkError] = useSubscribeToDepinMonitors(
page,
rowsPerPage
);
// Handlers
const handleChangePage = (event, newPage) => {
setPage(newPage);
};
const handleChangeRowsPerPage = (event) => {
setRowsPerPage(event.target.value);
setPage(0);
};
if (isLoading) {
return <SkeletonLayout />;
}
if (networkError) {
return (
<GenericFallback>
<Typography
variant="h1"
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
{t("networkError")}
</Typography>
<Typography>{t("checkConnection")}</Typography>
</GenericFallback>
);
}
if (count === 0) {
return (
<Fallback
vowelStart={false}
title="distributed uptime monitor"
checks={[
"Check if a server is online from multiple locations",
"Detect outages and performance issues in real time",
"Reduce false alarms by verifying downtime from different networks",
"Provide insights on regional availability and latency",
]}
link="/distributed-uptime/create"
isAdmin={isAdmin}
/>
);
}
return (
<Stack gap={theme.spacing(10)}>
<Breadcrumbs list={BREADCRUMBS} />
<CreateMonitorHeader
isAdmin={isAdmin}
shouldRender={!isLoading}
path="/distributed-uptime/create"
/>
<MonitorTable
isLoading={isLoading}
monitors={monitors}
/>
<Pagination
itemCount={count}
paginationLabel="monitors"
page={page}
rowsPerPage={rowsPerPage}
handleChangePage={handleChangePage}
handleChangeRowsPerPage={handleChangeRowsPerPage}
/>
</Stack>
);
};
export default DistributedUptimeMonitors;

View File

@@ -1,22 +0,0 @@
const VisuallyHiddenInput = ({ onChange }) => {
return (
<input
type="file"
accept="image/*"
onChange={onChange}
style={{
clip: "rect(0 0 0 0)",
clipPath: "inset(50%)",
height: 1,
overflow: "hidden",
position: "absolute",
bottom: 0,
left: 0,
whiteSpace: "nowrap",
width: 1,
}}
/>
);
};
export default VisuallyHiddenInput;

View File

@@ -1,322 +0,0 @@
// Components
import { Stack, Typography, Button, Box } from "@mui/material";
import ConfigBox from "../../../Components/ConfigBox";
import Checkbox from "../../../Components/Inputs/Checkbox";
import TextInput from "../../../Components/Inputs/TextInput";
import VisuallyHiddenInput from "./Components/VisuallyHiddenInput";
import Image from "../../../Components/Image";
import LogoPlaceholder from "../../../assets/Images/logo_placeholder.svg";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import Search from "../../../Components/Inputs/Search";
import MonitorList from "../../StatusPage/Create/Components/MonitorList";
// Utils
import { useTheme } from "@emotion/react";
import { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
import { useCreateStatusPage } from "../../StatusPage/Create/Hooks/useCreateStatusPage";
import { statusPageValidation } from "../../../Validation/validation";
import { buildErrors } from "../../../Validation/error";
import { createToast } from "../../../Utils/toastUtils";
import { useNavigate } from "react-router-dom";
import { useMonitorsFetch } from "../../StatusPage/Create/Hooks/useMonitorsFetch";
import { useTranslation } from "react-i18next";
import { useFetchDepinStatusPage } from "../../../Hooks/useFetchDepinStatusPage";
const CreateStatus = () => {
const theme = useTheme();
const { monitorId, url } = useParams();
const navigate = useNavigate();
const { t } = useTranslation();
const isCreate = typeof url === "undefined";
const [createStatusPage, isLoading, networkError] = useCreateStatusPage(isCreate);
const [statusPageIsLoading, statusPageNetworkError, statusPage, _, isPublished] =
useFetchDepinStatusPage({
url,
timeFrame: 30,
isCreate,
});
const [monitors, monitorsIsLoading, monitorsNetworkError] = useMonitorsFetch();
const BREADCRUMBS = [
{ name: "distributed uptime", path: "/distributed-uptime" },
{
name: "details",
path: `/distributed-uptime/${isCreate ? monitorId : statusPage?.monitors[0]}`,
},
{ name: isCreate ? "create status page" : "edit status page", path: `` },
];
// Local state
const [form, setForm] = useState({
type: "distributed",
isPublished: false,
url: url ?? Math.floor(Math.random() * 1000000).toFixed(0),
logo: undefined,
companyName: "",
monitors: [monitorId],
});
const [errors, setErrors] = useState({});
const [search, setSearch] = useState("");
const [selectedMonitors, setSelectedMonitors] = useState([]);
const handleFormChange = (e) => {
const { name, value, checked, type } = e.target;
// Check for errors
const { error } = statusPageValidation.validate(
{ [name]: value },
{ abortEarly: false }
);
setErrors((prev) => buildErrors(prev, name, error));
if (type === "checkbox") {
setForm({ ...form, [name]: checked });
return;
}
setForm({ ...form, [name]: value });
};
const handleMonitorsChange = (selectedMonitors) => {
handleFormChange({
target: {
name: "subMonitors",
value: selectedMonitors.map((monitor) => monitor._id),
},
});
setSelectedMonitors(selectedMonitors);
};
const handleImageUpload = (e) => {
const img = e.target?.files?.[0];
setForm((prev) => ({
...prev,
logo: img,
}));
};
const handleSubmit = async () => {
let logoToSubmit = undefined;
// Handle image
if (typeof form.logo !== "undefined" && typeof form.logo.src === "undefined") {
logoToSubmit = {
src: URL.createObjectURL(form.logo),
name: form.logo.name,
type: form.logo.type,
size: form.logo.size,
};
} else if (typeof form.logo !== "undefined") {
logoToSubmit = form.logo;
}
const formToSubmit = { ...form };
if (typeof logoToSubmit !== "undefined") {
formToSubmit.logo = logoToSubmit;
}
// Validate
const { error } = statusPageValidation.validate(formToSubmit, { abortEarly: false });
if (typeof error === "undefined") {
const success = await createStatusPage({ form: formToSubmit });
if (success) {
const verb = isCreate ? "created" : "updated";
createToast({ body: `Status page ${verb} successfully` });
navigate(`/status/distributed/${form.url}`);
}
return;
}
const newErrors = {};
error?.details?.forEach((err) => {
newErrors[err.path[0]] = err.message;
});
setErrors((prev) => ({ ...prev, ...newErrors }));
};
// If we are configuring, populate fields
useEffect(() => {
if (isCreate) return;
if (typeof statusPage === "undefined") {
return;
}
let newLogo = undefined;
if (statusPage.logo) {
newLogo = {
src: `data:${statusPage.logo.contentType};base64,${statusPage.logo.data}`,
name: "logo",
type: statusPage.logo.contentType,
size: null,
};
}
setForm((prev) => {
return {
...prev,
companyName: statusPage?.companyName,
isPublished: statusPage?.isPublished,
timezone: statusPage?.timezone,
monitors: statusPage?.monitors,
subMonitors: statusPage?.subMonitors.map((monitor) => monitor._id),
color: statusPage?.color,
logo: newLogo,
};
});
setSelectedMonitors(statusPage?.subMonitors);
}, [isCreate, statusPage]);
const imgSrc = form?.logo?.src
? form.logo.src
: form.logo
? URL.createObjectURL(form.logo)
: undefined;
return (
<Stack gap={theme.spacing(10)}>
<Breadcrumbs list={BREADCRUMBS} />
<Typography variant="h1">
<Typography
component="span"
fontSize="inherit"
>
{isCreate
? t("distributedUptimeStatusCreateYour")
: t("distributedUptimeStatusEditYour")}{" "}
</Typography>
<Typography
component="span"
variant="h2"
fontSize="inherit"
fontWeight="inherit"
>
{t("distributedUptimeStatusCreateStatusPage")}
</Typography>
</Typography>
<ConfigBox>
<Stack>
<Typography component="h2">
{t("distributedUptimeStatusCreateStatusPageAccess")}
</Typography>
<Typography component="p">
{t("distributedUptimeStatusCreateStatusPageReady")}
</Typography>
</Stack>
<Stack gap={theme.spacing(18)}>
<Checkbox
id="publish"
name="isPublished"
label={t("distributedUptimeStatusPublishedLabel")}
isChecked={form.isPublished}
onChange={handleFormChange}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Stack gap={theme.spacing(6)}>
<Typography component="h2">
{t("distributedUptimeStatusBasicInfoHeader")}
</Typography>
<Typography component="p">
{t("distributedUptimeStatusBasicInfoDescription")}
</Typography>
</Stack>
<Stack gap={theme.spacing(18)}>
<TextInput
id="companyName"
name="companyName"
type="text"
label={t("distributedUptimeStatusCompanyNameLabel")}
placeholder="Company name"
value={form.companyName}
onChange={handleFormChange}
helperText={errors["companyName"]}
error={errors["companyName"] ? true : false}
/>
<TextInput
id="url"
name="url"
type="url"
label={t("distributedUptimeStatusPageAddressLabel")}
disabled={!isCreate}
value={form.url}
onChange={handleFormChange}
helperText={errors["url"]}
error={errors["url"] ? true : false}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Stack gap={theme.spacing(6)}>
<Typography component="h2">{t("distributedUptimeStatusLogoHeader")}</Typography>
<Typography component="p">
{t("distributedUptimeStatusLogoDescription")}{" "}
</Typography>
</Stack>
<Stack
gap={theme.spacing(18)}
alignItems="center"
>
<Image
src={imgSrc}
alt="Logo"
minWidth={"300px"}
minHeight={"100px"}
maxWidth={"300px"}
maxHeight={"300px"}
placeholder={LogoPlaceholder}
/>
<Box>
<Button
component="label"
role={undefined}
variant="contained"
color="accent"
tabIndex={-1}
>
{t("distributedUptimeStatusLogoUploadButton")}
<VisuallyHiddenInput onChange={handleImageUpload} />
</Button>
</Box>
</Stack>
</ConfigBox>
<ConfigBox>
<Stack>
<Typography component="h2">
{t("distributedUptimeStatusStandardMonitorsHeader")}
</Typography>
<Typography component="p">
{t("distributedUptimeStatusStandardMonitorsDescription")}
</Typography>
</Stack>
<Stack gap={theme.spacing(18)}>
<Search
options={monitors ?? []}
multiple={true}
filteredBy="name"
value={selectedMonitors}
inputValue={search}
handleInputChange={setSearch}
handleChange={handleMonitorsChange}
/>
<MonitorList
monitors={monitors}
selectedMonitors={selectedMonitors}
setSelectedMonitors={handleMonitorsChange}
/>
</Stack>
</ConfigBox>
<Stack
direction="row"
justifyContent="flex-end"
>
<Button
variant="contained"
color="accent"
onClick={handleSubmit}
>
{t("settingsSave")}
</Button>
</Stack>
</Stack>
);
};
export default CreateStatus;

View File

@@ -1,66 +0,0 @@
// Components
import { Stack, Box } from "@mui/material";
import Host from "../../../../../Components/Host";
import DePINStatusPageBarChart from "../../../../../Components/Charts/DePINStatusPageBarChart";
import { StatusLabel } from "../../../../../Components/Label";
//Utils
import { useTheme } from "@mui/material/styles";
import useUtils from "../../../../Uptime/Monitors/Hooks/useUtils";
import PropTypes from "prop-types";
const MonitorsList = ({
isLoading = false,
shouldRender = true,
monitors = [],
timeFrame,
}) => {
const theme = useTheme();
const { determineState } = useUtils();
return (
<>
{monitors?.map((monitor) => {
const status = determineState(monitor);
return (
<Stack
key={monitor._id}
width="100%"
gap={theme.spacing(2)}
>
<Host
key={monitor._id}
url={monitor.url}
title={monitor.name}
percentageColor={monitor.percentageColor}
percentage={monitor.percentage}
/>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(20)}
>
<Box flex={9}>
<DePINStatusPageBarChart
checks={monitor?.checks?.slice().reverse()}
daysToShow={timeFrame}
/>
</Box>
<Box flex={1}>
<StatusLabel
status={status}
text={status}
customStyles={{ textTransform: "capitalize" }}
/>
</Box>
</Stack>
</Stack>
);
})}
</>
);
};
MonitorsList.propTypes = {
monitors: PropTypes.array.isRequired,
};
export default MonitorsList;

View File

@@ -1,14 +0,0 @@
import { Stack, Skeleton } from "@mui/material";
export const SkeletonLayout = () => {
return (
<Stack>
<Skeleton
variant="rectangular"
height={"90vh"}
/>
</Stack>
);
};
export default SkeletonLayout;

View File

@@ -1,42 +0,0 @@
import { Stack, Button, ButtonGroup } from "@mui/material";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
const TimeFrameHeader = ({ timeFrame, setTimeFrame, sx, ...props }) => {
const theme = useTheme();
const { t } = useTranslation();
return (
<Stack
direction="row"
justifyContent="flex-end"
sx={{ ...sx }}
{...props}
>
<ButtonGroup>
<Button
variant="group"
filled={(timeFrame === 30).toString()}
onClick={() => setTimeFrame(30)}
>
{t("distributedUptimeStatus30Days")}
</Button>
<Button
variant="group"
filled={(timeFrame === 60).toString()}
onClick={() => setTimeFrame(60)}
>
{t("distributedUptimeStatus60Days")}
</Button>
<Button
variant="group"
filled={(timeFrame === 90).toString()}
onClick={() => setTimeFrame(90)}
>
{t("distributedUptimeStatus90Days")}
</Button>
</ButtonGroup>
</Stack>
);
};
export default TimeFrameHeader;

View File

@@ -1,319 +0,0 @@
//Components
import DistributedUptimeMap from "../../DistributedUptime/Details/Components/DistributedUptimeMap";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import { Stack, Typography } from "@mui/material";
import DeviceTicker from "../../DistributedUptime/Details/Components/DeviceTicker";
import DistributedUptimeResponseChart from "../../DistributedUptime/Details/Components/DistributedUptimeResponseChart";
import NextExpectedCheck from "../../DistributedUptime/Details/Components/NextExpectedCheck";
import Footer from "../../DistributedUptime/Details/Components/Footer";
import StatBoxes from "../../DistributedUptime/Details/Components/StatBoxes";
import ControlsHeader from "../../StatusPage/Status/Components/ControlsHeader";
import MonitorTimeFrameHeader from "../../../Components/MonitorTimeFrameHeader";
import GenericFallback from "../../../Components/GenericFallback";
import Dialog from "../../../Components/Dialog";
import SkeletonLayout from "./Components/Skeleton";
import UptLogo from "../../../assets/icons/upt_logo.png";
import PeopleAltOutlinedIcon from "@mui/icons-material/PeopleAltOutlined";
import InfoBox from "../../../Components/InfoBox";
import StatusHeader from "../../DistributedUptime/Details/Components/StatusHeader";
import MonitorsList from "./Components/MonitorsList";
import { RowContainer } from "../../../Components/StandardContainer";
//Utils
import { useTheme } from "@mui/material/styles";
import { useState, useEffect, useRef } from "react";
import { useParams } from "react-router-dom";
import { useFetchDepinStatusPage } from "../../../Hooks/useFetchDepinStatusPage";
import { useSubscribeToDepinDetails } from "../../../Hooks/useSubscribeToDepinDetails";
import { useStatusPageDelete } from "../../StatusPage/Status/Hooks/useStatusPageDelete";
import TimeFrameHeader from "./Components/TimeframeHeader";
import SubHeader from "../../../Components/Subheader";
import { safelyParseFloat } from "../../../Utils/utils";
import { useNavigate, useLocation } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { useSelector, useDispatch } from "react-redux";
import { setMode } from "../../../Features/UI/uiSlice";
// Responsive
import { useMediaQuery } from "@mui/material";
const DistributedUptimeStatus = () => {
const { url } = useParams();
const location = useLocation();
const { t } = useTranslation();
const isPublic = location.pathname.startsWith("/status/distributed/public");
const elementToCapture = useRef(null);
// Redux state
const mode = useSelector((state) => state.ui.mode);
const originalModeRef = useRef(null);
// Local State
const [dateRange, setDateRange] = useState("recent");
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
const [timeFrame, setTimeFrame] = useState(30);
// Utils
const theme = useTheme();
const isSmallScreen = useMediaQuery(theme.breakpoints.down("sm"));
const navigate = useNavigate();
const dispatch = useDispatch();
const [
statusPageIsLoading,
statusPageNetworkError,
statusPage,
monitorId,
isPublished,
] = useFetchDepinStatusPage({
url,
timeFrame,
});
const [isLoading, networkError, connectionStatus, monitor, lastUpdateTrigger] =
useSubscribeToDepinDetails({ monitorId, dateRange, isPublic, isPublished });
const [deleteStatusPage, isDeleting] = useStatusPageDelete(() => {
navigate("/distributed-uptime");
}, url);
// Constants
const BREADCRUMBS = [
{ name: "Distributed Uptime", path: "/distributed-uptime" },
{ name: "details", path: `/distributed-uptime/${monitorId}` },
{ name: "status", path: `` },
];
let sx = {};
if (isPublic) {
sx = {
paddingTop: "10vh",
paddingRight: "10vw",
paddingBottom: "10vh",
paddingLeft: "10vw",
};
}
// Default to dark mode
useEffect(() => {
const cleanup = () => {
if (originalModeRef.current === null) {
originalModeRef.current = mode;
}
if (isPublic) {
dispatch(setMode(originalModeRef.current));
}
};
if (isPublic) {
dispatch(setMode("dark"));
}
window.addEventListener("beforeunload", cleanup);
return () => {
window.removeEventListener("beforeunload", cleanup);
};
}, [dispatch, isPublic]);
// Done loading, a status page doesn't exist
if (!statusPageIsLoading && typeof statusPage === "undefined") {
return (
<Stack sx={sx}>
<GenericFallback>
<Typography
variant="h1"
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
{t("distributedUptimeStatusPageNotSetUp")}
</Typography>
<Typography>{t("distributedUptimeStatusContactAdmin")}</Typography>
</GenericFallback>
</Stack>
);
}
// Done loading, a status page exists but is not public
if (!statusPageIsLoading && isPublic && statusPage.isPublished === false) {
return (
<Stack sx={sx}>
<GenericFallback>
<Typography>{t("distributedUptimeStatusPageNotPublic")}</Typography>
</GenericFallback>
</Stack>
);
}
if (isLoading || statusPageIsLoading) {
return <SkeletonLayout />;
}
if (networkError || statusPageNetworkError) {
return (
<GenericFallback>
<Typography
variant="h1"
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
{t("networkError")}
</Typography>
<Typography>{t("checkConnection")}</Typography>
</GenericFallback>
);
}
if (
typeof statusPage === "undefined" ||
typeof monitor === "undefined" ||
monitor.totalChecks === 0
) {
return (
<Stack gap={theme.spacing(10)}>
<Breadcrumbs list={BREADCRUMBS} />
<GenericFallback>
<Typography>{t("distributedUptimeDetailsNoMonitorHistory")}</Typography>
</GenericFallback>
</Stack>
);
}
return (
<Stack
ref={elementToCapture}
direction="column"
gap={theme.spacing(10)}
sx={{
...sx,
}}
>
{!isPublic && <Breadcrumbs list={BREADCRUMBS} />}
<ControlsHeader
statusPage={statusPage}
isPublic={isPublic}
isDeleting={isDeleting}
isDeleteOpen={isDeleteOpen}
setIsDeleteOpen={setIsDeleteOpen}
url={url}
type="distributed"
/>
<StatusHeader
monitor={monitor}
connectionStatus={connectionStatus}
elementToCapture={elementToCapture}
/>
<SubHeader
direction={{ s: "column", md: "row" }}
headerText={t("distributedStatusHeaderText")}
subHeaderText={t("distributedStatusSubHeaderText")}
gap={isSmallScreen ? theme.spacing(10) : 0}
alignItems={{ s: "flex-start", md: "flex-end" }}
>
<RowContainer>
<Stack>
<Typography variant={`body2`}>
{t("distributedRightCategoryTitle")}
</Typography>
<Typography variant={`h2`}>{statusPage.companyName}</Typography>
</Stack>
</RowContainer>
</SubHeader>
<NextExpectedCheck
lastUpdateTime={monitor?.timeSinceLastCheck ?? 0}
interval={monitor?.interval ?? 0}
trigger={lastUpdateTrigger}
/>
<MonitorTimeFrameHeader
dateRange={dateRange}
setDateRange={setDateRange}
/>
<Stack
gap={theme.spacing(8)}
direction={{ s: "column", md: "row" }}
>
<DistributedUptimeMap
checks={monitor?.groupedMapChecks ?? []}
width={isSmallScreen ? "100%" : "50%"}
/>
<Stack
width={{ s: "100%", md: "50%" }}
gap={theme.spacing(8)}
>
<Stack
direction="row"
gap={theme.spacing(8)}
>
<InfoBox
heading={t("distributedUptimeStatusDevices")}
subHeading={monitor?.totalChecks ?? 0}
icon={PeopleAltOutlinedIcon}
alt="Upt Logo"
sx={{ width: "50%" }}
/>
<InfoBox
heading={
isSmallScreen
? t("distributedUptimeStatusUpt")
: t("distributedUptimeStatusUptBurned")
}
subHeading={safelyParseFloat(monitor?.uptBurnt).toFixed(4)}
img={UptLogo}
alt={t("distributedUptimeStatusUptLogo")}
sx={{ width: "50%" }}
/>
</Stack>
<DeviceTicker
data={monitor?.latestChecks ?? []}
connectionStatus={connectionStatus}
/>
</Stack>
</Stack>
<DistributedUptimeResponseChart checks={monitor?.groupedChecks ?? []} />
<StatBoxes
monitor={monitor}
lastUpdateTrigger={lastUpdateTrigger}
/>
<SubHeader
shouldRender={statusPage?.subMonitors?.length > 0}
direction={{ s: "column", md: "row" }}
headerText={t("distributedStatusServerMonitors")}
subHeaderText={t("distributedStatusServerMonitorsDescription")}
gap={isSmallScreen ? theme.spacing(10) : 0}
alignItems={{ s: "flex-start", md: "flex-end" }}
>
<TimeFrameHeader
timeFrame={timeFrame}
setTimeFrame={setTimeFrame}
alignSelf="flex-start"
/>
</SubHeader>
<MonitorsList
monitors={statusPage?.subMonitors}
timeFrame={timeFrame}
/>
<Footer />
<Dialog
// open={isOpen.deleteStats}
title={t("distributedUptimeStatusPageDeleteDialog")}
onConfirm={() => {
deleteStatusPage();
setIsDeleteOpen(false);
}}
onCancel={() => {
setIsDeleteOpen(false);
}}
open={isDeleteOpen}
confirmationButtonLabel={t("distributedUptimeStatusPageDeleteConfirm")}
description={t("distributedUptimeStatusPageDeleteDescription")}
isLoading={isDeleting || isLoading}
/>
</Stack>
);
};
export default DistributedUptimeStatus;

View File

@@ -22,10 +22,6 @@ const Controls = ({ isDeleteOpen, setIsDeleteOpen, isDeleting, url, type }) => {
return null;
}
if (currentPath.startsWith("/status/distributed/public")) {
return null;
}
return (
<Stack
direction="row"
@@ -48,8 +44,6 @@ const Controls = ({ isDeleteOpen, setIsDeleteOpen, isDeleting, url, type }) => {
onClick={() => {
if (type === "uptime") {
navigate(`/status/uptime/configure/${url}`);
} else {
navigate(`/status/distributed/configure/${url}`);
}
}}
sx={{

View File

@@ -23,10 +23,7 @@ const StatusPagesTable = ({ data }) => {
onClick: (e, row) => {
if (row.isPublished) {
e.stopPropagation();
const url =
row.type === "distributed"
? `/status/distributed/public/${row.url}`
: `/status/uptime/public/${row.url}`;
const url = `/status/uptime/public/${row.url}`;
window.open(url, "_blank", "noopener,noreferrer");
}
},
@@ -79,11 +76,7 @@ const StatusPagesTable = ({ data }) => {
];
const handleRowClick = (statusPage) => {
if (statusPage.type === "distributed") {
navigate(`/status/distributed/${statusPage.url}`);
} else if (statusPage.type === "uptime") {
navigate(`/status/uptime/${statusPage.url}`);
}
navigate(`/status/uptime/${statusPage.url}`);
};
return (

View File

@@ -27,15 +27,6 @@ import Infrastructure from "../Pages/Infrastructure/Monitors";
import InfrastructureCreate from "../Pages/Infrastructure/Create";
import InfrastructureDetails from "../Pages/Infrastructure/Details";
// Distributed Uptime
import DistributedUptimeMonitors from "../Pages/DistributedUptime/Monitors";
import CreateDistributedUptime from "../Pages/DistributedUptime/Create";
import DistributedUptimeDetails from "../Pages/DistributedUptime/Details";
// Distributed Uptime Status
import CreateDistributedUptimeStatus from "../Pages/DistributedUptimeStatus/Create";
import DistributedUptimeStatus from "../Pages/DistributedUptimeStatus/Status";
// Server Status
import ServerUnreachable from "../Pages/ServerUnreachable";
@@ -99,39 +90,6 @@ const Routes = () => {
path="/uptime/configure/:monitorId/"
element={<UptimeConfigure />}
/>
{/* <Route
path="/distributed-uptime"
element={
<ProtectedDistributedUptimeRoute>
<DistributedUptimeMonitors />{" "}
</ProtectedDistributedUptimeRoute>
}
/> */}
{/* <Route
path="/distributed-uptime/create"
element={
<ProtectedDistributedUptimeRoute>
<CreateDistributedUptime />
</ProtectedDistributedUptimeRoute>
}
/>
<Route
path="/distributed-uptime/configure/:monitorId"
element={
<ProtectedDistributedUptimeRoute>
<CreateDistributedUptime />
</ProtectedDistributedUptimeRoute>
}
/> */}
{/* <Route
path="/distributed-uptime/:monitorId"
element={
<ProtectedDistributedUptimeRoute>
<DistributedUptimeDetails />
</ProtectedDistributedUptimeRoute>
}
/> */}
<Route
path="pagespeed"
@@ -180,43 +138,16 @@ const Routes = () => {
element={<Status />}
/>
{/* <Route
path="/status/distributed/:url"
element={
<ProtectedDistributedUptimeRoute>
<DistributedUptimeStatus />
</ProtectedDistributedUptimeRoute>
}
/> */}
<Route
path="status/uptime/create"
element={<CreateStatus />}
/>
{/* <Route
path="/status/distributed/create/:monitorId"
element={
<ProtectedDistributedUptimeRoute>
<CreateDistributedUptimeStatus />
</ProtectedDistributedUptimeRoute>
}
/> */}
<Route
path="status/uptime/configure/:url"
element={<CreateStatus />}
/>
{/* <Route
path="/status/distributed/configure/:url"
element={
<ProtectedDistributedUptimeRoute>
<CreateDistributedUptimeStatus />
</ProtectedDistributedUptimeRoute>
}
/> */}
<Route
path="integrations"
element={<Integrations />}
@@ -283,10 +214,6 @@ const Routes = () => {
path="/status/uptime/public/:url"
element={<Status />}
/>
{/* <Route
path="/status/distributed/public/:url"
element={<DistributedUptimeStatus />}
/> */}
<Route
path="/server-unreachable"

View File

@@ -847,7 +847,7 @@ class NetworkService {
*/
async fetchGithubLatestRelease() {
return this.axiosInstance.get(
"https://api.github.com/repos/bluewave-labs/bluewave-uptime/releases/latest",
"https://api.github.com/repos/bluewave-labs/checkmate/releases/latest",
{
headers: {
Authorization: null, // No authorization header for this request
@@ -856,141 +856,6 @@ class NetworkService {
);
}
getDistributedUptimeMonitors(config) {
const { teamId, limit, types, page, rowsPerPage, filter, field, order } = config;
const params = new URLSearchParams();
if (limit) params.append("limit", limit);
if (types) {
types.forEach((type) => {
params.append("type", type);
});
}
if (page) params.append("page", page);
if (rowsPerPage) params.append("rowsPerPage", rowsPerPage);
if (filter) params.append("filter", filter);
if (field) params.append("field", field);
if (order) params.append("order", order);
if (this.eventSource) {
this.eventSource.close();
}
const url = `${this.axiosInstance.defaults.baseURL}/distributed-uptime/monitors/${teamId}/initial?${params.toString()}`;
return this.axiosInstance.get(url);
}
subscribeToDistributedUptimeMonitors(config) {
const {
teamId,
onUpdate,
onError,
onOpen,
limit,
types,
page,
rowsPerPage,
filter,
field,
order,
} = config;
const params = new URLSearchParams();
if (limit) params.append("limit", limit);
if (types) {
types.forEach((type) => {
params.append("type", type);
});
}
if (page) params.append("page", page);
if (rowsPerPage) params.append("rowsPerPage", rowsPerPage);
if (filter) params.append("filter", filter);
if (field) params.append("field", field);
if (order) params.append("order", order);
if (this.eventSource) {
this.eventSource.close();
}
const url = `${this.axiosInstance.defaults.baseURL}/distributed-uptime/monitors/${teamId}?${params.toString()}`;
this.eventSource = new EventSource(url);
this.eventSource.onopen = () => {
onOpen?.();
};
this.eventSource.addEventListener("open", (e) => {});
this.eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
onUpdate(data);
};
this.eventSource.onerror = (error) => {
console.error("Monitor stream error:", error);
onError?.();
this.eventSource.close();
};
// Returns a cleanup function
return () => {
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
}
return () => {
console.log("Nothing to cleanup");
};
};
}
getDistributedUptimeDetails(config) {
const params = new URLSearchParams();
const { monitorId, dateRange, normalize, isPublic } = config;
if (dateRange) params.append("dateRange", dateRange);
if (normalize) params.append("normalize", normalize);
let url;
if (isPublic) {
url = `${this.axiosInstance.defaults.baseURL}/distributed-uptime/monitors/details/public/${monitorId}/initial?${params.toString()}`;
} else {
url = `${this.axiosInstance.defaults.baseURL}/distributed-uptime/monitors/details/${monitorId}/initial?${params.toString()}`;
}
return this.axiosInstance.get(url);
}
subscribeToDistributedUptimeDetails(config) {
const params = new URLSearchParams();
const { monitorId, onUpdate, onOpen, onError, dateRange, normalize } = config;
if (dateRange) params.append("dateRange", dateRange);
if (normalize) params.append("normalize", normalize);
const url = `${this.axiosInstance.defaults.baseURL}/distributed-uptime/monitors/details/${monitorId}?${params.toString()}`;
this.eventSource = new EventSource(url);
this.eventSource.onopen = (e) => {
onOpen?.();
};
this.eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
onUpdate(data);
};
this.eventSource.onerror = (error) => {
console.error("Monitor stream error:", error);
onError?.();
this.eventSource.close();
};
return () => {
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
}
};
}
async getStatusPage() {
return this.axiosInstance.get(`/status-page`, {
headers: {
@@ -1010,20 +875,6 @@ class NetworkService {
},
});
}
async getDistributedStatusPageByUrl(config) {
const { url, type, timeFrame } = config;
const params = new URLSearchParams();
params.append("type", type);
params.append("timeFrame", timeFrame);
return this.axiosInstance.get(
`/status-page/distributed/${url}?${params.toString()}`,
{
headers: {
"Content-Type": "application/json",
},
}
);
}
async getStatusPagesByTeamId(config) {
const { teamId } = config;

View File

@@ -239,7 +239,7 @@ const logoImageValidation = joi
.optional(); // Make entire object optional
const statusPageValidation = joi.object({
type: joi.string().valid("uptime", "distributed").required(),
type: joi.string().valid("uptime").required(),
isPublished: joi.bool(),
companyName: joi
.string()

View File

@@ -65,8 +65,6 @@
"settingsAppearanceDescription": "Switch between light and dark mode, or change user interface language",
"settingsThemeMode": "Theme Mode",
"settingsLanguage": "Language",
"settingsDistributedUptime": "Distributed uptime",
"settingsDistributedUptimeDescription": "Enable/disable distributed uptime monitoring.",
"settingsEnabled": "Enabled",
"settingsDisabled": "Disabled",
"settingsHistoryAndMonitoring": "History of monitoring",
@@ -86,8 +84,6 @@
"settingsRemoveAllMonitorsButton": "Remove all monitors",
"settingsRemoveAllMonitorsDialogTitle": "Do you want to remove all monitors?",
"settingsRemoveAllMonitorsDialogConfirm": "Yes, remove all monitors",
"settingsWallet": "Wallet",
"settingsWalletDescription": "Connect your wallet here. This is required for the Distributed Uptime monitor to connect to multiple nodes globally.",
"settingsAbout": "About",
"settingsDevelopedBy": "Developed by Bluewave Labs.",
"settingsSave": "Save",
@@ -136,10 +132,6 @@
"distributedUptimeCreateIncidentDescription": "When there is an incident, notify users.",
"distributedUptimeCreateAdvancedSettings": "Advanced settings",
"distributedUptimeDetailsNoMonitorHistory": "There is no check history for this monitor yet.",
"distributedUptimeDetailsFooterHeading": "Made with ❤️ by UpRock & Bluewave Labs",
"distributedUptimeDetailsFooterBuilt": "Built on",
"distributedUptimeDetailsFooterSolana": "Solana",
"distributedUptimeDetailsMonitorHeader": "Distributed Uptime Monitoring powered by DePIN",
"distributedUptimeDetailsStatusHeaderUptime": "Uptime:",
"distributedUptimeDetailsStatusHeaderLastUpdate": "Last updated",
"notifications": {
@@ -488,7 +480,6 @@
"uptime": "Uptime",
"pagespeed": "Pagespeed",
"infrastructure": "Infrastructure",
"distributedUptime": "Distributed Uptime",
"incidents": "Incidents",
"statusPages": "Status pages",
"maintenance": "Maintenance",

View File

@@ -87,7 +87,6 @@
"settingsRemoveAllMonitorsDialogTitle": "Você deseja remover todos os monitores?",
"settingsRemoveAllMonitorsDialogConfirm": "Sim, remova todos os monitores",
"settingsWallet": "Carteira",
"settingsWalletDescription": "Conecte sua carteira aqui. Isso é necessário para que o Distributed Uptime Monitor se conecte a vários nós globalmente.",
"settingsAbout": "Sobre",
"settingsDevelopedBy": "Desenvolvido pela Bluewave Labs.",
"settingsSave": "Salvar",

View File

@@ -65,8 +65,6 @@
"settingsAppearanceDescription": "Переключение между светлым и темным режимом или изменение языка пользовательского интерфейса",
"settingsThemeMode": "Тема",
"settingsLanguage": "Язык",
"settingsDistributedUptime": "Distributed uptime",
"settingsDistributedUptimeDescription": "Включить/выключить distributed uptime monitoring.",
"settingsEnabled": "Включено",
"settingsDisabled": "Выключено",
"settingsHistoryAndMonitoring": "История и мониторинг",
@@ -86,8 +84,6 @@
"settingsRemoveAllMonitorsButton": "Удалить все демонстрационные мониторы",
"settingsRemoveAllMonitorsDialogTitle": "Хотите удалить все мониторы?",
"settingsRemoveAllMonitorsDialogConfirm": "Да, очистить все мониторы",
"settingsWallet": "Кошелёк",
"settingsWalletDescription": "Подключите свой кошелек здесь. Это необходимо для того, чтобы монитор Distributed Uptime мог подключиться к нескольким узлам по всему миру.",
"settingsAbout": "О",
"settingsDevelopedBy": "Developed by Bluewave Labs.",
"settingsSave": "Сохранить",
@@ -139,7 +135,6 @@
"distributedUptimeDetailsFooterHeading": "Made with ❤️ by UpRock & Bluewave Labs",
"distributedUptimeDetailsFooterBuilt": "Built on",
"distributedUptimeDetailsFooterSolana": "Solana",
"distributedUptimeDetailsMonitorHeader": "Distributed Uptime Monitoring powered by DePIN",
"distributedUptimeDetailsStatusHeaderUptime": "Аптайм:",
"distributedUptimeDetailsStatusHeaderLastUpdate": "Последнее обновление",
"notifications": {

View File

@@ -4,25 +4,11 @@ const SERVICE_NAME = "diagnosticController";
class DiagnosticController {
constructor(db) {
this.db = db;
this.getDistributedUptimeDbExecutionStats =
this.getDistributedUptimeDbExecutionStats.bind(this);
this.getMonitorsByTeamIdExecutionStats =
this.getMonitorsByTeamIdExecutionStats.bind(this);
this.getDbStats = this.getDbStats.bind(this);
}
async getDistributedUptimeDbExecutionStats(req, res, next) {
try {
const data = await this.db.getDistributedUptimeDbExecutionStats(req);
return res.success({
msg: "OK",
data,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getDbExecutionStats"));
}
}
async getMonitorsByTeamIdExecutionStats(req, res, next) {
try {
const data = await this.db.getMonitorsByTeamIdExecutionStats(req);

View File

@@ -1,339 +0,0 @@
import { handleError } from "./controllerUtils.js";
import Monitor from "../db/models/Monitor.js";
import DistributedUptimeCheck from "../db/models/DistributedUptimeCheck.js";
const SERVICE_NAME = "DistributedUptimeQueueController";
class DistributedUptimeController {
constructor({ db, http, statusService, logger }) {
this.db = db;
this.http = http;
this.statusService = statusService;
this.logger = logger;
this.resultsCallback = this.resultsCallback.bind(this);
this.getDistributedUptimeMonitors = this.getDistributedUptimeMonitors.bind(this);
this.subscribeToDistributedUptimeMonitors =
this.subscribeToDistributedUptimeMonitors.bind(this);
this.subscribeToDistributedUptimeMonitorDetails =
this.subscribeToDistributedUptimeMonitorDetails.bind(this);
this.getDistributedUptimeMonitorDetails =
this.getDistributedUptimeMonitorDetails.bind(this);
}
async resultsCallback(req, res, next) {
try {
const { id, result } = req.body;
// Calculate response time
const {
first_byte_took,
body_read_took,
dns_took,
conn_took,
connect_took,
tls_took,
status_code,
error,
upt_burnt,
} = result;
// Calculate response time
const responseTime = first_byte_took / 1_000_000;
if (!isFinite(responseTime) || responseTime <= 0 || responseTime > 30000) {
this.logger.info({
message: `Unreasonable response time detected: ${responseTime}ms from first_byte_took: ${first_byte_took}ns`,
service: SERVICE_NAME,
method: "resultsCallback",
});
return;
}
// Calculate if server is up or down
const isErrorStatus = status_code >= 400;
const hasError = error !== "";
const status = isErrorStatus || hasError ? false : true;
// Build response
const distributedUptimeResponse = {
monitorId: id,
type: "distributed_http",
payload: result,
status,
code: status_code,
responseTime,
first_byte_took,
body_read_took,
dns_took,
conn_took,
connect_took,
tls_took,
upt_burnt,
};
if (error) {
const code = status_code || this.NETWORK_ERROR;
distributedUptimeResponse.code = code;
distributedUptimeResponse.message =
this.http.STATUS_CODES[code] || "Network Error";
} else {
distributedUptimeResponse.message = this.http.STATUS_CODES[status_code];
}
await this.statusService.updateStatus(distributedUptimeResponse);
res.status(200).json({ message: "OK" });
} catch (error) {
next(handleError(error, SERVICE_NAME, "resultsCallback"));
}
}
async getDistributedUptimeMonitors(req, res, next) {
try {
const monitors = await this.db.getMonitorsWithChecksByTeamId(req);
return res.success({
msg: "OK",
data: monitors,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getDistributedUptimeMonitors"));
}
}
async subscribeToDistributedUptimeMonitors(req, res, next) {
try {
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
res.setHeader("Access-Control-Allow-Origin", "*");
// Disable compression
req.headers["accept-encoding"] = "identity";
res.removeHeader("Content-Encoding");
const BATCH_DELAY = 1000;
let batchTimeout = null;
let opInProgress = false;
let monitorStream = null;
let checksStream = null;
// Do things here
const notifyChange = async () => {
if (opInProgress) {
// Get data
const { count, monitors } = await this.db.getMonitorsWithChecksByTeamId(req);
res.write(`data: ${JSON.stringify({ count, monitors })}\n\n`);
opInProgress = false;
}
batchTimeout = null;
};
const handleChange = () => {
opInProgress = true;
if (batchTimeout) clearTimeout(batchTimeout);
batchTimeout = setTimeout(notifyChange, BATCH_DELAY);
};
const createMonitorStream = () => {
if (monitorStream) {
try {
monitorStream.close();
} catch (error) {
this.logger.error({
message: "Error closing monitor stream",
service: SERVICE_NAME,
method: "subscribeToDistributedUptimeMonitors",
stack: error.stack,
});
}
}
monitorStream = Monitor.watch(
[{ $match: { operationType: { $in: ["insert", "update", "delete"] } } }],
{ fullDocument: "updateLookup" }
);
monitorStream.on("change", handleChange);
monitorStream.on("error", (error) => {
this.logger.error({
message: "Error in monitor stream",
service: SERVICE_NAME,
method: "subscribeToDistributedUptimeMonitors",
stack: error.stack,
});
createMonitorStream();
});
monitorStream.on("close", () => {
monitorStream = null;
});
};
const createChecksStream = () => {
if (checksStream) {
try {
checksStream.close();
} catch (error) {
this.logger.error({
message: "Error closing checks stream",
service: SERVICE_NAME,
method: "subscribeToDistributedUptimeMonitors",
details: error,
});
}
}
checksStream = DistributedUptimeCheck.watch(
[{ $match: { operationType: { $in: ["insert", "update", "delete"] } } }],
{ fullDocument: "updateLookup" }
);
checksStream.on("change", handleChange);
checksStream.on("error", (error) => {
this.logger.error({
message: "Error in checks stream",
service: SERVICE_NAME,
method: "subscribeToDistributedUptimeMonitors",
stack: error.stack,
});
createChecksStream();
});
checksStream.on("close", () => {
checksStream = null;
});
};
createMonitorStream();
createChecksStream();
req.on("close", () => {
if (batchTimeout) {
clearTimeout(batchTimeout);
}
monitorStream.close();
checksStream.close();
clearInterval(keepAlive);
});
// Keep connection alive
const keepAlive = setInterval(() => {
res.write(": keepalive\n\n");
}, 10000);
// Clean up on close
req.on("close", () => {
clearInterval(keepAlive);
});
} catch (error) {
this.logger.error({
message: "Error in subscribeToDistributedUptimeMonitors",
service: SERVICE_NAME,
method: "subscribeToDistributedUptimeMonitors",
stack: error.stack,
});
next(handleError(error, SERVICE_NAME, "subscribeToDistributedUptimeMonitors"));
}
}
async getDistributedUptimeMonitorDetails(req, res, next) {
try {
const monitor = await this.db.getDistributedUptimeDetailsById(req);
return res.success({
msg: "OK",
data: monitor,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getDistributedUptimeMonitorDetails"));
}
}
async subscribeToDistributedUptimeMonitorDetails(req, res, next) {
try {
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
res.setHeader("Access-Control-Allow-Origin", "*");
// disable compression
req.headers["accept-encoding"] = "identity";
res.removeHeader("Content-Encoding");
const BATCH_DELAY = 1000;
let batchTimeout = null;
let opInProgress = false;
let checksStream = null;
// Do things here
const notifyChange = async () => {
try {
if (opInProgress) {
// Get data
const monitor = await this.db.getDistributedUptimeDetailsById(req);
res.write(`data: ${JSON.stringify({ monitor })}\n\n`);
opInProgress = false;
}
batchTimeout = null;
} catch (error) {
opInProgress = false;
batchTimeout = null;
this.logger.error({
message: "Error in notifyChange",
service: SERVICE_NAME,
method: "subscribeToDistributedUptimeMonitorDetails",
stack: error.stack,
});
next(handleError(error, SERVICE_NAME, "getDistributedUptimeMonitorDetails"));
}
};
const handleChange = () => {
opInProgress = true;
if (batchTimeout) clearTimeout(batchTimeout);
batchTimeout = setTimeout(notifyChange, BATCH_DELAY);
};
const createCheckStream = () => {
if (checksStream) {
try {
checksStream.close();
} catch (error) {
this.logger.error({
message: "Error closing checks stream",
service: SERVICE_NAME,
method: "subscribeToDistributedUptimeMonitorDetails",
stack: error.stack,
});
}
}
checksStream = DistributedUptimeCheck.watch(
[{ $match: { operationType: { $in: ["insert", "update", "delete"] } } }],
{ fullDocument: "updateLookup" }
);
checksStream.on("change", handleChange);
checksStream.on("error", (error) => {
this.logger.error({
message: "Error in checks stream",
service: SERVICE_NAME,
method: "subscribeToDistributedUptimeMonitorDetails",
stack: error.stack,
});
createCheckStream();
});
checksStream.on("close", () => {
checksStream = null;
});
};
createCheckStream();
// Handle client disconnect
req.on("close", () => {
if (batchTimeout) {
clearTimeout(batchTimeout);
}
checksStream.close();
clearInterval(keepAlive);
});
// Keep connection alive
const keepAlive = setInterval(() => {
res.write(": keepalive\n\n");
}, 10000);
} catch (error) {
next(handleError(error, SERVICE_NAME, "getDistributedUptimeMonitorDetails"));
}
}
}
export default DistributedUptimeController;

View File

@@ -22,7 +22,6 @@ import logger from "../utils/logger.js";
import { handleError, handleValidationError } from "./controllerUtils.js";
import axios from "axios";
import seedDb from "../db/mongo/utils/seedDb.js";
import { seedDistributedTest } from "../db/mongo/utils/seedDb.js";
const SERVICE_NAME = "monitorController";
import pkg from "papaparse";
@@ -431,10 +430,6 @@ class MonitorController {
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.
@@ -743,11 +738,8 @@ class MonitorController {
const token = getTokenFromHeaders(req.headers);
const { jwtSecret } = this.settingsService.getSettings();
const { _id, teamId } = jwt.verify(token, jwtSecret);
if (type === "distributed_test") {
await seedDistributedTest(_id, teamId);
} else {
await seedDb(_id, teamId);
}
await seedDb(_id, teamId);
res.success({ msg: "Database seeded" });
} catch (error) {
next(handleError(error, SERVICE_NAME, "seedDb"));

View File

@@ -71,29 +71,6 @@ class StatusPageController {
}
};
getDistributedStatusPageByUrl = async (req, res, next) => {
try {
await getStatusPageParamValidation.validateAsync(req.params);
await getStatusPageQueryValidation.validateAsync(req.query);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const statusPage = await this.db.getDistributedStatusPageByUrl({
url: req.params.url,
daysToShow: req.params.timeFrame,
});
return res.success({
msg: this.stringService.statusPageByUrl,
data: statusPage,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getDistributedStatusPageByUrl"));
}
};
getStatusPageByUrl = async (req, res, next) => {
try {
await getStatusPageParamValidation.validateAsync(req.params);

View File

@@ -1,137 +0,0 @@
import mongoose from "mongoose";
import { BaseCheckSchema } from "./Check.js";
// {
// "id": "12123",
// "result": {
// "task_arrived": "2025-01-13T19:21:37.463466602Z",
// "dns_start": "2025-01-14T00:21:33.1801319+05:00",
// "dns_end": "2025-01-14T00:21:33.4582552+05:00",
// "conn_start": "2025-01-14T00:21:33.1801319+05:00",
// "conn_end": "2025-01-14T00:21:33.7076318+05:00",
// "connect_start": "2025-01-14T00:21:33.4582552+05:00",
// "connect_end": "2025-01-14T00:21:33.541899+05:00",
// "tls_hand_shake_start": "2025-01-14T00:21:33.541899+05:00",
// "tls_hand_shake_end": "2025-01-14T00:21:33.7076318+05:00",
// "body_read_start": "2025-01-14T00:21:34.1894707+05:00",
// "body_read_end": "2025-01-14T00:21:34.1894707+05:00",
// "wrote_request": "2025-01-14T00:21:33.7076318+05:00",
// "got_first_response_byte": "2025-01-14T00:21:34.1327652+05:00",
// "first_byte_took": 425133400,
// "body_read_took": 56030000,
// "dns_took": 278123300,
// "conn_took": 527499900,
// "connect_took": 83643800,
// "tls_took": 165732800,
// "sni_name": "uprock.com",
// "status_code": 200,
// "body_size": 19320,
// "request_header_size": 95,
// "response_header_size": 246,
// "response_headers": "X-Vercel-Id: bom1::iad1::sm87v-1736796096856-aec270c01f23\nDate: Mon, 13 Jan 2025 19:21:37 GMT\nServer: Vercel\nStrict-Transport-Security: max-age=63072000\nVary: RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url\nX-Matched-Path: /\nX-Powered-By: Next.js\nX-Vercel-Cache: MISS\nAge: 0\nCache-Control: private, no-cache, no-store, max-age=0, must-revalidate\nContent-Type: text/html; charset=utf-8",
// "error": "",
// "device_id": "d5f578e143a2cd603dd6bf5f846a86a538bde4a8fbe2ad1fca284ad9f033daf8",
// "ip_address": "223.123.19.0",
// "proof": "",
// "created_at": "2025-01-13T19:21:37.463466912Z",
// "continent": "AS",
// "country_code": "PK",
// "city": "Muzaffargarh",
// "upt_burnt" : "0.01",
// "location": {
// "lat": 71.0968,
// "lng": 30.0208
// },
// "payload": {
// "callback": "https://webhook.site/2a15b0af-545a-4ac2-b913-153b97592d7a",
// "x": "y"
// }
// }
// }
const LocationSchema = new mongoose.Schema(
{
lat: { type: Number, required: true },
lng: { type: Number, required: true },
},
{ _id: false }
);
const DistributedUptimeCheckSchema = mongoose.Schema(
{
...BaseCheckSchema.obj,
first_byte_took: {
type: Number,
required: false,
},
body_read_took: {
type: Number,
required: false,
},
dns_took: {
type: Number,
required: false,
},
conn_took: {
type: Number,
required: false,
},
connect_took: {
type: Number,
required: false,
},
tls_took: {
type: Number,
required: false,
},
location: {
type: LocationSchema,
required: false,
},
continent: {
type: String,
required: false,
},
countryCode: {
type: String,
required: false,
},
city: {
type: String,
required: false,
},
uptBurnt: {
type: mongoose.Schema.Types.Decimal128,
required: false,
},
count: {
type: Number,
required: false,
},
},
{ timestamps: true }
);
DistributedUptimeCheckSchema.pre("save", function (next) {
if (this.isModified("uptBurnt") && typeof this.uptBurnt === "string") {
this.uptBurnt = mongoose.Types.Decimal128.fromString(this.uptBurnt);
}
next();
});
DistributedUptimeCheckSchema.index({ createdAt: 1 });
DistributedUptimeCheckSchema.index({ monitorId: 1, updatedAt: 1 });
DistributedUptimeCheckSchema.index({ monitorId: 1, updatedAt: -1 });
DistributedUptimeCheckSchema.index(
{
monitorId: 1,
createdAt: -1,
city: 1,
"location.lat": 1,
"location.lng": 1,
responseTime: 1,
},
{ background: true }
);
export default mongoose.model("DistributedUptimeCheck", DistributedUptimeCheckSchema);

View File

@@ -28,16 +28,7 @@ const MonitorSchema = mongoose.Schema(
type: {
type: String,
required: true,
enum: [
"http",
"ping",
"pagespeed",
"hardware",
"docker",
"port",
"distributed_http",
"distributed_test",
],
enum: ["http", "ping", "pagespeed", "hardware", "docker", "port"],
},
ignoreTlsErrors: {
type: Boolean,

View File

@@ -18,7 +18,7 @@ const StatusPageSchema = mongoose.Schema(
type: String,
required: true,
default: "uptime",
enum: ["uptime", "distributed"],
enum: ["uptime"],
},
companyName: {
type: String,

View File

@@ -43,11 +43,6 @@ import * as hardwareCheckModule from "./modules/hardwareCheckModule.js";
import * as checkModule from "./modules/checkModule.js";
//****************************************
// Distributed Checks
//****************************************
import * as distributedCheckModule from "./modules/distributedCheckModule.js";
//****************************************
// Maintenance Window
//****************************************
@@ -85,7 +80,6 @@ class MongoDB {
Object.assign(this, pageSpeedCheckModule);
Object.assign(this, hardwareCheckModule);
Object.assign(this, checkModule);
Object.assign(this, distributedCheckModule);
Object.assign(this, maintenanceWindowModule);
Object.assign(this, notificationModule);
Object.assign(this, settingsModule);

View File

@@ -2,7 +2,6 @@ import Check from "../../models/Check.js";
import Monitor from "../../models/Monitor.js";
import HardwareCheck from "../../models/HardwareCheck.js";
import PageSpeedCheck from "../../models/PageSpeedCheck.js";
import DistributedUptimeCheck from "../../models/DistributedUptimeCheck.js";
import User from "../../models/User.js";
import logger from "../../../utils/logger.js";
import { ObjectId } from "mongodb";
@@ -111,8 +110,6 @@ const getChecksByMonitor = async (req) => {
port: Check,
pagespeed: PageSpeedCheck,
hardware: HardwareCheck,
distributed_http: DistributedUptimeCheck,
distributed_test: DistributedUptimeCheck,
};
const Model = checkModels[type];
@@ -202,12 +199,7 @@ const getChecksByTeam = async (req) => {
pipeline: [{ $match: matchStage }],
},
},
{
$unionWith: {
coll: "distributeduptimechecks",
pipeline: [{ $match: matchStage }],
},
},
{ $sort: { createdAt: sortOrder } },
{
$facet: {

View File

@@ -1,4 +1,3 @@
import DistributedUptimeCheck from "../../models/DistributedUptimeCheck.js";
import Monitor from "../../models/Monitor.js";
import { ObjectId } from "mongodb";
@@ -12,53 +11,6 @@ import {
} from "./monitorModuleQueries.js";
import { getDateRange } from "./monitorModule.js";
const getDistributedUptimeDbExecutionStats = async (req) => {
try {
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));
}
const { dateRange } = req.query;
const dates = getDateRange(dateRange);
const formatLookup = {
recent: "%Y-%m-%dT%H:%M:00Z",
day: {
$concat: [
{ $dateToString: { format: "%Y-%m-%dT%H:", date: "$createdAt" } },
{
$cond: [{ $lt: [{ $minute: "$createdAt" }, 30] }, "00:00Z", "30:00Z"],
},
],
},
week: "%Y-%m-%dT%H:00:00Z",
month: "%Y-%m-%dT00:00:00Z",
};
const dateString = formatLookup[dateRange];
const dePINDetailsByDateRangeStats = await DistributedUptimeCheck.aggregate(
buildDePINDetailsByDateRange(monitor, dates, dateString)
).explain("executionStats");
const latestChecksStats = await DistributedUptimeCheck.aggregate(
buildDePINLatestChecks(monitor)
).explain("executionStats");
return {
dePINDetailsByDateRangeStats,
latestChecksStats,
};
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getAllMonitorsWithUptimeStats";
throw error;
}
};
const getMonitorsByTeamIdExecutionStats = async (req) => {
try {
let { limit, type, page, rowsPerPage, filter, field, order } = req.query;
@@ -104,4 +56,4 @@ const getMonitorsByTeamIdExecutionStats = async (req) => {
}
};
export { getDistributedUptimeDbExecutionStats, getMonitorsByTeamIdExecutionStats };
export { getMonitorsByTeamIdExecutionStats };

View File

@@ -1,156 +0,0 @@
import DistributedUptimeCheck from "../../models/DistributedUptimeCheck.js";
import { ObjectId } from "mongodb";
const SERVICE_NAME = "distributedCheckModule";
const createDistributedCheck = async (checkData) => {
try {
if (typeof checkData.monitorId === "string") {
checkData.monitorId = ObjectId.createFromHexString(checkData.monitorId);
}
const check = await DistributedUptimeCheck.findOneAndUpdate(
{
monitorId: checkData.monitorId,
city: checkData.city,
},
[
{
$set: {
...checkData,
responseTime: {
$cond: {
if: { $ifNull: ["$count", false] },
then: {
$cond: {
// Check if the new value is an outlier (3x the current average)
if: {
$and: [
{ $gt: ["$responseTime", 0] },
{
$gt: [
checkData.responseTime,
{ $multiply: ["$responseTime", 3] },
],
},
],
},
then: "$responseTime", // Keep the current value if it's an outlier
else: {
// Normal case - calculate new average
$round: [
{
$divide: [
{
$add: [
{ $multiply: ["$responseTime", "$count"] },
checkData.responseTime,
],
},
{ $add: ["$count", 1] },
],
},
2,
],
},
},
},
else: checkData.responseTime,
},
},
count: { $add: [{ $ifNull: ["$count", 0] }, 1] },
},
},
],
{
upsert: true,
new: true,
runValidators: true,
}
);
return check;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "createCheck";
throw error;
}
};
const createDistributedChecks = async (checksData) => {
try {
if (!Array.isArray(checksData) || checksData.length === 0) {
return;
}
const bulkOps = checksData.map((checkData) => {
if (typeof checkData.monitorId === "string") {
checkData.monitorId = ObjectId.createFromHexString(checkData.monitorId);
}
return {
updateOne: {
filter: {
monitorId: checkData.monitorId,
city: checkData.city,
},
update: [
{
$set: {
...checkData,
responseTime: {
$cond: {
if: { $ifNull: ["$count", false] },
then: {
$round: [
{
$divide: [
{
$add: [
{ $multiply: ["$responseTime", "$count"] },
checkData.responseTime,
],
},
{ $add: ["$count", 1] },
],
},
2,
],
},
else: checkData.responseTime,
},
},
count: { $add: [{ $ifNull: ["$count", 0] }, 1] },
},
},
],
upsert: true,
},
};
});
await DistributedUptimeCheck.bulkWrite(bulkOps, {
ordered: false,
});
} catch (error) {
error.service = SERVICE_NAME;
error.method = "createDistributedChecks";
throw error;
}
};
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,
createDistributedChecks,
deleteDistributedChecksByMonitorId,
};

View File

@@ -3,7 +3,6 @@ import MonitorStats from "../../models/MonitorStats.js";
import Check from "../../models/Check.js";
import PageSpeedCheck from "../../models/PageSpeedCheck.js";
import HardwareCheck from "../../models/HardwareCheck.js";
import DistributedUptimeCheck from "../../models/DistributedUptimeCheck.js";
import Notification from "../../models/Notification.js";
import { NormalizeData, NormalizeDataUptimeDetails } from "../../../utils/dataUtils.js";
import ServiceRegistry from "../../../service/serviceRegistry.js";
@@ -385,72 +384,6 @@ const getUptimeDetailsById = async (req) => {
}
};
const getDistributedUptimeDetailsById = async (req) => {
try {
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));
}
const { dateRange, normalize } = req.query;
const dates = getDateRange(dateRange);
const formatLookup = {
recent: "%Y-%m-%dT%H:%M:00Z",
day: {
$concat: [
{ $dateToString: { format: "%Y-%m-%dT%H:", date: "$updatedAt" } },
{
$cond: [{ $lt: [{ $minute: "$updatedAt" }, 30] }, "00:00Z", "30:00Z"],
},
],
},
week: "%Y-%m-%dT%H:00:00Z",
month: "%Y-%m-%dT00:00:00Z",
};
const dateString = formatLookup[dateRange];
const monitorStatsResult = await MonitorStats.aggregate(
buildMonitorStatsPipeline(monitor)
);
const monitorStats = monitorStatsResult[0];
const dePINDetailsByDateRange = await DistributedUptimeCheck.aggregate(
buildDePINDetailsByDateRange(monitor, dates, dateString)
);
const latestChecks = await DistributedUptimeCheck.aggregate(
buildDePINLatestChecks(monitor)
);
const checkData = dePINDetailsByDateRange[0];
const normalizedGroupChecks = NormalizeDataUptimeDetails(
checkData.groupedChecks,
10,
100
);
const data = {
...monitor.toObject(),
latestChecks,
totalChecks: monitorStats?.totalChecks,
avgResponseTime: monitorStats?.avgResponseTime,
uptimePercentage: monitorStats?.uptimePercentage,
timeSinceLastCheck: monitorStats?.timeSinceLastCheck,
uptBurnt: monitorStats?.uptBurnt,
groupedChecks: normalizedGroupChecks,
groupedMapChecks: checkData.groupedMapChecks,
};
return data;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getDistributedUptimeDetailsById";
throw error;
}
};
/**
* Get stats by monitor ID
* @async
@@ -876,7 +809,6 @@ export {
getMonitorsAndSummaryByTeamId,
getMonitorsWithChecksByTeamId,
getUptimeDetailsById,
getDistributedUptimeDetailsById,
createMonitor,
createBulkMonitors,
deleteMonitor,

View File

@@ -613,8 +613,6 @@ const buildMonitorsWithChecksByTeamIdPipeline = ({
checksCollection = "pagespeedchecks";
} else if (type === "hardware") {
checksCollection = "hardwarechecks";
} else if (type === "distributed_http" || type === "distributed_test") {
checksCollection = "distributeduptimechecks";
}
monitorsPipeline.push({
$lookup: {
@@ -702,8 +700,6 @@ const buildFilteredMonitorsByTeamIdPipeline = ({
checksCollection = "pagespeedchecks";
} else if (type === "hardware") {
checksCollection = "hardwarechecks";
} else if (type === "distributed_http" || type === "distributed_test") {
checksCollection = "distributeduptimechecks";
}
pipeline.push({
$lookup: {
@@ -861,26 +857,6 @@ const buildGetMonitorsByTeamIdPipeline = (req) => {
},
]
: []),
...(limit
? [
{
$lookup: {
from: "distributeduptimechecks",
let: { monitorId: "$_id" },
pipeline: [
{
$match: {
$expr: { $eq: ["$monitorId", "$$monitorId"] },
},
},
{ $sort: { createdAt: -1 } },
...(limit ? [{ $limit: limit }] : []),
],
as: "distributeduptimechecks",
},
},
]
: []),
{
$addFields: {
@@ -899,14 +875,6 @@ const buildGetMonitorsByTeamIdPipeline = (req) => {
case: { $eq: ["$type", "hardware"] },
then: "$hardwarechecks",
},
{
case: { $eq: ["$type", "distributed_http"] },
then: "$distributeduptimechecks",
},
{
case: { $eq: ["$type", "distributed_test"] },
then: "$distributeduptimechecks",
},
],
default: [],
},

View File

@@ -61,105 +61,6 @@ const updateStatusPage = async (statusPageData, image) => {
}
};
const getDistributedStatusPageByUrl = async ({ url, daysToShow = 30 }) => {
const stringService = ServiceRegistry.get(StringService.SERVICE_NAME);
try {
const statusPage = await StatusPage.findOne({ url }).lean();
if (!statusPage) {
const error = new Error(stringService.statusPageNotFound);
error.status = 404;
throw error;
}
// No sub monitors, return status page
if (statusPage.subMonitors.length === 0) {
return statusPage;
}
// Sub monitors, return status page with sub monitors
const daysAgo = new Date();
daysAgo.setDate(daysAgo.getDate() - daysToShow);
const subMonitors = await Monitor.aggregate([
{ $match: { _id: { $in: statusPage.subMonitors } } },
{
$addFields: {
orderIndex: { $indexOfArray: [statusPage.subMonitors, "$_id"] },
},
},
// Return 30 days of checks by default
{
$lookup: {
from: "checks",
let: { monitorId: "$_id" },
pipeline: [
{
$match: {
$expr: {
$and: [
{ $eq: ["$monitorId", "$$monitorId"] },
{ $gte: ["$updatedAt", daysAgo] },
],
},
},
},
{
$group: {
_id: {
$dateToString: { format: "%Y-%m-%d", date: "$updatedAt" },
},
responseTime: {
$avg: "$responseTime",
},
trueCount: {
$sum: {
$cond: [{ $eq: ["$status", true] }, 1, 0],
},
},
totalCount: {
$sum: 1,
},
},
},
{
$project: {
_id: 1,
responseTime: 1,
upPercentage: {
$cond: [
{ $eq: ["$totalCount", 0] },
0,
{ $multiply: [{ $divide: ["$trueCount", "$totalCount"] }, 100] },
],
},
},
},
{
$sort: { _id: -1 },
},
],
as: "checks",
},
},
{ $sort: { orderIndex: 1 } },
{ $project: { orderIndex: 0 } },
]);
const normalizedSubMonitors = subMonitors.map((monitor) => {
return {
...monitor,
checks: NormalizeData(monitor.checks, 10, 100),
};
});
return { ...statusPage, subMonitors: normalizedSubMonitors };
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getDistributedStatusPageByUrl";
throw error;
}
};
const getStatusPageByUrl = async (url, type) => {
// TODO This is deprecated, can remove and have controller call getStatusPage
try {
@@ -306,7 +207,6 @@ export {
getStatusPagesByTeamId,
getStatusPage,
getStatusPageByUrl,
getDistributedStatusPageByUrl,
deleteStatusPage,
deleteStatusPagesByMonitorId,
};

View File

@@ -1,6 +1,5 @@
import Monitor from "../../models/Monitor.js";
import Check from "../../models/Check.js";
import DistributedUptimeCheck from "../../models/DistributedUptimeCheck.js";
import logger from "../../../utils/logger.js";
const generateRandomUrl = () => {
@@ -73,105 +72,4 @@ const seedDb = async (userId, teamId) => {
}
};
const generateDistributedChecks = (monitorId, teamId, count = 2880) => {
const checks = [];
const endTime = new Date();
const startTime = new Date(endTime - 48 * 60 * 60 * 1000);
// Sample locations for variety
const locations = [
{
city: "New York",
countryCode: "US",
continent: "NA",
location: { lat: 40.7128, lng: -74.006 },
},
{
city: "London",
countryCode: "GB",
continent: "EU",
location: { lat: 51.5074, lng: -0.1278 },
},
{
city: "Singapore",
countryCode: "SG",
continent: "AS",
location: { lat: 1.3521, lng: 103.8198 },
},
];
for (let i = 0; i < count; i++) {
const timestamp = new Date(startTime.getTime() + i * 60 * 1000);
const location = locations[Math.floor(Math.random() * locations.length)];
const status = Math.random() > 0.05; // 95% success rate
checks.push({
monitorId,
teamId,
status,
responseTime: Math.floor(Math.random() * 1000), // Random response time between 0-1000ms
first_byte_took: Math.floor(Math.random() * 300000), // 0-300ms
body_read_took: Math.floor(Math.random() * 100000), // 0-100ms
dns_took: Math.floor(Math.random() * 100000), // 0-100ms
conn_took: Math.floor(Math.random() * 200000), // 0-200ms
connect_took: Math.floor(Math.random() * 150000), // 0-150ms
tls_took: Math.floor(Math.random() * 200000), // 0-200ms
location: location.location,
continent: location.continent,
countryCode: location.countryCode,
city: location.city,
uptBurnt: "0.01", // Will be converted to Decimal128 by the schema
createdAt: timestamp,
updatedAt: timestamp,
});
}
return checks;
};
export const seedDistributedTest = async (userId, teamId) => {
try {
logger.info({
message: "Deleting all test monitors and checks",
service: "DB",
method: "seedDistributedTest",
});
const testMonitors = await Monitor.find({
type: "distributed_test",
});
testMonitors.forEach(async (monitor) => {
await DistributedUptimeCheck.deleteMany({ monitorId: monitor._id });
await Monitor.deleteOne({ _id: monitor._id });
});
logger.info({
message: "Adding test monitors and checks",
service: "DB",
method: "seedDistributedTest",
});
const monitor = await Monitor.create({
name: "Distributed Test",
url: "https://distributed-test.com",
type: "distributed_test",
userId,
teamId,
interval: 60000,
active: false,
});
const checks = generateDistributedChecks(monitor._id, teamId, 2800);
await DistributedUptimeCheck.insertMany(checks);
return monitor;
} catch (error) {
logger.error({
message: "Error seeding distributed test",
service: "DB",
method: "seedDistributedTest",
stack: error.stack,
});
throw error;
}
};
export default seedDb;

View File

@@ -36,9 +36,6 @@ import StatusPageController from "./controllers/statusPageController.js";
import QueueRoutes from "./routes/queueRoute.js";
import QueueController from "./controllers/queueController.js";
import DistributedUptimeRoutes from "./routes/distributedUptimeRoute.js";
import DistributedUptimeController from "./controllers/distributedUptimeController.js";
import NotificationRoutes from "./routes/notificationRoute.js";
import NotificationController from "./controllers/notificationController.js";
@@ -300,13 +297,6 @@ const startApp = async () => {
ServiceRegistry.get(StatusService.SERVICE_NAME)
);
const distributedUptimeController = new DistributedUptimeController({
db: ServiceRegistry.get(MongoDB.SERVICE_NAME),
http,
statusService: ServiceRegistry.get(StatusService.SERVICE_NAME),
logger,
});
const diagnosticController = new DiagnosticController(
ServiceRegistry.get(MongoDB.SERVICE_NAME)
);
@@ -322,9 +312,7 @@ const startApp = async () => {
);
const queueRoutes = new QueueRoutes(queueController);
const statusPageRoutes = new StatusPageRoutes(statusPageController);
const distributedUptimeRoutes = new DistributedUptimeRoutes(
distributedUptimeController
);
const notificationRoutes = new NotificationRoutes(notificationController);
const diagnosticRoutes = new DiagnosticRoutes(diagnosticController);
// Middleware
@@ -375,7 +363,6 @@ const startApp = async () => {
app.use("/api/v1/checks", verifyJWT, checkRoutes.getRouter());
app.use("/api/v1/maintenance-window", verifyJWT, maintenanceWindowRoutes.getRouter());
app.use("/api/v1/queue", verifyJWT, queueRoutes.getRouter());
app.use("/api/v1/distributed-uptime", distributedUptimeRoutes.getRouter());
app.use("/api/v1/status-page", statusPageRoutes.getRouter());
app.use("/api/v1/notifications", verifyJWT, notificationRoutes.getRouter());
app.use("/api/v1/diagnostic", verifyJWT, diagnosticRoutes.getRouter());

View File

@@ -7,11 +7,6 @@ class DiagnosticRoutes {
this.initRoutes();
}
initRoutes() {
this.router.get(
"/db/execution-stats/:monitorId",
this.diagnosticController.getDistributedUptimeDbExecutionStats
);
this.router.get(
"/db/get-monitors-by-team-id/:teamId",
this.diagnosticController.getMonitorsByTeamIdExecutionStats

View File

@@ -1,44 +0,0 @@
import { Router } from "express";
import { verifyJWT } from "../middleware/verifyJWT.js";
class DistributedUptimeRoutes {
constructor(distributedUptimeController) {
this.router = Router();
this.distributedUptimeController = distributedUptimeController;
this.initRoutes();
}
initRoutes() {
this.router.post("/callback", this.distributedUptimeController.resultsCallback);
this.router.get(
"/monitors/:teamId/initial",
verifyJWT,
this.distributedUptimeController.getDistributedUptimeMonitors
);
this.router.get(
"/monitors/:teamId",
this.distributedUptimeController.subscribeToDistributedUptimeMonitors
);
this.router.get(
"/monitors/details/:monitorId/initial",
verifyJWT,
this.distributedUptimeController.getDistributedUptimeMonitorDetails
);
this.router.get(
"/monitors/details/public/:monitorId/initial",
this.distributedUptimeController.getDistributedUptimeMonitorDetails
);
this.router.get(
"/monitors/details/:monitorId",
this.distributedUptimeController.subscribeToDistributedUptimeMonitorDetails
);
}
getRouter() {
return this.router;
}
}
export default DistributedUptimeRoutes;

View File

@@ -18,10 +18,6 @@ class StatusPageRoutes {
this.statusPageController.getStatusPagesByTeamId
);
this.router.get("/:url", this.statusPageController.getStatusPageByUrl);
this.router.get(
"/distributed/:url",
this.statusPageController.getDistributedStatusPageByUrl
);
this.router.post(
"/",

View File

@@ -1,4 +1,4 @@
const QUEUE_NAMES = ["uptime", "pagespeed", "hardware", "distributed"];
const QUEUE_NAMES = ["uptime", "pagespeed", "hardware"];
const SERVICE_NAME = "JobQueue";
const HEALTH_CHECK_INTERVAL = 10 * 60 * 1000; // 10 minutes
const QUEUE_LOOKUP = {
@@ -8,7 +8,6 @@ const QUEUE_LOOKUP = {
port: "uptime",
docker: "uptime",
pagespeed: "pagespeed",
distributed_http: "distributed",
};
const getSchedulerId = (monitor) => `scheduler:${monitor.type}:${monitor._id}`;

View File

@@ -256,13 +256,6 @@ class JobQueueHelper {
await job.updateProgress(30);
const monitor = job.data;
const networkResponse = await this.networkService.getStatus(monitor);
if (
job.data.type === "distributed_http" ||
job.data.type === "distributed_test"
) {
await job.updateProgress(100);
return true;
}
// If the network response is not found, we're done
if (!networkResponse) {

View File

@@ -29,10 +29,6 @@ class PulseQueueHelper {
}
const networkResponse = await this.networkService.getStatus(monitor);
if (monitor.type === "distributed_http" || monitor.type === "distributed_test") {
await job.updateProgress(100);
return true;
}
if (!networkResponse) {
throw new Error("No network response");

View File

@@ -7,7 +7,6 @@ const TYPE_MAP = {
docker: "checks",
pagespeed: "pagespeedChecks",
hardware: "hardwareChecks",
distributed_http: "distributedChecks",
};
class BufferService {
@@ -19,13 +18,11 @@ class BufferService {
checks: [],
pagespeedChecks: [],
hardwareChecks: [],
distributedChecks: [],
};
this.OPERATION_MAP = {
checks: this.db.createChecks,
pagespeedChecks: this.db.createPageSpeedChecks,
hardwareChecks: this.db.createHardwareChecks,
distributedChecks: this.db.createDistributedChecks,
};
this.scheduleNextFlush();

View File

@@ -23,8 +23,6 @@ class NetworkService {
this.TYPE_HARDWARE = "hardware";
this.TYPE_DOCKER = "docker";
this.TYPE_PORT = "port";
this.TYPE_DISTRIBUTED_HTTP = "distributed_http";
this.TYPE_DISTRIBUTED_TEST = "distributed_test";
this.SERVICE_NAME = SERVICE_NAME;
this.NETWORK_ERROR = 5000;
this.PING_ERROR = 5001;
@@ -414,34 +412,6 @@ class NetworkService {
}
}
async requestDistributedHttp(monitor) {
try {
const CALLBACK_URL = process.env.CALLBACK_URL;
const response = await this.axios.post(
UPROCK_ENDPOINT,
{
id: monitor._id,
url: monitor.url,
callback: `${CALLBACK_URL}/api/v1/distributed-uptime/callback`,
},
{
headers: {
"Content-Type": "application/json",
"x-checkmate-key": process.env.UPROCK_API_KEY,
},
}
);
if (response.data.success === false) {
throw new Error(response.data.message);
}
} catch (error) {
error.service = this.SERVICE_NAME;
error.method = "requestDistributedHttp";
throw error;
}
}
/**
* Handles unsupported job types by throwing an error with details.
*
@@ -517,10 +487,6 @@ class NetworkService {
return await this.requestDocker(monitor);
case this.TYPE_PORT:
return await this.requestPort(monitor);
case this.TYPE_DISTRIBUTED_HTTP:
return await this.requestDistributedHttp(monitor);
case this.TYPE_DISTRIBUTED_TEST:
return;
default:
return this.handleUnsupportedType(type);
}

View File

@@ -209,23 +209,6 @@ class StatusService {
tls_took,
};
if (type === "distributed_http") {
if (typeof payload === "undefined") {
return undefined;
}
check.continent = payload.continent;
check.countryCode = payload.country_code;
check.city = payload.city;
check.location = payload.location;
check.uptBurnt = payload.upt_burnt;
check.first_byte_took = payload.first_byte_took;
check.body_read_took = payload.body_read_took;
check.dns_took = payload.dns_took;
check.conn_took = payload.conn_took;
check.connect_took = payload.connect_took;
check.tls_took = payload.tls_took;
}
if (type === "pagespeed") {
if (typeof payload === "undefined") {
this.logger.warn({

View File

@@ -303,18 +303,7 @@ const getChecksParamValidation = joi.object({
});
const getChecksQueryValidation = joi.object({
type: joi
.string()
.valid(
"http",
"ping",
"pagespeed",
"hardware",
"docker",
"port",
"distributed_http",
"distributed_test"
),
type: joi.string().valid("http", "ping", "pagespeed", "hardware", "docker", "port"),
sortOrder: joi.string().valid("asc", "desc"),
limit: joi.number(),
dateRange: joi.string().valid("recent", "hour", "day", "week", "month", "all"),
@@ -446,14 +435,14 @@ const getStatusPageParamValidation = joi.object({
});
const getStatusPageQueryValidation = joi.object({
type: joi.string().valid("uptime", "distributed").required(),
type: joi.string().valid("uptime").required(),
timeFrame: joi.number().optional(),
});
const createStatusPageBodyValidation = joi.object({
userId: joi.string().required(),
teamId: joi.string().required(),
type: joi.string().valid("uptime", "distributed").required(),
type: joi.string().valid("uptime").required(),
companyName: joi.string().required(),
url: joi
.string()