[ML] Data Frames: Fixes start/stop/delete for failed transforms. (#40532) (#40632)

- Fixes handling of failed transforms according to #40298 (comment). This adapts the API endpoints to allow force/wait_for_completion flags where applicable.
- Renamed DATA_FRAME_RUNNING_STATE to DATA_FRAME_TASK_STATE to better reflect the API naming.
- #40129 introduced an observable to refresh the transform list. The transform list actions now make use of this too and no longer require getJobs() to get passed around as a deeply nested prop.
- The state handling for overall loading and errors for the transform list is fixed/improved by this PR.
- On initial load, the list no longer shows No data frame transforms found, only the loading indicator.
- If loading the transform list data fails, the list gets replaced by an error-callout to avoid displaying out-of-date data and access to actions which might not work anymore.
- Fixes a bug where the transform list would no longer pick up refresh triggers after the request returned an error.
- Failed state in in the transform list uses now the danger color for the badge and adds a tooltip with the text provided in the reason field of the transform's stats.
- Fixes a regression where the messages pane would no longer load.
This commit is contained in:
Walter Rafelsberger 2019-07-09 17:51:59 +02:00 committed by GitHub
parent d64dbb2945
commit e28c79901d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 262 additions and 154 deletions

View file

@ -1,26 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Data Frame: Job List <DataFrameJobList /> Minimal initialization 1`] = `
<EuiEmptyPrompt
actions={
Array [
<EuiButtonEmpty
color="primary"
iconSide="left"
isDisabled={true}
onClick={[Function]}
type="button"
>
Create your first data frame transform
</EuiButtonEmpty>,
]
}
data-test-subj="mlNoDataFrameJobsFound"
iconColor="subdued"
title={
<h2>
No data frame transforms found
</h2>
}
<ProgressBar
isLoading={false}
/>
`;

View file

@ -14,20 +14,21 @@ import {
EUI_MODAL_CONFIRM_BUTTON,
} from '@elastic/eui';
import { deleteJob } from './job_service';
import {
checkPermission,
createPermissionFailureMessage,
} from '../../../../../privilege/check_privilege';
import { DataFrameJobListRow, DATA_FRAME_RUNNING_STATE } from './common';
import { DataFrameJobListRow, DATA_FRAME_TASK_STATE } from './common';
interface DeleteActionProps {
item: DataFrameJobListRow;
deleteJob(d: DataFrameJobListRow): void;
}
export const DeleteAction: SFC<DeleteActionProps> = ({ deleteJob, item }) => {
const disabled = item.state.task_state === DATA_FRAME_RUNNING_STATE.STARTED;
export const DeleteAction: SFC<DeleteActionProps> = ({ item }) => {
const disabled = item.state.task_state === DATA_FRAME_TASK_STATE.STARTED;
const canDeleteDataFrameJob: boolean = checkPermission('canDeleteDataFrameJob');

View file

@ -14,6 +14,8 @@ import {
EUI_MODAL_CONFIRM_BUTTON,
} from '@elastic/eui';
import { startJob } from './job_service';
import {
checkPermission,
createPermissionFailureMessage,
@ -23,10 +25,9 @@ import { DataFrameJobListRow, isCompletedBatchJob } from './common';
interface StartActionProps {
item: DataFrameJobListRow;
startJob(d: DataFrameJobListRow): void;
}
export const StartAction: SFC<StartActionProps> = ({ startJob, item }) => {
export const StartAction: SFC<StartActionProps> = ({ item }) => {
const canStartStopDataFrameJob: boolean = checkPermission('canStartStopDataFrameJob');
const [isModalVisible, setModalVisible] = useState(false);

View file

@ -8,7 +8,7 @@ import { getActions } from './actions';
describe('Data Frame: Job List Actions', () => {
test('getActions()', () => {
const actions = getActions(() => {});
const actions = getActions();
expect(actions).toHaveLength(2);
expect(actions[0].isPrimary).toBeTruthy();

View file

@ -13,25 +13,21 @@ import {
createPermissionFailureMessage,
} from '../../../../../privilege/check_privilege';
import { DataFrameJobListRow, DATA_FRAME_RUNNING_STATE } from './common';
import { deleteJobFactory, startJobFactory, stopJobFactory } from './job_service';
import { DataFrameJobListRow, DATA_FRAME_TASK_STATE } from './common';
import { stopJob } from './job_service';
import { StartAction } from './action_start';
import { DeleteAction } from './action_delete';
export const getActions = (getJobs: () => void) => {
export const getActions = () => {
const canStartStopDataFrameJob: boolean = checkPermission('canStartStopDataFrameJob');
const deleteJob = deleteJobFactory(getJobs);
const startJob = startJobFactory(getJobs);
const stopJob = stopJobFactory(getJobs);
return [
{
isPrimary: true,
render: (item: DataFrameJobListRow) => {
if (item.state.task_state !== DATA_FRAME_RUNNING_STATE.STARTED) {
return <StartAction startJob={startJob} item={item} />;
if (item.state.task_state !== DATA_FRAME_TASK_STATE.STARTED) {
return <StartAction item={item} />;
}
const buttonStopText = i18n.translate('xpack.ml.dataframe.jobsList.stopActionName', {
@ -66,7 +62,7 @@ export const getActions = (getJobs: () => void) => {
},
{
render: (item: DataFrameJobListRow) => {
return <DeleteAction deleteJob={deleteJob} item={item} />;
return <DeleteAction item={item} />;
},
},
];

View file

@ -8,7 +8,7 @@ import { getColumns } from './columns';
describe('Data Frame: Job List Columns', () => {
test('getColumns()', () => {
const columns = getColumns(() => {}, [], () => {});
const columns = getColumns([], () => {});
expect(columns).toHaveLength(9);
expect(columns[0].isExpander).toBeTruthy();

View file

@ -13,19 +13,25 @@ import {
EuiFlexItem,
EuiProgress,
EuiText,
EuiToolTip,
RIGHT_ALIGNMENT,
} from '@elastic/eui';
import { JobId } from '../../../../common';
import { DataFrameJobListColumn, DataFrameJobListRow } from './common';
import { DATA_FRAME_TASK_STATE, DataFrameJobListColumn, DataFrameJobListRow } from './common';
import { getActions } from './actions';
enum TASK_STATE_COLOR {
failed = 'danger',
started = 'primary',
stopped = 'hollow',
}
export const getColumns = (
getJobs: () => void,
expandedRowItemIds: JobId[],
setExpandedRowItemIds: React.Dispatch<React.SetStateAction<JobId[]>>
) => {
const actions = getActions(getJobs);
const actions = getActions();
function toggleDetails(item: DataFrameJobListRow) {
const index = expandedRowItemIds.indexOf(item.config.id);
@ -94,7 +100,16 @@ export const getColumns = (
sortable: (item: DataFrameJobListRow) => item.state.task_state,
truncateText: true,
render(item: DataFrameJobListRow) {
const color = item.state.task_state === 'started' ? 'primary' : 'hollow';
const color = TASK_STATE_COLOR[item.state.task_state];
if (item.state.task_state === DATA_FRAME_TASK_STATE.FAILED) {
return (
<EuiToolTip content={item.state.reason}>
<EuiBadge color={color}>{item.state.task_state}</EuiBadge>
</EuiToolTip>
);
}
return <EuiBadge color={color}>{item.state.task_state}</EuiBadge>;
},
width: '100px',
@ -142,8 +157,10 @@ export const getColumns = (
{!isBatchTransform && (
<Fragment>
<EuiFlexItem style={{ width: '40px' }} grow={false}>
{item.state.task_state === 'started' && <EuiProgress color="primary" size="m" />}
{item.state.task_state !== 'started' && (
{item.state.task_state === DATA_FRAME_TASK_STATE.STARTED && (
<EuiProgress color="primary" size="m" />
)}
{item.state.task_state !== DATA_FRAME_TASK_STATE.STOPPED && (
<EuiProgress value={0} max={100} color="primary" size="m" />
)}
</EuiFlexItem>

View file

@ -6,7 +6,7 @@
import mockDataFrameJobListRow from './__mocks__/data_frame_job_list_row.json';
import { DATA_FRAME_RUNNING_STATE, isCompletedBatchJob } from './common';
import { DATA_FRAME_TASK_STATE, isCompletedBatchJob } from './common';
describe('Data Frame: isCompletedBatchJob()', () => {
test('isCompletedBatchJob()', () => {
@ -15,9 +15,7 @@ describe('Data Frame: 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(mockDataFrameJobListRow.state.task_state === DATA_FRAME_TASK_STATE.STOPPED).toBe(true);
expect(isCompletedBatchJob(mockDataFrameJobListRow)).toBe(true);
// adapt the mock config to resemble a non-completed job.

View file

@ -8,26 +8,27 @@ import { Dictionary } from '../../../../../../common/types/common';
import { JobId, DataFrameTransformWithId } from '../../../../common';
export enum DATA_FRAME_RUNNING_STATE {
export enum DATA_FRAME_TASK_STATE {
FAILED = 'failed',
STARTED = 'started',
STOPPED = 'stopped',
}
type RunningState = DATA_FRAME_RUNNING_STATE.STARTED | DATA_FRAME_RUNNING_STATE.STOPPED;
export interface DataFrameJobState {
checkpoint: number;
current_position: Dictionary<any>;
// indexer_state is a backend internal attribute
// and should not be considered in the UI.
indexer_state: RunningState;
indexer_state: DATA_FRAME_TASK_STATE;
progress?: {
docs_remaining: number;
percent_complete: number;
total_docs: number;
};
reason?: string;
// task_state is the attribute to check against if a job
// is running or not.
task_state: RunningState;
task_state: DATA_FRAME_TASK_STATE;
}
export interface DataFrameJobStats {
@ -67,6 +68,6 @@ export function isCompletedBatchJob(item: DataFrameJobListRow) {
return (
item.state.checkpoint === 1 &&
item.config.sync === undefined &&
item.state.task_state === DATA_FRAME_RUNNING_STATE.STOPPED
item.state.task_state === DATA_FRAME_TASK_STATE.STOPPED
);
}

View file

@ -4,11 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { SFC, useState } from 'react';
import React, { Fragment, SFC, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiButtonEmpty, EuiEmptyPrompt, SortDirection } from '@elastic/eui';
import { EuiButtonEmpty, EuiCallOut, EuiEmptyPrompt, SortDirection } from '@elastic/eui';
import { JobId, moveToDataFrameWizard, useRefreshTransformList } from '../../../../common';
import { checkPermission } from '../../../../../privilege/check_privilege';
@ -17,7 +17,7 @@ import { DataFrameJobListColumn, DataFrameJobListRow, ItemIdToExpandedRowMap } f
import { getJobsFactory } from './job_service';
import { getColumns } from './columns';
import { ExpandedRow } from './expanded_row';
import { TransformTable } from './transform_table';
import { ProgressBar, TransformTable } from './transform_table';
import { useRefreshInterval } from './use_refresh_interval';
function getItemIdToExpandedRowMap(
@ -37,47 +37,81 @@ function getItemIdToExpandedRowMap(
}
export const DataFrameJobList: SFC = () => {
const [dataFrameJobs, setDataFrameJobs] = useState<DataFrameJobListRow[]>([]);
const [isInitialized, setIsInitialized] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [blockRefresh, setBlockRefresh] = useState(false);
const [dataFrameJobs, setDataFrameJobs] = useState<DataFrameJobListRow[]>([]);
const [expandedRowItemIds, setExpandedRowItemIds] = useState<JobId[]>([]);
const [errorMessage, setErrorMessage] = useState<any>(undefined);
const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(10);
const [sortField, setSortField] = useState<string>(DataFrameJobListColumn.id);
const [sortDirection, setSortDirection] = useState<string>(SortDirection.ASC);
const disabled =
!checkPermission('canCreateDataFrameJob') ||
!checkPermission('canPreviewDataFrameJob') ||
!checkPermission('canStartStopDataFrameJob');
const getJobs = getJobsFactory(setDataFrameJobs, blockRefresh);
const getJobs = getJobsFactory(setDataFrameJobs, setErrorMessage, setIsInitialized, blockRefresh);
// Subscribe to the refresh observable to trigger reloading the jobs list.
useRefreshTransformList({ onRefresh: () => getJobs(true) });
useRefreshTransformList({ isLoading: setIsLoading, onRefresh: () => getJobs(true) });
// Call useRefreshInterval() after the subscription above is set up.
useRefreshInterval(setBlockRefresh);
if (dataFrameJobs.length === 0) {
// Before the jobs have been loaded for the first time, display the loading indicator only.
// Otherwise a user would see 'No data frame transforms found' during the initial loading.
if (!isInitialized) {
return <ProgressBar isLoading={isLoading} />;
}
if (typeof errorMessage !== 'undefined') {
return (
<EuiEmptyPrompt
title={
<h2>
{i18n.translate('xpack.ml.dataFrame.list.emptyPromptTitle', {
defaultMessage: 'No data frame transforms found',
})}
</h2>
}
actions={[
<EuiButtonEmpty onClick={moveToDataFrameWizard} isDisabled={disabled}>
{i18n.translate('xpack.ml.dataFrame.list.emptyPromptButtonText', {
defaultMessage: 'Create your first data frame transform',
})}
</EuiButtonEmpty>,
]}
data-test-subj="mlNoDataFrameJobsFound"
/>
<Fragment>
<ProgressBar isLoading={isLoading} />
<EuiCallOut
title={i18n.translate('xpack.ml.dataFrame.list.errorPromptTitle', {
defaultMessage: 'An error occurred getting the data frame transform list.',
})}
color="danger"
iconType="alert"
>
<pre>{JSON.stringify(errorMessage)}</pre>
</EuiCallOut>
</Fragment>
);
}
const columns = getColumns(getJobs, expandedRowItemIds, setExpandedRowItemIds);
if (dataFrameJobs.length === 0) {
return (
<Fragment>
<ProgressBar isLoading={isLoading} />
<EuiEmptyPrompt
title={
<h2>
{i18n.translate('xpack.ml.dataFrame.list.emptyPromptTitle', {
defaultMessage: 'No data frame transforms found',
})}
</h2>
}
actions={[
<EuiButtonEmpty onClick={moveToDataFrameWizard} isDisabled={disabled}>
{i18n.translate('xpack.ml.dataFrame.list.emptyPromptButtonText', {
defaultMessage: 'Create your first data frame transform',
})}
</EuiButtonEmpty>,
]}
data-test-subj="mlNoDataFrameJobsFound"
/>
</Fragment>
);
}
const columns = getColumns(expandedRowItemIds, setExpandedRowItemIds);
const sorting = {
sort: {
@ -113,19 +147,22 @@ export const DataFrameJobList: SFC = () => {
};
return (
<TransformTable
className="mlTransformTable"
columns={columns}
hasActions={false}
isExpandable={true}
isSelectable={false}
items={dataFrameJobs}
itemId={DataFrameJobListColumn.id}
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
onChange={onTableChange}
pagination={pagination}
sorting={sorting}
data-test-subj="mlDataFramesTableJobs"
/>
<Fragment>
<ProgressBar isLoading={isLoading} />
<TransformTable
className="mlTransformTable"
columns={columns}
hasActions={false}
isExpandable={true}
isSelectable={false}
items={dataFrameJobs}
itemId={DataFrameJobListColumn.id}
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
onChange={onTableChange}
pagination={pagination}
sorting={sorting}
data-test-subj="mlDataFramesTableJobs"
/>
</Fragment>
);
};

View file

@ -8,14 +8,20 @@ import { i18n } from '@kbn/i18n';
import { toastNotifications } from 'ui/notify';
import { ml } from '../../../../../../services/ml_api_service';
import { DataFrameJobListRow } from '../common';
import { refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../../../../../common';
import { GetJobs } from './get_jobs';
import { DATA_FRAME_TASK_STATE, DataFrameJobListRow } from '../common';
export const deleteJobFactory = (getJobs: GetJobs) => async (d: DataFrameJobListRow) => {
export const deleteJob = async (d: DataFrameJobListRow) => {
try {
if (d.state.task_state === DATA_FRAME_TASK_STATE.FAILED) {
await ml.dataFrame.stopDataFrameTransformsJob(
d.config.id,
d.state.task_state === DATA_FRAME_TASK_STATE.FAILED,
true
);
}
await ml.dataFrame.deleteDataFrameTransformsJob(d.config.id);
getJobs(true);
toastNotifications.addSuccess(
i18n.translate('xpack.ml.dataframe.jobsList.deleteJobSuccessMessage', {
defaultMessage: 'Data frame transform {jobId} deleted successfully.',
@ -30,4 +36,5 @@ export const deleteJobFactory = (getJobs: GetJobs) => async (d: DataFrameJobList
})
);
}
refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH);
};

View file

@ -4,8 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import { toastNotifications } from 'ui/notify';
import { ml } from '../../../../../../services/ml_api_service';
import {
DataFrameTransformWithId,
@ -28,35 +26,62 @@ interface GetDataFrameTransformsResponse {
transforms: DataFrameTransformWithId[];
}
interface GetDataFrameTransformsStatsResponse {
interface GetDataFrameTransformsStatsResponseOk {
node_failures?: object;
count: number;
transforms: DataFrameJobStateStats[];
}
const isGetDataFrameTransformsStatsResponseOk = (
arg: any
): arg is GetDataFrameTransformsStatsResponseOk => {
return (
{}.hasOwnProperty.call(arg, 'count') &&
{}.hasOwnProperty.call(arg, 'transforms') &&
Array.isArray(arg.transforms)
);
};
interface GetDataFrameTransformsStatsResponseError {
statusCode: number;
error: string;
message: string;
}
type GetDataFrameTransformsStatsResponse =
| GetDataFrameTransformsStatsResponseOk
| GetDataFrameTransformsStatsResponseError;
export type GetJobs = (forceRefresh?: boolean) => void;
export const getJobsFactory = (
setDataFrameJobs: React.Dispatch<React.SetStateAction<DataFrameJobListRow[]>>,
setErrorMessage: React.Dispatch<
React.SetStateAction<GetDataFrameTransformsStatsResponseError | undefined>
>,
setIsInitialized: React.Dispatch<React.SetStateAction<boolean>>,
blockRefresh: boolean
): GetJobs => {
let concurrentLoads = 0;
const getJobs = async (forceRefresh = false) => {
if (forceRefresh === true || blockRefresh === false) {
refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.LOADING);
concurrentLoads++;
if (concurrentLoads > 1) {
return;
}
try {
refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.LOADING);
concurrentLoads++;
if (concurrentLoads > 1) {
return;
}
const jobConfigs: GetDataFrameTransformsResponse = await ml.dataFrame.getDataFrameTransforms();
const jobStats: GetDataFrameTransformsStatsResponse = await ml.dataFrame.getDataFrameTransformsStats();
const tableRows = jobConfigs.transforms.reduce(
(reducedtableRows, config) => {
const stats = jobStats.transforms.find(d => config.id === d.id);
const stats = isGetDataFrameTransformsStatsResponseOk(jobStats)
? jobStats.transforms.find(d => config.id === d.id)
: undefined;
// A newly created job might not have corresponding stats yet.
// If that's the case we just skip the job and don't add it to the jobs list yet.
@ -77,22 +102,24 @@ export const getJobsFactory = (
);
setDataFrameJobs(tableRows);
concurrentLoads--;
if (concurrentLoads > 0) {
concurrentLoads = 0;
getJobs(true);
return;
}
setErrorMessage(undefined);
setIsInitialized(true);
refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.IDLE);
} catch (e) {
// An error is followed immediately by setting the state to idle.
// This way we're able to treat ERROR as a one-time-event like REFRESH.
refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.ERROR);
toastNotifications.addDanger(
i18n.translate('xpack.ml.dataframe.jobsList.errorGettingDataFrameJobsList', {
defaultMessage: 'An error occurred getting the data frame jobs list: {error}',
values: { error: JSON.stringify(e) },
})
);
refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.IDLE);
setDataFrameJobs([]);
setErrorMessage(e);
setIsInitialized(true);
}
concurrentLoads--;
if (concurrentLoads > 0) {
concurrentLoads = 0;
getJobs(true);
return;
}
}
};

View file

@ -5,6 +5,6 @@
*/
export { getJobsFactory } from './get_jobs';
export { deleteJobFactory } from './delete_job';
export { startJobFactory } from './start_job';
export { stopJobFactory } from './stop_job';
export { deleteJob } from './delete_job';
export { startJob } from './start_job';
export { stopJob } from './stop_job';

View file

@ -8,20 +8,22 @@ import { i18n } from '@kbn/i18n';
import { toastNotifications } from 'ui/notify';
import { ml } from '../../../../../../services/ml_api_service';
import { DataFrameJobListRow } from '../common';
import { refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../../../../../common';
import { GetJobs } from './get_jobs';
import { DATA_FRAME_TASK_STATE, DataFrameJobListRow } from '../common';
export const startJobFactory = (getJobs: GetJobs) => async (d: DataFrameJobListRow) => {
export const startJob = async (d: DataFrameJobListRow) => {
try {
await ml.dataFrame.startDataFrameTransformsJob(d.config.id);
await ml.dataFrame.startDataFrameTransformsJob(
d.config.id,
d.state.task_state === DATA_FRAME_TASK_STATE.FAILED
);
toastNotifications.addSuccess(
i18n.translate('xpack.ml.dataframe.jobsList.startJobSuccessMessage', {
defaultMessage: 'Data frame transform {jobId} started successfully.',
values: { jobId: d.config.id },
})
);
getJobs(true);
} catch (e) {
toastNotifications.addDanger(
i18n.translate('xpack.ml.dataframe.jobsList.startJobErrorMessage', {
@ -30,4 +32,5 @@ export const startJobFactory = (getJobs: GetJobs) => async (d: DataFrameJobListR
})
);
}
refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH);
};

View file

@ -8,26 +8,30 @@ import { i18n } from '@kbn/i18n';
import { toastNotifications } from 'ui/notify';
import { ml } from '../../../../../../services/ml_api_service';
import { DataFrameJobListRow } from '../common';
import { refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../../../../../common';
import { GetJobs } from './get_jobs';
import { DATA_FRAME_TASK_STATE, DataFrameJobListRow } from '../common';
export const stopJobFactory = (getJobs: GetJobs) => async (d: DataFrameJobListRow) => {
export const stopJob = async (d: DataFrameJobListRow) => {
try {
await ml.dataFrame.stopDataFrameTransformsJob(d.config.id);
getJobs(true);
await ml.dataFrame.stopDataFrameTransformsJob(
d.config.id,
d.state.task_state === DATA_FRAME_TASK_STATE.FAILED,
true
);
toastNotifications.addSuccess(
i18n.translate('xpack.ml.dataframe.jobsList.stopJobSuccessMessage', {
defaultMessage: 'Data frame job {jobId} stopped successfully.',
defaultMessage: 'Data frame transform {jobId} stopped successfully.',
values: { jobId: d.config.id },
})
);
} catch (e) {
toastNotifications.addDanger(
i18n.translate('xpack.ml.dataframe.jobsList.stopJobErrorMessage', {
defaultMessage: 'An error occurred stopping the data frame job {jobId}: {error}',
defaultMessage: 'An error occurred stopping the data frame transform {jobId}: {error}',
values: { jobId: d.config.id, error: JSON.stringify(e) },
})
);
}
refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH);
};

View file

@ -61,7 +61,7 @@ export const TransformMessagesPane: React.SFC<Props> = ({ transformId }) => {
};
};
useRefreshTransformList({ onRefresh: () => getMessagesFactory() });
useRefreshTransformList({ onRefresh: getMessagesFactory() });
const columns = [
{

View file

@ -7,12 +7,27 @@
// This component extends EuiInMemoryTable with some
// fixes and TS specs until the changes become available upstream.
import { Component } from 'react';
import React, { Component, Fragment } from 'react';
import { EuiInMemoryTable, EuiInMemoryTableProps } from '@elastic/eui';
import { EuiInMemoryTable, EuiInMemoryTableProps, EuiProgress } from '@elastic/eui';
import { ItemIdToExpandedRowMap } from './common';
// The built in loading progress bar of EuiInMemoryTable causes a full DOM replacement
// of the table and doesn't play well with auto-refreshing. That's why we're displaying
// our own progress bar on top of the table. `EuiProgress` after `isLoading` displays
// the loading indicator. The variation after `!isLoading` displays an empty progress
// bar fixed to 0%. Without it, the display would vertically jump when showing/hiding
// the progress bar.
export const ProgressBar = ({ isLoading = false }) => {
return (
<Fragment>
{isLoading && <EuiProgress size="xs" color="primary" />}
{!isLoading && <EuiProgress value={0} max={100} size="xs" />}
</Fragment>
);
};
// copied from EUI to be available to the extended getDerivedStateFromProps()
function findColumnByProp(columns: any, prop: any, value: any) {
for (let i = 0; i < columns.length; i++) {

View file

@ -53,15 +53,15 @@ export const dataFrame = {
data: obj
});
},
startDataFrameTransformsJob(jobId) {
startDataFrameTransformsJob(jobId, force = false) {
return http({
url: `${basePath}/_data_frame/transforms/${jobId}/_start`,
url: `${basePath}/_data_frame/transforms/${jobId}/_start?force=${force}`,
method: 'POST',
});
},
stopDataFrameTransformsJob(jobId) {
stopDataFrameTransformsJob(jobId, force = false, waitForCompletion = false) {
return http({
url: `${basePath}/_data_frame/transforms/${jobId}/_stop?force=true`,
url: `${basePath}/_data_frame/transforms/${jobId}/_stop?force=${force}&wait_for_completion=${waitForCompletion}`,
method: 'POST',
});
},

View file

@ -27,8 +27,12 @@ declare interface Ml {
createDataFrameTransformsJob(jobId: string, jobConfig: any): Promise<any>;
deleteDataFrameTransformsJob(jobId: string): Promise<any>;
getDataFrameTransformsPreview(payload: any): Promise<any>;
startDataFrameTransformsJob(jobId: string): Promise<any>;
stopDataFrameTransformsJob(jobId: string): Promise<any>;
startDataFrameTransformsJob(jobId: string, force?: boolean): Promise<any>;
stopDataFrameTransformsJob(
jobId: string,
force?: boolean,
waitForCompletion?: boolean
): Promise<any>;
getTransformAuditMessages(transformId: string): Promise<any>;
};

View file

@ -187,10 +187,13 @@ export const elasticsearchJsPlugin = (Client, config, components) => { // eslint
ml.startDataFrameTransformsJob = ca({
urls: [
{
fmt: '/_data_frame/transforms/<%=jobId%>/_start',
fmt: '/_data_frame/transforms/<%=jobId%>/_start?&force=<%=force%>',
req: {
jobId: {
type: 'string'
},
force: {
type: 'boolean'
}
}
}
@ -201,13 +204,16 @@ export const elasticsearchJsPlugin = (Client, config, components) => { // eslint
ml.stopDataFrameTransformsJob = ca({
urls: [
{
fmt: '/_data_frame/transforms/<%=jobId%>/_stop?&force=<%=force%>',
fmt: '/_data_frame/transforms/<%=jobId%>/_stop?&force=<%=force%>&wait_for_completion=<%waitForCompletion%>',
req: {
jobId: {
type: 'string'
},
force: {
type: 'boolean'
},
waitForCompletion: {
type: 'boolean'
}
}
}

View file

@ -110,8 +110,15 @@ export function dataFrameRoutes({ commonRouteConfig, elasticsearchPlugin, route
path: '/api/ml/_data_frame/transforms/{jobId}/_start',
handler(request) {
const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request);
const { jobId } = request.params;
return callWithRequest('ml.startDataFrameTransformsJob', { jobId })
const options = {
jobId: request.params.jobId
};
if (request.query.force !== undefined) {
options.force = request.query.force;
}
return callWithRequest('ml.startDataFrameTransformsJob', options)
.catch(resp => wrapError(resp));
},
config: {
@ -127,10 +134,15 @@ export function dataFrameRoutes({ commonRouteConfig, elasticsearchPlugin, route
const options = {
jobId: request.params.jobId
};
const force = request.query.force;
if (force !== undefined) {
options.force = force;
if (request.query.force !== undefined) {
options.force = request.query.force;
}
if (request.query.wait_for_completion !== undefined) {
options.waitForCompletion = request.query.wait_for_completion;
}
return callWithRequest('ml.stopDataFrameTransformsJob', options)
.catch(resp => wrapError(resp));
},

View file

@ -6265,7 +6265,6 @@
"xpack.ml.dataframe.jobsList.deleteModalCancelButton": "キャンセル",
"xpack.ml.dataframe.jobsList.deleteModalDeleteButton": "削除",
"xpack.ml.dataframe.jobsList.deleteModalTitle": "{jobId} を削除",
"xpack.ml.dataframe.jobsList.errorGettingDataFrameJobsList": "データフレームジョブリストの取得中にエラーが発生しました: {error}",
"xpack.ml.dataframe.jobsList.jobDetails.tabs.jobSettingsLabel": "ジョブの詳細",
"xpack.ml.dataframe.jobsList.rowCollapse": "{jobId} の詳細を非表示",
"xpack.ml.dataframe.jobsList.rowExpand": "{jobId} の詳細を表示",

View file

@ -6266,7 +6266,6 @@
"xpack.ml.dataframe.jobsList.deleteModalCancelButton": "取消",
"xpack.ml.dataframe.jobsList.deleteModalDeleteButton": "删除",
"xpack.ml.dataframe.jobsList.deleteModalTitle": "删除 {jobId}",
"xpack.ml.dataframe.jobsList.errorGettingDataFrameJobsList": "获取数据帧作业列表时发生错误:{error}",
"xpack.ml.dataframe.jobsList.jobDetails.tabs.jobSettingsLabel": "作业详情",
"xpack.ml.dataframe.jobsList.rowCollapse": "隐藏 {jobId} 的详情",
"xpack.ml.dataframe.jobsList.rowExpand": "显示 {jobId} 的详情",