From ac1b927dbc11bb2c438175791f4821486884735c Mon Sep 17 00:00:00 2001 From: Nikita Indik Date: Sat, 16 Nov 2024 12:20:08 +0100 Subject: [PATCH] Add `machine_learning_job_id` --- .../normalize_machine_learning_job_id.ts | 12 +++ .../machine_learning_job_id_edit/index.ts | 9 ++ .../machine_learning_job_id_edit.tsx | 22 +++++ .../machine_learning_job_selector.tsx | 28 ++++++ .../components/ml_job_select/index.tsx | 84 +---------------- .../ml_job_select/ml_job_combobox.tsx | 89 +++++++++++++++++++ .../components/ml_job_select/types.ts | 16 ++++ .../components/step_define_rule/index.tsx | 10 +-- .../machine_learning_job_id_adapter.tsx | 13 +++ .../machine_learning_job_id_form.tsx | 47 ++++++++++ .../machine_learning_rule_field_edit.tsx | 3 + .../pages/detection_engine/rules/helpers.tsx | 5 +- 12 files changed, 247 insertions(+), 91 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/utils/normalize_machine_learning_job_id.ts create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/machine_learning_job_id_edit/index.ts create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/machine_learning_job_id_edit/machine_learning_job_id_edit.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/machine_learning_job_id_edit/machine_learning_job_selector.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/ml_job_select/ml_job_combobox.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/ml_job_select/types.ts create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/machine_learning_job_id/machine_learning_job_id_adapter.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/machine_learning_job_id/machine_learning_job_id_form.tsx diff --git a/x-pack/plugins/security_solution/public/common/utils/normalize_machine_learning_job_id.ts b/x-pack/plugins/security_solution/public/common/utils/normalize_machine_learning_job_id.ts new file mode 100644 index 000000000000..7733e2be08c9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/utils/normalize_machine_learning_job_id.ts @@ -0,0 +1,12 @@ +/* + * 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 type { MachineLearningJobId } from '../../../common/api/detection_engine'; + +export function normalizeMachineLearningJobId(jobId: MachineLearningJobId): string[] { + return typeof jobId === 'string' ? [jobId] : jobId; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/machine_learning_job_id_edit/index.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/machine_learning_job_id_edit/index.ts new file mode 100644 index 000000000000..3f2d1c7232f6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/machine_learning_job_id_edit/index.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +export { MachineLearningJobIdEdit } from './machine_learning_job_id_edit'; +export { MachineLearningJobSelector } from './machine_learning_job_selector'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/machine_learning_job_id_edit/machine_learning_job_id_edit.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/machine_learning_job_id_edit/machine_learning_job_id_edit.tsx new file mode 100644 index 000000000000..0bea46edb696 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/machine_learning_job_id_edit/machine_learning_job_id_edit.tsx @@ -0,0 +1,22 @@ +/* + * 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 React from 'react'; +import { UseField } from '../../../../shared_imports'; +import { MlJobSelect } from '../ml_job_select'; + +const componentProps = { + describedByIds: ['machineLearningJobId'], +}; + +interface MachineLearningJobIdEditProps { + path: string; +} + +export function MachineLearningJobIdEdit({ path }: MachineLearningJobIdEditProps): JSX.Element { + return ; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/machine_learning_job_id_edit/machine_learning_job_selector.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/machine_learning_job_id_edit/machine_learning_job_selector.tsx new file mode 100644 index 000000000000..1d13740c8258 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/machine_learning_job_id_edit/machine_learning_job_selector.tsx @@ -0,0 +1,28 @@ +/* + * 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 React, { useMemo } from 'react'; +import { UseField } from '../../../../shared_imports'; +import { MlJobComboBox } from '../ml_job_select/ml_job_combobox'; +import { useSecurityJobs } from '../../../../common/components/ml_popover/hooks/use_security_jobs'; + +interface MachineLearningJobIdEditProps { + path: string; +} + +export function MachineLearningJobSelector({ path }: MachineLearningJobIdEditProps): JSX.Element { + const { loading, jobs } = useSecurityJobs(); + + const componentProps = useMemo(() => { + return { + jobs, + loading, + }; + }, [jobs, loading]); + + return ; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/ml_job_select/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/ml_job_select/index.tsx index e9c22457d746..3615b9b7b4b3 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/ml_job_select/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/ml_job_select/index.tsx @@ -5,17 +5,8 @@ * 2.0. */ -import React, { useCallback, useMemo } from 'react'; -import type { EuiComboBoxOptionOption } from '@elastic/eui'; -import { - EuiButton, - EuiComboBox, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiToolTip, - EuiText, -} from '@elastic/eui'; +import React, { useMemo } from 'react'; +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; import styled from 'styled-components'; import { isJobStarted } from '../../../../../common/machine_learning/helpers'; @@ -26,21 +17,7 @@ import { useKibana } from '../../../../common/lib/kibana'; import { HelpText } from './help_text'; import * as i18n from './translations'; - -interface MlJobValue { - id: string; - description: string; - name?: string; -} - -const JobDisplayContainer = styled.div` - width: 100%; - height: 100%; - display: flex; - flex-direction: column; -`; - -type MlJobOption = EuiComboBoxOptionOption; +import { MlJobComboBox } from './ml_job_combobox'; const MlJobSelectEuiFlexGroup = styled(EuiFlexGroup)` margin-bottom: 5px; @@ -50,62 +27,17 @@ const MlJobEuiButton = styled(EuiButton)` margin-top: 20px; `; -const JobDisplay: React.FC = ({ description, name, id }) => ( - - {name ?? id} - - -

{description}

-
-
-
-); - interface MlJobSelectProps { describedByIds: string[]; field: FieldHook; } -const renderJobOption = (option: MlJobOption) => ( - -); - export const MlJobSelect: React.FC = ({ describedByIds = [], field }) => { const jobIds = field.value as string[]; const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); const { loading, jobs } = useSecurityJobs(); const { getUrlForApp, navigateToApp } = useKibana().services.application; const mlUrl = getUrlForApp('ml'); - const handleJobSelect = useCallback( - (selectedJobOptions: MlJobOption[]): void => { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const selectedJobIds = selectedJobOptions.map((option) => option.value!.id); - field.setValue(selectedJobIds); - }, - [field] - ); - - const jobOptions = jobs.map((job) => ({ - value: { - id: job.id, - description: job.description, - name: job.customSettings?.security_app_display_name, - }, - // Make sure users can search for id or name. - // The label contains the name and id because EuiComboBox uses it for the textual search. - label: `${job.customSettings?.security_app_display_name} ${job.id}`, - })); - - const selectedJobOptions = jobOptions - .filter((option) => jobIds.includes(option.value.id)) - // 'label' defines what is rendered inside the selected ComboBoxPill - .map((options) => ({ ...options, label: options.value.name ?? options.value.id })); const notRunningJobIds = useMemo(() => { const selectedJobs = jobs.filter(({ id }) => jobIds.includes(id)); @@ -130,15 +62,7 @@ export const MlJobSelect: React.FC = ({ describedByIds = [], f > - + diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/ml_job_select/ml_job_combobox.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/ml_job_select/ml_job_combobox.tsx new file mode 100644 index 000000000000..213987eadb11 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/ml_job_select/ml_job_combobox.tsx @@ -0,0 +1,89 @@ +/* + * 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 React, { useCallback } from 'react'; +import styled from 'styled-components'; +import { EuiComboBox, EuiToolTip, EuiText } from '@elastic/eui'; +import type { MlJobOption, MlJobValue } from './types'; +import type { FieldHook } from '../../../../shared_imports'; +import type { SecurityJob } from '../../../../common/components/ml_popover/types'; +import * as i18n from './translations'; + +interface MlJobComboBoxProps { + field: FieldHook; + isLoading: boolean; + jobs: SecurityJob[]; +} + +export function MlJobComboBox({ field, isLoading, jobs }: MlJobComboBoxProps) { + const jobIds = field.value as string[]; + + const jobOptions = jobs.map((job) => ({ + value: { + id: job.id, + description: job.description, + name: job.customSettings?.security_app_display_name, + }, + // Make sure users can search for id or name. + // The label contains the name and id because EuiComboBox uses it for the textual search. + label: `${job.customSettings?.security_app_display_name} ${job.id}`, + })); + + const handleJobSelect = useCallback( + (selectedJobOptions: MlJobOption[]): void => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const selectedJobIds = selectedJobOptions.map((option) => option.value!.id); + field.setValue(selectedJobIds); + }, + [field] + ); + + const selectedJobOptions = jobOptions + .filter((option) => jobIds.includes(option.value.id)) + // 'label' defines what is rendered inside the selected ComboBoxPill + .map((options) => ({ ...options, label: options.value.name ?? options.value.id })); + + return ( + + ); +} + +const renderJobOption = (option: MlJobOption) => ( + +); + +const JobDisplay: React.FC = ({ description, name, id }) => ( + + {name ?? id} + + +

{description}

+
+
+
+); + +const JobDisplayContainer = styled.div` + width: 100%; + height: 100%; + display: flex; + flex-direction: column; +`; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/ml_job_select/types.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/ml_job_select/types.ts new file mode 100644 index 000000000000..a10a9da862d2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/ml_job_select/types.ts @@ -0,0 +1,16 @@ +/* + * 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 type { EuiComboBoxOptionOption } from '@elastic/eui'; + +export interface MlJobValue { + id: string; + description: string; + name?: string; +} + +export type MlJobOption = EuiComboBoxOptionOption; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx index 7085371eea27..a9d71a1f79bd 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx @@ -45,7 +45,6 @@ import type { QueryBarDefineRuleProps } from '../query_bar'; import { QueryBarDefineRule } from '../query_bar'; import { SelectRuleType } from '../select_rule_type'; import { AnomalyThresholdSlider } from '../anomaly_threshold_slider'; -import { MlJobSelect } from '../../../rule_creation/components/ml_job_select'; import { PickTimeline } from '../../../rule_creation/components/pick_timeline'; import { StepContentWrapper } from '../../../rule_creation/components/step_content_wrapper'; import { ThresholdInput } from '../threshold_input'; @@ -94,6 +93,7 @@ import { useMLRuleConfig } from '../../../../common/components/ml/hooks/use_ml_r import { AlertSuppressionEdit } from '../../../rule_creation/components/alert_suppression_edit'; import { ThresholdAlertSuppressionEdit } from '../../../rule_creation/components/threshold_alert_suppression_edit'; import { usePersistentAlertSuppressionState } from './use_persistent_alert_suppression_state'; +import { MachineLearningJobIdEdit } from '../../../rule_creation/components/machine_learning_job_id_edit'; const CommonUseField = getUseField({ component: Field }); @@ -858,13 +858,7 @@ const StepDefineRuleComponent: FC = ({ )} <> - + ; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/machine_learning_job_id/machine_learning_job_id_form.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/machine_learning_job_id/machine_learning_job_id_form.tsx new file mode 100644 index 000000000000..3ebef404e4b0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/machine_learning_job_id/machine_learning_job_id_form.tsx @@ -0,0 +1,47 @@ +/* + * 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 React from 'react'; +import type { FormData, FormSchema } from '../../../../../../../../shared_imports'; +import { schema } from '../../../../../../../rule_creation_ui/components/step_define_rule/schema'; +import { RuleFieldEditFormWrapper } from '../rule_field_edit_form_wrapper'; +import { type MachineLearningJobId } from '../../../../../../../../../common/api/detection_engine'; +import { normalizeMachineLearningJobId } from '../../../../../../../../common/utils/normalize_machine_learning_job_id'; +import { MachineLearningJobIdAdapter } from './machine_learning_job_id_adapter'; + +interface MachineLearningJobIdFormData { + machineLearningJobId: MachineLearningJobId; +} + +export function MachineLearningJobIdForm(): JSX.Element { + return ( + + ); +} + +function deserializer(defaultValue: FormData): MachineLearningJobIdFormData { + return { + machineLearningJobId: normalizeMachineLearningJobId(defaultValue.machine_learning_job_id), + }; +} + +function serializer(formData: FormData): { + machine_learning_job_id: MachineLearningJobId; +} { + return { + machine_learning_job_id: formData.machineLearningJobId, + }; +} + +const machineLearningJobIdSchema = { + machineLearningJobId: schema.machineLearningJobId, +} as FormSchema; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/machine_learning_rule_field_edit.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/machine_learning_rule_field_edit.tsx index 52b214b6a97d..da40ce843f72 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/machine_learning_rule_field_edit.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/machine_learning_rule_field_edit.tsx @@ -8,6 +8,7 @@ import React from 'react'; import type { UpgradeableMachineLearningFields } from '../../../../model/prebuilt_rule_upgrade/fields'; import { AlertSuppressionEditForm } from './fields/alert_suppression'; +import { MachineLearningJobIdForm } from './fields/machine_learning_job_id/machine_learning_job_id_form'; interface MachineLearningRuleFieldEditProps { fieldName: UpgradeableMachineLearningFields; @@ -17,6 +18,8 @@ export function MachineLearningRuleFieldEdit({ fieldName }: MachineLearningRuleF switch (fieldName) { case 'alert_suppression': return ; + case 'machine_learning_job_id': + return ; default: return null; // Will be replaced with `assertUnreachable(fieldName)` once all fields are implemented } diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx index 2b187928a17f..0f615afd36c5 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx @@ -47,6 +47,7 @@ import { DataSourceType, AlertSuppressionDurationType } from './types'; import { severityOptions } from '../../../../detection_engine/rule_creation_ui/components/step_about_rule/data'; import { DEFAULT_SUPPRESSION_MISSING_FIELDS_STRATEGY } from '../../../../../common/detection_engine/constants'; import type { RuleAction, RuleResponse } from '../../../../../common/api/detection_engine'; +import { normalizeMachineLearningJobId } from '../../../../common/utils/normalize_machine_learning_job_id'; export interface GetStepsData { aboutRuleData: AboutStepRule; @@ -97,9 +98,7 @@ export const getActionsStepsData = ( export const getMachineLearningJobId = (rule: RuleResponse): string[] | undefined => { if (rule.type === 'machine_learning') { - return typeof rule.machine_learning_job_id === 'string' - ? [rule.machine_learning_job_id] - : rule.machine_learning_job_id; + return normalizeMachineLearningJobId(rule.machine_learning_job_id); } return undefined; };