From 15edbc368fcfd7d8c43614f789df4c0ac27ff142 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Mon, 10 Aug 2020 17:18:36 -0400 Subject: [PATCH] [ML] ML on Kibana Management: Add ability to pass a group ID filter to job management page (#74533) * 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 --- .../analytics_list/analytics_list.tsx | 25 ++++++---- .../components/analytics_list/use_columns.tsx | 4 +- .../job_filter_bar/job_filter_bar.js | 26 ++++++---- .../components/jobs_list/job_description.js | 17 +++++-- .../components/jobs_list/jobs_list.js | 11 ++--- .../jobs/jobs_list/components/utils.d.ts | 4 +- .../jobs/jobs_list/components/utils.js | 21 +++++++-- .../jobs/jobs_list/components/utils.test.ts | 47 +++++++++++++++++++ .../public/application/util/get_job_id_url.ts | 20 -------- .../application/util/get_selected_ids_url.ts | 39 +++++++++++++++ 10 files changed, 160 insertions(+), 54 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.test.ts delete mode 100644 x-pack/plugins/ml/public/application/util/get_job_id_url.ts create mode 100644 x-pack/plugins/ml/public/application/util/get_selected_ids_url.ts diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx index 90e24f6da5d0a1..0652ec5f8acb17 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx @@ -38,7 +38,10 @@ import { getTaskStateBadge, getJobTypeBadge, useColumns } from './use_columns'; import { ExpandedRow } from './expanded_row'; import { AnalyticStatsBarStats, StatsBar } from '../../../../../components/stats_bar'; import { CreateAnalyticsButton } from '../create_analytics_button'; -import { getSelectedJobIdFromUrl } from '../../../../../jobs/jobs_list/components/utils'; +import { + getSelectedIdFromUrl, + getGroupQueryText, +} from '../../../../../jobs/jobs_list/components/utils'; import { SourceSelection } from '../source_selection'; function getItemIdToExpandedRowMap( @@ -99,16 +102,22 @@ export const DataFrameAnalyticsList: FC = ({ // Query text/job_id based on url but only after getAnalytics is done first // selectedJobIdFromUrlInitialized makes sure the query is only run once since analytics is being refreshed constantly - const [selectedJobIdFromUrlInitialized, setSelectedJobIdFromUrlInitialized] = useState(false); + const [selectedIdFromUrlInitialized, setSelectedIdFromUrlInitialized] = useState(false); useEffect(() => { - if (selectedJobIdFromUrlInitialized === false && analytics.length > 0) { - const selectedJobIdFromUrl = getSelectedJobIdFromUrl(window.location.href); - if (selectedJobIdFromUrl !== undefined) { - setSelectedJobIdFromUrlInitialized(true); - setSearchQueryText(selectedJobIdFromUrl); + if (selectedIdFromUrlInitialized === false && analytics.length > 0) { + const { jobId, groupIds } = getSelectedIdFromUrl(window.location.href); + let queryText = ''; + + if (groupIds !== undefined) { + queryText = getGroupQueryText(groupIds); + } else if (jobId !== undefined) { + queryText = jobId; } + + setSelectedIdFromUrlInitialized(true); + setSearchQueryText(queryText); } - }, [selectedJobIdFromUrlInitialized, analytics]); + }, [selectedIdFromUrlInitialized, analytics]); // Subscribe to the refresh observable to trigger reloading the analytics list. useRefreshAnalyticsList({ diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx index 9ed87ff9f8312d..7001681b6917aa 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx @@ -19,7 +19,7 @@ import { EuiLink, RIGHT_ALIGNMENT, } from '@elastic/eui'; -import { getJobIdUrl } from '../../../../../util/get_job_id_url'; +import { getJobIdUrl, TAB_IDS } from '../../../../../util/get_selected_ids_url'; import { getAnalysisType, DataFrameAnalyticsId } from '../../../../common'; import { @@ -137,7 +137,7 @@ export const progressColumn = { }; export const getDFAnalyticsJobIdLink = (item: DataFrameAnalyticsListRow) => ( - {item.id} + {item.id} ); export const useColumns = ( diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.js index b274a8d572adb5..6eb7b00e5620c6 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.js @@ -9,7 +9,7 @@ import React, { Component, Fragment } from 'react'; import { ml } from '../../../../services/ml_api_service'; import { JobGroup } from '../job_group'; -import { getSelectedJobIdFromUrl, clearSelectedJobIdFromUrl } from '../utils'; +import { getGroupQueryText, getSelectedIdFromUrl, clearSelectedJobIdFromUrl } from '../utils'; import { EuiSearchBar, EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -54,15 +54,23 @@ export class JobFilterBar extends Component { componentDidMount() { // If job id is selected in url, filter table to that id - const selectedId = getSelectedJobIdFromUrl(window.location.href); - if (selectedId !== undefined) { + let defaultQueryText; + const { jobId, groupIds } = getSelectedIdFromUrl(window.location.href); + + if (groupIds !== undefined) { + defaultQueryText = getGroupQueryText(groupIds); + } else if (jobId !== undefined) { + defaultQueryText = jobId; + } + + if (defaultQueryText !== undefined) { this.setState( { - selectedId, + defaultQueryText, }, () => { // trigger onChange with query for job id to trigger table filter - const query = EuiSearchBar.Query.parse(selectedId); + const query = EuiSearchBar.Query.parse(defaultQueryText); this.onChange({ query }); } ); @@ -87,7 +95,7 @@ export class JobFilterBar extends Component { }; render() { - const { error, selectedId } = this.state; + const { error, defaultQueryText } = this.state; const filters = [ { type: 'field_value_toggle_group', @@ -147,7 +155,7 @@ export class JobFilterBar extends Component { return ( - {selectedId === undefined && ( + {defaultQueryText === undefined && ( )} - {selectedId !== undefined && ( + {defaultQueryText !== undefined && (
{job.description}   - {job.groups.map((group) => ( - - ))} + {job.groups.map((group) => { + if (isManagementTable === true) { + return ( + + + + ); + } + return ; + })}
); } JobDescription.propTypes = { job: PropTypes.object.isRequired, + isManagementTable: PropTypes.bool, }; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js index 7ac0831234d6b6..c93d47c4f8a3d2 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js @@ -14,7 +14,7 @@ import { toLocaleString } from '../../../../util/string_utils'; import { ResultLinks, actionsMenuContent } from '../job_actions'; import { JobDescription } from './job_description'; import { JobIcon } from '../../../../components/job_message_icon'; -import { getJobIdUrl } from '../../../../util/get_job_id_url'; +import { getJobIdUrl, TAB_IDS } from '../../../../util/get_selected_ids_url'; import { TIME_FORMAT } from '../../../../../../common/constants/time_format'; import { EuiBadge, EuiBasicTable, EuiButtonIcon, EuiLink, EuiScreenReaderOnly } from '@elastic/eui'; @@ -71,7 +71,7 @@ export class JobsList extends Component { return id; } - return {id}; + return {id}; } getPageOfJobs(index, size, sortField, sortDirection) { @@ -189,10 +189,9 @@ export class JobsList extends Component { sortable: true, field: 'description', 'data-test-subj': 'mlJobListColumnDescription', - render: ( - description, - item // eslint-disable-line no-unused-vars - ) => , + render: (description, item) => ( + + ), textOnly: true, width: '20%', }, diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.d.ts b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.d.ts index 5f72d155cbd5a5..cf4fad9513de51 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.d.ts +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.d.ts @@ -3,5 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -export function getSelectedJobIdFromUrl(str: string): string; + +export function getSelectedIdFromUrl(str: string): { groupIds?: string[]; jobId?: string }; +export function getGroupQueryText(arr: string[]): string; export function clearSelectedJobIdFromUrl(str: string): void; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js index 6fabd0299a936f..913727bda67df0 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js @@ -370,21 +370,34 @@ function getUrlVars(url) { return vars; } -export function getSelectedJobIdFromUrl(url) { +export function getSelectedIdFromUrl(url) { + const result = {}; if (typeof url === 'string') { + const isGroup = url.includes('groupIds'); url = decodeURIComponent(url); - if (url.includes('mlManagement') && url.includes('jobId')) { + + if (url.includes('mlManagement')) { const urlParams = getUrlVars(url); const decodedJson = rison.decode(urlParams.mlManagement); - return decodedJson.jobId; + + if (isGroup) { + result.groupIds = decodedJson.groupIds; + } else { + result.jobId = decodedJson.jobId; + } } } + return result; +} + +export function getGroupQueryText(groupIds) { + return `groups:(${groupIds.join(' or ')})`; } export function clearSelectedJobIdFromUrl(url) { if (typeof url === 'string') { url = decodeURIComponent(url); - if (url.includes('mlManagement') && url.includes('jobId')) { + if (url.includes('mlManagement') && (url.includes('jobId') || url.includes('groupIds'))) { const urlParams = getUrlVars(url); const clearedParams = `ml#/jobs?_g=${urlParams._g}`; window.history.replaceState({}, document.title, clearedParams); diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.test.ts b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.test.ts new file mode 100644 index 00000000000000..e4c3c21c5a54a7 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.test.ts @@ -0,0 +1,47 @@ +/* + * 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 { getGroupQueryText, getSelectedIdFromUrl } from './utils'; + +describe('ML - Jobs List utils', () => { + const jobId = 'test_job_id_1'; + const jobIdUrl = `http://localhost:5601/aql/app/ml#/jobs?mlManagement=(jobId:${jobId})`; + const groupIdOne = 'test_group_id_1'; + const groupIdTwo = 'test_group_id_2'; + const groupIdsUrl = `http://localhost:5601/aql/app/ml#/jobs?mlManagement=(groupIds:!(${groupIdOne},${groupIdTwo}))`; + const groupIdUrl = `http://localhost:5601/aql/app/ml#/jobs?mlManagement=(groupIds:!(${groupIdOne}))`; + + describe('getSelectedIdFromUrl', () => { + it('should get selected job id from the url', () => { + const actual = getSelectedIdFromUrl(jobIdUrl); + expect(actual).toStrictEqual({ jobId }); + }); + + it('should get selected group ids from the url', () => { + const expected = { groupIds: [groupIdOne, groupIdTwo] }; + const actual = getSelectedIdFromUrl(groupIdsUrl); + expect(actual).toStrictEqual(expected); + }); + + it('should get selected group id from the url', () => { + const expected = { groupIds: [groupIdOne] }; + const actual = getSelectedIdFromUrl(groupIdUrl); + expect(actual).toStrictEqual(expected); + }); + }); + + describe('getGroupQueryText', () => { + it('should get query string for selected group ids', () => { + const actual = getGroupQueryText([groupIdOne, groupIdTwo]); + expect(actual).toBe(`groups:(${groupIdOne} or ${groupIdTwo})`); + }); + + it('should get query string for selected group id', () => { + const actual = getGroupQueryText([groupIdOne]); + expect(actual).toBe(`groups:(${groupIdOne})`); + }); + }); +}); diff --git a/x-pack/plugins/ml/public/application/util/get_job_id_url.ts b/x-pack/plugins/ml/public/application/util/get_job_id_url.ts deleted file mode 100644 index a6ca575f21b50b..00000000000000 --- a/x-pack/plugins/ml/public/application/util/get_job_id_url.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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 rison from 'rison-node'; - -import { getBasePath } from './dependency_cache'; - -export function getJobIdUrl(tabId: string, jobId: string): string { - // Create url for filtering by job id for kibana management table - const settings = { - jobId, - }; - const encoded = rison.encode(settings); - const url = `?mlManagement=${encoded}`; - const basePath = getBasePath(); - - return `${basePath.get()}/app/ml#/${tabId}${url}`; -} diff --git a/x-pack/plugins/ml/public/application/util/get_selected_ids_url.ts b/x-pack/plugins/ml/public/application/util/get_selected_ids_url.ts new file mode 100644 index 00000000000000..806626577008e0 --- /dev/null +++ b/x-pack/plugins/ml/public/application/util/get_selected_ids_url.ts @@ -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 rison from 'rison-node'; +import { getBasePath } from './dependency_cache'; + +export enum TAB_IDS { + DATA_FRAME_ANALYTICS = 'data_frame_analytics', + ANOMALY_DETECTION = 'jobs', +} + +function getSelectedIdsUrl(tabId: TAB_IDS, settings: { [key: string]: string | string[] }): string { + // Create url for filtering by job id or group ids for kibana management table + const encoded = rison.encode(settings); + const url = `?mlManagement=${encoded}`; + const basePath = getBasePath(); + + return `${basePath.get()}/app/ml#/${tabId}${url}`; +} + +// Create url for filtering by group ids for kibana management table +export function getGroupIdsUrl(tabId: TAB_IDS, ids: string[]): string { + const settings = { + groupIds: ids, + }; + + return getSelectedIdsUrl(tabId, settings); +} + +// Create url for filtering by job id for kibana management table +export function getJobIdUrl(tabId: TAB_IDS, id: string): string { + const settings = { + jobId: id, + }; + + return getSelectedIdsUrl(tabId, settings); +}