diff --git a/x-pack/plugins/ml/public/jobs/jobs_list_new/components/edit_job_flyout/tabs/job_details.js b/x-pack/plugins/ml/public/jobs/jobs_list_new/components/edit_job_flyout/tabs/job_details.js
index 779553ef43e4d..876f70994a85a 100644
--- a/x-pack/plugins/ml/public/jobs/jobs_list_new/components/edit_job_flyout/tabs/job_details.js
+++ b/x-pack/plugins/ml/public/jobs/jobs_list_new/components/edit_job_flyout/tabs/job_details.js
@@ -27,6 +27,7 @@ export class JobDetails extends Component {
this.state = {
description: '',
+ groups: [],
selectedGroups: [],
mml: '',
};
diff --git a/x-pack/plugins/ml/public/jobs/jobs_list_new/components/job_filter_bar/index.js b/x-pack/plugins/ml/public/jobs/jobs_list_new/components/job_filter_bar/index.js
new file mode 100644
index 0000000000000..dbf62a8b55162
--- /dev/null
+++ b/x-pack/plugins/ml/public/jobs/jobs_list_new/components/job_filter_bar/index.js
@@ -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 { JobFilterBar } from './job_filter_bar';
diff --git a/x-pack/plugins/ml/public/jobs/jobs_list_new/components/job_filter_bar/job_filter_bar.js b/x-pack/plugins/ml/public/jobs/jobs_list_new/components/job_filter_bar/job_filter_bar.js
new file mode 100644
index 0000000000000..297a27c869384
--- /dev/null
+++ b/x-pack/plugins/ml/public/jobs/jobs_list_new/components/job_filter_bar/job_filter_bar.js
@@ -0,0 +1,111 @@
+/*
+ * 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 PropTypes from 'prop-types';
+import React, {
+ Component
+} from 'react';
+
+import { ml } from 'plugins/ml/services/ml_api_service';
+import { JobGroup } from '../job_group';
+
+import './styles/main.less';
+
+import {
+ EuiSearchBar,
+} from '@elastic/eui';
+
+function loadGroups() {
+ return ml.jobs.groups()
+ .then((groups) => {
+ return groups.map(g => ({
+ value: g.id,
+ view: (
+
+ ({g.jobIds.length} job{(g.jobIds.length === 1) ? '' : 's'})
+
+ )
+ }));
+ })
+ .catch((error) => {
+ console.log(error);
+ return [];
+ });
+}
+
+export class JobFilterBar extends Component {
+ constructor(props) {
+ super(props);
+
+ this.setFilters = props.setFilters;
+ }
+
+ onChange = ({ query }) => {
+ const clauses = query.ast.clauses;
+ this.setFilters(clauses);
+ };
+
+ render() {
+ const filters = [
+ {
+ type: 'field_value_toggle_group',
+ field: 'job_state',
+ items: [
+ {
+ value: 'opened',
+ name: 'Opened'
+ },
+ {
+ value: 'closed',
+ name: 'Closed'
+ },
+ {
+ value: 'failed',
+ name: 'Failed'
+ }
+ ]
+ },
+ {
+ type: 'field_value_toggle_group',
+ field: 'datafeed_state',
+ items: [
+ {
+ value: 'started',
+ name: 'Started'
+ },
+ {
+ value: 'stopped',
+ name: 'Stopped'
+ }
+ ]
+ },
+ {
+ type: 'field_value_selection',
+ field: 'groups',
+ name: 'Group',
+ multiSelect: 'or',
+ cache: 10000,
+ options: () => loadGroups()
+ }
+
+ ];
+
+ return (
+
+ );
+ }
+}
+JobFilterBar.propTypes = {
+ setFilters: PropTypes.func.isRequired,
+};
+
diff --git a/x-pack/plugins/ml/public/jobs/jobs_list_new/components/job_filter_bar/styles/main.less b/x-pack/plugins/ml/public/jobs/jobs_list_new/components/job_filter_bar/styles/main.less
new file mode 100644
index 0000000000000..18259dfd675f1
--- /dev/null
+++ b/x-pack/plugins/ml/public/jobs/jobs_list_new/components/job_filter_bar/styles/main.less
@@ -0,0 +1,22 @@
+.euiFilterGroup {
+ max-width: 500px;
+
+ .euiPopover .euiPanel {
+ .group-item {
+ padding: 6px 12px;
+ }
+
+ .inline-group {
+ border: 1px solid #FFFFFF;
+ border-radius: 3px;
+ }
+
+ .euiFilterSelectItem:hover, .euiFilterSelectItem:focus {
+ text-decoration: none;
+ .inline-group {
+ border: 1px solid #555555;
+ box-shadow: 0px 1px 2px #999;
+ }
+ }
+ }
+}
diff --git a/x-pack/plugins/ml/public/jobs/jobs_list_new/components/job_group/index.js b/x-pack/plugins/ml/public/jobs/jobs_list_new/components/job_group/index.js
new file mode 100644
index 0000000000000..3b53348dd41f9
--- /dev/null
+++ b/x-pack/plugins/ml/public/jobs/jobs_list_new/components/job_group/index.js
@@ -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 { JobGroup } from './job_group';
diff --git a/x-pack/plugins/ml/public/jobs/jobs_list_new/components/job_group/job_group.js b/x-pack/plugins/ml/public/jobs/jobs_list_new/components/job_group/job_group.js
new file mode 100644
index 0000000000000..cf60ea1be5791
--- /dev/null
+++ b/x-pack/plugins/ml/public/jobs/jobs_list_new/components/job_group/job_group.js
@@ -0,0 +1,70 @@
+/*
+ * 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 PropTypes from 'prop-types';
+import React from 'react';
+
+import './styles/main.less';
+
+const COLORS = [
+ '#00B3A4', // euiColorVis0
+ '#3185FC', // euiColorVis1
+ '#DB1374', // euiColorVis2
+ '#490092', // euiColorVis3
+ // '#FEB6DB', // euiColorVis4 light pink, too hard to read with white text
+ '#E6C220', // euiColorVis5
+ '#BFA180', // euiColorVis6
+ '#F98510', // euiColorVis7
+ '#461A0A', // euiColorVis8
+ '#920000', // euiColorVis9
+
+ '#666666', // euiColorDarkShade
+ '#0079A5', // euiColorPrimary
+];
+
+const colorMap = {};
+
+export function JobGroup({ name }) {
+ return (
+
+ {name}
+
+ );
+}
+JobGroup.propTypes = {
+ name: PropTypes.string.isRequired,
+};
+
+// to ensure the same color is always used for a group name
+// the color choice is based on a hash of the group name
+function tabColor(name) {
+ if (colorMap[name] === undefined) {
+ const n = stringHash(name);
+ const color = COLORS[(n % COLORS.length)];
+ colorMap[name] = color;
+ return color;
+ } else {
+ return colorMap[name];
+ }
+}
+
+function stringHash(str) {
+ let hash = 0;
+ let chr = '';
+ if (str.length === 0) {
+ return hash;
+ }
+ for (let i = 0; i < str.length; i++) {
+ chr = str.charCodeAt(i);
+ hash = ((hash << 5) - hash) + chr;
+ hash |= 0;
+ }
+ return hash < 0 ? hash * -2 : hash;
+}
diff --git a/x-pack/plugins/ml/public/jobs/jobs_list_new/components/job_group/styles/main.less b/x-pack/plugins/ml/public/jobs/jobs_list_new/components/job_group/styles/main.less
new file mode 100644
index 0000000000000..333dd78a266c2
--- /dev/null
+++ b/x-pack/plugins/ml/public/jobs/jobs_list_new/components/job_group/styles/main.less
@@ -0,0 +1,10 @@
+.inline-group {
+ font-size: 12px;
+ background-color: #D9D9D9;
+ padding: 2px 5px;
+ border-radius: 2px;
+ display: inline-block;
+ margin: 0px 3px;
+ color: #FFFFFF;
+ vertical-align: text-top;
+}
diff --git a/x-pack/plugins/ml/public/jobs/jobs_list_new/components/jobs_list/job_description.js b/x-pack/plugins/ml/public/jobs/jobs_list_new/components/jobs_list/job_description.js
index 3737c337b6bf2..58384bedffdb5 100644
--- a/x-pack/plugins/ml/public/jobs/jobs_list_new/components/jobs_list/job_description.js
+++ b/x-pack/plugins/ml/public/jobs/jobs_list_new/components/jobs_list/job_description.js
@@ -8,23 +8,7 @@
import PropTypes from 'prop-types';
import React from 'react';
-const COLORS = [
- '#00B3A4', // euiColorVis0
- '#3185FC', // euiColorVis1
- '#DB1374', // euiColorVis2
- '#490092', // euiColorVis3
- // '#FEB6DB', // euiColorVis4 light pink, too hard to read with white text
- '#E6C220', // euiColorVis5
- '#BFA180', // euiColorVis6
- '#F98510', // euiColorVis7
- '#461A0A', // euiColorVis8
- '#920000', // euiColorVis9
-
- '#666666', // euiColorDarkShade
- '#0079A5', // euiColorPrimary
-];
-
-const colorMap = {};
+import { JobGroup } from '../job_group';
export function JobDescription({ job }) {
return (
@@ -42,44 +26,3 @@ export function JobDescription({ job }) {
JobDescription.propTypes = {
job: PropTypes.object.isRequired,
};
-
-function JobGroup({ name }) {
- return (
-
- {name}
-
- );
-}
-JobGroup.propTypes = {
- name: PropTypes.string.isRequired,
-};
-
-// to ensure the same color is always used for a group name
-// the color choice is based on a hash of the group name
-function tabColor(name) {
- if (colorMap[name] === undefined) {
- const n = stringHash(name);
- const color = COLORS[(n % COLORS.length)];
- colorMap[name] = color;
- return color;
- } else {
- return colorMap[name];
- }
-}
-
-function stringHash(str) {
- let hash = 0;
- let chr = '';
- if (str.length === 0) {
- return hash;
- }
- for (let i = 0; i < str.length; i++) {
- chr = str.charCodeAt(i);
- hash = ((hash << 5) - hash) + chr;
- hash |= 0;
- }
- return hash < 0 ? hash * -2 : hash;
-}
diff --git a/x-pack/plugins/ml/public/jobs/jobs_list_new/components/jobs_list/jobs_list.js b/x-pack/plugins/ml/public/jobs/jobs_list_new/components/jobs_list/jobs_list.js
index 88927adfdcc38..cd547feee49a5 100644
--- a/x-pack/plugins/ml/public/jobs/jobs_list_new/components/jobs_list/jobs_list.js
+++ b/x-pack/plugins/ml/public/jobs/jobs_list_new/components/jobs_list/jobs_list.js
@@ -76,7 +76,18 @@ export class JobsList extends Component {
list = sortBy(this.state.jobsSummaryList, (item) => item[sortField]);
list = (sortDirection === 'asc') ? list : list.reverse();
- const pageStart = (index * size);
+ let pageStart = (index * size);
+ if (pageStart >= list.length) {
+ // if the page start is larger than the number of items
+ // due to filters being applied, calculate a new page start
+ pageStart = Math.floor(list.length / size) * size;
+ // set the state out of the render cycle
+ setTimeout(() => {
+ this.setState({
+ pageIndex: (pageStart / size)
+ });
+ }, 0);
+ }
return {
pageOfItems: list.slice(pageStart, (pageStart + size)),
totalItemCount: list.length,
diff --git a/x-pack/plugins/ml/public/jobs/jobs_list_new/components/jobs_list/styles/main.less b/x-pack/plugins/ml/public/jobs/jobs_list_new/components/jobs_list/styles/main.less
index 2806197456c3f..3a83fa66e0c92 100644
--- a/x-pack/plugins/ml/public/jobs/jobs_list_new/components/jobs_list/styles/main.less
+++ b/x-pack/plugins/ml/public/jobs/jobs_list_new/components/jobs_list/styles/main.less
@@ -92,17 +92,6 @@
display: inline-block;
}
- .inline-group {
- font-size: 12px;
- background-color: #D9D9D9;
- padding: 2px 5px;
- border-radius: 2px;
- display: inline-block;
- margin: 0px 3px;
- color: #FFFFFF;
- vertical-align: text-top;
- }
-
.job-loading-spinner {
text-align: center;
}
diff --git a/x-pack/plugins/ml/public/jobs/jobs_list_new/components/jobs_list_view/jobs_list_view.js b/x-pack/plugins/ml/public/jobs/jobs_list_new/components/jobs_list_view/jobs_list_view.js
index ef9c178018e1a..d413604eae75e 100644
--- a/x-pack/plugins/ml/public/jobs/jobs_list_new/components/jobs_list_view/jobs_list_view.js
+++ b/x-pack/plugins/ml/public/jobs/jobs_list_new/components/jobs_list_view/jobs_list_view.js
@@ -8,9 +8,10 @@
import './styles/main.less';
import { ml } from 'plugins/ml/services/ml_api_service';
-import { loadFullJob } from '../utils';
+import { loadFullJob, filterJobs } from '../utils';
import { JobsList } from '../jobs_list';
import { JobDetails } from '../job_details';
+import { JobFilterBar } from '../job_filter_bar';
import { EditJobFlyout } from '../edit_job_flyout';
import { DeleteJobModal } from '../delete_job_modal';
import { StartDatafeedModal } from '../start_datafeed_modal';
@@ -26,9 +27,11 @@ export class JobsListView extends Component {
this.state = {
jobsSummaryList: [],
+ filteredJobsSummaryList: [],
fullJobsList: {},
selectedJobs: [],
- itemIdToExpandedRowMap: {}
+ itemIdToExpandedRowMap: {},
+ filterClauses: []
};
this.updateFunctions = {};
@@ -134,6 +137,26 @@ export class JobsListView extends Component {
this.setState({ selectedJobs });
}
+ refreshSelectedJobs() {
+ const selectedJobsIds = this.state.selectedJobs.map(j => j.id);
+ const filteredJobIds = this.state.filteredJobsSummaryList.map(j => j.id);
+
+ // refresh the jobs stored as selected
+ // only select those which are also in the filtered list
+ const selectedJobs = this.state.jobsSummaryList
+ .filter(j => selectedJobsIds.find(id => id === j.id))
+ .filter(j => filteredJobIds.find(id => id === j.id));
+
+ this.setState({ selectedJobs });
+ }
+
+ setFilters = (filterClauses) => {
+ const filteredJobsSummaryList = filterJobs(this.state.jobsSummaryList, filterClauses);
+ this.setState({ filteredJobsSummaryList, filterClauses }, () => {
+ this.refreshSelectedJobs();
+ });
+ }
+
refreshJobSummaryList(autoRefresh = true) {
if (this.blockAutoRefresh === false) {
const expandedJobsIds = Object.keys(this.state.itemIdToExpandedRowMap);
@@ -148,7 +171,10 @@ export class JobsListView extends Component {
job.latestTimeStampUnix = job.latestTimeStamp.unix;
return job;
});
- this.setState({ jobsSummaryList, fullJobsList });
+ const filteredJobsSummaryList = filterJobs(jobsSummaryList, this.state.filterClauses);
+ this.setState({ jobsSummaryList, filteredJobsSummaryList, fullJobsList }, () => {
+ this.refreshSelectedJobs();
+ });
Object.keys(this.updateFunctions).forEach((j) => {
this.updateFunctions[j].setState({ job: fullJobsList[j] });
@@ -176,9 +202,10 @@ export class JobsListView extends Component {
showDeleteJobModal={this.showDeleteJobModal}
refreshJobs={() => this.refreshJobSummaryList(false)}
/>
+
div:nth-child(1) {
+ width: 300px;
+ }
+ & > div:nth-child(2) {
+ }
}
diff --git a/x-pack/plugins/ml/public/jobs/jobs_list_new/components/multi_job_actions/styles/main.less b/x-pack/plugins/ml/public/jobs/jobs_list_new/components/multi_job_actions/styles/main.less
index 4a0ed99bcf551..7cbabafadf84f 100644
--- a/x-pack/plugins/ml/public/jobs/jobs_list_new/components/multi_job_actions/styles/main.less
+++ b/x-pack/plugins/ml/public/jobs/jobs_list_new/components/multi_job_actions/styles/main.less
@@ -1,5 +1,5 @@
.multi-select-actions {
- padding: 20px 0px;
+ padding: 10px 0px;
display: inline-block;
.jobs-selected-title {
diff --git a/x-pack/plugins/ml/public/jobs/jobs_list_new/components/utils.js b/x-pack/plugins/ml/public/jobs/jobs_list_new/components/utils.js
index 67c2416a4f439..e90d42be18651 100644
--- a/x-pack/plugins/ml/public/jobs/jobs_list_new/components/utils.js
+++ b/x-pack/plugins/ml/public/jobs/jobs_list_new/components/utils.js
@@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { each } from 'lodash';
import { toastNotifications } from 'ui/notify';
import { mlJobService } from 'plugins/ml/services/job_service';
@@ -136,3 +137,77 @@ export function deleteJobs(jobs, finish) {
}
});
}
+
+export function filterJobs(jobs, clauses) {
+ if (clauses.length === 0) {
+ return jobs;
+ }
+
+ // keep count of the number of matches we make as we're looping over the clauses
+ // we only want to return jobs which match all clauses, i.e. each search term is ANDed
+ const matches = jobs.reduce((p, c) => {
+ p[c.id] = {
+ job: c,
+ count: 0
+ };
+ return p;
+ }, {});
+
+ clauses.forEach((c) => {
+ // the search term could be negated with a minus, e.g. -bananas
+ const bool = (c.match === 'must');
+ let js = [];
+
+ if (c.type === 'term') {
+ // filter term based clauses, e.g. bananas
+ // match on id, description and memory_status
+ // if the term has been negated, AND the matches
+ if (bool === true) {
+ js = jobs.filter(job => ((
+ (stringMatch(job.id, c.value) === bool) ||
+ (stringMatch(job.description, c.value) === bool) ||
+ (stringMatch(job.memory_status, c.value) === bool)
+ )));
+ } else {
+ js = jobs.filter(job => ((
+ (stringMatch(job.id, c.value) === bool) &&
+ (stringMatch(job.description, c.value) === bool) &&
+ (stringMatch(job.memory_status, c.value) === bool)
+ )));
+ }
+ } else {
+ // filter other clauses, i.e. the toggle group buttons
+ if (Array.isArray(c.value)) {
+ // the groups value is an array of group ids
+ js = jobs.filter(job => (jobProperty(job, c.field).some(g => (c.value.indexOf(g) >= 0))));
+ } else {
+ js = jobs.filter(job => (jobProperty(job, c.field) === c.value));
+ }
+ }
+
+ js.forEach(j => (matches[j.id].count++));
+ });
+
+ // loop through the matches and return only those jobs which have match all the clauses
+ const filteredJobs = [];
+ each(matches, (m) => {
+ if (m.count >= clauses.length) {
+ filteredJobs.push(m.job);
+ }
+ });
+ return filteredJobs;
+}
+
+function stringMatch(str, substr) {
+ return ((str.toLowerCase().match(substr.toLowerCase()) === null) === false);
+}
+
+function jobProperty(job, prop) {
+ const propMap = {
+ job_state: 'jobState',
+ datafeed_state: 'datafeedState',
+ groups: 'groups',
+ };
+ return job[propMap[prop]];
+}
+
diff --git a/x-pack/plugins/ml/server/models/job_service/datafeeds.js b/x-pack/plugins/ml/server/models/job_service/datafeeds.js
index e1ee4f1955bc2..7caa8ff1a3175 100644
--- a/x-pack/plugins/ml/server/models/job_service/datafeeds.js
+++ b/x-pack/plugins/ml/server/models/job_service/datafeeds.js
@@ -43,8 +43,12 @@ export function datafeedsProvider(callWithRequest) {
results[datafeedId] = await doStart(datafeedId);
}, START_TIMEOUT);
- if (await openJob(jobId)) {
- results[datafeedId] = await doStart(datafeedId);
+ try {
+ if (await openJob(jobId)) {
+ results[datafeedId] = await doStart(datafeedId);
+ }
+ } catch (error) {
+ results[datafeedId] = { started: false, error };
}
}
@@ -59,6 +63,8 @@ export function datafeedsProvider(callWithRequest) {
} catch (error) {
if (error.statusCode === 409) {
opened = true;
+ } else {
+ throw error;
}
}
return opened;