Skip to content

Commit

Permalink
[ML] Data Frames: Jobs List Progress Bar (#36362) (#36427)
Browse files Browse the repository at this point in the history
- Adds a new column with a progress bar to the data frames jobs list.
- Updated the data frame jobs list empty table message to get rid of the Here be dragons ... message.
- Changes MINIMUM_REFRESH_INTERVAL_MS from 5000 to 1000 in ml/common/constants/jobs_list.js->ts. This change also affects the anomaly detection jobs list. It fixes a bug where setting the timefilter interval to less than 5s would stop updating the jobs list. This was a regression of a change in timefilter. Previously the minimum allowed interval settings was 5s.
- Now the correct timefilter based interval picker gets initialized and displayed for the data frame jobs list. The code is a replication of what is used for the anomaly detection job list using a custom hook in use_refresh_interval.ts.
  • Loading branch information
walterra authored May 10, 2019
1 parent 6e1edb2 commit b9799b5
Show file tree
Hide file tree
Showing 16 changed files with 248 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/


export const DEFAULT_REFRESH_INTERVAL_MS = 30000;
export const MINIMUM_REFRESH_INTERVAL_MS = 5000;
export const MINIMUM_REFRESH_INTERVAL_MS = 1000;
export const DELETING_JOBS_REFRESH_INTERVAL_MS = 2000;
1 change: 1 addition & 0 deletions x-pack/plugins/ml/public/data_frame/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
export * from './aggregations';
export * from './dropdown';
export * from './kibana_context';
export * from './navigation';
export * from './pivot_aggs';
export * from './pivot_group_by';
export * from './request';
9 changes: 9 additions & 0 deletions x-pack/plugins/ml/public/data_frame/common/navigation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* 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.
*/

export function moveToDataFrameWizard() {
window.location.href = `#/data_frames/new_job`;
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@ import {
createPermissionFailureMessage,
} from '../../../../../privilege/check_privilege';

function newJob() {
window.location.href = `#/data_frames/new_job`;
}
import { moveToDataFrameWizard } from '../../../../common';

export const CreateJobButton: SFC = () => {
const disabled =
Expand All @@ -26,7 +24,13 @@ export const CreateJobButton: SFC = () => {
!checkPermission('canStartStopDataFrameJob');

const button = (
<EuiButton disabled={disabled} fill onClick={newJob} iconType="plusInCircle" size="s">
<EuiButton
disabled={disabled}
fill
onClick={moveToDataFrameWizard}
iconType="plusInCircle"
size="s"
>
<FormattedMessage
id="xpack.ml.dataframe.jobsList.createDataFrameButton"
defaultMessage="Create data frame"
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ describe('Data Frame: Job List Columns', () => {
test('getColumns()', () => {
const columns = getColumns(() => {}, [], () => {});

expect(columns).toHaveLength(6);
expect(columns).toHaveLength(7);
expect(columns[0].isExpander).toBeTruthy();
expect(columns[1].name).toBe('ID');
expect(columns[2].name).toBe('Source index');
expect(columns[3].name).toBe('Target index');
expect(columns[4].name).toBe('Status');
expect(columns[5].name).toBe('Actions');
expect(columns[5].name).toBe('Progress');
expect(columns[6].name).toBe('Actions');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,15 @@

import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiBadge, EuiButtonIcon, RIGHT_ALIGNMENT } from '@elastic/eui';
import {
EuiBadge,
EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
EuiProgress,
EuiText,
RIGHT_ALIGNMENT,
} from '@elastic/eui';

import { DataFrameJobListColumn, DataFrameJobListRow, JobId } from './common';
import { getActions } from './actions';
Expand Down Expand Up @@ -81,6 +89,31 @@ export const getColumns = (
return <EuiBadge color={color}>{item.state.task_state}</EuiBadge>;
},
},
{
name: i18n.translate('xpack.ml.dataframe.progress', { defaultMessage: 'Progress' }),
sortable: true,
truncateText: true,
render(item: DataFrameJobListRow) {
let progress = 0;

if (item.state.progress !== undefined) {
progress = Math.round(item.state.progress.percent_complete);
}

return (
<EuiFlexGroup alignItems="center" gutterSize="xs">
<EuiFlexItem>
<EuiProgress value={progress} max={100} color="primary" size="m">
{progress}%
</EuiProgress>
</EuiFlexItem>
<EuiFlexItem>
<EuiText size="xs">{`${progress}%`}</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
},
},
{
name: i18n.translate('xpack.ml.dataframe.tableActionLabel', { defaultMessage: 'Actions' }),
actions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ export interface DataFrameJobState {
// indexer_state is a backend internal attribute
// and should not be considered in the UI.
indexer_state: RunningState;
progress?: {
docs_remaining: number;
percent_complete: number;
total_docs: number;
};
// task_state is the attribute to check against if a job
// is running or not.
task_state: RunningState;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* 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 { DataFrameJobList } from './job_list';

describe('Data Frame: Job List <DataFrameJobList />', () => {
test('Minimal initialization', () => {
const wrapper = shallow(<DataFrameJobList />);

expect(wrapper).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@
* you may not use this file except in compliance with the Elastic License.
*/

import React, { FunctionComponent, SFC, useEffect, useState } from 'react';
import React, { FunctionComponent, SFC, useState } from 'react';

import {
EuiButtonEmpty,
EuiEmptyPrompt,
EuiInMemoryTable,
EuiInMemoryTableProps,
SortDirection,
} from '@elastic/eui';

import { moveToDataFrameWizard } from '../../../../common';

import {
DataFrameJobListColumn,
DataFrameJobListRow,
Expand All @@ -22,6 +25,7 @@ import {
import { getJobsFactory } from './job_service';
import { getColumns } from './columns';
import { ExpandedRow } from './expanded_row';
import { useRefreshInterval } from './use_refresh_interval';

function getItemIdToExpandedRowMap(
itemIds: JobId[],
Expand Down Expand Up @@ -49,17 +53,23 @@ const ExpandableTable = (EuiInMemoryTable as any) as FunctionComponent<Expandabl

export const DataFrameJobList: SFC = () => {
const [dataFrameJobs, setDataFrameJobs] = useState<DataFrameJobListRow[]>([]);
const getJobs = getJobsFactory(setDataFrameJobs);

const [blockRefresh, setBlockRefresh] = useState(false);
const [expandedRowItemIds, setExpandedRowItemIds] = useState<JobId[]>([]);

// use this pattern so we don't return a promise, useEffects doesn't like that
useEffect(() => {
getJobs();
}, []);
const getJobs = getJobsFactory(setDataFrameJobs, blockRefresh);
useRefreshInterval(getJobs, setBlockRefresh);

if (dataFrameJobs.length === 0) {
return <EuiEmptyPrompt title={<h2>Here be Data Frame dragons!</h2>} iconType="editorStrike" />;
return (
<EuiEmptyPrompt
title={<h2>No data frame jobs found</h2>}
actions={[
<EuiButtonEmpty onClick={moveToDataFrameWizard}>
Create your first data frame job
</EuiButtonEmpty>,
]}
/>
);
}

const columns = getColumns(getJobs, expandedRowItemIds, setExpandedRowItemIds);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ import { ml } from '../../../../../../services/ml_api_service';

import { DataFrameJobListRow } from '../common';

export const deleteJobFactory = (getJobs: () => void) => async (d: DataFrameJobListRow) => {
import { GetJobs } from './get_jobs';

export const deleteJobFactory = (getJobs: GetJobs) => async (d: DataFrameJobListRow) => {
try {
await ml.dataFrame.deleteDataFrameTransformsJob(d.config.id);
getJobs();
getJobs(true);
toastNotifications.addSuccess(
i18n.translate('xpack.ml.dataframe.jobsList.deleteJobSuccessMessage', {
defaultMessage: 'Data frame job {jobId} deleted successfully.',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,31 +31,36 @@ interface GetDataFrameTransformsStatsResponse {
transforms: DataFrameJobStateStats[];
}

export type GetJobs = (forceRefresh?: boolean) => void;

export const getJobsFactory = (
setDataFrameJobs: React.Dispatch<React.SetStateAction<DataFrameJobListRow[]>>
) => async () => {
try {
const jobConfigs: GetDataFrameTransformsResponse = await ml.dataFrame.getDataFrameTransforms();
const jobStats: GetDataFrameTransformsStatsResponse = await ml.dataFrame.getDataFrameTransformsStats();

const tableRows = jobConfigs.transforms.map(config => {
const stats = jobStats.transforms.find(d => config.id === d.id);

if (stats === undefined) {
throw new Error('job stats not available');
}

// table with expandable rows requires `id` on the outer most level
return { config, id: config.id, state: stats.state, stats: stats.stats };
});

setDataFrameJobs(tableRows);
} catch (e) {
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) },
})
);
setDataFrameJobs: React.Dispatch<React.SetStateAction<DataFrameJobListRow[]>>,
blockRefresh: boolean
): GetJobs => async (forceRefresh = false) => {
if (forceRefresh === true || blockRefresh === false) {
try {
const jobConfigs: GetDataFrameTransformsResponse = await ml.dataFrame.getDataFrameTransforms();
const jobStats: GetDataFrameTransformsStatsResponse = await ml.dataFrame.getDataFrameTransformsStats();

const tableRows = jobConfigs.transforms.map(config => {
const stats = jobStats.transforms.find(d => config.id === d.id);

if (stats === undefined) {
throw new Error('job stats not available');
}

// table with expandable rows requires `id` on the outer most level
return { config, id: config.id, state: stats.state, stats: stats.stats };
});

setDataFrameJobs(tableRows);
} catch (e) {
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) },
})
);
}
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import { ml } from '../../../../../../services/ml_api_service';

import { DataFrameJobListRow } from '../common';

export const startJobFactory = (getJobs: () => void) => async (d: DataFrameJobListRow) => {
import { GetJobs } from './get_jobs';

export const startJobFactory = (getJobs: GetJobs) => async (d: DataFrameJobListRow) => {
try {
await ml.dataFrame.startDataFrameTransformsJob(d.config.id);
toastNotifications.addSuccess(
Expand All @@ -19,7 +21,7 @@ export const startJobFactory = (getJobs: () => void) => async (d: DataFrameJobLi
values: { jobId: d.config.id },
})
);
getJobs();
getJobs(true);
} catch (e) {
toastNotifications.addDanger(
i18n.translate('xpack.ml.dataframe.jobsList.startJobErrorMessage', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ import { ml } from '../../../../../../services/ml_api_service';

import { DataFrameJobListRow } from '../common';

export const stopJobFactory = (getJobs: () => void) => async (d: DataFrameJobListRow) => {
import { GetJobs } from './get_jobs';

export const stopJobFactory = (getJobs: GetJobs) => async (d: DataFrameJobListRow) => {
try {
await ml.dataFrame.stopDataFrameTransformsJob(d.config.id);
getJobs();
getJobs(true);
toastNotifications.addSuccess(
i18n.translate('xpack.ml.dataframe.jobsList.stopJobSuccessMessage', {
defaultMessage: 'Data frame job {jobId} stopped successfully.',
Expand Down
Loading

0 comments on commit b9799b5

Please sign in to comment.