restore repo structure

This commit is contained in:
Alex Holliday committed 2025-04-20 11:29:53 -07:00
1 parent 6df099a5b6
commit 8b7e3c650b
592 files changed
+37663 -127

No files matched your search

@@ -0,0 +1,353 @@
import { useState, useMemo } from "react";
import { useTranslation } from "react-i18next";
import PropTypes from "prop-types";
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";
import useNotifications from "../Hooks/useNotification";
// 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 = (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)}`;
};
// 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] = monitor?.notifications?.some(n => n.type === 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] = 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 = 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 (
<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}
isLoading={loading}
/>
</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}
loading={loading}
sx={{
width: 'auto',
minWidth: theme.spacing(60),
px: theme.spacing(8)
}}
>
{t('commonSave')}
</Button>
</DialogActions>
</Dialog>
);
};
NotificationIntegrationModal.propTypes = {
open: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
monitor: PropTypes.object.isRequired,
setMonitor: PropTypes.func.isRequired,
notificationTypes: PropTypes.array
};
export default NotificationIntegrationModal;
@@ -0,0 +1,109 @@
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 "../../../Components/Inputs/TextInput";
import Checkbox from "../../../Components/Inputs/Checkbox";
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 (
<>
<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)}
disabled={isLoading}
/>
</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] || isLoading}
/>
</Box>
);
})}
<Box sx={{ mt: theme.spacing(1) }}>
<Button
variant="text"
color="info"
onClick={() => handleTestNotification(type.id)}
disabled={!integrations[type.id] || !areAllFieldsFilled() || isLoading}
>
{isLoading ? (
<CircularProgress
size={theme.spacing(8)}
sx={{ mr: theme.spacing(1), color: theme.palette.accent.main}}
/>
) : null}
{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;
@@ -0,0 +1,128 @@
import { useState } from 'react';
import { useTranslation } from "react-i18next";
import { networkService } from '../../../Utils/NetworkService';
import { createToast } from '../../../Utils/toastUtils';
// 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;