From aac84240b237ec33aa72d986137f9decc5f64c38 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Wed, 2 Sep 2020 14:52:10 -0500 Subject: [PATCH] [ML] Extend MlUrlGenerator to support other ML pages (#75696) Co-authored-by: Dima Arnautov --- .../ml/common/constants/ml_url_generator.ts | 38 +++ .../ml/common/types/ml_url_generator.ts | 187 +++++++++++++ .../application/contexts/kibana/index.ts | 1 + .../contexts/kibana/use_create_url.ts | 20 ++ .../contexts/kibana/use_navigate_to_path.ts | 25 +- .../back_to_list_panel/back_to_list_panel.tsx | 13 +- .../view_results_panel/view_results_panel.tsx | 19 +- .../explorer/explorer_dashboard_service.ts | 19 +- .../ml/public/application/routing/router.tsx | 1 - .../application/routing/routes/explorer.tsx | 3 +- .../anomaly_detection_urls_generator.ts | 163 +++++++++++ .../ml/public/ml_url_generator/common.ts | 55 ++++ .../data_frame_analytics_urls_generator.ts | 70 +++++ .../data_visualizer_urls_generator.ts | 29 ++ .../ml/public/ml_url_generator/index.ts | 6 + .../ml_url_generator/ml_url_generator.test.ts | 262 ++++++++++++++++++ .../ml_url_generator/ml_url_generator.ts | 91 ++++++ x-pack/plugins/ml/public/plugin.ts | 2 +- .../open_in_anomaly_explorer_action.tsx | 30 +- .../plugins/ml/public/url_generator.test.ts | 34 --- x-pack/plugins/ml/public/url_generator.ts | 118 -------- .../lib/sample_data_sets/sample_data_sets.ts | 4 +- 22 files changed, 982 insertions(+), 208 deletions(-) create mode 100644 x-pack/plugins/ml/common/constants/ml_url_generator.ts create mode 100644 x-pack/plugins/ml/common/types/ml_url_generator.ts create mode 100644 x-pack/plugins/ml/public/application/contexts/kibana/use_create_url.ts create mode 100644 x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts create mode 100644 x-pack/plugins/ml/public/ml_url_generator/common.ts create mode 100644 x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts create mode 100644 x-pack/plugins/ml/public/ml_url_generator/data_visualizer_urls_generator.ts create mode 100644 x-pack/plugins/ml/public/ml_url_generator/index.ts create mode 100644 x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts create mode 100644 x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.ts delete mode 100644 x-pack/plugins/ml/public/url_generator.test.ts delete mode 100644 x-pack/plugins/ml/public/url_generator.ts diff --git a/x-pack/plugins/ml/common/constants/ml_url_generator.ts b/x-pack/plugins/ml/common/constants/ml_url_generator.ts new file mode 100644 index 00000000000000..44f33aa329e7ae --- /dev/null +++ b/x-pack/plugins/ml/common/constants/ml_url_generator.ts @@ -0,0 +1,38 @@ +/* + * 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 const ML_APP_URL_GENERATOR = 'ML_APP_URL_GENERATOR'; + +export const ML_PAGES = { + ANOMALY_DETECTION_JOBS_MANAGE: 'jobs', + ANOMALY_EXPLORER: 'explorer', + SINGLE_METRIC_VIEWER: 'timeseriesexplorer', + DATA_FRAME_ANALYTICS_JOBS_MANAGE: 'data_frame_analytics', + DATA_FRAME_ANALYTICS_EXPLORATION: 'data_frame_analytics/exploration', + /** + * Page: Data Visualizer + */ + DATA_VISUALIZER: 'datavisualizer', + /** + * Page: Data Visualizer + * Open data visualizer by selecting a Kibana index pattern or saved search + */ + DATA_VISUALIZER_INDEX_SELECT: 'datavisualizer_index_select', + /** + * Page: Data Visualizer + * Open data visualizer by importing data from a log file + */ + DATA_VISUALIZER_FILE: 'filedatavisualizer', + /** + * Page: Data Visualizer + * Open index data visualizer viewer page + */ + DATA_VISUALIZER_INDEX_VIEWER: 'jobs/new_job/datavisualizer', + ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE: `jobs/new_job/step/job_type`, + SETTINGS: 'settings', + CALENDARS_MANAGE: 'settings/calendars_list', + FILTER_LISTS_MANAGE: 'settings/filter_lists', +} as const; diff --git a/x-pack/plugins/ml/common/types/ml_url_generator.ts b/x-pack/plugins/ml/common/types/ml_url_generator.ts new file mode 100644 index 00000000000000..234be8b6faf909 --- /dev/null +++ b/x-pack/plugins/ml/common/types/ml_url_generator.ts @@ -0,0 +1,187 @@ +/* + * 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 { RefreshInterval, TimeRange } from '../../../../../src/plugins/data/common/query'; +import { JobId } from '../../../reporting/common/types'; +import { ML_PAGES } from '../constants/ml_url_generator'; + +type OptionalPageState = object | undefined; + +export type MLPageState = PageState extends OptionalPageState + ? { page: PageType; pageState?: PageState } + : PageState extends object + ? { page: PageType; pageState: PageState } + : { page: PageType }; + +export const ANALYSIS_CONFIG_TYPE = { + OUTLIER_DETECTION: 'outlier_detection', + REGRESSION: 'regression', + CLASSIFICATION: 'classification', +} as const; + +type DataFrameAnalyticsType = typeof ANALYSIS_CONFIG_TYPE[keyof typeof ANALYSIS_CONFIG_TYPE]; + +export interface MlCommonGlobalState { + time?: TimeRange; +} +export interface MlCommonAppState { + [key: string]: any; +} + +export interface MlIndexBasedSearchState { + index?: string; + savedSearchId?: string; +} + +export interface MlGenericUrlPageState extends MlIndexBasedSearchState { + globalState?: MlCommonGlobalState; + appState?: MlCommonAppState; + [key: string]: any; +} + +export interface MlGenericUrlState { + page: + | typeof ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER + | typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE; + pageState: MlGenericUrlPageState; +} + +export interface AnomalyDetectionQueryState { + jobId?: JobId; + groupIds?: string[]; +} + +export type AnomalyDetectionUrlState = MLPageState< + typeof ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE, + AnomalyDetectionQueryState | undefined +>; +export interface ExplorerAppState { + mlExplorerSwimlane: { + selectedType?: string; + selectedLanes?: string[]; + selectedTimes?: number[]; + showTopFieldValues?: boolean; + viewByFieldName?: string; + viewByPerPage?: number; + viewByFromPage?: number; + }; + mlExplorerFilter: { + influencersFilterQuery?: unknown; + filterActive?: boolean; + filteredFields?: string[]; + queryString?: string; + }; + query?: any; +} +export interface ExplorerGlobalState { + ml: { jobIds: JobId[] }; + time?: TimeRange; + refreshInterval?: RefreshInterval; +} + +export interface ExplorerUrlPageState { + /** + * Job IDs + */ + jobIds: JobId[]; + /** + * Optionally set the time range in the time picker. + */ + timeRange?: TimeRange; + /** + * Optionally set the refresh interval. + */ + refreshInterval?: RefreshInterval; + /** + * Optionally set the query. + */ + query?: any; + /** + * Optional state for the swim lane + */ + mlExplorerSwimlane?: ExplorerAppState['mlExplorerSwimlane']; + mlExplorerFilter?: ExplorerAppState['mlExplorerFilter']; +} + +export type ExplorerUrlState = MLPageState; + +export interface TimeSeriesExplorerGlobalState { + ml: { + jobIds: JobId[]; + }; + time?: TimeRange; + refreshInterval?: RefreshInterval; +} + +export interface TimeSeriesExplorerAppState { + zoom?: { + from?: string; + to?: string; + }; + mlTimeSeriesExplorer?: { + detectorIndex?: number; + entities?: Record; + }; + query?: any; +} + +export interface TimeSeriesExplorerPageState + extends Pick, + Pick { + jobIds: JobId[]; + timeRange?: TimeRange; + detectorIndex?: number; + entities?: Record; +} + +export type TimeSeriesExplorerUrlState = MLPageState< + typeof ML_PAGES.SINGLE_METRIC_VIEWER, + TimeSeriesExplorerPageState +>; + +export interface DataFrameAnalyticsQueryState { + jobId?: JobId | JobId[]; + groupIds?: string[]; +} + +export type DataFrameAnalyticsUrlState = MLPageState< + typeof ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE, + DataFrameAnalyticsQueryState | undefined +>; + +export interface DataVisualizerUrlState { + page: + | typeof ML_PAGES.DATA_VISUALIZER + | typeof ML_PAGES.DATA_VISUALIZER_FILE + | typeof ML_PAGES.DATA_VISUALIZER_INDEX_SELECT; +} + +export interface DataFrameAnalyticsExplorationQueryState { + ml: { + jobId: JobId; + analysisType: DataFrameAnalyticsType; + }; +} + +export type DataFrameAnalyticsExplorationUrlState = MLPageState< + typeof ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION, + { + jobId: JobId; + analysisType: DataFrameAnalyticsType; + } +>; + +/** + * Union type of ML URL state based on page + */ +export type MlUrlGeneratorState = + | AnomalyDetectionUrlState + | ExplorerUrlState + | TimeSeriesExplorerUrlState + | DataFrameAnalyticsUrlState + | DataFrameAnalyticsExplorationUrlState + | DataVisualizerUrlState + | MlGenericUrlState; diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/index.ts b/x-pack/plugins/ml/public/application/contexts/kibana/index.ts index 8a43ae12deb21c..44979a1b9c60ae 100644 --- a/x-pack/plugins/ml/public/application/contexts/kibana/index.ts +++ b/x-pack/plugins/ml/public/application/contexts/kibana/index.ts @@ -9,3 +9,4 @@ export { useNavigateToPath, NavigateToPath } from './use_navigate_to_path'; export { useUiSettings } from './use_ui_settings_context'; export { useTimefilter } from './use_timefilter'; export { useNotifications } from './use_notifications_context'; +export { useMlUrlGenerator } from './use_create_url'; diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/use_create_url.ts b/x-pack/plugins/ml/public/application/contexts/kibana/use_create_url.ts new file mode 100644 index 00000000000000..48385ad3ae6a82 --- /dev/null +++ b/x-pack/plugins/ml/public/application/contexts/kibana/use_create_url.ts @@ -0,0 +1,20 @@ +/* + * 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 { useMlKibana } from './kibana_context'; +import { ML_APP_URL_GENERATOR } from '../../../../common/constants/ml_url_generator'; + +export const useMlUrlGenerator = () => { + const { + services: { + share: { + urlGenerators: { getUrlGenerator }, + }, + }, + } = useMlKibana(); + + return getUrlGenerator(ML_APP_URL_GENERATOR); +}; diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/use_navigate_to_path.ts b/x-pack/plugins/ml/public/application/contexts/kibana/use_navigate_to_path.ts index f2db970bf5057e..96d41be03a142e 100644 --- a/x-pack/plugins/ml/public/application/contexts/kibana/use_navigate_to_path.ts +++ b/x-pack/plugins/ml/public/application/contexts/kibana/use_navigate_to_path.ts @@ -21,14 +21,19 @@ export const useNavigateToPath = () => { const location = useLocation(); - return useMemo( - () => (path: string | undefined, preserveSearch = false) => { - navigateToUrl( - getUrlForApp(PLUGIN_ID, { - path: `${path}${preserveSearch === true ? location.search : ''}`, - }) - ); - }, - [location] - ); + return useMemo(() => { + return (path: string | undefined, preserveSearch = false) => { + if (path === undefined) return; + const modifiedPath = `${path}${preserveSearch === true ? location.search : ''}`; + /** + * Handle urls generated by MlUrlGenerator where '/app/ml' is automatically prepended + */ + const url = modifiedPath.includes('/app/ml') + ? modifiedPath + : getUrlForApp(PLUGIN_ID, { + path: modifiedPath, + }); + navigateToUrl(url); + }; + }, [location]); }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/back_to_list_panel/back_to_list_panel.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/back_to_list_panel/back_to_list_panel.tsx index babb5571052702..0560c3150a424c 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/back_to_list_panel/back_to_list_panel.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/back_to_list_panel/back_to_list_panel.tsx @@ -7,13 +7,20 @@ import React, { FC, Fragment } from 'react'; import { EuiCard, EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useNavigateToPath } from '../../../../../contexts/kibana'; +import { useMlKibana, useMlUrlGenerator } from '../../../../../contexts/kibana'; +import { ML_PAGES } from '../../../../../../../common/constants/ml_url_generator'; export const BackToListPanel: FC = () => { - const navigateToPath = useNavigateToPath(); + const urlGenerator = useMlUrlGenerator(); + const { + services: { + application: { navigateToUrl }, + }, + } = useMlKibana(); const redirectToAnalyticsManagementPage = async () => { - await navigateToPath('/data_frame_analytics'); + const url = await urlGenerator.createUrl({ page: ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE }); + await navigateToUrl(url); }; return ( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/view_results_panel/view_results_panel.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/view_results_panel/view_results_panel.tsx index 23a16ba84ef923..b832bfb2549c6d 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/view_results_panel/view_results_panel.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/view_results_panel/view_results_panel.tsx @@ -7,20 +7,27 @@ import React, { FC, Fragment } from 'react'; import { EuiCard, EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useNavigateToPath } from '../../../../../contexts/kibana'; -import { getResultsUrl } from '../../../analytics_management/components/analytics_list/common'; +import { useMlUrlGenerator } from '../../../../../contexts/kibana'; import { ANALYSIS_CONFIG_TYPE } from '../../../../common/analytics'; - +import { ML_PAGES } from '../../../../../../../common/constants/ml_url_generator'; +import { useNavigateToPath } from '../../../../../contexts/kibana'; interface Props { jobId: string; analysisType: ANALYSIS_CONFIG_TYPE; } export const ViewResultsPanel: FC = ({ jobId, analysisType }) => { + const urlGenerator = useMlUrlGenerator(); const navigateToPath = useNavigateToPath(); - const redirectToAnalyticsManagementPage = async () => { - const path = getResultsUrl(jobId, analysisType); + const redirectToAnalyticsExplorationPage = async () => { + const path = await urlGenerator.createUrl({ + page: ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION, + pageState: { + jobId, + analysisType, + }, + }); await navigateToPath(path); }; @@ -38,7 +45,7 @@ export const ViewResultsPanel: FC = ({ jobId, analysisType }) => { defaultMessage: 'View results for the analytics job.', } )} - onClick={redirectToAnalyticsManagementPage} + onClick={redirectToAnalyticsExplorationPage} data-test-subj="analyticsWizardViewResultsCard" /> diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts b/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts index 4d697bcda1a065..e0ed2ea6cf5e08 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts +++ b/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts @@ -21,6 +21,7 @@ import { ExplorerChartsData } from './explorer_charts/explorer_charts_container_ import { EXPLORER_ACTION } from './explorer_constants'; import { AppStateSelectedCells, TimeRangeBounds } from './explorer_utils'; import { explorerReducer, getExplorerDefaultState, ExplorerState } from './reducers'; +import { ExplorerAppState } from '../../../common/types/ml_url_generator'; export const ALLOW_CELL_RANGE_SELECTION = true; @@ -49,24 +50,6 @@ const explorerState$: Observable = explorerFilteredAction$.pipe( shareReplay(1) ); -export interface ExplorerAppState { - mlExplorerSwimlane: { - selectedType?: string; - selectedLanes?: string[]; - selectedTimes?: number[]; - showTopFieldValues?: boolean; - viewByFieldName?: string; - viewByPerPage?: number; - viewByFromPage?: number; - }; - mlExplorerFilter: { - influencersFilterQuery?: unknown; - filterActive?: boolean; - filteredFields?: string[]; - queryString?: string; - }; -} - const explorerAppState$: Observable = explorerState$.pipe( map( (state: ExplorerState): ExplorerAppState => { diff --git a/x-pack/plugins/ml/public/application/routing/router.tsx b/x-pack/plugins/ml/public/application/routing/router.tsx index 56c9a19723fba1..22a17c4ea089a9 100644 --- a/x-pack/plugins/ml/public/application/routing/router.tsx +++ b/x-pack/plugins/ml/public/application/routing/router.tsx @@ -75,7 +75,6 @@ const MlRoutes: FC<{ pageDeps: PageDependencies; }> = ({ pageDeps }) => { const navigateToPath = useNavigateToPath(); - return ( <> {Object.entries(routes).map(([name, routeFactory]) => { diff --git a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx index 62d7e82a214b57..2f2fc77283ef79 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx @@ -22,7 +22,8 @@ import { useSelectedCells } from '../../explorer/hooks/use_selected_cells'; import { mlJobService } from '../../services/job_service'; import { ml } from '../../services/ml_api_service'; import { useExplorerData } from '../../explorer/actions'; -import { ExplorerAppState, explorerService } from '../../explorer/explorer_dashboard_service'; +import { ExplorerAppState } from '../../../../common/types/ml_url_generator'; +import { explorerService } from '../../explorer/explorer_dashboard_service'; import { getDateFormatTz } from '../../explorer/explorer_utils'; import { useJobSelection } from '../../components/job_selector/use_job_selection'; import { useShowCharts } from '../../components/controls/checkbox_showcharts'; 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 new file mode 100644 index 00000000000000..c4aebb108e7b92 --- /dev/null +++ b/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts @@ -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 isEmpty from 'lodash/isEmpty'; +import { + AnomalyDetectionQueryState, + AnomalyDetectionUrlState, + ExplorerAppState, + ExplorerGlobalState, + ExplorerUrlState, + MlGenericUrlState, + TimeSeriesExplorerAppState, + TimeSeriesExplorerGlobalState, + TimeSeriesExplorerUrlState, +} from '../../common/types/ml_url_generator'; +import { ML_PAGES } from '../../common/constants/ml_url_generator'; +import { createIndexBasedMlUrl } from './common'; +import { setStateToKbnUrl } from '../../../../../src/plugins/kibana_utils/public'; +/** + * Creates URL to the Anomaly Detection Job management page + */ +export function createAnomalyDetectionJobManagementUrl( + appBasePath: string, + params: AnomalyDetectionUrlState['pageState'] +): string { + let url = `${appBasePath}/${ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE}`; + if (!params || isEmpty(params)) { + return url; + } + const { jobId, groupIds } = params; + const queryState: AnomalyDetectionQueryState = { + jobId, + groupIds, + }; + + url = setStateToKbnUrl( + 'mlManagement', + queryState, + { useHash: false, storeInHashQuery: false }, + url + ); + return url; +} + +export function createAnomalyDetectionCreateJobSelectType( + appBasePath: string, + pageState: MlGenericUrlState['pageState'] +): string { + return createIndexBasedMlUrl( + appBasePath, + ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE, + pageState + ); +} + +/** + * Creates URL to the Anomaly Explorer page + */ +export function createExplorerUrl( + appBasePath: string, + params: ExplorerUrlState['pageState'] +): string { + let url = `${appBasePath}/${ML_PAGES.ANOMALY_EXPLORER}`; + + if (!params) { + return url; + } + const { + refreshInterval, + timeRange, + jobIds, + query, + mlExplorerSwimlane = {}, + mlExplorerFilter = {}, + } = params; + const appState: Partial = { + mlExplorerSwimlane, + mlExplorerFilter, + }; + if (query) appState.query = query; + + if (jobIds) { + const queryState: Partial = { + ml: { + jobIds, + }, + }; + + if (timeRange) queryState.time = timeRange; + if (refreshInterval) queryState.refreshInterval = refreshInterval; + + url = setStateToKbnUrl>( + '_g', + queryState, + { useHash: false, storeInHashQuery: false }, + url + ); + url = setStateToKbnUrl>( + '_a', + appState, + { useHash: false, storeInHashQuery: false }, + url + ); + } + + return url; +} + +/** + * Creates URL to the SingleMetricViewer page + */ +export function createSingleMetricViewerUrl( + appBasePath: string, + params: TimeSeriesExplorerUrlState['pageState'] +): string { + let url = `${appBasePath}/${ML_PAGES.SINGLE_METRIC_VIEWER}`; + if (!params) { + return url; + } + const { timeRange, jobIds, refreshInterval, zoom, query, detectorIndex, entities } = params; + + const queryState: TimeSeriesExplorerGlobalState = { + ml: { + jobIds, + }, + refreshInterval, + time: timeRange, + }; + + const appState: Partial = {}; + const mlTimeSeriesExplorer: Partial = {}; + + if (detectorIndex !== undefined) { + mlTimeSeriesExplorer.detectorIndex = detectorIndex; + } + if (entities !== undefined) { + mlTimeSeriesExplorer.entities = entities; + } + appState.mlTimeSeriesExplorer = mlTimeSeriesExplorer; + + if (zoom) appState.zoom = zoom; + if (query) + appState.query = { + query_string: query, + }; + url = setStateToKbnUrl( + '_g', + queryState, + { useHash: false, storeInHashQuery: false }, + url + ); + url = setStateToKbnUrl( + '_a', + appState, + { useHash: false, storeInHashQuery: false }, + url + ); + + return url; +} diff --git a/x-pack/plugins/ml/public/ml_url_generator/common.ts b/x-pack/plugins/ml/public/ml_url_generator/common.ts new file mode 100644 index 00000000000000..57cfc52045282b --- /dev/null +++ b/x-pack/plugins/ml/public/ml_url_generator/common.ts @@ -0,0 +1,55 @@ +/* + * 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 isEmpty from 'lodash/isEmpty'; +import { MlGenericUrlState } from '../../common/types/ml_url_generator'; +import { setStateToKbnUrl } from '../../../../../src/plugins/kibana_utils/public'; + +export function extractParams(urlState: UrlState) { + // page should be guaranteed to exist here but is unknown + // @ts-ignore + const { page, ...params } = urlState; + return { page, params }; +} + +/** + * Creates generic index based search ML url + * e.g. `jobs/new_job/datavisualizer?index=3da93760-e0af-11ea-9ad3-3bcfc330e42a` + */ +export function createIndexBasedMlUrl( + appBasePath: string, + page: MlGenericUrlState['page'], + pageState: MlGenericUrlState['pageState'] +): string { + const { globalState, appState, index, savedSearchId, ...restParams } = pageState; + let url = `${appBasePath}/${page}`; + + if (index !== undefined && savedSearchId === undefined) { + url = `${url}?index=${index}`; + } + if (index === undefined && savedSearchId !== undefined) { + url = `${url}?savedSearchId=${savedSearchId}`; + } + + if (!isEmpty(restParams)) { + Object.keys(restParams).forEach((key) => { + url = setStateToKbnUrl( + key, + restParams[key], + { useHash: false, storeInHashQuery: false }, + url + ); + }); + } + + if (globalState) { + url = setStateToKbnUrl('_g', globalState, { useHash: false, storeInHashQuery: false }, url); + } + if (appState) { + url = setStateToKbnUrl('_a', appState, { useHash: false, storeInHashQuery: false }, url); + } + return url; +} 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 new file mode 100644 index 00000000000000..8cf10a2acb64f2 --- /dev/null +++ b/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/** + * Creates URL to the DataFrameAnalytics page + */ +import { + DataFrameAnalyticsExplorationQueryState, + DataFrameAnalyticsExplorationUrlState, + DataFrameAnalyticsQueryState, + DataFrameAnalyticsUrlState, +} from '../../common/types/ml_url_generator'; +import { ML_PAGES } from '../../common/constants/ml_url_generator'; +import { setStateToKbnUrl } from '../../../../../src/plugins/kibana_utils/public'; + +export function createDataFrameAnalyticsJobManagementUrl( + appBasePath: string, + mlUrlGeneratorState: DataFrameAnalyticsUrlState['pageState'] +): string { + let url = `${appBasePath}/${ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE}`; + + if (mlUrlGeneratorState) { + const { jobId, groupIds } = mlUrlGeneratorState; + const queryState: Partial = { + jobId, + groupIds, + }; + + url = setStateToKbnUrl>( + 'mlManagement', + queryState, + { useHash: false, storeInHashQuery: false }, + url + ); + } + + return url; +} + +/** + * Creates URL to the DataFrameAnalytics Exploration page + */ +export function createDataFrameAnalyticsExplorationUrl( + appBasePath: string, + mlUrlGeneratorState: DataFrameAnalyticsExplorationUrlState['pageState'] +): string { + let url = `${appBasePath}/${ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION}`; + + if (mlUrlGeneratorState) { + const { jobId, analysisType } = mlUrlGeneratorState; + const queryState: DataFrameAnalyticsExplorationQueryState = { + ml: { + jobId, + analysisType, + }, + }; + + url = setStateToKbnUrl( + '_g', + queryState, + { useHash: false, storeInHashQuery: false }, + url + ); + } + + return url; +} diff --git a/x-pack/plugins/ml/public/ml_url_generator/data_visualizer_urls_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/data_visualizer_urls_generator.ts new file mode 100644 index 00000000000000..24693df5025d9d --- /dev/null +++ b/x-pack/plugins/ml/public/ml_url_generator/data_visualizer_urls_generator.ts @@ -0,0 +1,29 @@ +/* + * 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. + */ + +/** + * Creates URL to the Data Visualizer page + */ +import { DataVisualizerUrlState, MlGenericUrlState } from '../../common/types/ml_url_generator'; +import { createIndexBasedMlUrl } from './common'; +import { ML_PAGES } from '../../common/constants/ml_url_generator'; + +export function createDataVisualizerUrl( + appBasePath: string, + { page }: DataVisualizerUrlState +): string { + return `${appBasePath}/${page}`; +} + +/** + * Creates URL to the Index Data Visualizer + */ +export function createIndexDataVisualizerUrl( + appBasePath: string, + pageState: MlGenericUrlState['pageState'] +): string { + return createIndexBasedMlUrl(appBasePath, ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER, pageState); +} diff --git a/x-pack/plugins/ml/public/ml_url_generator/index.ts b/x-pack/plugins/ml/public/ml_url_generator/index.ts new file mode 100644 index 00000000000000..1579b187278c4f --- /dev/null +++ b/x-pack/plugins/ml/public/ml_url_generator/index.ts @@ -0,0 +1,6 @@ +/* + * 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 { MlUrlGenerator, registerUrlGenerator } from './ml_url_generator'; 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 new file mode 100644 index 00000000000000..55bc6d3668de78 --- /dev/null +++ b/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts @@ -0,0 +1,262 @@ +/* + * 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 { MlUrlGenerator } from './ml_url_generator'; +import { ML_PAGES } from '../../common/constants/ml_url_generator'; +import { ANALYSIS_CONFIG_TYPE } from '../../common/types/ml_url_generator'; + +describe('MlUrlGenerator', () => { + const urlGenerator = new MlUrlGenerator({ + appBasePath: '/app/ml', + useHash: false, + }); + + describe('AnomalyDetection', () => { + describe('Job Management Page', () => { + it('should generate valid URL for the Anomaly Detection job management page', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE, + }); + expect(url).toBe('/app/ml/jobs'); + }); + + it('should generate valid URL for the Anomaly Detection job management page for job', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE, + pageState: { + jobId: 'fq_single_1', + }, + }); + expect(url).toBe('/app/ml/jobs?mlManagement=(jobId:fq_single_1)'); + }); + + it('should generate valid URL for the Anomaly Detection job management page for groupIds', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE, + pageState: { + groupIds: ['farequote', 'categorization'], + }, + }); + expect(url).toBe('/app/ml/jobs?mlManagement=(groupIds:!(farequote,categorization))'); + }); + + it('should generate valid URL for the page for selecting the type of anomaly detection job to create', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE, + pageState: { + index: `3da93760-e0af-11ea-9ad3-3bcfc330e42a`, + globalState: { + time: { + from: 'now-30m', + to: 'now', + }, + }, + }, + }); + expect(url).toBe( + '/app/ml/jobs/new_job/step/job_type?index=3da93760-e0af-11ea-9ad3-3bcfc330e42a&_g=(time:(from:now-30m,to:now))' + ); + }); + }); + + describe('Anomaly Explorer Page', () => { + it('should generate valid URL for the Anomaly Explorer page', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.ANOMALY_EXPLORER, + pageState: { + jobIds: ['fq_single_1'], + mlExplorerSwimlane: { viewByFromPage: 2, viewByPerPage: 20 }, + refreshInterval: { + pause: false, + value: 0, + }, + timeRange: { + from: '2019-02-07T00:00:00.000Z', + to: '2020-08-13T17:15:00.000Z', + mode: 'absolute', + }, + query: { + analyze_wildcard: true, + query: '*', + }, + }, + }); + expect(url).toBe( + "/app/ml/explorer?_g=(ml:(jobIds:!(fq_single_1)),refreshInterval:(pause:!f,value:0),time:(from:'2019-02-07T00:00:00.000Z',mode:absolute,to:'2020-08-13T17:15:00.000Z'))&_a=(mlExplorerFilter:(),mlExplorerSwimlane:(viewByFromPage:2,viewByPerPage:20),query:(analyze_wildcard:!t,query:'*'))" + ); + }); + it('should generate valid URL for the Anomaly Explorer page for multiple jobIds', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.ANOMALY_EXPLORER, + pageState: { + jobIds: ['fq_single_1', 'logs_categorization_1'], + timeRange: { + from: '2019-02-07T00:00:00.000Z', + to: '2020-08-13T17:15:00.000Z', + mode: 'absolute', + }, + }, + }); + expect(url).toBe( + "/app/ml/explorer?_g=(ml:(jobIds:!(fq_single_1,logs_categorization_1)),time:(from:'2019-02-07T00:00:00.000Z',mode:absolute,to:'2020-08-13T17:15:00.000Z'))&_a=(mlExplorerFilter:(),mlExplorerSwimlane:())" + ); + }); + }); + + describe('Single Metric Viewer Page', () => { + it('should generate valid URL for the Single Metric Viewer page', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.SINGLE_METRIC_VIEWER, + pageState: { + jobIds: ['logs_categorization_1'], + refreshInterval: { + pause: false, + value: 0, + }, + timeRange: { + from: '2020-07-12T00:39:02.912Z', + to: '2020-07-22T15:52:18.613Z', + mode: 'absolute', + }, + query: { + analyze_wildcard: true, + query: '*', + }, + }, + }); + expect(url).toBe( + "/app/ml/timeseriesexplorer?_g=(ml:(jobIds:!(logs_categorization_1)),refreshInterval:(pause:!f,value:0),time:(from:'2020-07-12T00:39:02.912Z',mode:absolute,to:'2020-07-22T15:52:18.613Z'))&_a=(mlTimeSeriesExplorer:(),query:(query_string:(analyze_wildcard:!t,query:'*')))" + ); + }); + + it('should generate valid URL for the Single Metric Viewer page with extra settings', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.SINGLE_METRIC_VIEWER, + pageState: { + jobIds: ['logs_categorization_1'], + detectorIndex: 0, + entities: { mlcategory: '2' }, + refreshInterval: { + pause: false, + value: 0, + }, + timeRange: { + from: '2020-07-12T00:39:02.912Z', + to: '2020-07-22T15:52:18.613Z', + mode: 'absolute', + }, + zoom: { + from: '2020-07-20T23:58:29.367Z', + to: '2020-07-21T11:00:13.173Z', + }, + query: { + analyze_wildcard: true, + query: '*', + }, + }, + }); + expect(url).toBe( + "/app/ml/timeseriesexplorer?_g=(ml:(jobIds:!(logs_categorization_1)),refreshInterval:(pause:!f,value:0),time:(from:'2020-07-12T00:39:02.912Z',mode:absolute,to:'2020-07-22T15:52:18.613Z'))&_a=(mlTimeSeriesExplorer:(detectorIndex:0,entities:(mlcategory:'2')),query:(query_string:(analyze_wildcard:!t,query:'*')),zoom:(from:'2020-07-20T23:58:29.367Z',to:'2020-07-21T11:00:13.173Z'))" + ); + }); + }); + }); + + describe('DataFrameAnalytics', () => { + describe('JobManagement Page', () => { + it('should generate valid URL for the Data Frame Analytics job management page', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE, + }); + expect(url).toBe('/app/ml/data_frame_analytics'); + }); + it('should generate valid URL for the Data Frame Analytics job management page with jobId', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE, + pageState: { + jobId: 'grid_regression_1', + }, + }); + expect(url).toBe('/app/ml/data_frame_analytics?mlManagement=(jobId:grid_regression_1)'); + }); + + it('should generate valid URL for the Data Frame Analytics job management page with groupIds', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE, + pageState: { + groupIds: ['group_1', 'group_2'], + }, + }); + expect(url).toBe('/app/ml/data_frame_analytics?mlManagement=(groupIds:!(group_1,group_2))'); + }); + }); + + describe('ExplorationPage', () => { + it('should generate valid URL for the Data Frame Analytics exploration page for job', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION, + pageState: { + jobId: 'grid_regression_1', + analysisType: ANALYSIS_CONFIG_TYPE.REGRESSION, + }, + }); + expect(url).toBe( + '/app/ml/data_frame_analytics/exploration?_g=(ml:(analysisType:regression,jobId:grid_regression_1))' + ); + }); + }); + }); + + describe('DataVisualizer', () => { + it('should generate valid URL for the Data Visualizer page', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.DATA_VISUALIZER, + }); + expect(url).toBe('/app/ml/datavisualizer'); + }); + + it('should generate valid URL for the File Data Visualizer import page', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.DATA_VISUALIZER_FILE, + }); + expect(url).toBe('/app/ml/filedatavisualizer'); + }); + + it('should generate valid URL for the Index Data Visualizer select index pattern or saved search page', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.DATA_VISUALIZER_INDEX_SELECT, + }); + expect(url).toBe('/app/ml/datavisualizer_index_select'); + }); + + it('should generate valid URL for the Index Data Visualizer Viewer page', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER, + pageState: { + index: '3da93760-e0af-11ea-9ad3-3bcfc330e42a', + globalState: { + time: { + from: 'now-30m', + to: 'now', + }, + }, + }, + }); + expect(url).toBe( + '/app/ml/jobs/new_job/datavisualizer?index=3da93760-e0af-11ea-9ad3-3bcfc330e42a&_g=(time:(from:now-30m,to:now))' + ); + }); + }); + + it('should throw an error in case the page is not provided', async () => { + expect.assertions(1); + + // @ts-ignore + await urlGenerator.createUrl({ jobIds: ['test-job'] }).catch((e) => { + expect(e.message).toEqual('Page type is not provided or unknown'); + }); + }); +}); diff --git a/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.ts new file mode 100644 index 00000000000000..b69260d8d4157b --- /dev/null +++ b/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.ts @@ -0,0 +1,91 @@ +/* + * 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 { CoreSetup } from 'kibana/public'; +import { + SharePluginSetup, + UrlGeneratorsDefinition, + UrlGeneratorState, +} from '../../../../../src/plugins/share/public'; +import { MlStartDependencies } from '../plugin'; +import { ML_PAGES, ML_APP_URL_GENERATOR } from '../../common/constants/ml_url_generator'; +import { MlUrlGeneratorState } from '../../common/types/ml_url_generator'; +import { + createAnomalyDetectionJobManagementUrl, + createAnomalyDetectionCreateJobSelectType, + createExplorerUrl, + createSingleMetricViewerUrl, +} from './anomaly_detection_urls_generator'; +import { + createDataFrameAnalyticsJobManagementUrl, + createDataFrameAnalyticsExplorationUrl, +} from './data_frame_analytics_urls_generator'; +import { + createIndexDataVisualizerUrl, + createDataVisualizerUrl, +} from './data_visualizer_urls_generator'; + +declare module '../../../../../src/plugins/share/public' { + export interface UrlGeneratorStateMapping { + [ML_APP_URL_GENERATOR]: UrlGeneratorState; + } +} + +interface Params { + appBasePath: string; + useHash: boolean; +} + +export class MlUrlGenerator implements UrlGeneratorsDefinition { + constructor(private readonly params: Params) {} + + public readonly id = ML_APP_URL_GENERATOR; + + public readonly createUrl = async (mlUrlGeneratorState: MlUrlGeneratorState): Promise => { + const appBasePath = this.params.appBasePath; + switch (mlUrlGeneratorState.page) { + case ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE: + return createAnomalyDetectionJobManagementUrl(appBasePath, mlUrlGeneratorState.pageState); + case ML_PAGES.ANOMALY_EXPLORER: + return createExplorerUrl(appBasePath, mlUrlGeneratorState.pageState); + case ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE: + return createAnomalyDetectionCreateJobSelectType( + appBasePath, + mlUrlGeneratorState.pageState + ); + case ML_PAGES.SINGLE_METRIC_VIEWER: + return createSingleMetricViewerUrl(appBasePath, mlUrlGeneratorState.pageState); + case ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE: + return createDataFrameAnalyticsJobManagementUrl(appBasePath, mlUrlGeneratorState.pageState); + case ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION: + return createDataFrameAnalyticsExplorationUrl(appBasePath, mlUrlGeneratorState.pageState); + case ML_PAGES.DATA_VISUALIZER: + case ML_PAGES.DATA_VISUALIZER_FILE: + case ML_PAGES.DATA_VISUALIZER_INDEX_SELECT: + return createDataVisualizerUrl(appBasePath, mlUrlGeneratorState); + case ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER: + return createIndexDataVisualizerUrl(appBasePath, mlUrlGeneratorState.pageState); + default: + throw new Error('Page type is not provided or unknown'); + } + }; +} + +/** + * Registers the URL generator + */ +export function registerUrlGenerator( + share: SharePluginSetup, + core: CoreSetup +) { + const baseUrl = core.http.basePath.prepend('/app/ml'); + share.urlGenerators.registerUrlGenerator( + new MlUrlGenerator({ + appBasePath: baseUrl, + useHash: core.uiSettings.get('state:storeInSessionStorage'), + }) + ); +} diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts index 214b393a0fda92..3e8ab99e341ad5 100644 --- a/x-pack/plugins/ml/public/plugin.ts +++ b/x-pack/plugins/ml/public/plugin.ts @@ -34,7 +34,7 @@ import { registerFeature } from './register_feature'; import { UiActionsSetup, UiActionsStart } from '../../../../src/plugins/ui_actions/public'; import { registerMlUiActions } from './ui_actions'; import { KibanaLegacyStart } from '../../../../src/plugins/kibana_legacy/public'; -import { registerUrlGenerator } from './url_generator'; +import { registerUrlGenerator } from './ml_url_generator'; import { isFullLicense, isMlEnabled } from '../common/license'; import { registerEmbeddables } from './embeddables'; diff --git a/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx b/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx index e327befcf72938..e4d7cfa32c2cd5 100644 --- a/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx +++ b/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { ActionContextMapping, createAction } from '../../../../../src/plugins/ui_actions/public'; import { MlCoreSetup } from '../plugin'; -import { ML_APP_URL_GENERATOR } from '../url_generator'; +import { ML_APP_URL_GENERATOR } from '../../common/constants/ml_url_generator'; import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE, SwimLaneDrilldownContext } from '../embeddables'; export const OPEN_IN_ANOMALY_EXPLORER_ACTION = 'openInAnomalyExplorerAction'; @@ -32,19 +32,21 @@ export function createOpenInExplorerAction(getStartServices: MlCoreSetup['getSta return urlGenerator.createUrl({ page: 'explorer', - jobIds, - timeRange, - mlExplorerSwimlane: { - viewByFromPage: fromPage, - viewByPerPage: perPage, - viewByFieldName: viewBy, - ...(data - ? { - selectedType: data.type, - selectedTimes: data.times, - selectedLanes: data.lanes, - } - : {}), + pageState: { + jobIds, + timeRange, + mlExplorerSwimlane: { + viewByFromPage: fromPage, + viewByPerPage: perPage, + viewByFieldName: viewBy, + ...(data + ? { + selectedType: data.type, + selectedTimes: data.times, + selectedLanes: data.lanes, + } + : {}), + }, }, }); }, diff --git a/x-pack/plugins/ml/public/url_generator.test.ts b/x-pack/plugins/ml/public/url_generator.test.ts deleted file mode 100644 index 21dde124049577..00000000000000 --- a/x-pack/plugins/ml/public/url_generator.test.ts +++ /dev/null @@ -1,34 +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 { MlUrlGenerator } from './url_generator'; - -describe('MlUrlGenerator', () => { - const urlGenerator = new MlUrlGenerator({ - appBasePath: '/app/ml', - useHash: false, - }); - - it('should generate valid URL for the Anomaly Explorer page', async () => { - const url = await urlGenerator.createUrl({ - page: 'explorer', - jobIds: ['test-job'], - mlExplorerSwimlane: { viewByFromPage: 2, viewByPerPage: 20 }, - }); - expect(url).toBe( - '/app/ml/explorer#?_g=(ml:(jobIds:!(test-job)))&_a=(mlExplorerFilter:(),mlExplorerSwimlane:(viewByFromPage:2,viewByPerPage:20))' - ); - }); - - it('should throw an error in case the page is not provided', async () => { - expect.assertions(1); - - // @ts-ignore - await urlGenerator.createUrl({ jobIds: ['test-job'] }).catch((e) => { - expect(e.message).toEqual('Page type is not provided or unknown'); - }); - }); -}); diff --git a/x-pack/plugins/ml/public/url_generator.ts b/x-pack/plugins/ml/public/url_generator.ts deleted file mode 100644 index 4e08c57c0b2e0b..00000000000000 --- a/x-pack/plugins/ml/public/url_generator.ts +++ /dev/null @@ -1,118 +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 { CoreSetup } from 'kibana/public'; -import { - SharePluginSetup, - UrlGeneratorsDefinition, - UrlGeneratorState, -} from '../../../../src/plugins/share/public'; -import { TimeRange } from '../../../../src/plugins/data/public'; -import { setStateToKbnUrl } from '../../../../src/plugins/kibana_utils/public'; -import { JobId } from '../../reporting/common/types'; -import { ExplorerAppState } from './application/explorer/explorer_dashboard_service'; -import { MlStartDependencies } from './plugin'; - -declare module '../../../../src/plugins/share/public' { - export interface UrlGeneratorStateMapping { - [ML_APP_URL_GENERATOR]: UrlGeneratorState; - } -} - -export const ML_APP_URL_GENERATOR = 'ML_APP_URL_GENERATOR'; - -export interface ExplorerUrlState { - /** - * ML App Page - */ - page: 'explorer'; - /** - * Job IDs - */ - jobIds: JobId[]; - /** - * Optionally set the time range in the time picker. - */ - timeRange?: TimeRange; - /** - * Optional state for the swim lane - */ - mlExplorerSwimlane?: ExplorerAppState['mlExplorerSwimlane']; - mlExplorerFilter?: ExplorerAppState['mlExplorerFilter']; -} - -/** - * Union type of ML URL state based on page - */ -export type MlUrlGeneratorState = ExplorerUrlState; - -export interface ExplorerQueryState { - ml: { jobIds: JobId[] }; - time?: TimeRange; -} - -interface Params { - appBasePath: string; - useHash: boolean; -} - -export class MlUrlGenerator implements UrlGeneratorsDefinition { - constructor(private readonly params: Params) {} - - public readonly id = ML_APP_URL_GENERATOR; - - public readonly createUrl = async ({ page, ...params }: MlUrlGeneratorState): Promise => { - if (page === 'explorer') { - return this.createExplorerUrl(params); - } - throw new Error('Page type is not provided or unknown'); - }; - - /** - * Creates URL to the Anomaly Explorer page - */ - private createExplorerUrl({ - timeRange, - jobIds, - mlExplorerSwimlane = {}, - mlExplorerFilter = {}, - }: Omit): string { - const appState: ExplorerAppState = { - mlExplorerSwimlane, - mlExplorerFilter, - }; - - const queryState: ExplorerQueryState = { - ml: { - jobIds, - }, - }; - - if (timeRange) queryState.time = timeRange; - - let url = `${this.params.appBasePath}/explorer`; - url = setStateToKbnUrl('_g', queryState, { useHash: false }, url); - url = setStateToKbnUrl('_a', appState, { useHash: false }, url); - - return url; - } -} - -/** - * Registers the URL generator - */ -export function registerUrlGenerator( - share: SharePluginSetup, - core: CoreSetup -) { - const baseUrl = core.http.basePath.prepend('/app/ml'); - share.urlGenerators.registerUrlGenerator( - new MlUrlGenerator({ - appBasePath: baseUrl, - useHash: core.uiSettings.get('state:storeInSessionStorage'), - }) - ); -} diff --git a/x-pack/plugins/ml/server/lib/sample_data_sets/sample_data_sets.ts b/x-pack/plugins/ml/server/lib/sample_data_sets/sample_data_sets.ts index 902cc907a0eff1..28f12ead7924ba 100644 --- a/x-pack/plugins/ml/server/lib/sample_data_sets/sample_data_sets.ts +++ b/x-pack/plugins/ml/server/lib/sample_data_sets/sample_data_sets.ts @@ -18,7 +18,7 @@ export function initSampleDataSets(mlLicense: MlLicense, plugins: PluginsSetup) addAppLinksToSampleDataset('ecommerce', [ { path: - '/app/ml#/modules/check_view_or_create?id=sample_data_ecommerce&index=ff959d40-b880-11e8-a6d9-e546fe2bba5f', + '/app/ml/modules/check_view_or_create?id=sample_data_ecommerce&index=ff959d40-b880-11e8-a6d9-e546fe2bba5f', label: sampleDataLinkLabel, icon: 'machineLearningApp', }, @@ -27,7 +27,7 @@ export function initSampleDataSets(mlLicense: MlLicense, plugins: PluginsSetup) addAppLinksToSampleDataset('logs', [ { path: - '/app/ml#/modules/check_view_or_create?id=sample_data_weblogs&index=90943e30-9a47-11e8-b64d-95841ca0b247', + '/app/ml/modules/check_view_or_create?id=sample_data_weblogs&index=90943e30-9a47-11e8-b64d-95841ca0b247', label: sampleDataLinkLabel, icon: 'machineLearningApp', },