[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:
Walter Rafelsberger 2019-05-02 14:42:36 +02:00 committed by GitHub
parent 6871157963
commit 687b8ee87a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 750 additions and 43 deletions

View file

@ -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}}

View file

@ -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>
`;

View file

@ -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",
},
]
}
/>
`;

View file

@ -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>
`;

View file

@ -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>
`;

View file

@ -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');
});
});

View file

@ -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}
/>
);

View file

@ -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');
});
});

View file

@ -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,

View file

@ -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;
}

View file

@ -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();
});
});

View file

@ -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}
/>
);
};

View file

@ -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();
});
});

View file

@ -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>
);
};

View file

@ -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();
});
});

View file

@ -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}>&nbsp;</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -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}

View file

@ -12,11 +12,11 @@ import {
DataFrameJobListRow,
DataFrameJobState,
DataFrameJobStats,
jobId,
JobId,
} from '../common';
interface DataFrameJobStateStats {
id: jobId;
id: JobId;
state: DataFrameJobState;
stats: DataFrameJobStats;
}