mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
- 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.
This commit is contained in:
parent
6e1edb21a5
commit
b9799b58f1
16 changed files with 244 additions and 45 deletions
|
@ -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;
|
|
@ -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
x-pack/plugins/ml/public/data_frame/common/navigation.ts
Normal file
9
x-pack/plugins/ml/public/data_frame/common/navigation.ts
Normal file
|
@ -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`;
|
||||
}
|
|
@ -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 =
|
||||
|
@ -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"
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Data Frame: Job List <DataFrameJobList /> Minimal initialization 1`] = `
|
||||
<EuiEmptyPrompt
|
||||
actions={
|
||||
Array [
|
||||
<EuiButtonEmpty
|
||||
color="primary"
|
||||
iconSide="left"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Create your first data frame job
|
||||
</EuiButtonEmpty>,
|
||||
]
|
||||
}
|
||||
iconColor="subdued"
|
||||
title={
|
||||
<h2>
|
||||
No data frame jobs found
|
||||
</h2>
|
||||
}
|
||||
/>
|
||||
`;
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
|
@ -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[],
|
||||
|
@ -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);
|
||||
|
|
|
@ -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.',
|
||||
|
|
|
@ -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();
|
||||
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);
|
||||
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');
|
||||
}
|
||||
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 };
|
||||
});
|
||||
// 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(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) },
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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(
|
||||
|
@ -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', {
|
||||
|
|
|
@ -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.',
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* 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, { useEffect } from 'react';
|
||||
|
||||
import { timefilter } from 'ui/timefilter';
|
||||
|
||||
import {
|
||||
DEFAULT_REFRESH_INTERVAL_MS,
|
||||
MINIMUM_REFRESH_INTERVAL_MS,
|
||||
} from '../../../../../../common/constants/jobs_list';
|
||||
|
||||
import { GetJobs } from './job_service/get_jobs';
|
||||
|
||||
export const useRefreshInterval = (
|
||||
getJobs: GetJobs,
|
||||
setBlockRefresh: React.Dispatch<React.SetStateAction<boolean>>
|
||||
) => {
|
||||
useEffect(() => {
|
||||
let jobsRefreshInterval: null | number = null;
|
||||
|
||||
timefilter.disableTimeRangeSelector();
|
||||
timefilter.enableAutoRefreshSelector();
|
||||
|
||||
initAutoRefresh();
|
||||
initAutoRefreshUpdate();
|
||||
|
||||
function initAutoRefresh() {
|
||||
const { value } = timefilter.getRefreshInterval();
|
||||
if (value === 0) {
|
||||
// the auto refresher starts in an off state
|
||||
// so switch it on and set the interval to 30s
|
||||
timefilter.setRefreshInterval({
|
||||
pause: false,
|
||||
value: DEFAULT_REFRESH_INTERVAL_MS,
|
||||
});
|
||||
}
|
||||
|
||||
setAutoRefresh();
|
||||
}
|
||||
|
||||
function initAutoRefreshUpdate() {
|
||||
// update the interval if it changes
|
||||
timefilter.on('refreshIntervalUpdate', () => {
|
||||
setAutoRefresh();
|
||||
});
|
||||
}
|
||||
|
||||
function setAutoRefresh() {
|
||||
const { value, pause } = timefilter.getRefreshInterval();
|
||||
if (pause) {
|
||||
clearRefreshInterval();
|
||||
} else {
|
||||
setRefreshInterval(value);
|
||||
}
|
||||
getJobs(true);
|
||||
}
|
||||
|
||||
function setRefreshInterval(interval: number) {
|
||||
clearRefreshInterval();
|
||||
if (interval >= MINIMUM_REFRESH_INTERVAL_MS) {
|
||||
setBlockRefresh(false);
|
||||
const intervalId = window.setInterval(() => {
|
||||
getJobs();
|
||||
}, interval);
|
||||
jobsRefreshInterval = intervalId;
|
||||
}
|
||||
}
|
||||
|
||||
function clearRefreshInterval() {
|
||||
setBlockRefresh(true);
|
||||
if (jobsRefreshInterval !== null) {
|
||||
window.clearInterval(jobsRefreshInterval);
|
||||
}
|
||||
}
|
||||
|
||||
// useEffect cleanup
|
||||
return () => {
|
||||
clearRefreshInterval();
|
||||
};
|
||||
}, []); // [] as comparator makes sure this only runs once
|
||||
};
|
|
@ -435,4 +435,3 @@ export class JobsListView extends Component {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue