Skip to content

Commit

Permalink
Add machine_learning_job_id
Browse files Browse the repository at this point in the history
  • Loading branch information
nikitaindik committed Nov 16, 2024
1 parent 902f6db commit c195e14
Show file tree
Hide file tree
Showing 12 changed files with 247 additions and 91 deletions.
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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 <UseField path={path} component={MlJobSelect} componentProps={componentProps} />;
}
Original file line number Diff line number Diff line change
@@ -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 <UseField path={path} component={MlJobComboBox} componentProps={componentProps} />;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<MlJobValue>;
import { MlJobComboBox } from './ml_job_combobox';

const MlJobSelectEuiFlexGroup = styled(EuiFlexGroup)`
margin-bottom: 5px;
Expand All @@ -50,62 +27,17 @@ const MlJobEuiButton = styled(EuiButton)`
margin-top: 20px;
`;

const JobDisplay: React.FC<MlJobValue> = ({ description, name, id }) => (
<JobDisplayContainer>
<strong>{name ?? id}</strong>
<EuiToolTip content={description}>
<EuiText size="xs" color="subdued">
<p>{description}</p>
</EuiText>
</EuiToolTip>
</JobDisplayContainer>
);

interface MlJobSelectProps {
describedByIds: string[];
field: FieldHook;
}

const renderJobOption = (option: MlJobOption) => (
<JobDisplay
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
id={option.value!.id}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
description={option.value!.description}
name={option.value?.name}
/>
);

export const MlJobSelect: React.FC<MlJobSelectProps> = ({ 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<string[]>(() => {
const selectedJobs = jobs.filter(({ id }) => jobIds.includes(id));
Expand All @@ -130,15 +62,7 @@ export const MlJobSelect: React.FC<MlJobSelectProps> = ({ describedByIds = [], f
>
<EuiFlexGroup>
<EuiFlexItem>
<EuiComboBox
isLoading={loading}
onChange={handleJobSelect}
options={jobOptions}
placeholder={i18n.ML_JOB_SELECT_PLACEHOLDER_TEXT}
renderOption={renderJobOption}
rowHeight={50}
selectedOptions={selectedJobOptions}
/>
<MlJobComboBox field={field} isLoading={loading} jobs={jobs} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<EuiComboBox
isLoading={isLoading}
onChange={handleJobSelect}
options={jobOptions}
placeholder={i18n.ML_JOB_SELECT_PLACEHOLDER_TEXT}
renderOption={renderJobOption}
rowHeight={50}
selectedOptions={selectedJobOptions}
/>
);
}

const renderJobOption = (option: MlJobOption) => (
<JobDisplay
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
id={option.value!.id}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
description={option.value!.description}
name={option.value?.name}
/>
);

const JobDisplay: React.FC<MlJobValue> = ({ description, name, id }) => (
<JobDisplayContainer>
<strong>{name ?? id}</strong>
<EuiToolTip content={description}>
<EuiText size="xs" color="subdued">
<p>{description}</p>
</EuiText>
</EuiToolTip>
</JobDisplayContainer>
);

const JobDisplayContainer = styled.div`
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
`;
Original file line number Diff line number Diff line change
@@ -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<MlJobValue>;
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 });

Expand Down Expand Up @@ -858,13 +858,7 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
)}
<RuleTypeEuiFormRow $isVisible={isMlRule(ruleType)} fullWidth>
<>
<UseField
path="machineLearningJobId"
component={MlJobSelect}
componentProps={{
describedByIds: ['detectionEngineStepDefineRulemachineLearningJobId'],
}}
/>
<MachineLearningJobIdEdit path="machineLearningJobId" />
<UseField
path="anomalyThreshold"
component={AnomalyThresholdSlider}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* 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 { MachineLearningJobSelector } from '../../../../../../../rule_creation/components/machine_learning_job_id_edit/machine_learning_job_selector';

export function MachineLearningJobIdAdapter(): JSX.Element {
return <MachineLearningJobSelector path="machineLearningJobId" />;
}
Original file line number Diff line number Diff line change
@@ -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 (
<RuleFieldEditFormWrapper
component={MachineLearningJobIdAdapter}
ruleFieldFormSchema={machineLearningJobIdSchema}
deserializer={deserializer}
serializer={serializer}
/>
);
}

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<MachineLearningJobIdFormData>;
Loading

0 comments on commit c195e14

Please sign in to comment.