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 30d4bb34ea3455..c453de709a5d27 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?_a=(queryText:'id:(something)%20groups:(apm)')&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-5h,to:now-2h))"` + `"/app/ml/jobs?_a=(jobs:(queryText:'id:(something)%20groups:(apm)'))&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-5h,to:now-2h))"` ); }); diff --git a/x-pack/plugins/ml/common/constants/data_frame_analytics.ts b/x-pack/plugins/ml/common/constants/data_frame_analytics.ts index 5c8000566bb38a..958d5ae250185c 100644 --- a/x-pack/plugins/ml/common/constants/data_frame_analytics.ts +++ b/x-pack/plugins/ml/common/constants/data_frame_analytics.ts @@ -9,6 +9,7 @@ export const ANALYSIS_CONFIG_TYPE = { REGRESSION: 'regression', CLASSIFICATION: 'classification', } as const; + export const DEFAULT_RESULTS_FIELD = 'ml'; export const JOB_MAP_NODE_TYPES = { diff --git a/x-pack/plugins/ml/common/constants/ml_url_generator.ts b/x-pack/plugins/ml/common/constants/ml_url_generator.ts index a79e72a84c08e4..0c931d281d2d5b 100644 --- a/x-pack/plugins/ml/common/constants/ml_url_generator.ts +++ b/x-pack/plugins/ml/common/constants/ml_url_generator.ts @@ -11,6 +11,7 @@ export const ML_PAGES = { ANOMALY_EXPLORER: 'explorer', SINGLE_METRIC_VIEWER: 'timeseriesexplorer', DATA_FRAME_ANALYTICS_JOBS_MANAGE: 'data_frame_analytics', + DATA_FRAME_ANALYTICS_MODELS_MANAGE: 'data_frame_analytics/models', DATA_FRAME_ANALYTICS_EXPLORATION: 'data_frame_analytics/exploration', DATA_FRAME_ANALYTICS_MAP: 'data_frame_analytics/map', /** @@ -45,3 +46,5 @@ export const ML_PAGES = { ACCESS_DENIED: 'access-denied', OVERVIEW: 'overview', } as const; + +export type MlPages = typeof ML_PAGES[keyof typeof ML_PAGES]; diff --git a/x-pack/plugins/ml/common/types/common.ts b/x-pack/plugins/ml/common/types/common.ts index f04ff2539e4e96..4ae542c510a26e 100644 --- a/x-pack/plugins/ml/common/types/common.ts +++ b/x-pack/plugins/ml/common/types/common.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { MlPages } from '../constants/ml_url_generator'; + export interface Dictionary { [id: string]: TValue; } @@ -31,3 +33,15 @@ export type DeepReadonly = T extends Array type DeepReadonlyObject = { readonly [P in keyof T]: DeepReadonly; }; + +export interface ListingPageUrlState { + pageSize: number; + pageIndex: number; + sortField: string; + sortDirection: string; + queryText?: string; +} + +export type AppPageState = { + [key in MlPages]?: Partial; +}; diff --git a/x-pack/plugins/ml/common/util/string_utils.ts b/x-pack/plugins/ml/common/util/string_utils.ts index 4691bac0a065a4..ffb8b19dc9aa1a 100644 --- a/x-pack/plugins/ml/common/util/string_utils.ts +++ b/x-pack/plugins/ml/common/util/string_utils.ts @@ -45,5 +45,5 @@ export function getGroupQueryText(groupIds: string[]): string { } export function getJobQueryText(jobIds: string | string[]): string { - return Array.isArray(jobIds) ? `id:(${jobIds.join(' OR ')})` : jobIds; + return Array.isArray(jobIds) ? `id:(${jobIds.join(' OR ')})` : `id:${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 17ef84179ce635..63b7074ec3aaaa 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 @@ -7,10 +7,10 @@ import React, { FC, useCallback, useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { + EuiInMemoryTable, EuiCallOut, EuiFlexGroup, EuiFlexItem, - EuiInMemoryTable, EuiSearchBar, EuiSearchBarProps, EuiSpacer, @@ -30,13 +30,12 @@ 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 } 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'; +import { ListingPageUrlState } from '../../../../../../../common/types/common'; const filters: EuiSearchBarProps['filters'] = [ { @@ -84,17 +83,28 @@ interface Props { isManagementTable?: boolean; isMlEnabledInSpace?: boolean; blockRefresh?: boolean; + pageState: ListingPageUrlState; + updatePageState: (update: Partial) => void; } export const DataFrameAnalyticsList: FC = ({ isManagementTable = false, isMlEnabledInSpace = true, blockRefresh = false, + pageState, + updatePageState, }) => { + const searchQueryText = pageState.queryText ?? ''; + const setSearchQueryText = useCallback( + (value) => { + updatePageState({ queryText: value }); + }, + [updatePageState] + ); + const [isInitialized, setIsInitialized] = useState(false); const [isSourceIndexModalVisible, setIsSourceIndexModalVisible] = useState(false); const [isLoading, setIsLoading] = useState(false); const [filteredAnalytics, setFilteredAnalytics] = useState([]); - const [searchQueryText, setSearchQueryText] = useState(''); const [searchError, setSearchError] = useState(); const [analytics, setAnalytics] = useState([]); const [analyticsStats, setAnalyticsStats] = useState( @@ -102,9 +112,6 @@ export const DataFrameAnalyticsList: FC = ({ ); const [expandedRowItemIds, setExpandedRowItemIds] = useState([]); const [errorMessage, setErrorMessage] = useState(undefined); - // 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 [selectedIdFromUrlInitialized, setSelectedIdFromUrlInitialized] = useState(false); const disabled = !checkPermission('canCreateDataFrameAnalytics') || @@ -119,17 +126,20 @@ export const DataFrameAnalyticsList: FC = ({ isManagementTable ); - const updateFilteredItems = (queryClauses: any) => { - if (queryClauses.length) { - const filtered = filterAnalytics(analytics, queryClauses); - setFilteredAnalytics(filtered); - } else { - setFilteredAnalytics(analytics); - } - }; + const updateFilteredItems = useCallback( + (queryClauses: any[]) => { + if (queryClauses.length) { + const filtered = filterAnalytics(analytics, queryClauses); + setFilteredAnalytics(filtered); + } else { + setFilteredAnalytics(analytics); + } + }, + [analytics] + ); const filterList = () => { - if (searchQueryText !== '' && selectedIdFromUrlInitialized === true) { + if (searchQueryText !== '') { // trigger table filtering with query for job id to trigger table filter const query = EuiSearchBar.Query.parse(searchQueryText); let clauses: any = []; @@ -142,27 +152,9 @@ export const DataFrameAnalyticsList: FC = ({ } }; - useEffect(() => { - 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); - } else { - filterList(); - } - }, [selectedIdFromUrlInitialized, analytics]); - useEffect(() => { filterList(); - }, [selectedIdFromUrlInitialized, searchQueryText]); + }, [searchQueryText]); const getAnalyticsCallback = useCallback(() => getAnalytics(true), []); @@ -183,19 +175,19 @@ export const DataFrameAnalyticsList: FC = ({ ); const { onTableChange, pagination, sorting } = useTableSettings( - DataFrameAnalyticsListColumn.id, - filteredAnalytics + filteredAnalytics, + pageState, + updatePageState ); const handleSearchOnChange: EuiSearchBarProps['onChange'] = (search) => { if (search.error !== null) { setSearchError(search.error.message); - return false; + return; } setSearchError(undefined); setSearchQueryText(search.queryText); - return true; }; // Before the analytics have been loaded for the first time, display the loading indicator only. @@ -251,6 +243,7 @@ export const DataFrameAnalyticsList: FC = ({ ); + const search: EuiSearchBarProps = { query: searchQueryText, onChange: handleSearchOnChange, @@ -284,15 +277,13 @@ export const DataFrameAnalyticsList: FC = ({
allowNeutralSort={false} - className="mlAnalyticsInMemoryTable" columns={columns} - error={searchError} hasActions={false} isExpandable={true} + itemIdToExpandedRowMap={itemIdToExpandedRowMap} isSelectable={false} items={analytics} itemId={DataFrameAnalyticsListColumn.id} - itemIdToExpandedRowMap={itemIdToExpandedRowMap} loading={isLoading} onTableChange={onTableChange} pagination={pagination} @@ -302,6 +293,7 @@ export const DataFrameAnalyticsList: FC = ({ rowProps={(item) => ({ 'data-test-subj': `mlAnalyticsTableRow row-${item.id}`, })} + error={searchError} />
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts index 8c7c8b9db8b642..84c37ac8b816ba 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts @@ -116,14 +116,14 @@ export interface DataFrameAnalyticsListRow { } // Used to pass on attribute names to table columns -export enum DataFrameAnalyticsListColumn { - configDestIndex = 'config.dest.index', - configSourceIndex = 'config.source.index', - configCreateTime = 'config.create_time', - description = 'config.description', - id = 'id', - memoryStatus = 'stats.memory_usage.status', -} +export const DataFrameAnalyticsListColumn = { + configDestIndex: 'config.dest.index', + configSourceIndex: 'config.source.index', + configCreateTime: 'config.create_time', + description: 'config.description', + id: 'id', + memoryStatus: 'stats.memory_usage.status', +} as const; export type ItemIdToExpandedRowMap = Record; 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 2b63b9e7808193..93868ce0c17e6a 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 @@ -135,13 +135,13 @@ export const progressColumn = { 'data-test-subj': 'mlAnalyticsTableColumnProgress', }; -export const DFAnalyticsJobIdLink = ({ item }: { item: DataFrameAnalyticsListRow }) => { +export const DFAnalyticsJobIdLink = ({ jobId }: { jobId: string }) => { const href = useMlLink({ page: ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE, - pageState: { jobId: item.id }, + pageState: { jobId }, }); - return {item.id}; + return {jobId}; }; export const useColumns = ( @@ -199,13 +199,17 @@ export const useColumns = ( 'data-test-subj': 'mlAnalyticsTableRowDetailsToggle', }, { - name: 'ID', + field: DataFrameAnalyticsListColumn.id, + name: i18n.translate('xpack.ml.dataframe.analyticsList.id', { + defaultMessage: 'ID', + }), sortable: (item: DataFrameAnalyticsListRow) => item.id, truncateText: true, 'data-test-subj': 'mlAnalyticsTableColumnId', scope: 'row', - render: (item: DataFrameAnalyticsListRow) => - isManagementTable ? : item.id, + render: (jobId: string) => { + return isManagementTable ? : jobId; + }, }, { field: DataFrameAnalyticsListColumn.description, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts index 5b7d71dacccf8d..68774fb86fe964 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useState } from 'react'; import { Direction, EuiBasicTableProps, Pagination, PropertySort } from '@elastic/eui'; +import { useCallback, useMemo } from 'react'; +import { ListingPageUrlState } from '../../../../../../../common/types/common'; -const PAGE_SIZE = 10; const PAGE_SIZE_OPTIONS = [10, 25, 50]; // Copying from EUI EuiBasicTable types as type is not correctly picked up for table's onChange @@ -29,15 +29,6 @@ export interface CriteriaWithPagination extends Criteria { }; } -interface AnalyticsBasicTableSettings { - pageIndex: number; - pageSize: number; - totalItemCount: number; - hidePerPageOptions: boolean; - sortField: keyof T; - sortDirection: Direction; -} - interface UseTableSettingsReturnValue { onTableChange: EuiBasicTableProps['onChange']; pagination: Pagination; @@ -45,49 +36,44 @@ interface UseTableSettingsReturnValue { } export function useTableSettings( - sortByField: keyof TypeOfItem, - items: TypeOfItem[] + items: TypeOfItem[], + pageState: ListingPageUrlState, + updatePageState: (update: Partial) => void ): UseTableSettingsReturnValue { - const [tableSettings, setTableSettings] = useState>({ - pageIndex: 0, - pageSize: PAGE_SIZE, - totalItemCount: 0, - hidePerPageOptions: false, - sortField: sortByField, - sortDirection: 'asc', - }); - - const onTableChange: EuiBasicTableProps['onChange'] = ({ - page = { index: 0, size: PAGE_SIZE }, - sort = { field: sortByField, direction: 'asc' }, - }: CriteriaWithPagination) => { - const { index, size } = page; - const { field, direction } = sort; - - setTableSettings({ - ...tableSettings, - pageIndex: index, - pageSize: size, - sortField: field, - sortDirection: direction, - }); - }; + const { pageIndex, pageSize, sortField, sortDirection } = pageState; - const { pageIndex, pageSize, sortField, sortDirection } = tableSettings; + const onTableChange: EuiBasicTableProps['onChange'] = useCallback( + ({ page, sort }: CriteriaWithPagination) => { + const result = { + pageIndex: page?.index ?? pageState.pageIndex, + pageSize: page?.size ?? pageState.pageSize, + sortField: (sort?.field as string) ?? pageState.sortField, + sortDirection: sort?.direction ?? pageState.sortDirection, + }; + updatePageState(result); + }, + [pageState, updatePageState] + ); - const pagination = { - pageIndex, - pageSize, - totalItemCount: items.length, - pageSizeOptions: PAGE_SIZE_OPTIONS, - }; + const pagination = useMemo( + () => ({ + pageIndex, + pageSize, + totalItemCount: items.length, + pageSizeOptions: PAGE_SIZE_OPTIONS, + }), + [items, pageIndex, pageSize] + ); - const sorting = { - sort: { - field: sortField as string, - direction: sortDirection, - }, - }; + const sorting = useMemo( + () => ({ + sort: { + field: sortField as string, + direction: sortDirection as Direction, + }, + }), + [sortField, sortDirection] + ); return { onTableChange, pagination, sorting }; } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx index eaeae6cc645202..a5d3555fcc2788 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx @@ -50,9 +50,12 @@ export const AnalyticsNavigationBar: FC<{ selectedTabId?: string; jobId?: string return navTabs; }, [jobId !== undefined]); - const onTabClick = useCallback(async (tab: Tab) => { - await navigateToPath(tab.path, true); - }, []); + const onTabClick = useCallback( + async (tab: Tab) => { + await navigateToPath(tab.path, true); + }, + [navigateToPath] + ); return ( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/index.ts index 7c70a25071640c..77c794dce10ce9 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/index.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/index.ts @@ -6,8 +6,8 @@ export * from './models_list'; -export enum ModelsTableToConfigMapping { - id = 'model_id', - createdAt = 'create_time', - type = 'type', -} +export const ModelsTableToConfigMapping = { + id: 'model_id', + createdAt: 'create_time', + type: 'type', +} as const; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx index a87f11df937d34..2d74d08c4550c7 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx @@ -52,6 +52,8 @@ import { filterAnalyticsModels } from '../../../../common/search_bar_filters'; import { ML_PAGES } from '../../../../../../../common/constants/ml_url_generator'; import { DataFrameAnalysisConfigType } from '../../../../../../../common/types/data_frame_analytics'; import { timeFormatter } from '../../../../../../../common/util/date_utils'; +import { ListingPageUrlState } from '../../../../../../../common/types/common'; +import { usePageUrlState } from '../../../../../util/url_state'; type Stats = Omit; @@ -63,6 +65,13 @@ export type ModelItem = TrainedModelConfigResponse & { export type ModelItemFull = Required; +export const getDefaultModelsListState = (): ListingPageUrlState => ({ + pageIndex: 0, + pageSize: 10, + sortField: ModelsTableToConfigMapping.id, + sortDirection: 'asc', +}); + export const ModelsList: FC = () => { const { services: { @@ -71,12 +80,24 @@ export const ModelsList: FC = () => { } = useMlKibana(); const urlGenerator = useMlUrlGenerator(); + const [pageState, updatePageState] = usePageUrlState( + ML_PAGES.DATA_FRAME_ANALYTICS_MODELS_MANAGE, + getDefaultModelsListState() + ); + + const searchQueryText = pageState.queryText ?? ''; + const setSearchQueryText = useCallback( + (value) => { + updatePageState({ queryText: value }); + }, + [updatePageState] + ); + const canDeleteDataFrameAnalytics = capabilities.ml.canDeleteDataFrameAnalytics as boolean; const trainedModelsApiService = useTrainedModelsApiService(); const { toasts } = useNotifications(); - const [searchQueryText, setSearchQueryText] = useState(''); const [filteredModels, setFilteredModels] = useState([]); const [isLoading, setIsLoading] = useState(false); const [items, setItems] = useState([]); @@ -432,8 +453,9 @@ export const ModelsList: FC = () => { : []; const { onTableChange, pagination, sorting } = useTableSettings( - ModelsTableToConfigMapping.id, - filteredModels + filteredModels, + pageState, + updatePageState ); const toolsLeft = ( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx index c31a3b08aa7564..5a17b91818a1c4 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx @@ -33,11 +33,27 @@ import { UpgradeWarning } from '../../../components/upgrade'; import { AnalyticsNavigationBar } from './components/analytics_navigation_bar'; import { ModelsList } from './components/models_management'; import { JobMap } from '../job_map'; +import { usePageUrlState } from '../../../util/url_state'; +import { ListingPageUrlState } from '../../../../../common/types/common'; +import { DataFrameAnalyticsListColumn } from './components/analytics_list/common'; +import { ML_PAGES } from '../../../../../common/constants/ml_url_generator'; + +export const getDefaultDFAListState = (): ListingPageUrlState => ({ + pageIndex: 0, + pageSize: 10, + sortField: DataFrameAnalyticsListColumn.id, + sortDirection: 'asc', +}); export const Page: FC = () => { const [blockRefresh, setBlockRefresh] = useState(false); const [globalState] = useUrlState('_g'); + const [dfaPageState, setDfaPageState] = usePageUrlState( + ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE, + getDefaultDFAListState() + ); + useRefreshInterval(setBlockRefresh); const location = useLocation(); @@ -93,7 +109,11 @@ export const Page: FC = () => { {selectedTabId === 'map' && mapJobId && } {selectedTabId === 'data_frame_analytics' && ( - + )} {selectedTabId === 'models' && } 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 index f0fa62b7a3d8ae..1b1bea889925fc 100644 --- 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 @@ -22,8 +22,6 @@ import { JobGroup } from '../job_group'; import { useMlKibana } from '../../../../contexts/kibana'; interface JobFilterBarProps { - jobId: string; - groupIds: string[]; setFilters: (query: Query | null) => void; queryText?: string; } @@ -75,7 +73,7 @@ export const JobFilterBar: FC = ({ queryText, setFilters }) = useEffect(() => { setFilters(queryInstance); - }, []); + }, [queryText]); const filters: SearchFilterConfig[] = useMemo( () => [ 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 deleted file mode 100644 index b781199c852370..00000000000000 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.d.ts +++ /dev/null @@ -1,8 +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. - */ - -export function getSelectedIdFromUrl(str: string): { groupIds?: string[]; jobId?: 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 397062248689d2..338222e3ac4a2f 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 @@ -6,7 +6,6 @@ import { each } from 'lodash'; import { i18n } from '@kbn/i18n'; -import rison from 'rison-node'; import { mlJobService } from '../../../services/job_service'; import { @@ -367,31 +366,3 @@ function jobProperty(job, prop) { }; return job[propMap[prop]]; } - -function getUrlVars(url) { - const vars = {}; - url.replace(/[?&]+([^=&]+)=([^&]*)/gi, function (_, key, value) { - vars[key] = value; - }); - return vars; -} - -export function getSelectedIdFromUrl(url) { - const result = {}; - if (typeof url === 'string') { - const isGroup = url.includes('groupIds'); - url = decodeURIComponent(url); - - if (url.includes('mlManagement')) { - const urlParams = getUrlVars(url); - const decodedJson = rison.decode(urlParams.mlManagement); - - if (isGroup) { - result.groupIds = decodedJson.groupIds; - } else { - result.jobId = decodedJson.jobId; - } - } - } - return result; -} 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 deleted file mode 100644 index 4414be0b4fdcbf..00000000000000 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.test.ts +++ /dev/null @@ -1,35 +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 { 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); - }); - }); -}); 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 4c6469f6800a79..df50f53b811fa9 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,11 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, useCallback, useMemo } from 'react'; +import React, { FC } from 'react'; import { NavigationMenu } from '../../components/navigation_menu'; // @ts-ignore import { JobsListView } from './components/jobs_list_view/index'; -import { useUrlState } from '../../util/url_state'; +import { usePageUrlState } from '../../util/url_state'; +import { ML_PAGES } from '../../../../common/constants/ml_url_generator'; +import { ListingPageUrlState } from '../../../../common/types/common'; interface JobsPageProps { blockRefresh?: boolean; @@ -17,15 +19,7 @@ interface JobsPageProps { lastRefresh?: number; } -export interface AnomalyDetectionJobsListState { - pageSize: number; - pageIndex: number; - sortField: string; - sortDirection: string; - queryText?: string; -} - -export const getDefaultAnomalyDetectionJobsListState = (): AnomalyDetectionJobsListState => ({ +export const getDefaultAnomalyDetectionJobsListState = (): ListingPageUrlState => ({ pageIndex: 0, pageSize: 10, sortField: 'id', @@ -33,33 +27,15 @@ export const getDefaultAnomalyDetectionJobsListState = (): AnomalyDetectionJobsL }); 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] + const [pageState, setPageState] = usePageUrlState( + ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE, + getDefaultAnomalyDetectionJobsListState() ); 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 ad4b9ad78902bd..1089484449bab5 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 @@ -35,11 +35,10 @@ 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 { getDefaultAnomalyDetectionJobsListState } from '../../../../jobs/jobs_list/jobs'; import { getMlGlobalServices } from '../../../../app'; +import { ListingPageUrlState } from '../../../../../../common/types/common'; +import { getDefaultDFAListState } from '../../../../data_frame_analytics/pages/analytics_management/page'; interface Tab { 'data-test-subj': string; @@ -48,21 +47,28 @@ interface Tab { content: any; } -function useTabs(isMlEnabledInSpace: boolean): Tab[] { - const [jobsViewState, setJobsViewState] = useState( - getDefaultAnomalyDetectionJobsListState() - ); +function usePageState( + defaultState: T +): [T, (update: Partial) => void] { + const [pageState, setPageState] = useState(defaultState); const updateState = useCallback( - (update: Partial) => { - setJobsViewState({ - ...jobsViewState, + (update: Partial) => { + setPageState({ + ...pageState, ...update, }); }, - [jobsViewState] + [pageState] ); + return [pageState, updateState]; +} + +function useTabs(isMlEnabledInSpace: boolean): Tab[] { + const [adPageState, updateAdPageState] = usePageState(getDefaultAnomalyDetectionJobsListState()); + const [dfaPageState, updateDfaPageState] = usePageState(getDefaultDFAListState()); + return useMemo( () => [ { @@ -75,8 +81,8 @@ function useTabs(isMlEnabledInSpace: boolean): Tab[] { @@ -95,12 +101,14 @@ function useTabs(isMlEnabledInSpace: boolean): Tab[] { ), }, ], - [isMlEnabledInSpace, jobsViewState, updateState] + [isMlEnabledInSpace, adPageState, updateAdPageState, dfaPageState, updateDfaPageState] ); } diff --git a/x-pack/plugins/ml/public/application/util/url_state.tsx b/x-pack/plugins/ml/public/application/util/url_state.tsx index a3c70e11309047..448a888ab32c23 100644 --- a/x-pack/plugins/ml/public/application/util/url_state.tsx +++ b/x-pack/plugins/ml/public/application/util/url_state.tsx @@ -13,6 +13,7 @@ import { useHistory, useLocation } from 'react-router-dom'; import { Dictionary } from '../../../common/types/common'; import { getNestedProperty } from './object_utils'; +import { MlPages } from '../../../common/constants/ml_url_generator'; type Accessor = '_a' | '_g'; export type SetUrlState = ( @@ -150,3 +151,35 @@ export const useUrlState = (accessor: Accessor) => { ); return [urlState, setUrlState]; }; + +/** + * Hook for managing the URL state of the page. + */ +export const usePageUrlState = ( + pageKey: MlPages, + defaultState: PageUrlState +): [PageUrlState, (update: Partial) => void] => { + const [appState, setAppState] = useUrlState('_a'); + const pageState = appState?.[pageKey]; + + const resultPageState: PageUrlState = useMemo(() => { + return { + ...defaultState, + ...(pageState ?? {}), + }; + }, [pageState]); + + const onStateUpdate = useCallback( + (update: Partial, replace?: boolean) => { + setAppState(pageKey, { + ...(replace ? {} : resultPageState), + ...update, + }); + }, + [pageKey, resultPageState, setAppState] + ); + + return useMemo(() => { + return [resultPageState, onStateUpdate]; + }, [resultPageState, onStateUpdate]); +}; diff --git a/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts index 717d293ccd7faf..6d7e286a294761 100644 --- a/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts +++ b/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts @@ -19,8 +19,8 @@ import type { import { ML_PAGES } from '../../common/constants/ml_url_generator'; import { createGenericMlUrl } from './common'; import { setStateToKbnUrl } from '../../../../../src/plugins/kibana_utils/public'; -import type { AnomalyDetectionJobsListState } from '../application/jobs/jobs_list/jobs'; import { getGroupQueryText, getJobQueryText } from '../../common/util/string_utils'; +import { AppPageState, ListingPageUrlState } from '../../common/types/common'; /** * Creates URL to the Anomaly Detection Job management page */ @@ -41,11 +41,15 @@ export function createAnomalyDetectionJobManagementUrl( if (groupIds) { queryTextArr.push(getGroupQueryText(groupIds)); } - const queryState: Partial = { + const jobsListState: Partial = { ...(queryTextArr.length > 0 ? { queryText: queryTextArr.join(' ') } : {}), }; - url = setStateToKbnUrl>( + const queryState: AppPageState = { + [ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE]: jobsListState, + }; + + url = setStateToKbnUrl>( '_a', queryState, { useHash: false, storeInHashQuery: false }, diff --git a/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts index 6c58a9d28bcc20..dc9c3bd86cc63c 100644 --- a/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts +++ b/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts @@ -10,12 +10,13 @@ import { DataFrameAnalyticsExplorationQueryState, DataFrameAnalyticsExplorationUrlState, - DataFrameAnalyticsQueryState, DataFrameAnalyticsUrlState, MlCommonGlobalState, } from '../../common/types/ml_url_generator'; import { ML_PAGES } from '../../common/constants/ml_url_generator'; import { setStateToKbnUrl } from '../../../../../src/plugins/kibana_utils/public'; +import { getGroupQueryText, getJobQueryText } from '../../common/util/string_utils'; +import { AppPageState, ListingPageUrlState } from '../../common/types/common'; export function createDataFrameAnalyticsJobManagementUrl( appBasePath: string, @@ -26,13 +27,23 @@ export function createDataFrameAnalyticsJobManagementUrl( if (mlUrlGeneratorState) { const { jobId, groupIds, globalState } = mlUrlGeneratorState; if (jobId || groupIds) { - const queryState: Partial = { - jobId, - groupIds, + const queryTextArr = []; + if (jobId) { + queryTextArr.push(getJobQueryText(jobId)); + } + if (groupIds) { + queryTextArr.push(getGroupQueryText(groupIds)); + } + const jobsListState: Partial = { + ...(queryTextArr.length > 0 ? { queryText: queryTextArr.join(' ') } : {}), + }; + + const queryState: AppPageState = { + [ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE]: jobsListState, }; - 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 e7f12ead3ffe96..3f3d88f1a31d99 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?_a=(queryText:fq_single_1)'); + expect(url).toBe("/app/ml/jobs?_a=(jobs:(queryText:'id:fq_single_1'))"); }); it('should generate valid URL for the Anomaly Detection job management page for groupIds', async () => { @@ -40,7 +40,9 @@ describe('MlUrlGenerator', () => { groupIds: ['farequote', 'categorization'], }, }); - expect(url).toBe("/app/ml/jobs?_a=(queryText:'groups:(farequote%20or%20categorization)')"); + expect(url).toBe( + "/app/ml/jobs?_a=(jobs:(queryText:'groups:(farequote%20or%20categorization)'))" + ); }); it('should generate valid URL for the page for selecting the type of anomaly detection job to create', async () => { @@ -180,7 +182,9 @@ describe('MlUrlGenerator', () => { jobId: 'grid_regression_1', }, }); - expect(url).toBe('/app/ml/data_frame_analytics?mlManagement=(jobId:grid_regression_1)'); + expect(url).toBe( + "/app/ml/data_frame_analytics?_a=(data_frame_analytics:(queryText:'id:grid_regression_1'))" + ); }); it('should generate valid URL for the Data Frame Analytics job management page with groupIds', async () => { @@ -190,7 +194,9 @@ describe('MlUrlGenerator', () => { groupIds: ['group_1', 'group_2'], }, }); - expect(url).toBe('/app/ml/data_frame_analytics?mlManagement=(groupIds:!(group_1,group_2))'); + expect(url).toBe( + "/app/ml/data_frame_analytics?_a=(data_frame_analytics:(queryText:'groups:(group_1%20or%20group_2)'))" + ); }); }); 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 b0965f87085583..90ab5c2f888fe9 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,9 @@ describe('JobsTableComponent', () => { '[data-test-subj="jobs-table-link"]' ); await waitFor(() => - expect(href).toEqual('/app/ml/jobs?_a=(queryText:linux_anomalous_network_activity_ecs)') + expect(href).toEqual( + "/app/ml/jobs?_a=(jobs:(queryText:'id:linux_anomalous_network_activity_ecs'))" + ) ); }); @@ -72,7 +74,7 @@ describe('JobsTableComponent', () => { '[data-test-subj="jobs-table-link"]' ); await waitFor(() => - expect(href).toEqual("/app/ml/jobs?_a=(queryText:'job%20id%20with%20spaces')") + expect(href).toEqual("/app/ml/jobs?_a=(jobs:(queryText:'id:job%20id%20with%20spaces'))") ); });