diff --git a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts index 8c53e7b1f5a87..6bcba719deaee 100644 --- a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts +++ b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts @@ -7,6 +7,7 @@ import { Moment } from 'moment'; +import { MlCustomSettings } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { CombinedJob, CombinedJobWithStats } from './combined_job'; import type { MlAnomalyDetectionAlertRule } from '../alerts'; import type { MlJobBlocked } from './job'; @@ -39,6 +40,7 @@ export interface MlSummaryJob { alertingRules?: MlAnomalyDetectionAlertRule[]; jobTags: Record; bucketSpanSeconds: number; + customSettings?: MlCustomSettings; } export interface AuditMessage { diff --git a/x-pack/plugins/ml/public/application/components/delete_job_check_modal/delete_job_check_modal.tsx b/x-pack/plugins/ml/public/application/components/delete_job_check_modal/delete_job_check_modal.tsx index 5507cbfe155b4..6834f16386ce2 100644 --- a/x-pack/plugins/ml/public/application/components/delete_job_check_modal/delete_job_check_modal.tsx +++ b/x-pack/plugins/ml/public/application/components/delete_job_check_modal/delete_job_check_modal.tsx @@ -20,10 +20,12 @@ import { EuiButton, EuiLoadingSpinner, EuiText, + EuiSpacer, } from '@elastic/eui'; import { JobType, CanDeleteJobResponse } from '../../../../common/types/saved_objects'; import { useMlApiContext } from '../../contexts/kibana'; import { useToastNotificationService } from '../../services/toast_notification_service'; +import { ManagedJobsWarningCallout } from '../../jobs/jobs_list/components/confirm_modals/managed_jobs_warning_callout'; const shouldUnTagLabel = i18n.translate('xpack.ml.deleteJobCheckModal.shouldUnTagLabel', { defaultMessage: 'Remove job from current space', @@ -58,7 +60,8 @@ function getRespSummary(resp: CanDeleteJobResponse): JobCheckRespSummary { function getModalContent( jobIds: string[], - respSummary: JobCheckRespSummary + respSummary: JobCheckRespSummary, + hasManagedJob?: boolean ): ModalContentReturnType { const { canDelete, canRemoveFromSpace, canTakeAnyAction } = respSummary; @@ -107,13 +110,30 @@ function getModalContent( /> ), modalText: ( - - - + <> + {hasManagedJob ? ( + <> + + + + ) : null} + + + + + ), }; } else if (canRemoveFromSpace) { @@ -125,13 +145,27 @@ function getModalContent( /> ), modalText: ( - - - + <> + {hasManagedJob ? ( + <> + + + + ) : null} + + + + + ), }; } else { @@ -146,6 +180,7 @@ interface Props { jobType: JobType; jobIds: string[]; setDidUntag?: React.Dispatch>; + hasManagedJob?: boolean; } export const DeleteJobCheckModal: FC = ({ @@ -155,6 +190,7 @@ export const DeleteJobCheckModal: FC = ({ jobType, jobIds, setDidUntag, + hasManagedJob, }) => { const [buttonContent, setButtonContent] = useState(); const [modalContent, setModalContent] = useState(); @@ -180,7 +216,7 @@ export const DeleteJobCheckModal: FC = ({ return; } setJobCheckRespSummary(respSummary); - const { buttonText, modalText } = getModalContent(jobIds, respSummary); + const { buttonText, modalText } = getModalContent(jobIds, respSummary, hasManagedJob); setButtonContent(buttonText); setModalContent(modalText); }); @@ -188,7 +224,7 @@ export const DeleteJobCheckModal: FC = ({ setDidUntag(false); } setIsLoading(false); - }, []); + }, [hasManagedJob]); const onUntagClick = async () => { setIsUntagging(true); diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/confirm_modals/close_jobs_confirm_modal.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/confirm_modals/close_jobs_confirm_modal.tsx new file mode 100644 index 0000000000000..5e7b7a18e3363 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/confirm_modals/close_jobs_confirm_modal.tsx @@ -0,0 +1,136 @@ +/* + * 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, { FC, useState, useEffect, useCallback, useMemo } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { + EuiSpacer, + EuiModal, + EuiModalHeader, + EuiModalHeaderTitle, + EuiModalBody, + EuiModalFooter, + EuiButtonEmpty, + EuiButton, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { MlSummaryJob } from '../../../../../../common/types/anomaly_detection_jobs'; +import { isManagedJob } from '../../../jobs_utils'; +import { closeJobs } from '../utils'; +import { ManagedJobsWarningCallout } from './managed_jobs_warning_callout'; + +type ShowFunc = (jobs: MlSummaryJob[]) => void; + +interface Props { + setShowFunction(showFunc: ShowFunc): void; + unsetShowFunction(): void; + refreshJobs(): void; +} + +export const CloseJobsConfirmModal: FC = ({ + setShowFunction, + unsetShowFunction, + refreshJobs, +}) => { + const [modalVisible, setModalVisible] = useState(false); + const [hasManagedJob, setHasManaged] = useState(true); + const [jobsToReset, setJobsToReset] = useState([]); + + const jobIds = useMemo(() => jobsToReset.map(({ id }) => id), [jobsToReset]); + + useEffect(() => { + if (typeof setShowFunction === 'function') { + setShowFunction(showModal); + } + return () => { + if (typeof unsetShowFunction === 'function') { + unsetShowFunction(); + } + }; + }, []); + + const showModal = useCallback((jobs: MlSummaryJob[]) => { + setJobsToReset(jobs); + + if (jobs.some((j) => isManagedJob(j))) { + setModalVisible(true); + setHasManaged(true); + } + }, []); + + const closeModal = useCallback(() => { + setModalVisible(false); + setHasManaged(false); + }, []); + + if (modalVisible === false) { + return null; + } + + if (hasManagedJob) { + const title = ( + + ); + + return ( + + + {title} + + + + + <> + + + + + + + { + closeJobs(jobsToReset, refreshJobs); + closeModal(); + }} + fill + color="danger" + data-test-subj="mlCloseJobsConfirmModalButton" + > + + + + + + ); + } else { + return <>; + } +}; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/confirm_modals/managed_jobs_warning_callout.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/confirm_modals/managed_jobs_warning_callout.tsx new file mode 100644 index 0000000000000..5d6a72e927ecb --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/confirm_modals/managed_jobs_warning_callout.tsx @@ -0,0 +1,38 @@ +/* + * 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 { EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import React from 'react'; + +export const ManagedJobsWarningCallout = ({ + jobsCount, + action, + message, +}: { + jobsCount: number; + action?: string; + message?: string; +}) => { + return ( + <> + + + {message ?? ( + + )} + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/confirm_modals/stop_datafeeds_confirm_modal.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/confirm_modals/stop_datafeeds_confirm_modal.tsx new file mode 100644 index 0000000000000..1a6d9e1e433ff --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/confirm_modals/stop_datafeeds_confirm_modal.tsx @@ -0,0 +1,137 @@ +/* + * 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, { FC, useState, useEffect, useCallback, useMemo } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { + EuiSpacer, + EuiModal, + EuiModalHeader, + EuiModalHeaderTitle, + EuiModalBody, + EuiModalFooter, + EuiButtonEmpty, + EuiButton, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { MlSummaryJob } from '../../../../../../common/types/anomaly_detection_jobs'; +import { isManagedJob } from '../../../jobs_utils'; +import { stopDatafeeds } from '../utils'; +import { ManagedJobsWarningCallout } from './managed_jobs_warning_callout'; + +type ShowFunc = (jobs: MlSummaryJob[]) => void; + +interface Props { + setShowFunction(showFunc: ShowFunc): void; + unsetShowFunction(): void; + refreshJobs(): void; + showStopDatafeedsFlyout(job: MlSummaryJob[]): void; +} + +export const StopDatafeedsConfirmModal: FC = ({ + setShowFunction, + unsetShowFunction, + refreshJobs, +}) => { + const [modalVisible, setModalVisible] = useState(false); + const [hasManagedJob, setHasManaged] = useState(true); + const [jobsToStop, setJobsToStop] = useState([]); + + const jobIds = useMemo(() => jobsToStop.map(({ id }) => id), [jobsToStop]); + + useEffect(() => { + if (typeof setShowFunction === 'function') { + setShowFunction(showModal); + } + return () => { + if (typeof unsetShowFunction === 'function') { + unsetShowFunction(); + } + }; + }, []); + + const showModal = useCallback((jobs: MlSummaryJob[]) => { + setJobsToStop(jobs); + + if (jobs.some((j) => isManagedJob(j))) { + setModalVisible(true); + setHasManaged(true); + } + }, []); + + const closeModal = useCallback(() => { + setModalVisible(false); + setHasManaged(false); + }, []); + + if (modalVisible === false) { + return null; + } + + if (hasManagedJob) { + const title = ( + + ); + + return ( + + + {title} + + + + + <> + + + + + + + { + stopDatafeeds(jobsToStop, refreshJobs); + closeModal(); + }} + fill + color="danger" + data-test-subj="mlStopDatafeedsConfirmModalButton" + > + + + + + + ); + } else { + return <>; + } +}; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.tsx index 2f04e8ae86e73..49c09761bc815 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.tsx @@ -20,10 +20,13 @@ import { EuiText, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { deleteJobs } from '../utils'; import { DELETING_JOBS_REFRESH_INTERVAL_MS } from '../../../../../../common/constants/jobs_list'; import { DeleteJobCheckModal } from '../../../../components/delete_job_check_modal'; import { MlSummaryJob } from '../../../../../../common/types/anomaly_detection_jobs'; +import { isManagedJob } from '../../../jobs_utils'; +import { ManagedJobsWarningCallout } from '../confirm_modals/managed_jobs_warning_callout'; type ShowFunc = (jobs: MlSummaryJob[]) => void; @@ -38,6 +41,7 @@ export const DeleteJobModal: FC = ({ setShowFunction, unsetShowFunction, const [modalVisible, setModalVisible] = useState(false); const [jobIds, setJobIds] = useState([]); const [canDelete, setCanDelete] = useState(false); + const [hasManagedJob, setHasManagedJob] = useState(false); useEffect(() => { if (typeof setShowFunction === 'function') { @@ -52,6 +56,7 @@ export const DeleteJobModal: FC = ({ setShowFunction, unsetShowFunction, const showModal = useCallback((jobs: MlSummaryJob[]) => { setJobIds(jobs.map(({ id }) => id)); + setHasManagedJob(jobs.some((job) => isManagedJob(job))); setModalVisible(true); setDeleting(false); }, []); @@ -104,17 +109,30 @@ export const DeleteJobModal: FC = ({ setShowFunction, unsetShowFunction, ) : ( - - - + values={{ + jobsCount: jobIds.length, + }} + /> + + )}

@@ -155,6 +173,7 @@ export const DeleteJobModal: FC = ({ setShowFunction, unsetShowFunction, }} onCloseCallback={closeModal} refreshJobsCallback={refreshJobs} + hasManagedJob={hasManagedJob} /> ); diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js index c278f8790984f..3ede5cf813f44 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js @@ -22,6 +22,7 @@ import { EuiFlexItem, EuiTabbedContent, EuiConfirmModal, + EuiSpacer, } from '@elastic/eui'; import { JobDetails, Detectors, Datafeed, CustomUrls } from './tabs'; @@ -33,6 +34,8 @@ import { ml } from '../../../../services/ml_api_service'; import { withKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; import { collapseLiteralStrings } from '../../../../../../shared_imports'; import { DATAFEED_STATE, JOB_STATE } from '../../../../../../common/constants/states'; +import { isManagedJob } from '../../../jobs_utils'; +import { ManagedJobsWarningCallout } from '../confirm_modals/managed_jobs_warning_callout'; export class EditJobFlyoutUI extends Component { _initialJobFormState = null; @@ -412,6 +415,21 @@ export class EditJobFlyoutUI extends Component { /> + + {isManagedJob(job) ? ( + <> + + + + ) : null} {}} /> diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/management.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/management.js index 64bc7f4a517c2..5b8b4b386213d 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/management.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/management.js @@ -19,12 +19,15 @@ import { isResettable, } from '../utils'; import { i18n } from '@kbn/i18n'; +import { isManagedJob } from '../../../jobs_utils'; export function actionsMenuContent( showEditJobFlyout, showDeleteJobModal, showResetJobModal, showStartDatafeedModal, + showCloseJobsConfirmModal, + showStopDatafeedsConfirmModal, refreshJobs, showCreateAlertFlyout ) { @@ -65,7 +68,12 @@ export function actionsMenuContent( enabled: (item) => isJobBlocked(item) === false && canStartStopDatafeed, available: (item) => isStoppable([item]), onClick: (item) => { - stopDatafeeds([item], refreshJobs); + if (isManagedJob(item)) { + showStopDatafeedsConfirmModal([item]); + } else { + stopDatafeeds([item], refreshJobs); + } + closeMenu(true); }, 'data-test-subj': 'mlActionButtonStopDatafeed', @@ -97,7 +105,12 @@ export function actionsMenuContent( enabled: (item) => isJobBlocked(item) === false && canCloseJob, available: (item) => isClosable([item]), onClick: (item) => { - closeJobs([item], refreshJobs); + if (isManagedJob(item)) { + showCloseJobsConfirmModal([item]); + } else { + closeJobs([item], refreshJobs); + } + closeMenu(true); }, 'data-test-subj': 'mlActionButtonCloseJob', diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js index dea8fdd30e372..fd00058e64713 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js @@ -42,7 +42,7 @@ export function extractJobDetails(job, basePath, refreshJobList) { defaultMessage: 'Custom settings', }), position: 'right', - items: settings ? filterObjects(settings, true, true) : [], + items: settings ? filterObjects(settings, true, true).map(formatValues) : [], }; const jobTags = { diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js index 92bbc720d8ddd..6bbbe9e59f783 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js @@ -80,6 +80,10 @@ export function formatValues(obj) { typeof value === 'number' ? roundToDecimalPlace(value, 3).toLocaleString() : value, ]; + // boolean + case 'managed': + return [key, value.toString()]; + default: return [key, value]; } diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js index 5fe005c4b34ff..71a6b7d88fe1a 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js @@ -24,10 +24,12 @@ import { EuiIcon, EuiScreenReaderOnly, EuiToolTip, + EuiBadge, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { AnomalyDetectionJobIdLink } from './job_id_link'; +import { isManagedJob } from '../../../jobs_utils'; const PAGE_SIZE_OPTIONS = [10, 25, 50]; @@ -169,7 +171,31 @@ export class JobsList extends Component { truncateText: false, width: '15%', scope: 'row', - render: isManagementTable ? (id) => this.getJobIdLink(id) : undefined, + render: isManagementTable + ? (id) => this.getJobIdLink(id) + : (id, item) => { + if (!isManagedJob(item)) return id; + + return ( + <> + + {id}   + + + {i18n.translate('xpack.ml.jobsList.managedBadgeLabel', { + defaultMessage: 'Managed', + })} + + + + + ); + }, }, { field: 'auditMessage', @@ -340,6 +366,8 @@ export class JobsList extends Component { this.props.showDeleteJobModal, this.props.showResetJobModal, this.props.showStartDatafeedModal, + this.props.showCloseJobsConfirmModal, + this.props.showStopDatafeedsConfirmModal, this.props.refreshJobs, this.props.showCreateAlertFlyout ), @@ -414,7 +442,9 @@ JobsList.propTypes = { showEditJobFlyout: PropTypes.func, showDeleteJobModal: PropTypes.func, showStartDatafeedModal: PropTypes.func, + showCloseJobsConfirmModal: PropTypes.func, showCreateAlertFlyout: PropTypes.func, + showStopDatafeedsConfirmModal: PropTypes.func, refreshJobs: PropTypes.func, selectedJobsCount: PropTypes.number.isRequired, loading: PropTypes.bool, diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js index edcb38c6305ef..539d0c475bb00 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js @@ -29,6 +29,8 @@ import { RefreshJobsListButton } from '../refresh_jobs_list_button'; import { DELETING_JOBS_REFRESH_INTERVAL_MS } from '../../../../../../common/constants/jobs_list'; import { JobListMlAnomalyAlertFlyout } from '../../../../../alerting/ml_alerting_flyout'; +import { StopDatafeedsConfirmModal } from '../confirm_modals/stop_datafeeds_confirm_modal'; +import { CloseJobsConfirmModal } from '../confirm_modals/close_jobs_confirm_modal'; let blockingJobsRefreshTimeout = null; @@ -58,6 +60,8 @@ export class JobsListView extends Component { this.updateFunctions = {}; this.showEditJobFlyout = () => {}; + this.showStopDatafeedsConfirmModal = () => {}; + this.showCloseJobsConfirmModal = () => {}; this.showDeleteJobModal = () => {}; this.showResetJobModal = () => {}; this.showStartDatafeedModal = () => {}; @@ -193,6 +197,22 @@ export class JobsListView extends Component { this.showEditJobFlyout = () => {}; }; + setShowStopDatafeedsConfirmModalFunction = (func) => { + this.showStopDatafeedsConfirmModal = func; + }; + + unsetShowStopDatafeedsConfirmModalFunction = () => { + this.showStopDatafeedsConfirmModal = () => {}; + }; + + setShowCloseJobsConfirmModalFunction = (func) => { + this.showCloseJobsConfirmModal = func; + }; + + unsetShowCloseJobsConfirmModalFunction = () => { + this.showCloseJobsConfirmModal = () => {}; + }; + setShowDeleteJobModalFunction = (func) => { this.showDeleteJobModal = func; }; @@ -490,10 +510,12 @@ export class JobsListView extends Component { this.refreshJobSummaryList(true)} /> this.refreshJobSummaryList(true)} jobsViewState={this.props.jobsViewState} onJobsViewStateUpdate={this.props.onJobsViewStateUpdate} @@ -524,6 +548,18 @@ export class JobsListView extends Component { refreshJobs={() => this.refreshJobSummaryList(true)} allJobIds={jobIds} /> + this.refreshJobSummaryList(true)} + allJobIds={jobIds} + /> + + this.refreshJobSummaryList(true)} + /> { - closeJobs(this.props.jobs); + if (this.props.jobs.some((j) => isManagedJob(j))) { + this.props.showCloseJobsConfirmModal(this.props.jobs); + } else { + closeJobs(this.props.jobs); + } + this.closePopover(); }} data-test-subj="mlADJobListMultiSelectCloseJobActionButton" @@ -139,7 +145,11 @@ class MultiJobActionsMenuUI extends Component { icon="stop" disabled={this.canStartStopDatafeed === false} onClick={() => { - stopDatafeeds(this.props.jobs, this.props.refreshJobs); + if (this.props.jobs.some((j) => isManagedJob(j))) { + this.props.showStopDatafeedsConfirmModal(this.props.jobs); + } else { + stopDatafeeds(this.props.jobs, this.props.refreshJobs); + } this.closePopover(); }} data-test-subj="mlADJobListMultiSelectStopDatafeedActionButton" @@ -211,6 +221,7 @@ MultiJobActionsMenuUI.propTypes = { jobs: PropTypes.array.isRequired, showStartDatafeedModal: PropTypes.func.isRequired, showDeleteJobModal: PropTypes.func.isRequired, + showStopDatafeedsConfirmModal: PropTypes.func.isRequired, refreshJobs: PropTypes.func.isRequired, showCreateAlertFlyout: PropTypes.func.isRequired, }; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/multi_job_actions.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/multi_job_actions.js index 148f1a9276c50..eede9d4573225 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/multi_job_actions.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/multi_job_actions.js @@ -64,9 +64,11 @@ export class MultiJobActions extends Component { @@ -81,8 +83,10 @@ MultiJobActions.propTypes = { selectedJobs: PropTypes.array.isRequired, allJobIds: PropTypes.array.isRequired, showStartDatafeedModal: PropTypes.func.isRequired, + showCloseJobsConfirmModal: PropTypes.func.isRequired, showDeleteJobModal: PropTypes.func.isRequired, showResetJobModal: PropTypes.func.isRequired, + showStopDatafeedsConfirmModal: PropTypes.func.isRequired, refreshJobs: PropTypes.func.isRequired, showCreateAlertFlyout: PropTypes.func.isRequired, }; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/reset_job_modal/reset_job_modal.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/reset_job_modal/reset_job_modal.tsx index 45e928cdf3905..49a2b810c1000 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/reset_job_modal/reset_job_modal.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/reset_job_modal/reset_job_modal.tsx @@ -19,10 +19,13 @@ import { EuiText, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { resetJobs } from '../utils'; import type { MlSummaryJob } from '../../../../../../common/types/anomaly_detection_jobs'; import { RESETTING_JOBS_REFRESH_INTERVAL_MS } from '../../../../../../common/constants/jobs_list'; import { OpenJobsWarningCallout } from './open_jobs_warning_callout'; +import { isManagedJob } from '../../../jobs_utils'; +import { ManagedJobsWarningCallout } from '../confirm_modals/managed_jobs_warning_callout'; type ShowFunc = (jobs: MlSummaryJob[]) => void; @@ -37,6 +40,7 @@ export const ResetJobModal: FC = ({ setShowFunction, unsetShowFunction, r const [modalVisible, setModalVisible] = useState(false); const [jobIds, setJobIds] = useState([]); const [jobs, setJobs] = useState([]); + const [hasManagedJob, setHasManagedJob] = useState(false); useEffect(() => { if (typeof setShowFunction === 'function') { @@ -52,6 +56,8 @@ export const ResetJobModal: FC = ({ setShowFunction, unsetShowFunction, r const showModal = useCallback((tempJobs: MlSummaryJob[]) => { setJobIds(tempJobs.map(({ id }) => id)); setJobs(tempJobs); + setHasManagedJob(tempJobs.some((j) => isManagedJob(j))); + setModalVisible(true); setResetting(false); }, []); @@ -90,6 +96,22 @@ export const ResetJobModal: FC = ({ setShowFunction, unsetShowFunction, r <> + + {hasManagedJob === true ? ( + <> + + + + ) : null} + 0; + this.setState({ jobs, isModalVisible: true, @@ -98,6 +101,7 @@ export class StartDatafeedModal extends Component { allowCreateAlert, createAlert: false, now, + hasManagedJob: jobs.some((j) => isManagedJob(j)), }); }; @@ -164,6 +168,8 @@ export class StartDatafeedModal extends Component { setEndTime={this.setEndTime} now={now} setTimeRangeValid={this.setTimeRangeValid} + hasManagedJob={this.state.hasManagedJob} + jobsCount={startableJobs.length} /> {this.state.endTime === undefined && (
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/time_range_selector.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/time_range_selector.js index 20ffba0e11d4c..4300a918b948f 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/time_range_selector.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/time_range_selector.js @@ -9,12 +9,13 @@ import './_time_range_selector.scss'; import PropTypes from 'prop-types'; import React, { Component, useState, useEffect } from 'react'; -import { EuiDatePicker, EuiFieldText } from '@elastic/eui'; +import { EuiDatePicker, EuiFieldText, EuiSpacer } from '@elastic/eui'; import moment from 'moment'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { TIME_FORMAT } from '../../../../../../../common/constants/time_format'; +import { ManagedJobsWarningCallout } from '../../confirm_modals/managed_jobs_warning_callout'; export class TimeRangeSelector extends Component { constructor(props) { @@ -162,6 +163,24 @@ export class TimeRangeSelector extends Component { const { startItems, endItems } = this.getTabItems(); return (
+ {this.props.hasManagedJob === true && this.state.endTab !== 0 ? ( + <> + + + + ) : null}
, callback?: () => void): Promise; +export function closeJobs(jobs: Array<{ id: string }>, callback?: () => void): Promise; export function deleteJobs(jobs: Array<{ id: string }>, callback?: () => void): Promise; export function resetJobs(jobIds: string[], callback?: () => void): Promise; export function loadFullJob(jobId: string): Promise; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_utils.ts b/x-pack/plugins/ml/public/application/jobs/jobs_utils.ts new file mode 100644 index 0000000000000..103f079b88ff3 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/jobs_utils.ts @@ -0,0 +1,17 @@ +/* + * 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 { MlJob } from '@elastic/elasticsearch/lib/api/types'; +import { isPopulatedObject } from '../../../common'; +import { MlSummaryJob } from '../../../common/types/anomaly_detection_jobs'; + +export const isManagedJob = (job: MlSummaryJob | MlJob) => { + return ( + (isPopulatedObject(job, ['customSettings']) && job.customSettings.managed === true) || + (isPopulatedObject(job, ['custom_settings']) && job.custom_settings.managed === true) + ); +}; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts index 9cea7aef2e117..804a368174c76 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts @@ -767,6 +767,7 @@ export class JobCreator { this._detectors = this._job_config.analysis_config.detectors; this._influencers = this._job_config.analysis_config.influencers!; + if (this._job_config.groups === undefined) { this._job_config.groups = []; } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/apm_tx_metrics.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/apm_tx_metrics.json index f93b4fb009a14..9f67527ba677d 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/apm_tx_metrics.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/apm_tx_metrics.json @@ -48,6 +48,7 @@ }, "results_index_name" : "custom-apm", "custom_settings": { - "created_by": "ml-module-apm-transaction" + "created_by": "ml-module-apm-transaction", + "managed": true } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/ml/log_entry_rate.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/ml/log_entry_rate.json index 42ba15591e5c4..0cd99ae71765d 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/ml/log_entry_rate.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/ml/log_entry_rate.json @@ -25,6 +25,7 @@ "enabled": true }, "custom_settings": { + "managed": true, "created_by": "ml-module-logs-ui-analysis", "job_revision": 2 } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/ml/log_entry_categories_count.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/ml/log_entry_categories_count.json index 90f88275cb6d0..74f434f36e4ed 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/ml/log_entry_categories_count.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/ml/log_entry_categories_count.json @@ -37,6 +37,7 @@ "enabled": true }, "custom_settings": { + "managed": true, "created_by": "ml-module-logs-ui-categories", "job_revision": 2 } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_hosts/ml/hosts_memory_usage.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_hosts/ml/hosts_memory_usage.json index c5f62105613ba..07f2c7fe9a927 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_hosts/ml/hosts_memory_usage.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_hosts/ml/hosts_memory_usage.json @@ -39,6 +39,7 @@ "model_memory_limit": "64mb" }, "custom_settings": { + "managed": true, "created_by": "ml-module-metrics-ui-hosts", "custom_urls": [ { diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_hosts/ml/hosts_network_in.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_hosts/ml/hosts_network_in.json index 258fb87f5260c..6025777e49d05 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_hosts/ml/hosts_network_in.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_hosts/ml/hosts_network_in.json @@ -26,6 +26,7 @@ "model_memory_limit": "32mb" }, "custom_settings": { + "managed": true, "created_by": "ml-module-metrics-ui-hosts", "custom_urls": [ { diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_hosts/ml/hosts_network_out.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_hosts/ml/hosts_network_out.json index 381bc09bac46c..95b3950dfc35a 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_hosts/ml/hosts_network_out.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_hosts/ml/hosts_network_out.json @@ -26,6 +26,7 @@ "model_memory_limit": "32mb" }, "custom_settings": { + "managed": true, "created_by": "ml-module-metrics-ui-hosts", "custom_urls": [ { diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_k8s/ml/k8s_memory_usage.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_k8s/ml/k8s_memory_usage.json index ef57612e9f90e..8b76b328a05db 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_k8s/ml/k8s_memory_usage.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_k8s/ml/k8s_memory_usage.json @@ -42,6 +42,7 @@ "model_memory_limit": "64mb" }, "custom_settings": { + "managed": true, "created_by": "ml-module-metrics-ui-k8s", "custom_urls": [ { diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_k8s/ml/k8s_network_in.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_k8s/ml/k8s_network_in.json index 91f855a59add5..de591577d2d35 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_k8s/ml/k8s_network_in.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_k8s/ml/k8s_network_in.json @@ -28,6 +28,7 @@ "model_memory_limit": "32mb" }, "custom_settings": { + "managed": true, "created_by": "ml-module-metrics-ui-k8s", "custom_urls": [ { diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_k8s/ml/k8s_network_out.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_k8s/ml/k8s_network_out.json index e68866a655acf..61ce7b25eacc0 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_k8s/ml/k8s_network_out.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_k8s/ml/k8s_network_out.json @@ -28,6 +28,7 @@ "model_memory_limit": "32mb" }, "custom_settings": { + "managed": true, "created_by": "ml-module-metrics-ui-k8s", "custom_urls": [ { diff --git a/x-pack/plugins/ml/server/models/job_service/jobs.ts b/x-pack/plugins/ml/server/models/job_service/jobs.ts index ffc07e06db0f9..a79093caabbef 100644 --- a/x-pack/plugins/ml/server/models/job_service/jobs.ts +++ b/x-pack/plugins/ml/server/models/job_service/jobs.ts @@ -220,6 +220,7 @@ export function jobsProvider( const tempJob: MlSummaryJob = { id: job.job_id, description: job.description || '', + customSettings: job.custom_settings, groups: Array.isArray(job.groups) ? job.groups.sort() : [], processed_record_count: job.data_counts?.processed_record_count, earliestStartTimestampMs: getEarliestDatafeedStartTime( @@ -319,12 +320,19 @@ export function jobsProvider( if (jobResults && jobResults.jobs) { const job = jobResults.jobs.find((j) => j.job_id === jobId); if (job) { + removeUnClonableCustomSettings(job); result.job = job; } } return result; } + function removeUnClonableCustomSettings(job: Job) { + if (isPopulatedObject(job.custom_settings)) { + delete job.custom_settings.managed; + } + } + async function createFullJobsList(jobIds: string[] = []) { const jobs: CombinedJobWithStats[] = []; const groups: { [jobId: string]: string[] } = {}; diff --git a/x-pack/plugins/transform/public/app/common/managed_transforms_utils.ts b/x-pack/plugins/transform/public/app/common/managed_transforms_utils.ts new file mode 100644 index 0000000000000..5164245646d6e --- /dev/null +++ b/x-pack/plugins/transform/public/app/common/managed_transforms_utils.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 { TransformListRow } from './transform_list'; + +export const isManagedTransform = (transform: Partial) => { + return transform.config?._meta?.managed; +}; diff --git a/x-pack/plugins/transform/public/app/common/transform.ts b/x-pack/plugins/transform/public/app/common/transform.ts index c1f8f58657d5e..09fc1c24303d8 100644 --- a/x-pack/plugins/transform/public/app/common/transform.ts +++ b/x-pack/plugins/transform/public/app/common/transform.ts @@ -9,8 +9,8 @@ import { useEffect } from 'react'; import { BehaviorSubject } from 'rxjs'; import { filter, distinctUntilChanged } from 'rxjs/operators'; import { Subscription } from 'rxjs'; - -import { TransformId } from '../../../common/types/transform'; +import { cloneDeep } from 'lodash'; +import type { TransformConfigUnion, TransformId } from '../../../common/types/transform'; // Via https://github.com/elastic/elasticsearch/blob/master/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/utils/TransformStrings.java#L24 // Matches a string that contains lowercase characters, digits, hyphens, underscores or dots. @@ -78,3 +78,11 @@ export const useRefreshTransformList = ( }, }; }; + +export const overrideTransformForCloning = (originalConfig: TransformConfigUnion) => { + // 'Managed' means job is preconfigured and deployed by other solutions + // we should not clone this setting + const clonedConfig = cloneDeep(originalConfig); + delete clonedConfig._meta?.managed; + return clonedConfig; +}; diff --git a/x-pack/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx b/x-pack/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx index 76a3b06ef5574..c84f7cb97c959 100644 --- a/x-pack/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx +++ b/x-pack/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx @@ -19,7 +19,6 @@ import { EuiPageHeader, EuiSpacer, } from '@elastic/eui'; - import { APP_CREATE_TRANSFORM_CLUSTER_PRIVILEGES } from '../../../../common/constants'; import { TransformConfigUnion } from '../../../../common/types/transform'; @@ -32,8 +31,10 @@ import { breadcrumbService, docTitleService, BREADCRUMB_SECTION } from '../../se import { PrivilegesWrapper } from '../../lib/authorization'; import { Wizard } from '../create_transform/components/wizard'; +import { overrideTransformForCloning } from '../../common/transform'; type Props = RouteComponentProps<{ transformId: string }>; + export const CloneTransformSection: FC = ({ match, location }) => { const { indexPatternId }: Record = parse(location.search, { sort: false, @@ -82,7 +83,7 @@ export const CloneTransformSection: FC = ({ match, location }) => { setSavedObjectId(indexPatternId); - setTransformConfig(transformConfigs.transforms[0]); + setTransformConfig(overrideTransformForCloning(transformConfigs.transforms[0])); setErrorMessage(undefined); setIsInitialized(true); } catch (e) { diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_modal.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_modal.tsx index 2986fcce554e8..d5436d51c218b 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_modal.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_modal.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { FC, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EUI_MODAL_CONFIRM_BUTTON, @@ -15,8 +15,9 @@ import { EuiSpacer, EuiSwitch, } from '@elastic/eui'; - import { DeleteAction } from './use_delete_action'; +import { isManagedTransform } from '../../../../common/managed_transforms_utils'; +import { ManagedTransformsWarningCallout } from '../managed_transforms_callout/managed_transforms_callout'; export const DeleteActionModal: FC = ({ closeModal, @@ -31,6 +32,7 @@ export const DeleteActionModal: FC = ({ userCanDeleteIndex, userCanDeleteDataView, }) => { + const hasManagedTransforms = useMemo(() => items.some((t) => isManagedTransform(t)), [items]); const isBulkAction = items.length > 1; const bulkDeleteModalTitle = i18n.translate( @@ -47,6 +49,19 @@ export const DeleteActionModal: FC = ({ const bulkDeleteModalContent = ( <> + {hasManagedTransforms ? ( + <> + + + + ) : null} + { = ({ const deleteModalContent = ( <> + {hasManagedTransforms ? ( + <> + + + + ) : null} + {userCanDeleteIndex && ( { }; const openModal = (newItems: TransformListRow[]) => { - // EUI issue: Might trigger twice, one time as an array, - // one time as a single object. See https://github.com/elastic/eui/issues/3679 if (Array.isArray(newItems)) { setItems(newItems); setModalVisible(true); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_action_modal.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_action_modal.tsx index fb4fee986f6e0..7ad6897034e10 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_action_modal.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_action_modal.tsx @@ -8,7 +8,6 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiConfirmModal, EUI_MODAL_CONFIRM_BUTTON } from '@elastic/eui'; - import { StartAction } from './use_start_action'; export const StartActionModal: FC = ({ closeModal, items, startAndCloseModal }) => { diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/use_start_action.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/use_start_action.tsx index 2c45da45509e5..20910cf5fa0a5 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/use_start_action.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/use_start_action.tsx @@ -32,8 +32,6 @@ export const useStartAction = (forceDisable: boolean, transformNodes: number) => }; const openModal = (newItems: TransformListRow[]) => { - // EUI issue: Might trigger twice, one time as an array, - // one time as a single object. See https://github.com/elastic/eui/issues/3679 if (Array.isArray(newItems)) { setItems(newItems); setModalVisible(true); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/stop_action_modal.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/stop_action_modal.tsx new file mode 100644 index 0000000000000..ed53bb3b8d936 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/stop_action_modal.tsx @@ -0,0 +1,54 @@ +/* + * 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, { FC, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiConfirmModal, EUI_MODAL_CONFIRM_BUTTON } from '@elastic/eui'; +import { StopAction } from './use_stop_action'; +import { isManagedTransform } from '../../../../common/managed_transforms_utils'; +import { ManagedTransformsWarningCallout } from '../managed_transforms_callout/managed_transforms_callout'; + +export const StopActionModal: FC = ({ closeModal, items, stopAndCloseModal }) => { + const hasManagedTransforms = useMemo(() => items.some((t) => isManagedTransform(t)), [items]); + + const isBulkAction = items.length > 1; + + const bulkStopModalTitle = i18n.translate('xpack.transform.transformList.bulkStopModalTitle', { + defaultMessage: 'Stop {count} {count, plural, one {transform} other {transforms}}?', + values: { count: items && items.length }, + }); + const stopModalTitle = i18n.translate('xpack.transform.transformList.stopModalTitle', { + defaultMessage: 'Stop {transformId}?', + values: { transformId: items[0] && items[0].config.id }, + }); + + return ( + stopAndCloseModal(items)} + cancelButtonText={i18n.translate('xpack.transform.transformList.startModalCancelButton', { + defaultMessage: 'Cancel', + })} + confirmButtonText={i18n.translate('xpack.transform.transformList.startModalStopButton', { + defaultMessage: 'Stop', + })} + defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} + buttonColor="primary" + > + {hasManagedTransforms ? ( + + ) : null} + + ); +}; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/use_stop_action.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/use_stop_action.tsx index f498008bfd559..ac53ee83f6f65 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/use_stop_action.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/use_stop_action.tsx @@ -5,21 +5,39 @@ * 2.0. */ -import React, { useCallback, useContext, useMemo } from 'react'; - +import React, { useCallback, useContext, useMemo, useState } from 'react'; import { TRANSFORM_STATE } from '../../../../../../common/constants'; - import { AuthorizationContext } from '../../../../lib/authorization'; import { TransformListAction, TransformListRow } from '../../../../common'; import { useStopTransforms } from '../../../../hooks'; - import { isStopActionDisabled, stopActionNameText, StopActionName } from './stop_action_name'; +import { isManagedTransform } from '../../../../common/managed_transforms_utils'; export type StopAction = ReturnType; + export const useStopAction = (forceDisable: boolean) => { const { canStartStopTransform } = useContext(AuthorizationContext).capabilities; const stopTransforms = useStopTransforms(); + const [isModalVisible, setModalVisible] = useState(false); + const [items, setItems] = useState([]); + + const closeModal = () => setModalVisible(false); + + const openModal = (newItems: TransformListRow[]) => { + if (Array.isArray(newItems)) { + setItems(newItems); + setModalVisible(true); + } + }; + + const stopAndCloseModal = useCallback( + (transformSelection: TransformListRow[]) => { + setModalVisible(false); + stopTransforms(transformSelection.map((t) => ({ id: t.id, state: t.stats.state }))); + }, + [stopTransforms] + ); const clickHandler = useCallback( (i: TransformListRow) => stopTransforms([{ id: i.id, state: i.stats.state }]), @@ -37,11 +55,17 @@ export const useStopAction = (forceDisable: boolean) => { description: stopActionNameText, icon: 'stop', type: 'icon', - onClick: clickHandler, + onClick: (item: TransformListRow) => { + if (isManagedTransform(item)) { + openModal([item]); + } else { + clickHandler(item); + } + }, 'data-test-subj': 'transformActionStop', }), - [canStartStopTransform, forceDisable, clickHandler] + [canStartStopTransform, clickHandler, forceDisable] ); - return { action }; + return { action, closeModal, openModal, isModalVisible, items, stopAndCloseModal }; }; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx index 55225e0ff45c0..fd8360d02eca0 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx @@ -38,6 +38,8 @@ import { applyFormFieldsToTransformConfig, useEditTransformFlyout, } from './use_edit_transform_flyout'; +import { ManagedTransformsWarningCallout } from '../managed_transforms_callout/managed_transforms_callout'; +import { isManagedTransform } from '../../../../common/managed_transforms_utils'; interface EditTransformFlyoutProps { closeFlyout: () => void; @@ -99,6 +101,14 @@ export const EditTransformFlyout: FC = ({ + {isManagedTransform({ config }) ? ( + + ) : null} }> { + return ( + <> + + + + + + ); +}; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx index 8b7aaf1cf8fd2..cdf0c14409fdd 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx @@ -31,7 +31,6 @@ import { TransformListRow, TRANSFORM_LIST_COLUMN, } from '../../../../common'; -import { useStopTransforms } from '../../../../hooks'; import { AuthorizationContext } from '../../../../lib/authorization'; import { CreateTransformButton } from '../create_transform_button'; @@ -43,7 +42,7 @@ import { DeleteActionModal, } from '../action_delete'; import { useStartAction, StartActionName, StartActionModal } from '../action_start'; -import { StopActionName } from '../action_stop'; +import { StopActionName, useStopAction } from '../action_stop'; import { ItemIdToExpandedRowMap } from './common'; import { useColumns } from './use_columns'; @@ -52,6 +51,7 @@ import { transformFilters, filterTransforms } from './transform_search_bar_filte import { useTableSettings } from './use_table_settings'; import { useAlertRuleFlyout } from '../../../../../alerting/transform_alerting_flyout'; import { TransformHealthAlertRule } from '../../../../../../common/types/alerting'; +import { StopActionModal } from '../action_stop/stop_action_modal'; function getItemIdToExpandedRowMap( itemIds: TransformId[], @@ -92,9 +92,9 @@ export const TransformList: FC = ({ const [isActionsMenuOpen, setIsActionsMenuOpen] = useState(false); const bulkStartAction = useStartAction(false, transformNodes); const bulkDeleteAction = useDeleteAction(false); + const bulkStopAction = useStopAction(false); const [searchError, setSearchError] = useState(undefined); - const stopTransforms = useStopTransforms(); const { capabilities } = useContext(AuthorizationContext); const disabled = @@ -189,9 +189,9 @@ export const TransformList: FC = ({
,
- stopTransforms(transformSelection.map((t) => ({ id: t.id, state: t.stats.state }))) - } + onClick={() => { + bulkStopAction.openModal(transformSelection); + }} > @@ -283,6 +283,7 @@ export const TransformList: FC = ({ {/* Bulk Action Modals */} {bulkStartAction.isModalVisible && } {bulkDeleteAction.isModalVisible && } + {bulkStopAction.isModalVisible && } {/* Single Action Modals */} {singleActionModals} diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.tsx index 40b40cfa8c7ba..853c839a096fa 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.tsx @@ -19,6 +19,7 @@ import { useEditAction } from '../action_edit'; import { useStartAction, StartActionModal } from '../action_start'; import { useStopAction } from '../action_stop'; import { useCreateAlertRuleAction } from '../action_create_alert'; +import { StopActionModal } from '../action_stop/stop_action_modal'; export const useActions = ({ forceDisable, @@ -42,6 +43,8 @@ export const useActions = ({ modals: ( <> {startAction.isModalVisible && } + {stopAction.isModalVisible && } + {editAction.config && editAction.isFlyoutVisible && ( { - if (item.config?._meta?.managed !== true) return transformId; + if (!isManagedTransform(item)) return transformId; return ( <> {transformId}   - - {i18n.translate('xpack.transform.transformList.managedBadgeLabel', { - defaultMessage: 'Managed', + + > + + {i18n.translate('xpack.transform.transformList.managedBadgeLabel', { + defaultMessage: 'Managed', + })} + + ); },