From 43ce965668eaad627092cd85649610566e8c9353 Mon Sep 17 00:00:00 2001 From: Jiawei Wu <74562234+JiaweiWu@users.noreply.github.com> Date: Wed, 5 Jun 2024 16:24:57 -0700 Subject: [PATCH] [Response Ops][Rule Form V2] Rule Form V2: Rule Details (#183352) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Issue: https://github.com/elastic/kibana/issues/179105 Related PR: https://github.com/elastic/kibana/pull/180539 Part 1: https://github.com/elastic/kibana/pull/183325 Part 2 of 3 PRs of new rule form. This PR depends on the code from part 1, so only merge this when part 1 has been merged. This PR extracts the last section of the rule form, the rule details, from the original PR. The design philosophy in the PR is to create components that are devoid of any fetching or form logic. These are simply dumb components. I have also created a example plugin to demonstrate this PR. To access: 1. Run the branch with yarn start --run-examples 2. Navigate to http://localhost:5601/app/triggersActionsUiExample/rule_details And you should be able to play around with the components in this PR: Screenshot 2024-05-13 at 9 44 14 PM ### Checklist - [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: Zacqary Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../src/rule_form/index.ts | 2 + .../src/rule_form/rule_actions/index.ts | 9 ++ .../rule_actions/rule_actions.test.tsx | 33 +++++ .../rule_form/rule_actions/rule_actions.tsx | 31 +++++ .../src/rule_form/rule_details/index.ts | 9 ++ .../rule_details/rule_details.test.tsx | 79 ++++++++++++ .../rule_form/rule_details/rule_details.tsx | 117 ++++++++++++++++++ .../src/rule_form/translations.ts | 29 +++++ .../public/application.tsx | 18 +++ .../rule_form/rule_actions_sandbox.tsx | 13 ++ .../rule_form/rule_details_sandbox.tsx | 36 ++++++ .../public/components/sidebar.tsx | 10 ++ 12 files changed, 386 insertions(+) create mode 100644 packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/index.ts create mode 100644 packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions.test.tsx create mode 100644 packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions.tsx create mode 100644 packages/kbn-alerts-ui-shared/src/rule_form/rule_details/index.ts create mode 100644 packages/kbn-alerts-ui-shared/src/rule_form/rule_details/rule_details.test.tsx create mode 100644 packages/kbn-alerts-ui-shared/src/rule_form/rule_details/rule_details.tsx create mode 100644 x-pack/examples/triggers_actions_ui_example/public/components/rule_form/rule_actions_sandbox.tsx create mode 100644 x-pack/examples/triggers_actions_ui_example/public/components/rule_form/rule_details_sandbox.tsx diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/index.ts b/packages/kbn-alerts-ui-shared/src/rule_form/index.ts index dbdcd4efa464f..3751b1848d23e 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/index.ts +++ b/packages/kbn-alerts-ui-shared/src/rule_form/index.ts @@ -7,5 +7,7 @@ */ export * from './rule_definition'; +export * from './rule_actions'; +export * from './rule_details'; export * from './utils'; export * from './types'; diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/index.ts b/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/index.ts new file mode 100644 index 0000000000000..bd84316ee7e2d --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/index.ts @@ -0,0 +1,9 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +export * from './rule_actions'; diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions.test.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions.test.tsx new file mode 100644 index 0000000000000..19f1482072fd9 --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions.test.tsx @@ -0,0 +1,33 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { RuleActions } from './rule_actions'; + +const mockOnChange = jest.fn(); + +describe('Rule actions', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + test('Renders correctly', () => { + render(); + + expect(screen.getByTestId('ruleActions')).toBeInTheDocument(); + }); + + test('Calls onChange when button is click', () => { + render(); + + fireEvent.click(screen.getByTestId('ruleActionsAddActionButton')); + + expect(mockOnChange).toHaveBeenCalled(); + }); +}); diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions.tsx new file mode 100644 index 0000000000000..f49cd85bbe12f --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_actions/rule_actions.tsx @@ -0,0 +1,31 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiButton } from '@elastic/eui'; +import { ADD_ACTION_TEXT } from '../translations'; + +export interface RuleActionsProps { + onClick: () => void; +} + +export const RuleActions = (props: RuleActionsProps) => { + const { onClick } = props; + return ( +
+ + {ADD_ACTION_TEXT} + +
+ ); +}; diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_details/index.ts b/packages/kbn-alerts-ui-shared/src/rule_form/rule_details/index.ts new file mode 100644 index 0000000000000..a38da3214a07d --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_details/index.ts @@ -0,0 +1,9 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +export * from './rule_details'; diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_details/rule_details.test.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_details/rule_details.test.tsx new file mode 100644 index 0000000000000..58b327af94b47 --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_details/rule_details.test.tsx @@ -0,0 +1,79 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { fireEvent, render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { RuleDetails } from './rule_details'; + +const mockOnChange = jest.fn(); + +describe('RuleDetails', () => { + test('Renders correctly', () => { + render( + + ); + + expect(screen.getByTestId('ruleDetails')).toBeInTheDocument(); + }); + + test('Should allow name to be changed', () => { + render( + + ); + + fireEvent.change(screen.getByTestId('ruleDetailsNameInput'), { target: { value: 'hello' } }); + expect(mockOnChange).toHaveBeenCalledWith('name', 'hello'); + }); + + test('Should allow tags to be changed', () => { + render( + + ); + + userEvent.type(screen.getByTestId('comboBoxInput'), 'tag{enter}'); + expect(mockOnChange).toHaveBeenCalledWith('tags', ['tag']); + }); + + test('Should display error', () => { + render( + + ); + + expect(screen.getByText('name is invalid')).toBeInTheDocument(); + expect(screen.getByText('tags is invalid')).toBeInTheDocument(); + }); +}); diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_details/rule_details.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_details/rule_details.tsx new file mode 100644 index 0000000000000..a2cd9b6b02bcf --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_details/rule_details.tsx @@ -0,0 +1,117 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import React, { useCallback, useMemo } from 'react'; +import { + EuiDescribedFormGroup, + EuiFormRow, + EuiFieldText, + EuiComboBox, + EuiComboBoxOptionOption, + EuiText, +} from '@elastic/eui'; +import type { SanitizedRule, RuleTypeParams } from '@kbn/alerting-types'; +import type { RuleFormErrors } from '../types'; +import { + RULE_DETAILS_TITLE, + RULE_DETAILS_DESCRIPTION, + RULE_NAME_INPUT_TITLE, + RULE_TAG_INPUT_TITLE, +} from '../translations'; + +export interface RuleDetailsProps { + formValues: { + tags?: SanitizedRule['tags']; + name: SanitizedRule['name']; + }; + errors?: RuleFormErrors; + onChange: (property: string, value: unknown) => void; +} + +export const RuleDetails = (props: RuleDetailsProps) => { + const { formValues, errors = {}, onChange } = props; + + const { tags = [], name } = formValues; + + const tagsOptions = useMemo(() => { + return tags.map((tag: string) => ({ label: tag })); + }, [tags]); + + const onNameChange = useCallback( + (e: React.ChangeEvent) => { + onChange('name', e.target.value); + }, + [onChange] + ); + + const onAddTag = useCallback( + (searchValue: string) => { + onChange('tags', tags.concat([searchValue])); + }, + [onChange, tags] + ); + + const onSetTag = useCallback( + (options: Array>) => { + onChange( + 'tags', + options.map((selectedOption) => selectedOption.label) + ); + }, + [onChange] + ); + + const onBlur = useCallback(() => { + if (!tags) { + onChange('tags', []); + } + }, [onChange, tags]); + + return ( + {RULE_DETAILS_TITLE}} + description={ + +

{RULE_DETAILS_DESCRIPTION}

+
+ } + data-test-subj="ruleDetails" + > + 0} + error={errors.name} + > + + + 0} + error={errors.tags} + > + + +
+ ); +}; diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/translations.ts b/packages/kbn-alerts-ui-shared/src/rule_form/translations.ts index 4ae1cbfd206c4..d9c86b60e7d9f 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_form/translations.ts +++ b/packages/kbn-alerts-ui-shared/src/rule_form/translations.ts @@ -194,3 +194,32 @@ export const INTERVAL_WARNING_TEXT = (minimum: string) => 'Intervals less than {minimum} are not recommended due to performance considerations.', values: { minimum }, }); + +export const ADD_ACTION_TEXT = i18n.translate('alertsUIShared.ruleForm.ruleActions.addActionText', { + defaultMessage: 'Add action', +}); + +export const RULE_DETAILS_TITLE = i18n.translate('alertsUIShared.ruleForm.ruleDetails.title', { + defaultMessage: 'Rule name and tags', +}); + +export const RULE_DETAILS_DESCRIPTION = i18n.translate( + 'alertsUIShared.ruleForm.ruleDetails.description', + { + defaultMessage: 'Define a name and tags for your rule.', + } +); + +export const RULE_NAME_INPUT_TITLE = i18n.translate( + 'alertsUIShared.ruleForm.ruleDetails.ruleNameInputTitle', + { + defaultMessage: 'Rule name', + } +); + +export const RULE_TAG_INPUT_TITLE = i18n.translate( + 'alertsUIShared.ruleForm.ruleDetails.ruleTagsInputTitle', + { + defaultMessage: 'Tags', + } +); diff --git a/x-pack/examples/triggers_actions_ui_example/public/application.tsx b/x-pack/examples/triggers_actions_ui_example/public/application.tsx index 6b1dfe98c22b2..b605a1245ab8d 100644 --- a/x-pack/examples/triggers_actions_ui_example/public/application.tsx +++ b/x-pack/examples/triggers_actions_ui_example/public/application.tsx @@ -39,6 +39,8 @@ import { AlertsTableSandbox } from './components/alerts_table_sandbox'; import { RulesSettingsLinkSandbox } from './components/rules_settings_link_sandbox'; import { RuleDefinitionSandbox } from './components/rule_form/rule_definition_sandbox'; +import { RuleActionsSandbox } from './components/rule_form/rule_actions_sandbox'; +import { RuleDetailsSandbox } from './components/rule_form/rule_details_sandbox'; export interface TriggersActionsUiExampleComponentParams { http: CoreStart['http']; @@ -174,6 +176,22 @@ const TriggersActionsUiExampleApp = ({ )} /> + ( + + + + )} + /> + ( + + + + )} + /> ); diff --git a/x-pack/examples/triggers_actions_ui_example/public/components/rule_form/rule_actions_sandbox.tsx b/x-pack/examples/triggers_actions_ui_example/public/components/rule_form/rule_actions_sandbox.tsx new file mode 100644 index 0000000000000..3114bad2a56bb --- /dev/null +++ b/x-pack/examples/triggers_actions_ui_example/public/components/rule_form/rule_actions_sandbox.tsx @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { RuleActions } from '@kbn/alerts-ui-shared/src/rule_form'; + +export const RuleActionsSandbox = () => { + return {}} />; +}; diff --git a/x-pack/examples/triggers_actions_ui_example/public/components/rule_form/rule_details_sandbox.tsx b/x-pack/examples/triggers_actions_ui_example/public/components/rule_form/rule_details_sandbox.tsx new file mode 100644 index 0000000000000..6c1b83d79f46c --- /dev/null +++ b/x-pack/examples/triggers_actions_ui_example/public/components/rule_form/rule_details_sandbox.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState, useCallback } from 'react'; +import { RuleDetails } from '@kbn/alerts-ui-shared/src/rule_form'; +import { EuiCodeBlock, EuiTitle } from '@elastic/eui'; + +export const RuleDetailsSandbox = () => { + const [formValues, setFormValues] = useState({ + tags: [], + name: 'test-rule', + }); + + const onChange = useCallback((property: string, value: unknown) => { + setFormValues((prevFormValues) => ({ + ...prevFormValues, + [property]: value, + })); + }, []); + + return ( + <> +
+ +

Form State

+
+ {JSON.stringify(formValues, null, 2)} +
+ + + ); +}; diff --git a/x-pack/examples/triggers_actions_ui_example/public/components/sidebar.tsx b/x-pack/examples/triggers_actions_ui_example/public/components/sidebar.tsx index caaad858b4cc4..a6dd96190574b 100644 --- a/x-pack/examples/triggers_actions_ui_example/public/components/sidebar.tsx +++ b/x-pack/examples/triggers_actions_ui_example/public/components/sidebar.tsx @@ -85,6 +85,16 @@ export const Sidebar = () => { name: 'Rule Definition', onClick: () => history.push('/rule_definition'), }, + { + id: 'rule-actions', + name: 'Rule Actions', + onClick: () => history.push('/rule_actions'), + }, + { + id: 'rule-details', + name: 'Rule Details', + onClick: () => history.push('/rule_details'), + }, ], }, ]}