diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx index be00364cab92e6..30d4bb34ea3455 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx @@ -19,6 +19,6 @@ test('MLLink produces the correct URL', async () => { ); expect(href).toMatchInlineSnapshot( - `"/app/ml/jobs?mlManagement=(groupIds:!(apm),jobId:!(something))&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-5h,to:now-2h))"` + `"/app/ml/jobs?_a=(queryText:'id:(something)%20groups:(apm)')&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-5h,to:now-2h))"` ); }); diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/setup_flyout/module_list_card.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/setup_flyout/module_list_card.tsx index 67f2c8d58ec0d0..39c21fdc228dfc 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/setup_flyout/module_list_card.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/setup_flyout/module_list_card.tsx @@ -6,10 +6,11 @@ import { EuiCard, EuiIcon, EuiButtonEmpty, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { SetupStatus } from '../../../../../common/log_analysis'; import { CreateJobButton, RecreateJobButton } from '../../log_analysis_setup/create_job_button'; -import { useLinkProps } from '../../../../hooks/use_link_props'; +import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana'; +import { mountReactNode } from '../../../../../../../../src/core/public/utils'; export const LogAnalysisModuleListCard: React.FC<{ jobId: string; @@ -26,6 +27,39 @@ export const LogAnalysisModuleListCard: React.FC<{ moduleStatus, onViewSetup, }) => { + const { + services: { + ml, + application: { navigateToUrl }, + notifications: { toasts }, + }, + } = useKibanaContextForPlugin(); + + const [viewInMlLink, setViewInMlLink] = useState(''); + + const getMlUrl = async () => { + if (!ml.urlGenerator) { + toasts.addWarning({ + title: mountReactNode( + + ), + }); + return; + } + setViewInMlLink(await ml.urlGenerator.createUrl({ page: 'jobs', pageState: { jobId } })); + }; + + useEffect(() => { + getMlUrl(); + }); + + const navigateToMlApp = async () => { + await navigateToUrl(viewInMlLink); + }; + const moduleIcon = moduleStatus.type === 'required' ? ( @@ -33,12 +67,6 @@ export const LogAnalysisModuleListCard: React.FC<{ ); - const viewInMlLinkProps = useLinkProps({ - app: 'ml', - pathname: '/jobs', - search: { mlManagement: `(jobId:${jobId})` }, - }); - const moduleSetupButton = moduleStatus.type === 'required' ? ( @@ -50,13 +78,17 @@ export const LogAnalysisModuleListCard: React.FC<{ ) : ( <> - - - - + {viewInMlLink ? ( + <> + + + + + + ) : null} ); diff --git a/x-pack/plugins/infra/public/types.ts b/x-pack/plugins/infra/public/types.ts index 6ff699066eb15a..116345b35fdcea 100644 --- a/x-pack/plugins/infra/public/types.ts +++ b/x-pack/plugins/infra/public/types.ts @@ -18,6 +18,7 @@ import type { ObservabilityPluginStart, } from '../../observability/public'; import type { SpacesPluginStart } from '../../spaces/public'; +import { MlPluginStart } from '../../ml/public'; // Our own setup and start contract values export type InfraClientSetupExports = void; @@ -38,6 +39,7 @@ export interface InfraClientStartDeps { spaces: SpacesPluginStart; triggersActionsUi: TriggersAndActionsUIPublicPluginSetup; usageCollection: UsageCollectionStart; + ml: MlPluginStart; } export type InfraClientCoreSetup = CoreSetup; diff --git a/x-pack/plugins/ml/common/util/string_utils.test.ts b/x-pack/plugins/ml/common/util/string_utils.test.ts index 8afc7e52c9fa50..3503e2be35e867 100644 --- a/x-pack/plugins/ml/common/util/string_utils.test.ts +++ b/x-pack/plugins/ml/common/util/string_utils.test.ts @@ -4,7 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { renderTemplate, getMedianStringLength, stringHash } from './string_utils'; +import { + renderTemplate, + getMedianStringLength, + stringHash, + getGroupQueryText, +} from './string_utils'; const strings: string[] = [ 'foo', @@ -54,4 +59,19 @@ describe('ML - string utils', () => { expect(hash1).not.toBe(hash2); }); }); + + describe('getGroupQueryText', () => { + const groupIdOne = 'test_group_id_1'; + const groupIdTwo = 'test_group_id_2'; + + 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/common/util/string_utils.ts b/x-pack/plugins/ml/common/util/string_utils.ts index b4591fd2943e67..4691bac0a065a4 100644 --- a/x-pack/plugins/ml/common/util/string_utils.ts +++ b/x-pack/plugins/ml/common/util/string_utils.ts @@ -39,3 +39,11 @@ export function stringHash(str: string): number { } return hash < 0 ? hash * -2 : hash; } + +export function getGroupQueryText(groupIds: string[]): string { + return `groups:(${groupIds.join(' or ')})`; +} + +export function getJobQueryText(jobIds: string | string[]): string { + return Array.isArray(jobIds) ? `id:(${jobIds.join(' OR ')})` : jobIds; +} 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 8ed2436843e0ec..17ef84179ce635 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 @@ -30,15 +30,13 @@ 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 { - getSelectedIdFromUrl, - getGroupQueryText, -} from '../../../../../jobs/jobs_list/components/utils'; +import { getSelectedIdFromUrl } from '../../../../../jobs/jobs_list/components/utils'; import { SourceSelection } from '../source_selection'; import { filterAnalytics } from '../../../../common/search_bar_filters'; import { AnalyticsEmptyPrompt } from './empty_prompt'; import { useTableSettings } from './use_table_settings'; import { RefreshAnalyticsListButton } from '../refresh_analytics_list_button'; +import { getGroupQueryText } from '../../../../../../../common/util/string_utils'; const filters: EuiSearchBarProps['filters'] = [ { diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/index.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/index.ts similarity index 100% rename from x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/index.js rename to x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/index.ts 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 deleted file mode 100644 index 08373542c12346..00000000000000 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.js +++ /dev/null @@ -1,210 +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 PropTypes from 'prop-types'; -import React, { Component, Fragment } from 'react'; - -import { ml } from '../../../../services/ml_api_service'; -import { JobGroup } from '../job_group'; -import { - getGroupQueryText, - getSelectedIdFromUrl, - clearSelectedJobIdFromUrl, - getJobQueryText, -} from '../utils'; - -import { EuiSearchBar, EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - -function loadGroups() { - return ml.jobs - .groups() - .then((groups) => { - return groups.map((g) => ({ - value: g.id, - view: ( -
- -   - - - -
- ), - })); - }) - .catch((error) => { - console.log(error); - return []; - }); -} - -export class JobFilterBar extends Component { - constructor(props) { - super(props); - - this.state = { error: null }; - this.setFilters = props.setFilters; - } - - urlFilterIdCleared = false; - - componentDidMount() { - // If job id is selected in url, filter table to that id - let defaultQueryText; - const { jobId, groupIds } = getSelectedIdFromUrl(window.location.href); - - if (groupIds !== undefined) { - defaultQueryText = getGroupQueryText(groupIds); - } else if (jobId !== undefined) { - defaultQueryText = getJobQueryText(jobId); - } - - if (defaultQueryText !== undefined) { - this.setState( - { - defaultQueryText, - }, - () => { - // trigger onChange with query for job id to trigger table filter - const query = EuiSearchBar.Query.parse(defaultQueryText); - this.onChange({ query }); - } - ); - } - } - - onChange = ({ query, error }) => { - if (error) { - this.setState({ error }); - } else { - if (query.text === '' && this.urlFilterIdCleared === false) { - this.urlFilterIdCleared = true; - clearSelectedJobIdFromUrl(window.location.href); - } - let clauses = []; - if (query && query.ast !== undefined && query.ast.clauses !== undefined) { - clauses = query.ast.clauses; - } - this.setFilters(clauses); - this.setState({ error: null }); - } - }; - - render() { - const { error, defaultQueryText } = this.state; - const filters = [ - { - type: 'field_value_toggle_group', - field: 'job_state', - items: [ - { - value: 'opened', - name: i18n.translate('xpack.ml.jobsList.jobFilterBar.openedLabel', { - defaultMessage: 'Opened', - }), - }, - { - value: 'closed', - name: i18n.translate('xpack.ml.jobsList.jobFilterBar.closedLabel', { - defaultMessage: 'Closed', - }), - }, - { - value: 'failed', - name: i18n.translate('xpack.ml.jobsList.jobFilterBar.failedLabel', { - defaultMessage: 'Failed', - }), - }, - ], - }, - { - type: 'field_value_toggle_group', - field: 'datafeed_state', - items: [ - { - value: 'started', - name: i18n.translate('xpack.ml.jobsList.jobFilterBar.startedLabel', { - defaultMessage: 'Started', - }), - }, - { - value: 'stopped', - name: i18n.translate('xpack.ml.jobsList.jobFilterBar.stoppedLabel', { - defaultMessage: 'Stopped', - }), - }, - ], - }, - { - type: 'field_value_selection', - field: 'groups', - name: i18n.translate('xpack.ml.jobsList.jobFilterBar.groupLabel', { - defaultMessage: 'Group', - }), - multiSelect: 'or', - cache: 10000, - options: () => loadGroups(), - }, - ]; - // if prop flag for default filter set to true - // set defaultQuery to job id and force trigger filter with onChange - pass it the query object for the job id - return ( - - - {defaultQueryText === undefined && ( - - )} - {defaultQueryText !== undefined && ( - - )} - - - - - - ); - } -} -JobFilterBar.propTypes = { - setFilters: PropTypes.func.isRequired, -}; - -function getError(error) { - if (error) { - return i18n.translate('xpack.ml.jobsList.jobFilterBar.invalidSearchErrorMessage', { - defaultMessage: 'Invalid search: {errorMessage}', - values: { errorMessage: error.message }, - }); - } - - return ''; -} diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.tsx new file mode 100644 index 00000000000000..f0fa62b7a3d8ae --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.tsx @@ -0,0 +1,163 @@ +/* + * 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 React, { FC, Fragment, useCallback, useEffect, useMemo, useState } from 'react'; + +import { + EuiSearchBar, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + SearchFilterConfig, + EuiSearchBarProps, + Query, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +// @ts-ignore +import { JobGroup } from '../job_group'; +import { useMlKibana } from '../../../../contexts/kibana'; + +interface JobFilterBarProps { + jobId: string; + groupIds: string[]; + setFilters: (query: Query | null) => void; + queryText?: string; +} + +export const JobFilterBar: FC = ({ queryText, setFilters }) => { + const [error, setError] = useState(null); + const { + services: { + mlServices: { mlApiServices }, + }, + } = useMlKibana(); + + const loadGroups = useCallback(async () => { + try { + const response = await mlApiServices.jobs.groups(); + return response.map((g: any) => ({ + value: g.id, + view: ( +
+ +   + + + +
+ ), + })); + } catch (e) { + return []; + } + }, []); + + const queryInstance: Query = useMemo(() => { + return EuiSearchBar.Query.parse(queryText ?? ''); + }, [queryText]); + + const onChange: EuiSearchBarProps['onChange'] = ({ query, error: queryError }) => { + if (error) { + setError(queryError); + } else { + setFilters(query); + setError(null); + } + }; + + useEffect(() => { + setFilters(queryInstance); + }, []); + + const filters: SearchFilterConfig[] = useMemo( + () => [ + { + type: 'field_value_toggle_group', + field: 'job_state', + items: [ + { + value: 'opened', + name: i18n.translate('xpack.ml.jobsList.jobFilterBar.openedLabel', { + defaultMessage: 'Opened', + }), + }, + { + value: 'closed', + name: i18n.translate('xpack.ml.jobsList.jobFilterBar.closedLabel', { + defaultMessage: 'Closed', + }), + }, + { + value: 'failed', + name: i18n.translate('xpack.ml.jobsList.jobFilterBar.failedLabel', { + defaultMessage: 'Failed', + }), + }, + ], + }, + { + type: 'field_value_toggle_group', + field: 'datafeed_state', + items: [ + { + value: 'started', + name: i18n.translate('xpack.ml.jobsList.jobFilterBar.startedLabel', { + defaultMessage: 'Started', + }), + }, + { + value: 'stopped', + name: i18n.translate('xpack.ml.jobsList.jobFilterBar.stoppedLabel', { + defaultMessage: 'Stopped', + }), + }, + ], + }, + { + type: 'field_value_selection', + field: 'groups', + name: i18n.translate('xpack.ml.jobsList.jobFilterBar.groupLabel', { + defaultMessage: 'Group', + }), + multiSelect: 'or', + cache: 10000, + options: () => loadGroups(), + }, + ], + [] + ); + + const errorText = useMemo(() => { + if (error === null) return ''; + + return i18n.translate('xpack.ml.jobsList.jobFilterBar.invalidSearchErrorMessage', { + defaultMessage: 'Invalid search: {errorMessage}', + values: { errorMessage: error.message }, + }); + }, [error]); + + return ( + + + + + + + + + ); +}; 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 fba23d241be4c8..4213b0a6df1fb1 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 @@ -22,7 +22,6 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { AnomalyDetectionJobIdLink } from './job_id_link'; -const PAGE_SIZE = 10; const PAGE_SIZE_OPTIONS = [10, 25, 50]; // 'isManagementTable' bool prop to determine when to configure table for use in Kibana management page @@ -32,11 +31,7 @@ export class JobsList extends Component { this.state = { jobsSummaryList: props.jobsSummaryList, - pageIndex: 0, - pageSize: PAGE_SIZE, itemIdToExpandedRowMap: {}, - sortField: 'id', - sortDirection: 'asc', }; } @@ -54,7 +49,7 @@ export class JobsList extends Component { const { field: sortField, direction: sortDirection } = sort; - this.setState({ + this.props.onJobsViewStateUpdate({ pageIndex, pageSize, sortField, @@ -88,7 +83,7 @@ export class JobsList extends Component { pageStart = Math.floor((listLength - 1) / size) * size; // set the state out of the render cycle setTimeout(() => { - this.setState({ + this.props.onJobsViewStateUpdate({ pageIndex: pageStart / size, }); }, 0); @@ -301,7 +296,7 @@ export class JobsList extends Component { }); } - const { pageIndex, pageSize, sortField, sortDirection } = this.state; + const { pageIndex, pageSize, sortField, sortDirection } = this.props.jobsViewState; const { pageOfItems, totalItemCount } = this.getPageOfJobs( pageIndex, @@ -371,6 +366,8 @@ JobsList.propTypes = { refreshJobs: PropTypes.func, selectedJobsCount: PropTypes.number.isRequired, loading: PropTypes.bool, + jobsViewState: PropTypes.object, + onJobsViewStateUpdate: PropTypes.func, }; JobsList.defaultProps = { isManagementTable: false, diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js index 9eb7a03f0f5d74..570172abb28c1e 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js @@ -222,8 +222,14 @@ export class JobsListView extends Component { this.setState({ selectedJobs }); } - setFilters = (filterClauses) => { + setFilters = (query) => { + const filterClauses = (query && query.ast && query.ast.clauses) || []; const filteredJobsSummaryList = filterJobs(this.state.jobsSummaryList, filterClauses); + + this.props.onJobsViewStateUpdate({ + queryText: query?.text, + }); + this.setState({ filteredJobsSummaryList, filterClauses }, () => { this.refreshSelectedJobs(); }); @@ -358,7 +364,10 @@ export class JobsListView extends Component {
- +
@@ -434,7 +445,10 @@ export class JobsListView extends Component { showDeleteJobModal={this.showDeleteJobModal} refreshJobs={() => this.refreshJobSummaryList(true)} /> - + this.refreshJobSummaryList(true)} + jobsViewState={this.props.jobsViewState} + onJobsViewStateUpdate={this.props.onJobsViewStateUpdate} selectedJobsCount={this.state.selectedJobs.length} loading={loading} /> 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 75d6b149fda08b..b781199c852370 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 @@ -5,6 +5,4 @@ */ export function getSelectedIdFromUrl(str: string): { groupIds?: string[]; jobId?: string }; -export function getGroupQueryText(arr: string[]): string; -export function getJobQueryText(arr: string | 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 bc85153928a4b6..397062248689d2 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 @@ -395,22 +395,3 @@ export function getSelectedIdFromUrl(url) { } return result; } - -export function getGroupQueryText(groupIds) { - return `groups:(${groupIds.join(' or ')})`; -} - -export function getJobQueryText(jobIds) { - return Array.isArray(jobIds) ? `id:(${jobIds.join(' OR ')})` : jobIds; -} - -export function clearSelectedJobIdFromUrl(url) { - if (typeof url === 'string') { - url = decodeURIComponent(url); - if (url.includes('mlManagement') && (url.includes('jobId') || url.includes('groupIds'))) { - const urlParams = getUrlVars(url); - const clearedParams = `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 index e4c3c21c5a54a7..4414be0b4fdcbf 100644 --- 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 @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getGroupQueryText, getSelectedIdFromUrl } from './utils'; +import { getSelectedIdFromUrl } from './utils'; describe('ML - Jobs List utils', () => { const jobId = 'test_job_id_1'; @@ -32,16 +32,4 @@ describe('ML - Jobs List utils', () => { 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/jobs/jobs_list/jobs.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/jobs.tsx index 1e45f285945721..4c6469f6800a79 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/jobs.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/jobs.tsx @@ -4,12 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC } from 'react'; - +import React, { FC, useCallback, useMemo } from 'react'; import { NavigationMenu } from '../../components/navigation_menu'; - // @ts-ignore import { JobsListView } from './components/jobs_list_view/index'; +import { useUrlState } from '../../util/url_state'; interface JobsPageProps { blockRefresh?: boolean; @@ -18,11 +17,49 @@ interface JobsPageProps { lastRefresh?: number; } +export interface AnomalyDetectionJobsListState { + pageSize: number; + pageIndex: number; + sortField: string; + sortDirection: string; + queryText?: string; +} + +export const getDefaultAnomalyDetectionJobsListState = (): AnomalyDetectionJobsListState => ({ + pageIndex: 0, + pageSize: 10, + sortField: 'id', + sortDirection: 'asc', +}); + export const JobsPage: FC = (props) => { + const [appState, setAppState] = useUrlState('_a'); + + const jobListState: AnomalyDetectionJobsListState = useMemo(() => { + return { + ...getDefaultAnomalyDetectionJobsListState(), + ...(appState ?? {}), + }; + }, [appState]); + + const onJobsViewStateUpdate = useCallback( + (update: Partial) => { + setAppState({ + ...jobListState, + ...update, + }); + }, + [appState, setAppState] + ); + return (
- +
); }; diff --git a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx index 61dfea8897e82f..ad4b9ad78902bd 100644 --- a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx +++ b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect, useState, Fragment, FC } from 'react'; +import React, { useEffect, useState, Fragment, FC, useMemo, useCallback } from 'react'; import { Router } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { CoreStart } from 'kibana/public'; @@ -35,6 +35,11 @@ import { JobsListView } from '../../../../jobs/jobs_list/components/jobs_list_vi import { DataFrameAnalyticsList } from '../../../../data_frame_analytics/pages/analytics_management/components/analytics_list'; import { AccessDeniedPage } from '../access_denied_page'; import { SharePluginStart } from '../../../../../../../../../src/plugins/share/public'; +import { + AnomalyDetectionJobsListState, + getDefaultAnomalyDetectionJobsListState, +} from '../../../../jobs/jobs_list/jobs'; +import { getMlGlobalServices } from '../../../../app'; interface Tab { 'data-test-subj': string; @@ -43,38 +48,60 @@ interface Tab { content: any; } -function getTabs(isMlEnabledInSpace: boolean): Tab[] { - return [ - { - 'data-test-subj': 'mlStackManagementJobsListAnomalyDetectionTab', - id: 'anomaly_detection_jobs', - name: i18n.translate('xpack.ml.management.jobsList.anomalyDetectionTab', { - defaultMessage: 'Anomaly detection', - }), - content: ( - - - - - ), - }, - { - 'data-test-subj': 'mlStackManagementJobsListAnalyticsTab', - id: 'analytics_jobs', - name: i18n.translate('xpack.ml.management.jobsList.analyticsTab', { - defaultMessage: 'Analytics', - }), - content: ( - - - - - ), +function useTabs(isMlEnabledInSpace: boolean): Tab[] { + const [jobsViewState, setJobsViewState] = useState( + getDefaultAnomalyDetectionJobsListState() + ); + + const updateState = useCallback( + (update: Partial) => { + setJobsViewState({ + ...jobsViewState, + ...update, + }); }, - ]; + [jobsViewState] + ); + + return useMemo( + () => [ + { + 'data-test-subj': 'mlStackManagementJobsListAnomalyDetectionTab', + id: 'anomaly_detection_jobs', + name: i18n.translate('xpack.ml.management.jobsList.anomalyDetectionTab', { + defaultMessage: 'Anomaly detection', + }), + content: ( + + + + + ), + }, + { + 'data-test-subj': 'mlStackManagementJobsListAnalyticsTab', + id: 'analytics_jobs', + name: i18n.translate('xpack.ml.management.jobsList.analyticsTab', { + defaultMessage: 'Analytics', + }), + content: ( + + + + + ), + }, + ], + [isMlEnabledInSpace, jobsViewState, updateState] + ); } export const JobsListPage: FC<{ @@ -85,7 +112,7 @@ export const JobsListPage: FC<{ const [initialized, setInitialized] = useState(false); const [accessDenied, setAccessDenied] = useState(false); const [isMlEnabledInSpace, setIsMlEnabledInSpace] = useState(false); - const tabs = getTabs(isMlEnabledInSpace); + const tabs = useTabs(isMlEnabledInSpace); const [currentTabId, setCurrentTabId] = useState(tabs[0].id); const I18nContext = coreStart.i18n.Context; @@ -129,7 +156,7 @@ export const JobsListPage: FC<{ setCurrentTabId(id); }} size="s" - tabs={getTabs(isMlEnabledInSpace)} + tabs={tabs} initialSelectedTab={tabs[0]} /> ); @@ -142,7 +169,9 @@ export const JobsListPage: FC<{ return ( - + = { + ...(queryTextArr.length > 0 ? { queryText: queryTextArr.join(' ') } : {}), }; - url = setStateToKbnUrl( - 'mlManagement', + url = setStateToKbnUrl>( + '_a', queryState, { useHash: false, storeInHashQuery: false }, url diff --git a/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts b/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts index 754f5bec57a07b..e7f12ead3ffe96 100644 --- a/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts +++ b/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts @@ -30,7 +30,7 @@ describe('MlUrlGenerator', () => { jobId: 'fq_single_1', }, }); - expect(url).toBe('/app/ml/jobs?mlManagement=(jobId:fq_single_1)'); + expect(url).toBe('/app/ml/jobs?_a=(queryText:fq_single_1)'); }); it('should generate valid URL for the Anomaly Detection job management page for groupIds', async () => { @@ -40,7 +40,7 @@ describe('MlUrlGenerator', () => { groupIds: ['farequote', 'categorization'], }, }); - expect(url).toBe('/app/ml/jobs?mlManagement=(groupIds:!(farequote,categorization))'); + expect(url).toBe("/app/ml/jobs?_a=(queryText:'groups:(farequote%20or%20categorization)')"); }); it('should generate valid URL for the page for selecting the type of anomaly detection job to create', async () => { diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.test.tsx index 156475f63aa65f..b0965f87085583 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.test.tsx @@ -55,7 +55,7 @@ describe('JobsTableComponent', () => { '[data-test-subj="jobs-table-link"]' ); await waitFor(() => - expect(href).toEqual('/app/ml/jobs?mlManagement=(jobId:linux_anomalous_network_activity_ecs)') + expect(href).toEqual('/app/ml/jobs?_a=(queryText:linux_anomalous_network_activity_ecs)') ); }); @@ -72,7 +72,7 @@ describe('JobsTableComponent', () => { '[data-test-subj="jobs-table-link"]' ); await waitFor(() => - expect(href).toEqual("/app/ml/jobs?mlManagement=(jobId:'job%20id%20with%20spaces')") + expect(href).toEqual("/app/ml/jobs?_a=(queryText:'job%20id%20with%20spaces')") ); });