From 28b81623d0cd21bc84f358e656aa146ee6416051 Mon Sep 17 00:00:00 2001 From: Jovan Cvetkovic Date: Fri, 9 Jun 2023 17:18:39 +0200 Subject: [PATCH 01/63] [FEATURE] Add composite monitor type #573 Signed-off-by: Jovan Cvetkovic --- yarn.lock | 108 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 97 insertions(+), 11 deletions(-) diff --git a/yarn.lock b/yarn.lock index a004c785f..2bd4694c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -232,6 +232,90 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" +"@node-rs/xxhash-android-arm-eabi@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@node-rs/xxhash-android-arm-eabi/-/xxhash-android-arm-eabi-1.4.0.tgz#55ace4d3882686d1e379aaf613e1338d78f13fc8" + integrity sha512-JuZNqt5/znWkIGteikQdS+HT9S0JsMYi06S4yzU/sMKLCIPvD0MnCTXlYtuDcgRIKScCaepAsSQVomnAyLFNNA== + +"@node-rs/xxhash-android-arm64@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@node-rs/xxhash-android-arm64/-/xxhash-android-arm64-1.4.0.tgz#2290c53ceabda804afb4c45679613d833a6385a0" + integrity sha512-BZzQO5jlgsIr9HhiqTwZjYqlfVeZiu+7PaoAdNEOq+i/SjyAqv1jGSkyek4rBSAiodyNkXcbE0eQtomeN6a55w== + +"@node-rs/xxhash-darwin-arm64@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@node-rs/xxhash-darwin-arm64/-/xxhash-darwin-arm64-1.4.0.tgz#96df4f48b13deb6899e84ed0882bdbd0a4856f13" + integrity sha512-JlEAzTsQaqJaWVse/JP//6QKBIhzqzTlvNY4uEbi8TaZMfvDDhW//ClXM6CkSV799GJxAYPu1LXa4+OeBQpa7Q== + +"@node-rs/xxhash-darwin-x64@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@node-rs/xxhash-darwin-x64/-/xxhash-darwin-x64-1.4.0.tgz#9df3ca3a87354dd5386aadfa20ad032a299c2b8f" + integrity sha512-9ycVJfzLvw1wc6Tgq0giLkMn5nGOBawTeOA17t27dQFdY/scZPz583DO7w+eznMnlzUXwoLiloanUebRhy+piQ== + +"@node-rs/xxhash-freebsd-x64@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@node-rs/xxhash-freebsd-x64/-/xxhash-freebsd-x64-1.4.0.tgz#24b0c0bfd33429303688b4af78f9d323daa0fb5b" + integrity sha512-vFRDr6qA0gHWQDjuSxXcdzM4Ppk+5VebEhc76zkWrRVc6RG60fxLo5B4j6QwMwXGTYaG8HMv/nQhAgbnOCWWxQ== + +"@node-rs/xxhash-linux-arm-gnueabihf@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@node-rs/xxhash-linux-arm-gnueabihf/-/xxhash-linux-arm-gnueabihf-1.4.0.tgz#4c09f70cd39429fb1a52f3567085e949603d4817" + integrity sha512-0KS6y1caqbtPanos9XNMekWpozCHA6QSlQzaZyn9Hn+Z+mYpR5+NoWixefhp06jt59qF9+LkkF3C9fSEHYmq/w== + +"@node-rs/xxhash-linux-arm64-gnu@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@node-rs/xxhash-linux-arm64-gnu/-/xxhash-linux-arm64-gnu-1.4.0.tgz#e92d7026614506fb4db309977127fd8589fabd7c" + integrity sha512-QI97JK2qiQhVgRtUBMgA1ZjPLpwnz11SE2Mw1jryejmyH9EXKKiCyt2FweO6MVP7bEuMxcdajBho4pEL7s/QsA== + +"@node-rs/xxhash-linux-arm64-musl@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@node-rs/xxhash-linux-arm64-musl/-/xxhash-linux-arm64-musl-1.4.0.tgz#a8b16233a86c116e6af32a69278248d17b2d09e7" + integrity sha512-dtMid4OMkNBYGJkjoT1jdkENpV8m8MGp3lliDN8C+2znZUQM8KFRTXRkfaq4lgzu3Y2XeYzsLOoBsBd3Hgf7gA== + +"@node-rs/xxhash-linux-x64-gnu@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@node-rs/xxhash-linux-x64-gnu/-/xxhash-linux-x64-gnu-1.4.0.tgz#385ec91396ebaa2b73abf419be3971ec893dcbd1" + integrity sha512-OeOQL10cG62wL1IVoeC74xESmefHU7r3xiZMTP2hK5Dh3FdF2sa3x/Db9BcGXlaokg/lMGDxuTuzOLC2Rv/wlQ== + +"@node-rs/xxhash-linux-x64-musl@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@node-rs/xxhash-linux-x64-musl/-/xxhash-linux-x64-musl-1.4.0.tgz#715bb962502b0ec69e1fc19db22ac035c63d30c7" + integrity sha512-kZ8wNi5bH9b+ZpuPlSbFd6JXk8CKbfCvCPZ0Vk0IqLkzB6PihQflnZPM9r0QZ2jtFgyfWmpbFK4YxwX9YcyLog== + +"@node-rs/xxhash-win32-arm64-msvc@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@node-rs/xxhash-win32-arm64-msvc/-/xxhash-win32-arm64-msvc-1.4.0.tgz#4a3a4ebcb50c73e4309e429b28eb44dbf8f7f71f" + integrity sha512-Ggv66jlhQvj4XgQqNgl2JKQ7My/97PvPZi5jKbcS7t65wJC36J6XERQwRPdupO8UH63XfPqb7HJqrgmiz8tmlA== + +"@node-rs/xxhash-win32-ia32-msvc@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@node-rs/xxhash-win32-ia32-msvc/-/xxhash-win32-ia32-msvc-1.4.0.tgz#fdfdb43e41113a8baf15779ca53bb637d2e1bc8f" + integrity sha512-mYpF1+7unqKKGsPn7Y8X6SqP2Bc5BU5dsHBKhAGAuvrMg9W63zM+YWM8/fpNGfFlOrjiKRvXHZ96nrZyzoxeBw== + +"@node-rs/xxhash-win32-x64-msvc@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@node-rs/xxhash-win32-x64-msvc/-/xxhash-win32-x64-msvc-1.4.0.tgz#aee714a4ae0121f3947f94139adf13f5b6d93d12" + integrity sha512-rKuqWHuQNlrfjIOkQW3oCBta/GUlyVoUkKB13aVr8uixOs/eneuDaYJx2h02FAAWlWCKADFnMxgDl0LVFBy53w== + +"@node-rs/xxhash@^1.3.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@node-rs/xxhash/-/xxhash-1.4.0.tgz#1e75850e0e530c9224e8e5ba4056d52e8868291b" + integrity sha512-UpSOParhMqbQ7hsYovN2e+uqvWqHJiCDvFl8gDzMcXgBY/PkI2zo2zhdRAZdz48c6/dke+0WjCKy90wDVQxS6g== + optionalDependencies: + "@node-rs/xxhash-android-arm-eabi" "1.4.0" + "@node-rs/xxhash-android-arm64" "1.4.0" + "@node-rs/xxhash-darwin-arm64" "1.4.0" + "@node-rs/xxhash-darwin-x64" "1.4.0" + "@node-rs/xxhash-freebsd-x64" "1.4.0" + "@node-rs/xxhash-linux-arm-gnueabihf" "1.4.0" + "@node-rs/xxhash-linux-arm64-gnu" "1.4.0" + "@node-rs/xxhash-linux-arm64-musl" "1.4.0" + "@node-rs/xxhash-linux-x64-gnu" "1.4.0" + "@node-rs/xxhash-linux-x64-musl" "1.4.0" + "@node-rs/xxhash-win32-arm64-msvc" "1.4.0" + "@node-rs/xxhash-win32-ia32-msvc" "1.4.0" + "@node-rs/xxhash-win32-x64-msvc" "1.4.0" + "@samverschueren/stream-to-observable@^0.3.0": version "0.3.1" resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz#a21117b19ee9be70c379ec1877537ef2e1c63301" @@ -2868,7 +2952,7 @@ loader-runner@^2.4.0: resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw== -loader-utils@1.4.2, loader-utils@^1.2.3: +loader-utils@1.4.2, loader-utils@^2.0.4: version "1.4.2" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.2.tgz#29a957f3a63973883eb684f10ffd3d151fec01a3" integrity sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg== @@ -4402,11 +4486,12 @@ tapable@^1.0.0, tapable@^1.1.3: resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== -terser-webpack-plugin@^1.4.3: - version "1.4.5" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz#a217aefaea330e734ffacb6120ec1fa312d6040b" - integrity sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw== +"terser-webpack-plugin@npm:@amoo-miki/terser-webpack-plugin@1.4.5-rc.2": + version "1.4.5-rc.2" + resolved "https://registry.yarnpkg.com/@amoo-miki/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5-rc.2.tgz#046c062ef22c126c2544718674bc6624e3651b9c" + integrity sha512-JFSGSzsWgSHEqQXlnHDh3gw+jdVdVlWM2Irdps9P/yWYNY/5VjuG8sdoW4mbuP8/HM893/k8N+ipbeqsd8/xpA== dependencies: + "@node-rs/xxhash" "^1.3.0" cacache "^12.0.2" find-cache-dir "^2.1.0" is-wsl "^1.1.0" @@ -4699,11 +4784,12 @@ webpack-sources@^1.4.0, webpack-sources@^1.4.1: source-list-map "^2.0.0" source-map "~0.6.1" -webpack@^4.41.5: - version "4.46.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.46.0.tgz#bf9b4404ea20a073605e0a011d188d77cb6ad542" - integrity sha512-6jJuJjg8znb/xRItk7bkT0+Q7AHCYjjFnvKIWQPkNIOyRqoCGvkOs0ipeQzrqz4l5FtN5ZI/ukEHroeX/o1/5Q== +"webpack@npm:@amoo-miki/webpack@4.46.0-rc.2": + version "4.46.0-rc.2" + resolved "https://registry.yarnpkg.com/@amoo-miki/webpack/-/webpack-4.46.0-rc.2.tgz#36824597c14557a7bb0a8e13203e30275e7b02bd" + integrity sha512-Y/ZqxTHOoDF1kz3SR63Y9SZGTDUpZNNFrisTRHofWhP8QvNX3LMN+TCmEP56UfLaiLVKMcaiFjx8kFb2TgyBaQ== dependencies: + "@node-rs/xxhash" "^1.3.0" "@webassemblyjs/ast" "1.9.0" "@webassemblyjs/helper-module-context" "1.9.0" "@webassemblyjs/wasm-edit" "1.9.0" @@ -4716,7 +4802,7 @@ webpack@^4.41.5: eslint-scope "^4.0.3" json-parse-better-errors "^1.0.2" loader-runner "^2.4.0" - loader-utils "^1.2.3" + loader-utils "^2.0.4" memory-fs "^0.4.1" micromatch "^3.1.10" mkdirp "^0.5.3" @@ -4724,7 +4810,7 @@ webpack@^4.41.5: node-libs-browser "^2.2.1" schema-utils "^1.0.0" tapable "^1.1.3" - terser-webpack-plugin "^1.4.3" + terser-webpack-plugin "npm:@amoo-miki/terser-webpack-plugin@1.4.5-rc.2" watchpack "^1.7.4" webpack-sources "^1.4.1" From 11b5b0dfa726b07c0c2eddc5fc7a439bb6d41db7 Mon Sep 17 00:00:00 2001 From: Jovan Cvetkovic Date: Thu, 15 Jun 2023 19:51:12 +0200 Subject: [PATCH 02/63] [FEATURE] Add composite monitor type #573 Signed-off-by: Jovan Cvetkovic --- .../AssociateMonitors/AssociateMonitors.tsx | 29 ++ .../components/MonitorsList.tsx | 230 +++++++++++++++ .../utils/constants.js | 1 + .../components/MonitorType/MonitorType.js | 31 +- .../components/Schedule/Schedule.js | 4 +- .../containers/CreateMonitor/CreateMonitor.js | 104 ++++--- .../CreateMonitor/utils/constants.js | 2 + .../CreateMonitor/utils/formikToMonitor.js | 60 +++- .../MonitorDetails/MonitorDetails.js | 4 - .../WorkflowDetails/WorkflowDetails.tsx | 69 +++++ .../components/Action/actions/Message.js | 17 +- .../ExpressionQuery/ExpressionQuery.js | 275 ++++++++++++++++++ .../ExpressionQuery/ExpressionQuery.test.ts | 14 + .../CreateTrigger/utils/formikToTrigger.js | 90 ++++++ .../CompositeMonitorsAlertTrigger.js | 38 +++ .../DefineCompositeLevelTrigger.js | 169 +++++++++++ .../NotificationConfigDialog.js | 149 ++++++++++ .../TriggerNotifications.js | 140 +++++++++ .../TriggerNotificationsContent.js | 91 ++++++ .../DefineCompositeLevelTrigger/index.js | 8 + public/utils/constants.js | 1 + server/clusters/alerting/alertingPlugin.js | 8 + server/routes/monitors.js | 10 + server/services/MonitorService.js | 22 ++ yarn.lock | 108 +------ 25 files changed, 1524 insertions(+), 150 deletions(-) create mode 100644 public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.tsx create mode 100644 public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.tsx create mode 100644 public/pages/CreateMonitor/containers/WorkflowDetails/WorkflowDetails.tsx create mode 100644 public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js create mode 100644 public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.test.ts create mode 100644 public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/CompositeMonitorsAlertTrigger.js create mode 100644 public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/DefineCompositeLevelTrigger.js create mode 100644 public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/NotificationConfigDialog.js create mode 100644 public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotifications.js create mode 100644 public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotificationsContent.js create mode 100644 public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/index.js diff --git a/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.tsx b/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.tsx new file mode 100644 index 000000000..9bac7683d --- /dev/null +++ b/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.tsx @@ -0,0 +1,29 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { Fragment } from 'react'; +import { EuiSpacer, EuiText } from '@elastic/eui'; +import MonitorsList from './components/MonitorsList'; + +const AssociateMonitors = ({ monitors, options, history }) => { + const onUpdate = () => {}; + + return ( + + +

Associate monitors

+
+ + Associate two or more monitors to run as part of this flow. + + + + + +
+ ); +}; + +export default AssociateMonitors; diff --git a/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.tsx b/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.tsx new file mode 100644 index 000000000..2fc7eccf1 --- /dev/null +++ b/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.tsx @@ -0,0 +1,230 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { Fragment, useState, useEffect } from 'react'; +import * as _ from 'lodash'; +import { + EuiButton, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiText, + EuiToolTip, +} from '@elastic/eui'; +import { + FormikComboBox, + FormikInputWrapper, + FormikFormRow, +} from '../../../../../components/FormControls'; + +const MonitorsList = ({ monitors = [], options = [], history }) => { + const [selectedOptions, setSelectedOptions] = useState({}); + const [monitorOptions, setMonitorOptions] = useState([]); + const [monitorFields, setMonitorFields] = useState( + _.reduce( + monitors.length ? monitors : [0, 1], + (result, value, key) => { + result.push(key); + return result; + }, + [] + ) + ); + + useEffect(() => { + const newOptions = [...options].map((monitor) => ({ + label: monitor.monitor_name, + value: monitor.monitor_id, + })); + setMonitorOptions(newOptions); + + let newSelected = monitors.length ? monitors : []; + setSelectedOptions(Object.assign({}, newSelected)); + }, []); + + const onChange = (options, monitorIdx, form) => { + let newSelected = { + ...selectedOptions, + }; + if (options[0]) { + newSelected[monitorIdx] = options[0]; + } else { + delete newSelected[monitorIdx]; + } + setSelectedOptions(newSelected); + + updateMonitorOptions(newSelected); + + onBlur(monitorIdx, form); + }; + + const updateMonitorOptions = (selected) => { + const newMonitorOptions = [...monitorOptions]; + newMonitorOptions.forEach((mon) => { + mon.disabled = isSelected(selected, mon); + }); + setMonitorOptions([...newMonitorOptions]); + }; + + const onBlur = (monitorIdx, form) => { + form.setFieldTouched('associatedMonitors', true); + form.setFieldTouched(`associatedMonitor_${monitorIdx}`, true); + + form.setFieldValue('associatedMonitors', Object.values(selectedOptions)); + form.setFieldError('associatedMonitors', validate()); + }; + + const isSelected = (selected, monitor) => { + let isSelected = false; + for (const key in selected) { + if (selected.hasOwnProperty(key)) { + if (_.isEqual(selected[key], monitor)) { + isSelected = true; + break; + } + } + } + return isSelected; + }; + + const onAddMonitor = () => { + let nextIndex = Math.max(...monitorFields) + 1; + const newMonitorFields = [...monitorFields, nextIndex]; + setMonitorFields(newMonitorFields); + }; + + const onRemoveMonitor = (monitorIdx, idx, form) => { + const newSelected = { ...selectedOptions }; + delete newSelected[monitorIdx]; + setSelectedOptions(newSelected); + + const newMonitorFields = [...monitorFields]; + newMonitorFields.splice(idx, 1); + setMonitorFields(newMonitorFields); + + updateMonitorOptions(newSelected); + + onBlur(monitorIdx, form); + }; + + const isValid = () => Object.keys(selectedOptions).length > 1; + const validate = () => { + if (!isValid()) return 'Required.'; + }; + + return ( + validate(), + }} + render={({ field, form }) => ( + form.touched['associatedMonitors'] && !isValid(), + error: () => validate(), + }} + > + + {monitorFields.map((monitorIdx, idx) => ( + + + onChange(options, monitorIdx, form), + onBlur: (e, field, form) => onBlur(monitorIdx, form), + options: monitorOptions, + singleSelection: { asPlainText: true }, + selectedOptions: selectedOptions[monitorIdx] + ? [selectedOptions[monitorIdx]] + : undefined, + 'data-test-subj': `monitors_list_${monitorIdx}`, + fullWidth: true, + }} + /> + + {selectedOptions[monitorIdx] && ( + + + + + + )} + {monitorFields.length > 2 && ( + + + onRemoveMonitor(monitorIdx, idx, form)} + /> + + + )} + + ))} + + onAddMonitor()} + disabled={ + monitorFields.length >= 10 || + monitorOptions.length <= Object.keys(selectedOptions).length + } + > + Associate another monitor + + + You can associate up to {10 - monitorFields.length} more monitors. + + + + )} + /> + ); +}; + +export default MonitorsList; diff --git a/public/pages/CreateMonitor/components/DocumentLevelMonitorQueries/utils/constants.js b/public/pages/CreateMonitor/components/DocumentLevelMonitorQueries/utils/constants.js index 497d882eb..d9494b581 100644 --- a/public/pages/CreateMonitor/components/DocumentLevelMonitorQueries/utils/constants.js +++ b/public/pages/CreateMonitor/components/DocumentLevelMonitorQueries/utils/constants.js @@ -6,6 +6,7 @@ import { OPERATORS_MAP } from '../../MonitorExpressions/expressions/utils/constants'; export const DOC_LEVEL_INPUT_FIELD = 'doc_level_input'; +export const COMPOSITE_INPUT_FIELD = 'composite_input'; /** * A list of the operators currently supported for defining queries through the UI. diff --git a/public/pages/CreateMonitor/components/MonitorType/MonitorType.js b/public/pages/CreateMonitor/components/MonitorType/MonitorType.js index 1ba2a1ef9..ebb45aac0 100644 --- a/public/pages/CreateMonitor/components/MonitorType/MonitorType.js +++ b/public/pages/CreateMonitor/components/MonitorType/MonitorType.js @@ -25,6 +25,9 @@ const onChangeDefinition = (e, form) => { form.setFieldValue('searchType', FORMIK_INITIAL_VALUES.searchType); form.setFieldValue('triggerDefinitions', FORMIK_INITIAL_TRIGGER_VALUES.triggerConditions); switch (type) { + case MONITOR_TYPE.COMPOSITE_LEVEL: + form.setFieldValue('searchType', SEARCH_TYPE.GRAPH); + break; case MONITOR_TYPE.CLUSTER_METRICS: form.setFieldValue('searchType', SEARCH_TYPE.CLUSTER_METRICS); break; @@ -56,9 +59,17 @@ const clusterMetricsDescription = ( ); -const documentLevelDescription = ( // TODO DRAFT: confirm wording +const documentLevelDescription = // TODO DRAFT: confirm wording + ( + + Per document monitors allow you to run queries on new documents as they're indexed. + + ); + +const compositeLevelDescription = ( - Per document monitors allow you to run queries on new documents as they're indexed. + Composite monitors allow you to monitor the states of existing monitors and to reduce alert + noise. ); @@ -128,6 +139,22 @@ const MonitorType = ({ values }) => ( }} /> + + onChangeDefinition(e, form), + children: compositeLevelDescription, + 'data-test-subj': 'compositeLevelMonitorRadioCard', + }} + /> + ); diff --git a/public/pages/CreateMonitor/components/Schedule/Schedule.js b/public/pages/CreateMonitor/components/Schedule/Schedule.js index f5e16378a..4a3fe134a 100644 --- a/public/pages/CreateMonitor/components/Schedule/Schedule.js +++ b/public/pages/CreateMonitor/components/Schedule/Schedule.js @@ -11,7 +11,7 @@ import Interval from './Frequencies/Interval'; const Schedule = ({ isAd }) => ( -

Schedule

+

Define workflow schedule

{isAd ? ( @@ -31,7 +31,7 @@ const Schedule = ({ isAd }) => ( ) : (
- +
)} diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/CreateMonitor.js b/public/pages/CreateMonitor/containers/CreateMonitor/CreateMonitor.js index bee4ae8a2..700f31039 100644 --- a/public/pages/CreateMonitor/containers/CreateMonitor/CreateMonitor.js +++ b/public/pages/CreateMonitor/containers/CreateMonitor/CreateMonitor.js @@ -32,6 +32,8 @@ import { } from '../../../CreateTrigger/containers/CreateTrigger/utils/formikToTrigger'; import { triggerToFormik } from '../../../CreateTrigger/containers/CreateTrigger/utils/triggerToFormik'; import { TRIGGER_TYPE } from '../../../CreateTrigger/containers/CreateTrigger/utils/constants'; +import WorkflowDetails from '../WorkflowDetails/WorkflowDetails'; +import CompositeMonitorsAlertTrigger from '../../../CreateTrigger/containers/DefineCompositeLevelTrigger/CompositeMonitorsAlertTrigger'; export default class CreateMonitor extends Component { static defaultProps = { @@ -110,9 +112,14 @@ export default class CreateMonitor extends Component { async onCreate(monitor, { setSubmitting, setErrors }) { const { httpClient, notifications } = this.props; try { - const resp = await httpClient.post('../api/alerting/monitors', { - body: JSON.stringify(monitor), - }); + const resp = await httpClient.post( + `../api/alerting/${ + monitor.monitor_type === MONITOR_TYPE.COMPOSITE_LEVEL ? 'workflows' : 'monitors' + }`, + { + body: JSON.stringify(monitor), + } + ); setSubmitting(false); const { ok, @@ -242,6 +249,8 @@ export default class CreateMonitor extends Component { monitor = { ...monitor, ...triggers }; } + console.log('Monitor', monitor); + console.log('Value', values); if (edit) this.onUpdate(monitor, formikBag); else this.onCreate(monitor, formikBag); } @@ -288,42 +297,67 @@ export default class CreateMonitor extends Component { detectorId={this.props.detectorId} setFlyout={this.props.setFlyout} /> + - {values.searchType !== SEARCH_TYPE.AD && ( -
- - -
- )} + + + - - {(triggerArrayHelpers) => ( - + {values.searchType !== SEARCH_TYPE.AD && + values.monitor_type !== MONITOR_TYPE.COMPOSITE_LEVEL && ( +
+ + +
)} -
+ + {values.monitor_type === MONITOR_TYPE.COMPOSITE_LEVEL ? ( + + ) : ( + + {(triggerArrayHelpers) => ( + + )} + + )} diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js b/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js index b846481f3..2973014e7 100644 --- a/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js +++ b/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js @@ -61,6 +61,8 @@ export const FORMIK_INITIAL_VALUES = { bucketUnitOfTime: 'h', // m = minute, h = hour, d = day filters: [], // array of FORMIK_INITIAL_WHERE_EXPRESSION_VALUES detectorId: '', + associatedMonitors: [], + expressionQuery: null, }; export const FORMIK_INITIAL_AGG_VALUES = { diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor.js b/public/pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor.js index 03d19ee2c..ec0b03e28 100644 --- a/public/pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor.js +++ b/public/pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor.js @@ -14,6 +14,7 @@ import { getApiType, } from '../../../components/ClusterMetricsMonitor/utils/clusterMetricsMonitorHelpers'; import { + COMPOSITE_INPUT_FIELD, DOC_LEVEL_INPUT_FIELD, DOC_LEVEL_QUERY_MAP, } from '../../../components/DocumentLevelMonitorQueries/utils/constants'; @@ -29,11 +30,37 @@ export function formikToMonitor(values) { [DOC_LEVEL_INPUT_FIELD]: formikToDocLevelQueriesUiMetadata(values), search: { searchType: values.searchType }, }; + case MONITOR_TYPE.COMPOSITE_LEVEL: + return { + [COMPOSITE_INPUT_FIELD]: formikToCompositeUiMetadata(values), + search: { searchType: values.searchType }, + }; default: return { search: formikToUiSearch(values) }; } }; + if (values.monitor_type === MONITOR_TYPE.COMPOSITE_LEVEL) { + const enabled_time = new Date(); + return { + last_update_time: enabled_time.getTime(), + owner: 'alerting', + type: 'workflow', + enabled_time: enabled_time.getTime(), + enabled: !values.disabled, + workflow_type: 'composite', + schema_version: 0, + name: values.name, + schedule, + inputs: [formikToInputs(values)], + triggers: [], + ui_metadata: { + schedule: uiSchedule, + ...monitorUiMetadata(), + }, + }; + } + return { name: values.name, type: 'monitor', @@ -56,11 +83,26 @@ export function formikToInputs(values) { return formikToClusterMetricsInput(values); case MONITOR_TYPE.DOC_LEVEL: return formikToDocLevelInput(values); + case MONITOR_TYPE.COMPOSITE_LEVEL: + return formikToCompositeInput(values); default: return formikToSearch(values); } } +export function formikToCompositeInput(values) { + return { + composite_input: { + sequence: { + delegates: values.associatedMonitors.map((monitor, idx) => ({ + order: idx + 1, + monitor_id: monitor.value, + })), + }, + }, + }; +} + export function formikToSearch(values) { const isAD = values.searchType === SEARCH_TYPE.AD; let query = isAD ? formikToAdQuery(values) : formikToQuery(values); @@ -144,15 +186,8 @@ export function formikToAd(values) { } export function formikToUiSearch(values) { - const { - searchType, - timeField, - aggregations, - groupBy, - bucketValue, - bucketUnitOfTime, - filters, - } = values; + const { searchType, timeField, aggregations, groupBy, bucketValue, bucketUnitOfTime, filters } = + values; return { searchType, timeField, @@ -270,6 +305,13 @@ export function formikToDocLevelQueriesUiMetadata(values) { return { queries: _.get(values, 'queries', []) }; } +export function formikToCompositeUiMetadata(values) { + return { + associatedMonitors: _.get(values, 'associatedMonitors', []), + query: _.get(values, '', ''), + }; +} + export function formikToCompositeAggregation(values) { const { aggregations, groupBy } = values; diff --git a/public/pages/CreateMonitor/containers/MonitorDetails/MonitorDetails.js b/public/pages/CreateMonitor/containers/MonitorDetails/MonitorDetails.js index 327b886f5..fe766b79b 100644 --- a/public/pages/CreateMonitor/containers/MonitorDetails/MonitorDetails.js +++ b/public/pages/CreateMonitor/containers/MonitorDetails/MonitorDetails.js @@ -8,7 +8,6 @@ import { EuiSpacer } from '@elastic/eui'; import ContentPanel from '../../../../components/ContentPanel'; import FormikFieldText from '../../../../components/FormControls/FormikFieldText'; import { hasError, isInvalid, required, validateMonitorName } from '../../../../utils/validate'; -import Schedule from '../../components/Schedule'; import MonitorDefinitionCard from '../../components/MonitorDefinitionCard'; import MonitorType from '../../components/MonitorType'; import AnomalyDetectors from '../AnomalyDetectors/AnomalyDetectors'; @@ -102,9 +101,6 @@ const MonitorDetails = ({ {anomalyDetectorContent.content} ) : null} - - - ); }; diff --git a/public/pages/CreateMonitor/containers/WorkflowDetails/WorkflowDetails.tsx b/public/pages/CreateMonitor/containers/WorkflowDetails/WorkflowDetails.tsx new file mode 100644 index 000000000..62868e509 --- /dev/null +++ b/public/pages/CreateMonitor/containers/WorkflowDetails/WorkflowDetails.tsx @@ -0,0 +1,69 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { Fragment, useEffect, useState } from 'react'; +import ContentPanel from '../../../../components/ContentPanel'; +import Schedule from '../../components/Schedule'; +import AssociateMonitors from '../../components/AssociateMonitors/AssociateMonitors'; +import { EuiSpacer } from '@elastic/eui'; + +const WorkflowDetails = ({ isAd, isComposite, httpClient, history }) => { + const [selectedMonitors, setSelectedMonitors] = useState([]); + const [monitorOptions, setMonitorOptions] = useState([]); + + const getMonitors = async () => { + const response = await httpClient.get('../api/alerting/monitors', { + query: { + from: 0, + size: 5000, + search: '', + sortField: 'name', + sortDirection: 'desc', + state: 'all', + }, + }); + + if (response.ok) { + const { monitors, totalMonitors } = response; + return monitors.map((monitor) => ({ monitor_id: monitor.id, monitor_name: monitor.name })); + } else { + console.log('error getting monitors:', response); + return []; + } + }; + + useEffect(() => { + getMonitors().then((monitors) => { + setMonitorOptions(monitors); + }); + }, []); + + return ( + + + {isComposite && ( + + + + + )} + + ); +}; + +export default WorkflowDetails; diff --git a/public/pages/CreateTrigger/components/Action/actions/Message.js b/public/pages/CreateTrigger/components/Action/actions/Message.js index d754125d0..7c5f5f5af 100644 --- a/public/pages/CreateTrigger/components/Action/actions/Message.js +++ b/public/pages/CreateTrigger/components/Action/actions/Message.js @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import _ from 'lodash'; import Mustache from 'mustache'; import { @@ -181,6 +181,10 @@ export default function Message( actionableAlertsSelections = []; _.set(action, 'action_execution_policy.action_execution_scope', actionExecutionScopeId); break; + case MONITOR_TYPE.COMPOSITE_LEVEL: + displayActionableAlertsOptions = false; + displayThrottlingSettings = true; + break; default: displayActionableAlertsOptions = false; displayThrottlingSettings = actionExecutionScopeId !== NOTIFY_OPTIONS_VALUES.PER_EXECUTION; @@ -204,6 +208,17 @@ export default function Message( preview = err.message; console.error('There was an error rendering mustache template', err); } + + console.log( + 'MESSAGE', + action, + context, + index, + isSubjectDisabled, + sendTestMessage, + fieldPath, + values + ); return (
{!isSubjectDisabled ? ( diff --git a/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js b/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js new file mode 100644 index 000000000..1997017e7 --- /dev/null +++ b/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js @@ -0,0 +1,275 @@ +import React, { useEffect, useState } from 'react'; +import { + EuiFlexItem, + EuiFlexGroup, + EuiPopover, + EuiComboBox, + EuiButtonIcon, + EuiExpression, +} from '@elastic/eui'; +import * as _ from 'lodash'; +import { FormikFormRow, FormikInputWrapper } from '../../../../components/FormControls'; + +const ExpressionQuery = ({ + selections, + dataTestSubj, + onChange, + value, + defaultText, + label, + name = 'expressionQueries', +}) => { + const DEFAULT_DESCRIPTION = defaultText ? defaultText : 'Select'; + const OPERATORS = ['AND', 'OR', 'NOT']; + const [usedExpressions, setUsedExpressions] = useState([]); + + useEffect(() => { + let expressions = []; + if (value?.length) { + let values = [...value]; + if (OPERATORS.indexOf(values[0]?.description) === -1) values = ['', ...values]; + + let counter = 0; + values.map((exp, idx) => { + if (idx % 2 === 0) { + expressions.push({ + description: exp.description, + isOpen: false, + monitor_name: '', + monitor_id: '', + }); + counter++; + } else { + const currentIndex = idx - counter; + expressions[currentIndex] = { ...expressions[currentIndex], ...exp }; + } + }); + } else { + expressions = []; + } + + setUsedExpressions(expressions); + }, []); + + const getValue = (expressions) => + expressions.map((exp) => ({ + condition: _.toLower(exp.description), + monitor_id: exp.monitor_id, + monitor_name: exp.name, + })); + + const changeMonitor = (selection, exp, idx, form) => { + const expressions = _.cloneDeep(usedExpressions); + expressions[idx] = { + ...expressions[idx], + monitor_id: selection[0].monitor_id, + monitor_name: selection[0].label, + }; + + setUsedExpressions(expressions); + onBlur(form, expressions); + }; + + const changeCondition = (selection, exp, idx, form) => { + const expressions = _.cloneDeep(usedExpressions); + expressions[idx] = { ...expressions[idx], description: selection[0].description }; + setUsedExpressions(expressions); + onBlur(form, expressions); + }; + + const onBlur = (form, expressions) => { + form.setFieldTouched('expressionQueries', true); + form.setFieldValue('triggerDefinitions[0].triggerConditions', getValue(expressions)); + form.setFieldError('expressionQueries', validate()); + }; + + const openPopover = (idx) => { + const expressions = _.cloneDeep(usedExpressions); + expressions[idx] = { ...expressions[idx], isOpen: !expressions[idx].isOpen }; + setUsedExpressions(expressions); + }; + + const closePopover = (idx) => { + const expressions = _.cloneDeep(usedExpressions); + expressions[idx] = { ...expressions[idx], isOpen: false }; + setUsedExpressions(expressions); + }; + + const renderOptions = (expression, idx, form) => ( + + + changeCondition(selection, expression, idx, form)} + options={[ + { description: '', label: '' }, + { description: 'AND', label: 'AND' }, + { description: 'OR', label: 'OR' }, + { description: 'NOT', label: 'NOT' }, + ]} + /> + + {renderMonitorOptions(expression, idx, form)} + + { + const usedExp = _.cloneDeep(usedExpressions); + usedExp.splice(idx, 1); + usedExp.length && (usedExp[0].description = ''); + setUsedExpressions([...usedExp]); + }} + iconType={'trash'} + color="danger" + aria-label={'Remove condition'} + style={{ marginTop: '4px' }} + /> + + + ); + + const renderMonitorOptions = (expression, idx, form) => ( + changeMonitor(selection, expression, idx, form)} + selectedOptions={[ + { + label: expression.monitor_name, + monitor_id: expression.monitor_id, + }, + ]} + style={{ width: '250px' }} + options={(() => { + const differences = _.differenceBy(selections, usedExpressions, 'monitor_id'); + return [ + { + monitor_id: expression.monitor_id, + label: expression.monitor_name, + }, + ...differences.map((sel) => ({ + monitor_id: sel.monitor_id, + label: sel.label, + })), + ]; + })()} + /> + ); + + const isValid = () => usedExpressions.length > 1; + + const validate = () => { + if (!isValid()) return 'At least two monitors should be selected.'; + }; + + return ( + validate(), + }} + render={({ field, form }) => ( + form.touched['expressionQueries'] && !isValid(), + error: () => validate(), + }} + > + + {!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' }} + /> + + )} + + + )} + /> + ); +}; + +export default ExpressionQuery; diff --git a/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.test.ts b/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.test.ts new file mode 100644 index 000000000..49e1b709c --- /dev/null +++ b/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.test.ts @@ -0,0 +1,14 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { render } from '@testing-library/react'; + +describe(' spec', () => { + it('renders the component', () => { + const tree = render(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/public/pages/CreateTrigger/containers/CreateTrigger/utils/formikToTrigger.js b/public/pages/CreateTrigger/containers/CreateTrigger/utils/formikToTrigger.js index dfae41837..7d34ad1f2 100644 --- a/public/pages/CreateTrigger/containers/CreateTrigger/utils/formikToTrigger.js +++ b/public/pages/CreateTrigger/containers/CreateTrigger/utils/formikToTrigger.js @@ -35,6 +35,8 @@ export function formikToTriggerDefinition(values, monitorUiMetadata) { return formikToBucketLevelTrigger(values, monitorUiMetadata); case MONITOR_TYPE.DOC_LEVEL: return formikToDocumentLevelTrigger(values, monitorUiMetadata); + case MONITOR_TYPE.COMPOSITE_LEVEL: + return formikToCompositeLevelTrigger(values, monitorUiMetadata); default: return formikToQueryLevelTrigger(values, monitorUiMetadata); } @@ -86,6 +88,20 @@ export function formikToDocumentLevelTrigger(values, monitorUiMetadata) { }; } +export function formikToCompositeLevelTrigger(values, monitorUiMetadata) { + const condition = formikToCompositeTriggerCondition(values, monitorUiMetadata); + const actions = formikToCompositeTriggerAction(values); + return { + chained_alert_trigger: { + id: values.id, + name: values.name, + severity: values.severity, + condition: condition, + actions: actions, + }, + }; +} + export function formikToDocumentLevelTriggerCondition(values, monitorUiMetadata) { const triggerConditions = _.get(values, 'triggerConditions', []); const searchType = _.get(monitorUiMetadata, 'search.searchType', SEARCH_TYPE.QUERY); @@ -99,6 +115,28 @@ export function formikToDocumentLevelTriggerCondition(values, monitorUiMetadata) }; } +export function formikToCompositeTriggerCondition(values, monitorUiMetadata) { + const conditionMap = { + and: '&&', + or: '||', + not: '!', + '': '', + }; + + const triggerConditions = _.get(values, 'triggerConditions', []); + const source = triggerConditions.reduce((query, expression) => { + query += ` ${conditionMap[expression.condition]} (monitor[id=${expression.monitor_id}])`; + return query.trim(); + }, ''); + + return { + script: { + lang: 'painless', + source: source, + }, + }; +} + export function getDocumentLevelScriptSource(conditions) { const scriptSourceContents = []; conditions.forEach((condition) => { @@ -171,6 +209,51 @@ export function formikToBucketLevelTriggerAction(values) { return actions; } +export function formikToCompositeTriggerAction(values) { + const actions = values.actions; + const executionPolicyPath = 'action_execution_policy.action_execution_scope'; + if (actions && actions.length > 0) { + return actions.map((action) => { + let formattedAction = _.cloneDeep(action); + + switch (formattedAction.throttle_enabled) { + case true: + _.set(formattedAction, 'throttle.unit', FORMIK_INITIAL_ACTION_VALUES.throttle.unit); + break; + case false: + formattedAction = _.omit(formattedAction, ['throttle']); + break; + } + + const notifyOption = _.get(formattedAction, `${executionPolicyPath}`); + const notifyOptionId = _.isString(notifyOption) ? notifyOption : _.keys(notifyOption)[0]; + switch (notifyOptionId) { + case NOTIFY_OPTIONS_VALUES.PER_ALERT: + const actionableAlerts = _.get( + formattedAction, + `${executionPolicyPath}.${NOTIFY_OPTIONS_VALUES.PER_ALERT}.actionable_alerts`, + [] + ); + _.set( + formattedAction, + `${executionPolicyPath}.${NOTIFY_OPTIONS_VALUES.PER_ALERT}.actionable_alerts`, + actionableAlerts.map((entry) => entry.value) + ); + break; + case NOTIFY_OPTIONS_VALUES.PER_EXECUTION: + _.set( + formattedAction, + `${executionPolicyPath}.${NOTIFY_OPTIONS_VALUES.PER_EXECUTION}`, + {} + ); + break; + } + return formattedAction; + }); + } + return actions; +} + export function formikToTriggerUiMetadata(values, monitorUiMetadata) { switch (monitorUiMetadata.monitor_type) { case MONITOR_TYPE.QUERY_LEVEL: @@ -221,6 +304,13 @@ export function formikToTriggerUiMetadata(values, monitorUiMetadata) { docLevelTriggersUiMetadata[trigger.name] = triggerMetadata; }); return docLevelTriggersUiMetadata; + + case MONITOR_TYPE.COMPOSITE_LEVEL: + const compositeTriggersUiMetadata = {}; + _.get(values, 'triggerDefinitions', []).forEach((trigger) => { + compositeTriggersUiMetadata[trigger.name] = _.get(trigger, 'triggerConditions', []); + }); + return compositeTriggersUiMetadata; } } diff --git a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/CompositeMonitorsAlertTrigger.js b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/CompositeMonitorsAlertTrigger.js new file mode 100644 index 000000000..b5d40400a --- /dev/null +++ b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/CompositeMonitorsAlertTrigger.js @@ -0,0 +1,38 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { Fragment } from 'react'; +import DefineCompositeLevelTrigger from './DefineCompositeLevelTrigger'; + +const CompositeMonitorsAlertTrigger = ({ + edit, + triggerArrayHelpers, + monitor, + monitorValues, + triggerValues, + isDarkMode, + httpClient, + notifications, + notificationService, + plugins, +}) => { + return ( + + + + ); +}; +export default CompositeMonitorsAlertTrigger; diff --git a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/DefineCompositeLevelTrigger.js b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/DefineCompositeLevelTrigger.js new file mode 100644 index 000000000..035817ff0 --- /dev/null +++ b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/DefineCompositeLevelTrigger.js @@ -0,0 +1,169 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import _ from 'lodash'; +import { EuiAccordion, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; +import { FormikFieldText, FormikSelect } from '../../../../components/FormControls'; +import { hasError, isInvalid } from '../../../../utils/validate'; +import { DEFAULT_TRIGGER_NAME, SEVERITY_OPTIONS } from '../../utils/constants'; +import { validateTriggerName } from '../DefineTrigger/utils/validation'; +import ExpressionQuery from '../../components/ExpressionQuery/ExpressionQuery'; +import TriggerNotifications from './TriggerNotifications'; +import ContentPanel from '../../../../components/ContentPanel'; +import { FORMIK_INITIAL_TRIGGER_VALUES } from '../CreateTrigger/utils/constants'; + +const defaultRowProps = { + label: 'Trigger name', + style: { paddingLeft: '10px' }, + isInvalid, + error: hasError, +}; + +const defaultInputProps = { isInvalid }; + +const selectFieldProps = { + validate: () => {}, +}; + +const selectRowProps = { + label: 'Severity level', + style: { paddingLeft: '10px', marginTop: '0px' }, + isInvalid, + error: hasError, +}; + +const selectInputProps = { + options: SEVERITY_OPTIONS, +}; + +const propTypes = { + monitorValues: PropTypes.object.isRequired, + triggerValues: PropTypes.object.isRequired, + isDarkMode: PropTypes.bool.isRequired, +}; + +export const titleTemplate = (title, subTitle) => ( + +
{title}
+ {subTitle && ( + + {subTitle} + + )} + +
+); + +class DefineCompositeLevelTrigger extends Component { + constructor(props) { + super(props); + this.state = {}; + } + + render() { + const { + edit, + monitorValues, + triggers, + triggerValues, + isDarkMode, + httpClient, + notifications, + notificationService, + plugins, + selectedMonitors, + } = this.props; + const fieldPath = `triggerDefinitions[0].`; + const triggerName = _.get(triggerValues, `${fieldPath}name`, DEFAULT_TRIGGER_NAME); + const triggerDefinitions = _.get(triggerValues, 'triggerDefinitions', []); + _.set(triggerValues, 'triggerDefinitions', [ + { + ...FORMIK_INITIAL_TRIGGER_VALUES, + ...triggerDefinitions[0], + severity: 1, + name: triggerName, + }, + ]); + + const monitorList = monitorValues?.associatedMonitors + ? monitorValues.associatedMonitors?.map((monitor) => ({ + label: monitor.label.replaceAll(' ', '_'), + monitor_id: monitor.value, + })) + : []; + + return ( + + + + + + + + [monitor, { description: 'and' }])) + .slice(0, -1)} + onChange={() => {}} + dataTestSubj={'composite_expression_query'} + defaultText={'Select associated monitor'} + /> + + + + {titleTemplate('Alert severity')} + + + + + + + ); + } +} + +DefineCompositeLevelTrigger.propTypes = propTypes; + +export default DefineCompositeLevelTrigger; diff --git a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/NotificationConfigDialog.js b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/NotificationConfigDialog.js new file mode 100644 index 000000000..aedb55566 --- /dev/null +++ b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/NotificationConfigDialog.js @@ -0,0 +1,149 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { Fragment } from 'react'; +import { + EuiButton, + EuiSpacer, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, +} from '@elastic/eui'; +import { titleTemplate } from './DefineCompositeLevelTrigger'; +import Message from '../../components/Action/actions'; +import { DEFAULT_MESSAGE_SOURCE, FORMIK_INITIAL_ACTION_VALUES } from '../../utils/constants'; +import { getTriggerContext } from '../../utils/helper'; +import { formikToMonitor } from '../../../CreateMonitor/containers/CreateMonitor/utils/formikToMonitor'; +import _ from 'lodash'; +import { formikToTrigger } from '../CreateTrigger/utils/formikToTrigger'; +import { MONITOR_TYPE } from '../../../../utils/constants'; +import { TRIGGER_TYPE } from '../CreateTrigger/utils/constants'; +import { backendErrorNotification } from '../../../../utils/helpers'; +import { checkForError } from '../ConfigureActions/ConfigureActions'; + +const NotificationConfigDialog = ({ + channel, + closeModal, + triggerValues, + httpClient, + notifications, +}) => { + const triggerIndex = 0; + const monitor = formikToMonitor(triggerValues); + const context = getTriggerContext({}, monitor, triggerValues, 0); + + const initialActionValues = _.cloneDeep(FORMIK_INITIAL_ACTION_VALUES); + let action = _.get(triggerValues, 'triggerDefinitions[0].actions[0]', { + ...initialActionValues, + }); + + const sendTestMessage = async (index) => { + const flattenedDestinations = []; + // TODO: For bucket-level triggers, sendTestMessage will only send a test message if there is + // at least one bucket of data from the monitor input query. + let testTrigger = _.cloneDeep( + formikToTrigger(triggerValues, monitor.ui_metadata)[triggerIndex] + ); + let action; + let condition; + + switch (monitor.monitor_type) { + case MONITOR_TYPE.BUCKET_LEVEL: + action = _.get(testTrigger, `${TRIGGER_TYPE.BUCKET_LEVEL}.actions[${index}]`); + condition = { + ..._.get(testTrigger, `${TRIGGER_TYPE.BUCKET_LEVEL}.condition`), + buckets_path: { _count: '_count' }, + script: { + source: 'params._count >= 0', + }, + }; + _.set(testTrigger, `${TRIGGER_TYPE.BUCKET_LEVEL}.actions`, [action]); + _.set(testTrigger, `${TRIGGER_TYPE.BUCKET_LEVEL}.condition`, condition); + break; + case MONITOR_TYPE.DOC_LEVEL: + action = _.get(testTrigger, `${TRIGGER_TYPE.DOC_LEVEL}.actions[${index}]`); + condition = { + ..._.get(testTrigger, `${TRIGGER_TYPE.DOC_LEVEL}.condition`), + script: { lang: 'painless', source: 'return true' }, + }; + _.set(testTrigger, `${TRIGGER_TYPE.DOC_LEVEL}.actions`, [action]); + _.set(testTrigger, `${TRIGGER_TYPE.DOC_LEVEL}.condition`, condition); + break; + default: + action = _.get(testTrigger, `actions[${index}]`); + condition = { + ..._.get(testTrigger, 'condition'), + script: { lang: 'painless', source: 'return true' }, + }; + _.set(testTrigger, 'actions', [action]); + _.set(testTrigger, 'condition', condition); + break; + } + + const testMonitor = { ...monitor, triggers: [{ ...testTrigger }] }; + + try { + const response = await httpClient.post('../api/alerting/monitors/_execute', { + query: { dryrun: false }, + body: JSON.stringify(testMonitor), + }); + let error = null; + if (response.ok) { + error = checkForError(response, error); + if (!_.isEmpty(action.destination_id)) { + const destinationName = _.get( + _.find(flattenedDestinations, { value: action.destination_id }), + 'label' + ); + notifications.toasts.addSuccess(`Test message sent to "${destinationName}."`); + } + } + if (error || !response.ok) { + const errorMessage = error == null ? response.resp : error; + console.error('There was an error trying to send test message', errorMessage); + backendErrorNotification(notifications, 'send', 'test message', errorMessage); + } + } catch (err) { + console.error('There was an error trying to send test message', err); + } + }; + + console.log('ACTION', action); + return ( + closeModal()}> + + +

Configure notification

+
+
+ + {titleTemplate('Customize message')} + + + + + + + closeModal()}>Close + closeModal()} fill> + Update + + +
+ ); +}; + +export default NotificationConfigDialog; diff --git a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotifications.js b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotifications.js new file mode 100644 index 000000000..67eb337af --- /dev/null +++ b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotifications.js @@ -0,0 +1,140 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { Fragment, useState, useEffect } from 'react'; +import { + EuiSpacer, + EuiButton, + EuiText, + EuiAccordion, + EuiHorizontalRule, + EuiButtonIcon, +} from '@elastic/eui'; +import TriggerNotificationsContent from './TriggerNotificationsContent'; +import { titleTemplate } from './DefineCompositeLevelTrigger'; +import { MAX_CHANNELS_RESULT_SIZE, OS_NOTIFICATION_PLUGIN } from '../../../../utils/constants'; +import { CHANNEL_TYPES } from '../../utils/constants'; + +const TriggerNotifications = ({ + httpClient, + actions = [], + plugins, + notifications, + notificationService, + triggerValues, +}) => { + const [channels, setChannels] = useState([]); + const [options, setOptions] = useState([]); + + useEffect(() => { + let newChannels = [...actions]; + if (_.isEmpty(newChannels)) + newChannels = [ + { + name: '', + id: '', + }, + ]; + setChannels(newChannels); + + getChannels().then((channels) => setOptions(channels)); + }, []); + + const getChannels = async () => { + const hasNotificationPlugin = plugins.indexOf(OS_NOTIFICATION_PLUGIN) !== -1; + + let channels = []; + let index = 0; + const getChannels = async () => { + const getChannelsQuery = { + from_index: index, + max_items: MAX_CHANNELS_RESULT_SIZE, + config_type: CHANNEL_TYPES, + sort_field: 'name', + sort_order: 'asc', + }; + + const channelsResponse = await notificationService.getChannels(getChannelsQuery); + + channels = channels.concat( + channelsResponse.items.map((channel) => ({ + label: channel.name, + value: channel.config_id, + type: channel.config_type, + description: channel.description, + })) + ); + + if (channelsResponse.total && channels.length < channelsResponse.total) { + index += MAX_CHANNELS_RESULT_SIZE; + await getChannels(); + } + }; + + if (hasNotificationPlugin) { + await getChannels(); + } + + return channels; + }; + + const onAddNotification = () => { + const newChannels = [...channels]; + newChannels.push({ + label: '', + value: '', + }); + setChannels(newChannels); + }; + + const onRemoveNotification = (idx) => { + const newChannels = [...channels]; + newChannels.splice(idx, 1); + setChannels(newChannels); + }; + + return ( + + {titleTemplate('Notifications')} + + {channels.length && + channels.map((channel, idx) => ( + {`Notification ${idx + 1}`}} + paddingSize={'s'} + extraAction={ + channels.length > 1 && ( + onRemoveNotification(idx)} + size={'s'} + /> + ) + } + > + + + ))} + + onAddNotification()}>Add notification + + ); +}; + +export default TriggerNotifications; diff --git a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotificationsContent.js b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotificationsContent.js new file mode 100644 index 000000000..3fd2dabb5 --- /dev/null +++ b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotificationsContent.js @@ -0,0 +1,91 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { Fragment, useState } from 'react'; +import { EuiPanel, EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiSpacer } from '@elastic/eui'; +import { FormikComboBox } from '../../../../components/FormControls'; +import NotificationConfigDialog from './NotificationConfigDialog'; +import _ from 'lodash'; +import { FORMIK_INITIAL_ACTION_VALUES } from '../../utils/constants'; + +const TriggerNotificationsContent = ({ + channel, + options, + idx, + triggerValues, + httpClient, + notifications, +}) => { + const [selected, setSelected] = useState([]); + const [isModalVisible, setIsModalVisible] = useState(false); + + const onChange = (selectedOptions) => { + setSelected(selectedOptions); + const initialActionValues = _.cloneDeep(FORMIK_INITIAL_ACTION_VALUES); + _.set(triggerValues, 'triggerDefinitions[0].actions[0]', initialActionValues); + }; + + const showConfig = (channels) => setIsModalVisible(true); + + return ( + + + + + onChange(selectedOptions), + singleSelection: { asPlainText: true }, + }} + /> + + + + Manage channels + + + + {selected.length ? ( + + + showConfig()}> + Configure notification + + + ) : null} + + {isModalVisible && ( + setIsModalVisible(false)} + channel={channel} + triggerValues={triggerValues} + httpClient={httpClient} + notifications={notifications} + /> + )} + + ); +}; + +export default TriggerNotificationsContent; diff --git a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/index.js b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/index.js new file mode 100644 index 000000000..dff1a2d91 --- /dev/null +++ b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/index.js @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import DefineCompositeLevelTrigger from './DefineCompositeLevelTrigger'; + +export default DefineCompositeLevelTrigger; diff --git a/public/utils/constants.js b/public/utils/constants.js index 9511ae0d2..cfe51489c 100644 --- a/public/utils/constants.js +++ b/public/utils/constants.js @@ -30,6 +30,7 @@ export const MONITOR_TYPE = { BUCKET_LEVEL: 'bucket_level_monitor', CLUSTER_METRICS: 'cluster_metrics_monitor', DOC_LEVEL: 'doc_level_monitor', + COMPOSITE_LEVEL: 'composite', }; export const DESTINATION_ACTIONS = { diff --git a/server/clusters/alerting/alertingPlugin.js b/server/clusters/alerting/alertingPlugin.js index 8564d0020..46cde5562 100644 --- a/server/clusters/alerting/alertingPlugin.js +++ b/server/clusters/alerting/alertingPlugin.js @@ -46,6 +46,14 @@ export default function alertingPlugin(Client, config, components) { method: 'POST', }); + alerting.createWorkflow = ca({ + url: { + fmt: `${API_ROUTE_PREFIX}/workflows?refresh=wait_for`, + }, + needBody: true, + method: 'POST', + }); + alerting.deleteMonitor = ca({ url: { fmt: `${MONITOR_BASE_API}/<%=monitorId%>`, diff --git a/server/routes/monitors.js b/server/routes/monitors.js index 6cc4967c0..a447327df 100644 --- a/server/routes/monitors.js +++ b/server/routes/monitors.js @@ -45,6 +45,16 @@ export default function (services, router) { monitorService.createMonitor ); + router.post( + { + path: '/api/alerting/workflows', + validate: { + body: schema.any(), + }, + }, + monitorService.createWorkflow + ); + router.post( { path: '/api/alerting/monitors/_execute', diff --git a/server/services/MonitorService.js b/server/services/MonitorService.js index 81d788520..0ed7554ac 100644 --- a/server/services/MonitorService.js +++ b/server/services/MonitorService.js @@ -35,6 +35,28 @@ export default class MonitorService { } }; + createWorkflow = async (context, req, res) => { + try { + const params = { body: req.body }; + const { callAsCurrentUser } = await this.esDriver.asScoped(req); + const createResponse = await callAsCurrentUser('alerting.createWorkflow', params); + return res.ok({ + body: { + ok: true, + resp: createResponse, + }, + }); + } catch (err) { + console.error('Alerting - MonitorService - createWorkflow:', err); + return res.ok({ + body: { + ok: false, + resp: err.message, + }, + }); + } + }; + deleteMonitor = async (context, req, res) => { try { const { id } = req.params; diff --git a/yarn.lock b/yarn.lock index 4c675f913..d35b800d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -232,90 +232,6 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" -"@node-rs/xxhash-android-arm-eabi@1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@node-rs/xxhash-android-arm-eabi/-/xxhash-android-arm-eabi-1.4.0.tgz#55ace4d3882686d1e379aaf613e1338d78f13fc8" - integrity sha512-JuZNqt5/znWkIGteikQdS+HT9S0JsMYi06S4yzU/sMKLCIPvD0MnCTXlYtuDcgRIKScCaepAsSQVomnAyLFNNA== - -"@node-rs/xxhash-android-arm64@1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@node-rs/xxhash-android-arm64/-/xxhash-android-arm64-1.4.0.tgz#2290c53ceabda804afb4c45679613d833a6385a0" - integrity sha512-BZzQO5jlgsIr9HhiqTwZjYqlfVeZiu+7PaoAdNEOq+i/SjyAqv1jGSkyek4rBSAiodyNkXcbE0eQtomeN6a55w== - -"@node-rs/xxhash-darwin-arm64@1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@node-rs/xxhash-darwin-arm64/-/xxhash-darwin-arm64-1.4.0.tgz#96df4f48b13deb6899e84ed0882bdbd0a4856f13" - integrity sha512-JlEAzTsQaqJaWVse/JP//6QKBIhzqzTlvNY4uEbi8TaZMfvDDhW//ClXM6CkSV799GJxAYPu1LXa4+OeBQpa7Q== - -"@node-rs/xxhash-darwin-x64@1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@node-rs/xxhash-darwin-x64/-/xxhash-darwin-x64-1.4.0.tgz#9df3ca3a87354dd5386aadfa20ad032a299c2b8f" - integrity sha512-9ycVJfzLvw1wc6Tgq0giLkMn5nGOBawTeOA17t27dQFdY/scZPz583DO7w+eznMnlzUXwoLiloanUebRhy+piQ== - -"@node-rs/xxhash-freebsd-x64@1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@node-rs/xxhash-freebsd-x64/-/xxhash-freebsd-x64-1.4.0.tgz#24b0c0bfd33429303688b4af78f9d323daa0fb5b" - integrity sha512-vFRDr6qA0gHWQDjuSxXcdzM4Ppk+5VebEhc76zkWrRVc6RG60fxLo5B4j6QwMwXGTYaG8HMv/nQhAgbnOCWWxQ== - -"@node-rs/xxhash-linux-arm-gnueabihf@1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@node-rs/xxhash-linux-arm-gnueabihf/-/xxhash-linux-arm-gnueabihf-1.4.0.tgz#4c09f70cd39429fb1a52f3567085e949603d4817" - integrity sha512-0KS6y1caqbtPanos9XNMekWpozCHA6QSlQzaZyn9Hn+Z+mYpR5+NoWixefhp06jt59qF9+LkkF3C9fSEHYmq/w== - -"@node-rs/xxhash-linux-arm64-gnu@1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@node-rs/xxhash-linux-arm64-gnu/-/xxhash-linux-arm64-gnu-1.4.0.tgz#e92d7026614506fb4db309977127fd8589fabd7c" - integrity sha512-QI97JK2qiQhVgRtUBMgA1ZjPLpwnz11SE2Mw1jryejmyH9EXKKiCyt2FweO6MVP7bEuMxcdajBho4pEL7s/QsA== - -"@node-rs/xxhash-linux-arm64-musl@1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@node-rs/xxhash-linux-arm64-musl/-/xxhash-linux-arm64-musl-1.4.0.tgz#a8b16233a86c116e6af32a69278248d17b2d09e7" - integrity sha512-dtMid4OMkNBYGJkjoT1jdkENpV8m8MGp3lliDN8C+2znZUQM8KFRTXRkfaq4lgzu3Y2XeYzsLOoBsBd3Hgf7gA== - -"@node-rs/xxhash-linux-x64-gnu@1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@node-rs/xxhash-linux-x64-gnu/-/xxhash-linux-x64-gnu-1.4.0.tgz#385ec91396ebaa2b73abf419be3971ec893dcbd1" - integrity sha512-OeOQL10cG62wL1IVoeC74xESmefHU7r3xiZMTP2hK5Dh3FdF2sa3x/Db9BcGXlaokg/lMGDxuTuzOLC2Rv/wlQ== - -"@node-rs/xxhash-linux-x64-musl@1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@node-rs/xxhash-linux-x64-musl/-/xxhash-linux-x64-musl-1.4.0.tgz#715bb962502b0ec69e1fc19db22ac035c63d30c7" - integrity sha512-kZ8wNi5bH9b+ZpuPlSbFd6JXk8CKbfCvCPZ0Vk0IqLkzB6PihQflnZPM9r0QZ2jtFgyfWmpbFK4YxwX9YcyLog== - -"@node-rs/xxhash-win32-arm64-msvc@1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@node-rs/xxhash-win32-arm64-msvc/-/xxhash-win32-arm64-msvc-1.4.0.tgz#4a3a4ebcb50c73e4309e429b28eb44dbf8f7f71f" - integrity sha512-Ggv66jlhQvj4XgQqNgl2JKQ7My/97PvPZi5jKbcS7t65wJC36J6XERQwRPdupO8UH63XfPqb7HJqrgmiz8tmlA== - -"@node-rs/xxhash-win32-ia32-msvc@1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@node-rs/xxhash-win32-ia32-msvc/-/xxhash-win32-ia32-msvc-1.4.0.tgz#fdfdb43e41113a8baf15779ca53bb637d2e1bc8f" - integrity sha512-mYpF1+7unqKKGsPn7Y8X6SqP2Bc5BU5dsHBKhAGAuvrMg9W63zM+YWM8/fpNGfFlOrjiKRvXHZ96nrZyzoxeBw== - -"@node-rs/xxhash-win32-x64-msvc@1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@node-rs/xxhash-win32-x64-msvc/-/xxhash-win32-x64-msvc-1.4.0.tgz#aee714a4ae0121f3947f94139adf13f5b6d93d12" - integrity sha512-rKuqWHuQNlrfjIOkQW3oCBta/GUlyVoUkKB13aVr8uixOs/eneuDaYJx2h02FAAWlWCKADFnMxgDl0LVFBy53w== - -"@node-rs/xxhash@^1.3.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@node-rs/xxhash/-/xxhash-1.4.0.tgz#1e75850e0e530c9224e8e5ba4056d52e8868291b" - integrity sha512-UpSOParhMqbQ7hsYovN2e+uqvWqHJiCDvFl8gDzMcXgBY/PkI2zo2zhdRAZdz48c6/dke+0WjCKy90wDVQxS6g== - optionalDependencies: - "@node-rs/xxhash-android-arm-eabi" "1.4.0" - "@node-rs/xxhash-android-arm64" "1.4.0" - "@node-rs/xxhash-darwin-arm64" "1.4.0" - "@node-rs/xxhash-darwin-x64" "1.4.0" - "@node-rs/xxhash-freebsd-x64" "1.4.0" - "@node-rs/xxhash-linux-arm-gnueabihf" "1.4.0" - "@node-rs/xxhash-linux-arm64-gnu" "1.4.0" - "@node-rs/xxhash-linux-arm64-musl" "1.4.0" - "@node-rs/xxhash-linux-x64-gnu" "1.4.0" - "@node-rs/xxhash-linux-x64-musl" "1.4.0" - "@node-rs/xxhash-win32-arm64-msvc" "1.4.0" - "@node-rs/xxhash-win32-ia32-msvc" "1.4.0" - "@node-rs/xxhash-win32-x64-msvc" "1.4.0" - "@samverschueren/stream-to-observable@^0.3.0": version "0.3.1" resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz#a21117b19ee9be70c379ec1877537ef2e1c63301" @@ -2952,7 +2868,7 @@ loader-runner@^2.4.0: resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw== -loader-utils@1.4.2, loader-utils@^2.0.4: +loader-utils@1.4.2, loader-utils@^1.2.3: version "1.4.2" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.2.tgz#29a957f3a63973883eb684f10ffd3d151fec01a3" integrity sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg== @@ -4491,12 +4407,11 @@ tapable@^1.0.0, tapable@^1.1.3: resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== -"terser-webpack-plugin@npm:@amoo-miki/terser-webpack-plugin@1.4.5-rc.2": - version "1.4.5-rc.2" - resolved "https://registry.yarnpkg.com/@amoo-miki/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5-rc.2.tgz#046c062ef22c126c2544718674bc6624e3651b9c" - integrity sha512-JFSGSzsWgSHEqQXlnHDh3gw+jdVdVlWM2Irdps9P/yWYNY/5VjuG8sdoW4mbuP8/HM893/k8N+ipbeqsd8/xpA== +terser-webpack-plugin@^1.4.3: + version "1.4.5" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz#a217aefaea330e734ffacb6120ec1fa312d6040b" + integrity sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw== dependencies: - "@node-rs/xxhash" "^1.3.0" cacache "^12.0.2" find-cache-dir "^2.1.0" is-wsl "^1.1.0" @@ -4789,12 +4704,11 @@ webpack-sources@^1.4.0, webpack-sources@^1.4.1: source-list-map "^2.0.0" source-map "~0.6.1" -"webpack@npm:@amoo-miki/webpack@4.46.0-rc.2": - version "4.46.0-rc.2" - resolved "https://registry.yarnpkg.com/@amoo-miki/webpack/-/webpack-4.46.0-rc.2.tgz#36824597c14557a7bb0a8e13203e30275e7b02bd" - integrity sha512-Y/ZqxTHOoDF1kz3SR63Y9SZGTDUpZNNFrisTRHofWhP8QvNX3LMN+TCmEP56UfLaiLVKMcaiFjx8kFb2TgyBaQ== +webpack@^4.41.5: + version "4.46.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.46.0.tgz#bf9b4404ea20a073605e0a011d188d77cb6ad542" + integrity sha512-6jJuJjg8znb/xRItk7bkT0+Q7AHCYjjFnvKIWQPkNIOyRqoCGvkOs0ipeQzrqz4l5FtN5ZI/ukEHroeX/o1/5Q== dependencies: - "@node-rs/xxhash" "^1.3.0" "@webassemblyjs/ast" "1.9.0" "@webassemblyjs/helper-module-context" "1.9.0" "@webassemblyjs/wasm-edit" "1.9.0" @@ -4807,7 +4721,7 @@ webpack-sources@^1.4.0, webpack-sources@^1.4.1: eslint-scope "^4.0.3" json-parse-better-errors "^1.0.2" loader-runner "^2.4.0" - loader-utils "^2.0.4" + loader-utils "^1.2.3" memory-fs "^0.4.1" micromatch "^3.1.10" mkdirp "^0.5.3" @@ -4815,7 +4729,7 @@ webpack-sources@^1.4.0, webpack-sources@^1.4.1: node-libs-browser "^2.2.1" schema-utils "^1.0.0" tapable "^1.1.3" - terser-webpack-plugin "npm:@amoo-miki/terser-webpack-plugin@1.4.5-rc.2" + terser-webpack-plugin "^1.4.3" watchpack "^1.7.4" webpack-sources "^1.4.1" From a8aa47a40617bd14c8290682c8fb3e1f2233774c Mon Sep 17 00:00:00 2001 From: Jovan Cvetkovic Date: Thu, 15 Jun 2023 20:16:28 +0200 Subject: [PATCH 03/63] [FEATURE] Add composite monitor type #573 Signed-off-by: Jovan Cvetkovic --- .../AssociateMonitors/components/MonitorsList.tsx | 2 +- .../components/Action/actions/Message.js | 12 +----------- .../components/ExpressionQuery/ExpressionQuery.js | 1 - .../DefineCompositeLevelTrigger.js | 6 ++---- .../NotificationConfigDialog.js | 4 ++-- .../TriggerNotificationsContent.js | 11 ++++++++++- 6 files changed, 16 insertions(+), 20 deletions(-) diff --git a/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.tsx b/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.tsx index 2fc7eccf1..e636cddd6 100644 --- a/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.tsx +++ b/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.tsx @@ -20,7 +20,7 @@ import { FormikFormRow, } from '../../../../../components/FormControls'; -const MonitorsList = ({ monitors = [], options = [], history }) => { +const MonitorsList = ({ monitors = [], options = [] }) => { const [selectedOptions, setSelectedOptions] = useState({}); const [monitorOptions, setMonitorOptions] = useState([]); const [monitorFields, setMonitorFields] = useState( diff --git a/public/pages/CreateTrigger/components/Action/actions/Message.js b/public/pages/CreateTrigger/components/Action/actions/Message.js index 7c5f5f5af..a5456d291 100644 --- a/public/pages/CreateTrigger/components/Action/actions/Message.js +++ b/public/pages/CreateTrigger/components/Action/actions/Message.js @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState, useEffect } from 'react'; +import React, { useState } from 'react'; import _ from 'lodash'; import Mustache from 'mustache'; import { @@ -209,16 +209,6 @@ export default function Message( console.error('There was an error rendering mustache template', err); } - console.log( - 'MESSAGE', - action, - context, - index, - isSubjectDisabled, - sendTestMessage, - fieldPath, - values - ); return (
{!isSubjectDisabled ? ( diff --git a/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js b/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js index 1997017e7..65ac5f761 100644 --- a/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js +++ b/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js @@ -13,7 +13,6 @@ import { FormikFormRow, FormikInputWrapper } from '../../../../components/FormCo const ExpressionQuery = ({ selections, dataTestSubj, - onChange, value, defaultText, label, diff --git a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/DefineCompositeLevelTrigger.js b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/DefineCompositeLevelTrigger.js index 035817ff0..0f70bb6dc 100644 --- a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/DefineCompositeLevelTrigger.js +++ b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/DefineCompositeLevelTrigger.js @@ -6,11 +6,10 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import _ from 'lodash'; -import { EuiAccordion, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; +import { EuiSpacer, EuiText } from '@elastic/eui'; import { FormikFieldText, FormikSelect } from '../../../../components/FormControls'; import { hasError, isInvalid } from '../../../../utils/validate'; import { DEFAULT_TRIGGER_NAME, SEVERITY_OPTIONS } from '../../utils/constants'; -import { validateTriggerName } from '../DefineTrigger/utils/validation'; import ExpressionQuery from '../../components/ExpressionQuery/ExpressionQuery'; import TriggerNotifications from './TriggerNotifications'; import ContentPanel from '../../../../components/ContentPanel'; @@ -75,10 +74,9 @@ class DefineCompositeLevelTrigger extends Component { notifications, notificationService, plugins, - selectedMonitors, } = this.props; const fieldPath = `triggerDefinitions[0].`; - const triggerName = _.get(triggerValues, `${fieldPath}name`, DEFAULT_TRIGGER_NAME); + const triggerName = _.get(triggerValues, `${fieldPath}name`, 'Trigger'); const triggerDefinitions = _.get(triggerValues, 'triggerDefinitions', []); _.set(triggerValues, 'triggerDefinitions', [ { diff --git a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/NotificationConfigDialog.js b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/NotificationConfigDialog.js index aedb55566..375075426 100644 --- a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/NotificationConfigDialog.js +++ b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/NotificationConfigDialog.js @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { Fragment } from 'react'; +import React from 'react'; import { EuiButton, EuiSpacer, @@ -15,7 +15,7 @@ import { } from '@elastic/eui'; import { titleTemplate } from './DefineCompositeLevelTrigger'; import Message from '../../components/Action/actions'; -import { DEFAULT_MESSAGE_SOURCE, FORMIK_INITIAL_ACTION_VALUES } from '../../utils/constants'; +import { FORMIK_INITIAL_ACTION_VALUES } from '../../utils/constants'; import { getTriggerContext } from '../../utils/helper'; import { formikToMonitor } from '../../../CreateMonitor/containers/CreateMonitor/utils/formikToMonitor'; import _ from 'lodash'; diff --git a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotificationsContent.js b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotificationsContent.js index 3fd2dabb5..a54fee443 100644 --- a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotificationsContent.js +++ b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotificationsContent.js @@ -24,7 +24,16 @@ const TriggerNotificationsContent = ({ const onChange = (selectedOptions) => { setSelected(selectedOptions); const initialActionValues = _.cloneDeep(FORMIK_INITIAL_ACTION_VALUES); - _.set(triggerValues, 'triggerDefinitions[0].actions[0]', initialActionValues); + _.set(triggerValues, 'triggerDefinitions[0].actions[0]', { + ...initialActionValues, + destination_id: selectedOptions[0]?.value, + subject_template: { + lang: 'mustache', + source: 'Monitor {{ctx.monitor.name}} triggered an alert {{ctx.trigger.name}}', + }, + }); + + console.log('CHANNEL SELECTED', _.get(triggerValues, 'triggerDefinitions[0].actions[0]', {})); }; const showConfig = (channels) => setIsModalVisible(true); From 83d6eb6218c28e7a16a649b9bf458f9a7c108e6f Mon Sep 17 00:00:00 2001 From: Jovan Cvetkovic Date: Fri, 16 Jun 2023 11:52:47 +0200 Subject: [PATCH 04/63] [FEATURE] Add composite monitor type #573 Signed-off-by: Jovan Cvetkovic --- .../containers/CreateMonitor/CreateMonitor.js | 11 +++- .../CreateMonitor/utils/formikToMonitor.js | 4 +- .../ExpressionQuery/ExpressionQuery.js | 1 + .../CreateTrigger/utils/constants.js | 1 + .../CreateTrigger/utils/formikToTrigger.js | 1 + .../DefineCompositeLevelTrigger.js | 4 +- .../NotificationConfigDialog.js | 62 +++++-------------- .../TriggerNotifications.js | 45 +++++++------- .../TriggerNotificationsContent.js | 11 ++-- 9 files changed, 59 insertions(+), 81 deletions(-) diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/CreateMonitor.js b/public/pages/CreateMonitor/containers/CreateMonitor/CreateMonitor.js index 700f31039..f98a29a17 100644 --- a/public/pages/CreateMonitor/containers/CreateMonitor/CreateMonitor.js +++ b/public/pages/CreateMonitor/containers/CreateMonitor/CreateMonitor.js @@ -114,7 +114,9 @@ export default class CreateMonitor extends Component { try { const resp = await httpClient.post( `../api/alerting/${ - monitor.monitor_type === MONITOR_TYPE.COMPOSITE_LEVEL ? 'workflows' : 'monitors' + monitor.workflow_type && monitor.workflow_type === MONITOR_TYPE.COMPOSITE_LEVEL + ? 'workflows' + : 'monitors' }`, { body: JSON.stringify(monitor), @@ -183,6 +185,9 @@ export default class CreateMonitor extends Component { case MONITOR_TYPE.DOC_LEVEL: triggerType = TRIGGER_TYPE.DOC_LEVEL; break; + case MONITOR_TYPE.COMPOSITE_LEVEL: + triggerType = TRIGGER_TYPE.COMPOSITE_LEVEL; + break; default: triggerType = TRIGGER_TYPE.QUERY_LEVEL; break; @@ -249,8 +254,8 @@ export default class CreateMonitor extends Component { monitor = { ...monitor, ...triggers }; } - console.log('Monitor', monitor); - console.log('Value', values); + if (monitor.monitor_type === MONITOR_TYPE.COMPOSITE_LEVEL) delete monitor.monitor_type; + if (edit) this.onUpdate(monitor, formikBag); else this.onCreate(monitor, formikBag); } diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor.js b/public/pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor.js index ec0b03e28..6c57cb5a8 100644 --- a/public/pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor.js +++ b/public/pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor.js @@ -48,7 +48,8 @@ export function formikToMonitor(values) { type: 'workflow', enabled_time: enabled_time.getTime(), enabled: !values.disabled, - workflow_type: 'composite', + monitor_type: MONITOR_TYPE.COMPOSITE_LEVEL, + workflow_type: MONITOR_TYPE.COMPOSITE_LEVEL, schema_version: 0, name: values.name, schedule, @@ -56,6 +57,7 @@ export function formikToMonitor(values) { triggers: [], ui_metadata: { schedule: uiSchedule, + monitor_type: values.monitor_type, ...monitorUiMetadata(), }, }; diff --git a/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js b/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js index 65ac5f761..13180ce09 100644 --- a/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js +++ b/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js @@ -17,6 +17,7 @@ const ExpressionQuery = ({ defaultText, label, name = 'expressionQueries', + triggerValues, }) => { const DEFAULT_DESCRIPTION = defaultText ? defaultText : 'Select'; const OPERATORS = ['AND', 'OR', 'NOT']; diff --git a/public/pages/CreateTrigger/containers/CreateTrigger/utils/constants.js b/public/pages/CreateTrigger/containers/CreateTrigger/utils/constants.js index 3af7fc473..a84dea9b1 100644 --- a/public/pages/CreateTrigger/containers/CreateTrigger/utils/constants.js +++ b/public/pages/CreateTrigger/containers/CreateTrigger/utils/constants.js @@ -9,6 +9,7 @@ export const TRIGGER_TYPE = { ALERT_TRIGGER: 'alerting_trigger', QUERY_LEVEL: 'query_level_trigger', DOC_LEVEL: 'document_level_trigger', + COMPOSITE_LEVEL: 'composite_level_trigger', }; export const FORMIK_INITIAL_BUCKET_SELECTOR_VALUES = { diff --git a/public/pages/CreateTrigger/containers/CreateTrigger/utils/formikToTrigger.js b/public/pages/CreateTrigger/containers/CreateTrigger/utils/formikToTrigger.js index 7d34ad1f2..0ef88c089 100644 --- a/public/pages/CreateTrigger/containers/CreateTrigger/utils/formikToTrigger.js +++ b/public/pages/CreateTrigger/containers/CreateTrigger/utils/formikToTrigger.js @@ -293,6 +293,7 @@ export function formikToTriggerUiMetadata(values, monitorUiMetadata) { bucketLevelTriggersUiMetadata[trigger.name] = triggerMetadata; }); return bucketLevelTriggersUiMetadata; + case MONITOR_TYPE.DOC_LEVEL: const docLevelTriggersUiMetadata = {}; _.get(values, 'triggerDefinitions', []).forEach((trigger) => { diff --git a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/DefineCompositeLevelTrigger.js b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/DefineCompositeLevelTrigger.js index 0f70bb6dc..651084030 100644 --- a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/DefineCompositeLevelTrigger.js +++ b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/DefineCompositeLevelTrigger.js @@ -86,7 +86,7 @@ class DefineCompositeLevelTrigger extends Component { name: triggerName, }, ]); - + const triggerActions = _.get(triggerValues, 'triggerDefinitions[0].actions', []); const monitorList = monitorValues?.associatedMonitors ? monitorValues.associatedMonitors?.map((monitor) => ({ label: monitor.label.replaceAll(' ', '_'), @@ -135,6 +135,7 @@ class DefineCompositeLevelTrigger extends Component { onChange={() => {}} dataTestSubj={'composite_expression_query'} defaultText={'Select associated monitor'} + triggerValues={triggerValues} /> @@ -156,6 +157,7 @@ class DefineCompositeLevelTrigger extends Component { notifications={notifications} notificationService={notificationService} triggerValues={triggerValues} + triggerActions={triggerActions} /> ); diff --git a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/NotificationConfigDialog.js b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/NotificationConfigDialog.js index 375075426..76c08283e 100644 --- a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/NotificationConfigDialog.js +++ b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/NotificationConfigDialog.js @@ -20,8 +20,6 @@ import { getTriggerContext } from '../../utils/helper'; import { formikToMonitor } from '../../../CreateMonitor/containers/CreateMonitor/utils/formikToMonitor'; import _ from 'lodash'; import { formikToTrigger } from '../CreateTrigger/utils/formikToTrigger'; -import { MONITOR_TYPE } from '../../../../utils/constants'; -import { TRIGGER_TYPE } from '../CreateTrigger/utils/constants'; import { backendErrorNotification } from '../../../../utils/helpers'; import { checkForError } from '../ConfigureActions/ConfigureActions'; @@ -31,58 +29,31 @@ const NotificationConfigDialog = ({ triggerValues, httpClient, notifications, + actionIndex, }) => { const triggerIndex = 0; const monitor = formikToMonitor(triggerValues); + delete monitor.monitor_type; const context = getTriggerContext({}, monitor, triggerValues, 0); const initialActionValues = _.cloneDeep(FORMIK_INITIAL_ACTION_VALUES); - let action = _.get(triggerValues, 'triggerDefinitions[0].actions[0]', { + let action = _.get(triggerValues, `triggerDefinitions[0].actions[${actionIndex}]`, { ...initialActionValues, }); + console.log('Initial actions', action); const sendTestMessage = async (index) => { - const flattenedDestinations = []; - // TODO: For bucket-level triggers, sendTestMessage will only send a test message if there is - // at least one bucket of data from the monitor input query. let testTrigger = _.cloneDeep( formikToTrigger(triggerValues, monitor.ui_metadata)[triggerIndex] ); - let action; - let condition; - switch (monitor.monitor_type) { - case MONITOR_TYPE.BUCKET_LEVEL: - action = _.get(testTrigger, `${TRIGGER_TYPE.BUCKET_LEVEL}.actions[${index}]`); - condition = { - ..._.get(testTrigger, `${TRIGGER_TYPE.BUCKET_LEVEL}.condition`), - buckets_path: { _count: '_count' }, - script: { - source: 'params._count >= 0', - }, - }; - _.set(testTrigger, `${TRIGGER_TYPE.BUCKET_LEVEL}.actions`, [action]); - _.set(testTrigger, `${TRIGGER_TYPE.BUCKET_LEVEL}.condition`, condition); - break; - case MONITOR_TYPE.DOC_LEVEL: - action = _.get(testTrigger, `${TRIGGER_TYPE.DOC_LEVEL}.actions[${index}]`); - condition = { - ..._.get(testTrigger, `${TRIGGER_TYPE.DOC_LEVEL}.condition`), - script: { lang: 'painless', source: 'return true' }, - }; - _.set(testTrigger, `${TRIGGER_TYPE.DOC_LEVEL}.actions`, [action]); - _.set(testTrigger, `${TRIGGER_TYPE.DOC_LEVEL}.condition`, condition); - break; - default: - action = _.get(testTrigger, `actions[${index}]`); - condition = { - ..._.get(testTrigger, 'condition'), - script: { lang: 'painless', source: 'return true' }, - }; - _.set(testTrigger, 'actions', [action]); - _.set(testTrigger, 'condition', condition); - break; - } + const action = _.get(testTrigger, `chained_alert_trigger.actions[${index}]`); + const condition = { + ..._.get(testTrigger, 'chained_alert_trigger.condition'), + script: { lang: 'painless', source: 'return true' }, + }; + _.set(testTrigger, 'actions', [action]); + _.set(testTrigger, 'condition', condition); const testMonitor = { ...monitor, triggers: [{ ...testTrigger }] }; @@ -94,13 +65,8 @@ const NotificationConfigDialog = ({ let error = null; if (response.ok) { error = checkForError(response, error); - if (!_.isEmpty(action.destination_id)) { - const destinationName = _.get( - _.find(flattenedDestinations, { value: action.destination_id }), - 'label' - ); - notifications.toasts.addSuccess(`Test message sent to "${destinationName}."`); - } + if (!_.isEmpty(action.destination_id)) + notifications.toasts.addSuccess(`Test message sent to "${action.name}."`); } if (error || !response.ok) { const errorMessage = error == null ? response.resp : error; @@ -127,7 +93,7 @@ const NotificationConfigDialog = ({ { - const [channels, setChannels] = useState([]); + const [actions, setActions] = useState([]); const [options, setOptions] = useState([]); useEffect(() => { - let newChannels = [...actions]; - if (_.isEmpty(newChannels)) - newChannels = [ + let newActions = [...triggerActions]; + if (_.isEmpty(newActions)) + newActions = [ { name: '', id: '', }, ]; - setChannels(newChannels); + setActions(newActions); getChannels().then((channels) => setOptions(channels)); }, []); @@ -81,40 +81,40 @@ const TriggerNotifications = ({ }; const onAddNotification = () => { - const newChannels = [...channels]; - newChannels.push({ + const newActions = [...actions]; + newActions.push({ label: '', value: '', }); - setChannels(newChannels); + setActions(newActions); }; const onRemoveNotification = (idx) => { - const newChannels = [...channels]; - newChannels.splice(idx, 1); - setChannels(newChannels); + const newActions = [...actions]; + newActions.splice(idx, 1); + setActions(newActions); }; return ( {titleTemplate('Notifications')} - {channels.length && - channels.map((channel, idx) => ( + {actions.length && + actions.map((channel, actionIndex) => ( {`Notification ${idx + 1}`}} + title={`Notification ${actionIndex + 1}`} + key={`notification-accordion-${actionIndex}`} + id={`notification-accordion-${actionIndex}`} + initialIsOpen={!actionIndex} + buttonContent={{`Notification ${actionIndex + 1}`}} paddingSize={'s'} extraAction={ - channels.length > 1 && ( + actions.length > 1 && ( onRemoveNotification(idx)} + onClick={() => onRemoveNotification(actionIndex)} size={'s'} /> ) @@ -123,11 +123,10 @@ const TriggerNotifications = ({ ))} diff --git a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotificationsContent.js b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotificationsContent.js index a54fee443..e3abb9d29 100644 --- a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotificationsContent.js +++ b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotificationsContent.js @@ -13,7 +13,7 @@ import { FORMIK_INITIAL_ACTION_VALUES } from '../../utils/constants'; const TriggerNotificationsContent = ({ channel, options, - idx, + actionIndex, triggerValues, httpClient, notifications, @@ -23,17 +23,17 @@ const TriggerNotificationsContent = ({ const onChange = (selectedOptions) => { setSelected(selectedOptions); + const initialActionValues = _.cloneDeep(FORMIK_INITIAL_ACTION_VALUES); - _.set(triggerValues, 'triggerDefinitions[0].actions[0]', { + _.set(triggerValues, `triggerDefinitions[0].actions[${actionIndex}]`, { ...initialActionValues, destination_id: selectedOptions[0]?.value, + name: selectedOptions[0]?.label, subject_template: { lang: 'mustache', source: 'Monitor {{ctx.monitor.name}} triggered an alert {{ctx.trigger.name}}', }, }); - - console.log('CHANNEL SELECTED', _.get(triggerValues, 'triggerDefinitions[0].actions[0]', {})); }; const showConfig = (channels) => setIsModalVisible(true); @@ -49,7 +49,7 @@ const TriggerNotificationsContent = ({ }} > )} From 567db1dc601e249b17e6d0346322f080558ee031 Mon Sep 17 00:00:00 2001 From: Jovan Cvetkovic Date: Fri, 16 Jun 2023 12:04:36 +0200 Subject: [PATCH 05/63] [FEATURE] Add composite monitor type #573 Signed-off-by: Jovan Cvetkovic --- .../components/ExpressionQuery/ExpressionQuery.js | 5 ++--- .../containers/CreateTrigger/utils/formikToTrigger.js | 2 +- .../DefineCompositeLevelTrigger.js | 4 ++-- .../TriggerNotificationsContent.js | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js b/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js index 13180ce09..97bf47b0d 100644 --- a/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js +++ b/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js @@ -16,8 +16,7 @@ const ExpressionQuery = ({ value, defaultText, label, - name = 'expressionQueries', - triggerValues, + formikName = 'expressionQueries', }) => { const DEFAULT_DESCRIPTION = defaultText ? defaultText : 'Select'; const OPERATORS = ['AND', 'OR', 'NOT']; @@ -79,7 +78,7 @@ const ExpressionQuery = ({ const onBlur = (form, expressions) => { form.setFieldTouched('expressionQueries', true); - form.setFieldValue('triggerDefinitions[0].triggerConditions', getValue(expressions)); + form.setFieldValue(formikName, getValue(expressions)); form.setFieldError('expressionQueries', validate()); }; diff --git a/public/pages/CreateTrigger/containers/CreateTrigger/utils/formikToTrigger.js b/public/pages/CreateTrigger/containers/CreateTrigger/utils/formikToTrigger.js index 0ef88c089..83e09017c 100644 --- a/public/pages/CreateTrigger/containers/CreateTrigger/utils/formikToTrigger.js +++ b/public/pages/CreateTrigger/containers/CreateTrigger/utils/formikToTrigger.js @@ -115,7 +115,7 @@ export function formikToDocumentLevelTriggerCondition(values, monitorUiMetadata) }; } -export function formikToCompositeTriggerCondition(values, monitorUiMetadata) { +export function formikToCompositeTriggerCondition(values) { const conditionMap = { and: '&&', or: '||', diff --git a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/DefineCompositeLevelTrigger.js b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/DefineCompositeLevelTrigger.js index 651084030..a06b8aa22 100644 --- a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/DefineCompositeLevelTrigger.js +++ b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/DefineCompositeLevelTrigger.js @@ -9,7 +9,7 @@ import _ from 'lodash'; import { EuiSpacer, EuiText } from '@elastic/eui'; import { FormikFieldText, FormikSelect } from '../../../../components/FormControls'; import { hasError, isInvalid } from '../../../../utils/validate'; -import { DEFAULT_TRIGGER_NAME, SEVERITY_OPTIONS } from '../../utils/constants'; +import { SEVERITY_OPTIONS } from '../../utils/constants'; import ExpressionQuery from '../../components/ExpressionQuery/ExpressionQuery'; import TriggerNotifications from './TriggerNotifications'; import ContentPanel from '../../../../components/ContentPanel'; @@ -123,7 +123,7 @@ class DefineCompositeLevelTrigger extends Component { setIsModalVisible(true); + const showConfig = () => setIsModalVisible(true); return ( From f6b39fefef4b60c690277bbde6eb141c743fbd46 Mon Sep 17 00:00:00 2001 From: Jovan Cvetkovic Date: Sun, 18 Jun 2023 20:31:08 +0200 Subject: [PATCH 06/63] [FEATURE] Add composite monitor type #573 Signed-off-by: Jovan Cvetkovic --- .../AssociateMonitors/AssociateMonitors.tsx | 4 +-- .../components/MonitorsList.tsx | 12 ++++--- .../containers/CreateMonitor/CreateMonitor.js | 1 + .../CreateMonitor/utils/monitorToFormik.js | 4 +++ .../WorkflowDetails/WorkflowDetails.tsx | 14 ++++++-- .../ExpressionQuery/ExpressionQuery.js | 1 + .../CreateTrigger/utils/constants.js | 2 +- .../CreateTrigger/utils/triggerToFormik.js | 26 ++++++++++++++ .../DefineCompositeLevelTrigger.js | 1 - .../NotificationConfigDialog.js | 33 +++++++++++++----- .../TriggerNotifications.js | 2 +- .../TriggerNotificationsContent.js | 4 +++ .../containers/MonitorDetails.js | 9 ++--- .../containers/Triggers/Triggers.js | 2 ++ server/clusters/alerting/alertingPlugin.js | 14 ++++++++ server/routes/monitors.js | 12 +++++++ server/services/MonitorService.js | 34 +++++++++++++++++++ server/services/utils/constants.js | 1 + 18 files changed, 151 insertions(+), 25 deletions(-) diff --git a/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.tsx b/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.tsx index 9bac7683d..3b96a01aa 100644 --- a/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.tsx +++ b/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.tsx @@ -7,7 +7,7 @@ import React, { Fragment } from 'react'; import { EuiSpacer, EuiText } from '@elastic/eui'; import MonitorsList from './components/MonitorsList'; -const AssociateMonitors = ({ monitors, options, history }) => { +const AssociateMonitors = ({ monitors, options, history, values }) => { const onUpdate = () => {}; return ( @@ -21,7 +21,7 @@ const AssociateMonitors = ({ monitors, options, history }) => { - + ); }; diff --git a/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.tsx b/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.tsx index e636cddd6..830212845 100644 --- a/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.tsx +++ b/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.tsx @@ -20,9 +20,10 @@ import { FormikFormRow, } from '../../../../../components/FormControls'; -const MonitorsList = ({ monitors = [], options = [] }) => { +const MonitorsList = ({ monitors = [], options = [], values }) => { const [selectedOptions, setSelectedOptions] = useState({}); const [monitorOptions, setMonitorOptions] = useState([]); + const [monitorFields, setMonitorFields] = useState( _.reduce( monitors.length ? monitors : [0, 1], @@ -41,9 +42,11 @@ const MonitorsList = ({ monitors = [], options = [] }) => { })); setMonitorOptions(newOptions); - let newSelected = monitors.length ? monitors : []; - setSelectedOptions(Object.assign({}, newSelected)); - }, []); + // let newSelected = monitors.length ? monitors : []; + // setSelectedOptions(Object.assign({}, newSelected)); + + // _.set(values, 'associatedMonitors', Object.values(newSelected)); + }, [monitors, options, values]); const onChange = (options, monitorIdx, form) => { let newSelected = { @@ -72,7 +75,6 @@ const MonitorsList = ({ monitors = [], options = [] }) => { const onBlur = (monitorIdx, form) => { form.setFieldTouched('associatedMonitors', true); form.setFieldTouched(`associatedMonitor_${monitorIdx}`, true); - form.setFieldValue('associatedMonitors', Object.values(selectedOptions)); form.setFieldError('associatedMonitors', validate()); }; diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/CreateMonitor.js b/public/pages/CreateMonitor/containers/CreateMonitor/CreateMonitor.js index f98a29a17..0da64435b 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 [selectedMonitors, setSelectedMonitors] = useState([]); const [monitorOptions, setMonitorOptions] = useState([]); @@ -17,7 +17,7 @@ const WorkflowDetails = ({ isAd, isComposite, httpClient, history }) => { const response = await httpClient.get('../api/alerting/monitors', { query: { from: 0, - size: 5000, + size: 1000, search: '', sortField: 'name', sortDirection: 'desc', @@ -37,8 +37,15 @@ const WorkflowDetails = ({ isAd, isComposite, httpClient, history }) => { useEffect(() => { getMonitors().then((monitors) => { setMonitorOptions(monitors); + + // const getMonitorById = (id) => monitors.find((mon) => mon.monitor_id === id); + // const newSelectedMonitors = values.inputs.map((monitor) => ({ + // value: monitor.monitor_id, + // label: getMonitorById(monitor.monitor_id)?.monitor_name, + // })); + // setSelectedMonitors(newSelectedMonitors); }); - }, []); + }, [values]); return ( { monitors={selectedMonitors} options={monitorOptions} history={history} + values={values} /> )} diff --git a/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js b/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js index 97bf47b0d..39e06ed53 100644 --- a/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js +++ b/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js @@ -23,6 +23,7 @@ const ExpressionQuery = ({ const [usedExpressions, setUsedExpressions] = useState([]); useEffect(() => { + debugger; let expressions = []; if (value?.length) { let values = [...value]; diff --git a/public/pages/CreateTrigger/containers/CreateTrigger/utils/constants.js b/public/pages/CreateTrigger/containers/CreateTrigger/utils/constants.js index a84dea9b1..7a4ee9f61 100644 --- a/public/pages/CreateTrigger/containers/CreateTrigger/utils/constants.js +++ b/public/pages/CreateTrigger/containers/CreateTrigger/utils/constants.js @@ -9,7 +9,7 @@ export const TRIGGER_TYPE = { ALERT_TRIGGER: 'alerting_trigger', QUERY_LEVEL: 'query_level_trigger', DOC_LEVEL: 'document_level_trigger', - COMPOSITE_LEVEL: 'composite_level_trigger', + COMPOSITE_LEVEL: 'chained_alert_trigger', }; export const FORMIK_INITIAL_BUCKET_SELECTOR_VALUES = { diff --git a/public/pages/CreateTrigger/containers/CreateTrigger/utils/triggerToFormik.js b/public/pages/CreateTrigger/containers/CreateTrigger/utils/triggerToFormik.js index 87afacc19..06ca721ae 100644 --- a/public/pages/CreateTrigger/containers/CreateTrigger/utils/triggerToFormik.js +++ b/public/pages/CreateTrigger/containers/CreateTrigger/utils/triggerToFormik.js @@ -35,6 +35,8 @@ export function triggerDefinitionToFormik(trigger, monitor) { return bucketLevelTriggerToFormik(trigger, monitor); case MONITOR_TYPE.DOC_LEVEL: return documentLevelTriggerToFormik(trigger, monitor); + case MONITOR_TYPE.COMPOSITE_LEVEL: + return compositeTriggerToFormik(trigger, monitor); default: return queryLevelTriggerToFormik(trigger, monitor); } @@ -216,6 +218,30 @@ export function documentLevelTriggerToFormik(trigger, monitor) { }; } +export function compositeTriggerToFormik(trigger, monitor) { + const { + id, + name, + severity, + condition: { script }, + actions, + minTimeBetweenExecutions, + rollingWindowSize, + } = trigger[TRIGGER_TYPE.COMPOSITE_LEVEL]; + const triggerUiMetadata = _.get(monitor, `ui_metadata.triggers[${name}]`); + return { + ..._.cloneDeep(FORMIK_INITIAL_TRIGGER_VALUES), + id, + name, + severity, + script, + actions: getExecutionPolicyActions(actions), + minTimeBetweenExecutions, + rollingWindowSize, + triggerConditions: triggerUiMetadata, + }; +} + export function getExecutionPolicyActions(actions) { const executionPolicyPath = 'action_execution_policy.action_execution_scope'; return _.cloneDeep(actions).map((action) => { diff --git a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/DefineCompositeLevelTrigger.js b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/DefineCompositeLevelTrigger.js index a06b8aa22..0733aaae0 100644 --- a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/DefineCompositeLevelTrigger.js +++ b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/DefineCompositeLevelTrigger.js @@ -93,7 +93,6 @@ class DefineCompositeLevelTrigger extends Component { monitor_id: monitor.value, })) : []; - return ( { - let testTrigger = _.cloneDeep( - formikToTrigger(triggerValues, monitor.ui_metadata)[triggerIndex] - ); + const mon = _.cloneDeep(monitor); + const tv = _.cloneDeep(triggerValues); + let testTrigger = _.cloneDeep(formikToTrigger(tv, mon.ui_metadata)[triggerIndex]); + testTrigger = { + ...testTrigger, + name: _.get(tv, 'triggerDefinitions[0].name', ''), + severity: _.get(tv, 'triggerDefinitions[0].severity', ''), + }; const action = _.get(testTrigger, `chained_alert_trigger.actions[${index}]`); const condition = { ..._.get(testTrigger, 'chained_alert_trigger.condition'), script: { lang: 'painless', source: 'return true' }, }; - _.set(testTrigger, 'actions', [action]); - _.set(testTrigger, 'condition', condition); - const testMonitor = { ...monitor, triggers: [{ ...testTrigger }] }; + let triggers = _.cloneDeep(testTrigger); + + delete triggers.chained_alert_trigger; + delete triggers.min_time_between_executions; + delete triggers.rolling_window_size; + + _.set(triggers, 'actions', [action]); + _.set(triggers, 'condition', condition); + + const testMonitor = { ...monitor, triggers: [{ ...triggers }] }; + + // clean up actions and triggers + delete testMonitor.enabled_time; + delete testMonitor.last_update_time; + delete testMonitor.schema_version; + delete testMonitor.ui_metadata.composite_input; + delete testMonitor.ui_metadata.monitor_type; try { const response = await httpClient.post('../api/alerting/monitors/_execute', { @@ -78,7 +96,6 @@ const NotificationConfigDialog = ({ } }; - console.log('ACTION', action); return ( closeModal()}> diff --git a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotifications.js b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotifications.js index cbce16083..8eaeeeb7a 100644 --- a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotifications.js +++ b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotifications.js @@ -40,7 +40,7 @@ const TriggerNotifications = ({ setActions(newActions); getChannels().then((channels) => setOptions(channels)); - }, []); + }, [triggerValues]); const getChannels = async () => { const hasNotificationPlugin = plugins.indexOf(OS_NOTIFICATION_PLUGIN) !== -1; diff --git a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotificationsContent.js b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotificationsContent.js index c448a673a..f82574f9a 100644 --- a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotificationsContent.js +++ b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotificationsContent.js @@ -9,6 +9,7 @@ import { FormikComboBox } from '../../../../components/FormControls'; import NotificationConfigDialog from './NotificationConfigDialog'; import _ from 'lodash'; import { FORMIK_INITIAL_ACTION_VALUES } from '../../utils/constants'; +import { NOTIFY_OPTIONS_VALUES } from '../../components/Action/actions/Message'; const TriggerNotificationsContent = ({ channel, @@ -33,6 +34,9 @@ const TriggerNotificationsContent = ({ lang: 'mustache', source: 'Monitor {{ctx.monitor.name}} triggered an alert {{ctx.trigger.name}}', }, + action_execution_policy: { + action_execution_scope: NOTIFY_OPTIONS_VALUES.PER_ALERT, + }, }); }; diff --git a/public/pages/MonitorDetails/containers/MonitorDetails.js b/public/pages/MonitorDetails/containers/MonitorDetails.js index 1a461e8e0..bf216009a 100644 --- a/public/pages/MonitorDetails/containers/MonitorDetails.js +++ b/public/pages/MonitorDetails/containers/MonitorDetails.js @@ -72,6 +72,8 @@ export default class MonitorDetails extends Component { }; } + isWorkflow = () => new URLSearchParams(this.props.location.search).get('type') === 'workflow'; + componentDidMount() { this.getMonitor(this.props.match.params.monitorId); } @@ -112,7 +114,7 @@ export default class MonitorDetails extends Component { getMonitor = (id) => { const { httpClient } = this.props; httpClient - .get(`../api/alerting/monitors/${id}`) + .get(`../api/alerting/${this.isWorkflow() ? 'workflows' : 'monitors'}/${id}`) .then((resp) => { const { ok, @@ -450,9 +452,8 @@ export default class MonitorDetails extends Component { {this.renderTableTabs()} {this.state.tabContent}
- ) : ( - this.renderAlertsTable() - )} + ) : // this.renderAlertsTable() + null} {isJsonModalOpen && ( diff --git a/public/pages/MonitorDetails/containers/Triggers/Triggers.js b/public/pages/MonitorDetails/containers/Triggers/Triggers.js index 8a12a5373..63838636f 100644 --- a/public/pages/MonitorDetails/containers/Triggers/Triggers.js +++ b/public/pages/MonitorDetails/containers/Triggers/Triggers.js @@ -24,6 +24,8 @@ export function getUnwrappedTriggers(monitor) { return trigger[TRIGGER_TYPE.BUCKET_LEVEL]; case MONITOR_TYPE.DOC_LEVEL: return trigger[TRIGGER_TYPE.DOC_LEVEL]; + case MONITOR_TYPE.COMPOSITE_LEVEL: + return trigger[TRIGGER_TYPE.COMPOSITE_LEVEL]; default: return trigger[TRIGGER_TYPE.QUERY_LEVEL]; } diff --git a/server/clusters/alerting/alertingPlugin.js b/server/clusters/alerting/alertingPlugin.js index 46cde5562..aa3b5c317 100644 --- a/server/clusters/alerting/alertingPlugin.js +++ b/server/clusters/alerting/alertingPlugin.js @@ -9,6 +9,7 @@ import { DESTINATION_BASE_API, EMAIL_ACCOUNT_BASE_API, EMAIL_GROUP_BASE_API, + WORKFLOW_BASE_API, } from '../../services/utils/constants'; export default function alertingPlugin(Client, config, components) { @@ -25,6 +26,19 @@ export default function alertingPlugin(Client, config, components) { method: 'GET', }); + alerting.getWorkflow = ca({ + url: { + fmt: `${API_ROUTE_PREFIX}/workflows/<%=monitorId%>`, + req: { + monitorId: { + type: 'string', + required: true, + }, + }, + }, + method: 'GET', + }); + alerting.getMonitor = ca({ url: { fmt: `${MONITOR_BASE_API}/<%=monitorId%>`, diff --git a/server/routes/monitors.js b/server/routes/monitors.js index a447327df..f8a21a9c2 100644 --- a/server/routes/monitors.js +++ b/server/routes/monitors.js @@ -68,6 +68,18 @@ export default function (services, router) { monitorService.executeMonitor ); + router.get( + { + path: '/api/alerting/workflows/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + monitorService.getWorkflow + ); + router.get( { path: '/api/alerting/monitors/{id}', diff --git a/server/services/MonitorService.js b/server/services/MonitorService.js index 0ed7554ac..08c1ce2cb 100644 --- a/server/services/MonitorService.js +++ b/server/services/MonitorService.js @@ -146,6 +146,40 @@ export default class MonitorService { } }; + getWorkflow = async (context, req, res) => { + try { + const { id } = req.params; + const params = { monitorId: id }; + const { callAsCurrentUser } = await this.esDriver.asScoped(req); + const getResponse = await callAsCurrentUser('alerting.getWorkflow', params); + const monitor = _.get(getResponse, 'workflow', null); + const version = _.get(getResponse, '_version', null); + const ifSeqNo = _.get(getResponse, '_seq_no', null); + const ifPrimaryTerm = _.get(getResponse, '_primary_term', null); + monitor.monitor_type = monitor.workflow_type; + + return res.ok({ + body: { + ok: true, + resp: monitor, + activeCount: 0, + dayCount: 0, + version, + ifSeqNo, + ifPrimaryTerm, + }, + }); + } catch (err) { + console.error('Alerting - MonitorService - getMonitor:', err); + return res.ok({ + body: { + ok: false, + resp: err.message, + }, + }); + } + }; + updateMonitor = async (context, req, res) => { try { const { id } = req.params; diff --git a/server/services/utils/constants.js b/server/services/utils/constants.js index d43b8fe8a..c8ba9897a 100644 --- a/server/services/utils/constants.js +++ b/server/services/utils/constants.js @@ -5,6 +5,7 @@ export const API_ROUTE_PREFIX = '/_plugins/_alerting'; export const MONITOR_BASE_API = `${API_ROUTE_PREFIX}/monitors`; +export const WORKFLOW_BASE_API = `${API_ROUTE_PREFIX}/workflows`; export const AD_BASE_API = `/_plugins/_anomaly_detection/detectors`; export const DESTINATION_BASE_API = `${API_ROUTE_PREFIX}/destinations`; export const EMAIL_ACCOUNT_BASE_API = `${DESTINATION_BASE_API}/email_accounts`; From 2201c0e3ca70fed18ceed5029a0ca078a56c8c9f Mon Sep 17 00:00:00 2001 From: Jovan Cvetkovic Date: Mon, 19 Jun 2023 15:27:31 +0200 Subject: [PATCH 07/63] [FEATURE] Add composite monitor type #573 Signed-off-by: Jovan Cvetkovic --- .../components/MonitorsList.tsx | 9 ++- .../ExpressionQuery/ExpressionQuery.js | 8 +- .../CreateTrigger/utils/triggerToFormik.js | 4 - .../NotificationConfigDialog.js | 74 ++++++++++++------- .../TriggerNotifications.js | 1 + .../containers/Monitors/utils/tableUtils.js | 5 +- server/services/MonitorService.js | 2 - 7 files changed, 62 insertions(+), 41 deletions(-) diff --git a/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.tsx b/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.tsx index 830212845..4403171ca 100644 --- a/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.tsx +++ b/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.tsx @@ -61,7 +61,7 @@ const MonitorsList = ({ monitors = [], options = [], values }) => { updateMonitorOptions(newSelected); - onBlur(monitorIdx, form); + updateFormik(monitorIdx, form); }; const updateMonitorOptions = (selected) => { @@ -72,7 +72,7 @@ const MonitorsList = ({ monitors = [], options = [], values }) => { setMonitorOptions([...newMonitorOptions]); }; - const onBlur = (monitorIdx, form) => { + const updateFormik = (monitorIdx, form) => { form.setFieldTouched('associatedMonitors', true); form.setFieldTouched(`associatedMonitor_${monitorIdx}`, true); form.setFieldValue('associatedMonitors', Object.values(selectedOptions)); @@ -109,10 +109,11 @@ const MonitorsList = ({ monitors = [], options = [], values }) => { updateMonitorOptions(newSelected); - onBlur(monitorIdx, form); + updateFormik(monitorIdx, form); }; const isValid = () => Object.keys(selectedOptions).length > 1; + const validate = () => { if (!isValid()) return 'Required.'; }; @@ -149,7 +150,7 @@ const MonitorsList = ({ monitors = [], options = [], values }) => { !selectedOptions[monitorIdx], placeholder: 'Select a monitor', onChange: (options, field, form) => onChange(options, monitorIdx, form), - onBlur: (e, field, form) => onBlur(monitorIdx, form), + onBlur: (e, field, form) => updateFormik(monitorIdx, form), options: monitorOptions, singleSelection: { asPlainText: true }, selectedOptions: selectedOptions[monitorIdx] diff --git a/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js b/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js index 39e06ed53..c4c5a1703 100644 --- a/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js +++ b/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js @@ -23,7 +23,6 @@ const ExpressionQuery = ({ const [usedExpressions, setUsedExpressions] = useState([]); useEffect(() => { - debugger; let expressions = []; if (value?.length) { let values = [...value]; @@ -164,10 +163,13 @@ const ExpressionQuery = ({ /> ); - const isValid = () => usedExpressions.length > 1; + const isValid = () => selections.length > 1 && usedExpressions.length > 1; const validate = () => { - if (!isValid()) return 'At least two monitors should be selected.'; + if (selections.length < 2) + return 'Trigger condition requires at least two associated monitors.'; + if (usedExpressions.length < 2) + return 'Trigger condition requires at least two monitors selected.'; }; return ( diff --git a/public/pages/CreateTrigger/containers/CreateTrigger/utils/triggerToFormik.js b/public/pages/CreateTrigger/containers/CreateTrigger/utils/triggerToFormik.js index 06ca721ae..69171ddaf 100644 --- a/public/pages/CreateTrigger/containers/CreateTrigger/utils/triggerToFormik.js +++ b/public/pages/CreateTrigger/containers/CreateTrigger/utils/triggerToFormik.js @@ -225,8 +225,6 @@ export function compositeTriggerToFormik(trigger, monitor) { severity, condition: { script }, actions, - minTimeBetweenExecutions, - rollingWindowSize, } = trigger[TRIGGER_TYPE.COMPOSITE_LEVEL]; const triggerUiMetadata = _.get(monitor, `ui_metadata.triggers[${name}]`); return { @@ -236,8 +234,6 @@ export function compositeTriggerToFormik(trigger, monitor) { severity, script, actions: getExecutionPolicyActions(actions), - minTimeBetweenExecutions, - rollingWindowSize, triggerConditions: triggerUiMetadata, }; } diff --git a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/NotificationConfigDialog.js b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/NotificationConfigDialog.js index dcbd7b812..b994220c7 100644 --- a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/NotificationConfigDialog.js +++ b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/NotificationConfigDialog.js @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { EuiButton, EuiSpacer, @@ -22,6 +22,7 @@ import _ from 'lodash'; import { formikToTrigger } from '../CreateTrigger/utils/formikToTrigger'; import { backendErrorNotification } from '../../../../utils/helpers'; import { checkForError } from '../ConfigureActions/ConfigureActions'; +import { TRIGGER_TYPE } from '../CreateTrigger/utils/constants'; const NotificationConfigDialog = ({ channel, @@ -41,39 +42,53 @@ const NotificationConfigDialog = ({ ...initialActionValues, }); + const fieldPath = 'triggerDefinitions[0]'; + const [initialValues, setInitialValues] = useState({}); + + useEffect(() => { + setInitialValues({ + subject_source: _.get( + triggerValues, + `${fieldPath}actions.${actionIndex}.subject_template.source`, + '' + ), + message_source: _.get( + triggerValues, + `${fieldPath}actions.${actionIndex}.message_template.source`, + '' + ), + throttle_enabled: _.get( + triggerValues, + `${fieldPath}actions.${actionIndex}.throttle_enabled`, + '' + ), + throttle_value: _.get(triggerValues, `${fieldPath}actions.${actionIndex}.throttle.value`, ''), + }); + }, []); + const sendTestMessage = async (index) => { - const mon = _.cloneDeep(monitor); - const tv = _.cloneDeep(triggerValues); - let testTrigger = _.cloneDeep(formikToTrigger(tv, mon.ui_metadata)[triggerIndex]); + const monitorData = _.cloneDeep(monitor); + let testTrigger = _.cloneDeep( + formikToTrigger(triggerValues, monitorData.ui_metadata)[triggerIndex] + ); testTrigger = { ...testTrigger, - name: _.get(tv, 'triggerDefinitions[0].name', ''), - severity: _.get(tv, 'triggerDefinitions[0].severity', ''), + name: _.get(triggerValues, 'triggerDefinitions[0].name', ''), + severity: _.get(triggerValues, 'triggerDefinitions[0].severity', ''), }; - const action = _.get(testTrigger, `chained_alert_trigger.actions[${index}]`); + const action = _.get(testTrigger, `${TRIGGER_TYPE.COMPOSITE_LEVEL}.actions[${index}]`); const condition = { - ..._.get(testTrigger, 'chained_alert_trigger.condition'), + ..._.get(testTrigger, `${TRIGGER_TYPE.COMPOSITE_LEVEL}.condition`), script: { lang: 'painless', source: 'return true' }, }; - let triggers = _.cloneDeep(testTrigger); - - delete triggers.chained_alert_trigger; - delete triggers.min_time_between_executions; - delete triggers.rolling_window_size; + delete testTrigger[TRIGGER_TYPE.COMPOSITE_LEVEL]; - _.set(triggers, 'actions', [action]); - _.set(triggers, 'condition', condition); + _.set(testTrigger, 'actions', [action]); + _.set(testTrigger, 'condition', condition); - const testMonitor = { ...monitor, triggers: [{ ...triggers }] }; - - // clean up actions and triggers - delete testMonitor.enabled_time; - delete testMonitor.last_update_time; - delete testMonitor.schema_version; - delete testMonitor.ui_metadata.composite_input; - delete testMonitor.ui_metadata.monitor_type; + const testMonitor = { ...monitor, triggers: [{ ...testTrigger }] }; try { const response = await httpClient.post('../api/alerting/monitors/_execute', { @@ -96,8 +111,17 @@ const NotificationConfigDialog = ({ } }; + const clearConfig = () => { + _.set( + triggerValues, + `${fieldPath}actions.${actionIndex}.subject_template.source`, + initialValues.subject_source + ); + closeModal(); + }; + return ( - closeModal()}> + clearConfig()}>

Configure notification

@@ -120,7 +144,7 @@ const NotificationConfigDialog = ({ /> - closeModal()}>Close + clearConfig()}>Close closeModal()} fill> Update diff --git a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotifications.js b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotifications.js index 8eaeeeb7a..da0ebba04 100644 --- a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotifications.js +++ b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotifications.js @@ -92,6 +92,7 @@ const TriggerNotifications = ({ const onRemoveNotification = (idx) => { const newActions = [...actions]; newActions.splice(idx, 1); + _.set(triggerValues, 'triggerDefinitions[0].actions', newActions); setActions(newActions); }; diff --git a/public/pages/Monitors/containers/Monitors/utils/tableUtils.js b/public/pages/Monitors/containers/Monitors/utils/tableUtils.js index 4aab897e9..60201bd1f 100644 --- a/public/pages/Monitors/containers/Monitors/utils/tableUtils.js +++ b/public/pages/Monitors/containers/Monitors/utils/tableUtils.js @@ -22,7 +22,7 @@ export const columns = [ sortable: true, textOnly: true, render: (name, item) => ( - + {name} ), @@ -39,8 +39,7 @@ export const columns = [ 2. Monitors are created when security plugin is disabled, these will have empty User object. (`monitor.user.name`, `monitor.user.roles` are empty ) 3. Monitors are created when security plugin is enabled, these will have an User object. */ - render: (_, item) => - item.monitor.user && item.monitor.user.name ? item.monitor.user.name : '-', + render: (_, item) => (item.user && item.user.name ? item.user.name : '-'), }, { field: 'latestAlert', diff --git a/server/services/MonitorService.js b/server/services/MonitorService.js index 08c1ce2cb..0e1512509 100644 --- a/server/services/MonitorService.js +++ b/server/services/MonitorService.js @@ -231,7 +231,6 @@ export default class MonitorService { }; } - const filter = [{ term: { 'monitor.type': 'monitor' } }]; if (state !== 'all') { const enabled = state === 'enabled'; filter.push({ term: { 'monitor.enabled': enabled } }); @@ -252,7 +251,6 @@ export default class MonitorService { ...monitorSortPageData, query: { bool: { - filter, must, }, }, From 654a5f10dea751dbee4db65ae18b1bffee47b315 Mon Sep 17 00:00:00 2001 From: Jovan Cvetkovic Date: Mon, 19 Jun 2023 15:32:49 +0200 Subject: [PATCH 08/63] [FEATURE] Add composite monitor type #573 Signed-off-by: Jovan Cvetkovic --- .../NotificationConfigDialog.js | 21 +++---------------- 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/NotificationConfigDialog.js b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/NotificationConfigDialog.js index b994220c7..42468477a 100644 --- a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/NotificationConfigDialog.js +++ b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/NotificationConfigDialog.js @@ -47,22 +47,7 @@ const NotificationConfigDialog = ({ useEffect(() => { setInitialValues({ - subject_source: _.get( - triggerValues, - `${fieldPath}actions.${actionIndex}.subject_template.source`, - '' - ), - message_source: _.get( - triggerValues, - `${fieldPath}actions.${actionIndex}.message_template.source`, - '' - ), - throttle_enabled: _.get( - triggerValues, - `${fieldPath}actions.${actionIndex}.throttle_enabled`, - '' - ), - throttle_value: _.get(triggerValues, `${fieldPath}actions.${actionIndex}.throttle.value`, ''), + [`action${actionIndex}`]: _.get(triggerValues, `${fieldPath}actions.${actionIndex}`, ''), }); }, []); @@ -114,8 +99,8 @@ const NotificationConfigDialog = ({ const clearConfig = () => { _.set( triggerValues, - `${fieldPath}actions.${actionIndex}.subject_template.source`, - initialValues.subject_source + `${fieldPath}actions.${actionIndex}`, + initialValues[`action${actionIndex}`] ); closeModal(); }; From 39b4d0d5c28bb55eea5c601a076edb03a4b46cd6 Mon Sep 17 00:00:00 2001 From: Jovan Cvetkovic Date: Mon, 19 Jun 2023 15:57:24 +0200 Subject: [PATCH 09/63] [FEATURE] Add composite monitor type #573 Signed-off-by: Jovan Cvetkovic --- .../components/AssociateMonitors/components/MonitorsList.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.tsx b/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.tsx index 4403171ca..b9df3cf81 100644 --- a/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.tsx +++ b/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.tsx @@ -69,6 +69,7 @@ const MonitorsList = ({ monitors = [], options = [], values }) => { newMonitorOptions.forEach((mon) => { mon.disabled = isSelected(selected, mon); }); + setMonitorOptions([...newMonitorOptions]); }; @@ -83,7 +84,7 @@ const MonitorsList = ({ monitors = [], options = [], values }) => { let isSelected = false; for (const key in selected) { if (selected.hasOwnProperty(key)) { - if (_.isEqual(selected[key], monitor)) { + if (selected[key].value === monitor.value) { isSelected = true; break; } From be4b4bfe1e3f889afd88ecb1c65ee1e1dbe525b8 Mon Sep 17 00:00:00 2001 From: Jovan Cvetkovic Date: Mon, 19 Jun 2023 18:05:39 +0200 Subject: [PATCH 10/63] [FEATURE] Add composite monitor type #573 Signed-off-by: Jovan Cvetkovic --- .../AssociateMonitors/AssociateMonitors.tsx | 4 +-- .../components/MonitorsList.tsx | 15 ++++++---- .../WorkflowDetails/WorkflowDetails.tsx | 29 ++++++++++--------- .../DefineCompositeLevelTrigger.js | 1 + 4 files changed, 28 insertions(+), 21 deletions(-) diff --git a/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.tsx b/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.tsx index 3b96a01aa..ede80d1d2 100644 --- a/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.tsx +++ b/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.tsx @@ -7,7 +7,7 @@ import React, { Fragment } from 'react'; import { EuiSpacer, EuiText } from '@elastic/eui'; import MonitorsList from './components/MonitorsList'; -const AssociateMonitors = ({ monitors, options, history, values }) => { +const AssociateMonitors = ({ monitors, options }) => { const onUpdate = () => {}; return ( @@ -21,7 +21,7 @@ const AssociateMonitors = ({ monitors, options, history, values }) => { - + ); }; diff --git a/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.tsx b/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.tsx index b9df3cf81..fd2bed0fb 100644 --- a/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.tsx +++ b/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.tsx @@ -20,7 +20,7 @@ import { FormikFormRow, } from '../../../../../components/FormControls'; -const MonitorsList = ({ monitors = [], options = [], values }) => { +const MonitorsList = ({ monitors = [], options = [] }) => { const [selectedOptions, setSelectedOptions] = useState({}); const [monitorOptions, setMonitorOptions] = useState([]); @@ -42,11 +42,14 @@ const MonitorsList = ({ monitors = [], options = [], values }) => { })); setMonitorOptions(newOptions); - // let newSelected = monitors.length ? monitors : []; - // setSelectedOptions(Object.assign({}, newSelected)); - - // _.set(values, 'associatedMonitors', Object.values(newSelected)); - }, [monitors, options, values]); + let newSelected = monitors.length + ? monitors.map((monitor) => ({ + label: monitor.monitor_name, + value: monitor.monitor_id, + })) + : []; + setSelectedOptions(Object.assign({}, newSelected)); + }, [monitors, options]); const onChange = (options, monitorIdx, form) => { let newSelected = { diff --git a/public/pages/CreateMonitor/containers/WorkflowDetails/WorkflowDetails.tsx b/public/pages/CreateMonitor/containers/WorkflowDetails/WorkflowDetails.tsx index b294c466c..ac7b69a97 100644 --- a/public/pages/CreateMonitor/containers/WorkflowDetails/WorkflowDetails.tsx +++ b/public/pages/CreateMonitor/containers/WorkflowDetails/WorkflowDetails.tsx @@ -8,6 +8,7 @@ import ContentPanel from '../../../../components/ContentPanel'; import Schedule from '../../components/Schedule'; import AssociateMonitors from '../../components/AssociateMonitors/AssociateMonitors'; import { EuiSpacer } from '@elastic/eui'; +import * as _ from 'lodash'; const WorkflowDetails = ({ isAd, isComposite, httpClient, history, values }) => { const [selectedMonitors, setSelectedMonitors] = useState([]); @@ -38,14 +39,21 @@ const WorkflowDetails = ({ isAd, isComposite, httpClient, history, values }) => getMonitors().then((monitors) => { setMonitorOptions(monitors); - // const getMonitorById = (id) => monitors.find((mon) => mon.monitor_id === id); - // const newSelectedMonitors = values.inputs.map((monitor) => ({ - // value: monitor.monitor_id, - // label: getMonitorById(monitor.monitor_id)?.monitor_name, - // })); - // setSelectedMonitors(newSelectedMonitors); + const inputIds = values.inputs?.map((input) => input.monitor_id); + if (inputIds && inputIds.length) { + const selected = monitors.filter((monitor) => inputIds.indexOf(monitor.monitor_id) !== -1); + setSelectedMonitors(selected); + _.set( + values, + 'associatedMonitors', + selected.map((monitor) => ({ + value: monitor.monitor_id, + label: monitor.monitor_name, + })) + ); + } }); - }, [values]); + }, [values.inputs]); return ( {isComposite && ( - + )} diff --git a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/DefineCompositeLevelTrigger.js b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/DefineCompositeLevelTrigger.js index 0733aaae0..37129cbc3 100644 --- a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/DefineCompositeLevelTrigger.js +++ b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/DefineCompositeLevelTrigger.js @@ -75,6 +75,7 @@ class DefineCompositeLevelTrigger extends Component { notificationService, plugins, } = this.props; + const fieldPath = `triggerDefinitions[0].`; const triggerName = _.get(triggerValues, `${fieldPath}name`, 'Trigger'); const triggerDefinitions = _.get(triggerValues, 'triggerDefinitions', []); From 1792d4cf0adab2367798100c2e15f9fafe01e4f9 Mon Sep 17 00:00:00 2001 From: Jovan Cvetkovic Date: Mon, 19 Jun 2023 19:27:12 +0200 Subject: [PATCH 11/63] [FEATURE] Add composite monitor type #573 Signed-off-by: Jovan Cvetkovic --- .../NotificationConfigDialog.js | 1 - .../TriggerNotifications.js | 7 ++++--- .../TriggerNotificationsContent.js | 17 +++++++++++++---- .../MonitorDetails/containers/MonitorDetails.js | 15 +++++++++++---- server/clusters/alerting/alertingPlugin.js | 15 +++++++++++++++ server/routes/monitors.js | 17 +++++++++++++++++ server/services/MonitorService.js | 6 +++++- 7 files changed, 65 insertions(+), 13 deletions(-) diff --git a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/NotificationConfigDialog.js b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/NotificationConfigDialog.js index 42468477a..eef6535e7 100644 --- a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/NotificationConfigDialog.js +++ b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/NotificationConfigDialog.js @@ -25,7 +25,6 @@ import { checkForError } from '../ConfigureActions/ConfigureActions'; import { TRIGGER_TYPE } from '../CreateTrigger/utils/constants'; const NotificationConfigDialog = ({ - channel, closeModal, triggerValues, httpClient, diff --git a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotifications.js b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotifications.js index da0ebba04..bd5389fbe 100644 --- a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotifications.js +++ b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotifications.js @@ -37,10 +37,11 @@ const TriggerNotifications = ({ id: '', }, ]; + setActions(newActions); getChannels().then((channels) => setOptions(channels)); - }, [triggerValues]); + }, [triggerValues, plugins]); const getChannels = async () => { const hasNotificationPlugin = plugins.indexOf(OS_NOTIFICATION_PLUGIN) !== -1; @@ -101,7 +102,7 @@ const TriggerNotifications = ({ {titleTemplate('Notifications')} {actions.length && - actions.map((channel, actionIndex) => ( + actions.map((action, actionIndex) => ( { + setSelected([ + { + label: action.name, + value: action.config_id, + type: action.config_type, + description: action.description, + }, + ]); + }, [action]); + const onChange = (selectedOptions) => { setSelected(selectedOptions); @@ -64,7 +75,6 @@ const TriggerNotificationsContent = ({ }} inputProps={{ isInvalid: !selected.length, - value: channel, placeholder: 'Select a channel to get notified', options: options, selectedOptions: selected, @@ -91,7 +101,6 @@ const TriggerNotificationsContent = ({ {isModalVisible && ( setIsModalVisible(false)} - channel={channel} triggerValues={triggerValues} httpClient={httpClient} notifications={notifications} diff --git a/public/pages/MonitorDetails/containers/MonitorDetails.js b/public/pages/MonitorDetails/containers/MonitorDetails.js index bf216009a..babe7e62d 100644 --- a/public/pages/MonitorDetails/containers/MonitorDetails.js +++ b/public/pages/MonitorDetails/containers/MonitorDetails.js @@ -170,10 +170,17 @@ export default class MonitorDetails extends Component { this.setState({ updating: true }); return httpClient - .put(`../api/alerting/monitors/${monitorId}`, { - query: { ...query }, - body: JSON.stringify({ ...monitor, ...update }), - }) + .put( + `../api/alerting/${ + monitor.workflow_type && monitor.workflow_type === MONITOR_TYPE.COMPOSITE_LEVEL + ? 'workflows' + : 'monitors' + }/${monitorId}`, + { + query: { ...query }, + body: JSON.stringify({ ...monitor, ...update }), + } + ) .then((resp) => { if (resp.ok) { const { version: monitorVersion } = resp; diff --git a/server/clusters/alerting/alertingPlugin.js b/server/clusters/alerting/alertingPlugin.js index aa3b5c317..743705bb3 100644 --- a/server/clusters/alerting/alertingPlugin.js +++ b/server/clusters/alerting/alertingPlugin.js @@ -96,6 +96,21 @@ export default function alertingPlugin(Client, config, components) { method: 'PUT', }); + // TODO DRAFT: May need to add 'refresh' assignment here again. + alerting.updateWorkflow = ca({ + url: { + fmt: `${API_ROUTE_PREFIX}/workflows/<%=monitorId%>`, + req: { + monitorId: { + type: 'string', + required: true, + }, + }, + }, + needBody: true, + method: 'PUT', + }); + alerting.getMonitors = ca({ url: { fmt: `${MONITOR_BASE_API}/_search`, diff --git a/server/routes/monitors.js b/server/routes/monitors.js index f8a21a9c2..4d46437bc 100644 --- a/server/routes/monitors.js +++ b/server/routes/monitors.js @@ -109,6 +109,23 @@ export default function (services, router) { monitorService.updateMonitor ); + router.put( + { + path: '/api/alerting/workflows/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + query: schema.object({ + ifSeqNo: schema.maybe(schema.number()), + ifPrimaryTerm: schema.maybe(schema.number()), + }), + body: schema.any(), + }, + }, + monitorService.updateMonitor + ); + router.delete( { path: '/api/alerting/monitors/{id}', diff --git a/server/services/MonitorService.js b/server/services/MonitorService.js index 0e1512509..8e4cec408 100644 --- a/server/services/MonitorService.js +++ b/server/services/MonitorService.js @@ -184,6 +184,7 @@ export default class MonitorService { try { const { id } = req.params; const params = { monitorId: id, body: req.body, refresh: 'wait_for' }; + const { type } = req.body; // TODO DRAFT: Are we sure we need to include ifSeqNo and ifPrimaryTerm from the UI side when updating monitors? const { ifSeqNo, ifPrimaryTerm } = req.query; @@ -193,7 +194,10 @@ export default class MonitorService { } const { callAsCurrentUser } = await this.esDriver.asScoped(req); - const updateResponse = await callAsCurrentUser('alerting.updateMonitor', params); + const updateResponse = await callAsCurrentUser( + `alerting.${type === 'workflow' ? 'updateWorkflow' : 'updateMonitor'}`, + params + ); const { _version, _id } = updateResponse; return res.ok({ body: { From 8e5723bfec2caf4bc2a59556cb73077e45248275 Mon Sep 17 00:00:00 2001 From: Jovan Cvetkovic Date: Tue, 20 Jun 2023 11:24:47 +0200 Subject: [PATCH 12/63] [FEATURE] Add composite monitor type #573 Signed-off-by: Jovan Cvetkovic --- .../sample_composite_level_monitor.json | 218 ++++++++++++++++++ .../composite_level_monitor_spec.js | 122 ++++++++++ cypress/support/commands.js | 48 +++- cypress/support/constants.js | 1 + package.json | 3 + .../__snapshots__/MonitorType.test.js.snap | 62 +++++ .../AnomalyDetector.test.js.snap | 12 + .../__snapshots__/CreateMonitor.test.js.snap | 2 + .../__snapshots__/DefineMonitor.test.js.snap | 2 + .../__snapshots__/MonitorIndex.test.js.snap | 10 + .../DefineCompositeLevelTrigger.js | 6 +- .../AcknowledgeAlertsModal.test.js.snap | 2 + 12 files changed, 476 insertions(+), 12 deletions(-) create mode 100644 cypress/fixtures/sample_composite_level_monitor.json create mode 100644 cypress/integration/composite_level_monitor_spec.js diff --git a/cypress/fixtures/sample_composite_level_monitor.json b/cypress/fixtures/sample_composite_level_monitor.json new file mode 100644 index 000000000..20a0fed18 --- /dev/null +++ b/cypress/fixtures/sample_composite_level_monitor.json @@ -0,0 +1,218 @@ +{ + "sample_composite_monitor": { + "type": "workflow", + "schema_version": 0, + "name": "sample_component_level_monitor", + "workflow_type": "composite", + "enabled": true, + "enabled_time": 1686908176848, + "schedule": { + "period": { + "interval": 1, + "unit": "MINUTES" + } + }, + "inputs": [ + { + "composite_input": { + "sequence": { + "delegates": [ + { + "order": 1, + "monitor_id": "qdYBw4gB2qeAWe54jQyZ" + }, + { + "order": 2, + "monitor_id": "rtYBw4gB2qeAWe54wAx5" + } + ] + } + } + } + ], + "triggers": [ + { + "chained_alert_trigger": { + "id": "pNaQw4gB2qeAWe54Fg2U", + "name": "sample_trigger", + "severity": "1", + "condition": { + "script": { + "source": "(monitor[id=qdYBw4gB2qeAWe54jQyZ]) && (monitor[id=rtYBw4gB2qeAWe54wAx5])", + "lang": "painless" + } + }, + "actions": [ + { + "id": "pdaQw4gB2qeAWe54Fg2U", + "name": "sample_channel", + "destination_id": "6dYFw4gB2qeAWe54NgyL", + "message_template": { + "source": "Monitor {{ctx.monitor.name}} just entered alert status. Please investigate the issue.\n - Trigger: {{ctx.trigger.name}}\n - Severity: {{ctx.trigger.severity}}\n - Period start: {{ctx.periodStart}}\n - Period end: {{ctx.periodEnd}}", + "lang": "mustache" + }, + "throttle_enabled": false, + "subject_template": { + "source": "Monitor {{ctx.monitor.name}} triggered an alert {{ctx.trigger.name}}", + "lang": "mustache" + } + } + ] + } + } + ], + "last_update_time": 1686908180116, + "owner": "alerting", + "monitor_type": "composite" + }, + "sample_composite_index": { + "mappings": { + "properties": { + "audit_category": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "audit_node_host_name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "audit_node_id": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } + } + }, + "sample_composite_associated_monitor_1": { + "name": "monitor_1", + "type": "monitor", + "monitor_type": "doc_level_monitor", + "enabled": false, + "schedule": { + "period": { + "unit": "MINUTES", + "interval": 1 + } + }, + "inputs": [ + { + "doc_level_input": { + "description": "", + "indices": ["sample_index_1"], + "queries": [ + { + "id": "monitor_1_query_1", + "name": "monitor_1_query_1", + "query": "NOT (audit_category:\"sample_text\")", + "tags": [] + }, + { + "id": "monitor_1_query_2", + "name": "monitor_1_query_2", + "query": "NOT (audit_node_host_name:\"sample_text\")", + "tags": [] + }, + { + "id": "monitor_1_query_3", + "name": "monitor_1_query_3", + "query": "NOT (audit_node_id:\"sample_text\")", + "tags": [] + } + ] + } + } + ], + "triggers": [ + { + "document_level_trigger": { + "id": "sample_trigger_id_1", + "name": "monitor_1_query_2", + "severity": "1", + "condition": { + "script": { + "source": "query[name=monitor_1_query_1] || query[name=monitor_1_query_2] && query[name=monitor_1_query_3]", + "lang": "painless" + } + }, + "actions": [] + } + } + ] + }, + "sample_composite_associated_monitor_2": { + "name": "monitor_2", + "type": "monitor", + "monitor_type": "doc_level_monitor", + "enabled": false, + "schedule": { + "period": { + "unit": "MINUTES", + "interval": 1 + } + }, + "inputs": [ + { + "doc_level_input": { + "description": "", + "indices": ["sample_index_2"], + "queries": [ + { + "id": "monitor_2_query_1", + "name": "monitor_2_query_1", + "query": "NOT (audit_category:\"sample_text\")", + "tags": [] + }, + { + "id": "monitor_2_query_2", + "name": "monitor_2_query_2", + "query": "NOT (audit_node_host_name:\"sample_text\")", + "tags": [] + }, + { + "id": "monitor_2_query_3", + "name": "monitor_2_query_3", + "query": "NOT (audit_node_id:\"sample_text\")", + "tags": [] + } + ] + } + } + ], + "triggers": [ + { + "document_level_trigger": { + "id": "sample_trigger_2", + "name": "monitor_2_query_2", + "severity": "1", + "condition": { + "script": { + "source": "query[name=monitor_2_query_1] || query[name=monitor_2_query_2] && query[name=monitor_2_query_3]", + "lang": "painless" + } + }, + "actions": [] + } + } + ] + }, + "sample_composite_associated_index_document": { + "audit_category": "FAILED_LOGIN", + "audit_node_host_name": "127.0.0.1", + "audit_node_id": "sample_node_id" + } +} diff --git a/cypress/integration/composite_level_monitor_spec.js b/cypress/integration/composite_level_monitor_spec.js new file mode 100644 index 000000000..854e72fc5 --- /dev/null +++ b/cypress/integration/composite_level_monitor_spec.js @@ -0,0 +1,122 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { PLUGIN_NAME } from '../support/constants'; +import sampleCompositeJson from '../fixtures/sample_composite_level_monitor.json'; + +const sample_index_1 = 'sample_index_1'; +const sample_index_2 = 'sample_index_2'; +const SAMPLE_VISUAL_EDITOR_MONITOR = 'sample_visual_editor_composite_level_monitor'; +const SAMPLE_COMPOSITE_LEVEL_MONITOR = 'sample_composite_level_monitor'; + +const clearAll = () => { + cy.deleteAllMonitors(); + cy.deleteIndexByName(sample_index_1); + cy.deleteIndexByName(sample_index_2); + cy.deleteAllAlerts(); +}; + +describe('CompositeLevelMonitor', () => { + before(() => { + clearAll(); + + // Create indices + cy.createIndexByName(sample_index_1, sampleCompositeJson.sample_composite_index); + cy.createIndexByName(sample_index_2, sampleCompositeJson.sample_composite_index); + + // Create asociated monitors + cy.createMonitor(sampleCompositeJson.sample_composite_associated_monitor_1); + cy.createMonitor(sampleCompositeJson.sample_composite_associated_monitor_2); + }); + + beforeEach(() => { + // Set welcome screen tracking to false + localStorage.setItem('home:welcome:show', 'false'); + + // Visit Alerting OpenSearch Dashboards + cy.visit(`${Cypress.env('opensearch_dashboards')}/app/${PLUGIN_NAME}#/monitors`); + + // Common text to wait for to confirm page loaded, give up to 20 seconds for initial load + cy.contains('Create monitor', { timeout: 20000 }); + }); + + describe('can be created', () => { + beforeEach(() => { + // Go to create monitor page + cy.contains('Create monitor').click({ force: true }); + + // Select the Composite-Level Monitor type + cy.get('[data-test-subj="compositeLevelMonitorRadioCard"]').click({ force: true }); + }); + + it('by visual editor', () => { + // Select visual editor for method of definition + cy.get('[data-test-subj="visualEditorRadioCard"]').click({ force: true }); + + // Wait for input to load and then type in the monitor name + cy.get('input[name="name"]').type(SAMPLE_VISUAL_EDITOR_MONITOR); + + // Select associated monitors + cy.get('[data-test-subj="monitors_list_0"]') + .type(sampleCompositeJson.sample_composite_associated_monitor_1.name) + .type('{enter}'); + cy.get('[data-test-subj="monitors_list_1"]') + .type(sampleCompositeJson.sample_composite_associated_monitor_2.name) + .type('{enter}'); + + // Type trigger name + cy.get('[data-test-subj="composite-trigger-name"]') + .type('{selectall}') + .type('{backspace}') + .type('Composite trigger'); + + // Add associated monitors to condition + cy.get('[data-test-subj="condition-add-selection-btn"]').click(); + cy.get('[data-test-subj="condition-add-selection-btn"]').click(); + + // TODO: Test with Notifications plugin + // Select notification channel + // cy.get('[title="Notification 1"]').type('Channel name'); + + cy.intercept('api/alerting/workflows').as('createMonitorRequest'); + cy.intercept('api/alerting/monitors').as('getMonitorsRequest'); + cy.get('button').contains('Create').click({ force: true }); + + // Wait for monitor to be created + cy.wait('@createMonitorRequest').then((interceptor) => { + const monitorID = interceptor.response.body.resp._id; + + cy.contains('Loading monitors'); + cy.wait('@getMonitorsRequest'); + + // Let monitor's table render the rows before querying + cy.wait(1000).then(() => { + cy.get('table tbody td').contains(SAMPLE_VISUAL_EDITOR_MONITOR); + + // Load sample data + cy.insertDocumentToIndex( + sample_index_1, + undefined, + sampleCompositeJson.sample_composite_associated_index_document + ); + cy.insertDocumentToIndex( + sample_index_2, + undefined, + sampleCompositeJson.sample_composite_associated_index_document + ); + + cy.wait(1000).then(() => { + cy.executeCompositeMonitor(monitorID); + + cy.get('[role="tab"]').contains('Alerts').click(); + cy.get('table tbody td').contains('Composite trigger'); + }); + }); + }); + }); + }); + + after(() => clearAll()); +}); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index b3d4b1abd..9568d43ba 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -85,6 +85,23 @@ Cypress.Commands.add('createAndExecuteMonitor', (monitorJSON) => { ); }); +Cypress.Commands.add('executeCompositeMonitor', (monitorID) => { + cy.request('POST', `${Cypress.env('opensearch')}${API.WORKFLOW_BASE}/${monitorID}/_execute`); +}); + +Cypress.Commands.add('deleteAllAlerts', () => { + cy.request({ + method: 'POST', + url: `${Cypress.env('opensearch')}/.opendistro-alerting-alert*/_delete_by_query`, + body: { + query: { + match_all: {}, + }, + }, + failOnStatusCode: false, + }); +}); + Cypress.Commands.add('deleteMonitorByName', (monitorName) => { const body = { query: { @@ -110,9 +127,7 @@ Cypress.Commands.add('deleteAllMonitors', () => { const body = { size: 200, query: { - exists: { - field: 'monitor', - }, + match_all: {}, }, }; cy.request({ @@ -122,11 +137,18 @@ Cypress.Commands.add('deleteAllMonitors', () => { body, }).then((response) => { if (response.status === 200) { - for (let i = 0; i < response.body.hits.total.value; i++) { - cy.request( - 'DELETE', - `${Cypress.env('opensearch')}${API.MONITOR_BASE}/${response.body.hits.hits[i]._id}` - ); + const monitors = response.body.hits.hits.sort((monitor) => + monitor._source.type === 'workflow' ? -1 : 1 + ); + console.log('MONITORS', monitors); + for (let i = 0; i < monitors.length; i++) { + cy.request({ + method: 'DELETE', + url: `${Cypress.env('opensearch')}${ + monitors[i]._source.type === 'workflow' ? API.WORKFLOW_BASE : API.MONITOR_BASE + }/${monitors[i]._id}`, + failOnStatusCode: false, + }); } } else { cy.log('Failed to get all monitors.', response); @@ -134,12 +156,16 @@ Cypress.Commands.add('deleteAllMonitors', () => { }); }); -Cypress.Commands.add('createIndexByName', (indexName) => { - cy.request('PUT', `${Cypress.env('opensearch')}/${indexName}`); +Cypress.Commands.add('createIndexByName', (indexName, body = {}) => { + cy.request('PUT', `${Cypress.env('opensearch')}/${indexName}`, body); }); Cypress.Commands.add('deleteIndexByName', (indexName) => { - cy.request('DELETE', `${Cypress.env('opensearch')}/${indexName}`); + cy.request({ + method: 'DELETE', + url: `${Cypress.env('opensearch')}/${indexName}`, + failOnStatusCode: false, + }); }); Cypress.Commands.add('insertDocumentToIndex', (indexName, documentId, documentBody) => { diff --git a/cypress/support/constants.js b/cypress/support/constants.js index 66865cff7..6fb34cffc 100644 --- a/cypress/support/constants.js +++ b/cypress/support/constants.js @@ -13,6 +13,7 @@ export const INDEX = { export const API = { MONITOR_BASE: `${API_ROUTE_PREFIX}/monitors`, + WORKFLOW_BASE: `${API_ROUTE_PREFIX}/workflows`, DESTINATION_BASE: `${API_ROUTE_PREFIX}/destinations`, }; diff --git a/package.json b/package.json index 2c0c5084b..9f46a36de 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,9 @@ "lint": "../../node_modules/.bin/eslint '**/*.js' -c .eslintrc --ignore-path .gitignore", "test:jest:windows": "SET TZ=UTC ../../node_modules/.bin/jest --config ./test/jest.config.js", "test:jest": "TZ=UTC ../../node_modules/.bin/jest --config ./test/jest.config.js", + "test:jest:update-snapshots": "yarn run test:jest -u", + "cypress:open": "cypress open", + "cypress:run": "cypress run", "build": "yarn plugin-helpers build", "plugin-helpers": "node ../../scripts/plugin_helpers", "postbuild": "echo Renaming build artifact to [$npm_package_config_id-$npm_package_version.zip] && mv build/$npm_package_config_id*.zip build/$npm_package_config_id-$npm_package_version.zip" diff --git a/public/pages/CreateMonitor/components/MonitorType/__snapshots__/MonitorType.test.js.snap b/public/pages/CreateMonitor/components/MonitorType/__snapshots__/MonitorType.test.js.snap index 4178b864f..d817580f5 100644 --- a/public/pages/CreateMonitor/components/MonitorType/__snapshots__/MonitorType.test.js.snap +++ b/public/pages/CreateMonitor/components/MonitorType/__snapshots__/MonitorType.test.js.snap @@ -262,5 +262,67 @@ exports[`MonitorType renders 1`] = `
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+ Composite monitors allow you to monitor the states of existing monitors and to reduce alert noise. +
+
+
+
+
+
+
+
`; diff --git a/public/pages/CreateMonitor/containers/AnomalyDetectors/__tests__/__snapshots__/AnomalyDetector.test.js.snap b/public/pages/CreateMonitor/containers/AnomalyDetectors/__tests__/__snapshots__/AnomalyDetector.test.js.snap index ad99e0e92..d59bbb56f 100644 --- a/public/pages/CreateMonitor/containers/AnomalyDetectors/__tests__/__snapshots__/AnomalyDetector.test.js.snap +++ b/public/pages/CreateMonitor/containers/AnomalyDetectors/__tests__/__snapshots__/AnomalyDetector.test.js.snap @@ -6,6 +6,7 @@ exports[`AnomalyDetectors renders 1`] = ` Object { "aggregationType": "count", "aggregations": Array [], + "associatedMonitors": Array [], "bucketUnitOfTime": "h", "bucketValue": 1, "cronExpression": "0 */1 * * *", @@ -13,6 +14,7 @@ exports[`AnomalyDetectors renders 1`] = ` "description": "", "detectorId": "", "disabled": false, + "expressionQuery": null, "fieldName": Array [], "filters": Array [], "frequency": "interval", @@ -70,6 +72,7 @@ exports[`AnomalyDetectors renders 1`] = ` Object { "aggregationType": "count", "aggregations": Array [], + "associatedMonitors": Array [], "bucketUnitOfTime": "h", "bucketValue": 1, "cronExpression": "0 */1 * * *", @@ -77,6 +80,7 @@ exports[`AnomalyDetectors renders 1`] = ` "description": "", "detectorId": "", "disabled": false, + "expressionQuery": null, "fieldName": Array [], "filters": Array [], "frequency": "interval", @@ -195,6 +199,7 @@ exports[`AnomalyDetectors renders 1`] = ` "initialValues": Object { "aggregationType": "count", "aggregations": Array [], + "associatedMonitors": Array [], "bucketUnitOfTime": "h", "bucketValue": 1, "cronExpression": "0 */1 * * *", @@ -202,6 +207,7 @@ exports[`AnomalyDetectors renders 1`] = ` "description": "", "detectorId": "", "disabled": false, + "expressionQuery": null, "fieldName": Array [], "filters": Array [], "frequency": "interval", @@ -278,6 +284,7 @@ exports[`AnomalyDetectors renders 1`] = ` "values": Object { "aggregationType": "count", "aggregations": Array [], + "associatedMonitors": Array [], "bucketUnitOfTime": "h", "bucketValue": 1, "cronExpression": "0 */1 * * *", @@ -285,6 +292,7 @@ exports[`AnomalyDetectors renders 1`] = ` "description": "", "detectorId": "", "disabled": false, + "expressionQuery": null, "fieldName": Array [], "filters": Array [], "frequency": "interval", @@ -409,6 +417,7 @@ exports[`AnomalyDetectors renders 1`] = ` "initialValues": Object { "aggregationType": "count", "aggregations": Array [], + "associatedMonitors": Array [], "bucketUnitOfTime": "h", "bucketValue": 1, "cronExpression": "0 */1 * * *", @@ -416,6 +425,7 @@ exports[`AnomalyDetectors renders 1`] = ` "description": "", "detectorId": "", "disabled": false, + "expressionQuery": null, "fieldName": Array [], "filters": Array [], "frequency": "interval", @@ -492,6 +502,7 @@ exports[`AnomalyDetectors renders 1`] = ` "values": Object { "aggregationType": "count", "aggregations": Array [], + "associatedMonitors": Array [], "bucketUnitOfTime": "h", "bucketValue": 1, "cronExpression": "0 */1 * * *", @@ -499,6 +510,7 @@ exports[`AnomalyDetectors renders 1`] = ` "description": "", "detectorId": "", "disabled": false, + "expressionQuery": null, "fieldName": Array [], "filters": Array [], "frequency": "interval", diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/__snapshots__/CreateMonitor.test.js.snap b/public/pages/CreateMonitor/containers/CreateMonitor/__snapshots__/CreateMonitor.test.js.snap index a0ddd0f2c..32b0b9450 100644 --- a/public/pages/CreateMonitor/containers/CreateMonitor/__snapshots__/CreateMonitor.test.js.snap +++ b/public/pages/CreateMonitor/containers/CreateMonitor/__snapshots__/CreateMonitor.test.js.snap @@ -14,6 +14,7 @@ exports[`CreateMonitor renders 1`] = ` "adResultIndex": undefined, "aggregationType": "count", "aggregations": Array [], + "associatedMonitors": Array [], "bucketUnitOfTime": "h", "bucketValue": 1, "cronExpression": "0 */1 * * *", @@ -21,6 +22,7 @@ exports[`CreateMonitor renders 1`] = ` "description": "", "detectorId": "", "disabled": false, + "expressionQuery": null, "fieldName": Array [], "filters": Array [], "frequency": "interval", diff --git a/public/pages/CreateMonitor/containers/DefineMonitor/__snapshots__/DefineMonitor.test.js.snap b/public/pages/CreateMonitor/containers/DefineMonitor/__snapshots__/DefineMonitor.test.js.snap index 3bcc5af6f..4d811f708 100644 --- a/public/pages/CreateMonitor/containers/DefineMonitor/__snapshots__/DefineMonitor.test.js.snap +++ b/public/pages/CreateMonitor/containers/DefineMonitor/__snapshots__/DefineMonitor.test.js.snap @@ -11,6 +11,7 @@ exports[`DefineMonitor renders 1`] = ` Object { "aggregationType": "count", "aggregations": Array [], + "associatedMonitors": Array [], "bucketUnitOfTime": "h", "bucketValue": 1, "cronExpression": "0 */1 * * *", @@ -18,6 +19,7 @@ exports[`DefineMonitor renders 1`] = ` "description": "", "detectorId": "", "disabled": false, + "expressionQuery": null, "fieldName": Array [], "filters": Array [], "frequency": "interval", diff --git a/public/pages/CreateMonitor/containers/MonitorIndex/__snapshots__/MonitorIndex.test.js.snap b/public/pages/CreateMonitor/containers/MonitorIndex/__snapshots__/MonitorIndex.test.js.snap index 9745f7cf0..09351d512 100644 --- a/public/pages/CreateMonitor/containers/MonitorIndex/__snapshots__/MonitorIndex.test.js.snap +++ b/public/pages/CreateMonitor/containers/MonitorIndex/__snapshots__/MonitorIndex.test.js.snap @@ -6,6 +6,7 @@ exports[`MonitorIndex renders 1`] = ` Object { "aggregationType": "count", "aggregations": Array [], + "associatedMonitors": Array [], "bucketUnitOfTime": "h", "bucketValue": 1, "cronExpression": "0 */1 * * *", @@ -13,6 +14,7 @@ exports[`MonitorIndex renders 1`] = ` "description": "", "detectorId": "", "disabled": false, + "expressionQuery": null, "fieldName": Array [], "filters": Array [], "frequency": "interval", @@ -143,6 +145,7 @@ exports[`MonitorIndex renders 1`] = ` "initialValues": Object { "aggregationType": "count", "aggregations": Array [], + "associatedMonitors": Array [], "bucketUnitOfTime": "h", "bucketValue": 1, "cronExpression": "0 */1 * * *", @@ -150,6 +153,7 @@ exports[`MonitorIndex renders 1`] = ` "description": "", "detectorId": "", "disabled": false, + "expressionQuery": null, "fieldName": Array [], "filters": Array [], "frequency": "interval", @@ -226,6 +230,7 @@ exports[`MonitorIndex renders 1`] = ` "values": Object { "aggregationType": "count", "aggregations": Array [], + "associatedMonitors": Array [], "bucketUnitOfTime": "h", "bucketValue": 1, "cronExpression": "0 */1 * * *", @@ -233,6 +238,7 @@ exports[`MonitorIndex renders 1`] = ` "description": "", "detectorId": "", "disabled": false, + "expressionQuery": null, "fieldName": Array [], "filters": Array [], "frequency": "interval", @@ -373,6 +379,7 @@ exports[`MonitorIndex renders 1`] = ` "initialValues": Object { "aggregationType": "count", "aggregations": Array [], + "associatedMonitors": Array [], "bucketUnitOfTime": "h", "bucketValue": 1, "cronExpression": "0 */1 * * *", @@ -380,6 +387,7 @@ exports[`MonitorIndex renders 1`] = ` "description": "", "detectorId": "", "disabled": false, + "expressionQuery": null, "fieldName": Array [], "filters": Array [], "frequency": "interval", @@ -456,6 +464,7 @@ exports[`MonitorIndex renders 1`] = ` "values": Object { "aggregationType": "count", "aggregations": Array [], + "associatedMonitors": Array [], "bucketUnitOfTime": "h", "bucketValue": 1, "cronExpression": "0 */1 * * *", @@ -463,6 +472,7 @@ exports[`MonitorIndex renders 1`] = ` "description": "", "detectorId": "", "disabled": false, + "expressionQuery": null, "fieldName": Array [], "filters": Array [], "frequency": "interval", diff --git a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/DefineCompositeLevelTrigger.js b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/DefineCompositeLevelTrigger.js index 37129cbc3..6737854a4 100644 --- a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/DefineCompositeLevelTrigger.js +++ b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/DefineCompositeLevelTrigger.js @@ -117,7 +117,11 @@ class DefineCompositeLevelTrigger extends Component { paddingLeft: 0, }, }} - inputProps={{ ...defaultInputProps, value: triggerName }} + inputProps={{ + ...defaultInputProps, + value: triggerName, + 'data-test-subj': 'composite-trigger-name', + }} /> diff --git a/public/pages/Dashboard/components/AcknowledgeAlertsModal/__snapshots__/AcknowledgeAlertsModal.test.js.snap b/public/pages/Dashboard/components/AcknowledgeAlertsModal/__snapshots__/AcknowledgeAlertsModal.test.js.snap index 2286b8431..659fd3a86 100644 --- a/public/pages/Dashboard/components/AcknowledgeAlertsModal/__snapshots__/AcknowledgeAlertsModal.test.js.snap +++ b/public/pages/Dashboard/components/AcknowledgeAlertsModal/__snapshots__/AcknowledgeAlertsModal.test.js.snap @@ -35,6 +35,7 @@ exports[`AcknowledgeAlertsModal renders 1`] = ` Object { "aggregationType": "count", "aggregations": Array [], + "associatedMonitors": Array [], "bucketUnitOfTime": "h", "bucketValue": 1, "cronExpression": "0 */1 * * *", @@ -42,6 +43,7 @@ exports[`AcknowledgeAlertsModal renders 1`] = ` "description": "", "detectorId": "", "disabled": false, + "expressionQuery": null, "fieldName": Array [], "filters": Array [], "frequency": "interval", From 12f0e6f5b1d0b5e3b5784e64f0d87f53ef6782f2 Mon Sep 17 00:00:00 2001 From: Jovan Cvetkovic Date: Tue, 20 Jun 2023 14:24:17 +0200 Subject: [PATCH 13/63] [FEATURE] Add composite monitor type #573 Signed-off-by: Jovan Cvetkovic --- .../composite_level_monitor_spec.js | 55 +++++++++++-------- cypress/support/commands.js | 4 ++ 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/cypress/integration/composite_level_monitor_spec.js b/cypress/integration/composite_level_monitor_spec.js index 854e72fc5..9581422d6 100644 --- a/cypress/integration/composite_level_monitor_spec.js +++ b/cypress/integration/composite_level_monitor_spec.js @@ -15,7 +15,9 @@ const clearAll = () => { cy.deleteAllMonitors(); cy.deleteIndexByName(sample_index_1); cy.deleteIndexByName(sample_index_2); - cy.deleteAllAlerts(); + + // wait until alerts index finishes writing docs + cy.wait(1000).then(() => cy.deleteAllAlerts()); }; describe('CompositeLevelMonitor', () => { @@ -81,7 +83,7 @@ describe('CompositeLevelMonitor', () => { // cy.get('[title="Notification 1"]').type('Channel name'); cy.intercept('api/alerting/workflows').as('createMonitorRequest'); - cy.intercept('api/alerting/monitors').as('getMonitorsRequest'); + cy.intercept(`api/alerting/monitors?*`).as('getMonitorsRequest'); cy.get('button').contains('Create').click({ force: true }); // Wait for monitor to be created @@ -89,29 +91,36 @@ describe('CompositeLevelMonitor', () => { const monitorID = interceptor.response.body.resp._id; cy.contains('Loading monitors'); - cy.wait('@getMonitorsRequest'); - - // Let monitor's table render the rows before querying - cy.wait(1000).then(() => { - cy.get('table tbody td').contains(SAMPLE_VISUAL_EDITOR_MONITOR); - - // Load sample data - cy.insertDocumentToIndex( - sample_index_1, - undefined, - sampleCompositeJson.sample_composite_associated_index_document - ); - cy.insertDocumentToIndex( - sample_index_2, - undefined, - sampleCompositeJson.sample_composite_associated_index_document - ); + cy.wait('@getMonitorsRequest').then((interceptor) => { + const monitors = interceptor.response.body.monitors; + const monitor1 = monitors.filter((monitor) => monitor.name === 'monitor_1'); + const monitor2 = monitors.filter((monitor) => monitor.name === 'monitor_2'); + // Let monitor's table render the rows before querying cy.wait(1000).then(() => { - cy.executeCompositeMonitor(monitorID); - - cy.get('[role="tab"]').contains('Alerts').click(); - cy.get('table tbody td').contains('Composite trigger'); + cy.get('table tbody td').contains(SAMPLE_VISUAL_EDITOR_MONITOR); + + // Load sample data + cy.insertDocumentToIndex( + sample_index_1, + undefined, + sampleCompositeJson.sample_composite_associated_index_document + ); + cy.insertDocumentToIndex( + sample_index_2, + undefined, + sampleCompositeJson.sample_composite_associated_index_document + ); + + cy.wait(1000).then(() => { + cy.executeCompositeMonitor(monitorID); + debugger; + monitor1[0] && cy.executeMonitor(monitor1[0].id); + monitor2[0] && cy.executeMonitor(monitor2[0].id); + + cy.get('[role="tab"]').contains('Alerts').click(); + cy.get('table tbody td').contains('Composite trigger'); + }); }); }); }); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 9568d43ba..4bfcd67f0 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -85,6 +85,10 @@ Cypress.Commands.add('createAndExecuteMonitor', (monitorJSON) => { ); }); +Cypress.Commands.add('executeMonitor', (monitorID) => { + cy.request('POST', `${Cypress.env('opensearch')}${API.MONITOR_BASE}/${monitorID}/_execute`); +}); + Cypress.Commands.add('executeCompositeMonitor', (monitorID) => { cy.request('POST', `${Cypress.env('opensearch')}${API.WORKFLOW_BASE}/${monitorID}/_execute`); }); From 88d7ac99a372a647c7773e8397a9d71872e535ad Mon Sep 17 00:00:00 2001 From: Jovan Cvetkovic Date: Tue, 20 Jun 2023 18:44:53 +0200 Subject: [PATCH 14/63] [FEATURE] Add composite monitor type #573 Signed-off-by: Jovan Cvetkovic --- .../CreateMonitor/utils/constants.js | 2 +- .../CreateMonitor/utils/monitorToFormik.js | 14 ++++- .../WorkflowDetails/WorkflowDetails.tsx | 44 +++++++------- .../ExpressionQuery/ExpressionQuery.js | 29 ++-------- .../CreateTrigger/utils/formikToTrigger.js | 7 ++- .../CreateTrigger/utils/triggerToFormik.js | 10 +++- .../DefineCompositeLevelTrigger.js | 58 ++++++++++++++++--- 7 files changed, 103 insertions(+), 61 deletions(-) diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js b/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js index 2973014e7..e2109ec67 100644 --- a/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js +++ b/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js @@ -62,7 +62,7 @@ export const FORMIK_INITIAL_VALUES = { filters: [], // array of FORMIK_INITIAL_WHERE_EXPRESSION_VALUES detectorId: '', associatedMonitors: [], - expressionQuery: null, + expressionQueries: [], }; export const FORMIK_INITIAL_AGG_VALUES = { diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/utils/monitorToFormik.js b/public/pages/CreateMonitor/containers/CreateMonitor/utils/monitorToFormik.js index 43d460a40..d2614c1ed 100644 --- a/public/pages/CreateMonitor/containers/CreateMonitor/utils/monitorToFormik.js +++ b/public/pages/CreateMonitor/containers/CreateMonitor/utils/monitorToFormik.js @@ -41,7 +41,19 @@ export default function monitorToFormik(monitor) { case MONITOR_TYPE.DOC_LEVEL: return docLevelInputToFormik(monitor); case MONITOR_TYPE.COMPOSITE_LEVEL: - return { inputs: _.get(monitor, 'inputs[0].composite_input.sequence.delegates', []) }; + const associatedMonitors = _.get( + monitor, + 'inputs[0].composite_input.sequence.delegates', + [] + ); + + return { + inputs: associatedMonitors, + associatedMonitors: associatedMonitors.map((mon) => ({ + label: '', + value: mon.monitor_id, + })), + }; default: return { index: indicesToFormik(inputs[0].search.indices), diff --git a/public/pages/CreateMonitor/containers/WorkflowDetails/WorkflowDetails.tsx b/public/pages/CreateMonitor/containers/WorkflowDetails/WorkflowDetails.tsx index ac7b69a97..a00669255 100644 --- a/public/pages/CreateMonitor/containers/WorkflowDetails/WorkflowDetails.tsx +++ b/public/pages/CreateMonitor/containers/WorkflowDetails/WorkflowDetails.tsx @@ -10,33 +10,33 @@ import AssociateMonitors from '../../components/AssociateMonitors/AssociateMonit import { EuiSpacer } from '@elastic/eui'; import * as _ from 'lodash'; +export const getMonitors = async (httpClient) => { + const response = await httpClient.get('../api/alerting/monitors', { + query: { + from: 0, + size: 1000, + search: '', + sortField: 'name', + sortDirection: 'desc', + state: 'all', + }, + }); + + if (response.ok) { + const { monitors, totalMonitors } = response; + return monitors.map((monitor) => ({ monitor_id: monitor.id, monitor_name: monitor.name })); + } else { + console.log('error getting monitors:', response); + return []; + } +}; + const WorkflowDetails = ({ isAd, isComposite, httpClient, history, values }) => { const [selectedMonitors, setSelectedMonitors] = useState([]); const [monitorOptions, setMonitorOptions] = useState([]); - const getMonitors = async () => { - const response = await httpClient.get('../api/alerting/monitors', { - query: { - from: 0, - size: 1000, - search: '', - sortField: 'name', - sortDirection: 'desc', - state: 'all', - }, - }); - - if (response.ok) { - const { monitors, totalMonitors } = response; - return monitors.map((monitor) => ({ monitor_id: monitor.id, monitor_name: monitor.name })); - } else { - console.log('error getting monitors:', response); - return []; - } - }; - useEffect(() => { - getMonitors().then((monitors) => { + getMonitors(httpClient).then((monitors) => { setMonitorOptions(monitors); const inputIds = values.inputs?.map((input) => input.monitor_id); diff --git a/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js b/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js index c4c5a1703..f642de55f 100644 --- a/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js +++ b/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js @@ -17,38 +17,17 @@ const ExpressionQuery = ({ defaultText, label, formikName = 'expressionQueries', + triggerValues, }) => { const DEFAULT_DESCRIPTION = defaultText ? defaultText : 'Select'; - const OPERATORS = ['AND', 'OR', 'NOT']; const [usedExpressions, setUsedExpressions] = useState([]); useEffect(() => { - let expressions = []; if (value?.length) { - let values = [...value]; - if (OPERATORS.indexOf(values[0]?.description) === -1) values = ['', ...values]; - - let counter = 0; - values.map((exp, idx) => { - if (idx % 2 === 0) { - expressions.push({ - description: exp.description, - isOpen: false, - monitor_name: '', - monitor_id: '', - }); - counter++; - } else { - const currentIndex = idx - counter; - expressions[currentIndex] = { ...expressions[currentIndex], ...exp }; - } - }); - } else { - expressions = []; + setUsedExpressions(value); + _.set(triggerValues, formikName, getValue(value)); } - - setUsedExpressions(expressions); - }, []); + }, [value]); const getValue = (expressions) => expressions.map((exp) => ({ diff --git a/public/pages/CreateTrigger/containers/CreateTrigger/utils/formikToTrigger.js b/public/pages/CreateTrigger/containers/CreateTrigger/utils/formikToTrigger.js index 83e09017c..2c1e7d332 100644 --- a/public/pages/CreateTrigger/containers/CreateTrigger/utils/formikToTrigger.js +++ b/public/pages/CreateTrigger/containers/CreateTrigger/utils/formikToTrigger.js @@ -125,14 +125,15 @@ export function formikToCompositeTriggerCondition(values) { const triggerConditions = _.get(values, 'triggerConditions', []); const source = triggerConditions.reduce((query, expression) => { - query += ` ${conditionMap[expression.condition]} (monitor[id=${expression.monitor_id}])`; - return query.trim(); + query += ` ${conditionMap[expression.condition]} monitor[id=${expression.monitor_id}]`; + query = query.trim(); + return query; }, ''); return { script: { lang: 'painless', - source: source, + source: `(${source})`, }, }; } diff --git a/public/pages/CreateTrigger/containers/CreateTrigger/utils/triggerToFormik.js b/public/pages/CreateTrigger/containers/CreateTrigger/utils/triggerToFormik.js index 69171ddaf..4b45d270c 100644 --- a/public/pages/CreateTrigger/containers/CreateTrigger/utils/triggerToFormik.js +++ b/public/pages/CreateTrigger/containers/CreateTrigger/utils/triggerToFormik.js @@ -14,6 +14,7 @@ import { ACTIONABLE_ALERTS_OPTIONS_LABELS, NOTIFY_OPTIONS_VALUES, } from '../../../components/Action/actions/Message'; +import { convertQueryToExpressions } from '../../DefineCompositeLevelTrigger/DefineCompositeLevelTrigger'; export function triggerToFormik(trigger, monitor) { return _.isArray(trigger) @@ -226,7 +227,12 @@ export function compositeTriggerToFormik(trigger, monitor) { condition: { script }, actions, } = trigger[TRIGGER_TYPE.COMPOSITE_LEVEL]; - const triggerUiMetadata = _.get(monitor, `ui_metadata.triggers[${name}]`); + // TODO this should be saved in ui_metadata, currently workflows don't save ui_metadata + // const triggerUiMetadata = _.get(monitor, `ui_metadata.triggers[${name}]`); + const triggerConditions = convertQueryToExpressions( + monitor.triggers[0].chained_alert_trigger.condition.script.source, + [] + ); return { ..._.cloneDeep(FORMIK_INITIAL_TRIGGER_VALUES), id, @@ -234,7 +240,7 @@ export function compositeTriggerToFormik(trigger, monitor) { severity, script, actions: getExecutionPolicyActions(actions), - triggerConditions: triggerUiMetadata, + triggerConditions: triggerConditions, //triggerUiMetadata, }; } diff --git a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/DefineCompositeLevelTrigger.js b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/DefineCompositeLevelTrigger.js index 6737854a4..441d9c0ea 100644 --- a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/DefineCompositeLevelTrigger.js +++ b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/DefineCompositeLevelTrigger.js @@ -14,6 +14,7 @@ import ExpressionQuery from '../../components/ExpressionQuery/ExpressionQuery'; import TriggerNotifications from './TriggerNotifications'; import ContentPanel from '../../../../components/ContentPanel'; import { FORMIK_INITIAL_TRIGGER_VALUES } from '../CreateTrigger/utils/constants'; +import { getMonitors } from '../../../CreateMonitor/containers/WorkflowDetails/WorkflowDetails'; const defaultRowProps = { label: 'Trigger name', @@ -57,19 +58,64 @@ export const titleTemplate = (title, subTitle) => ( ); +export const convertQueryToExpressions = (query, monitors) => { + const conditionMap = { + '&&': 'and', + '||': 'or', + '!': 'not', + '': '', + }; + const queryToExpressionRegex = new RegExp('(&& )?(\\|\\| )?(monitor\\[id=(.*?)\\])', 'g'); + const matcher = query.matchAll(queryToExpressionRegex); + let match; + let expressions = []; + while ((match = matcher.next().value)) { + const monitorId = match[4]?.trim(); + const monitor = monitors.filter((mon) => mon.monitor_id === monitorId); + expressions.push({ + description: conditionMap[match[1]?.trim()] || '', + isOpen: false, + monitor_name: monitor[0]?.monitor_name, + monitor_id: monitorId, + }); + } + + return expressions; +}; + class DefineCompositeLevelTrigger extends Component { constructor(props) { super(props); - this.state = {}; + this.state = { + expressions: [], + }; + } + + componentDidMount() { + getMonitors(this.props.httpClient).then((monitors) => { + const inputIds = this.props.monitorValues.inputs?.map((input) => input.monitor_id); + if (inputIds && inputIds.length) { + const selectedMonitors = monitors.filter( + (monitor) => inputIds.indexOf(monitor.monitor_id) !== -1 + ); + + const expressions = convertQueryToExpressions( + this.props.triggerValues.triggerDefinitions[0].script.source, + selectedMonitors + ); + + this.setState({ + expressions, + }); + } + }); } render() { const { edit, monitorValues, - triggers, triggerValues, - isDarkMode, httpClient, notifications, notificationService, @@ -94,6 +140,7 @@ class DefineCompositeLevelTrigger extends Component { monitor_id: monitor.value, })) : []; + return ( [monitor, { description: 'and' }])) - .slice(0, -1)} - onChange={() => {}} + value={this.state.expressions} dataTestSubj={'composite_expression_query'} defaultText={'Select associated monitor'} triggerValues={triggerValues} From 11ebb8dc48bcef49b70ae5353410dacc5b76d8f5 Mon Sep 17 00:00:00 2001 From: Jovan Cvetkovic Date: Wed, 21 Jun 2023 11:52:39 +0200 Subject: [PATCH 15/63] [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} /> From 59ec9d16a28c02b1f7cc635200563f808c63e5f2 Mon Sep 17 00:00:00 2001 From: Amardeepsingh Siglani Date: Wed, 21 Jun 2023 19:58:53 -0700 Subject: [PATCH 16/63] new columns with deletion modal for monitors list; added condition column to triggers Signed-off-by: Amardeepsingh Siglani --- public/components/Breadcrumbs/Breadcrumbs.js | 5 +- .../DeleteModal/DeleteMonitorModal.tsx | 62 ++++++++ .../containers/MonitorDetails.js | 4 +- .../containers/Triggers/Triggers.js | 17 +- .../MonitorActions/MonitorActions.js | 3 +- .../Monitors/containers/Monitors/Monitors.js | 148 ++++++++++-------- .../containers/Monitors/utils/helpers.js | 18 +++ .../containers/Monitors/utils/tableUtils.js | 27 +++- server/services/AlertService.js | 2 + server/services/MonitorService.js | 11 +- 10 files changed, 215 insertions(+), 82 deletions(-) create mode 100644 public/components/DeleteModal/DeleteMonitorModal.tsx diff --git a/public/components/Breadcrumbs/Breadcrumbs.js b/public/components/Breadcrumbs/Breadcrumbs.js index 3ba41a8ff..1fa9d6ebd 100644 --- a/public/components/Breadcrumbs/Breadcrumbs.js +++ b/public/components/Breadcrumbs/Breadcrumbs.js @@ -106,7 +106,7 @@ export async function getBreadcrumb(route, routeState, httpClient) { // This condition is true for any auto generated 20 character long, // URL-safe, base64-encoded document ID by opensearch if (RegExp(/^[0-9a-z_-]{20}$/i).test(base)) { - const { action } = queryString.parse(`?${queryParams}`); + const { action, type } = queryString.parse(`?${queryParams}`); switch (action) { case DESTINATION_ACTIONS.UPDATE_DESTINATION: const destinationName = _.get(routeState, 'destinationToEdit.name', base); @@ -119,7 +119,8 @@ export async function getBreadcrumb(route, routeState, httpClient) { // TODO::Everything else is considered as monitor, we should break this. let monitorName = base; try { - const response = await httpClient.get(`../api/alerting/monitors/${base}`); + const searchPool = type === 'workflow' ? 'workflows' : 'monitors'; + const response = await httpClient.get(`../api/alerting/${searchPool}/${base}`); if (response.ok) { monitorName = response.resp.name; } diff --git a/public/components/DeleteModal/DeleteMonitorModal.tsx b/public/components/DeleteModal/DeleteMonitorModal.tsx new file mode 100644 index 000000000..7b12c4436 --- /dev/null +++ b/public/components/DeleteModal/DeleteMonitorModal.tsx @@ -0,0 +1,62 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { ChangeEvent, Component } from 'react'; +import { + EuiConfirmModal, + EuiOverlayMask +} from '@elastic/eui'; + +interface DeleteModalProps { + monitorNames: string[]; + onClickDelete: (event?: any) => void; + closeDeleteModal: (event?: any) => void; +} + +export const DEFAULT_DELETION_TEXT = 'delete'; + +export default class DeleteMonitorModal extends Component { + render() { + const { + monitorNames, + closeDeleteModal, + onClickDelete + } = this.props; + + let warningHeading = `Delete monitor ${monitorNames[0]}?`; + let warningBody: React.ReactNode = 'This action cannot be undone.'; + + if (monitorNames.length > 1) { + warningHeading = `Delete ${monitorNames.length} monitors?`; + warningBody = ( + <> + {`The following monitors will be permanently deleted. ${warningBody}`} +
    + {monitorNames.map((name, idx) =>
  • {name}
  • )} +
+ + ) + } + + return ( + + { + onClickDelete(); + closeDeleteModal(); + }} + cancelButtonText={'Cancel'} + confirmButtonText={'Delete'} + buttonColor={'danger'} + defaultFocusedButton="confirm" + > + {warningBody} + + + ); + } +} diff --git a/public/pages/MonitorDetails/containers/MonitorDetails.js b/public/pages/MonitorDetails/containers/MonitorDetails.js index babe7e62d..adc7bb3f0 100644 --- a/public/pages/MonitorDetails/containers/MonitorDetails.js +++ b/public/pages/MonitorDetails/containers/MonitorDetails.js @@ -72,7 +72,9 @@ export default class MonitorDetails extends Component { }; } - isWorkflow = () => new URLSearchParams(this.props.location.search).get('type') === 'workflow'; + isWorkflow = () => { + return new URLSearchParams(this.props.location.search).get('type') === 'workflow'; + }; componentDidMount() { this.getMonitor(this.props.match.params.monitorId); diff --git a/public/pages/MonitorDetails/containers/Triggers/Triggers.js b/public/pages/MonitorDetails/containers/Triggers/Triggers.js index 63838636f..3109cd19a 100644 --- a/public/pages/MonitorDetails/containers/Triggers/Triggers.js +++ b/public/pages/MonitorDetails/containers/Triggers/Triggers.js @@ -98,7 +98,15 @@ export default class Triggers extends Component { name: 'Name', sortable: true, truncateText: true, - width: '25%', + width: '15%', + }, + { + name: 'Condition -- (formatting pending...)', + truncateText: true, + render: (item) => { + return item.condition.script.source; + }, + width: '50%', }, { field: 'actions', @@ -106,18 +114,19 @@ export default class Triggers extends Component { sortable: true, truncateText: false, render: (actions) => actions.length, - width: '25%', + width: '15%', }, { field: 'severity', name: 'Severity', sortable: true, truncateText: false, - width: '50%', + width: '20%', }, ]; const sorting = { sort: { field, direction } }; + const items = getUnwrappedTriggers(monitor); return ( { // TODO: Support bulk acknowledge alerts across multiple monitors after figuring out the correct parameter for getAlerts API. // Disabling the acknowledge button for now when more than 1 monitors selected. - const { isEditDisabled } = this.props; + const { isEditDisabled, isDeleteDisabled } = this.props; const actions = [ Delete , diff --git a/public/pages/Monitors/containers/Monitors/Monitors.js b/public/pages/Monitors/containers/Monitors/Monitors.js index 9367aafa0..c9c8926d8 100644 --- a/public/pages/Monitors/containers/Monitors/Monitors.js +++ b/public/pages/Monitors/containers/Monitors/Monitors.js @@ -18,6 +18,7 @@ import { columns as staticColumns } from './utils/tableUtils'; import { MONITOR_ACTIONS, MONITOR_TYPE } from '../../../../utils/constants'; import { backendErrorNotification } from '../../../../utils/helpers'; import { displayAcknowledgedAlertsToast } from '../../../Dashboard/utils/helpers'; +import DeleteMonitorModal from '../../../../components/DeleteModal/DeleteMonitorModal'; const MAX_MONITOR_COUNT = 1000; @@ -45,6 +46,7 @@ export default class Monitors extends Component { monitors: [], monitorState: state, loadingMonitors: true, + monitorItemsToDelete: undefined, }; this.getMonitors = _.debounce(this.getMonitors.bind(this), 500, { leading: true }); @@ -135,17 +137,25 @@ export default class Monitors extends Component { const unwrappedMonitors = []; monitors.forEach((monitor) => { const monitorType = _.get(monitor, 'monitor.monitor.monitor_type', 'monitor.monitor_type'); + let unwrappedMonitor = monitor.monitor; switch (monitorType) { case MONITOR_TYPE.CLUSTER_METRICS: - let unwrappedMonitor = monitor.monitor; _.set(monitor, 'monitor', unwrappedMonitor.monitor); _.set(monitor, 'name', monitor.monitor.name); _.set(monitor, 'enabled', monitor.monitor.enabled); - unwrappedMonitors.push(monitor); + _.set(monitor, 'item_type', monitorType); break; default: - unwrappedMonitors.push(monitor); + _.set( + monitor, + 'item_type', + unwrappedMonitor.monitor_type || unwrappedMonitor.workflow_type, + '-' + ); + break; } + + unwrappedMonitors.push(monitor); }); return unwrappedMonitors; }; @@ -294,7 +304,9 @@ export default class Monitors extends Component { } onClickDelete(item) { - this.deleteMonitors([item]); + this.setState({ + monitorItemsToDelete: [item], + }); } onClickDisable(item) { @@ -310,7 +322,7 @@ export default class Monitors extends Component { } onBulkDelete() { - this.deleteMonitors(this.state.selectedItems); + this.setState({ monitorItemsToDelete: this.state.selectedItems }); } onBulkDisable() { @@ -404,67 +416,77 @@ export default class Monitors extends Component { }; return ( - - } - bodyStyles={{ padding: 'initial' }} - title="Monitors" - > - - - - - {showAcknowledgeModal && ( - + + } + bodyStyles={{ padding: 'initial' }} + title="Monitors" + > + - )} - + + {showAcknowledgeModal && ( + - } - onChange={this.onTableChange} - pagination={pagination} - selection={selection} - sorting={sorting} - /> - + )} + + + } + onChange={this.onTableChange} + pagination={pagination} + selection={selection} + sorting={sorting} + /> + + {this.state.monitorItemsToDelete && ( + item.name)} + closeDeleteModal={() => this.setState({ monitorItemsToDelete: undefined })} + onClickDelete={() => this.deleteMonitors(this.state.monitorItemsToDelete)} + /> + )} + ); } } diff --git a/public/pages/Monitors/containers/Monitors/utils/helpers.js b/public/pages/Monitors/containers/Monitors/utils/helpers.js index 806fb74d0..847bd3a5e 100644 --- a/public/pages/Monitors/containers/Monitors/utils/helpers.js +++ b/public/pages/Monitors/containers/Monitors/utils/helpers.js @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { MONITOR_TYPE } from '../../../../../utils/constants'; import { DEFAULT_QUERY_PARAMS } from './constants'; import queryString from 'query-string'; @@ -25,3 +26,20 @@ export function getURLQueryParams(location) { state, }; } + +export function getItemLevelType(itemType) { + switch (itemType) { + case MONITOR_TYPE.QUERY_LEVEL: + return 'Per query'; + case MONITOR_TYPE.BUCKET_LEVEL: + return 'Per bucket'; + case MONITOR_TYPE.CLUSTER_METRICS: + return 'Per cluster metrics'; + case MONITOR_TYPE.DOC_LEVEL: + return 'Per document'; + case MONITOR_TYPE.COMPOSITE_LEVEL: + return 'Composite'; + default: + return '-'; + } +} diff --git a/public/pages/Monitors/containers/Monitors/utils/tableUtils.js b/public/pages/Monitors/containers/Monitors/utils/tableUtils.js index 60201bd1f..ad2ae9248 100644 --- a/public/pages/Monitors/containers/Monitors/utils/tableUtils.js +++ b/public/pages/Monitors/containers/Monitors/utils/tableUtils.js @@ -8,6 +8,7 @@ import { EuiLink } from '@elastic/eui'; import moment from 'moment'; import { DEFAULT_EMPTY_DATA } from '../../../../../utils/constants'; import { PLUGIN_NAME } from '../../../../../../utils/constants'; +import { getItemLevelType } from './helpers'; const renderTime = (time) => { const momentTime = moment(time); @@ -22,11 +23,28 @@ export const columns = [ sortable: true, textOnly: true, render: (name, item) => ( - + {name} ), }, + { + field: 'enabled', + name: 'State', + sortable: false, + truncateText: false, + render: (enabled) => (enabled ? 'Enabled' : 'Disabled'), + }, + { + field: 'item_type', + name: 'Type', + sortable: false, + truncateText: false, + render: (item_type) => getItemLevelType(item_type), + }, { field: 'user', name: 'Last updated by', @@ -48,13 +66,6 @@ export const columns = [ truncateText: true, textOnly: true, }, - { - field: 'enabled', - name: 'State', - sortable: false, - truncateText: false, - render: (enabled) => (enabled ? 'Enabled' : 'Disabled'), - }, { field: 'lastNotificationTime', name: 'Last notification time', diff --git a/server/services/AlertService.js b/server/services/AlertService.js index 80d0a1d8b..6f00b2589 100644 --- a/server/services/AlertService.js +++ b/server/services/AlertService.js @@ -19,6 +19,7 @@ export default class AlertService { } getAlerts = async (context, req, res) => { + console.log('****** GET ALERTS *****'); const { from = 0, size = 20, @@ -78,6 +79,7 @@ export default class AlertService { const { callAsCurrentUser } = this.esDriver.asScoped(req); try { const resp = await callAsCurrentUser('alerting.getAlerts', params); + console.log(params); const alerts = resp.alerts.map((hit) => { const alert = hit; const id = hit.alert_id; diff --git a/server/services/MonitorService.js b/server/services/MonitorService.js index 8e4cec408..1869c5324 100644 --- a/server/services/MonitorService.js +++ b/server/services/MonitorService.js @@ -80,6 +80,7 @@ export default class MonitorService { }; getMonitor = async (context, req, res) => { + console.log('****** GET MONITOR *****'); try { const { id } = req.params; const params = { monitorId: id }; @@ -147,6 +148,7 @@ export default class MonitorService { }; getWorkflow = async (context, req, res) => { + console.log('****** GET WORKFLOW *****'); try { const { id } = req.params; const params = { monitorId: id }; @@ -170,7 +172,7 @@ export default class MonitorService { }, }); } catch (err) { - console.error('Alerting - MonitorService - getMonitor:', err); + console.error('Alerting - MonitorService - getWorkflow:', err); return res.ok({ body: { ok: false, @@ -218,6 +220,7 @@ export default class MonitorService { }; getMonitors = async (context, req, res) => { + console.log('****** GET MONITORS *****'); try { const { from, size, search, sortDirection, sortField, state } = req.query; @@ -235,9 +238,11 @@ export default class MonitorService { }; } + const should = []; if (state !== 'all') { const enabled = state === 'enabled'; - filter.push({ term: { 'monitor.enabled': enabled } }); + should.push({ term: { 'monitor.enabled': enabled } }); + should.push({ term: { 'workflow.enabled': enabled } }); } const monitorSorts = { name: 'monitor.name.keyword' }; @@ -255,7 +260,7 @@ export default class MonitorService { ...monitorSortPageData, query: { bool: { - must, + should, }, }, }, From 727a6920847cc8dd8dd699fac2127ec34cb74f17 Mon Sep 17 00:00:00 2001 From: Jovan Cvetkovic Date: Thu, 22 Jun 2023 11:05:20 +0200 Subject: [PATCH 17/63] Code review #573 Signed-off-by: Jovan Cvetkovic --- .../AssociateMonitors/AssociateMonitors.tsx | 2 +- .../ExpressionQuery/ExpressionQuery.js | 149 ++++++++++-------- 2 files changed, 81 insertions(+), 70 deletions(-) diff --git a/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.tsx b/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.tsx index e47558c52..2203f5371 100644 --- a/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.tsx +++ b/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.tsx @@ -49,7 +49,7 @@ const AssociateMonitors = ({ _.set(monitorValues, 'associatedMonitorsEditor', JSON.stringify(value, null, 4)); _.set(monitorValues, 'associatedMonitors', delegatesToMonitors(value)); } else { - if (options?.length) { + if (options?.length && !graphUi) { const value = { ...queryTemplate }; const firstTwo = options.slice(0, 2); firstTwo.map((monitor, idx) => { diff --git a/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js b/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js index bb0447797..3713712a5 100644 --- a/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js +++ b/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js @@ -21,8 +21,19 @@ const ExpressionQuery = ({ triggerValues, isDarkMode = false, }) => { - const DEFAULT_DESCRIPTION = defaultText ? defaultText : 'Select'; - const [usedExpressions, setUsedExpressions] = useState([]); + const DEFAULT_CONDITION = 'AND'; + const DEFAULT_NAME = defaultText ? defaultText : 'Select'; + const DEFAULT_EXPRESSION = { + description: '', + isOpen: false, + monitor_id: '', + monitor_name: DEFAULT_NAME, + }; + const DEFAULT_NEXT_EXPRESSION = { + ...DEFAULT_EXPRESSION, + description: DEFAULT_CONDITION, + }; + const [usedExpressions, setUsedExpressions] = useState([DEFAULT_EXPRESSION]); const [graphUi, setGraphUi] = useState(triggerValues.searchType === 'graph'); const [editorValue, setEditorValue] = useState(''); @@ -46,7 +57,7 @@ const ExpressionQuery = ({ setEditorValue(script); _.set(triggerValues, 'triggerDefinitions[0].script.source', script); } - }, [value, triggerValues.searchType]); + }, [value, triggerValues.searchType, selections]); const getValue = (expressions) => expressions.map((exp) => ({ @@ -57,42 +68,79 @@ const ExpressionQuery = ({ const changeMonitor = (selection, exp, idx, form) => { const expressions = _.cloneDeep(usedExpressions); - expressions[idx] = { - ...expressions[idx], - monitor_id: selection[0].monitor_id, - monitor_name: selection[0].label, - }; + let monitor = selection[0]; + if (monitor) { + expressions[idx] = { + ...expressions[idx], + monitor_id: monitor.monitor_id, + monitor_name: monitor.label, + }; + } else { + expressions[idx] = idx ? DEFAULT_NEXT_EXPRESSION : DEFAULT_EXPRESSION; + } setUsedExpressions(expressions); - onBlur(form, expressions); + onChange(form, expressions); }; const changeCondition = (selection, exp, idx, form) => { const expressions = _.cloneDeep(usedExpressions); expressions[idx] = { ...expressions[idx], description: selection[0].description }; setUsedExpressions(expressions); - onBlur(form, expressions); + onChange(form, expressions); }; - const onBlur = (form, expressions) => { + const onChange = (form, expressions) => { form.setFieldTouched('expressionQueries', true); form.setFieldValue(formikName, getValue(expressions)); + }; + + const onBlur = (form, expressions) => { + onChange(form, expressions); form.setFieldError('expressionQueries', validate()); }; - const openPopover = (idx) => { + const openPopover = (idx = 0, form) => { const expressions = _.cloneDeep(usedExpressions); expressions[idx] = { ...expressions[idx], isOpen: !expressions[idx].isOpen }; setUsedExpressions(expressions); }; - const closePopover = (idx) => { + const closePopover = (idx, form) => { const expressions = _.cloneDeep(usedExpressions); expressions[idx] = { ...expressions[idx], isOpen: false }; setUsedExpressions(expressions); + onBlur(form, usedExpressions); + form.setFieldTouched(`expressionQueries_${idx}`, true); + }; + + const onRemoveExpression = (idx) => { + const expressions = _.cloneDeep(usedExpressions); + expressions.splice(idx, 1); + expressions.length && (expressions[0].description = ''); + + if (!expressions?.length) { + expressions.push(DEFAULT_EXPRESSION); + } + setUsedExpressions([...expressions]); + }; + + const hasInvalidExpression = () => { + return !!usedExpressions.filter((expression) => expression.monitor_id === '')?.length; + }; + + const isValid = () => + selections.length > 1 && usedExpressions.length > 1 && !hasInvalidExpression(); + + const validate = () => { + if (selections.length < 2) + return 'Trigger condition requires at least two associated monitors.'; + if (usedExpressions.length < 2) + return 'Trigger condition requires at least two monitors selected.'; + if (hasInvalidExpression()) return 'Invalid expressions.'; }; - const renderOptions = (expression, idx, form) => ( + const renderOptions = (expression, idx = 0, form) => ( changeCondition(selection, expression, idx, form)} + onBlur={() => onBlur(form, usedExpressions)} options={[ { description: '', label: '' }, { description: 'AND', label: 'AND' }, @@ -118,12 +167,7 @@ const ExpressionQuery = ({ { - const usedExp = _.cloneDeep(usedExpressions); - usedExp.splice(idx, 1); - usedExp.length && (usedExp[0].description = ''); - setUsedExpressions([...usedExp]); - }} + onClick={() => onRemoveExpression(idx)} iconType={'trash'} color="danger" aria-label={'Remove condition'} @@ -138,6 +182,7 @@ const ExpressionQuery = ({ singleSelection={{ asPlainText: true }} compressed onChange={(selection) => changeMonitor(selection, expression, idx, form)} + onBlur={() => onBlur(form, usedExpressions)} selectedOptions={[ { label: expression.monitor_name, @@ -161,15 +206,6 @@ const ExpressionQuery = ({ /> ); - const isValid = () => selections.length > 1 && usedExpressions.length > 1; - - const validate = () => { - if (selections.length < 2) - return 'Trigger condition requires at least two associated monitors.'; - if (usedExpressions.length < 2) - return 'Trigger condition requires at least two monitors selected.'; - }; - return ( form.touched['expressionQueries'] && graphUi && !isValid(), + isInvalid: () => { + return form.touched['expressionQueries'] && graphUi && !isValid(); + }, error: () => graphUi && validate(), style: { maxWidth: 'inherit', @@ -195,46 +233,29 @@ const ExpressionQuery = ({ data-test-subj={dataTestSubj} className={'expressionQueries'} > - {!usedExpressions.length && ( - - onBlur(form, usedExpressions)} - /> - } - isOpen={false} - panelPaddingSize="s" - anchorPosition="rightDown" - closePopover={() => onBlur(form, usedExpressions)} - /> - - )} {usedExpressions.map((expression, idx) => ( { e.preventDefault(); - openPopover(idx); + openPopover(idx, form); }} /> } isOpen={expression.isOpen} - closePopover={() => closePopover(idx)} + closePopover={() => closePopover(idx, form)} panelPaddingSize="s" anchorPosition="rightDown" > @@ -247,19 +268,10 @@ const ExpressionQuery = ({ { 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); + expressions.push({ + ...DEFAULT_NEXT_EXPRESSION, + }); + setUsedExpressions(expressions); }} color={'primary'} iconType="plusInCircleFilled" @@ -290,7 +302,6 @@ const ExpressionQuery = ({ form.setFieldValue('expressionQueries', query); }, onBlur: (e, field, form) => { - console.log('### triggerValues', triggerValues); form.setFieldTouched('expressionQueries', true); }, 'data-test-subj': 'expressionQueriesCodeEditor', From bf802c11609f2ccdb6f7eca84aa7632e9ccc52b6 Mon Sep 17 00:00:00 2001 From: Jovan Cvetkovic Date: Thu, 22 Jun 2023 11:05:56 +0200 Subject: [PATCH 18/63] Code review #573 Signed-off-by: Jovan Cvetkovic --- .../CreateTrigger/components/ExpressionQuery/ExpressionQuery.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js b/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js index 3713712a5..8c4a7c443 100644 --- a/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js +++ b/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js @@ -100,7 +100,7 @@ const ExpressionQuery = ({ form.setFieldError('expressionQueries', validate()); }; - const openPopover = (idx = 0, form) => { + const openPopover = (idx = 0) => { const expressions = _.cloneDeep(usedExpressions); expressions[idx] = { ...expressions[idx], isOpen: !expressions[idx].isOpen }; setUsedExpressions(expressions); From 475807fb61091d6b353502989ebf1789e24461f0 Mon Sep 17 00:00:00 2001 From: Jovan Cvetkovic Date: Thu, 22 Jun 2023 11:43:07 +0200 Subject: [PATCH 19/63] Code review #573 Signed-off-by: Jovan Cvetkovic --- .../TriggerNotifications.js | 1 + .../TriggerNotificationsContent.js | 39 ++++++++++++------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotifications.js b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotifications.js index bd5389fbe..089e923bd 100644 --- a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotifications.js +++ b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotifications.js @@ -129,6 +129,7 @@ const TriggerNotifications = ({ notifications={notifications} triggerValues={triggerValues} httpClient={httpClient} + hasNotifications={plugins.indexOf(OS_NOTIFICATION_PLUGIN) !== -1} /> ))} diff --git a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotificationsContent.js b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotificationsContent.js index 41cc9e919..b437f4404 100644 --- a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotificationsContent.js +++ b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotificationsContent.js @@ -10,6 +10,7 @@ import NotificationConfigDialog from './NotificationConfigDialog'; import _ from 'lodash'; import { FORMIK_INITIAL_ACTION_VALUES } from '../../utils/constants'; import { NOTIFY_OPTIONS_VALUES } from '../../components/Action/actions/Message'; +import { required } from '../../../../utils/validate'; const TriggerNotificationsContent = ({ action, @@ -18,19 +19,21 @@ const TriggerNotificationsContent = ({ triggerValues, httpClient, notifications, + hasNotifications, }) => { const [selected, setSelected] = useState([]); const [isModalVisible, setIsModalVisible] = useState(false); useEffect(() => { - setSelected([ - { - label: action.name, - value: action.config_id, - type: action.config_type, - description: action.description, - }, - ]); + action?.config_id && + setSelected([ + { + label: action.name, + value: action.config_id, + type: action.config_type, + description: action.description, + }, + ]); }, [action]); const onChange = (selectedOptions) => { @@ -51,6 +54,11 @@ const TriggerNotificationsContent = ({ }); }; + const onBlur = (e, field, form) => { + form.setFieldTouched(field.name, true); + form.setFieldError(field.name, required(form.values[field.name])); + }; + const showConfig = () => setIsModalVisible(true); return ( @@ -66,25 +74,28 @@ const TriggerNotificationsContent = ({ form.touched[name] && !selected?.length, + error: (name, form) => form.touched[name] && !selected?.length && 'Required.', }} inputProps={{ - isInvalid: !selected.length, placeholder: 'Select a channel to get notified', options: options, selectedOptions: selected, onChange: (selectedOptions) => onChange(selectedOptions), + onBlur: (e, field, form) => onBlur(e, field, form), singleSelection: { asPlainText: true }, }} /> - + Manage channels From c79666694451c29938385422c7c51a3af502ae34 Mon Sep 17 00:00:00 2001 From: Jovan Cvetkovic Date: Thu, 22 Jun 2023 11:50:26 +0200 Subject: [PATCH 20/63] Code review #573 Signed-off-by: Jovan Cvetkovic --- .../{AssociateMonitors.tsx => AssociateMonitors.js} | 0 .../components/{MonitorsList.tsx => MonitorsList.js} | 2 +- .../containers/WorkflowDetails/WorkflowDetails.tsx | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename public/pages/CreateMonitor/components/AssociateMonitors/{AssociateMonitors.tsx => AssociateMonitors.js} (100%) rename public/pages/CreateMonitor/components/AssociateMonitors/components/{MonitorsList.tsx => MonitorsList.js} (99%) diff --git a/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.tsx b/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.js similarity index 100% rename from public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.tsx rename to public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.js diff --git a/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.tsx b/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.js similarity index 99% rename from public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.tsx rename to public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.js index fd2bed0fb..444c11aae 100644 --- a/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.tsx +++ b/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.js @@ -24,7 +24,7 @@ const MonitorsList = ({ monitors = [], options = [] }) => { const [selectedOptions, setSelectedOptions] = useState({}); const [monitorOptions, setMonitorOptions] = useState([]); - const [monitorFields, setMonitorFields] = useState( + const [monitorFields, setMonitorFields] = useState( _.reduce( monitors.length ? monitors : [0, 1], (result, value, key) => { diff --git a/public/pages/CreateMonitor/containers/WorkflowDetails/WorkflowDetails.tsx b/public/pages/CreateMonitor/containers/WorkflowDetails/WorkflowDetails.tsx index d31cf77f9..e96434a72 100644 --- a/public/pages/CreateMonitor/containers/WorkflowDetails/WorkflowDetails.tsx +++ b/public/pages/CreateMonitor/containers/WorkflowDetails/WorkflowDetails.tsx @@ -6,7 +6,7 @@ import React, { Fragment, useEffect, useState } from 'react'; import ContentPanel from '../../../../components/ContentPanel'; import Schedule from '../../components/Schedule'; -import AssociateMonitors from '../../components/AssociateMonitors/AssociateMonitors'; +import AssociateMonitors from '../../components/AssociateMonitors/AssociateMonitors.js'; import { EuiSpacer } from '@elastic/eui'; import * as _ from 'lodash'; From 2ad090aa3f94eadf956a35104bfc3d1a0eb5c4aa Mon Sep 17 00:00:00 2001 From: Jovan Cvetkovic Date: Thu, 22 Jun 2023 11:52:08 +0200 Subject: [PATCH 21/63] Code review #573 Signed-off-by: Jovan Cvetkovic --- .../AssociateMonitors/AssociateMonitors.js | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.js b/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.js index 2203f5371..e25f17c12 100644 --- a/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.js +++ b/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.js @@ -48,23 +48,21 @@ const AssociateMonitors = ({ _.set(monitorValues, 'associatedMonitorsEditor', JSON.stringify(value, null, 4)); _.set(monitorValues, 'associatedMonitors', delegatesToMonitors(value)); - } else { - if (options?.length && !graphUi) { - 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, - }); + } else if (options?.length && !graphUi) { + 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.'); - } + try { + _.set(monitorValues, 'associatedMonitorsEditor', JSON.stringify(value, null, 4)); + _.set(monitorValues, 'associatedMonitors', delegatesToMonitors(value)); + } catch (e) { + console.log('No monitor options are available.'); } } From bcd173bdfb1e72f73311fb05352b4dd32f7f054c Mon Sep 17 00:00:00 2001 From: Jovan Cvetkovic Date: Thu, 22 Jun 2023 11:53:45 +0200 Subject: [PATCH 22/63] Code review #573 Signed-off-by: Jovan Cvetkovic --- .../components/AssociateMonitors/AssociateMonitors.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.js b/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.js index e25f17c12..56b3b8df3 100644 --- a/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.js +++ b/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.js @@ -34,7 +34,7 @@ const AssociateMonitors = ({ useEffect(() => { if (monitors?.length) { const value = { ...queryTemplate }; - monitors.map((monitor, idx) => { + monitors.forEach((monitor, idx) => { let delegate = { order: idx + 1, monitor_id: monitor.monitor_id, From 8a024033ce77651fe4d0086d23ace253e7a1e6b0 Mon Sep 17 00:00:00 2001 From: Jovan Cvetkovic Date: Thu, 22 Jun 2023 11:55:48 +0200 Subject: [PATCH 23/63] Code review #573 Signed-off-by: Jovan Cvetkovic --- .../components/AssociateMonitors/AssociateMonitors.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.js b/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.js index 56b3b8df3..59b55c658 100644 --- a/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.js +++ b/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.js @@ -46,8 +46,12 @@ const AssociateMonitors = ({ }); }); - _.set(monitorValues, 'associatedMonitorsEditor', JSON.stringify(value, null, 4)); - _.set(monitorValues, 'associatedMonitors', delegatesToMonitors(value)); + try { + _.set(monitorValues, 'associatedMonitorsEditor', JSON.stringify(value, null, 4)); + _.set(monitorValues, 'associatedMonitors', delegatesToMonitors(value)); + } catch (e) { + console.log('No monitor options are available.'); + } } else if (options?.length && !graphUi) { const value = { ...queryTemplate }; const firstTwo = options.slice(0, 2); From 87829fd9e90ebd2d9c37a176b504fe88945f7c71 Mon Sep 17 00:00:00 2001 From: Jovan Cvetkovic Date: Thu, 22 Jun 2023 12:03:55 +0200 Subject: [PATCH 24/63] Code review #573 Signed-off-by: Jovan Cvetkovic --- .../AssociateMonitors/AssociateMonitors.js | 18 +++++++++------- .../components/MonitorsList.js | 21 +++++++++++-------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.js b/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.js index 59b55c658..761717927 100644 --- a/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.js +++ b/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.js @@ -19,6 +19,8 @@ const AssociateMonitors = ({ }) => { const [graphUi, setGraphUi] = useState(searchType === 'graph'); + const fieldName = 'associatedMonitors'; + const fieldEditorName = 'associatedMonitorsEditor'; const queryTemplate = { sequence: { delegates: [], @@ -47,8 +49,8 @@ const AssociateMonitors = ({ }); try { - _.set(monitorValues, 'associatedMonitorsEditor', JSON.stringify(value, null, 4)); - _.set(monitorValues, 'associatedMonitors', delegatesToMonitors(value)); + _.set(monitorValues, fieldEditorName, JSON.stringify(value, null, 4)); + _.set(monitorValues, fieldName, delegatesToMonitors(value)); } catch (e) { console.log('No monitor options are available.'); } @@ -63,8 +65,8 @@ const AssociateMonitors = ({ }); try { - _.set(monitorValues, 'associatedMonitorsEditor', JSON.stringify(value, null, 4)); - _.set(monitorValues, 'associatedMonitors', delegatesToMonitors(value)); + _.set(monitorValues, fieldEditorName, JSON.stringify(value, null, 4)); + _.set(monitorValues, fieldName, delegatesToMonitors(value)); } catch (e) { console.log('No monitor options are available.'); } @@ -75,10 +77,10 @@ const AssociateMonitors = ({ const onCodeChange = useCallback( (query, field, form) => { - form.setFieldValue('associatedMonitorsEditor', query); + form.setFieldValue(fieldEditorName, query); try { const code = JSON.parse(query); - form.setFieldValue('associatedMonitors', delegatesToMonitors(code)); + form.setFieldValue(fieldName, delegatesToMonitors(code)); } catch (e) { console.error('Invalid json.'); } @@ -101,7 +103,7 @@ const AssociateMonitors = ({ ) : ( form.setFieldTouched('associatedMonitorsEditor', true), + onBlur: (e, field, form) => form.setFieldTouched(fieldEditorName, true), 'data-test-subj': 'associatedMonitorsCodeEditor', }} /> diff --git a/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.js b/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.js index 444c11aae..4d75553c2 100644 --- a/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.js +++ b/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.js @@ -24,6 +24,9 @@ const MonitorsList = ({ monitors = [], options = [] }) => { const [selectedOptions, setSelectedOptions] = useState({}); const [monitorOptions, setMonitorOptions] = useState([]); + const fieldName = 'associatedMonitors'; + const fieldInputNamePrefix = 'associatedMonitor_'; + const [monitorFields, setMonitorFields] = useState( _.reduce( monitors.length ? monitors : [0, 1], @@ -77,10 +80,10 @@ const MonitorsList = ({ monitors = [], options = [] }) => { }; const updateFormik = (monitorIdx, form) => { - form.setFieldTouched('associatedMonitors', true); - form.setFieldTouched(`associatedMonitor_${monitorIdx}`, true); - form.setFieldValue('associatedMonitors', Object.values(selectedOptions)); - form.setFieldError('associatedMonitors', validate()); + form.setFieldTouched(fieldName, true); + form.setFieldTouched(`${fieldInputNamePrefix}${monitorIdx}`, true); + form.setFieldValue(fieldName, Object.values(selectedOptions)); + form.setFieldError(fieldName, validate()); }; const isSelected = (selected, monitor) => { @@ -130,11 +133,11 @@ const MonitorsList = ({ monitors = [], options = [] }) => { }} render={({ field, form }) => ( form.touched['associatedMonitors'] && !isValid(), + isInvalid: () => form.touched[fieldName] && !isValid(), error: () => validate(), }} > @@ -146,11 +149,11 @@ const MonitorsList = ({ monitors = [], options = [] }) => { > onChange(options, monitorIdx, form), From 4e66dd96d338fff7f20ae91d9a1a8ca7a9fede37 Mon Sep 17 00:00:00 2001 From: Jovan Cvetkovic Date: Thu, 22 Jun 2023 12:10:07 +0200 Subject: [PATCH 25/63] Code review #573 Signed-off-by: Jovan Cvetkovic --- .../CreateMonitor/containers/CreateMonitor/CreateMonitor.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/CreateMonitor.js b/public/pages/CreateMonitor/containers/CreateMonitor/CreateMonitor.js index c00dfb5c1..5c5c9a8c3 100644 --- a/public/pages/CreateMonitor/containers/CreateMonitor/CreateMonitor.js +++ b/public/pages/CreateMonitor/containers/CreateMonitor/CreateMonitor.js @@ -254,8 +254,6 @@ export default class CreateMonitor extends Component { monitor = { ...monitor, ...triggers }; } - if (monitor.monitor_type === MONITOR_TYPE.COMPOSITE_LEVEL) delete monitor.monitor_type; - if (edit) this.onUpdate(monitor, formikBag); else this.onCreate(monitor, formikBag); } From 277506ef53e14148e2b6bb05b1b55c35a0ca01b5 Mon Sep 17 00:00:00 2001 From: Jovan Cvetkovic Date: Thu, 22 Jun 2023 12:11:25 +0200 Subject: [PATCH 26/63] Code review #573 Signed-off-by: Jovan Cvetkovic --- .../CreateTrigger/components/ExpressionQuery/ExpressionQuery.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js b/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js index 8c4a7c443..82b4f4dee 100644 --- a/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js +++ b/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js @@ -22,7 +22,7 @@ const ExpressionQuery = ({ isDarkMode = false, }) => { const DEFAULT_CONDITION = 'AND'; - const DEFAULT_NAME = defaultText ? defaultText : 'Select'; + const DEFAULT_NAME = defaultText ?? 'Select'; const DEFAULT_EXPRESSION = { description: '', isOpen: false, From 64aca9250ec5a3350f293b8f462a7cbe60bc93ae Mon Sep 17 00:00:00 2001 From: Jovan Cvetkovic Date: Thu, 22 Jun 2023 12:17:21 +0200 Subject: [PATCH 27/63] Code review #573 Signed-off-by: Jovan Cvetkovic --- .../ExpressionQuery/ExpressionQuery.js | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js b/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js index 82b4f4dee..e15a14a25 100644 --- a/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js +++ b/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js @@ -33,6 +33,13 @@ const ExpressionQuery = ({ ...DEFAULT_EXPRESSION, description: DEFAULT_CONDITION, }; + const EXPRESSION_CONDITIONS_MAP = [ + { description: '', label: '' }, + { description: 'AND', label: 'AND' }, + { description: 'OR', label: 'OR' }, + { description: 'NOT', label: 'NOT' }, + ]; + const [usedExpressions, setUsedExpressions] = useState([DEFAULT_EXPRESSION]); const [graphUi, setGraphUi] = useState(triggerValues.searchType === 'graph'); const [editorValue, setEditorValue] = useState(''); @@ -155,12 +162,7 @@ const ExpressionQuery = ({ ]} onChange={(selection) => changeCondition(selection, expression, idx, form)} onBlur={() => onBlur(form, usedExpressions)} - options={[ - { description: '', label: '' }, - { description: 'AND', label: 'AND' }, - { description: 'OR', label: 'OR' }, - { description: 'NOT', label: 'NOT' }, - ]} + options={EXPRESSION_CONDITIONS_MAP} /> {renderMonitorOptions(expression, idx, form)} @@ -218,9 +220,7 @@ const ExpressionQuery = ({ form={form} rowProps={{ label: label, - isInvalid: () => { - return form.touched['expressionQueries'] && graphUi && !isValid(); - }, + isInvalid: () => form.touched['expressionQueries'] && graphUi && !isValid(), error: () => graphUi && validate(), style: { maxWidth: 'inherit', @@ -248,10 +248,7 @@ const ExpressionQuery = ({ description={expression.description} value={expression.monitor_name} isActive={!!selections?.length} - onClick={(e) => { - e.preventDefault(); - openPopover(idx, form); - }} + onClick={(e) => openPopover(idx)} /> } isOpen={expression.isOpen} @@ -301,9 +298,7 @@ const ExpressionQuery = ({ _.set(triggerValues, 'triggerDefinitions[0].script.source', query); form.setFieldValue('expressionQueries', query); }, - onBlur: (e, field, form) => { - form.setFieldTouched('expressionQueries', true); - }, + onBlur: (e, field, form) => form.setFieldTouched('expressionQueries', true), 'data-test-subj': 'expressionQueriesCodeEditor', }} /> From f98973577b5e065e88076c155b9b48b933531b56 Mon Sep 17 00:00:00 2001 From: Jovan Cvetkovic Date: Thu, 22 Jun 2023 12:20:32 +0200 Subject: [PATCH 28/63] Code review #573 Signed-off-by: Jovan Cvetkovic --- .../ExpressionQuery/ExpressionQuery.js | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js b/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js index e15a14a25..04280e723 100644 --- a/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js +++ b/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useCallback } from 'react'; import { EuiFlexItem, EuiFlexGroup, @@ -121,16 +121,19 @@ const ExpressionQuery = ({ form.setFieldTouched(`expressionQueries_${idx}`, true); }; - const onRemoveExpression = (idx) => { - const expressions = _.cloneDeep(usedExpressions); - expressions.splice(idx, 1); - expressions.length && (expressions[0].description = ''); + const onRemoveExpression = useCallback( + (idx) => { + const expressions = _.cloneDeep(usedExpressions); + expressions.splice(idx, 1); + expressions.length && (expressions[0].description = ''); - if (!expressions?.length) { - expressions.push(DEFAULT_EXPRESSION); - } - setUsedExpressions([...expressions]); - }; + if (!expressions?.length) { + expressions.push(DEFAULT_EXPRESSION); + } + setUsedExpressions([...expressions]); + }, + [usedExpressions] + ); const hasInvalidExpression = () => { return !!usedExpressions.filter((expression) => expression.monitor_id === '')?.length; @@ -214,7 +217,7 @@ const ExpressionQuery = ({ fieldProps={{ validate: () => graphUi && validate(), }} - render={({ field, form }) => ( + render={({ form }) => ( openPopover(idx)} + onClick={() => openPopover(idx)} /> } isOpen={expression.isOpen} From ed738c8456e19ca6a7905d954dacf8ca6b95fa9c Mon Sep 17 00:00:00 2001 From: Jovan Cvetkovic Date: Fri, 23 Jun 2023 15:27:51 +0200 Subject: [PATCH 29/63] Code review #573 Signed-off-by: Jovan Cvetkovic --- .../AssociateMonitors/AssociateMonitors.js | 129 ++----- .../components/MonitorsEditor.js | 86 +++++ .../components/MonitorsList.js | 187 ++++++---- .../containers/CreateMonitor/CreateMonitor.js | 14 +- .../CreateMonitor/utils/constants.js | 14 +- .../CreateMonitor/utils/formikToMonitor.js | 9 +- .../CreateMonitor/utils/monitorToFormik.js | 12 +- .../WorkflowDetails/WorkflowDetails.js | 39 +++ .../WorkflowDetails/WorkflowDetails.tsx | 86 ----- .../CompositeTriggerCondition.js | 93 +++++ .../ExpressionBuilder.js | 327 ++++++++++++++++++ .../ExpressionEditor.js | 52 +++ .../ExpressionQuery.test.ts | 0 .../ExpressionQuery/ExpressionQuery.js | 315 ----------------- .../CreateTrigger/utils/constants.js | 11 + .../CreateTrigger/utils/formikToTrigger.js | 22 +- .../CreateTrigger/utils/triggerToFormik.js | 15 +- .../CompositeMonitorsAlertTrigger.js | 12 +- .../DefineCompositeLevelTrigger.js | 108 +----- 19 files changed, 797 insertions(+), 734 deletions(-) create mode 100644 public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsEditor.js create mode 100644 public/pages/CreateMonitor/containers/WorkflowDetails/WorkflowDetails.js delete mode 100644 public/pages/CreateMonitor/containers/WorkflowDetails/WorkflowDetails.tsx create mode 100644 public/pages/CreateTrigger/components/CompositeTriggerCondition/CompositeTriggerCondition.js create mode 100644 public/pages/CreateTrigger/components/CompositeTriggerCondition/ExpressionBuilder.js create mode 100644 public/pages/CreateTrigger/components/CompositeTriggerCondition/ExpressionEditor.js rename public/pages/CreateTrigger/components/{ExpressionQuery => CompositeTriggerCondition}/ExpressionQuery.test.ts (100%) delete mode 100644 public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js diff --git a/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.js b/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.js index 761717927..a14a59035 100644 --- a/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.js +++ b/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.js @@ -3,90 +3,38 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { Fragment, useState, useEffect, useCallback } from 'react'; +import React, { Fragment, useState, useEffect } 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, - searchType = 'graph', - isDarkMode, - monitorValues, -}) => { - const [graphUi, setGraphUi] = useState(searchType === 'graph'); - - const fieldName = 'associatedMonitors'; - const fieldEditorName = 'associatedMonitorsEditor'; - const queryTemplate = { - sequence: { - delegates: [], +import MonitorsEditor from './components/MonitorsEditor'; + +export const getMonitors = async (httpClient) => { + const response = await httpClient.get('../api/alerting/monitors', { + query: { + from: 0, + size: 1000, + search: '', + sortField: 'name', + sortDirection: 'desc', + state: 'all', }, - }; + }); + + if (response.ok) { + const { monitors, totalMonitors } = response; + return monitors.map((monitor) => ({ monitor_id: monitor.id, monitor_name: monitor.name })); + } else { + console.log('error getting monitors:', response); + return []; + } +}; - const delegatesToMonitors = (value) => - value.sequence.delegates.map((monitor) => ({ - label: '', - value: monitor.monitor_id, - })); +const AssociateMonitors = ({ isDarkMode, values, httpClient }) => { + const [graphUi, setGraphUi] = useState(false); useEffect(() => { - if (monitors?.length) { - const value = { ...queryTemplate }; - monitors.forEach((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, - }); - }); - - try { - _.set(monitorValues, fieldEditorName, JSON.stringify(value, null, 4)); - _.set(monitorValues, fieldName, delegatesToMonitors(value)); - } catch (e) { - console.log('No monitor options are available.'); - } - } else if (options?.length && !graphUi) { - 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, fieldEditorName, JSON.stringify(value, null, 4)); - _.set(monitorValues, fieldName, 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(fieldEditorName, query); - try { - const code = JSON.parse(query); - form.setFieldValue(fieldName, delegatesToMonitors(code)); - } catch (e) { - console.error('Invalid json.'); - } - }, - [options, monitors] - ); + setGraphUi(values.searchType === 'graph'); + }, [values.searchType]); return ( @@ -100,30 +48,9 @@ const AssociateMonitors = ({ {graphUi ? ( - + ) : ( - form.setFieldTouched(fieldEditorName, true), - 'data-test-subj': 'associatedMonitorsCodeEditor', - }} - /> + )} ); diff --git a/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsEditor.js b/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsEditor.js new file mode 100644 index 000000000..9d8bf16c8 --- /dev/null +++ b/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsEditor.js @@ -0,0 +1,86 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState, useEffect } from 'react'; +import { FormikCodeEditor } from '../../../../../components/FormControls'; +import { hasError, isInvalid, validateExtractionQuery } from '../../../../../utils/validate'; + +const MonitorsEditor = ({ values, isDarkMode }) => { + const codeFieldName = 'associatedMonitorsEditor'; + const formikValueName = 'associatedMonitors'; + const [editorValue, setEditorValue] = useState(''); + + useEffect(() => { + try { + const code = JSON.stringify(values.associatedMonitors, null, 4); + // _.set(values, codeFieldName, code); + setEditorValue(code); + } catch (e) {} + }, [values.associatedMonitors]); + + const onCodeChange = (codeValue, field, form) => { + form.setFieldValue(codeFieldName, codeValue); + form.setFieldTouched(codeFieldName, true); + setEditorValue(codeValue); + + try { + const code = JSON.parse(codeValue); // test the code before setting it to formik + form.setFieldValue(formikValueName, code); + } catch (e) { + console.error('Invalid json.'); + } + }; + + const isInvalid = (name, form) => { + if (form.touched[codeFieldName]) { + try { + const associatedMonitors = form.values[name]; + const json = JSON.parse(associatedMonitors); + return !json.sequence?.delegates?.length; + } catch (e) { + return false; + } + } + }; + + const hasError = (name, form) => { + try { + const associatedMonitors = form.values[name]; + const json = JSON.parse(associatedMonitors); + return ( + json.sequence?.delegates?.length < 2 && + 'Delegates list can not be empty or have less then two associated monitors.' + ); + } catch (e) { + return 'Invalid json.'; + } + }; + + return ( + isInvalid(name, form), + // error: (name, form) => hasError(name, form), + }} + inputProps={{ + mode: 'json', + width: '80%', + height: '300px', + theme: isDarkMode ? 'sense-dark' : 'github', + onChange: onCodeChange, + onBlur: (e, field, form) => form.setFieldTouched(codeFieldName, true), + value: editorValue, + 'data-test-subj': codeFieldName, + }} + /> + ); +}; + +export default MonitorsEditor; diff --git a/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.js b/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.js index 4d75553c2..0cd9dcb18 100644 --- a/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.js +++ b/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.js @@ -19,71 +19,118 @@ import { FormikInputWrapper, FormikFormRow, } from '../../../../../components/FormControls'; +import { DEFAULT_ASSOCIATED_MONITORS_VALUE } from '../../../containers/CreateMonitor/utils/constants'; +import { getMonitors } from '../AssociateMonitors'; -const MonitorsList = ({ monitors = [], options = [] }) => { - const [selectedOptions, setSelectedOptions] = useState({}); - const [monitorOptions, setMonitorOptions] = useState([]); +const MonitorsList = ({ values, httpClient }) => { + const formikFieldName = 'associatedMonitorsList'; + const formikValueName = 'associatedMonitors'; - const fieldName = 'associatedMonitors'; - const fieldInputNamePrefix = 'associatedMonitor_'; + const [fields, setFields] = useState([0, 1]); + const [options, setOptions] = useState([]); + const [selection, setSelection] = useState({}); - const [monitorFields, setMonitorFields] = useState( + useEffect(() => { + const monitorOptions = _.get(values, 'monitorOptions', []); + if (monitorOptions.length) { + setOptions(monitorsToOptions(monitorOptions)); + const selected = formikToSelection(monitorOptions); + setFields(generateFields(selected)); + } else { + getMonitors(httpClient).then((monitors) => { + _.set(values, 'monitorOptions', monitors); + + setOptions(monitorsToOptions(monitors)); + const selected = formikToSelection(monitors); + setFields(generateFields(selected)); + }); + } + }, [values]); + + const formikToSelection = (options) => { + const associatedMonitors = _.get( + values, + 'associatedMonitors', + DEFAULT_ASSOCIATED_MONITORS_VALUE + ); + const selected = {}; + associatedMonitors.sequence.delegates.forEach((monitor, index) => { + const filteredOption = options.filter((option) => option.monitor_id === monitor.monitor_id); + selected[index] = { + label: filteredOption[0]?.monitor_name || '', + value: monitor.monitor_id, + }; + }); + + setSelection(selected); + return selected; + }; + + const generateFields = (selected) => _.reduce( - monitors.length ? monitors : [0, 1], + Object.keys(selected).length > 1 ? Object.keys(selected) : [0, 1], (result, value, key) => { result.push(key); return result; }, [] - ) - ); + ); - useEffect(() => { - const newOptions = [...options].map((monitor) => ({ + const monitorsToOptions = (monitors) => + monitors.map((monitor, index) => ({ label: monitor.monitor_name, value: monitor.monitor_id, })); - setMonitorOptions(newOptions); - - let newSelected = monitors.length - ? monitors.map((monitor) => ({ - label: monitor.monitor_name, - value: monitor.monitor_id, - })) - : []; - setSelectedOptions(Object.assign({}, newSelected)); - }, [monitors, options]); const onChange = (options, monitorIdx, form) => { - let newSelected = { - ...selectedOptions, + let selected = { + ...selection, }; if (options[0]) { - newSelected[monitorIdx] = options[0]; + selected[monitorIdx] = options[0]; } else { - delete newSelected[monitorIdx]; + delete selected[monitorIdx]; } - setSelectedOptions(newSelected); + setSelection(selected); - updateMonitorOptions(newSelected); + updateSelection(selected); - updateFormik(monitorIdx, form); + setFormikValues(selected, monitorIdx, form); }; - const updateMonitorOptions = (selected) => { - const newMonitorOptions = [...monitorOptions]; + const updateSelection = (selected) => { + const newMonitorOptions = [...options]; newMonitorOptions.forEach((mon) => { mon.disabled = isSelected(selected, mon); }); - setMonitorOptions([...newMonitorOptions]); + setOptions([...newMonitorOptions]); }; - const updateFormik = (monitorIdx, form) => { - form.setFieldTouched(fieldName, true); - form.setFieldTouched(`${fieldInputNamePrefix}${monitorIdx}`, true); - form.setFieldValue(fieldName, Object.values(selectedOptions)); - form.setFieldError(fieldName, validate()); + const setFormikValues = (selected, monitorIdx, form) => { + // form.setFieldTouched(formikFieldName, true); + // form.setFieldTouched(`${formikFieldName}_${monitorIdx}`, true); + // form.setFieldError(formikFieldName, validate()); + + const associatedMonitors = _.get( + values, + 'associatedMonitors', + DEFAULT_ASSOCIATED_MONITORS_VALUE + ); + associatedMonitors.sequence.delegates = selectionToFormik(selected); + form.setFieldValue(formikValueName, associatedMonitors); + }; + + const selectionToFormik = (selection) => { + const monitors = []; + Object.values(selection).forEach((monitor, index) => { + monitors.push({ + order: index + 1, + monitor_id: monitor.value, + }); + }); + + return monitors; }; const isSelected = (selected, monitor) => { @@ -100,26 +147,26 @@ const MonitorsList = ({ monitors = [], options = [] }) => { }; const onAddMonitor = () => { - let nextIndex = Math.max(...monitorFields) + 1; - const newMonitorFields = [...monitorFields, nextIndex]; - setMonitorFields(newMonitorFields); + let nextIndex = Math.max(...fields) + 1; + const newMonitorFields = [...fields, nextIndex]; + setFields(newMonitorFields); }; const onRemoveMonitor = (monitorIdx, idx, form) => { - const newSelected = { ...selectedOptions }; - delete newSelected[monitorIdx]; - setSelectedOptions(newSelected); + const selected = { ...selection }; + delete selected[monitorIdx]; + setSelection(selected); - const newMonitorFields = [...monitorFields]; + const newMonitorFields = [...fields]; newMonitorFields.splice(idx, 1); - setMonitorFields(newMonitorFields); + setFields(newMonitorFields); - updateMonitorOptions(newSelected); + updateSelection(selected); - updateFormik(monitorIdx, form); + setFormikValues(selected, monitorIdx, form); }; - const isValid = () => Object.keys(selectedOptions).length > 1; + const isValid = () => Object.keys(selection).length > 1; const validate = () => { if (!isValid()) return 'Required.'; @@ -127,48 +174,46 @@ const MonitorsList = ({ monitors = [], options = [] }) => { return ( validate(), - }} + name={formikFieldName} + fieldProps={ + { + // validate: () => validate(), + } + } render={({ field, form }) => ( form.touched[fieldName] && !isValid(), + isInvalid: () => form.touched[formikFieldName] && !isValid(), error: () => validate(), }} > - {monitorFields.map((monitorIdx, idx) => ( + {fields.map((monitorIdx, idx) => ( onChange(options, monitorIdx, form), - onBlur: (e, field, form) => updateFormik(monitorIdx, form), - options: monitorOptions, + // onBlur: (e, field, form) => setFormikValues(monitorIdx, form), + options: options, singleSelection: { asPlainText: true }, - selectedOptions: selectedOptions[monitorIdx] - ? [selectedOptions[monitorIdx]] - : undefined, + selectedOptions: selection[monitorIdx] ? [selection[monitorIdx]] : undefined, 'data-test-subj': `monitors_list_${monitorIdx}`, fullWidth: true, }} /> - {selectedOptions[monitorIdx] && ( + {selection[monitorIdx] && ( { iconType={'inspect'} color="text" target={'_blank'} - href={`alerting#/monitors/${selectedOptions[monitorIdx].value}?alertState=ALL&from=0&monitorIds=${selectedOptions[monitorIdx].value}&search=&severityLevel=ALL&size=20&sortDirection=desc&sortField=start_time`} + href={`alerting#/monitors/${selection[monitorIdx].value}?alertState=ALL&from=0&monitorIds=${selection[monitorIdx].value}&search=&severityLevel=ALL&size=20&sortDirection=desc&sortField=start_time`} /> )} - {monitorFields.length > 2 && ( + {fields.length > 2 && ( { ))} - onAddMonitor()} - disabled={ - monitorFields.length >= 10 || - monitorOptions.length <= Object.keys(selectedOptions).length - } - > + onAddMonitor()} disabled={fields.length >= 10}> Associate another monitor - You can associate up to {10 - monitorFields.length} more monitors. + You can associate up to {10 - fields.length} more monitors. diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/CreateMonitor.js b/public/pages/CreateMonitor/containers/CreateMonitor/CreateMonitor.js index 5c5c9a8c3..de32bec4d 100644 --- a/public/pages/CreateMonitor/containers/CreateMonitor/CreateMonitor.js +++ b/public/pages/CreateMonitor/containers/CreateMonitor/CreateMonitor.js @@ -303,14 +303,7 @@ export default class CreateMonitor extends Component { - + @@ -333,10 +326,7 @@ export default class CreateMonitor extends Component { {values.monitor_type === MONITOR_TYPE.COMPOSITE_LEVEL ? ( ({ - order: idx + 1, - monitor_id: monitor.value, - })), - }, - }, + composite_input: values.associatedMonitors, }; } diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/utils/monitorToFormik.js b/public/pages/CreateMonitor/containers/CreateMonitor/utils/monitorToFormik.js index d2614c1ed..945a1a3e0 100644 --- a/public/pages/CreateMonitor/containers/CreateMonitor/utils/monitorToFormik.js +++ b/public/pages/CreateMonitor/containers/CreateMonitor/utils/monitorToFormik.js @@ -41,18 +41,8 @@ export default function monitorToFormik(monitor) { case MONITOR_TYPE.DOC_LEVEL: return docLevelInputToFormik(monitor); case MONITOR_TYPE.COMPOSITE_LEVEL: - const associatedMonitors = _.get( - monitor, - 'inputs[0].composite_input.sequence.delegates', - [] - ); - return { - inputs: associatedMonitors, - associatedMonitors: associatedMonitors.map((mon) => ({ - label: '', - value: mon.monitor_id, - })), + associatedMonitors: _.get(monitor, 'inputs[0].composite_input', {}), }; default: return { diff --git a/public/pages/CreateMonitor/containers/WorkflowDetails/WorkflowDetails.js b/public/pages/CreateMonitor/containers/WorkflowDetails/WorkflowDetails.js new file mode 100644 index 000000000..1d3129279 --- /dev/null +++ b/public/pages/CreateMonitor/containers/WorkflowDetails/WorkflowDetails.js @@ -0,0 +1,39 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { Fragment } from 'react'; +import ContentPanel from '../../../../components/ContentPanel'; +import Schedule from '../../components/Schedule'; +import AssociateMonitors from '../../components/AssociateMonitors/AssociateMonitors'; +import { EuiSpacer } from '@elastic/eui'; +import { MONITOR_TYPE, SEARCH_TYPE } from '../../../../utils/constants'; + +const WorkflowDetails = ({ values, isDarkMode, httpClient }) => { + const isAd = values.searchType === SEARCH_TYPE.AD; + const isComposite = values.monitor_type === MONITOR_TYPE.COMPOSITE_LEVEL; + return ( + + + + {isComposite && ( + + + + + )} + + ); +}; + +export default WorkflowDetails; diff --git a/public/pages/CreateMonitor/containers/WorkflowDetails/WorkflowDetails.tsx b/public/pages/CreateMonitor/containers/WorkflowDetails/WorkflowDetails.tsx deleted file mode 100644 index e96434a72..000000000 --- a/public/pages/CreateMonitor/containers/WorkflowDetails/WorkflowDetails.tsx +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { Fragment, useEffect, useState } from 'react'; -import ContentPanel from '../../../../components/ContentPanel'; -import Schedule from '../../components/Schedule'; -import AssociateMonitors from '../../components/AssociateMonitors/AssociateMonitors.js'; -import { EuiSpacer } from '@elastic/eui'; -import * as _ from 'lodash'; - -export const getMonitors = async (httpClient) => { - const response = await httpClient.get('../api/alerting/monitors', { - query: { - from: 0, - size: 1000, - search: '', - sortField: 'name', - sortDirection: 'desc', - state: 'all', - }, - }); - - if (response.ok) { - const { monitors, totalMonitors } = response; - return monitors.map((monitor) => ({ monitor_id: monitor.id, monitor_name: monitor.name })); - } else { - console.log('error getting monitors:', response); - return []; - } -}; - -const WorkflowDetails = ({ isAd, isComposite, httpClient, history, values, isDarkMode }) => { - const [selectedMonitors, setSelectedMonitors] = useState([]); - const [monitorOptions, setMonitorOptions] = useState([]); - - useEffect(() => { - getMonitors(httpClient).then((monitors) => { - setMonitorOptions(monitors); - - const inputIds = values.inputs?.map((input) => input.monitor_id); - if (inputIds?.length) { - const selected = monitors.filter((monitor) => inputIds.indexOf(monitor.monitor_id) !== -1); - setSelectedMonitors(selected); - _.set( - values, - 'associatedMonitors', - selected.map((monitor) => ({ - value: monitor.monitor_id, - label: monitor.monitor_name, - })) - ); - } - }); - }, [values.inputs]); - - return ( - - - {isComposite && ( - - - - - )} - - ); -}; - -export default WorkflowDetails; diff --git a/public/pages/CreateTrigger/components/CompositeTriggerCondition/CompositeTriggerCondition.js b/public/pages/CreateTrigger/components/CompositeTriggerCondition/CompositeTriggerCondition.js new file mode 100644 index 000000000..2f4ee827c --- /dev/null +++ b/public/pages/CreateTrigger/components/CompositeTriggerCondition/CompositeTriggerCondition.js @@ -0,0 +1,93 @@ +import React, { useEffect, useState, useCallback } from 'react'; +import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; +import { FormikFormRow, FormikInputWrapper } from '../../../../components/FormControls'; +import ExpressionBuilder from './ExpressionBuilder'; +import ExpressionEditor from './ExpressionEditor'; + +export const conditionToExpressions = (query, monitors) => { + const conditionMap = { + '&&': 'and', + '||': 'or', + '!': 'not', + '': '', + }; + const queryToExpressionRegex = new RegExp('( && || \\|\\| )?(monitor\\[id=(.*?)\\])', 'gm'); + const matcher = query.matchAll(queryToExpressionRegex); + let match; + let expressions = []; + while ((match = matcher.next())) { + const monitorId = match[4]?.trim(); + const monitor = monitors.filter((mon) => mon.monitor_id === monitorId); + expressions.push({ + description: conditionMap[match[1]?.trim()] || '', + isOpen: false, + monitor_name: monitor[0]?.monitor_name, + monitor_id: monitorId, + }); + } + + return expressions; +}; + +const CompositeTriggerCondition = ({ + label, + formikFieldPath = '', + formikFieldName = 'triggerCondition', + values, + isDarkMode = false, + httpClient, +}) => { + const formikFullFieldName = `${formikFieldPath}${formikFieldName}`; + const [graphUi, setGraphUi] = useState(values.searchType === 'graph'); + + useEffect(() => { + setGraphUi(values.searchType === 'graph'); + }, [values.searchType]); + + const isValid = () => true; + const validate = () => {}; + return ( + graphUi && validate(), + }} + render={({ form }) => ( + form.touched[formikFullFieldName] && !isValid(), + error: () => validate(), + style: { + maxWidth: 'inherit', + }, + }} + > + + + {graphUi ? ( + + ) : ( + + )} + + + + )} + /> + ); +}; + +export default CompositeTriggerCondition; diff --git a/public/pages/CreateTrigger/components/CompositeTriggerCondition/ExpressionBuilder.js b/public/pages/CreateTrigger/components/CompositeTriggerCondition/ExpressionBuilder.js new file mode 100644 index 000000000..d70275b71 --- /dev/null +++ b/public/pages/CreateTrigger/components/CompositeTriggerCondition/ExpressionBuilder.js @@ -0,0 +1,327 @@ +import React, { useEffect, useState, useCallback } from 'react'; +import { + EuiFlexItem, + EuiFlexGroup, + EuiPopover, + EuiComboBox, + EuiButtonIcon, + EuiExpression, +} from '@elastic/eui'; +import * as _ from 'lodash'; +import { FormikFormRow, FormikInputWrapper } from '../../../../components/FormControls'; +import { getMonitors } from '../../../CreateMonitor/components/AssociateMonitors/AssociateMonitors'; + +export const conditionToExpressions = (condition, monitors) => { + if (!condition.length) return []; + + const conditionMap = { + '&&': 'AND', + '||': 'OR', + '!': 'NOT', + '': '', + }; + const queryToExpressionRegex = new RegExp(/( && || \|\| )?(monitor\[id=(.*?)\])/, 'gm'); + const matcher = condition.matchAll(queryToExpressionRegex); + let match; + let expressions = []; + let counter = 0; + while ((match = matcher.next().value)) { + if (counter && !match[1]) return; // Didn't find condition after the first match + + const monitorId = match[3]?.trim(); // match [3] is the monitor_id + const monitor = monitors.filter((mon) => mon.monitor_id === monitorId); + expressions.push({ + description: conditionMap[match[1]?.trim()] || '', // match [1] is the description/condition + isOpen: false, + monitor_name: monitor[0]?.monitor_name, + monitor_id: monitorId, + }); + + counter++; + } + + return expressions; +}; + +const ExpressionBuilder = ({ formikFieldPath = '', formikFieldName, values, httpClient }) => { + const formikFullFieldName = `${formikFieldPath}${formikFieldName}`; + + const DEFAULT_CONDITION = 'AND'; + const DEFAULT_NAME = 'Select associated monitor'; + const DEFAULT_EXPRESSION = { + description: '', + isOpen: false, + monitor_id: '', + monitor_name: DEFAULT_NAME, + }; + const DEFAULT_NEXT_EXPRESSION = { + ...DEFAULT_EXPRESSION, + description: DEFAULT_CONDITION, + }; + const EXPRESSION_CONDITIONS_MAP = [ + { description: '', label: '' }, + { description: 'AND', label: 'AND' }, + { description: 'OR', label: 'OR' }, + { description: 'NOT', label: 'NOT' }, + ]; + + const [usedExpressions, setUsedExpressions] = useState([DEFAULT_EXPRESSION]); + const [options, setOptions] = useState([]); + + useEffect(() => { + const monitors = _.get(values, 'monitorOptions', []); + if (monitors.length) { + setInitialValues(monitors); + } else { + getMonitors(httpClient).then((monitors) => { + _.set(values, 'monitorOptions', monitors); + setInitialValues(monitors); + }); + } + }, [values.associatedMonitors?.sequence?.delegates, values[formikFullFieldName]]); + + const setInitialValues = (monitors) => { + const monitorOptions = []; + const associatedMonitors = _.get(values, 'associatedMonitors', {}); + associatedMonitors.sequence.delegates.forEach((monitor, index) => { + const filteredOption = monitors.filter((option) => option.monitor_id === monitor.monitor_id); + monitorOptions.push({ + label: filteredOption[0]?.monitor_name || '', + monitor_id: monitor.monitor_id, + }); + }); + setOptions(monitorOptions); + + const condition = _.get(values, formikFullFieldName, ''); + const expressions = conditionToExpressions(condition, monitors); + setUsedExpressions(expressions.length ? expressions : [DEFAULT_EXPRESSION]); + }; + + const expressionsToCondition = (expressions) => { + const conditionMap = { + AND: '&&', + OR: '||', + NOT: '!', + '': '', + }; + + const condition = expressions.reduce((query, expression) => { + if (expression?.monitor_id) { + query += ` ${conditionMap[expression.description]} monitor[id=${expression.monitor_id}]`; + query = query.trim(); + } + return query; + }, ''); + + return `(${condition})`; + }; + + const changeMonitor = (selection, exp, idx, form) => { + const expressions = _.cloneDeep(usedExpressions); + let monitor = selection[0]; + + if (monitor?.monitor_id) { + expressions[idx] = { + ...expressions[idx], + monitor_id: monitor.monitor_id, + monitor_name: monitor.label, + }; + } else { + expressions[idx] = idx ? DEFAULT_NEXT_EXPRESSION : DEFAULT_EXPRESSION; + } + + setUsedExpressions(expressions); + onChange(form, expressions); + }; + + const changeCondition = (selection, exp, idx, form) => { + const expressions = _.cloneDeep(usedExpressions); + + expressions[idx] = { ...expressions[idx], description: selection[0].description }; + setUsedExpressions(expressions); + onChange(form, expressions); + }; + + const onChange = (form, expressions) => { + form.setFieldValue(formikFullFieldName, expressionsToCondition(expressions)); + }; + + const onBlur = (form, expressions) => { + onChange(form, expressions); + form.setFieldTouched(`${formikFullFieldName}_value`, true); + }; + + const openPopover = (idx = 0) => { + const expressions = _.cloneDeep(usedExpressions); + expressions[idx] = { ...expressions[idx], isOpen: !expressions[idx].isOpen }; + setUsedExpressions(expressions); + }; + + const closePopover = (idx, form) => { + const expressions = _.cloneDeep(usedExpressions); + expressions[idx] = { ...expressions[idx], isOpen: false }; + setUsedExpressions(expressions); + onBlur(form, expressions); + form.setFieldTouched(`expressionQueries_${idx}`, true); + }; + + const onRemoveExpression = useCallback( + (form, idx) => { + const expressions = _.cloneDeep(usedExpressions); + expressions.splice(idx, 1); + expressions.length && (expressions[0].description = ''); + + if (!expressions?.length) { + expressions.push(DEFAULT_EXPRESSION); + } + setUsedExpressions([...expressions]); + onChange(form, expressions); + }, + [usedExpressions] + ); + + const hasInvalidExpression = () => + !!usedExpressions.filter((expression) => expression.monitor_id === '')?.length; + + const isValid = () => options.length > 1 && usedExpressions.length > 1 && !hasInvalidExpression(); + + const validate = () => { + if (options.length < 2) return 'Trigger condition requires at least two associated monitors.'; + if (usedExpressions.length < 2) + return 'Trigger condition requires at least two monitors selected.'; + if (hasInvalidExpression()) return 'Invalid expressions.'; + }; + + const renderOptions = (expression, idx = 0, form) => ( + + + changeCondition(selection, expression, idx, form)} + onBlur={() => onBlur(form, usedExpressions)} + options={EXPRESSION_CONDITIONS_MAP} + /> + + {renderMonitorOptions(expression, idx, form)} + + onRemoveExpression(form, idx)} + iconType={'trash'} + color="danger" + aria-label={'Remove condition'} + style={{ marginTop: '4px' }} + /> + + + ); + + const renderMonitorOptions = (expression, idx, form) => ( + changeMonitor(selection, expression, idx, form)} + onBlur={() => onBlur(form, usedExpressions)} + selectedOptions={[ + { + label: expression.monitor_name, + monitor_id: expression.monitor_id, + }, + ]} + style={{ width: '250px' }} + options={(() => { + const differences = _.differenceBy(options, usedExpressions, 'monitor_id'); + return [ + { + monitor_id: expression.monitor_id, + label: expression.monitor_name, + }, + ...differences.map((sel) => ({ + monitor_id: sel.monitor_id, + label: sel.label, + })), + ]; + })()} + /> + ); + + return ( + validate(), + }} + render={({ form }) => ( + form.touched[`${formikFullFieldName}_value`] && !isValid(), + error: () => validate(), + style: { + maxWidth: 'inherit', + }, + }} + > + + {usedExpressions.map((expression, idx) => ( + + openPopover(idx)} + /> + } + isOpen={expression.isOpen} + closePopover={() => closePopover(idx, form)} + panelPaddingSize="s" + anchorPosition="upCenter" + > + {renderOptions(expression, idx, form)} + + + ))} + {options.length > usedExpressions.length && ( + + { + const expressions = _.cloneDeep(usedExpressions); + expressions.push({ + ...DEFAULT_NEXT_EXPRESSION, + }); + setUsedExpressions(expressions); + }} + color={'primary'} + iconType="plusInCircleFilled" + aria-label={'Add one more condition'} + data-test-subj={'condition-add-options-btn'} + style={{ marginTop: '1px' }} + /> + + )} + + + )} + /> + ); +}; + +export default ExpressionBuilder; diff --git a/public/pages/CreateTrigger/components/CompositeTriggerCondition/ExpressionEditor.js b/public/pages/CreateTrigger/components/CompositeTriggerCondition/ExpressionEditor.js new file mode 100644 index 000000000..80224d4a1 --- /dev/null +++ b/public/pages/CreateTrigger/components/CompositeTriggerCondition/ExpressionEditor.js @@ -0,0 +1,52 @@ +import React, { useEffect, useState } from 'react'; +import * as _ from 'lodash'; +import { FormikCodeEditor } from '../../../../components/FormControls'; + +const ExpressionEditor = ({ values, formikFieldName, formikFieldPath, isDarkMode = false }) => { + const [editorValue, setEditorValue] = useState(''); + const formikFullFieldName = `${formikFieldPath}${formikFieldName}`; + const formikFullCodeFieldName = 'triggerConditionsCode'; + + useEffect(() => { + const code = _.get(values, formikFullFieldName, ''); + // _.set(values, formikFullCodeFieldName, code); + setEditorValue(code); + }, [values]); + + const isInvalid = (name, form) => !form.values[name]?.length; + + const hasError = (name, form) => { + return !form.values[name]?.length && 'Invalid condition.'; + }; + + return ( + form.touched[name] && isInvalid(name, form), + // error: (name, form) => hasError(name, form), + }} + inputProps={{ + // isInvalid: (name, form) => form.touched[name] && isInvalid(name, form), + mode: 'text', + width: '80%', + height: '300px', + theme: isDarkMode ? 'sense-dark' : 'github', + value: editorValue, + onChange: (code, field, form) => { + form.setFieldTouched(formikFullCodeFieldName, true); + form.setFieldValue(formikFullFieldName, code); + form.setFieldValue(formikFullCodeFieldName, code); + }, + onBlur: (e, field, form) => form.setFieldTouched(field.name, true), + 'data-test-subj': 'compositeTriggerConditionEditor', + }} + /> + ); +}; + +export default ExpressionEditor; diff --git a/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.test.ts b/public/pages/CreateTrigger/components/CompositeTriggerCondition/ExpressionQuery.test.ts similarity index 100% rename from public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.test.ts rename to public/pages/CreateTrigger/components/CompositeTriggerCondition/ExpressionQuery.test.ts diff --git a/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js b/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js deleted file mode 100644 index 04280e723..000000000 --- a/public/pages/CreateTrigger/components/ExpressionQuery/ExpressionQuery.js +++ /dev/null @@ -1,315 +0,0 @@ -import React, { useEffect, useState, useCallback } from 'react'; -import { - EuiFlexItem, - EuiFlexGroup, - EuiPopover, - EuiComboBox, - EuiButtonIcon, - EuiExpression, -} from '@elastic/eui'; -import * as _ from 'lodash'; -import { FormikFormRow, FormikInputWrapper } from '../../../../components/FormControls'; -import { FormikCodeEditor } from '../../../../components/FormControls'; - -const ExpressionQuery = ({ - selections, - dataTestSubj, - value, - defaultText, - label, - formikName = 'expressionQueries', - triggerValues, - isDarkMode = false, -}) => { - const DEFAULT_CONDITION = 'AND'; - const DEFAULT_NAME = defaultText ?? 'Select'; - const DEFAULT_EXPRESSION = { - description: '', - isOpen: false, - monitor_id: '', - monitor_name: DEFAULT_NAME, - }; - const DEFAULT_NEXT_EXPRESSION = { - ...DEFAULT_EXPRESSION, - description: DEFAULT_CONDITION, - }; - const EXPRESSION_CONDITIONS_MAP = [ - { description: '', label: '' }, - { description: 'AND', label: 'AND' }, - { description: 'OR', label: 'OR' }, - { description: 'NOT', label: 'NOT' }, - ]; - - const [usedExpressions, setUsedExpressions] = useState([DEFAULT_EXPRESSION]); - 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)); - } - - 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, selections]); - - const getValue = (expressions) => - expressions.map((exp) => ({ - condition: _.toLower(exp.description), - monitor_id: exp.monitor_id, - monitor_name: exp.name, - })); - - const changeMonitor = (selection, exp, idx, form) => { - const expressions = _.cloneDeep(usedExpressions); - let monitor = selection[0]; - if (monitor) { - expressions[idx] = { - ...expressions[idx], - monitor_id: monitor.monitor_id, - monitor_name: monitor.label, - }; - } else { - expressions[idx] = idx ? DEFAULT_NEXT_EXPRESSION : DEFAULT_EXPRESSION; - } - - setUsedExpressions(expressions); - onChange(form, expressions); - }; - - const changeCondition = (selection, exp, idx, form) => { - const expressions = _.cloneDeep(usedExpressions); - expressions[idx] = { ...expressions[idx], description: selection[0].description }; - setUsedExpressions(expressions); - onChange(form, expressions); - }; - - const onChange = (form, expressions) => { - form.setFieldTouched('expressionQueries', true); - form.setFieldValue(formikName, getValue(expressions)); - }; - - const onBlur = (form, expressions) => { - onChange(form, expressions); - form.setFieldError('expressionQueries', validate()); - }; - - const openPopover = (idx = 0) => { - const expressions = _.cloneDeep(usedExpressions); - expressions[idx] = { ...expressions[idx], isOpen: !expressions[idx].isOpen }; - setUsedExpressions(expressions); - }; - - const closePopover = (idx, form) => { - const expressions = _.cloneDeep(usedExpressions); - expressions[idx] = { ...expressions[idx], isOpen: false }; - setUsedExpressions(expressions); - onBlur(form, usedExpressions); - form.setFieldTouched(`expressionQueries_${idx}`, true); - }; - - const onRemoveExpression = useCallback( - (idx) => { - const expressions = _.cloneDeep(usedExpressions); - expressions.splice(idx, 1); - expressions.length && (expressions[0].description = ''); - - if (!expressions?.length) { - expressions.push(DEFAULT_EXPRESSION); - } - setUsedExpressions([...expressions]); - }, - [usedExpressions] - ); - - const hasInvalidExpression = () => { - return !!usedExpressions.filter((expression) => expression.monitor_id === '')?.length; - }; - - const isValid = () => - selections.length > 1 && usedExpressions.length > 1 && !hasInvalidExpression(); - - const validate = () => { - if (selections.length < 2) - return 'Trigger condition requires at least two associated monitors.'; - if (usedExpressions.length < 2) - return 'Trigger condition requires at least two monitors selected.'; - if (hasInvalidExpression()) return 'Invalid expressions.'; - }; - - const renderOptions = (expression, idx = 0, form) => ( - - - changeCondition(selection, expression, idx, form)} - onBlur={() => onBlur(form, usedExpressions)} - options={EXPRESSION_CONDITIONS_MAP} - /> - - {renderMonitorOptions(expression, idx, form)} - - onRemoveExpression(idx)} - iconType={'trash'} - color="danger" - aria-label={'Remove condition'} - style={{ marginTop: '4px' }} - /> - - - ); - - const renderMonitorOptions = (expression, idx, form) => ( - changeMonitor(selection, expression, idx, form)} - onBlur={() => onBlur(form, usedExpressions)} - selectedOptions={[ - { - label: expression.monitor_name, - monitor_id: expression.monitor_id, - }, - ]} - style={{ width: '250px' }} - options={(() => { - const differences = _.differenceBy(selections, usedExpressions, 'monitor_id'); - return [ - { - monitor_id: expression.monitor_id, - label: expression.monitor_name, - }, - ...differences.map((sel) => ({ - monitor_id: sel.monitor_id, - label: sel.label, - })), - ]; - })()} - /> - ); - - return ( - graphUi && validate(), - }} - render={({ form }) => ( - form.touched['expressionQueries'] && graphUi && !isValid(), - error: () => graphUi && validate(), - style: { - maxWidth: 'inherit', - }, - }} - > - {graphUi ? ( - - {usedExpressions.map((expression, idx) => ( - - openPopover(idx)} - /> - } - isOpen={expression.isOpen} - closePopover={() => closePopover(idx, form)} - panelPaddingSize="s" - anchorPosition="rightDown" - > - {renderOptions(expression, idx, form)} - - - ))} - {selections.length > usedExpressions.length && ( - - { - const expressions = _.cloneDeep(usedExpressions); - expressions.push({ - ...DEFAULT_NEXT_EXPRESSION, - }); - setUsedExpressions(expressions); - }} - 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) => form.setFieldTouched('expressionQueries', true), - 'data-test-subj': 'expressionQueriesCodeEditor', - }} - /> - )} - - )} - /> - ); -}; - -export default ExpressionQuery; diff --git a/public/pages/CreateTrigger/containers/CreateTrigger/utils/constants.js b/public/pages/CreateTrigger/containers/CreateTrigger/utils/constants.js index 7a4ee9f61..b34e433e3 100644 --- a/public/pages/CreateTrigger/containers/CreateTrigger/utils/constants.js +++ b/public/pages/CreateTrigger/containers/CreateTrigger/utils/constants.js @@ -68,6 +68,17 @@ export const FORMIK_INITIAL_TRIGGER_VALUES = { actions: undefined, }; +export const FORMIK_COMPOSITE_INITIAL_TRIGGER_VALUES = { + name: '', + severity: '1', + script: { + lang: 'painless', + source: ``, + }, + triggerConditions: '', + actions: undefined, +}; + export const FORMIK_INITIAL_DOC_LEVEL_SCRIPT = { lang: FORMIK_INITIAL_TRIGGER_VALUES.script.lang, source: '(query[name=] || query[name=]) && query[tag=]', diff --git a/public/pages/CreateTrigger/containers/CreateTrigger/utils/formikToTrigger.js b/public/pages/CreateTrigger/containers/CreateTrigger/utils/formikToTrigger.js index afdc5979c..ed9fe4ed4 100644 --- a/public/pages/CreateTrigger/containers/CreateTrigger/utils/formikToTrigger.js +++ b/public/pages/CreateTrigger/containers/CreateTrigger/utils/formikToTrigger.js @@ -116,28 +116,12 @@ export function formikToDocumentLevelTriggerCondition(values, monitorUiMetadata) } export function formikToCompositeTriggerCondition(values) { - const conditionMap = { - and: '&&', - or: '||', - not: '!', - '': '', - }; - - const triggerConditions = _.get(values, 'triggerConditions', []); - 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', ''); - } + const triggerConditions = _.get(values, 'triggerConditions', ''); return { script: { lang: 'painless', - source: `(${source})`, + source: triggerConditions, }, }; } @@ -314,7 +298,7 @@ export function formikToTriggerUiMetadata(values, monitorUiMetadata) { case MONITOR_TYPE.COMPOSITE_LEVEL: const compositeTriggersUiMetadata = {}; _.get(values, 'triggerDefinitions', []).forEach((trigger) => { - compositeTriggersUiMetadata[trigger.name] = _.get(trigger, 'triggerConditions', []); + compositeTriggersUiMetadata[trigger.name] = _.get(trigger, 'triggerConditions', ''); }); return compositeTriggersUiMetadata; } diff --git a/public/pages/CreateTrigger/containers/CreateTrigger/utils/triggerToFormik.js b/public/pages/CreateTrigger/containers/CreateTrigger/utils/triggerToFormik.js index 4b45d270c..36e416c42 100644 --- a/public/pages/CreateTrigger/containers/CreateTrigger/utils/triggerToFormik.js +++ b/public/pages/CreateTrigger/containers/CreateTrigger/utils/triggerToFormik.js @@ -14,7 +14,7 @@ import { ACTIONABLE_ALERTS_OPTIONS_LABELS, NOTIFY_OPTIONS_VALUES, } from '../../../components/Action/actions/Message'; -import { convertQueryToExpressions } from '../../DefineCompositeLevelTrigger/DefineCompositeLevelTrigger'; +import { conditionToExpressions } from '../../../components/CompositeTriggerCondition/CompositeTriggerCondition'; export function triggerToFormik(trigger, monitor) { return _.isArray(trigger) @@ -227,12 +227,13 @@ export function compositeTriggerToFormik(trigger, monitor) { condition: { script }, actions, } = trigger[TRIGGER_TYPE.COMPOSITE_LEVEL]; - // TODO this should be saved in ui_metadata, currently workflows don't save ui_metadata - // const triggerUiMetadata = _.get(monitor, `ui_metadata.triggers[${name}]`); - const triggerConditions = convertQueryToExpressions( - monitor.triggers[0].chained_alert_trigger.condition.script.source, - [] + + const triggerConditions = _.get( + monitor, + 'triggers[0].chained_alert_trigger.condition.script.source', + '' ); + return { ..._.cloneDeep(FORMIK_INITIAL_TRIGGER_VALUES), id, @@ -240,7 +241,7 @@ export function compositeTriggerToFormik(trigger, monitor) { severity, script, actions: getExecutionPolicyActions(actions), - triggerConditions: triggerConditions, //triggerUiMetadata, + triggerConditions: triggerConditions, }; } diff --git a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/CompositeMonitorsAlertTrigger.js b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/CompositeMonitorsAlertTrigger.js index b5d40400a..9590925c0 100644 --- a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/CompositeMonitorsAlertTrigger.js +++ b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/CompositeMonitorsAlertTrigger.js @@ -7,25 +7,17 @@ import React, { Fragment } from 'react'; import DefineCompositeLevelTrigger from './DefineCompositeLevelTrigger'; const CompositeMonitorsAlertTrigger = ({ - edit, - triggerArrayHelpers, - monitor, - monitorValues, - triggerValues, isDarkMode, httpClient, notifications, notificationService, plugins, + values, }) => { return ( ( ); -export const convertQueryToExpressions = (query, monitors) => { - const conditionMap = { - '&&': 'and', - '||': 'or', - '!': 'not', - '': '', - }; - const queryToExpressionRegex = new RegExp('(&& )?(\\|\\| )?(monitor\\[id=(.*?)\\])', 'g'); - const matcher = query.matchAll(queryToExpressionRegex); - let match; - let expressions = []; - while ((match = matcher.next().value)) { - const monitorId = match[4]?.trim(); - const monitor = monitors.filter((mon) => mon.monitor_id === monitorId); - expressions.push({ - description: conditionMap[match[1]?.trim()] || '', - isOpen: false, - monitor_name: monitor[0]?.monitor_name, - monitor_id: monitorId, - }); - } - - return expressions; -}; - class DefineCompositeLevelTrigger extends Component { - constructor(props) { - super(props); - this.state = { - expressions: [], - }; - } - - componentDidMount() { - getMonitors(this.props.httpClient).then((monitors) => { - const inputIds = this.props.monitorValues.inputs?.map((input) => input.monitor_id); - if (inputIds && inputIds.length) { - const selectedMonitors = monitors.filter( - (monitor) => inputIds.indexOf(monitor.monitor_id) !== -1 - ); - - const expressions = convertQueryToExpressions( - this.props.triggerValues.triggerDefinitions[0].script.source, - selectedMonitors - ); - - this.setState({ - expressions, - }); - } - }); - } - render() { - const { - edit, - monitorValues, - triggerValues, - httpClient, - notifications, - notificationService, - plugins, - } = this.props; - - const fieldPath = `triggerDefinitions[0].`; - const triggerName = _.get(triggerValues, `${fieldPath}name`, 'Trigger'); - const triggerDefinitions = _.get(triggerValues, 'triggerDefinitions', []); - _.set(triggerValues, 'triggerDefinitions', [ + const { values, httpClient, notifications, notificationService, plugins } = this.props; + + const formikFieldPath = `triggerDefinitions[0].`; + const triggerName = _.get(values, `${formikFieldPath}name`, 'Trigger'); + const triggerDefinitions = _.get(values, 'triggerDefinitions', []); + _.set(values, 'triggerDefinitions', [ { - ...FORMIK_INITIAL_TRIGGER_VALUES, + ...FORMIK_COMPOSITE_INITIAL_TRIGGER_VALUES, ...triggerDefinitions[0], severity: 1, name: triggerName, }, ]); - const triggerActions = _.get(triggerValues, 'triggerDefinitions[0].actions', []); - const monitorList = monitorValues?.associatedMonitors - ? monitorValues.associatedMonitors?.map((monitor) => ({ - label: monitor.label?.replaceAll(' ', '_'), - monitor_id: monitor.value, - })) - : []; + const triggerActions = _.get(values, `${formikFieldPath}.actions`, []); return ( - {titleTemplate('Alert severity')} From 5a7fe5e369a94a53964503dba092d8005fde1a26 Mon Sep 17 00:00:00 2001 From: Jovan Cvetkovic Date: Fri, 23 Jun 2023 17:27:04 +0200 Subject: [PATCH 30/63] Code review #573 Signed-off-by: Jovan Cvetkovic --- .../components/MonitorsList.js | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.js b/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.js index 0cd9dcb18..921935728 100644 --- a/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.js +++ b/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.js @@ -98,6 +98,11 @@ const MonitorsList = ({ values, httpClient }) => { setFormikValues(selected, monitorIdx, form); }; + const onBlur = (e, field, form) => { + form.setFieldTouched(formikFieldName, true); + form.setFieldTouched(field.name, true); + }; + const updateSelection = (selected) => { const newMonitorOptions = [...options]; newMonitorOptions.forEach((mon) => { @@ -108,10 +113,6 @@ const MonitorsList = ({ values, httpClient }) => { }; const setFormikValues = (selected, monitorIdx, form) => { - // form.setFieldTouched(formikFieldName, true); - // form.setFieldTouched(`${formikFieldName}_${monitorIdx}`, true); - // form.setFieldError(formikFieldName, validate()); - const associatedMonitors = _.get( values, 'associatedMonitors', @@ -175,18 +176,19 @@ const MonitorsList = ({ values, httpClient }) => { return ( validate(), - } - } + fieldProps={{ + validate: () => validate(), + }} render={({ field, form }) => ( form.touched[formikFieldName] && !isValid(), + isInvalid: () => { + console.log(form.touched[formikFieldName], isValid()); + return form.touched[formikFieldName] && !isValid(); + }, error: () => validate(), }} > @@ -204,7 +206,7 @@ const MonitorsList = ({ values, httpClient }) => { form.touched[`${formikFieldName}_${monitorIdx}`] && !selection[monitorIdx], placeholder: 'Select a monitor', onChange: (options, field, form) => onChange(options, monitorIdx, form), - // onBlur: (e, field, form) => setFormikValues(monitorIdx, form), + onBlur: (e, field, form) => onBlur(e, field, form), options: options, singleSelection: { asPlainText: true }, selectedOptions: selection[monitorIdx] ? [selection[monitorIdx]] : undefined, From b43dda71bcc123093c1b583e3b467fbb429d5632 Mon Sep 17 00:00:00 2001 From: Jovan Cvetkovic Date: Fri, 23 Jun 2023 17:27:27 +0200 Subject: [PATCH 31/63] Code review #573 Signed-off-by: Jovan Cvetkovic --- .../components/AssociateMonitors/components/MonitorsList.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.js b/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.js index 921935728..4045f47a3 100644 --- a/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.js +++ b/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.js @@ -77,7 +77,7 @@ const MonitorsList = ({ values, httpClient }) => { ); const monitorsToOptions = (monitors) => - monitors.map((monitor, index) => ({ + monitors.map((monitor) => ({ label: monitor.monitor_name, value: monitor.monitor_id, })); @@ -179,7 +179,7 @@ const MonitorsList = ({ values, httpClient }) => { fieldProps={{ validate: () => validate(), }} - render={({ field, form }) => ( + render={({ form }) => ( Date: Fri, 23 Jun 2023 17:33:58 +0200 Subject: [PATCH 32/63] Code review #573 Signed-off-by: Jovan Cvetkovic --- .../components/AssociateMonitors/components/MonitorsList.js | 5 +---- .../containers/CreateMonitor/utils/constants.js | 1 + 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.js b/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.js index 4045f47a3..487ac2082 100644 --- a/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.js +++ b/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.js @@ -185,10 +185,7 @@ const MonitorsList = ({ values, httpClient }) => { form={form} rowProps={{ label: 'Monitor', - isInvalid: () => { - console.log(form.touched[formikFieldName], isValid()); - return form.touched[formikFieldName] && !isValid(); - }, + isInvalid: () => form.touched[formikFieldName] && !isValid(), error: () => validate(), }} > diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js b/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js index 0675b74f5..1d150e704 100644 --- a/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js +++ b/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js @@ -73,6 +73,7 @@ export const FORMIK_INITIAL_VALUES = { filters: [], // array of FORMIK_INITIAL_WHERE_EXPRESSION_VALUES detectorId: '', associatedMonitors: DEFAULT_ASSOCIATED_MONITORS_VALUE, + associatedMonitorsList: [], }; export const FORMIK_INITIAL_AGG_VALUES = { From c806609bc74df0ec658c723c5e96f4ff801219af Mon Sep 17 00:00:00 2001 From: Jovan Cvetkovic Date: Fri, 23 Jun 2023 17:34:13 +0200 Subject: [PATCH 33/63] Code review #573 Signed-off-by: Jovan Cvetkovic --- .../CreateMonitor/containers/CreateMonitor/utils/constants.js | 1 - 1 file changed, 1 deletion(-) diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js b/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js index 1d150e704..6e5cf9ebe 100644 --- a/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js +++ b/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js @@ -9,7 +9,6 @@ import { } from '../../../components/MonitorExpressions/expressions/utils/constants'; import { MONITOR_TYPE } from '../../../../../utils/constants'; import { SUPPORTED_DOC_LEVEL_QUERY_OPERATORS } from '../../../components/DocumentLevelMonitorQueries/utils/constants'; -import { QUERY_OPERATORS } from '../../../../Dashboard/components/FindingsDashboard/findingsUtils'; export const BUCKET_COUNT = 5; From 77c23d7b95f4a31a3404facc80c7453d7e520d2f Mon Sep 17 00:00:00 2001 From: Jovan Cvetkovic Date: Fri, 23 Jun 2023 17:42:48 +0200 Subject: [PATCH 34/63] Code review #573 Signed-off-by: Jovan Cvetkovic --- .../components/AssociateMonitors/components/MonitorsList.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.js b/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.js index 487ac2082..a90a63e8d 100644 --- a/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.js +++ b/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.js @@ -200,7 +200,9 @@ const MonitorsList = ({ values, httpClient }) => { name={`${formikFieldName}_${monitorIdx}`} inputProps={{ isInvalid: - form.touched[`${formikFieldName}_${monitorIdx}`] && !selection[monitorIdx], + (form.touched[`${formikFieldName}_${monitorIdx}`] || + form.touched[formikFieldName]) && + !selection[monitorIdx], placeholder: 'Select a monitor', onChange: (options, field, form) => onChange(options, monitorIdx, form), onBlur: (e, field, form) => onBlur(e, field, form), From fa03e0fae2e5150771df2d276cca63611504ec2b Mon Sep 17 00:00:00 2001 From: Jovan Cvetkovic Date: Fri, 23 Jun 2023 19:14:41 +0200 Subject: [PATCH 35/63] Code review #573 Signed-off-by: Jovan Cvetkovic --- .../containers/CreateMonitor/CreateMonitor.js | 1 + .../CompositeTriggerCondition.js | 2 ++ .../ExpressionBuilder.js | 26 ++++++++++++++----- .../CompositeMonitorsAlertTrigger.js | 2 ++ .../DefineCompositeLevelTrigger.js | 3 ++- 5 files changed, 27 insertions(+), 7 deletions(-) diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/CreateMonitor.js b/public/pages/CreateMonitor/containers/CreateMonitor/CreateMonitor.js index de32bec4d..049422409 100644 --- a/public/pages/CreateMonitor/containers/CreateMonitor/CreateMonitor.js +++ b/public/pages/CreateMonitor/containers/CreateMonitor/CreateMonitor.js @@ -327,6 +327,7 @@ export default class CreateMonitor extends Component { {values.monitor_type === MONITOR_TYPE.COMPOSITE_LEVEL ? ( { @@ -71,6 +72,7 @@ const CompositeTriggerCondition = ({ diff --git a/public/pages/CreateTrigger/components/CompositeTriggerCondition/ExpressionBuilder.js b/public/pages/CreateTrigger/components/CompositeTriggerCondition/ExpressionBuilder.js index d70275b71..a94cb2543 100644 --- a/public/pages/CreateTrigger/components/CompositeTriggerCondition/ExpressionBuilder.js +++ b/public/pages/CreateTrigger/components/CompositeTriggerCondition/ExpressionBuilder.js @@ -43,8 +43,15 @@ export const conditionToExpressions = (condition, monitors) => { return expressions; }; -const ExpressionBuilder = ({ formikFieldPath = '', formikFieldName, values, httpClient }) => { +const ExpressionBuilder = ({ + formikFieldPath = '', + formikFieldName, + values, + touched, + httpClient, +}) => { const formikFullFieldName = `${formikFieldPath}${formikFieldName}`; + const formikFullFieldValue = _.replace(`${formikFullFieldName}_value`, /[.\[\]]/gm, '_'); const DEFAULT_CONDITION = 'AND'; const DEFAULT_NAME = 'Select associated monitor'; @@ -69,6 +76,10 @@ const ExpressionBuilder = ({ formikFieldPath = '', formikFieldName, values, http const [options, setOptions] = useState([]); useEffect(() => { + // initializing formik because these are generic fields and formik won't pick them up until fields is updated + _.set(touched, formikFullFieldValue, false); + _.set(values, formikFullFieldValue, ''); + const monitors = _.get(values, 'monitorOptions', []); if (monitors.length) { setInitialValues(monitors); @@ -148,7 +159,7 @@ const ExpressionBuilder = ({ formikFieldPath = '', formikFieldName, values, http const onBlur = (form, expressions) => { onChange(form, expressions); - form.setFieldTouched(`${formikFullFieldName}_value`, true); + form.setFieldTouched(formikFullFieldValue, true); }; const openPopover = (idx = 0) => { @@ -255,16 +266,16 @@ const ExpressionBuilder = ({ formikFieldPath = '', formikFieldName, values, http return ( validate(), }} render={({ form }) => ( form.touched[`${formikFullFieldName}_value`] && !isValid(), + isInvalid: () => form.touched[formikFullFieldValue] && !isValid(), error: () => validate(), style: { maxWidth: 'inherit', @@ -287,7 +298,10 @@ const ExpressionBuilder = ({ formikFieldPath = '', formikFieldName, values, http description={expression.description} value={expression.monitor_name} isActive={!!options?.length} - onClick={() => openPopover(idx)} + onClick={() => { + form.setFieldTouched(formikFullFieldValue, true); + openPopover(idx); + }} /> } isOpen={expression.isOpen} diff --git a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/CompositeMonitorsAlertTrigger.js b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/CompositeMonitorsAlertTrigger.js index 9590925c0..390438578 100644 --- a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/CompositeMonitorsAlertTrigger.js +++ b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/CompositeMonitorsAlertTrigger.js @@ -13,11 +13,13 @@ const CompositeMonitorsAlertTrigger = ({ notificationService, plugins, values, + touched, }) => { return ( ( class DefineCompositeLevelTrigger extends Component { render() { - const { values, httpClient, notifications, notificationService, plugins } = this.props; + const { values, httpClient, notifications, notificationService, plugins, touched } = this.props; const formikFieldPath = `triggerDefinitions[0].`; const triggerName = _.get(values, `${formikFieldPath}name`, 'Trigger'); @@ -113,6 +113,7 @@ class DefineCompositeLevelTrigger extends Component { 'An alert will trigger when the following monitors generate active alerts.' )} values={values} + touched={touched} isDarkMode={this.props.isDarkMode} httpClient={httpClient} /> From 5ec5aad78e3b6cd1c1805bd915404aba7354cb95 Mon Sep 17 00:00:00 2001 From: Jovan Cvetkovic Date: Fri, 23 Jun 2023 19:22:06 +0200 Subject: [PATCH 36/63] Code review #573 Signed-off-by: Jovan Cvetkovic --- .../CompositeTriggerCondition.js | 27 +------------------ .../CreateTrigger/utils/triggerToFormik.js | 1 - 2 files changed, 1 insertion(+), 27 deletions(-) diff --git a/public/pages/CreateTrigger/components/CompositeTriggerCondition/CompositeTriggerCondition.js b/public/pages/CreateTrigger/components/CompositeTriggerCondition/CompositeTriggerCondition.js index 4012a5e78..31155b557 100644 --- a/public/pages/CreateTrigger/components/CompositeTriggerCondition/CompositeTriggerCondition.js +++ b/public/pages/CreateTrigger/components/CompositeTriggerCondition/CompositeTriggerCondition.js @@ -1,34 +1,9 @@ -import React, { useEffect, useState, useCallback } from 'react'; +import React, { useEffect, useState } from 'react'; import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; import { FormikFormRow, FormikInputWrapper } from '../../../../components/FormControls'; import ExpressionBuilder from './ExpressionBuilder'; import ExpressionEditor from './ExpressionEditor'; -export const conditionToExpressions = (query, monitors) => { - const conditionMap = { - '&&': 'and', - '||': 'or', - '!': 'not', - '': '', - }; - const queryToExpressionRegex = new RegExp('( && || \\|\\| )?(monitor\\[id=(.*?)\\])', 'gm'); - const matcher = query.matchAll(queryToExpressionRegex); - let match; - let expressions = []; - while ((match = matcher.next())) { - const monitorId = match[4]?.trim(); - const monitor = monitors.filter((mon) => mon.monitor_id === monitorId); - expressions.push({ - description: conditionMap[match[1]?.trim()] || '', - isOpen: false, - monitor_name: monitor[0]?.monitor_name, - monitor_id: monitorId, - }); - } - - return expressions; -}; - const CompositeTriggerCondition = ({ label, formikFieldPath = '', diff --git a/public/pages/CreateTrigger/containers/CreateTrigger/utils/triggerToFormik.js b/public/pages/CreateTrigger/containers/CreateTrigger/utils/triggerToFormik.js index 36e416c42..1faa58917 100644 --- a/public/pages/CreateTrigger/containers/CreateTrigger/utils/triggerToFormik.js +++ b/public/pages/CreateTrigger/containers/CreateTrigger/utils/triggerToFormik.js @@ -14,7 +14,6 @@ import { ACTIONABLE_ALERTS_OPTIONS_LABELS, NOTIFY_OPTIONS_VALUES, } from '../../../components/Action/actions/Message'; -import { conditionToExpressions } from '../../../components/CompositeTriggerCondition/CompositeTriggerCondition'; export function triggerToFormik(trigger, monitor) { return _.isArray(trigger) From a9cef2ecfb796463e67b5cf6041536d401c41b97 Mon Sep 17 00:00:00 2001 From: Jovan Cvetkovic Date: Fri, 23 Jun 2023 20:39:48 +0200 Subject: [PATCH 37/63] Code review #573 Signed-off-by: Jovan Cvetkovic --- .../AssociateMonitors/AssociateMonitors.js | 4 +- .../components/MonitorsEditor.js | 45 ++++++++++--------- .../components/MonitorsList.js | 2 +- .../containers/CreateMonitor/CreateMonitor.js | 7 ++- .../CreateMonitor/utils/constants.js | 1 + .../WorkflowDetails/WorkflowDetails.js | 9 +++- .../ExpressionBuilder.js | 2 +- .../ExpressionEditor.js | 18 +++++--- 8 files changed, 54 insertions(+), 34 deletions(-) diff --git a/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.js b/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.js index a14a59035..5a5fb7964 100644 --- a/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.js +++ b/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.js @@ -29,7 +29,7 @@ export const getMonitors = async (httpClient) => { } }; -const AssociateMonitors = ({ isDarkMode, values, httpClient }) => { +const AssociateMonitors = ({ isDarkMode, values, httpClient, errors }) => { const [graphUi, setGraphUi] = useState(false); useEffect(() => { @@ -50,7 +50,7 @@ const AssociateMonitors = ({ isDarkMode, values, httpClient }) => { {graphUi ? ( ) : ( - + )} ); diff --git a/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsEditor.js b/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsEditor.js index 9d8bf16c8..38389620c 100644 --- a/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsEditor.js +++ b/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsEditor.js @@ -3,11 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState, useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { FormikCodeEditor } from '../../../../../components/FormControls'; -import { hasError, isInvalid, validateExtractionQuery } from '../../../../../utils/validate'; -const MonitorsEditor = ({ values, isDarkMode }) => { +const MonitorsEditor = ({ values, isDarkMode, errors }) => { const codeFieldName = 'associatedMonitorsEditor'; const formikValueName = 'associatedMonitors'; const [editorValue, setEditorValue] = useState(''); @@ -15,7 +14,7 @@ const MonitorsEditor = ({ values, isDarkMode }) => { useEffect(() => { try { const code = JSON.stringify(values.associatedMonitors, null, 4); - // _.set(values, codeFieldName, code); + _.set(values, codeFieldName, code); setEditorValue(code); } catch (e) {} }, [values.associatedMonitors]); @@ -34,25 +33,26 @@ const MonitorsEditor = ({ values, isDarkMode }) => { }; const isInvalid = (name, form) => { - if (form.touched[codeFieldName]) { - try { - const associatedMonitors = form.values[name]; - const json = JSON.parse(associatedMonitors); - return !json.sequence?.delegates?.length; - } catch (e) { - return false; - } + try { + const associatedMonitors = form.values[name]; + const json = JSON.parse(associatedMonitors); + return !json.sequence?.delegates?.length || json.sequence?.delegates?.length < 2; + } catch (e) { + return true; } }; const hasError = (name, form) => { + const associatedMonitors = form.values[name]; + return validate(associatedMonitors); + }; + + const validate = (value) => { try { - const associatedMonitors = form.values[name]; - const json = JSON.parse(associatedMonitors); - return ( - json.sequence?.delegates?.length < 2 && - 'Delegates list can not be empty or have less then two associated monitors.' - ); + const json = JSON.parse(value); + if (!json.sequence?.delegates?.length || json.sequence?.delegates?.length < 2) { + return 'Delegates list can not be empty or have less then two associated monitors.'; + } } catch (e) { return 'Invalid json.'; } @@ -62,14 +62,17 @@ const MonitorsEditor = ({ values, isDarkMode }) => { isInvalid(name, form), - // error: (name, form) => hasError(name, form), + isInvalid: (name, form) => form.touched[codeFieldName] && isInvalid(name, form), + error: hasError, }} inputProps={{ + isInvalid: (name, form) => isInvalid(name, form), mode: 'json', width: '80%', height: '300px', diff --git a/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.js b/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.js index a90a63e8d..483e41f42 100644 --- a/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.js +++ b/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.js @@ -177,7 +177,7 @@ const MonitorsList = ({ values, httpClient }) => { validate(), + validate: validate, }} render={({ form }) => ( - + diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js b/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js index 6e5cf9ebe..9098f5047 100644 --- a/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js +++ b/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js @@ -73,6 +73,7 @@ export const FORMIK_INITIAL_VALUES = { detectorId: '', associatedMonitors: DEFAULT_ASSOCIATED_MONITORS_VALUE, associatedMonitorsList: [], + associatedMonitorsEditor: '', }; export const FORMIK_INITIAL_AGG_VALUES = { diff --git a/public/pages/CreateMonitor/containers/WorkflowDetails/WorkflowDetails.js b/public/pages/CreateMonitor/containers/WorkflowDetails/WorkflowDetails.js index 1d3129279..d8c6714ca 100644 --- a/public/pages/CreateMonitor/containers/WorkflowDetails/WorkflowDetails.js +++ b/public/pages/CreateMonitor/containers/WorkflowDetails/WorkflowDetails.js @@ -10,7 +10,7 @@ import AssociateMonitors from '../../components/AssociateMonitors/AssociateMonit import { EuiSpacer } from '@elastic/eui'; import { MONITOR_TYPE, SEARCH_TYPE } from '../../../../utils/constants'; -const WorkflowDetails = ({ values, isDarkMode, httpClient }) => { +const WorkflowDetails = ({ values, isDarkMode, httpClient, errors }) => { const isAd = values.searchType === SEARCH_TYPE.AD; const isComposite = values.monitor_type === MONITOR_TYPE.COMPOSITE_LEVEL; return ( @@ -29,7 +29,12 @@ const WorkflowDetails = ({ values, isDarkMode, httpClient }) => { {isComposite && ( - + )} diff --git a/public/pages/CreateTrigger/components/CompositeTriggerCondition/ExpressionBuilder.js b/public/pages/CreateTrigger/components/CompositeTriggerCondition/ExpressionBuilder.js index a94cb2543..3d77f5130 100644 --- a/public/pages/CreateTrigger/components/CompositeTriggerCondition/ExpressionBuilder.js +++ b/public/pages/CreateTrigger/components/CompositeTriggerCondition/ExpressionBuilder.js @@ -105,7 +105,7 @@ const ExpressionBuilder = ({ const condition = _.get(values, formikFullFieldName, ''); const expressions = conditionToExpressions(condition, monitors); - setUsedExpressions(expressions.length ? expressions : [DEFAULT_EXPRESSION]); + setUsedExpressions(expressions?.length ? expressions : [DEFAULT_EXPRESSION]); }; const expressionsToCondition = (expressions) => { diff --git a/public/pages/CreateTrigger/components/CompositeTriggerCondition/ExpressionEditor.js b/public/pages/CreateTrigger/components/CompositeTriggerCondition/ExpressionEditor.js index 80224d4a1..ae69f3bef 100644 --- a/public/pages/CreateTrigger/components/CompositeTriggerCondition/ExpressionEditor.js +++ b/public/pages/CreateTrigger/components/CompositeTriggerCondition/ExpressionEditor.js @@ -5,11 +5,11 @@ import { FormikCodeEditor } from '../../../../components/FormControls'; const ExpressionEditor = ({ values, formikFieldName, formikFieldPath, isDarkMode = false }) => { const [editorValue, setEditorValue] = useState(''); const formikFullFieldName = `${formikFieldPath}${formikFieldName}`; - const formikFullCodeFieldName = 'triggerConditionsCode'; + const formikFullCodeFieldName = _.replace(`${formikFullFieldName}_code`, /[.\[\]]/gm, '_'); useEffect(() => { const code = _.get(values, formikFullFieldName, ''); - // _.set(values, formikFullCodeFieldName, code); + _.set(values, formikFullCodeFieldName, code); setEditorValue(code); }, [values]); @@ -19,19 +19,25 @@ const ExpressionEditor = ({ values, formikFieldName, formikFieldPath, isDarkMode return !form.values[name]?.length && 'Invalid condition.'; }; + const validate = (value) => { + if (!value?.length) return 'Invalid condition.'; + }; + return ( form.touched[name] && isInvalid(name, form), - // error: (name, form) => hasError(name, form), + isInvalid: (name, form) => form.touched[name] && isInvalid(name, form), + error: (name, form) => hasError(name, form), }} inputProps={{ - // isInvalid: (name, form) => form.touched[name] && isInvalid(name, form), + isInvalid: (name, form) => form.touched[name] && isInvalid(name, form), mode: 'text', width: '80%', height: '300px', From c9df028e82b6e58c9e8cf526dca70c54c5c71356 Mon Sep 17 00:00:00 2001 From: Jovan Cvetkovic Date: Mon, 26 Jun 2023 15:36:41 +0200 Subject: [PATCH 38/63] Cypress tests Signed-off-by: Jovan Cvetkovic --- .../sample_composite_level_monitor.json | 62 +++++++++- .../acknowledge_alerts_modal_spec.js | 1 + .../composite_level_monitor_spec.js | 113 +++++++++++++----- cypress/support/commands.js | 17 +-- package.json | 4 +- .../ExpressionBuilder.js | 3 + 6 files changed, 159 insertions(+), 41 deletions(-) diff --git a/cypress/fixtures/sample_composite_level_monitor.json b/cypress/fixtures/sample_composite_level_monitor.json index 20a0fed18..9b1f67bf4 100644 --- a/cypress/fixtures/sample_composite_level_monitor.json +++ b/cypress/fixtures/sample_composite_level_monitor.json @@ -2,7 +2,7 @@ "sample_composite_monitor": { "type": "workflow", "schema_version": 0, - "name": "sample_component_level_monitor", + "name": "sampleComponentLevelMonitor", "workflow_type": "composite", "enabled": true, "enabled_time": 1686908176848, @@ -99,7 +99,7 @@ } }, "sample_composite_associated_monitor_1": { - "name": "monitor_1", + "name": "monitorOne", "type": "monitor", "monitor_type": "doc_level_monitor", "enabled": false, @@ -155,7 +155,63 @@ ] }, "sample_composite_associated_monitor_2": { - "name": "monitor_2", + "name": "monitorTwo", + "type": "monitor", + "monitor_type": "doc_level_monitor", + "enabled": false, + "schedule": { + "period": { + "unit": "MINUTES", + "interval": 1 + } + }, + "inputs": [ + { + "doc_level_input": { + "description": "", + "indices": ["sample_index_2"], + "queries": [ + { + "id": "monitor_2_query_1", + "name": "monitor_2_query_1", + "query": "NOT (audit_category:\"sample_text\")", + "tags": [] + }, + { + "id": "monitor_2_query_2", + "name": "monitor_2_query_2", + "query": "NOT (audit_node_host_name:\"sample_text\")", + "tags": [] + }, + { + "id": "monitor_2_query_3", + "name": "monitor_2_query_3", + "query": "NOT (audit_node_id:\"sample_text\")", + "tags": [] + } + ] + } + } + ], + "triggers": [ + { + "document_level_trigger": { + "id": "sample_trigger_2", + "name": "monitor_2_query_2", + "severity": "1", + "condition": { + "script": { + "source": "query[name=monitor_2_query_1] || query[name=monitor_2_query_2] && query[name=monitor_2_query_3]", + "lang": "painless" + } + }, + "actions": [] + } + } + ] + }, + "sample_composite_associated_monitor_3": { + "name": "monitorThree", "type": "monitor", "monitor_type": "doc_level_monitor", "enabled": false, diff --git a/cypress/integration/acknowledge_alerts_modal_spec.js b/cypress/integration/acknowledge_alerts_modal_spec.js index daf439b05..16e68c539 100644 --- a/cypress/integration/acknowledge_alerts_modal_spec.js +++ b/cypress/integration/acknowledge_alerts_modal_spec.js @@ -18,6 +18,7 @@ const TWENTY_SECONDS = 20000; describe('AcknowledgeAlertsModal', () => { before(() => { // Delete any existing monitors + cy.deleteAllAlerts(); cy.deleteAllMonitors(); // Load sample data diff --git a/cypress/integration/composite_level_monitor_spec.js b/cypress/integration/composite_level_monitor_spec.js index 006c742bd..ca5e4e284 100644 --- a/cypress/integration/composite_level_monitor_spec.js +++ b/cypress/integration/composite_level_monitor_spec.js @@ -3,21 +3,20 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { PLUGIN_NAME } from '../support/constants'; +import { API, PLUGIN_NAME } from '../support/constants'; import sampleCompositeJson from '../fixtures/sample_composite_level_monitor.json'; +import * as _ from 'lodash'; const sample_index_1 = 'sample_index_1'; const sample_index_2 = 'sample_index_2'; const SAMPLE_VISUAL_EDITOR_MONITOR = 'sample_visual_editor_composite_level_monitor'; -const SAMPLE_COMPOSITE_LEVEL_MONITOR = 'sample_composite_level_monitor'; const clearAll = () => { - cy.deleteAllMonitors(); cy.deleteIndexByName(sample_index_1); cy.deleteIndexByName(sample_index_2); - // wait until alerts index finishes writing docs - cy.wait(1000).then(() => cy.deleteAllAlerts()); + cy.deleteAllAlerts(); + cy.deleteAllMonitors(); }; describe('CompositeLevelMonitor', () => { @@ -28,25 +27,25 @@ describe('CompositeLevelMonitor', () => { cy.createIndexByName(sample_index_1, sampleCompositeJson.sample_composite_index); cy.createIndexByName(sample_index_2, sampleCompositeJson.sample_composite_index); - // Create asociated monitors + // Create associated monitors cy.createMonitor(sampleCompositeJson.sample_composite_associated_monitor_1); cy.createMonitor(sampleCompositeJson.sample_composite_associated_monitor_2); + cy.createMonitor(sampleCompositeJson.sample_composite_associated_monitor_3); }); beforeEach(() => { // Set welcome screen tracking to false localStorage.setItem('home:welcome:show', 'false'); - - // Visit Alerting OpenSearch Dashboards - cy.visit(`${Cypress.env('opensearch_dashboards')}/app/${PLUGIN_NAME}#/monitors`); - - // Common text to wait for to confirm page loaded, give up to 20 seconds for initial load - cy.contains('Create monitor', { timeout: 20000 }); }); - let monitorId; describe('can be created', () => { beforeEach(() => { + // Visit Alerting OpenSearch Dashboards + cy.visit(`${Cypress.env('opensearch_dashboards')}/app/${PLUGIN_NAME}#/monitors`); + + // Common text to wait for to confirm page loaded, give up to 20 seconds for initial load + cy.contains('Create monitor', { timeout: 20000 }); + // Go to create monitor page cy.contains('Create monitor').click({ force: true }); @@ -63,10 +62,10 @@ describe('CompositeLevelMonitor', () => { // Select associated monitors cy.get('[data-test-subj="monitors_list_0"]') - .type(sampleCompositeJson.sample_composite_associated_monitor_1.name) + .type('monitorOne', { delay: 50 }) .type('{enter}'); cy.get('[data-test-subj="monitors_list_1"]') - .type(sampleCompositeJson.sample_composite_associated_monitor_2.name) + .type('monitorTwo', { delay: 50 }) .type('{enter}'); // Type trigger name @@ -76,8 +75,19 @@ describe('CompositeLevelMonitor', () => { .type('Composite trigger'); // Add associated monitors to condition - cy.get('[data-test-subj="condition-add-selection-btn"]').click(); - cy.get('[data-test-subj="condition-add-selection-btn"]').click(); + cy.get('[data-test-subj="condition-add-options-btn"]').click({ force: true }); + + cy.get('[data-test-subj="select-expression_0"]').click({ force: true }); + cy.wait(1000); + cy.get('[data-test-subj="monitors-combobox-0"]') + .type('monitorOne', { delay: 50 }) + .type('{enter}'); + + cy.get('[data-test-subj="select-expression_1"]').click({ force: true }); + cy.wait(1000); + cy.get('[data-test-subj="monitors-combobox-1"]') + .type('monitorTwo', { delay: 50 }) + .type('{enter}'); // TODO: Test with Notifications plugin // Select notification channel @@ -89,7 +99,7 @@ describe('CompositeLevelMonitor', () => { // Wait for monitor to be created cy.wait('@createMonitorRequest').then((interceptor) => { - monitorId = interceptor.response.body.resp._id; + const monitorId = interceptor.response.body.resp._id; cy.contains('Loading monitors'); cy.wait('@getMonitorsRequest').then((interceptor) => { @@ -118,7 +128,7 @@ describe('CompositeLevelMonitor', () => { monitor1[0] && cy.executeMonitor(monitor1[0].id); monitor2[0] && cy.executeMonitor(monitor2[0].id); - cy.get('[role="tab"]').contains('Alerts').click(); + cy.get('[role="tab"]').contains('Alerts').click({ force: true }); cy.get('table tbody td').contains('Composite trigger'); }); }); @@ -129,20 +139,67 @@ 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.`); - } + const body = { + size: 200, + query: { + match_all: {}, + }, + }; + cy.request({ + method: 'GET', + url: `${Cypress.env('opensearch')}${API.MONITOR_BASE}/_search`, + failOnStatusCode: false, // In case there is no alerting config index in cluster, where the status code is 404 + body, + }).then((response) => { + if (response.status === 200) { + const monitors = response.body.hits.hits; + const createdMonitor = _.find( + monitors, + (monitor) => monitor._source.name === SAMPLE_VISUAL_EDITOR_MONITOR + ); + if (createdMonitor) { + cy.visit( + `${Cypress.env('opensearch_dashboards')}/app/${PLUGIN_NAME}#/monitors/${ + createdMonitor._id + }?action=update-monitor&type=workflow` + ); + } else { + cy.log('Failed to get created monitor ', SAMPLE_VISUAL_EDITOR_MONITOR); + throw new Error(`Failed to get created monitor ${SAMPLE_VISUAL_EDITOR_MONITOR}`); + } + } else { + cy.log('Failed to get all monitors.', response); + } + }); }); it('by visual editor', () => { // Verify edit page cy.contains('Edit monitor', { timeout: 20000 }); + cy.get('input[name="name"]').type('_edited'); + + cy.get('label').contains('Visual editor').click({ force: true }); + + cy.get('button').contains('Associate another monitor').click({ force: true }); + + cy.get('[data-test-subj="monitors_list_2"]') + .type('monitorThree', { delay: 50 }) + .type('{enter}'); + + cy.get('[data-test-subj="condition-add-options-btn"]').click({ force: true }); + cy.get('[data-test-subj="select-expression_2"]').click({ force: true }); + cy.wait(1000); + cy.get('[data-test-subj="monitors-combobox-2"]') + .type('monitorThree', { delay: 50 }) + .type('{enter}'); + + cy.intercept('api/alerting/workflows/*').as('updateMonitorRequest'); + cy.get('button').contains('Update').click({ force: true }); + + // Wait for monitor to be created + cy.wait('@updateMonitorRequest').then(() => { + cy.get('.euiTitle--large').contains(`${SAMPLE_VISUAL_EDITOR_MONITOR}_edited`); + }); }); }); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 4bfcd67f0..e05711de0 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -144,15 +144,16 @@ Cypress.Commands.add('deleteAllMonitors', () => { const monitors = response.body.hits.hits.sort((monitor) => monitor._source.type === 'workflow' ? -1 : 1 ); - console.log('MONITORS', monitors); for (let i = 0; i < monitors.length; i++) { - cy.request({ - method: 'DELETE', - url: `${Cypress.env('opensearch')}${ - monitors[i]._source.type === 'workflow' ? API.WORKFLOW_BASE : API.MONITOR_BASE - }/${monitors[i]._id}`, - failOnStatusCode: false, - }); + if (monitors[i]._id) { + cy.request({ + method: 'DELETE', + url: `${Cypress.env('opensearch')}${ + monitors[i]._source.type === 'workflow' ? API.WORKFLOW_BASE : API.MONITOR_BASE + }/${monitors[i]._id}`, + failOnStatusCode: false, + }); + } } } else { cy.log('Failed to get all monitors.', response); diff --git a/package.json b/package.json index 9f46a36de..a002e7d18 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,8 @@ "test:jest:windows": "SET TZ=UTC ../../node_modules/.bin/jest --config ./test/jest.config.js", "test:jest": "TZ=UTC ../../node_modules/.bin/jest --config ./test/jest.config.js", "test:jest:update-snapshots": "yarn run test:jest -u", - "cypress:open": "cypress open", - "cypress:run": "cypress run", + "cypress:run:browser": "cypress open", + "cypress:run:ci": "cypress run", "build": "yarn plugin-helpers build", "plugin-helpers": "node ../../scripts/plugin_helpers", "postbuild": "echo Renaming build artifact to [$npm_package_config_id-$npm_package_version.zip] && mv build/$npm_package_config_id*.zip build/$npm_package_config_id-$npm_package_version.zip" diff --git a/public/pages/CreateTrigger/components/CompositeTriggerCondition/ExpressionBuilder.js b/public/pages/CreateTrigger/components/CompositeTriggerCondition/ExpressionBuilder.js index 3d77f5130..12d09db25 100644 --- a/public/pages/CreateTrigger/components/CompositeTriggerCondition/ExpressionBuilder.js +++ b/public/pages/CreateTrigger/components/CompositeTriggerCondition/ExpressionBuilder.js @@ -209,6 +209,7 @@ const ExpressionBuilder = ({ { const differences = _.differenceBy(options, usedExpressions, 'monitor_id'); return [ @@ -302,6 +304,7 @@ const ExpressionBuilder = ({ form.setFieldTouched(formikFullFieldValue, true); openPopover(idx); }} + data-test-subj={`select-expression_${idx}`} /> } isOpen={expression.isOpen} From dcedac0fe6ae6d3d798278fb5cfa5be16e5728d1 Mon Sep 17 00:00:00 2001 From: Jovan Cvetkovic Date: Mon, 26 Jun 2023 19:54:24 +0200 Subject: [PATCH 39/63] Cypress tests Signed-off-by: Jovan Cvetkovic --- .../AnomalyDetector.test.js.snap | 54 ++++++++++++++----- .../__snapshots__/CreateMonitor.test.js.snap | 9 +++- .../__snapshots__/DefineMonitor.test.js.snap | 9 +++- .../__snapshots__/MonitorIndex.test.js.snap | 45 ++++++++++++---- .../AcknowledgeAlertsModal.test.js.snap | 9 +++- 5 files changed, 98 insertions(+), 28 deletions(-) diff --git a/public/pages/CreateMonitor/containers/AnomalyDetectors/__tests__/__snapshots__/AnomalyDetector.test.js.snap b/public/pages/CreateMonitor/containers/AnomalyDetectors/__tests__/__snapshots__/AnomalyDetector.test.js.snap index d59bbb56f..12772468c 100644 --- a/public/pages/CreateMonitor/containers/AnomalyDetectors/__tests__/__snapshots__/AnomalyDetector.test.js.snap +++ b/public/pages/CreateMonitor/containers/AnomalyDetectors/__tests__/__snapshots__/AnomalyDetector.test.js.snap @@ -6,7 +6,13 @@ exports[`AnomalyDetectors renders 1`] = ` Object { "aggregationType": "count", "aggregations": Array [], - "associatedMonitors": Array [], + "associatedMonitors": Object { + "sequence": Object { + "delegates": Array [], + }, + }, + "associatedMonitorsEditor": "", + "associatedMonitorsList": Array [], "bucketUnitOfTime": "h", "bucketValue": 1, "cronExpression": "0 */1 * * *", @@ -14,7 +20,6 @@ exports[`AnomalyDetectors renders 1`] = ` "description": "", "detectorId": "", "disabled": false, - "expressionQuery": null, "fieldName": Array [], "filters": Array [], "frequency": "interval", @@ -72,7 +77,13 @@ exports[`AnomalyDetectors renders 1`] = ` Object { "aggregationType": "count", "aggregations": Array [], - "associatedMonitors": Array [], + "associatedMonitors": Object { + "sequence": Object { + "delegates": Array [], + }, + }, + "associatedMonitorsEditor": "", + "associatedMonitorsList": Array [], "bucketUnitOfTime": "h", "bucketValue": 1, "cronExpression": "0 */1 * * *", @@ -80,7 +91,6 @@ exports[`AnomalyDetectors renders 1`] = ` "description": "", "detectorId": "", "disabled": false, - "expressionQuery": null, "fieldName": Array [], "filters": Array [], "frequency": "interval", @@ -199,7 +209,13 @@ exports[`AnomalyDetectors renders 1`] = ` "initialValues": Object { "aggregationType": "count", "aggregations": Array [], - "associatedMonitors": Array [], + "associatedMonitors": Object { + "sequence": Object { + "delegates": Array [], + }, + }, + "associatedMonitorsEditor": "", + "associatedMonitorsList": Array [], "bucketUnitOfTime": "h", "bucketValue": 1, "cronExpression": "0 */1 * * *", @@ -207,7 +223,6 @@ exports[`AnomalyDetectors renders 1`] = ` "description": "", "detectorId": "", "disabled": false, - "expressionQuery": null, "fieldName": Array [], "filters": Array [], "frequency": "interval", @@ -284,7 +299,13 @@ exports[`AnomalyDetectors renders 1`] = ` "values": Object { "aggregationType": "count", "aggregations": Array [], - "associatedMonitors": Array [], + "associatedMonitors": Object { + "sequence": Object { + "delegates": Array [], + }, + }, + "associatedMonitorsEditor": "", + "associatedMonitorsList": Array [], "bucketUnitOfTime": "h", "bucketValue": 1, "cronExpression": "0 */1 * * *", @@ -292,7 +313,6 @@ exports[`AnomalyDetectors renders 1`] = ` "description": "", "detectorId": "", "disabled": false, - "expressionQuery": null, "fieldName": Array [], "filters": Array [], "frequency": "interval", @@ -417,7 +437,13 @@ exports[`AnomalyDetectors renders 1`] = ` "initialValues": Object { "aggregationType": "count", "aggregations": Array [], - "associatedMonitors": Array [], + "associatedMonitors": Object { + "sequence": Object { + "delegates": Array [], + }, + }, + "associatedMonitorsEditor": "", + "associatedMonitorsList": Array [], "bucketUnitOfTime": "h", "bucketValue": 1, "cronExpression": "0 */1 * * *", @@ -425,7 +451,6 @@ exports[`AnomalyDetectors renders 1`] = ` "description": "", "detectorId": "", "disabled": false, - "expressionQuery": null, "fieldName": Array [], "filters": Array [], "frequency": "interval", @@ -502,7 +527,13 @@ exports[`AnomalyDetectors renders 1`] = ` "values": Object { "aggregationType": "count", "aggregations": Array [], - "associatedMonitors": Array [], + "associatedMonitors": Object { + "sequence": Object { + "delegates": Array [], + }, + }, + "associatedMonitorsEditor": "", + "associatedMonitorsList": Array [], "bucketUnitOfTime": "h", "bucketValue": 1, "cronExpression": "0 */1 * * *", @@ -510,7 +541,6 @@ exports[`AnomalyDetectors renders 1`] = ` "description": "", "detectorId": "", "disabled": false, - "expressionQuery": null, "fieldName": Array [], "filters": Array [], "frequency": "interval", diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/__snapshots__/CreateMonitor.test.js.snap b/public/pages/CreateMonitor/containers/CreateMonitor/__snapshots__/CreateMonitor.test.js.snap index 32b0b9450..344bc83ca 100644 --- a/public/pages/CreateMonitor/containers/CreateMonitor/__snapshots__/CreateMonitor.test.js.snap +++ b/public/pages/CreateMonitor/containers/CreateMonitor/__snapshots__/CreateMonitor.test.js.snap @@ -14,7 +14,13 @@ exports[`CreateMonitor renders 1`] = ` "adResultIndex": undefined, "aggregationType": "count", "aggregations": Array [], - "associatedMonitors": Array [], + "associatedMonitors": Object { + "sequence": Object { + "delegates": Array [], + }, + }, + "associatedMonitorsEditor": "", + "associatedMonitorsList": Array [], "bucketUnitOfTime": "h", "bucketValue": 1, "cronExpression": "0 */1 * * *", @@ -22,7 +28,6 @@ exports[`CreateMonitor renders 1`] = ` "description": "", "detectorId": "", "disabled": false, - "expressionQuery": null, "fieldName": Array [], "filters": Array [], "frequency": "interval", diff --git a/public/pages/CreateMonitor/containers/DefineMonitor/__snapshots__/DefineMonitor.test.js.snap b/public/pages/CreateMonitor/containers/DefineMonitor/__snapshots__/DefineMonitor.test.js.snap index 4d811f708..111809b22 100644 --- a/public/pages/CreateMonitor/containers/DefineMonitor/__snapshots__/DefineMonitor.test.js.snap +++ b/public/pages/CreateMonitor/containers/DefineMonitor/__snapshots__/DefineMonitor.test.js.snap @@ -11,7 +11,13 @@ exports[`DefineMonitor renders 1`] = ` Object { "aggregationType": "count", "aggregations": Array [], - "associatedMonitors": Array [], + "associatedMonitors": Object { + "sequence": Object { + "delegates": Array [], + }, + }, + "associatedMonitorsEditor": "", + "associatedMonitorsList": Array [], "bucketUnitOfTime": "h", "bucketValue": 1, "cronExpression": "0 */1 * * *", @@ -19,7 +25,6 @@ exports[`DefineMonitor renders 1`] = ` "description": "", "detectorId": "", "disabled": false, - "expressionQuery": null, "fieldName": Array [], "filters": Array [], "frequency": "interval", diff --git a/public/pages/CreateMonitor/containers/MonitorIndex/__snapshots__/MonitorIndex.test.js.snap b/public/pages/CreateMonitor/containers/MonitorIndex/__snapshots__/MonitorIndex.test.js.snap index 09351d512..cffc953c0 100644 --- a/public/pages/CreateMonitor/containers/MonitorIndex/__snapshots__/MonitorIndex.test.js.snap +++ b/public/pages/CreateMonitor/containers/MonitorIndex/__snapshots__/MonitorIndex.test.js.snap @@ -6,7 +6,13 @@ exports[`MonitorIndex renders 1`] = ` Object { "aggregationType": "count", "aggregations": Array [], - "associatedMonitors": Array [], + "associatedMonitors": Object { + "sequence": Object { + "delegates": Array [], + }, + }, + "associatedMonitorsEditor": "", + "associatedMonitorsList": Array [], "bucketUnitOfTime": "h", "bucketValue": 1, "cronExpression": "0 */1 * * *", @@ -14,7 +20,6 @@ exports[`MonitorIndex renders 1`] = ` "description": "", "detectorId": "", "disabled": false, - "expressionQuery": null, "fieldName": Array [], "filters": Array [], "frequency": "interval", @@ -145,7 +150,13 @@ exports[`MonitorIndex renders 1`] = ` "initialValues": Object { "aggregationType": "count", "aggregations": Array [], - "associatedMonitors": Array [], + "associatedMonitors": Object { + "sequence": Object { + "delegates": Array [], + }, + }, + "associatedMonitorsEditor": "", + "associatedMonitorsList": Array [], "bucketUnitOfTime": "h", "bucketValue": 1, "cronExpression": "0 */1 * * *", @@ -153,7 +164,6 @@ exports[`MonitorIndex renders 1`] = ` "description": "", "detectorId": "", "disabled": false, - "expressionQuery": null, "fieldName": Array [], "filters": Array [], "frequency": "interval", @@ -230,7 +240,13 @@ exports[`MonitorIndex renders 1`] = ` "values": Object { "aggregationType": "count", "aggregations": Array [], - "associatedMonitors": Array [], + "associatedMonitors": Object { + "sequence": Object { + "delegates": Array [], + }, + }, + "associatedMonitorsEditor": "", + "associatedMonitorsList": Array [], "bucketUnitOfTime": "h", "bucketValue": 1, "cronExpression": "0 */1 * * *", @@ -238,7 +254,6 @@ exports[`MonitorIndex renders 1`] = ` "description": "", "detectorId": "", "disabled": false, - "expressionQuery": null, "fieldName": Array [], "filters": Array [], "frequency": "interval", @@ -379,7 +394,13 @@ exports[`MonitorIndex renders 1`] = ` "initialValues": Object { "aggregationType": "count", "aggregations": Array [], - "associatedMonitors": Array [], + "associatedMonitors": Object { + "sequence": Object { + "delegates": Array [], + }, + }, + "associatedMonitorsEditor": "", + "associatedMonitorsList": Array [], "bucketUnitOfTime": "h", "bucketValue": 1, "cronExpression": "0 */1 * * *", @@ -387,7 +408,6 @@ exports[`MonitorIndex renders 1`] = ` "description": "", "detectorId": "", "disabled": false, - "expressionQuery": null, "fieldName": Array [], "filters": Array [], "frequency": "interval", @@ -464,7 +484,13 @@ exports[`MonitorIndex renders 1`] = ` "values": Object { "aggregationType": "count", "aggregations": Array [], - "associatedMonitors": Array [], + "associatedMonitors": Object { + "sequence": Object { + "delegates": Array [], + }, + }, + "associatedMonitorsEditor": "", + "associatedMonitorsList": Array [], "bucketUnitOfTime": "h", "bucketValue": 1, "cronExpression": "0 */1 * * *", @@ -472,7 +498,6 @@ exports[`MonitorIndex renders 1`] = ` "description": "", "detectorId": "", "disabled": false, - "expressionQuery": null, "fieldName": Array [], "filters": Array [], "frequency": "interval", diff --git a/public/pages/Dashboard/components/AcknowledgeAlertsModal/__snapshots__/AcknowledgeAlertsModal.test.js.snap b/public/pages/Dashboard/components/AcknowledgeAlertsModal/__snapshots__/AcknowledgeAlertsModal.test.js.snap index 659fd3a86..85b6e9ade 100644 --- a/public/pages/Dashboard/components/AcknowledgeAlertsModal/__snapshots__/AcknowledgeAlertsModal.test.js.snap +++ b/public/pages/Dashboard/components/AcknowledgeAlertsModal/__snapshots__/AcknowledgeAlertsModal.test.js.snap @@ -35,7 +35,13 @@ exports[`AcknowledgeAlertsModal renders 1`] = ` Object { "aggregationType": "count", "aggregations": Array [], - "associatedMonitors": Array [], + "associatedMonitors": Object { + "sequence": Object { + "delegates": Array [], + }, + }, + "associatedMonitorsEditor": "", + "associatedMonitorsList": Array [], "bucketUnitOfTime": "h", "bucketValue": 1, "cronExpression": "0 */1 * * *", @@ -43,7 +49,6 @@ exports[`AcknowledgeAlertsModal renders 1`] = ` "description": "", "detectorId": "", "disabled": false, - "expressionQuery": null, "fieldName": Array [], "filters": Array [], "frequency": "interval", From ed1bef736c0d86ee56b39cf504f1f6364df30037 Mon Sep 17 00:00:00 2001 From: Jovan Cvetkovic Date: Tue, 27 Jun 2023 11:21:29 +0200 Subject: [PATCH 40/63] Unit tests Signed-off-by: Jovan Cvetkovic --- .../AssociateMonitors.test.js | 22 ++ .../AssociateMonitors.test.js.snap | 71 ++++ .../components/MonitorsEditor.test.js | 22 ++ .../components/MonitorsList.test.js | 22 ++ .../__snapshots__/MonitorsEditor.test.js.snap | 49 +++ .../__snapshots__/MonitorsList.test.js.snap | 183 +++++++++ .../__snapshots__/VisualGraph.test.js.snap | 22 +- .../WorkflowDetails/WorkflowDetails.test.js | 22 ++ .../WorkflowDetails.test.js.snap | 251 +++++++++++++ .../CompositeTriggerCondition.test.js | 30 ++ .../ExpressionBuilder.test.js | 28 ++ .../ExpressionEditor.test.js | 27 ++ .../ExpressionQuery.test.ts | 14 - .../CompositeTriggerCondition.test.js.snap | 79 ++++ .../ExpressionBuilder.test.js.snap | 48 +++ .../ExpressionEditor.test.js.snap | 49 +++ .../CompositeMonitorsAlertTrigger.test.js | 30 ++ .../DefineCompositeLevelTrigger.test.js | 30 ++ .../NotificationConfigDialog.test.js | 28 ++ .../TriggerNotifications.test.js | 29 ++ .../TriggerNotificationsContent.test.js | 47 +++ ...CompositeMonitorsAlertTrigger.test.js.snap | 308 ++++++++++++++++ .../DefineCompositeLevelTrigger.test.js.snap | 308 ++++++++++++++++ .../NotificationConfigDialog.test.js.snap | 3 + .../TriggerNotifications.test.js.snap | 39 ++ .../TriggerNotificationsContent.test.js.snap | 348 ++++++++++++++++++ 26 files changed, 2084 insertions(+), 25 deletions(-) create mode 100644 public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.test.js create mode 100644 public/pages/CreateMonitor/components/AssociateMonitors/__snapshots__/AssociateMonitors.test.js.snap create mode 100644 public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsEditor.test.js create mode 100644 public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.test.js create mode 100644 public/pages/CreateMonitor/components/AssociateMonitors/components/__snapshots__/MonitorsEditor.test.js.snap create mode 100644 public/pages/CreateMonitor/components/AssociateMonitors/components/__snapshots__/MonitorsList.test.js.snap create mode 100644 public/pages/CreateMonitor/containers/WorkflowDetails/WorkflowDetails.test.js create mode 100644 public/pages/CreateMonitor/containers/WorkflowDetails/__snapshots__/WorkflowDetails.test.js.snap create mode 100644 public/pages/CreateTrigger/components/CompositeTriggerCondition/CompositeTriggerCondition.test.js create mode 100644 public/pages/CreateTrigger/components/CompositeTriggerCondition/ExpressionBuilder.test.js create mode 100644 public/pages/CreateTrigger/components/CompositeTriggerCondition/ExpressionEditor.test.js delete mode 100644 public/pages/CreateTrigger/components/CompositeTriggerCondition/ExpressionQuery.test.ts create mode 100644 public/pages/CreateTrigger/components/CompositeTriggerCondition/__snapshots__/CompositeTriggerCondition.test.js.snap create mode 100644 public/pages/CreateTrigger/components/CompositeTriggerCondition/__snapshots__/ExpressionBuilder.test.js.snap create mode 100644 public/pages/CreateTrigger/components/CompositeTriggerCondition/__snapshots__/ExpressionEditor.test.js.snap create mode 100644 public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/CompositeMonitorsAlertTrigger.test.js create mode 100644 public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/DefineCompositeLevelTrigger.test.js create mode 100644 public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/NotificationConfigDialog.test.js create mode 100644 public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotifications.test.js create mode 100644 public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotificationsContent.test.js create mode 100644 public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/__snapshots__/CompositeMonitorsAlertTrigger.test.js.snap create mode 100644 public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/__snapshots__/DefineCompositeLevelTrigger.test.js.snap create mode 100644 public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/__snapshots__/NotificationConfigDialog.test.js.snap create mode 100644 public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/__snapshots__/TriggerNotifications.test.js.snap create mode 100644 public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/__snapshots__/TriggerNotificationsContent.test.js.snap diff --git a/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.test.js b/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.test.js new file mode 100644 index 000000000..f9debfe3e --- /dev/null +++ b/public/pages/CreateMonitor/components/AssociateMonitors/AssociateMonitors.test.js @@ -0,0 +1,22 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { render } from 'enzyme'; + +import AssociateMonitors from './AssociateMonitors'; +import { Formik } from 'formik'; +import { FORMIK_INITIAL_VALUES } from '../../containers/CreateMonitor/utils/constants'; + +describe('AssociateMonitors', () => { + test('renders', () => { + const component = ( + {}}> + + + ); + expect(render(component)).toMatchSnapshot(); + }); +}); diff --git a/public/pages/CreateMonitor/components/AssociateMonitors/__snapshots__/AssociateMonitors.test.js.snap b/public/pages/CreateMonitor/components/AssociateMonitors/__snapshots__/AssociateMonitors.test.js.snap new file mode 100644 index 000000000..6d72ed363 --- /dev/null +++ b/public/pages/CreateMonitor/components/AssociateMonitors/__snapshots__/AssociateMonitors.test.js.snap @@ -0,0 +1,71 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AssociateMonitors renders 1`] = ` +Array [ +
+

+ Associate monitors +

+
, +
+
+ Associate two or more monitors to run as part of this workflow. +
+
, +
, +
+
+ +
+
+
+ +
+
+
+
, +] +`; diff --git a/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsEditor.test.js b/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsEditor.test.js new file mode 100644 index 000000000..1b32fcdae --- /dev/null +++ b/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsEditor.test.js @@ -0,0 +1,22 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { render } from 'enzyme'; + +import { Formik } from 'formik'; +import { FORMIK_INITIAL_VALUES } from '../../../containers/CreateMonitor/utils/constants'; +import MonitorsEditor from './MonitorsEditor'; + +describe('MonitorsEditor', () => { + test('renders', () => { + const component = ( + {}}> + + + ); + expect(render(component)).toMatchSnapshot(); + }); +}); diff --git a/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.test.js b/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.test.js new file mode 100644 index 000000000..4b3102d3b --- /dev/null +++ b/public/pages/CreateMonitor/components/AssociateMonitors/components/MonitorsList.test.js @@ -0,0 +1,22 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { render } from 'enzyme'; + +import { Formik } from 'formik'; +import { FORMIK_INITIAL_VALUES } from '../../../containers/CreateMonitor/utils/constants'; +import MonitorsList from './MonitorsList'; + +describe('MonitorsList', () => { + test('renders', () => { + const component = ( + {}}> + + + ); + expect(render(component)).toMatchSnapshot(); + }); +}); diff --git a/public/pages/CreateMonitor/components/AssociateMonitors/components/__snapshots__/MonitorsEditor.test.js.snap b/public/pages/CreateMonitor/components/AssociateMonitors/components/__snapshots__/MonitorsEditor.test.js.snap new file mode 100644 index 000000000..4726f7a90 --- /dev/null +++ b/public/pages/CreateMonitor/components/AssociateMonitors/components/__snapshots__/MonitorsEditor.test.js.snap @@ -0,0 +1,49 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MonitorsEditor renders 1`] = ` +
+
+ +
+
+
+ +
+
+
+
+`; diff --git a/public/pages/CreateMonitor/components/AssociateMonitors/components/__snapshots__/MonitorsList.test.js.snap b/public/pages/CreateMonitor/components/AssociateMonitors/components/__snapshots__/MonitorsList.test.js.snap new file mode 100644 index 000000000..8c5b8f61b --- /dev/null +++ b/public/pages/CreateMonitor/components/AssociateMonitors/components/__snapshots__/MonitorsList.test.js.snap @@ -0,0 +1,183 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MonitorsList renders 1`] = ` +
+
+ +
+
+
+
+ +
+
+
+ +
+
+ +
+
+ You can associate up to 8 more monitors. +
+
+
+
+`; diff --git a/public/pages/CreateMonitor/components/VisualGraph/__snapshots__/VisualGraph.test.js.snap b/public/pages/CreateMonitor/components/VisualGraph/__snapshots__/VisualGraph.test.js.snap index 4daf8404c..ed2250957 100644 --- a/public/pages/CreateMonitor/components/VisualGraph/__snapshots__/VisualGraph.test.js.snap +++ b/public/pages/CreateMonitor/components/VisualGraph/__snapshots__/VisualGraph.test.js.snap @@ -90,7 +90,7 @@ exports[`VisualGraph renders 1`] = ` text-anchor="middle" transform="translate(0, 14)" > - 03 PM + 04 PM - 04 PM + 05 PM - 05 PM + 06 PM - 06 PM + 07 PM - 07 PM + 08 PM - 08 PM + 09 PM @@ -631,7 +631,7 @@ exports[`VisualGraph renders with bucket level monitor 1`] = ` text-anchor="middle" transform="translate(0, 14)" > - 04 PM + 05 PM - 05 PM + 06 PM - 06 PM + 07 PM - 07 PM + 08 PM - 08 PM + 09 PM diff --git a/public/pages/CreateMonitor/containers/WorkflowDetails/WorkflowDetails.test.js b/public/pages/CreateMonitor/containers/WorkflowDetails/WorkflowDetails.test.js new file mode 100644 index 000000000..6f91e7438 --- /dev/null +++ b/public/pages/CreateMonitor/containers/WorkflowDetails/WorkflowDetails.test.js @@ -0,0 +1,22 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { render } from 'enzyme'; + +import { Formik } from 'formik'; +import { FORMIK_INITIAL_VALUES } from '../CreateMonitor/utils/constants'; +import WorkflowDetails from './WorkflowDetails'; + +describe('WorkflowDetails', () => { + test('renders', () => { + const component = ( + {}}> + + + ); + expect(render(component)).toMatchSnapshot(); + }); +}); diff --git a/public/pages/CreateMonitor/containers/WorkflowDetails/__snapshots__/WorkflowDetails.test.js.snap b/public/pages/CreateMonitor/containers/WorkflowDetails/__snapshots__/WorkflowDetails.test.js.snap new file mode 100644 index 000000000..5145a7795 --- /dev/null +++ b/public/pages/CreateMonitor/containers/WorkflowDetails/__snapshots__/WorkflowDetails.test.js.snap @@ -0,0 +1,251 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`WorkflowDetails renders 1`] = ` +
+
+
+

+ Workflow +

+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ Define workflow schedule +

+
+
+
+
+
+
+ +
+
+
+
+ +
+ +
+ EuiIconMock +
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+ +
+ +
+ EuiIconMock +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ +
+ EuiIconMock +
+
+
+
+
+
+
+
+
+
+
+
+
+`; diff --git a/public/pages/CreateTrigger/components/CompositeTriggerCondition/CompositeTriggerCondition.test.js b/public/pages/CreateTrigger/components/CompositeTriggerCondition/CompositeTriggerCondition.test.js new file mode 100644 index 000000000..aa2660de7 --- /dev/null +++ b/public/pages/CreateTrigger/components/CompositeTriggerCondition/CompositeTriggerCondition.test.js @@ -0,0 +1,30 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { render } from 'enzyme'; + +import { Formik } from 'formik'; +import CompositeTriggerCondition from './CompositeTriggerCondition'; +import { FORMIK_INITIAL_VALUES } from '../../../CreateMonitor/containers/CreateMonitor/utils/constants'; + +describe('CompositeTriggerCondition', () => { + test('renders', () => { + const component = ( + {}}> + + + ); + expect(render(component)).toMatchSnapshot(); + }); +}); diff --git a/public/pages/CreateTrigger/components/CompositeTriggerCondition/ExpressionBuilder.test.js b/public/pages/CreateTrigger/components/CompositeTriggerCondition/ExpressionBuilder.test.js new file mode 100644 index 000000000..b9cefe251 --- /dev/null +++ b/public/pages/CreateTrigger/components/CompositeTriggerCondition/ExpressionBuilder.test.js @@ -0,0 +1,28 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { render } from 'enzyme'; + +import { Formik } from 'formik'; +import { FORMIK_INITIAL_VALUES } from '../../../CreateMonitor/containers/CreateMonitor/utils/constants'; +import ExpressionBuilder from './ExpressionBuilder'; + +describe('ExpressionBuilder', () => { + test('renders', () => { + const component = ( + {}}> + + + ); + expect(render(component)).toMatchSnapshot(); + }); +}); diff --git a/public/pages/CreateTrigger/components/CompositeTriggerCondition/ExpressionEditor.test.js b/public/pages/CreateTrigger/components/CompositeTriggerCondition/ExpressionEditor.test.js new file mode 100644 index 000000000..ef0bf0b0d --- /dev/null +++ b/public/pages/CreateTrigger/components/CompositeTriggerCondition/ExpressionEditor.test.js @@ -0,0 +1,27 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { render } from 'enzyme'; + +import { Formik } from 'formik'; +import { FORMIK_INITIAL_VALUES } from '../../../CreateMonitor/containers/CreateMonitor/utils/constants'; +import ExpressionEditor from './ExpressionEditor'; + +describe('ExpressionEditor', () => { + test('renders', () => { + const component = ( + {}}> + + + ); + expect(render(component)).toMatchSnapshot(); + }); +}); diff --git a/public/pages/CreateTrigger/components/CompositeTriggerCondition/ExpressionQuery.test.ts b/public/pages/CreateTrigger/components/CompositeTriggerCondition/ExpressionQuery.test.ts deleted file mode 100644 index 49e1b709c..000000000 --- a/public/pages/CreateTrigger/components/CompositeTriggerCondition/ExpressionQuery.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from 'react'; -import { render } from '@testing-library/react'; - -describe(' spec', () => { - it('renders the component', () => { - const tree = render(); - expect(tree).toMatchSnapshot(); - }); -}); diff --git a/public/pages/CreateTrigger/components/CompositeTriggerCondition/__snapshots__/CompositeTriggerCondition.test.js.snap b/public/pages/CreateTrigger/components/CompositeTriggerCondition/__snapshots__/CompositeTriggerCondition.test.js.snap new file mode 100644 index 000000000..1ab84caab --- /dev/null +++ b/public/pages/CreateTrigger/components/CompositeTriggerCondition/__snapshots__/CompositeTriggerCondition.test.js.snap @@ -0,0 +1,79 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CompositeTriggerCondition renders 1`] = ` +
+
+ +
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+`; diff --git a/public/pages/CreateTrigger/components/CompositeTriggerCondition/__snapshots__/ExpressionBuilder.test.js.snap b/public/pages/CreateTrigger/components/CompositeTriggerCondition/__snapshots__/ExpressionBuilder.test.js.snap new file mode 100644 index 000000000..ebb2d47f4 --- /dev/null +++ b/public/pages/CreateTrigger/components/CompositeTriggerCondition/__snapshots__/ExpressionBuilder.test.js.snap @@ -0,0 +1,48 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ExpressionBuilder renders 1`] = ` +
+
+
+
+
+
+ +
+
+
+
+
+
+`; diff --git a/public/pages/CreateTrigger/components/CompositeTriggerCondition/__snapshots__/ExpressionEditor.test.js.snap b/public/pages/CreateTrigger/components/CompositeTriggerCondition/__snapshots__/ExpressionEditor.test.js.snap new file mode 100644 index 000000000..0f1e38a2f --- /dev/null +++ b/public/pages/CreateTrigger/components/CompositeTriggerCondition/__snapshots__/ExpressionEditor.test.js.snap @@ -0,0 +1,49 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ExpressionEditor renders 1`] = ` +
+
+ +
+
+
+ +
+
+
+
+`; diff --git a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/CompositeMonitorsAlertTrigger.test.js b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/CompositeMonitorsAlertTrigger.test.js new file mode 100644 index 000000000..9e5a81334 --- /dev/null +++ b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/CompositeMonitorsAlertTrigger.test.js @@ -0,0 +1,30 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { render } from 'enzyme'; + +import { Formik } from 'formik'; +import { FORMIK_INITIAL_VALUES } from '../../../CreateMonitor/containers/CreateMonitor/utils/constants'; +import CompositeMonitorsAlertTrigger from './CompositeMonitorsAlertTrigger'; + +describe('CompositeMonitorsAlertTrigger', () => { + test('renders', () => { + const component = ( + {}}> + + + ); + expect(render(component)).toMatchSnapshot(); + }); +}); diff --git a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/DefineCompositeLevelTrigger.test.js b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/DefineCompositeLevelTrigger.test.js new file mode 100644 index 000000000..242cb7261 --- /dev/null +++ b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/DefineCompositeLevelTrigger.test.js @@ -0,0 +1,30 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { render } from 'enzyme'; + +import { Formik } from 'formik'; +import { FORMIK_INITIAL_VALUES } from '../../../CreateMonitor/containers/CreateMonitor/utils/constants'; +import DefineCompositeLevelTrigger from './DefineCompositeLevelTrigger'; + +describe('DefineCompositeLevelTrigger', () => { + test('renders', () => { + const component = ( + {}}> + + + ); + expect(render(component)).toMatchSnapshot(); + }); +}); diff --git a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/NotificationConfigDialog.test.js b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/NotificationConfigDialog.test.js new file mode 100644 index 000000000..49fe11d6f --- /dev/null +++ b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/NotificationConfigDialog.test.js @@ -0,0 +1,28 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { render } from 'enzyme'; + +import { Formik } from 'formik'; +import { FORMIK_INITIAL_VALUES } from '../../../CreateMonitor/containers/CreateMonitor/utils/constants'; +import NotificationConfigDialog from './NotificationConfigDialog'; + +describe('NotificationConfigDialog', () => { + test('renders', () => { + const component = ( + {}}> + {}} + triggerValues={FORMIK_INITIAL_VALUES} + httpClient={{}} + notifications={{}} + actionIndex={0} + /> + + ); + expect(render(component)).toMatchSnapshot(); + }); +}); diff --git a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotifications.test.js b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotifications.test.js new file mode 100644 index 000000000..d06015c51 --- /dev/null +++ b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotifications.test.js @@ -0,0 +1,29 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { render } from 'enzyme'; + +import { Formik } from 'formik'; +import { FORMIK_INITIAL_VALUES } from '../../../CreateMonitor/containers/CreateMonitor/utils/constants'; +import TriggerNotifications from './TriggerNotifications'; + +describe('TriggerNotifications', () => { + test('renders', () => { + const component = ( + {}}> + + + ); + expect(render(component)).toMatchSnapshot(); + }); +}); diff --git a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotificationsContent.test.js b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotificationsContent.test.js new file mode 100644 index 000000000..fb50e538e --- /dev/null +++ b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/TriggerNotificationsContent.test.js @@ -0,0 +1,47 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { render } from 'enzyme'; + +import { Formik } from 'formik'; +import { FORMIK_INITIAL_VALUES } from '../../../CreateMonitor/containers/CreateMonitor/utils/constants'; +import TriggerNotifications from './TriggerNotifications'; +import TriggerNotificationsContent from './TriggerNotificationsContent'; + +describe('TriggerNotificationsContent', () => { + test('renders without notifications', () => { + const component = ( + {}}> + + + ); + expect(render(component)).toMatchSnapshot(); + }); + test('renders with notifications', () => { + const component = ( + {}}> + + + ); + expect(render(component)).toMatchSnapshot(); + }); +}); diff --git a/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/__snapshots__/CompositeMonitorsAlertTrigger.test.js.snap b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/__snapshots__/CompositeMonitorsAlertTrigger.test.js.snap new file mode 100644 index 000000000..5b1f3ba13 --- /dev/null +++ b/public/pages/CreateTrigger/containers/DefineCompositeLevelTrigger/__snapshots__/CompositeMonitorsAlertTrigger.test.js.snap @@ -0,0 +1,308 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CompositeMonitorsAlertTrigger renders 1`] = ` +
+
+
+

+ Alert trigger +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+