From 9475426e0eb55b5550fd2eecfd18c580c1681bd3 Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Wed, 22 Jan 2025 12:53:08 -0600 Subject: [PATCH] [Response Ops] [Rule Form] Add Show Request and Add Action screens to flyout (#206154) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Part of #195211 - Adds Show Request screen to the new rule form flyout
Screenshot Screenshot 2025-01-10 at 1 30 15 PM
- Renders the action connectors UI within the flyout instead of opening a modal
Screenshot Screenshot 2025-01-10 at 1 28 38 PM
- Duplicates the dropdown filter design from the flyout UI within the action connectors modal when displayed on a smaller screen
Screenshot Screenshot 2025-01-10 at 1 30 28 PM
### Implementation notes In order to get the action connectors UI to render the same way in both a modal and the flyout, without duplicating a large amount of code, I had to introduce a little bit of complexity. Within the Rule Page, it's as simple as opening the UI inside a modal, but the flyout cannot open a second flyout; it has to know when and how to completely replace its own contents. - The bulk of the action connectors UI is now moved to ``. `` and `` act as wrappers for this component. - The `` step no longer handles rendering the connector UI, because it's not at a high enough level to know if it's in the `` or the ``. Instead, it simply sends a signal up the context hierarchy to `setIsConnectorsScreenVisible`. - A new context called `RuleFormScreenContext` keeps track of `isConnectorsScreenVisible`, a state for whether or not the action connectors "screen" is open, regardless of whether that screen is displayed in a modal or a flyout. - The Rule Page uses `isConnectorsScreenVisible` to determine whether to render the modal. This works the same way as it used to, but handled by the `` instead of the `` component. - The Rule Flyout uses `isConnectorsScreenVisible` to determine whether to continue to render `` or to completely replace its contents with `` For consistency, this PR also moves the Show Request modal/flyout screen into the same system. ### Testing To test the new flyout, edit `packages/response-ops/rule_form/src/create_rule_form.tsx` and `packages/response-ops/rule_form/src/edit_rule_form.tsx` so that they render `` instead of ``.
Use this diff block ```diff diff --git a/packages/response-ops/rule_form/src/create_rule_form.tsx b/packages/response-ops/rule_form/src/create_rule_form.tsx index 2f5e0472dcd..564744b96ec 100644 --- a/packages/response-ops/rule_form/src/create_rule_form.tsx +++ b/packages/response-ops/rule_form/src/create_rule_form.tsx @@ -31,6 +31,7 @@ import { parseRuleCircuitBreakerErrorMessage, } from './utils'; import { RULE_CREATE_SUCCESS_TEXT, RULE_CREATE_ERROR_TEXT } from './translations'; +import { RuleFlyout } from './rule_flyout'; export interface CreateRuleFormProps { ruleTypeId: string; @@ -199,7 +200,7 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => { }), }} > - + ); diff --git a/packages/response-ops/rule_form/src/edit_rule_form.tsx b/packages/response-ops/rule_form/src/edit_rule_form.tsx index 392447114ed..41aecd7245a 100644 --- a/packages/response-ops/rule_form/src/edit_rule_form.tsx +++ b/packages/response-ops/rule_form/src/edit_rule_form.tsx @@ -26,6 +26,7 @@ import { import { RULE_EDIT_ERROR_TEXT, RULE_EDIT_SUCCESS_TEXT } from './translations'; import { getAvailableRuleTypes, parseRuleCircuitBreakerErrorMessage } from './utils'; import { DEFAULT_VALID_CONSUMERS, getDefaultFormData } from './constants'; +import { RuleFlyout } from './rule_flyout'; export interface EditRuleFormProps { id: string; @@ -193,7 +194,7 @@ export const EditRuleForm = (props: EditRuleFormProps) => { showMustacheAutocompleteSwitch, }} > - + ); ```
### Still Todo 1. Replace all instances of the v1 rule flyout with this new one (it's used heavily in solutions, not in Stack Management) ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: Elastic Machine (cherry picked from commit 8004e3e70ad63b938d724eafd561533eeb225cd9) --- .../response-ops/rule_form/src/hooks/index.ts | 1 + .../src/hooks/use_rule_form_screen_context.ts | 15 + .../src/hooks/use_rule_form_steps.tsx | 30 +- .../rule_form/src/request_code_block/index.ts | 10 + .../request_code_block/request_code_block.tsx | 67 +++ .../src/rule_actions/rule_actions.test.tsx | 32 +- .../src/rule_actions/rule_actions.tsx | 69 +-- .../rule_actions_connectors_body.test.tsx | 100 ++++ .../rule_actions_connectors_body.tsx | 467 ++++++++++++++++++ .../rule_actions_connectors_modal.test.tsx | 94 +--- .../rule_actions_connectors_modal.tsx | 340 +------------ .../src/rule_flyout/rule_flyout.test.tsx | 24 +- .../rule_form/src/rule_flyout/rule_flyout.tsx | 59 ++- .../src/rule_flyout/rule_flyout_body.tsx | 11 +- .../rule_flyout_select_connector.tsx | 65 +++ .../rule_flyout/rule_flyout_show_request.tsx | 73 +++ .../response-ops/rule_form/src/rule_form.scss | 30 +- .../response-ops/rule_form/src/rule_form.tsx | 5 +- .../src/rule_form_screen_context/index.ts | 10 + .../rule_form_screen_context.tsx | 41 ++ .../rule_form/src/rule_page/rule_page.tsx | 8 +- .../src/rule_page/rule_page_footer.test.tsx | 10 +- .../src/rule_page/rule_page_footer.tsx | 17 +- .../rule_page_show_request_modal.test.tsx | 16 +- .../rule_page_show_request_modal.tsx | 114 +---- .../rule_form/src/translations.ts | 63 +++ 26 files changed, 1124 insertions(+), 647 deletions(-) create mode 100644 src/platform/packages/shared/response-ops/rule_form/src/hooks/use_rule_form_screen_context.ts create mode 100644 src/platform/packages/shared/response-ops/rule_form/src/request_code_block/index.ts create mode 100644 src/platform/packages/shared/response-ops/rule_form/src/request_code_block/request_code_block.tsx create mode 100644 src/platform/packages/shared/response-ops/rule_form/src/rule_actions/rule_actions_connectors_body.test.tsx create mode 100644 src/platform/packages/shared/response-ops/rule_form/src/rule_actions/rule_actions_connectors_body.tsx create mode 100644 src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout_select_connector.tsx create mode 100644 src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout_show_request.tsx create mode 100644 src/platform/packages/shared/response-ops/rule_form/src/rule_form_screen_context/index.ts create mode 100644 src/platform/packages/shared/response-ops/rule_form/src/rule_form_screen_context/rule_form_screen_context.tsx diff --git a/src/platform/packages/shared/response-ops/rule_form/src/hooks/index.ts b/src/platform/packages/shared/response-ops/rule_form/src/hooks/index.ts index aef31dc3d5135..d10d3e3328883 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/hooks/index.ts +++ b/src/platform/packages/shared/response-ops/rule_form/src/hooks/index.ts @@ -10,3 +10,4 @@ export * from './use_rule_form_dispatch'; export * from './use_rule_form_state'; export * from './use_rule_form_steps'; +export * from './use_rule_form_screen_context'; diff --git a/src/platform/packages/shared/response-ops/rule_form/src/hooks/use_rule_form_screen_context.ts b/src/platform/packages/shared/response-ops/rule_form/src/hooks/use_rule_form_screen_context.ts new file mode 100644 index 0000000000000..806d65c1f8241 --- /dev/null +++ b/src/platform/packages/shared/response-ops/rule_form/src/hooks/use_rule_form_screen_context.ts @@ -0,0 +1,15 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { useContext } from 'react'; +import { RuleFormScreenContext } from '../rule_form_screen_context'; + +export const useRuleFormScreenContext = () => { + return useContext(RuleFormScreenContext); +}; diff --git a/src/platform/packages/shared/response-ops/rule_form/src/hooks/use_rule_form_steps.tsx b/src/platform/packages/shared/response-ops/rule_form/src/hooks/use_rule_form_steps.tsx index 74b926a2bc20c..80dc1ff05ae1f 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/hooks/use_rule_form_steps.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/hooks/use_rule_form_steps.tsx @@ -149,13 +149,7 @@ const useCommonRuleFormSteps = ({ ? { title: RULE_FORM_PAGE_RULE_ACTIONS_TITLE, status: actionsStatus, - children: ( - <> - - - - - ), + children: , } : null, [RuleFormStepId.DETAILS]: { @@ -163,13 +157,7 @@ const useCommonRuleFormSteps = ({ ? RULE_FORM_PAGE_RULE_DETAILS_TITLE_SHORT : RULE_FORM_PAGE_RULE_DETAILS_TITLE, status: ruleDetailsStatus, - children: ( - <> - - - - - ), + children: , }, }), [ruleDefinitionStatus, canReadConnectors, actionsStatus, ruleDetailsStatus, shortTitles] @@ -210,7 +198,7 @@ export const useRuleFormSteps: () => RuleFormVerticalSteps = () => { const mappedSteps = useMemo(() => { return stepOrder - .map((stepId) => { + .map((stepId, index) => { const step = steps[stepId]; return step ? { @@ -227,6 +215,12 @@ export const useRuleFormSteps: () => RuleFormVerticalSteps = () => { stepId={stepId} > {step.children} + {index > 0 && ( + <> + + + + )} ), } @@ -246,8 +240,10 @@ interface RuleFormHorizontalSteps { hasNextStep: boolean; hasPreviousStep: boolean; } -export const useRuleFormHorizontalSteps: () => RuleFormHorizontalSteps = () => { - const [currentStep, setCurrentStep] = useState(STEP_ORDER[0]); +export const useRuleFormHorizontalSteps: ( + initialStep?: RuleFormStepId +) => RuleFormHorizontalSteps = (initialStep = STEP_ORDER[0]) => { + const [currentStep, setCurrentStep] = useState(initialStep); const [touchedSteps, setTouchedSteps] = useState>( STEP_ORDER.reduce( (result, stepId) => ({ ...result, [stepId]: false }), diff --git a/src/platform/packages/shared/response-ops/rule_form/src/request_code_block/index.ts b/src/platform/packages/shared/response-ops/rule_form/src/request_code_block/index.ts new file mode 100644 index 0000000000000..18fa50fae60cb --- /dev/null +++ b/src/platform/packages/shared/response-ops/rule_form/src/request_code_block/index.ts @@ -0,0 +1,10 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export * from './request_code_block'; diff --git a/src/platform/packages/shared/response-ops/rule_form/src/request_code_block/request_code_block.tsx b/src/platform/packages/shared/response-ops/rule_form/src/request_code_block/request_code_block.tsx new file mode 100644 index 0000000000000..ff229564cc281 --- /dev/null +++ b/src/platform/packages/shared/response-ops/rule_form/src/request_code_block/request_code_block.tsx @@ -0,0 +1,67 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { omit, pick } from 'lodash'; +import React, { useMemo } from 'react'; +import { EuiCodeBlock } from '@elastic/eui'; +import { + CreateRuleBody, + UPDATE_FIELDS_WITH_ACTIONS, + UpdateRuleBody, + transformCreateRuleBody, + transformUpdateRuleBody, +} from '../common/apis'; +import { BASE_ALERTING_API_PATH } from '../constants'; +import { useRuleFormState } from '../hooks'; +import { SHOW_REQUEST_MODAL_ERROR } from '../translations'; +import { RuleFormData } from '../types'; + +const stringifyBodyRequest = ({ + formData, + isEdit, +}: { + formData: RuleFormData; + isEdit: boolean; +}): string => { + try { + const request = isEdit + ? transformUpdateRuleBody(pick(formData, UPDATE_FIELDS_WITH_ACTIONS) as UpdateRuleBody) + : transformCreateRuleBody(omit(formData, 'id') as CreateRuleBody); + return JSON.stringify(request, null, 2); + } catch { + return SHOW_REQUEST_MODAL_ERROR; + } +}; + +interface RequestCodeBlockProps { + isEdit: boolean; + 'data-test-subj'?: string; +} +export const RequestCodeBlock = (props: RequestCodeBlockProps) => { + const { isEdit, 'data-test-subj': dataTestSubj } = props; + const { formData, id, multiConsumerSelection } = useRuleFormState(); + + const formattedRequest = useMemo(() => { + return stringifyBodyRequest({ + formData: { + ...formData, + ...(multiConsumerSelection ? { consumer: multiConsumerSelection } : {}), + }, + isEdit, + }); + }, [formData, isEdit, multiConsumerSelection]); + + return ( + + {`${isEdit ? 'PUT' : 'POST'} kbn:${BASE_ALERTING_API_PATH}/rule${ + isEdit ? `/${id}` : '' + }\n${formattedRequest}`} + + ); +}; diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_actions/rule_actions.test.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_actions/rule_actions.test.tsx index e172405e3695b..de7e92ef7d756 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_actions/rule_actions.test.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_actions/rule_actions.test.tsx @@ -28,6 +28,7 @@ const http = httpServiceMock.createStartContract(); jest.mock('../hooks', () => ({ useRuleFormState: jest.fn(), useRuleFormDispatch: jest.fn(), + useRuleFormScreenContext: jest.fn(), })); jest.mock('./rule_actions_system_actions_item', () => ({ @@ -94,7 +95,8 @@ const mockValidate = jest.fn().mockResolvedValue({ errors: {}, }); -const { useRuleFormState, useRuleFormDispatch } = jest.requireMock('../hooks'); +const { useRuleFormState, useRuleFormDispatch, useRuleFormScreenContext } = + jest.requireMock('../hooks'); const { useLoadConnectors, useLoadConnectorTypes, useLoadRuleTypeAadTemplateField } = jest.requireMock('../common/hooks'); @@ -109,6 +111,7 @@ const mockActions = [getAction('1'), getAction('2')]; const mockSystemActions = [getSystemAction('3')]; const mockOnChange = jest.fn(); +const mockSetIsConnectorsScreenVisible = jest.fn(); describe('ruleActions', () => { beforeEach(() => { @@ -167,6 +170,9 @@ describe('ruleActions', () => { aadTemplateFields: [], }); useRuleFormDispatch.mockReturnValue(mockOnChange); + useRuleFormScreenContext.mockReturnValue({ + setIsConnectorsScreenVisible: mockSetIsConnectorsScreenVisible, + }); }); afterEach(() => { @@ -216,29 +222,7 @@ describe('ruleActions', () => { render(); await userEvent.click(screen.getByTestId('ruleActionsAddActionButton')); - expect(screen.getByText('RuleActionsConnectorsModal')).toBeInTheDocument(); - }); - - test('should call onSelectConnector with the correct parameters', async () => { - render(); - - await userEvent.click(screen.getByTestId('ruleActionsAddActionButton')); - expect(screen.getByText('RuleActionsConnectorsModal')).toBeInTheDocument(); - - await userEvent.click(screen.getByText('select connector')); - expect(mockOnChange).toHaveBeenCalledWith({ - payload: { - actionTypeId: 'actionType-1', - frequency: { notifyWhen: 'onActionGroupChange', summary: false, throttle: null }, - group: 'test', - id: 'connector-1', - params: { key: 'value' }, - uuid: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', - }, - type: 'addAction', - }); - - expect(screen.queryByText('RuleActionsConnectorsModal')).not.toBeInTheDocument(); + expect(mockSetIsConnectorsScreenVisible).toHaveBeenCalledWith(true); }); test('should use the rule producer ID if it is not a multi-consumer rule', async () => { diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_actions/rule_actions.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_actions/rule_actions.tsx index 168a7d4f5a3c3..e3f9e76410c87 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_actions/rule_actions.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_actions/rule_actions.tsx @@ -9,21 +9,17 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiImage, EuiSpacer, EuiText } from '@elastic/eui'; import { RuleSystemAction } from '@kbn/alerting-types'; -import { ActionConnector } from '@kbn/alerts-ui-shared'; import React, { useCallback, useMemo, useState } from 'react'; import useEffectOnce from 'react-use/lib/useEffectOnce'; -import { v4 as uuidv4 } from 'uuid'; -import { RuleAction, RuleFormParamsErrors } from '../common/types'; -import { DEFAULT_FREQUENCY, MULTI_CONSUMER_RULE_TYPE_IDS } from '../constants'; -import { useRuleFormDispatch, useRuleFormState } from '../hooks'; +import { RuleAction } from '../common/types'; +import { MULTI_CONSUMER_RULE_TYPE_IDS } from '../constants'; +import { useRuleFormState, useRuleFormScreenContext } from '../hooks'; import { ADD_ACTION_DESCRIPTION_TEXT, ADD_ACTION_HEADER, ADD_ACTION_OPTIONAL_TEXT, ADD_ACTION_TEXT, } from '../translations'; -import { getDefaultParams } from '../utils'; -import { RuleActionsConnectorsModal } from './rule_actions_connectors_modal'; import { RuleActionsItem } from './rule_actions_item'; import { RuleActionsSystemActionsItem } from './rule_actions_system_actions_item'; @@ -40,69 +36,19 @@ const useRuleActionsIllustration = () => { }; export const RuleActions = () => { - const [isConnectorModalOpen, setIsConnectorModalOpen] = useState(false); const ruleActionsIllustration = useRuleActionsIllustration(); + const { setIsConnectorsScreenVisible } = useRuleFormScreenContext(); const { formData: { actions, consumer }, - plugins: { actionTypeRegistry }, multiConsumerSelection, selectedRuleType, connectorTypes, } = useRuleFormState(); - const dispatch = useRuleFormDispatch(); - const onModalOpen = useCallback(() => { - setIsConnectorModalOpen(true); - }, []); - - const onModalClose = useCallback(() => { - setIsConnectorModalOpen(false); - }, []); - - const onSelectConnector = useCallback( - async (connector: ActionConnector) => { - const { id, actionTypeId } = connector; - const uuid = uuidv4(); - const group = selectedRuleType.defaultActionGroupId; - const actionTypeModel = actionTypeRegistry.get(actionTypeId); - - const params = - getDefaultParams({ - group, - ruleType: selectedRuleType, - actionTypeModel, - }) || {}; - - dispatch({ - type: 'addAction', - payload: { - id, - actionTypeId, - uuid, - params, - group, - frequency: DEFAULT_FREQUENCY, - }, - }); - - const res: { errors: RuleFormParamsErrors } = await actionTypeRegistry - .get(actionTypeId) - ?.validateParams(params); - - dispatch({ - type: 'setActionParamsError', - payload: { - uuid, - errors: res.errors, - }, - }); - - onModalClose(); - }, - [dispatch, onModalClose, selectedRuleType, actionTypeRegistry] - ); + setIsConnectorsScreenVisible(true); + }, [setIsConnectorsScreenVisible]); const producerId = useMemo(() => { if (MULTI_CONSUMER_RULE_TYPE_IDS.includes(selectedRuleType.id)) { @@ -184,9 +130,6 @@ export const RuleActions = () => { - {isConnectorModalOpen && ( - - )} ); }; diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_actions/rule_actions_connectors_body.test.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_actions/rule_actions_connectors_body.test.tsx new file mode 100644 index 0000000000000..0ac987016ff0e --- /dev/null +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_actions/rule_actions_connectors_body.test.tsx @@ -0,0 +1,100 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { RuleActionsConnectorsBody } from './rule_actions_connectors_body'; +import type { ActionConnector, ActionTypeModel } from '@kbn/alerts-ui-shared'; +import { TypeRegistry } from '@kbn/alerts-ui-shared/lib'; +import { ActionType } from '@kbn/actions-types'; +import { + getActionType, + getActionTypeModel, + getConnector, +} from '../common/test_utils/actions_test_utils'; + +jest.mock('../hooks', () => ({ + useRuleFormState: jest.fn(), + useRuleFormDispatch: jest.fn(), +})); + +jest.mock('../utils', () => ({ + getDefaultParams: jest.fn(), +})); + +const { useRuleFormState, useRuleFormDispatch } = jest.requireMock('../hooks'); + +const mockConnectors: ActionConnector[] = [getConnector('1'), getConnector('2')]; + +const mockActionTypes: ActionType[] = [getActionType('1'), getActionType('2')]; + +const mockOnSelectConnector = jest.fn(); + +const mockOnChange = jest.fn(); + +describe('ruleActionsConnectorsBody', () => { + beforeEach(() => { + const actionTypeRegistry = new TypeRegistry(); + actionTypeRegistry.register(getActionTypeModel('1', { id: 'actionType-1' })); + actionTypeRegistry.register(getActionTypeModel('2', { id: 'actionType-2' })); + + useRuleFormState.mockReturnValue({ + plugins: { + actionTypeRegistry, + }, + formData: { + actions: [], + }, + connectors: mockConnectors, + connectorTypes: mockActionTypes, + aadTemplateFields: [], + selectedRuleType: { + defaultActionGroupId: 'default', + }, + }); + useRuleFormDispatch.mockReturnValue(mockOnChange); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should call onSelectConnector when connector is clicked', async () => { + render(); + + await userEvent.click(screen.getByText('connector-1')); + await waitFor(() => + expect(mockOnSelectConnector).toHaveBeenLastCalledWith({ + actionTypeId: 'actionType-1', + config: { config: 'config-1' }, + id: 'connector-1', + isDeprecated: false, + isPreconfigured: false, + isSystemAction: false, + name: 'connector-1', + secrets: { secret: 'secret' }, + }) + ); + + await userEvent.click(screen.getByText('connector-2')); + await waitFor(() => + expect(mockOnSelectConnector).toHaveBeenLastCalledWith({ + actionTypeId: 'actionType-2', + config: { config: 'config-2' }, + id: 'connector-2', + isDeprecated: false, + isPreconfigured: false, + isSystemAction: false, + name: 'connector-2', + secrets: { secret: 'secret' }, + }) + ); + }); +}); diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_actions/rule_actions_connectors_body.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_actions/rule_actions_connectors_body.tsx new file mode 100644 index 0000000000000..a454856864fec --- /dev/null +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_actions/rule_actions_connectors_body.tsx @@ -0,0 +1,467 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { + EuiButton, + EuiCard, + EuiEmptyPrompt, + EuiFacetButton, + EuiFacetGroup, + EuiFieldSearch, + EuiFilterButton, + EuiFilterGroup, + EuiPopover, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiIcon, + EuiLoadingSpinner, + EuiSpacer, + EuiText, + EuiToolTip, + useEuiTheme, + EuiSelectable, + EuiSelectableProps, + useCurrentEuiBreakpoint, +} from '@elastic/eui'; +import { ActionConnector, checkActionFormActionTypeEnabled } from '@kbn/alerts-ui-shared'; +import React, { Suspense, useCallback, useMemo, useState } from 'react'; +import { v4 as uuidv4 } from 'uuid'; +import { RuleFormParamsErrors } from '../common/types'; +import { DEFAULT_FREQUENCY } from '../constants'; +import { useRuleFormDispatch, useRuleFormState } from '../hooks'; +import { + ACTION_TYPE_MODAL_EMPTY_TEXT, + ACTION_TYPE_MODAL_EMPTY_TITLE, + ACTION_TYPE_MODAL_FILTER_ALL, + ACTION_TYPE_MODAL_FILTER_LIST_TITLE, + MODAL_SEARCH_CLEAR_FILTERS_TEXT, + MODAL_SEARCH_PLACEHOLDER, +} from '../translations'; +import { getDefaultParams } from '../utils'; + +type ConnectorsMap = Record; + +export interface RuleActionsConnectorsBodyProps { + onSelectConnector: (connector?: ActionConnector) => void; + responsiveOverflow?: 'auto' | 'hidden'; +} + +export const RuleActionsConnectorsBody = ({ + onSelectConnector, + responsiveOverflow = 'auto', +}: RuleActionsConnectorsBodyProps) => { + const [searchValue, setSearchValue] = useState(''); + const [selectedConnectorType, setSelectedConnectorType] = useState('all'); + const [isConenctorFilterPopoverOpen, setIsConenctorFilterPopoverOpen] = useState(false); + + const { euiTheme } = useEuiTheme(); + + const currentBreakpoint = useCurrentEuiBreakpoint() ?? 'm'; + + const { + plugins: { actionTypeRegistry }, + formData: { actions }, + connectors, + connectorTypes, + selectedRuleType, + } = useRuleFormState(); + + const dispatch = useRuleFormDispatch(); + + const onSelectConnectorInternal = useCallback( + async (connector: ActionConnector) => { + const { id, actionTypeId } = connector; + const uuid = uuidv4(); + const group = selectedRuleType.defaultActionGroupId; + const actionTypeModel = actionTypeRegistry.get(actionTypeId); + + const params = + getDefaultParams({ + group, + ruleType: selectedRuleType, + actionTypeModel, + }) || {}; + + dispatch({ + type: 'addAction', + payload: { + id, + actionTypeId, + uuid, + params, + group, + frequency: DEFAULT_FREQUENCY, + }, + }); + + const res: { errors: RuleFormParamsErrors } = await actionTypeRegistry + .get(actionTypeId) + ?.validateParams(params); + + dispatch({ + type: 'setActionParamsError', + payload: { + uuid, + errors: res.errors, + }, + }); + + // Send connector to onSelectConnector mainly for testing purposes, dispatch handles form data updates + onSelectConnector(connector); + }, + [dispatch, onSelectConnector, selectedRuleType, actionTypeRegistry] + ); + + const preconfiguredConnectors = useMemo(() => { + return connectors.filter((connector) => connector.isPreconfigured); + }, [connectors]); + + const availableConnectors = useMemo(() => { + return connectors.filter(({ actionTypeId }) => { + const actionType = connectorTypes.find(({ id }) => id === actionTypeId); + const actionTypeModel = actionTypeRegistry.get(actionTypeId); + + if (!actionType) { + return false; + } + + if (!actionTypeModel.actionParamsFields) { + return false; + } + + const checkEnabledResult = checkActionFormActionTypeEnabled( + actionType, + preconfiguredConnectors + ); + + if (!actionType.enabledInConfig && !checkEnabledResult.isEnabled) { + return false; + } + + return true; + }); + }, [connectors, connectorTypes, preconfiguredConnectors, actionTypeRegistry]); + + const onSearchChange = useCallback((e: React.ChangeEvent) => { + setSearchValue(e.target.value); + }, []); + + const onConnectorOptionSelect = useCallback( + (id: string) => () => { + setSelectedConnectorType((prev) => { + if (prev === id) { + return 'all'; + } + return id; + }); + }, + [] + ); + + const onClearFilters = useCallback(() => { + setSearchValue(''); + setSelectedConnectorType('all'); + }, []); + + const connectorsMap: ConnectorsMap | null = useMemo(() => { + return availableConnectors.reduce((result, { actionTypeId }) => { + const actionTypeModel = actionTypeRegistry.get(actionTypeId); + const subtype = actionTypeModel.subtype; + + const shownActionTypeId = actionTypeModel.hideInUi + ? subtype?.filter((type) => type.id !== actionTypeId)[0].id + : undefined; + + const currentActionTypeId = shownActionTypeId ? shownActionTypeId : actionTypeId; + + if (result[currentActionTypeId]) { + result[currentActionTypeId].total += 1; + } else { + result[currentActionTypeId] = { + actionTypeId: currentActionTypeId, + total: 1, + name: connectorTypes.find(({ id }) => id === currentActionTypeId)?.name || '', + }; + } + + return result; + }, {}); + }, [availableConnectors, connectorTypes, actionTypeRegistry]); + + const filteredConnectors = useMemo(() => { + return availableConnectors + .filter(({ actionTypeId }) => { + const subtype = actionTypeRegistry.get(actionTypeId).subtype?.map((type) => type.id); + + if (selectedConnectorType === 'all' || selectedConnectorType === '') { + return true; + } + + if (subtype?.includes(selectedConnectorType)) { + return subtype.includes(actionTypeId); + } + + return selectedConnectorType === actionTypeId; + }) + .filter(({ actionTypeId, name }) => { + const trimmedSearchValue = searchValue.trim().toLocaleLowerCase(); + if (trimmedSearchValue === '') { + return true; + } + const actionTypeModel = actionTypeRegistry.get(actionTypeId); + const actionType = connectorTypes.find(({ id }) => id === actionTypeId); + const textSearchTargets = [ + name.toLocaleLowerCase(), + actionTypeModel.selectMessage?.toLocaleLowerCase(), + actionTypeModel.actionTypeTitle?.toLocaleLowerCase(), + actionType?.name?.toLocaleLowerCase(), + ]; + return textSearchTargets.some((text) => text?.includes(trimmedSearchValue)); + }); + }, [availableConnectors, selectedConnectorType, searchValue, connectorTypes, actionTypeRegistry]); + + const connectorFacetButtons = useMemo(() => { + return ( + + + {ACTION_TYPE_MODAL_FILTER_ALL} + + {Object.values(connectorsMap) + .sort((a, b) => a.name.localeCompare(b.name)) + .map(({ actionTypeId, name, total }) => { + return ( + + {name} + + ); + })} + + ); + }, [availableConnectors, connectorsMap, selectedConnectorType, onConnectorOptionSelect]); + + const toggleFilterPopover = useCallback(() => { + setIsConenctorFilterPopoverOpen((prev) => !prev); + }, []); + const closeFilterPopover = useCallback(() => { + setIsConenctorFilterPopoverOpen(false); + }, []); + const connectorFilterButton = useMemo(() => { + const button = ( + + {ACTION_TYPE_MODAL_FILTER_LIST_TITLE} + + ); + + const options: EuiSelectableProps['options'] = Object.values(connectorsMap) + .sort((a, b) => a.name.localeCompare(b.name)) + .map(({ actionTypeId, name }) => ({ + label: name, + checked: selectedConnectorType === actionTypeId ? 'on' : undefined, + onClick: onConnectorOptionSelect(actionTypeId), + })); + + return ( + + + + {(list) =>
{list}
} +
+
+
+ ); + }, [ + closeFilterPopover, + connectorsMap, + isConenctorFilterPopoverOpen, + onConnectorOptionSelect, + toggleFilterPopover, + selectedConnectorType, + ]); + + const connectorCards = useMemo(() => { + if (!filteredConnectors.length) { + return ( + {ACTION_TYPE_MODAL_EMPTY_TITLE}} + body={ + +

{ACTION_TYPE_MODAL_EMPTY_TEXT}

+
+ } + actions={ + + {MODAL_SEARCH_CLEAR_FILTERS_TEXT} + + } + /> + ); + } + return ( + + {filteredConnectors.map((connector) => { + const { id, actionTypeId, name } = connector; + const actionTypeModel = actionTypeRegistry.get(actionTypeId); + const actionType = connectorTypes.find((item) => item.id === actionTypeId); + + if (!actionType) { + return null; + } + + const checkEnabledResult = checkActionFormActionTypeEnabled( + actionType, + preconfiguredConnectors + ); + + const isSystemActionsSelected = Boolean( + actionTypeModel.isSystemActionType && + actions.find((action) => action.actionTypeId === actionTypeModel.id) + ); + + const isDisabled = !checkEnabledResult.isEnabled || isSystemActionsSelected; + + const connectorCard = ( + + }> + + + + } + title={name} + description={ + <> + {actionTypeModel.selectMessage} + + + {actionType?.name} + + + } + onClick={() => onSelectConnectorInternal(connector)} + /> + ); + + return ( + + {checkEnabledResult.isEnabled && connectorCard} + {!checkEnabledResult.isEnabled && ( + + {connectorCard} + + )} + + ); + })} + + ); + }, [ + actions, + preconfiguredConnectors, + filteredConnectors, + actionTypeRegistry, + connectorTypes, + onSelectConnectorInternal, + onClearFilters, + ]); + + return ( + <> + + + + + + + + + {connectorFilterButton} + + + + + + + + + {connectorFacetButtons} + + + {connectorCards} + + + + + + ); +}; diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_actions/rule_actions_connectors_modal.test.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_actions/rule_actions_connectors_modal.test.tsx index d8c183820d3cb..c8e87fe3a3cff 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_actions/rule_actions_connectors_modal.test.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_actions/rule_actions_connectors_modal.test.tsx @@ -23,18 +23,16 @@ import { jest.mock('../hooks', () => ({ useRuleFormState: jest.fn(), useRuleFormDispatch: jest.fn(), + useRuleFormScreenContext: jest.fn(), })); -const { useRuleFormState, useRuleFormDispatch } = jest.requireMock('../hooks'); +const { useRuleFormState, useRuleFormDispatch, useRuleFormScreenContext } = + jest.requireMock('../hooks'); const mockConnectors: ActionConnector[] = [getConnector('1'), getConnector('2')]; const mockActionTypes: ActionType[] = [getActionType('1'), getActionType('2')]; -const mockOnClose = jest.fn(); - -const mockOnSelectConnector = jest.fn(); - const mockOnChange = jest.fn(); describe('ruleActionsConnectorsModal', () => { @@ -55,6 +53,10 @@ describe('ruleActionsConnectorsModal', () => { aadTemplateFields: [], }); useRuleFormDispatch.mockReturnValue(mockOnChange); + useRuleFormScreenContext.mockReturnValue({ + setIsConnectorsScreenVisible: false, + setIsShowRequestScreenVisible: false, + }); }); afterEach(() => { @@ -62,16 +64,12 @@ describe('ruleActionsConnectorsModal', () => { }); test('renders correctly', () => { - render( - - ); + render(); expect(screen.getByTestId('ruleActionsConnectorsModal')); }); test('should render connectors and filters', () => { - render( - - ); + render(); expect(screen.getByText('connector-1')).toBeInTheDocument(); expect(screen.getByText('connector-2')).toBeInTheDocument(); @@ -86,9 +84,7 @@ describe('ruleActionsConnectorsModal', () => { }); test('should allow for searching of connectors', async () => { - render( - - ); + render(); // Type first connector await userEvent.type(screen.getByTestId('ruleActionsConnectorsModalSearch'), 'connector-1'); @@ -116,9 +112,7 @@ describe('ruleActionsConnectorsModal', () => { }); test('should allow for filtering of connectors', async () => { - render( - - ); + render(); const filterButtonGroup = screen.getByTestId('ruleActionsConnectorsModalFilterButtonGroup'); @@ -134,40 +128,8 @@ describe('ruleActionsConnectorsModal', () => { expect(screen.getAllByTestId('ruleActionsConnectorsModalCard').length).toEqual(2); }); - test('should call onSelectConnector when connector is clicked', async () => { - render( - - ); - - await userEvent.click(screen.getByText('connector-1')); - expect(mockOnSelectConnector).toHaveBeenLastCalledWith({ - actionTypeId: 'actionType-1', - config: { config: 'config-1' }, - id: 'connector-1', - isDeprecated: false, - isPreconfigured: false, - isSystemAction: false, - name: 'connector-1', - secrets: { secret: 'secret' }, - }); - - await userEvent.click(screen.getByText('connector-2')); - expect(mockOnSelectConnector).toHaveBeenLastCalledWith({ - actionTypeId: 'actionType-2', - config: { config: 'config-2' }, - id: 'connector-2', - isDeprecated: false, - isPreconfigured: false, - isSystemAction: false, - name: 'connector-2', - secrets: { secret: 'secret' }, - }); - }); - test('should not render connector if action type doesnt exist', () => { - render( - - ); + render(); expect(screen.queryByText('connector2')).not.toBeInTheDocument(); }); @@ -188,9 +150,7 @@ describe('ruleActionsConnectorsModal', () => { connectorTypes: mockActionTypes, }); - render( - - ); + render(); expect(screen.queryByText('connector2')).not.toBeInTheDocument(); }); @@ -227,9 +187,7 @@ describe('ruleActionsConnectorsModal', () => { connectorTypes: mockActionTypes, }); - render( - - ); + render(); const filterButtonGroup = screen.getByTestId('ruleActionsConnectorsModalFilterButtonGroup'); expect(within(filterButtonGroup).getByText('actionType: 1')).toBeInTheDocument(); expect(within(filterButtonGroup).queryByText('actionType: 2')).not.toBeInTheDocument(); @@ -270,9 +228,7 @@ describe('ruleActionsConnectorsModal', () => { connectorTypes: mockActionTypes, }); - render( - - ); + render(); const filterButtonGroup = screen.getByTestId('ruleActionsConnectorsModalFilterButtonGroup'); await userEvent.click(within(filterButtonGroup).getByText('actionType: 1')); @@ -302,9 +258,7 @@ describe('ruleActionsConnectorsModal', () => { connectorTypes: mockActionTypes, }); - render( - - ); + render(); expect(screen.queryByText('connector-2')).not.toBeInTheDocument(); }); @@ -326,9 +280,7 @@ describe('ruleActionsConnectorsModal', () => { connectorTypes: [getActionType('1'), getActionType('2', { enabledInConfig: false })], }); - render( - - ); + render(); expect(screen.queryByText('connector-2')).not.toBeInTheDocument(); }); @@ -350,9 +302,7 @@ describe('ruleActionsConnectorsModal', () => { connectorTypes: [getActionType('1'), getActionType('2', { enabledInConfig: false })], }); - render( - - ); + render(); expect(screen.getByText('connector-2')).toBeInTheDocument(); }); @@ -374,9 +324,7 @@ describe('ruleActionsConnectorsModal', () => { connectorTypes: [getActionType('1'), getActionType('2', { enabledInLicense: false })], }); - render( - - ); + render(); expect(screen.getByText('connector-2')).toBeDisabled(); }); @@ -399,9 +347,7 @@ describe('ruleActionsConnectorsModal', () => { connectorTypes: [getActionType('1'), getActionType('2', { isSystemActionType: true })], }); - render( - - ); + render(); expect(screen.getByText('connector-2')).toBeDisabled(); }); diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_actions/rule_actions_connectors_modal.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_actions/rule_actions_connectors_modal.tsx index d411e468a8a83..2eea99329c3cd 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_actions/rule_actions_connectors_modal.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_actions/rule_actions_connectors_modal.tsx @@ -8,317 +8,39 @@ */ import { - EuiButton, - EuiCard, - EuiEmptyPrompt, - EuiFacetButton, - EuiFacetGroup, - EuiFieldSearch, - EuiFlexGroup, - EuiFlexItem, - EuiHorizontalRule, - EuiIcon, - EuiLoadingSpinner, EuiModal, EuiModalBody, EuiModalHeader, EuiModalHeaderTitle, - EuiSpacer, - EuiText, - EuiToolTip, useCurrentEuiBreakpoint, useEuiTheme, } from '@elastic/eui'; -import { ActionConnector, checkActionFormActionTypeEnabled } from '@kbn/alerts-ui-shared'; -import React, { Suspense, useCallback, useMemo, useState } from 'react'; -import { useRuleFormState } from '../hooks'; -import { - ACTION_TYPE_MODAL_EMPTY_TEXT, - ACTION_TYPE_MODAL_EMPTY_TITLE, - ACTION_TYPE_MODAL_FILTER_ALL, - ACTION_TYPE_MODAL_TITLE, - MODAL_SEARCH_CLEAR_FILTERS_TEXT, - MODAL_SEARCH_PLACEHOLDER, -} from '../translations'; - -type ConnectorsMap = Record; - -export interface RuleActionsConnectorsModalProps { - onClose: () => void; - onSelectConnector: (connector: ActionConnector) => void; -} - -export const RuleActionsConnectorsModal = (props: RuleActionsConnectorsModalProps) => { - const { onClose, onSelectConnector } = props; - - const [searchValue, setSearchValue] = useState(''); - const [selectedConnectorType, setSelectedConnectorType] = useState('all'); +import React, { useCallback } from 'react'; +import { ACTION_TYPE_MODAL_TITLE } from '../translations'; +import { RuleActionsConnectorsBody } from './rule_actions_connectors_body'; +import { useRuleFormScreenContext } from '../hooks'; +export const RuleActionsConnectorsModal = () => { const { euiTheme } = useEuiTheme(); const currentBreakpoint = useCurrentEuiBreakpoint() ?? 'm'; const isFullscreenPortrait = ['s', 'xs'].includes(currentBreakpoint); - const { - plugins: { actionTypeRegistry }, - formData: { actions }, - connectors, - connectorTypes, - } = useRuleFormState(); - - const preconfiguredConnectors = useMemo(() => { - return connectors.filter((connector) => connector.isPreconfigured); - }, [connectors]); - - const availableConnectors = useMemo(() => { - return connectors.filter(({ actionTypeId }) => { - const actionType = connectorTypes.find(({ id }) => id === actionTypeId); - const actionTypeModel = actionTypeRegistry.get(actionTypeId); - - if (!actionType) { - return false; - } - - if (!actionTypeModel.actionParamsFields) { - return false; - } - - const checkEnabledResult = checkActionFormActionTypeEnabled( - actionType, - preconfiguredConnectors - ); - - if (!actionType.enabledInConfig && !checkEnabledResult.isEnabled) { - return false; - } - - return true; - }); - }, [connectors, connectorTypes, preconfiguredConnectors, actionTypeRegistry]); - - const onSearchChange = useCallback((e: React.ChangeEvent) => { - setSearchValue(e.target.value); - }, []); - - const onConnectorOptionSelect = useCallback( - (id: string) => () => { - setSelectedConnectorType((prev) => { - if (prev === id) { - return ''; - } - return id; - }); - }, - [] - ); - - const onClearFilters = useCallback(() => { - setSearchValue(''); - setSelectedConnectorType('all'); - }, []); - - const connectorsMap: ConnectorsMap | null = useMemo(() => { - return availableConnectors.reduce((result, { actionTypeId }) => { - const actionTypeModel = actionTypeRegistry.get(actionTypeId); - const subtype = actionTypeModel.subtype; - - const shownActionTypeId = actionTypeModel.hideInUi - ? subtype?.filter((type) => type.id !== actionTypeId)[0].id - : undefined; - - const currentActionTypeId = shownActionTypeId ? shownActionTypeId : actionTypeId; - - if (result[currentActionTypeId]) { - result[currentActionTypeId].total += 1; - } else { - result[currentActionTypeId] = { - actionTypeId: currentActionTypeId, - total: 1, - name: connectorTypes.find(({ id }) => id === currentActionTypeId)?.name || '', - }; - } - - return result; - }, {}); - }, [availableConnectors, connectorTypes, actionTypeRegistry]); - - const filteredConnectors = useMemo(() => { - return availableConnectors - .filter(({ actionTypeId }) => { - const subtype = actionTypeRegistry.get(actionTypeId).subtype?.map((type) => type.id); - - if (selectedConnectorType === 'all' || selectedConnectorType === '') { - return true; - } - - if (subtype?.includes(selectedConnectorType)) { - return subtype.includes(actionTypeId); - } - - return selectedConnectorType === actionTypeId; - }) - .filter(({ actionTypeId, name }) => { - const trimmedSearchValue = searchValue.trim().toLocaleLowerCase(); - if (trimmedSearchValue === '') { - return true; - } - const actionTypeModel = actionTypeRegistry.get(actionTypeId); - const actionType = connectorTypes.find(({ id }) => id === actionTypeId); - const textSearchTargets = [ - name.toLocaleLowerCase(), - actionTypeModel.selectMessage?.toLocaleLowerCase(), - actionTypeModel.actionTypeTitle?.toLocaleLowerCase(), - actionType?.name?.toLocaleLowerCase(), - ]; - return textSearchTargets.some((text) => text?.includes(trimmedSearchValue)); - }); - }, [availableConnectors, selectedConnectorType, searchValue, connectorTypes, actionTypeRegistry]); - - const connectorFacetButtons = useMemo(() => { - return ( - - - {ACTION_TYPE_MODAL_FILTER_ALL} - - {Object.values(connectorsMap) - .sort((a, b) => a.name.localeCompare(b.name)) - .map(({ actionTypeId, name, total }) => { - return ( - - {name} - - ); - })} - - ); - }, [availableConnectors, connectorsMap, selectedConnectorType, onConnectorOptionSelect]); - - const connectorCards = useMemo(() => { - if (!filteredConnectors.length) { - return ( - {ACTION_TYPE_MODAL_EMPTY_TITLE}} - body={ - -

{ACTION_TYPE_MODAL_EMPTY_TEXT}

-
- } - actions={ - - {MODAL_SEARCH_CLEAR_FILTERS_TEXT} - - } - /> - ); - } - return ( - - {filteredConnectors.map((connector) => { - const { id, actionTypeId, name } = connector; - const actionTypeModel = actionTypeRegistry.get(actionTypeId); - const actionType = connectorTypes.find((item) => item.id === actionTypeId); - - if (!actionType) { - return null; - } - - const checkEnabledResult = checkActionFormActionTypeEnabled( - actionType, - preconfiguredConnectors - ); - - const isSystemActionsSelected = Boolean( - actionTypeModel.isSystemActionType && - actions.find((action) => action.actionTypeId === actionTypeModel.id) - ); - - const isDisabled = !checkEnabledResult.isEnabled || isSystemActionsSelected; - - const connectorCard = ( - - }> - - - - } - title={name} - description={ - <> - {actionTypeModel.selectMessage} - - - {actionType?.name} - - - } - onClick={() => onSelectConnector(connector)} - /> - ); - - return ( - - {checkEnabledResult.isEnabled && connectorCard} - {!checkEnabledResult.isEnabled && ( - - {connectorCard} - - )} - - ); - })} - - ); - }, [ - actions, - preconfiguredConnectors, - filteredConnectors, - actionTypeRegistry, - connectorTypes, - onSelectConnector, - onClearFilters, - ]); - - const responseiveHeight = isFullscreenPortrait ? 'initial' : '80vh'; + const responsiveHeight = isFullscreenPortrait ? 'initial' : '80vh'; const responsiveOverflow = isFullscreenPortrait ? 'auto' : 'hidden'; + const { setIsConnectorsScreenVisible } = useRuleFormScreenContext(); + const onClose = useCallback(() => { + setIsConnectorsScreenVisible(false); + }, [setIsConnectorsScreenVisible]); + return ( {ACTION_TYPE_MODAL_TITLE} - - - - - - - - - - - - - {connectorFacetButtons} - - {connectorCards} - - - - + + ); diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout.test.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout.test.tsx index ec8f85d025fb8..8525ba7b5a057 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout.test.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout.test.tsx @@ -8,7 +8,7 @@ */ import React from 'react'; -import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { fireEvent, render, screen } from '@testing-library/react'; import { RuleFlyout } from './rule_flyout'; import { RULE_FORM_PAGE_RULE_DEFINITION_TITLE_SHORT, @@ -110,30 +110,22 @@ describe('ruleFlyout', () => { render(); fireEvent.click(screen.getByTestId('ruleFlyoutFooterNextStepButton')); - await waitFor(() => - expect(screen.getByTestId('ruleFlyoutFooterPreviousStepButton')).toBeInTheDocument() - ); + expect(await screen.findByTestId('ruleFlyoutFooterPreviousStepButton')).toBeInTheDocument(); fireEvent.click(screen.getByTestId('ruleFlyoutFooterNextStepButton')); - await waitFor(() => - expect(screen.getByTestId('ruleFlyoutFooterSaveButton')).toBeInTheDocument() - ); + expect(await screen.findByTestId('ruleFlyoutFooterSaveButton')).toBeInTheDocument(); + fireEvent.click(screen.getByTestId('ruleFlyoutFooterPreviousStepButton')); - await waitFor(() => - expect(screen.getByTestId('ruleFlyoutFooterNextStepButton')).toBeInTheDocument() - ); + expect(await screen.findByTestId('ruleFlyoutFooterNextStepButton')).toBeInTheDocument(); }); test('should call onSave when save button is pressed', async () => { render(); fireEvent.click(screen.getByTestId('ruleFlyoutFooterNextStepButton')); - await waitFor(() => - expect(screen.getByTestId('ruleFlyoutFooterPreviousStepButton')).toBeInTheDocument() - ); + expect(await screen.findByTestId('ruleFlyoutFooterPreviousStepButton')).toBeInTheDocument(); fireEvent.click(screen.getByTestId('ruleFlyoutFooterNextStepButton')); - await waitFor(() => - expect(screen.getByTestId('ruleFlyoutFooterSaveButton')).toBeInTheDocument() - ); + expect(await screen.findByTestId('ruleFlyoutFooterSaveButton')).toBeInTheDocument(); + fireEvent.click(screen.getByTestId('ruleFlyoutFooterSaveButton')); expect(onSave).toHaveBeenCalledWith({ diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout.tsx index f1a873302b823..4262319d4bda3 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout.tsx @@ -8,9 +8,13 @@ */ import { EuiFlyout, EuiPortal } from '@elastic/eui'; -import React from 'react'; +import React, { useState, useCallback, useMemo } from 'react'; import type { RuleFormData } from '../types'; +import { RuleFormStepId } from '../constants'; import { RuleFlyoutBody } from './rule_flyout_body'; +import { RuleFlyoutShowRequest } from './rule_flyout_show_request'; +import { useRuleFormScreenContext } from '../hooks'; +import { RuleFlyoutSelectConnector } from './rule_flyout_select_connector'; interface RuleFlyoutProps { isEdit?: boolean; @@ -19,10 +23,39 @@ interface RuleFlyoutProps { onSave: (formData: RuleFormData) => void; } -// Wrapper component for the rule flyout. Currently only displays RuleFlyoutBody, but will be extended to conditionally -// display the Show Request UI or the Action Connector UI. These UIs take over the entire flyout, so we need to swap out -// their body elements entirely to avoid adding another EuiFlyout element to the DOM -export const RuleFlyout = ({ onSave, isEdit, isSaving, onCancel = () => {} }: RuleFlyoutProps) => { +export const RuleFlyout = ({ + onSave, + isEdit = false, + isSaving = false, + onCancel = () => {}, +}: RuleFlyoutProps) => { + const [initialStep, setInitialStep] = useState(undefined); + + const { + isConnectorsScreenVisible, + isShowRequestScreenVisible, + setIsShowRequestScreenVisible, + setIsConnectorsScreenVisible, + } = useRuleFormScreenContext(); + const onCloseConnectorsScreen = useCallback(() => { + setInitialStep(RuleFormStepId.ACTIONS); + setIsConnectorsScreenVisible(false); + }, [setIsConnectorsScreenVisible]); + + const onOpenShowRequest = useCallback( + () => setIsShowRequestScreenVisible(true), + [setIsShowRequestScreenVisible] + ); + const onCloseShowRequest = useCallback(() => { + setInitialStep(RuleFormStepId.DETAILS); + setIsShowRequestScreenVisible(false); + }, [setIsShowRequestScreenVisible]); + + const hideCloseButton = useMemo( + () => isShowRequestScreenVisible || isConnectorsScreenVisible, + [isConnectorsScreenVisible, isShowRequestScreenVisible] + ); + return ( {} }: Ru size="m" maxWidth={500} className="ruleFormFlyout__container" + hideCloseButton={hideCloseButton} > - + {isShowRequestScreenVisible ? ( + + ) : isConnectorsScreenVisible ? ( + + ) : ( + + )} ); diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout_body.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout_body.tsx index ec5590b3a587b..62244c5629a98 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout_body.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout_body.tsx @@ -28,19 +28,24 @@ import { hasRuleErrors } from '../validation'; import { RuleFlyoutCreateFooter } from './rule_flyout_create_footer'; import { RuleFlyoutEditFooter } from './rule_flyout_edit_footer'; import { RuleFlyoutEditTabs } from './rule_flyout_edit_tabs'; +import { RuleFormStepId } from '../constants'; interface RuleFlyoutBodyProps { isEdit?: boolean; isSaving?: boolean; onCancel: () => void; onSave: (formData: RuleFormData) => void; + onShowRequest: () => void; + initialStep?: RuleFormStepId; } export const RuleFlyoutBody = ({ isEdit = false, isSaving = false, + initialStep, onCancel, onSave, + onShowRequest, }: RuleFlyoutBodyProps) => { const { formData, @@ -77,7 +82,7 @@ export const RuleFlyoutBody = ({ goToPreviousStep, hasNextStep, hasPreviousStep, - } = useRuleFormHorizontalSteps(); + } = useRuleFormHorizontalSteps(initialStep); const { actions } = formData; @@ -133,7 +138,7 @@ export const RuleFlyoutBody = ({ {} /* TODO */} + onShowRequest={onShowRequest} isSaving={isSaving} hasErrors={hasErrors} /> @@ -141,7 +146,7 @@ export const RuleFlyoutBody = ({ {} /* TODO */} + onShowRequest={onShowRequest} goToNextStep={goToNextStep} goToPreviousStep={goToPreviousStep} isSaving={isSaving} diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout_select_connector.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout_select_connector.tsx new file mode 100644 index 0000000000000..1f17fb6435a49 --- /dev/null +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout_select_connector.tsx @@ -0,0 +1,65 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiTitle, + EuiButtonEmpty, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import React from 'react'; +import { + ACTION_TYPE_MODAL_TITLE, + RULE_FLYOUT_FOOTER_BACK_TEXT, + RULE_FLYOUT_HEADER_BACK_TEXT, +} from '../translations'; +import { RuleActionsConnectorsBody } from '../rule_actions/rule_actions_connectors_body'; + +interface RuleFlyoutSelectConnectorProps { + onClose: () => void; +} +export const RuleFlyoutSelectConnector = ({ onClose }: RuleFlyoutSelectConnectorProps) => { + return ( + <> + + + + + + + +

{ACTION_TYPE_MODAL_TITLE}

+
+
+
+
+ + + + + + {RULE_FLYOUT_FOOTER_BACK_TEXT} + + + + ); +}; diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout_show_request.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout_show_request.tsx new file mode 100644 index 0000000000000..fa6c14f996316 --- /dev/null +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_flyout/rule_flyout_show_request.tsx @@ -0,0 +1,73 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiText, + EuiTitle, + EuiButtonEmpty, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, +} from '@elastic/eui'; +import React from 'react'; +import { + SHOW_REQUEST_MODAL_SUBTITLE, + SHOW_REQUEST_MODAL_TITLE, + RULE_FLYOUT_FOOTER_BACK_TEXT, + RULE_FLYOUT_HEADER_BACK_TEXT, +} from '../translations'; +import { RequestCodeBlock } from '../request_code_block'; + +interface RuleFlyoutShowRequestProps { + isEdit: boolean; + onClose: () => void; +} +export const RuleFlyoutShowRequest = ({ isEdit, onClose }: RuleFlyoutShowRequestProps) => { + return ( + <> + + + + + + + +

{SHOW_REQUEST_MODAL_TITLE(isEdit)}

+
+
+
+
+ +

+ {SHOW_REQUEST_MODAL_SUBTITLE(isEdit)} +

+ + +
+ + + {RULE_FLYOUT_FOOTER_BACK_TEXT} + + + + ); +}; diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_form.scss b/src/platform/packages/shared/response-ops/rule_form/src/rule_form.scss index fd905ed8f9d04..0564dc5847979 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_form.scss +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_form.scss @@ -6,7 +6,11 @@ container-type: inline-size; } -@container (max-width: 768px) { +.actionConnectorModal__container { + container-type: inline-size; +} + +@container (max-width: 767px) { .euiDescribedFormGroup { flex-direction: column; } @@ -24,4 +28,28 @@ .ruleDefinitionHeaderRuleTypeDescription, .ruleDefinitionHeaderDocsLink { font-size: $euiFontSizeS; } +} + +[class*='showForContainer'] { + display: none; +} + +@container (max-width: 767px) and (min-width: 575px) { + .hideForContainer--s { + display: none; + } + + .showForContainer--s { + display: initial !important; + } +} + +@container (max-width: 574px) { + .hideForContainer--xs { + display: none; + } + + .showForContainer--xs { + display: initial !important; + } } \ No newline at end of file diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_form.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_form.tsx index 5b3f43a5bd4ba..61ef0d775d505 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_form.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_form.tsx @@ -19,6 +19,7 @@ import { } from './translations'; import { RuleFormPlugins } from './types'; import './rule_form.scss'; +import { RuleFormScreenContextProvider } from './rule_form_screen_context'; const queryClient = new QueryClient(); @@ -117,7 +118,9 @@ export const RuleForm = (props: RuleFormProps) => { return ( -
{ruleFormComponent}
+ +
{ruleFormComponent}
+
); }; diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_form_screen_context/index.ts b/src/platform/packages/shared/response-ops/rule_form/src/rule_form_screen_context/index.ts new file mode 100644 index 0000000000000..1804a351dcd40 --- /dev/null +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_form_screen_context/index.ts @@ -0,0 +1,10 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export * from './rule_form_screen_context'; diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_form_screen_context/rule_form_screen_context.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_form_screen_context/rule_form_screen_context.tsx new file mode 100644 index 0000000000000..15c346266c922 --- /dev/null +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_form_screen_context/rule_form_screen_context.tsx @@ -0,0 +1,41 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React, { createContext, useState } from 'react'; + +/* + * A generic wrapper for keeping track of which screens to show on top of the Rule Form + * This provides logic that works on both the Rule Page, which displays these screens in a modal, + * and the Rule Flyout, which displays these screens by replacing the entire content of the flyout. + */ +const initialRuleFormScreenContextState = { + isConnectorsScreenVisible: false, + isShowRequestScreenVisible: false, + setIsConnectorsScreenVisible: (show: boolean) => {}, + setIsShowRequestScreenVisible: (show: boolean) => {}, +}; + +export const RuleFormScreenContext = createContext(initialRuleFormScreenContextState); + +export const RuleFormScreenContextProvider: React.FC = ({ children }) => { + const [isConnectorsScreenVisible, setIsConnectorsScreenVisible] = useState(false); + const [isShowRequestScreenVisible, setIsShowRequestScreenVisible] = useState(false); + return ( + + {children} + + ); +}; diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_page/rule_page.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_page/rule_page.tsx index 52c25ee79a5d8..5479d552f2b1a 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_page/rule_page.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_page/rule_page.tsx @@ -20,7 +20,7 @@ import { } from '@elastic/eui'; import { checkActionFormActionTypeEnabled } from '@kbn/alerts-ui-shared'; import React, { useCallback, useMemo, useState } from 'react'; -import { useRuleFormState, useRuleFormSteps } from '../hooks'; +import { useRuleFormScreenContext, useRuleFormState, useRuleFormSteps } from '../hooks'; import { DISABLED_ACTIONS_WARNING_TITLE, RULE_FORM_CANCEL_MODAL_CANCEL, @@ -32,6 +32,8 @@ import { import type { RuleFormData } from '../types'; import { RulePageFooter } from './rule_page_footer'; import { RulePageNameInput } from './rule_page_name_input'; +import { RuleActionsConnectorsModal } from '../rule_actions/rule_actions_connectors_modal'; +import { RulePageShowRequestModal } from './rule_page_show_request_modal'; export interface RulePageProps { isEdit?: boolean; @@ -68,6 +70,8 @@ export const RulePage = (props: RulePageProps) => { } }, [touched, onCancel]); + const { isConnectorsScreenVisible, isShowRequestScreenVisible } = useRuleFormScreenContext(); + const hasActionsDisabled = useMemo(() => { const preconfiguredConnectors = connectors.filter((connector) => connector.isPreconfigured); return actions.some((action) => { @@ -149,6 +153,8 @@ export const RulePage = (props: RulePageProps) => {

{RULE_FORM_CANCEL_MODAL_DESCRIPTION}

)} + {isConnectorsScreenVisible && } + {isShowRequestScreenVisible && } ); }; diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_page/rule_page_footer.test.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_page/rule_page_footer.test.tsx index d937c60aa3a52..adf54ed9fb55f 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_page/rule_page_footer.test.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_page/rule_page_footer.test.tsx @@ -23,16 +23,19 @@ jest.mock('../validation/validate_form', () => ({ jest.mock('../hooks', () => ({ useRuleFormState: jest.fn(), + useRuleFormScreenContext: jest.fn(), })); const { hasRuleErrors } = jest.requireMock('../validation/validate_form'); -const { useRuleFormState } = jest.requireMock('../hooks'); +const { useRuleFormState, useRuleFormScreenContext } = jest.requireMock('../hooks'); const onSave = jest.fn(); const onCancel = jest.fn(); hasRuleErrors.mockReturnValue(false); +const mockSetIsShowRequestScreenVisible = jest.fn(); + describe('rulePageFooter', () => { beforeEach(() => { useRuleFormState.mockReturnValue({ @@ -51,6 +54,9 @@ describe('rulePageFooter', () => { actions: [], }, }); + useRuleFormScreenContext.mockReturnValue({ + setIsShowRequestScreenVisible: mockSetIsShowRequestScreenVisible, + }); }); afterEach(() => { @@ -77,7 +83,7 @@ describe('rulePageFooter', () => { render(); fireEvent.click(screen.getByTestId('rulePageFooterShowRequestButton')); - expect(screen.getByTestId('rulePageShowRequestModal')).toBeInTheDocument(); + expect(mockSetIsShowRequestScreenVisible).toHaveBeenCalledWith(true); }); test('should show create rule confirmation', () => { diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_page/rule_page_footer.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_page/rule_page_footer.tsx index 62a0e4b64e4f1..375d4c320c205 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_page/rule_page_footer.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_page/rule_page_footer.tsx @@ -15,9 +15,8 @@ import { RULE_PAGE_FOOTER_CREATE_TEXT, RULE_PAGE_FOOTER_SAVE_TEXT, } from '../translations'; -import { useRuleFormState } from '../hooks'; +import { useRuleFormScreenContext, useRuleFormState } from '../hooks'; import { hasRuleErrors } from '../validation'; -import { RulePageShowRequestModal } from './rule_page_show_request_modal'; import { RulePageConfirmCreateRule } from './rule_page_confirm_create_rule'; export interface RulePageFooterProps { @@ -28,9 +27,10 @@ export interface RulePageFooterProps { } export const RulePageFooter = (props: RulePageFooterProps) => { - const [showRequestModal, setShowRequestModal] = useState(false); const [showCreateConfirmation, setShowCreateConfirmation] = useState(false); + const { setIsShowRequestScreenVisible } = useRuleFormScreenContext(); + const { isEdit = false, isSaving = false, onCancel, onSave } = props; const { @@ -68,12 +68,8 @@ export const RulePageFooter = (props: RulePageFooterProps) => { }, [isEdit]); const onOpenShowRequestModalClick = useCallback(() => { - setShowRequestModal(true); - }, []); - - const onCloseShowRequestModalClick = useCallback(() => { - setShowRequestModal(false); - }, []); + setIsShowRequestScreenVisible(true); + }, [setIsShowRequestScreenVisible]); const onSaveClick = useCallback(() => { if (isEdit) { @@ -134,9 +130,6 @@ export const RulePageFooter = (props: RulePageFooterProps) => { - {showRequestModal && ( - - )} {showCreateConfirmation && ( ({ useRuleFormState: jest.fn(), + useRuleFormScreenContext: jest.fn(), })); -const { useRuleFormState } = jest.requireMock('../hooks'); +const { useRuleFormState, useRuleFormScreenContext } = jest.requireMock('../hooks'); const formData: RuleFormData = { params: { @@ -46,6 +47,13 @@ const formData: RuleFormData = { const onCloseMock = jest.fn(); describe('rulePageShowRequestModal', () => { + beforeEach(() => { + useRuleFormScreenContext.mockReturnValue({ + isShowRequestScreenVisible: false, + setIsShowRequestScreenVisible: onCloseMock, + }); + }); + afterEach(() => { jest.clearAllMocks(); }); @@ -53,7 +61,7 @@ describe('rulePageShowRequestModal', () => { test('renders create request correctly', async () => { useRuleFormState.mockReturnValue({ formData, multiConsumerSelection: 'logs' }); - render(); + render(); expect(screen.getByTestId('modalHeaderTitle').textContent).toBe('Create alerting rule request'); expect(screen.getByTestId('modalSubtitle').textContent).toBe( @@ -103,7 +111,7 @@ describe('rulePageShowRequestModal', () => { id: 'test-id', }); - render(); + render(); expect(screen.getByTestId('modalHeaderTitle').textContent).toBe('Edit alerting rule request'); expect(screen.getByTestId('modalSubtitle').textContent).toBe( @@ -151,7 +159,7 @@ describe('rulePageShowRequestModal', () => { id: 'test-id', }); - render(); + render(); fireEvent.click(screen.getByLabelText('Closes this modal window')); expect(onCloseMock).toHaveBeenCalled(); }); diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_page/rule_page_show_request_modal.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_page/rule_page_show_request_modal.tsx index 49d2f08fc60ab..b9adc3ca4ead1 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/rule_page/rule_page_show_request_modal.tsx +++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_page/rule_page_show_request_modal.tsx @@ -7,67 +7,32 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React, { useMemo } from 'react'; -import { pick, omit } from 'lodash'; -import { i18n } from '@kbn/i18n'; import { + EuiFlexGroup, + EuiFlexItem, EuiModal, + EuiModalBody, EuiModalHeader, EuiModalHeaderTitle, - EuiModalBody, - EuiCodeBlock, EuiText, EuiTextColor, - EuiFlexGroup, - EuiFlexItem, } from '@elastic/eui'; -import { BASE_ALERTING_API_PATH } from '../constants'; -import { RuleFormData } from '../types'; -import { - CreateRuleBody, - UPDATE_FIELDS_WITH_ACTIONS, - UpdateRuleBody, - transformCreateRuleBody, - transformUpdateRuleBody, -} from '../common/apis'; -import { useRuleFormState } from '../hooks'; - -const stringifyBodyRequest = ({ - formData, - isEdit, -}: { - formData: RuleFormData; - isEdit: boolean; -}): string => { - try { - const request = isEdit - ? transformUpdateRuleBody(pick(formData, UPDATE_FIELDS_WITH_ACTIONS) as UpdateRuleBody) - : transformCreateRuleBody(omit(formData, 'id') as CreateRuleBody); - return JSON.stringify(request, null, 2); - } catch { - return SHOW_REQUEST_MODAL_ERROR; - } -}; +import React, { useCallback } from 'react'; +import { RequestCodeBlock } from '../request_code_block'; +import { SHOW_REQUEST_MODAL_SUBTITLE, SHOW_REQUEST_MODAL_TITLE } from '../translations'; +import { useRuleFormScreenContext } from '../hooks'; export interface RulePageShowRequestModalProps { - onClose: () => void; isEdit?: boolean; } export const RulePageShowRequestModal = (props: RulePageShowRequestModalProps) => { - const { onClose, isEdit = false } = props; + const { isEdit = false } = props; + const { setIsShowRequestScreenVisible } = useRuleFormScreenContext(); - const { formData, id, multiConsumerSelection } = useRuleFormState(); - - const formattedRequest = useMemo(() => { - return stringifyBodyRequest({ - formData: { - ...formData, - ...(multiConsumerSelection ? { consumer: multiConsumerSelection } : {}), - }, - isEdit, - }); - }, [formData, isEdit, multiConsumerSelection]); + const onClose = useCallback(() => { + setIsShowRequestScreenVisible(false); + }, [setIsShowRequestScreenVisible]); return ( - - {`${isEdit ? 'PUT' : 'POST'} kbn:${BASE_ALERTING_API_PATH}/rule${ - isEdit ? `/${id}` : '' - }\n${formattedRequest}`} - + ); }; - -const SHOW_REQUEST_MODAL_EDIT = i18n.translate( - 'responseOpsRuleForm.ruleForm.showRequestModal.subheadingTitleEdit', - { - defaultMessage: 'edit', - } -); - -const SHOW_REQUEST_MODAL_CREATE = i18n.translate( - 'responseOpsRuleForm.ruleForm.showRequestModal.subheadingTitleCreate', - { - defaultMessage: 'create', - } -); - -const SHOW_REQUEST_MODAL_SUBTITLE = (edit: boolean) => - i18n.translate('responseOpsRuleForm.ruleForm.showRequestModal.subheadingTitle', { - defaultMessage: 'This Kibana request will {requestType} this rule.', - values: { requestType: edit ? SHOW_REQUEST_MODAL_EDIT : SHOW_REQUEST_MODAL_CREATE }, - }); - -const SHOW_REQUEST_MODAL_TITLE_EDIT = i18n.translate( - 'responseOpsRuleForm.ruleForm.showRequestModal.headerTitleEdit', - { - defaultMessage: 'Edit', - } -); - -const SHOW_REQUEST_MODAL_TITLE_CREATE = i18n.translate( - 'responseOpsRuleForm.ruleForm.showRequestModal.headerTitleCreate', - { - defaultMessage: 'Create', - } -); - -const SHOW_REQUEST_MODAL_TITLE = (edit: boolean) => - i18n.translate('responseOpsRuleForm.ruleForm.showRequestModal.headerTitle', { - defaultMessage: '{requestType} alerting rule request', - values: { - requestType: edit ? SHOW_REQUEST_MODAL_TITLE_EDIT : SHOW_REQUEST_MODAL_TITLE_CREATE, - }, - }); - -const SHOW_REQUEST_MODAL_ERROR = i18n.translate( - 'responseOpsRuleForm.ruleForm.showRequestModal.somethingWentWrongDescription', - { - defaultMessage: 'Sorry about that, something went wrong.', - } -); diff --git a/src/platform/packages/shared/response-ops/rule_form/src/translations.ts b/src/platform/packages/shared/response-ops/rule_form/src/translations.ts index eebe9dd2157c3..91cac8e9e99f2 100644 --- a/src/platform/packages/shared/response-ops/rule_form/src/translations.ts +++ b/src/platform/packages/shared/response-ops/rule_form/src/translations.ts @@ -343,6 +343,13 @@ export const RULE_FLYOUT_HEADER_EDIT_TITLE = i18n.translate( } ); +export const RULE_FLYOUT_HEADER_BACK_TEXT = i18n.translate( + 'responseOpsRuleForm.ruleForm.ruleFlyoutHeader.backText', + { + defaultMessage: 'Back', + } +); + export const RULE_FLYOUT_FOOTER_CANCEL_TEXT = i18n.translate( 'responseOpsRuleForm.ruleForm.ruleFlyoutFooter.cancelText', { @@ -663,6 +670,13 @@ export const ACTION_TYPE_MODAL_FILTER_ALL = i18n.translate( } ); +export const ACTION_TYPE_MODAL_FILTER_LIST_TITLE = i18n.translate( + 'responseOpsRuleForm.ruleForm.actionTypeModalFilterListTitle', + { + defaultMessage: 'Filter', + } +); + export const ACTION_TYPE_MODAL_EMPTY_TITLE = i18n.translate( 'responseOpsRuleForm.ruleForm.actionTypeModalEmptyTitle', { @@ -730,3 +744,52 @@ export const DISABLED_ACTIONS_WARNING_TITLE = i18n.translate( defaultMessage: 'This rule has actions that are disabled', } ); + +export const SHOW_REQUEST_MODAL_EDIT = i18n.translate( + 'responseOpsRuleForm.ruleForm.showRequestModal.subheadingTitleEdit', + { + defaultMessage: 'edit', + } +); + +export const SHOW_REQUEST_MODAL_CREATE = i18n.translate( + 'responseOpsRuleForm.ruleForm.showRequestModal.subheadingTitleCreate', + { + defaultMessage: 'create', + } +); + +export const SHOW_REQUEST_MODAL_SUBTITLE = (edit: boolean) => + i18n.translate('responseOpsRuleForm.ruleForm.showRequestModal.subheadingTitle', { + defaultMessage: 'This Kibana request will {requestType} this rule.', + values: { requestType: edit ? SHOW_REQUEST_MODAL_EDIT : SHOW_REQUEST_MODAL_CREATE }, + }); + +export const SHOW_REQUEST_MODAL_TITLE_EDIT = i18n.translate( + 'responseOpsRuleForm.ruleForm.showRequestModal.headerTitleEdit', + { + defaultMessage: 'Edit', + } +); + +export const SHOW_REQUEST_MODAL_TITLE_CREATE = i18n.translate( + 'responseOpsRuleForm.ruleForm.showRequestModal.headerTitleCreate', + { + defaultMessage: 'Create', + } +); + +export const SHOW_REQUEST_MODAL_TITLE = (edit: boolean) => + i18n.translate('responseOpsRuleForm.ruleForm.showRequestModal.headerTitle', { + defaultMessage: '{requestType} alerting rule request', + values: { + requestType: edit ? SHOW_REQUEST_MODAL_TITLE_EDIT : SHOW_REQUEST_MODAL_TITLE_CREATE, + }, + }); + +export const SHOW_REQUEST_MODAL_ERROR = i18n.translate( + 'responseOpsRuleForm.ruleForm.showRequestModal.somethingWentWrongDescription', + { + defaultMessage: 'Sorry about that, something went wrong.', + } +);