diff --git a/public/components/Flyout/flyouts/components/AlertsDashboardFlyoutComponent.js b/public/components/Flyout/flyouts/components/AlertsDashboardFlyoutComponent.js index cc695f837..bafddddac 100644 --- a/public/components/Flyout/flyouts/components/AlertsDashboardFlyoutComponent.js +++ b/public/components/Flyout/flyouts/components/AlertsDashboardFlyoutComponent.js @@ -148,7 +148,7 @@ export default class AlertsDashboardFlyoutComponent extends Component { this.setState({ tabContent: this.renderAlertsTable() }); } - getBucketLevelGraphConditions = (trigger) => { + getMultipleGraphConditions = (trigger) => { let conditions = _.get(trigger, 'condition.script.source'); if (_.isEmpty(conditions)) { return '-'; @@ -514,11 +514,21 @@ export default class AlertsDashboardFlyoutComponent extends Component { const groupBy = _.get(monitor, MONITOR_GROUP_BY); const condition = - (searchType === SEARCH_TYPE.GRAPH && monitorType === MONITOR_TYPE.BUCKET_LEVEL) || - MONITOR_TYPE.DOC_LEVEL - ? this.getBucketLevelGraphConditions(trigger) + searchType === SEARCH_TYPE.GRAPH && + (monitorType === MONITOR_TYPE.BUCKET_LEVEL || monitorType === MONITOR_TYPE.DOC_LEVEL) + ? this.getMultipleGraphConditions(trigger) : _.get(trigger, 'condition.script.source', '-'); + let displayMultipleConditions; + switch (monitorType) { + case MONITOR_TYPE.BUCKET_LEVEL: + case MONITOR_TYPE.DOC_LEVEL: + displayMultipleConditions = true; + break; + default: + displayMultipleConditions = false; + } + const filters = monitorType === MONITOR_TYPE.BUCKET_LEVEL && searchType === SEARCH_TYPE.GRAPH ? this.getBucketLevelGraphFilter(trigger) @@ -534,7 +544,15 @@ export default class AlertsDashboardFlyoutComponent extends Component { ? `${bucketValue} ${bucketUnitOfTime}` : '-'; - const displayTableTabs = monitorType === MONITOR_TYPE.DOC_LEVEL; + let displayTableTabs; + switch (monitorType) { + case MONITOR_TYPE.DOC_LEVEL: + displayTableTabs = true; + break; + default: + displayTableTabs = false; + break; + } return (
@@ -590,9 +608,7 @@ export default class AlertsDashboardFlyoutComponent extends Component { - - {monitorType === MONITOR_TYPE.BUCKET_LEVEL ? 'Conditions' : 'Condition'} - + {displayMultipleConditions ? 'Conditions' : 'Condition'}

{loadingMonitors || loading ? 'Loading conditions...' : condition}

diff --git a/public/pages/CreateMonitor/components/DocumentLevelMonitorQueries/ConfigureDocumentLevelQueries.js b/public/pages/CreateMonitor/components/DocumentLevelMonitorQueries/ConfigureDocumentLevelQueries.js index 499b42fe5..c4965de37 100644 --- a/public/pages/CreateMonitor/components/DocumentLevelMonitorQueries/ConfigureDocumentLevelQueries.js +++ b/public/pages/CreateMonitor/components/DocumentLevelMonitorQueries/ConfigureDocumentLevelQueries.js @@ -8,7 +8,8 @@ import _ from 'lodash'; import { connect, FieldArray } from 'formik'; import { EuiButton, EuiSpacer } from '@elastic/eui'; import { inputLimitText } from '../../../../utils/helpers'; -import DocumentLevelQuery, { getInitialQueryValues } from './DocumentLevelQuery'; +import DocumentLevelQuery from './DocumentLevelQuery'; +import { FORMIK_INITIAL_DOCUMENT_LEVEL_QUERY_VALUES } from '../../containers/CreateMonitor/utils/constants'; export const MAX_QUERIES = 10; // TODO DRAFT: Placeholder limit @@ -23,7 +24,8 @@ class ConfigureDocumentLevelQueries extends Component { dataTypes, formik: { values }, } = this.props; - if (_.isEmpty(values.queries)) arrayHelpers.push(_.cloneDeep(getInitialQueryValues())); + if (_.isEmpty(values.queries)) + arrayHelpers.push(_.cloneDeep(FORMIK_INITIAL_DOCUMENT_LEVEL_QUERY_VALUES)); const numOfQueries = values.queries.length; return (
@@ -44,7 +46,9 @@ class ConfigureDocumentLevelQueries extends Component { arrayHelpers.push(_.cloneDeep(getInitialQueryValues(numOfQueries)))} + onClick={() => + arrayHelpers.push(_.cloneDeep(FORMIK_INITIAL_DOCUMENT_LEVEL_QUERY_VALUES)) + } disabled={numOfQueries >= MAX_QUERIES} > {numOfQueries === 0 ? 'Add query' : 'Add another query'} diff --git a/public/pages/CreateMonitor/components/DocumentLevelMonitorQueries/ConfigureDocumentLevelQueryTags.js b/public/pages/CreateMonitor/components/DocumentLevelMonitorQueries/ConfigureDocumentLevelQueryTags.js index ddca82962..176883f73 100644 --- a/public/pages/CreateMonitor/components/DocumentLevelMonitorQueries/ConfigureDocumentLevelQueryTags.js +++ b/public/pages/CreateMonitor/components/DocumentLevelMonitorQueries/ConfigureDocumentLevelQueryTags.js @@ -5,9 +5,13 @@ import React, { Component } from 'react'; import { connect, FieldArray } from 'formik'; -import { EuiButtonEmpty } from '@elastic/eui'; +import { EuiButtonEmpty, EuiSpacer, EuiText } from '@elastic/eui'; import { inputLimitText } from '../../../../utils/helpers'; -import DocumentLevelQueryTag from './DocumentLevelQueryTag'; +import DocumentLevelQueryTag, { DOC_LEVEL_TAG_TOOLTIP } from './DocumentLevelQueryTag'; +import IconToolTip from '../../../../components/IconToolTip'; +import _ from 'lodash'; +import { FormikFormRow } from '../../../../components/FormControls'; +import { hasError, isInvalid } from '../../../../utils/validate'; export const MAX_TAGS = 10; // TODO DRAFT: Placeholder limit @@ -19,37 +23,59 @@ class ConfigureDocumentLevelQueryTags extends Component { renderTags(arrayHelpers) { const { - formik: { values }, + formik: { errors, values }, formFieldName = '', query, queryIndex, } = this.props; const numOfTags = query.tags.length; + const tagsErrors = _.get(errors, `${formFieldName}.tags`, []); + const containsErrors = !_.isEmpty(tagsErrors); return (
- {values.queries[queryIndex].tags.map((tag, index) => { - return ( - - - - ); - })} -
- arrayHelpers.push('')} - disabled={numOfTags >= MAX_TAGS} - style={{ paddingTop: '5px' }} - > - + Add tag - - {inputLimitText(numOfTags, MAX_TAGS, 'tag', 'tags')} -
+ + Tags + - optional + + + + + {_.isEmpty(query.tags) && ( +
+ No tags defined. +
+ )} + + +
+ {values.queries[queryIndex].tags.map((tag, index) => { + return ( + + + + ); + })} +
+
+ + arrayHelpers.push('')} + disabled={numOfTags >= MAX_TAGS} + style={{ paddingTop: '5px' }} + > + + Add tag + + {inputLimitText(numOfTags, MAX_TAGS, 'tag', 'tags')}
); } @@ -57,7 +83,7 @@ class ConfigureDocumentLevelQueryTags extends Component { render() { const { formFieldName } = this.props; return ( - + {(arrayHelpers) => this.renderTags(arrayHelpers)} ); diff --git a/public/pages/CreateMonitor/components/DocumentLevelMonitorQueries/DocumentLevelQuery.js b/public/pages/CreateMonitor/components/DocumentLevelMonitorQueries/DocumentLevelQuery.js index 0bed64e16..2b9eeb434 100644 --- a/public/pages/CreateMonitor/components/DocumentLevelMonitorQueries/DocumentLevelQuery.js +++ b/public/pages/CreateMonitor/components/DocumentLevelMonitorQueries/DocumentLevelQuery.js @@ -4,32 +4,23 @@ */ import React, { Component } from 'react'; -import _ from 'lodash'; import { connect } from 'formik'; -import { - EuiButton, - EuiFlexGroup, - EuiFlexItem, - EuiHorizontalRule, - EuiSpacer, - EuiText, -} from '@elastic/eui'; +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; import { FormikComboBox, FormikFieldText, FormikSelect } from '../../../../components/FormControls'; -import { hasError, isInvalid, required } from '../../../../utils/validate'; -import { FORMIK_INITIAL_DOCUMENT_LEVEL_QUERY_VALUES } from '../../containers/CreateMonitor/utils/constants'; -import { DOC_LEVEL_TAG_TOOLTIP } from './DocumentLevelQueryTag'; -import IconToolTip from '../../../../components/IconToolTip'; +import { + hasError, + isInvalid, + required, + validateIllegalCharacters, +} from '../../../../utils/validate'; import ConfigureDocumentLevelQueryTags from './ConfigureDocumentLevelQueryTags'; import { getIndexFields } from '../MonitorExpressions/expressions/utils/dataTypes'; import { QUERY_OPERATORS } from '../../../Dashboard/components/FindingsDashboard/utils'; const ALLOWED_DATA_TYPES = ['number', 'text', 'keyword', 'boolean']; -export const getInitialQueryValues = (queryIndexNum = 0) => - _.cloneDeep({ - ...FORMIK_INITIAL_DOCUMENT_LEVEL_QUERY_VALUES, - queryName: `Query ${queryIndexNum + 1}`, - }); +// TODO DRAFT: implement validation +export const ILLEGAL_QUERY_NAME_CHARACTERS = [' ']; class DocumentLevelQuery extends Component { constructor(props) { @@ -46,14 +37,19 @@ class DocumentLevelQuery extends Component { @@ -124,19 +120,6 @@ class DocumentLevelQuery extends Component { - - Tags - - optional - - - - - {_.isEmpty(query.tags) && ( -
- No tags defined. -
- )} - { const MonitorDefinitionCard = ({ values, plugins }) => { const hasADPlugin = plugins.indexOf(OS_AD_PLUGIN) !== -1; - const isBucketLevelMonitor = values.monitor_type === MONITOR_TYPE.BUCKET_LEVEL; - + let supportsADOption; + switch (values.monitor_type) { + case MONITOR_TYPE.QUERY_LEVEL: + supportsADOption = true; + break; + default: + supportsADOption = false; + } return (
@@ -68,8 +74,8 @@ const MonitorDefinitionCard = ({ values, plugins }) => { }} /> - {/*// Only show the anomaly detector option when anomaly detection plugin is present, but not for bucket-level monitors.*/} - {hasADPlugin && !isBucketLevelMonitor && ( + {/*// Only show the anomaly detector option when anomaly detection plugin is present, and for supporting monitors.*/} + {hasADPlugin && supportsADOption && (
-
-
-
-
-
- -
-
-
- -
-
-
`; diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/CreateMonitor.js b/public/pages/CreateMonitor/containers/CreateMonitor/CreateMonitor.js index 4741501a3..36dbe7468 100644 --- a/public/pages/CreateMonitor/containers/CreateMonitor/CreateMonitor.js +++ b/public/pages/CreateMonitor/containers/CreateMonitor/CreateMonitor.js @@ -170,13 +170,15 @@ export default class CreateMonitor extends Component { let triggerType; switch (monitor_type) { - case MONITOR_TYPE.QUERY_LEVEL: - case MONITOR_TYPE.CLUSTER_METRICS: - triggerType = TRIGGER_TYPE.QUERY_LEVEL; - break; case MONITOR_TYPE.BUCKET_LEVEL: triggerType = TRIGGER_TYPE.BUCKET_LEVEL; break; + case MONITOR_TYPE.DOC_LEVEL: + triggerType = TRIGGER_TYPE.DOC_LEVEL; + break; + default: + triggerType = TRIGGER_TYPE.QUERY_LEVEL; + break; } if (_.isArray(triggerToEdit)) { diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js b/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js index 52acb4ff2..1197e06c9 100644 --- a/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js +++ b/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js @@ -64,7 +64,7 @@ export const FORMIK_INITIAL_AGG_VALUES = { export const FORMIK_INITIAL_DOCUMENT_LEVEL_QUERY_VALUES = { id: undefined, - queryName: 'Query name', + queryName: '', field: '', operator: QUERY_OPERATORS[0].value, query: '', diff --git a/public/pages/CreateTrigger/components/Action/__snapshots__/Action.test.js.snap b/public/pages/CreateTrigger/components/Action/__snapshots__/Action.test.js.snap index fd01e21ee..e5ec21cde 100644 --- a/public/pages/CreateTrigger/components/Action/__snapshots__/Action.test.js.snap +++ b/public/pages/CreateTrigger/components/Action/__snapshots__/Action.test.js.snap @@ -389,19 +389,24 @@ exports[`Action renders with Notifications plugin installed 1`] = `
-
+
- - Perform action - -
- Per monitor execution +
+ + Perform action + +
+ Per monitor execution +
+
-
+
- - Perform action - -
- Per monitor execution +
+ + Perform action + +
+ Per monitor execution +
+
) : ( - +
+ + +
)} {actionExecutionScopeId !== NOTIFY_OPTIONS_VALUES.PER_EXECUTION ? ( diff --git a/public/pages/CreateTrigger/components/Action/actions/__snapshots__/Message.test.js.snap b/public/pages/CreateTrigger/components/Action/actions/__snapshots__/Message.test.js.snap index 1fb13b85c..2f7ddbbc7 100644 --- a/public/pages/CreateTrigger/components/Action/actions/__snapshots__/Message.test.js.snap +++ b/public/pages/CreateTrigger/components/Action/actions/__snapshots__/Message.test.js.snap @@ -165,19 +165,24 @@ exports[`Message renders 1`] = `
-
+
- - Perform action - -
- Per monitor execution +
+ + Perform action + +
+ Per monitor execution +
+
{ + const triggerConditions = _.get(triggerValues, `${fieldPath}triggerConditions`, []); + const selectedQueriesAndTags = triggerConditions.map((condition) => + _.get(condition, 'query.queryName') + ); const queries = _.get(monitorValues, 'queries', []); + const tagSelectOptions = []; const querySelectOptions = queries.map((query) => { query.tags.forEach((tag) => { const tagOption = { label: tag, value: { queryName: tag, operator: '==', expression: `${QUERY_IDENTIFIERS.TAG}${tag}` }, + disabled: _.includes(selectedQueriesAndTags, tag), }; if (!_.includes(tagSelectOptions, tagOption)) tagSelectOptions.push(tagOption); }); return { label: query.queryName, value: { ...query, expression: `${QUERY_IDENTIFIERS.NAME}${query.queryName}` }, + disabled: _.includes(selectedQueriesAndTags, query.queryName), }; }); - const triggerConditions = _.get(triggerValues, `${fieldPath}triggerConditions`, []); - if (_.isEmpty(triggerConditions)) { + if (_.isEmpty(triggerConditions)) arrayHelpers.push(_.cloneDeep(FORMIK_INITIAL_TRIGGER_CONDITION_VALUES)); - } return triggerConditions.map((triggerCondition, index) => (
diff --git a/public/pages/Dashboard/components/AcknowledgeAlertsModal/AcknowledgeAlertsModal.js b/public/pages/Dashboard/components/AcknowledgeAlertsModal/AcknowledgeAlertsModal.js index 03ce1c9a6..24fc10261 100644 --- a/public/pages/Dashboard/components/AcknowledgeAlertsModal/AcknowledgeAlertsModal.js +++ b/public/pages/Dashboard/components/AcknowledgeAlertsModal/AcknowledgeAlertsModal.js @@ -177,6 +177,7 @@ export default class AcknowledgeAlertsModal extends Component { }; acknowledgeAlerts = async () => { + this.setState({ loading: true }); const { selectedItems } = this.state; const { httpClient, notifications } = this.props; @@ -227,7 +228,7 @@ export default class AcknowledgeAlertsModal extends Component { alertState, monitorIds ); - this.setState({ ...this.state, selectedItems: [] }); + this.setState({ ...this.state, loading: false, selectedItems: [] }); }; onSeverityLevelChange = (e) => { @@ -305,6 +306,7 @@ export default class AcknowledgeAlertsModal extends Component { switch (monitorType) { case MONITOR_TYPE.QUERY_LEVEL: case MONITOR_TYPE.CLUSTER_METRICS: + case MONITOR_TYPE.DOC_LEVEL: return `${item.id}-${item.version}`; case MONITOR_TYPE.BUCKET_LEVEL: return item.id; diff --git a/public/pages/Dashboard/containers/Dashboard.js b/public/pages/Dashboard/containers/Dashboard.js index c85b20093..3c2202fa5 100644 --- a/public/pages/Dashboard/containers/Dashboard.js +++ b/public/pages/Dashboard/containers/Dashboard.js @@ -18,6 +18,8 @@ import { } from '../../../utils/constants'; import { backendErrorNotification } from '../../../utils/helpers'; import { + displayAcknowledgedAlertsToast, + filterActiveAlerts, getInitialSize, getQueryObjectFromState, getURLQueryParams, @@ -190,6 +192,63 @@ export default class Dashboard extends Component { this.setState({ ...this.state, loadingMonitors: false, monitors: monitors }); } + // TODO: exists in both Dashboard and Monitors, should be moved to redux when implemented + acknowledgeAlert = async () => { + const { selectedItems } = this.state; + const { httpClient, notifications, perAlertView } = this.props; + + if (!selectedItems.length) return; + + let selectedAlerts = perAlertView ? selectedItems : _.get(selectedItems, '0.alerts', []); + selectedAlerts = filterActiveAlerts(selectedAlerts); + + const monitorAlerts = selectedAlerts.reduce((monitorAlerts, alert) => { + const { id, monitor_id: monitorId } = alert; + if (monitorAlerts[monitorId]) monitorAlerts[monitorId].push(id); + else monitorAlerts[monitorId] = [id]; + return monitorAlerts; + }, {}); + + Object.entries(monitorAlerts).map(([monitorId, alerts]) => + httpClient + .post(`../api/alerting/monitors/${monitorId}/_acknowledge/alerts`, { + body: JSON.stringify({ alerts }), + }) + .then((resp) => { + if (!resp.ok) { + backendErrorNotification(notifications, 'acknowledge', 'alert', resp.resp); + } else { + const successfulCount = _.get(resp, 'resp.success', []).length; + displayAcknowledgedAlertsToast(notifications, successfulCount); + } + }) + .catch((error) => error) + ); + + this.setState({ selectedItems: [] }); + const { + page, + size, + search, + sortField, + sortDirection, + severityLevel, + alertState, + monitorIds, + } = this.state; + this.getAlerts( + page * size, + size, + search, + sortField, + sortDirection, + severityLevel, + alertState, + monitorIds + ); + this.refreshDashboard(); + }; + onTableChange = ({ page: tablePage = {}, sort = {} }) => { const { index: page, size } = tablePage; const { field: sortField, direction: sortDirection } = sort; @@ -373,7 +432,7 @@ export default class Dashboard extends Component { // The acknowledge button is disabled when viewing by per alerts, and no item selected or per trigger view and item selected is not 1. const actions = [ diff --git a/public/pages/MonitorDetails/containers/MonitorDetails.js b/public/pages/MonitorDetails/containers/MonitorDetails.js index c999df5f7..dc9c897c7 100644 --- a/public/pages/MonitorDetails/containers/MonitorDetails.js +++ b/public/pages/MonitorDetails/containers/MonitorDetails.js @@ -158,10 +158,18 @@ export default class MonitorDetails extends Component { notifications, } = this.props; const { monitor, ifSeqNo, ifPrimaryTerm } = this.state; + + let query = { ifSeqNo, ifPrimaryTerm }; + switch (monitor.monitor_type) { + case MONITOR_TYPE.DOC_LEVEL: + query = {}; + break; + } + this.setState({ updating: true }); return httpClient .put(`../api/alerting/monitors/${monitorId}`, { - query: { ifSeqNo, ifPrimaryTerm }, + query: { ...query }, body: JSON.stringify({ ...monitor, ...update }), }) .then((resp) => { diff --git a/public/utils/validate.js b/public/utils/validate.js index 6b03a0d09..6e7ee63fa 100644 --- a/public/utils/validate.js +++ b/public/utils/validate.js @@ -31,6 +31,9 @@ export const validateActionName = (monitor, trigger) => (value) => { case MONITOR_TYPE.BUCKET_LEVEL: actions = _.get(trigger, `${TRIGGER_TYPE.BUCKET_LEVEL}.actions`, []); break; + case MONITOR_TYPE.DOC_LEVEL: + actions = _.get(trigger, `${TRIGGER_TYPE.DOC_LEVEL}.actions`, []); + break; } const matches = actions.filter((action) => action.name === value); if (matches.length > 1) return 'Action name is already used.'; @@ -56,6 +59,26 @@ export const required = (value) => { if (!value) return 'Required.'; }; +export const validateIllegalCharacters = (illegalCharacters = ILLEGAL_CHARACTERS) => (value) => { + if (_.isEmpty(value)) return required(value); + + const illegalCharactersString = illegalCharacters.join(' '); + let errorText = `Contains invalid characters. Cannot contain: ${illegalCharactersString}`; + + if (_.includes(illegalCharacters, ' ')) { + errorText = + illegalCharacters.length === 1 + ? 'Cannot contain spaces.' + : `Contains invalid characters or spaces. Cannot contain: ${illegalCharactersString}`; + } + + let includesIllegalCharacter = false; + illegalCharacters.forEach((character) => { + if (_.includes(value, character)) includesIllegalCharacter = true; + }); + if (includesIllegalCharacter) return errorText; +}; + export const validateRequiredNumber = (value) => { if (value === undefined || typeof value === 'string') return 'Provide a value.'; }; diff --git a/server/clusters/alerting/alertingPlugin.js b/server/clusters/alerting/alertingPlugin.js index 6a056fed4..8564d0020 100644 --- a/server/clusters/alerting/alertingPlugin.js +++ b/server/clusters/alerting/alertingPlugin.js @@ -59,22 +59,15 @@ export default function alertingPlugin(Client, config, components) { method: 'DELETE', }); + // TODO DRAFT: May need to add 'refresh' assignment here again. alerting.updateMonitor = ca({ url: { - fmt: `${MONITOR_BASE_API}/<%=monitorId%>?if_seq_no=<%=ifSeqNo%>&if_primary_term=<%=ifPrimaryTerm%>&refresh=wait_for`, + fmt: `${MONITOR_BASE_API}/<%=monitorId%>`, req: { monitorId: { type: 'string', required: true, }, - ifSeqNo: { - type: 'string', - required: true, - }, - ifPrimaryTerm: { - type: 'string', - required: true, - }, }, }, needBody: true, diff --git a/server/routes/monitors.js b/server/routes/monitors.js index 520c95c73..6cc4967c0 100644 --- a/server/routes/monitors.js +++ b/server/routes/monitors.js @@ -78,8 +78,8 @@ export default function (services, router) { id: schema.string(), }), query: schema.object({ - ifSeqNo: schema.number(), - ifPrimaryTerm: schema.number(), + ifSeqNo: schema.maybe(schema.number()), + ifPrimaryTerm: schema.maybe(schema.number()), }), body: schema.any(), }, diff --git a/server/services/MonitorService.js b/server/services/MonitorService.js index 6c0646e12..81d788520 100644 --- a/server/services/MonitorService.js +++ b/server/services/MonitorService.js @@ -127,8 +127,15 @@ export default class MonitorService { updateMonitor = async (context, req, res) => { try { const { id } = req.params; + const params = { monitorId: id, body: req.body, refresh: 'wait_for' }; + + // TODO DRAFT: Are we sure we need to include ifSeqNo and ifPrimaryTerm from the UI side when updating monitors? const { ifSeqNo, ifPrimaryTerm } = req.query; - const params = { monitorId: id, ifSeqNo, ifPrimaryTerm, body: req.body }; + if (ifSeqNo && ifPrimaryTerm) { + params.if_seq_no = ifSeqNo; + params.if_primary_term = ifPrimaryTerm; + } + const { callAsCurrentUser } = await this.esDriver.asScoped(req); const updateResponse = await callAsCurrentUser('alerting.updateMonitor', params); const { _version, _id } = updateResponse;