diff --git a/README.md b/README.md
index 129953fb9..30b406684 100644
--- a/README.md
+++ b/README.md
@@ -79,9 +79,8 @@ You can see the memory footprint of MongoDB and Redis on the same server (398Mb
If you have any questions, suggestions or comments, you have several options:
-- [Discord channel](https://discord.gg/NAb6H3UTjK)
-- [GitHub Discussions](https://github.com/bluewave-labs/bluewave-uptime/discussions)
-- [Reddit group](https://www.reddit.com/r/CheckmateMonitoring/)
+- [Discord channel](https://discord.gg/NAb6H3UTjK) (preferred)
+- [GitHub Discussions](https://github.com/bluewave-labs/bluewave-uptime/discussions) (we check here from time to time)
Feel free to ask questions or share your ideas - we'd love to hear from you!
diff --git a/client/package-lock.json b/client/package-lock.json
index 282ab2611..d860ae844 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -26,10 +26,10 @@
"joi": "17.13.3",
"lucide-react": "^0.562.0",
"mui-color-input": "^6.0.0",
- "pretty-ms": "9.3.0",
+ "pretty-ms": "^9.3.0",
"react": "18.3.1",
"react-dom": "^18.2.0",
- "react-hook-form": "7.63.0",
+ "react-hook-form": "^7.63.0",
"react-i18next": "^15.4.0",
"react-icons": "5.5.0",
"react-redux": "9.2.0",
@@ -40,7 +40,7 @@
"redux-persist": "6.0.0",
"swr": "^2.3.6",
"vite-plugin-svgr": "^4.2.0",
- "zod": "4.1.11"
+ "zod": "^4.1.11"
},
"devDependencies": {
"@types/node": "24.5.2",
diff --git a/client/package.json b/client/package.json
index 7c5acb441..09599143a 100644
--- a/client/package.json
+++ b/client/package.json
@@ -31,10 +31,10 @@
"joi": "17.13.3",
"lucide-react": "^0.562.0",
"mui-color-input": "^6.0.0",
- "pretty-ms": "9.3.0",
+ "pretty-ms": "^9.3.0",
"react": "18.3.1",
"react-dom": "^18.2.0",
- "react-hook-form": "7.63.0",
+ "react-hook-form": "^7.63.0",
"react-i18next": "^15.4.0",
"react-icons": "5.5.0",
"react-redux": "9.2.0",
@@ -45,7 +45,7 @@
"redux-persist": "6.0.0",
"swr": "^2.3.6",
"vite-plugin-svgr": "^4.2.0",
- "zod": "4.1.11"
+ "zod": "^4.1.11"
},
"devDependencies": {
"@types/node": "24.5.2",
diff --git a/client/src/App.jsx b/client/src/App.jsx
index af2f6c11c..da83f9061 100644
--- a/client/src/App.jsx
+++ b/client/src/App.jsx
@@ -37,7 +37,14 @@ function App() {
-
+
);
}
diff --git a/client/src/Components/v1/Charts/BarChart/index.jsx b/client/src/Components/v1/Charts/BarChart/index.jsx
index 9f0ed8aa6..77b836cae 100644
--- a/client/src/Components/v1/Charts/BarChart/index.jsx
+++ b/client/src/Components/v1/Charts/BarChart/index.jsx
@@ -1,6 +1,6 @@
import { useTheme } from "@emotion/react";
import { Box, Stack, Tooltip, Typography } from "@mui/material";
-import { formatDateWithTz } from "../../../../Utils/timeUtils.js";
+import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js";
import { useEffect, useState } from "react";
import "./index.css";
import { useSelector } from "react-redux";
diff --git a/client/src/Components/v1/Charts/DePINStatusPageBarChart/index.jsx b/client/src/Components/v1/Charts/DePINStatusPageBarChart/index.jsx
index b37990e46..8b6c2b165 100644
--- a/client/src/Components/v1/Charts/DePINStatusPageBarChart/index.jsx
+++ b/client/src/Components/v1/Charts/DePINStatusPageBarChart/index.jsx
@@ -5,7 +5,7 @@ import { Stack, Box, Tooltip, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import { useState, useEffect } from "react";
import PropTypes from "prop-types";
-import { formatDateWithTz } from "../../../../Utils/timeUtils.js";
+import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js";
import { useSelector } from "react-redux";
const PlaceholderCheck = ({ daysToShow }) => {
diff --git a/client/src/Components/v1/Charts/MonitorDetailsAreaChart/index.jsx b/client/src/Components/v1/Charts/MonitorDetailsAreaChart/index.jsx
index a9ef04a53..dafca7b40 100644
--- a/client/src/Components/v1/Charts/MonitorDetailsAreaChart/index.jsx
+++ b/client/src/Components/v1/Charts/MonitorDetailsAreaChart/index.jsx
@@ -12,7 +12,7 @@ import { Box, Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import { useMemo, useState } from "react";
import { useSelector } from "react-redux";
-import { formatDateWithTz } from "../../../../Utils/timeUtils.js";
+import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js";
import {
tooltipDateFormatLookup,
tickDateFormatLookup,
diff --git a/client/src/Components/v1/Charts/StatusPageBarChart/index.jsx b/client/src/Components/v1/Charts/StatusPageBarChart/index.jsx
index 309201b84..2dc5e61f0 100644
--- a/client/src/Components/v1/Charts/StatusPageBarChart/index.jsx
+++ b/client/src/Components/v1/Charts/StatusPageBarChart/index.jsx
@@ -1,6 +1,6 @@
import { useTheme } from "@emotion/react";
import { Box, Stack, Tooltip, Typography } from "@mui/material";
-import { formatDateWithTz } from "../../../../Utils/timeUtils.js";
+import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js";
import { useEffect, useState, forwardRef } from "react";
import { useSelector } from "react-redux";
import PropTypes from "prop-types";
diff --git a/client/src/Components/v1/Charts/Utils/chartUtils.jsx b/client/src/Components/v1/Charts/Utils/chartUtils.jsx
index 1bab80595..7797ba462 100644
--- a/client/src/Components/v1/Charts/Utils/chartUtils.jsx
+++ b/client/src/Components/v1/Charts/Utils/chartUtils.jsx
@@ -2,7 +2,7 @@ import PropTypes from "prop-types";
import { useSelector } from "react-redux";
import { useTheme } from "@mui/material";
import { Text } from "recharts";
-import { formatDateWithTz } from "../../../../Utils/timeUtils.js";
+import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js";
import { Box, Stack, Typography } from "@mui/material";
import { tickDateFormatLookup, tooltipDateFormatLookup } from "./chartUtilFunctions.js";
/**
diff --git a/client/src/Components/v1/FilterHeader/index.jsx b/client/src/Components/v1/FilterHeader/index.jsx
deleted file mode 100644
index de2c7f0e9..000000000
--- a/client/src/Components/v1/FilterHeader/index.jsx
+++ /dev/null
@@ -1,100 +0,0 @@
-import { Checkbox, FormControl, ListItemText, MenuItem, Select } from "@mui/material";
-import { useTheme } from "@emotion/react";
-import PropTypes from "prop-types";
-import Icon from "../Icon";
-
-/**
- * A reusable filter header component that displays a dropdown menu with selectable options.
- *
- * @component
- * @param {Object} props - The component props.
- * @param {string} props.header - The header text to display when no options are selected.
- * @param {Array} props.options - An array of options to display in the dropdown menu. Each option should have a `value` and `label`.
- * @param {Array} [props.value] - The currently selected values.
- * @param {Function} props.onChange - The callback function to handle changes in the selected values.
- * @param {boolean} [props.multiple=true] - Whether multiple options can be selected.
- * @returns {JSX.Element} The rendered FilterHeader component.
- */
-
-const FilterHeader = ({ header, options, value, onChange, multiple = true }) => {
- const theme = useTheme();
-
- const controlledValue = value === undefined ? [] : value; // Ensure value is always treated as an array for controlled component purposes
-
- return (
-
-
-
- );
-};
-
-FilterHeader.propTypes = {
- header: PropTypes.string.isRequired,
- options: PropTypes.arrayOf(
- PropTypes.shape({
- value: PropTypes.string.isRequired,
- label: PropTypes.string.isRequired,
- })
- ).isRequired,
- value: PropTypes.arrayOf(PropTypes.string),
- onChange: PropTypes.func.isRequired,
- multiple: PropTypes.bool,
-};
-
-export default FilterHeader;
diff --git a/client/src/Components/v1/MonitorActions/index.jsx b/client/src/Components/v1/MonitorActions/index.jsx
deleted file mode 100644
index f63fc23fe..000000000
--- a/client/src/Components/v1/MonitorActions/index.jsx
+++ /dev/null
@@ -1,119 +0,0 @@
-import * as React from "react";
-import Button from "@mui/material/Button";
-import ButtonGroup from "@mui/material/ButtonGroup";
-import Icon from "../Icon";
-import ClickAwayListener from "@mui/material/ClickAwayListener";
-import Grow from "@mui/material/Grow";
-import Paper from "@mui/material/Paper";
-import Popper from "@mui/material/Popper";
-import MenuItem from "@mui/material/MenuItem";
-import MenuList from "@mui/material/MenuList";
-import { useNavigate } from "react-router-dom";
-import { useTranslation } from "react-i18next";
-import { createToast } from "../../../Utils/toastUtils.jsx";
-import { useExportMonitors } from "../../../Hooks/monitorHooks.js";
-
-const MonitorActions = ({ isLoading }) => {
- const [open, setOpen] = React.useState(false);
- const anchorRef = React.useRef(null);
- const [selectedIndex, setSelectedIndex] = React.useState(0);
- const navigate = useNavigate();
- const { t } = useTranslation();
- const [exportMonitors, isExporting] = useExportMonitors();
-
- const options = [t("monitorActions.import"), t("monitorActions.export")];
-
- const handleClick = async () => {
- if (selectedIndex === 0) {
- // Import
- navigate("/uptime/bulk-import");
- } else {
- // Export
- const [success, error] = await exportMonitors();
- if (!success) {
- createToast({ body: error || t("export.failed") });
- }
- }
- };
-
- const handleMenuItemClick = (event, index) => {
- setSelectedIndex(index);
- setOpen(false);
- };
-
- const handleToggle = () => {
- setOpen((prevOpen) => !prevOpen);
- };
-
- const handleClose = (event) => {
- if (anchorRef.current && anchorRef.current.contains(event.target)) {
- return;
- }
- setOpen(false);
- };
-
- return (
-
-
-
-
-
-
- {({ TransitionProps, placement }) => (
-
-
-
-
-
-
-
- )}
-
-
- );
-};
-
-export default MonitorActions;
diff --git a/client/src/Components/v1/MonitorCountHeader/index.jsx b/client/src/Components/v1/MonitorCountHeader/index.jsx
deleted file mode 100644
index eba644d26..000000000
--- a/client/src/Components/v1/MonitorCountHeader/index.jsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import { Stack, Typography } from "@mui/material";
-import PropTypes from "prop-types";
-import { useTheme } from "@emotion/react";
-import SkeletonLayout from "./skeleton.jsx";
-
-const MonitorCountHeader = ({
- isLoading = false,
- monitorCount,
- heading = "monitors",
- sx,
- children,
-}) => {
- const theme = useTheme();
- if (isLoading) return ;
-
- if (monitorCount === 1) {
- heading = "monitor";
- }
-
- return (
-
- {monitorCount} {heading}
- {children}
-
- );
-};
-
-MonitorCountHeader.propTypes = {
- isLoading: PropTypes.bool,
- monitorCount: PropTypes.number,
- heading: PropTypes.string,
- children: PropTypes.node,
- sx: PropTypes.object,
-};
-
-export default MonitorCountHeader;
diff --git a/client/src/Components/v1/MonitorCountHeader/skeleton.jsx b/client/src/Components/v1/MonitorCountHeader/skeleton.jsx
deleted file mode 100644
index 9b4971c32..000000000
--- a/client/src/Components/v1/MonitorCountHeader/skeleton.jsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import { Stack, Skeleton } from "@mui/material";
-import { useTheme } from "@emotion/react";
-
-const SkeletonLayout = () => {
- const theme = useTheme();
- return (
-
-
-
-
- );
-};
-
-export default SkeletonLayout;
diff --git a/client/src/Components/v1/MonitorCreateHeader/index.jsx b/client/src/Components/v1/MonitorCreateHeader/index.jsx
index 90c12f5ec..c3a979eb9 100644
--- a/client/src/Components/v1/MonitorCreateHeader/index.jsx
+++ b/client/src/Components/v1/MonitorCreateHeader/index.jsx
@@ -3,7 +3,6 @@ import { useNavigate } from "react-router-dom";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next";
import { useTheme } from "@emotion/react";
-import MonitorActions from "../MonitorActions/index.jsx";
const CreateMonitorHeader = ({ isAdmin, label, isLoading = true, path, bulkPath }) => {
const navigate = useNavigate();
diff --git a/client/src/Components/v1/MonitorDetailsControlHeader/index.jsx b/client/src/Components/v1/MonitorDetailsControlHeader/index.jsx
deleted file mode 100644
index 0793e7149..000000000
--- a/client/src/Components/v1/MonitorDetailsControlHeader/index.jsx
+++ /dev/null
@@ -1,158 +0,0 @@
-import Stack from "@mui/material/Stack";
-import Status from "./status.jsx";
-import Skeleton from "./skeleton.jsx";
-import Button from "@mui/material/Button";
-import { Tooltip } from "@mui/material";
-import Icon from "../Icon";
-
-// Utils
-import PropTypes from "prop-types";
-import { useNavigate } from "react-router-dom";
-import { useTheme } from "@mui/material/styles";
-import { usePauseMonitor } from "../../../Hooks/monitorHooks.js";
-import { useSendTestEmail } from "../../../Hooks/useSendTestEmail.js";
-import { useTranslation } from "react-i18next";
-import { useTestAllNotifications } from "../../../Hooks/useNotifications.js";
-/**
- * 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();
-
- const isTestNotificationsDisabled = monitor?.notifications?.length === 0;
-
- const tooltipTitle = isTestNotificationsDisabled ? t("testNotificationsDisabled") : "";
-
- // const [isSending, emailError, sendTestEmail] = useSendTestEmail();
-
- const [testAllNotifications, isSending, errorAllNotifications] =
- useTestAllNotifications();
-
- if (isLoading) {
- return ;
- }
-
- return (
-
-
-
-
-
-
-
- }
- disabled={isTestNotificationsDisabled}
- onClick={() => {
- testAllNotifications({ monitorId: monitor?.id });
- }}
- sx={{
- whiteSpace: "nowrap",
- }}
- >
- {t("sendTestNotifications")}
-
-
-
-
- {isAdmin && (
-
- ) : (
-
- )
- }
- onClick={() => {
- pauseMonitor({
- monitorId: monitor?.id,
- triggerUpdate,
- });
- }}
- >
- {monitor?.isActive ? t("pause") : t("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/v1/MonitorDetailsControlHeader/skeleton.jsx b/client/src/Components/v1/MonitorDetailsControlHeader/skeleton.jsx
deleted file mode 100644
index 64dc0547f..000000000
--- a/client/src/Components/v1/MonitorDetailsControlHeader/skeleton.jsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { Stack, Skeleton } from "@mui/material";
-
-const SkeletonLayout = () => {
- return (
-
-
-
-
- );
-};
-
-export default SkeletonLayout;
diff --git a/client/src/Components/v1/MonitorDetailsControlHeader/status.jsx b/client/src/Components/v1/MonitorDetailsControlHeader/status.jsx
deleted file mode 100644
index febf449d4..000000000
--- a/client/src/Components/v1/MonitorDetailsControlHeader/status.jsx
+++ /dev/null
@@ -1,51 +0,0 @@
-// Components
-import Stack from "@mui/material/Stack";
-import PulseDot from "../Animated/PulseDot.jsx";
-import Typography from "@mui/material/Typography";
-import Dot from "../Dot/index.jsx";
-// Utils
-import { formatDurationRounded } from "../../../Utils/timeUtils.js";
-import PropTypes from "prop-types";
-import { useTheme } from "@emotion/react";
-import { useMonitorUtils } from "../../../Hooks/useMonitorUtils.js";
-import { formatMonitorUrl } from "../../../Utils/utils.js";
-/**
- * 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 } = useMonitorUtils();
-
- return (
-
- {monitor?.name}
-
-
- {formatMonitorUrl(monitor?.url)}
-
-
- Checking every {formatDurationRounded(monitor?.interval)}.
-
-
-
- );
-};
-
-Status.propTypes = {
- monitor: PropTypes.object,
-};
-
-export default Status;
diff --git a/client/src/Components/v1/NotificationConfig/index.jsx b/client/src/Components/v1/NotificationConfig/index.jsx
deleted file mode 100644
index f1ed30ad1..000000000
--- a/client/src/Components/v1/NotificationConfig/index.jsx
+++ /dev/null
@@ -1,127 +0,0 @@
-// Components
-import Stack from "@mui/material/Stack";
-import Typography from "@mui/material/Typography";
-import Divider from "@mui/material/Divider";
-import Icon from "../Icon";
-import Search from "../Inputs/Search/index.jsx";
-
-// Utils
-import { useState, useEffect } from "react";
-import { useTheme } from "@mui/material/styles";
-import PropTypes from "prop-types";
-
-const NotificationConfig = ({
- notifications,
- setMonitor,
- setNotifications,
- //FieldWrapper's props
- gap,
- labelMb,
- labelFontWeight,
- labelVariant,
- labelSx = {},
- sx = {},
-}) => {
- // Local state
- const [notificationsSearch, setNotificationsSearch] = useState("");
- const [selectedNotifications, setSelectedNotifications] = useState([]);
-
- const handleSearch = (value) => {
- setSelectedNotifications(value);
- setMonitor((prev) => {
- return {
- ...prev,
- notifications: value.map((notification) => notification.id),
- };
- });
- };
-
- // Handlers
- const handleDelete = (id) => {
- const updatedNotifications = selectedNotifications.filter(
- (notification) => notification.id !== id
- );
-
- setSelectedNotifications(updatedNotifications);
- setMonitor((prev) => {
- return {
- ...prev,
- notifications: updatedNotifications.map((notification) => notification.id),
- };
- });
- };
-
- // Setup
- const theme = useTheme();
-
- useEffect(() => {
- if (setNotifications) {
- const toSet = setNotifications.map((notification) => {
- return notifications.find((n) => n.id === notification);
- });
- setSelectedNotifications(toSet);
- }
- }, [setNotifications, notifications]);
-
- return (
-
- {
- handleSearch(value);
- }}
- labelMb={labelMb}
- labelVariant={labelVariant}
- labelFontWeight={labelFontWeight}
- labelSx={labelSx}
- gap={gap}
- sx={{
- ...sx,
- }}
- />
-
- {selectedNotifications.map((notification, index) => (
-
-
- {notification.notificationName}
-
- {
- handleDelete(notification.id);
- }}
- style={{ cursor: "pointer" }}
- />
- {index < selectedNotifications.length - 1 && }
-
- ))}
-
-
- );
-};
-
-NotificationConfig.propTypes = {
- notifications: PropTypes.array,
- setMonitor: PropTypes.func,
- setNotifications: PropTypes.array,
-};
-
-export default NotificationConfig;
diff --git a/client/src/Components/v1/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx b/client/src/Components/v1/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx
deleted file mode 100644
index 2aae996f6..000000000
--- a/client/src/Components/v1/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx
+++ /dev/null
@@ -1,419 +0,0 @@
-import { useState, useMemo, useEffect, useCallback } from "react";
-import { useTranslation } from "react-i18next";
-import PropTypes from "prop-types";
-
-import {
- Dialog,
- DialogContent,
- DialogActions,
- Button,
- Typography,
- Box,
- Tabs,
- Tab,
- Stack,
-} from "@mui/material";
-import { useTheme } from "@emotion/react";
-import TabPanel from "./TabPanel.jsx";
-import TabComponent from "./TabComponent.jsx";
-import useNotifications from "../Hooks/useNotification.js";
-
-// Define constants for notification types to avoid magic values
-const NOTIFICATION_TYPES = {
- SLACK: "slack",
- DISCORD: "discord",
- TELEGRAM: "telegram",
- WEBHOOK: "webhook",
-};
-
-// Define constants for field IDs
-const FIELD_IDS = {
- WEBHOOK: "webhook",
- TOKEN: "token",
- CHAT_ID: "chatId",
- URL: "url",
-};
-
-const NotificationIntegrationModal = ({
- open,
- onClose,
- monitor,
- setMonitor,
- // Optional prop to configure available notification types
- notificationTypes = null,
-}) => {
- const { t } = useTranslation();
- const theme = useTheme();
- const [tabValue, setTabValue] = useState(0);
-
- const [loading, _, sendTestNotification] = useNotifications();
-
- // Helper to get the field state key with error handling
- const getFieldKey = useCallback(
- (typeId, fieldId) => {
- if (typeof typeId !== "string" || typeId === "") {
- throw new Error(t("errorInvalidTypeId"));
- }
-
- if (typeof fieldId !== "string" || fieldId === "") {
- throw new Error(t("errorInvalidFieldId"));
- }
-
- return `${typeId}${fieldId.charAt(0).toUpperCase() + fieldId.slice(1)}`;
- },
- [t]
- );
-
- // Define notification types
- const DEFAULT_NOTIFICATION_TYPES = [
- {
- id: NOTIFICATION_TYPES.SLACK,
- label: t("notifications.slack.label"),
- description: t("notifications.slack.description"),
- fields: [
- {
- id: FIELD_IDS.WEBHOOK,
- label: t("notifications.slack.webhookLabel"),
- placeholder: t("notifications.slack.webhookPlaceholder"),
- type: "text",
- },
- ],
- },
- {
- id: NOTIFICATION_TYPES.DISCORD,
- label: t("notifications.discord.label"),
- description: t("notifications.discord.description"),
- fields: [
- {
- id: FIELD_IDS.WEBHOOK,
- label: t("notifications.discord.webhookLabel"),
- placeholder: t("notifications.discord.webhookPlaceholder"),
- type: "text",
- },
- ],
- },
- {
- id: NOTIFICATION_TYPES.TELEGRAM,
- label: t("notifications.telegram.label"),
- description: t("notifications.telegram.description"),
- fields: [
- {
- id: FIELD_IDS.TOKEN,
- label: t("notifications.telegram.tokenLabel"),
- placeholder: t("notifications.telegram.tokenPlaceholder"),
- type: "text",
- },
- {
- id: FIELD_IDS.CHAT_ID,
- label: t("notifications.telegram.chatIdLabel"),
- placeholder: t("notifications.telegram.chatIdPlaceholder"),
- type: "text",
- },
- ],
- },
- {
- id: NOTIFICATION_TYPES.WEBHOOK,
- label: t("notifications.webhook.label"),
- description: t("notifications.webhook.description"),
- fields: [
- {
- id: FIELD_IDS.URL,
- label: t("notifications.webhook.urlLabel"),
- placeholder: t("notifications.webhook.urlPlaceholder"),
- type: "text",
- },
- ],
- },
- ];
-
- // Use provided notification types or default to our translated ones
- const activeNotificationTypes = notificationTypes || DEFAULT_NOTIFICATION_TYPES;
-
- // Memoized function to initialize integrations state
- const initialIntegrationsState = useMemo(() => {
- const state = {};
-
- activeNotificationTypes.forEach((type) => {
- // Add enabled flag for each notification type
- state[type.id] = false;
-
- // Add state for each field in the notification type
- type.fields.forEach((field) => {
- const fieldKey = getFieldKey(type.id, field.id);
- state[fieldKey] = "";
- });
- });
-
- return state;
- }, [activeNotificationTypes, getFieldKey]); // Only recompute when these dependencies change
-
- const [integrations, setIntegrations] = useState(initialIntegrationsState);
-
- useEffect(() => {
- if (open) {
- const extractNotificationValues = () => {
- const values = {};
-
- if (!monitor?.notifications || !Array.isArray(monitor.notifications)) {
- return values;
- }
-
- monitor.notifications.forEach((notification) => {
- // Handle notification based on its structure
- if (notification.type === "webhook" && notification.platform) {
- if (typeof notification.config === "undefined") return;
- const platform = notification.platform;
- values[platform] = true; // Set platform as enabled
-
- // Extract configuration based on platform
- switch (platform) {
- case NOTIFICATION_TYPES.SLACK:
- case NOTIFICATION_TYPES.DISCORD:
- if (notification.config.webhookUrl) {
- values[getFieldKey(platform, FIELD_IDS.WEBHOOK)] =
- notification.config.webhookUrl;
- }
- break;
- case NOTIFICATION_TYPES.TELEGRAM:
- if (notification.config.botToken) {
- values[getFieldKey(platform, FIELD_IDS.TOKEN)] =
- notification.config.botToken;
- }
- if (notification.config.chatId) {
- values[getFieldKey(platform, FIELD_IDS.CHAT_ID)] =
- notification.config.chatId;
- }
- break;
- case NOTIFICATION_TYPES.WEBHOOK:
- if (notification.config.webhookUrl) {
- values[getFieldKey(platform, FIELD_IDS.URL)] =
- notification.config.webhookUrl;
- }
- break;
- }
- }
- });
-
- return values;
- };
-
- const extractedValues = extractNotificationValues();
- setIntegrations((prev) => ({
- ...prev,
- ...extractedValues,
- }));
- }
- }, [open, monitor, getFieldKey]);
-
- const handleChangeTab = (event, newValue) => {
- setTabValue(newValue);
- };
-
- const handleIntegrationChange = (type, checked) => {
- setIntegrations((prev) => ({
- ...prev,
- [type]: checked,
- }));
- };
-
- const handleInputChange = (type, value) => {
- setIntegrations((prev) => ({
- ...prev,
- [type]: value,
- }));
- };
-
- const handleTestNotification = async (type) => {
- // Get the notification type details
- const notificationType = activeNotificationTypes.find((t) => t.id === type);
-
- if (typeof notificationType === "undefined") {
- return;
- }
-
- // Prepare config object based on notification type
- const config = {};
-
- // Add each field value to the config object
- notificationType.fields.forEach((field) => {
- const fieldKey = getFieldKey(type, field.id);
- config[field.id] = integrations[fieldKey];
- });
-
- await sendTestNotification(type, config);
- };
-
- // In NotificationIntegrationModal.jsx, update the handleSave function:
-
- const handleSave = () => {
- // Get existing notifications
- const notifications = [...(monitor?.notifications || [])];
-
- // Get all notification types IDs
- const existingTypes = activeNotificationTypes.map((type) => type.id);
-
- // Filter out notifications that are configurable in this modal
- const filteredNotifications = notifications.filter((notification) => {
- if (notification.platform) {
- return !existingTypes.includes(notification.platform);
- }
-
- return !existingTypes.includes(notification.type);
- });
-
- // Add each enabled notification with its configured fields
- activeNotificationTypes.forEach((type) => {
- if (integrations[type.id]) {
- let notificationObject = {
- type: "webhook",
- platform: type.id, // Set platform to identify the specific service
- config: {},
- };
-
- // Configure based on notification type
- switch (type.id) {
- case "slack":
- case "discord":
- notificationObject.config.webhookUrl =
- integrations[getFieldKey(type.id, "webhook")];
- break;
- case "telegram":
- notificationObject.config.botToken =
- integrations[getFieldKey(type.id, "token")];
- notificationObject.config.chatId =
- integrations[getFieldKey(type.id, "chatId")];
- break;
- case "webhook":
- notificationObject.config.webhookUrl =
- integrations[getFieldKey(type.id, "url")];
- break;
- }
-
- filteredNotifications.push(notificationObject);
- }
- });
-
- // Update monitor with new notifications
- setMonitor((prev) => ({
- ...prev,
- notifications: filteredNotifications,
- }));
-
- onClose();
- };
-
- return (
-
- );
-};
-
-NotificationIntegrationModal.propTypes = {
- open: PropTypes.bool.isRequired,
- onClose: PropTypes.func.isRequired,
- monitor: PropTypes.object.isRequired,
- setMonitor: PropTypes.func.isRequired,
- notificationTypes: PropTypes.array,
-};
-
-export default NotificationIntegrationModal;
diff --git a/client/src/Components/v1/NotificationIntegrationModal/Components/TabComponent.jsx b/client/src/Components/v1/NotificationIntegrationModal/Components/TabComponent.jsx
deleted file mode 100644
index 87fb2c301..000000000
--- a/client/src/Components/v1/NotificationIntegrationModal/Components/TabComponent.jsx
+++ /dev/null
@@ -1,115 +0,0 @@
-import React from "react";
-import { Typography, Box, Button, CircularProgress } from "@mui/material";
-import { useTranslation } from "react-i18next";
-import { useTheme } from "@emotion/react";
-import TextInput from "../../Inputs/TextInput/index.jsx";
-import Checkbox from "../../Inputs/Checkbox/index.jsx";
-
-const TabComponent = ({
- type,
- integrations,
- handleIntegrationChange,
- handleInputChange,
- handleTestNotification,
- isLoading,
-}) => {
- const theme = useTheme();
- const { t } = useTranslation();
-
- // Helper to get the field state key (e.g., slackWebhook, telegramToken)
- const getFieldKey = (typeId, fieldId) => {
- return `${typeId}${fieldId.charAt(0).toUpperCase() + fieldId.slice(1)}`;
- };
-
- // Check if all fields have values to enable test button
- const areAllFieldsFilled = () => {
- return type.fields.every((field) => {
- const fieldKey = getFieldKey(type.id, field.id);
- return integrations[fieldKey];
- });
- };
-
- return (
- <>
-
- {type.label}
-
-
-
- {type.description}
-
-
-
- handleIntegrationChange(type.id, e.target.checked)}
- disabled={isLoading}
- />
-
-
- {type.fields.map((field) => {
- const fieldKey = getFieldKey(type.id, field.id);
-
- return (
-
-
- {field.label}
-
-
- handleInputChange(fieldKey, e.target.value)}
- disabled={!integrations[type.id] || isLoading}
- />
-
- );
- })}
-
-
-
-
- >
- );
-};
-
-export default TabComponent;
diff --git a/client/src/Components/v1/NotificationIntegrationModal/Components/TabPanel.jsx b/client/src/Components/v1/NotificationIntegrationModal/Components/TabPanel.jsx
deleted file mode 100644
index 93546e7bc..000000000
--- a/client/src/Components/v1/NotificationIntegrationModal/Components/TabPanel.jsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import React from "react";
-import PropTypes from "prop-types";
-import { Box } from "@mui/material";
-import { useTheme } from "@emotion/react";
-
-/**
- * TabPanel component that displays content for the selected tab.
- *
- * @component
- * @param {Object} props - The component props.
- * @param {React.ReactNode} props.children - The content to be displayed when this tab panel is selected.
- * @param {number} props.value - The currently selected tab value.
- * @param {number} props.index - The index of this specific tab panel.
- * @param {Object} props.other - Any additional props to be spread to the root element.
- * @returns {React.ReactElement|null} The rendered tab panel or null if not selected.
- */
-function TabPanel({ children, value, index, ...other }) {
- const theme = useTheme();
-
- return (
-
- {value === index && {children}}
-
- );
-}
-
-TabPanel.propTypes = {
- children: PropTypes.node,
-
- index: PropTypes.number.isRequired,
-
- value: PropTypes.number.isRequired,
-};
-
-export default TabPanel;
diff --git a/client/src/Components/v1/NotificationIntegrationModal/Hooks/useNotification.js b/client/src/Components/v1/NotificationIntegrationModal/Hooks/useNotification.js
deleted file mode 100644
index c0712fa5f..000000000
--- a/client/src/Components/v1/NotificationIntegrationModal/Hooks/useNotification.js
+++ /dev/null
@@ -1,129 +0,0 @@
-import { useState } from "react";
-import { useTranslation } from "react-i18next";
-import { networkService } from "../../../../Utils/NetworkService.js";
-import { createToast } from "../../../../Utils/toastUtils.jsx";
-
-// Define constants for notification types to avoid magic values
-const NOTIFICATION_TYPES = {
- SLACK: "slack",
- DISCORD: "discord",
- TELEGRAM: "telegram",
- WEBHOOK: "webhook",
-};
-
-// Define constants for field IDs
-const FIELD_IDS = {
- WEBHOOK: "webhook",
- TOKEN: "token",
- CHAT_ID: "chatId",
- URL: "url",
-};
-
-/**
- * Custom hook for notification-related operations
- */
-const useNotifications = () => {
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState(undefined);
- const { t } = useTranslation();
-
- /**
- * Send a test notification
- * @param {string} type - The notification type (slack, discord, telegram, webhook)
- * @param {object} config - Configuration object with necessary params
- */
- const sendTestNotification = async (type, config) => {
- setLoading(true);
- setError(undefined);
-
- // Validation based on notification type
- let payload = { platform: type };
- let isValid = true;
- let errorMessage = "";
-
- switch (type) {
- case NOTIFICATION_TYPES.SLACK:
- payload.webhookUrl = config.webhook;
- if (typeof payload.webhookUrl === "undefined" || payload.webhookUrl === "") {
- isValid = false;
- errorMessage = t("notifications.slack.webhookRequired");
- }
- break;
-
- case NOTIFICATION_TYPES.DISCORD:
- payload.webhookUrl = config.webhook;
- if (typeof payload.webhookUrl === "undefined" || payload.webhookUrl === "") {
- isValid = false;
- errorMessage = t("notifications.discord.webhookRequired");
- }
- break;
-
- case NOTIFICATION_TYPES.TELEGRAM:
- payload.botToken = config.token;
- payload.chatId = config.chatId;
- if (
- typeof payload.botToken === "undefined" ||
- payload.botToken === "" ||
- typeof payload.chatId === "undefined" ||
- payload.chatId === ""
- ) {
- isValid = false;
- errorMessage = t("notifications.telegram.fieldsRequired");
- }
- break;
-
- case NOTIFICATION_TYPES.WEBHOOK:
- payload.webhookUrl = config.url;
- payload.platform = NOTIFICATION_TYPES.SLACK;
- if (typeof payload.webhookUrl === "undefined" || payload.webhookUrl === "") {
- isValid = false;
- errorMessage = t("notifications.webhook.urlRequired");
- }
- break;
-
- default:
- isValid = false;
- errorMessage = t("notifications.unsupportedType");
- }
-
- // If validation fails, show error and return
- if (isValid === false) {
- createToast({
- body: errorMessage,
- variant: "error",
- });
- setLoading(false);
- return;
- }
-
- try {
- const response = await networkService.testNotification({
- platform: type,
- payload: payload,
- });
-
- if (response.data.success === true) {
- createToast({
- body: t("notifications.testSuccess"),
- variant: "info",
- });
- } else {
- throw new Error(response.data.msg || t("notifications.testFailed"));
- }
- } catch (error) {
- const errorMsg =
- error.response?.data?.msg || error.message || t("notifications.networkError");
- createToast({
- body: `${t("notifications.testFailed")}: ${errorMsg}`,
- variant: "error",
- });
- setError(errorMsg);
- } finally {
- setLoading(false);
- }
- };
-
- return [loading, error, sendTestNotification];
-};
-
-export default useNotifications;
diff --git a/client/src/Components/v2/common/charts/HeatmapResponseTime.tsx b/client/src/Components/v2/common/charts/HeatmapResponseTime.tsx
index 97289ba43..972291f57 100644
--- a/client/src/Components/v2/common/charts/HeatmapResponseTime.tsx
+++ b/client/src/Components/v2/common/charts/HeatmapResponseTime.tsx
@@ -1,6 +1,6 @@
import Box from "@mui/material/Box";
import { useTheme } from "@mui/material/styles";
-import type { Check } from "@/Types/Check";
+import type { CheckSnapshot } from "@/Types/Check";
import { getResponseColor } from "@/Utils/DataUtils";
import { HeatmapResponseTimeTooltip } from "@/Components/v2/common/charts/HeatmapResponseTimeTooltip";
import type { SxProps } from "@mui/material/styles";
@@ -13,7 +13,7 @@ interface PlaceholderCheck {
}
interface HeatmapResponseTimeProps {
- checks: Check[];
+ checks: CheckSnapshot[];
gap?: ResponsiveStyleValue;
availabilityCellSx?: SxProps;
responseCellSx?: SxProps;
@@ -34,7 +34,7 @@ export const HeatmapResponseTime = ({
const latestChecks = checks.slice(-25).reverse();
- let data: Array;
+ let data: Array;
if (latestChecks.length !== 25) {
const placeholders = Array(25 - latestChecks.length).fill({
status: "placeholder" as const,
diff --git a/client/src/Components/v2/common/charts/HeatmapResponseTimeTooltip.tsx b/client/src/Components/v2/common/charts/HeatmapResponseTimeTooltip.tsx
index 448a60e49..ec9ad4d26 100644
--- a/client/src/Components/v2/common/charts/HeatmapResponseTimeTooltip.tsx
+++ b/client/src/Components/v2/common/charts/HeatmapResponseTimeTooltip.tsx
@@ -2,12 +2,14 @@ import Stack from "@mui/material/Stack";
import Tooltip from "@mui/material/Tooltip";
import Typography from "@mui/material/Typography";
import { formatDateWithTz } from "@/Utils/TimeUtils";
-import type { Check } from "@/Types/Check";
+import type { CheckSnapshot } from "@/Types/Check";
import { useTheme } from "@mui/material/styles";
import { useSelector } from "react-redux";
import type { RootState } from "@/Types/state";
-
-type HeatmapCheck = Check | { status: "placeholder"; responseTime: 0; createdAt: "" };
+import { useTranslation } from "react-i18next";
+type HeatmapCheck =
+ | CheckSnapshot
+ | { status: "placeholder"; responseTime: 0; createdAt: "" };
export const HeatmapResponseTimeTooltip = ({
children,
@@ -16,6 +18,7 @@ export const HeatmapResponseTimeTooltip = ({
children: React.ReactElement;
check: HeatmapCheck;
}) => {
+ const { t } = useTranslation();
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
const theme = useTheme();
@@ -52,15 +55,19 @@ export const HeatmapResponseTimeTooltip = ({
{formatDateWithTz(check?.createdAt, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone)}
- {check?.originalResponseTime && (
+ {check?.responseTime && (
- Response Time: {check.originalResponseTime.toFixed()} ms
+ {t("common.labels.responseTime")}: {check.responseTime.toFixed()} ms
)}
Status:{" "}
- {check?.status}
+
+ {check?.status === true
+ ? t("pages.common.monitors.status.up")
+ : t("pages.common.monitors.status.down")}
+
}
diff --git a/client/src/Components/v2/common/charts/HistogramResponseTime.tsx b/client/src/Components/v2/common/charts/HistogramResponseTime.tsx
index bbe3bd9d4..e3b82cd50 100644
--- a/client/src/Components/v2/common/charts/HistogramResponseTime.tsx
+++ b/client/src/Components/v2/common/charts/HistogramResponseTime.tsx
@@ -1,11 +1,11 @@
import Box from "@mui/material/Box";
import { useTheme } from "@mui/material/styles";
import type { ResponsiveStyleValue } from "@mui/system";
-import type { Check } from "@/Types/Check";
+import type { CheckSnapshot } from "@/Types/Check";
import { HeatmapResponseTimeTooltip } from "./HeatmapResponseTimeTooltip";
interface HistogramResponseTimeProps {
- checks: Check[];
+ checks: CheckSnapshot[];
height?: ResponsiveStyleValue;
gap?: ResponsiveStyleValue;
}
diff --git a/client/src/Components/v2/common/controls/HeaderTimeRange.tsx b/client/src/Components/v2/common/controls/HeaderTimeRange.tsx
new file mode 100644
index 000000000..c66ee9ff6
--- /dev/null
+++ b/client/src/Components/v2/common/controls/HeaderTimeRange.tsx
@@ -0,0 +1,82 @@
+import Stack from "@mui/material/Stack";
+import Box from "@mui/material/Box";
+import Typography from "@mui/material/Typography";
+import { ToggleButtonGroup, ToggleButton } from "@/Components/v2/inputs";
+import { useTheme } from "@mui/material/styles";
+import CircularProgress from "@mui/material/CircularProgress";
+import { useTranslation } from "react-i18next";
+import { useMediaQuery } from "@mui/material";
+
+interface MonitorTimeFrameHeaderProps {
+ isLoading?: boolean;
+ hasDateRange?: boolean;
+ dateRange: string;
+ setDateRange: (dateRange: string) => void;
+}
+
+export const HeaderTimeRange = ({
+ isLoading = false,
+ hasDateRange = true,
+ dateRange,
+ setDateRange,
+}: MonitorTimeFrameHeaderProps) => {
+ const theme = useTheme();
+ const { t } = useTranslation();
+ const isSmallScreen = useMediaQuery(theme.breakpoints.down("md"));
+ const handleChange = (
+ _event: React.MouseEvent,
+ newValue: string | null
+ ) => {
+ if (newValue !== null) {
+ setDateRange(newValue);
+ }
+ };
+
+ let timeFramePicker = null;
+
+ if (hasDateRange) {
+ timeFramePicker = (
+
+
+ {t("components.headerTimeRange.labels.recent")}
+
+
+ {t("components.headerTimeRange.labels.day")}
+
+
+ {t("components.headerTimeRange.labels.week")}
+
+
+ {t("components.headerTimeRange.labels.month")}
+
+
+ );
+ }
+
+ return (
+
+
+ {isLoading && }
+
+
+ {t(`components.headerTimeRange.description.${dateRange}`)}
+
+ {timeFramePicker}
+
+ );
+};
diff --git a/client/src/Components/v2/common/index.ts b/client/src/Components/v2/common/index.ts
index e354983eb..2ea3bf3f7 100644
--- a/client/src/Components/v2/common/index.ts
+++ b/client/src/Components/v2/common/index.ts
@@ -2,3 +2,4 @@ export * from "../common/charts/HistogramResponseTime";
export * from "../common/charts/HeatmapResponseTime";
export * from "../common/charts/HeatmapResponseTimeTooltip";
export * from "../common/controls/HeaderCreate";
+export * from "../common/controls/HeaderTimeRange";
diff --git a/client/src/Components/v2/design-elements/BaseChart.tsx b/client/src/Components/v2/design-elements/BaseChart.tsx
new file mode 100644
index 000000000..fd9251915
--- /dev/null
+++ b/client/src/Components/v2/design-elements/BaseChart.tsx
@@ -0,0 +1,72 @@
+import { BaseBox } from "../design-elements";
+import Stack from "@mui/material/Stack";
+import Box from "@mui/material/Box";
+import Typography from "@mui/material/Typography";
+import type { ResponsiveStyleValue } from "@mui/system";
+
+import { useTheme } from "@mui/material/styles";
+
+type BaseChartProps = React.PropsWithChildren<{
+ icon: React.ReactNode;
+ title: string;
+ width?: number | string;
+ maxWidth?: number | string;
+ padding?: number | string | ResponsiveStyleValue;
+}>;
+
+export const BaseChart = ({
+ children,
+ icon,
+ title,
+ width = "100%",
+ maxWidth = "100%",
+ padding,
+}: BaseChartProps) => {
+ const theme = useTheme();
+ return (
+
+
+
+ {icon && (
+
+ {icon}
+
+ )}
+ {title}
+
+ {children}
+
+
+ );
+};
diff --git a/client/src/Components/v2/design-elements/BasePage.tsx b/client/src/Components/v2/design-elements/BasePage.tsx
index c853a8d30..de20fc005 100644
--- a/client/src/Components/v2/design-elements/BasePage.tsx
+++ b/client/src/Components/v2/design-elements/BasePage.tsx
@@ -1,15 +1,51 @@
import Stack from "@mui/material/Stack";
+import Alert from "@mui/material/Alert";
+import Link from "@mui/material/Link";
import {
ErrorFallback,
EmptyFallback,
EmptyMonitorFallback,
+ BaseFallback,
} from "@/Components/v2/design-elements/Fallback";
import { Breadcrumb } from "@/Components/v2/design-elements/Breadcrumb";
import CircularProgress from "@mui/material/CircularProgress";
import type { StackProps } from "@mui/material/Stack";
import { useTheme } from "@mui/material/styles";
-import { useTranslation } from "react-i18next";
+import { Trans, useTranslation } from "react-i18next";
+import { Link as RouterLink } from "react-router-dom";
+import { Typography } from "@mui/material";
+
+export const PageSpeedKeyPriorityFallback = () => {
+ return (
+
+
+
+
+ ),
+ }}
+ />
+
+
+
+ );
+};
+
interface BasePageProps extends StackProps {
loading?: boolean;
error?: boolean;
@@ -55,7 +91,7 @@ export const BasePage = ({
spacing={theme.spacing(10)}
{...props}
>
-
+
{children}
);
@@ -114,6 +150,7 @@ interface MonitorBasePageWithStatesProps extends StackProps {
page: string;
actionLink?: string;
children: React.ReactNode;
+ priorityFallback?: React.ReactNode;
}
const isEmpty = (items: any[]) => {
@@ -129,12 +166,25 @@ export const MonitorBasePageWithStates = ({
page,
actionLink,
children,
+ priorityFallback,
...props
}: MonitorBasePageWithStatesProps) => {
const { t } = useTranslation();
const showLoading = loading && (!items || items.length === 0);
+ if (priorityFallback) {
+ return (
+
+ {priorityFallback}
+
+ );
+ }
+
if (!loading && isEmpty(items)) {
return (
{
+const isId = (segment: string): boolean => {
+ return segment.length === 24 || /^[a-f0-9-]{36}$/.test(segment);
+};
+
+const actionSegments = ["create", "configure"];
+
+export const Breadcrumb = () => {
const { t } = useTranslation();
const theme = useTheme();
const location = useLocation();
- // Don't show breadcrumb on dashboard
- if (location.pathname === "/" || location.pathname === "/uptime") {
- return null;
- }
+ const segments = location.pathname.split("/").filter((x) => x);
- const pathnames = breadcrumbOverride || location.pathname.split("/").filter((x) => x);
+ // Build simplified breadcrumb: "uptime" or "uptime / details" or "uptime / create"
+ const basePage = segments[0] || t("common.breadcrumbs.home");
- // Don't show if only one level (e.g., /uptime)
- if (pathnames.length === 0) {
- return null;
- }
+ const secondSegment = segments[1];
+ const isActionPage = secondSegment && actionSegments.includes(secondSegment); // create/config
+ const isDetailsPage = secondSegment && isId(secondSegment); // details
+ const hasSubPage = isActionPage || isDetailsPage;
+
+ const getSubPageLabel = (): string => {
+ if (isActionPage) {
+ return secondSegment.charAt(0).toUpperCase() + secondSegment.slice(1);
+ }
+ return t("common.breadcrumbs.details");
+ };
return (
-
-
+ {hasSubPage ? (
+
+
+ {basePage.charAt(0).toUpperCase() + basePage.slice(1)}
+
+
+ ) : (
- Home
+ {basePage.charAt(0).toUpperCase() + basePage.slice(1)}
-
- {pathnames.map((value, index) => {
- const last = index === pathnames.length - 1;
- const to = `/${pathnames.slice(0, index + 1).join("/")}`;
-
- let displayName = "";
-
- // Paths that are IDs should be detail pages
- if (value.length === 24 || value.match(/^[a-f0-9-]{36}$/)) {
- displayName = "Details";
- } else {
- displayName =
- t(`common.breadcrumbs.${value}`) ||
- value.charAt(0).toUpperCase() + value.slice(1);
- }
-
- return last ? (
-
- {displayName}
-
- ) : (
-
-
- {displayName}
-
-
- );
- })}
+ )}
+ {hasSubPage && (
+
+ {getSubPageLabel()}
+
+ )}
);
};
diff --git a/client/src/Components/v2/design-elements/Dot.tsx b/client/src/Components/v2/design-elements/Dot.tsx
new file mode 100644
index 000000000..541e90812
--- /dev/null
+++ b/client/src/Components/v2/design-elements/Dot.tsx
@@ -0,0 +1,23 @@
+import type { CSSProperties } from "react";
+
+interface DotProps {
+ color?: string;
+ size?: string;
+ style?: CSSProperties;
+}
+
+export const Dot = ({ color = "gray", size = "4px", style }: DotProps) => {
+ return (
+
+ );
+};
diff --git a/client/src/Components/v2/design-elements/Gauge.tsx b/client/src/Components/v2/design-elements/Gauge.tsx
new file mode 100644
index 000000000..34c57487d
--- /dev/null
+++ b/client/src/Components/v2/design-elements/Gauge.tsx
@@ -0,0 +1,106 @@
+import Box from "@mui/material/Box";
+import Typography from "@mui/material/Typography";
+
+import { useTheme } from "@mui/material/styles";
+import { useMemo, useState, useEffect } from "react";
+import { getInfraGaugeColor } from "@/Utils/MonitorUtils";
+
+const MINIMUM_VALUE = 0;
+const MAXIMUM_VALUE = 100;
+
+export const Gauge = ({
+ isLoading = false,
+ progress = 0,
+ radius = 70,
+ strokeWidth = 15,
+ precision = 1,
+ unit = "%",
+}: {
+ isLoading?: boolean;
+ progress?: number;
+ radius?: number;
+ strokeWidth?: number;
+ precision?: number;
+ unit?: string;
+}) => {
+ const theme = useTheme();
+ const progressWithinRange = Math.max(MINIMUM_VALUE, Math.min(progress, MAXIMUM_VALUE));
+
+ // Calculate the length of the stroke for the circle
+ const { circumference, totalSize, strokeLength } = useMemo(
+ () => ({
+ circumference: 2 * Math.PI * radius,
+ totalSize: radius * 2 + strokeWidth * 2,
+ strokeLength: (progress / 100) * (2 * Math.PI * radius),
+ }),
+ [radius, strokeWidth, progress]
+ );
+
+ const [offset, setOffset] = useState(circumference);
+ useEffect(() => {
+ setOffset(circumference);
+ const timer = setTimeout(() => {
+ setOffset(circumference - strokeLength);
+ }, 100);
+
+ return () => clearTimeout(timer);
+ }, [progress, circumference, strokeLength]);
+
+ const fillColor = getInfraGaugeColor(progressWithinRange, theme);
+
+ if (isLoading) {
+ return;
+ }
+
+ return (
+
+
+
+
+ {`${progressWithinRange.toFixed(precision)}${unit}`}
+
+
+ );
+};
diff --git a/client/src/Components/v2/design-elements/Icon.tsx b/client/src/Components/v2/design-elements/Icon.tsx
new file mode 100644
index 000000000..b32cd84b2
--- /dev/null
+++ b/client/src/Components/v2/design-elements/Icon.tsx
@@ -0,0 +1,18 @@
+import type { LucideIcon } from "lucide-react";
+
+interface IconProps {
+ icon: LucideIcon;
+ size?: number;
+ strokeWidth?: number;
+}
+
+const Icon = ({ icon: Icon, size = 20, strokeWidth = 1.5 }: IconProps) => {
+ return (
+
+ );
+};
+
+export default Icon;
diff --git a/client/src/Components/v2/design-elements/MonitorStatus.tsx b/client/src/Components/v2/design-elements/MonitorStatus.tsx
new file mode 100644
index 000000000..3d5e81845
--- /dev/null
+++ b/client/src/Components/v2/design-elements/MonitorStatus.tsx
@@ -0,0 +1,58 @@
+import type { Monitor } from "@/Types/Monitor";
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import { PulseDot, Dot } from "@/Components/v2/design-elements";
+import { getStatusColor, formatUrl } from "@/Utils/MonitorUtils";
+import { useTheme } from "@mui/material/styles";
+import prettyMilliseconds from "pretty-ms";
+import { typographyLevels } from "@/Utils/Theme/v2Palette";
+import useMediaQuery from "@mui/material/useMediaQuery";
+export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => {
+ const theme = useTheme();
+ const isSmall = useMediaQuery(theme.breakpoints.down("md"));
+
+ if (!monitor) {
+ return null;
+ }
+ return (
+
+
+ {monitor.name}
+
+
+
+
+ {formatUrl(monitor?.url)}
+
+ {!isSmall && (
+ <>
+
+
+ Checking every {prettyMilliseconds(monitor?.interval, { verbose: true })}
+
+ >
+ )}
+
+
+ );
+};
diff --git a/client/src/Components/v2/design-elements/PulseDot.tsx b/client/src/Components/v2/design-elements/PulseDot.tsx
new file mode 100644
index 000000000..ab792b628
--- /dev/null
+++ b/client/src/Components/v2/design-elements/PulseDot.tsx
@@ -0,0 +1,48 @@
+import { Box, Stack, useTheme } from "@mui/material";
+
+interface PulseDotProps {
+ color: string;
+}
+
+export const PulseDot = ({ color }: PulseDotProps) => {
+ const theme = useTheme();
+
+ return (
+
+
+
+ );
+};
diff --git a/client/src/Components/v2/design-elements/SplitBox.tsx b/client/src/Components/v2/design-elements/SplitBox.tsx
new file mode 100644
index 000000000..3a778dbcf
--- /dev/null
+++ b/client/src/Components/v2/design-elements/SplitBox.tsx
@@ -0,0 +1,78 @@
+import Stack from "@mui/material/Stack";
+import Box from "@mui/material/Box";
+import Typography from "@mui/material/Typography";
+import { useTheme } from "@mui/material/styles";
+import useMediaQuery from "@mui/material/useMediaQuery";
+export const SplitBox = ({
+ left,
+ right,
+}: {
+ left: React.ReactNode;
+ right: React.ReactNode;
+}) => {
+ const theme = useTheme();
+ const isSmall = useMediaQuery(theme.breakpoints.down("md"));
+ return (
+
+
+ {left}
+
+
+ {right}
+
+
+ );
+};
+
+export const ConfigBox = ({
+ title,
+ subtitle,
+ leftContent,
+ rightContent,
+}: {
+ title: string;
+ subtitle: string;
+ leftContent?: React.ReactNode;
+ rightContent: React.ReactNode;
+}) => {
+ const theme = useTheme();
+ return (
+
+
+ {title}
+
+ {subtitle}
+ {leftContent}
+
+ }
+ right={rightContent}
+ />
+ );
+};
diff --git a/client/src/Components/v2/design-elements/StatBox.tsx b/client/src/Components/v2/design-elements/StatBox.tsx
new file mode 100644
index 000000000..b78a003dc
--- /dev/null
+++ b/client/src/Components/v2/design-elements/StatBox.tsx
@@ -0,0 +1,86 @@
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import Box from "@mui/material/Box";
+import { useTheme } from "@mui/material/styles";
+import { lighten } from "@mui/material/styles";
+import useMediaQuery from "@mui/material/useMediaQuery";
+import type { PaletteKey } from "@/Utils/Theme/v2Theme";
+import { BaseBox, TooltipWithInfo } from "@/Components/v2/design-elements";
+import type { SxProps } from "@mui/material";
+
+type GradientBox = React.PropsWithChildren<{
+ palette?: PaletteKey;
+ sx?: SxProps;
+}>;
+
+export const GradientBox = ({ children, palette, sx }: GradientBox) => {
+ const theme = useTheme();
+ const isSmall = useMediaQuery(theme.breakpoints.down("md"));
+ const isLight = theme.palette.mode === "light";
+ const paper = theme.palette.background.paper;
+ const paperStart = lighten(paper, isLight ? 0.06 : 0.08);
+ const paperEnd = paper;
+ const bg = palette
+ ? `linear-gradient(135deg, ${theme.palette[palette].light} 0%, ${theme.palette[palette].main} 100%)`
+ : `linear-gradient(135deg, ${paperStart} 0%, ${paperEnd} 100%)`;
+
+ return (
+
+ {children}
+
+ );
+};
+
+type StatBoxProps = React.PropsWithChildren<{
+ title: string;
+ subtitle: string;
+ palette?: PaletteKey;
+ sx?: SxProps;
+ tooltip?: string;
+ onClick?: () => void;
+}>;
+
+export const StatBox = ({
+ title,
+ subtitle,
+ palette,
+ children,
+ sx,
+ tooltip,
+ onClick,
+}: StatBoxProps) => {
+ const theme = useTheme();
+ const textColor = palette ? theme.palette[palette].contrastText : "inherit";
+
+ return (
+
+
+
+ {title}
+ {tooltip && (
+
+ )}
+
+ {subtitle}
+ {children}
+
+
+ );
+};
diff --git a/client/src/Components/v2/design-elements/StatusBox.tsx b/client/src/Components/v2/design-elements/StatusBox.tsx
index 6b1c1920c..bf7979cca 100644
--- a/client/src/Components/v2/design-elements/StatusBox.tsx
+++ b/client/src/Components/v2/design-elements/StatusBox.tsx
@@ -4,12 +4,16 @@ import Box from "@mui/material/Box";
import { BaseBox } from "@/Components/v2/design-elements";
import Background from "@/assets/Images/background-grid.svg?react";
import { useTranslation } from "react-i18next";
+import type { SxProps } from "@mui/material";
import { useTheme } from "@mui/material/styles";
-type StatusBoxProps = React.PropsWithChildren<{ children: React.ReactNode }>;
+type StatusBoxProps = React.PropsWithChildren<{
+ children: React.ReactNode;
+ sx?: SxProps;
+}>;
-export const BGBox = ({ children }: StatusBoxProps) => {
+export const BGBox = ({ children, sx }: StatusBoxProps) => {
const theme = useTheme();
return (
{
position: "relative",
flex: 1,
padding: theme.spacing(4),
+ ...sx,
}}
>
{
const theme = useTheme();
return (
-
+
{
/>
);
};
+export const TotalChecksBox = ({ n }: { n: number }) => {
+ const theme = useTheme();
+ const { t } = useTranslation();
+ return (
+
+ );
+};
+export const DownChecksBox = ({ n }: { n: number }) => {
+ const theme = useTheme();
+ const { t } = useTranslation();
+ return (
+
+ );
+};
+export const UpChecksBox = ({ n }: { n: number }) => {
+ const theme = useTheme();
+ const { t } = useTranslation();
+ return (
+
+ );
+};
export const InitializingStatusBox = ({ n }: { n: number }) => {
const theme = useTheme();
diff --git a/client/src/Components/v2/design-elements/StatusLabel.tsx b/client/src/Components/v2/design-elements/StatusLabel.tsx
index 30adf5917..c9991f0d4 100644
--- a/client/src/Components/v2/design-elements/StatusLabel.tsx
+++ b/client/src/Components/v2/design-elements/StatusLabel.tsx
@@ -17,14 +17,17 @@ export const StatusLabel = ({
sx,
}: {
status: MonitorStatus;
- isActive: boolean;
+ isActive?: boolean;
sx?: SxProps;
}) => {
const { t } = useTranslation();
const theme = useTheme();
const palette = getStatusPalette(status);
- const determineStatus = (isActive: boolean, status: MonitorStatus): string => {
+ const determineStatus = (
+ isActive: boolean | undefined,
+ status: MonitorStatus
+ ): string => {
if (isActive === false) {
return t("pages.common.monitors.status.paused");
}
diff --git a/client/src/Components/v2/design-elements/Table.tsx b/client/src/Components/v2/design-elements/Table.tsx
index c87fa41b8..0516f1ddd 100644
--- a/client/src/Components/v2/design-elements/Table.tsx
+++ b/client/src/Components/v2/design-elements/Table.tsx
@@ -147,7 +147,7 @@ export function DataTable<
{
+ const theme = useTheme();
+ return (
+
+ {props.children}
+
+ );
+};
+
+import MuiTab from "@mui/material/Tab";
+import type { TabProps } from "@mui/material/Tab";
+interface CustomTabProps extends TabProps {}
+
+export const Tab = (props: CustomTabProps) => {
+ const theme = useTheme();
+ return (
+
+ );
+};
diff --git a/client/src/Components/v2/design-elements/Tooltip.tsx b/client/src/Components/v2/design-elements/Tooltip.tsx
new file mode 100644
index 000000000..f83b1331c
--- /dev/null
+++ b/client/src/Components/v2/design-elements/Tooltip.tsx
@@ -0,0 +1,79 @@
+import MuiTooltip from "@mui/material/Tooltip";
+import { useTheme } from "@mui/material/styles";
+import { Info } from "lucide-react";
+
+import type { TooltipProps } from "@mui/material/Tooltip";
+
+type StyledTooltipProps = TooltipProps;
+
+export const Tooltip = ({ placement = "top", ...props }: StyledTooltipProps) => {
+ const theme = useTheme();
+
+ return (
+
+ {props.children}
+
+ );
+};
+
+export interface TooltipWithInfoProps extends Omit {
+ iconSize?: number;
+ iconColor?: string;
+}
+
+export const TooltipWithInfo = ({
+ iconSize = 14,
+ iconColor,
+ ...props
+}: TooltipWithInfoProps) => {
+ const theme = useTheme();
+
+ const defaultColor = theme.palette.text.secondary;
+
+ return (
+
+
+ {
+ e.currentTarget.style.opacity = "1";
+ }}
+ onMouseLeave={(e) => {
+ e.currentTarget.style.opacity = "0.7";
+ }}
+ />
+
+
+ );
+};
diff --git a/client/src/Components/v2/design-elements/index.tsx b/client/src/Components/v2/design-elements/index.tsx
index 374dabef7..a96fea052 100644
--- a/client/src/Components/v2/design-elements/index.tsx
+++ b/client/src/Components/v2/design-elements/index.tsx
@@ -8,3 +8,13 @@ export * from "./BaseBox";
export * from "./Table";
export { DataTable as Table } from "./Table";
export * from "./StatusLabel";
+export * from "./PulseDot";
+export * from "./Dot";
+export * from "./MonitorStatus";
+export { default as Icon } from "./Icon";
+export * from "./Tooltip";
+export * from "./StatBox";
+export * from "./BaseChart";
+export * from "./Gauge";
+export * from "./Tabs";
+export * from "./SplitBox";
diff --git a/client/src/Components/v2/inputs/Dialog.tsx b/client/src/Components/v2/inputs/Dialog.tsx
new file mode 100644
index 000000000..373d95023
--- /dev/null
+++ b/client/src/Components/v2/inputs/Dialog.tsx
@@ -0,0 +1,56 @@
+import Dialog from "@mui/material/Dialog";
+import DialogActions from "@mui/material/DialogActions";
+import DialogContent from "@mui/material/DialogContent";
+import DialogContentText from "@mui/material/DialogContentText";
+import DialogTitle from "@mui/material/DialogTitle";
+import { Button } from "@/Components/v2/inputs";
+import { typographyLevels } from "@/Utils/Theme/v2Palette";
+import { useTranslation } from "react-i18next";
+
+export const DialogInput = ({
+ open,
+ title,
+ content,
+ onConfirm,
+ onCancel,
+ confirmColor = "primary",
+ cancelColor = "error",
+ loading = false,
+}: {
+ open: boolean;
+ title?: string;
+ content?: string;
+ onConfirm?(item: any): any;
+ onCancel?(item: any): any;
+ confirmColor?: "error" | "primary";
+ cancelColor?: "error" | "primary";
+ loading?: boolean;
+}) => {
+ const { t } = useTranslation();
+ return (
+
+ );
+};
diff --git a/client/src/Components/v2/inputs/Radio.tsx b/client/src/Components/v2/inputs/Radio.tsx
new file mode 100644
index 000000000..ad8dc3374
--- /dev/null
+++ b/client/src/Components/v2/inputs/Radio.tsx
@@ -0,0 +1,88 @@
+import Radio from "@mui/material/Radio";
+import type { RadioProps } from "@mui/material/Radio";
+import { useTheme } from "@mui/material/styles";
+import { Circle, CircleDot } from "lucide-react";
+import FormControlLabel from "@mui/material/FormControlLabel";
+import Typography from "@mui/material/Typography";
+
+interface RadioInputProps extends RadioProps {}
+
+export const RadioInput = ({ ...props }: RadioInputProps) => {
+ const theme = useTheme();
+ return (
+
+ }
+ checkedIcon={
+
+ }
+ sx={{
+ padding: 0,
+ mt: theme.spacing(0.5),
+ color: theme.palette.text.secondary,
+ "&.Mui-checked": {
+ color: theme.palette.primary.main,
+ "& svg circle": {
+ fill: theme.palette.primary.main,
+ },
+ },
+ "& .MuiSvgIcon-root": {
+ fontSize: 16,
+ },
+ "& svg": {
+ stroke: "currentColor",
+ },
+ "& svg path, & svg line, & svg polyline, & svg rect, & svg circle": {
+ stroke: "currentColor",
+ fill: "none",
+ },
+ }}
+ />
+ );
+};
+
+export const RadioWithDescription = ({
+ label,
+ description,
+ ...props
+}: RadioInputProps & { label: string; description: string }) => {
+ const theme = useTheme();
+ return (
+ }
+ label={
+ <>
+ {label}
+
+ {description}
+
+ >
+ }
+ sx={{
+ alignItems: "flex-start",
+ p: theme.spacing(2.5),
+ m: theme.spacing(-2.5),
+ borderRadius: theme.shape.borderRadius,
+
+ "&:hover": {
+ backgroundColor: theme.palette.background.paper,
+ },
+ "& .MuiButtonBase-root": {
+ p: 0,
+ mr: theme.spacing(6),
+ },
+ }}
+ />
+ );
+};
diff --git a/client/src/Components/v2/inputs/Select.tsx b/client/src/Components/v2/inputs/Select.tsx
index 057b0e72c..b9675acd7 100644
--- a/client/src/Components/v2/inputs/Select.tsx
+++ b/client/src/Components/v2/inputs/Select.tsx
@@ -1,7 +1,7 @@
import Select from "@mui/material/Select";
-import React from "react";
-import { useTheme } from "@mui/material/styles";
import type { SelectProps } from "@mui/material/Select";
+import React, { forwardRef } from "react";
+import { useTheme } from "@mui/material/styles";
import Stack from "@mui/material/Stack";
import { FieldLabel } from "./FieldLabel";
import { ChevronDown } from "lucide-react";
@@ -14,18 +14,15 @@ interface SelectInputProps extends Omit, "label"> {
placeholderColor?: string;
}
-export const SelectInput = ({
- fieldLabel,
- required,
- placeholder,
- placeholderColor,
- ...props
-}: SelectInputProps) => {
+const SelectInputInner = (
+ { fieldLabel, required, placeholder, placeholderColor, ...props }: SelectInputProps,
+ ref: React.ForwardedRef
+) => {
const theme = useTheme();
const emptyPlaceholderColor = placeholderColor || theme.palette.text.disabled;
- const renderValue = (selected: any) => {
- const isMultiple = Boolean((props as any).multiple);
+ const renderValue = (selected: unknown) => {
+ const isMultiple = Boolean((props as { multiple?: boolean }).multiple);
const isEmpty = isMultiple
? !Array.isArray(selected) || selected.length === 0
: selected === undefined || selected === null || selected === "";
@@ -39,7 +36,7 @@ export const SelectInput = ({
const capitalized = items.map(
(item) => item.charAt(0).toUpperCase() + item.slice(1)
);
- return ({capitalized.join(" | ")}) as any;
+ return {capitalized.join(" | ")};
}
const nodes = React.Children.toArray(props.children as React.ReactNode);
@@ -59,6 +56,7 @@ export const SelectInput = ({
const select = (