From 7155bcfcb9937449c30817d6859f7eab781de236 Mon Sep 17 00:00:00 2001 From: Ersin Erdal <92688503+ersin-erdal@users.noreply.github.com> Date: Fri, 26 Aug 2022 14:37:17 +0200 Subject: [PATCH] Use "compatibility" rather than "availability" on connectors table (#139024) * Use compatibility rather than availability on connectors table * Replace Availability with Compatibility in create connector flyout. --- .../common/connector_feature_config.test.ts | 19 +- .../common/connector_feature_config.ts | 52 +- .../translations/translations/fr-FR.json | 2 - .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - .../create_connector_flyout/header.tsx | 19 +- .../create_connector_flyout/index.test.tsx | 7 +- .../create_connector_flyout/index.tsx | 3 +- .../actions_connectors_list.test.tsx | 1052 +++++++++-------- .../components/actions_connectors_list.tsx | 30 +- .../triggers_actions_ui/public/types.ts | 2 +- 11 files changed, 655 insertions(+), 535 deletions(-) diff --git a/x-pack/plugins/actions/common/connector_feature_config.test.ts b/x-pack/plugins/actions/common/connector_feature_config.test.ts index 078a9110564d4..5aea0a7c72bd8 100644 --- a/x-pack/plugins/actions/common/connector_feature_config.test.ts +++ b/x-pack/plugins/actions/common/connector_feature_config.test.ts @@ -5,7 +5,11 @@ * 2.0. */ -import { areValidFeatures, getConnectorFeatureName } from './connector_feature_config'; +import { + areValidFeatures, + getConnectorCompatibility, + getConnectorFeatureName, +} from './connector_feature_config'; describe('areValidFeatures', () => { it('returns true when all inputs are valid features', () => { @@ -35,3 +39,16 @@ describe('getConnectorFeatureName', () => { expect(getConnectorFeatureName('foo')).toEqual('foo'); }); }); + +describe('getConnectorCompatibility', () => { + it('returns the compatibility list for valid feature ids', () => { + expect(getConnectorCompatibility(['alerting', 'cases', 'uptime', 'siem'])).toEqual([ + 'Alerting Rules', + 'Cases', + ]); + }); + + it('skips invalid feature ids', () => { + expect(getConnectorCompatibility(['foo', 'bar', 'cases'])).toEqual(['Cases']); + }); +}); diff --git a/x-pack/plugins/actions/common/connector_feature_config.ts b/x-pack/plugins/actions/common/connector_feature_config.ts index 2395bc59b4034..6e9adab5de5a8 100644 --- a/x-pack/plugins/actions/common/connector_feature_config.ts +++ b/x-pack/plugins/actions/common/connector_feature_config.ts @@ -18,6 +18,7 @@ interface ConnectorFeatureConfig { * This will be displayed to end-users, so a translatable string is advised for i18n. */ name: string; + compatibility: string; } export const AlertingConnectorFeatureId = 'alerting'; @@ -25,11 +26,26 @@ export const CasesConnectorFeatureId = 'cases'; export const UptimeConnectorFeatureId = 'uptime'; export const SecurityConnectorFeatureId = 'siem'; +const compatibilityAlertingRules = i18n.translate( + 'xpack.actions.availableConnectorFeatures.compatibility.alertingRules', + { + defaultMessage: 'Alerting Rules', + } +); + +const compatibilityCases = i18n.translate( + 'xpack.actions.availableConnectorFeatures.compatibility.cases', + { + defaultMessage: 'Cases', + } +); + export const AlertingConnectorFeature: ConnectorFeatureConfig = { id: AlertingConnectorFeatureId, name: i18n.translate('xpack.actions.availableConnectorFeatures.alerting', { defaultMessage: 'Alerting', }), + compatibility: compatibilityAlertingRules, }; export const CasesConnectorFeature: ConnectorFeatureConfig = { @@ -37,6 +53,7 @@ export const CasesConnectorFeature: ConnectorFeatureConfig = { name: i18n.translate('xpack.actions.availableConnectorFeatures.cases', { defaultMessage: 'Cases', }), + compatibility: compatibilityCases, }; export const UptimeConnectorFeature: ConnectorFeatureConfig = { @@ -44,6 +61,7 @@ export const UptimeConnectorFeature: ConnectorFeatureConfig = { name: i18n.translate('xpack.actions.availableConnectorFeatures.uptime', { defaultMessage: 'Uptime', }), + compatibility: compatibilityAlertingRules, }; export const SecuritySolutionFeature: ConnectorFeatureConfig = { @@ -51,23 +69,35 @@ export const SecuritySolutionFeature: ConnectorFeatureConfig = { name: i18n.translate('xpack.actions.availableConnectorFeatures.securitySolution', { defaultMessage: 'Security Solution', }), + compatibility: compatibilityAlertingRules, }; -const AllAvailableConnectorFeatures: ConnectorFeatureConfig[] = [ - AlertingConnectorFeature, - CasesConnectorFeature, - UptimeConnectorFeature, - SecuritySolutionFeature, -]; +const AllAvailableConnectorFeatures = { + [AlertingConnectorFeature.id]: AlertingConnectorFeature, + [CasesConnectorFeature.id]: CasesConnectorFeature, + [UptimeConnectorFeature.id]: UptimeConnectorFeature, + [SecuritySolutionFeature.id]: SecuritySolutionFeature, +}; export function areValidFeatures(ids: string[]) { - return ids.every( - (id: string) => - !!AllAvailableConnectorFeatures.find((config: ConnectorFeatureConfig) => config.id === id) - ); + return ids.every((id: string) => !!AllAvailableConnectorFeatures[id]); } export function getConnectorFeatureName(id: string) { - const featureConfig = AllAvailableConnectorFeatures.find((config) => config.id === id); + const featureConfig = AllAvailableConnectorFeatures[id]; return featureConfig ? featureConfig.name : id; } + +export function getConnectorCompatibility(featureIds?: string[]): string[] { + const compatibility = new Set(); + + if (featureIds && featureIds.length > 0) { + for (const featureId of featureIds) { + if (AllAvailableConnectorFeatures[featureId]) { + compatibility.add(AllAvailableConnectorFeatures[featureId].compatibility); + } + } + } + + return Array.from(compatibility); +} diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 8d405c00cc551..6558334913435 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -32647,7 +32647,6 @@ "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.runConnectorDisabledDescription": "Impossible d'exécuter les connecteurs", "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.runConnectorName": "Exécuter", "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actionTypeTitle": "Type", - "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.featureIdsTitle": "Disponibilité", "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.fixButtonLabel": "Corriger", "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.nameTitle": "Nom", "xpack.triggersActionsUI.sections.actionsConnectorsList.filters.actionTypeIdName": "Type", @@ -32675,7 +32674,6 @@ "xpack.triggersActionsUI.sections.addAction.casesWebhookAction.error.requiredUpdateMethodText": "La méthode de mise à jour du cas est requise.", "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredAuthUserNameText": "Le nom d'utilisateur est requis.", "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredMethodText": "La méthode est requise.", - "xpack.triggersActionsUI.sections.addConnectorForm.flyoutHeaderAvailability": "Disponibilité :", "xpack.triggersActionsUI.sections.addConnectorForm.selectConnectorFlyoutTitle": "Sélectionner un connecteur", "xpack.triggersActionsUI.sections.addConnectorForm.updateSuccessNotificationText": "Création de \"{connectorName}\" effectuée", "xpack.triggersActionsUI.sections.addModalConnectorForm.cancelButtonLabel": "Annuler", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 92f7bb0888cda..5a6e17d82c29c 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -32622,7 +32622,6 @@ "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.runConnectorDisabledDescription": "コネクターを実行できません", "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.runConnectorName": "実行", "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actionTypeTitle": "型", - "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.featureIdsTitle": "可用性", "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.fixButtonLabel": "修正", "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.nameTitle": "名前", "xpack.triggersActionsUI.sections.actionsConnectorsList.filters.actionTypeIdName": "型", @@ -32650,7 +32649,6 @@ "xpack.triggersActionsUI.sections.addAction.casesWebhookAction.error.requiredUpdateMethodText": "ケースメソッドの更新は必須です。", "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredAuthUserNameText": "ユーザー名が必要です。", "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredMethodText": "メソッドが必要です。", - "xpack.triggersActionsUI.sections.addConnectorForm.flyoutHeaderAvailability": "可用性:", "xpack.triggersActionsUI.sections.addConnectorForm.selectConnectorFlyoutTitle": "コネクターを選択", "xpack.triggersActionsUI.sections.addConnectorForm.updateSuccessNotificationText": "「{connectorName}」を作成しました", "xpack.triggersActionsUI.sections.addModalConnectorForm.cancelButtonLabel": "キャンセル", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 4884259e023cb..107dcec2ce65b 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -32656,7 +32656,6 @@ "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.runConnectorDisabledDescription": "无法运行连接器", "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.runConnectorName": "运行", "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actionTypeTitle": "类型", - "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.featureIdsTitle": "可用性", "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.fixButtonLabel": "修复", "xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.nameTitle": "名称", "xpack.triggersActionsUI.sections.actionsConnectorsList.filters.actionTypeIdName": "类型", @@ -32684,7 +32683,6 @@ "xpack.triggersActionsUI.sections.addAction.casesWebhookAction.error.requiredUpdateMethodText": "“更新案例方法”必填。", "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredAuthUserNameText": "“用户名”必填。", "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredMethodText": "“方法”必填", - "xpack.triggersActionsUI.sections.addConnectorForm.flyoutHeaderAvailability": "可用性:", "xpack.triggersActionsUI.sections.addConnectorForm.selectConnectorFlyoutTitle": "选择连接器", "xpack.triggersActionsUI.sections.addConnectorForm.updateSuccessNotificationText": "已创建“{connectorName}”", "xpack.triggersActionsUI.sections.addModalConnectorForm.cancelButtonLabel": "取消", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/header.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/header.tsx index 6369ec5a2dcbf..2c12431dc12cb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/header.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/header.tsx @@ -19,14 +19,13 @@ import { EuiBetaBadge, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { getConnectorFeatureName } from '@kbn/actions-plugin/common'; import { betaBadgeProps } from '../beta_badge_props'; interface Props { icon?: IconType | null; actionTypeName?: string | null; actionTypeMessage?: string | null; - featureIds?: string[] | null; + compatibility?: string[] | null; isExperimental?: boolean; } @@ -34,7 +33,7 @@ const FlyoutHeaderComponent: React.FC = ({ icon, actionTypeName, actionTypeMessage, - featureIds, + compatibility, isExperimental, }) => { return ( @@ -74,23 +73,23 @@ const FlyoutHeaderComponent: React.FC = ({ {actionTypeMessage} - {featureIds && featureIds.length > 0 && ( + {compatibility && compatibility.length > 0 && ( <> {' '} - {featureIds.map((featureId: string) => ( - - {getConnectorFeatureName(featureId)} + {compatibility.map((compatibilityItem: string) => ( + + {compatibilityItem} ))} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/index.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/index.test.tsx index 722667921ad26..f432f8fd6634c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/index.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/index.test.tsx @@ -246,7 +246,7 @@ describe('CreateConnectorFlyout', () => { expect(getByText('Select a connector')).toBeInTheDocument(); }); - it('shows the feature id badges when the connector type is selected', async () => { + it('shows the compatibility badges when the connector type is selected', async () => { const { getByTestId, getByText } = appMockRenderer.render( { expect(getByTestId('test-connector-text-field')).toBeInTheDocument(); }); - expect(getByTestId('create-connector-flyout-header-availability')).toBeInTheDocument(); - expect(getByText('Alerting')).toBeInTheDocument(); - expect(getByText('Security Solution')).toBeInTheDocument(); + expect(getByTestId('create-connector-flyout-header-compatibility')).toBeInTheDocument(); + expect(getByText('Alerting Rules')).toBeInTheDocument(); }); it('shows the icon when the connector type is selected', async () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/index.tsx index f34c5d4e6dc76..f25d86458632a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/index.tsx @@ -8,6 +8,7 @@ import React, { memo, ReactNode, useCallback, useEffect, useRef, useState } from 'react'; import { EuiFlyout, EuiFlyoutBody } from '@elastic/eui'; +import { getConnectorCompatibility } from '@kbn/actions-plugin/common'; import { ActionConnector, ActionType, @@ -156,7 +157,7 @@ const CreateConnectorFlyoutComponent: React.FC = ({ icon={actionTypeModel?.iconClass} actionTypeName={actionType?.name} actionTypeMessage={actionTypeModel?.selectMessage} - featureIds={actionType?.supportedFeatureIds} + compatibility={getConnectorCompatibility(actionType?.supportedFeatureIds)} isExperimental={actionTypeModel?.isExperimental} /> { - let wrapper: ReactWrapper; +describe('actions_connectors_list', () => { + describe('component empty', () => { + let wrapper: ReactWrapper; - async function setup() { - loadAllActions.mockResolvedValueOnce([]); - loadActionTypes.mockResolvedValueOnce([ - { - id: 'test', - name: 'Test', - supportedFeatureIds: ['alerting'], - }, - { - id: 'test2', - name: 'Test2', - supportedFeatureIds: ['alerting'], - }, - ]); - actionTypeRegistry.has.mockReturnValue(true); - - const [ - { - application: { capabilities }, - }, - ] = await mocks.getStartServices(); - - // eslint-disable-next-line react-hooks/rules-of-hooks - useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; - // eslint-disable-next-line react-hooks/rules-of-hooks - useKibanaMock().services.application.capabilities = { - ...capabilities, - actions: { - delete: true, - save: true, - show: true, - }, - }; - wrapper = mountWithIntl(); - - // Wait for active space to resolve before requesting the component to update - await act(async () => { - await nextTick(); - wrapper.update(); + async function setup() { + loadAllActions.mockResolvedValueOnce([]); + loadActionTypes.mockResolvedValueOnce([ + { + id: 'test', + name: 'Test', + supportedFeatureIds: ['alerting'], + }, + { + id: 'test2', + name: 'Test2', + supportedFeatureIds: ['alerting'], + }, + ]); + actionTypeRegistry.has.mockReturnValue(true); + + const [ + { + application: { capabilities }, + }, + ] = await mocks.getStartServices(); + + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.application.capabilities = { + ...capabilities, + actions: { + delete: true, + save: true, + show: true, + }, + }; + wrapper = mountWithIntl(); + + // Wait for active space to resolve before requesting the component to update + await act(async () => { + await nextTick(); + wrapper.update(); + }); + } + + it('renders empty prompt', async () => { + await setup(); + expect( + wrapper.find('[data-test-subj="createFirstConnectorEmptyPrompt"]').find('EuiEmptyPrompt') + ).toHaveLength(1); + expect( + wrapper.find('[data-test-subj="createFirstActionButton"]').find('EuiButton') + ).toHaveLength(1); }); - } - - it('renders empty prompt', async () => { - await setup(); - expect( - wrapper.find('[data-test-subj="createFirstConnectorEmptyPrompt"]').find('EuiEmptyPrompt') - ).toHaveLength(1); - expect( - wrapper.find('[data-test-subj="createFirstActionButton"]').find('EuiButton') - ).toHaveLength(1); - }); - test('if click create button should render CreateConnectorFlyout', async () => { - await setup(); - wrapper.find('[data-test-subj="createFirstActionButton"]').first().simulate('click'); - expect(wrapper.find('[data-test-subj="create-connector-flyout"]').exists()).toBeTruthy(); + test('if click create button should render CreateConnectorFlyout', async () => { + await setup(); + wrapper.find('[data-test-subj="createFirstActionButton"]').first().simulate('click'); + expect(wrapper.find('[data-test-subj="create-connector-flyout"]').exists()).toBeTruthy(); + }); }); -}); - -describe('actions_connectors_list component with items', () => { - let wrapper: ReactWrapper; - async function setup(actionConnectors?: ActionConnector[]) { - loadAllActions.mockResolvedValueOnce( - actionConnectors ?? [ + describe('component with items', () => { + let wrapper: ReactWrapper; + + async function setup(actionConnectors?: ActionConnector[]) { + loadAllActions.mockResolvedValueOnce( + actionConnectors ?? [ + { + id: '1', + actionTypeId: 'test', + description: 'My test', + isPreconfigured: false, + isDeprecated: false, + referencedByCount: 1, + config: {}, + }, + { + id: '2', + actionTypeId: 'test2', + description: 'My test 2', + referencedByCount: 1, + isPreconfigured: false, + isDeprecated: false, + config: {}, + }, + { + id: '3', + actionTypeId: 'test2', + description: 'My preconfigured test 2', + isMissingSecrets: true, + referencedByCount: 1, + isPreconfigured: true, + isDeprecated: false, + config: {}, + }, + { + id: '4', + actionTypeId: 'nonexistent', + description: 'My invalid connector type', + referencedByCount: 1, + isPreconfigured: false, + isDeprecated: false, + config: {}, + }, + { + id: '5', + actionTypeId: 'test3', + description: 'action with all feature ids', + referencedByCount: 1, + isPreconfigured: false, + isDeprecated: false, + config: {}, + }, + { + id: '6', + actionTypeId: 'test4', + description: 'only cases', + referencedByCount: 1, + isPreconfigured: false, + isDeprecated: false, + config: {}, + }, + ] + ); + loadActionTypes.mockResolvedValueOnce([ { - id: '1', - actionTypeId: 'test', - description: 'My test', - isPreconfigured: false, - isDeprecated: false, - referencedByCount: 1, - config: {}, + id: 'test', + name: 'Test', + enabled: true, + supportedFeatureIds: ['alerting'], }, { - id: '2', - actionTypeId: 'test2', - description: 'My test 2', - referencedByCount: 1, - isPreconfigured: false, - isDeprecated: false, - config: {}, + id: 'test2', + name: 'Test2', + enabled: true, + supportedFeatureIds: ['alerting', 'cases'], }, { - id: '3', - actionTypeId: 'test2', - description: 'My preconfigured test 2', - isMissingSecrets: true, - referencedByCount: 1, - isPreconfigured: true, - isDeprecated: false, - config: {}, + id: 'test3', + name: 'Test3', + enabled: true, + supportedFeatureIds: ['alerting', 'cases', 'siem', 'uptime'], }, { - id: '4', - actionTypeId: 'nonexistent', - description: 'My invalid connector type', - referencedByCount: 1, - isPreconfigured: false, - isDeprecated: false, - config: {}, + id: 'test4', + name: 'Test4', + enabled: true, + supportedFeatureIds: ['cases'], }, - ] - ); - loadActionTypes.mockResolvedValueOnce([ - { - id: 'test', - name: 'Test', - enabled: true, - supportedFeatureIds: ['alerting'], - }, - { - id: 'test2', - name: 'Test2', - enabled: true, - supportedFeatureIds: ['alerting', 'cases'], - }, - ]); - - const [ - { - application: { capabilities }, - }, - ] = await mocks.getStartServices(); - - const mockedActionParamsFields = React.lazy(async () => ({ - default() { - return <>; - }, - })); - - actionTypeRegistry.get.mockReturnValue({ - id: 'test', - iconClass: 'test', - selectMessage: 'test', - validateParams: (): Promise> => { - const validationResult = { errors: {} }; - return Promise.resolve(validationResult); - }, - actionConnectorFields: null, - actionParamsFields: mockedActionParamsFields, - }); - // eslint-disable-next-line react-hooks/rules-of-hooks - useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; - // eslint-disable-next-line react-hooks/rules-of-hooks - useKibanaMock().services.application.capabilities = { - ...capabilities, - actions: { - delete: true, - save: true, - show: true, - }, - }; - wrapper = mountWithIntl(); - - // Wait for active space to resolve before requesting the component to update - await act(async () => { - await nextTick(); - wrapper.update(); - }); - - expect(loadAllActions).toHaveBeenCalled(); - } + ]); - it('renders table of connectors', async () => { - await setup(); - expect(wrapper.find('EuiInMemoryTable')).toHaveLength(1); - expect(wrapper.find('EuiTableRow')).toHaveLength(4); + const [ + { + application: { capabilities }, + }, + ] = await mocks.getStartServices(); - const featureIdsBadges = wrapper.find( - 'EuiBadge[data-test-subj="connectorsTableCell-featureIds"]' - ); - expect(featureIdsBadges).toHaveLength(5); + const mockedActionParamsFields = React.lazy(async () => ({ + default() { + return <>; + }, + })); - expect(uniq(featureIdsBadges.map((badge) => badge.text()))).toEqual(['Alerting', 'Cases']); - }); + actionTypeRegistry.get.mockReturnValue({ + id: 'test', + iconClass: 'test', + selectMessage: 'test', + validateParams: (): Promise> => { + const validationResult = { errors: {} }; + return Promise.resolve(validationResult); + }, + actionConnectorFields: null, + actionParamsFields: mockedActionParamsFields, + }); + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.application.capabilities = { + ...capabilities, + actions: { + delete: true, + save: true, + show: true, + }, + }; + wrapper = mountWithIntl(); + + // Wait for active space to resolve before requesting the component to update + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(loadAllActions).toHaveBeenCalled(); + } + + it('renders table of connectors', async () => { + await setup(); + expect(wrapper.find('EuiInMemoryTable')).toHaveLength(1); + expect(wrapper.find('EuiTableRow')).toHaveLength(6); + + expect( + wrapper + .find('tr[data-test-subj="connectors-row"]') + .at(0) + .find('div[data-test-subj="compatibility-content"]') + .text() + ).toBe('Alerting Rules'); + + expect( + wrapper + .find('tr[data-test-subj="connectors-row"]') + .at(1) + .find('div[data-test-subj="compatibility-content"]') + .text() + ).toBe('Alerting RulesCases'); + + expect( + wrapper + .find('tr[data-test-subj="connectors-row"]') + .at(2) + .find('div[data-test-subj="compatibility-content"]') + .text() + ).toBe('Alerting RulesCases'); + + expect( + wrapper + .find('tr[data-test-subj="connectors-row"]') + .at(3) + .find('div[data-test-subj="compatibility-content"]') + .text() + ).toBe(''); + + expect( + wrapper + .find('tr[data-test-subj="connectors-row"]') + .at(4) + .find('div[data-test-subj="compatibility-content"]') + .text() + ).toBe('Alerting RulesCases'); + + expect( + wrapper + .find('tr[data-test-subj="connectors-row"]') + .at(5) + .find('div[data-test-subj="compatibility-content"]') + .text() + ).toBe('Cases'); + }); - it('renders table with preconfigured connectors', async () => { - await setup(); - expect(wrapper.find('[data-test-subj="preConfiguredTitleMessage"]')).toHaveLength(2); - }); + it('renders table with preconfigured connectors', async () => { + await setup(); + expect(wrapper.find('[data-test-subj="preConfiguredTitleMessage"]')).toHaveLength(2); + }); - it('renders unknown connector type as disabled', async () => { - await setup(); - expect(wrapper.find('button[data-test-subj="edit4"]').getDOMNode()).toBeDisabled(); - expect( - wrapper.find('button[data-test-subj="deleteConnector"]').last().getDOMNode() - ).not.toBeDisabled(); - expect( - wrapper.find('button[data-test-subj="runConnector"]').last().getDOMNode() - ).toBeDisabled(); - }); + it('renders unknown connector type as disabled', async () => { + await setup(); + expect(wrapper.find('button[data-test-subj="edit4"]').getDOMNode()).toBeDisabled(); + expect( + wrapper.find('button[data-test-subj="deleteConnector"]').last().getDOMNode() + ).not.toBeDisabled(); + expect( + wrapper.find('button[data-test-subj="runConnector"]').last().getDOMNode() + ).toBeDisabled(); + }); - it('renders fix button when connector secrets is missing', async () => { - await setup(); - expect( - wrapper.find('button[data-test-subj="deleteConnector"]').last().getDOMNode() - ).not.toBeDisabled(); - expect( - wrapper.find('button[data-test-subj="fixConnectorButton"]').last().getDOMNode() - ).not.toBeDisabled(); - }); + it('renders fix button when connector secrets is missing', async () => { + await setup(); + expect( + wrapper.find('button[data-test-subj="deleteConnector"]').last().getDOMNode() + ).not.toBeDisabled(); + expect( + wrapper.find('button[data-test-subj="fixConnectorButton"]').last().getDOMNode() + ).not.toBeDisabled(); + }); - it('supports pagination', async () => { - await setup( - times(15, (index) => ({ - id: `connector${index}`, - actionTypeId: 'test', - name: `My test ${index}`, - secrets: {}, - description: `My test ${index}`, - isPreconfigured: false, - isDeprecated: false, - referencedByCount: 1, - config: {}, - })) - ); - expect(wrapper.find('[data-test-subj="actionsTable"]').first().prop('pagination')) - .toMatchInlineSnapshot(` + it('supports pagination', async () => { + await setup( + times(15, (index) => ({ + id: `connector${index}`, + actionTypeId: 'test', + name: `My test ${index}`, + secrets: {}, + description: `My test ${index}`, + isPreconfigured: false, + isDeprecated: false, + referencedByCount: 1, + config: {}, + })) + ); + expect(wrapper.find('[data-test-subj="actionsTable"]').first().prop('pagination')) + .toMatchInlineSnapshot(` Object { "initialPageIndex": 0, "pageIndex": 0, } `); - wrapper.find('[data-test-subj="pagination-button-1"]').first().simulate('click'); - expect(wrapper.find('[data-test-subj="actionsTable"]').first().prop('pagination')) - .toMatchInlineSnapshot(` + wrapper.find('[data-test-subj="pagination-button-1"]').first().simulate('click'); + expect(wrapper.find('[data-test-subj="actionsTable"]').first().prop('pagination')) + .toMatchInlineSnapshot(` Object { "initialPageIndex": 0, "pageIndex": 1, } `); - }); + }); - test('if select item for edit should render EditConnectorFlyout', async () => { - await setup(); - await wrapper.find('[data-test-subj="edit1"]').first().find('button').simulate('click'); - expect(wrapper.find('[data-test-subj="edit-connector-flyout"]').exists()).toBeTruthy(); - }); + test('if select item for edit should render EditConnectorFlyout', async () => { + await setup(); + await wrapper.find('[data-test-subj="edit1"]').first().find('button').simulate('click'); + expect(wrapper.find('[data-test-subj="edit-connector-flyout"]').exists()).toBeTruthy(); + }); - test('if delete item that is used in a rule should show a warning in the popup', async () => { - await setup(); - await wrapper.find('.euiButtonIcon').last().simulate('click'); - expect(wrapper.find('[data-test-subj="deleteConnectorsConfirmation"]').exists()).toBeTruthy(); - expect( - wrapper - .find('[data-test-subj="deleteConnectorsConfirmation"]') - .text() - .includes('This connector is used in a rule') - ); + test('if delete item that is used in a rule should show a warning in the popup', async () => { + await setup(); + await wrapper.find('.euiButtonIcon').last().simulate('click'); + expect(wrapper.find('[data-test-subj="deleteConnectorsConfirmation"]').exists()).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="deleteConnectorsConfirmation"]') + .text() + .includes('This connector is used in a rule') + ); + }); }); -}); -describe('actions_connectors_list component empty with show only capability', () => { - let wrapper: ReactWrapper; + describe('component empty with show only capability', () => { + let wrapper: ReactWrapper; - async function setup() { - loadAllActions.mockResolvedValueOnce([]); - loadActionTypes.mockResolvedValueOnce([ - { - id: 'test', - name: 'Test', - supportedFeatureIds: ['alerting'], - }, - { - id: 'test2', - name: 'Test2', - supportedFeatureIds: ['alerting'], - }, - ]); - const [ - { - application: { capabilities }, - }, - ] = await mocks.getStartServices(); - - // eslint-disable-next-line react-hooks/rules-of-hooks - useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; - // eslint-disable-next-line react-hooks/rules-of-hooks - useKibanaMock().services.application.capabilities = { - ...capabilities, - actions: { - show: true, - save: false, - delete: false, - }, - }; - wrapper = mountWithIntl(); - - // Wait for active space to resolve before requesting the component to update - await act(async () => { - await nextTick(); - wrapper.update(); + async function setup() { + loadAllActions.mockResolvedValueOnce([]); + loadActionTypes.mockResolvedValueOnce([ + { + id: 'test', + name: 'Test', + supportedFeatureIds: ['alerting'], + }, + { + id: 'test2', + name: 'Test2', + supportedFeatureIds: ['alerting'], + }, + ]); + const [ + { + application: { capabilities }, + }, + ] = await mocks.getStartServices(); + + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.application.capabilities = { + ...capabilities, + actions: { + show: true, + save: false, + delete: false, + }, + }; + wrapper = mountWithIntl(); + + // Wait for active space to resolve before requesting the component to update + await act(async () => { + await nextTick(); + wrapper.update(); + }); + } + + it('renders no permissions to create connector', async () => { + await setup(); + expect(wrapper.find('[defaultMessage="No permissions to create connectors"]')).toHaveLength( + 1 + ); + expect(wrapper.find('[data-test-subj="createActionButton"]')).toHaveLength(0); }); - } - - it('renders no permissions to create connector', async () => { - await setup(); - expect(wrapper.find('[defaultMessage="No permissions to create connectors"]')).toHaveLength(1); - expect(wrapper.find('[data-test-subj="createActionButton"]')).toHaveLength(0); }); -}); -describe('actions_connectors_list with show only capability', () => { - let wrapper: ReactWrapper; - - async function setup() { - loadAllActions.mockResolvedValueOnce([ - { - id: '1', - actionTypeId: 'test', - description: 'My test', - referencedByCount: 1, - config: {}, - }, - { - id: '2', - actionTypeId: 'test2', - description: 'My test 2', - referencedByCount: 1, - config: {}, - }, - ]); - loadActionTypes.mockResolvedValueOnce([ - { - id: 'test', - name: 'Test', - supportedFeatureIds: ['alerting'], - }, - { - id: 'test2', - name: 'Test2', - supportedFeatureIds: ['alerting'], - }, - ]); - const [ - { - application: { capabilities }, - }, - ] = await mocks.getStartServices(); - - // eslint-disable-next-line react-hooks/rules-of-hooks - useKibanaMock().services.application.capabilities = { - ...capabilities, - actions: { - show: true, - save: false, - delete: false, - }, - }; - // eslint-disable-next-line react-hooks/rules-of-hooks - useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; - wrapper = mountWithIntl(); - - // Wait for active space to resolve before requesting the component to update - await act(async () => { - await nextTick(); - wrapper.update(); - }); - } - - it('renders table of connectors with delete button disabled', async () => { - await setup(); - expect(wrapper.find('EuiInMemoryTable')).toHaveLength(1); - expect(wrapper.find('EuiTableRow')).toHaveLength(2); - wrapper.find('EuiTableRow').forEach((elem) => { - const deleteButton = elem.find('[data-test-subj="deleteConnector"]').first(); - expect(deleteButton).toBeTruthy(); - expect(deleteButton.prop('isDisabled')).toBeTruthy(); + describe('with show only capability', () => { + let wrapper: ReactWrapper; + + async function setup() { + loadAllActions.mockResolvedValueOnce([ + { + id: '1', + actionTypeId: 'test', + description: 'My test', + referencedByCount: 1, + config: {}, + }, + { + id: '2', + actionTypeId: 'test2', + description: 'My test 2', + referencedByCount: 1, + config: {}, + }, + ]); + loadActionTypes.mockResolvedValueOnce([ + { + id: 'test', + name: 'Test', + supportedFeatureIds: ['alerting'], + }, + { + id: 'test2', + name: 'Test2', + supportedFeatureIds: ['alerting'], + }, + ]); + const [ + { + application: { capabilities }, + }, + ] = await mocks.getStartServices(); + + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.application.capabilities = { + ...capabilities, + actions: { + show: true, + save: false, + delete: false, + }, + }; + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; + wrapper = mountWithIntl(); + + // Wait for active space to resolve before requesting the component to update + await act(async () => { + await nextTick(); + wrapper.update(); + }); + } + + it('renders table of connectors with delete button disabled', async () => { + await setup(); + expect(wrapper.find('EuiInMemoryTable')).toHaveLength(1); + expect(wrapper.find('EuiTableRow')).toHaveLength(2); + wrapper.find('EuiTableRow').forEach((elem) => { + const deleteButton = elem.find('[data-test-subj="deleteConnector"]').first(); + expect(deleteButton).toBeTruthy(); + expect(deleteButton.prop('isDisabled')).toBeTruthy(); + }); }); }); -}); -describe('actions_connectors_list component with disabled items', () => { - let wrapper: ReactWrapper; - - async function setup() { - loadAllActions.mockResolvedValueOnce([ - { - id: '1', - actionTypeId: 'test', - description: 'My test', - referencedByCount: 1, - config: {}, - }, - { - id: '2', - actionTypeId: 'test2', - description: 'My test 2', - referencedByCount: 1, - config: {}, - }, - ]); - loadActionTypes.mockResolvedValueOnce([ - { - id: 'test', - name: 'Test', - enabled: false, - enabledInConfig: false, - enabledInLicense: true, - supportedFeatureIds: ['alerting'], - }, - { - id: 'test2', - name: 'Test2', - enabled: false, - enabledInConfig: true, - enabledInLicense: false, - supportedFeatureIds: ['alerting'], - }, - ]); - - const [ - { - application: { capabilities }, - }, - ] = await mocks.getStartServices(); - - // eslint-disable-next-line react-hooks/rules-of-hooks - useKibanaMock().services.application.capabilities = { - ...capabilities, - actions: { - show: true, - save: true, - delete: true, - }, - }; - // eslint-disable-next-line react-hooks/rules-of-hooks - useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; - wrapper = mountWithIntl(); - - // Wait for active space to resolve before requesting the component to update - await act(async () => { - await nextTick(); - wrapper.update(); - }); + describe('component with disabled items', () => { + let wrapper: ReactWrapper; - expect(loadAllActions).toHaveBeenCalled(); - } - - it('renders table of connectors', async () => { - await setup(); - expect(wrapper.find('EuiInMemoryTable')).toHaveLength(1); - expect(wrapper.find('EuiTableRow')).toHaveLength(2); - expect(wrapper.find('EuiTableRow').at(0).prop('className')).toEqual( - 'actConnectorsList__tableRowDisabled' - ); - expect(wrapper.find('EuiTableRow').at(1).prop('className')).toEqual( - 'actConnectorsList__tableRowDisabled' - ); - }); -}); + async function setup() { + loadAllActions.mockResolvedValueOnce([ + { + id: '1', + actionTypeId: 'test', + description: 'My test', + referencedByCount: 1, + config: {}, + }, + { + id: '2', + actionTypeId: 'test2', + description: 'My test 2', + referencedByCount: 1, + config: {}, + }, + ]); + loadActionTypes.mockResolvedValueOnce([ + { + id: 'test', + name: 'Test', + enabled: false, + enabledInConfig: false, + enabledInLicense: true, + supportedFeatureIds: ['alerting'], + }, + { + id: 'test2', + name: 'Test2', + enabled: false, + enabledInConfig: true, + enabledInLicense: false, + supportedFeatureIds: ['alerting'], + }, + ]); -describe('actions_connectors_list component with deprecated connectors', () => { - let wrapper: ReactWrapper; - - async function setup() { - loadAllActions.mockResolvedValueOnce([ - { - id: '1', - actionTypeId: '.servicenow', - description: 'My test', - referencedByCount: 1, - config: { usesTableApi: true }, - isDeprecated: true, - }, - { - id: '2', - actionTypeId: '.servicenow-sir', - description: 'My test 2', - referencedByCount: 1, - config: { usesTableApi: true }, - isDeprecated: true, - }, - ]); - loadActionTypes.mockResolvedValueOnce([ - { - id: 'test', - name: '.servicenow', - enabled: false, - enabledInConfig: false, - enabledInLicense: true, - supportedFeatureIds: ['alerting'], - }, - { - id: 'test2', - name: '.servicenow-sir', - enabled: false, - enabledInConfig: true, - enabledInLicense: false, - supportedFeatureIds: ['alerting'], - }, - ]); - - const [ - { - application: { capabilities }, - }, - ] = await mocks.getStartServices(); - - // eslint-disable-next-line react-hooks/rules-of-hooks - useKibanaMock().services.application.capabilities = { - ...capabilities, - actions: { - show: true, - save: true, - delete: true, - }, - }; - // eslint-disable-next-line react-hooks/rules-of-hooks - useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; - wrapper = mountWithIntl( - ({ eui: { euiSizeS: '15px' }, darkMode: true })}> - - - ); - - // Wait for active space to resolve before requesting the component to update - await act(async () => { - await nextTick(); - wrapper.update(); + const [ + { + application: { capabilities }, + }, + ] = await mocks.getStartServices(); + + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.application.capabilities = { + ...capabilities, + actions: { + show: true, + save: true, + delete: true, + }, + }; + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; + wrapper = mountWithIntl(); + + // Wait for active space to resolve before requesting the component to update + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(loadAllActions).toHaveBeenCalled(); + } + + it('renders table of connectors', async () => { + await setup(); + expect(wrapper.find('EuiInMemoryTable')).toHaveLength(1); + expect(wrapper.find('EuiTableRow')).toHaveLength(2); + expect(wrapper.find('EuiTableRow').at(0).prop('className')).toEqual( + 'actConnectorsList__tableRowDisabled' + ); + expect(wrapper.find('EuiTableRow').at(1).prop('className')).toEqual( + 'actConnectorsList__tableRowDisabled' + ); }); + }); + + describe('component with deprecated connectors', () => { + let wrapper: ReactWrapper; - expect(loadAllActions).toHaveBeenCalled(); - } + async function setup() { + loadAllActions.mockResolvedValueOnce([ + { + id: '1', + actionTypeId: '.servicenow', + description: 'My test', + referencedByCount: 1, + config: { usesTableApi: true }, + isDeprecated: true, + }, + { + id: '2', + actionTypeId: '.servicenow-sir', + description: 'My test 2', + referencedByCount: 1, + config: { usesTableApi: true }, + isDeprecated: true, + }, + ]); + loadActionTypes.mockResolvedValueOnce([ + { + id: 'test', + name: '.servicenow', + enabled: false, + enabledInConfig: false, + enabledInLicense: true, + supportedFeatureIds: ['alerting'], + }, + { + id: 'test2', + name: '.servicenow-sir', + enabled: false, + enabledInConfig: true, + enabledInLicense: false, + supportedFeatureIds: ['alerting'], + }, + ]); - it('shows the warning icon', async () => { - await setup(); - expect(wrapper.find('EuiInMemoryTable')).toHaveLength(1); - expect(wrapper.find('EuiTableRow')).toHaveLength(2); - expect(wrapper.find('.euiToolTipAnchor [aria-label="Warning"]').exists()).toBe(true); + const [ + { + application: { capabilities }, + }, + ] = await mocks.getStartServices(); + + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.application.capabilities = { + ...capabilities, + actions: { + show: true, + save: true, + delete: true, + }, + }; + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; + wrapper = mountWithIntl( + ({ eui: { euiSizeS: '15px' }, darkMode: true })}> + + + ); + + // Wait for active space to resolve before requesting the component to update + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(loadAllActions).toHaveBeenCalled(); + } + + it('shows the warning icon', async () => { + await setup(); + expect(wrapper.find('EuiInMemoryTable')).toHaveLength(1); + expect(wrapper.find('EuiTableRow')).toHaveLength(2); + expect(wrapper.find('.euiToolTipAnchor [aria-label="Warning"]').exists()).toBe(true); + }); }); }); 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 5c58ac5dcff56..249f9f503fcf8 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 @@ -27,7 +27,7 @@ import { i18n } from '@kbn/i18n'; import { omit } from 'lodash'; import { FormattedMessage } from '@kbn/i18n-react'; import { withTheme, EuiTheme } from '@kbn/kibana-react-plugin/common'; -import { getConnectorFeatureName } from '@kbn/actions-plugin/common'; +import { getConnectorCompatibility } from '@kbn/actions-plugin/common'; import { loadAllActions, loadActionTypes, deleteActions } from '../../../lib/action_connector_api'; import { hasDeleteActionsCapability, @@ -138,8 +138,8 @@ const ActionsConnectorsList: React.FunctionComponent = () => { actionType: actionTypesIndex[action.actionTypeId] ? actionTypesIndex[action.actionTypeId].name : action.actionTypeId, - featureIds: actionTypesIndex[action.actionTypeId] - ? actionTypesIndex[action.actionTypeId].supportedFeatureIds + compatibility: actionTypesIndex[action.actionTypeId] + ? getConnectorCompatibility(actionTypesIndex[action.actionTypeId].supportedFeatureIds) : [], }; }) @@ -272,22 +272,28 @@ const ActionsConnectorsList: React.FunctionComponent = () => { truncateText: true, }, { - field: 'featureIds', + field: 'compatibility', + 'data-test-subj': 'connectorsTableCell-compatibility', name: i18n.translate( - 'xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.featureIdsTitle', + 'xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.compatibility', { - defaultMessage: 'Availability', + defaultMessage: 'Compatibility', } ), sortable: false, truncateText: true, - render: (availability: string[]) => { + render: (compatibility: string[]) => { return ( - - {(availability ?? []).map((featureId: string) => ( - - - {getConnectorFeatureName(featureId)} + + {compatibility.map((compatibilityItem: string) => ( + + + {compatibilityItem} ))} diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index b6011b92de871..ac2a80212825b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -246,7 +246,7 @@ export type ActionConnectorWithoutId< export type ActionConnectorTableItem = ActionConnector & { actionType: ActionType['name']; - featureIds: ActionType['supportedFeatureIds']; + compatibility: string[]; }; type AsActionVariables = {