diff --git a/x-pack/plugins/ml/public/components/rule_editor/__snapshots__/actions_section.test.js.snap b/x-pack/plugins/ml/public/components/rule_editor/__snapshots__/actions_section.test.js.snap new file mode 100644 index 000000000000..58af7022fc52 --- /dev/null +++ b/x-pack/plugins/ml/public/components/rule_editor/__snapshots__/actions_section.test.js.snap @@ -0,0 +1,265 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ActionsSection renders with no actions selected 1`] = ` + + +

+ Choose the action(s) to take when the rule matches an anomaly. +

+
+ + + + + + + + + + + + + + + + + + +
+`; + +exports[`ActionsSection renders with skip_result and skip_model_update selected 1`] = ` + + +

+ Choose the action(s) to take when the rule matches an anomaly. +

+
+ + + + + + + + + + + + + + + + + + +
+`; + +exports[`ActionsSection renders with skip_result selected 1`] = ` + + +

+ Choose the action(s) to take when the rule matches an anomaly. +

+
+ + + + + + + + + + + + + + + + + + +
+`; diff --git a/x-pack/plugins/ml/public/components/rule_editor/__snapshots__/condition_expression.test.js.snap b/x-pack/plugins/ml/public/components/rule_editor/__snapshots__/condition_expression.test.js.snap new file mode 100644 index 000000000000..1f69cc7657c1 --- /dev/null +++ b/x-pack/plugins/ml/public/components/rule_editor/__snapshots__/condition_expression.test.js.snap @@ -0,0 +1,395 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ConditionExpression renders with appliesTo, operator and value supplied 1`] = ` + + + + } + closePopover={[Function]} + id="appliesToPopover" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + withTitle={true} + > +
+ + When + + + + +
+
+
+ + + } + closePopover={[Function]} + id="operatorValuePopover" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + withTitle={true} + > +
+ + Is + + + + + + + + + + + +
+
+
+ + + +
+`; + +exports[`ConditionExpression renders with only value supplied 1`] = ` + + + + } + closePopover={[Function]} + id="appliesToPopover" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + withTitle={true} + > +
+ + When + + + + +
+
+
+ + + } + closePopover={[Function]} + id="operatorValuePopover" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + withTitle={true} + > +
+ + Is + + + + + + + + + + + +
+
+
+ + + +
+`; diff --git a/x-pack/plugins/ml/public/components/rule_editor/__snapshots__/conditions_section.test.js.snap b/x-pack/plugins/ml/public/components/rule_editor/__snapshots__/conditions_section.test.js.snap new file mode 100644 index 000000000000..e55f17d5d23b --- /dev/null +++ b/x-pack/plugins/ml/public/components/rule_editor/__snapshots__/conditions_section.test.js.snap @@ -0,0 +1,96 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ConditionsSectionExpression don't render when not enabled with conditions 1`] = `""`; + +exports[`ConditionsSectionExpression don't render when the section is not enabled 1`] = `""`; + +exports[`ConditionsSectionExpression renders when enabled with empty conditions supplied 1`] = ` + + + + Add new condition + + +`; + +exports[`ConditionsSectionExpression renders when enabled with no conditions supplied 1`] = ` + + + + Add new condition + + +`; + +exports[`ConditionsSectionExpression renders when enabled with one condition 1`] = ` + + + + + Add new condition + + +`; + +exports[`ConditionsSectionExpression renders when enabled with two conditions 1`] = ` + + + + + + Add new condition + + +`; diff --git a/x-pack/plugins/ml/public/components/rule_editor/__snapshots__/rule_editor_flyout.test.js.snap b/x-pack/plugins/ml/public/components/rule_editor/__snapshots__/rule_editor_flyout.test.js.snap new file mode 100644 index 000000000000..63dd75af73c8 --- /dev/null +++ b/x-pack/plugins/ml/public/components/rule_editor/__snapshots__/rule_editor_flyout.test.js.snap @@ -0,0 +1,776 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RuleEditorFlyout don't render after closing the flyout 1`] = `""`; + +exports[`RuleEditorFlyout don't render when not opened 1`] = `""`; + +exports[`RuleEditorFlyout renders the flyout after adding a condition to a rule 1`] = ` + + + + +

+ Create Rule +

+
+
+ + + + +

+ Rules instruct anomaly detectors to change their behavior based on domain-specific knowledge that you provide. When you create a rule, you can specify conditions, scope, and actions. When the conditions of a rule are satisfied, its actions are triggered. + + Learn more + +

+
+ + +

+ Action +

+
+ + + +

+ Conditions +

+
+ + + + + + + +

+ Changes to rules take effect for new results only. +

+

+ To apply these changes to existing results you must clone and rerun the job. Note rerunning the job may take some time and should only be done once you have completed all your changes to the rules for this job. +

+
+
+ + + + + Close + + + + + Save + + + + +
+
+`; + +exports[`RuleEditorFlyout renders the flyout after setting the rule to edit 1`] = ` + + + + +

+ Edit Rule +

+
+
+ + + + +

+ Rules instruct anomaly detectors to change their behavior based on domain-specific knowledge that you provide. When you create a rule, you can specify conditions, scope, and actions. When the conditions of a rule are satisfied, its actions are triggered. + + Learn more + +

+
+ + +

+ Action +

+
+ + + +

+ Conditions +

+
+ + + + + + + +

+ Changes to rules take effect for new results only. +

+

+ To apply these changes to existing results you must clone and rerun the job. Note rerunning the job may take some time and should only be done once you have completed all your changes to the rules for this job. +

+
+
+ + + + + Close + + + + + Save + + + + +
+
+`; + +exports[`RuleEditorFlyout renders the flyout for creating a rule with conditions only 1`] = ` + + + + +

+ Create Rule +

+
+
+ + + + +

+ Rules instruct anomaly detectors to change their behavior based on domain-specific knowledge that you provide. When you create a rule, you can specify conditions, scope, and actions. When the conditions of a rule are satisfied, its actions are triggered. + + Learn more + +

+
+ + +

+ Action +

+
+ + + +

+ Conditions +

+
+ + + + + + + +

+ Changes to rules take effect for new results only. +

+

+ To apply these changes to existing results you must clone and rerun the job. Note rerunning the job may take some time and should only be done once you have completed all your changes to the rules for this job. +

+
+
+ + + + + Close + + + + + Save + + + + +
+
+`; + +exports[`RuleEditorFlyout renders the select action component for a detector with a rule 1`] = ` + + + + +

+ Edit Rules +

+
+
+ + + + + + + + Close + + + + +
+
+`; diff --git a/x-pack/plugins/ml/public/components/rule_editor/__snapshots__/scope_expression.test.js.snap b/x-pack/plugins/ml/public/components/rule_editor/__snapshots__/scope_expression.test.js.snap new file mode 100644 index 000000000000..9ad80b38ed0b --- /dev/null +++ b/x-pack/plugins/ml/public/components/rule_editor/__snapshots__/scope_expression.test.js.snap @@ -0,0 +1,496 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ScopeExpression renders when empty list of filter IDs is supplied 1`] = ` + + + + + + + + +`; + +exports[`ScopeExpression renders when enabled set to false 1`] = ` + + + + + + + + + + } + closePopover={[Function]} + id="operatorValuePopover" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + withTitle={true} + > +
+ + Is + + + + + + + + + + + +
+
+
+
+`; + +exports[`ScopeExpression renders when filter ID and type supplied 1`] = ` + + + + + + + + + + } + closePopover={[Function]} + id="operatorValuePopover" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + withTitle={true} + > +
+ + Is + + + + + + + + + + + +
+
+
+
+`; + +exports[`ScopeExpression renders when no filter ID or type supplied 1`] = ` + + + + + + + + + + } + closePopover={[Function]} + id="operatorValuePopover" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + withTitle={true} + > +
+ + Is + + + + + + + + + + + +
+
+
+
+`; diff --git a/x-pack/plugins/ml/public/components/rule_editor/__snapshots__/scope_section.test.js.snap b/x-pack/plugins/ml/public/components/rule_editor/__snapshots__/scope_section.test.js.snap new file mode 100644 index 000000000000..2db4f0c742f4 --- /dev/null +++ b/x-pack/plugins/ml/public/components/rule_editor/__snapshots__/scope_section.test.js.snap @@ -0,0 +1,188 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ScopeSection don't render when no partitioning fields 1`] = `""`; + +exports[`ScopeSection false canGetFilters privilege show NoPermissionCallOut when no filter list IDs 1`] = ` + + +

+ Scope +

+
+ + + + + + + +
+`; + +exports[`ScopeSection renders when enabled with no scope supplied 1`] = ` + + +

+ Scope +

+
+ + + + + + + +
+`; + +exports[`ScopeSection renders when enabled with scope supplied 1`] = ` + + +

+ Scope +

+
+ + + + + + + +
+`; + +exports[`ScopeSection renders when not enabled 1`] = ` + + +

+ Scope +

+
+ + + + +
+`; + +exports[`ScopeSection show NoFilterListsCallOut when no filter list IDs 1`] = ` + + +

+ Scope +

+
+ + + + + + + +
+`; diff --git a/x-pack/plugins/ml/public/components/rule_editor/actions_section.test.js b/x-pack/plugins/ml/public/components/rule_editor/actions_section.test.js new file mode 100644 index 000000000000..2ed4098fa355 --- /dev/null +++ b/x-pack/plugins/ml/public/components/rule_editor/actions_section.test.js @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + + +import { shallow } from 'enzyme'; +import React from 'react'; + +import { ActionsSection } from './actions_section'; +import { ACTION } from '../../../common/constants/detector_rule'; + +describe('ActionsSection', () => { + + const onSkipResultChange = jest.fn(() => {}); + const onSkipModelUpdateChange = jest.fn(() => {}); + + const requiredProps = { + onSkipResultChange, + onSkipModelUpdateChange, + }; + + test('renders with no actions selected', () => { + const props = { + ...requiredProps, + actions: [], + }; + + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + + }); + + test('renders with skip_result selected', () => { + const props = { + ...requiredProps, + actions: [ACTION.SKIP_RESULT], + }; + + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + + + }); + + test('renders with skip_result and skip_model_update selected', () => { + + const component = shallow( + {}} + onSkipModelUpdateChange={() => {}} + /> + ); + + expect(component).toMatchSnapshot(); + + }); + +}); diff --git a/x-pack/plugins/ml/public/components/rule_editor/components/detector_description_list/__snapshots__/detector_description_list.test.js.snap b/x-pack/plugins/ml/public/components/rule_editor/components/detector_description_list/__snapshots__/detector_description_list.test.js.snap new file mode 100644 index 000000000000..6a64df207466 --- /dev/null +++ b/x-pack/plugins/ml/public/components/rule_editor/components/detector_description_list/__snapshots__/detector_description_list.test.js.snap @@ -0,0 +1,23 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DetectorDescriptionList render for farequote detector 1`] = ` + +`; diff --git a/x-pack/plugins/ml/public/components/rule_editor/components/detector_description_list/detector_description_list.test.js b/x-pack/plugins/ml/public/components/rule_editor/components/detector_description_list/detector_description_list.test.js new file mode 100644 index 000000000000..60e03fdf3e81 --- /dev/null +++ b/x-pack/plugins/ml/public/components/rule_editor/components/detector_description_list/detector_description_list.test.js @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + + +import { shallow } from 'enzyme'; +import React from 'react'; + +import { DetectorDescriptionList } from './detector_description_list'; + +describe('DetectorDescriptionList', () => { + + test('render for farequote detector', () => { + + const props = { + job: { + job_id: 'farequote' + }, + detector: { + detector_description: 'mean response time' + } + }; + + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + + }); + +}); diff --git a/x-pack/plugins/ml/public/components/rule_editor/condition_expression.test.js b/x-pack/plugins/ml/public/components/rule_editor/condition_expression.test.js new file mode 100644 index 000000000000..f8dbb61b38dc --- /dev/null +++ b/x-pack/plugins/ml/public/components/rule_editor/condition_expression.test.js @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// Mock the mlJobService that is imported for saving rules. +jest.mock('../../services/job_service.js', () => 'mlJobService'); + +import { shallow } from 'enzyme'; +import React from 'react'; + +import { ConditionExpression } from './condition_expression'; +import { APPLIES_TO, OPERATOR } from '../../../common/constants/detector_rule'; + +describe('ConditionExpression', () => { + + const updateCondition = jest.fn(() => {}); + const deleteCondition = jest.fn(() => {}); + + const requiredProps = { + index: 0, + updateCondition, + deleteCondition, + }; + + test('renders with only value supplied', () => { + const props = { + ...requiredProps, + value: 123, + }; + + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); + + test('renders with appliesTo, operator and value supplied', () => { + const props = { + ...requiredProps, + appliesTo: APPLIES_TO.DIFF_FROM_TYPICAL, + operator: OPERATOR.GREATER_THAN, + value: 123, + }; + + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); + + +}); diff --git a/x-pack/plugins/ml/public/components/rule_editor/conditions_section.test.js b/x-pack/plugins/ml/public/components/rule_editor/conditions_section.test.js new file mode 100644 index 000000000000..de676b9dce42 --- /dev/null +++ b/x-pack/plugins/ml/public/components/rule_editor/conditions_section.test.js @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +// Mock the mlJobService that is imported for saving rules. +jest.mock('../../services/job_service.js', () => 'mlJobService'); + +import { shallow } from 'enzyme'; +import React from 'react'; + +import { ConditionsSection } from './conditions_section'; +import { getNewConditionDefaults } from './utils'; +import { APPLIES_TO, OPERATOR } from '../../../common/constants/detector_rule'; + +describe('ConditionsSectionExpression', () => { + + const addCondition = jest.fn(() => {}); + const updateCondition = jest.fn(() => {}); + const deleteCondition = jest.fn(() => {}); + + const testCondition = { + applies_to: APPLIES_TO.TYPICAL, + operator: OPERATOR.GREATER_THAN_OR_EQUAL, + value: 1.23 + }; + + const requiredProps = { + addCondition, + updateCondition, + deleteCondition, + }; + + test(`don't render when the section is not enabled`, () => { + const props = { + ...requiredProps, + isEnabled: false, + }; + + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); + + test('renders when enabled with no conditions supplied', () => { + const props = { + ...requiredProps, + isEnabled: true, + }; + + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); + + test('renders when enabled with empty conditions supplied', () => { + const props = { + ...requiredProps, + isEnabled: true, + conditions: [], + }; + + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); + + test('renders when enabled with one condition', () => { + const props = { + ...requiredProps, + isEnabled: true, + conditions: [getNewConditionDefaults()], + }; + + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); + + test('renders when enabled with two conditions', () => { + const props = { + ...requiredProps, + isEnabled: true, + conditions: [getNewConditionDefaults(), testCondition], + }; + + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); + + test(`don't render when not enabled with conditions`, () => { + const props = { + ...requiredProps, + isEnabled: false, + conditions: [getNewConditionDefaults(), testCondition], + }; + + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); + +}); diff --git a/x-pack/plugins/ml/public/components/rule_editor/rule_editor_flyout.js b/x-pack/plugins/ml/public/components/rule_editor/rule_editor_flyout.js index 527a3d051f2c..8cc57db20d24 100644 --- a/x-pack/plugins/ml/public/components/rule_editor/rule_editor_flyout.js +++ b/x-pack/plugins/ml/public/components/rule_editor/rule_editor_flyout.js @@ -34,7 +34,7 @@ import { toastNotifications } from 'ui/notify'; import { DetectorDescriptionList } from './components/detector_description_list'; import { ActionsSection } from './actions_section'; -import { checkPermission } from 'plugins/ml/privilege/check_privilege'; +import { checkPermission } from '../../privilege/check_privilege'; import { ConditionsSection } from './conditions_section'; import { ScopeSection } from './scope_section'; import { SelectRuleAction } from './select_rule_action'; @@ -47,9 +47,9 @@ import { } from './utils'; import { ACTION, CONDITIONS_NOT_SUPPORTED_FUNCTIONS } from '../../../common/constants/detector_rule'; -import { getPartitioningFieldNames } from 'plugins/ml/../common/util/job_utils'; -import { mlJobService } from 'plugins/ml/services/job_service'; -import { ml } from 'plugins/ml/services/ml_api_service'; +import { getPartitioningFieldNames } from '../../../common/util/job_utils'; +import { mlJobService } from '../../services/job_service'; +import { ml } from '../../services/ml_api_service'; import { metadata } from 'ui/metadata'; import './styles/main.less'; diff --git a/x-pack/plugins/ml/public/components/rule_editor/rule_editor_flyout.test.js b/x-pack/plugins/ml/public/components/rule_editor/rule_editor_flyout.test.js new file mode 100644 index 000000000000..ee9c9bbc6a8a --- /dev/null +++ b/x-pack/plugins/ml/public/components/rule_editor/rule_editor_flyout.test.js @@ -0,0 +1,145 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + + +// Mock the services required for reading and writing job data. +jest.mock('../../services/job_service', () => ({ + mlJobService: { + getJob: () => { + return { + job_id: 'farequote_no_by', + description: 'Overall response time', + analysis_config: { + bucket_span: '5m', + detectors: [ + { + detector_description: 'mean(responsetime)', + function: 'mean', + field_name: 'responsetime', + detector_index: 0, + }, + { + detector_description: 'min(responsetime)', + function: 'max', + field_name: 'responsetime', + detector_index: 1, + custom_rules: [ + { + actions: [ + 'skip_result' + ], + conditions: [ + { + applies_to: 'diff_from_typical', + operator: 'lte', + value: 123 + } + ] + } + ] + } + ] + }, + }; + } + } +})); +jest.mock('../../services/ml_api_service', () => 'ml'); +jest.mock('../../privilege/check_privilege', () => ({ + checkPermission: () => true +})); + +import { shallow } from 'enzyme'; +import React from 'react'; + +import { RuleEditorFlyout } from './rule_editor_flyout'; + +const NO_RULE_ANOMALY = { + jobId: 'farequote_no_by', + detectorIndex: 0, + source: { + function: 'mean', + } +}; + +const RULE_ANOMALY = { + jobId: 'farequote_no_by', + detectorIndex: 1, + source: { + function: 'max', + } +}; + +function prepareTest() { + + const setShowFunction = jest.fn(() => {}); + const unsetShowFunction = jest.fn(() => {}); + + const requiredProps = { + setShowFunction, + unsetShowFunction, + }; + + const component = ( + + ); + + const wrapper = shallow(component); + + return { wrapper }; +} + +describe('RuleEditorFlyout', () => { + + test(`don't render when not opened`, () => { + const test1 = prepareTest(); + expect(test1.wrapper).toMatchSnapshot(); + }); + + test('renders the flyout for creating a rule with conditions only', () => { + const test2 = prepareTest(); + test2.wrapper.instance().showFlyout(NO_RULE_ANOMALY); + test2.wrapper.update(); + expect(test2.wrapper).toMatchSnapshot(); + }); + + test('renders the flyout after adding a condition to a rule', () => { + const test3 = prepareTest(); + const instance = test3.wrapper.instance(); + instance.showFlyout(NO_RULE_ANOMALY); + instance.addCondition(); + test3.wrapper.update(); + expect(test3.wrapper).toMatchSnapshot(); + }); + + test('renders the select action component for a detector with a rule', () => { + const test4 = prepareTest(); + const instance = test4.wrapper.instance(); + instance.showFlyout(RULE_ANOMALY); + test4.wrapper.update(); + expect(test4.wrapper).toMatchSnapshot(); + }); + + test('renders the flyout after setting the rule to edit', () => { + const test5 = prepareTest(); + const instance = test5.wrapper.instance(); + instance.showFlyout(RULE_ANOMALY); + instance.setEditRuleIndex(0); + test5.wrapper.update(); + expect(test5.wrapper).toMatchSnapshot(); + }); + + test(`don't render after closing the flyout`, () => { + const test6 = prepareTest(); + const instance = test6.wrapper.instance(); + instance.showFlyout(RULE_ANOMALY); + instance.setEditRuleIndex(0); + instance.closeFlyout(); + test6.wrapper.update(); + expect(test6.wrapper).toMatchSnapshot(); + }); + +}); diff --git a/x-pack/plugins/ml/public/components/rule_editor/scope_expression.js b/x-pack/plugins/ml/public/components/rule_editor/scope_expression.js index 64bbe0be2da9..6486562faddf 100644 --- a/x-pack/plugins/ml/public/components/rule_editor/scope_expression.js +++ b/x-pack/plugins/ml/public/components/rule_editor/scope_expression.js @@ -157,7 +157,7 @@ export class ScopeExpression extends Component { button={( diff --git a/x-pack/plugins/ml/public/components/rule_editor/scope_expression.test.js b/x-pack/plugins/ml/public/components/rule_editor/scope_expression.test.js new file mode 100644 index 000000000000..b36e2192f056 --- /dev/null +++ b/x-pack/plugins/ml/public/components/rule_editor/scope_expression.test.js @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + + +// Mock the mlJobService that is imported for saving rules. +jest.mock('../../services/job_service.js', () => 'mlJobService'); + +import { shallow } from 'enzyme'; +import React from 'react'; + +import { ScopeExpression } from './scope_expression'; +import { FILTER_TYPE } from '../../../common/constants/detector_rule'; + +describe('ScopeExpression', () => { + + const testFilterListIds = [ + 'web_domains', + 'safe_domains', + 'uk_domains', + ]; + const updateScope = jest.fn(() => {}); + + const requiredProps = { + fieldName: 'domain', + updateScope, + }; + + + test('renders when no filter ID or type supplied', () => { + const props = { + ...requiredProps, + filterListIds: testFilterListIds, + enabled: true, + }; + + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); + + test('renders when empty list of filter IDs is supplied', () => { + const props = { + ...requiredProps, + filterListIds: [], + enabled: true, + }; + + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); + + test('renders when filter ID and type supplied', () => { + const props = { + ...requiredProps, + filterListIds: testFilterListIds, + filterId: 'safe_domains', + filterType: FILTER_TYPE.INCLUDE, + enabled: true, + }; + + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); + + test('renders when enabled set to false', () => { + const props = { + ...requiredProps, + filterListIds: testFilterListIds, + filterId: 'safe_domains', + filterType: FILTER_TYPE.INCLUDE, + enabled: false, + }; + + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); + + +}); diff --git a/x-pack/plugins/ml/public/components/rule_editor/scope_section.js b/x-pack/plugins/ml/public/components/rule_editor/scope_section.js index 1195d93b2f8d..446a3df9f5cc 100644 --- a/x-pack/plugins/ml/public/components/rule_editor/scope_section.js +++ b/x-pack/plugins/ml/public/components/rule_editor/scope_section.js @@ -21,7 +21,7 @@ import { } from '@elastic/eui'; import { ScopeExpression } from './scope_expression'; -import { checkPermission } from 'plugins/ml/privilege/check_privilege'; +import { checkPermission } from '../../privilege/check_privilege'; import { getScopeFieldDefaults } from './utils'; diff --git a/x-pack/plugins/ml/public/components/rule_editor/scope_section.test.js b/x-pack/plugins/ml/public/components/rule_editor/scope_section.test.js new file mode 100644 index 000000000000..57a3d35a5af6 --- /dev/null +++ b/x-pack/plugins/ml/public/components/rule_editor/scope_section.test.js @@ -0,0 +1,160 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + + +// Mock the mlJobService that is imported for saving rules. +jest.mock('../../services/job_service.js', () => 'mlJobService'); + +// Create a mock for the canGetFilters privilege check. +// The mock is hoisted to the top, so need to prefix the mock function +// with 'mock' so it can be used lazily. +const mockCheckPermission = jest.fn(() => true); +jest.mock('../../privilege/check_privilege', () => ({ + checkPermission: (privilege) => mockCheckPermission(privilege) +})); + +import { shallow } from 'enzyme'; +import React from 'react'; + +import { ScopeSection } from './scope_section'; +import { FILTER_TYPE } from '../../../common/constants/detector_rule'; + + +describe('ScopeSection', () => { + + const testFilterListIds = [ + 'web_domains', + 'safe_domains', + 'uk_domains', + ]; + + const testScope = { + domain: { + filter_id: 'uk_domains', + filter_type: FILTER_TYPE.INCLUDE, + enabled: true, + } + }; + + const onEnabledChange = jest.fn(() => {}); + const updateScope = jest.fn(() => {}); + + const requiredProps = { + filterListIds: testFilterListIds, + onEnabledChange, + updateScope, + }; + + test('renders when not enabled', () => { + const props = { + ...requiredProps, + partitioningFieldNames: ['domain'], + isEnabled: false, + }; + + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); + + test(`don't render when no partitioning fields`, () => { + const props = { + ...requiredProps, + partitioningFieldNames: [], + isEnabled: false, + }; + + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); + + test('show NoFilterListsCallOut when no filter list IDs', () => { + const props = { + ...requiredProps, + partitioningFieldNames: ['domain'], + filterListIds: [], + isEnabled: true, + }; + + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); + + test('renders when enabled with no scope supplied', () => { + const props = { + ...requiredProps, + partitioningFieldNames: ['domain'], + isEnabled: true, + }; + + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); + + test('renders when enabled with scope supplied', () => { + const props = { + ...requiredProps, + partitioningFieldNames: ['domain'], + scope: testScope, + isEnabled: true, + }; + + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); + +}); + +describe('ScopeSection false canGetFilters privilege', () => { + + beforeEach(() => { + jest.resetModules(); + }); + + const onEnabledChange = jest.fn(() => {}); + const updateScope = jest.fn(() => {}); + + const requiredProps = { + onEnabledChange, + updateScope, + }; + + + test('show NoPermissionCallOut when no filter list IDs', () => { + + mockCheckPermission.mockImplementationOnce(() => { + return false; + }); + + const props = { + ...requiredProps, + partitioningFieldNames: ['domain'], + filterListIds: [], + isEnabled: true, + }; + + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); + +}); diff --git a/x-pack/plugins/ml/public/components/rule_editor/select_rule_action/__snapshots__/delete_rule_modal.test.js.snap b/x-pack/plugins/ml/public/components/rule_editor/select_rule_action/__snapshots__/delete_rule_modal.test.js.snap new file mode 100644 index 000000000000..1fc18f640afc --- /dev/null +++ b/x-pack/plugins/ml/public/components/rule_editor/select_rule_action/__snapshots__/delete_rule_modal.test.js.snap @@ -0,0 +1,52 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DeleteRuleModal renders as delete button after opening and closing modal 1`] = ` + + + Delete rule + + +`; + +exports[`DeleteRuleModal renders as delete button when not visible 1`] = ` + + + Delete rule + + +`; + +exports[`DeleteRuleModal renders modal after clicking delete rule link 1`] = ` + + + Delete rule + + + +

+ Are you sure you want to delete this rule? +

+
+
+
+`; diff --git a/x-pack/plugins/ml/public/components/rule_editor/select_rule_action/__snapshots__/rule_action_panel.test.js.snap b/x-pack/plugins/ml/public/components/rule_editor/select_rule_action/__snapshots__/rule_action_panel.test.js.snap new file mode 100644 index 000000000000..e17c666053d1 --- /dev/null +++ b/x-pack/plugins/ml/public/components/rule_editor/select_rule_action/__snapshots__/rule_action_panel.test.js.snap @@ -0,0 +1,124 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RuleActionPanel renders panel for rule with a condition 1`] = ` + + + Edit rule + , + "title": "actions", + }, + Object { + "description": , + "title": "", + }, + ] + } + textStyle="normal" + type="column" + /> + +`; + +exports[`RuleActionPanel renders panel for rule with a condition and scope 1`] = ` + + + Edit rule + , + "title": "actions", + }, + Object { + "description": , + "title": "", + }, + ] + } + textStyle="normal" + type="column" + /> + +`; + +exports[`RuleActionPanel renders panel for rule with scope 1`] = ` + + + Edit rule + , + "title": "actions", + }, + Object { + "description": , + "title": "", + }, + ] + } + textStyle="normal" + type="column" + /> + +`; diff --git a/x-pack/plugins/ml/public/components/rule_editor/select_rule_action/delete_rule_modal.test.js b/x-pack/plugins/ml/public/components/rule_editor/select_rule_action/delete_rule_modal.test.js new file mode 100644 index 000000000000..c7617c815263 --- /dev/null +++ b/x-pack/plugins/ml/public/components/rule_editor/select_rule_action/delete_rule_modal.test.js @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + + +import { shallow } from 'enzyme'; +import React from 'react'; + +import { DeleteRuleModal } from './delete_rule_modal'; + +describe('DeleteRuleModal', () => { + + const deleteRuleAtIndex = jest.fn(() => {}); + + const requiredProps = { + ruleIndex: 0, + deleteRuleAtIndex, + }; + + test('renders as delete button when not visible', () => { + const props = { + ...requiredProps, + }; + + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + + }); + + test('renders modal after clicking delete rule link', () => { + const props = { + ...requiredProps, + }; + + const wrapper = shallow(); + wrapper.find('EuiLink').simulate('click'); + wrapper.update(); + expect(wrapper).toMatchSnapshot(); + + }); + + test('renders as delete button after opening and closing modal', () => { + const props = { + ...requiredProps, + }; + + const wrapper = shallow(); + wrapper.find('EuiLink').simulate('click'); + const instance = wrapper.instance(); + instance.closeModal(); + wrapper.update(); + expect(wrapper).toMatchSnapshot(); + + }); + +}); diff --git a/x-pack/plugins/ml/public/components/rule_editor/select_rule_action/rule_action_panel.js b/x-pack/plugins/ml/public/components/rule_editor/select_rule_action/rule_action_panel.js index 8116f207d417..f73f06d42174 100644 --- a/x-pack/plugins/ml/public/components/rule_editor/select_rule_action/rule_action_panel.js +++ b/x-pack/plugins/ml/public/components/rule_editor/select_rule_action/rule_action_panel.js @@ -80,6 +80,7 @@ export function RuleActionPanel({ ); } RuleActionPanel.propTypes = { + job: PropTypes.object.isRequired, detectorIndex: PropTypes.number.isRequired, ruleIndex: PropTypes.number.isRequired, setEditRuleIndex: PropTypes.func.isRequired, diff --git a/x-pack/plugins/ml/public/components/rule_editor/select_rule_action/rule_action_panel.test.js b/x-pack/plugins/ml/public/components/rule_editor/select_rule_action/rule_action_panel.test.js new file mode 100644 index 000000000000..8212b30a5308 --- /dev/null +++ b/x-pack/plugins/ml/public/components/rule_editor/select_rule_action/rule_action_panel.test.js @@ -0,0 +1,120 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +jest.mock('../../../services/job_service.js', () => 'mlJobService'); + +import { shallow } from 'enzyme'; +import React from 'react'; + +import { RuleActionPanel } from './rule_action_panel'; +import { ACTION } from '../../../../common/constants/detector_rule'; + +describe('RuleActionPanel', () => { + + const job = { + job_id: 'farequote', + analysis_config: { + detectors: [ + { + detector_description: 'mean response time', + custom_rules: [ + { + actions: [ + ACTION.SKIP_RESULT + ], + conditions: [ + { + applies_to: 'actual', + operator: 'lt', + value: 1 + } + ] + }, + { + actions: [ + ACTION.SKIP_MODEL_UPDATE + ], + scope: { + instance: { + filter_id: 'eu-airlines', + filter_type: 'exclude' + } + } + }, + { + actions: [ + ACTION.SKIP_MODEL_UPDATE + ], + scope: { + instance: { + filter_id: 'eu-airlines', + filter_type: 'exclude' + } + }, + conditions: [ + { + applies_to: 'actual', + operator: 'gt', + value: 500 + } + ] + }, + ], + detector_index: 0 + } + ] + }, + }; + + test('renders panel for rule with a condition', () => { + + const component = shallow( + {}} + deleteRuleAtIndex={() => {}} + /> + ); + + expect(component).toMatchSnapshot(); + + }); + + test('renders panel for rule with scope ', () => { + + const component = shallow( + {}} + deleteRuleAtIndex={() => {}} + /> + ); + + expect(component).toMatchSnapshot(); + + }); + + test('renders panel for rule with a condition and scope ', () => { + + const component = shallow( + {}} + deleteRuleAtIndex={() => {}} + /> + ); + + expect(component).toMatchSnapshot(); + + }); + +}); diff --git a/x-pack/plugins/ml/public/components/rule_editor/utils.js b/x-pack/plugins/ml/public/components/rule_editor/utils.js index c18a321914de..d60e50cd6353 100644 --- a/x-pack/plugins/ml/public/components/rule_editor/utils.js +++ b/x-pack/plugins/ml/public/components/rule_editor/utils.js @@ -12,7 +12,7 @@ import { } from '../../../common/constants/detector_rule'; import { cloneDeep } from 'lodash'; -import { mlJobService } from 'plugins/ml/services/job_service'; +import { mlJobService } from '../../services/job_service'; export function getNewConditionDefaults() { return { @@ -212,7 +212,7 @@ export function filterTypeToText(filterType) { return 'not in'; default: - return filterType; + return (filterType !== undefined) ? filterType : ''; } } @@ -228,7 +228,7 @@ export function appliesToText(appliesTo) { return 'diff from typical'; default: - return appliesTo; + return (appliesTo !== undefined) ? appliesTo : ''; } } @@ -247,6 +247,6 @@ export function operatorToText(operator) { return 'greater than or equal to'; default: - return operator; + return (operator !== undefined) ? operator : ''; } } diff --git a/x-pack/plugins/ml/public/services/job_service.js b/x-pack/plugins/ml/public/services/job_service.js index a6baa69c34a7..825141a586a0 100644 --- a/x-pack/plugins/ml/public/services/job_service.js +++ b/x-pack/plugins/ml/public/services/job_service.js @@ -11,12 +11,12 @@ import angular from 'angular'; import moment from 'moment'; import { parseInterval } from 'ui/utils/parse_interval'; -import { ml } from 'plugins/ml/services/ml_api_service'; +import { ml } from './ml_api_service'; -import { labelDuplicateDetectorDescriptions } from 'plugins/ml/../common/util/anomaly_utils'; -import { mlMessageBarService } from 'plugins/ml/components/messagebar/messagebar_service'; -import { isWebUrl } from 'plugins/ml/util/string_utils'; -import { ML_DATA_PREVIEW_COUNT } from 'plugins/ml/../common/util/job_utils'; +import { labelDuplicateDetectorDescriptions } from '../../common/util/anomaly_utils'; +import { mlMessageBarService } from '../components/messagebar/messagebar_service'; +import { isWebUrl } from '../util/string_utils'; +import { ML_DATA_PREVIEW_COUNT } from '../../common/util/job_utils'; const msgs = mlMessageBarService; let jobs = [];