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 ( + + + + {/* Left sidebar with tabs */} + + + {t('notifications.addOrEditNotifications')} + + + + {activeNotificationTypes.map((type) => ( + + ))} + + + + {/* Right side content */} + + {activeNotificationTypes.map((type, index) => ( + + + + ))} + + + + + + + + ); +}; + +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 ( + + ); +} + +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" + } + } + }