Skip to content

Commit

Permalink
[Response Ops][Rule Form V2] Rule Form V2: Rule Details (#183352)
Browse files Browse the repository at this point in the history
## Summary
Issue: #179105
Related PR: #180539

Part 1: #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:

<img width="1281" alt="Screenshot 2024-05-13 at 9 44 14 PM"
src="https://github.com/elastic/kibana/assets/74562234/7ca900e3-ca9a-4810-8b24-7c3ea41055d6">

### 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 <[email protected]>
Co-authored-by: kibanamachine <[email protected]>
  • Loading branch information
JiaweiWu authored Jun 5, 2024
1 parent 90d1bc6 commit 43ce965
Show file tree
Hide file tree
Showing 12 changed files with 386 additions and 0 deletions.
2 changes: 2 additions & 0 deletions packages/kbn-alerts-ui-shared/src/rule_form/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@
*/

export * from './rule_definition';
export * from './rule_actions';
export * from './rule_details';
export * from './utils';
export * from './types';
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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(<RuleActions onClick={mockOnChange} />);

expect(screen.getByTestId('ruleActions')).toBeInTheDocument();
});

test('Calls onChange when button is click', () => {
render(<RuleActions onClick={mockOnChange} />);

fireEvent.click(screen.getByTestId('ruleActionsAddActionButton'));

expect(mockOnChange).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -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 (
<div data-test-subj="ruleActions">
<EuiButton
iconType="push"
iconSide="left"
onClick={onClick}
data-test-subj="ruleActionsAddActionButton"
>
{ADD_ACTION_TEXT}
</EuiButton>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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(
<RuleDetails
formValues={{
name: 'test',
tags: [],
}}
onChange={mockOnChange}
/>
);

expect(screen.getByTestId('ruleDetails')).toBeInTheDocument();
});

test('Should allow name to be changed', () => {
render(
<RuleDetails
formValues={{
name: 'test',
tags: [],
}}
onChange={mockOnChange}
/>
);

fireEvent.change(screen.getByTestId('ruleDetailsNameInput'), { target: { value: 'hello' } });
expect(mockOnChange).toHaveBeenCalledWith('name', 'hello');
});

test('Should allow tags to be changed', () => {
render(
<RuleDetails
formValues={{
name: 'test',
tags: [],
}}
onChange={mockOnChange}
/>
);

userEvent.type(screen.getByTestId('comboBoxInput'), 'tag{enter}');
expect(mockOnChange).toHaveBeenCalledWith('tags', ['tag']);
});

test('Should display error', () => {
render(
<RuleDetails
formValues={{
name: 'test',
tags: [],
}}
errors={{
name: 'name is invalid',
tags: 'tags is invalid',
}}
onChange={mockOnChange}
/>
);

expect(screen.getByText('name is invalid')).toBeInTheDocument();
expect(screen.getByText('tags is invalid')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -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<RuleTypeParams>['tags'];
name: SanitizedRule<RuleTypeParams>['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<HTMLInputElement>) => {
onChange('name', e.target.value);
},
[onChange]
);

const onAddTag = useCallback(
(searchValue: string) => {
onChange('tags', tags.concat([searchValue]));
},
[onChange, tags]
);

const onSetTag = useCallback(
(options: Array<EuiComboBoxOptionOption<string>>) => {
onChange(
'tags',
options.map((selectedOption) => selectedOption.label)
);
},
[onChange]
);

const onBlur = useCallback(() => {
if (!tags) {
onChange('tags', []);
}
}, [onChange, tags]);

return (
<EuiDescribedFormGroup
fullWidth
title={<h3>{RULE_DETAILS_TITLE}</h3>}
description={
<EuiText>
<p>{RULE_DETAILS_DESCRIPTION}</p>
</EuiText>
}
data-test-subj="ruleDetails"
>
<EuiFormRow
fullWidth
label={RULE_NAME_INPUT_TITLE}
isInvalid={errors.name?.length > 0}
error={errors.name}
>
<EuiFieldText
fullWidth
value={name}
onChange={onNameChange}
data-test-subj="ruleDetailsNameInput"
/>
</EuiFormRow>
<EuiFormRow
fullWidth
label={RULE_TAG_INPUT_TITLE}
isInvalid={errors.tags?.length > 0}
error={errors.tags}
>
<EuiComboBox
fullWidth
noSuggestions
data-test-subj="ruleDetailsTagsInput"
selectedOptions={tagsOptions}
onCreateOption={onAddTag}
onChange={onSetTag}
onBlur={onBlur}
/>
</EuiFormRow>
</EuiDescribedFormGroup>
);
};
29 changes: 29 additions & 0 deletions packages/kbn-alerts-ui-shared/src/rule_form/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}
);
18 changes: 18 additions & 0 deletions x-pack/examples/triggers_actions_ui_example/public/application.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand Down Expand Up @@ -174,6 +176,22 @@ const TriggersActionsUiExampleApp = ({
</Page>
)}
/>
<Route
path="/rule_actions"
render={() => (
<Page title="Rule Actions">
<RuleActionsSandbox />
</Page>
)}
/>
<Route
path="/rule_details"
render={() => (
<Page title="Rule Details">
<RuleDetailsSandbox />
</Page>
)}
/>
</EuiPage>
</Router>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <RuleActions onClick={() => {}} />;
};
Loading

0 comments on commit 43ce965

Please sign in to comment.