diff --git a/x-pack/plugins/alerts/common/alert_instance_summary.ts b/x-pack/plugins/alerts/common/alert_instance_summary.ts index 08c3b2fc2c241..1aa183a141eab 100644 --- a/x-pack/plugins/alerts/common/alert_instance_summary.ts +++ b/x-pack/plugins/alerts/common/alert_instance_summary.ts @@ -27,5 +27,6 @@ export interface AlertInstanceSummary { export interface AlertInstanceStatus { status: AlertInstanceStatusValues; muted: boolean; + actionGroupId?: string; activeStartDate?: string; } diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts index a53e49337f385..9cb2a33222d23 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts @@ -118,12 +118,12 @@ describe('getAlertInstanceSummary()', () => { .addExecute() .addNewInstance('instance-currently-active') .addNewInstance('instance-previously-active') - .addActiveInstance('instance-currently-active') - .addActiveInstance('instance-previously-active') + .addActiveInstance('instance-currently-active', 'action group A') + .addActiveInstance('instance-previously-active', 'action group B') .advanceTime(10000) .addExecute() .addResolvedInstance('instance-previously-active') - .addActiveInstance('instance-currently-active') + .addActiveInstance('instance-currently-active', 'action group A') .getEvents(); const eventsResult = { ...AlertInstanceSummaryFindEventsResult, @@ -144,16 +144,19 @@ describe('getAlertInstanceSummary()', () => { "id": "1", "instances": Object { "instance-currently-active": Object { + "actionGroupId": "action group A", "activeStartDate": "2019-02-12T21:01:22.479Z", "muted": false, "status": "Active", }, "instance-muted-no-activity": Object { + "actionGroupId": undefined, "activeStartDate": undefined, "muted": true, "status": "OK", }, "instance-previously-active": Object { + "actionGroupId": undefined, "activeStartDate": undefined, "muted": false, "status": "OK", diff --git a/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.test.ts b/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.test.ts index 566a1770c0658..f9e4a2908d6ce 100644 --- a/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.test.ts +++ b/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.test.ts @@ -104,11 +104,13 @@ describe('alertInstanceSummaryFromEventLog', () => { Object { "instances": Object { "instance-1": Object { + "actionGroupId": undefined, "activeStartDate": undefined, "muted": true, "status": "OK", }, "instance-2": Object { + "actionGroupId": undefined, "activeStartDate": undefined, "muted": true, "status": "OK", @@ -184,7 +186,7 @@ describe('alertInstanceSummaryFromEventLog', () => { const events = eventsFactory .addExecute() .addNewInstance('instance-1') - .addActiveInstance('instance-1') + .addActiveInstance('instance-1', 'action group A') .advanceTime(10000) .addExecute() .addResolvedInstance('instance-1') @@ -202,6 +204,7 @@ describe('alertInstanceSummaryFromEventLog', () => { Object { "instances": Object { "instance-1": Object { + "actionGroupId": undefined, "activeStartDate": undefined, "muted": false, "status": "OK", @@ -218,7 +221,7 @@ describe('alertInstanceSummaryFromEventLog', () => { const eventsFactory = new EventsFactory(); const events = eventsFactory .addExecute() - .addActiveInstance('instance-1') + .addActiveInstance('instance-1', 'action group A') .advanceTime(10000) .addExecute() .addResolvedInstance('instance-1') @@ -236,6 +239,7 @@ describe('alertInstanceSummaryFromEventLog', () => { Object { "instances": Object { "instance-1": Object { + "actionGroupId": undefined, "activeStartDate": undefined, "muted": false, "status": "OK", @@ -253,10 +257,10 @@ describe('alertInstanceSummaryFromEventLog', () => { const events = eventsFactory .addExecute() .addNewInstance('instance-1') - .addActiveInstance('instance-1') + .addActiveInstance('instance-1', 'action group A') .advanceTime(10000) .addExecute() - .addActiveInstance('instance-1') + .addActiveInstance('instance-1', 'action group A') .getEvents(); const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ @@ -271,6 +275,79 @@ describe('alertInstanceSummaryFromEventLog', () => { Object { "instances": Object { "instance-1": Object { + "actionGroupId": "action group A", + "activeStartDate": "2020-06-18T00:00:00.000Z", + "muted": false, + "status": "Active", + }, + }, + "lastRun": "2020-06-18T00:00:10.000Z", + "status": "Active", + } + `); + }); + + test('alert with currently active instance with no action group in event log', async () => { + const alert = createAlert({}); + const eventsFactory = new EventsFactory(); + const events = eventsFactory + .addExecute() + .addNewInstance('instance-1') + .addActiveInstance('instance-1', undefined) + .advanceTime(10000) + .addExecute() + .addActiveInstance('instance-1', undefined) + .getEvents(); + + const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ + alert, + events, + dateStart, + dateEnd, + }); + + const { lastRun, status, instances } = summary; + expect({ lastRun, status, instances }).toMatchInlineSnapshot(` + Object { + "instances": Object { + "instance-1": Object { + "actionGroupId": undefined, + "activeStartDate": "2020-06-18T00:00:00.000Z", + "muted": false, + "status": "Active", + }, + }, + "lastRun": "2020-06-18T00:00:10.000Z", + "status": "Active", + } + `); + }); + + test('alert with currently active instance that switched action groups', async () => { + const alert = createAlert({}); + const eventsFactory = new EventsFactory(); + const events = eventsFactory + .addExecute() + .addNewInstance('instance-1') + .addActiveInstance('instance-1', 'action group A') + .advanceTime(10000) + .addExecute() + .addActiveInstance('instance-1', 'action group B') + .getEvents(); + + const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ + alert, + events, + dateStart, + dateEnd, + }); + + const { lastRun, status, instances } = summary; + expect({ lastRun, status, instances }).toMatchInlineSnapshot(` + Object { + "instances": Object { + "instance-1": Object { + "actionGroupId": "action group B", "activeStartDate": "2020-06-18T00:00:00.000Z", "muted": false, "status": "Active", @@ -287,10 +364,10 @@ describe('alertInstanceSummaryFromEventLog', () => { const eventsFactory = new EventsFactory(); const events = eventsFactory .addExecute() - .addActiveInstance('instance-1') + .addActiveInstance('instance-1', 'action group A') .advanceTime(10000) .addExecute() - .addActiveInstance('instance-1') + .addActiveInstance('instance-1', 'action group A') .getEvents(); const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ @@ -305,6 +382,7 @@ describe('alertInstanceSummaryFromEventLog', () => { Object { "instances": Object { "instance-1": Object { + "actionGroupId": "action group A", "activeStartDate": undefined, "muted": false, "status": "Active", @@ -322,12 +400,12 @@ describe('alertInstanceSummaryFromEventLog', () => { const events = eventsFactory .addExecute() .addNewInstance('instance-1') - .addActiveInstance('instance-1') + .addActiveInstance('instance-1', 'action group A') .addNewInstance('instance-2') - .addActiveInstance('instance-2') + .addActiveInstance('instance-2', 'action group B') .advanceTime(10000) .addExecute() - .addActiveInstance('instance-1') + .addActiveInstance('instance-1', 'action group A') .addResolvedInstance('instance-2') .getEvents(); @@ -343,11 +421,13 @@ describe('alertInstanceSummaryFromEventLog', () => { Object { "instances": Object { "instance-1": Object { + "actionGroupId": "action group A", "activeStartDate": "2020-06-18T00:00:00.000Z", "muted": true, "status": "Active", }, "instance-2": Object { + "actionGroupId": undefined, "activeStartDate": undefined, "muted": true, "status": "OK", @@ -365,19 +445,19 @@ describe('alertInstanceSummaryFromEventLog', () => { const events = eventsFactory .addExecute() .addNewInstance('instance-1') - .addActiveInstance('instance-1') + .addActiveInstance('instance-1', 'action group A') .addNewInstance('instance-2') - .addActiveInstance('instance-2') + .addActiveInstance('instance-2', 'action group B') .advanceTime(10000) .addExecute() - .addActiveInstance('instance-1') + .addActiveInstance('instance-1', 'action group A') .addResolvedInstance('instance-2') .advanceTime(10000) .addExecute() - .addActiveInstance('instance-1') + .addActiveInstance('instance-1', 'action group B') .advanceTime(10000) .addExecute() - .addActiveInstance('instance-1') + .addActiveInstance('instance-1', 'action group B') .getEvents(); const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ @@ -392,11 +472,13 @@ describe('alertInstanceSummaryFromEventLog', () => { Object { "instances": Object { "instance-1": Object { + "actionGroupId": "action group B", "activeStartDate": "2020-06-18T00:00:00.000Z", "muted": false, "status": "Active", }, "instance-2": Object { + "actionGroupId": undefined, "activeStartDate": undefined, "muted": false, "status": "OK", @@ -452,14 +534,17 @@ export class EventsFactory { return this; } - addActiveInstance(instanceId: string): EventsFactory { + addActiveInstance(instanceId: string, actionGroupId: string | undefined): EventsFactory { + const kibanaAlerting = actionGroupId + ? { instance_id: instanceId, action_group_id: actionGroupId } + : { instance_id: instanceId }; this.events.push({ '@timestamp': this.date, event: { provider: EVENT_LOG_PROVIDER, action: EVENT_LOG_ACTIONS.activeInstance, }, - kibana: { alerting: { instance_id: instanceId } }, + kibana: { alerting: kibanaAlerting }, }); return this; } diff --git a/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.ts b/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.ts index 9a5e870c8199a..8fed97a74435d 100644 --- a/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.ts +++ b/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.ts @@ -78,10 +78,12 @@ export function alertInstanceSummaryFromEventLog( // intentionally no break here case EVENT_LOG_ACTIONS.activeInstance: status.status = 'Active'; + status.actionGroupId = event?.kibana?.alerting?.action_group_id; break; case EVENT_LOG_ACTIONS.resolvedInstance: status.status = 'OK'; status.activeStartDate = undefined; + status.actionGroupId = undefined; } } @@ -118,6 +120,7 @@ function getAlertInstanceStatus( const status: AlertInstanceStatus = { status: 'OK', muted: false, + actionGroupId: undefined, activeStartDate: undefined, }; instances.set(instanceId, status); diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts index 86e78dea66a09..4d0d69010914e 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts @@ -292,6 +292,7 @@ describe('Task Runner', () => { kibana: { alerting: { instance_id: '1', + action_group_id: 'default', }, saved_objects: [ { @@ -302,7 +303,7 @@ describe('Task Runner', () => { }, ], }, - message: "test:1: 'alert-name' active instance: '1'", + message: "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", }); expect(eventLogger.logEvent).toHaveBeenCalledWith({ event: { @@ -424,6 +425,7 @@ describe('Task Runner', () => { }, "kibana": Object { "alerting": Object { + "action_group_id": undefined, "instance_id": "1", }, "saved_objects": Array [ @@ -445,6 +447,7 @@ describe('Task Runner', () => { }, "kibana": Object { "alerting": Object { + "action_group_id": "default", "instance_id": "1", }, "saved_objects": Array [ @@ -456,7 +459,7 @@ describe('Task Runner', () => { }, ], }, - "message": "test:1: 'alert-name' active instance: '1'", + "message": "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", }, ], Array [ @@ -565,6 +568,7 @@ describe('Task Runner', () => { }, "kibana": Object { "alerting": Object { + "action_group_id": undefined, "instance_id": "2", }, "saved_objects": Array [ @@ -586,6 +590,7 @@ describe('Task Runner', () => { }, "kibana": Object { "alerting": Object { + "action_group_id": "default", "instance_id": "1", }, "saved_objects": Array [ @@ -597,7 +602,7 @@ describe('Task Runner', () => { }, ], }, - "message": "test:1: 'alert-name' active instance: '1'", + "message": "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", }, ], ] diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.ts index 2611ba766173b..6a49f67268d69 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import type { PublicMethodsOf } from '@kbn/utility-types'; -import { pickBy, mapValues, without } from 'lodash'; +import { Dictionary, pickBy, mapValues, without } from 'lodash'; import { Logger, KibanaRequest } from '../../../../../src/core/server'; import { TaskRunnerContext } from './task_runner_factory'; import { ConcreteTaskInstance, throwUnrecoverableError } from '../../../task_manager/server'; @@ -224,11 +224,10 @@ export class TaskRunner { const instancesWithScheduledActions = pickBy(alertInstances, (alertInstance: AlertInstance) => alertInstance.hasScheduledActions() ); - const currentAlertInstanceIds = Object.keys(instancesWithScheduledActions); generateNewAndResolvedInstanceEvents({ eventLogger, originalAlertInstanceIds, - currentAlertInstanceIds, + currentAlertInstances: instancesWithScheduledActions, alertId, alertLabel, namespace, @@ -382,7 +381,7 @@ export class TaskRunner { interface GenerateNewAndResolvedInstanceEventsParams { eventLogger: IEventLogger; originalAlertInstanceIds: string[]; - currentAlertInstanceIds: string[]; + currentAlertInstances: Dictionary; alertId: string; alertLabel: string; namespace: string | undefined; @@ -393,9 +392,10 @@ function generateNewAndResolvedInstanceEvents(params: GenerateNewAndResolvedInst eventLogger, alertId, namespace, - currentAlertInstanceIds, + currentAlertInstances, originalAlertInstanceIds, } = params; + const currentAlertInstanceIds = Object.keys(currentAlertInstances); const newIds = without(currentAlertInstanceIds, ...originalAlertInstanceIds); const resolvedIds = without(originalAlertInstanceIds, ...currentAlertInstanceIds); @@ -411,11 +411,12 @@ function generateNewAndResolvedInstanceEvents(params: GenerateNewAndResolvedInst } for (const id of currentAlertInstanceIds) { - const message = `${params.alertLabel} active instance: '${id}'`; - logInstanceEvent(id, EVENT_LOG_ACTIONS.activeInstance, message); + const actionGroup = currentAlertInstances[id].getScheduledActionOptions()?.actionGroup; + const message = `${params.alertLabel} active instance: '${id}' in actionGroup: '${actionGroup}'`; + logInstanceEvent(id, EVENT_LOG_ACTIONS.activeInstance, message, actionGroup); } - function logInstanceEvent(instanceId: string, action: string, message: string) { + function logInstanceEvent(instanceId: string, action: string, message: string, group?: string) { const event: IEvent = { event: { action, @@ -423,6 +424,7 @@ function generateNewAndResolvedInstanceEvents(params: GenerateNewAndResolvedInst kibana: { alerting: { instance_id: instanceId, + action_group_id: group, }, saved_objects: [ { diff --git a/x-pack/plugins/event_log/generated/mappings.json b/x-pack/plugins/event_log/generated/mappings.json index 0a858969c4f6a..5c7eb50117d9b 100644 --- a/x-pack/plugins/event_log/generated/mappings.json +++ b/x-pack/plugins/event_log/generated/mappings.json @@ -81,6 +81,10 @@ "instance_id": { "type": "keyword", "ignore_above": 1024 + }, + "action_group_id": { + "type": "keyword", + "ignore_above": 1024 } } }, diff --git a/x-pack/plugins/event_log/generated/schemas.ts b/x-pack/plugins/event_log/generated/schemas.ts index 57fe90a8e876e..3dbb43b15350f 100644 --- a/x-pack/plugins/event_log/generated/schemas.ts +++ b/x-pack/plugins/event_log/generated/schemas.ts @@ -60,6 +60,7 @@ export const EventSchema = schema.maybe( alerting: schema.maybe( schema.object({ instance_id: ecsString(), + action_group_id: ecsString(), }) ), saved_objects: schema.maybe( diff --git a/x-pack/plugins/event_log/scripts/mappings.js b/x-pack/plugins/event_log/scripts/mappings.js index fd149d132031e..c9af2b0aa57fb 100644 --- a/x-pack/plugins/event_log/scripts/mappings.js +++ b/x-pack/plugins/event_log/scripts/mappings.js @@ -18,6 +18,10 @@ exports.EcsKibanaExtensionsMappings = { type: 'keyword', ignore_above: 1024, }, + action_group_id: { + type: 'keyword', + ignore_above: 1024, + }, }, }, // array of saved object references, for "linking" via search @@ -63,6 +67,7 @@ exports.EcsEventLogProperties = [ 'user.name', 'kibana.server_uuid', 'kibana.alerting.instance_id', + 'kibana.alerting.action_group_id', 'kibana.saved_objects.rel', 'kibana.saved_objects.namespace', 'kibana.saved_objects.id', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx index 1272024557bb6..abd8127962561 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx @@ -301,6 +301,7 @@ export const AlertDetails: React.FunctionComponent = ({ ) : ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.test.tsx index e1287d299b6e9..25bbe977fd76a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.test.tsx @@ -7,7 +7,7 @@ import * as React from 'react'; import uuid from 'uuid'; import { shallow } from 'enzyme'; import { AlertInstances, AlertInstanceListItem, alertInstanceToListItem } from './alert_instances'; -import { Alert, AlertInstanceSummary, AlertInstanceStatus } from '../../../../types'; +import { Alert, AlertInstanceSummary, AlertInstanceStatus, AlertType } from '../../../../types'; import { EuiBasicTable } from '@elastic/eui'; const fakeNow = new Date('2020-02-09T23:15:41.941Z'); @@ -34,15 +34,18 @@ jest.mock('../../../app_context', () => { describe('alert_instances', () => { it('render a list of alert instances', () => { const alert = mockAlert(); + const alertType = mockAlertType(); const alertInstanceSummary = mockAlertInstanceSummary({ instances: { first_instance: { status: 'OK', muted: false, + actionGroupId: 'default', }, second_instance: { status: 'Active', muted: false, + actionGroupId: 'action group id unknown', }, }, }); @@ -51,14 +54,14 @@ describe('alert_instances', () => { // active first alertInstanceToListItem( fakeNow.getTime(), - alert, + alertType, 'second_instance', alertInstanceSummary.instances.second_instance ), // ok second alertInstanceToListItem( fakeNow.getTime(), - alert, + alertType, 'first_instance', alertInstanceSummary.instances.first_instance ), @@ -69,6 +72,7 @@ describe('alert_instances', () => { @@ -80,6 +84,7 @@ describe('alert_instances', () => { it('render a hidden field with duration epoch', () => { const alert = mockAlert(); + const alertType = mockAlertType(); const alertInstanceSummary = mockAlertInstanceSummary(); expect( @@ -88,6 +93,7 @@ describe('alert_instances', () => { durationEpoch={fake2MinutesAgo.getTime()} {...mockAPIs} alert={alert} + alertType={alertType} readOnly={false} alertInstanceSummary={alertInstanceSummary} /> @@ -99,6 +105,7 @@ describe('alert_instances', () => { it('render all active alert instances', () => { const alert = mockAlert(); + const alertType = mockAlertType(); const instances: Record = { ['us-central']: { status: 'OK', @@ -114,6 +121,7 @@ describe('alert_instances', () => { { .find(EuiBasicTable) .prop('items') ).toEqual([ - alertInstanceToListItem(fakeNow.getTime(), alert, 'us-central', instances['us-central']), - alertInstanceToListItem(fakeNow.getTime(), alert, 'us-east', instances['us-east']), + alertInstanceToListItem(fakeNow.getTime(), alertType, 'us-central', instances['us-central']), + alertInstanceToListItem(fakeNow.getTime(), alertType, 'us-east', instances['us-east']), ]); }); @@ -132,6 +140,7 @@ describe('alert_instances', () => { const alert = mockAlert({ mutedInstanceIds: ['us-west', 'us-east'], }); + const alertType = mockAlertType(); const instanceUsWest: AlertInstanceStatus = { status: 'OK', muted: false }; const instanceUsEast: AlertInstanceStatus = { status: 'OK', muted: false }; @@ -140,6 +149,7 @@ describe('alert_instances', () => { { .find(EuiBasicTable) .prop('items') ).toEqual([ - alertInstanceToListItem(fakeNow.getTime(), alert, 'us-west', instanceUsWest), - alertInstanceToListItem(fakeNow.getTime(), alert, 'us-east', instanceUsEast), + alertInstanceToListItem(fakeNow.getTime(), alertType, 'us-west', instanceUsWest), + alertInstanceToListItem(fakeNow.getTime(), alertType, 'us-east', instanceUsEast), ]); }); }); describe('alertInstanceToListItem', () => { it('handles active instances', () => { - const alert = mockAlert(); + const alertType = mockAlertType({ + actionGroups: [ + { id: 'default', name: 'Default Action Group' }, + { id: 'testing', name: 'Test Action Group' }, + ], + }); const start = fake2MinutesAgo; const instance: AlertInstanceStatus = { status: 'Active', muted: false, activeStartDate: fake2MinutesAgo.toISOString(), + actionGroupId: 'testing', }; - expect(alertInstanceToListItem(fakeNow.getTime(), alert, 'id', instance)).toEqual({ + expect(alertInstanceToListItem(fakeNow.getTime(), alertType, 'id', instance)).toEqual({ instance: 'id', - status: { label: 'Active', healthColor: 'primary' }, + status: { label: 'Active', actionGroup: 'Test Action Group', healthColor: 'primary' }, start, sortPriority: 0, duration: fakeNow.getTime() - fake2MinutesAgo.getTime(), @@ -184,20 +200,38 @@ describe('alertInstanceToListItem', () => { }); }); - it('handles active muted instances', () => { - const alert = mockAlert({ - mutedInstanceIds: ['id'], + it('handles active instances with no action group id', () => { + const alertType = mockAlertType(); + const start = fake2MinutesAgo; + const instance: AlertInstanceStatus = { + status: 'Active', + muted: false, + activeStartDate: fake2MinutesAgo.toISOString(), + }; + + expect(alertInstanceToListItem(fakeNow.getTime(), alertType, 'id', instance)).toEqual({ + instance: 'id', + status: { label: 'Active', actionGroup: 'Default Action Group', healthColor: 'primary' }, + start, + sortPriority: 0, + duration: fakeNow.getTime() - fake2MinutesAgo.getTime(), + isMuted: false, }); + }); + + it('handles active muted instances', () => { + const alertType = mockAlertType(); const start = fake2MinutesAgo; const instance: AlertInstanceStatus = { status: 'Active', muted: true, activeStartDate: fake2MinutesAgo.toISOString(), + actionGroupId: 'default', }; - expect(alertInstanceToListItem(fakeNow.getTime(), alert, 'id', instance)).toEqual({ + expect(alertInstanceToListItem(fakeNow.getTime(), alertType, 'id', instance)).toEqual({ instance: 'id', - status: { label: 'Active', healthColor: 'primary' }, + status: { label: 'Active', actionGroup: 'Default Action Group', healthColor: 'primary' }, start, sortPriority: 0, duration: fakeNow.getTime() - fake2MinutesAgo.getTime(), @@ -206,15 +240,16 @@ describe('alertInstanceToListItem', () => { }); it('handles active instances with start date', () => { - const alert = mockAlert(); + const alertType = mockAlertType(); const instance: AlertInstanceStatus = { status: 'Active', muted: false, + actionGroupId: 'default', }; - expect(alertInstanceToListItem(fakeNow.getTime(), alert, 'id', instance)).toEqual({ + expect(alertInstanceToListItem(fakeNow.getTime(), alertType, 'id', instance)).toEqual({ instance: 'id', - status: { label: 'Active', healthColor: 'primary' }, + status: { label: 'Active', actionGroup: 'Default Action Group', healthColor: 'primary' }, start: undefined, duration: 0, sortPriority: 0, @@ -223,14 +258,13 @@ describe('alertInstanceToListItem', () => { }); it('handles muted inactive instances', () => { - const alert = mockAlert({ - mutedInstanceIds: ['id'], - }); + const alertType = mockAlertType(); const instance: AlertInstanceStatus = { status: 'OK', muted: true, + actionGroupId: 'default', }; - expect(alertInstanceToListItem(fakeNow.getTime(), alert, 'id', instance)).toEqual({ + expect(alertInstanceToListItem(fakeNow.getTime(), alertType, 'id', instance)).toEqual({ instance: 'id', status: { label: 'OK', healthColor: 'subdued' }, start: undefined, @@ -268,6 +302,23 @@ function mockAlert(overloads: Partial = {}): Alert { }; } +function mockAlertType(overloads: Partial = {}): AlertType { + return { + id: 'test.testAlertType', + name: 'My Test Alert Type', + actionGroups: [{ id: 'default', name: 'Default Action Group' }], + actionVariables: { + context: [], + state: [], + params: [], + }, + defaultActionGroupId: 'default', + authorizedConsumers: {}, + producer: 'alerts', + ...overloads, + }; +} + function mockAlertInstanceSummary( overloads: Partial = {} ): AlertInstanceSummary { @@ -288,6 +339,7 @@ function mockAlertInstanceSummary( foo: { status: 'OK', muted: false, + actionGroupId: 'testActionGroup', }, }, }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx index 0648f34927db3..ed05d81646c4a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx @@ -11,8 +11,14 @@ import { EuiBasicTable, EuiHealth, EuiSpacer, EuiSwitch } from '@elastic/eui'; // @ts-ignore import { RIGHT_ALIGNMENT, CENTER_ALIGNMENT } from '@elastic/eui/lib/services'; import { padStart, chunk } from 'lodash'; -import { AlertInstanceStatusValues } from '../../../../../../alerts/common'; -import { Alert, AlertInstanceSummary, AlertInstanceStatus, Pagination } from '../../../../types'; +import { ActionGroup, AlertInstanceStatusValues } from '../../../../../../alerts/common'; +import { + Alert, + AlertInstanceSummary, + AlertInstanceStatus, + AlertType, + Pagination, +} from '../../../../types'; import { ComponentOpts as AlertApis, withBulkAlertOperations, @@ -21,6 +27,7 @@ import { DEFAULT_SEARCH_PAGE_SIZE } from '../../../constants'; type AlertInstancesProps = { alert: Alert; + alertType: AlertType; readOnly: boolean; alertInstanceSummary: AlertInstanceSummary; requestRefresh: () => Promise; @@ -48,7 +55,12 @@ export const alertInstancesTableColumns = ( { defaultMessage: 'Status' } ), render: (value: AlertInstanceListItemStatus, instance: AlertInstanceListItem) => { - return {value.label}; + return ( + + {value.label} + {value.actionGroup ? ` (${value.actionGroup})` : ``} + + ); }, sortable: false, 'data-test-subj': 'alertInstancesTableCell-status', @@ -113,6 +125,7 @@ function durationAsString(duration: Duration): string { export function AlertInstances({ alert, + alertType, readOnly, alertInstanceSummary, muteAlertInstance, @@ -127,7 +140,7 @@ export function AlertInstances({ const alertInstances = Object.entries(alertInstanceSummary.instances) .map(([instanceId, instance]) => - alertInstanceToListItem(durationEpoch, alert, instanceId, instance) + alertInstanceToListItem(durationEpoch, alertType, instanceId, instance) ) .sort((leftInstance, rightInstance) => leftInstance.sortPriority - rightInstance.sortPriority); @@ -180,6 +193,7 @@ function getPage(items: any[], pagination: Pagination) { interface AlertInstanceListItemStatus { label: string; healthColor: string; + actionGroup?: string; } export interface AlertInstanceListItem { instance: string; @@ -200,16 +214,28 @@ const INACTIVE_LABEL = i18n.translate( { defaultMessage: 'OK' } ); +function getActionGroupName(alertType: AlertType, actionGroupId?: string): string | undefined { + actionGroupId = actionGroupId || alertType.defaultActionGroupId; + const actionGroup = alertType?.actionGroups?.find( + (group: ActionGroup) => group.id === actionGroupId + ); + return actionGroup?.name; +} + export function alertInstanceToListItem( durationEpoch: number, - alert: Alert, + alertType: AlertType, instanceId: string, instance: AlertInstanceStatus ): AlertInstanceListItem { const isMuted = !!instance?.muted; const status = instance?.status === 'Active' - ? { label: ACTIVE_LABEL, healthColor: 'primary' } + ? { + label: ACTIVE_LABEL, + actionGroup: getActionGroupName(alertType, instance?.actionGroupId), + healthColor: 'primary', + } : { label: INACTIVE_LABEL, healthColor: 'subdued' }; const start = instance?.activeStartDate ? new Date(instance.activeStartDate) : undefined; const duration = start ? durationEpoch - start.valueOf() : 0; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.test.tsx index 603f06d0bbae4..3a171d469d4ad 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.test.tsx @@ -8,7 +8,7 @@ import uuid from 'uuid'; import { shallow } from 'enzyme'; import { ToastsApi } from 'kibana/public'; import { AlertInstancesRoute, getAlertInstanceSummary } from './alert_instances_route'; -import { Alert, AlertInstanceSummary } from '../../../../types'; +import { Alert, AlertInstanceSummary, AlertType } from '../../../../types'; import { EuiLoadingSpinner } from '@elastic/eui'; const fakeNow = new Date('2020-02-09T23:15:41.941Z'); @@ -23,10 +23,11 @@ jest.mock('../../../app_context', () => { describe('alert_instance_summary_route', () => { it('render a loader while fetching data', () => { const alert = mockAlert(); + const alertType = mockAlertType(); expect( shallow( - + ).containsMatchingElement() ).toBeTruthy(); }); @@ -140,6 +141,23 @@ function mockAlert(overloads: Partial = {}): Alert { }; } +function mockAlertType(overloads: Partial = {}): AlertType { + return { + id: 'test.testAlertType', + name: 'My Test Alert Type', + actionGroups: [{ id: 'default', name: 'Default Action Group' }], + actionVariables: { + context: [], + state: [], + params: [], + }, + defaultActionGroupId: 'default', + authorizedConsumers: {}, + producer: 'alerts', + ...overloads, + }; +} + function mockAlertInstanceSummary(overloads: Partial = {}): any { const summary: AlertInstanceSummary = { id: 'alert-id', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.tsx index 9137a26a32dd4..83a09e9eafcc1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.tsx @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { ToastsApi } from 'kibana/public'; import React, { useState, useEffect } from 'react'; import { EuiLoadingSpinner } from '@elastic/eui'; -import { Alert, AlertInstanceSummary } from '../../../../types'; +import { Alert, AlertInstanceSummary, AlertType } from '../../../../types'; import { useAppDependencies } from '../../../app_context'; import { ComponentOpts as AlertApis, @@ -18,12 +18,14 @@ import { AlertInstancesWithApi as AlertInstances } from './alert_instances'; type WithAlertInstanceSummaryProps = { alert: Alert; + alertType: AlertType; readOnly: boolean; requestRefresh: () => Promise; } & Pick; export const AlertInstancesRoute: React.FunctionComponent = ({ alert, + alertType, readOnly, requestRefresh, loadAlertInstanceSummary: loadAlertInstanceSummary, @@ -48,6 +50,7 @@ export const AlertInstancesRoute: React.FunctionComponent diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts index a5dff437283ae..dbf8eb162fca7 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts @@ -134,7 +134,7 @@ export default function eventLogTests({ getService }: FtrProviderContext) { validateInstanceEvent(event, `resolved instance: 'instance'`); break; case 'active-instance': - validateInstanceEvent(event, `active instance: 'instance'`); + validateInstanceEvent(event, `active instance: 'instance' in actionGroup: 'default'`); break; // this will get triggered as we add new event actions default: diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_instance_summary.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_instance_summary.ts index 563127e028a62..22034328e5275 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_instance_summary.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_instance_summary.ts @@ -226,6 +226,7 @@ export default function createGetAlertInstanceSummaryTests({ getService }: FtrPr instanceA: { status: 'Active', muted: false, + actionGroupId: 'default', activeStartDate: actualInstances.instanceA.activeStartDate, }, instanceB: { @@ -235,6 +236,7 @@ export default function createGetAlertInstanceSummaryTests({ getService }: FtrPr instanceC: { status: 'Active', muted: true, + actionGroupId: 'default', activeStartDate: actualInstances.instanceC.activeStartDate, }, instanceD: { diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts index 4c97c8556d7df..9e4006681dc8d 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts @@ -392,21 +392,21 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(instancesList.map((instance) => omit(instance, 'duration'))).to.eql([ { instance: 'us-central', - status: 'Active', + status: 'Active (Default)', start: moment(dateOnAllInstancesFromApiResponse['us-central']) .utc() .format('D MMM YYYY @ HH:mm:ss'), }, { instance: 'us-east', - status: 'Active', + status: 'Active (Default)', start: moment(dateOnAllInstancesFromApiResponse['us-east']) .utc() .format('D MMM YYYY @ HH:mm:ss'), }, { instance: 'us-west', - status: 'Active', + status: 'Active (Default)', start: moment(dateOnAllInstancesFromApiResponse['us-west']) .utc() .format('D MMM YYYY @ HH:mm:ss'),