-
Notifications
You must be signed in to change notification settings - Fork 8.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Security Solution] Remove extra data structures in create/edit rule …
…forms (#157749) ## Summary This PR refactors the forms used by the Detection Engine create and edit rule pages. The existing forms have a lot of complexity in how data is managed: each of the 4 steps (Define, About, Schedule, and Actions) map to a separate component, and these individual components own the form for that step. However, some steps have dependencies on data in other steps. In addition, Rule Preview and submitting the rule to the API require the data from all steps. To resolve this, I moved the form ownership from the individual step components to the Create/Edit Rule page components and passed the appropriate form into each individual step component. With the forms owned at the Page level, many of the additional data structures we previously needed to pass form data around are no longer necessary. #### Removed - `formHooks`, `setFormHooks` - These hooks were used to let step components pass the form's `validate` function back up to the Page component. With the forms created in the Page component, the `validate` functions are available without additional structures. - `stepsData`, `setStepsData` - This data structure held the form data from each step and the validation status. We now use the live form data through `useFormData` without additional sidecar data structures. - `defineRuleData`/`aboutRuleData`/`scheduleRuleData` - Sidecar data structures for rule preview, also held the form data from each step. We now use the live form data through `useFormData` without additional sidecar data structures. #### Simplified - `editStep()` no longer relies on `formHooks` - `submitStep()` no longer relies on `formHooks` - Step forms no longer need `watch` and `onChange` configurations ### Key Implementation Notes In order for `useFormData` to be reliable, the forms for all steps must stay in the DOM at all times while on the Create/Edit pages. Thus the "read only" view has been separated from the editable view, and the visibility of each view is toggled by hiding elements with CSS rather than removing them from the DOM. If the form were to be removed from the DOM when switching to a read only view, the values from `useFormData` would reset to the default values so we would need a sidecar object to hold the form values when the form is not rendered. It would also require re-loading the values from the sidecar into the form when the form gets re-added to the DOM. Keeping the form in the DOM at all times allows us to rely on `useFormData` consistently, which simplifies the data model. ### Bug Fixes - When moving back to step 1 from step 2 by clicking "Edit", data is no longer lost in step 2 --------- Co-authored-by: kibanamachine <[email protected]>
- Loading branch information
1 parent
0e3e902
commit 9abed02
Showing
29 changed files
with
1,102 additions
and
1,179 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
155 changes: 155 additions & 0 deletions
155
x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/form.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
/* | ||
* 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 { useState, useMemo, useEffect } from 'react'; | ||
import type { DataViewBase } from '@kbn/es-query'; | ||
import { isThreatMatchRule } from '../../../../common/detection_engine/utils'; | ||
import type { | ||
AboutStepRule, | ||
ActionsStepRule, | ||
DefineStepRule, | ||
ScheduleStepRule, | ||
} from '../../../detections/pages/detection_engine/rules/types'; | ||
import { DataSourceType } from '../../../detections/pages/detection_engine/rules/types'; | ||
import { useKibana } from '../../../common/lib/kibana'; | ||
import { useForm, useFormData } from '../../../shared_imports'; | ||
import { schema as defineRuleSchema } from '../../../detections/components/rules/step_define_rule/schema'; | ||
import type { EqlOptionsSelected } from '../../../../common/search_strategy'; | ||
import { | ||
schema as aboutRuleSchema, | ||
threatMatchAboutSchema, | ||
} from '../../../detections/components/rules/step_about_rule/schema'; | ||
import { schema as scheduleRuleSchema } from '../../../detections/components/rules/step_schedule_rule/schema'; | ||
import { getSchema as getActionsRuleSchema } from '../../../detections/components/rules/step_rule_actions/get_schema'; | ||
import { useFetchIndex } from '../../../common/containers/source'; | ||
|
||
export interface UseRuleFormsProps { | ||
defineStepDefault: DefineStepRule; | ||
aboutStepDefault: AboutStepRule; | ||
scheduleStepDefault: ScheduleStepRule; | ||
actionsStepDefault: ActionsStepRule; | ||
} | ||
|
||
export const useRuleForms = ({ | ||
defineStepDefault, | ||
aboutStepDefault, | ||
scheduleStepDefault, | ||
actionsStepDefault, | ||
}: UseRuleFormsProps) => { | ||
const { | ||
triggersActionsUi: { actionTypeRegistry }, | ||
} = useKibana().services; | ||
// DEFINE STEP FORM | ||
const { form: defineStepForm } = useForm<DefineStepRule>({ | ||
defaultValue: defineStepDefault, | ||
options: { stripEmptyFields: false }, | ||
schema: defineRuleSchema, | ||
}); | ||
const [eqlOptionsSelected, setEqlOptionsSelected] = useState<EqlOptionsSelected>( | ||
defineStepDefault.eqlOptions | ||
); | ||
const [defineStepFormData] = useFormData<DefineStepRule | {}>({ | ||
form: defineStepForm, | ||
}); | ||
// FormData doesn't populate on the first render, so we use the defaultValue if the formData | ||
// doesn't have what we wanted | ||
const defineStepData = | ||
'index' in defineStepFormData | ||
? { ...defineStepFormData, eqlOptions: eqlOptionsSelected } | ||
: defineStepDefault; | ||
|
||
// ABOUT STEP FORM | ||
const typeDependentAboutRuleSchema = useMemo( | ||
() => (isThreatMatchRule(defineStepData.ruleType) ? threatMatchAboutSchema : aboutRuleSchema), | ||
[defineStepData.ruleType] | ||
); | ||
const { form: aboutStepForm } = useForm<AboutStepRule>({ | ||
defaultValue: aboutStepDefault, | ||
options: { stripEmptyFields: false }, | ||
schema: typeDependentAboutRuleSchema, | ||
}); | ||
const [aboutStepFormData] = useFormData<AboutStepRule | {}>({ | ||
form: aboutStepForm, | ||
}); | ||
const aboutStepData = 'name' in aboutStepFormData ? aboutStepFormData : aboutStepDefault; | ||
|
||
// SCHEDULE STEP FORM | ||
const { form: scheduleStepForm } = useForm<ScheduleStepRule>({ | ||
defaultValue: scheduleStepDefault, | ||
options: { stripEmptyFields: false }, | ||
schema: scheduleRuleSchema, | ||
}); | ||
const [scheduleStepFormData] = useFormData<ScheduleStepRule | {}>({ | ||
form: scheduleStepForm, | ||
}); | ||
const scheduleStepData = | ||
'interval' in scheduleStepFormData ? scheduleStepFormData : scheduleStepDefault; | ||
|
||
// ACTIONS STEP FORM | ||
const schema = useMemo(() => getActionsRuleSchema({ actionTypeRegistry }), [actionTypeRegistry]); | ||
const { form: actionsStepForm } = useForm<ActionsStepRule>({ | ||
defaultValue: actionsStepDefault, | ||
options: { stripEmptyFields: false }, | ||
schema, | ||
}); | ||
const [actionsStepFormData] = useFormData<ActionsStepRule | {}>({ | ||
form: actionsStepForm, | ||
}); | ||
const actionsStepData = | ||
'actions' in actionsStepFormData ? actionsStepFormData : actionsStepDefault; | ||
|
||
return { | ||
defineStepForm, | ||
defineStepData, | ||
aboutStepForm, | ||
aboutStepData, | ||
scheduleStepForm, | ||
scheduleStepData, | ||
actionsStepForm, | ||
actionsStepData, | ||
eqlOptionsSelected, | ||
setEqlOptionsSelected, | ||
}; | ||
}; | ||
|
||
interface UseRuleIndexPatternProps { | ||
dataSourceType: DataSourceType; | ||
index: string[]; | ||
dataViewId: string | undefined; | ||
} | ||
|
||
export const useRuleIndexPattern = ({ | ||
dataSourceType, | ||
index, | ||
dataViewId, | ||
}: UseRuleIndexPatternProps) => { | ||
const { data } = useKibana().services; | ||
const [isIndexPatternLoading, { browserFields, indexPatterns: initIndexPattern }] = | ||
useFetchIndex(index); | ||
const [indexPattern, setIndexPattern] = useState<DataViewBase>(initIndexPattern); | ||
// Why do we need this? to ensure the query bar auto-suggest gets the latest updates | ||
// when the index pattern changes | ||
// when we select new dataView | ||
// when we choose some other dataSourceType | ||
useEffect(() => { | ||
if (dataSourceType === DataSourceType.IndexPatterns && !isIndexPatternLoading) { | ||
setIndexPattern(initIndexPattern); | ||
} | ||
|
||
if (dataSourceType === DataSourceType.DataView) { | ||
const fetchDataView = async () => { | ||
if (dataViewId != null) { | ||
const dv = await data.dataViews.get(dataViewId); | ||
setIndexPattern(dv); | ||
} | ||
}; | ||
|
||
fetchDataView(); | ||
} | ||
}, [dataSourceType, isIndexPatternLoading, data, dataViewId, initIndexPattern]); | ||
return { indexPattern, isIndexPatternLoading, browserFields }; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.