diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts
index 971e69729269e..e66791d7012dc 100644
--- a/packages/kbn-doc-links/src/get_doc_links.ts
+++ b/packages/kbn-doc-links/src/get_doc_links.ts
@@ -375,8 +375,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => {
linux_deadlock: `${SECURITY_SOLUTION_DOCS}ts-management.html#linux-deadlock`,
},
packageActionTroubleshooting: {
- // TODO: Pending to be updated when docs are ready
- es_connection: '',
+ es_connection: `${SECURITY_SOLUTION_DOCS}ts-management.html`,
},
responseActions: `${SECURITY_SOLUTION_DOCS}response-actions.html`,
configureEndpointIntegrationPolicy: `${SECURITY_SOLUTION_DOCS}configure-endpoint-integration-policy.html`,
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/build_ml_jobs_description.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/build_ml_jobs_description.tsx
new file mode 100644
index 0000000000000..7012c4276ee45
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/build_ml_jobs_description.tsx
@@ -0,0 +1,14 @@
+/*
+ * 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 { ListItems } from './types';
+import { MlJobsDescription } from '../ml_jobs_description';
+
+export const buildMlJobsDescription = (jobIds: string[], label: string): ListItems => ({
+ title: label,
+ description: ,
+});
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx
index d22bff896eb0b..b9b2abffdc96d 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx
@@ -45,7 +45,7 @@ import {
buildRequiredFieldsDescription,
buildAlertSuppressionDescription,
} from './helpers';
-import { buildMlJobsDescription } from './ml_job_description';
+import { buildMlJobsDescription } from './build_ml_jobs_description';
import { buildActionsDescription } from './actions_description';
import { buildThrottleDescription } from './throttle_description';
import { THREAT_QUERY_LABEL } from './translations';
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.test.tsx
deleted file mode 100644
index 0dc7b14ea8e1e..0000000000000
--- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.test.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * 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 { shallow } from 'enzyme';
-
-import { mockOpenedJob, mockSecurityJobs } from '../../../../common/components/ml_popover/api.mock';
-import { MlJobDescription, AuditIcon, JobStatusBadge } from './ml_job_description';
-
-jest.mock('../../../../common/lib/kibana');
-
-describe('MlJobDescription', () => {
- it('renders correctly', () => {
- const wrapper = shallow(
- {}} />
- );
-
- expect(wrapper.find('[data-test-subj="machineLearningJobId"]')).toHaveLength(1);
- });
-});
-
-describe('AuditIcon', () => {
- it('renders correctly', () => {
- const wrapper = shallow();
-
- expect(wrapper.find('EuiToolTip')).toHaveLength(0);
- });
-});
-
-describe('JobStatusBadge', () => {
- it('renders correctly', () => {
- const wrapper = shallow();
-
- expect(wrapper.find('EuiBadge')).toHaveLength(1);
- });
-});
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.tsx
deleted file mode 100644
index abe5318b92c13..0000000000000
--- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.tsx
+++ /dev/null
@@ -1,150 +0,0 @@
-/*
- * 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 { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink, EuiToolTip } from '@elastic/eui';
-
-import type { MlSummaryJob } from '@kbn/ml-plugin/public';
-import { ML_PAGES, useMlHref } from '@kbn/ml-plugin/public';
-import { isJobStarted } from '../../../../../common/machine_learning/helpers';
-import { useEnableDataFeed } from '../../../../common/components/ml_popover/hooks/use_enable_data_feed';
-import type { SecurityJob } from '../../../../common/components/ml_popover/types';
-import { JobSwitch } from '../../../../common/components/ml_popover/jobs_table/job_switch';
-import { useSecurityJobs } from '../../../../common/components/ml_popover/hooks/use_security_jobs';
-import { useKibana } from '../../../../common/lib/kibana';
-import type { ListItems } from './types';
-import * as i18n from './translations';
-
-enum MessageLevels {
- info = 'info',
- warning = 'warning',
- error = 'error',
-}
-
-const AuditIconComponent: React.FC<{
- message: MlSummaryJob['auditMessage'];
-}> = ({ message }) => {
- if (!message) {
- return null;
- }
-
- let color = 'primary';
- let icon = 'alert';
-
- if (message.level === MessageLevels.info) {
- icon = 'iInCircle';
- } else if (message.level === MessageLevels.warning) {
- color = 'warning';
- } else if (message.level === MessageLevels.error) {
- color = 'danger';
- }
-
- return (
-
-
-
- );
-};
-
-export const AuditIcon = React.memo(AuditIconComponent);
-
-const JobStatusBadgeComponent: React.FC<{ job: MlSummaryJob }> = ({ job }) => {
- const isStarted = isJobStarted(job.jobState, job.datafeedState);
- const color = isStarted ? 'success' : 'danger';
- const text = isStarted ? i18n.ML_JOB_STARTED : i18n.ML_JOB_STOPPED;
-
- return (
-
- {text}
-
- );
-};
-
-export const JobStatusBadge = React.memo(JobStatusBadgeComponent);
-
-const JobLink = styled(EuiLink)`
- margin-right: ${({ theme }) => theme.eui.euiSizeS};
-`;
-
-const Wrapper = styled.div`
- overflow: hidden;
-`;
-
-const MlJobDescriptionComponent: React.FC<{
- job: SecurityJob;
- loading: boolean;
- refreshJob: (job: SecurityJob) => void;
-}> = ({ job, loading, refreshJob }) => {
- const {
- services: { http, ml },
- } = useKibana();
- const { enableDatafeed, isLoading: isLoadingEnableDataFeed } = useEnableDataFeed();
- const jobId = job.id;
- const jobUrl = useMlHref(ml, http.basePath.get(), {
- page: ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE,
- pageState: {
- jobId: [jobId],
- },
- });
-
- const jobIdSpan = {jobId};
- const isStarted = isJobStarted(job.jobState, job.datafeedState);
-
- const handleJobStateChange = useCallback(
- async (_, latestTimestampMs: number, enable: boolean) => {
- await enableDatafeed(job, latestTimestampMs, enable);
- refreshJob(job);
- },
- [enableDatafeed, job, refreshJob]
- );
-
- return (
-
-
-
-
-
-
-
-
-
-
- {isStarted ? i18n.ML_STOP_JOB_LABEL : i18n.ML_RUN_JOB_LABEL}
-
-
-
- );
-};
-
-export const MlJobDescription = React.memo(MlJobDescriptionComponent);
-
-const MlJobsDescription: React.FC<{ jobIds: string[] }> = ({ jobIds }) => {
- const { loading, jobs, refetch: refreshJobs } = useSecurityJobs();
- const relevantJobs = jobs.filter((job) => jobIds.includes(job.id));
- return (
- <>
- {relevantJobs.map((job) => (
-
- ))}
- >
- );
-};
-
-export const buildMlJobsDescription = (jobIds: string[], label: string): ListItems => ({
- title: label,
- description: ,
-});
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/translations.ts
similarity index 85%
rename from x-pack/plugins/security_solution/public/detections/components/rules/description_step/translations.tsx
rename to x-pack/plugins/security_solution/public/detections/components/rules/description_step/translations.ts
index db72259d131f1..b4b2df5515342 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/translations.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/translations.ts
@@ -91,34 +91,6 @@ export const NEW_TERMS_TYPE_DESCRIPTION = i18n.translate(
}
);
-export const ML_RUN_JOB_LABEL = i18n.translate(
- 'xpack.securitySolution.detectionEngine.ruleDescription.mlRunJobLabel',
- {
- defaultMessage: 'Run job',
- }
-);
-
-export const ML_STOP_JOB_LABEL = i18n.translate(
- 'xpack.securitySolution.detectionEngine.ruleDescription.mlStopJobLabel',
- {
- defaultMessage: 'Stop job',
- }
-);
-
-export const ML_JOB_STARTED = i18n.translate(
- 'xpack.securitySolution.detectionEngine.ruleDescription.mlJobStartedDescription',
- {
- defaultMessage: 'Started',
- }
-);
-
-export const ML_JOB_STOPPED = i18n.translate(
- 'xpack.securitySolution.detectionEngine.ruleDescription.mlJobStoppedDescription',
- {
- defaultMessage: 'Stopped',
- }
-);
-
export const THRESHOLD_RESULTS_ALL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDescription.thresholdResultsAllDescription',
{
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/ml_audit_icon/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/ml_audit_icon/index.tsx
new file mode 100644
index 0000000000000..d953cded9dd6d
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/ml_audit_icon/index.tsx
@@ -0,0 +1,8 @@
+/*
+ * 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 { MlAuditIcon } from './ml_audit_icon';
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/ml_audit_icon/ml_audit_icon.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/ml_audit_icon/ml_audit_icon.test.tsx
new file mode 100644
index 0000000000000..7e6ca9a79bda0
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/ml_audit_icon/ml_audit_icon.test.tsx
@@ -0,0 +1,27 @@
+/*
+ * 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 { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+
+import { MlAuditIcon } from './ml_audit_icon';
+
+describe('MlAuditIcon', () => {
+ it('should render null if message is undefined', () => {
+ const { container } = render();
+
+ expect(container.firstChild).toBeNull();
+ });
+
+ it('should render tooltip with message text when hover over the icon', async () => {
+ render();
+
+ userEvent.hover(screen.getByTestId('mlJobAuditIcon'));
+
+ expect(await screen.findByText('mock audit text')).toBeInTheDocument();
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/ml_audit_icon/ml_audit_icon.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/ml_audit_icon/ml_audit_icon.tsx
new file mode 100644
index 0000000000000..19255b7fb95af
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/ml_audit_icon/ml_audit_icon.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 type { FC } from 'react';
+import React, { memo } from 'react';
+import { EuiIcon, EuiToolTip } from '@elastic/eui';
+
+import type { MlSummaryJob } from '@kbn/ml-plugin/public';
+
+enum MessageLevels {
+ info = 'info',
+ warning = 'warning',
+ error = 'error',
+}
+
+interface MlAuditIconProps {
+ message: MlSummaryJob['auditMessage'];
+}
+
+const MlAuditIconComponent: FC = ({ message }) => {
+ if (!message) {
+ return null;
+ }
+
+ let color = 'primary';
+ let icon = 'alert';
+
+ if (message.level === MessageLevels.info) {
+ icon = 'iInCircle';
+ } else if (message.level === MessageLevels.warning) {
+ color = 'warning';
+ } else if (message.level === MessageLevels.error) {
+ color = 'danger';
+ }
+
+ return (
+
+
+
+ );
+};
+
+export const MlAuditIcon = memo(MlAuditIconComponent);
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_link/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_link/index.tsx
new file mode 100644
index 0000000000000..909f6f03092c5
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_link/index.tsx
@@ -0,0 +1,8 @@
+/*
+ * 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 { MlJobLink } from './ml_job_link';
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_link/ml_job_link.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_link/ml_job_link.tsx
new file mode 100644
index 0000000000000..a3eefb1240486
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_link/ml_job_link.tsx
@@ -0,0 +1,41 @@
+/*
+ * 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, { memo } from 'react';
+import styled from 'styled-components';
+import { EuiLink } from '@elastic/eui';
+
+import { ML_PAGES, useMlHref } from '@kbn/ml-plugin/public';
+import { useKibana } from '../../../../common/lib/kibana';
+
+const StyledJobEuiLInk = styled(EuiLink)`
+ margin-right: ${({ theme }) => theme.eui.euiSizeS};
+`;
+
+interface MlJobLinkProps {
+ jobId: string;
+}
+
+const MlJobLinkComponent: React.FC = ({ jobId }) => {
+ const {
+ services: { http, ml },
+ } = useKibana();
+ const jobUrl = useMlHref(ml, http.basePath.get(), {
+ page: ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE,
+ pageState: {
+ jobId: [jobId],
+ },
+ });
+
+ return (
+
+ {jobId}
+
+ );
+};
+
+export const MlJobLink = memo(MlJobLinkComponent);
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_status_badge/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_status_badge/index.tsx
new file mode 100644
index 0000000000000..170a02b30651d
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_status_badge/index.tsx
@@ -0,0 +1,8 @@
+/*
+ * 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 { MlJobStatusBadge } from './ml_job_status_badge';
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_status_badge/ml_job_status_badge.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_status_badge/ml_job_status_badge.test.tsx
new file mode 100644
index 0000000000000..010aa82fc81ce
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_status_badge/ml_job_status_badge.test.tsx
@@ -0,0 +1,39 @@
+/*
+ * 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 { render, screen } from '@testing-library/react';
+
+import { isJobStarted } from '../../../../../common/machine_learning/helpers';
+import { mockOpenedJob } from '../../../../common/components/ml_popover/api.mock';
+
+import { MlJobStatusBadge } from './ml_job_status_badge';
+
+jest.mock('../../../../../common/machine_learning/helpers');
+
+const isJobStartedMock = isJobStarted as jest.Mock;
+
+describe('MlJobStatusBadge', () => {
+ it('should call isJobStarted helper', () => {
+ render();
+
+ expect(isJobStarted).toHaveBeenCalledWith(mockOpenedJob.jobState, mockOpenedJob.datafeedState);
+ });
+
+ it('should render started if isJobStarted return true', () => {
+ isJobStartedMock.mockReturnValueOnce(true);
+ render();
+
+ expect(screen.getByTestId('machineLearningJobStatus')).toHaveTextContent('Started');
+ });
+
+ it('should render stopped if isJobStarted return false', () => {
+ isJobStartedMock.mockReturnValueOnce(false);
+ render();
+
+ expect(screen.getByTestId('machineLearningJobStatus')).toHaveTextContent('Stopped');
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_status_badge/ml_job_status_badge.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_status_badge/ml_job_status_badge.tsx
new file mode 100644
index 0000000000000..c6200b485529c
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_status_badge/ml_job_status_badge.tsx
@@ -0,0 +1,33 @@
+/*
+ * 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 { FC } from 'react';
+import React, { memo } from 'react';
+import { EuiBadge } from '@elastic/eui';
+
+import type { MlSummaryJob } from '@kbn/ml-plugin/public';
+import { isJobStarted } from '../../../../../common/machine_learning/helpers';
+
+import * as i18n from './translations';
+
+interface JobStatusBadgeProps {
+ job: MlSummaryJob;
+}
+
+const MlJobStatusBadgeComponent: FC = ({ job }) => {
+ const isStarted = isJobStarted(job.jobState, job.datafeedState);
+ const color = isStarted ? 'success' : 'danger';
+ const text = isStarted ? i18n.ML_JOB_STARTED : i18n.ML_JOB_STOPPED;
+
+ return (
+
+ {text}
+
+ );
+};
+
+export const MlJobStatusBadge = memo(MlJobStatusBadgeComponent);
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_status_badge/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_status_badge/translations.ts
new file mode 100644
index 0000000000000..001bd3991fc39
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_status_badge/translations.ts
@@ -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 { i18n } from '@kbn/i18n';
+
+export const ML_JOB_STARTED = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.ruleDescription.mlJobStartedDescription',
+ {
+ defaultMessage: 'Started',
+ }
+);
+
+export const ML_JOB_STOPPED = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.ruleDescription.mlJobStoppedDescription',
+ {
+ defaultMessage: 'Stopped',
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/admin/ml_admin_job_description.integration.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/admin/ml_admin_job_description.integration.test.tsx
new file mode 100644
index 0000000000000..04f70ba18ad03
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/admin/ml_admin_job_description.integration.test.tsx
@@ -0,0 +1,119 @@
+/*
+ * 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 { render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import noop from 'lodash/noop';
+import { TestProviders } from '../../../../../common/mock';
+import { useEnableDataFeed } from '../../../../../common/components/ml_popover/hooks/use_enable_data_feed';
+import type { SecurityJob } from '../../../../../common/components/ml_popover/types';
+
+import { MlAdminJobDescription } from './ml_admin_job_description';
+
+jest.mock('../../../../../common/lib/kibana');
+jest.mock('../../../../../common/components/ml_popover/hooks/use_enable_data_feed', () => ({
+ useEnableDataFeed: jest.fn(),
+}));
+
+import { mockSecurityJobs } from '../../../../../common/components/ml_popover/api.mock';
+
+const securityJobNotStarted: SecurityJob = {
+ ...mockSecurityJobs[0],
+ jobState: 'closed',
+ datafeedState: 'stopped',
+ auditMessage: {
+ text: 'Test warning',
+ },
+};
+
+const useEnableDataFeedMock = (useEnableDataFeed as jest.Mock).mockReturnValue({
+ isLoading: false,
+});
+
+describe('MlAdminJobDescription', () => {
+ it('should enable datafeed and call refreshJob when enabling job', async () => {
+ const refreshJobSpy = jest.fn();
+ const enableDatafeedSpy = jest.fn();
+ useEnableDataFeedMock.mockReturnValueOnce({
+ enableDatafeed: enableDatafeedSpy,
+ });
+
+ render(
+ ,
+ {
+ wrapper: TestProviders,
+ }
+ );
+
+ userEvent.click(screen.getByTestId('job-switch'));
+ expect(enableDatafeedSpy).toHaveBeenCalledWith(
+ securityJobNotStarted,
+ securityJobNotStarted.latestTimestampMs,
+ true
+ );
+
+ await waitFor(() => {
+ expect(refreshJobSpy).toHaveBeenCalledWith(securityJobNotStarted);
+ });
+ });
+
+ it('should render loading spinner when job start is in progress', async () => {
+ useEnableDataFeedMock.mockReturnValueOnce({
+ isLoading: true,
+ });
+
+ render(
+ ,
+ {
+ wrapper: TestProviders,
+ }
+ );
+
+ expect(screen.getByTestId('job-switch-loader')).toBeInTheDocument();
+ await waitFor(() => {
+ expect(screen.queryByTestId('job-switch')).toBeNull();
+ });
+ });
+
+ it('should render loading spinner when loading property passed', async () => {
+ render(, {
+ wrapper: TestProviders,
+ });
+
+ expect(screen.getByTestId('job-switch-loader')).toBeInTheDocument();
+ await waitFor(() => {
+ expect(screen.queryByTestId('job-switch')).toBeNull();
+ });
+ });
+
+ it('should render job details correctly', async () => {
+ render(, {
+ wrapper: TestProviders,
+ });
+
+ // link to job
+ const linkElement = screen.getByTestId('machineLearningJobLink');
+
+ expect(linkElement).toHaveAttribute('href', expect.any(String));
+ expect(linkElement).toHaveTextContent(securityJobNotStarted.id);
+
+ // audit icon
+ expect(screen.getByTestId('mlJobAuditIcon')).toBeInTheDocument();
+
+ // job status
+ expect(screen.getByTestId('machineLearningJobStatus')).toHaveTextContent('Stopped');
+
+ // job action label
+ await waitFor(() => {
+ expect(screen.getByTestId('mlJobActionLabel')).toHaveTextContent('Run job');
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/admin/ml_admin_job_description.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/admin/ml_admin_job_description.tsx
new file mode 100644
index 0000000000000..7d4616a004364
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/admin/ml_admin_job_description.tsx
@@ -0,0 +1,52 @@
+/*
+ * 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 { FC } from 'react';
+import React, { useCallback, useMemo, memo } from 'react';
+
+import { useEnableDataFeed } from '../../../../../common/components/ml_popover/hooks/use_enable_data_feed';
+import type { SecurityJob } from '../../../../../common/components/ml_popover/types';
+import { JobSwitch } from '../../../../../common/components/ml_popover/jobs_table/job_switch';
+
+import { MlJobItem } from '../ml_job_item';
+
+interface MlAdminJobDescriptionProps {
+ job: SecurityJob;
+ loading: boolean;
+ refreshJob: (job: SecurityJob) => void;
+}
+
+const MlAdminJobDescriptionComponent: FC = ({
+ job,
+ loading,
+ refreshJob,
+}) => {
+ const { enableDatafeed, isLoading: isLoadingEnableDataFeed } = useEnableDataFeed();
+
+ const handleJobStateChange = useCallback(
+ async (_, latestTimestampMs: number, enable: boolean) => {
+ await enableDatafeed(job, latestTimestampMs, enable);
+ refreshJob(job);
+ },
+ [enableDatafeed, job, refreshJob]
+ );
+
+ const switchComponent = useMemo(
+ () => (
+
+ ),
+ [handleJobStateChange, isLoadingEnableDataFeed, job, loading]
+ );
+
+ return ;
+};
+
+export const MlAdminJobDescription = memo(MlAdminJobDescriptionComponent);
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/admin/ml_admin_jobs_description.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/admin/ml_admin_jobs_description.test.tsx
new file mode 100644
index 0000000000000..7c974ae907659
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/admin/ml_admin_jobs_description.test.tsx
@@ -0,0 +1,49 @@
+/*
+ * 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 { render, screen } from '@testing-library/react';
+import type { MlSummaryJob } from '@kbn/ml-plugin/public';
+
+import { MlAdminJobsDescription } from './ml_admin_jobs_description';
+
+import { useSecurityJobs } from '../../../../../common/components/ml_popover/hooks/use_security_jobs';
+
+jest.mock(
+ './ml_admin_job_description',
+ () =>
+ ({
+ MlAdminJobDescription: (props) => {
+ return {props.job.id}
;
+ },
+ } as Record>)
+);
+
+jest.mock('../../../../../common/components/ml_popover/hooks/use_security_jobs');
+
+const useSecurityJobsMock = useSecurityJobs as jest.Mock;
+
+describe('MlAdminJobsDescription', () => {
+ it('should render null if admin permissions absent', () => {
+ useSecurityJobsMock.mockReturnValueOnce({ jobs: [], isMlAdmin: false });
+ const { container } = render();
+
+ expect(container.firstChild).toBeNull();
+ });
+
+ it('should render only jobs with job ids passed as props', () => {
+ useSecurityJobsMock.mockReturnValueOnce({
+ jobs: [{ id: 'mock-1' }, { id: 'mock-2' }, { id: 'mock-3' }],
+ isMlAdmin: true,
+ });
+ render();
+ const expectedJobs = screen.getAllByTestId('adminMock');
+
+ expect(expectedJobs).toHaveLength(2);
+ expect(expectedJobs[0]).toHaveTextContent('mock-1');
+ expect(expectedJobs[1]).toHaveTextContent('mock-2');
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/admin/ml_admin_jobs_description.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/admin/ml_admin_jobs_description.tsx
new file mode 100644
index 0000000000000..1fe676df75364
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/admin/ml_admin_jobs_description.tsx
@@ -0,0 +1,37 @@
+/*
+ * 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 { FC } from 'react';
+import React, { memo } from 'react';
+
+import { useSecurityJobs } from '../../../../../common/components/ml_popover/hooks/use_security_jobs';
+
+import { MlAdminJobDescription } from './ml_admin_job_description';
+
+interface MlAdminJobsDescriptionProps {
+ jobIds: string[];
+}
+
+const MlAdminJobsDescriptionComponent: FC = ({ jobIds }) => {
+ const { loading, jobs, refetch: refreshJobs, isMlAdmin } = useSecurityJobs();
+
+ if (!isMlAdmin) {
+ return null;
+ }
+
+ const relevantJobs = jobs.filter((job) => jobIds.includes(job.id));
+
+ return (
+ <>
+ {relevantJobs.map((job) => (
+
+ ))}
+ >
+ );
+};
+
+export const MlAdminJobsDescription = memo(MlAdminJobsDescriptionComponent);
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/index.tsx
new file mode 100644
index 0000000000000..4a467e5dc4587
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/index.tsx
@@ -0,0 +1,8 @@
+/*
+ * 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 { MlJobsDescription } from './ml_jobs_description';
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/ml_job_item.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/ml_job_item.tsx
new file mode 100644
index 0000000000000..377600652c7d0
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/ml_job_item.tsx
@@ -0,0 +1,51 @@
+/*
+ * 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 { FC, ReactNode } from 'react';
+import React, { memo } from 'react';
+import styled from 'styled-components';
+import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+
+import type { MlSummaryJob } from '@kbn/ml-plugin/public';
+import * as i18n from './translations';
+
+import { isJobStarted } from '../../../../../common/machine_learning/helpers';
+
+import { MlJobLink } from '../ml_job_link/ml_job_link';
+import { MlAuditIcon } from '../ml_audit_icon';
+import { MlJobStatusBadge } from '../ml_job_status_badge';
+
+const Wrapper = styled.div`
+ overflow: hidden;
+`;
+
+const MlJobItemComponent: FC<{
+ job: MlSummaryJob;
+ switchComponent: ReactNode;
+}> = ({ job, switchComponent, ...props }) => {
+ const isStarted = isJobStarted(job.jobState, job.datafeedState);
+
+ return (
+
+
+
+
+
+
+
+
+
+ {switchComponent}
+
+ {isStarted ? i18n.ML_STOP_JOB_LABEL : i18n.ML_RUN_JOB_LABEL}
+
+
+
+ );
+};
+
+export const MlJobItem = memo(MlJobItemComponent);
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/ml_jobs_description.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/ml_jobs_description.test.tsx
new file mode 100644
index 0000000000000..a6ce1f1210d40
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/ml_jobs_description.test.tsx
@@ -0,0 +1,49 @@
+/*
+ * 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 { render, screen } from '@testing-library/react';
+
+import { hasMlUserPermissions } from '../../../../../common/machine_learning/has_ml_user_permissions';
+import { hasMlAdminPermissions } from '../../../../../common/machine_learning/has_ml_admin_permissions';
+
+import { MlJobsDescription } from './ml_jobs_description';
+
+jest.mock('./admin/ml_admin_jobs_description', () => ({
+ MlAdminJobsDescription: () => ,
+}));
+jest.mock('./user/ml_user_jobs_description', () => ({
+ MlUserJobsDescription: () => ,
+}));
+jest.mock('../../../../common/components/ml/hooks/use_ml_capabilities');
+jest.mock('../../../../../common/machine_learning/has_ml_admin_permissions');
+jest.mock('../../../../../common/machine_learning/has_ml_user_permissions');
+
+const hasMlUserPermissionsMock = hasMlUserPermissions as jest.Mock;
+
+describe('MlUserJobDescription', () => {
+ it('should render null if no ML permissions available', () => {
+ const { container } = render();
+
+ expect(container.firstChild).toBeNull();
+ });
+
+ it('should render user jobs component if ML permissions is for user only', () => {
+ hasMlUserPermissionsMock.mockReturnValueOnce(true);
+ render();
+
+ expect(screen.getByTestId('userJobs')).toBeInTheDocument();
+ expect(screen.queryByTestId('adminJobs')).not.toBeInTheDocument();
+ });
+
+ it('should render admin jobs component if ML permissions is for admin', () => {
+ (hasMlAdminPermissions as jest.Mock).mockReturnValueOnce(true);
+ render();
+
+ expect(screen.getByTestId('adminJobs')).toBeInTheDocument();
+ expect(screen.queryByTestId('userJobs')).not.toBeInTheDocument();
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/ml_jobs_description.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/ml_jobs_description.tsx
new file mode 100644
index 0000000000000..68ddaa0f7584b
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/ml_jobs_description.tsx
@@ -0,0 +1,39 @@
+/*
+ * 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 { FC } from 'react';
+import React, { memo } from 'react';
+
+import { useMlCapabilities } from '../../../../common/components/ml/hooks/use_ml_capabilities';
+import { hasMlAdminPermissions } from '../../../../../common/machine_learning/has_ml_admin_permissions';
+import { hasMlUserPermissions } from '../../../../../common/machine_learning/has_ml_user_permissions';
+
+import { MlAdminJobsDescription } from './admin/ml_admin_jobs_description';
+import { MlUserJobsDescription } from './user/ml_user_jobs_description';
+
+interface MlJobsDescriptionProps {
+ jobIds: string[];
+}
+
+const MlJobsDescriptionComponent: FC = ({ jobIds }) => {
+ const mlCapabilities = useMlCapabilities();
+
+ const isMlUser = hasMlUserPermissions(mlCapabilities);
+ const isMlAdmin = hasMlAdminPermissions(mlCapabilities);
+
+ if (isMlAdmin) {
+ return ;
+ }
+
+ if (isMlUser) {
+ return ;
+ }
+
+ return null;
+};
+
+export const MlJobsDescription = memo(MlJobsDescriptionComponent);
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/translations.ts
new file mode 100644
index 0000000000000..7650a4cb4ec59
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/translations.ts
@@ -0,0 +1,35 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+
+export const ML_RUN_JOB_LABEL = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.ruleDescription.mlRunJobLabel',
+ {
+ defaultMessage: 'Run job',
+ }
+);
+
+export const ML_STOP_JOB_LABEL = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.ruleDescription.mlStopJobLabel',
+ {
+ defaultMessage: 'Stop job',
+ }
+);
+
+export const ML_JOB_STOPPED = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.ruleDescription.mlJobStoppedDescription',
+ {
+ defaultMessage: 'Stopped',
+ }
+);
+
+export const ML_ADMIN_REQUIRED = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.ruleDescription.mlAdminPermissionsRequiredDescription',
+ {
+ defaultMessage: 'ML Admin Permissions required to perform this action',
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/user/ml_user_job_description.integration.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/user/ml_user_job_description.integration.test.tsx
new file mode 100644
index 0000000000000..1d6a946c2634c
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/user/ml_user_job_description.integration.test.tsx
@@ -0,0 +1,61 @@
+/*
+ * 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 { render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+
+import { TestProviders } from '../../../../../common/mock';
+
+import { MlUserJobDescription } from './ml_user_job_description';
+
+jest.mock('../../../../../common/lib/kibana');
+
+import { mockOpenedJob } from '../../../../../common/components/ml_popover/api.mock';
+
+describe('MlUserJobDescription', () => {
+ it('should render switch component disabled', async () => {
+ render(, { wrapper: TestProviders });
+ await waitFor(() => {
+ expect(screen.getByTestId('mlUserJobSwitch')).toBeDisabled();
+ });
+ });
+
+ it('should render toast that shows admin permissions required', async () => {
+ render(, { wrapper: TestProviders });
+
+ userEvent.hover(screen.getByTestId('mlUserJobSwitch').parentNode as Element);
+
+ await waitFor(() => {
+ expect(
+ screen.getByText('ML Admin Permissions required to perform this action')
+ ).toBeInTheDocument();
+ });
+ });
+
+ it('should render job details correctly', async () => {
+ render(, {
+ wrapper: TestProviders,
+ });
+
+ // link to job
+ const linkElement = screen.getByTestId('machineLearningJobLink');
+
+ expect(linkElement).toHaveAttribute('href', expect.any(String));
+ expect(linkElement).toHaveTextContent(mockOpenedJob.id);
+
+ // audit icon is not present as auditMessage empty
+ expect(screen.queryByTestId('mlJobAuditIcon')).toBeNull();
+
+ // job status
+ expect(screen.getByTestId('machineLearningJobStatus')).toHaveTextContent('Started');
+
+ // job action label
+ await waitFor(() => {
+ expect(screen.getByTestId('mlJobActionLabel')).toHaveTextContent('Stop job');
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/user/ml_user_job_description.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/user/ml_user_job_description.tsx
new file mode 100644
index 0000000000000..3b4e4fc2ffc70
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/user/ml_user_job_description.tsx
@@ -0,0 +1,42 @@
+/*
+ * 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 { FC } from 'react';
+import React, { useMemo, memo } from 'react';
+import { EuiSwitch, EuiToolTip } from '@elastic/eui';
+import noop from 'lodash/noop';
+
+import type { MlSummaryJob } from '@kbn/ml-plugin/public';
+
+import * as i18n from '../translations';
+
+import { isJobStarted } from '../../../../../../common/machine_learning/helpers';
+
+import { MlJobItem } from '../ml_job_item';
+
+const MlUserJobDescriptionComponent: FC<{
+ job: MlSummaryJob;
+}> = ({ job }) => {
+ const switchComponent = useMemo(
+ () => (
+
+
+
+ ),
+ [job]
+ );
+
+ return ;
+};
+
+export const MlUserJobDescription = memo(MlUserJobDescriptionComponent);
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/user/ml_user_jobs_description.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/user/ml_user_jobs_description.test.tsx
new file mode 100644
index 0000000000000..69eb3dbb41b81
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/user/ml_user_jobs_description.test.tsx
@@ -0,0 +1,49 @@
+/*
+ * 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 { render, screen } from '@testing-library/react';
+import type { MlSummaryJob } from '@kbn/ml-plugin/public';
+
+import { MlUserJobsDescription } from './ml_user_jobs_description';
+
+import { useInstalledSecurityJobs } from '../../../../../common/components/ml/hooks/use_installed_security_jobs';
+
+jest.mock(
+ './ml_user_job_description',
+ () =>
+ ({
+ MlUserJobDescription: (props) => {
+ return {props.job.id}
;
+ },
+ } as Record>)
+);
+
+jest.mock('../../../../../common/components/ml/hooks/use_installed_security_jobs');
+
+const useInstalledSecurityJobsMock = useInstalledSecurityJobs as jest.Mock;
+
+describe('MlUsersJobDescription', () => {
+ it('should render null if user permissions absent', () => {
+ useInstalledSecurityJobsMock.mockReturnValueOnce({ jobs: [], isMlUser: false });
+ const { container } = render();
+
+ expect(container.firstChild).toBeNull();
+ });
+
+ it('should render only jobs with job ids passed as props', () => {
+ useInstalledSecurityJobsMock.mockReturnValueOnce({
+ jobs: [{ id: 'mock-1' }, { id: 'mock-2' }, { id: 'mock-3' }],
+ isMlUser: true,
+ });
+ render();
+
+ const expectedJobs = screen.getAllByTestId('userMock');
+ expect(expectedJobs).toHaveLength(2);
+ expect(expectedJobs[0]).toHaveTextContent('mock-1');
+ expect(expectedJobs[1]).toHaveTextContent('mock-2');
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/user/ml_user_jobs_description.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/user/ml_user_jobs_description.tsx
new file mode 100644
index 0000000000000..5f743d371d531
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/ml_jobs_description/user/ml_user_jobs_description.tsx
@@ -0,0 +1,37 @@
+/*
+ * 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 { FC } from 'react';
+import React, { memo } from 'react';
+
+import { useInstalledSecurityJobs } from '../../../../../common/components/ml/hooks/use_installed_security_jobs';
+
+import { MlUserJobDescription } from './ml_user_job_description';
+
+interface MlUserJobsDescriptionProps {
+ jobIds: string[];
+}
+
+const MlUserJobsDescriptionComponent: FC = ({ jobIds }) => {
+ const { isMlUser, jobs } = useInstalledSecurityJobs();
+
+ if (!isMlUser) {
+ return null;
+ }
+
+ const relevantJobs = jobs.filter((job) => jobIds.includes(job.id));
+
+ return (
+ <>
+ {relevantJobs.map((job) => (
+
+ ))}
+ >
+ );
+};
+
+export const MlUserJobsDescription = memo(MlUserJobsDescriptionComponent);
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx
index 96422a717cb50..1f47db53a1087 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx
@@ -563,7 +563,7 @@ export const schema: FormSchema = {
label: i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupByFieldsLabel',
{
- defaultMessage: 'Suppress Alerts By',
+ defaultMessage: 'Suppress alerts by',
}
),
labelAppend: (
diff --git a/x-pack/test/functional/config.base.js b/x-pack/test/functional/config.base.js
index 1f4e987829be3..ea6a865ecbc58 100644
--- a/x-pack/test/functional/config.base.js
+++ b/x-pack/test/functional/config.base.js
@@ -175,6 +175,9 @@ export default async function ({ readConfigFile }) {
connectors: {
pathname: '/app/management/insightsAndAlerting/triggersActionsConnectors/',
},
+ triggersActions: {
+ pathname: '/app/management/insightsAndAlerting/triggersActions',
+ },
},
// choose where screenshots should be saved
diff --git a/x-pack/test/functional/services/actions/api.ts b/x-pack/test/functional/services/actions/api.ts
index a6ea0a2666119..89eef12cb3370 100644
--- a/x-pack/test/functional/services/actions/api.ts
+++ b/x-pack/test/functional/services/actions/api.ts
@@ -9,6 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context';
export function ActionsAPIServiceProvider({ getService }: FtrProviderContext) {
const kbnSupertest = getService('supertest');
+ const log = getService('log');
return {
async createConnector({
@@ -37,10 +38,24 @@ export function ActionsAPIServiceProvider({ getService }: FtrProviderContext) {
},
async deleteConnector(id: string) {
- return kbnSupertest
+ log.debug(`Deleting connector with id '${id}'...`);
+ const rsp = kbnSupertest
.delete(`/api/actions/connector/${id}`)
.set('kbn-xsrf', 'foo')
.expect(204, '');
+ log.debug('> Connector deleted.');
+ return rsp;
+ },
+
+ async deleteAllConnectors() {
+ const { body } = await kbnSupertest
+ .get(`/api/actions/connectors`)
+ .set('kbn-xsrf', 'foo')
+ .expect(200);
+
+ for (const connector of body) {
+ await this.deleteConnector(connector.id);
+ }
},
};
}
diff --git a/x-pack/test/functional/services/ml/alerting.ts b/x-pack/test/functional/services/ml/alerting.ts
index 7007f0d00f284..8906ee94d0abe 100644
--- a/x-pack/test/functional/services/ml/alerting.ts
+++ b/x-pack/test/functional/services/ml/alerting.ts
@@ -26,9 +26,9 @@ export function MachineLearningAlertingProvider(
return {
async selectAnomalyDetectionAlertType() {
- await testSubjects.click('xpack.ml.anomaly_detection_alert-SelectOption');
await retry.tryForTime(5000, async () => {
- await testSubjects.existOrFail(`mlAnomalyAlertForm`);
+ await testSubjects.click('xpack.ml.anomaly_detection_alert-SelectOption');
+ await testSubjects.existOrFail(`mlAnomalyAlertForm`, { timeout: 1000 });
});
},
@@ -50,8 +50,14 @@ export function MachineLearningAlertingProvider(
},
async selectResultType(resultType: string) {
- await testSubjects.click(`mlAnomalyAlertResult_${resultType}`);
- await this.assertResultTypeSelection(resultType);
+ if (
+ (await testSubjects.exists(`mlAnomalyAlertResult_${resultType}_selected`, {
+ timeout: 1000,
+ })) === false
+ ) {
+ await testSubjects.click(`mlAnomalyAlertResult_${resultType}`);
+ await this.assertResultTypeSelection(resultType);
+ }
},
async assertResultTypeSelection(resultType: string) {
@@ -168,5 +174,56 @@ export function MachineLearningAlertingProvider(
mlApi.assertResponseStatusCode(204, status, body);
}
},
+
+ async openNotifySelection() {
+ await retry.tryForTime(5000, async () => {
+ await testSubjects.click('notifyWhenSelect');
+ await testSubjects.existOrFail('onActionGroupChange', { timeout: 1000 });
+ });
+ },
+
+ async setRuleName(rulename: string) {
+ await testSubjects.setValue('ruleNameInput', rulename);
+ },
+
+ async scrollRuleNameIntoView() {
+ await testSubjects.scrollIntoView('ruleNameInput');
+ },
+
+ async selectSlackConnectorType() {
+ await retry.tryForTime(5000, async () => {
+ await testSubjects.click('.slack-alerting-ActionTypeSelectOption');
+ await testSubjects.existOrFail('createActionConnectorButton-0', { timeout: 1000 });
+ });
+ },
+
+ async clickCreateConnectorButton() {
+ await retry.tryForTime(5000, async () => {
+ await testSubjects.click('createActionConnectorButton-0');
+ await testSubjects.existOrFail('connectorAddModal', { timeout: 1000 });
+ });
+ },
+
+ async setConnectorName(connectorname: string) {
+ await testSubjects.setValue('nameInput', connectorname);
+ },
+
+ async setWebhookUrl(webhookurl: string) {
+ await testSubjects.setValue('slackWebhookUrlInput', webhookurl);
+ },
+
+ async clickSaveActionButton() {
+ await retry.tryForTime(5000, async () => {
+ await testSubjects.click('saveActionButtonModal');
+ await testSubjects.existOrFail('addNewActionConnectorActionGroup-0', { timeout: 1000 });
+ });
+ },
+
+ async openAddRuleVariable() {
+ await retry.tryForTime(5000, async () => {
+ await testSubjects.click('messageAddVariableButton');
+ await testSubjects.existOrFail('variableMenuButton-alert.actionGroup', { timeout: 1000 });
+ });
+ },
};
}
diff --git a/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/generate_anomaly_alerts.ts b/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/generate_anomaly_alerts.ts
new file mode 100644
index 0000000000000..b561a61095ab1
--- /dev/null
+++ b/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/generate_anomaly_alerts.ts
@@ -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 type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
+import { DATAFEED_STATE } from '@kbn/ml-plugin/common/constants/states';
+import { FtrProviderContext } from '../../../ftr_provider_context';
+
+import { ECOMMERCE_INDEX_PATTERN } from '..';
+
+function createTestJobAndDatafeed() {
+ const timestamp = Date.now();
+ const jobId = `high_sum_total_sales_${timestamp}`;
+
+ return {
+ job: {
+ job_id: jobId,
+ description: 'test_job',
+ groups: ['ecommerce'],
+ analysis_config: {
+ bucket_span: '1h',
+ detectors: [
+ {
+ detector_description: 'High total sales',
+ function: 'high_sum',
+ field_name: 'taxful_total_price',
+ over_field_name: 'customer_full_name.keyword',
+ detector_index: 0,
+ },
+ ],
+ influencers: ['customer_full_name.keyword', 'category.keyword'],
+ },
+ data_description: {
+ time_field: 'order_date',
+ time_format: 'epoch_ms',
+ },
+ analysis_limits: {
+ model_memory_limit: '13mb',
+ categorization_examples_limit: 4,
+ },
+ },
+ datafeed: {
+ datafeed_id: `datafeed-${jobId}`,
+ job_id: jobId,
+ query: {
+ bool: {
+ must: [
+ {
+ match_all: {},
+ },
+ ],
+ filter: [],
+ must_not: [],
+ },
+ },
+ query_delay: '120s',
+ indices: [ECOMMERCE_INDEX_PATTERN],
+ } as unknown as estypes.MlDatafeed,
+ };
+}
+
+export default ({ getPageObjects, getService }: FtrProviderContext) => {
+ const esArchiver = getService('esArchiver');
+ const ml = getService('ml');
+ const pageObjects = getPageObjects(['triggersActionsUI']);
+ const commonScreenshots = getService('commonScreenshots');
+ const browser = getService('browser');
+ const actions = getService('actions');
+
+ const screenshotDirectories = ['ml_docs', 'anomaly_detection'];
+
+ let testJobId = '';
+
+ describe('anomaly detection alert', function () {
+ before(async () => {
+ await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/ecommerce');
+ await ml.testResources.createIndexPatternIfNeeded('ft_ecommerce', 'order_date');
+
+ const { job, datafeed } = createTestJobAndDatafeed();
+
+ testJobId = job.job_id;
+
+ // Set up jobs
+ // @ts-expect-error not full interface
+ await ml.api.createAnomalyDetectionJob(job);
+ await ml.api.openAnomalyDetectionJob(job.job_id);
+ await ml.api.createDatafeed(datafeed);
+ await ml.api.startDatafeed(datafeed.datafeed_id);
+ await ml.api.waitForDatafeedState(datafeed.datafeed_id, DATAFEED_STATE.STARTED);
+ await ml.api.assertJobResultsExist(job.job_id);
+ });
+
+ after(async () => {
+ await ml.api.deleteAnomalyDetectionJobES(testJobId);
+ await ml.api.cleanMlIndices();
+ await ml.alerting.cleanAnomalyDetectionRules();
+ await actions.api.deleteAllConnectors();
+ });
+
+ describe('overview page alert flyout controls', () => {
+ it('alert flyout screenshot', async () => {
+ await ml.navigation.navigateToAlertsAndAction();
+ await pageObjects.triggersActionsUI.clickCreateAlertButton();
+ await ml.alerting.setRuleName('test-ecommerce');
+
+ await ml.alerting.openNotifySelection();
+ await commonScreenshots.takeScreenshot('ml-rule', screenshotDirectories, 1920, 1400);
+
+ // close popover
+ await browser.pressKeys(browser.keys.ESCAPE);
+
+ await ml.alerting.selectAnomalyDetectionAlertType();
+ await ml.testExecution.logTestStep('should have correct default values');
+ await ml.alerting.assertSeverity(75);
+ await ml.alerting.assertPreviewButtonState(false);
+ await ml.testExecution.logTestStep('should complete the alert params');
+ await ml.alerting.selectJobs([testJobId]);
+ await ml.alerting.selectResultType('bucket');
+ await ml.alerting.setSeverity(75);
+ await ml.testExecution.logTestStep('should populate advanced settings with default values');
+ await ml.alerting.assertTopNBuckets(1);
+ await ml.alerting.assertLookbackInterval('123m');
+ await ml.testExecution.logTestStep('should preview the alert condition');
+ await ml.alerting.assertPreviewButtonState(false);
+ await ml.alerting.setTestInterval('1y');
+ await ml.alerting.assertPreviewButtonState(true);
+ await ml.alerting.scrollRuleNameIntoView();
+ await ml.testExecution.logTestStep('take screenshot');
+ await commonScreenshots.takeScreenshot(
+ 'ml-anomaly-alert-severity',
+ screenshotDirectories,
+ 1920,
+ 1400
+ );
+ await ml.alerting.selectSlackConnectorType();
+ await ml.testExecution.logTestStep('should open connectors');
+ await ml.alerting.clickCreateConnectorButton();
+ await ml.alerting.setConnectorName('test-connector');
+ await ml.alerting.setWebhookUrl('https://www.elastic.co');
+ await ml.alerting.clickSaveActionButton();
+ await ml.alerting.openAddRuleVariable();
+ await ml.testExecution.logTestStep('take screenshot');
+ await commonScreenshots.takeScreenshot(
+ 'ml-anomaly-alert-messages',
+ screenshotDirectories,
+ 1920,
+ 1400
+ );
+ });
+ });
+ });
+};
diff --git a/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/index.ts b/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/index.ts
index 389a240eaa464..4d092c36d6545 100644
--- a/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/index.ts
+++ b/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/index.ts
@@ -10,6 +10,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ loadTestFile }: FtrProviderContext) {
describe('anomaly detection', function () {
loadTestFile(require.resolve('./geographic_data'));
+ loadTestFile(require.resolve('./generate_anomaly_alerts'));
loadTestFile(require.resolve('./population_analysis'));
loadTestFile(require.resolve('./custom_urls'));
loadTestFile(require.resolve('./mapping_anomalies'));
diff --git a/x-pack/test/screenshot_creation/config.ts b/x-pack/test/screenshot_creation/config.ts
index c83e90658ff72..5464806c20726 100644
--- a/x-pack/test/screenshot_creation/config.ts
+++ b/x-pack/test/screenshot_creation/config.ts
@@ -8,6 +8,7 @@
import Fs from 'fs';
import { CA_CERT_PATH } from '@kbn/dev-utils';
import { FtrConfigProviderContext } from '@kbn/test';
+import { pageObjects } from './page_objects';
import { services } from './services';
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
@@ -28,6 +29,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
// default to the xpack functional config
...xpackFunctionalConfig.getAll(),
servers,
+ pageObjects,
services,
testFiles: [require.resolve('./apps')],
junit: {