Skip to content

Commit 4c586a6

Browse files
authored
[FEATURE] Rule form validation on submit (#264)
* WIP: Formik validation for rule editor Signed-off-by: Aleksandar Djindjic <[email protected]> * add more fields Signed-off-by: Aleksandar Djindjic <[email protected]> * handing submition to the backend in RuleEditor Signed-off-by: Aleksandar Djindjic <[email protected]> * update yaml rule editor snapshot Signed-off-by: Aleksandar Djindjic <[email protected]> * fix cypress testing fails Signed-off-by: Aleksandar Djindjic <[email protected]> * fix cypress test Signed-off-by: Aleksandar Djindjic <[email protected]> * update snapshot Signed-off-by: Aleksandar Djindjic <[email protected]> * useCallback optimization Signed-off-by: Aleksandar Djindjic <[email protected]> Signed-off-by: Aleksandar Djindjic <[email protected]>
1 parent ec6526d commit 4c586a6

File tree

11 files changed

+562
-340
lines changed

11 files changed

+562
-340
lines changed

cypress/integration/2_rules.spec.js

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -70,33 +70,35 @@ describe('Rules', () => {
7070
cy.get('[data-test-subj="rule_name_field"]').type(SAMPLE_RULE.name);
7171

7272
// Enter the log type
73-
cy.get('[data-test-subj="rule_type_dropdown"]').select(SAMPLE_RULE.logType);
73+
cy.get('[data-test-subj="rule_type_dropdown"]').type(SAMPLE_RULE.logType);
7474

7575
// Enter the description
7676
cy.get('[data-test-subj="rule_description_field"]').type(SAMPLE_RULE.description);
7777

78-
// Enter the detection
79-
cy.get('[data-test-subj="rule_detection_field"]').type(SAMPLE_RULE.detection);
80-
8178
// Enter the severity
82-
cy.get('[data-test-subj="rule_severity_dropdown"]').select(SAMPLE_RULE.severity);
79+
cy.get('[data-test-subj="rule_severity_dropdown"]').type(SAMPLE_RULE.severity);
8380

8481
// Enter the tags
8582
SAMPLE_RULE.tags.forEach((tag) =>
8683
cy.get('[data-test-subj="rule_tags_dropdown"]').type(`${tag}{enter}{esc}`)
8784
);
8885

8986
// Enter the reference
90-
cy.get('[data-test-subj="rule_references_field_0"]').type(SAMPLE_RULE.references);
87+
cy.get('[data-test-subj="rule_references_-_optional_field_0"]').type(SAMPLE_RULE.references);
9188

9289
// Enter the false positive cases
93-
cy.get('[data-test-subj="rule_false_positive_cases_field_0"]').type(SAMPLE_RULE.falsePositive);
90+
cy.get('[data-test-subj="rule_false_positive_cases_-_optional_field_0"]').type(
91+
SAMPLE_RULE.falsePositive
92+
);
9493

9594
// Enter the author
9695
cy.get('[data-test-subj="rule_author_field"]').type(SAMPLE_RULE.author);
9796

9897
// Enter the log type
99-
cy.get('[data-test-subj="rule_status_dropdown"]').select(SAMPLE_RULE.status);
98+
cy.get('[data-test-subj="rule_status_dropdown"]').type(SAMPLE_RULE.status);
99+
100+
// Enter the detection
101+
cy.get('[data-test-subj="rule_detection_field"]').type(SAMPLE_RULE.detection);
100102

101103
// Switch to YAML editor
102104
cy.get('[data-test-subj="change-editor-type"] label:nth-child(2)').click({
@@ -110,7 +112,7 @@ describe('Rules', () => {
110112
}).as('getRules');
111113

112114
// Click "create" button
113-
cy.get('[data-test-subj="create_rule_button"]').click({
115+
cy.get('[data-test-subj="submit_rule_form_button"]').click({
114116
force: true,
115117
});
116118

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,5 +65,8 @@
6565
},
6666
"engines": {
6767
"yarn": "^1.21.1"
68+
},
69+
"dependencies": {
70+
"formik": "^2.2.9"
6871
}
6972
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
import { useEffect, useState } from 'react';
6+
import { useFormikContext } from 'formik';
7+
import { NotificationsStart } from 'opensearch-dashboards/public';
8+
import { errorNotificationToast } from '../../../../utils/helpers';
9+
10+
export const FormSubmitionErrorToastNotification = ({
11+
notifications,
12+
}: {
13+
notifications?: NotificationsStart;
14+
}) => {
15+
const { submitCount, isValid } = useFormikContext();
16+
const [prevSubmitCount, setPrevSubmitCount] = useState(submitCount);
17+
18+
useEffect(() => {
19+
if (isValid) return;
20+
21+
if (submitCount === prevSubmitCount) return;
22+
23+
setPrevSubmitCount(submitCount);
24+
25+
errorNotificationToast(
26+
notifications!,
27+
'create',
28+
'rule',
29+
'Some fields are invalid. Fix all highlighted error(s) before continuing.'
30+
);
31+
}, [submitCount, isValid]);
32+
return null;
33+
};

public/pages/Rules/components/RuleEditor/RuleEditor.tsx

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,28 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import React, { useState } from 'react';
6+
import React, { useState, useCallback } from 'react';
7+
import { RouteComponentProps } from 'react-router-dom';
8+
import { NotificationsStart } from 'opensearch-dashboards/public';
9+
import { RuleService } from '../../../../services';
10+
import { ROUTES } from '../../../../utils/constants';
711
import { ContentPanel } from '../../../../components/ContentPanel';
812
import { EuiSpacer, EuiButtonGroup } from '@elastic/eui';
913
import { Rule } from '../../../../../models/interfaces';
1014
import { RuleEditorFormState, ruleEditorStateDefaultValue } from './RuleEditorFormState';
1115
import { mapFormToRule, mapRuleToForm } from './mappers';
1216
import { VisualRuleEditor } from './VisualRuleEditor';
1317
import { YamlRuleEditor } from './YamlRuleEditor';
18+
import { validateRule } from '../../utils/helpers';
19+
import { errorNotificationToast } from '../../../../utils/helpers';
1420

1521
export interface RuleEditorProps {
1622
title: string;
17-
FooterActions: React.FC<{ rule: Rule }>;
1823
rule?: Rule;
24+
history: RouteComponentProps['history'];
25+
notifications?: NotificationsStart;
26+
ruleService: RuleService;
27+
mode: 'create' | 'edit';
1928
}
2029

2130
export interface VisualEditorFormErrorsState {
@@ -35,7 +44,14 @@ const editorTypes = [
3544
},
3645
];
3746

38-
export const RuleEditor: React.FC<RuleEditorProps> = ({ title, rule, FooterActions }) => {
47+
export const RuleEditor: React.FC<RuleEditorProps> = ({
48+
history,
49+
notifications,
50+
title,
51+
rule,
52+
ruleService,
53+
mode,
54+
}) => {
3955
const [ruleEditorFormState, setRuleEditorFormState] = useState<RuleEditorFormState>(
4056
rule
4157
? { ...mapRuleToForm(rule), id: ruleEditorStateDefaultValue.id }
@@ -48,15 +64,44 @@ export const RuleEditor: React.FC<RuleEditorProps> = ({ title, rule, FooterActio
4864
setSelectedEditorType(optionId);
4965
};
5066

51-
const getRule = (): Rule => {
52-
return mapFormToRule(ruleEditorFormState);
53-
};
54-
5567
const onYamlRuleEditorChange = (value: Rule) => {
5668
const formState = mapRuleToForm(value);
5769
setRuleEditorFormState(formState);
5870
};
5971

72+
const onSubmit = async () => {
73+
const submitingRule = mapFormToRule(ruleEditorFormState);
74+
if (!validateRule(submitingRule, notifications!, 'create')) {
75+
return;
76+
}
77+
78+
let result;
79+
if (mode === 'edit') {
80+
if (!rule) {
81+
console.error('No rule id found');
82+
return;
83+
}
84+
result = await ruleService.updateRule(rule?.id, submitingRule.category, submitingRule);
85+
} else {
86+
result = await ruleService.createRule(submitingRule);
87+
}
88+
89+
if (!result.ok) {
90+
errorNotificationToast(
91+
notifications!,
92+
mode === 'create' ? 'create' : 'save',
93+
'rule',
94+
result.error
95+
);
96+
} else {
97+
history.replace(ROUTES.RULES);
98+
}
99+
};
100+
101+
const goToRulesList = useCallback(() => {
102+
history.replace(ROUTES.RULES);
103+
}, [history]);
104+
60105
return (
61106
<>
62107
<ContentPanel title={title}>
@@ -70,20 +115,26 @@ export const RuleEditor: React.FC<RuleEditorProps> = ({ title, rule, FooterActio
70115
<EuiSpacer size="xl" />
71116
{selectedEditorType === 'visual' && (
72117
<VisualRuleEditor
118+
mode={mode}
119+
notifications={notifications}
73120
ruleEditorFormState={ruleEditorFormState}
74121
setRuleEditorFormState={setRuleEditorFormState}
122+
cancel={goToRulesList}
123+
submit={onSubmit}
75124
/>
76125
)}
77126
{selectedEditorType === 'yaml' && (
78127
<YamlRuleEditor
128+
mode={mode}
79129
rule={mapFormToRule(ruleEditorFormState)}
80130
change={onYamlRuleEditorChange}
131+
cancel={goToRulesList}
132+
submit={onSubmit}
81133
/>
82134
)}
83135
<EuiSpacer />
84136
</ContentPanel>
85137
<EuiSpacer size="xl" />
86-
<FooterActions rule={getRule()} />
87138
</>
88139
);
89140
};

0 commit comments

Comments
 (0)