diff --git a/src/Components/NotificationIntegrationModal/NotificationIntegrationModal.jsx b/src/Components/NotificationIntegrationModal/NotificationIntegrationModal.jsx
new file mode 100644
index 000000000..30587833a
--- /dev/null
+++ b/src/Components/NotificationIntegrationModal/NotificationIntegrationModal.jsx
@@ -0,0 +1,273 @@
+import { useState, useMemo } from "react";
+import { useTranslation } from "react-i18next";
+import {
+ Dialog,
+ DialogContent,
+ DialogActions,
+ Button,
+ Typography,
+ Box,
+ Tabs,
+ Tab
+} from "@mui/material";
+import { useTheme } from "@emotion/react";
+import TabPanel from "./TabPanel";
+import TabComponent from "./TabComponent";
+
+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);
+
+ // Define notification types
+ const DEFAULT_NOTIFICATION_TYPES = [
+ {
+ id: 'slack',
+ label: t('notifications.slack.label'),
+ description: t('notifications.slack.description'),
+ fields: [
+ {
+ id: 'webhook',
+ label: t('notifications.slack.webhookLabel'),
+ placeholder: t('notifications.slack.webhookPlaceholder'),
+ type: 'text'
+ }
+ ]
+ },
+ {
+ id: 'discord',
+ label: t('notifications.discord.label'),
+ description: t('notifications.discord.description'),
+ fields: [
+ {
+ id: 'webhook',
+ label: t('notifications.discord.webhookLabel'),
+ placeholder: t('notifications.discord.webhookPlaceholder'),
+ type: 'text'
+ }
+ ]
+ },
+ {
+ id: 'telegram',
+ label: t('notifications.telegram.label'),
+ description: t('notifications.telegram.description'),
+ fields: [
+ {
+ id: 'token',
+ label: t('notifications.telegram.tokenLabel'),
+ placeholder: t('notifications.telegram.tokenPlaceholder'),
+ type: 'text'
+ },
+ {
+ id: 'chatId',
+ label: t('notifications.telegram.chatIdLabel'),
+ placeholder: t('notifications.telegram.chatIdPlaceholder'),
+ type: 'text'
+ }
+ ]
+ },
+ {
+ id: 'webhook',
+ label: t('notifications.webhook.label'),
+ description: t('notifications.webhook.description'),
+ fields: [
+ {
+ id: '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] = monitor?.notifications?.some(n => n.type === type.id) || false;
+
+ // Add state for each field in the notification type
+ type.fields.forEach(field => {
+ const fieldKey = `${type.id}${field.id.charAt(0).toUpperCase() + field.id.slice(1)}`;
+ state[fieldKey] = monitor?.notifications?.find(n => n.type === type.id)?.[field.id] || "";
+ });
+ });
+
+ return state;
+ }, [monitor, activeNotificationTypes]); // Only recompute when these dependencies change
+
+ const [integrations, setIntegrations] = useState(initialIntegrationsState);
+
+ 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 = (type) => {
+ console.log(`Testing ${type} notification`);
+ //implement the test notification functionality
+ };
+
+ const handleSave = () => {
+ //notifications array for selected integrations
+ 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 => !existingTypes.includes(notification.type)
+ );
+
+ // Add each enabled notification with its configured fields
+ activeNotificationTypes.forEach(type => {
+ if (integrations[type.id]) {
+ const notificationObject = {
+ type: type.id
+ };
+
+ // Add each field value to the notification object
+ type.fields.forEach(field => {
+ const fieldKey = `${type.id}${field.id.charAt(0).toUpperCase() + field.id.slice(1)}`;
+ notificationObject[field.id] = integrations[fieldKey];
+ });
+
+ filteredNotifications.push(notificationObject);
+ }
+ });
+
+ // Update monitor with new notifications
+ setMonitor(prev => ({
+ ...prev,
+ notifications: filteredNotifications
+ }));
+
+ onClose();
+ };
+
+ return (
+
+ );
+};
+
+export default NotificationIntegrationModal;
\ No newline at end of file
diff --git a/src/Components/NotificationIntegrationModal/TabComponent.jsx b/src/Components/NotificationIntegrationModal/TabComponent.jsx
new file mode 100644
index 000000000..89216a8c1
--- /dev/null
+++ b/src/Components/NotificationIntegrationModal/TabComponent.jsx
@@ -0,0 +1,100 @@
+import React from "react";
+import {
+ Typography,
+ Box,
+ Button
+} from "@mui/material";
+import { useTranslation } from "react-i18next";
+import { useTheme } from "@emotion/react";
+import TextInput from "../../../src/Components/Inputs/TextInput";
+import Checkbox from "../../../src/Components/Inputs/Checkbox";
+
+const TabComponent = ({
+ type,
+ integrations,
+ handleIntegrationChange,
+ handleInputChange,
+ handleTestNotification
+}) => {
+ 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)}
+ />
+
+
+ {type.fields.map(field => {
+ const fieldKey = getFieldKey(type.id, field.id);
+
+ return (
+
+
+ {field.label}
+
+
+ handleInputChange(fieldKey, e.target.value)}
+ disabled={!integrations[type.id]}
+ />
+
+ );
+ })}
+
+
+
+
+ >
+ );
+};
+
+export default TabComponent;
\ No newline at end of file
diff --git a/src/Components/NotificationIntegrationModal/TabPanel.jsx b/src/Components/NotificationIntegrationModal/TabPanel.jsx
new file mode 100644
index 000000000..a9252e710
--- /dev/null
+++ b/src/Components/NotificationIntegrationModal/TabPanel.jsx
@@ -0,0 +1,46 @@
+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;
\ No newline at end of file
diff --git a/src/Pages/Uptime/Create/index.jsx b/src/Pages/Uptime/Create/index.jsx
index edad33d20..3b295f586 100644
--- a/src/Pages/Uptime/Create/index.jsx
+++ b/src/Pages/Uptime/Create/index.jsx
@@ -23,6 +23,7 @@ import Radio from "../../../Components/Inputs/Radio";
import Checkbox from "../../../Components/Inputs/Checkbox";
import Select from "../../../Components/Inputs/Select";
import ConfigBox from "../../../Components/ConfigBox";
+import NotificationIntegrationModal from "../../../Components/NotificationIntegrationModal/NotificationIntegrationModal";
const CreateMonitor = () => {
const MS_PER_MINUTE = 60000;
const SELECT_VALUES = [
@@ -80,6 +81,11 @@ const CreateMonitor = () => {
];
// State
+ const [isNotificationModalOpen, setIsNotificationModalOpen] = useState(false);
+
+ const handleOpenNotificationModal = () => {
+ setIsNotificationModalOpen(true);
+ };
const [errors, setErrors] = useState({});
const [https, setHttps] = useState(true);
const [monitor, setMonitor] = useState({
@@ -407,15 +413,15 @@ const CreateMonitor = () => {
onChange={(event) => handleNotifications(event, "email")}
/>
-
+ {/*
-
+ */}
@@ -512,6 +518,13 @@ const CreateMonitor = () => {
+
+ setIsNotificationModalOpen(false)}
+ monitor={monitor}
+ setMonitor={setMonitor}
+ />
);
};
diff --git a/src/Utils/Theme/globalTheme.js b/src/Utils/Theme/globalTheme.js
index 8d225e895..6e05fa592 100644
--- a/src/Utils/Theme/globalTheme.js
+++ b/src/Utils/Theme/globalTheme.js
@@ -117,6 +117,33 @@ const baseTheme = (palette) => ({
color: `${theme.palette.secondary.contrastText} !important`,
},
},
+
+ {
+ props: { variant: 'text', color: 'info' },
+ style: {
+ textDecoration: 'underline',
+ color: theme.palette.text.primary,
+ padding: 0,
+ margin: 0,
+ fontSize: typographyLevels.m,
+ fontWeight: theme.typography.body2.fontWeight,
+ backgroundColor: 'transparent',
+ '&:hover': {
+ backgroundColor: 'transparent',
+ textDecoration: 'underline'
+ },
+ "&.Mui-disabled": {
+ backgroundColor: theme.palette.secondary.main,
+ color: theme.palette.primary.contrastText,
+ "&.MuiButton-text": {
+ backgroundColor: 'transparent'
+ }
+ },
+ minWidth: 0,
+ boxShadow: 'none',
+ border: 'none'
+ },
+ },
],
height: 34,
fontWeight: 400,
@@ -328,35 +355,68 @@ const baseTheme = (palette) => ({
},
MuiTab: {
styleOverrides: {
- root: ({ theme }) => ({
- fontSize: 13,
- color: theme.palette.tertiary.contrastText,
- backgroundColor: theme.palette.tertiary.main,
- textTransform: "none",
- minWidth: "fit-content",
- paddingY: theme.spacing(6),
- fontWeight: 400,
- borderBottom: "2px solid transparent",
- borderRight: `1px solid ${theme.palette.primary.lowContrast}`,
- "&:first-of-type": { borderTopLeftRadius: "8px" },
- "&:last-child": { borderTopRightRadius: "8px", borderRight: 0 },
- "&:focus-visible": {
- color: theme.palette.primary.contrastText,
- borderColor: theme.palette.tertiary.contrastText,
- borderRightColor: theme.palette.primary.lowContrast,
- },
- "&.Mui-selected": {
- backgroundColor: theme.palette.secondary.main,
- color: theme.palette.secondary.contrastText,
- borderColor: theme.palette.secondary.contrastText,
- borderRightColor: theme.palette.primary.lowContrast,
- },
- "&:hover": {
- borderColor: theme.palette.primary.lowContrast,
- },
- }),
+ root: ({ theme }) => ({
+ fontSize: theme.typography.fontSize - 1,
+ color: theme.palette.tertiary.contrastText,
+ backgroundColor: theme.palette.tertiary.main,
+ textTransform: "none",
+ minWidth: "fit-content",
+ padding: `${theme.spacing(6)}px ${theme.spacing(4)}px`,
+ fontWeight: 400,
+ borderBottom: `${theme.shape.borderThick}px solid transparent`,
+ borderRight: `${theme.shape.borderRadius / 2}px solid ${theme.palette.primary.lowContrast}`,
+ "&:first-of-type": { borderTopLeftRadius: theme.shape.borderRadius * 4 },
+ "&:last-child": { borderTopRightRadius: theme.shape.borderRadius * 4, borderRight: 0 },
+ "&:focus-visible": {
+ color: theme.palette.primary.contrastText,
+ borderColor: theme.palette.tertiary.contrastText,
+ borderRightColor: theme.palette.primary.lowContrast,
+ },
+ "&.Mui-selected": {
+ backgroundColor: theme.palette.secondary.main,
+ color: theme.palette.secondary.contrastText,
+ borderColor: theme.palette.secondary.contrastText,
+ borderRightColor: theme.palette.primary.lowContrast,
+ },
+ "&:hover": {
+ borderColor: theme.palette.primary.lowContrast,
+ },
+ }),
},
- },
+ variants: [
+ {
+ props: { orientation: 'vertical' },
+ style: ({ theme }) => ({
+ alignItems: 'flex-start',
+ padding: `${theme.spacing(1)}px ${theme.spacing(2)}px ${theme.spacing(1)}px ${theme.spacing(6)}px`,
+ minHeight: theme.spacing(12),
+ color: theme.palette.primary.contrastText,
+ backgroundColor: theme.palette.primary.main,
+ border: 'none',
+ borderBottom: 'none',
+ borderRight: 'none',
+ borderRadius: theme.shape.borderRadius * 3,
+ margin: `${theme.spacing(1)}px ${theme.spacing(2)}px`,
+ '&.Mui-selected': {
+ color: theme.palette.primary.contrastText,
+ backgroundColor: theme.palette.tertiary.main,
+ opacity: 1,
+ border: 'none',
+ borderBottom: 'none',
+ borderRight: 'none',
+ borderRadius: theme.shape.borderRadius * 3,
+ minHeight: theme.spacing(14)
+ },
+ '&:hover': {
+ backgroundColor: theme.palette.tertiary.main,
+ border: 'none',
+ borderRadius: theme.shape.borderRadius * 3,
+ minHeight: theme.spacing(14)
+ }
+ }),
+ },
+ ],
+ },
MuiSvgIcon: {
styleOverrides: {
root: ({ theme }) => ({
@@ -366,13 +426,23 @@ const baseTheme = (palette) => ({
},
MuiTabs: {
styleOverrides: {
- root: ({ theme }) => ({
- "& .MuiTabs-indicator": {
- backgroundColor: theme.palette.tertiary.contrastText,
- },
- }),
+ root: ({ theme }) => ({
+ "& .MuiTabs-indicator": {
+ backgroundColor: theme.palette.tertiary.contrastText,
+ },
+ }),
},
- },
+ variants: [
+ {
+ props: { orientation: 'vertical' },
+ style: {
+ "& .MuiTabs-indicator": {
+ display: 'none',
+ }
+ },
+ },
+ ],
+ },
MuiSwitch: {
styleOverrides: {
root: ({ theme }) => ({
diff --git a/src/locales/gb.json b/src/locales/gb.json
index 521d0eb18..60d0899cb 100644
--- a/src/locales/gb.json
+++ b/src/locales/gb.json
@@ -141,5 +141,38 @@
"settingsDemoMonitorsAdded": "Successfully added demo monitors",
"settingsFailedToAddDemoMonitors": "Failed to add demo monitors",
"settingsMonitorsDeleted": "Successfully deleted all monitors",
- "settingsFailedToDeleteMonitors": "Failed to delete all monitors"
+ "settingsFailedToDeleteMonitors": "Failed to delete all monitors",
+
+ "notifications": {
+ "enableNotifications": "Enable {{platform}} notifications",
+ "testNotification": "Test notification",
+ "addOrEditNotifications": "Add or edit notifications",
+ "slack": {
+ "label": "Slack",
+ "description": "To enable Slack notifications, create a Slack app and enable incoming webhooks. After that, simply provide the webhook URL here.",
+ "webhookLabel": "Webhook URL",
+ "webhookPlaceholder": "https://hooks.slack.com/services/..."
+ },
+ "discord": {
+ "label": "Discord",
+ "description": "To send data to a Discord channel from Checkmate via Discord notifications using webhooks, you can use Discord's incoming Webhooks feature.",
+ "webhookLabel": "Discord Webhook URL",
+ "webhookPlaceholder": "https://discord.com/api/webhooks/..."
+ },
+ "telegram": {
+ "label": "Telegram",
+ "description": "To enable Telegram notifications, create a Telegram bot using BotFather, an official bot for creating and managing Telegram bots. Then, get the API token and chat ID and write them down here.",
+ "tokenLabel": "Your bot token",
+ "tokenPlaceholder": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11",
+ "chatIdLabel": "Your Chat ID",
+ "chatIdPlaceholder": "-1001234567890"
+ },
+ "webhook": {
+ "label": "Webhooks",
+ "description": "You can set up a custom webhook to receive notifications when incidents occur.",
+ "urlLabel": "Webhook URL",
+ "urlPlaceholder": "https://your-server.com/webhook"
+ }
+ }
+
}