mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[ML] Data Frame Jobs List improvements (#35788)
- The start/stop/delete buttons have been tweaked to include icon+text. - A status column has been added to the jobs list to indicate stopped/started state. - Expanded rows are now properly populated with tabs for job details (state&stats) and the job JSON config.
This commit is contained in:
parent
6871157963
commit
687b8ee87a
18 changed files with 750 additions and 43 deletions
|
@ -0,0 +1 @@
|
|||
{"config":{"id":"fq_date_histogram_1m_1441","source":{"index":["farequote-2019"],"query":{"match_all":{}}},"dest":{"index":"fq_data_histogram_1m_1441"},"pivot":{"group_by":{"date_histogram(@timestamp)":{"date_histogram":{"field":"@timestamp","interval":"1m"}}},"aggregations":{"avg(response)":{"avg":{"field":"responsetime"}}}}},"id":"fq_date_histogram_1m_1441","state":{"task_state":"stopped","indexer_state":"stopped","current_position":{"date_histogram(@timestamp)":1549929540000},"checkpoint":1},"stats":{"pages_processed":0,"documents_processed":0,"documents_indexed":0,"trigger_count":0,"index_time_in_ms":0,"index_total":0,"index_failures":0,"search_time_in_ms":0,"search_total":0,"search_failures":0}}
|
|
@ -0,0 +1,18 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Data Frame: Job List Actions <DeleteAction /> Minimal initialization 1`] = `
|
||||
<Fragment>
|
||||
<EuiButtonEmpty
|
||||
aria-label="Delete"
|
||||
color="text"
|
||||
disabled={false}
|
||||
iconSide="left"
|
||||
iconType="trash"
|
||||
onClick={[Function]}
|
||||
size="xs"
|
||||
type="button"
|
||||
>
|
||||
Delete
|
||||
</EuiButtonEmpty>
|
||||
</Fragment>
|
||||
`;
|
|
@ -0,0 +1,210 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Data Frame: Job List <ExpandedRow /> Minimal initialization 1`] = `
|
||||
<EuiTabbedContent
|
||||
expand={false}
|
||||
initialSelectedTab={
|
||||
Object {
|
||||
"content": <JobDetailsPane
|
||||
sections={
|
||||
Array [
|
||||
Object {
|
||||
"items": Array [
|
||||
Object {
|
||||
"description": "stopped",
|
||||
"title": "task_state",
|
||||
},
|
||||
Object {
|
||||
"description": "stopped",
|
||||
"title": "indexer_state",
|
||||
},
|
||||
Object {
|
||||
"description": "{\\"date_histogram(@timestamp)\\":1549929540000}",
|
||||
"title": "current_position",
|
||||
},
|
||||
Object {
|
||||
"description": "1",
|
||||
"title": "checkpoint",
|
||||
},
|
||||
],
|
||||
"position": "left",
|
||||
"title": "State",
|
||||
},
|
||||
Object {
|
||||
"items": Array [
|
||||
Object {
|
||||
"description": "0",
|
||||
"title": "pages_processed",
|
||||
},
|
||||
Object {
|
||||
"description": "0",
|
||||
"title": "documents_processed",
|
||||
},
|
||||
Object {
|
||||
"description": "0",
|
||||
"title": "documents_indexed",
|
||||
},
|
||||
Object {
|
||||
"description": "0",
|
||||
"title": "trigger_count",
|
||||
},
|
||||
Object {
|
||||
"description": "0",
|
||||
"title": "index_time_in_ms",
|
||||
},
|
||||
Object {
|
||||
"description": "0",
|
||||
"title": "index_total",
|
||||
},
|
||||
Object {
|
||||
"description": "0",
|
||||
"title": "index_failures",
|
||||
},
|
||||
Object {
|
||||
"description": "0",
|
||||
"title": "search_time_in_ms",
|
||||
},
|
||||
Object {
|
||||
"description": "0",
|
||||
"title": "search_total",
|
||||
},
|
||||
Object {
|
||||
"description": "0",
|
||||
"title": "search_failures",
|
||||
},
|
||||
],
|
||||
"position": "right",
|
||||
"title": "Stats",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>,
|
||||
"id": "job-details",
|
||||
"name": "Job details",
|
||||
}
|
||||
}
|
||||
onTabClick={[Function]}
|
||||
size="s"
|
||||
tabs={
|
||||
Array [
|
||||
Object {
|
||||
"content": <JobDetailsPane
|
||||
sections={
|
||||
Array [
|
||||
Object {
|
||||
"items": Array [
|
||||
Object {
|
||||
"description": "stopped",
|
||||
"title": "task_state",
|
||||
},
|
||||
Object {
|
||||
"description": "stopped",
|
||||
"title": "indexer_state",
|
||||
},
|
||||
Object {
|
||||
"description": "{\\"date_histogram(@timestamp)\\":1549929540000}",
|
||||
"title": "current_position",
|
||||
},
|
||||
Object {
|
||||
"description": "1",
|
||||
"title": "checkpoint",
|
||||
},
|
||||
],
|
||||
"position": "left",
|
||||
"title": "State",
|
||||
},
|
||||
Object {
|
||||
"items": Array [
|
||||
Object {
|
||||
"description": "0",
|
||||
"title": "pages_processed",
|
||||
},
|
||||
Object {
|
||||
"description": "0",
|
||||
"title": "documents_processed",
|
||||
},
|
||||
Object {
|
||||
"description": "0",
|
||||
"title": "documents_indexed",
|
||||
},
|
||||
Object {
|
||||
"description": "0",
|
||||
"title": "trigger_count",
|
||||
},
|
||||
Object {
|
||||
"description": "0",
|
||||
"title": "index_time_in_ms",
|
||||
},
|
||||
Object {
|
||||
"description": "0",
|
||||
"title": "index_total",
|
||||
},
|
||||
Object {
|
||||
"description": "0",
|
||||
"title": "index_failures",
|
||||
},
|
||||
Object {
|
||||
"description": "0",
|
||||
"title": "search_time_in_ms",
|
||||
},
|
||||
Object {
|
||||
"description": "0",
|
||||
"title": "search_total",
|
||||
},
|
||||
Object {
|
||||
"description": "0",
|
||||
"title": "search_failures",
|
||||
},
|
||||
],
|
||||
"position": "right",
|
||||
"title": "Stats",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>,
|
||||
"id": "job-details",
|
||||
"name": "Job details",
|
||||
},
|
||||
Object {
|
||||
"content": <JobJsonPane
|
||||
json={
|
||||
Object {
|
||||
"dest": Object {
|
||||
"index": "fq_data_histogram_1m_1441",
|
||||
},
|
||||
"id": "fq_date_histogram_1m_1441",
|
||||
"pivot": Object {
|
||||
"aggregations": Object {
|
||||
"avg(response)": Object {
|
||||
"avg": Object {
|
||||
"field": "responsetime",
|
||||
},
|
||||
},
|
||||
},
|
||||
"group_by": Object {
|
||||
"date_histogram(@timestamp)": Object {
|
||||
"date_histogram": Object {
|
||||
"field": "@timestamp",
|
||||
"interval": "1m",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"source": Object {
|
||||
"index": Array [
|
||||
"farequote-2019",
|
||||
],
|
||||
"query": Object {
|
||||
"match_all": Object {},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
/>,
|
||||
"id": "job-json",
|
||||
"name": "JSON",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
`;
|
|
@ -0,0 +1,59 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Data Frame: Job List Expanded Row <JobDetailsPane /> Minimal initialization 1`] = `
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiSpacer
|
||||
size="s"
|
||||
/>
|
||||
<Section
|
||||
key="the-section-title"
|
||||
section={
|
||||
Object {
|
||||
"items": Array [
|
||||
Object {
|
||||
"description": "the-item-description",
|
||||
"title": "the-item-title",
|
||||
},
|
||||
],
|
||||
"position": "left",
|
||||
"title": "the-section-title",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiSpacer
|
||||
size="s"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
`;
|
||||
|
||||
exports[`Data Frame: Job List Expanded Row <Section /> Minimal initialization 1`] = `
|
||||
<EuiPanel
|
||||
grow={true}
|
||||
hasShadow={false}
|
||||
paddingSize="m"
|
||||
>
|
||||
<EuiTitle
|
||||
size="xs"
|
||||
>
|
||||
<span>
|
||||
the-section-title
|
||||
</span>
|
||||
</EuiTitle>
|
||||
<EuiDescriptionList
|
||||
compressed={true}
|
||||
listItems={
|
||||
Array [
|
||||
Object {
|
||||
"description": "the-item-description",
|
||||
"title": "the-item-title",
|
||||
},
|
||||
]
|
||||
}
|
||||
type="column"
|
||||
/>
|
||||
</EuiPanel>
|
||||
`;
|
|
@ -0,0 +1,58 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Data Frame: Job List Expanded Row <JobJsonPane /> Minimal initialization 1`] = `
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem
|
||||
style={
|
||||
Object {
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
>
|
||||
<EuiSpacer
|
||||
size="s"
|
||||
/>
|
||||
<EuiCodeEditor
|
||||
mode="json"
|
||||
readOnly={true}
|
||||
setOptions={Object {}}
|
||||
value="{
|
||||
\\"id\\": \\"fq_date_histogram_1m_1441\\",
|
||||
\\"source\\": {
|
||||
\\"index\\": [
|
||||
\\"farequote-2019\\"
|
||||
],
|
||||
\\"query\\": {
|
||||
\\"match_all\\": {}
|
||||
}
|
||||
},
|
||||
\\"dest\\": {
|
||||
\\"index\\": \\"fq_data_histogram_1m_1441\\"
|
||||
},
|
||||
\\"pivot\\": {
|
||||
\\"group_by\\": {
|
||||
\\"date_histogram(@timestamp)\\": {
|
||||
\\"date_histogram\\": {
|
||||
\\"field\\": \\"@timestamp\\",
|
||||
\\"interval\\": \\"1m\\"
|
||||
}
|
||||
}
|
||||
},
|
||||
\\"aggregations\\": {
|
||||
\\"avg(response)\\": {
|
||||
\\"avg\\": {
|
||||
\\"field\\": \\"responsetime\\"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
`;
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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, getActions } from './actions';
|
||||
|
||||
import dataFrameJobListRow from './__mocks__/data_frame_job_list_row.json';
|
||||
|
||||
describe('Data Frame: Job List Actions <DeleteAction />', () => {
|
||||
test('Minimal initialization', () => {
|
||||
const item: DataFrameJobListRow = dataFrameJobListRow;
|
||||
const props = {
|
||||
disabled: false,
|
||||
item,
|
||||
deleteJob(d: DataFrameJobListRow) {},
|
||||
};
|
||||
|
||||
const wrapper = shallow(<DeleteAction {...props} />);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Data Frame: Job List Actions', () => {
|
||||
test('getActions()', () => {
|
||||
const actions = getActions(() => {});
|
||||
|
||||
expect(actions).toHaveLength(2);
|
||||
expect(actions[0].isPrimary).toBeTruthy();
|
||||
expect(typeof actions[0].render).toBe('function');
|
||||
expect(typeof actions[1].render).toBe('function');
|
||||
});
|
||||
});
|
|
@ -22,7 +22,7 @@ interface DeleteActionProps {
|
|||
deleteJob(d: DataFrameJobListRow): void;
|
||||
}
|
||||
|
||||
const DeleteAction: SFC<DeleteActionProps> = ({ deleteJob, disabled, item }) => {
|
||||
export const DeleteAction: SFC<DeleteActionProps> = ({ deleteJob, disabled, item }) => {
|
||||
const [isModalVisible, setModalVisible] = useState(false);
|
||||
|
||||
const closeModal = () => setModalVisible(false);
|
||||
|
@ -32,17 +32,21 @@ const DeleteAction: SFC<DeleteActionProps> = ({ deleteJob, disabled, item }) =>
|
|||
};
|
||||
const openModal = () => setModalVisible(true);
|
||||
|
||||
const buttonDeleteText = i18n.translate('xpack.ml.dataframe.jobsList.deleteActionName', {
|
||||
defaultMessage: 'Delete',
|
||||
});
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiButtonEmpty
|
||||
color="danger"
|
||||
size="xs"
|
||||
color="text"
|
||||
disabled={disabled}
|
||||
iconType="trash"
|
||||
onClick={openModal}
|
||||
aria-label={i18n.translate('xpack.ml.dataframe.jobsList.deleteActionName', {
|
||||
defaultMessage: 'Delete',
|
||||
})}
|
||||
/>
|
||||
aria-label={buttonDeleteText}
|
||||
>
|
||||
{buttonDeleteText}
|
||||
</EuiButtonEmpty>
|
||||
{isModalVisible && (
|
||||
<EuiOverlayMask>
|
||||
<EuiConfirmModal
|
||||
|
@ -88,30 +92,36 @@ export const getActions = (getJobs: () => void) => {
|
|||
{
|
||||
isPrimary: true,
|
||||
render: (item: DataFrameJobListRow) => {
|
||||
if (
|
||||
item.state.indexer_state !== DATA_FRAME_RUNNING_STATE.STARTED &&
|
||||
item.state.task_state !== DATA_FRAME_RUNNING_STATE.STARTED
|
||||
) {
|
||||
if (item.state.task_state !== DATA_FRAME_RUNNING_STATE.STARTED) {
|
||||
const buttonStartText = i18n.translate('xpack.ml.dataframe.jobsList.startActionName', {
|
||||
defaultMessage: 'Start',
|
||||
});
|
||||
return (
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
color="text"
|
||||
iconType="play"
|
||||
onClick={() => startJob(item)}
|
||||
aria-label={i18n.translate('xpack.ml.dataframe.jobsList.startActionName', {
|
||||
defaultMessage: 'Start',
|
||||
})}
|
||||
/>
|
||||
aria-label={buttonStartText}
|
||||
>
|
||||
{buttonStartText}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
}
|
||||
|
||||
const buttonStopText = i18n.translate('xpack.ml.dataframe.jobsList.stopActionName', {
|
||||
defaultMessage: 'Stop',
|
||||
});
|
||||
return (
|
||||
<EuiButtonEmpty
|
||||
color="danger"
|
||||
size="xs"
|
||||
color="text"
|
||||
iconType="stop"
|
||||
onClick={() => stopJob(item)}
|
||||
aria-label={i18n.translate('xpack.ml.dataframe.jobsList.stopActionName', {
|
||||
defaultMessage: 'Stop',
|
||||
})}
|
||||
/>
|
||||
aria-label={buttonStopText}
|
||||
>
|
||||
{buttonStopText}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
@ -120,10 +130,7 @@ export const getActions = (getJobs: () => void) => {
|
|||
return (
|
||||
<DeleteAction
|
||||
deleteJob={deleteJob}
|
||||
disabled={
|
||||
item.state.indexer_state === DATA_FRAME_RUNNING_STATE.STARTED ||
|
||||
item.state.task_state === DATA_FRAME_RUNNING_STATE.STARTED
|
||||
}
|
||||
disabled={item.state.task_state === DATA_FRAME_RUNNING_STATE.STARTED}
|
||||
item={item}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 { getColumns } from './columns';
|
||||
|
||||
describe('Data Frame: Job List Columns', () => {
|
||||
test('getColumns()', () => {
|
||||
const columns = getColumns(() => {}, [], () => {});
|
||||
|
||||
expect(columns).toHaveLength(6);
|
||||
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');
|
||||
});
|
||||
});
|
|
@ -6,26 +6,29 @@
|
|||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiButtonIcon, RIGHT_ALIGNMENT } from '@elastic/eui';
|
||||
import { EuiBadge, EuiButtonIcon, RIGHT_ALIGNMENT } from '@elastic/eui';
|
||||
|
||||
import { DataFrameJobListColumn, DataFrameJobListRow, ItemIdToExpandedRowMap } from './common';
|
||||
import { DataFrameJobListColumn, DataFrameJobListRow, JobId } from './common';
|
||||
import { getActions } from './actions';
|
||||
|
||||
export const getColumns = (
|
||||
getJobs: () => void,
|
||||
itemIdToExpandedRowMap: ItemIdToExpandedRowMap,
|
||||
setItemIdToExpandedRowMap: React.Dispatch<React.SetStateAction<ItemIdToExpandedRowMap>>
|
||||
expandedRowItemIds: JobId[],
|
||||
setExpandedRowItemIds: React.Dispatch<React.SetStateAction<JobId[]>>
|
||||
) => {
|
||||
const actions = getActions(getJobs);
|
||||
|
||||
function toggleDetails(item: DataFrameJobListRow) {
|
||||
if (itemIdToExpandedRowMap[item.config.id]) {
|
||||
delete itemIdToExpandedRowMap[item.config.id];
|
||||
const index = expandedRowItemIds.indexOf(item.config.id);
|
||||
if (index !== -1) {
|
||||
expandedRowItemIds.splice(index, 1);
|
||||
setExpandedRowItemIds([...expandedRowItemIds]);
|
||||
} else {
|
||||
itemIdToExpandedRowMap[item.config.id] = <div>EXPAND {item.config.id}</div>;
|
||||
expandedRowItemIds.push(item.config.id);
|
||||
}
|
||||
// spread to a new object otherwise the component wouldn't re-render
|
||||
setItemIdToExpandedRowMap({ ...itemIdToExpandedRowMap });
|
||||
|
||||
// spread to a new array otherwise the component wouldn't re-render
|
||||
setExpandedRowItemIds([...expandedRowItemIds]);
|
||||
}
|
||||
|
||||
return [
|
||||
|
@ -37,15 +40,17 @@ export const getColumns = (
|
|||
<EuiButtonIcon
|
||||
onClick={() => toggleDetails(item)}
|
||||
aria-label={
|
||||
itemIdToExpandedRowMap[item.config.id]
|
||||
expandedRowItemIds.includes(item.config.id)
|
||||
? i18n.translate('xpack.ml.dataframe.jobsList.rowCollapse', {
|
||||
defaultMessage: 'Collapse',
|
||||
defaultMessage: 'Hide details for {jobId}',
|
||||
values: { jobId: item.config.id },
|
||||
})
|
||||
: i18n.translate('xpack.ml.dataframe.jobsList.rowExpand', {
|
||||
defaultMessage: 'Expand',
|
||||
defaultMessage: 'Show details for {jobId}',
|
||||
values: { jobId: item.config.id },
|
||||
})
|
||||
}
|
||||
iconType={itemIdToExpandedRowMap[item.config.id] ? 'arrowUp' : 'arrowDown'}
|
||||
iconType={expandedRowItemIds.includes(item.config.id) ? 'arrowUp' : 'arrowDown'}
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
@ -67,6 +72,15 @@ export const getColumns = (
|
|||
sortable: true,
|
||||
truncateText: true,
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.ml.dataframe.status', { defaultMessage: 'Status' }),
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
render(item: DataFrameJobListRow) {
|
||||
const color = item.state.task_state === 'started' ? 'primary' : 'hollow';
|
||||
return <EuiBadge color={color}>{item.state.task_state}</EuiBadge>;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.ml.dataframe.tableActionLabel', { defaultMessage: 'Actions' }),
|
||||
actions,
|
||||
|
|
|
@ -6,11 +6,11 @@
|
|||
|
||||
import { Dictionary } from '../../../../../../common/types/common';
|
||||
|
||||
export type jobId = string;
|
||||
export type JobId = string;
|
||||
|
||||
export interface DataFrameJob {
|
||||
dest: string;
|
||||
id: jobId;
|
||||
id: JobId;
|
||||
source: string;
|
||||
}
|
||||
|
||||
|
@ -23,7 +23,11 @@ type RunningState = DATA_FRAME_RUNNING_STATE.STARTED | DATA_FRAME_RUNNING_STATE.
|
|||
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;
|
||||
// task_state is the attribute to check against if a job
|
||||
// is running or not.
|
||||
task_state: RunningState;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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 { ExpandedRow } from './expanded_row';
|
||||
|
||||
import dataFrameJobListRow from './__mocks__/data_frame_job_list_row.json';
|
||||
|
||||
describe('Data Frame: Job List <ExpandedRow />', () => {
|
||||
test('Minimal initialization', () => {
|
||||
const item: DataFrameJobListRow = dataFrameJobListRow;
|
||||
|
||||
const wrapper = shallow(<ExpandedRow item={item} />);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* 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, { SFC } from 'react';
|
||||
|
||||
import { EuiTabbedContent } from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { DataFrameJobListRow } from './common';
|
||||
import { JobDetailsPane, SectionConfig } from './job_details_pane';
|
||||
import { JobJsonPane } from './job_json_pane';
|
||||
|
||||
function getItemDescription(value: any) {
|
||||
if (typeof value === 'object') {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
interface Props {
|
||||
item: DataFrameJobListRow;
|
||||
}
|
||||
|
||||
export const ExpandedRow: SFC<Props> = ({ item }) => {
|
||||
const state: SectionConfig = {
|
||||
title: 'State',
|
||||
items: Object.entries(item.state).map(s => {
|
||||
return { title: s[0].toString(), description: getItemDescription(s[1]) };
|
||||
}),
|
||||
position: 'left',
|
||||
};
|
||||
|
||||
const stats: SectionConfig = {
|
||||
title: 'Stats',
|
||||
items: Object.entries(item.stats).map(s => {
|
||||
return { title: s[0].toString(), description: getItemDescription(s[1]) };
|
||||
}),
|
||||
position: 'right',
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
id: 'job-details',
|
||||
name: i18n.translate('xpack.ml.dataframe.jobsList.jobDetails.tabs.jobSettingsLabel', {
|
||||
defaultMessage: 'Job details',
|
||||
}),
|
||||
content: <JobDetailsPane sections={[state, stats]} />,
|
||||
},
|
||||
{
|
||||
id: 'job-json',
|
||||
name: 'JSON',
|
||||
content: <JobJsonPane json={item.config} />,
|
||||
},
|
||||
];
|
||||
return (
|
||||
<EuiTabbedContent
|
||||
size="s"
|
||||
tabs={tabs}
|
||||
initialSelectedTab={tabs[0]}
|
||||
onTabClick={() => {}}
|
||||
expand={false}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 { JobDetailsPane, Section, SectionConfig } from './job_details_pane';
|
||||
|
||||
const section: SectionConfig = {
|
||||
title: 'the-section-title',
|
||||
position: 'left',
|
||||
items: [
|
||||
{
|
||||
title: 'the-item-title',
|
||||
description: 'the-item-description',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('Data Frame: Job List Expanded Row <JobDetailsPane />', () => {
|
||||
test('Minimal initialization', () => {
|
||||
const wrapper = shallow(<JobDetailsPane sections={[section]} />);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Data Frame: Job List Expanded Row <Section />', () => {
|
||||
test('Minimal initialization', () => {
|
||||
const wrapper = shallow(<Section section={section} />);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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, { SFC } from 'react';
|
||||
|
||||
import {
|
||||
EuiDescriptionList,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPanel,
|
||||
EuiTitle,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
|
||||
export interface SectionItem {
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
export interface SectionConfig {
|
||||
title: string;
|
||||
position: 'left' | 'right';
|
||||
items: SectionItem[];
|
||||
}
|
||||
|
||||
interface SectionProps {
|
||||
section: SectionConfig;
|
||||
}
|
||||
|
||||
export const Section: SFC<SectionProps> = ({ section }) => {
|
||||
if (section.items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiPanel>
|
||||
<EuiTitle size="xs">
|
||||
<span>{section.title}</span>
|
||||
</EuiTitle>
|
||||
<EuiDescriptionList compressed type="column" listItems={section.items} />
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
||||
|
||||
interface JobDetailsPaneProps {
|
||||
sections: SectionConfig[];
|
||||
}
|
||||
|
||||
export const JobDetailsPane: SFC<JobDetailsPaneProps> = ({ sections }) => {
|
||||
return (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiSpacer size="s" />
|
||||
{sections
|
||||
.filter(s => s.position === 'left')
|
||||
.map(s => (
|
||||
<Section section={s} key={s.title} />
|
||||
))}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiSpacer size="s" />
|
||||
{sections
|
||||
.filter(s => s.position === 'right')
|
||||
.map(s => (
|
||||
<Section section={s} key={s.title} />
|
||||
))}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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 './__mocks__/data_frame_job_list_row.json';
|
||||
|
||||
import { JobJsonPane } from './job_json_pane';
|
||||
|
||||
describe('Data Frame: Job List Expanded Row <JobJsonPane />', () => {
|
||||
test('Minimal initialization', () => {
|
||||
const wrapper = shallow(<JobJsonPane json={dataFrameJobListRow.config} />);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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, { SFC } from 'react';
|
||||
|
||||
import {
|
||||
// @ts-ignore
|
||||
EuiCodeEditor,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
|
||||
interface Props {
|
||||
json: object;
|
||||
}
|
||||
|
||||
export const JobJsonPane: SFC<Props> = ({ json }) => {
|
||||
return (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem style={{ width: '100%' }}>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiCodeEditor value={JSON.stringify(json, null, 2)} readOnly={true} mode="json" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}> </EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -13,9 +13,31 @@ import {
|
|||
SortDirection,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { DataFrameJobListColumn, DataFrameJobListRow, ItemIdToExpandedRowMap } from './common';
|
||||
import {
|
||||
DataFrameJobListColumn,
|
||||
DataFrameJobListRow,
|
||||
ItemIdToExpandedRowMap,
|
||||
JobId,
|
||||
} from './common';
|
||||
import { getJobsFactory } from './job_service';
|
||||
import { getColumns } from './columns';
|
||||
import { ExpandedRow } from './expanded_row';
|
||||
|
||||
function getItemIdToExpandedRowMap(
|
||||
itemIds: JobId[],
|
||||
dataFrameJobs: DataFrameJobListRow[]
|
||||
): ItemIdToExpandedRowMap {
|
||||
return itemIds.reduce(
|
||||
(m: ItemIdToExpandedRowMap, jobId: JobId) => {
|
||||
const item = dataFrameJobs.find(job => job.config.id === jobId);
|
||||
if (item !== undefined) {
|
||||
m[jobId] = <ExpandedRow item={item} />;
|
||||
}
|
||||
return m;
|
||||
},
|
||||
{} as ItemIdToExpandedRowMap
|
||||
);
|
||||
}
|
||||
|
||||
// TODO EUI's types for EuiInMemoryTable is missing these props
|
||||
interface ExpandableTableProps extends EuiInMemoryTableProps {
|
||||
|
@ -29,7 +51,7 @@ export const DataFrameJobList: SFC = () => {
|
|||
const [dataFrameJobs, setDataFrameJobs] = useState<DataFrameJobListRow[]>([]);
|
||||
const getJobs = getJobsFactory(setDataFrameJobs);
|
||||
|
||||
const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState<ItemIdToExpandedRowMap>({});
|
||||
const [expandedRowItemIds, setExpandedRowItemIds] = useState<JobId[]>([]);
|
||||
|
||||
// use this pattern so we don't return a promise, useEffects doesn't like that
|
||||
useEffect(() => {
|
||||
|
@ -40,7 +62,7 @@ export const DataFrameJobList: SFC = () => {
|
|||
return <EuiEmptyPrompt title={<h2>Here be Data Frame dragons!</h2>} iconType="editorStrike" />;
|
||||
}
|
||||
|
||||
const columns = getColumns(getJobs, itemIdToExpandedRowMap, setItemIdToExpandedRowMap);
|
||||
const columns = getColumns(getJobs, expandedRowItemIds, setExpandedRowItemIds);
|
||||
|
||||
const sorting = {
|
||||
sort: {
|
||||
|
@ -49,6 +71,8 @@ export const DataFrameJobList: SFC = () => {
|
|||
},
|
||||
};
|
||||
|
||||
const itemIdToExpandedRowMap = getItemIdToExpandedRowMap(expandedRowItemIds, dataFrameJobs);
|
||||
|
||||
return (
|
||||
<ExpandableTable
|
||||
columns={columns}
|
||||
|
|
|
@ -12,11 +12,11 @@ import {
|
|||
DataFrameJobListRow,
|
||||
DataFrameJobState,
|
||||
DataFrameJobStats,
|
||||
jobId,
|
||||
JobId,
|
||||
} from '../common';
|
||||
|
||||
interface DataFrameJobStateStats {
|
||||
id: jobId;
|
||||
id: JobId;
|
||||
state: DataFrameJobState;
|
||||
stats: DataFrameJobStats;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue