diff --git a/x-pack/plugins/ml/public/data_frame/components/job_create/job_create_form.tsx b/x-pack/plugins/ml/public/data_frame/components/job_create/job_create_form.tsx index c88e73e03e6f..2460ae03170b 100644 --- a/x-pack/plugins/ml/public/data_frame/components/job_create/job_create_form.tsx +++ b/x-pack/plugins/ml/public/data_frame/components/job_create/job_create_form.tsx @@ -211,16 +211,19 @@ export const JobCreateForm: SFC = React.memo( return `PUT _data_frame/transforms/${jobId}\n${JSON.stringify(jobConfig, null, 2)}\n\n`; } - const ITEM_STYLE = { width: '300px' }; + // TODO move this to SASS + const FLEX_GROUP_STYLE = { height: '90px', maxWidth: '800px' }; + const FLEX_ITEM_STYLE = { width: '200px' }; + const PANEL_ITEM_STYLE = { width: '300px' }; return ( {!created && ( - - + + {i18n.translate('xpack.ml.dataframe.jobCreateForm.createAndStartDataFrameButton', { - defaultMessage: 'Create & start', + defaultMessage: 'Create and start', })} @@ -230,7 +233,7 @@ export const JobCreateForm: SFC = React.memo( 'xpack.ml.dataframe.jobCreateForm.createAndStartDataFrameDescription', { defaultMessage: - 'Create and start the data frame job. After the job is started, you will be offered options to continue exploring the data frame job.', + 'Creates and starts the data frame job. A data frame job will increase search and indexing load in your cluster. Please stop the job if excessive load is experienced. After the job is started, you will be offered options to continue exploring the data frame job.', } )} @@ -238,9 +241,9 @@ export const JobCreateForm: SFC = React.memo( )} {created && ( - - - + + + {i18n.translate('xpack.ml.dataframe.jobCreateForm.startDataFrameButton', { defaultMessage: 'Start', })} @@ -250,14 +253,14 @@ export const JobCreateForm: SFC = React.memo( {i18n.translate('xpack.ml.dataframe.jobCreateForm.startDataFrameDescription', { defaultMessage: - 'Starts the data frame job. After the job is started, you will be offered options to continue exploring the data frame job.', + 'Starts the data frame job. A data frame job will increase search and indexing load in your cluster. Please stop the job if excessive load is experienced. After the job is started, you will be offered options to continue exploring the data frame job.', })} )} - - + + {i18n.translate('xpack.ml.dataframe.jobCreateForm.createDataFrameButton', { defaultMessage: 'Create', @@ -273,8 +276,8 @@ export const JobCreateForm: SFC = React.memo( - - + + {(copy: () => void) => ( @@ -324,7 +327,7 @@ export const JobCreateForm: SFC = React.memo( - + } title={i18n.translate('xpack.ml.dataframe.jobCreateForm.jobsListCardTitle', { @@ -340,7 +343,7 @@ export const JobCreateForm: SFC = React.memo( /> {started === true && createIndexPattern === true && indexPatternId === undefined && ( - + @@ -357,7 +360,7 @@ export const JobCreateForm: SFC = React.memo( )} {started === true && indexPatternId !== undefined && ( - + } title={i18n.translate('xpack.ml.dataframe.jobCreateForm.discoverCardTitle', { diff --git a/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/__snapshots__/actions.test.tsx.snap b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/__snapshots__/action_delete.test.tsx.snap similarity index 100% rename from x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/__snapshots__/actions.test.tsx.snap rename to x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/__snapshots__/action_delete.test.tsx.snap diff --git a/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/__snapshots__/action_start.test.tsx.snap b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/__snapshots__/action_start.test.tsx.snap new file mode 100644 index 000000000000..bdd95fd1d4a7 --- /dev/null +++ b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/__snapshots__/action_start.test.tsx.snap @@ -0,0 +1,24 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Data Frame: Job List Actions Minimal initialization 1`] = ` + + + + Start + + + +`; diff --git a/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/action_delete.test.tsx b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/action_delete.test.tsx new file mode 100644 index 000000000000..5496f5eedb1d --- /dev/null +++ b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/action_delete.test.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { shallow } from 'enzyme'; +import React from 'react'; + +import { DataFrameJobListRow } from './common'; +import { DeleteAction } from './action_delete'; + +import dataFrameJobListRow from './__mocks__/data_frame_job_list_row.json'; + +describe('Data Frame: Job List Actions ', () => { + test('Minimal initialization', () => { + const item: DataFrameJobListRow = dataFrameJobListRow; + const props = { + disabled: false, + item, + deleteJob(d: DataFrameJobListRow) {}, + }; + + const wrapper = shallow(); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/action_delete.tsx b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/action_delete.tsx new file mode 100644 index 000000000000..0c0da522cf43 --- /dev/null +++ b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/action_delete.tsx @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, SFC, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiButtonEmpty, + EuiConfirmModal, + EuiOverlayMask, + EuiToolTip, + EUI_MODAL_CONFIRM_BUTTON, +} from '@elastic/eui'; + +import { + checkPermission, + createPermissionFailureMessage, +} from '../../../../../privilege/check_privilege'; + +import { DataFrameJobListRow, DATA_FRAME_RUNNING_STATE } from './common'; + +interface DeleteActionProps { + item: DataFrameJobListRow; + deleteJob(d: DataFrameJobListRow): void; +} + +export const DeleteAction: SFC = ({ deleteJob, item }) => { + const disabled = item.state.task_state === DATA_FRAME_RUNNING_STATE.STARTED; + + const canDeleteDataFrameJob: boolean = checkPermission('canDeleteDataFrameJob'); + + const [isModalVisible, setModalVisible] = useState(false); + + const closeModal = () => setModalVisible(false); + const deleteAndCloseModal = () => { + setModalVisible(false); + deleteJob(item); + }; + const openModal = () => setModalVisible(true); + + const buttonDeleteText = i18n.translate('xpack.ml.dataframe.jobsList.deleteActionName', { + defaultMessage: 'Delete', + }); + + let deleteButton = ( + + {buttonDeleteText} + + ); + + if (disabled || !canDeleteDataFrameJob) { + deleteButton = ( + + {deleteButton} + + ); + } + + return ( + + {deleteButton} + {isModalVisible && ( + + +

+ {i18n.translate('xpack.ml.dataframe.jobsList.deleteModalBody', { + defaultMessage: 'Are you sure you want to delete this job?', + })} +

+
+
+ )} +
+ ); +}; diff --git a/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/action_start.test.tsx b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/action_start.test.tsx new file mode 100644 index 000000000000..ed527b234ada --- /dev/null +++ b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/action_start.test.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { shallow } from 'enzyme'; +import React from 'react'; + +import { DataFrameJobListRow } from './common'; +import { StartAction } from './action_start'; + +import dataFrameJobListRow from './__mocks__/data_frame_job_list_row.json'; + +describe('Data Frame: Job List Actions ', () => { + test('Minimal initialization', () => { + const item: DataFrameJobListRow = dataFrameJobListRow; + const props = { + disabled: false, + item, + startJob(d: DataFrameJobListRow) {}, + }; + + const wrapper = shallow(); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/action_start.tsx b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/action_start.tsx new file mode 100644 index 000000000000..9f61bbae9fdc --- /dev/null +++ b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/action_start.tsx @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, SFC, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiButtonEmpty, + EuiConfirmModal, + EuiOverlayMask, + EuiToolTip, + EUI_MODAL_CONFIRM_BUTTON, +} from '@elastic/eui'; + +import { + checkPermission, + createPermissionFailureMessage, +} from '../../../../../privilege/check_privilege'; + +import { DataFrameJobListRow, isCompletedBatchJob } from './common'; + +interface StartActionProps { + item: DataFrameJobListRow; + startJob(d: DataFrameJobListRow): void; +} + +export const StartAction: SFC = ({ startJob, item }) => { + const canStartStopDataFrameJob: boolean = checkPermission('canStartStopDataFrameJob'); + + const [isModalVisible, setModalVisible] = useState(false); + + const closeModal = () => setModalVisible(false); + const startAndCloseModal = () => { + setModalVisible(false); + startJob(item); + }; + const openModal = () => setModalVisible(true); + + const buttonStartText = i18n.translate('xpack.ml.dataframe.jobsList.startActionName', { + defaultMessage: 'Start', + }); + + // Disable start for batch jobs which have completed. + const completedBatchJob = isCompletedBatchJob(item); + + let startButton = ( + + {buttonStartText} + + ); + + if (!canStartStopDataFrameJob || completedBatchJob) { + startButton = ( + + {startButton} + + ); + } + + return ( + + {startButton} + {isModalVisible && ( + + +

+ {i18n.translate('xpack.ml.dataframe.jobsList.startModalBody', { + defaultMessage: + 'A data frame job will increase search and indexing load in your cluster. Please stop the job if excessive load is experienced. Are you sure you want to start this job?', + })} +

+
+
+ )} +
+ ); +}; diff --git a/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/actions.test.tsx b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/actions.test.tsx index a0991aa8fdd0..53a70593b31e 100644 --- a/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/actions.test.tsx +++ b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/actions.test.tsx @@ -4,28 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { shallow } from 'enzyme'; -import React from 'react'; - -import { DataFrameJobListRow } from './common'; -import { DeleteAction, getActions } from './actions'; - -import dataFrameJobListRow from './__mocks__/data_frame_job_list_row.json'; - -describe('Data Frame: Job List Actions ', () => { - test('Minimal initialization', () => { - const item: DataFrameJobListRow = dataFrameJobListRow; - const props = { - disabled: false, - item, - deleteJob(d: DataFrameJobListRow) {}, - }; - - const wrapper = shallow(); - - expect(wrapper).toMatchSnapshot(); - }); -}); +import { getActions } from './actions'; describe('Data Frame: Job List Actions', () => { test('getActions()', () => { diff --git a/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/actions.tsx b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/actions.tsx index ae999360b1c0..465bbc40b40a 100644 --- a/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/actions.tsx +++ b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/actions.tsx @@ -4,15 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, SFC, useState } from 'react'; +import React from 'react'; import { i18n } from '@kbn/i18n'; -import { - EuiButtonEmpty, - EuiConfirmModal, - EuiOverlayMask, - EuiToolTip, - EUI_MODAL_CONFIRM_BUTTON, -} from '@elastic/eui'; +import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; import { checkPermission, @@ -22,96 +16,8 @@ import { import { DataFrameJobListRow, DATA_FRAME_RUNNING_STATE } from './common'; import { deleteJobFactory, startJobFactory, stopJobFactory } from './job_service'; -interface DeleteActionProps { - disabled: boolean; - item: DataFrameJobListRow; - deleteJob(d: DataFrameJobListRow): void; -} - -export const DeleteAction: SFC = ({ deleteJob, disabled, item }) => { - const canDeleteDataFrameJob: boolean = checkPermission('canDeleteDataFrameJob'); - - const [isModalVisible, setModalVisible] = useState(false); - - const closeModal = () => setModalVisible(false); - const deleteAndCloseModal = () => { - setModalVisible(false); - deleteJob(item); - }; - const openModal = () => setModalVisible(true); - - const buttonDeleteText = i18n.translate('xpack.ml.dataframe.jobsList.deleteActionName', { - defaultMessage: 'Delete', - }); - - let deleteButton = ( - - {buttonDeleteText} - - ); - - if (disabled || !canDeleteDataFrameJob) { - deleteButton = ( - - {deleteButton} - - ); - } - - return ( - - {deleteButton} - {isModalVisible && ( - - -

- {i18n.translate('xpack.ml.dataframe.jobsList.deleteModalBody', { - defaultMessage: 'Are you sure you want to delete this job?', - })} -

-
-
- )} -
- ); -}; +import { StartAction } from './action_start'; +import { DeleteAction } from './action_delete'; export const getActions = (getJobs: () => void) => { const canStartStopDataFrameJob: boolean = checkPermission('canStartStopDataFrameJob'); @@ -125,35 +31,7 @@ export const getActions = (getJobs: () => void) => { isPrimary: true, render: (item: DataFrameJobListRow) => { if (item.state.task_state !== DATA_FRAME_RUNNING_STATE.STARTED) { - const buttonStartText = i18n.translate('xpack.ml.dataframe.jobsList.startActionName', { - defaultMessage: 'Start', - }); - - const startButton = ( - startJob(item)} - aria-label={buttonStartText} - > - {buttonStartText} - - ); - - if (!canStartStopDataFrameJob) { - return ( - - {startButton} - - ); - } - - return startButton; + return ; } const buttonStopText = i18n.translate('xpack.ml.dataframe.jobsList.stopActionName', { @@ -188,13 +66,7 @@ export const getActions = (getJobs: () => void) => { }, { render: (item: DataFrameJobListRow) => { - return ( - - ); + return ; }, }, ]; diff --git a/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/common.test.ts b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/common.test.ts new file mode 100644 index 000000000000..ae903746fdfc --- /dev/null +++ b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/common.test.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import mockDataFrameJobListRow from './__mocks__/data_frame_job_list_row.json'; + +import { DATA_FRAME_RUNNING_STATE, isCompletedBatchJob } from './common'; + +describe('Data Frame: isCompletedBatchJob()', () => { + test('isCompletedBatchJob()', () => { + // check the job config/state against the conditions + // that will be used by isCompletedBatchJob() + // followed by a call to isCompletedBatchJob() itself + expect(mockDataFrameJobListRow.state.checkpoint === 1).toBe(true); + expect(mockDataFrameJobListRow.sync === undefined).toBe(true); + expect(mockDataFrameJobListRow.state.task_state === DATA_FRAME_RUNNING_STATE.STOPPED).toBe( + true + ); + expect(isCompletedBatchJob(mockDataFrameJobListRow)).toBe(true); + + // adapt the mock config to resemble a non-completed job. + mockDataFrameJobListRow.state.checkpoint = 0; + expect(isCompletedBatchJob(mockDataFrameJobListRow)).toBe(false); + }); +}); diff --git a/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/common.ts b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/common.ts index 2ae0af0e9d81..8195ee374383 100644 --- a/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/common.ts +++ b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/common.ts @@ -12,6 +12,7 @@ export interface DataFrameJob { dest: string; id: JobId; source: string; + sync?: object; } export enum DATA_FRAME_RUNNING_STATE { @@ -63,3 +64,13 @@ export enum DataFrameJobListColumn { } export type ItemIdToExpandedRowMap = Dictionary; + +export function isCompletedBatchJob(item: DataFrameJobListRow) { + // If `checkpoint=1`, `sync` is missing from the config and state is stopped, + // then this is a completed batch data frame job. + return ( + item.state.checkpoint === 1 && + item.config.sync === undefined && + item.state.task_state === DATA_FRAME_RUNNING_STATE.STOPPED + ); +} diff --git a/x-pack/plugins/ml/public/services/ml_api_service/data_frame.js b/x-pack/plugins/ml/public/services/ml_api_service/data_frame.js index 0fb2d7faa362..c2bb802e1eb6 100644 --- a/x-pack/plugins/ml/public/services/ml_api_service/data_frame.js +++ b/x-pack/plugins/ml/public/services/ml_api_service/data_frame.js @@ -60,7 +60,7 @@ export const dataFrame = { }, stopDataFrameTransformsJob(jobId) { return http({ - url: `${basePath}/_data_frame/transforms/${jobId}/_stop`, + url: `${basePath}/_data_frame/transforms/${jobId}/_stop?force=true`, method: 'POST', }); }, diff --git a/x-pack/plugins/ml/server/client/elasticsearch_ml.js b/x-pack/plugins/ml/server/client/elasticsearch_ml.js index d7c2b06e1561..4a25036e0ad0 100644 --- a/x-pack/plugins/ml/server/client/elasticsearch_ml.js +++ b/x-pack/plugins/ml/server/client/elasticsearch_ml.js @@ -187,10 +187,13 @@ export const elasticsearchJsPlugin = (Client, config, components) => { ml.stopDataFrameTransformsJob = ca({ urls: [ { - fmt: '/_data_frame/transforms/<%=jobId%>/_stop', + fmt: '/_data_frame/transforms/<%=jobId%>/_stop?&force=<%=force%>', req: { jobId: { type: 'string' + }, + force: { + type: 'boolean' } } } diff --git a/x-pack/plugins/ml/server/routes/data_frame.js b/x-pack/plugins/ml/server/routes/data_frame.js index ae4d0ee17ee6..336e386bb728 100644 --- a/x-pack/plugins/ml/server/routes/data_frame.js +++ b/x-pack/plugins/ml/server/routes/data_frame.js @@ -109,8 +109,14 @@ export function dataFrameRoutes(server, commonRouteConfig) { path: '/api/ml/_data_frame/transforms/{jobId}/_stop', handler(request) { const callWithRequest = callWithRequestFactory(server, request); - const { jobId } = request.params; - return callWithRequest('ml.stopDataFrameTransformsJob', { jobId }) + const options = { + jobId: request.params.jobId + }; + const force = request.query.force; + if (force !== undefined) { + options.force = force; + } + return callWithRequest('ml.stopDataFrameTransformsJob', options) .catch(resp => wrapError(resp)); }, config: {