[ML] Extend MlUrlGenerator to support other ML pages (#75696)

Co-authored-by: Dima Arnautov <dmitrii.arnautov@elastic.co>
This commit is contained in:
Quynh Nguyen 2020-09-02 14:52:10 -05:00 committed by GitHub
parent d9dc47ef36
commit aac84240b2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 982 additions and 208 deletions

View file

@ -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;

View file

@ -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<PageType, PageState> = 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<typeof ML_PAGES.ANOMALY_EXPLORER, ExplorerUrlPageState>;
export interface TimeSeriesExplorerGlobalState {
ml: {
jobIds: JobId[];
};
time?: TimeRange;
refreshInterval?: RefreshInterval;
}
export interface TimeSeriesExplorerAppState {
zoom?: {
from?: string;
to?: string;
};
mlTimeSeriesExplorer?: {
detectorIndex?: number;
entities?: Record<string, string>;
};
query?: any;
}
export interface TimeSeriesExplorerPageState
extends Pick<TimeSeriesExplorerAppState, 'zoom' | 'query'>,
Pick<TimeSeriesExplorerGlobalState, 'refreshInterval'> {
jobIds: JobId[];
timeRange?: TimeRange;
detectorIndex?: number;
entities?: Record<string, string>;
}
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;

View file

@ -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';

View file

@ -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);
};

View file

@ -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]);
};

View file

@ -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 (

View file

@ -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<Props> = ({ 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<Props> = ({ jobId, analysisType }) => {
defaultMessage: 'View results for the analytics job.',
}
)}
onClick={redirectToAnalyticsManagementPage}
onClick={redirectToAnalyticsExplorationPage}
data-test-subj="analyticsWizardViewResultsCard"
/>
</Fragment>

View file

@ -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<ExplorerState> = 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<ExplorerAppState> = explorerState$.pipe(
map(
(state: ExplorerState): ExplorerAppState => {

View file

@ -75,7 +75,6 @@ const MlRoutes: FC<{
pageDeps: PageDependencies;
}> = ({ pageDeps }) => {
const navigateToPath = useNavigateToPath();
return (
<>
{Object.entries(routes).map(([name, routeFactory]) => {

View file

@ -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';

View file

@ -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<AnomalyDetectionQueryState>(
'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<ExplorerAppState> = {
mlExplorerSwimlane,
mlExplorerFilter,
};
if (query) appState.query = query;
if (jobIds) {
const queryState: Partial<ExplorerGlobalState> = {
ml: {
jobIds,
},
};
if (timeRange) queryState.time = timeRange;
if (refreshInterval) queryState.refreshInterval = refreshInterval;
url = setStateToKbnUrl<Partial<ExplorerGlobalState>>(
'_g',
queryState,
{ useHash: false, storeInHashQuery: false },
url
);
url = setStateToKbnUrl<Partial<ExplorerAppState>>(
'_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<TimeSeriesExplorerAppState> = {};
const mlTimeSeriesExplorer: Partial<TimeSeriesExplorerAppState['mlTimeSeriesExplorer']> = {};
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<TimeSeriesExplorerGlobalState>(
'_g',
queryState,
{ useHash: false, storeInHashQuery: false },
url
);
url = setStateToKbnUrl<TimeSeriesExplorerAppState>(
'_a',
appState,
{ useHash: false, storeInHashQuery: false },
url
);
return url;
}

View file

@ -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: UrlState) {
// page should be guaranteed to exist here but <UrlState> 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;
}

View file

@ -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<DataFrameAnalyticsQueryState> = {
jobId,
groupIds,
};
url = setStateToKbnUrl<Partial<DataFrameAnalyticsQueryState>>(
'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<DataFrameAnalyticsExplorationQueryState>(
'_g',
queryState,
{ useHash: false, storeInHashQuery: false },
url
);
}
return url;
}

View file

@ -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);
}

View file

@ -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';

View file

@ -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');
});
});
});

View file

@ -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<MlUrlGeneratorState>;
}
}
interface Params {
appBasePath: string;
useHash: boolean;
}
export class MlUrlGenerator implements UrlGeneratorsDefinition<typeof ML_APP_URL_GENERATOR> {
constructor(private readonly params: Params) {}
public readonly id = ML_APP_URL_GENERATOR;
public readonly createUrl = async (mlUrlGeneratorState: MlUrlGeneratorState): Promise<string> => {
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<MlStartDependencies>
) {
const baseUrl = core.http.basePath.prepend('/app/ml');
share.urlGenerators.registerUrlGenerator(
new MlUrlGenerator({
appBasePath: baseUrl,
useHash: core.uiSettings.get('state:storeInSessionStorage'),
})
);
}

View file

@ -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';

View file

@ -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,
}
: {}),
},
},
});
},

View file

@ -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');
});
});
});

View file

@ -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<MlUrlGeneratorState>;
}
}
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<typeof ML_APP_URL_GENERATOR> {
constructor(private readonly params: Params) {}
public readonly id = ML_APP_URL_GENERATOR;
public readonly createUrl = async ({ page, ...params }: MlUrlGeneratorState): Promise<string> => {
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<ExplorerUrlState, 'page'>): string {
const appState: ExplorerAppState = {
mlExplorerSwimlane,
mlExplorerFilter,
};
const queryState: ExplorerQueryState = {
ml: {
jobIds,
},
};
if (timeRange) queryState.time = timeRange;
let url = `${this.params.appBasePath}/explorer`;
url = setStateToKbnUrl<ExplorerQueryState>('_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<MlStartDependencies>
) {
const baseUrl = core.http.basePath.prepend('/app/ml');
share.urlGenerators.registerUrlGenerator(
new MlUrlGenerator({
appBasePath: baseUrl,
useHash: core.uiSettings.get('state:storeInSessionStorage'),
})
);
}

View file

@ -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',
},