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;
};