[Security Solutions][Detection Engine] Updates the edit rules page to only have what is selected for editing (#79233)

## Summary

Before when you would edit rules you get all the rules as disabled but you cannot switch between them in edit mode as it's already a rule you created:
<img width="1063" alt="Screen Shot 2020-10-01 at 5 06 18 PM" src="https://user-images.githubusercontent.com/1151048/94872518-0bdaba00-040a-11eb-8b7d-3b3a59980e99.png">

After, now we remove those cards and only show the card of the rule type you're editing:
<img width="1074" alt="Screen Shot 2020-10-02 at 6 29 48 PM" src="https://user-images.githubusercontent.com/1151048/94978954-50835580-04dd-11eb-9e08-8e473fc216bf.png">

Changes the card's icon placement and text.

Before:
<img width="1098" alt="Screen Shot 2020-10-02 at 9 27 44 AM" src="https://user-images.githubusercontent.com/1151048/94979008-9dffc280-04dd-11eb-8bce-88cd49b063a8.png">

After:
<img width="1190" alt="Screen Shot 2020-10-02 at 6 31 01 PM" src="https://user-images.githubusercontent.com/1151048/94979005-95a78780-04dd-11eb-92ec-e81bc3ce436e.png">

Fixes the Schedule/Actions and weirdness with CSS.
Before:
<img width="588" alt="Screen Shot 2020-10-02 at 5 42 09 PM" src="https://user-images.githubusercontent.com/1151048/94979030-c5568f80-04dd-11eb-9c91-683d75dc37da.png">
<img width="516" alt="Screen Shot 2020-10-02 at 5 42 05 PM" src="https://user-images.githubusercontent.com/1151048/94979035-c8ea1680-04dd-11eb-9049-120c9d676b1a.png">

After:
<img width="1099" alt="Screen Shot 2020-10-02 at 5 42 51 PM" src="https://user-images.githubusercontent.com/1151048/94979038-d0112480-04dd-11eb-8a2a-8eb314023669.png">

<img width="1067" alt="Screen Shot 2020-10-02 at 5 42 57 PM" src="https://user-images.githubusercontent.com/1151048/94979040-d30c1500-04dd-11eb-85ab-a09e4def6adb.png">

### Checklist

Delete any items that are not applicable to this PR.

- [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
- [ ] This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist)
- [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)
- [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers)

### For maintainers

- [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
This commit is contained in:
Frank Hassanabad 2020-10-03 09:08:02 -06:00 committed by GitHub
parent 8a8066ffe8
commit 6da7dc8695
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 553 additions and 380 deletions

View file

@ -7,33 +7,3 @@ exports[`WrapperPage it renders 1`] = `
</p>
</Memo(WrapperPageComponent)>
`;
exports[`WrapperPage restrict width custom max width when restrictWidth is number 1`] = `
<Memo(WrapperPageComponent)
restrictWidth={600}
>
<p>
Test page
</p>
</Memo(WrapperPageComponent)>
`;
exports[`WrapperPage restrict width custom max width when restrictWidth is string 1`] = `
<Memo(WrapperPageComponent)
restrictWidth="600px"
>
<p>
Test page
</p>
</Memo(WrapperPageComponent)>
`;
exports[`WrapperPage restrict width default max width when restrictWidth is true 1`] = `
<Memo(WrapperPageComponent)
restrictWidth={true}
>
<p>
Test page
</p>
</Memo(WrapperPageComponent)>
`;

View file

@ -22,42 +22,4 @@ describe('WrapperPage', () => {
expect(wrapper.find('Memo(WrapperPageComponent)')).toMatchSnapshot();
});
describe('restrict width', () => {
test('default max width when restrictWidth is true', () => {
const wrapper = shallow(
<TestProviders>
<WrapperPage restrictWidth>
<p>{'Test page'}</p>
</WrapperPage>
</TestProviders>
);
expect(wrapper.find('Memo(WrapperPageComponent)')).toMatchSnapshot();
});
test('custom max width when restrictWidth is number', () => {
const wrapper = shallow(
<TestProviders>
<WrapperPage restrictWidth={600}>
<p>{'Test page'}</p>
</WrapperPage>
</TestProviders>
);
expect(wrapper.find('Memo(WrapperPageComponent)')).toMatchSnapshot();
});
test('custom max width when restrictWidth is string', () => {
const wrapper = shallow(
<TestProviders>
<WrapperPage restrictWidth="600px">
<p>{'Test page'}</p>
</WrapperPage>
</TestProviders>
);
expect(wrapper.find('Memo(WrapperPageComponent)')).toMatchSnapshot();
});
});
});

View file

@ -16,16 +16,6 @@ import { AppGlobalStyle } from '../page/index';
const Wrapper = styled.div`
padding: ${(props) => `${props.theme.eui.paddingSizes.l}`};
&.siemWrapperPage--restrictWidthDefault,
&.siemWrapperPage--restrictWidthCustom {
box-sizing: content-box;
margin: 0 auto;
}
&.siemWrapperPage--restrictWidthDefault {
max-width: 1000px;
}
&.siemWrapperPage--fullHeight {
height: 100%;
display: flex;
@ -58,7 +48,6 @@ interface WrapperPageProps {
const WrapperPageComponent: React.FC<WrapperPageProps & CommonProps> = ({
children,
className,
restrictWidth,
style,
noPadding,
noTimeline,
@ -74,20 +63,10 @@ const WrapperPageComponent: React.FC<WrapperPageProps & CommonProps> = ({
'siemWrapperPage--noPadding': noPadding,
'siemWrapperPage--withTimeline': !noTimeline,
'siemWrapperPage--fullHeight': globalFullScreen,
'siemWrapperPage--restrictWidthDefault':
restrictWidth && typeof restrictWidth === 'boolean' && restrictWidth === true,
'siemWrapperPage--restrictWidthCustom': restrictWidth && typeof restrictWidth !== 'boolean',
});
let customStyle: WrapperPageProps['style'];
if (restrictWidth && typeof restrictWidth !== 'boolean') {
const value = typeof restrictWidth === 'number' ? `${restrictWidth}px` : restrictWidth;
customStyle = { ...style, maxWidth: value };
}
return (
<Wrapper className={classes} style={customStyle || style} {...otherProps}>
<Wrapper className={classes} style={style} {...otherProps}>
{children}
<AppGlobalStyle />
</Wrapper>

View file

@ -5,21 +5,235 @@
*/
import React from 'react';
import { shallow } from 'enzyme';
import { mount, shallow } from 'enzyme';
import { SelectRuleType } from './index';
import { useFormFieldMock } from '../../../../common/mock';
import { TestProviders, useFormFieldMock } from '../../../../common/mock';
jest.mock('../../../../common/lib/kibana');
describe('SelectRuleType', () => {
// I do this to avoid the messy warning from happening
// Warning: React does not recognize the `isVisible` prop on a DOM element.
beforeEach(() => {
jest.spyOn(console, 'error').mockImplementation(jest.fn());
});
afterEach(() => {
jest.spyOn(console, 'error').mockRestore();
});
it('renders correctly', () => {
const Component = () => {
const field = useFormFieldMock();
return <SelectRuleType field={field} />;
return (
<SelectRuleType
field={field}
describedByIds={[]}
isUpdateView={false}
hasValidLicense={true}
isMlAdmin={true}
/>
);
};
const wrapper = shallow(<Component />);
expect(wrapper.dive().find('[data-test-subj="selectRuleType"]')).toHaveLength(1);
});
describe('update mode vs. non-update mode', () => {
it('renders all the cards when not in update mode', () => {
const field = useFormFieldMock<unknown>({ value: 'query' });
const wrapper = mount(
<TestProviders>
<SelectRuleType
describedByIds={[]}
field={field}
isUpdateView={false}
hasValidLicense={true}
isMlAdmin={true}
/>
</TestProviders>
);
expect(wrapper.find('[data-test-subj="customRuleType"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="machineLearningRuleType"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="thresholdRuleType"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="eqlRuleType"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="threatMatchRuleType"]').exists()).toBeTruthy();
});
it('renders only the card selected when in update mode of "eql"', () => {
const field = useFormFieldMock<unknown>({ value: 'eql' });
const wrapper = mount(
<TestProviders>
<SelectRuleType
describedByIds={[]}
field={field}
isUpdateView={true}
hasValidLicense={true}
isMlAdmin={true}
/>
</TestProviders>
);
expect(wrapper.find('[data-test-subj="customRuleType"]').exists()).toBeFalsy();
expect(wrapper.find('[data-test-subj="machineLearningRuleType"]').exists()).toBeFalsy();
expect(wrapper.find('[data-test-subj="thresholdRuleType"]').exists()).toBeFalsy();
expect(wrapper.find('[data-test-subj="eqlRuleType"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="threatMatchRuleType"]').exists()).toBeFalsy();
});
it('renders only the card selected when in update mode of "machine_learning', () => {
const field = useFormFieldMock<unknown>({ value: 'machine_learning' });
const wrapper = mount(
<TestProviders>
<SelectRuleType
describedByIds={[]}
field={field}
isUpdateView={true}
hasValidLicense={true}
isMlAdmin={true}
/>
</TestProviders>
);
expect(wrapper.find('[data-test-subj="customRuleType"]').exists()).toBeFalsy();
expect(wrapper.find('[data-test-subj="machineLearningRuleType"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="thresholdRuleType"]').exists()).toBeFalsy();
expect(wrapper.find('[data-test-subj="eqlRuleType"]').exists()).toBeFalsy();
expect(wrapper.find('[data-test-subj="threatMatchRuleType"]').exists()).toBeFalsy();
});
it('renders only the card selected when in update mode of "query', () => {
const field = useFormFieldMock<unknown>({ value: 'query' });
const wrapper = mount(
<TestProviders>
<SelectRuleType
describedByIds={[]}
field={field}
isUpdateView={true}
hasValidLicense={true}
isMlAdmin={true}
/>
</TestProviders>
);
expect(wrapper.find('[data-test-subj="customRuleType"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="machineLearningRuleType"]').exists()).toBeFalsy();
expect(wrapper.find('[data-test-subj="thresholdRuleType"]').exists()).toBeFalsy();
expect(wrapper.find('[data-test-subj="eqlRuleType"]').exists()).toBeFalsy();
expect(wrapper.find('[data-test-subj="threatMatchRuleType"]').exists()).toBeFalsy();
});
it('renders only the card selected when in update mode of "threshold"', () => {
const field = useFormFieldMock<unknown>({ value: 'threshold' });
const wrapper = mount(
<TestProviders>
<SelectRuleType
describedByIds={[]}
field={field}
isUpdateView={true}
hasValidLicense={true}
isMlAdmin={true}
/>
</TestProviders>
);
expect(wrapper.find('[data-test-subj="customRuleType"]').exists()).toBeFalsy();
expect(wrapper.find('[data-test-subj="machineLearningRuleType"]').exists()).toBeFalsy();
expect(wrapper.find('[data-test-subj="thresholdRuleType"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="eqlRuleType"]').exists()).toBeFalsy();
expect(wrapper.find('[data-test-subj="threatMatchRuleType"]').exists()).toBeFalsy();
});
it('renders only the card selected when in update mode of "threat_match', () => {
const field = useFormFieldMock<unknown>({ value: 'threat_match' });
const wrapper = mount(
<TestProviders>
<SelectRuleType
describedByIds={[]}
field={field}
isUpdateView={true}
hasValidLicense={true}
isMlAdmin={true}
/>
</TestProviders>
);
expect(wrapper.find('[data-test-subj="customRuleType"]').exists()).toBeFalsy();
expect(wrapper.find('[data-test-subj="machineLearningRuleType"]').exists()).toBeFalsy();
expect(wrapper.find('[data-test-subj="thresholdRuleType"]').exists()).toBeFalsy();
expect(wrapper.find('[data-test-subj="eqlRuleType"]').exists()).toBeFalsy();
expect(wrapper.find('[data-test-subj="threatMatchRuleType"]').exists()).toBeTruthy();
});
});
describe('permissions', () => {
it('renders machine learning as disabled if "hasValidLicense" is false and it is not selected', () => {
const field = useFormFieldMock<unknown>({ value: 'query' });
const wrapper = mount(
<TestProviders>
<SelectRuleType
describedByIds={[]}
field={field}
isUpdateView={false}
hasValidLicense={false}
isMlAdmin={true}
/>
</TestProviders>
);
expect(
wrapper.find('[data-test-subj="machineLearningRuleType"]').first().prop('isDisabled')
).toEqual(true);
});
it('renders machine learning as not disabled if "hasValidLicense" is false and it is selected', () => {
const field = useFormFieldMock<unknown>({ value: 'machine_learning' });
const wrapper = mount(
<TestProviders>
<SelectRuleType
describedByIds={[]}
field={field}
isUpdateView={false}
hasValidLicense={false}
isMlAdmin={true}
/>
</TestProviders>
);
expect(
wrapper.find('[data-test-subj="machineLearningRuleType"]').first().prop('isDisabled')
).toEqual(false);
});
it('renders machine learning as disabled if "isMlAdmin" is false and it is not selected', () => {
const field = useFormFieldMock<unknown>({ value: 'query' });
const wrapper = mount(
<TestProviders>
<SelectRuleType
describedByIds={[]}
field={field}
isUpdateView={false}
hasValidLicense={true}
isMlAdmin={false}
/>
</TestProviders>
);
expect(
wrapper.find('[data-test-subj="machineLearningRuleType"]').first().prop('isDisabled')
).toEqual(true);
});
it('renders machine learning as not disabled if "isMlAdmin" is false and it is selected', () => {
const field = useFormFieldMock<unknown>({ value: 'machine_learning' });
const wrapper = mount(
<TestProviders>
<SelectRuleType
describedByIds={[]}
field={field}
isUpdateView={false}
hasValidLicense={true}
isMlAdmin={false}
/>
</TestProviders>
);
expect(
wrapper.find('[data-test-subj="machineLearningRuleType"]').first().prop('isDisabled')
).toEqual(false);
});
});
});

View file

@ -22,19 +22,19 @@ import { MlCardDescription } from './ml_card_description';
import EqlSearchIcon from './eql_search_icon.svg';
interface SelectRuleTypeProps {
describedByIds?: string[];
describedByIds: string[];
field: FieldHook;
hasValidLicense?: boolean;
isMlAdmin?: boolean;
isReadOnly?: boolean;
hasValidLicense: boolean;
isMlAdmin: boolean;
isUpdateView: boolean;
}
export const SelectRuleType: React.FC<SelectRuleTypeProps> = ({
describedByIds = [],
field,
isReadOnly = false,
hasValidLicense = false,
isMlAdmin = false,
isUpdateView,
hasValidLicense,
isMlAdmin,
}) => {
const ruleType = field.value as Type;
const setType = useCallback(
@ -48,54 +48,54 @@ export const SelectRuleType: React.FC<SelectRuleTypeProps> = ({
const setQuery = useCallback(() => setType('query'), [setType]);
const setThreshold = useCallback(() => setType('threshold'), [setType]);
const setThreatMatch = useCallback(() => setType('threat_match'), [setType]);
const mlCardDisabled = isReadOnly || !hasValidLicense || !isMlAdmin;
const licensingUrl = useKibana().services.application.getUrlForApp('kibana', {
path: '#/management/stack/license_management',
});
const eqlSelectableConfig = useMemo(
() => ({
isDisabled: isReadOnly,
onClick: setEql,
isSelected: isEqlRule(ruleType),
isVisible: !isUpdateView || isEqlRule(ruleType),
}),
[isReadOnly, ruleType, setEql]
[ruleType, setEql, isUpdateView]
);
const querySelectableConfig = useMemo(
() => ({
isDisabled: isReadOnly,
onClick: setQuery,
isSelected: isQueryRule(ruleType),
isVisible: !isUpdateView || isQueryRule(ruleType),
}),
[isReadOnly, ruleType, setQuery]
[ruleType, setQuery, isUpdateView]
);
const mlSelectableConfig = useMemo(
() => ({
isDisabled: mlCardDisabled,
isDisabled: !hasValidLicense || !isMlAdmin,
onClick: setMl,
isSelected: isMlRule(ruleType),
isVisible: !isUpdateView || isMlRule(ruleType),
}),
[mlCardDisabled, ruleType, setMl]
[ruleType, setMl, isUpdateView, hasValidLicense, isMlAdmin]
);
const thresholdSelectableConfig = useMemo(
() => ({
isDisabled: isReadOnly,
onClick: setThreshold,
isSelected: isThresholdRule(ruleType),
isVisible: !isUpdateView || isThresholdRule(ruleType),
}),
[isReadOnly, ruleType, setThreshold]
[ruleType, setThreshold, isUpdateView]
);
const threatMatchSelectableConfig = useMemo(
() => ({
isDisabled: isReadOnly,
onClick: setThreatMatch,
isSelected: isThreatMatchRule(ruleType),
isVisible: !isUpdateView || isThreatMatchRule(ruleType),
}),
[isReadOnly, ruleType, setThreatMatch]
[ruleType, setThreatMatch, isUpdateView]
);
return (
@ -105,63 +105,83 @@ export const SelectRuleType: React.FC<SelectRuleTypeProps> = ({
describedByIds={describedByIds}
label={field.label}
>
<EuiFlexGrid columns={4}>
<EuiFlexItem>
<EuiCard
data-test-subj="customRuleType"
title={i18n.QUERY_TYPE_TITLE}
description={i18n.QUERY_TYPE_DESCRIPTION}
icon={<EuiIcon size="l" type="search" />}
isDisabled={querySelectableConfig.isDisabled && !querySelectableConfig.isSelected}
selectable={querySelectableConfig}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiCard
data-test-subj="machineLearningRuleType"
title={i18n.ML_TYPE_TITLE}
description={
<MlCardDescription subscriptionUrl={licensingUrl} hasValidLicense={hasValidLicense} />
}
icon={<EuiIcon size="l" type="machineLearningApp" />}
isDisabled={mlSelectableConfig.isDisabled && !mlSelectableConfig.isSelected}
selectable={mlSelectableConfig}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiCard
data-test-subj="thresholdRuleType"
title={i18n.THRESHOLD_TYPE_TITLE}
description={i18n.THRESHOLD_TYPE_DESCRIPTION}
icon={<EuiIcon size="l" type="indexFlush" />}
isDisabled={
thresholdSelectableConfig.isDisabled && !thresholdSelectableConfig.isSelected
}
selectable={thresholdSelectableConfig}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiCard
data-test-subj="eqlRuleType"
title={i18n.EQL_TYPE_TITLE}
description={i18n.EQL_TYPE_DESCRIPTION}
icon={<EuiIcon size="l" type={EqlSearchIcon} />}
isDisabled={eqlSelectableConfig.isDisabled && !eqlSelectableConfig.isSelected}
selectable={eqlSelectableConfig}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiCard
data-test-subj="threatMatchRuleType"
title={i18n.THREAT_MATCH_TYPE_TITLE}
description={i18n.THREAT_MATCH_TYPE_DESCRIPTION}
icon={<EuiIcon size="l" type="list" />}
isDisabled={
threatMatchSelectableConfig.isDisabled && !threatMatchSelectableConfig.isSelected
}
selectable={threatMatchSelectableConfig}
/>
</EuiFlexItem>
<EuiFlexGrid columns={3}>
{querySelectableConfig.isVisible && (
<EuiFlexItem>
<EuiCard
data-test-subj="customRuleType"
title={i18n.QUERY_TYPE_TITLE}
titleSize="xs"
description={i18n.QUERY_TYPE_DESCRIPTION}
icon={<EuiIcon size="xl" type="search" />}
selectable={querySelectableConfig}
layout="horizontal"
textAlign="left"
/>
</EuiFlexItem>
)}
{mlSelectableConfig.isVisible && (
<EuiFlexItem>
<EuiCard
data-test-subj="machineLearningRuleType"
title={i18n.ML_TYPE_TITLE}
titleSize="xs"
description={
<MlCardDescription
subscriptionUrl={licensingUrl}
hasValidLicense={hasValidLicense}
/>
}
icon={<EuiIcon size="l" type="machineLearningApp" />}
isDisabled={mlSelectableConfig.isDisabled && !mlSelectableConfig.isSelected}
selectable={mlSelectableConfig}
layout="horizontal"
textAlign="left"
/>
</EuiFlexItem>
)}
{thresholdSelectableConfig.isVisible && (
<EuiFlexItem>
<EuiCard
data-test-subj="thresholdRuleType"
title={i18n.THRESHOLD_TYPE_TITLE}
titleSize="xs"
description={i18n.THRESHOLD_TYPE_DESCRIPTION}
icon={<EuiIcon size="l" type="indexFlush" />}
selectable={thresholdSelectableConfig}
layout="horizontal"
textAlign="left"
/>
</EuiFlexItem>
)}
{eqlSelectableConfig.isVisible && (
<EuiFlexItem>
<EuiCard
data-test-subj="eqlRuleType"
title={i18n.EQL_TYPE_TITLE}
titleSize="xs"
description={i18n.EQL_TYPE_DESCRIPTION}
icon={<EuiIcon size="l" type={EqlSearchIcon} />}
selectable={eqlSelectableConfig}
layout="horizontal"
textAlign="left"
/>
</EuiFlexItem>
)}
{threatMatchSelectableConfig.isVisible && (
<EuiFlexItem>
<EuiCard
data-test-subj="threatMatchRuleType"
title={i18n.THREAT_MATCH_TYPE_TITLE}
titleSize="xs"
description={i18n.THREAT_MATCH_TYPE_DESCRIPTION}
icon={<EuiIcon size="l" type="list" />}
selectable={threatMatchSelectableConfig}
layout="horizontal"
textAlign="left"
/>
</EuiFlexItem>
)}
</EuiFlexGrid>
</EuiFormRow>
);

View file

@ -236,7 +236,7 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
component={SelectRuleType}
componentProps={{
describedByIds: ['detectionEngineStepDefineRuleType'],
isReadOnly: isUpdateView,
isUpdateView,
hasValidLicense: hasMlLicense(mlCapabilities),
isMlAdmin: hasMlAdminPermissions(mlCapabilities),
}}

View file

@ -4,7 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiButtonEmpty, EuiAccordion, EuiHorizontalRule, EuiPanel, EuiSpacer } from '@elastic/eui';
import {
EuiButtonEmpty,
EuiAccordion,
EuiHorizontalRule,
EuiPanel,
EuiSpacer,
EuiFlexGroup,
} from '@elastic/eui';
import React, { useCallback, useRef, useState, useMemo } from 'react';
import { useHistory } from 'react-router-dom';
import styled, { StyledComponent } from 'styled-components';
@ -28,7 +35,12 @@ import { StepScheduleRule } from '../../../../components/rules/step_schedule_rul
import { StepRuleActions } from '../../../../components/rules/step_rule_actions';
import { DetectionEngineHeaderPage } from '../../../../components/detection_engine_header_page';
import * as RuleI18n from '../translations';
import { redirectToDetections, getActionMessageParams, userHasNoPermissions } from '../helpers';
import {
redirectToDetections,
getActionMessageParams,
userHasNoPermissions,
MaxWidthEuiFlexItem,
} from '../helpers';
import { RuleStep, RuleStepsFormData, RuleStepsFormHooks } from '../types';
import { formatRule, stepIsValid } from './helpers';
import * as i18n from './translations';
@ -273,151 +285,155 @@ const CreateRulePageComponent: React.FC = () => {
return (
<>
<WrapperPage restrictWidth>
<DetectionEngineHeaderPage
backOptions={{
href: getRulesUrl(),
text: i18n.BACK_TO_RULES,
pageId: SecurityPageName.detections,
}}
border
isLoading={isLoading || loading}
title={i18n.PAGE_TITLE}
/>
<MyEuiPanel zindex={4}>
<StepDefineRuleAccordion
initialIsOpen={true}
id={RuleStep.defineRule}
buttonContent={defineRuleButton}
paddingSize="xs"
ref={defineRuleRef}
onToggle={handleAccordionToggle.bind(null, RuleStep.defineRule)}
extraAction={
stepsData.current[RuleStep.defineRule].isValid && (
<EuiButtonEmpty
data-test-subj="edit-define-rule"
iconType="pencil"
size="xs"
onClick={() => editStep(RuleStep.defineRule)}
>
{i18n.EDIT_RULE}
</EuiButtonEmpty>
)
}
>
<EuiHorizontalRule margin="m" />
<StepDefineRule
addPadding={true}
defaultValues={stepsData.current[RuleStep.defineRule].data}
isReadOnlyView={activeStep !== RuleStep.defineRule}
<WrapperPage>
<EuiFlexGroup direction="row" justifyContent="spaceAround">
<MaxWidthEuiFlexItem>
<DetectionEngineHeaderPage
backOptions={{
href: getRulesUrl(),
text: i18n.BACK_TO_RULES,
pageId: SecurityPageName.detections,
}}
border
isLoading={isLoading || loading}
setForm={setFormHook}
onSubmit={() => submitStep(RuleStep.defineRule)}
descriptionColumns="singleSplit"
title={i18n.PAGE_TITLE}
/>
</StepDefineRuleAccordion>
</MyEuiPanel>
<EuiSpacer size="l" />
<MyEuiPanel zindex={3}>
<EuiAccordion
initialIsOpen={false}
id={RuleStep.aboutRule}
buttonContent={aboutRuleButton}
paddingSize="xs"
ref={aboutRuleRef}
onToggle={handleAccordionToggle.bind(null, RuleStep.aboutRule)}
extraAction={
stepsData.current[RuleStep.aboutRule].isValid && (
<EuiButtonEmpty
data-test-subj="edit-about-rule"
iconType="pencil"
size="xs"
onClick={() => editStep(RuleStep.aboutRule)}
>
{i18n.EDIT_RULE}
</EuiButtonEmpty>
)
}
>
<EuiHorizontalRule margin="m" />
<StepAboutRule
addPadding={true}
defaultValues={stepsData.current[RuleStep.aboutRule].data}
defineRuleData={stepsData.current[RuleStep.defineRule].data}
descriptionColumns="singleSplit"
isReadOnlyView={activeStep !== RuleStep.aboutRule}
isLoading={isLoading || loading}
setForm={setFormHook}
onSubmit={() => submitStep(RuleStep.aboutRule)}
/>
</EuiAccordion>
</MyEuiPanel>
<EuiSpacer size="l" />
<MyEuiPanel zindex={2}>
<EuiAccordion
initialIsOpen={false}
id={RuleStep.scheduleRule}
buttonContent={scheduleRuleButton}
paddingSize="xs"
ref={scheduleRuleRef}
onToggle={handleAccordionToggle.bind(null, RuleStep.scheduleRule)}
extraAction={
stepsData.current[RuleStep.scheduleRule].isValid && (
<EuiButtonEmpty
iconType="pencil"
size="xs"
onClick={() => editStep(RuleStep.scheduleRule)}
>
{i18n.EDIT_RULE}
</EuiButtonEmpty>
)
}
>
<EuiHorizontalRule margin="m" />
<StepScheduleRule
addPadding={true}
defaultValues={stepsData.current[RuleStep.scheduleRule].data}
descriptionColumns="singleSplit"
isReadOnlyView={activeStep !== RuleStep.scheduleRule}
isLoading={isLoading || loading}
setForm={setFormHook}
onSubmit={() => submitStep(RuleStep.scheduleRule)}
/>
</EuiAccordion>
</MyEuiPanel>
<EuiSpacer size="l" />
<MyEuiPanel zindex={1}>
<EuiAccordion
initialIsOpen={false}
id={RuleStep.ruleActions}
buttonContent={ruleActionsButton}
paddingSize="xs"
ref={ruleActionsRef}
onToggle={handleAccordionToggle.bind(null, RuleStep.ruleActions)}
extraAction={
stepsData.current[RuleStep.ruleActions].isValid && (
<EuiButtonEmpty
iconType="pencil"
size="xs"
onClick={() => editStep(RuleStep.ruleActions)}
>
{i18n.EDIT_RULE}
</EuiButtonEmpty>
)
}
>
<EuiHorizontalRule margin="m" />
<StepRuleActions
addPadding={true}
defaultValues={stepsData.current[RuleStep.ruleActions].data}
isReadOnlyView={activeStep !== RuleStep.ruleActions}
isLoading={isLoading || loading}
setForm={setFormHook}
onSubmit={() => submitStep(RuleStep.ruleActions)}
actionMessageParams={actionMessageParams}
/>
</EuiAccordion>
</MyEuiPanel>
<MyEuiPanel zindex={4}>
<StepDefineRuleAccordion
initialIsOpen={true}
id={RuleStep.defineRule}
buttonContent={defineRuleButton}
paddingSize="xs"
ref={defineRuleRef}
onToggle={handleAccordionToggle.bind(null, RuleStep.defineRule)}
extraAction={
stepsData.current[RuleStep.defineRule].isValid && (
<EuiButtonEmpty
data-test-subj="edit-define-rule"
iconType="pencil"
size="xs"
onClick={() => editStep(RuleStep.defineRule)}
>
{i18n.EDIT_RULE}
</EuiButtonEmpty>
)
}
>
<EuiHorizontalRule margin="m" />
<StepDefineRule
addPadding={true}
defaultValues={stepsData.current[RuleStep.defineRule].data}
isReadOnlyView={activeStep !== RuleStep.defineRule}
isLoading={isLoading || loading}
setForm={setFormHook}
onSubmit={() => submitStep(RuleStep.defineRule)}
descriptionColumns="singleSplit"
/>
</StepDefineRuleAccordion>
</MyEuiPanel>
<EuiSpacer size="l" />
<MyEuiPanel zindex={3}>
<EuiAccordion
initialIsOpen={false}
id={RuleStep.aboutRule}
buttonContent={aboutRuleButton}
paddingSize="xs"
ref={aboutRuleRef}
onToggle={handleAccordionToggle.bind(null, RuleStep.aboutRule)}
extraAction={
stepsData.current[RuleStep.aboutRule].isValid && (
<EuiButtonEmpty
data-test-subj="edit-about-rule"
iconType="pencil"
size="xs"
onClick={() => editStep(RuleStep.aboutRule)}
>
{i18n.EDIT_RULE}
</EuiButtonEmpty>
)
}
>
<EuiHorizontalRule margin="m" />
<StepAboutRule
addPadding={true}
defaultValues={stepsData.current[RuleStep.aboutRule].data}
defineRuleData={stepsData.current[RuleStep.defineRule].data}
descriptionColumns="singleSplit"
isReadOnlyView={activeStep !== RuleStep.aboutRule}
isLoading={isLoading || loading}
setForm={setFormHook}
onSubmit={() => submitStep(RuleStep.aboutRule)}
/>
</EuiAccordion>
</MyEuiPanel>
<EuiSpacer size="l" />
<MyEuiPanel zindex={2}>
<EuiAccordion
initialIsOpen={false}
id={RuleStep.scheduleRule}
buttonContent={scheduleRuleButton}
paddingSize="xs"
ref={scheduleRuleRef}
onToggle={handleAccordionToggle.bind(null, RuleStep.scheduleRule)}
extraAction={
stepsData.current[RuleStep.scheduleRule].isValid && (
<EuiButtonEmpty
iconType="pencil"
size="xs"
onClick={() => editStep(RuleStep.scheduleRule)}
>
{i18n.EDIT_RULE}
</EuiButtonEmpty>
)
}
>
<EuiHorizontalRule margin="m" />
<StepScheduleRule
addPadding={true}
defaultValues={stepsData.current[RuleStep.scheduleRule].data}
descriptionColumns="singleSplit"
isReadOnlyView={activeStep !== RuleStep.scheduleRule}
isLoading={isLoading || loading}
setForm={setFormHook}
onSubmit={() => submitStep(RuleStep.scheduleRule)}
/>
</EuiAccordion>
</MyEuiPanel>
<EuiSpacer size="l" />
<MyEuiPanel zindex={1}>
<EuiAccordion
initialIsOpen={false}
id={RuleStep.ruleActions}
buttonContent={ruleActionsButton}
paddingSize="xs"
ref={ruleActionsRef}
onToggle={handleAccordionToggle.bind(null, RuleStep.ruleActions)}
extraAction={
stepsData.current[RuleStep.ruleActions].isValid && (
<EuiButtonEmpty
iconType="pencil"
size="xs"
onClick={() => editStep(RuleStep.ruleActions)}
>
{i18n.EDIT_RULE}
</EuiButtonEmpty>
)
}
>
<EuiHorizontalRule margin="m" />
<StepRuleActions
addPadding={true}
defaultValues={stepsData.current[RuleStep.ruleActions].data}
isReadOnlyView={activeStep !== RuleStep.ruleActions}
isLoading={isLoading || loading}
setForm={setFormHook}
onSubmit={() => submitStep(RuleStep.ruleActions)}
actionMessageParams={actionMessageParams}
/>
</EuiAccordion>
</MyEuiPanel>
</MaxWidthEuiFlexItem>
</EuiFlexGroup>
</WrapperPage>
<SpyRoute pageName={SecurityPageName.detections} />

View file

@ -47,6 +47,7 @@ import {
redirectToDetections,
getActionMessageParams,
userHasNoPermissions,
MaxWidthEuiFlexItem,
} from '../helpers';
import * as ruleI18n from '../translations';
import { RuleStep, RuleStepsFormHooks, RuleStepsFormData, RuleStepsData } from '../types';
@ -332,75 +333,79 @@ const EditRulePageComponent: FC = () => {
return (
<>
<WrapperPage restrictWidth>
<DetectionEngineHeaderPage
backOptions={{
href: getRuleDetailsUrl(ruleId ?? ''),
text: `${i18n.BACK_TO} ${rule?.name ?? ''}`,
pageId: SecurityPageName.detections,
}}
isLoading={isLoading}
title={i18n.PAGE_TITLE}
/>
{invalidSteps.length > 0 && (
<EuiCallOut title={i18n.SORRY_ERRORS} color="danger" iconType="alert">
<FormattedMessage
id="xpack.securitySolution.detectionEngine.rule.editRule.errorMsgDescription"
defaultMessage="You have an invalid input in {countError, plural, one {this tab} other {these tabs}}: {tabHasError}"
values={{
countError: invalidSteps.length,
tabHasError: invalidSteps
.map((t) => {
if (t === RuleStep.aboutRule) {
return ruleI18n.ABOUT;
} else if (t === RuleStep.defineRule) {
return ruleI18n.DEFINITION;
} else if (t === RuleStep.scheduleRule) {
return ruleI18n.SCHEDULE;
} else if (t === RuleStep.ruleActions) {
return ruleI18n.RULE_ACTIONS;
}
return t;
})
.join(', '),
<WrapperPage>
<EuiFlexGroup direction="row" justifyContent="spaceAround">
<MaxWidthEuiFlexItem>
<DetectionEngineHeaderPage
backOptions={{
href: getRuleDetailsUrl(ruleId ?? ''),
text: `${i18n.BACK_TO} ${rule?.name ?? ''}`,
pageId: SecurityPageName.detections,
}}
/>
</EuiCallOut>
)}
<EuiTabbedContent
initialSelectedTab={tabs[0]}
selectedTab={tabs.find((t) => t.id === activeStep)}
onTabClick={onTabClick}
tabs={tabs}
/>
<EuiSpacer />
<EuiFlexGroup
alignItems="center"
gutterSize="s"
justifyContent="flexEnd"
responsive={false}
>
<EuiFlexItem grow={false}>
<EuiButton iconType="cross" onClick={goToDetailsRule}>
{i18n.CANCEL}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="ruleEditSubmitButton"
fill
onClick={onSubmit}
iconType="save"
isLoading={isLoading}
isDisabled={loading}
title={i18n.PAGE_TITLE}
/>
{invalidSteps.length > 0 && (
<EuiCallOut title={i18n.SORRY_ERRORS} color="danger" iconType="alert">
<FormattedMessage
id="xpack.securitySolution.detectionEngine.rule.editRule.errorMsgDescription"
defaultMessage="You have an invalid input in {countError, plural, one {this tab} other {these tabs}}: {tabHasError}"
values={{
countError: invalidSteps.length,
tabHasError: invalidSteps
.map((t) => {
if (t === RuleStep.aboutRule) {
return ruleI18n.ABOUT;
} else if (t === RuleStep.defineRule) {
return ruleI18n.DEFINITION;
} else if (t === RuleStep.scheduleRule) {
return ruleI18n.SCHEDULE;
} else if (t === RuleStep.ruleActions) {
return ruleI18n.RULE_ACTIONS;
}
return t;
})
.join(', '),
}}
/>
</EuiCallOut>
)}
<EuiTabbedContent
initialSelectedTab={tabs[0]}
selectedTab={tabs.find((t) => t.id === activeStep)}
onTabClick={onTabClick}
tabs={tabs}
/>
<EuiSpacer />
<EuiFlexGroup
alignItems="center"
gutterSize="s"
justifyContent="flexEnd"
responsive={false}
>
{i18n.SAVE_CHANGES}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton iconType="cross" onClick={goToDetailsRule}>
{i18n.CANCEL}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="ruleEditSubmitButton"
fill
onClick={onSubmit}
iconType="save"
isLoading={isLoading}
isDisabled={loading}
>
{i18n.SAVE_CHANGES}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</MaxWidthEuiFlexItem>
</EuiFlexGroup>
</WrapperPage>

View file

@ -9,6 +9,8 @@ import moment from 'moment';
import memoizeOne from 'memoize-one';
import { useLocation } from 'react-router-dom';
import styled from 'styled-components';
import { EuiFlexItem } from '@elastic/eui';
import { ActionVariable } from '../../../../../../triggers_actions_ui/public';
import { RuleAlertAction } from '../../../../../common/detection_engine/types';
import { assertUnreachable } from '../../../../../common/utility_types';
@ -377,3 +379,8 @@ export const getActionMessageParams = memoizeOne((ruleType: Type | undefined): A
// typed as null not undefined as the initial state for this value is null.
export const userHasNoPermissions = (canUserCRUD: boolean | null): boolean =>
canUserCRUD != null ? !canUserCRUD : false;
export const MaxWidthEuiFlexItem = styled(EuiFlexItem)`
max-width: 1000px;
overflow: hidden;
`;