From f00314913adedccfd095a3d5aa2c105a606a28fe Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Thu, 5 Mar 2020 17:10:30 -0800 Subject: [PATCH] Added possibility to embed connectors create and edit flyouts (#58514) (#59496) * Added possibility to embed connectors flyout * Fixed type checks and removed example from siem start page * Fixed jest tests * Fixed failing tests * fixed type check * Added config for siem tests * Fixed failing tests * Fixed due to comments * Added missing documentation --- x-pack/plugins/triggers_actions_ui/README.md | 217 +++++++++++++++++- .../context/actions_connectors_context.tsx | 18 +- .../action_connector_form.test.tsx | 13 +- .../action_type_menu.test.tsx | 42 ++-- .../action_type_menu.tsx | 44 +++- .../connector_add_flyout.test.tsx | 60 +++-- .../connector_add_flyout.tsx | 61 +++-- .../connector_add_modal.test.tsx | 50 +--- .../connector_edit_flyout.test.tsx | 27 +-- .../connector_edit_flyout.tsx | 48 ++-- .../components/actions_connectors_list.tsx | 20 +- .../triggers_actions_ui/public/index.ts | 5 + .../triggers_actions_ui/public/plugin.ts | 73 +++--- .../apps/triggers_actions_ui/alerts.ts | 3 + 14 files changed, 452 insertions(+), 229 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/README.md b/x-pack/plugins/triggers_actions_ui/README.md index c6a7808356b86..ccd33c99f9e1c 100644 --- a/x-pack/plugins/triggers_actions_ui/README.md +++ b/x-pack/plugins/triggers_actions_ui/README.md @@ -43,6 +43,8 @@ Table of Contents - [Action type model definition](#action-type-model-definition) - [Register action type model](#register-action-type-model) - [Create and register new action type UI example](#reate-and-register-new-action-type-ui-example) + - [Embed the Create Connector flyout within any Kibana plugin](#embed-the-create-connector-flyout-within-any-kibana-plugin) + - [Embed the Edit Connector flyout within any Kibana plugin](#embed-the-edit-connector-flyout-within-any-kibana-plugin) ## Built-in Alert Types @@ -667,6 +669,7 @@ const [alertFlyoutVisible, setAlertFlyoutVisibility] = useState(false); uiSettings, charts, dataFieldsFormats, + metadata: { test: 'some value', fields: ['test'] }, }} > @@ -690,7 +693,7 @@ interface AlertAddProps { AlertsContextProvider value options: ``` -export interface AlertsContextValue { +export interface AlertsContextValue> { addFlyoutVisible: boolean; setAddFlyoutVisibility: React.Dispatch>; reloadAlerts?: () => Promise; @@ -704,6 +707,7 @@ export interface AlertsContextValue { >; charts?: ChartsPluginSetup; dataFieldsFormats?: Pick; + metadata?: MetaData; } ``` @@ -719,6 +723,7 @@ export interface AlertsContextValue { |toastNotifications|Optional toast messages.| |charts|Optional property, which is needed to display visualization of alert type expression. Will be changed after visualization refactoring.| |dataFieldsFormats|Optional property, which is needed to display visualization of alert type expression. Will be changed after visualization refactoring.| +|metadata|Optional generic property, which allows to define component specific metadata. This metadata can be used for passing down preloaded data for Alert type expression component.| ## Build and register Action Types @@ -1198,3 +1203,213 @@ Clicking on the select card for `Example Action Type` will open the action type or create a new connector: ![Example Action Type with empty connectors list](https://i.imgur.com/EamA9Xv.png) + +## Embed the Create Connector flyout within any Kibana plugin + +Follow the instructions bellow to embed the Create Connector flyout within any Kibana plugin: +1. Add TriggersAndActionsUIPublicPluginSetup and TriggersAndActionsUIPublicPluginStart to Kibana plugin setup dependencies: + +``` +import { + TriggersAndActionsUIPublicPluginSetup, + TriggersAndActionsUIPublicPluginStart, + } from '../../../../../x-pack/plugins/triggers_actions_ui/public'; + +triggers_actions_ui: TriggersAndActionsUIPublicPluginSetup; +... + +triggers_actions_ui: TriggersAndActionsUIPublicPluginStart; +``` +Then this dependency will be used to embed Create Connector flyout or register new action type. + +2. Add Create Connector flyout to React component: +``` +// import section +import { ActionsConnectorsContextProvider, ConnectorAddFlyout } from '../../../../../../../triggers_actions_ui/public'; + +// in the component state definition section +const [addFlyoutVisible, setAddFlyoutVisibility] = useState(false); + +// load required dependancied +const { http, triggers_actions_ui, toastNotifications, capabilities } = useKibana().services; + +const connector = { + secrets: {}, + id: 'test', + actionTypeId: '.index', + actionType: 'Index', + name: 'action-connector', + referencedByCount: 0, + config: {}, + }; + +// UI control item for open flyout + setAddFlyoutVisibility(true)} +> + + + +// in render section of component + + + +``` + +ConnectorAddFlyout Props definition: +``` +export interface ConnectorAddFlyoutProps { + addFlyoutVisible: boolean; + setAddFlyoutVisibility: React.Dispatch>; + actionTypes?: ActionType[]; +} +``` + +|Property|Description| +|---|---| +|addFlyoutVisible|Visibility state of the Create Connector flyout.| +|setAddFlyoutVisibility|Function for changing visibility state of the Create Connector flyout.| +|actionTypes|Optional property, that allows to define only specific action types list which is available for a current plugin.| + +ActionsConnectorsContextValue options: +``` +export interface ActionsConnectorsContextValue { + http: HttpSetup; + actionTypeRegistry: TypeRegistry; + toastNotifications: Pick< + ToastsApi, + 'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError' + >; + capabilities: ApplicationStart['capabilities']; + reloadConnectors?: () => Promise; +} +``` + +|Property|Description| +|---|---| +|http|HttpSetup needed for executing API calls.| +|actionTypeRegistry|Registry for action types.| +|capabilities|Property, which is defining action current user usage capabilities like canSave or canDelete.| +|toastNotifications|Toast messages.| +|reloadConnectors|Optional function, which will be executed if connector was saved sucsessfuly, like reload list of connecotrs.| + + +## Embed the Edit Connector flyout within any Kibana plugin + +Follow the instructions bellow to embed the Edit Connector flyout within any Kibana plugin: +1. Add TriggersAndActionsUIPublicPluginSetup and TriggersAndActionsUIPublicPluginStart to Kibana plugin setup dependencies: + +``` +import { + TriggersAndActionsUIPublicPluginSetup, + TriggersAndActionsUIPublicPluginStart, + } from '../../../../../x-pack/plugins/triggers_actions_ui/public'; + +triggers_actions_ui: TriggersAndActionsUIPublicPluginSetup; +... + +triggers_actions_ui: TriggersAndActionsUIPublicPluginStart; +``` +Then this dependency will be used to embed Edit Connector flyout. + +2. Add Create Connector flyout to React component: +``` +// import section +import { ActionsConnectorsContextProvider, ConnectorEditFlyout } from '../../../../../../../triggers_actions_ui/public'; + +// in the component state definition section +const [editFlyoutVisible, setEditFlyoutVisibility] = useState(false); + +// load required dependancied +const { http, triggers_actions_ui, toastNotifications, capabilities } = useKibana().services; + +// UI control item for open flyout + setEditFlyoutVisibility(true)} +> + + + +// in render section of component + + + + +``` + +ConnectorEditFlyout Props definition: +``` +export interface ConnectorEditProps { + initialConnector: ActionConnectorTableItem; + editFlyoutVisible: boolean; + setEditFlyoutVisibility: React.Dispatch>; +} +``` + +|Property|Description| +|---|---| +|initialConnector|Property, that allows to define the initial state of edited connector.| +|editFlyoutVisible|Visibility state of the Edit Connector flyout.| +|setEditFlyoutVisibility|Function for changing visibility state of the Edit Connector flyout.| + +ActionsConnectorsContextValue options: +``` +export interface ActionsConnectorsContextValue { + http: HttpSetup; + actionTypeRegistry: TypeRegistry; + toastNotifications: Pick< + ToastsApi, + 'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError' + >; + capabilities: ApplicationStart['capabilities']; + reloadConnectors?: () => Promise; +} +``` + +|Property|Description| +|---|---| +|http|HttpSetup needed for executing API calls.| +|actionTypeRegistry|Registry for action types.| +|capabilities|Property, which is defining action current user usage capabilities like canSave or canDelete.| +|toastNotifications|Toast messages.| +|reloadConnectors|Optional function, which will be executed if connector was saved sucsessfuly, like reload list of connecotrs.| diff --git a/x-pack/plugins/triggers_actions_ui/public/application/context/actions_connectors_context.tsx b/x-pack/plugins/triggers_actions_ui/public/application/context/actions_connectors_context.tsx index 11786950d0f26..b49cdc3d7d8b8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/context/actions_connectors_context.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/context/actions_connectors_context.tsx @@ -5,15 +5,19 @@ */ import React, { createContext, useContext } from 'react'; -import { ActionType } from '../../types'; +import { HttpSetup, ToastsApi, ApplicationStart } from 'kibana/public'; +import { ActionTypeModel } from '../../types'; +import { TypeRegistry } from '../type_registry'; export interface ActionsConnectorsContextValue { - addFlyoutVisible: boolean; - editFlyoutVisible: boolean; - setEditFlyoutVisibility: React.Dispatch>; - setAddFlyoutVisibility: React.Dispatch>; - actionTypesIndex: Record | undefined; - reloadConnectors: () => Promise; + http: HttpSetup; + actionTypeRegistry: TypeRegistry; + toastNotifications: Pick< + ToastsApi, + 'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError' + >; + capabilities: ApplicationStart['capabilities']; + reloadConnectors?: () => Promise; } const ActionsConnectorsContext = createContext(null as any); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx index f7becb16c244a..800863e46034e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx @@ -9,26 +9,21 @@ import { coreMock } from '../../../../../../../src/core/public/mocks'; import { actionTypeRegistryMock } from '../../action_type_registry.mock'; import { ValidationResult, ActionConnector } from '../../../types'; import { ActionConnectorForm } from './action_connector_form'; +import { ActionsConnectorsContextValue } from '../../context/actions_connectors_context'; const actionTypeRegistry = actionTypeRegistryMock.create(); describe('action_connector_form', () => { - let deps: any; + let deps: ActionsConnectorsContextValue; beforeAll(async () => { const mocks = coreMock.createSetup(); const [ { - chrome, - docLinks, application: { capabilities }, }, ] = await mocks.getStartServices(); deps = { - chrome, - docLinks, toastNotifications: mocks.notifications.toasts, - injectedMetadata: mocks.injectedMetadata, http: mocks.http, - uiSettings: mocks.uiSettings, capabilities: { ...capabilities, actions: { @@ -37,11 +32,7 @@ describe('action_connector_form', () => { show: true, }, }, - legacy: { - MANAGEMENT_BREADCRUMB: { set: () => {} } as any, - }, actionTypeRegistry: actionTypeRegistry as any, - alertTypeRegistry: {} as any, }; }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.test.tsx index c1c6d9d94e810..4f098165033e7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.test.tsx @@ -6,31 +6,28 @@ import * as React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { coreMock } from '../../../../../../../src/core/public/mocks'; -import { ActionsConnectorsContextProvider } from '../../context/actions_connectors_context'; +import { + ActionsConnectorsContextProvider, + ActionsConnectorsContextValue, +} from '../../context/actions_connectors_context'; import { actionTypeRegistryMock } from '../../action_type_registry.mock'; import { ActionTypeMenu } from './action_type_menu'; import { ValidationResult } from '../../../types'; const actionTypeRegistry = actionTypeRegistryMock.create(); describe('connector_add_flyout', () => { - let deps: any; + let deps: ActionsConnectorsContextValue; beforeAll(async () => { const mockes = coreMock.createSetup(); const [ { - chrome, - docLinks, application: { capabilities }, }, ] = await mockes.getStartServices(); deps = { - chrome, - docLinks, - toastNotifications: mockes.notifications.toasts, - injectedMetadata: mockes.injectedMetadata, http: mockes.http, - uiSettings: mockes.uiSettings, + toastNotifications: mockes.notifications.toasts, capabilities: { ...capabilities, actions: { @@ -39,11 +36,7 @@ describe('connector_add_flyout', () => { show: true, }, }, - legacy: { - MANAGEMENT_BREADCRUMB: { set: () => {} } as any, - }, actionTypeRegistry: actionTypeRegistry as any, - alertTypeRegistry: {} as any, }; }); @@ -68,14 +61,10 @@ describe('connector_add_flyout', () => { const wrapper = mountWithIntl( {}, - editFlyoutVisible: false, - setEditFlyoutVisibility: state => {}, - actionTypesIndex: { - 'first-action-type': { id: 'first-action-type', name: 'first', enabled: true }, - 'second-action-type': { id: 'second-action-type', name: 'second', enabled: true }, - }, + http: deps!.http, + actionTypeRegistry: deps!.actionTypeRegistry, + capabilities: deps!.capabilities, + toastNotifications: deps!.toastNotifications, reloadConnectors: () => { return new Promise(() => {}); }, @@ -83,12 +72,17 @@ describe('connector_add_flyout', () => { > ); - expect(wrapper.find('[data-test-subj="first-action-type-card"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="second-action-type-card"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="my-action-type-card"]').exists()).toBeTruthy(); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.tsx index ddd08cf6d6d79..a63665a68fb6b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.tsx @@ -3,24 +3,46 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { EuiFlexItem, EuiCard, EuiIcon, EuiFlexGrid } from '@elastic/eui'; -import { ActionType, ActionTypeModel } from '../../../types'; +import { i18n } from '@kbn/i18n'; +import { ActionType, ActionTypeIndex } from '../../../types'; +import { loadActionTypes } from '../../lib/action_connector_api'; import { useActionsConnectorsContext } from '../../context/actions_connectors_context'; -import { TypeRegistry } from '../../type_registry'; interface Props { onActionTypeChange: (actionType: ActionType) => void; - actionTypeRegistry: TypeRegistry; + actionTypes?: ActionType[]; } -export const ActionTypeMenu = ({ onActionTypeChange, actionTypeRegistry }: Props) => { - const { actionTypesIndex } = useActionsConnectorsContext(); - if (!actionTypesIndex) { - return null; - } +export const ActionTypeMenu = ({ onActionTypeChange, actionTypes }: Props) => { + const { http, toastNotifications, actionTypeRegistry } = useActionsConnectorsContext(); + const [actionTypesIndex, setActionTypesIndex] = useState(undefined); - const actionTypes = Object.entries(actionTypesIndex) + useEffect(() => { + (async () => { + try { + const availableActionTypes = actionTypes ?? (await loadActionTypes({ http })); + const index: ActionTypeIndex = {}; + for (const actionTypeItem of availableActionTypes) { + index[actionTypeItem.id] = actionTypeItem; + } + setActionTypesIndex(index); + } catch (e) { + if (toastNotifications) { + toastNotifications.addDanger({ + title: i18n.translate( + 'xpack.triggersActionsUI.sections.actionsConnectorsList.unableToLoadActionTypesMessage', + { defaultMessage: 'Unable to load action types' } + ), + }); + } + } + })(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const registeredActionTypes = Object.entries(actionTypesIndex ?? []) .filter(([index]) => actionTypeRegistry.has(index)) .map(([index, actionType]) => { const actionTypeModel = actionTypeRegistry.get(index); @@ -33,7 +55,7 @@ export const ActionTypeMenu = ({ onActionTypeChange, actionTypeRegistry }: Props }; }); - const cardNodes = actionTypes + const cardNodes = registeredActionTypes .sort((a, b) => a.name.localeCompare(b.name)) .map((item, index) => { return ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx index 6b87002a1d2cf..cf0edbe422495 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx @@ -7,37 +7,28 @@ import * as React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { coreMock } from '../../../../../../../src/core/public/mocks'; import { ConnectorAddFlyout } from './connector_add_flyout'; -import { ActionsConnectorsContextProvider } from '../../context/actions_connectors_context'; +import { + ActionsConnectorsContextProvider, + ActionsConnectorsContextValue, +} from '../../context/actions_connectors_context'; import { actionTypeRegistryMock } from '../../action_type_registry.mock'; import { ValidationResult } from '../../../types'; -import { AppContextProvider } from '../../app_context'; -import { AppDeps } from '../../app'; -import { chartPluginMock } from '../../../../../../../src/plugins/charts/public/mocks'; -import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; const actionTypeRegistry = actionTypeRegistryMock.create(); describe('connector_add_flyout', () => { - let deps: AppDeps | null; + let deps: ActionsConnectorsContextValue; beforeAll(async () => { const mocks = coreMock.createSetup(); const [ { - chrome, - docLinks, application: { capabilities }, }, ] = await mocks.getStartServices(); deps = { - chrome, - docLinks, - dataPlugin: dataPluginMock.createStartContract(), - charts: chartPluginMock.createStartContract(), toastNotifications: mocks.notifications.toasts, - injectedMetadata: mocks.injectedMetadata, http: mocks.http, - uiSettings: mocks.uiSettings, capabilities: { ...capabilities, actions: { @@ -46,9 +37,7 @@ describe('connector_add_flyout', () => { show: true, }, }, - setBreadcrumbs: jest.fn(), actionTypeRegistry: actionTypeRegistry as any, - alertTypeRegistry: {} as any, }; }); @@ -71,24 +60,29 @@ describe('connector_add_flyout', () => { actionTypeRegistry.has.mockReturnValue(true); const wrapper = mountWithIntl( - - {}, - editFlyoutVisible: false, - setEditFlyoutVisibility: state => {}, - actionTypesIndex: { - 'my-action-type': { id: 'my-action-type', name: 'test', enabled: true }, + { + return new Promise(() => {}); + }, + }} + > + {}} + actionTypes={[ + { + id: actionType.id, + enabled: true, + name: 'Test', }, - reloadConnectors: () => { - return new Promise(() => {}); - }, - }} - > - - - + ]} + /> + ); expect(wrapper.find('ActionTypeMenu')).toHaveLength(1); expect(wrapper.find('[data-test-subj="my-action-type-card"]').exists()).toBeTruthy(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx index 1eabf2441da4f..1b86116781084 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx @@ -20,18 +20,33 @@ import { EuiBetaBadge, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useActionsConnectorsContext } from '../../context/actions_connectors_context'; import { ActionTypeMenu } from './action_type_menu'; import { ActionConnectorForm, validateBaseProperties } from './action_connector_form'; import { ActionType, ActionConnector, IErrorObject } from '../../../types'; -import { useAppDependencies } from '../../app_context'; import { connectorReducer } from './connector_reducer'; import { hasSaveActionsCapability } from '../../lib/capabilities'; import { createActionConnector } from '../../lib/action_connector_api'; +import { useActionsConnectorsContext } from '../../context/actions_connectors_context'; + +export interface ConnectorAddFlyoutProps { + addFlyoutVisible: boolean; + setAddFlyoutVisibility: React.Dispatch>; + actionTypes?: ActionType[]; +} -export const ConnectorAddFlyout = () => { +export const ConnectorAddFlyout = ({ + addFlyoutVisible, + setAddFlyoutVisibility, + actionTypes, +}: ConnectorAddFlyoutProps) => { let hasErrors = false; - const { http, toastNotifications, capabilities, actionTypeRegistry } = useAppDependencies(); + const { + http, + toastNotifications, + capabilities, + actionTypeRegistry, + reloadConnectors, + } = useActionsConnectorsContext(); const [actionType, setActionType] = useState(undefined); // hooks @@ -48,11 +63,6 @@ export const ConnectorAddFlyout = () => { dispatch({ command: { type: 'setConnector' }, payload: { key: 'connector', value } }); }; - const { - addFlyoutVisible, - setAddFlyoutVisibility, - reloadConnectors, - } = useActionsConnectorsContext(); const [isSaving, setIsSaving] = useState(false); const closeFlyout = useCallback(() => { @@ -79,10 +89,7 @@ export const ConnectorAddFlyout = () => { let actionTypeModel; if (!actionType) { currentForm = ( - + ); } else { actionTypeModel = actionTypeRegistry.get(actionType.id); @@ -108,17 +115,19 @@ export const ConnectorAddFlyout = () => { const onActionConnectorSave = async (): Promise => await createActionConnector({ http, connector }) .then(savedConnector => { - toastNotifications.addSuccess( - i18n.translate( - 'xpack.triggersActionsUI.sections.addConnectorForm.updateSuccessNotificationText', - { - defaultMessage: "Created '{connectorName}'", - values: { - connectorName: savedConnector.name, - }, - } - ) - ); + if (toastNotifications) { + toastNotifications.addSuccess( + i18n.translate( + 'xpack.triggersActionsUI.sections.addConnectorForm.updateSuccessNotificationText', + { + defaultMessage: "Created '{connectorName}'", + values: { + connectorName: savedConnector.name, + }, + } + ) + ); + } return savedConnector; }) .catch(errorRes => { @@ -218,7 +227,9 @@ export const ConnectorAddFlyout = () => { setIsSaving(false); if (savedAction) { closeFlyout(); - reloadConnectors(); + if (reloadConnectors) { + reloadConnectors(); + } } }} > diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx index d9f3e98919d76..94c2b823e8bcf 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx @@ -7,35 +7,24 @@ import * as React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { coreMock } from '../../../../../../../src/core/public/mocks'; import { ConnectorAddModal } from './connector_add_modal'; -import { ActionsConnectorsContextProvider } from '../../context/actions_connectors_context'; import { actionTypeRegistryMock } from '../../action_type_registry.mock'; import { ValidationResult } from '../../../types'; -import { AppDeps } from '../../app'; -import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; -import { chartPluginMock } from '../../../../../../../src/plugins/charts/public/mocks'; +import { ActionsConnectorsContextValue } from '../../context/actions_connectors_context'; const actionTypeRegistry = actionTypeRegistryMock.create(); describe('connector_add_modal', () => { - let deps: AppDeps | null; + let deps: ActionsConnectorsContextValue; beforeAll(async () => { const mocks = coreMock.createSetup(); const [ { - chrome, - docLinks, application: { capabilities }, }, ] = await mocks.getStartServices(); deps = { - chrome, - docLinks, - dataPlugin: dataPluginMock.createStartContract(), - charts: chartPluginMock.createStartContract(), toastNotifications: mocks.notifications.toasts, - injectedMetadata: mocks.injectedMetadata, http: mocks.http, - uiSettings: mocks.uiSettings, capabilities: { ...capabilities, actions: { @@ -44,9 +33,7 @@ describe('connector_add_modal', () => { show: true, }, }, - setBreadcrumbs: jest.fn(), actionTypeRegistry: actionTypeRegistry as any, - alertTypeRegistry: {} as any, }; }); it('renders connector modal form if addModalVisible is true', () => { @@ -75,30 +62,15 @@ describe('connector_add_modal', () => { const wrapper = deps ? mountWithIntl( - {}, - editFlyoutVisible: false, - setEditFlyoutVisibility: state => {}, - actionTypesIndex: { - 'my-action-type': { id: 'my-action-type', name: 'test', enabled: true }, - }, - reloadConnectors: () => { - return new Promise(() => {}); - }, - }} - > - {}} - actionType={actionType} - http={deps.http} - actionTypeRegistry={deps.actionTypeRegistry} - alertTypeRegistry={deps.alertTypeRegistry} - toastNotifications={deps.toastNotifications} - /> - + {}} + actionType={actionType} + http={deps.http} + actionTypeRegistry={deps.actionTypeRegistry} + alertTypeRegistry={{} as any} + toastNotifications={deps.toastNotifications} + /> ) : undefined; expect(wrapper?.find('EuiModalHeader')).toHaveLength(1); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx index a82003759d973..f9aa2cad8bfc6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx @@ -11,8 +11,6 @@ import { actionTypeRegistryMock } from '../../action_type_registry.mock'; import { ValidationResult } from '../../../types'; import { ConnectorEditFlyout } from './connector_edit_flyout'; import { AppContextProvider } from '../../app_context'; -import { chartPluginMock } from '../../../../../../../src/plugins/charts/public/mocks'; -import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; const actionTypeRegistry = actionTypeRegistryMock.create(); let deps: any; @@ -22,18 +20,11 @@ describe('connector_edit_flyout', () => { const mockes = coreMock.createSetup(); const [ { - chrome, - docLinks, application: { capabilities }, }, ] = await mockes.getStartServices(); deps = { - chrome, - docLinks, - dataPlugin: dataPluginMock.createStartContract(), - charts: chartPluginMock.createStartContract(), toastNotifications: mockes.notifications.toasts, - injectedMetadata: mockes.injectedMetadata, http: mockes.http, uiSettings: mockes.uiSettings, capabilities: { @@ -44,7 +35,6 @@ describe('connector_edit_flyout', () => { show: true, }, }, - setBreadcrumbs: jest.fn(), actionTypeRegistry: actionTypeRegistry as any, alertTypeRegistry: {} as any, }; @@ -82,19 +72,20 @@ describe('connector_edit_flyout', () => { {}, - editFlyoutVisible: true, - setEditFlyoutVisibility: state => {}, - actionTypesIndex: { - 'test-action-type-id': { id: 'test-action-type-id', name: 'test', enabled: true }, - }, + http: deps.http, + toastNotifications: deps.toastNotifications, + capabilities: deps.capabilities, + actionTypeRegistry: deps.actionTypeRegistry, reloadConnectors: () => { return new Promise(() => {}); }, }} > - + {}} + /> ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx index 6fe555fd74b39..c52bb8cc08f6f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx @@ -19,27 +19,33 @@ import { EuiBetaBadge, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useActionsConnectorsContext } from '../../context/actions_connectors_context'; import { ActionConnectorForm, validateBaseProperties } from './action_connector_form'; -import { useAppDependencies } from '../../app_context'; import { ActionConnectorTableItem, ActionConnector, IErrorObject } from '../../../types'; import { connectorReducer } from './connector_reducer'; import { updateActionConnector } from '../../lib/action_connector_api'; import { hasSaveActionsCapability } from '../../lib/capabilities'; +import { useActionsConnectorsContext } from '../../context/actions_connectors_context'; export interface ConnectorEditProps { initialConnector: ActionConnectorTableItem; + editFlyoutVisible: boolean; + setEditFlyoutVisibility: React.Dispatch>; } -export const ConnectorEditFlyout = ({ initialConnector }: ConnectorEditProps) => { +export const ConnectorEditFlyout = ({ + initialConnector, + editFlyoutVisible, + setEditFlyoutVisibility, +}: ConnectorEditProps) => { let hasErrors = false; - const { http, toastNotifications, capabilities, actionTypeRegistry } = useAppDependencies(); - const canSave = hasSaveActionsCapability(capabilities); const { - editFlyoutVisible, - setEditFlyoutVisibility, + http, + toastNotifications, + capabilities, + actionTypeRegistry, reloadConnectors, } = useActionsConnectorsContext(); + const canSave = hasSaveActionsCapability(capabilities); const closeFlyout = useCallback(() => setEditFlyoutVisibility(false), [setEditFlyoutVisibility]); const [{ connector }, dispatch] = useReducer(connectorReducer, { connector: { ...initialConnector, secrets: {} }, @@ -63,17 +69,19 @@ export const ConnectorEditFlyout = ({ initialConnector }: ConnectorEditProps) => const onActionConnectorSave = async (): Promise => await updateActionConnector({ http, connector, id: connector.id }) .then(savedConnector => { - toastNotifications.addSuccess( - i18n.translate( - 'xpack.triggersActionsUI.sections.editConnectorForm.updateSuccessNotificationText', - { - defaultMessage: "Updated '{connectorName}'", - values: { - connectorName: savedConnector.name, - }, - } - ) - ); + if (toastNotifications) { + toastNotifications.addSuccess( + i18n.translate( + 'xpack.triggersActionsUI.sections.editConnectorForm.updateSuccessNotificationText', + { + defaultMessage: "Updated '{connectorName}'", + values: { + connectorName: savedConnector.name, + }, + } + ) + ); + } return savedConnector; }) .catch(errorRes => { @@ -151,7 +159,9 @@ export const ConnectorEditFlyout = ({ initialConnector }: ConnectorEditProps) => setIsSaving(false); if (savedAction) { closeFlyout(); - reloadConnectors(); + if (reloadConnectors) { + reloadConnectors(); + } } }} > 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 f48e27791419d..4e514281be0ea 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 @@ -18,16 +18,16 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ActionsConnectorsContextProvider } from '../../../context/actions_connectors_context'; import { useAppDependencies } from '../../../app_context'; import { loadAllActions, loadActionTypes } from '../../../lib/action_connector_api'; import { ActionConnector, ActionConnectorTableItem, ActionTypeIndex } from '../../../../types'; import { ConnectorAddFlyout, ConnectorEditFlyout } from '../../action_connector_form'; import { hasDeleteActionsCapability, hasSaveActionsCapability } from '../../../lib/capabilities'; import { DeleteConnectorsModal } from '../../../components/delete_connectors_modal'; +import { ActionsConnectorsContextProvider } from '../../../context/actions_connectors_context'; export const ActionsConnectorsList: React.FunctionComponent = () => { - const { http, toastNotifications, capabilities } = useAppDependencies(); + const { http, toastNotifications, capabilities, actionTypeRegistry } = useAppDependencies(); const canDelete = hasDeleteActionsCapability(capabilities); const canSave = hasSaveActionsCapability(capabilities); @@ -377,19 +377,23 @@ export const ActionsConnectorsList: React.FunctionComponent = () => { {data.length === 0 && !canSave && noPermissionPrompt} - + {editedConnectorItem ? ( ) : null} diff --git a/x-pack/plugins/triggers_actions_ui/public/index.ts b/x-pack/plugins/triggers_actions_ui/public/index.ts index 0be0a919112f8..74af4a77d0ef0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/index.ts @@ -8,7 +8,12 @@ import { PluginInitializerContext } from 'src/core/public'; import { Plugin } from './plugin'; export { AlertsContextProvider } from './application/context/alerts_context'; +export { ActionsConnectorsContextProvider } from './application/context/actions_connectors_context'; export { AlertAdd } from './application/sections/alert_form'; +export { + ConnectorAddFlyout, + ConnectorEditFlyout, +} from './application/sections/action_connector_form'; export function plugin(ctx: PluginInitializerContext) { return new Plugin(ctx); diff --git a/x-pack/plugins/triggers_actions_ui/public/plugin.ts b/x-pack/plugins/triggers_actions_ui/public/plugin.ts index 459197d80d7aa..9f975cba3c0d1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/public/plugin.ts @@ -22,7 +22,10 @@ export interface TriggersAndActionsUIPublicPluginSetup { alertTypeRegistry: TypeRegistry; } -export type Start = void; +export interface TriggersAndActionsUIPublicPluginStart { + actionTypeRegistry: TypeRegistry; + alertTypeRegistry: TypeRegistry; +} interface PluginsStart { data: DataPublicPluginStart; @@ -30,7 +33,9 @@ interface PluginsStart { management: ManagementStart; } -export class Plugin implements CorePlugin { +export class Plugin + implements + CorePlugin { private actionTypeRegistry: TypeRegistry; private alertTypeRegistry: TypeRegistry; @@ -57,44 +62,46 @@ export class Plugin implements CorePlugin { + boot({ + dataPlugin: plugins.data, + charts: plugins.charts, + element: params.element, + toastNotifications: core.notifications.toasts, + injectedMetadata: core.injectedMetadata, + http: core.http, + uiSettings: core.uiSettings, + docLinks: core.docLinks, + chrome: core.chrome, + savedObjects: core.savedObjects.client, + I18nContext: core.i18n.Context, + capabilities: core.application.capabilities, + setBreadcrumbs: params.setBreadcrumbs, + actionTypeRegistry: this.actionTypeRegistry, + alertTypeRegistry: this.alertTypeRegistry, + }); + return () => {}; + }, + }); } - - plugins.management.sections.getSection('kibana')!.registerApp({ - id: 'triggersActions', - title: i18n.translate('xpack.triggersActionsUI.managementSection.displayName', { - defaultMessage: 'Alerts and Actions', - }), - order: 7, - mount: params => { - boot({ - dataPlugin: plugins.data, - charts: plugins.charts, - element: params.element, - toastNotifications: core.notifications.toasts, - injectedMetadata: core.injectedMetadata, - http: core.http, - uiSettings: core.uiSettings, - docLinks: core.docLinks, - chrome: core.chrome, - savedObjects: core.savedObjects.client, - I18nContext: core.i18n.Context, - capabilities: core.application.capabilities, - setBreadcrumbs: params.setBreadcrumbs, - actionTypeRegistry: this.actionTypeRegistry, - alertTypeRegistry: this.alertTypeRegistry, - }); - return () => {}; - }, - }); + return { + actionTypeRegistry: this.actionTypeRegistry, + alertTypeRegistry: this.alertTypeRegistry, + }; } public stop() {} diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts index 25ebc6d610f86..75ae6b9ea7c21 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts @@ -60,7 +60,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.click('thresholdAlertTimeFieldSelect'); const fieldOptions = await find.allByCssSelector('#thresholdTimeField option'); await fieldOptions[1].click(); + // need this two out of popup clicks to close them await nameInput.click(); + await testSubjects.click('intervalInput'); + await testSubjects.click('.slack-ActionTypeSelectOption'); await testSubjects.click('createActionConnectorButton'); const connectorNameInput = await testSubjects.find('nameInput');