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"