mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-05-12 12:39:05 -05:00
Merge pull request #1855 from bluewave-labs/feat/notification-modal
Implemented notification modal.
This commit is contained in:
@@ -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;
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user