mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
- 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:
parent
d64dbb2945
commit
e28c79901d
23 changed files with 262 additions and 154 deletions
|
@ -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}
|
||||
/>
|
||||
`;
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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} />;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -61,7 +61,7 @@ export const TransformMessagesPane: React.SFC<Props> = ({ transformId }) => {
|
|||
};
|
||||
};
|
||||
|
||||
useRefreshTransformList({ onRefresh: () => getMessagesFactory() });
|
||||
useRefreshTransformList({ onRefresh: getMessagesFactory() });
|
||||
|
||||
const columns = [
|
||||
{
|
||||
|
|
|
@ -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++) {
|
|
@ -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',
|
||||
});
|
||||
},
|
||||
|
|
|
@ -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>;
|
||||
};
|
||||
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
},
|
||||
|
|
|
@ -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} の詳細を表示",
|
||||
|
|
|
@ -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} 的详情",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue