Merge pull request #1855 from bluewave-labs/feat/notification-modal

Implemented notification modal.
This commit is contained in:
Alexander Holliday
2025-03-10 10:26:30 -07:00
committed by GitHub
6 changed files with 573 additions and 38 deletions
@@ -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 (
<Dialog
open={open}
onClose={onClose}
fullWidth
maxWidth="md"
sx={{
'& .MuiDialog-paper': {
width: `calc(80% - ${theme.spacing(40)})`,
maxWidth: `${theme.breakpoints.values.md - 70}px`
}
}}
>
<DialogContent>
<Box sx={{
display: 'flex',
height: `calc(26vh - ${theme.spacing(20)})`
}}>
{/* Left sidebar with tabs */}
<Box sx={{
borderRight: 1,
borderColor: theme.palette.primary.lowContrast,
width: '30%',
maxWidth: theme.spacing(120),
pr: theme.spacing(10)
}}>
<Typography variant="subtitle1" sx={{
my: theme.spacing(1),
fontWeight: 'bold',
fontSize: theme.typography.fontSize * 0.9,
color: theme.palette.primary.contrastTextSecondary,
pl: theme.spacing(4)
}}>
{t('notifications.addOrEditNotifications')}
</Typography>
<Tabs
orientation="vertical"
variant="scrollable"
value={tabValue}
onChange={handleChangeTab}
aria-label="Notification tabs"
>
{activeNotificationTypes.map((type) => (
<Tab
key={type.id}
label={type.label}
orientation="vertical"
disableRipple
/>
))}
</Tabs>
</Box>
{/* Right side content */}
<Box sx={{
flex: 1,
pl: theme.spacing(7.5),
overflowY: 'auto'
}}>
{activeNotificationTypes.map((type, index) => (
<TabPanel key={type.id} value={tabValue} index={index}>
<TabComponent
type={type}
integrations={integrations}
handleIntegrationChange={handleIntegrationChange}
handleInputChange={handleInputChange}
handleTestNotification={handleTestNotification}
/>
</TabPanel>
))}
</Box>
</Box>
</DialogContent>
<DialogActions sx={{
p: theme.spacing(4),
display: 'flex',
justifyContent: 'flex-end',
mb: theme.spacing(5),
mr: theme.spacing(5)
}}>
<Button
variant="contained"
color="accent"
onClick={handleSave}
sx={{
width: 'auto',
minWidth: theme.spacing(60),
px: theme.spacing(8)
}}
>
{t('common.save', 'Save')}
</Button>
</DialogActions>
</Dialog>
);
};
export default NotificationIntegrationModal;
@@ -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 (
<>
<Typography variant="subtitle1" component="h4" sx={{
fontWeight: 'bold',
color: theme.palette.primary.contrastTextSecondary
}}>
{type.label}
</Typography>
<Typography sx={{
mt: theme.spacing(0.5),
mb: theme.spacing(1.5),
color: theme.palette.primary.contrastTextTertiary
}}>
{type.description}
</Typography>
<Box sx={{ pl: theme.spacing(1.5) }}>
<Checkbox
id={`enable-${type.id}`}
label={t('notifications.enableNotifications', { platform: type.label })}
isChecked={integrations[type.id]}
onChange={(e) => handleIntegrationChange(type.id, e.target.checked)}
/>
</Box>
{type.fields.map(field => {
const fieldKey = getFieldKey(type.id, field.id);
return (
<Box key={field.id} sx={{ mt: theme.spacing(1) }}>
<Typography sx={{
mb: theme.spacing(2),
fontWeight: 'bold',
color: theme.palette.primary.contrastTextSecondary
}}>
{field.label}
</Typography>
<TextInput
id={`${type.id}-${field.id}`}
type={field.type}
placeholder={field.placeholder}
value={integrations[fieldKey]}
onChange={(e) => handleInputChange(fieldKey, e.target.value)}
disabled={!integrations[type.id]}
/>
</Box>
);
})}
<Box sx={{ mt: theme.spacing(1) }}>
<Button
variant="text"
color="info"
onClick={() => handleTestNotification(type.id)}
disabled={!integrations[type.id] || !areAllFieldsFilled()}
>
{t('notifications.testNotification')}
</Button>
</Box>
</>
);
};
export default TabComponent;
@@ -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 (
<div
role="tabpanel"
hidden={value !== index}
id={`notification-tabpanel-${index}`}
aria-labelledby={`notification-tab-${index}`}
{...other}
>
{value === index && (
<Box sx={{ pt: theme.spacing(3) }}>
{children}
</Box>
)}
</div>
);
}
TabPanel.propTypes = {
children: PropTypes.node,
index: PropTypes.number.isRequired,
value: PropTypes.number.isRequired
};
export default TabPanel;
+16 -3
View File
@@ -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")}
/>
<Box mt={theme.spacing(2)}>
{/* <Box mt={theme.spacing(2)}>
<Button
variant="contained"
color="accent"
onClick={handleAddNotification}
onClick={handleOpenNotificationModal}
>
Notification Integration
</Button>
</Box>
</Box> */}
</Stack>
</ConfigBox>
@@ -512,6 +518,13 @@ const CreateMonitor = () => {
</Button>
</Stack>
</Stack>
<NotificationIntegrationModal
open={isNotificationModalOpen}
onClose={() => setIsNotificationModalOpen(false)}
monitor={monitor}
setMonitor={setMonitor}
/>
</Box>
);
};
+104 -34
View File
@@ -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 }) => ({
+34 -1
View File
@@ -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"
}
}
}