From 553bf165f2753aca662155653ec528934e9bbf82 Mon Sep 17 00:00:00 2001 From: yadhap Dahal Date: Wed, 17 Jan 2024 17:11:17 -0500 Subject: [PATCH 1/8] temp commit --- server/jobs/submitJobMonitoring.js | 606 +++++++++--------- ...230228142810-create-jobMonitoring-table.js | 81 ++- server/models/jobMonitoring.js | 87 ++- server/routes/jobmonitoring/read.js | 457 ++++--------- server/tempFiles/Tombolo-Notifications.JSON | 1 - 5 files changed, 573 insertions(+), 659 deletions(-) delete mode 100644 server/tempFiles/Tombolo-Notifications.JSON diff --git a/server/jobs/submitJobMonitoring.js b/server/jobs/submitJobMonitoring.js index caa650a83..440dcb9d6 100644 --- a/server/jobs/submitJobMonitoring.js +++ b/server/jobs/submitJobMonitoring.js @@ -1,304 +1,304 @@ -const { parentPort, workerData } = require("worker_threads"); -const { v4: uuidv4 } = require("uuid"); -const axios = require("axios"); - -const hpccUtil = require("../utils/hpcc-util"); -const logger = require("../config/logger"); -const models = require("../models"); -const JobMonitoring = models.jobMonitoring; -const cluster = models.cluster; -const Monitoring_notifications = models.monitoring_notifications; -const { notify } = require("../routes/notifications/email-notification"); -const {jobMonitoringEmailBody,msTeamsCardBody} = require("./jobMonitoringNotificationTemplates"); - -(async () => { - try { - // 1. Get job monitoring_id from worker - const { - job: { - worker: { jobMonitoring_id }, - }, - } = workerData; - - // 2. Get job monitoring details - const { - metaData, - cluster_id, - application_id, - name: monitoringName, - } = await JobMonitoring.findOne({ - where: { id: jobMonitoring_id }, - }); - - // Destructure job monitoring metaData - const { - notificationConditions, - notifications, - jobName, - costLimits, - last_monitored, - unfinishedWorkUnits, - thresholdTime, - } = metaData; - - //3. Get cluster offset & time stamp etc - const { timezone_offset, name: cluster_name } = await cluster.findOne({ - where: { id: cluster_id }, - raw: true, - }); - const now = new Date(); - const utcTime = now.getTime(); - const remoteUtcTime = new Date(utcTime + timezone_offset * 60000); - const timeStamp = remoteUtcTime.toISOString(); - - //Readable timestamp - const ts = new Date(timeStamp); - const readableTimestamp = ts.toLocaleString("en-US", { - dateStyle: "short", - timeStyle: "short", - }); - - // Check if a notification condition is met - const checkIfNotificationConditionMet = (notificationConditions, wu) => { - const metConditions = { - name: wu.Jobname, - wuId: wu.Wuid, - State: wu.State, - clusterName: cluster_name, - issues: [], - }; - - if ( - notificationConditions.includes("aborted") && - wu.State === "aborted" - ) { - metConditions.issues.push({ Alert: "Work unit Aborted" }); - } - - if (notificationConditions.includes("failed") && wu.State === "failed") { - metConditions.issues.push({ Alert: "Work unit Failed" }); - } - - if ( - notificationConditions.includes("unknown") && - wu.State === "unknown" - ) { - metConditions.issues.push({ Alert: "Work state Unknown" }); - } - - if ( - notificationConditions.includes("thresholdTimeExceeded") && - thresholdTime < wu.TotalClusterTime - ) { - } - - if ( - notificationConditions.includes("maxExecutionCost") && - wu.ExecuteCost > costLimits.maxExecutionCost - ) { - } - - if ( - notificationConditions.includes("maxFileAccessCost") && - wu.FileAccessCost > costLimits.maxFileAccessCost - ) { - } - - if ( - notificationConditions.includes("maxCompileCost") && - wu.CompileCost > costLimits.maxCompileCost - ) { - } - - if ( - notificationConditions.includes("maxTotalCost") && - wu.FileAccessCost + wu.CompileCost + wu.FileAccessCost > - costLimits.maxCompileCost - ) { - } - - if (metConditions.issues.length > 0) { - return metConditions; - } else { - return null; - } - }; - - //4. Get all workUnits with a given name that were executed since last time this monitoring ran - const wuService = await hpccUtil.getWorkunitsService(cluster_id); - const { - Workunits: { ECLWorkunit }, - } = await wuService.WUQuery({ - Jobname: jobName, - StartDate: last_monitored, - }); - - // 5. Iterate through all the work units, If they are in wait, blocked or running status separate them. - let newUnfinishedWorkUnits = [...unfinishedWorkUnits]; - const notificationsToSend = []; - ECLWorkunit.forEach((wu) => { - if ( - wu.State === "wait" || - wu.State === "running" || - wu.State === "blocked" - ) { - newUnfinishedWorkUnits.push(wu.Wuid ); - } - const conditionMetWus = checkIfNotificationConditionMet( - notificationConditions, - wu - ); - - if (conditionMetWus) { - notificationsToSend.push(conditionMetWus); - } +// const { parentPort, workerData } = require("worker_threads"); +// const { v4: uuidv4 } = require("uuid"); +// const axios = require("axios"); + +// const hpccUtil = require("../utils/hpcc-util"); +// const logger = require("../config/logger"); +// const models = require("../models"); +// const JobMonitoring = models.jobMonitoring; +// const cluster = models.cluster; +// const Monitoring_notifications = models.monitoring_notifications; +// const { notify } = require("../routes/notifications/email-notification"); +// const {jobMonitoringEmailBody,msTeamsCardBody} = require("./jobMonitoringNotificationTemplates"); + +// (async () => { +// try { +// // 1. Get job monitoring_id from worker +// const { +// job: { +// worker: { jobMonitoring_id }, +// }, +// } = workerData; + +// // 2. Get job monitoring details +// const { +// metaData, +// cluster_id, +// application_id, +// name: monitoringName, +// } = await JobMonitoring.findOne({ +// where: { id: jobMonitoring_id }, +// }); + +// // Destructure job monitoring metaData +// const { +// notificationConditions, +// notifications, +// jobName, +// costLimits, +// last_monitored, +// unfinishedWorkUnits, +// thresholdTime, +// } = metaData; + +// //3. Get cluster offset & time stamp etc +// const { timezone_offset, name: cluster_name } = await cluster.findOne({ +// where: { id: cluster_id }, +// raw: true, +// }); +// const now = new Date(); +// const utcTime = now.getTime(); +// const remoteUtcTime = new Date(utcTime + timezone_offset * 60000); +// const timeStamp = remoteUtcTime.toISOString(); + +// //Readable timestamp +// const ts = new Date(timeStamp); +// const readableTimestamp = ts.toLocaleString("en-US", { +// dateStyle: "short", +// timeStyle: "short", +// }); + +// // Check if a notification condition is met +// const checkIfNotificationConditionMet = (notificationConditions, wu) => { +// const metConditions = { +// name: wu.Jobname, +// wuId: wu.Wuid, +// State: wu.State, +// clusterName: cluster_name, +// issues: [], +// }; + +// if ( +// notificationConditions.includes("aborted") && +// wu.State === "aborted" +// ) { +// metConditions.issues.push({ Alert: "Work unit Aborted" }); +// } + +// if (notificationConditions.includes("failed") && wu.State === "failed") { +// metConditions.issues.push({ Alert: "Work unit Failed" }); +// } + +// if ( +// notificationConditions.includes("unknown") && +// wu.State === "unknown" +// ) { +// metConditions.issues.push({ Alert: "Work state Unknown" }); +// } + +// if ( +// notificationConditions.includes("thresholdTimeExceeded") && +// thresholdTime < wu.TotalClusterTime +// ) { +// } + +// if ( +// notificationConditions.includes("maxExecutionCost") && +// wu.ExecuteCost > costLimits.maxExecutionCost +// ) { +// } + +// if ( +// notificationConditions.includes("maxFileAccessCost") && +// wu.FileAccessCost > costLimits.maxFileAccessCost +// ) { +// } + +// if ( +// notificationConditions.includes("maxCompileCost") && +// wu.CompileCost > costLimits.maxCompileCost +// ) { +// } + +// if ( +// notificationConditions.includes("maxTotalCost") && +// wu.FileAccessCost + wu.CompileCost + wu.FileAccessCost > +// costLimits.maxCompileCost +// ) { +// } + +// if (metConditions.issues.length > 0) { +// return metConditions; +// } else { +// return null; +// } +// }; + +// //4. Get all workUnits with a given name that were executed since last time this monitoring ran +// const wuService = await hpccUtil.getWorkunitsService(cluster_id); +// const { +// Workunits: { ECLWorkunit }, +// } = await wuService.WUQuery({ +// Jobname: jobName, +// StartDate: last_monitored, +// }); + +// // 5. Iterate through all the work units, If they are in wait, blocked or running status separate them. +// let newUnfinishedWorkUnits = [...unfinishedWorkUnits]; +// const notificationsToSend = []; +// ECLWorkunit.forEach((wu) => { +// if ( +// wu.State === "wait" || +// wu.State === "running" || +// wu.State === "blocked" +// ) { +// newUnfinishedWorkUnits.push(wu.Wuid ); +// } +// const conditionMetWus = checkIfNotificationConditionMet( +// notificationConditions, +// wu +// ); + +// if (conditionMetWus) { +// notificationsToSend.push(conditionMetWus); +// } - }); - - // Iterate over jobs that are/were unfinished - check if status has changed - const wuToStopMonitoring = []; - for (let wuid of unfinishedWorkUnits) { - const { - Workunits: { ECLWorkunit }, - } = await wuService.WUQuery({ - Wuid: wuid, - }); - const conditionMet = checkIfNotificationConditionMet( - notificationConditions, - ECLWorkunit[0] - ); - - if (conditionMet) { - notificationsToSend.push(conditionMet); - } - - // Stop monitoring if wu reached end of life cycle - if ( - ECLWorkunit[0].State != "wait" && - ECLWorkunit[0].State != "running" && - ECLWorkunit[0].State != "blocked" - ) { - wuToStopMonitoring.push(wuid); - } - } - - newUnfinishedWorkUnits = newUnfinishedWorkUnits.filter((wu) => - !wuToStopMonitoring.includes(wu) - ); - - // If conditions met send out notifications - const notificationDetails = {}; // channel and recipients {eMail: ['abc@d.com']} - notifications.forEach((notification) => { - notificationDetails[notification.channel] = notification.recipients; - }); - - const sentNotifications = []; - - // E-mail - if (notificationDetails.eMail && notificationsToSend.length > 0) { - try { - const notification_id = uuidv4(); - const emailBody = jobMonitoringEmailBody({ - notificationsToSend, - jobName, - monitoringName, - timeStamp: readableTimestamp, - }); - const response = await notify({ - to: notificationDetails.eMail, - from: process.env.EMAIL_SENDER, - subject: `Job Monitoring Alert`, - text: emailBody, - html: emailBody, - }); - - // If notification sent successfully - if (response.accepted && response.accepted.length > 0) { - sentNotifications.push({ - id: notification_id, - application_id: application_id, - monitoring_type: "jobMonitoring", - monitoring_id: jobMonitoring_id, - notification_reason: "Met notification conditions", - status: "notified", - notification_channel: "eMail", - metaData: { notificationBody: emailBody }, - }); - } - } catch (err) { - logger.error(err); - } - } - - if (notificationDetails.msTeams && notificationsToSend.length > 0) { - const recipients = notificationDetails.msTeams; - const cardBody = jobMonitoringEmailBody({ - notificationsToSend, - jobName, - monitoringName, - timeStamp: readableTimestamp, - }); - - for (let recipient of recipients) { - try { - const notification_id = uuidv4(); - - const response = await axios.post( - recipient, - msTeamsCardBody(cardBody) - ); - - - if (response.status === 200) { - sentNotifications.push({ - id: notification_id, - application_id: application_id, - monitoring_type: "jobMonitoring", - monitoring_id: jobMonitoring_id, - notification_reason: "Met notification conditions", - status: "notified", - notification_channel: "msTeams", - metaData: { notificationBody: cardBody }, - }); - } - } catch (err) { - logger.error(err); - } - } - } - - // Update job monitoring metadata - await JobMonitoring.update( - { - metaData: { - ...metaData, - // last_monitored: "2023-06-28T09:06:01.857Z", - last_monitored: timeStamp, - unfinishedWorkUnits: newUnfinishedWorkUnits, - }, - }, - { where: { id: jobMonitoring_id } } - ); - - // Insert notification into notification table - if (sentNotifications.length > 0) { - for (let notification of sentNotifications) { - try { - await Monitoring_notifications.create(notification); - } catch (err) { - logger.error(err); - } - } - } - // ---------------------------------------------------------------- - } catch (err) { - logger.error(err); - } finally { - if (parentPort) parentPort.postMessage("done"); - else process.exit(0); - } -})(); - - - -// 5. way to check if cluster is K8 or not should be changed and teams notification \ No newline at end of file +// }); + +// // Iterate over jobs that are/were unfinished - check if status has changed +// const wuToStopMonitoring = []; +// for (let wuid of unfinishedWorkUnits) { +// const { +// Workunits: { ECLWorkunit }, +// } = await wuService.WUQuery({ +// Wuid: wuid, +// }); +// const conditionMet = checkIfNotificationConditionMet( +// notificationConditions, +// ECLWorkunit[0] +// ); + +// if (conditionMet) { +// notificationsToSend.push(conditionMet); +// } + +// // Stop monitoring if wu reached end of life cycle +// if ( +// ECLWorkunit[0].State != "wait" && +// ECLWorkunit[0].State != "running" && +// ECLWorkunit[0].State != "blocked" +// ) { +// wuToStopMonitoring.push(wuid); +// } +// } + +// newUnfinishedWorkUnits = newUnfinishedWorkUnits.filter((wu) => +// !wuToStopMonitoring.includes(wu) +// ); + +// // If conditions met send out notifications +// const notificationDetails = {}; // channel and recipients {eMail: ['abc@d.com']} +// notifications.forEach((notification) => { +// notificationDetails[notification.channel] = notification.recipients; +// }); + +// const sentNotifications = []; + +// // E-mail +// if (notificationDetails.eMail && notificationsToSend.length > 0) { +// try { +// const notification_id = uuidv4(); +// const emailBody = jobMonitoringEmailBody({ +// notificationsToSend, +// jobName, +// monitoringName, +// timeStamp: readableTimestamp, +// }); +// const response = await notify({ +// to: notificationDetails.eMail, +// from: process.env.EMAIL_SENDER, +// subject: `Job Monitoring Alert`, +// text: emailBody, +// html: emailBody, +// }); + +// // If notification sent successfully +// if (response.accepted && response.accepted.length > 0) { +// sentNotifications.push({ +// id: notification_id, +// application_id: application_id, +// monitoring_type: "jobMonitoring", +// monitoring_id: jobMonitoring_id, +// notification_reason: "Met notification conditions", +// status: "notified", +// notification_channel: "eMail", +// metaData: { notificationBody: emailBody }, +// }); +// } +// } catch (err) { +// logger.error(err); +// } +// } + +// if (notificationDetails.msTeams && notificationsToSend.length > 0) { +// const recipients = notificationDetails.msTeams; +// const cardBody = jobMonitoringEmailBody({ +// notificationsToSend, +// jobName, +// monitoringName, +// timeStamp: readableTimestamp, +// }); + +// for (let recipient of recipients) { +// try { +// const notification_id = uuidv4(); + +// const response = await axios.post( +// recipient, +// msTeamsCardBody(cardBody) +// ); + + +// if (response.status === 200) { +// sentNotifications.push({ +// id: notification_id, +// application_id: application_id, +// monitoring_type: "jobMonitoring", +// monitoring_id: jobMonitoring_id, +// notification_reason: "Met notification conditions", +// status: "notified", +// notification_channel: "msTeams", +// metaData: { notificationBody: cardBody }, +// }); +// } +// } catch (err) { +// logger.error(err); +// } +// } +// } + +// // Update job monitoring metadata +// await JobMonitoring.update( +// { +// metaData: { +// ...metaData, +// // last_monitored: "2023-06-28T09:06:01.857Z", +// last_monitored: timeStamp, +// unfinishedWorkUnits: newUnfinishedWorkUnits, +// }, +// }, +// { where: { id: jobMonitoring_id } } +// ); + +// // Insert notification into notification table +// if (sentNotifications.length > 0) { +// for (let notification of sentNotifications) { +// try { +// await Monitoring_notifications.create(notification); +// } catch (err) { +// logger.error(err); +// } +// } +// } +// // ---------------------------------------------------------------- +// } catch (err) { +// logger.error(err); +// } finally { +// if (parentPort) parentPort.postMessage("done"); +// else process.exit(0); +// } +// })(); + + + +// // 5. way to check if cluster is K8 or not should be changed and teams notification \ No newline at end of file diff --git a/server/migrations/20230228142810-create-jobMonitoring-table.js b/server/migrations/20230228142810-create-jobMonitoring-table.js index edf0c2ae0..450867269 100644 --- a/server/migrations/20230228142810-create-jobMonitoring-table.js +++ b/server/migrations/20230228142810-create-jobMonitoring-table.js @@ -1,47 +1,86 @@ "use strict"; module.exports = { - up: (queryInterface, Sequelize) => { - return queryInterface.createTable("jobMonitoring", { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable("jobMonitoring", { id: { allowNull: false, primaryKey: true, type: Sequelize.UUID, defaultValue: Sequelize.UUIDV4, }, - name: { + applicationId: { + type: Sequelize.UUID, allowNull: false, - type: Sequelize.DataTypes.STRING, + references: { + model: "application", + key: "id", + }, + onUpdate: "CASCADE", + onDelete: "CASCADE", }, - cron: { + monitoringName: { allowNull: false, - type: Sequelize.DataTypes.STRING, + type: Sequelize.STRING, + unique: true, }, isActive: { allowNull: false, - type: Sequelize.DataTypes.BOOLEAN, - defaultValue: false, + type: Sequelize.BOOLEAN, + }, + // isApproved: { + // allowNull: true, + // type: Sequelize.BOOLEAN, + // }, + + approvalStatus: { + allowNull: false, + type: Sequelize.ENUM("Approved", "Rejected", "Pending"), + }, + approvedBy: { + allowNull: true, + type: Sequelize.STRING, + }, + approvedAt: { + allowNull: true, + type: Sequelize.DATE, + }, + approverComment: { + allowNull: true, + type: Sequelize.STRING, + }, + description: { + allowNull: false, + type: Sequelize.TEXT, }, - cluster_id: { + monitoringScope: { + allowNull: false, + type: Sequelize.STRING, + }, + clusterId: { type: Sequelize.UUID, + allowNull: false, references: { model: "cluster", key: "id", }, onUpdate: "CASCADE", - onDelete: "CASCADE", + onDelete: "NO ACTION", }, - application_id: { - type: Sequelize.UUID, - references: { - model: "application", - key: "id", - }, - onUpdate: "CASCADE", - onDelete: "CASCADE", + jobName: { + allowNull: false, + type: Sequelize.STRING, }, metaData: { + allowNull: false, type: Sequelize.JSON, - allowNull: true, + }, + createdBy: { + allowNull: false, + type: Sequelize.STRING, + }, + lastUpdatedBy: { + allowNull: false, + type: Sequelize.STRING, }, createdAt: { allowNull: false, @@ -57,7 +96,7 @@ module.exports = { }, }); }, - down: (queryInterface, Sequelize) => { - return queryInterface.dropTable("jobMonitoring"); + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable("jobMonitoring"); }, }; diff --git a/server/models/jobMonitoring.js b/server/models/jobMonitoring.js index 7f1c5f557..4757c8424 100644 --- a/server/models/jobMonitoring.js +++ b/server/models/jobMonitoring.js @@ -4,50 +4,103 @@ module.exports = (sequelize, DataTypes) => { "jobMonitoring", { id: { + allowNull: false, primaryKey: true, type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, + }, + applicationId: { + type: DataTypes.UUID, allowNull: false, - autoIncrement: false, }, - name: { + monitoringName: { allowNull: false, type: DataTypes.STRING, unique: true, }, - cron: { + isActive: { + allowNull: false, + type: DataTypes.BOOLEAN, + }, + // isApproved: { + // allowNull: true, + // type: DataTypes.BOOLEAN, + // }, + approvalStatus:{ allowNull: false, + type: DataTypes.ENUM("Approved", "Rejected", "Pending"), + }, + approvedBy: { + allowNull: true, type: DataTypes.STRING, }, - application_id: { + approvedAt:{ + allowNull: true, + type: DataTypes.DATE, + }, + approverComment: { + allowNull: true, + type: DataTypes.STRING, + }, + description: { allowNull: false, - type: DataTypes.UUID, + type: DataTypes.TEXT, }, - cluster_id: { + monitoringScope: { + allowNull: false, + type: DataTypes.STRING, + }, + clusterId: { type: DataTypes.UUID, allowNull: false, }, + jobName: { + allowNull: false, + type: DataTypes.STRING, + }, metaData: { + allowNull: false, type: DataTypes.JSON, - allowNull: true, }, - isActive: { - type: DataTypes.BOOLEAN, + createdBy: { + allowNull: false, + type: DataTypes.STRING, + }, + lastUpdatedBy: { + allowNull: false, + type: DataTypes.STRING, + }, + createdAt: { + allowNull: false, + type: DataTypes.DATE, + }, + updatedAt: { + allowNull: false, + type: DataTypes.DATE, + }, + deletedAt: { allowNull: true, + type: DataTypes.DATE, }, }, - { paranoid: true, freezeTableName: true } + { + paranoid: true, + freezeTableName: true, + } ); + JobMonitoring.associate = function (models) { - // Define association here - JobMonitoring.belongsTo(models.cluster, { foreignKey: "cluster_id" }); JobMonitoring.belongsTo(models.application, { - foreignKey: "application_id", - }); - JobMonitoring.hasMany(models.monitoring_notifications, { - foreignKey: "application_id", + foreignKey: "applicationId", onDelete: "CASCADE", + onUpdate: "CASCADE", + }); + JobMonitoring.belongsTo(models.cluster, { + foreignKey: "clusterId", + onDelete: "NO ACTION", + onUpdate: "CASCADE", }); }; + return JobMonitoring; -}; +}; \ No newline at end of file diff --git a/server/routes/jobmonitoring/read.js b/server/routes/jobmonitoring/read.js index 7f8ce5777..1aa0e8596 100644 --- a/server/routes/jobmonitoring/read.js +++ b/server/routes/jobmonitoring/read.js @@ -1,371 +1,194 @@ -const models = require("../../models"); -const express = require("express"); +const express = require('express'); +const router = express.Router(); +const { body, check } = require("express-validator"); + +//Local imports const logger = require("../../config/logger"); -const validatorUtil = require("../../utils/validator"); -const { body, param, validationResult } = require("express-validator"); -const hpccJSComms = require("@hpcc-js/comms"); -const hpccUtil = require("../../utils/hpcc-util"); -const JobScheduler = require("../../job-scheduler"); +const models = require("../../models"); +const { validationResult } = require("express-validator"); +//Constants const JobMonitoring = models.jobMonitoring; -const router = express.Router(); -// Create Cluster Monitoring +// Create new job monitoring router.post( "/", [ - //Validation middleware - body("name").isString().withMessage("Invalid job monitoring name"), - body("application_id").isUUID(4).withMessage("Invalid application id"), - body("cluster_id").isUUID(4).withMessage("Invalid cluster id"), - body("cron").custom((value) => { - const valArray = value.split(" "); - if (valArray.length > 5) { - throw new Error( - `Expected number of cron parts 5, received ${valArray.length}` - ); - } else { - return Promise.resolve("Good to go"); - } - }), - // body("isActive").isBoolean().withMessage("Invalid is active flag"), - body("metaData").isObject().withMessage("Invalid job monitoring meta data"), + body("monitoringName") + .notEmpty() + .withMessage("Monitoring name is required"), + body("description").notEmpty().withMessage("Description is required"), + body("monitoringScope") + .notEmpty() + .withMessage("Monitoring scope is required"), + body("clusterId").isUUID().withMessage("Cluster ID must be a valid UUID"), + body("isActive").isBoolean().withMessage("isActive must be a boolean"), + body("jobName").notEmpty().withMessage("Job name is required"), + body("applicationId") + .isUUID() + .withMessage("Application ID must be a valid UUID"), + body("metaData") + .isObject() + .withMessage("Meta data must be an object if provided"), + body("createdBy").notEmpty().withMessage("Created by is required"), + body("lastUpdatedBy").notEmpty().withMessage("Last updated by is required"), ], async (req, res) => { + // Handle the POST request here try { - // Check for errors - return if exists - const errors = validationResult(req).formatWith( - validatorUtil.errorFormatter - ); - - if (!errors.isEmpty()) - return res.status(422).json({ success: false, errors: errors.array() }); - - //create - req.body.metaData.unfinishedWorkUnits = []; - const jobMonitoring = await JobMonitoring.create(req.body); - res.status(201).send(jobMonitoring); - - //Add job to bree- if start monitoring checked - if (req.body.isActive) { - const { id, cron } = jobMonitoring; - - JobScheduler.createJobMonitoringBreeJob({ - jobMonitoring_id: id, - cron, - }); + // Validate the req.body + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); } - } catch (err) { - logger.error(err); - res - .status(503) - .send({ success: false, message: "Failed to create job monitoring" }); - } - } -); - -// Get all cluster monitoring -router.get( - "/all/:application_id", - [param("application_id").isUUID().withMessage("Invalid application ID")], - async (req, res) => { - try { - //Check for errors - return if exists - const errors = validationResult(req).formatWith( - validatorUtil.errorFormatter - ); - - // return if error(s) exist - if (!errors.isEmpty()) - return res.status(422).json({ success: false, errors: errors.array() }); - const { application_id } = req.params; - const jobMonitorings = await JobMonitoring.findAll({ - where: { application_id }, - raw: true, - }); - - res.status(200).send(jobMonitorings); + //Save the job monitoring + const response = await JobMonitoring.create({...req.body, approvalStatus: 'Pending'}, { raw: true }); + res.status(200).send(response); } catch (err) { logger.error(err); - res - .status(503) - .send({ success: false, message: "Failed to fetch job monitorings" }); + res.status(500).send("Failed to save job monitoring"); } } ); -// Get one cluster monitoring -router.get( - "/:id", - [param("id").isUUID(4).withMessage("Invalid job monitoring ID")], - async (req, res) => { + // Get all Job monitorings + router.get("/", async (req, res) => { try { - //Check for errors - return if exists - const errors = validationResult(req).formatWith( - validatorUtil.errorFormatter - ); - - // return if error(s) exist - if (!errors.isEmpty()) - return res.status(422).json({ success: false, errors: errors.array() }); - - const { id } = req.params; - const jobMonitoring = await JobMonitoring.findOne({ - where: { id }, - raw: true, - }); - res.status(200).send(jobMonitoring); + const jobMonitorings = await JobMonitoring.findAll(); + res.status(200).json(jobMonitorings); } catch (err) { logger.error(err); - res - .status(503) - .send({ success: false, message: "Failed to fetch job monitoring" }); + res.status(500).send("Failed to get job monitorings"); } - } -); + }); -//Delete -router.delete( - "/:id", - [param("id").isUUID(4).withMessage("Invalid cluster monitoring ID")], - async (req, res) => { + // Get a single job monitoring + router.get("/:id", async (req, res) => { try { - //Check for errors - return if exists - const errors = validationResult(req).formatWith( - validatorUtil.errorFormatter - ); - - // return if error(s) exist - if (!errors.isEmpty()) - return res.status(422).json({ success: false, errors: errors.array() }); - - const { id } = req.params; - const deleted = await JobMonitoring.destroy({ - where: { id }, - }); - - res.status(200).send({ deleted }); + const jobMonitoring = await JobMonitoring.findByPk(req.params.id); + res.status(200).json(jobMonitoring); } catch (err) { logger.error(err); - res - .status(503) - .json({ success: false, message: "Failed to delete job monitoring" }); + res.status(500).send("Failed to get job monitoring"); } - } -); - -// Pause or start monitoring -router.put( - "/jobMonitoringStatus/:id", - [param("id").isUUID(4).withMessage("Invalid file monitoring Id")], - async (req, res, next) => { - try { - const errors = validationResult(req).formatWith( - validatorUtil.errorFormatter - ); - if (!errors.isEmpty()) - return res.status(422).json({ success: false, errors: errors.array() }); - const { id } = req.params; - const monitoring = await JobMonitoring.findOne({ - where: { id }, - raw: true, - }); - const { cron, isActive } = monitoring; + }); + + // Patch a single job monitoring with express validator + router.patch( "/", + [ + body("id").isUUID().withMessage("ID must be a valid UUID"), + body("monitoringName") + .notEmpty() + .withMessage("Monitoring name is required"), + body("description").notEmpty().withMessage("Description is required"), + body("monitoringScope") + .notEmpty() + .withMessage("Monitoring scope is required"), + body("clusterId").isUUID().withMessage("Cluster ID must be a valid UUID"), + body("isActive").isBoolean().withMessage("isActive must be a boolean"), + body("jobName").notEmpty().withMessage("Job name is required"), + body("applicationId") + .isUUID() + .withMessage("Application ID must be a valid UUID"), + body("metaData") + .isObject() + .withMessage("Meta data must be an object if provided"), + body("createdBy").notEmpty().withMessage("Created by is required"), + body("lastUpdatedBy").notEmpty().withMessage("Last updated by is required"), + ], + async (req, res) => { + try { + // Validate the req.body + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } - // flipping isActive - await JobMonitoring.update( - { isActive: !isActive }, - { where: { id: id } } - ); + //Payload + const payload = req.body; + payload.approvalStatus = "Pending"; + payload.approverComment = null; + payload.approvedBy = null; + payload.approvedAt = null; - // If isActive, it is in bre - remove from bree - if (isActive) { - await JobScheduler.removeJobFromScheduler(`Job Monitoring - ${id}`); - } - - // If isActive = false, add it to bre - if (!isActive) { - JobScheduler.createJobMonitoringBreeJob({ - jobMonitoring_id: id, - cron, + //Update the job monitoring + await JobMonitoring.update(req.body, { + where: { id: req.body.id }, }); + res.status(200).send(payload); + } catch (err) { + logger.error(err); + res.status(500).send("Failed to update job monitoring"); } - - res.status(200).send("Update successful"); - } catch (err) { - logger.error(err); } - } -); - -// Update Monitoring -router.put( - "/", - [ - body("id").isUUID().withMessage("Invalid job monitoring ID"), - body("name").isString().withMessage("Invalid job monitoring name"), - body("application_id").isUUID(4).withMessage("Invalid application id"), - body("cluster_id").isUUID(4).withMessage("Invalid cluster id"), - body("cron").custom((value) => { - const valArray = value.split(" "); - if (valArray.length > 5) { - throw new Error( - `Expected number of cron parts 5, received ${valArray.length}` - ); - } else { - return Promise.resolve("Good to go"); - } - }), - // body("isActive").isBoolean().withMessage("Invalid is active flag"), - body("metaData").isObject().withMessage("Invalid job monitoring meta data"), - ], - async (req, res) => { - try { - //Check for errors - return if exists - const errors = validationResult(req).formatWith( - validatorUtil.errorFormatter - ); - - // return if error(s) exist + ); + + // Reject or approve monitoring + router.patch( + "/evaluate", + [ + // Add validation rules here + body("approverComment") + .notEmpty() + .isString() + .withMessage("Approval comment must be a string") + .isLength({ min: 4, max: 200 }) + .withMessage( + "Approval comment must be between 4 and 200 characters long" + ), + body("id").isUUID().withMessage("Invalid id"), + body("approvalStatus") + .notEmpty() + .isString() + .withMessage("Accepted must be a string"), + body("approvedBy").notEmpty().isString().withMessage("Approved by must be a string"), + ], + async (req, res) => { + const errors = validationResult(req); if (!errors.isEmpty()) { - logger.error(errors); - return res.status(422).json({ success: false, errors: errors.array() }); + return res.status(503).send("Failed save your evaluation"); } - //Existing cluster monitoring details - let { id, isActive, cron } = req.body; - - const existingMonitoringDetails = await JobMonitoring.findOne({ - where: { id }, - }); - const { - metaData: { last_monitored, unfinishedWorkUnits }, - } = existingMonitoringDetails; - - const newData = req.body; // Cleaning required - // Do not reset last_monitored value and jobs that are unfinished - newData.metaData.last_monitored = last_monitored; - newData.metaData.unfinishedWorkUnits = unfinishedWorkUnits; - - const updated = await JobMonitoring.update(newData, { - where: { id }, - }); - - //TODO - Add or delete from bree - if (updated == 1) { - const monitoringUniqueName = `Job Monitoring - ${id}`; - const breeJobs = JobScheduler.getAllJobs(); - const jobIndex = breeJobs.findIndex( - (job) => job.name === monitoringUniqueName + try { + const { id, approverComment, approvalStatus, approvedBy } = req.body; + await JobMonitoring.update( + { approvalStatus, approverComment, approvedBy, approvedAt: new Date() }, + { where: { id } } ); + res.status(200).send("Successfully saved your evaluation"); - //Add to bree - if (jobIndex > 0 && !isActive) { - JobScheduler.removeAllFromBree(monitoringUniqueName); - } - - // Remove from bree - if (jobIndex < 0 && isActive) { - JobScheduler.createJobMonitoringBreeJob({ - jobMonitoring_id: id, - cron, - }); - } + } catch (err) { + logger.error(err); + res.status(500).send("Failed to evaluate job monitoring"); } - - res.status(200).send({ updated }); - } catch (err) { - logger.error(err); - res.status(503).json({ success: false, message: "Failed to update" }); } - } -); + ); -// Get cluster monitoring engines -router.get( - "/clusterEngines/:cluster_id", - [param("cluster_id").isUUID().withMessage("Invalid cluster ID")], +//Delete a single job monitoring +router.delete( + "/:id", + [check("id", "Invalid id").isUUID()], async (req, res) => { - try { - //Check for errors - return if exists - const errors = validationResult(req).formatWith( - validatorUtil.errorFormatter - ); - - // return if error(s) exist - if (!errors.isEmpty()) - return res.status(422).json({ success: false, errors: errors.array() }); + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(503).send("Failed to delete"); + } - const { cluster_id } = req.params; - let cluster = await hpccUtil.getCluster(cluster_id); - const { thor_host, thor_port, username, hash } = cluster; - const clusterDetails = { - baseUrl: `${thor_host}:${thor_port}`, - userID: username || "", - password: hash || "", - }; - const topologyService = new hpccJSComms.TopologyService(clusterDetails); - const clusterEngines = await topologyService.TpListTargetClusters(); - res.status(200).send(clusterEngines); + try { + await JobMonitoring.destroy({ where: { id: req.params.id } }); + res.status(200).send("success"); } catch (err) { logger.error(err); - res - .status(503) - .json({ success: false, message: "Failed to get engines" }); + res.status(500).send("Failed to delete job monitoring"); } } ); -// Get target cluster usage -router.get( - "/targetClusterUsage/:cluster_id", - [ - param("cluster_id").isUUID().withMessage("Invalid cluster ID"), - body("engines").isArray().withMessage("Invalid engines"), - ], - async (req, res) => { - try { - //Check for errors - return if exists - const errors = validationResult(req).formatWith( - validatorUtil.errorFormatter - ); - // return if error(s) exist - if (!errors.isEmpty()) - return res.status(422).json({ success: false, errors: errors.array() }); - const { cluster_id } = req.params; - const { engines } = req.body; - let cluster = await hpccUtil.getCluster(cluster_id); - const { thor_host, thor_port, username, hash } = cluster; - const clusterDetails = { - baseUrl: `${thor_host}:${thor_port}`, - userID: username || "", - password: hash || "", - }; - const machineService = new hpccJSComms.MachineService(clusterDetails); - - const targetClusterUsage = await machineService.GetTargetClusterUsageEx( - engines - ); - const clusterUsage = []; - targetClusterUsage.forEach(function (details) { - clusterUsage.push({ - engine: details.Name, - size: details.max, - }); - }); - res.status(200).send(clusterUsage); - } catch (err) { - logger.error(err); - res - .status(503) - .json({ success: false, message: "Failed to get cluster usage" }); - } - } -); +// Export the router module.exports = router; diff --git a/server/tempFiles/Tombolo-Notifications.JSON b/server/tempFiles/Tombolo-Notifications.JSON deleted file mode 100644 index 0637a088a..000000000 --- a/server/tempFiles/Tombolo-Notifications.JSON +++ /dev/null @@ -1 +0,0 @@ -[] \ No newline at end of file From 724399c35381e15527ffb0779dcc0999180db40e Mon Sep 17 00:00:00 2001 From: yadhap Dahal Date: Thu, 18 Jan 2024 16:11:05 -0500 Subject: [PATCH 2/8] Job monitoring UI and API wrap up --- client-reactjs/package-lock.json | 14 + client-reactjs/package.json | 1 + .../notifications/MsTeams/VewHookDetails.jsx | 9 +- .../AddEditJobMonitoringModal.jsx | 189 +++++++ .../jobMonitoring/AddJobMonitoringBtn.jsx | 17 + .../jobMonitoring/ApproveRejectModal.jsx | 166 ++++++ .../AsrSpecificMonitoringDetails.jsx | 165 ++++++ .../AsrSpecificNotificationsDetails.jsx | 55 ++ .../jobMonitoring/JobMonitoringBasicTab.jsx | 241 ++++++--- .../JobMonitoringNotificationTab.jsx | 285 +++------- .../jobMonitoring/JobMonitoringTab.jsx | 186 +++---- .../jobMonitoring/JobMonitoringTable.jsx | 319 +++++------ .../jobMonitoring/MonitoringDetailsModal.jsx | 283 ++++++++++ .../jobMonitoring/SchedulePicker.jsx | 401 ++++++++++++++ .../application/jobMonitoring/index.jsx | 502 +++++++++--------- .../jobMonitoring/jobMonitoring.css | 96 ++++ .../jobMonitoring/jobMonitoringUtils.js | 115 ++++ .../src/components/common/InfoDrawer.jsx | 6 + .../src/components/common/scheduleOptions.js | 74 +++ .../userGuides/JobMonitoringScopeTypes.jsx | 32 ++ .../components/userGuides/JobNamePattern.jsx | 116 ++++ 21 files changed, 2423 insertions(+), 849 deletions(-) create mode 100644 client-reactjs/src/components/application/jobMonitoring/AddEditJobMonitoringModal.jsx create mode 100644 client-reactjs/src/components/application/jobMonitoring/AddJobMonitoringBtn.jsx create mode 100644 client-reactjs/src/components/application/jobMonitoring/ApproveRejectModal.jsx create mode 100644 client-reactjs/src/components/application/jobMonitoring/AsrSpecificMonitoringDetails.jsx create mode 100644 client-reactjs/src/components/application/jobMonitoring/AsrSpecificNotificationsDetails.jsx create mode 100644 client-reactjs/src/components/application/jobMonitoring/MonitoringDetailsModal.jsx create mode 100644 client-reactjs/src/components/application/jobMonitoring/SchedulePicker.jsx create mode 100644 client-reactjs/src/components/application/jobMonitoring/jobMonitoring.css create mode 100644 client-reactjs/src/components/application/jobMonitoring/jobMonitoringUtils.js create mode 100644 client-reactjs/src/components/common/scheduleOptions.js create mode 100644 client-reactjs/src/components/userGuides/JobMonitoringScopeTypes.jsx create mode 100644 client-reactjs/src/components/userGuides/JobNamePattern.jsx diff --git a/client-reactjs/package-lock.json b/client-reactjs/package-lock.json index f028f26bf..ac9fa5b75 100644 --- a/client-reactjs/package-lock.json +++ b/client-reactjs/package-lock.json @@ -40,6 +40,7 @@ "redux-thunk": "^2.3.0", "socket.io-client": "^4.1.3", "styled-components": "^5.3.0", + "validator": "^13.11.0", "yaml": "^2.2.2" }, "devDependencies": { @@ -23927,6 +23928,14 @@ "node": ">= 8" } }, + "node_modules/validator": { + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", + "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/value-equal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", @@ -43116,6 +43125,11 @@ } } }, + "validator": { + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", + "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==" + }, "value-equal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", diff --git a/client-reactjs/package.json b/client-reactjs/package.json index 0e8fb74f6..7ffd41fd8 100644 --- a/client-reactjs/package.json +++ b/client-reactjs/package.json @@ -35,6 +35,7 @@ "redux-thunk": "^2.3.0", "socket.io-client": "^4.1.3", "styled-components": "^5.3.0", + "validator": "^13.11.0", "yaml": "^2.2.2" }, "devDependencies": { diff --git a/client-reactjs/src/components/admin/notifications/MsTeams/VewHookDetails.jsx b/client-reactjs/src/components/admin/notifications/MsTeams/VewHookDetails.jsx index 6ba07dc82..ea5b5244f 100644 --- a/client-reactjs/src/components/admin/notifications/MsTeams/VewHookDetails.jsx +++ b/client-reactjs/src/components/admin/notifications/MsTeams/VewHookDetails.jsx @@ -1,18 +1,23 @@ import React from 'react'; -import { Modal, Descriptions } from 'antd'; +import { Modal, Descriptions, Tooltip } from 'antd'; import { Constants } from '../../../common/Constants'; function VewHookDetails({ showHooksDetailModal, setShowHooksDetailModal, selectedHook }) { return ( setShowHooksDetailModal(false)}> {selectedHook && ( {selectedHook.name} - {selectedHook.url} + + + {selectedHook.url.length > 80 ? selectedHook.url.substring(0, 80) + '...' : selectedHook.url} + + {selectedHook.createdBy} {selectedHook.approved ? 'Approved' : 'Not Approved'} diff --git a/client-reactjs/src/components/application/jobMonitoring/AddEditJobMonitoringModal.jsx b/client-reactjs/src/components/application/jobMonitoring/AddEditJobMonitoringModal.jsx new file mode 100644 index 000000000..4d393b92d --- /dev/null +++ b/client-reactjs/src/components/application/jobMonitoring/AddEditJobMonitoringModal.jsx @@ -0,0 +1,189 @@ +import React, { useState } from 'react'; +import { Modal, Tabs, Button, Badge } from 'antd'; +import { LoadingOutlined } from '@ant-design/icons'; +import { v4 as uuidv4 } from 'uuid'; + +import JobMonitoringBasicTab from './JobMonitoringBasicTab.jsx'; +import JobMonitoringTab from './JobMonitoringTab'; +import JobMonitoringNotificationTab from './JobMonitoringNotificationTab.jsx'; + +const AddEditJobMonitoringModal = ({ + displayAddJobMonitoringModal, + setDisplayAddJobMonitoringModal, + monitoringScope, + setMonitoringScope, + handleSaveJobMonitoring, + intermittentScheduling, + setIntermittentScheduling, + setCompleteSchedule, + completeSchedule, + cron, + setCron, + cronMessage, + setCronMessage, + erroneousScheduling, + form, + clusters, + teamsHooks, + setSelectedMonitoring, + savingJobMonitoring, + jobMonitorings, + setEditingData, + isEditing, + asrIntegration, + erroneousTabs, + setErroneousTabs, + setErroneousScheduling, +}) => { + const [activeTab, setActiveTab] = useState('0'); + // Keep track of visited tabs, some form fields are loaded only when tab is visited. This is to avoid validation errors + const [visitedTabs, setVisitedTabs] = useState(['0']); + + // Handle tab change + const handleTabChange = (key) => { + setActiveTab(key); + if (!visitedTabs.includes(key)) { + setVisitedTabs([...visitedTabs, key]); + } + }; + + //Tabs for modal + const tabs = [ + { + label: 'Basic', + component: () => ( + + ), + id: 1, + }, + { + label: 'Monitoring Details', + id: 2, + component: () => ( + + ), + }, + { + label: 'Notifications', + id: 3, + component: () => ( + + ), + }, + ]; + + // When next button is clicked, go to next tab + const handleNext = () => { + const nextTab = (parseInt(activeTab) + 1).toString(); + setActiveTab(nextTab); + setVisitedTabs([...visitedTabs, nextTab]); + }; + + // When previous button is clicked, go back to previous tab + const handlePrevious = () => { + const previousTab = (parseInt(activeTab) - 1).toString(); + setActiveTab(previousTab); + setVisitedTabs([...visitedTabs, previousTab]); + }; + + const handleCancel = () => { + setIntermittentScheduling({ schedulingType: 'daily', id: uuidv4() }); + setCompleteSchedule([]); + form.resetFields(); + setDisplayAddJobMonitoringModal(false); + setActiveTab('0'); + setVisitedTabs(['0']); + setSelectedMonitoring(null); + setEditingData({ isEditing: false }); + setErroneousTabs([]); + setErroneousScheduling(false); + }; + + //Render footer buttons based on active tab + const renderFooter = () => { + if (activeTab === '0') { + return ( + <> + + + ); + } else if (activeTab === '1') { + return ( + <> + + + + ); + } else { + return ( + <> + + + + ); + } + }; + + return ( + + handleTabChange(key)}> + {tabs.map((tab, index) => ( + + {`${tab.label}`} + + ) : ( + `${tab.label}` + ) + }> + {tab.component()} + + ))} + + + ); +}; + +export default AddEditJobMonitoringModal; diff --git a/client-reactjs/src/components/application/jobMonitoring/AddJobMonitoringBtn.jsx b/client-reactjs/src/components/application/jobMonitoring/AddJobMonitoringBtn.jsx new file mode 100644 index 000000000..68be93029 --- /dev/null +++ b/client-reactjs/src/components/application/jobMonitoring/AddJobMonitoringBtn.jsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { Button } from 'antd'; +import Text from '../../common/Text'; + +// Add new job monitoring button +function AddJobMonitoringBtn({ handleAddJobMonitoringButtonClick }) { + return ( + + ); +} + +export default AddJobMonitoringBtn; diff --git a/client-reactjs/src/components/application/jobMonitoring/ApproveRejectModal.jsx b/client-reactjs/src/components/application/jobMonitoring/ApproveRejectModal.jsx new file mode 100644 index 000000000..0dcf53763 --- /dev/null +++ b/client-reactjs/src/components/application/jobMonitoring/ApproveRejectModal.jsx @@ -0,0 +1,166 @@ +import React, { useState, useEffect } from 'react'; +import { Modal, Form, Input, Button, message, Tooltip } from 'antd'; + +import { authHeader } from '../../common/AuthHeader.js'; +import { Constants } from '../../common/Constants'; + +const ApproveRejectModal = ({ + id, + displayAddRejectModal, + setDisplayAddRejectModal, + setSelectedMonitoring, + user, + selectedMonitoring, + setJobMonitorings, +}) => { + const [form] = Form.useForm(); + const [savingEvaluation, setSavingEvaluation] = useState(false); + const [monitoringEvaluated, setMonitoringEvaluated] = useState(false); + + //When component mounts check if monitoring is already evaluated + useEffect(() => { + if (selectedMonitoring) { + if (selectedMonitoring?.approvalStatus !== 'Pending') { + setMonitoringEvaluated(true); + } else { + setMonitoringEvaluated(false); + } + } + }, [selectedMonitoring]); + + // When cancel button is clicked + const handleCancel = () => { + setDisplayAddRejectModal(false); + form.resetFields(); + setSelectedMonitoring(null); + }; + + // When reject or accepted is clicked + const handleSubmit = async ({ action }) => { + setSavingEvaluation(true); + let fromErr = false; + try { + await form.validateFields(); + } catch (error) { + fromErr = true; + } + + if (fromErr) { + console.log('Form error'); + return; + } + + try { + const formData = form.getFieldsValue(); + formData.id = id; + formData.approvalStatus = action; + formData.approvedBy = JSON.stringify({ + id: user.id, + name: `${user.firstName} ${user.lastName}`, + email: user.email, + }); + const payload = { + method: 'PATCH', + header: authHeader(), + body: JSON.stringify(formData), + }; + + const response = await fetch(`/api/jobmonitoring/evaluate`, payload); + + if (!response.ok) { + message.error('Error saving your response'); + } else { + message.success('Your response has been saved'); + form.resetFields(); + setSelectedMonitoring(null); + setDisplayAddRejectModal(false); + setJobMonitorings((prev) => { + const index = prev.findIndex((item) => item.id === id); + prev[index] = { + ...prev[index], + approvalStatus: action, + approvedBy: JSON.stringify({ + id: user.id, + name: `${user.firstName} ${user.lastName}`, + email: user.email, + }), + approvedAt: new Date(), + approverComment: formData.approverComment, + }; + return [...prev]; + }); + } + } catch (error) { + message.error(error.message); + } finally { + setSavingEvaluation(false); + } + }; + + return ( + handleSubmit({ action: 'Rejected' })} + disabled={savingEvaluation}> + Reject + , + , + ] + : [ + , + , + ] + }> + <> + {monitoringEvaluated && selectedMonitoring ? ( +
+ This monitoring was{' '} + {selectedMonitoring?.approvalStatus} by{' '} + {JSON.parse(selectedMonitoring?.approvedBy)?.email}
}> + + {JSON.parse(selectedMonitoring?.approvedBy)?.name}{' '} + + + on {new Date(selectedMonitoring?.approvedAt).toLocaleDateString('en-US', Constants.DATE_FORMAT_OPTIONS)}. + + ) : ( +
+ + + +
+ )} + +
+ ); +}; + +export default ApproveRejectModal; diff --git a/client-reactjs/src/components/application/jobMonitoring/AsrSpecificMonitoringDetails.jsx b/client-reactjs/src/components/application/jobMonitoring/AsrSpecificMonitoringDetails.jsx new file mode 100644 index 000000000..1e233f8b2 --- /dev/null +++ b/client-reactjs/src/components/application/jobMonitoring/AsrSpecificMonitoringDetails.jsx @@ -0,0 +1,165 @@ +// Desc: This file contains the form for ASR specific monitoring details +import React, { useEffect, useState } from 'react'; +const { Form, Row, Col, Input, Select } = require('antd'); + +//Constants +const { Option } = Select; +const jobRunType = [ + { label: 'Daytime', value: 'Daytime' }, + { label: 'Overnight', value: 'Overnight' }, + { label: 'AM', value: 'AM' }, + { label: 'PM', value: 'PM' }, + { label: 'Every 2 Days', value: 'Every2days' }, +]; +const severityLevels = [0, 1, 2, 3]; + +function AsrSpecificMonitoringDetails({ form }) { + //Local States + const [domain, setDomain] = useState([]); + const [categories, setCategories] = useState(null); + const [selectedDomain, setSelectedDomain] = useState(null); + + //Effects + useEffect(() => { + getDomains(); + getProductCategory(); + }, []); + + //Function to get domain + const getDomains = async () => { + //TODO: Fetch domain from Fido server - this is just a place holder function + setDomain([ + { + label: 'Domain - 1', + value: 'domain1', + }, + { + label: 'Domain - 2', + value: 'domain2', + }, + ]); + }; + + // Function to get product category + const getProductCategory = async (_domain) => { + //TODO: Fetch product Category from Fido server - this is just a place holder function + setCategories({ + domain1: [ + { label: 'Category-1a', value: 'category1' }, + { label: 'Category-2a', value: 'category2' }, + ], + domain2: [ + { label: 'Category-1b', value: 'category1' }, + { label: 'Category-2b', value: 'category2' }, + ], + }); + }; + return ( +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + { + if (form.getFieldValue('notificationCondition')?.includes('ThresholdExceeded')) { + if (!value) { + return Promise.reject(new Error('This field is required')); + } else if (!(parseInt(value) > 0 && parseInt(value) < 1440)) { + return Promise.reject(new Error('Threshold should be between 0 and 1440')); + } + } + }, + }, + ]}> + + + + +
+ ); +} + +export default AsrSpecificMonitoringDetails; diff --git a/client-reactjs/src/components/application/jobMonitoring/AsrSpecificNotificationsDetails.jsx b/client-reactjs/src/components/application/jobMonitoring/AsrSpecificNotificationsDetails.jsx new file mode 100644 index 000000000..f90cd8fe0 --- /dev/null +++ b/client-reactjs/src/components/application/jobMonitoring/AsrSpecificNotificationsDetails.jsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { Form, Select } from 'antd'; +import { isEmail } from 'validator'; + +function AsrSpecificNotificationsDetails() { + return ( + <> + { + if (!value) { + return Promise.resolve(); + } + if (value.length > 20) { + return Promise.reject(new Error('Max 20 emails allowed')); + } + if (!value.every((v) => isEmail(v))) { + return Promise.reject(new Error('One or more emails are invalid')); + } + return Promise.resolve(); + }, + }, + ]}> + + + + ); +} + +export default AsrSpecificNotificationsDetails; diff --git a/client-reactjs/src/components/application/jobMonitoring/JobMonitoringBasicTab.jsx b/client-reactjs/src/components/application/jobMonitoring/JobMonitoringBasicTab.jsx index e5d6aaf96..402975f0a 100644 --- a/client-reactjs/src/components/application/jobMonitoring/JobMonitoringBasicTab.jsx +++ b/client-reactjs/src/components/application/jobMonitoring/JobMonitoringBasicTab.jsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { Form, Select, AutoComplete } from 'antd'; +import { Form, Select, AutoComplete, Input, Card } from 'antd'; import { InfoCircleOutlined } from '@ant-design/icons'; import { debounce } from 'lodash'; @@ -7,31 +7,31 @@ import { authHeader, handleError } from '../../common/AuthHeader.js'; import InfoDrawer from '../../common/InfoDrawer'; const { Option } = Select; +const { TextArea } = Input; +//Monitoring scope options const monitoringScopeOptions = [ - { label: 'Single Job Monitoring', value: 'individualJob' }, - { label: 'Cluster-Wide Monitoring', value: 'cluster' }, + { + label: 'Specific job', + value: 'SpecificJob', + }, + { + label: 'Cluster-wide monitoring', + value: 'ClusterWideMonitoring', + }, + { + label: 'Monitoring by Job Pattern', + value: 'PatternMatching', + }, ]; -function ClusterMonitoringBasicTab({ - clusters, - handleClusterChange, - selectedCluster, - monitoringScope, - setMonitoringScope, - setSelectedJob, -}) { +function JobMonitoringBasicTab({ form, clusters, monitoringScope, setMonitoringScope, jobMonitorings, isEditing }) { + //Local State + const [showUserGuide, setShowUserGuide] = useState(false); + const [selectedUserGuideName, setSelectedUserGuideName] = useState(''); const [jobs, setJobs] = useState([]); const [fetchingJobs, setFetchingJobs] = useState(false); - const [open, setOpen] = useState(false); - - const showDrawer = () => { - setOpen(true); - }; - - const onClose = () => { - setOpen(false); - }; + const [selectedCluster, setSelectedCluster] = useState(null); // Get jobs function const getJobs = debounce(async (value) => { @@ -79,74 +79,151 @@ function ClusterMonitoringBasicTab({ setJobs([]); }; - //When job is selected - const onJobSelect = (jobName) => { - const selectedJobDetails = jobs.find((job) => job.value === jobName); - setSelectedJob(selectedJobDetails); - }; - return ( - <> - - - - - - - - - {selectedCluster && monitoringScope === 'individualJob' ? ( + +
- Job Name - - showDrawer()} /> - - - - } - name="jobName" - validateTrigger={['onChange', 'onBlur']} + label="Monitoring Name" + name="monitoringName" rules={[ { required: true, message: 'Required filed' }, - { max: 256, message: 'Maximum of 256 characters allowed' }, + { max: 100, message: 'Maximum of 100 characters allowed' }, + () => ({ + validator(_, value) { + if (isEditing) return Promise.resolve(); + if (!value || !jobMonitorings.find((job) => job.monitoringName === value)) { + return Promise.resolve(); + } + return Promise.reject(new Error('Monitoring name must be unique')); + }, + }), ]}> - onJobSelect(jobName)} - loading={fetchingJobs}> + - ) : null} - + + +