Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: fe/config du monitor #1752

Merged
merged 14 commits into from
Feb 13, 2025
2 changes: 1 addition & 1 deletion Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export const updateUptimeMonitor = createAsyncThunk(
const res = await networkService.updateMonitor({
authToken: authToken,
monitorId: monitor._id,
updatedFields: updatedFields,
monitor,
});
return res.data;
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useState } from "react";
import { networkService } from "../../../../main";
import { useSelector } from "react-redux";
import { createToast } from "../../../../Utils/toastUtils";

const useCreateDistributedUptimeMonitor = ({ isCreate, monitorId }) => {
const { authToken, user } = useSelector((state) => state.auth);

const [isLoading, setIsLoading] = useState(false);
const [networkError, setNetworkError] = useState(false);
const createDistributedUptimeMonitor = async ({ form }) => {
setIsLoading(true);
try {
if (isCreate) {
await networkService.createMonitor({ authToken, monitor: form });
} else {
await networkService.updateMonitor({ authToken, monitor: form, monitorId });
}

return true;
} catch (error) {
setNetworkError(true);
createToast({ body: error?.response?.data?.msg ?? error.message });
return false;
} finally {
setIsLoading(false);
}
};

return [createDistributedUptimeMonitor, isLoading, networkError];
};

export { useCreateDistributedUptimeMonitor };
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useEffect, useState } from "react";
import { networkService } from "../../../../main";
import { createToast } from "../../../../Utils/toastUtils";

export const useMonitorFetch = ({ authToken, monitorId, isCreate }) => {
const [networkError, setNetworkError] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [monitor, setMonitor] = useState(undefined);

useEffect(() => {
const fetchMonitors = async () => {
try {
if (isCreate) return;
const res = await networkService.getUptimeDetailsById({
authToken: authToken,
monitorId: monitorId,
normalize: true,
});
setMonitor(res?.data?.data ?? {});
} catch (error) {
setNetworkError(true);
createToast({ body: error.message });
} finally {
setIsLoading(false);
}
};
fetchMonitors();
}, [authToken, monitorId, isCreate]);
Comment on lines +10 to +28
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Knees weak from missing cleanup and retries!

The fetch logic needs some improvements:

  1. Add abort controller for cleanup
  2. Add retry mechanism for network resilience
  3. Consider adding request caching

Here's how to make it stronger:

 	useEffect(() => {
+		const controller = new AbortController();
+		let retryCount = 0;
+		const MAX_RETRIES = 3;
+
 		const fetchMonitors = async () => {
 			try {
 				if (isCreate) return;
 				const res = await networkService.getUptimeDetailsById({
 					authToken: authToken,
 					monitorId: monitorId,
 					normalize: true,
+					signal: controller.signal
 				});
 				setMonitor(res?.data?.data ?? {});
 			} catch (error) {
+				if (error.name === 'AbortError') return;
+				
+				if (retryCount < MAX_RETRIES) {
+					retryCount++;
+					console.log(`Retrying fetch (${retryCount}/${MAX_RETRIES})...`);
+					await new Promise(resolve => setTimeout(resolve, 1000 * retryCount));
+					return fetchMonitors();
+				}
+
 				setNetworkError(true);
 				createToast({ body: error.message });
 			} finally {
 				setIsLoading(false);
 			}
 		};
 		fetchMonitors();
+		
+		return () => controller.abort();
 	}, [authToken, monitorId, isCreate]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
const fetchMonitors = async () => {
try {
if (isCreate) return;
const res = await networkService.getUptimeDetailsById({
authToken: authToken,
monitorId: monitorId,
normalize: true,
});
setMonitor(res?.data?.data ?? {});
} catch (error) {
setNetworkError(true);
createToast({ body: error.message });
} finally {
setIsLoading(false);
}
};
fetchMonitors();
}, [authToken, monitorId, isCreate]);
useEffect(() => {
const controller = new AbortController();
let retryCount = 0;
const MAX_RETRIES = 3;
const fetchMonitors = async () => {
try {
if (isCreate) return;
const res = await networkService.getUptimeDetailsById({
authToken: authToken,
monitorId: monitorId,
normalize: true,
signal: controller.signal
});
setMonitor(res?.data?.data ?? {});
} catch (error) {
if (error.name === 'AbortError') return;
if (retryCount < MAX_RETRIES) {
retryCount++;
console.log(`Retrying fetch (${retryCount}/${MAX_RETRIES})...`);
await new Promise(resolve => setTimeout(resolve, 1000 * retryCount));
return fetchMonitors();
}
setNetworkError(true);
createToast({ body: error.message });
} finally {
setIsLoading(false);
}
};
fetchMonitors();
return () => controller.abort();
}, [authToken, monitorId, isCreate]);

return [monitor, isLoading, networkError];
};

export default useMonitorFetch;
98 changes: 67 additions & 31 deletions Client/src/Pages/DistributedUptime/Create/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,15 @@ import { createToast } from "../../../Utils/toastUtils";

// Utility
import { useTheme } from "@emotion/react";
import { useState } from "react";
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useSelector, useDispatch } from "react-redux";
import { useSelector } from "react-redux";
import { monitorValidation } from "../../../Validation/validation";
import { createUptimeMonitor } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
import { useLocation } from "react-router-dom";
import { useParams } from "react-router-dom";
import { useCreateDistributedUptimeMonitor } from "./Hooks/useCreateDistributedUptimeMonitor";
import { useMonitorFetch } from "./Hooks/useMonitorFetch";

// Constants
const BREADCRUMBS = [
{ name: `distributed uptime`, path: "/distributed-uptime" },
{ name: "create", path: `/distributed-uptime/create` },
];
const MS_PER_MINUTE = 60000;
const SELECT_VALUES = [
{ _id: 1, name: "1 minute" },
Expand All @@ -34,18 +31,30 @@ const SELECT_VALUES = [
{ _id: 5, name: "5 minutes" },
];

const parseUrl = (url) => {
try {
return new URL(url);
} catch (error) {
return null;
}
};

const CreateDistributedUptime = () => {
const location = useLocation();
const isCreate = location.pathname.startsWith("/distributed-uptime/create");
const { monitorId } = useParams();
const isCreate = typeof monitorId === "undefined";

const BREADCRUMBS = [
{ name: `distributed uptime`, path: "/distributed-uptime" },
{ name: isCreate ? "create" : "configure", path: `` },
];

// Redux state
const { user, authToken } = useSelector((state) => state.auth);
const isLoading = useSelector((state) => state.uptimeMonitors.isLoading);

// Local state
const [https, setHttps] = useState(true);
const [notifications, setNotifications] = useState([]);
const [monitor, setMonitor] = useState({
const [form, setForm] = useState({
type: "distributed_http",
name: "",
url: "",
Expand All @@ -55,20 +64,44 @@ const CreateDistributedUptime = () => {

//utils
const theme = useTheme();
const dispatch = useDispatch();
const navigate = useNavigate();
const [createDistributedUptimeMonitor, isLoading, networkError] =
useCreateDistributedUptimeMonitor({ isCreate, monitorId });

const [monitor, monitorIsLoading, monitorNetworkError] = useMonitorFetch({
authToken,
monitorId,
isCreate,
});

// Effect to set monitor to fetched monitor
useEffect(() => {
if (typeof monitor !== "undefined") {
const parsedUrl = parseUrl(monitor?.url);
const protocol = parsedUrl?.protocol?.replace(":", "") || "";
setHttps(protocol === "https");

const newForm = {
name: monitor.name,
interval: monitor.interval / MS_PER_MINUTE,
url: parsedUrl.host,
type: monitor.type,
};

setForm(newForm);
}
}, [monitor]);

// Handlers
const handleCreateMonitor = async (event) => {
const monitorToSubmit = { ...monitor };
const handleCreateMonitor = async () => {
const monitorToSubmit = { ...form };

// Prepend protocol to url
monitorToSubmit.url = `http${https ? "s" : ""}://` + monitorToSubmit.url;

const { error } = monitorValidation.validate(monitorToSubmit, {
abortEarly: false,
});

if (error) {
const newErrors = {};
error.details.forEach((err) => {
Expand All @@ -80,16 +113,14 @@ const CreateDistributedUptime = () => {
}

// Append needed fields
monitorToSubmit.description = monitor.name;
monitorToSubmit.interval = monitor.interval * MS_PER_MINUTE;
monitorToSubmit.description = form.name;
monitorToSubmit.interval = form.interval * MS_PER_MINUTE;
monitorToSubmit.teamId = user.teamId;
monitorToSubmit.userId = user._id;
monitorToSubmit.notifications = notifications;

const action = await dispatch(
createUptimeMonitor({ authToken, monitor: monitorToSubmit })
);
if (action.meta.requestStatus === "fulfilled") {
const success = await createDistributedUptimeMonitor({ form: monitorToSubmit });
if (success) {
createToast({ body: "Monitor created successfully!" });
navigate("/distributed-uptime");
} else {
Expand All @@ -98,9 +129,10 @@ const CreateDistributedUptime = () => {
};

const handleChange = (event) => {
const { name, value } = event.target;
setMonitor({
...monitor,
let { name, value } = event.target;

setForm({
...form,
[name]: value,
});
const { error } = monitorValidation.validate(
Expand Down Expand Up @@ -178,7 +210,8 @@ const CreateDistributedUptime = () => {
label="URL to monitor"
https={https}
placeholder={"www.google.com"}
value={monitor.url}
disabled={!isCreate}
value={form.url}
name="url"
onChange={handleChange}
error={errors["url"] ? true : false}
Expand All @@ -190,7 +223,7 @@ const CreateDistributedUptime = () => {
label="Display name"
isOptional={true}
placeholder={"Google"}
value={monitor.name}
value={form.name}
name="name"
onChange={handleChange}
error={errors["name"] ? true : false}
Expand All @@ -217,8 +250,11 @@ const CreateDistributedUptime = () => {
checked={true}
onChange={handleChange}
/>
{monitor.type === "http" || monitor.type === "distributed_http" ? (
<ButtonGroup sx={{ ml: theme.spacing(16) }}>
{form.type === "http" || form.type === "distributed_http" ? (
<ButtonGroup
disabled={!isCreate}
sx={{ ml: theme.spacing(16) }}
>
<Button
variant="group"
filled={https.toString()}
Expand Down Expand Up @@ -282,7 +318,7 @@ const CreateDistributedUptime = () => {
id="monitor-interval"
label="Check frequency"
name="interval"
value={monitor.interval || 1}
value={form.interval}
onChange={handleChange}
items={SELECT_VALUES}
/>
Expand All @@ -299,7 +335,7 @@ const CreateDistributedUptime = () => {
disabled={!Object.values(errors).every((value) => value === undefined)}
loading={isLoading}
>
Create monitor
{isCreate ? "Create monitor" : "Configure monitor"}
</LoadingButton>
</Stack>
</Stack>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Components
import { Box, Stack, Typography, Button } from "@mui/material";
import Image from "../../../../../Components/Image";
import SettingsIcon from "../../../../../assets/icons/settings-bold.svg?react";

//Utils
import { useTheme } from "@mui/material/styles";
import { useNavigate } from "react-router-dom";
import { useLocation } from "react-router-dom";
import PropTypes from "prop-types";

const Controls = ({ isDeleteOpen, setIsDeleteOpen, isDeleting, monitorId }) => {
const theme = useTheme();
const navigate = useNavigate();

return (
<Stack
direction="row"
gap={theme.spacing(2)}
>
<Box>
<Button
variant="contained"
color="error"
onClick={() => setIsDeleteOpen(!isDeleteOpen)}
loading={isDeleting}
>
Delete
</Button>
</Box>
<Box>
<Button
variant="contained"
color="secondary"
onClick={() => {
navigate(`/distributed-uptime/configure/${monitorId}`);
}}
sx={{
px: theme.spacing(5),
"& svg": {
mr: theme.spacing(3),
"& path": {
stroke: theme.palette.secondary.contrastText,
},
},
}}
>
<SettingsIcon /> Configure
</Button>
</Box>
</Stack>
);
};

Controls.propTypes = {
isDeleting: PropTypes.bool,
monitorId: PropTypes.string,
isDeleteOpen: PropTypes.bool.isRequired,
setIsDeleteOpen: PropTypes.func.isRequired,
};

const ControlsHeader = ({ isDeleting, isDeleteOpen, setIsDeleteOpen, monitorId }) => {
const theme = useTheme();

return (
<Stack
alignSelf="flex-start"
direction="row"
width="100%"
gap={theme.spacing(2)}
justifyContent="flex-end"
alignItems="flex-end"
>
<Controls
isDeleting={isDeleting}
isDeleteOpen={isDeleteOpen}
setIsDeleteOpen={setIsDeleteOpen}
monitorId={monitorId}
/>
</Stack>
);
};

ControlsHeader.propTypes = {
monitorId: PropTypes.string,
isDeleting: PropTypes.bool,
isDeleteOpen: PropTypes.bool.isRequired,
setIsDeleteOpen: PropTypes.func.isRequired,
};

export default ControlsHeader;
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { useSelector } from "react-redux";
import { useState } from "react";
import { networkService } from "../../../../main";
import { createToast } from "../../../../Utils/toastUtils";

const useDeleteMonitor = ({ monitorId }) => {
const [isLoading, setIsLoading] = useState(false);
const { authToken } = useSelector((state) => state.auth);
const deleteMonitor = async () => {
try {
setIsLoading(true);
await networkService.deleteMonitorById({ authToken, monitorId });
return true;
} catch (error) {
createToast({
body: error.message,
});
return false;
} finally {
setIsLoading(false);
}
};

return [deleteMonitor, isLoading];
};

export { useDeleteMonitor };
Loading