diff --git a/client-reactjs/package-lock.json b/client-reactjs/package-lock.json index b600ca6a1..432e906dc 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..ad3590b40 --- /dev/null +++ b/client-reactjs/src/components/application/jobMonitoring/AddEditJobMonitoringModal.jsx @@ -0,0 +1,187 @@ +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, + 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 = () => { + form.resetFields(); + setIntermittentScheduling({ schedulingType: 'daily', id: uuidv4() }); + setCompleteSchedule([]); + setDisplayAddJobMonitoringModal(false); + setActiveTab('0'); + setVisitedTabs(['0']); + setSelectedMonitoring(null); + setEditingData({ isEditing: false }); + setErroneousTabs([]); + setErroneousScheduling(false); + setActiveTab('0'); + setMonitoringScope(null); + }; + + //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..6bef23c0b --- /dev/null +++ b/client-reactjs/src/components/application/jobMonitoring/AsrSpecificMonitoringDetails.jsx @@ -0,0 +1,167 @@ +// Desc: This file contains the form for ASR specific monitoring details +import React, { useEffect, useState } from 'react'; +const { Form, Row, Col, Input, Select, message } = require('antd'); + +import { getDomains, getProductCategories } from './jobMonitoringUtils'; + +//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 [productCategories, setProductCategories] = useState([]); + const [selectedDomain, setSelectedDomain] = useState(null); + + //Effects + useEffect(() => { + // Get domains + (async () => { + try { + const domainData = await getDomains(); + setDomain(domainData); + } catch (error) { + message.error('Error fetching domains'); + } + })(); + + // Get product categories + if (selectedDomain) { + (async () => { + try { + const productCategories = await getProductCategories({ domainId: selectedDomain }); + setProductCategories(productCategories); + } catch (error) { + message.error('Error fetching product category'); + } + })(); + } + }, [selectedDomain]); + + //Handle domain change function + const handleDomainChange = (value) => { + form.setFieldsValue({ productCategory: undefined }); + setSelectedDomain(value); + }; + + 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} - + + +