Skip to content

Commit

Permalink
[FEATURE] Rule form validation on submit (#264)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
djindjic authored Dec 29, 2022
1 parent ec6526d commit 4c586a6
Show file tree
Hide file tree
Showing 11 changed files with 562 additions and 340 deletions.
20 changes: 11 additions & 9 deletions cypress/integration/2_rules.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,33 +70,35 @@ describe('Rules', () => {
cy.get('[data-test-subj="rule_name_field"]').type(SAMPLE_RULE.name);

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

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

// Enter the detection
cy.get('[data-test-subj="rule_detection_field"]').type(SAMPLE_RULE.detection);

// Enter the severity
cy.get('[data-test-subj="rule_severity_dropdown"]').select(SAMPLE_RULE.severity);
cy.get('[data-test-subj="rule_severity_dropdown"]').type(SAMPLE_RULE.severity);

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

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

// Enter the false positive cases
cy.get('[data-test-subj="rule_false_positive_cases_field_0"]').type(SAMPLE_RULE.falsePositive);
cy.get('[data-test-subj="rule_false_positive_cases_-_optional_field_0"]').type(
SAMPLE_RULE.falsePositive
);

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

// Enter the log type
cy.get('[data-test-subj="rule_status_dropdown"]').select(SAMPLE_RULE.status);
cy.get('[data-test-subj="rule_status_dropdown"]').type(SAMPLE_RULE.status);

// Enter the detection
cy.get('[data-test-subj="rule_detection_field"]').type(SAMPLE_RULE.detection);

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

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

Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,8 @@
},
"engines": {
"yarn": "^1.21.1"
},
"dependencies": {
"formik": "^2.2.9"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/
import { useEffect, useState } from 'react';
import { useFormikContext } from 'formik';
import { NotificationsStart } from 'opensearch-dashboards/public';
import { errorNotificationToast } from '../../../../utils/helpers';

export const FormSubmitionErrorToastNotification = ({
notifications,
}: {
notifications?: NotificationsStart;
}) => {
const { submitCount, isValid } = useFormikContext();
const [prevSubmitCount, setPrevSubmitCount] = useState(submitCount);

useEffect(() => {
if (isValid) return;

if (submitCount === prevSubmitCount) return;

setPrevSubmitCount(submitCount);

errorNotificationToast(
notifications!,
'create',
'rule',
'Some fields are invalid. Fix all highlighted error(s) before continuing.'
);
}, [submitCount, isValid]);
return null;
};
67 changes: 59 additions & 8 deletions public/pages/Rules/components/RuleEditor/RuleEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,28 @@
* SPDX-License-Identifier: Apache-2.0
*/

import React, { useState } from 'react';
import React, { useState, useCallback } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { NotificationsStart } from 'opensearch-dashboards/public';
import { RuleService } from '../../../../services';
import { ROUTES } from '../../../../utils/constants';
import { ContentPanel } from '../../../../components/ContentPanel';
import { EuiSpacer, EuiButtonGroup } from '@elastic/eui';
import { Rule } from '../../../../../models/interfaces';
import { RuleEditorFormState, ruleEditorStateDefaultValue } from './RuleEditorFormState';
import { mapFormToRule, mapRuleToForm } from './mappers';
import { VisualRuleEditor } from './VisualRuleEditor';
import { YamlRuleEditor } from './YamlRuleEditor';
import { validateRule } from '../../utils/helpers';
import { errorNotificationToast } from '../../../../utils/helpers';

export interface RuleEditorProps {
title: string;
FooterActions: React.FC<{ rule: Rule }>;
rule?: Rule;
history: RouteComponentProps['history'];
notifications?: NotificationsStart;
ruleService: RuleService;
mode: 'create' | 'edit';
}

export interface VisualEditorFormErrorsState {
Expand All @@ -35,7 +44,14 @@ const editorTypes = [
},
];

export const RuleEditor: React.FC<RuleEditorProps> = ({ title, rule, FooterActions }) => {
export const RuleEditor: React.FC<RuleEditorProps> = ({
history,
notifications,
title,
rule,
ruleService,
mode,
}) => {
const [ruleEditorFormState, setRuleEditorFormState] = useState<RuleEditorFormState>(
rule
? { ...mapRuleToForm(rule), id: ruleEditorStateDefaultValue.id }
Expand All @@ -48,15 +64,44 @@ export const RuleEditor: React.FC<RuleEditorProps> = ({ title, rule, FooterActio
setSelectedEditorType(optionId);
};

const getRule = (): Rule => {
return mapFormToRule(ruleEditorFormState);
};

const onYamlRuleEditorChange = (value: Rule) => {
const formState = mapRuleToForm(value);
setRuleEditorFormState(formState);
};

const onSubmit = async () => {
const submitingRule = mapFormToRule(ruleEditorFormState);
if (!validateRule(submitingRule, notifications!, 'create')) {
return;
}

let result;
if (mode === 'edit') {
if (!rule) {
console.error('No rule id found');
return;
}
result = await ruleService.updateRule(rule?.id, submitingRule.category, submitingRule);
} else {
result = await ruleService.createRule(submitingRule);
}

if (!result.ok) {
errorNotificationToast(
notifications!,
mode === 'create' ? 'create' : 'save',
'rule',
result.error
);
} else {
history.replace(ROUTES.RULES);
}
};

const goToRulesList = useCallback(() => {
history.replace(ROUTES.RULES);
}, [history]);

return (
<>
<ContentPanel title={title}>
Expand All @@ -70,20 +115,26 @@ export const RuleEditor: React.FC<RuleEditorProps> = ({ title, rule, FooterActio
<EuiSpacer size="xl" />
{selectedEditorType === 'visual' && (
<VisualRuleEditor
mode={mode}
notifications={notifications}
ruleEditorFormState={ruleEditorFormState}
setRuleEditorFormState={setRuleEditorFormState}
cancel={goToRulesList}
submit={onSubmit}
/>
)}
{selectedEditorType === 'yaml' && (
<YamlRuleEditor
mode={mode}
rule={mapFormToRule(ruleEditorFormState)}
change={onYamlRuleEditorChange}
cancel={goToRulesList}
submit={onSubmit}
/>
)}
<EuiSpacer />
</ContentPanel>
<EuiSpacer size="xl" />
<FooterActions rule={getRule()} />
</>
);
};
Loading

0 comments on commit 4c586a6

Please sign in to comment.