diff --git a/.github/workflows/distribution-deploy.yml b/.github/workflows/distribution-deploy.yml
index b96a0f3d7..69f3afd73 100644
--- a/.github/workflows/distribution-deploy.yml
+++ b/.github/workflows/distribution-deploy.yml
@@ -11,6 +11,9 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
@@ -36,6 +39,9 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
diff --git a/client/package-lock.json b/client/package-lock.json
index b7859e0fe..71b7c6efe 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -52,7 +52,7 @@
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.6",
"prettier": "^3.3.3",
- "vite": "^5.2.0"
+ "vite": "^5.4.19"
}
},
"node_modules/@ampproject/remapping": {
@@ -6544,9 +6544,9 @@
}
},
"node_modules/vite": {
- "version": "5.4.18",
- "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.18.tgz",
- "integrity": "sha512-1oDcnEp3lVyHCuQ2YFelM4Alm2o91xNoMncRm1U7S+JdYfYOvbiGZ3/CxGttrOu2M/KcGz7cRC2DoNUA6urmMA==",
+ "version": "5.4.19",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz",
+ "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==",
"license": "MIT",
"dependencies": {
"esbuild": "^0.21.3",
diff --git a/client/package.json b/client/package.json
index ba745503a..f8f699be0 100644
--- a/client/package.json
+++ b/client/package.json
@@ -63,6 +63,6 @@
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.6",
"prettier": "^3.3.3",
- "vite": "^5.2.0"
+ "vite": "^5.4.19"
}
}
diff --git a/client/src/Components/MonitorDetailsControlHeader/index.jsx b/client/src/Components/MonitorDetailsControlHeader/index.jsx
new file mode 100644
index 000000000..920d5a29c
--- /dev/null
+++ b/client/src/Components/MonitorDetailsControlHeader/index.jsx
@@ -0,0 +1,118 @@
+import Stack from "@mui/material/Stack";
+import Status from "./status";
+import Skeleton from "./skeleton";
+import Button from "@mui/material/Button";
+import SettingsOutlinedIcon from "@mui/icons-material/SettingsOutlined";
+import PauseOutlinedIcon from "@mui/icons-material/PauseOutlined";
+import PlayArrowOutlinedIcon from "@mui/icons-material/PlayArrowOutlined";
+import EmailIcon from "@mui/icons-material/Email";
+
+// Utils
+import PropTypes from "prop-types";
+import { useNavigate } from "react-router-dom";
+import { useTheme } from "@mui/material/styles";
+import { usePauseMonitor } from "../../Hooks/useMonitorControls";
+import { useSendTestEmail } from "../../Hooks/useSendTestEmail";
+import { useTranslation } from "react-i18next";
+
+/**
+ * MonitorDetailsControlHeader component displays the control header for monitor details.
+ * It includes status display, pause/resume button, and a configure button for admins.
+ *
+ * @component
+ * @param {Object} props - Component props
+ * @param {string} props.path - The base path for navigation
+ * @param {boolean} [props.isLoading=false] - Flag indicating if the data is loading
+ * @param {boolean} [props.isAdmin=false] - Flag indicating if the user is an admin
+ * @param {Object} props.monitor - The monitor object containing details
+ * @param {Function} props.triggerUpdate - Function to trigger an update
+ * @returns {JSX.Element} The rendered component
+ */
+const MonitorDetailsControlHeader = ({
+ path,
+ isLoading = false,
+ isAdmin = false,
+ monitor,
+ triggerUpdate,
+}) => {
+ const navigate = useNavigate();
+ const theme = useTheme();
+ const { t } = useTranslation();
+ const [pauseMonitor, isPausing, error] = usePauseMonitor({
+ monitorId: monitor?._id,
+ triggerUpdate,
+ });
+
+ const [isSending, emailError, sendTestEmail] = useSendTestEmail();
+
+ if (isLoading) {
+ return ;
+ }
+
+ return (
+
+
+
+
+ }
+ onClick={sendTestEmail}
+ >
+ {t("sendTestEmail")}
+
+
+ :
+ }
+ onClick={() => {
+ pauseMonitor();
+ }}
+ >
+ {monitor?.isActive ? "Pause" : "Resume"}
+
+
+ {isAdmin && (
+ }
+ onClick={() => navigate(`/${path}/configure/${monitor._id}`)}
+ >
+ Configure
+
+ )}
+
+
+ );
+};
+
+MonitorDetailsControlHeader.propTypes = {
+ path: PropTypes.string,
+ isLoading: PropTypes.bool,
+ isAdmin: PropTypes.bool,
+ monitor: PropTypes.object,
+ triggerUpdate: PropTypes.func,
+};
+
+export default MonitorDetailsControlHeader;
diff --git a/client/src/Components/MonitorDetailsControlHeader/skeleton.jsx b/client/src/Components/MonitorDetailsControlHeader/skeleton.jsx
new file mode 100644
index 000000000..64dc0547f
--- /dev/null
+++ b/client/src/Components/MonitorDetailsControlHeader/skeleton.jsx
@@ -0,0 +1,23 @@
+import { Stack, Skeleton } from "@mui/material";
+
+const SkeletonLayout = () => {
+ return (
+
+
+
+
+ );
+};
+
+export default SkeletonLayout;
diff --git a/client/src/Components/MonitorDetailsControlHeader/status.jsx b/client/src/Components/MonitorDetailsControlHeader/status.jsx
new file mode 100644
index 000000000..9e01dd502
--- /dev/null
+++ b/client/src/Components/MonitorDetailsControlHeader/status.jsx
@@ -0,0 +1,56 @@
+// Components
+import Stack from "@mui/material/Stack";
+import PulseDot from "../../Components/Animated/PulseDot";
+import Typography from "@mui/material/Typography";
+import Dot from "../../Components/Dot";
+// Utils
+import { formatDurationRounded } from "../../Utils/timeUtils";
+import PropTypes from "prop-types";
+import { useTheme } from "@emotion/react";
+import useUtils from "../../Pages/Uptime/Monitors/Hooks/useUtils";
+
+/**
+ * Status component displays the status information of a monitor.
+ * It includes the monitor's name, URL, and check interval.
+ *
+ * @component
+ * @param {Object} props - Component props
+ * @param {Object} props.monitor - The monitor object containing details
+ * @param {string} props.monitor.name - The name of the monitor
+ * @param {string} props.monitor.url - The URL of the monitor
+ * @param {number} props.monitor.interval - The interval at which the monitor checks
+ * @returns {JSX.Element} The rendered component
+ */
+const Status = ({ monitor }) => {
+ const theme = useTheme();
+ const { statusColor, determineState } = useUtils();
+
+ return (
+
+ {monitor?.name}
+
+
+
+ {monitor?.url?.replace(/^https?:\/\//, "") || "..."}
+
+
+
+ Checking every {formatDurationRounded(monitor?.interval)}.
+
+
+
+ );
+};
+
+Status.propTypes = {
+ monitor: PropTypes.object,
+};
+
+export default Status;
diff --git a/client/src/Hooks/useFetchUptimeMonitorDetails.js b/client/src/Hooks/useFetchUptimeMonitorDetails.js
index c8cb81e65..3c0ac7ee6 100644
--- a/client/src/Hooks/useFetchUptimeMonitorDetails.js
+++ b/client/src/Hooks/useFetchUptimeMonitorDetails.js
@@ -3,13 +3,12 @@ import { networkService } from "../main";
import { useNavigate } from "react-router-dom";
import { createToast } from "../Utils/toastUtils";
-export const useFetchUptimeMonitorDetails = ({ monitorId, dateRange }) => {
+export const useFetchUptimeMonitorDetails = ({ monitorId, dateRange, trigger }) => {
const [networkError, setNetworkError] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [monitor, setMonitor] = useState(undefined);
const [monitorStats, setMonitorStats] = useState(undefined);
const navigate = useNavigate();
-
useEffect(() => {
const fetchMonitors = async () => {
try {
@@ -29,7 +28,7 @@ export const useFetchUptimeMonitorDetails = ({ monitorId, dateRange }) => {
}
};
fetchMonitors();
- }, [dateRange, monitorId, navigate]);
+ }, [dateRange, monitorId, navigate, trigger]);
return [monitor, monitorStats, isLoading, networkError];
};
diff --git a/client/src/Hooks/useMonitorControls.js b/client/src/Hooks/useMonitorControls.js
new file mode 100644
index 000000000..b47603b58
--- /dev/null
+++ b/client/src/Hooks/useMonitorControls.js
@@ -0,0 +1,28 @@
+import { useState } from "react";
+import { networkService } from "../main";
+import { createToast } from "../Utils/toastUtils";
+
+const usePauseMonitor = ({ monitorId, triggerUpdate }) => {
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(undefined);
+ const pauseMonitor = async () => {
+ try {
+ setIsLoading(true);
+ const res = await networkService.pauseMonitorById({ monitorId });
+ createToast({
+ body: res.data.data.isActive
+ ? "Monitor resumed successfully"
+ : "Monitor paused successfully",
+ });
+ triggerUpdate();
+ } catch (error) {
+ setError(error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return [pauseMonitor, isLoading, error];
+};
+
+export { usePauseMonitor };
diff --git a/client/src/Hooks/useSendTestEmail.js b/client/src/Hooks/useSendTestEmail.js
index fef45c3ea..ca6cfcf9e 100644
--- a/client/src/Hooks/useSendTestEmail.js
+++ b/client/src/Hooks/useSendTestEmail.js
@@ -2,11 +2,13 @@ import { useState } from "react";
import { networkService } from "../main";
import { useSelector } from "react-redux";
import { createToast } from "../Utils/toastUtils";
+import { useTranslation } from "react-i18next";
const useSendTestEmail = () => {
const [isSending, setIsSending] = useState(false);
const [error, setError] = useState(null);
const user = useSelector((state) => state.auth.user);
+ const { t } = useTranslation();
const sendTestEmail = async () => {
try {
@@ -16,12 +18,15 @@ const useSendTestEmail = () => {
const response = await networkService.sendTestEmail({ to: user.email });
if (typeof response?.data?.data?.messageId !== "undefined") {
createToast({
- body: "Test email sent successfully",
+ body: t("emailSent"),
});
} else {
- throw new Error("Failed to send test email");
+ throw new Error(t("failedToSendEmail"));
}
} catch (error) {
+ createToast({
+ body: t("failedToSendEmail"),
+ });
setError(error);
} finally {
setIsSending(false);
diff --git a/client/src/Pages/Infrastructure/Create/index.jsx b/client/src/Pages/Infrastructure/Create/index.jsx
index 144f499fd..a928037b7 100644
--- a/client/src/Pages/Infrastructure/Create/index.jsx
+++ b/client/src/Pages/Infrastructure/Create/index.jsx
@@ -294,7 +294,7 @@ const CreateInfrastructureMonitor = () => {
- {t("settingsGeneralSettings")}
+ {t("settingsGeneralSettings")}
{t("infrastructureCreateGeneralSettingsDescription")}
@@ -368,7 +368,7 @@ const CreateInfrastructureMonitor = () => {
- {t("distributedUptimeCreateIncidentNotification")}
+ {t("distributedUptimeCreateIncidentNotification")}
{t("distributedUptimeCreateIncidentDescription")}
@@ -385,7 +385,7 @@ const CreateInfrastructureMonitor = () => {
- {t("infrastructureCustomizeAlerts")}
+ {t("infrastructureCustomizeAlerts")}
{t("infrastructureAlertNotificationDescription")}
@@ -432,7 +432,7 @@ const CreateInfrastructureMonitor = () => {
- {t("distributedUptimeCreateAdvancedSettings")}
+ {t("distributedUptimeCreateAdvancedSettings")}
- {t("settingsGeneralSettings")}
+ {t("settingsGeneralSettings")}
{t("pageSpeedConfigureSettingsDescription")}
@@ -349,7 +349,7 @@ const PageSpeedConfigure = () => {
- {t("distributedUptimeCreateIncidentNotification")}
+ {t("distributedUptimeCreateIncidentNotification")}
{t("distributedUptimeCreateIncidentDescription")}
@@ -405,7 +405,7 @@ const PageSpeedConfigure = () => {
- {t("distributedUptimeCreateAdvancedSettings")}
+ {t("distributedUptimeCreateAdvancedSettings")}