diff --git a/Tombolo/client-reactjs/src/components/admin/Integrations/asr/DomainModal.jsx b/Tombolo/client-reactjs/src/components/admin/Integrations/asr/DomainModal.jsx index 6f69ddef..f0ebed22 100644 --- a/Tombolo/client-reactjs/src/components/admin/Integrations/asr/DomainModal.jsx +++ b/Tombolo/client-reactjs/src/components/admin/Integrations/asr/DomainModal.jsx @@ -1,13 +1,16 @@ // Package imports import React, { useEffect } from 'react'; import { useSelector } from 'react-redux'; -import { Modal, Form, Input, Select, message } from 'antd'; +import { Modal, Form, Input, Select, message, Row, Col } from 'antd'; +import { isEmail } from 'validator'; //Local Imports import { createNewDomain, getDomains, updateDomain } from './asr-integration-util.js'; // Constants const { Option } = Select; +const severityThresholds = [0, 1, 2, 3]; +const regions = ['UK', 'USA']; const DomainModal = ({ domainModalOpen, @@ -25,7 +28,13 @@ const DomainModal = ({ if (selectedDomain) { let activityTypesIds = selectedDomain.activityTypes.map((d) => d.id); activityTypesIds = activityTypesIds.filter((id) => id !== null); - form.setFieldsValue({ name: selectedDomain.name, monitoringTypeIds: activityTypesIds }); + form.setFieldsValue({ + name: selectedDomain.name, + region: selectedDomain.region, + severityThreshold: selectedDomain.severityThreshold, + monitoringTypeIds: activityTypesIds, + severityAlertRecipients: selectedDomain.severityAlertRecipients, + }); } }, [selectedDomain]); @@ -123,14 +132,74 @@ const DomainModal = ({ okText={selectedDomain ? 'Update' : 'Save'} maskClosable={false}>
+ + + + + + + + + + + + + + + + + + + + - + label="Severity E-mail Recipients" + name="severityAlertRecipients" + required + rules={[ + { + validator: (_, value) => { + if (!value || value.length === 0) { + return Promise.reject(new Error('Please add at least one email!')); + } + if (value.length > 20) { + return Promise.reject(new Error('Too many emails')); + } + if (!value.every((v) => isEmail(v))) { + return Promise.reject(new Error('One or more emails are invalid')); + } + return Promise.resolve(); + }, + }, + ]}> + + + + - - )} - Activate Megaphone alerts @@ -241,52 +159,6 @@ function GeneralSettingsEditModal({ )} - - - Activate NOC alerts - - - {displayRecipients.nocAlerts && ( - - - - - - - - { - if (value && !isEmail(value)) { - return Promise.reject(new Error('Invalid email')); - } else if (value.length > 255) { - return Promise.reject(new Error('Email provided exceeds the maximum length')); - } - return Promise.resolve(); - }, - }, - ]}> - - - - - )}
diff --git a/Tombolo/client-reactjs/src/components/admin/Integrations/asr/GeneralTab.jsx b/Tombolo/client-reactjs/src/components/admin/Integrations/asr/GeneralTab.jsx index 627795c4..53e382b6 100644 --- a/Tombolo/client-reactjs/src/components/admin/Integrations/asr/GeneralTab.jsx +++ b/Tombolo/client-reactjs/src/components/admin/Integrations/asr/GeneralTab.jsx @@ -7,66 +7,21 @@ import { CheckSquareFilled, CloseSquareFilled } from '@ant-design/icons'; import '../integrations.css'; function GeneralTab({ integrationDetails, teamsChannels }) { - const [severity3AlertRecipients, setSeverity3AlertRecipients] = useState(null); const [megaphoneAlertRecipients, setMegaphoneAlertRecipients] = useState(null); - const [nocAlerts, setNocAlerts] = useState(null); - const [minSeverityLevelForNocAlert, setMinSeverityLevelForNocAlert] = useState(null); // Effects useEffect(() => { - const severity3EmailContacts = - integrationDetails?.appSpecificIntegrationMetaData?.severity3Alerts?.emailContacts || []; const megaphoneEmailContacts = integrationDetails?.appSpecificIntegrationMetaData?.megaPhoneAlerts?.emailContacts || []; const megaPhoneTeamsContacts = integrationDetails?.appSpecificIntegrationMetaData?.megaPhoneAlerts?.teamsChannel || []; - const nocEmailContacts = integrationDetails?.appSpecificIntegrationMetaData?.nocAlerts?.emailContacts || []; - const minSeverityLevelForNocAlert = - integrationDetails?.appSpecificIntegrationMetaData?.nocAlerts?.severityLevelForNocAlerts || null; - setSeverity3AlertRecipients({ emailContacts: severity3EmailContacts }); setMegaphoneAlertRecipients({ emailContacts: megaphoneEmailContacts, teamsChannel: megaPhoneTeamsContacts }); - setNocAlerts({ emailContacts: nocEmailContacts }); - setMinSeverityLevelForNocAlert(minSeverityLevelForNocAlert); }, [integrationDetails]); // JSX for General Tab return ( -
- {integrationDetails?.appSpecificIntegrationMetaData?.severity3Alerts?.active ? ( - <> - - - - Severity 3 alerts ACTIVE - - - }> - {severity3AlertRecipients?.emailContacts.map((e, i) => { - return ( - - {e} - - ); - })} - - - ) : ( - - - - - Severity 3 alerts INACTIVE - - - - )} -
-
{integrationDetails?.appSpecificIntegrationMetaData?.megaPhoneAlerts?.active ? ( <> @@ -112,47 +67,6 @@ function GeneralTab({ integrationDetails, teamsChannels }) { )}
- -
- {integrationDetails?.appSpecificIntegrationMetaData?.nocAlerts?.active ? ( - <> - - - - NOC alerts ACTIVE for severity{' '} - {minSeverityLevelForNocAlert < 3 ? ( - - {minSeverityLevelForNocAlert} and above - - ) : ( - '' - )} - - - }> - {nocAlerts.emailContacts.map((e, i) => { - return ( - - {e} - - ); - })} - - - ) : ( - - - - - NOC alerts INACTIVE - - - - )} -
); } diff --git a/Tombolo/server/migrations/20240229155642-create-asr_domains-table.js b/Tombolo/server/migrations/20240229155642-create-asr_domains-table.js index 42ba5975..e4dc3558 100644 --- a/Tombolo/server/migrations/20240229155642-create-asr_domains-table.js +++ b/Tombolo/server/migrations/20240229155642-create-asr_domains-table.js @@ -63,4 +63,4 @@ module.exports = { down: async (queryInterface, Sequelize) => { await queryInterface.dropTable("asr_domains"); }, -}; \ No newline at end of file +}; diff --git a/Tombolo/server/models/asr_domains.js b/Tombolo/server/models/asr_domains.js index 9876d2e2..7363878b 100644 --- a/Tombolo/server/models/asr_domains.js +++ b/Tombolo/server/models/asr_domains.js @@ -26,7 +26,7 @@ module.exports = (sequelize, DataTypes) => { allowNull: false, type: DataTypes.JSON, }, - metaData:{ + metaData: { allowNull: true, type: DataTypes.JSON, }, @@ -74,7 +74,7 @@ module.exports = (sequelize, DataTypes) => { foreignKey: "domain_id", as: "associatedProducts", }); - } + }; return AsrDomains; }; diff --git a/Tombolo/server/routes/asr/read.js b/Tombolo/server/routes/asr/read.js index 64ba2a9c..520ba28d 100644 --- a/Tombolo/server/routes/asr/read.js +++ b/Tombolo/server/routes/asr/read.js @@ -28,7 +28,9 @@ router.post( body("severityThreshold") .isInt() .withMessage("Severity threshold is required and must be an integer"), - body("severityAlertRecipients").isArray().withMessage("Severity alert recipients must be an array"), + body("severityAlertRecipients") + .isArray() + .withMessage("Severity alert recipients must be an array"), ], async (req, res) => { try { @@ -41,11 +43,23 @@ router.post( /* if monitoring type is provided, create domain, next iterate over monitoringTypeId and make entry to asr_domain_monitoring_types*/ - const { name, region, severityThreshold, severityAlertRecipients, monitoringTypeIds, createdBy } = - req.body; + const { + name, + region, + severityThreshold, + severityAlertRecipients, + monitoringTypeIds, + createdBy, + } = req.body; let domain; if (monitoringTypeIds) { - domain = await Domains.create({ name, region, severityThreshold, severityAlertRecipients, createdBy }); + domain = await Domains.create({ + name, + region, + severityThreshold, + severityAlertRecipients, + createdBy, + }); // create domain monitoring type mapping const createPromises = monitoringTypeIds.map((monitoringId) => { @@ -61,7 +75,13 @@ router.post( // if no monitoring type is provided, create domain without monitoring type else { - domain = await Domains.create({ name, region, severityThreshold,severityAlertRecipients, createdBy }); + domain = await Domains.create({ + name, + region, + severityThreshold, + severityAlertRecipients, + createdBy, + }); } res.status(200).json({ message: "Domain created successfully", domain }); } catch (error) { @@ -72,53 +92,56 @@ router.post( ); //Get All domains and associated monitoring types -router.get("/domains/", async(req, res) => { - try{ - // get all domains and the associated monitoring types by using includes - const domains = await Domains.findAll({ - include: [ - { - model: MonitoringTypes, - through: { - attributes: [], // Exclude the junction table from the result - }, - as: "monitoringTypes", // Alias you used when defining the association - attributes: ["id", "name"], - }, - ], - raw: true, - }); - - res.status(200).json(domains); - }catch(err){ - logger.error(err); - res.status(500).json({message: 'Failed to fetch domains'}); - } +router.get("/domains/", async (req, res) => { + try { + // get all domains and the associated monitoring types by using includes + const domains = await Domains.findAll({ + include: [ + { + model: MonitoringTypes, + through: { + attributes: [], // Exclude the junction table from the result + }, + as: "monitoringTypes", // Alias you used when defining the association + attributes: ["id", "name"], + }, + ], + raw: true, + }); + + res.status(200).json(domains); + } catch (err) { + logger.error(err); + res.status(500).json({ message: "Failed to fetch domains" }); + } }); -router.get("/domainsOnly/", async(req, res) => { - try{ +router.get("/domainsOnly/", async (req, res) => { + try { // get all domains only const domains = await Domains.findAll({ raw: true, }); res.status(200).json(domains); - }catch(err){ + } catch (err) { logger.error(err); - res.status(500).json({message: 'Failed to fetch domains'}); + res.status(500).json({ message: "Failed to fetch domains" }); } }); - // Update a domain router.patch( "/domains/:id", [ param("id").isUUID().withMessage("ID must be a UUID"), body("name").notEmpty().withMessage("Domain name is required"), - body("severityThreshold").isInt().withMessage("Severity threshold is required and must be an integer"), - body("severityAlertRecipients").isArray().withMessage("Severity alert recipients must be an array"), + body("severityThreshold") + .isInt() + .withMessage("Severity threshold is required and must be an integer"), + body("severityAlertRecipients") + .isArray() + .withMessage("Severity alert recipients must be an array"), body("monitoringTypeIds") .optional() .isArray() @@ -139,7 +162,7 @@ router.patch( name, region, severityThreshold, - severityAlertRecipients, + severityAlertRecipients, monitoringTypeIds, updatedBy, } = req.body; @@ -179,12 +202,19 @@ router.patch( }); } else { response = await Domains.update( - { name, region, severityThreshold, severityAlertRecipients, updatedBy }, + { + name, + region, + severityThreshold, + severityAlertRecipients, + updatedBy, + }, { where: { id: req.params.id } } ); } - const message = response[0] === 0 ? "Domain not found" : "Successfully updated domain"; + const message = + response[0] === 0 ? "Domain not found" : "Successfully updated domain"; res.status(200).json({ message }); } catch (err) { logger.error(err); @@ -194,12 +224,11 @@ router.patch( ); // Delete a domain - this should also delete monitoring types to domain mapping -router.delete("/domains/:id", -[ - param("id").isUUID().withMessage("ID must be a UUID") -], - async(req, res) => { - try{ +router.delete( + "/domains/:id", + [param("id").isUUID().withMessage("ID must be a UUID")], + async (req, res) => { + try { //Validate const errors = validationResult(req); if (!errors.isEmpty()) { @@ -211,84 +240,100 @@ router.delete("/domains/:id", const message = response === 0 ? "Domain not found" : "Domain deleted successfully"; res.status(200).json({ message }); - }catch(err){ - logger.error(err); - res.status(500).json({message: 'Failed to delete domain'}); + } catch (err) { + logger.error(err); + res.status(500).json({ message: "Failed to delete domain" }); } -}); + } +); // ----------------------------------- Products ------------------------------------- //Create a new product -router.post("/products/", -[ +router.post( + "/products/", + [ body("name").notEmpty().withMessage("Product name is required"), body("shortCode").notEmpty().withMessage("Short code is required"), body("tier").notEmpty().withMessage("Tier is required"), body("createdBy").notEmpty().withMessage("Created by is required"), - body("domainIds").optional().isArray().withMessage("Domain ID must be an array of UUIDs"), -], -async(req, res) => { - try{ - // Validate the request - const errors = validationResult(req); - if(!errors.isEmpty()){ - logger.error(errors.array()); - return res.status(400).json({message: "Failed to save product"}); - } - - // If domainId is provided, create product domain relationship also - const {name, shortCode, tier, createdBy, domainIds} = req.body; - - let product; - if(domainIds){ - product = await Products.create({name, shortCode, tier, createdBy}); - - //Create product domain mapping - const createPromises = domainIds.map((domainId) => { - return DomainProduct.create({product_id: product.id, domain_id: domainId, createdBy}); - }); - await Promise.all(createPromises); - }else{ - product = await Products.create({name, shortCode, tier, createdBy}); - } - res.status(200).json({message: "Product created successfully", product}); - - }catch(error){ - console.log(error) - logger.error(error); - res.status(500).json({message: 'Failed to create product'}); + body("domainIds") + .optional() + .isArray() + .withMessage("Domain ID must be an array of UUIDs"), + ], + async (req, res) => { + try { + // Validate the request + const errors = validationResult(req); + if (!errors.isEmpty()) { + logger.error(errors.array()); + return res.status(400).json({ message: "Failed to save product" }); + } + + // If domainId is provided, create product domain relationship also + const { name, shortCode, tier, createdBy, domainIds } = req.body; + + let product; + if (domainIds) { + product = await Products.create({ name, shortCode, tier, createdBy }); + + //Create product domain mapping + const createPromises = domainIds.map((domainId) => { + return DomainProduct.create({ + product_id: product.id, + domain_id: domainId, + createdBy, + }); + }); + await Promise.all(createPromises); + } else { + product = await Products.create({ name, shortCode, tier, createdBy }); + } + res + .status(200) + .json({ message: "Product created successfully", product }); + } catch (error) { + console.log(error); + logger.error(error); + res.status(500).json({ message: "Failed to create product" }); } -} + } ); // Get all products and related domains -router.get("/products/", async(req, res) => { - try { - // get all products and the associated domains - const products = await Products.findAll({ - include: [ - { - model: Domains, - through: { - attributes: [], // Exclude the junction table from the result - }, - as: "associatedDomains", - attributes: ["id", "name", "region", "severityThreshold", "severityAlertRecipients"], - }, - ], - order: [["createdAt", "DESC"]], - raw: true, - }); - - res.status(200).json(products); - } catch (err) { - logger.error(err); - res.status(500).json({ message: "Failed to fetch domains" }); - } +router.get("/products/", async (req, res) => { + try { + // get all products and the associated domains + const products = await Products.findAll({ + include: [ + { + model: Domains, + through: { + attributes: [], // Exclude the junction table from the result + }, + as: "associatedDomains", + attributes: [ + "id", + "name", + "region", + "severityThreshold", + "severityAlertRecipients", + ], + }, + ], + order: [["createdAt", "DESC"]], + raw: true, + }); + + res.status(200).json(products); + } catch (err) { + logger.error(err); + res.status(500).json({ message: "Failed to fetch domains" }); + } }); // Get all products only -router.get("/productsOnly/", async(req, res) => { +router.get("/productsOnly/", async (req, res) => { try { // get all products only const products = await Products.findAll({ @@ -382,82 +427,81 @@ router.put( ); // Delete a product -router.delete("/products/:id", -[ - param("id").isUUID().withMessage("ID must be a UUID") -], async(req, res) => { - try{ +router.delete( + "/products/:id", + [param("id").isUUID().withMessage("ID must be a UUID")], + async (req, res) => { + try { //Validate const errors = validationResult(req); - if(!errors.isEmpty()){ - logger.error(errors.array()); - return res.status(400).json({message: "Failed to delete product"}); + if (!errors.isEmpty()) { + logger.error(errors.array()); + return res.status(400).json({ message: "Failed to delete product" }); } - const response = await Products.destroy({where: {id: req.params.id}}); + const response = await Products.destroy({ where: { id: req.params.id } }); - const message = response === 0 ? "Product not found" : "Product deleted successfully"; - res.status(200).json({message}); - }catch(err){ - logger.error(err); - res.status(500).json({message: 'Failed to delete product'}); + const message = + response === 0 ? "Product not found" : "Product deleted successfully"; + res.status(200).json({ message }); + } catch (err) { + logger.error(err); + res.status(500).json({ message: "Failed to delete product" }); } -}); + } +); // ------------------------------------------------------------------------------------------------ // Get all domains for specific monitoring (activity) type -router.get("/domainsForSpecificMonitoring/:monitoringTypeId", -[param("monitoringTypeId").isString().isLength({min: 1})], - async(req, res) => { - try{ - //Validate request - const errors = validationResult(req); - if(!errors.isEmpty()){ - return res.status(400).send("Invalid monitoringTypeId"); - } +router.get( + "/domainsForSpecificMonitoring/:monitoringTypeId", + [param("monitoringTypeId").isString().isLength({ min: 1 })], + async (req, res) => { + try { + //Validate request + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).send("Invalid monitoringTypeId"); + } - const monitoringTypeId = req.params.monitoringTypeId; - - // Make call to db and get all domains for the activity type - const domains = await DomainMonitoringTypes.findAll({ - where: { monitoring_type_id: monitoringTypeId }, - include: [ - { - model: Domains, - attributes: [ - "id", - "name", - "region", - "severityThreshold", - "severityAlertRecipients", - ], - }, - ], - raw: true, - }); + const monitoringTypeId = req.params.monitoringTypeId; - // Remove junction table attributes and rename the domain object keys - const response = domains.map((domain) => { - return {id : domain["asr_domain.id"], - name : domain["asr_domain.name"], - } - }); + // Make call to db and get all domains for the activity type + const domains = await DomainMonitoringTypes.findAll({ + where: { monitoring_type_id: monitoringTypeId }, + include: [ + { + model: Domains, + attributes: [ + "id", + "name", + "region", + "severityThreshold", + "severityAlertRecipients", + ], + }, + ], + raw: true, + }); - res.status(200).json(response); - }catch(error){ - logger.error(error) - res.status(500).send("Unable to fetch domains"); - } -}); + // Remove junction table attributes and rename the domain object keys + const response = domains.map((domain) => { + return { id: domain["asr_domain.id"], name: domain["asr_domain.name"] }; + }); + res.status(200).json(response); + } catch (error) { + logger.error(error); + res.status(500).send("Unable to fetch domains"); + } + } +); -// Route to get product category for specific domain +// Route to get product category for specific domain router.get( "/productCategoriesForSpecificDomain/:domainId", - [ - param("domainId").isUUID().withMessage("Domain ID must be a UUID"), - ], + [param("domainId").isUUID().withMessage("Domain ID must be a UUID")], async (req, res) => { try { //Validate request @@ -499,4 +543,3 @@ router.get( ); module.exports = router; -