From 78ff455b6e01b47e453cc827bd51fc8a8ac4cd96 Mon Sep 17 00:00:00 2001 From: Antonio Date: Thu, 16 May 2024 11:22:13 +0200 Subject: [PATCH] [MGMTXP] [Connectors] Added the `additional_info` field to the ITOM connector UI (#183380) Fixes #171320 ## Summary The additional_info field for the ServiceNow ITOM connector already existed in the backend. When creating an action it was being pre-populated with some rule and alert data and sent to the backend with the form. In this PR I am making that field visible and editable by the user while preserving the original default values. Screenshot 2024-05-14 at 12 28 25 ## Release notes Added support for the additional info field in the ServiceNow ITOM connector. --- .../lib/servicenow/translations.ts | 29 ++++++++ .../servicenow_itom/servicenow_itom.test.tsx | 34 ++++++++- .../servicenow_itom/servicenow_itom.tsx | 10 ++- .../servicenow_itom_params.test.tsx | 51 +++++++++++-- .../servicenow_itom_params.tsx | 72 ++++++++++++++----- 5 files changed, 171 insertions(+), 25 deletions(-) diff --git a/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/translations.ts b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/translations.ts index 547e641c019da1..8f519e96618819 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/translations.ts +++ b/x-pack/plugins/stack_connectors/public/connector_types/lib/servicenow/translations.ts @@ -412,3 +412,32 @@ export const EVENT_ACTION_LABEL = i18n.translate( defaultMessage: 'Event action', } ); + +export const ADDITIONAL_INFO = i18n.translate( + 'xpack.stackConnectors.components.serviceNowITOM.additionalInfoLabel', + { + defaultMessage: 'Additional info', + } +); + +export const ADDITIONAL_INFO_HELP = i18n.translate( + 'xpack.stackConnectors.components.serviceNowITOM.additionalInfoHelpTooltip', + { + defaultMessage: 'Additional info help', + } +); + +export const ADDITIONAL_INFO_HELP_TEXT = i18n.translate( + 'xpack.stackConnectors.components.serviceNowITOM.additionalInfoHelpTooltipText', + { + defaultMessage: + 'The rule automatically generates information about each event. You can change or add more custom fields in JSON format.', + } +); + +export const ADDITIONAL_INFO_JSON_ERROR = i18n.translate( + 'xpack.stackConnectors.components.serviceNowITOM.additionalInfoError', + { + defaultMessage: 'The additional info field does not have a valid JSON format.', + } +); diff --git a/x-pack/plugins/stack_connectors/public/connector_types/servicenow_itom/servicenow_itom.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/servicenow_itom/servicenow_itom.test.tsx index 7673f4caf588e3..c5063e0950b367 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/servicenow_itom/servicenow_itom.test.tsx +++ b/x-pack/plugins/stack_connectors/public/connector_types/servicenow_itom/servicenow_itom.test.tsx @@ -33,7 +33,22 @@ describe('servicenow action params validation', () => { const actionParams = { subActionParams: { severity: 'Critical' } }; expect(await connectorTypeModel.validateParams(actionParams)).toEqual({ - errors: { ['severity']: [] }, + errors: { + ['severity']: [], + ['additional_info']: [], + }, + }); + }); + + test(`${SERVICENOW_ITOM_CONNECTOR_TYPE_ID}: params validation succeeds when additional_info is an empty string`, async () => { + const connectorTypeModel = connectorTypeRegistry.get(SERVICENOW_ITOM_CONNECTOR_TYPE_ID); + const actionParams = { subActionParams: { severity: 'Critical', additional_info: '' } }; + + expect(await connectorTypeModel.validateParams(actionParams)).toEqual({ + errors: { + ['severity']: [], + ['additional_info']: [], + }, }); }); @@ -42,7 +57,22 @@ describe('servicenow action params validation', () => { const actionParams = { subActionParams: { severity: null } }; expect(await connectorTypeModel.validateParams(actionParams)).toEqual({ - errors: { ['severity']: ['Severity is required.'] }, + errors: { + ['severity']: ['Severity is required.'], + ['additional_info']: [], + }, + }); + }); + + test(`${SERVICENOW_ITOM_CONNECTOR_TYPE_ID}: params validation fails when additional_info is not valid JSON`, async () => { + const connectorTypeModel = connectorTypeRegistry.get(SERVICENOW_ITOM_CONNECTOR_TYPE_ID); + const actionParams = { subActionParams: { severity: 'Critical', additional_info: 'foobar' } }; + + expect(await connectorTypeModel.validateParams(actionParams)).toEqual({ + errors: { + ['severity']: [], + ['additional_info']: ['The additional info field does not have a valid JSON format.'], + }, }); }); }); diff --git a/x-pack/plugins/stack_connectors/public/connector_types/servicenow_itom/servicenow_itom.tsx b/x-pack/plugins/stack_connectors/public/connector_types/servicenow_itom/servicenow_itom.tsx index 8c9d7e877f0eed..6fd62d22029dcc 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/servicenow_itom/servicenow_itom.tsx +++ b/x-pack/plugins/stack_connectors/public/connector_types/servicenow_itom/servicenow_itom.tsx @@ -45,14 +45,20 @@ export function getServiceNowITOMConnectorType(): ConnectorTypeModel< const translations = await import('../lib/servicenow/translations'); const errors = { severity: new Array(), + additional_info: new Array(), }; - const validationResult = { errors }; if (actionParams?.subActionParams?.severity == null) { errors.severity.push(translations.SEVERITY_REQUIRED); } - return validationResult; + try { + JSON.parse(actionParams.subActionParams?.additional_info || '{}'); + } catch (error) { + errors.additional_info.push(translations.ADDITIONAL_INFO_JSON_ERROR); + } + + return { errors }; }, actionParamsFields: lazy(() => import('./servicenow_itom_params')), }; diff --git a/x-pack/plugins/stack_connectors/public/connector_types/servicenow_itom/servicenow_itom_params.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/servicenow_itom/servicenow_itom_params.test.tsx index cc67186790be74..ac200df8abde00 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/servicenow_itom/servicenow_itom_params.test.tsx +++ b/x-pack/plugins/stack_connectors/public/connector_types/servicenow_itom/servicenow_itom_params.test.tsx @@ -94,6 +94,7 @@ describe('ServiceNowITOMParamsFields renders', () => { expect(wrapper.find('[data-test-subj="message_keyInput"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="severitySelect"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="descriptionTextArea"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="additional_infoJsonEditor"]').exists()).toBeTruthy(); }); test('If severity has errors, form row is invalid', () => { @@ -117,8 +118,24 @@ describe('ServiceNowITOMParamsFields renders', () => { mount(); expect(editAction.mock.calls[0][1]).toEqual({ message_key: '{{rule.id}}:{{alert.id}}', - additional_info: - '{"alert":{"id":"{{alert.id}}","actionGroup":"{{alert.actionGroup}}","actionSubgroup":"{{alert.actionSubgroup}}","actionGroupName":"{{alert.actionGroupName}}"},"rule":{"id":"{{rule.id}}","name":"{{rule.name}}","type":"{{rule.type}}"},"date":"{{date}}"}', + additional_info: JSON.stringify( + { + alert: { + id: '{{alert.id}}', + actionGroup: '{{alert.actionGroup}}', + actionSubgroup: '{{alert.actionSubgroup}}', + actionGroupName: '{{alert.actionGroupName}}', + }, + rule: { + id: '{{rule.id}}', + name: '{{rule.name}}', + type: '{{rule.type}}', + }, + date: '{{date}}', + }, + null, + 4 + ), }); }); @@ -140,8 +157,24 @@ describe('ServiceNowITOMParamsFields renders', () => { expect(editAction.mock.calls.length).toEqual(1); expect(editAction.mock.calls[0][1]).toEqual({ message_key: '{{rule.id}}:{{alert.id}}', - additional_info: - '{"alert":{"id":"{{alert.id}}","actionGroup":"{{alert.actionGroup}}","actionSubgroup":"{{alert.actionSubgroup}}","actionGroupName":"{{alert.actionGroupName}}"},"rule":{"id":"{{rule.id}}","name":"{{rule.name}}","type":"{{rule.type}}"},"date":"{{date}}"}', + additional_info: JSON.stringify( + { + alert: { + id: '{{alert.id}}', + actionGroup: '{{alert.actionGroup}}', + actionSubgroup: '{{alert.actionSubgroup}}', + actionGroupName: '{{alert.actionGroupName}}', + }, + rule: { + id: '{{rule.id}}', + name: '{{rule.name}}', + type: '{{rule.type}}', + }, + date: '{{date}}', + }, + null, + 4 + ), }); }); @@ -177,5 +210,15 @@ describe('ServiceNowITOMParamsFields renders', () => { expect(editAction.mock.calls[0][1][field.key]).toEqual(changeEvent.target.value); }) ); + + test('additional_info update triggers editAction correctly', () => { + const newValue = '{"foo": "bar"}' as unknown as React.ChangeEvent; + const wrapper = mount(); + const theField = wrapper.find('[data-test-subj="additional_infoJsonEditor"]').first(); + + theField.prop('onChange')!(newValue); + + expect(editAction.mock.calls[0][1].additional_info).toEqual(newValue); + }); }); }); diff --git a/x-pack/plugins/stack_connectors/public/connector_types/servicenow_itom/servicenow_itom_params.tsx b/x-pack/plugins/stack_connectors/public/connector_types/servicenow_itom/servicenow_itom_params.tsx index bc059fb8e592e2..2518b56b97ef45 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/servicenow_itom/servicenow_itom_params.tsx +++ b/x-pack/plugins/stack_connectors/public/connector_types/servicenow_itom/servicenow_itom_params.tsx @@ -6,8 +6,11 @@ */ import React, { useCallback, useEffect, useRef, useMemo } from 'react'; -import { EuiFormRow, EuiSpacer, EuiTitle, EuiText, EuiSelect } from '@elastic/eui'; -import type { ActionParamsProps } from '@kbn/triggers-actions-ui-plugin/public'; +import { EuiFormRow, EuiSpacer, EuiTitle, EuiText, EuiSelect, EuiIconTip } from '@elastic/eui'; +import { + ActionParamsProps, + JsonEditorWithMessageVariables, +} from '@kbn/triggers-actions-ui-plugin/public'; import { TextAreaWithMessageVariables, TextFieldWithMessageVariables, @@ -34,20 +37,24 @@ const fields: Array<{ { label: i18n.MESSAGE_KEY, fieldKey: 'message_key' }, ]; -const additionalInformation = JSON.stringify({ - alert: { - id: '{{alert.id}}', - actionGroup: '{{alert.actionGroup}}', - actionSubgroup: '{{alert.actionSubgroup}}', - actionGroupName: '{{alert.actionGroupName}}', - }, - rule: { - id: '{{rule.id}}', - name: '{{rule.name}}', - type: '{{rule.type}}', +const additionalInformation = JSON.stringify( + { + alert: { + id: '{{alert.id}}', + actionGroup: '{{alert.actionGroup}}', + actionSubgroup: '{{alert.actionSubgroup}}', + actionGroupName: '{{alert.actionGroupName}}', + }, + rule: { + id: '{{rule.id}}', + name: '{{rule.name}}', + type: '{{rule.type}}', + }, + date: '{{date}}', }, - date: '{{date}}', -}); + null, + 4 +); const ServiceNowITOMParamsFields: React.FunctionComponent< ActionParamsProps @@ -57,8 +64,7 @@ const ServiceNowITOMParamsFields: React.FunctionComponent< [actionParams.subActionParams] ); - const { description, severity } = params; - + const { description, severity, additional_info: additionalInfo } = params; const { http, notifications: { toasts }, @@ -159,6 +165,38 @@ const ServiceNowITOMParamsFields: React.FunctionComponent< inputTargetValue={description ?? undefined} label={i18n.DESCRIPTION_LABEL} /> + 0 + } + > + + {i18n.ADDITIONAL_INFO} + + + } + onDocumentsChange={(json: string) => { + editSubActionProperty('additional_info', json); + }} + /> + ); };