From fe41d6c62f0d636b93003789a3040fb75d6c7531 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Wed, 8 Dec 2021 11:02:16 -0500 Subject: [PATCH] [Cases] Adding deprecated icon to additional actions dropdown selectors (#115287) * adding deprecated icon to the other actions list * Adding deprecated text to list view * Each action type can render the dropdown row * Refactoring and fixing todos * Fixing jest tests * Adding and fixing other tests * Fixing functional test * Fixing tests * Adjusting text to match cases * Fixing tests * Addressing pr feedback * Renaming connector dropdown to selection * Fixing type errors * Fixing type error * Fixing translation error * Fixing test * Addressing ux feedback and using ComboBox * Extracting customConnectorSelectItem to an interface Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - x-pack/plugins/triggers_actions_ui/README.md | 15 ++ .../servicenow/deprecated_callout.tsx | 30 +-- .../servicenow/helpers.test.ts | 70 +++---- .../servicenow/helpers.ts | 37 ++-- .../servicenow/servicenow.tsx | 9 + .../servicenow/servicenow_connectors.tsx | 20 +- .../servicenow/servicenow_itsm_params.tsx | 9 +- .../servicenow/servicenow_selection_row.tsx | 39 ++++ .../servicenow/servicenow_sir_params.tsx | 11 +- .../action_form.test.tsx | 61 +++--- .../action_type_form.tsx | 180 ++++++------------ .../connector_add_inline.tsx | 115 +++++------ .../connectors_selection.test.tsx | 126 ++++++++++++ .../connectors_selection.tsx | 141 ++++++++++++++ .../components/actions_connectors_list.tsx | 27 ++- .../application/sections/common/connectors.ts | 23 +++ .../common/connectors_seleciton.test.tsx | 59 ++++++ .../public/common/connectors_selection.tsx | 70 +++++++ .../triggers_actions_ui/public/types.ts | 8 + .../apps/triggers_actions_ui/details.ts | 6 +- 22 files changed, 751 insertions(+), 307 deletions(-) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_selection_row.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connectors_selection.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connectors_selection.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/common/connectors.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/common/connectors_seleciton.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/common/connectors_selection.tsx diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index f9c74ce61c49f..94242731c8777 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -25956,7 +25956,6 @@ "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.deleteActionDisabledDescription": "コネクターを削除できません", "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.deleteActionName": "削除", "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.fixActionDescription": "コネクター構成を修正", - "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.isDeprecatedDescription": "このコネクターは廃止予定です。更新するか新しく作成してください。", "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.missingSecretsDescription": "機密情報はインポートされませんでした", "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.runConnectorDescription": "このコネクターを実行", "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.runConnectorDisabledDescription": "コネクターを実行できません", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index c1c273d541911..e00cdb7a0bc27 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -26404,7 +26404,6 @@ "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.deleteActionDisabledDescription": "无法删除连接器", "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.deleteActionName": "删除", "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.fixActionDescription": "修复连接器配置", - "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.isDeprecatedDescription": "此连接器已过时。请进行更新,或创建新连接器。", "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.missingSecretsDescription": "未导入敏感信息", "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.runConnectorDescription": "运行此连接器", "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.runConnectorDisabledDescription": "无法运行连接器", diff --git a/x-pack/plugins/triggers_actions_ui/README.md b/x-pack/plugins/triggers_actions_ui/README.md index d4967552080fe..dacd26cdf7eeb 100644 --- a/x-pack/plugins/triggers_actions_ui/README.md +++ b/x-pack/plugins/triggers_actions_ui/README.md @@ -1117,6 +1117,7 @@ Each action type should be defined as an `ActionTypeModel` object with the follo validateParams: (actionParams: any) => Promise; actionConnectorFields: React.FunctionComponent | null; actionParamsFields: React.LazyExoticComponent>>; + customConnectorSelectItem?: CustomConnectorSelectionItem; ``` |Property|Description| |---|---| @@ -1127,6 +1128,20 @@ Each action type should be defined as an `ActionTypeModel` object with the follo |validateParams|Validation function for action params.| |actionConnectorFields|A lazy loaded React component for building UI of current action type connector.| |actionParamsFields|A lazy loaded React component for building UI of current action type params. Displayed as a part of Create Alert flyout.| +|customConnectorSelectItem|Optional, an object for customizing the selection row of the action connector form.| + +### CustomConnectorSelectionItem Properties + +``` + getText: (connector: ActionConnector) => string; + getComponent: (connector: ActionConnector) => React. + LazyExoticComponent | undefined; +``` + +|Property|Description| +|---|---| +|getText|Function for returning the text to display for the row.| +|getComponent|Function for returning a lazy loaded React component for customizing the selection row of the action connector form. Or undefined if if no customization is needed.| ## Register action type model diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/deprecated_callout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/deprecated_callout.tsx index d01a32564490d..bcf56f10f6f0f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/deprecated_callout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/deprecated_callout.tsx @@ -11,13 +11,21 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; interface Props { - onMigrate: () => void; + onMigrate?: () => void; } const DeprecatedCalloutComponent: React.FC = ({ onMigrate }) => { + const update = + onMigrate != null ? ( + + {updateThisConnectorMessage} + + ) : ( + {updateThisConnectorMessage} + ); + return ( <> - = ({ onMigrate }) => { defaultMessage="{update} {create} " id="xpack.triggersActionsUI.components.builtinActionTypes.servicenow.appInstallationInfo" values={{ - update: ( - - {i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.deprecatedCalloutMigrate', - { - defaultMessage: 'Update this connector,', - } - )} - - ), + update, create: ( {i18n.translate( @@ -63,3 +62,10 @@ const DeprecatedCalloutComponent: React.FC = ({ onMigrate }) => { }; export const DeprecatedCallout = memo(DeprecatedCalloutComponent); + +const updateThisConnectorMessage = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.deprecatedCalloutMigrate', + { + defaultMessage: 'Update this connector,', + } +); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/helpers.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/helpers.test.ts index 9a8094f53d501..912b308d8d79c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/helpers.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/helpers.test.ts @@ -5,7 +5,26 @@ * 2.0. */ -import { isRESTApiError, isFieldInvalid, isDeprecatedConnector } from './helpers'; +import { + isRESTApiError, + isFieldInvalid, + getConnectorDescriptiveTitle, + getSelectedConnectorIcon, +} from './helpers'; +import { ActionConnector } from '../../../../types'; + +const deprecatedConnector: ActionConnector = { + secrets: {}, + config: { + usesTableApi: true, + }, + id: 'test', + actionTypeId: '.servicenow', + name: 'Test', + isPreconfigured: false, +}; + +const validConnector = { ...deprecatedConnector, config: { usesTableApi: false } }; describe('helpers', () => { describe('isRESTApiError', () => { @@ -49,50 +68,23 @@ describe('helpers', () => { }); }); - describe('isDeprecatedConnector', () => { - const connector = { - id: 'test', - actionTypeId: '.webhook', - name: 'Test', - config: { apiUrl: 'http://example.com', usesTableApi: false }, - secrets: { username: 'test', password: 'test' }, - isPreconfigured: false as const, - }; - - it('returns false if the connector is not defined', () => { - expect(isDeprecatedConnector()).toBe(false); + describe('getConnectorDescriptiveTitle', () => { + it('adds deprecated to the connector name when the connector usesTableApi', () => { + expect(getConnectorDescriptiveTitle(deprecatedConnector)).toEqual('Test (deprecated)'); }); - it('returns false if the connector is not ITSM or SecOps', () => { - expect(isDeprecatedConnector(connector)).toBe(false); - }); - - it('returns false if the connector is .servicenow and the usesTableApi=false', () => { - expect(isDeprecatedConnector({ ...connector, actionTypeId: '.servicenow' })).toBe(false); - }); - - it('returns false if the connector is .servicenow-sir and the usesTableApi=false', () => { - expect(isDeprecatedConnector({ ...connector, actionTypeId: '.servicenow-sir' })).toBe(false); + it('does not add deprecated when the connector has usesTableApi:false', () => { + expect(getConnectorDescriptiveTitle(validConnector)).toEqual('Test'); }); + }); - it('returns true if the connector is .servicenow and the usesTableApi=true', () => { - expect( - isDeprecatedConnector({ - ...connector, - actionTypeId: '.servicenow', - config: { ...connector.config, usesTableApi: true }, - }) - ).toBe(true); + describe('getSelectedConnectorIcon', () => { + it('returns undefined when the connector has usesTableApi:false', () => { + expect(getSelectedConnectorIcon(validConnector)).toBeUndefined(); }); - it('returns true if the connector is .servicenow-sir and the usesTableApi=true', () => { - expect( - isDeprecatedConnector({ - ...connector, - actionTypeId: '.servicenow-sir', - config: { ...connector.config, usesTableApi: true }, - }) - ).toBe(true); + it('returns a component when the connector has usesTableApi:true', () => { + expect(getSelectedConnectorIcon(deprecatedConnector)).toBeDefined(); }); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/helpers.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/helpers.ts index de0b30b9acb2f..610759a569fd3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/helpers.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/helpers.ts @@ -5,9 +5,14 @@ * 2.0. */ +import { lazy, ComponentType } from 'react'; import { EuiSelectOption } from '@elastic/eui'; -import { IErrorObject } from '../../../../../public/types'; -import { AppInfo, Choice, RESTApiError, ServiceNowActionConnector } from './types'; +import { AppInfo, Choice, RESTApiError } from './types'; +import { ActionConnector, IErrorObject } from '../../../../types'; +import { + deprecatedMessage, + checkConnectorIsDeprecated, +} from '../../../../common/connectors_selection'; export const DEFAULT_CORRELATION_ID = '{{rule.id}}:{{alert.id}}'; @@ -22,22 +27,20 @@ export const isFieldInvalid = ( error: string | IErrorObject | string[] ): boolean => error !== undefined && error.length > 0 && field != null; -// TODO: Remove when the applications are certified -export const isDeprecatedConnector = (connector?: ServiceNowActionConnector): boolean => { - if (connector == null) { - return false; - } +export const getConnectorDescriptiveTitle = (connector: ActionConnector) => { + let title = connector.name; - if (connector.actionTypeId === '.servicenow' || connector.actionTypeId === '.servicenow-sir') { - /** - * Connector's prior to the Elastic ServiceNow application - * use the Table API (https://developer.servicenow.com/dev.do#!/reference/api/rome/rest/c_TableAPI) - * Connectors after the Elastic ServiceNow application use the - * Import Set API (https://developer.servicenow.com/dev.do#!/reference/api/rome/rest/c_ImportSetAPI) - * A ServiceNow connector is considered deprecated if it uses the Table API. - */ - return !!connector.config.usesTableApi; + if (checkConnectorIsDeprecated(connector)) { + title += ` ${deprecatedMessage}`; } - return false; + return title; +}; + +export const getSelectedConnectorIcon = ( + actionConnector: ActionConnector +): React.LazyExoticComponent> | undefined => { + if (checkConnectorIsDeprecated(actionConnector)) { + return lazy(() => import('./servicenow_selection_row')); + } }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx index 7267e11ae7327..c38afbe5e748f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx @@ -21,6 +21,7 @@ import { ServiceNowITOMActionParams, } from './types'; import { isValidUrl } from '../../../lib/value_validators'; +import { getConnectorDescriptiveTitle, getSelectedConnectorIcon } from './helpers'; const validateConnector = async ( action: ServiceNowActionConnector @@ -138,6 +139,10 @@ export function getServiceNowITSMActionType(): ActionTypeModel< return validationResult; }, actionParamsFields: lazy(() => import('./servicenow_itsm_params')), + customConnectorSelectItem: { + getText: getConnectorDescriptiveTitle, + getComponent: getSelectedConnectorIcon, + }, }; } @@ -174,6 +179,10 @@ export function getServiceNowSIRActionType(): ActionTypeModel< return validationResult; }, actionParamsFields: lazy(() => import('./servicenow_sir_params')), + customConnectorSelectItem: { + getText: getConnectorDescriptiveTitle, + getComponent: getSelectedConnectorIcon, + }, }; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx index 7c53d4322ddb9..415e99eee7fdc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx @@ -7,6 +7,7 @@ import React, { useCallback, useEffect, useState } from 'react'; +import { EuiSpacer } from '@elastic/eui'; import { ActionConnectorFieldsProps } from '../../../../types'; import * as i18n from './translations'; @@ -15,11 +16,16 @@ import { useKibana } from '../../../../common/lib/kibana'; import { DeprecatedCallout } from './deprecated_callout'; import { useGetAppInfo } from './use_get_app_info'; import { ApplicationRequiredCallout } from './application_required_callout'; -import { isRESTApiError, isDeprecatedConnector } from './helpers'; +import { isRESTApiError } from './helpers'; import { InstallationCallout } from './installation_callout'; import { UpdateConnector } from './update_connector'; import { updateActionConnector } from '../../../lib/action_connector_api'; import { Credentials } from './credentials'; +import { checkConnectorIsDeprecated } from '../../../../common/connectors_selection'; + +// eslint-disable-next-line import/no-default-export +export { ServiceNowConnectorFields as default }; + // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { snExternalServiceConfig } from '../../../../../../actions/server/builtin_action_types/servicenow/config'; @@ -40,7 +46,7 @@ const ServiceNowConnectorFields: React.FC )} - {!requiresNewApplication && } + {!requiresNewApplication && } void }) => ( + <> + + + +); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.tsx index ca4eb72bcd5c1..dba788403980f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.tsx @@ -19,13 +19,14 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { useKibana } from '../../../../common/lib/kibana'; import { ActionParamsProps } from '../../../../types'; -import { ServiceNowITSMActionParams, Choice, Fields, ServiceNowActionConnector } from './types'; +import { ServiceNowITSMActionParams, Choice, Fields } from './types'; import { TextAreaWithMessageVariables } from '../../text_area_with_message_variables'; import { TextFieldWithMessageVariables } from '../../text_field_with_message_variables'; import { useGetChoices } from './use_get_choices'; -import { choicesToEuiOptions, DEFAULT_CORRELATION_ID, isDeprecatedConnector } from './helpers'; +import { choicesToEuiOptions, DEFAULT_CORRELATION_ID } from './helpers'; import * as i18n from './translations'; +import { checkConnectorIsDeprecated } from '../../../../common/connectors_selection'; const useGetChoicesFields = ['urgency', 'severity', 'impact', 'category', 'subcategory']; const defaultFields: Fields = { @@ -46,6 +47,8 @@ const ServiceNowParamsFields: React.FunctionComponent< notifications: { toasts }, } = useKibana().services; + const isDeprecatedActionConnector = checkConnectorIsDeprecated(actionConnector); + const actionConnectorRef = useRef(actionConnector?.id ?? ''); const { incident, comments } = useMemo( () => @@ -238,7 +241,7 @@ const ServiceNowParamsFields: React.FunctionComponent< - {!isDeprecatedConnector(actionConnector as unknown as ServiceNowActionConnector) && ( + {!isDeprecatedActionConnector && ( <> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_selection_row.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_selection_row.tsx new file mode 100644 index 0000000000000..65068c6f56a07 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_selection_row.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { EuiIconTip } from '@elastic/eui'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; + +import { ActionConnector } from '../../../../types'; +import { connectorDeprecatedMessage } from '../../../../common/connectors_selection'; + +// eslint-disable-next-line import/no-default-export +export { ServiceNowSelectableRowIcon as default }; + +export function ServiceNowSelectableRowIcon({ + actionConnector, +}: { + actionConnector: ActionConnector; +}) { + return ( + + ); +} + +const deprecatedTooltipTitle = i18n.translate( + 'xpack.triggersActionsUI.sections.actionForm.deprecatedTooltipTitle', + { + defaultMessage: 'Deprecated connector', + } +); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_sir_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_sir_params.tsx index f4d0cfecb675b..daffc7849e348 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_sir_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_sir_params.tsx @@ -24,8 +24,10 @@ import { TextFieldWithMessageVariables } from '../../text_field_with_message_var import * as i18n from './translations'; import { useGetChoices } from './use_get_choices'; -import { ServiceNowSIRActionParams, Fields, Choice, ServiceNowActionConnector } from './types'; -import { choicesToEuiOptions, isDeprecatedConnector, DEFAULT_CORRELATION_ID } from './helpers'; +import { ServiceNowSIRActionParams, Fields, Choice } from './types'; +import { choicesToEuiOptions, DEFAULT_CORRELATION_ID } from './helpers'; +import { DeprecatedCallout } from './deprecated_callout'; +import { checkConnectorIsDeprecated } from '../../../../common/connectors_selection'; const useGetChoicesFields = ['category', 'subcategory', 'priority']; const defaultFields: Fields = { @@ -43,6 +45,8 @@ const ServiceNowSIRParamsFields: React.FunctionComponent< notifications: { toasts }, } = useKibana().services; + const isDeprecatedActionConnector = checkConnectorIsDeprecated(actionConnector); + const actionConnectorRef = useRef(actionConnector?.id ?? ''); const { incident, comments } = useMemo( () => @@ -145,6 +149,7 @@ const ServiceNowSIRParamsFields: React.FunctionComponent< return ( <> + {isDeprecatedActionConnector && }

{i18n.SECURITY_INCIDENT}

@@ -223,7 +228,7 @@ const ServiceNowSIRParamsFields: React.FunctionComponent<
- {!isDeprecatedConnector(actionConnector as unknown as ServiceNowActionConnector) && ( + {!isDeprecatedActionConnector && ( <> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx index 6e6c6aa52b3e3..a26349949446d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx @@ -483,7 +483,7 @@ describe('action_form', () => { `[data-test-subj="${actionType.id}-ActionTypeSelectOption"]` ); actionOption.first().simulate('click'); - const combobox = wrapper.find(`[data-test-subj="selectActionConnector-${actionType.id}"]`); + const combobox = wrapper.find(`[data-test-subj="selectActionConnector-${actionType.id}-0"]`); const numConnectors = allActions.filter( (action) => action.actionTypeId === actionType.id ).length; @@ -494,35 +494,50 @@ describe('action_form', () => { numConnectors - numConnectorsWithMissingSecrets ); expect((combobox.first().props() as any).options).toMatchInlineSnapshot(` - Array [ - Object { - "id": "test", - "key": "test", - "label": "Test connector ", - }, - Object { - "id": "test2", - "key": "test2", - "label": "Test connector 2 (preconfigured)", - }, - ] - `); + Array [ + Object { + "data-test-subj": "dropdown-connector-test", + "key": "test", + "label": "Test connector", + "value": Object { + "id": "test", + "prependComponent": undefined, + "title": "Test connector", + }, + }, + Object { + "data-test-subj": "dropdown-connector-test2", + "key": "test2", + "label": "Test connector 2", + "value": Object { + "id": "test2", + "prependComponent": undefined, + "title": "Test connector 2", + }, + }, + ] + `); }); it('renders only preconfigured connectors for the selected preconfigured action type', async () => { const wrapper = await setup(); const actionOption = wrapper.find('[data-test-subj="preconfigured-ActionTypeSelectOption"]'); actionOption.first().simulate('click'); - const combobox = wrapper.find('[data-test-subj="selectActionConnector-preconfigured"]'); + const combobox = wrapper.find('[data-test-subj="selectActionConnector-preconfigured-1"]'); expect((combobox.first().props() as any).options).toMatchInlineSnapshot(` - Array [ - Object { - "id": "test3", - "key": "test3", - "label": "Preconfigured Only (preconfigured)", - }, - ] - `); + Array [ + Object { + "data-test-subj": "dropdown-connector-test3", + "key": "test3", + "label": "Preconfigured Only", + "value": Object { + "id": "test3", + "prependComponent": undefined, + "title": "Preconfigured Only", + }, + }, + ] + `); }); it('does not render "Add connector" button for preconfigured only action type', async () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx index c190ee564230f..10244213614e2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx @@ -14,14 +14,12 @@ import { EuiIcon, EuiSpacer, EuiFormRow, - EuiComboBox, EuiAccordion, EuiButtonIcon, EuiButtonEmpty, EuiIconTip, EuiText, EuiFormLabel, - EuiFormControlLayout, EuiSuperSelect, EuiBadge, EuiErrorBoundary, @@ -43,6 +41,7 @@ import { ActionAccordionFormProps, ActionGroupWithMessageVariables } from './act import { transformActionVariables } from '../../lib/action_variables'; import { useKibana } from '../../../common/lib/kibana'; import { DefaultActionParams } from '../../lib/get_defaults_for_action_params'; +import { ConnectorsSelection } from './connectors_selection'; export type ActionTypeFormProps = { actionItem: AlertAction; @@ -140,32 +139,6 @@ export const ActionTypeForm = ({ }, [actionItem]); const canSave = hasSaveActionsCapability(capabilities); - const getSelectedOptions = (actionItemId: string) => { - const selectedConnector = connectors.find((connector) => connector.id === actionItemId); - if ( - !selectedConnector || - // if selected connector is not preconfigured and action type is for preconfiguration only, - // do not show regular connectors of this type - (actionTypesIndex && - !actionTypesIndex[selectedConnector.actionTypeId].enabledInConfig && - !selectedConnector.isPreconfigured) - ) { - return []; - } - const optionTitle = `${selectedConnector.name} ${ - selectedConnector.isPreconfigured ? preconfiguredMessage : '' - }`; - return [ - { - label: optionTitle, - value: optionTitle, - id: actionItemId, - 'data-test-subj': 'itemActionConnector', - }, - ]; - }; - - const actionType = actionTypesIndex[actionItem.actionTypeId]; const actionGroupDisplay = ( actionGroupId: string, @@ -189,18 +162,6 @@ export const ActionTypeForm = ({ ? isActionGroupDisabledForActionType(actionGroupId, actionTypeId) : false; - const optionsList = connectors - .filter( - (connectorItem) => - connectorItem.actionTypeId === actionItem.actionTypeId && - // include only enabled by config connectors or preconfigured - (actionType.enabledInConfig || connectorItem.isPreconfigured) - ) - .map(({ name, id, isPreconfigured }) => ({ - label: `${name} ${isPreconfigured ? preconfiguredMessage : ''}`, - key: id, - id, - })); const actionTypeRegistered = actionTypeRegistry.get(actionConnector.actionTypeId); if (!actionTypeRegistered) return null; @@ -214,90 +175,73 @@ export const ActionTypeForm = ({ <> {actionGroups && selectedActionGroup && setActionGroupIdByIndex && ( <> - - - - - - } - > - ({ - value, - inputDisplay: actionGroupDisplay(value, name, actionItem.actionTypeId), - disabled: isActionGroupDisabled(value, actionItem.actionTypeId), - 'data-test-subj': `addNewActionConnectorActionGroup-${index}-option-${value}`, - }))} - valueOfSelected={selectedActionGroup.id} - onChange={(group) => { - setActionGroupIdByIndex(group, index); - setActionGroup(group); - }} + + - - - + + } + fullWidth + id={`addNewActionConnectorActionGroup-${actionItem.actionTypeId}`} + data-test-subj={`addNewActionConnectorActionGroup-${index}`} + options={actionGroups.map(({ id: value, name }) => ({ + value, + inputDisplay: actionGroupDisplay(value, name, actionItem.actionTypeId), + disabled: isActionGroupDisabled(value, actionItem.actionTypeId), + 'data-test-subj': `addNewActionConnectorActionGroup-${index}-option-${value}`, + }))} + valueOfSelected={selectedActionGroup.id} + onChange={(group) => { + setActionGroupIdByIndex(group, index); + setActionGroup(group); + }} + /> + )} - - - + } + labelAppend={ + canSave && + actionTypesIndex && + actionTypesIndex[actionConnector.actionTypeId].enabledInConfig ? ( + - } - labelAppend={ - canSave && - actionTypesIndex && - actionTypesIndex[actionConnector.actionTypeId].enabledInConfig ? ( - - - - ) : null - } - > - { - onConnectorSelected(selectedOptions[0].id ?? ''); - }} - isClearable={false} - /> - - - + + ) : null + } + > + + {ParamsFieldsComponent ? ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_inline.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_inline.tsx index 540470ed83eaa..cd274c542c9d5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_inline.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_inline.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { @@ -21,14 +21,14 @@ import { EuiText, EuiFormRow, EuiButtonEmpty, - EuiComboBox, - EuiComboBoxOptionOption, EuiIconTip, } from '@elastic/eui'; import { AlertAction, ActionTypeIndex, ActionConnector } from '../../../types'; import { hasSaveActionsCapability } from '../../lib/capabilities'; import { ActionAccordionFormProps } from './action_form'; import { useKibana } from '../../../common/lib/kibana'; +import { getValidConnectors } from '../common/connectors'; +import { ConnectorsSelection } from './connectors_selection'; type AddConnectorInFormProps = { actionTypesIndex: ActionTypeIndex; @@ -56,15 +56,17 @@ export const AddConnectorInline = ({ application: { capabilities }, } = useKibana().services; const canSave = hasSaveActionsCapability(capabilities); - const [connectorOptionsList, setConnectorOptionsList] = useState([]); + const [hasConnectors, setHasConnectors] = useState(false); const [isEmptyActionId, setIsEmptyActionId] = useState(false); - const [errors, setErrors] = useState([]); const actionTypeName = actionTypesIndex ? actionTypesIndex[actionItem.actionTypeId].name : actionItem.actionTypeId; - const actionType = actionTypesIndex[actionItem.actionTypeId]; const actionTypeRegistered = actionTypeRegistry.get(actionItem.actionTypeId); + const connectorDropdownErrors = useMemo( + () => [`Unable to load ${actionTypeRegistered.actionTypeTitle} connector`], + [actionTypeRegistered.actionTypeTitle] + ); const noConnectorsLabel = ( { - if (connectors) { - const altConnectorOptions = connectors - .filter( - (connector) => - connector.actionTypeId === actionItem.actionTypeId && - // include only enabled by config connectors or preconfigured - (actionType?.enabledInConfig || connector.isPreconfigured) - ) - .map(({ name, id, isPreconfigured }) => ({ - label: `${name} ${isPreconfigured ? '(preconfigured)' : ''}`, - key: id, - id, - })); - setConnectorOptionsList(altConnectorOptions); + const filteredConnectors = getValidConnectors(connectors, actionItem, actionTypesIndex); - if (altConnectorOptions.length > 0) { - setErrors([`Unable to load ${actionTypeRegistered.actionTypeTitle} connector`]); - } + if (filteredConnectors.length > 0) { + setHasConnectors(true); } setIsEmptyActionId(!!emptyActionsIds.find((emptyId: string) => actionItem.id === emptyId)); @@ -111,54 +99,41 @@ export const AddConnectorInline = ({ }, []); const connectorsDropdown = ( - - - - } - labelAppend={ - - - - } - error={errors} - isInvalid={errors.length > 0} + + } + labelAppend={ + - { - // On selecting a option from this combo box, this component will - // be removed but the EuiComboBox performs some additional updates on - // closing the dropdown. Wrapping in a `setTimeout` to avoid `React state - // update on an unmounted component` warnings. - setTimeout(() => { - onSelectConnector(selectedOptions[0].id ?? ''); - }); - }} - isClearable={false} + - - - + + } + error={connectorDropdownErrors} + isInvalid + > + + ); return ( @@ -223,7 +198,7 @@ export const AddConnectorInline = ({ paddingSize="l" > {canSave ? ( - connectorOptionsList.length > 0 ? ( + hasConnectors ? ( connectorsDropdown ) : ( { + const mockedActionParamsFields = React.lazy(async () => ({ + default() { + return ( + <> + true} + fullWidth + /> + + ); + }, + })); + + const actionItem = { + id: 'testId', + actionTypeId: '.pagerduty', + group: 'recovered', + params: { + eventAction: 'recovered', + dedupKey: undefined, + summary: '2323', + source: 'source', + severity: '1', + timestamp: new Date().toISOString(), + component: 'test', + group: 'group', + class: 'test class', + }, + }; + + const actionTypeIndex: Record = { + '.pagerduty': { + id: '.pagerduty', + enabled: true, + name: 'Test', + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'basic', + }, + }; + + const connectors = [ + { + actionTypeId: '.pagerduty', + config: { + apiUrl: 'http:\\test', + }, + id: 'testId', + isPreconfigured: false, + name: 'test pagerduty', + secrets: {}, + }, + ]; + + const actionType = actionTypeRegistryMock.createMockActionTypeModel({ + id: '.pagerduty', + iconClass: 'test', + selectMessage: 'test', + validateConnector: (): Promise> => { + return Promise.resolve({}); + }, + validateParams: (): Promise> => { + const validationResult = { errors: {} }; + return Promise.resolve(validationResult); + }, + actionConnectorFields: null, + actionParamsFields: mockedActionParamsFields, + }); + + beforeEach(() => {}); + + it('renders a selector', () => { + const wrapper = mountWithIntl( + + + + ); + + expect( + wrapper.find('[data-test-subj="selectActionConnector-.pagerduty-0"]').exists() + ).toBeTruthy(); + }); + + it('renders the title of the connector', () => { + render( + + + + ); + + expect(screen.queryAllByText('test pagerduty')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connectors_selection.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connectors_selection.tsx new file mode 100644 index 0000000000000..0964cd7387e80 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connectors_selection.tsx @@ -0,0 +1,141 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; +import React, { useCallback, useMemo, useState } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { ActionConnector, ActionTypeIndex, ActionTypeModel, AlertAction } from '../../../types'; +import { getValidConnectors } from '../common/connectors'; + +interface ConnectorOption { + title: string; + id: string; + prependComponent?: JSX.Element; +} + +interface SelectionProps { + actionItem: AlertAction; + accordionIndex: number; + actionTypesIndex: ActionTypeIndex; + actionTypeRegistered: ActionTypeModel; + connectors: ActionConnector[]; + onConnectorSelected: (id: string) => void; +} + +export const ConnectorsSelection = React.memo(ConnectorsSelectionComponent); + +function ConnectorsSelectionComponent({ + actionItem, + accordionIndex, + actionTypesIndex, + actionTypeRegistered, + connectors, + onConnectorSelected, +}: SelectionProps) { + const validConnectors = useMemo( + () => getValidConnectors(connectors, actionItem, actionTypesIndex), + [actionItem, actionTypesIndex, connectors] + ); + + const selectedConnectors = useMemo( + () => getValueOfSelectedConnector(actionItem.id, validConnectors, actionTypeRegistered), + [actionItem.id, validConnectors, actionTypeRegistered] + ); + + const options = useMemo( + () => createConnectorOptions(validConnectors, actionTypeRegistered), + [validConnectors, actionTypeRegistered] + ); + + const [selectedOption, setSelectedOption] = useState< + EuiComboBoxOptionOption | undefined + >(selectedConnectors.length > 0 ? selectedConnectors[0] : undefined); + + const onChange = useCallback( + (connectorOptions: Array>) => { + setSelectedOption(connectorOptions[0]); + onConnectorSelected(connectorOptions[0].value?.id ?? ''); + }, + [onConnectorSelected] + ); + + return ( + + ); +} + +const getValueOfSelectedConnector = ( + actionItemId: string, + connectors: ActionConnector[], + actionTypeRegistered: ActionTypeModel +): Array> => { + const selectedConnector = connectors.find((connector) => connector.id === actionItemId); + + if (!selectedConnector) { + return []; + } + + return [createOption(selectedConnector, actionTypeRegistered)]; +}; + +const createConnectorOptions = ( + connectors: ActionConnector[], + actionTypeRegistered: ActionTypeModel +): Array> => + connectors.map((connector) => createOption(connector, actionTypeRegistered)); + +const createOption = (connector: ActionConnector, actionTypeRegistered: ActionTypeModel) => { + const title = getTitle(connector, actionTypeRegistered); + + let prependComponent: JSX.Element | undefined; + + if (actionTypeRegistered.customConnectorSelectItem != null) { + const CustomPrependComponent = + actionTypeRegistered.customConnectorSelectItem.getComponent(connector); + if (CustomPrependComponent) { + prependComponent = ; + } + } + + return { + label: title, + value: { + title, + id: connector.id, + prependComponent, + }, + key: connector.id, + 'data-test-subj': `dropdown-connector-${connector.id}`, + }; +}; + +const getTitle = (connector: ActionConnector, actionTypeRegistered: ActionTypeModel) => { + if (actionTypeRegistered.customConnectorSelectItem != null) { + return actionTypeRegistered.customConnectorSelectItem.getText(connector); + } + + return connector.name; +}; + +const incidentManagemSystem = i18n.translate( + 'xpack.triggersActionsUI.sections.actionForm.incidentManagementSystemLabel', + { + defaultMessage: 'Incident management system', + } +); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx index 3c4e39554f485..d001d08bf63f3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx @@ -40,7 +40,6 @@ import { ActionConnectorTableItem, ActionTypeIndex, EditConectorTabs, - UserConfiguredActionConnector, } from '../../../../types'; import { EmptyConnectorsPrompt } from '../../../components/prompts/empty_connectors_prompt'; import { useKibana } from '../../../../common/lib/kibana'; @@ -48,6 +47,11 @@ import { DEFAULT_HIDDEN_ACTION_TYPES } from '../../../../'; import { CenterJustifiedSpinner } from '../../../components/center_justified_spinner'; import ConnectorEditFlyout from '../../action_connector_form/connector_edit_flyout'; import ConnectorAddFlyout from '../../action_connector_form/connector_add_flyout'; +import { + connectorDeprecatedMessage, + deprecatedMessage, + checkConnectorIsDeprecated, +} from '../../../../common/connectors_selection'; const ConnectorIconTipWithSpacing = withTheme(({ theme }: { theme: EuiTheme }) => { return ( @@ -65,10 +69,7 @@ const ConnectorIconTipWithSpacing = withTheme(({ theme }: { theme: EuiTheme }) = size="m" type="alert" color="warning" - content={i18n.translate( - 'xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.isDeprecatedDescription', - { defaultMessage: 'This connector is deprecated. Update it, or create a new one.' } - )} + content={connectorDeprecatedMessage} position="right" /> )} @@ -198,18 +199,12 @@ const ActionsConnectorsList: React.FunctionComponent = () => { actionTypesIndex && actionTypesIndex[item.actionTypeId] ); - const itemConfig = ( - item as UserConfiguredActionConnector, Record> - ).config; - /** * TODO: Remove when connectors can provide their own UX message. * Issue: https://github.com/elastic/kibana/issues/114507 */ - const hasSNApplication = - item?.actionTypeId === '.servicenow' || item?.actionTypeId === '.servicenow-sir'; - - const showDeprecatedTooltip = hasSNApplication && itemConfig?.usesTableApi; + const showDeprecatedTooltip = checkConnectorIsDeprecated(item); + const name = getConnectorName(value, item); const link = ( <> @@ -219,7 +214,7 @@ const ActionsConnectorsList: React.FunctionComponent = () => { key={item.id} disabled={actionTypesIndex ? !actionTypesIndex[item.actionTypeId]?.enabled : true} > - {value} + {name} {item.isMissingSecrets ? ( action.actionTypeId === actionTypeId).length; } +function getConnectorName(name: string, connector: ActionConnector): string { + return checkConnectorIsDeprecated(connector) ? `${name} ${deprecatedMessage}` : name; +} + const DeleteOperation: React.FunctionComponent<{ item: ActionConnectorTableItem; canDelete: boolean; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/connectors.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/connectors.ts new file mode 100644 index 0000000000000..4bf7f036ba10a --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/connectors.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ActionConnector, ActionTypeIndex, AlertAction } from '../../../types'; + +export const getValidConnectors = ( + connectors: ActionConnector[], + actionItem: AlertAction, + actionTypesIndex: ActionTypeIndex +): ActionConnector[] => { + const actionType = actionTypesIndex[actionItem.actionTypeId]; + + return connectors.filter( + (connector) => + connector.actionTypeId === actionItem.actionTypeId && + // include only enabled by config connectors or preconfigured + (actionType?.enabledInConfig || connector.isPreconfigured) + ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/connectors_seleciton.test.tsx b/x-pack/plugins/triggers_actions_ui/public/common/connectors_seleciton.test.tsx new file mode 100644 index 0000000000000..8e55b71699d65 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/connectors_seleciton.test.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { checkConnectorIsDeprecated } from './connectors_selection'; + +describe('Connectors select', () => { + describe('checkConnectorIsDeprecated', () => { + const connector = { + id: 'test', + actionTypeId: '.webhook', + name: 'Test', + config: { apiUrl: 'http://example.com', usesTableApi: false }, + secrets: { username: 'test', password: 'test' }, + isPreconfigured: false as const, + }; + + it('returns false if the connector is not defined', () => { + expect(checkConnectorIsDeprecated()).toBe(false); + }); + + it('returns false if the connector is not ITSM or SecOps', () => { + expect(checkConnectorIsDeprecated(connector)).toBe(false); + }); + + it('returns false if the connector is .servicenow and the usesTableApi=false', () => { + expect(checkConnectorIsDeprecated({ ...connector, actionTypeId: '.servicenow' })).toBe(false); + }); + + it('returns false if the connector is .servicenow-sir and the usesTableApi=false', () => { + expect(checkConnectorIsDeprecated({ ...connector, actionTypeId: '.servicenow-sir' })).toBe( + false + ); + }); + + it('returns true if the connector is .servicenow and the usesTableApi=true', () => { + expect( + checkConnectorIsDeprecated({ + ...connector, + actionTypeId: '.servicenow', + config: { ...connector.config, usesTableApi: true }, + }) + ).toBe(true); + }); + + it('returns true if the connector is .servicenow-sir and the usesTableApi=true', () => { + expect( + checkConnectorIsDeprecated({ + ...connector, + actionTypeId: '.servicenow-sir', + config: { ...connector.config, usesTableApi: true }, + }) + ).toBe(true); + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/common/connectors_selection.tsx b/x-pack/plugins/triggers_actions_ui/public/common/connectors_selection.tsx new file mode 100644 index 0000000000000..334a10c95e112 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/connectors_selection.tsx @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { ServiceNowActionConnector } from '../application/components/builtin_action_types/servicenow/types'; +import { ActionConnector, UserConfiguredActionConnector } from '../types'; + +export const preconfiguredMessage = i18n.translate( + 'xpack.triggersActionsUI.sections.actionForm.preconfiguredTitleMessage', + { + defaultMessage: '(preconfigured)', + } +); + +export const deprecatedMessage = i18n.translate( + 'xpack.triggersActionsUI.sections.deprecatedTitleMessage', + { + defaultMessage: '(deprecated)', + } +); + +export const connectorDeprecatedMessage = i18n.translate( + 'xpack.triggersActionsUI.sections.isDeprecatedDescription', + { defaultMessage: 'This connector is deprecated. Update it, or create a new one.' } +); + +export const checkConnectorIsDeprecated = ( + connector?: ActionConnector | ServiceNowActionConnector +): boolean => { + if (connector == null) { + return false; + } + + if ( + isConnectorWithConfig(connector) && + (connector.actionTypeId === '.servicenow' || connector.actionTypeId === '.servicenow-sir') + ) { + /** + * Connectors after the Elastic ServiceNow application use the + * Import Set API (https://developer.servicenow.com/dev.do#!/reference/api/rome/rest/c_ImportSetAPI) + * A ServiceNow connector is considered deprecated if it uses the Table API. + * + * All other connectors do not have the usesTableApi config property + * so the function will always return false for them. + */ + return !!connector.config.usesTableApi; + } + + return false; +}; + +type ConnectorWithUnknownConfig = UserConfiguredActionConnector< + Record, + Record +>; + +const isConnectorWithConfig = ( + connector: ActionConnector | ServiceNowActionConnector +): connector is ConnectorWithUnknownConfig => { + const unsafeConnector = connector as UserConfiguredActionConnector< + Record, + Record + >; + + return unsafeConnector.config != null; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index b61e8fbe40f7d..0902669c5694c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -118,6 +118,13 @@ export interface Sorting { direction: string; } +interface CustomConnectorSelectionItem { + getText: (actionConnector: ActionConnector) => string; + getComponent: ( + actionConnector: ActionConnector + ) => React.LazyExoticComponent> | undefined; +} + export interface ActionTypeModel { id: string; iconClass: IconType; @@ -135,6 +142,7 @@ export interface ActionTypeModel > | null; actionParamsFields: React.LazyExoticComponent>>; + customConnectorSelectItem?: CustomConnectorSelectionItem; } export interface GenericValidationResult { 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 4629fd9d5e56e..d350e88bcf248 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 @@ -369,7 +369,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(await testSubjects.exists('addNewActionConnectorActionGroup-0')).to.eql(false); expect(await testSubjects.exists('alertActionAccordion-0')).to.eql(true); - await comboBox.set('selectActionConnector-.slack-0', 'Slack#xyztest (preconfigured)'); + expect(await testSubjects.exists('selectActionConnector-.slack-0')).to.eql(true); + // click the super selector the reveal the options + await testSubjects.click('selectActionConnector-.slack-0'); + // click the available option (my-slack1 is a preconfigured connector created before this test runs) + await testSubjects.click('dropdown-connector-my-slack1'); expect(await testSubjects.exists('addNewActionConnectorActionGroup-0')).to.eql(true); });