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 ( + + + + + + + + + {isAdmin && ( + + )} + + + ); +}; + +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("distributedUptimeCreateSelectURL")} @@ -251,7 +251,7 @@ const CreatePageSpeed = () => { - {t("distributedUptimeCreateChecks")} + {t("distributedUptimeCreateChecks")} {t("distributedUptimeCreateChecksDescription")} @@ -300,7 +300,7 @@ const CreatePageSpeed = () => { - {t("distributedUptimeCreateIncidentNotification")} + {t("distributedUptimeCreateIncidentNotification")} {t("distributedUptimeCreateIncidentDescription")} @@ -320,7 +320,7 @@ const CreatePageSpeed = () => { - {t("distributedUptimeCreateAdvancedSettings")} + {t("distributedUptimeCreateAdvancedSettings")}