Skip to content

Commit 1fcea3d

Browse files
[ML] ML on Kibana Management: Add ability to pass a group ID filter to job management page (#74533) (#74713)
* handle group id in url for anomaly detection * filter analytics list by group id. * handle list of groupIds * ensure analytics can handle jobid in url. rename util function * add tests for getSelectedIdFromUrl and getGroupQueryText * keep groupIds as array of strings and jobId as single string * fix tests and update types # Conflicts: # x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js
1 parent 11cbc12 commit 1fcea3d

File tree

10 files changed

+160
-54
lines changed

10 files changed

+160
-54
lines changed

x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@ import { getTaskStateBadge, getJobTypeBadge, useColumns } from './use_columns';
3838
import { ExpandedRow } from './expanded_row';
3939
import { AnalyticStatsBarStats, StatsBar } from '../../../../../components/stats_bar';
4040
import { CreateAnalyticsButton } from '../create_analytics_button';
41-
import { getSelectedJobIdFromUrl } from '../../../../../jobs/jobs_list/components/utils';
41+
import {
42+
getSelectedIdFromUrl,
43+
getGroupQueryText,
44+
} from '../../../../../jobs/jobs_list/components/utils';
4245
import { SourceSelection } from '../source_selection';
4346

4447
function getItemIdToExpandedRowMap(
@@ -99,16 +102,22 @@ export const DataFrameAnalyticsList: FC<Props> = ({
99102

100103
// Query text/job_id based on url but only after getAnalytics is done first
101104
// selectedJobIdFromUrlInitialized makes sure the query is only run once since analytics is being refreshed constantly
102-
const [selectedJobIdFromUrlInitialized, setSelectedJobIdFromUrlInitialized] = useState(false);
105+
const [selectedIdFromUrlInitialized, setSelectedIdFromUrlInitialized] = useState(false);
103106
useEffect(() => {
104-
if (selectedJobIdFromUrlInitialized === false && analytics.length > 0) {
105-
const selectedJobIdFromUrl = getSelectedJobIdFromUrl(window.location.href);
106-
if (selectedJobIdFromUrl !== undefined) {
107-
setSelectedJobIdFromUrlInitialized(true);
108-
setSearchQueryText(selectedJobIdFromUrl);
107+
if (selectedIdFromUrlInitialized === false && analytics.length > 0) {
108+
const { jobId, groupIds } = getSelectedIdFromUrl(window.location.href);
109+
let queryText = '';
110+
111+
if (groupIds !== undefined) {
112+
queryText = getGroupQueryText(groupIds);
113+
} else if (jobId !== undefined) {
114+
queryText = jobId;
109115
}
116+
117+
setSelectedIdFromUrlInitialized(true);
118+
setSearchQueryText(queryText);
110119
}
111-
}, [selectedJobIdFromUrlInitialized, analytics]);
120+
}, [selectedIdFromUrlInitialized, analytics]);
112121

113122
// Subscribe to the refresh observable to trigger reloading the analytics list.
114123
useRefreshAnalyticsList({

x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
EuiLink,
2020
RIGHT_ALIGNMENT,
2121
} from '@elastic/eui';
22-
import { getJobIdUrl } from '../../../../../util/get_job_id_url';
22+
import { getJobIdUrl, TAB_IDS } from '../../../../../util/get_selected_ids_url';
2323

2424
import { getAnalysisType, DataFrameAnalyticsId } from '../../../../common';
2525
import {
@@ -137,7 +137,7 @@ export const progressColumn = {
137137
};
138138

139139
export const getDFAnalyticsJobIdLink = (item: DataFrameAnalyticsListRow) => (
140-
<EuiLink href={getJobIdUrl('data_frame_analytics', item.id)}>{item.id}</EuiLink>
140+
<EuiLink href={getJobIdUrl(TAB_IDS.DATA_FRAME_ANALYTICS, item.id)}>{item.id}</EuiLink>
141141
);
142142

143143
export const useColumns = (

x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.js

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import React, { Component, Fragment } from 'react';
99

1010
import { ml } from '../../../../services/ml_api_service';
1111
import { JobGroup } from '../job_group';
12-
import { getSelectedJobIdFromUrl, clearSelectedJobIdFromUrl } from '../utils';
12+
import { getGroupQueryText, getSelectedIdFromUrl, clearSelectedJobIdFromUrl } from '../utils';
1313

1414
import { EuiSearchBar, EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui';
1515
import { i18n } from '@kbn/i18n';
@@ -54,15 +54,23 @@ export class JobFilterBar extends Component {
5454

5555
componentDidMount() {
5656
// If job id is selected in url, filter table to that id
57-
const selectedId = getSelectedJobIdFromUrl(window.location.href);
58-
if (selectedId !== undefined) {
57+
let defaultQueryText;
58+
const { jobId, groupIds } = getSelectedIdFromUrl(window.location.href);
59+
60+
if (groupIds !== undefined) {
61+
defaultQueryText = getGroupQueryText(groupIds);
62+
} else if (jobId !== undefined) {
63+
defaultQueryText = jobId;
64+
}
65+
66+
if (defaultQueryText !== undefined) {
5967
this.setState(
6068
{
61-
selectedId,
69+
defaultQueryText,
6270
},
6371
() => {
6472
// trigger onChange with query for job id to trigger table filter
65-
const query = EuiSearchBar.Query.parse(selectedId);
73+
const query = EuiSearchBar.Query.parse(defaultQueryText);
6674
this.onChange({ query });
6775
}
6876
);
@@ -87,7 +95,7 @@ export class JobFilterBar extends Component {
8795
};
8896

8997
render() {
90-
const { error, selectedId } = this.state;
98+
const { error, defaultQueryText } = this.state;
9199
const filters = [
92100
{
93101
type: 'field_value_toggle_group',
@@ -147,7 +155,7 @@ export class JobFilterBar extends Component {
147155
return (
148156
<EuiFlexGroup direction="column">
149157
<EuiFlexItem data-test-subj="mlJobListSearchBar" grow={false}>
150-
{selectedId === undefined && (
158+
{defaultQueryText === undefined && (
151159
<EuiSearchBar
152160
box={{
153161
incremental: true,
@@ -157,12 +165,12 @@ export class JobFilterBar extends Component {
157165
className="mlJobFilterBar"
158166
/>
159167
)}
160-
{selectedId !== undefined && (
168+
{defaultQueryText !== undefined && (
161169
<EuiSearchBar
162170
box={{
163171
incremental: true,
164172
}}
165-
defaultQuery={selectedId}
173+
defaultQuery={defaultQueryText}
166174
filters={filters}
167175
onChange={this.onChange}
168176
className="mlJobFilterBar"

x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/job_description.js

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,28 @@ import PropTypes from 'prop-types';
88
import React from 'react';
99

1010
import { JobGroup } from '../job_group';
11+
import { getGroupIdsUrl, TAB_IDS } from '../../../../util/get_selected_ids_url';
1112

12-
export function JobDescription({ job }) {
13+
export function JobDescription({ job, isManagementTable }) {
1314
return (
1415
<React.Fragment>
1516
<div className="job-description">
1617
{job.description} &nbsp;
17-
{job.groups.map((group) => (
18-
<JobGroup key={group} name={group} />
19-
))}
18+
{job.groups.map((group) => {
19+
if (isManagementTable === true) {
20+
return (
21+
<a key={group} href={getGroupIdsUrl(TAB_IDS.ANOMALY_DETECTION, [group])}>
22+
<JobGroup name={group} />
23+
</a>
24+
);
25+
}
26+
return <JobGroup key={group} name={group} />;
27+
})}
2028
</div>
2129
</React.Fragment>
2230
);
2331
}
2432
JobDescription.propTypes = {
2533
job: PropTypes.object.isRequired,
34+
isManagementTable: PropTypes.bool,
2635
};

x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { toLocaleString } from '../../../../util/string_utils';
1414
import { ResultLinks, actionsMenuContent } from '../job_actions';
1515
import { JobDescription } from './job_description';
1616
import { JobIcon } from '../../../../components/job_message_icon';
17-
import { getJobIdUrl } from '../../../../util/get_job_id_url';
17+
import { getJobIdUrl, TAB_IDS } from '../../../../util/get_selected_ids_url';
1818
import { TIME_FORMAT } from '../../../../../../common/constants/time_format';
1919

2020
import { EuiBadge, EuiBasicTable, EuiButtonIcon, EuiLink, EuiScreenReaderOnly } from '@elastic/eui';
@@ -71,7 +71,7 @@ export class JobsList extends Component {
7171
return id;
7272
}
7373

74-
return <EuiLink href={getJobIdUrl('jobs', id)}>{id}</EuiLink>;
74+
return <EuiLink href={getJobIdUrl(TAB_IDS.ANOMALY_DETECTION, id)}>{id}</EuiLink>;
7575
}
7676

7777
getPageOfJobs(index, size, sortField, sortDirection) {
@@ -189,10 +189,9 @@ export class JobsList extends Component {
189189
sortable: true,
190190
field: 'description',
191191
'data-test-subj': 'mlJobListColumnDescription',
192-
render: (
193-
description,
194-
item // eslint-disable-line no-unused-vars
195-
) => <JobDescription job={item} />,
192+
render: (description, item) => (
193+
<JobDescription job={item} isManagementTable={isManagementTable} />
194+
),
196195
textOnly: true,
197196
width: '20%',
198197
},

x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.d.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,7 @@
33
* or more contributor license agreements. Licensed under the Elastic License;
44
* you may not use this file except in compliance with the Elastic License.
55
*/
6-
export function getSelectedJobIdFromUrl(str: string): string;
6+
7+
export function getSelectedIdFromUrl(str: string): { groupIds?: string[]; jobId?: string };
8+
export function getGroupQueryText(arr: string[]): string;
79
export function clearSelectedJobIdFromUrl(str: string): void;

x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -370,21 +370,34 @@ function getUrlVars(url) {
370370
return vars;
371371
}
372372

373-
export function getSelectedJobIdFromUrl(url) {
373+
export function getSelectedIdFromUrl(url) {
374+
const result = {};
374375
if (typeof url === 'string') {
376+
const isGroup = url.includes('groupIds');
375377
url = decodeURIComponent(url);
376-
if (url.includes('mlManagement') && url.includes('jobId')) {
378+
379+
if (url.includes('mlManagement')) {
377380
const urlParams = getUrlVars(url);
378381
const decodedJson = rison.decode(urlParams.mlManagement);
379-
return decodedJson.jobId;
382+
383+
if (isGroup) {
384+
result.groupIds = decodedJson.groupIds;
385+
} else {
386+
result.jobId = decodedJson.jobId;
387+
}
380388
}
381389
}
390+
return result;
391+
}
392+
393+
export function getGroupQueryText(groupIds) {
394+
return `groups:(${groupIds.join(' or ')})`;
382395
}
383396

384397
export function clearSelectedJobIdFromUrl(url) {
385398
if (typeof url === 'string') {
386399
url = decodeURIComponent(url);
387-
if (url.includes('mlManagement') && url.includes('jobId')) {
400+
if (url.includes('mlManagement') && (url.includes('jobId') || url.includes('groupIds'))) {
388401
const urlParams = getUrlVars(url);
389402
const clearedParams = `ml#/jobs?_g=${urlParams._g}`;
390403
window.history.replaceState({}, document.title, clearedParams);
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { getGroupQueryText, getSelectedIdFromUrl } from './utils';
8+
9+
describe('ML - Jobs List utils', () => {
10+
const jobId = 'test_job_id_1';
11+
const jobIdUrl = `http://localhost:5601/aql/app/ml#/jobs?mlManagement=(jobId:${jobId})`;
12+
const groupIdOne = 'test_group_id_1';
13+
const groupIdTwo = 'test_group_id_2';
14+
const groupIdsUrl = `http://localhost:5601/aql/app/ml#/jobs?mlManagement=(groupIds:!(${groupIdOne},${groupIdTwo}))`;
15+
const groupIdUrl = `http://localhost:5601/aql/app/ml#/jobs?mlManagement=(groupIds:!(${groupIdOne}))`;
16+
17+
describe('getSelectedIdFromUrl', () => {
18+
it('should get selected job id from the url', () => {
19+
const actual = getSelectedIdFromUrl(jobIdUrl);
20+
expect(actual).toStrictEqual({ jobId });
21+
});
22+
23+
it('should get selected group ids from the url', () => {
24+
const expected = { groupIds: [groupIdOne, groupIdTwo] };
25+
const actual = getSelectedIdFromUrl(groupIdsUrl);
26+
expect(actual).toStrictEqual(expected);
27+
});
28+
29+
it('should get selected group id from the url', () => {
30+
const expected = { groupIds: [groupIdOne] };
31+
const actual = getSelectedIdFromUrl(groupIdUrl);
32+
expect(actual).toStrictEqual(expected);
33+
});
34+
});
35+
36+
describe('getGroupQueryText', () => {
37+
it('should get query string for selected group ids', () => {
38+
const actual = getGroupQueryText([groupIdOne, groupIdTwo]);
39+
expect(actual).toBe(`groups:(${groupIdOne} or ${groupIdTwo})`);
40+
});
41+
42+
it('should get query string for selected group id', () => {
43+
const actual = getGroupQueryText([groupIdOne]);
44+
expect(actual).toBe(`groups:(${groupIdOne})`);
45+
});
46+
});
47+
});

x-pack/plugins/ml/public/application/util/get_job_id_url.ts

Lines changed: 0 additions & 20 deletions
This file was deleted.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
import rison from 'rison-node';
7+
import { getBasePath } from './dependency_cache';
8+
9+
export enum TAB_IDS {
10+
DATA_FRAME_ANALYTICS = 'data_frame_analytics',
11+
ANOMALY_DETECTION = 'jobs',
12+
}
13+
14+
function getSelectedIdsUrl(tabId: TAB_IDS, settings: { [key: string]: string | string[] }): string {
15+
// Create url for filtering by job id or group ids for kibana management table
16+
const encoded = rison.encode(settings);
17+
const url = `?mlManagement=${encoded}`;
18+
const basePath = getBasePath();
19+
20+
return `${basePath.get()}/app/ml#/${tabId}${url}`;
21+
}
22+
23+
// Create url for filtering by group ids for kibana management table
24+
export function getGroupIdsUrl(tabId: TAB_IDS, ids: string[]): string {
25+
const settings = {
26+
groupIds: ids,
27+
};
28+
29+
return getSelectedIdsUrl(tabId, settings);
30+
}
31+
32+
// Create url for filtering by job id for kibana management table
33+
export function getJobIdUrl(tabId: TAB_IDS, id: string): string {
34+
const settings = {
35+
jobId: id,
36+
};
37+
38+
return getSelectedIdsUrl(tabId, settings);
39+
}

0 commit comments

Comments
 (0)