Skip to content

Commit

Permalink
[ML] Adding jobs stats bar to top of jobs list (#20527) (#20558)
Browse files Browse the repository at this point in the history
* [ML] Adding jobs stats bar to top of jobs list

* unsetting update function

* changing node variable name

* small refactor

* using props copy of `updateJobStats`

* adding proptypes

* adding fixed height
  • Loading branch information
jgowdyelastic authored Jul 9, 2018
1 parent b61d509 commit 42f2518
Show file tree
Hide file tree
Showing 8 changed files with 231 additions and 42 deletions.
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,14 @@
.jobs-stats-bar-new {

height: 42px;
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

0 comments on commit 42f2518

Please sign in to comment.