From 11ebb8dc48bcef49b70ae5353410dacc5b76d8f5 Mon Sep 17 00:00:00 2001 From: Jovan Cvetkovic Date: Wed, 21 Jun 2023 11:52:39 +0200 Subject: [PATCH] [FEATURE] Add composite monitor type #573 Signed-off-by: Jovan Cvetkovic --- .../composite_level_monitor_spec.js | 25 +- .../AssociateMonitors/AssociateMonitors.tsx | 109 ++++++++- .../containers/CreateMonitor/CreateMonitor.js | 1 + .../WorkflowDetails/WorkflowDetails.tsx | 12 +- .../ExpressionQuery/ExpressionQuery.js | 216 +++++++++++------- .../CreateTrigger/utils/formikToTrigger.js | 6 +- .../DefineCompositeLevelTrigger.js | 3 +- 7 files changed, 276 insertions(+), 96 deletions(-) diff --git a/cypress/integration/composite_level_monitor_spec.js b/cypress/integration/composite_level_monitor_spec.js index 9581422d6..006c742bd 100644 --- a/cypress/integration/composite_level_monitor_spec.js +++ b/cypress/integration/composite_level_monitor_spec.js @@ -44,6 +44,7 @@ describe('CompositeLevelMonitor', () => { cy.contains('Create monitor', { timeout: 20000 }); }); + let monitorId; describe('can be created', () => { beforeEach(() => { // Go to create monitor page @@ -88,7 +89,7 @@ describe('CompositeLevelMonitor', () => { // Wait for monitor to be created cy.wait('@createMonitorRequest').then((interceptor) => { - const monitorID = interceptor.response.body.resp._id; + monitorId = interceptor.response.body.resp._id; cy.contains('Loading monitors'); cy.wait('@getMonitorsRequest').then((interceptor) => { @@ -113,8 +114,7 @@ describe('CompositeLevelMonitor', () => { ); cy.wait(1000).then(() => { - cy.executeCompositeMonitor(monitorID); - debugger; + cy.executeCompositeMonitor(monitorId); monitor1[0] && cy.executeMonitor(monitor1[0].id); monitor2[0] && cy.executeMonitor(monitor2[0].id); @@ -127,5 +127,24 @@ describe('CompositeLevelMonitor', () => { }); }); + describe('can be edited', () => { + beforeEach(() => { + if (monitorId) { + cy.visit( + `${Cypress.env( + 'opensearch_dashboards' + )}/app/${PLUGIN_NAME}#/monitors/${monitorId}?action=update-monitor&type=workflow` + ); + } else { + throw new Error(`Monitor with ID: ${monitorId} not found or not created.`); + } + }); + + it('by visual editor', () => { + // Verify edit page + cy.contains('Edit monitor', { timeout: 20000 }); + }); + }); + after(() => clearAll()); }); diff --git a/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.tsx b/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.tsx index ede80d1d2..e47558c52 100644 --- a/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.tsx +++ b/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.tsx @@ -3,12 +3,86 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { Fragment } from 'react'; +import React, { Fragment, useState, useEffect, useCallback } from 'react'; import { EuiSpacer, EuiText } from '@elastic/eui'; import MonitorsList from './components/MonitorsList'; +import { FormikCodeEditor } from '../../../../components/FormControls'; +import * as _ from 'lodash'; +import { isInvalid, hasError, validateExtractionQuery } from '../../../../utils/validate'; -const AssociateMonitors = ({ monitors, options }) => { - const onUpdate = () => {}; +const AssociateMonitors = ({ + monitors, + options, + searchType = 'graph', + isDarkMode, + monitorValues, +}) => { + const [graphUi, setGraphUi] = useState(searchType === 'graph'); + + const queryTemplate = { + sequence: { + delegates: [], + }, + }; + + const delegatesToMonitors = (value) => + value.sequence.delegates.map((monitor) => ({ + label: '', + value: monitor.monitor_id, + })); + + useEffect(() => { + if (monitors?.length) { + const value = { ...queryTemplate }; + monitors.map((monitor, idx) => { + let delegate = { + order: idx + 1, + monitor_id: monitor.monitor_id, + }; + value.sequence.delegates.push(delegate); + _.set(monitorValues, `associatedMonitor_${idx}`, { + label: monitor.monitor_name || '', + value: monitor.monitor_id, + }); + }); + + _.set(monitorValues, 'associatedMonitorsEditor', JSON.stringify(value, null, 4)); + _.set(monitorValues, 'associatedMonitors', delegatesToMonitors(value)); + } else { + if (options?.length) { + const value = { ...queryTemplate }; + const firstTwo = options.slice(0, 2); + firstTwo.map((monitor, idx) => { + value.sequence.delegates.push({ + order: idx + 1, + monitor_id: monitor.monitor_id, + }); + }); + + try { + _.set(monitorValues, 'associatedMonitorsEditor', JSON.stringify(value, null, 4)); + _.set(monitorValues, 'associatedMonitors', delegatesToMonitors(value)); + } catch (e) { + console.log('No monitor options are available.'); + } + } + } + + setGraphUi(searchType === 'graph'); + }, [searchType, monitors, options]); + + const onCodeChange = useCallback( + (query, field, form) => { + form.setFieldValue('associatedMonitorsEditor', query); + try { + const code = JSON.parse(query); + form.setFieldValue('associatedMonitors', delegatesToMonitors(code)); + } catch (e) { + console.error('Invalid json.'); + } + }, + [options, monitors] + ); return ( @@ -16,12 +90,37 @@ const AssociateMonitors = ({ monitors, options }) => {

Associate monitors

- Associate two or more monitors to run as part of this flow. + Associate two or more monitors to run as part of this workflow. - + {graphUi ? ( + + ) : ( + form.setFieldTouched('associatedMonitorsEditor', true), + 'data-test-subj': 'associatedMonitorsCodeEditor', + }} + /> + )}
); }; diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/CreateMonitor.js b/public/pages/CreateMonitor/containers/CreateMonitor/CreateMonitor.js index 0da64435b..c00dfb5c1 100644 --- a/public/pages/CreateMonitor/containers/CreateMonitor/CreateMonitor.js +++ b/public/pages/CreateMonitor/containers/CreateMonitor/CreateMonitor.js @@ -306,6 +306,7 @@ export default class CreateMonitor extends Component { { } }; -const WorkflowDetails = ({ isAd, isComposite, httpClient, history, values }) => { +const WorkflowDetails = ({ isAd, isComposite, httpClient, history, values, isDarkMode }) => { const [selectedMonitors, setSelectedMonitors] = useState([]); const [monitorOptions, setMonitorOptions] = useState([]); @@ -40,7 +40,7 @@ const WorkflowDetails = ({ isAd, isComposite, httpClient, history, values }) => setMonitorOptions(monitors); const inputIds = values.inputs?.map((input) => input.monitor_id); - if (inputIds && inputIds.length) { + if (inputIds?.length) { const selected = monitors.filter((monitor) => inputIds.indexOf(monitor.monitor_id) !== -1); setSelectedMonitors(selected); _.set( @@ -70,7 +70,13 @@ const WorkflowDetails = ({ isAd, isComposite, httpClient, history, values }) => {isComposite && ( - + )} diff --git a/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js b/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js index f642de55f..bb0447797 100644 --- a/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js +++ b/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js @@ -9,6 +9,7 @@ import { } from '@elastic/eui'; import * as _ from 'lodash'; import { FormikFormRow, FormikInputWrapper } from '../../../../components/FormControls'; +import { FormikCodeEditor } from '../../../../components/FormControls'; const ExpressionQuery = ({ selections, @@ -18,16 +19,34 @@ const ExpressionQuery = ({ label, formikName = 'expressionQueries', triggerValues, + isDarkMode = false, }) => { const DEFAULT_DESCRIPTION = defaultText ? defaultText : 'Select'; const [usedExpressions, setUsedExpressions] = useState([]); + const [graphUi, setGraphUi] = useState(triggerValues.searchType === 'graph'); + const [editorValue, setEditorValue] = useState(''); + + const getQueryTemplate = (monitor_id) => `monitor[id=${monitor_id}]`; + const queryConditionOperator = '&&'; useEffect(() => { if (value?.length) { setUsedExpressions(value); _.set(triggerValues, formikName, getValue(value)); } - }, [value]); + + setGraphUi(triggerValues.searchType === 'graph'); + + if (selections?.length) { + const editorValues = []; + selections.map((selection) => { + editorValues.push(getQueryTemplate(selection.monitor_id)); + }); + const script = editorValues.join(` ${queryConditionOperator} `); + setEditorValue(script); + _.set(triggerValues, 'triggerDefinitions[0].script.source', script); + } + }, [value, triggerValues.searchType]); const getValue = (expressions) => expressions.map((exp) => ({ @@ -155,7 +174,7 @@ const ExpressionQuery = ({ validate(), + validate: () => graphUi && validate(), }} render={({ field, form }) => ( form.touched['expressionQueries'] && !isValid(), - error: () => validate(), + isInvalid: () => form.touched['expressionQueries'] && graphUi && !isValid(), + error: () => graphUi && validate(), + style: { + maxWidth: 'inherit', + }, }} > - - {!usedExpressions.length && ( - - onBlur(form, usedExpressions)} - /> - } - isOpen={false} - panelPaddingSize="s" - anchorPosition="rightDown" - closePopover={() => onBlur(form, usedExpressions)} - /> - - )} - {usedExpressions.map((expression, idx) => ( - - { - e.preventDefault(); - openPopover(idx); - }} - /> - } - isOpen={expression.isOpen} - closePopover={() => closePopover(idx)} - panelPaddingSize="s" - anchorPosition="rightDown" - > - {renderOptions(expression, idx, form)} - - - ))} - {selections.length > usedExpressions.length && ( - - { - const expressions = _.cloneDeep(usedExpressions); - const differences = _.differenceBy(selections, expressions, 'monitor_id'); - const newExpressions = [ - ...expressions, - { - description: usedExpressions.length ? 'AND' : '', - isOpen: false, - monitor_name: differences[0]?.label, - monitor_id: differences[0]?.monitor_id, - }, - ]; + {graphUi ? ( + + {!usedExpressions.length && ( + + onBlur(form, usedExpressions)} + /> + } + isOpen={false} + panelPaddingSize="s" + anchorPosition="rightDown" + closePopover={() => onBlur(form, usedExpressions)} + /> + + )} + {usedExpressions.map((expression, idx) => ( + + { + e.preventDefault(); + openPopover(idx); + }} + /> + } + isOpen={expression.isOpen} + closePopover={() => closePopover(idx)} + panelPaddingSize="s" + anchorPosition="rightDown" + > + {renderOptions(expression, idx, form)} + + + ))} + {selections.length > usedExpressions.length && ( + + { + const expressions = _.cloneDeep(usedExpressions); + const differences = _.differenceBy(selections, expressions, 'monitor_id'); + const newExpressions = [ + ...expressions, + { + description: usedExpressions.length ? 'AND' : '', + isOpen: false, + monitor_name: differences[0]?.label, + monitor_id: differences[0]?.monitor_id, + }, + ]; - setUsedExpressions(newExpressions); - onBlur(form, newExpressions); - }} - color={'primary'} - iconType="plusInCircleFilled" - aria-label={'Add one more condition'} - data-test-subj={'condition-add-selection-btn'} - style={{ marginTop: '1px' }} - /> - - )} - + setUsedExpressions(newExpressions); + onBlur(form, newExpressions); + }} + color={'primary'} + iconType="plusInCircleFilled" + aria-label={'Add one more condition'} + data-test-subj={'condition-add-selection-btn'} + style={{ marginTop: '1px' }} + /> + + )} + + ) : ( + { + _.set(triggerValues, 'triggerDefinitions[0].script.source', query); + form.setFieldValue('expressionQueries', query); + }, + onBlur: (e, field, form) => { + console.log('### triggerValues', triggerValues); + form.setFieldTouched('expressionQueries', true); + }, + 'data-test-subj': 'expressionQueriesCodeEditor', + }} + /> + )} )} /> diff --git a/public/pages/CreateTrigger/containers/CreateTrigger/utils/formikToTrigger.js b/public/pages/CreateTrigger/containers/CreateTrigger/utils/formikToTrigger.js index 2c1e7d332..afdc5979c 100644 --- a/public/pages/CreateTrigger/containers/CreateTrigger/utils/formikToTrigger.js +++ b/public/pages/CreateTrigger/containers/CreateTrigger/utils/formikToTrigger.js @@ -124,12 +124,16 @@ export function formikToCompositeTriggerCondition(values) { }; const triggerConditions = _.get(values, 'triggerConditions', []); - const source = triggerConditions.reduce((query, expression) => { + let source = triggerConditions.reduce((query, expression) => { query += ` ${conditionMap[expression.condition]} monitor[id=${expression.monitor_id}]`; query = query.trim(); return query; }, ''); + if (!source) { + source = _.get(values, 'script.source', ''); + } + return { script: { lang: 'painless', diff --git a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/DefineCompositeLevelTrigger.js b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/DefineCompositeLevelTrigger.js index 441d9c0ea..091300499 100644 --- a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/DefineCompositeLevelTrigger.js +++ b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/DefineCompositeLevelTrigger.js @@ -136,7 +136,7 @@ class DefineCompositeLevelTrigger extends Component { const triggerActions = _.get(triggerValues, 'triggerDefinitions[0].actions', []); const monitorList = monitorValues?.associatedMonitors ? monitorValues.associatedMonitors?.map((monitor) => ({ - label: monitor.label.replaceAll(' ', '_'), + label: monitor.label?.replaceAll(' ', '_'), monitor_id: monitor.value, })) : []; @@ -184,6 +184,7 @@ class DefineCompositeLevelTrigger extends Component { dataTestSubj={'composite_expression_query'} defaultText={'Select associated monitor'} triggerValues={triggerValues} + isDarkMode={this.props.isDarkMode} />