Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ML] Adding jobs stats bar to top of jobs list #20527

Merged
merged 7 commits into from
Jul 9, 2018
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { DeleteJobModal } from '../delete_job_modal';
import { StartDatafeedModal } from '../start_datafeed_modal';
import { MultiJobActions } from '../multi_job_actions';

import PropTypes from 'prop-types';
import React, {
Component
} from 'react';
Expand All @@ -35,7 +36,7 @@ export class JobsListView extends Component {
fullJobsList: {},
selectedJobs: [],
itemIdToExpandedRowMap: {},
filterClauses: []
filterClauses: [],
};

this.updateFunctions = {};
Expand Down Expand Up @@ -227,6 +228,7 @@ export class JobsListView extends Component {
const filteredJobsSummaryList = filterJobs(jobsSummaryList, this.state.filterClauses);
this.setState({ jobsSummaryList, filteredJobsSummaryList, fullJobsList }, () => {
this.refreshSelectedJobs();
this.props.updateJobStats(jobsSummaryList);
});

Object.keys(this.updateFunctions).forEach((j) => {
Expand Down Expand Up @@ -281,3 +283,6 @@ export class JobsListView extends Component {
);
}
}
JobsListView.propTypes = {
updateJobStats: PropTypes.func.isRequired,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* 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 { JobStatsBar } from './jobs_stats_bar';
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* 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 './styles/main.less';
import { JOB_STATE, DATAFEED_STATE } from 'plugins/ml/../common/constants/states';

import PropTypes from 'prop-types';
import React, {
Component,
} from 'react';

function createJobStats(jobsSummaryList) {

const jobStats = {
activeNodes: { label: 'Active ML Nodes', value: 0, show: true },
total: { label: 'Total jobs', value: 0, show: true },
open: { label: 'Open jobs', value: 0, show: true },
closed: { label: 'Closed jobs', value: 0, show: true },
failed: { label: 'Failed jobs', value: 0, show: false },
activeDatafeeds: { label: 'Active datafeeds', value: 0, show: true }
};

if (jobsSummaryList === undefined) {
return jobStats;
}

// object to keep track of nodes being used by jobs
const mlNodes = {};
let failedJobs = 0;

jobsSummaryList.forEach((job) => {
if (job.jobState === JOB_STATE.OPENED) {
jobStats.open.value++;
} else if (job.jobState === JOB_STATE.CLOSED) {
jobStats.closed.value++;
} else if (job.jobState === JOB_STATE.FAILED) {
failedJobs++;
}

if (job.hasDatafeed && job.datafeedState === DATAFEED_STATE.STARTED) {
jobStats.activeDatafeeds.value++;
}

if (job.nodeName !== undefined) {
mlNodes[job.nodeName] = {};
}
});

jobStats.total.value = jobsSummaryList.length;

// // Only show failed jobs if it is non-zero
if (failedJobs) {
jobStats.failed.value = failedJobs;
jobStats.failed.show = true;
} else {
jobStats.failed.show = false;
}

jobStats.activeNodes.value = Object.keys(mlNodes).length;

return jobStats;
}

function Stat({ stat }) {
return (
<span className="stat">
<span className="stat-label">{stat.label}</span>: <span className="stat-value">{stat.value}</span>
</span>
);
}
Stat.propTypes = {
stat: PropTypes.object.isRequired,
};

export class JobStatsBar extends Component {
constructor(props) {
super(props);
this.state = {
jobsSummaryList: [],
jobStats: {},
};
}

updateJobStats = (jobsSummaryList) => {
const jobStats = createJobStats(jobsSummaryList);
this.setState({
jobsSummaryList,
jobStats,
});
};

componentDidMount() {
this.props.setUpdateJobStats(this.updateJobStats);
}

componentWillUnmount() {
this.props.unsetUpdateJobStats();
}

render() {
const { jobStats } = this.state;
const stats = Object.keys(jobStats).map(k => jobStats[k]);

return (
<div className="jobs-stats-bar-new">
{
stats.filter(s => (s.show)).map(s => <Stat key={s.label} stat={s} />)
}
</div>
);
}
}
JobStatsBar.propTypes = {
setUpdateJobStats: PropTypes.func.isRequired,
unsetUpdateJobStats: PropTypes.func.isRequired,
};

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.jobs-stats-bar-new {

padding: 14px;
background-color: #EFF0F1;

.stat {
margin-right: 10px;
.stat-label {}
.stat-value {
font-weight: bold
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* 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 { NodeAvailableWarning } from './node_available_warning';
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* 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 { mlNodesAvailable, permissionToViewMlNodeCount } from 'plugins/ml/ml_nodes_check/check_ml_nodes';

import React from 'react';

import {
EuiCallOut,
EuiLink,
EuiSpacer,
} from '@elastic/eui';

export function NodeAvailableWarning() {
const isCloud = false; // placeholder for future specific cloud functionality
if ((mlNodesAvailable() === true) || (permissionToViewMlNodeCount() === false)) {
return (<span />);
} else {
return (
<React.Fragment>
<EuiCallOut
title="No ML nodes available"
color="warning"
iconType="alert"
>
<p>
There are no ML nodes available.<br />
You will not be able to create or run jobs.
{isCloud &&
<span ng-if="isCloud">
&nbsp;This can be configured in Cloud <EuiLink href="#">here</EuiLink>.
</span>
}
</p>
</EuiCallOut>
<EuiSpacer size="m" />
</React.Fragment>
);
}
}
70 changes: 29 additions & 41 deletions x-pack/plugins/ml/public/jobs/jobs_list_new/jobs.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,69 +8,57 @@
import './styles/main.less';
import { NewJobButton } from './components/new_job_button';
import { JobsListView } from './components/jobs_list_view';
import { mlNodesAvailable, permissionToViewMlNodeCount } from 'plugins/ml/ml_nodes_check/check_ml_nodes';
import { JobStatsBar } from './components/jobs_stats_bar';
import { NodeAvailableWarning } from './components/node_available_warning';

import React, {
Component
} from 'react';

import {
EuiCallOut,
EuiLink,
EuiSpacer,
} from '@elastic/eui';

function NodeAvailableWarning() {
const isCloud = false; // placeholder for future specific cloud functionality
if ((mlNodesAvailable() === true) || (permissionToViewMlNodeCount() === false)) {
return (<span />);
} else {
return (
<React.Fragment>
<EuiCallOut
title="No ML nodes available"
color="warning"
iconType="alert"
>
<p>
There are no ML nodes available.<br />
You will not be able to create or run jobs.
{isCloud &&
<span ng-if="isCloud">
&nbsp;This can be configured in Cloud <EuiLink href="#">here</EuiLink>.
</span>
}
</p>
</EuiCallOut>
<EuiSpacer size="m" />
</React.Fragment>
);
}
}

export class JobsPage extends Component {
constructor(props) {
super(props);
this.state = {
jobsSummaryList: [],
updateJobStats: () => {},
};
}

setUpdateJobStats = (updateJobStats) => {
this.setState({ updateJobStats });
}

unsetUpdateJobStats = () => {
this.setUpdateJobStats(() => {});
}

render() {
return (
<div className="job-management">
<NodeAvailableWarning />
<header>
<div className="new-job-button-container">
<NewJobButton />
</div>
</header>
<React.Fragment>
<JobStatsBar
setUpdateJobStats={this.setUpdateJobStats}
unsetUpdateJobStats={this.unsetUpdateJobStats}
/>
<div className="job-management">
<NodeAvailableWarning />
<header>
<div className="new-job-button-container">
<NewJobButton />
</div>
</header>

<div className="clear" />
<div className="clear" />

<EuiSpacer size="s" />
<EuiSpacer size="s" />

<JobsListView />
</div>
<JobsListView updateJobStats={this.state.updateJobStats} />
</div>
</React.Fragment>
);
}
}
1 change: 1 addition & 0 deletions x-pack/plugins/ml/server/models/job_service/jobs.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export function jobsProvider(callWithRequest) {
datafeedState: (hasDatafeed && job.datafeed_config.state) ? job.datafeed_config.state : '',
latestTimeStamp,
earliestTimeStamp,
nodeName: (job.node) ? job.node.name : undefined,
};
if (jobIds.find(j => (j === tempJob.id))) {
tempJob.fullJob = job;
Expand Down