[ML] Migrate internal urls to non-hash paths (#76735)

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Quynh Nguyen 2020-09-17 12:09:08 -05:00 committed by GitHub
parent 11f100b1ac
commit d88b3a6dde
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
121 changed files with 2294 additions and 1038 deletions

View file

@ -4,6 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
export const PLUGIN_ID = 'ml';
export const PLUGIN_ICON = 'machineLearningApp';
export const PLUGIN_ICON_SOLUTION = 'logoKibana';
export const ML_APP_NAME = i18n.translate('xpack.ml.navMenu.mlAppNameText', {
defaultMessage: 'Machine Learning',
});

View file

@ -4,4 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const ANALYSIS_CONFIG_TYPE = {
OUTLIER_DETECTION: 'outlier_detection',
REGRESSION: 'regression',
CLASSIFICATION: 'classification',
} as const;
export const DEFAULT_RESULTS_FIELD = 'ml';

View file

@ -31,8 +31,16 @@ export const ML_PAGES = {
* Open index data visualizer viewer page
*/
DATA_VISUALIZER_INDEX_VIEWER: 'jobs/new_job/datavisualizer',
ANOMALY_DETECTION_CREATE_JOB: `jobs/new_job`,
ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE: `jobs/new_job/step/job_type`,
ANOMALY_DETECTION_CREATE_JOB_SELECT_INDEX: `jobs/new_job/step/index_or_search`,
SETTINGS: 'settings',
CALENDARS_MANAGE: 'settings/calendars_list',
CALENDARS_NEW: 'settings/calendars_list/new_calendar',
CALENDARS_EDIT: 'settings/calendars_list/edit_calendar',
FILTER_LISTS_MANAGE: 'settings/filter_lists',
FILTER_LISTS_NEW: 'settings/filter_lists/new_filter_list',
FILTER_LISTS_EDIT: 'settings/filter_lists/edit_filter_list',
ACCESS_DENIED: 'access-denied',
OVERVIEW: 'overview',
} as const;

View file

@ -6,6 +6,7 @@
import Boom from 'boom';
import { EsErrorBody } from '../util/errors';
import { ANALYSIS_CONFIG_TYPE } from '../constants/data_frame_analytics';
export interface DeleteDataFrameAnalyticsWithIndexStatus {
success: boolean;
@ -81,8 +82,4 @@ export interface DataFrameAnalyticsConfig {
allow_lazy_start?: boolean;
}
export enum ANALYSIS_CONFIG_TYPE {
OUTLIER_DETECTION = 'outlier_detection',
REGRESSION = 'regression',
CLASSIFICATION = 'classification',
}
export type DataFrameAnalysisConfigType = typeof ANALYSIS_CONFIG_TYPE[keyof typeof ANALYSIS_CONFIG_TYPE];

View file

@ -5,27 +5,21 @@
*/
import { RefreshInterval, TimeRange } from '../../../../../src/plugins/data/common/query';
import { JobId } from '../../../reporting/common/types';
import { JobId } from './anomaly_detection_jobs/job';
import { ML_PAGES } from '../constants/ml_url_generator';
import { DataFrameAnalysisConfigType } from './data_frame_analytics';
type OptionalPageState = object | undefined;
export type MLPageState<PageType, PageState> = PageState extends OptionalPageState
? { page: PageType; pageState?: PageState }
? { page: PageType; pageState?: PageState; excludeBasePath?: boolean }
: 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];
? { page: PageType; pageState: PageState; excludeBasePath?: boolean }
: { page: PageType; excludeBasePath?: boolean };
export interface MlCommonGlobalState {
time?: TimeRange;
refreshInterval?: RefreshInterval;
}
export interface MlCommonAppState {
[key: string]: any;
@ -42,16 +36,28 @@ export interface MlGenericUrlPageState extends MlIndexBasedSearchState {
[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 type MlGenericUrlState = MLPageState<
| typeof ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER
| typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB
| typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE
| typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_INDEX
| typeof ML_PAGES.OVERVIEW
| typeof ML_PAGES.CALENDARS_MANAGE
| typeof ML_PAGES.CALENDARS_NEW
| typeof ML_PAGES.FILTER_LISTS_MANAGE
| typeof ML_PAGES.FILTER_LISTS_NEW
| typeof ML_PAGES.SETTINGS
| typeof ML_PAGES.ACCESS_DENIED
| typeof ML_PAGES.DATA_VISUALIZER
| typeof ML_PAGES.DATA_VISUALIZER_FILE
| typeof ML_PAGES.DATA_VISUALIZER_INDEX_SELECT,
MlGenericUrlPageState | undefined
>;
export interface AnomalyDetectionQueryState {
jobId?: JobId;
groupIds?: string[];
globalState?: MlCommonGlobalState;
}
export type AnomalyDetectionUrlState = MLPageState<
@ -86,7 +92,7 @@ export interface ExplorerUrlPageState {
/**
* Job IDs
*/
jobIds: JobId[];
jobIds?: JobId[];
/**
* Optionally set the time range in the time picker.
*/
@ -104,6 +110,7 @@ export interface ExplorerUrlPageState {
*/
mlExplorerSwimlane?: ExplorerAppState['mlExplorerSwimlane'];
mlExplorerFilter?: ExplorerAppState['mlExplorerFilter'];
globalState?: MlCommonGlobalState;
}
export type ExplorerUrlState = MLPageState<typeof ML_PAGES.ANOMALY_EXPLORER, ExplorerUrlPageState>;
@ -122,6 +129,7 @@ export interface TimeSeriesExplorerAppState {
to?: string;
};
mlTimeSeriesExplorer?: {
forecastId?: string;
detectorIndex?: number;
entities?: Record<string, string>;
};
@ -131,10 +139,12 @@ export interface TimeSeriesExplorerAppState {
export interface TimeSeriesExplorerPageState
extends Pick<TimeSeriesExplorerAppState, 'zoom' | 'query'>,
Pick<TimeSeriesExplorerGlobalState, 'refreshInterval'> {
jobIds: JobId[];
jobIds?: JobId[];
timeRange?: TimeRange;
detectorIndex?: number;
entities?: Record<string, string>;
forecastId?: string;
globalState?: MlCommonGlobalState;
}
export type TimeSeriesExplorerUrlState = MLPageState<
@ -145,6 +155,7 @@ export type TimeSeriesExplorerUrlState = MLPageState<
export interface DataFrameAnalyticsQueryState {
jobId?: JobId | JobId[];
groupIds?: string[];
globalState?: MlCommonGlobalState;
}
export type DataFrameAnalyticsUrlState = MLPageState<
@ -152,17 +163,10 @@ export type DataFrameAnalyticsUrlState = MLPageState<
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;
analysisType: DataFrameAnalysisConfigType;
};
}
@ -170,7 +174,24 @@ export type DataFrameAnalyticsExplorationUrlState = MLPageState<
typeof ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION,
{
jobId: JobId;
analysisType: DataFrameAnalyticsType;
analysisType: DataFrameAnalysisConfigType;
globalState?: MlCommonGlobalState;
}
>;
export type CalendarEditUrlState = MLPageState<
typeof ML_PAGES.CALENDARS_EDIT,
{
calendarId: string;
globalState?: MlCommonGlobalState;
}
>;
export type FilterEditUrlState = MLPageState<
typeof ML_PAGES.FILTER_LISTS_EDIT,
{
filterId: string;
globalState?: MlCommonGlobalState;
}
>;
@ -183,5 +204,6 @@ export type MlUrlGeneratorState =
| TimeSeriesExplorerUrlState
| DataFrameAnalyticsUrlState
| DataFrameAnalyticsExplorationUrlState
| DataVisualizerUrlState
| CalendarEditUrlState
| FilterEditUrlState
| MlGenericUrlState;

View file

@ -9,8 +9,8 @@ import {
ClassificationAnalysis,
OutlierAnalysis,
RegressionAnalysis,
ANALYSIS_CONFIG_TYPE,
} from '../types/data_frame_analytics';
import { ANALYSIS_CONFIG_TYPE } from '../../common/constants/data_frame_analytics';
export const isOutlierAnalysis = (arg: any): arg is OutlierAnalysis => {
const keys = Object.keys(arg);

View file

@ -16,7 +16,8 @@
"embeddable",
"uiActions",
"kibanaLegacy",
"indexPatternManagement"
"indexPatternManagement",
"discover"
],
"optionalPlugins": [
"home",

View file

@ -20,6 +20,7 @@ import { MlSetupDependencies, MlStartDependencies } from '../plugin';
import { MlRouter } from './routing';
import { mlApiServicesProvider } from './services/ml_api_service';
import { HttpService } from './services/http_service';
import { ML_APP_URL_GENERATOR, ML_PAGES } from '../../common/constants/ml_url_generator';
export type MlDependencies = Omit<MlSetupDependencies, 'share' | 'indexPatternManagement'> &
MlStartDependencies;
@ -50,11 +51,21 @@ export interface MlServicesContext {
export type MlGlobalServices = ReturnType<typeof getMlGlobalServices>;
const App: FC<AppProps> = ({ coreStart, deps, appMountParams }) => {
const redirectToMlAccessDeniedPage = async () => {
const accessDeniedPageUrl = await deps.share.urlGenerators
.getUrlGenerator(ML_APP_URL_GENERATOR)
.createUrl({
page: ML_PAGES.ACCESS_DENIED,
});
await coreStart.application.navigateToUrl(accessDeniedPageUrl);
};
const pageDeps = {
history: appMountParams.history,
indexPatterns: deps.data.indexPatterns,
config: coreStart.uiSettings!,
setBreadcrumbs: coreStart.chrome!.setBreadcrumbs,
redirectToMlAccessDeniedPage,
};
const services = {
appName: 'ML',

View file

@ -33,10 +33,12 @@ export function checkGetManagementMlJobsResolver() {
});
}
export function checkGetJobsCapabilitiesResolver(): Promise<MlCapabilities> {
export function checkGetJobsCapabilitiesResolver(
redirectToMlAccessDeniedPage: () => Promise<void>
): Promise<MlCapabilities> {
return new Promise((resolve, reject) => {
getCapabilities()
.then(({ capabilities, isPlatinumOrTrialLicense }) => {
.then(async ({ capabilities, isPlatinumOrTrialLicense }) => {
_capabilities = capabilities;
// the minimum privilege for using ML with a platinum or trial license is being able to get the transforms list.
// all other functionality is controlled by the return capabilities object.
@ -46,21 +48,23 @@ export function checkGetJobsCapabilitiesResolver(): Promise<MlCapabilities> {
if (_capabilities.canGetJobs || isPlatinumOrTrialLicense === false) {
return resolve(_capabilities);
} else {
window.location.href = '#/access-denied';
await redirectToMlAccessDeniedPage();
return reject();
}
})
.catch((e) => {
window.location.href = '#/access-denied';
.catch(async (e) => {
await redirectToMlAccessDeniedPage();
return reject();
});
});
}
export function checkCreateJobsCapabilitiesResolver(): Promise<MlCapabilities> {
export function checkCreateJobsCapabilitiesResolver(
redirectToJobsManagementPage: () => Promise<void>
): Promise<MlCapabilities> {
return new Promise((resolve, reject) => {
getCapabilities()
.then(({ capabilities, isPlatinumOrTrialLicense }) => {
.then(async ({ capabilities, isPlatinumOrTrialLicense }) => {
_capabilities = capabilities;
// if the license is basic (isPlatinumOrTrialLicense === false) then do not redirect,
// allow the promise to resolve as the separate license check will redirect then user to
@ -69,34 +73,36 @@ export function checkCreateJobsCapabilitiesResolver(): Promise<MlCapabilities> {
return resolve(_capabilities);
} else {
// if the user has no permission to create a job,
// redirect them back to the Transforms Management page
window.location.href = '#/jobs';
// redirect them back to the Anomaly Detection Management page
await redirectToJobsManagementPage();
return reject();
}
})
.catch((e) => {
window.location.href = '#/jobs';
.catch(async (e) => {
await redirectToJobsManagementPage();
return reject();
});
});
}
export function checkFindFileStructurePrivilegeResolver(): Promise<MlCapabilities> {
export function checkFindFileStructurePrivilegeResolver(
redirectToMlAccessDeniedPage: () => Promise<void>
): Promise<MlCapabilities> {
return new Promise((resolve, reject) => {
getCapabilities()
.then(({ capabilities }) => {
.then(async ({ capabilities }) => {
_capabilities = capabilities;
// the minimum privilege for using ML with a basic license is being able to use the datavisualizer.
// all other functionality is controlled by the return _capabilities object
if (_capabilities.canFindFileStructure) {
return resolve(_capabilities);
} else {
window.location.href = '#/access-denied';
await redirectToMlAccessDeniedPage();
return reject();
}
})
.catch((e) => {
window.location.href = '#/access-denied';
.catch(async (e) => {
await redirectToMlAccessDeniedPage();
return reject();
});
});

View file

@ -1,170 +1,527 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AnnotationsTable Initialization with annotations prop. 1`] = `
<Fragment>
<EuiInMemoryTable
className="eui-textOverflowWrap"
columns={
Array [
Object {
"field": "annotation",
"name": "Annotation",
"scope": "row",
"sortable": true,
"width": "40%",
},
Object {
"dataType": "date",
"field": "timestamp",
"name": "From",
"render": [Function],
"sortable": true,
},
Object {
"dataType": "date",
"field": "end_timestamp",
"name": "To",
"render": [Function],
"sortable": true,
},
Object {
"dataType": "date",
"field": "modified_time",
"name": "Last modified date",
"render": [Function],
"sortable": true,
},
Object {
"field": "modified_username",
"name": "Last modified by",
"sortable": true,
},
Object {
"field": "event",
"name": "Event",
"sortable": true,
"width": "10%",
},
Object {
"field": "partition_field_value",
"name": "Partition",
"sortable": true,
},
Object {
"field": "over_field_value",
"name": "Over",
"sortable": true,
},
Object {
"field": "by_field_value",
"name": "By",
"sortable": true,
},
Object {
"actions": Array [
Object {
"render": [Function],
},
Object {
"render": [Function],
},
],
"align": "right",
"name": "Actions",
"width": "60px",
},
Object {
"dataType": "boolean",
"field": "current_series",
"name": "current_series",
"render": [Function],
"width": "0px",
},
]
}
compressed={true}
items={
Array [
Object {
"_id": "KCCkDWgB_ZdQ1MFDSYPi",
"annotation": "Major spike.",
"create_time": 1546417097181,
"create_username": "<user unknown>",
"end_timestamp": 1455041968976,
"job_id": "farequote",
"modified_time": 1546417097181,
"modified_username": "<user unknown>",
"timestamp": 1455026177994,
"type": "annotation",
},
]
}
pagination={
<AnnotationsTableUI
annotations={
Array [
Object {
"pageSizeOptions": Array [
5,
10,
25,
],
}
}
responsive={true}
rowProps={[Function]}
search={
Object {
"box": Object {
"incremental": true,
"schema": true,
},
"defaultQuery": "event:(user or delayed_data)",
"filters": Array [
Object {
"field": "event",
"multiSelect": "or",
"name": "Event",
"options": Array [],
"type": "field_value_selection",
"_id": "KCCkDWgB_ZdQ1MFDSYPi",
"annotation": "Major spike.",
"create_time": 1546417097181,
"create_username": "<user unknown>",
"end_timestamp": 1455041968976,
"job_id": "farequote",
"modified_time": 1546417097181,
"modified_username": "<user unknown>",
"timestamp": 1455026177994,
"type": "annotation",
},
]
}
intl={
Object {
"defaultFormats": Object {},
"defaultLocale": "en",
"formatDate": [Function],
"formatHTMLMessage": [Function],
"formatMessage": [Function],
"formatNumber": [Function],
"formatPlural": [Function],
"formatRelative": [Function],
"formatTime": [Function],
"formats": Object {
"date": Object {
"full": Object {
"day": "numeric",
"month": "long",
"weekday": "long",
"year": "numeric",
},
"long": Object {
"day": "numeric",
"month": "long",
"year": "numeric",
},
"medium": Object {
"day": "numeric",
"month": "short",
"year": "numeric",
},
"short": Object {
"day": "numeric",
"month": "numeric",
"year": "2-digit",
},
],
}
}
sorting={
Object {
"sort": Object {
"direction": "asc",
"field": "timestamp",
},
}
"number": Object {
"currency": Object {
"style": "currency",
},
"percent": Object {
"style": "percent",
},
},
"relative": Object {
"days": Object {
"units": "day",
},
"hours": Object {
"units": "hour",
},
"minutes": Object {
"units": "minute",
},
"months": Object {
"units": "month",
},
"seconds": Object {
"units": "second",
},
"years": Object {
"units": "year",
},
},
"time": Object {
"full": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
"timeZoneName": "short",
},
"long": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
"timeZoneName": "short",
},
"medium": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
},
"short": Object {
"hour": "numeric",
"minute": "numeric",
},
},
},
"formatters": Object {
"getDateTimeFormat": [Function],
"getMessageFormat": [Function],
"getNumberFormat": [Function],
"getPluralFormat": [Function],
"getRelativeFormat": [Function],
},
"locale": "en",
"messages": Object {},
"now": [Function],
"onError": [Function],
"textComponent": Symbol(react.fragment),
"timeZone": null,
}
}
kibana={
Object {
"notifications": Object {
"toasts": Object {
"danger": [Function],
"show": [Function],
"success": [Function],
"warning": [Function],
},
},
"overlays": Object {
"openFlyout": [Function],
"openModal": [Function],
},
"services": Object {},
}
}
/>
`;
exports[`AnnotationsTable Initialization with job config prop. 1`] = `
<AnnotationsTableUI
intl={
Object {
"defaultFormats": Object {},
"defaultLocale": "en",
"formatDate": [Function],
"formatHTMLMessage": [Function],
"formatMessage": [Function],
"formatNumber": [Function],
"formatPlural": [Function],
"formatRelative": [Function],
"formatTime": [Function],
"formats": Object {
"date": Object {
"full": Object {
"day": "numeric",
"month": "long",
"weekday": "long",
"year": "numeric",
},
"long": Object {
"day": "numeric",
"month": "long",
"year": "numeric",
},
"medium": Object {
"day": "numeric",
"month": "short",
"year": "numeric",
},
"short": Object {
"day": "numeric",
"month": "numeric",
"year": "2-digit",
},
},
"number": Object {
"currency": Object {
"style": "currency",
},
"percent": Object {
"style": "percent",
},
},
"relative": Object {
"days": Object {
"units": "day",
},
"hours": Object {
"units": "hour",
},
"minutes": Object {
"units": "minute",
},
"months": Object {
"units": "month",
},
"seconds": Object {
"units": "second",
},
"years": Object {
"units": "year",
},
},
"time": Object {
"full": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
"timeZoneName": "short",
},
"long": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
"timeZoneName": "short",
},
"medium": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
},
"short": Object {
"hour": "numeric",
"minute": "numeric",
},
},
},
"formatters": Object {
"getDateTimeFormat": [Function],
"getMessageFormat": [Function],
"getNumberFormat": [Function],
"getPluralFormat": [Function],
"getRelativeFormat": [Function],
},
"locale": "en",
"messages": Object {},
"now": [Function],
"onError": [Function],
"textComponent": Symbol(react.fragment),
"timeZone": null,
}
}
jobs={
Array [
Object {
"analysis_config": Object {
"bucket_span": "15m",
"detectors": Array [
Object {
"detector_description": "count",
"detector_index": 0,
"function": "count",
},
],
"influencers": Array [],
"summary_count_field_name": "doc_count",
},
"analysis_limits": Object {
"categorization_examples_limit": 4,
"model_memory_limit": "10mb",
},
"create_time": 1546418356716,
"custom_settings": Object {
"created_by": "single-metric-wizard",
},
"data_counts": Object {
"bucket_count": 478,
"earliest_record_timestamp": 1454804096000,
"empty_bucket_count": 0,
"input_bytes": 21554,
"input_field_count": 479,
"input_record_count": 479,
"invalid_date_count": 0,
"job_id": "farequote",
"last_data_time": 1546418357578,
"latest_record_timestamp": 1455234298000,
"missing_field_count": 0,
"out_of_order_timestamp_count": 0,
"processed_field_count": 479,
"processed_record_count": 479,
"sparse_bucket_count": 0,
},
"data_description": Object {
"time_field": "@timestamp",
"time_format": "epoch_ms",
},
"datafeed_config": Object {
"aggregations": Object {
"buckets": Object {
"aggregations": Object {
"@timestamp": Object {
"max": Object {
"field": "@timestamp",
},
},
},
"date_histogram": Object {
"field": "@timestamp",
"fixed_interval": "15m",
"keyed": false,
"min_doc_count": 0,
"offset": 0,
"order": Object {
"_key": "asc",
},
},
},
},
"chunking_config": Object {
"mode": "manual",
"time_span": "900000000ms",
},
"datafeed_id": "datafeed-farequote",
"delayed_data_check_config": Object {
"enabled": true,
},
"indices": Array [
"farequote",
],
"job_id": "farequote",
"query": Object {
"bool": Object {
"adjust_pure_negative": true,
"boost": 1,
"must": Array [
Object {
"query_string": Object {
"analyze_wildcard": true,
"auto_generate_synonyms_phrase_query": true,
"boost": 1,
"default_operator": "or",
"enable_position_increments": true,
"escape": false,
"fields": Array [],
"fuzziness": "AUTO",
"fuzzy_max_expansions": 50,
"fuzzy_prefix_length": 0,
"fuzzy_transpositions": true,
"max_determinized_states": 10000,
"phrase_slop": 0,
"query": "*",
"type": "best_fields",
},
},
],
},
},
"query_delay": "115823ms",
"scroll_size": 1000,
"state": "stopped",
},
"description": "",
"established_model_memory": 42102,
"finished_time": 1546418359427,
"job_id": "farequote",
"job_type": "anomaly_detector",
"job_version": "7.0.0",
"model_plot_config": Object {
"enabled": true,
},
"model_size_stats": Object {
"bucket_allocation_failures_count": 0,
"job_id": "farequote",
"log_time": 1546418359000,
"memory_status": "ok",
"model_bytes": 42102,
"result_type": "model_size_stats",
"timestamp": 1455232500000,
"total_by_field_count": 3,
"total_over_field_count": 0,
"total_partition_field_count": 2,
},
"model_snapshot_id": "1546418359",
"model_snapshot_min_version": "6.4.0",
"model_snapshot_retention_days": 1,
"results_index_name": "shared",
"state": "closed",
},
]
}
kibana={
Object {
"notifications": Object {
"toasts": Object {
"danger": [Function],
"show": [Function],
"success": [Function],
"warning": [Function],
},
},
"overlays": Object {
"openFlyout": [Function],
"openModal": [Function],
},
"services": Object {},
}
}
/>
`;
exports[`AnnotationsTable Minimal initialization without props. 1`] = `
<AnnotationsTableUI
intl={
Object {
"defaultFormats": Object {},
"defaultLocale": "en",
"formatDate": [Function],
"formatHTMLMessage": [Function],
"formatMessage": [Function],
"formatNumber": [Function],
"formatPlural": [Function],
"formatRelative": [Function],
"formatTime": [Function],
"formats": Object {
"date": Object {
"full": Object {
"day": "numeric",
"month": "long",
"weekday": "long",
"year": "numeric",
},
"long": Object {
"day": "numeric",
"month": "long",
"year": "numeric",
},
"medium": Object {
"day": "numeric",
"month": "short",
"year": "numeric",
},
"short": Object {
"day": "numeric",
"month": "numeric",
"year": "2-digit",
},
},
"number": Object {
"currency": Object {
"style": "currency",
},
"percent": Object {
"style": "percent",
},
},
"relative": Object {
"days": Object {
"units": "day",
},
"hours": Object {
"units": "hour",
},
"minutes": Object {
"units": "minute",
},
"months": Object {
"units": "month",
},
"seconds": Object {
"units": "second",
},
"years": Object {
"units": "year",
},
},
"time": Object {
"full": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
"timeZoneName": "short",
},
"long": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
"timeZoneName": "short",
},
"medium": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
},
"short": Object {
"hour": "numeric",
"minute": "numeric",
},
},
},
"formatters": Object {
"getDateTimeFormat": [Function],
"getMessageFormat": [Function],
"getNumberFormat": [Function],
"getPluralFormat": [Function],
"getRelativeFormat": [Function],
},
"locale": "en",
"messages": Object {},
"now": [Function],
"onError": [Function],
"textComponent": Symbol(react.fragment),
"timeZone": null,
}
}
kibana={
Object {
"notifications": Object {
"toasts": Object {
"danger": [Function],
"show": [Function],
"success": [Function],
"warning": [Function],
},
},
"overlays": Object {
"openFlyout": [Function],
"openModal": [Function],
},
"services": Object {},
}
tableLayout="fixed"
/>
</Fragment>
`;
exports[`AnnotationsTable Initialization with job config prop. 1`] = `
<EuiFlexGroup
justifyContent="spaceAround"
>
<EuiFlexItem
grow={false}
>
<EuiLoadingSpinner
size="l"
/>
</EuiFlexItem>
</EuiFlexGroup>
`;
exports[`AnnotationsTable Minimal initialization without props. 1`] = `
<EuiCallOut
iconType="iInCircle"
role="alert"
title={
<FormattedMessage
defaultMessage="No annotations created for this job"
id="xpack.ml.annotationsTable.annotationsNotCreatedTitle"
values={Object {}}
/>
}
/>
`;

View file

@ -13,7 +13,6 @@
import uniq from 'lodash/uniq';
import PropTypes from 'prop-types';
import rison from 'rison-node';
import React, { Component, Fragment } from 'react';
import memoizeOne from 'memoize-one';
import {
@ -54,12 +53,15 @@ import {
ANNOTATION_EVENT_USER,
ANNOTATION_EVENT_DELAYED_DATA,
} from '../../../../../common/constants/annotations';
import { withKibana } from '../../../../../../../../src/plugins/kibana_react/public';
import { ML_APP_URL_GENERATOR, ML_PAGES } from '../../../../../common/constants/ml_url_generator';
import { PLUGIN_ID } from '../../../../../common/constants/app';
const CURRENT_SERIES = 'current_series';
/**
* Table component for rendering the lists of annotations for an ML job.
*/
export class AnnotationsTable extends Component {
class AnnotationsTableUI extends Component {
static propTypes = {
annotations: PropTypes.array,
jobs: PropTypes.array,
@ -199,7 +201,17 @@ export class AnnotationsTable extends Component {
}
}
openSingleMetricView = (annotation = {}) => {
openSingleMetricView = async (annotation = {}) => {
const {
services: {
application: { navigateToApp },
share: {
urlGenerators: { getUrlGenerator },
},
},
} = this.props.kibana;
// Creates the link to the Single Metric Viewer.
// Set the total time range from the start to the end of the annotation.
const job = this.getJob(annotation.job_id);
@ -210,30 +222,10 @@ export class AnnotationsTable extends Component {
);
const from = new Date(dataCounts.earliest_record_timestamp).toISOString();
const to = new Date(resultLatest).toISOString();
const globalSettings = {
ml: {
jobIds: [job.job_id],
},
refreshInterval: {
display: 'Off',
pause: false,
value: 0,
},
time: {
from,
to,
mode: 'absolute',
},
};
const appState = {
query: {
query_string: {
analyze_wildcard: true,
query: '*',
},
},
const timeRange = {
from,
to,
mode: 'absolute',
};
let mlTimeSeriesExplorer = {};
const entityCondition = {};
@ -247,11 +239,11 @@ export class AnnotationsTable extends Component {
};
if (annotation.timestamp < dataCounts.earliest_record_timestamp) {
globalSettings.time.from = new Date(annotation.timestamp).toISOString();
timeRange.from = new Date(annotation.timestamp).toISOString();
}
if (annotation.end_timestamp > dataCounts.latest_record_timestamp) {
globalSettings.time.to = new Date(annotation.end_timestamp).toISOString();
timeRange.to = new Date(annotation.end_timestamp).toISOString();
}
}
@ -274,14 +266,34 @@ export class AnnotationsTable extends Component {
entityCondition[annotation.by_field_name] = annotation.by_field_value;
}
mlTimeSeriesExplorer.entities = entityCondition;
appState.mlTimeSeriesExplorer = mlTimeSeriesExplorer;
// appState.mlTimeSeriesExplorer = mlTimeSeriesExplorer;
const _g = rison.encode(globalSettings);
const _a = rison.encode(appState);
const mlUrlGenerator = getUrlGenerator(ML_APP_URL_GENERATOR);
const singleMetricViewerLink = await mlUrlGenerator.createUrl({
page: ML_PAGES.SINGLE_METRIC_VIEWER,
pageState: {
timeRange,
refreshInterval: {
display: 'Off',
pause: false,
value: 0,
},
jobIds: [job.job_id],
query: {
query_string: {
analyze_wildcard: true,
query: '*',
},
},
...mlTimeSeriesExplorer,
},
excludeBasePath: true,
});
const url = `?_g=${_g}&_a=${_a}`;
addItemToRecentlyAccessed('timeseriesexplorer', job.job_id, url);
window.open(`#/timeseriesexplorer${url}`, '_self');
addItemToRecentlyAccessed('timeseriesexplorer', job.job_id, singleMetricViewerLink);
await navigateToApp(PLUGIN_ID, {
path: singleMetricViewerLink,
});
};
onMouseOverRow = (record) => {
@ -686,3 +698,5 @@ export class AnnotationsTable extends Component {
);
}
}
export const AnnotationsTable = withKibana(AnnotationsTableUI);

View file

@ -29,6 +29,8 @@ import { getUrlForRecord, openCustomUrlWindow } from '../../util/custom_url_util
import { formatHumanReadableDateTimeSeconds } from '../../util/date_utils';
import { getIndexPatternIdFromName } from '../../util/index_utils';
import { replaceStringTokens } from '../../util/string_utils';
import { ML_APP_URL_GENERATOR, ML_PAGES } from '../../../../common/constants/ml_url_generator';
import { PLUGIN_ID } from '../../../../common/constants/app';
/*
* Component for rendering the links menu inside a cell in the anomalies table.
*/
@ -142,7 +144,18 @@ class LinksMenuUI extends Component {
}
};
viewSeries = () => {
viewSeries = async () => {
const {
services: {
application: { navigateToApp },
share: {
urlGenerators: { getUrlGenerator },
},
},
} = this.props.kibana;
const mlUrlGenerator = getUrlGenerator(ML_APP_URL_GENERATOR);
const record = this.props.anomaly.source;
const bounds = this.props.bounds;
const from = bounds.min.toISOString(); // e.g. 2016-02-08T16:00:00.000Z
@ -171,44 +184,36 @@ class LinksMenuUI extends Component {
entityCondition[record.by_field_name] = record.by_field_value;
}
// Use rison to build the URL .
const _g = rison.encode({
ml: {
const singleMetricViewerLink = await mlUrlGenerator.createUrl({
excludeBasePath: true,
page: ML_PAGES.SINGLE_METRIC_VIEWER,
pageState: {
jobIds: [record.job_id],
},
refreshInterval: {
display: 'Off',
pause: false,
value: 0,
},
time: {
from: from,
to: to,
mode: 'absolute',
},
});
const _a = rison.encode({
mlTimeSeriesExplorer: {
refreshInterval: {
display: 'Off',
pause: false,
value: 0,
},
timeRange: {
from: from,
to: to,
mode: 'absolute',
},
zoom: {
from: zoomFrom,
to: zoomTo,
},
detectorIndex: record.detector_index,
entities: entityCondition,
},
query: {
query_string: {
analyze_wildcard: true,
query: '*',
},
},
});
// Need to encode the _a parameter in case any entities contain unsafe characters such as '+'.
let path = '#/timeseriesexplorer';
path += `?_g=${_g}&_a=${encodeURIComponent(_a)}`;
window.open(path, '_blank');
await navigateToApp(PLUGIN_ID, {
path: singleMetricViewerLink,
});
};
viewExamples = () => {

View file

@ -6,13 +6,22 @@
import React from 'react';
import { Router } from 'react-router-dom';
import { render, fireEvent } from '@testing-library/react';
import { render } from '@testing-library/react';
import { createBrowserHistory } from 'history';
import { I18nProvider } from '@kbn/i18n/react';
import { AnomalyResultsViewSelector } from './index';
jest.mock('../../contexts/kibana', () => {
return {
useMlUrlGenerator: () => ({
createUrl: jest.fn(),
}),
useNavigateToPath: () => jest.fn(),
};
});
describe('AnomalyResultsViewSelector', () => {
test('should create selector with correctly selected value', () => {
const history = createBrowserHistory();
@ -31,27 +40,4 @@ describe('AnomalyResultsViewSelector', () => {
getByTestId('mlAnomalyResultsViewSelectorSingleMetricViewer').hasAttribute('checked')
).toBe(true);
});
test('should open window to other results view when clicking on non-checked input', () => {
// Create mock for window.open
const mockedOpen = jest.fn();
const originalOpen = window.open;
window.open = mockedOpen;
const history = createBrowserHistory();
const { getByTestId } = render(
<I18nProvider>
<Router history={history}>
<AnomalyResultsViewSelector viewId="timeseriesexplorer" />
</Router>
</I18nProvider>
);
fireEvent.click(getByTestId('mlAnomalyResultsViewSelectorExplorer'));
expect(mockedOpen).toHaveBeenCalledWith('#/explorer', '_self');
// Clean-up window.open.
window.open = originalOpen;
});
});

View file

@ -5,21 +5,25 @@
*/
import React, { FC, useMemo } from 'react';
import { encode } from 'rison-node';
import { EuiButtonGroup } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useUrlState } from '../../util/url_state';
import { useMlUrlGenerator, useNavigateToPath } from '../../contexts/kibana';
import { ML_PAGES } from '../../../../common/constants/ml_url_generator';
interface Props {
viewId: 'timeseriesexplorer' | 'explorer';
viewId: typeof ML_PAGES.SINGLE_METRIC_VIEWER | typeof ML_PAGES.ANOMALY_EXPLORER;
}
// Component for rendering a set of buttons for switching between the Anomaly Detection results views.
export const AnomalyResultsViewSelector: FC<Props> = ({ viewId }) => {
const urlGenerator = useMlUrlGenerator();
const navigateToPath = useNavigateToPath();
const toggleButtonsIcons = useMemo(
() => [
{
@ -28,7 +32,7 @@ export const AnomalyResultsViewSelector: FC<Props> = ({ viewId }) => {
defaultMessage: 'View results in the Single Metric Viewer',
}),
iconType: 'visLine',
value: 'timeseriesexplorer',
value: ML_PAGES.SINGLE_METRIC_VIEWER,
'data-test-subj': 'mlAnomalyResultsViewSelectorSingleMetricViewer',
},
{
@ -37,7 +41,7 @@ export const AnomalyResultsViewSelector: FC<Props> = ({ viewId }) => {
defaultMessage: 'View results in the Anomaly Explorer',
}),
iconType: 'visTable',
value: 'explorer',
value: ML_PAGES.ANOMALY_EXPLORER,
'data-test-subj': 'mlAnomalyResultsViewSelectorExplorer',
},
],
@ -46,9 +50,14 @@ export const AnomalyResultsViewSelector: FC<Props> = ({ viewId }) => {
const [globalState] = useUrlState('_g');
const onChangeView = (newViewId: string) => {
const fullGlobalStateString = globalState !== undefined ? `?_g=${encode(globalState)}` : '';
window.open(`#/${newViewId}${fullGlobalStateString}`, '_self');
const onChangeView = async (newViewId: Props['viewId']) => {
const url = await urlGenerator.createUrl({
page: newViewId,
pageState: {
globalState,
},
});
await navigateToPath(url);
};
return (
@ -60,7 +69,7 @@ export const AnomalyResultsViewSelector: FC<Props> = ({ viewId }) => {
data-test-subj="mlAnomalyResultsViewSelector"
options={toggleButtonsIcons}
idSelected={viewId}
onChange={onChangeView}
onChange={(newViewId: string) => onChangeView(newViewId as Props['viewId'])}
isIconOnly
/>
);

View file

@ -22,16 +22,19 @@ export const useCreateADLinks = () => {
const userTimeSettings = useUiSettings().get(ANOMALY_DETECTION_DEFAULT_TIME_RANGE);
const createLinkWithUserDefaults = useCallback(
(location, jobList) => {
const resultsPageUrl = mlJobService.createResultsUrlForJobs(
return mlJobService.createResultsUrlForJobs(
jobList,
location,
useUserTimeSettings === true && userTimeSettings !== undefined
? userTimeSettings
: undefined
);
return `${basePath.get()}/app/ml${resultsPageUrl}`;
},
[basePath]
);
return { createLinkWithUserDefaults };
};
export type CreateLinkWithUserDefaults = ReturnType<
typeof useCreateADLinks
>['createLinkWithUserDefaults'];

View file

@ -32,6 +32,7 @@ import { UseIndexDataReturnType } from './types';
import { DecisionPathPopover } from './feature_importance/decision_path_popover';
import { TopClasses } from '../../../../common/types/feature_importance';
import { DEFAULT_RESULTS_FIELD } from '../../../../common/constants/data_frame_analytics';
import { DataFrameAnalysisConfigType } from '../../../../common/types/data_frame_analytics';
// TODO Fix row hovering + bar highlighting
// import { hoveredRow$ } from './column_chart';
@ -44,7 +45,7 @@ export const DataGridTitle: FC<{ title: string }> = ({ title }) => (
interface PropsWithoutHeader extends UseIndexDataReturnType {
baseline?: number;
analysisType?: ANALYSIS_CONFIG_TYPE;
analysisType?: DataFrameAnalysisConfigType;
resultsField?: string;
dataTestSubj: string;
toastNotifications: CoreSetup['notifications']['toasts'];

View file

@ -13,10 +13,11 @@ import { FeatureImportance, TopClasses } from '../../../../../common/types/featu
import { ANALYSIS_CONFIG_TYPE } from '../../../data_frame_analytics/common';
import { ClassificationDecisionPath } from './decision_path_classification';
import { useMlKibana } from '../../../contexts/kibana';
import { DataFrameAnalysisConfigType } from '../../../../../common/types/data_frame_analytics';
interface DecisionPathPopoverProps {
featureImportance: FeatureImportance[];
analysisType: ANALYSIS_CONFIG_TYPE;
analysisType: DataFrameAnalysisConfigType;
predictionFieldName?: string;
baseline?: number;
predictedValue?: number | string | undefined;

View file

@ -9,11 +9,16 @@ import PropTypes from 'prop-types';
import { EuiIcon, EuiFlexItem } from '@elastic/eui';
import { CreateJobLinkCard } from '../create_job_link_card';
import { useMlKibana } from '../../contexts/kibana';
export const RecognizedResult = ({ config, indexPattern, savedSearch }) => {
const {
services: {
http: { basePath },
},
} = useMlKibana();
const id = savedSearch === null ? `index=${indexPattern.id}` : `savedSearchId=${savedSearch.id}`;
const href = `#/jobs/new_job/recognize?id=${config.id}&${id}`;
const href = `${basePath.get()}/app/ml/jobs/new_job/recognize?id=${config.id}&${id}`;
let logo = null;
// if a logo is available, use that, otherwise display the id

View file

@ -4,15 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC, useState } from 'react';
import { encode } from 'rison-node';
import React, { FC, useState, useEffect } from 'react';
import { EuiTabs, EuiTab, EuiLink } from '@elastic/eui';
import { EuiTabs, EuiTab } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useUrlState } from '../../util/url_state';
import { TabId } from './navigation_menu';
import { useMlKibana, useMlUrlGenerator, useNavigateToPath } from '../../contexts/kibana';
import { MlUrlGeneratorState } from '../../../../common/types/ml_url_generator';
import { useUrlState } from '../../util/url_state';
import { ML_APP_NAME } from '../../../../common/constants/app';
export interface Tab {
id: TabId;
@ -66,20 +66,57 @@ function getTabs(disableLinks: boolean): Tab[] {
}
interface TabData {
testSubject: string;
pathId?: string;
pathId?: MlUrlGeneratorState['page'];
name: string;
}
const TAB_DATA: Record<TabId, TabData> = {
overview: { testSubject: 'mlMainTab overview' },
overview: {
testSubject: 'mlMainTab overview',
name: i18n.translate('xpack.ml.overviewTabLabel', {
defaultMessage: 'Overview',
}),
},
// Note that anomaly detection jobs list is mapped to ml#/jobs.
anomaly_detection: { testSubject: 'mlMainTab anomalyDetection', pathId: 'jobs' },
data_frame_analytics: { testSubject: 'mlMainTab dataFrameAnalytics' },
datavisualizer: { testSubject: 'mlMainTab dataVisualizer' },
settings: { testSubject: 'mlMainTab settings' },
'access-denied': { testSubject: 'mlMainTab overview' },
anomaly_detection: {
testSubject: 'mlMainTab anomalyDetection',
name: i18n.translate('xpack.ml.anomalyDetectionTabLabel', {
defaultMessage: 'Anomaly Detection',
}),
pathId: 'jobs',
},
data_frame_analytics: {
testSubject: 'mlMainTab dataFrameAnalytics',
name: i18n.translate('xpack.ml.dataFrameAnalyticsTabLabel', {
defaultMessage: 'Data Frame Analytics',
}),
},
datavisualizer: {
testSubject: 'mlMainTab dataVisualizer',
name: i18n.translate('xpack.ml.dataVisualizerTabLabel', {
defaultMessage: 'Data Visualizer',
}),
},
settings: {
testSubject: 'mlMainTab settings',
name: i18n.translate('xpack.ml.settingsTabLabel', {
defaultMessage: 'Settings',
}),
},
'access-denied': {
testSubject: 'mlMainTab overview',
name: i18n.translate('xpack.ml.accessDeniedTabLabel', {
defaultMessage: 'Access Denied',
}),
},
};
export const MainTabs: FC<Props> = ({ tabId, disableLinks }) => {
const {
services: {
chrome: { docTitle },
},
} = useMlKibana();
const [globalState] = useUrlState('_g');
const [selectedTabId, setSelectedTabId] = useState(tabId);
function onSelectedTabChanged(id: TabId) {
@ -87,16 +124,40 @@ export const MainTabs: FC<Props> = ({ tabId, disableLinks }) => {
}
const tabs = getTabs(disableLinks);
const mlUrlGenerator = useMlUrlGenerator();
const navigateToPath = useNavigateToPath();
const redirectToTab = async (defaultPathId: MlUrlGeneratorState['page']) => {
const pageState =
globalState?.refreshInterval !== undefined
? {
globalState: {
refreshInterval: globalState.refreshInterval,
},
}
: undefined;
// TODO - Fix ts so passing pageState won't default to MlGenericUrlState when pageState is passed in
// @ts-ignore
const path = await mlUrlGenerator.createUrl({
page: defaultPathId,
// only retain the refreshInterval part of globalState
// appState will not be considered.
pageState,
});
await navigateToPath(path, false);
};
useEffect(() => {
docTitle.change([TAB_DATA[selectedTabId].name, ML_APP_NAME]);
}, [selectedTabId]);
return (
<EuiTabs display="condensed">
{tabs.map((tab: Tab) => {
const { id, disabled } = tab;
const testSubject = TAB_DATA[id].testSubject;
const defaultPathId = TAB_DATA[id].pathId || id;
// globalState (e.g. selected jobs and time range) should be retained when changing pages.
// appState will not be considered.
const fullGlobalStateString = globalState !== undefined ? `?_g=${encode(globalState)}` : '';
const defaultPathId = (TAB_DATA[id].pathId || id) as MlUrlGeneratorState['page'];
return disabled ? (
<EuiTab key={`${id}-key`} className={'mlNavigationMenu__mainTab'} disabled={true}>
@ -104,21 +165,18 @@ export const MainTabs: FC<Props> = ({ tabId, disableLinks }) => {
</EuiTab>
) : (
<div className="euiTab" key={`div-${id}-key`}>
<EuiLink
<EuiTab
data-test-subj={testSubject + (id === selectedTabId ? ' selected' : '')}
href={`#/${defaultPathId}${fullGlobalStateString}`}
key={`${id}-key`}
color="text"
className={'mlNavigationMenu__mainTab'}
onClick={() => {
onSelectedTabChanged(id);
redirectToTab(defaultPathId);
}}
isSelected={id === selectedTabId}
key={`tab-${id}-key`}
>
<EuiTab
className={'mlNavigationMenu__mainTab'}
onClick={() => onSelectedTabChanged(id)}
isSelected={id === selectedTabId}
key={`tab-${id}-key`}
>
{tab.name}
</EuiTab>
</EuiLink>
{tab.name}
</EuiTab>
</div>
);
})}

View file

@ -17,8 +17,19 @@ import { ScopeExpression } from './scope_expression';
import { checkPermission } from '../../capabilities/check_capabilities';
import { getScopeFieldDefaults } from './utils';
import { FormattedMessage } from '@kbn/i18n/react';
import { ML_PAGES } from '../../../../common/constants/ml_url_generator';
import { useMlUrlGenerator, useNavigateToPath } from '../../contexts/kibana';
function NoFilterListsCallOut() {
const mlUrlGenerator = useMlUrlGenerator();
const navigateToPath = useNavigateToPath();
const redirectToFilterManagementPage = async () => {
const path = await mlUrlGenerator.createUrl({
page: ML_PAGES.FILTER_LISTS_MANAGE,
});
await navigateToPath(path, true);
};
return (
<EuiCallOut
title={
@ -36,7 +47,7 @@ function NoFilterListsCallOut() {
to create the list of values you want to include or exclude in the rule."
values={{
filterListsLink: (
<EuiLink href="#/settings/filter_lists">
<EuiLink onClick={redirectToFilterManagementPage}>
<FormattedMessage
id="xpack.ml.ruleEditor.scopeSection.createFilterListsDescription.filterListsLinkText"
defaultMessage="Filter Lists"

View file

@ -9,4 +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';
export { useMlUrlGenerator, useMlLink } from './use_create_url';

View file

@ -22,6 +22,6 @@ interface StartPlugins {
share: SharePluginStart;
}
export type StartServices = CoreStart &
StartPlugins & { kibanaVersion: string } & MlServicesContext;
StartPlugins & { appName: string; kibanaVersion: string } & MlServicesContext;
export const useMlKibana = () => useKibana<StartServices>();
export type MlKibanaReactContextValue = KibanaReactContextValue<StartServices>;

View file

@ -4,8 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { useCallback, useEffect, useState } from 'react';
import { useMlKibana } from './kibana_context';
import { ML_APP_URL_GENERATOR } from '../../../../common/constants/ml_url_generator';
import { MlUrlGeneratorState } from '../../../../common/types/ml_url_generator';
import { useUrlState } from '../../util/url_state';
export const useMlUrlGenerator = () => {
const {
@ -18,3 +21,59 @@ export const useMlUrlGenerator = () => {
return getUrlGenerator(ML_APP_URL_GENERATOR);
};
export const useMlLink = (params: MlUrlGeneratorState): string => {
const [href, setHref] = useState<string>(params.page);
const mlUrlGenerator = useMlUrlGenerator();
useEffect(() => {
let isCancelled = false;
const generateUrl = async (_params: MlUrlGeneratorState) => {
const url = await mlUrlGenerator.createUrl(_params);
if (!isCancelled) {
setHref(url);
}
};
generateUrl(params);
return () => {
isCancelled = true;
};
}, [params]);
return href;
};
export const useCreateAndNavigateToMlLink = (
page: MlUrlGeneratorState['page']
): (() => Promise<void>) => {
const mlUrlGenerator = useMlUrlGenerator();
const [globalState] = useUrlState('_g');
const {
services: {
application: { navigateToUrl },
},
} = useMlKibana();
const redirectToMlPage = useCallback(
async (_page: MlUrlGeneratorState['page']) => {
const pageState =
globalState?.refreshInterval !== undefined
? {
globalState: {
refreshInterval: globalState.refreshInterval,
},
}
: undefined;
// TODO: fix ts only interpreting it as MlUrlGenericState if pageState is passed
// @ts-ignore
const url = await mlUrlGenerator.createUrl({ page: _page, pageState });
await navigateToUrl(url);
},
[mlUrlGenerator, navigateToUrl]
);
// returns the onClick callback
return useCallback(() => redirectToMlPage(page), [redirectToMlPage, page]);
};

View file

@ -15,8 +15,8 @@ import { SavedSearchQuery } from '../../contexts/ml';
import {
AnalysisConfig,
ClassificationAnalysis,
DataFrameAnalysisConfigType,
RegressionAnalysis,
ANALYSIS_CONFIG_TYPE,
} from '../../../../common/types/data_frame_analytics';
import {
isOutlierAnalysis,
@ -26,6 +26,7 @@ import {
getDependentVar,
getPredictedFieldName,
} from '../../../../common/util/analytics_utils';
import { ANALYSIS_CONFIG_TYPE } from '../../../../common/constants/data_frame_analytics';
export type IndexPattern = string;
export enum ANALYSIS_ADVANCED_FIELDS {
@ -429,7 +430,7 @@ interface LoadEvalDataConfig {
predictionFieldName?: string;
searchQuery?: ResultsSearchQuery;
ignoreDefaultQuery?: boolean;
jobType: ANALYSIS_CONFIG_TYPE;
jobType: DataFrameAnalysisConfigType;
requiresKeyword?: boolean;
}
@ -550,7 +551,7 @@ export {
isRegressionAnalysis,
isClassificationAnalysis,
getPredictionFieldName,
ANALYSIS_CONFIG_TYPE,
getDependentVar,
getPredictedFieldName,
ANALYSIS_CONFIG_TYPE,
};

View file

@ -14,7 +14,6 @@ export {
UpdateDataFrameAnalyticsConfig,
IndexPattern,
REFRESH_ANALYTICS_LIST_STATE,
ANALYSIS_CONFIG_TYPE,
OUTLIER_ANALYSIS_METHOD,
RegressionEvaluateResponse,
getValuesFromResponse,
@ -26,6 +25,7 @@ export {
SEARCH_SIZE,
defaultSearchQuery,
SearchQuery,
ANALYSIS_CONFIG_TYPE,
} from './analytics';
export {

View file

@ -8,7 +8,7 @@ import React, { Fragment, FC } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiFormRow, EuiSelect } from '@elastic/eui';
import { ANALYSIS_CONFIG_TYPE } from '../../../../common';
import { ANALYSIS_CONFIG_TYPE } from '../../../../../../../common/constants/data_frame_analytics';
import { AnalyticsJobType } from '../../../analytics_management/hooks/use_create_analytics_form/state';

View file

@ -16,6 +16,7 @@ import { BASIC_NUMERICAL_TYPES, EXTENDED_NUMERICAL_TYPES } from '../../../../com
import { CATEGORICAL_TYPES } from './form_options_validation';
import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public';
import { newJobCapsService } from '../../../../../services/new_job_capabilities_service';
import { DataFrameAnalysisConfigType } from '../../../../../../../common/types/data_frame_analytics';
const containsClassificationFieldsCb = ({ name, type }: Field) =>
!OMIT_FIELDS.includes(name) &&
@ -32,13 +33,13 @@ const containsRegressionFieldsCb = ({ name, type }: Field) =>
const containsOutlierFieldsCb = ({ name, type }: Field) =>
!OMIT_FIELDS.includes(name) && name !== EVENT_RATE_FIELD_ID && BASIC_NUMERICAL_TYPES.has(type);
const callbacks: Record<ANALYSIS_CONFIG_TYPE, (f: Field) => boolean> = {
const callbacks: Record<DataFrameAnalysisConfigType, (f: Field) => boolean> = {
[ANALYSIS_CONFIG_TYPE.CLASSIFICATION]: containsClassificationFieldsCb,
[ANALYSIS_CONFIG_TYPE.REGRESSION]: containsRegressionFieldsCb,
[ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION]: containsOutlierFieldsCb,
};
const messages: Record<ANALYSIS_CONFIG_TYPE, JSX.Element> = {
const messages: Record<DataFrameAnalysisConfigType, JSX.Element> = {
[ANALYSIS_CONFIG_TYPE.CLASSIFICATION]: (
<FormattedMessage
id="xpack.ml.dataframe.analytics.create.sourceObjectClassificationHelpText"

View file

@ -18,13 +18,13 @@ import { ml } from '../../../../../services/ml_api_service';
import { BackToListPanel } from '../back_to_list_panel';
import { ViewResultsPanel } from '../view_results_panel';
import { ProgressStats } from './progress_stats';
import { ANALYSIS_CONFIG_TYPE } from '../../../../common/analytics';
import { DataFrameAnalysisConfigType } from '../../../../../../../common/types/data_frame_analytics';
export const PROGRESS_REFRESH_INTERVAL_MS = 1000;
interface Props {
jobId: string;
jobType: ANALYSIS_CONFIG_TYPE;
jobType: DataFrameAnalysisConfigType;
showProgress: boolean;
}

View file

@ -8,12 +8,12 @@ import React, { FC, Fragment } from 'react';
import { EuiCard, EuiIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
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';
import { DataFrameAnalysisConfigType } from '../../../../../../../common/types/data_frame_analytics';
interface Props {
jobId: string;
analysisType: ANALYSIS_CONFIG_TYPE;
analysisType: DataFrameAnalysisConfigType;
}
export const ViewResultsPanel: FC<Props> = ({ jobId, analysisType }) => {

View file

@ -29,7 +29,6 @@ import {
SEARCH_SIZE,
defaultSearchQuery,
getAnalysisType,
ANALYSIS_CONFIG_TYPE,
} from '../../../../common';
import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/use_columns';
import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common';
@ -39,6 +38,7 @@ import { IndexPatternPrompt } from '../index_pattern_prompt';
import { useExplorationResults } from './use_exploration_results';
import { useMlKibana } from '../../../../../contexts/kibana';
import { DataFrameAnalysisConfigType } from '../../../../../../../common/types/data_frame_analytics';
const showingDocs = i18n.translate(
'xpack.ml.dataframe.analytics.explorationResults.documentsShownHelpText',
@ -195,7 +195,7 @@ export const ExplorationResultsTable: FC<Props> = React.memo(
{...classificationData}
dataTestSubj="mlExplorationDataGrid"
toastNotifications={getToastNotifications()}
analysisType={(analysisType as unknown) as ANALYSIS_CONFIG_TYPE}
analysisType={(analysisType as unknown) as DataFrameAnalysisConfigType}
/>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -26,11 +26,12 @@ import { OutlierExploration } from './components/outlier_exploration';
import { RegressionExploration } from './components/regression_exploration';
import { ClassificationExploration } from './components/classification_exploration';
import { ANALYSIS_CONFIG_TYPE } from '../../common/analytics';
import { ANALYSIS_CONFIG_TYPE } from '../../../../../common/constants/data_frame_analytics';
import { DataFrameAnalysisConfigType } from '../../../../../common/types/data_frame_analytics';
export const Page: FC<{
jobId: string;
analysisType: ANALYSIS_CONFIG_TYPE;
analysisType: DataFrameAnalysisConfigType;
}> = ({ jobId, analysisType }) => (
<Fragment>
<NavigationMenu tabId="data_frame_analytics" />

View file

@ -7,24 +7,32 @@
import React, { useCallback, useMemo } from 'react';
import { getAnalysisType } from '../../../../common/analytics';
import { useNavigateToPath } from '../../../../../contexts/kibana';
import { useMlUrlGenerator, useNavigateToPath } from '../../../../../contexts/kibana';
import {
getResultsUrl,
DataFrameAnalyticsListAction,
DataFrameAnalyticsListRow,
} from '../analytics_list/common';
import { DataFrameAnalyticsListAction, DataFrameAnalyticsListRow } from '../analytics_list/common';
import { getViewLinkStatus } from './get_view_link_status';
import { viewActionButtonText, ViewButton } from './view_button';
import { DataFrameAnalysisConfigType } from '../../../../../../../common/types/data_frame_analytics';
import { ML_PAGES } from '../../../../../../../common/constants/ml_url_generator';
export type ViewAction = ReturnType<typeof useViewAction>;
export const useViewAction = () => {
const mlUrlGenerator = useMlUrlGenerator();
const navigateToPath = useNavigateToPath();
const redirectToTab = async (jobId: string, analysisType: DataFrameAnalysisConfigType) => {
const path = await mlUrlGenerator.createUrl({
page: ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION,
pageState: { jobId, analysisType },
});
await navigateToPath(path, false);
};
const clickHandler = useCallback((item: DataFrameAnalyticsListRow) => {
const analysisType = getAnalysisType(item.config.analysis);
navigateToPath(getResultsUrl(item.id, analysisType));
const analysisType = getAnalysisType(item.config.analysis) as DataFrameAnalysisConfigType;
redirectToTab(item.id, analysisType);
}, []);
const action: DataFrameAnalyticsListAction = useMemo(

View file

@ -15,12 +15,8 @@ import {
EuiSearchBarProps,
EuiSpacer,
} from '@elastic/eui';
import {
DataFrameAnalyticsId,
useRefreshAnalyticsList,
ANALYSIS_CONFIG_TYPE,
} from '../../../../common';
import { ANALYSIS_CONFIG_TYPE } from '../../../../../../../common/constants/data_frame_analytics';
import { DataFrameAnalyticsId, useRefreshAnalyticsList } from '../../../../common';
import { checkPermission } from '../../../../../capabilities/check_capabilities';
import {

View file

@ -9,11 +9,8 @@ import { EuiTableActionsColumnType, Query, Ast } from '@elastic/eui';
import { DATA_FRAME_TASK_STATE } from './data_frame_task_state';
export { DATA_FRAME_TASK_STATE };
import {
DataFrameAnalyticsId,
DataFrameAnalyticsConfig,
ANALYSIS_CONFIG_TYPE,
} from '../../../../common';
import { DataFrameAnalyticsId, DataFrameAnalyticsConfig } from '../../../../common';
import { DataFrameAnalysisConfigType } from '../../../../../../../common/types/data_frame_analytics';
export enum DATA_FRAME_MODE {
BATCH = 'batch',
@ -111,10 +108,7 @@ export interface DataFrameAnalyticsListRow {
checkpointing: object;
config: DataFrameAnalyticsConfig;
id: DataFrameAnalyticsId;
job_type:
| ANALYSIS_CONFIG_TYPE.CLASSIFICATION
| ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION
| ANALYSIS_CONFIG_TYPE.REGRESSION;
job_type: DataFrameAnalysisConfigType;
mode: string;
state: DataFrameAnalyticsStats['state'];
stats: DataFrameAnalyticsStats;
@ -137,10 +131,6 @@ export function isCompletedAnalyticsJob(stats: DataFrameAnalyticsStats) {
return stats.state === DATA_FRAME_TASK_STATE.STOPPED && progress === 100;
}
export function getResultsUrl(jobId: string, analysisType: ANALYSIS_CONFIG_TYPE | string) {
return `#/data_frame_analytics/exploration?_g=(ml:(jobId:${jobId},analysisType:${analysisType}))`;
}
// The single Action type is not exported as is
// from EUI so we use that code to get the single
// Action type from the array of actions.

View file

@ -19,8 +19,6 @@ import {
EuiLink,
RIGHT_ALIGNMENT,
} from '@elastic/eui';
import { getJobIdUrl, TAB_IDS } from '../../../../../util/get_selected_ids_url';
import { getAnalysisType, DataFrameAnalyticsId } from '../../../../common';
import {
getDataFrameAnalyticsProgressPhase,
@ -32,6 +30,8 @@ import {
DataFrameAnalyticsStats,
} from './common';
import { useActions } from './use_actions';
import { useMlLink } from '../../../../../contexts/kibana';
import { ML_PAGES } from '../../../../../../../common/constants/ml_url_generator';
enum TASK_STATE_COLOR {
analyzing = 'primary',
@ -134,9 +134,14 @@ export const progressColumn = {
'data-test-subj': 'mlAnalyticsTableColumnProgress',
};
export const getDFAnalyticsJobIdLink = (item: DataFrameAnalyticsListRow) => (
<EuiLink href={getJobIdUrl(TAB_IDS.DATA_FRAME_ANALYTICS, item.id)}>{item.id}</EuiLink>
);
export const DFAnalyticsJobIdLink = ({ item }: { item: DataFrameAnalyticsListRow }) => {
const href = useMlLink({
page: ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE,
pageState: { jobId: item.id },
});
return <EuiLink href={href}>{item.id}</EuiLink>;
};
export const useColumns = (
expandedRowItemIds: DataFrameAnalyticsId[],
@ -145,7 +150,6 @@ export const useColumns = (
isMlEnabledInSpace: boolean = true
) => {
const { actions, modals } = useActions(isManagementTable);
function toggleDetails(item: DataFrameAnalyticsListRow) {
const index = expandedRowItemIds.indexOf(item.config.id);
if (index !== -1) {
@ -200,7 +204,7 @@ export const useColumns = (
'data-test-subj': 'mlAnalyticsTableColumnId',
scope: 'row',
render: (item: DataFrameAnalyticsListRow) =>
isManagementTable ? getDFAnalyticsJobIdLink(item) : item.id,
isManagementTable ? <DFAnalyticsJobIdLink item={item} /> : item.id,
},
{
field: DataFrameAnalyticsListColumn.description,

View file

@ -29,21 +29,23 @@ import { useInferenceApiService } from '../../../../../services/ml_api_service/i
import { ModelsTableToConfigMapping } from './index';
import { TIME_FORMAT } from '../../../../../../../common/constants/time_format';
import { DeleteModelsModal } from './delete_models_modal';
import { useMlKibana, useNotifications } from '../../../../../contexts/kibana';
import { useMlKibana, useMlUrlGenerator, useNotifications } from '../../../../../contexts/kibana';
import { ExpandedRow } from './expanded_row';
import { getResultsUrl } from '../analytics_list/common';
import {
ModelConfigResponse,
ModelPipelines,
TrainedModelStat,
} from '../../../../../../../common/types/inference';
import {
getAnalysisType,
REFRESH_ANALYTICS_LIST_STATE,
refreshAnalyticsList$,
useRefreshAnalyticsList,
} from '../../../../common';
import { useTableSettings } from '../analytics_list/use_table_settings';
import { filterAnalyticsModels, AnalyticsSearchBar } from '../analytics_search_bar';
import { ML_PAGES } from '../../../../../../../common/constants/ml_url_generator';
import { DataFrameAnalysisConfigType } from '../../../../../../../common/types/data_frame_analytics';
type Stats = Omit<TrainedModelStat, 'model_id'>;
@ -61,6 +63,7 @@ export const ModelsList: FC = () => {
application: { navigateToUrl, capabilities },
},
} = useMlKibana();
const urlGenerator = useMlUrlGenerator();
const canDeleteDataFrameAnalytics = capabilities.ml.canDeleteDataFrameAnalytics as boolean;
@ -278,12 +281,19 @@ export const ModelsList: FC = () => {
type: 'icon',
available: (item) => item.metadata?.analytics_config?.id,
onClick: async (item) => {
await navigateToUrl(
getResultsUrl(
item.metadata?.analytics_config.id,
Object.keys(item.metadata?.analytics_config.analysis)[0]
)
);
if (item.metadata?.analytics_config === undefined) return;
const url = await urlGenerator.createUrl({
page: ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION,
pageState: {
jobId: item.metadata?.analytics_config.id as string,
analysisType: getAnalysisType(
item.metadata?.analytics_config.analysis
) as DataFrameAnalysisConfigType,
},
});
await navigateToUrl(url);
},
isPrimary: true,
},

View file

@ -33,13 +33,13 @@ import {
JOB_ID_MAX_LENGTH,
ALLOWED_DATA_UNITS,
} from '../../../../../../../common/constants/validation';
import { ANALYSIS_CONFIG_TYPE } from '../../../../../../../common/constants/data_frame_analytics';
import {
getDependentVar,
getNumTopFeatureImportanceValues,
getTrainingPercent,
isRegressionAnalysis,
isClassificationAnalysis,
ANALYSIS_CONFIG_TYPE,
NUM_TOP_FEATURE_IMPORTANCE_VALUES_MIN,
TRAINING_PERCENT_MIN,
TRAINING_PERCENT_MAX,

View file

@ -8,13 +8,14 @@ import { DeepPartial, DeepReadonly } from '../../../../../../../common/types/com
import { checkPermission } from '../../../../../capabilities/check_capabilities';
import { mlNodesAvailable } from '../../../../../ml_nodes_check';
import { ANALYSIS_CONFIG_TYPE, defaultSearchQuery } from '../../../../common/analytics';
import { defaultSearchQuery, getAnalysisType } from '../../../../common/analytics';
import { CloneDataFrameAnalyticsConfig } from '../../components/action_clone';
import {
DataFrameAnalyticsConfig,
DataFrameAnalyticsId,
DataFrameAnalysisConfigType,
} from '../../../../../../../common/types/data_frame_analytics';
import { ANALYSIS_CONFIG_TYPE } from '../../../../../../../common/constants/data_frame_analytics';
export enum DEFAULT_MODEL_MEMORY_LIMIT {
regression = '100mb',
outlier_detection = '50mb',
@ -28,7 +29,7 @@ export const UNSET_CONFIG_ITEM = '--';
export type EsIndexName = string;
export type DependentVariable = string;
export type IndexPatternTitle = string;
export type AnalyticsJobType = ANALYSIS_CONFIG_TYPE | undefined;
export type AnalyticsJobType = DataFrameAnalysisConfigType | undefined;
type IndexPatternId = string;
export type SourceIndexMap = Record<
IndexPatternTitle,
@ -290,7 +291,7 @@ export function getFormStateFromJobConfig(
analyticsJobConfig: Readonly<CloneDataFrameAnalyticsConfig>,
isClone: boolean = true
): Partial<State['form']> {
const jobType = Object.keys(analyticsJobConfig.analysis)[0] as ANALYSIS_CONFIG_TYPE;
const jobType = getAnalysisType(analyticsJobConfig.analysis) as DataFrameAnalysisConfigType;
const resultState: Partial<State['form']> = {
jobType,

View file

@ -11,7 +11,7 @@ import {
GetDataFrameAnalyticsStatsResponseOk,
} from '../../../../../services/ml_api_service/data_frame_analytics';
import {
ANALYSIS_CONFIG_TYPE,
getAnalysisType,
REFRESH_ANALYTICS_LIST_STATE,
refreshAnalyticsList$,
} from '../../../../common';
@ -25,6 +25,7 @@ import {
isDataFrameAnalyticsStopped,
} from '../../components/analytics_list/common';
import { AnalyticStatsBarStats } from '../../../../../components/stats_bar';
import { DataFrameAnalysisConfigType } from '../../../../../../../common/types/data_frame_analytics';
export const isGetDataFrameAnalyticsStatsResponseOk = (
arg: any
@ -143,7 +144,7 @@ export const getAnalyticsFactory = (
checkpointing: {},
config,
id: config.id,
job_type: Object.keys(config.analysis)[0] as ANALYSIS_CONFIG_TYPE,
job_type: getAnalysisType(config.analysis) as DataFrameAnalysisConfigType,
mode: DATA_FRAME_MODE.BATCH,
state: stats.state,
stats,

View file

@ -52,7 +52,10 @@ function startTrialDescription() {
export const DatavisualizerSelector: FC = () => {
useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false });
const {
services: { licenseManagement },
services: {
licenseManagement,
http: { basePath },
},
} = useMlKibana();
const navigateToPath = useNavigateToPath();
@ -183,7 +186,10 @@ export const DatavisualizerSelector: FC = () => {
}
description={startTrialDescription()}
footer={
<EuiButton target="_blank" href="management/stack/license_management/home">
<EuiButton
target="_blank"
href={`${basePath.get()}/app/management/stack/license_management/home`}
>
<FormattedMessage
id="xpack.ml.datavisualizer.selector.startTrialButtonLabel"
defaultMessage="Start trial"

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC, useState, useEffect } from 'react';
import React, { FC, useState, useEffect, useCallback } from 'react';
import moment from 'moment';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiFlexGroup, EuiFlexItem, EuiCard, EuiIcon } from '@elastic/eui';
@ -12,7 +12,13 @@ import { ml } from '../../../../services/ml_api_service';
import { isFullLicense } from '../../../../license';
import { checkPermission } from '../../../../capabilities/check_capabilities';
import { mlNodesAvailable } from '../../../../ml_nodes_check/check_ml_nodes';
import { useMlKibana } from '../../../../contexts/kibana';
import { useMlKibana, useMlUrlGenerator, useNavigateToPath } from '../../../../contexts/kibana';
import { ML_PAGES } from '../../../../../../common/constants/ml_url_generator';
import { MlCommonGlobalState } from '../../../../../../common/types/ml_url_generator';
import {
DISCOVER_APP_URL_GENERATOR,
DiscoverUrlGeneratorState,
} from '../../../../../../../../../src/plugins/discover/public';
const RECHECK_DELAY_MS = 3000;
@ -36,12 +42,70 @@ export const ResultsLinks: FC<Props> = ({
to: 'now',
});
const [showCreateJobLink, setShowCreateJobLink] = useState(false);
const [globalStateString, setGlobalStateString] = useState('');
const [globalState, setGlobalState] = useState<MlCommonGlobalState | undefined>();
const [discoverLink, setDiscoverLink] = useState('');
const {
services: {
http: { basePath },
},
} = useMlKibana();
const mlUrlGenerator = useMlUrlGenerator();
const navigateToPath = useNavigateToPath();
const {
services: {
share: {
urlGenerators: { getUrlGenerator },
},
},
} = useMlKibana();
useEffect(() => {
let unmounted = false;
const getDiscoverUrl = async (): Promise<void> => {
const state: DiscoverUrlGeneratorState = {
indexPatternId,
};
if (globalState?.time) {
state.timeRange = globalState.time;
}
if (!unmounted) {
const discoverUrlGenerator = getUrlGenerator(DISCOVER_APP_URL_GENERATOR);
const discoverUrl = await discoverUrlGenerator.createUrl(state);
setDiscoverLink(discoverUrl);
}
};
getDiscoverUrl();
return () => {
unmounted = true;
};
}, [indexPatternId, getUrlGenerator]);
const openInDataVisualizer = useCallback(async () => {
const path = await mlUrlGenerator.createUrl({
page: ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER,
pageState: {
index: indexPatternId,
globalState,
},
});
await navigateToPath(path);
}, [indexPatternId, globalState]);
const redirectToADCreateJobsSelectTypePage = useCallback(async () => {
const path = await mlUrlGenerator.createUrl({
page: ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE,
pageState: {
index: indexPatternId,
globalState,
},
});
await navigateToPath(path);
}, [indexPatternId, globalState]);
useEffect(() => {
setShowCreateJobLink(checkPermission('canCreateJob') && mlNodesAvailable());
@ -49,11 +113,13 @@ export const ResultsLinks: FC<Props> = ({
}, []);
useEffect(() => {
const _g =
timeFieldName !== undefined
? `&_g=(time:(from:'${duration.from}',mode:quick,to:'${duration.to}'))`
: '';
setGlobalStateString(_g);
const _globalState: MlCommonGlobalState = {
time: {
from: duration.from,
to: duration.to,
},
};
setGlobalState(_globalState);
}, [duration]);
async function updateTimeValues(recheck = true) {
@ -89,7 +155,7 @@ export const ResultsLinks: FC<Props> = ({
/>
}
description=""
href={`${basePath.get()}/app/discover#/?&_a=(index:'${indexPatternId}')${globalStateString}`}
href={discoverLink}
/>
</EuiFlexItem>
)}
@ -108,7 +174,7 @@ export const ResultsLinks: FC<Props> = ({
/>
}
description=""
href={`#/jobs/new_job/step/job_type?index=${indexPatternId}${globalStateString}`}
onClick={redirectToADCreateJobsSelectTypePage}
/>
</EuiFlexItem>
)}
@ -124,7 +190,7 @@ export const ResultsLinks: FC<Props> = ({
/>
}
description=""
href={`#/jobs/new_job/datavisualizer?index=${indexPatternId}${globalStateString}`}
onClick={openInDataVisualizer}
/>
</EuiFlexItem>
)}

View file

@ -9,11 +9,11 @@ import React, { FC, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { EuiSpacer, EuiText, EuiTitle, EuiFlexGroup } from '@elastic/eui';
import { Link } from 'react-router-dom';
import { IndexPattern } from '../../../../../../../../../src/plugins/data/public';
import { CreateJobLinkCard } from '../../../../components/create_job_link_card';
import { DataRecognizer } from '../../../../components/data_recognizer';
import { getBasePath } from '../../../../util/dependency_cache';
import { ML_PAGES } from '../../../../../../common/constants/ml_url_generator';
interface Props {
indexPattern: IndexPattern;
@ -21,7 +21,6 @@ interface Props {
export const ActionsPanel: FC<Props> = ({ indexPattern }) => {
const [recognizerResultsCount, setRecognizerResultsCount] = useState(0);
const basePath = getBasePath();
const recognizerResults = {
count: 0,
@ -29,12 +28,7 @@ export const ActionsPanel: FC<Props> = ({ indexPattern }) => {
setRecognizerResultsCount(recognizerResults.count);
},
};
function openAdvancedJobWizard() {
// TODO - pass the search string to the advanced job page as well as the index pattern
// (add in with new advanced job wizard?)
window.open(`${basePath.get()}/app/ml/jobs/new_job/advanced?index=${indexPattern.id}`, '_self');
}
const createJobLink = `/${ML_PAGES.ANOMALY_DETECTION_CREATE_JOB}/advanced?index=${indexPattern.id}`;
// Note we use display:none for the DataRecognizer section as it needs to be
// passed the recognizerResults object, and then run the recognizer check which
@ -78,19 +72,19 @@ export const ActionsPanel: FC<Props> = ({ indexPattern }) => {
</p>
</EuiText>
<EuiSpacer size="m" />
<CreateJobLinkCard
icon="createAdvancedJob"
title={i18n.translate('xpack.ml.datavisualizer.actionsPanel.advancedTitle', {
defaultMessage: 'Advanced',
})}
description={i18n.translate('xpack.ml.datavisualizer.actionsPanel.advancedDescription', {
defaultMessage:
'Use the full range of options to create a job for more advanced use cases',
})}
onClick={openAdvancedJobWizard}
href={`${basePath.get()}/app/ml/jobs/new_job/advanced?index=${indexPattern.id}`}
data-test-subj="mlDataVisualizerCreateAdvancedJobCard"
/>
<Link to={createJobLink}>
<CreateJobLinkCard
icon="createAdvancedJob"
title={i18n.translate('xpack.ml.datavisualizer.actionsPanel.advancedTitle', {
defaultMessage: 'Advanced',
})}
description={i18n.translate('xpack.ml.datavisualizer.actionsPanel.advancedDescription', {
defaultMessage:
'Use the full range of options to create a job for more advanced use cases',
})}
data-test-subj="mlDataVisualizerCreateAdvancedJobCard"
/>
</Link>
</div>
);
};

View file

@ -3,17 +3,20 @@
exports[`ExplorerNoInfluencersFound snapshot 1`] = `
<EuiEmptyPrompt
actions={
<EuiButton
color="primary"
fill={true}
href="ml#/jobs"
<Link
to="/jobs"
>
<FormattedMessage
defaultMessage="Create job"
id="xpack.ml.explorer.createNewJobLinkText"
values={Object {}}
/>
</EuiButton>
<EuiButton
color="primary"
fill={true}
>
<FormattedMessage
defaultMessage="Create job"
id="xpack.ml.explorer.createNewJobLinkText"
values={Object {}}
/>
</EuiButton>
</Link>
}
data-test-subj="mlNoJobsFound"
iconType="alert"

View file

@ -7,25 +7,40 @@
/*
* React component for rendering EuiEmptyPrompt when no jobs were found.
*/
import { Link } from 'react-router-dom';
import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiEmptyPrompt, EuiButton } from '@elastic/eui';
import { ML_PAGES } from '../../../../../common/constants/ml_url_generator';
import { useMlLink } from '../../../contexts/kibana/use_create_url';
export const ExplorerNoJobsFound = () => (
<EuiEmptyPrompt
iconType="alert"
title={
<h2>
<FormattedMessage id="xpack.ml.explorer.noJobsFoundLabel" defaultMessage="No jobs found" />
</h2>
}
actions={
<EuiButton color="primary" fill href="ml#/jobs">
<FormattedMessage id="xpack.ml.explorer.createNewJobLinkText" defaultMessage="Create job" />
</EuiButton>
}
data-test-subj="mlNoJobsFound"
/>
);
export const ExplorerNoJobsFound = () => {
const ADJobsManagementUrl = useMlLink({
page: ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE,
excludeBasePath: true,
});
return (
<EuiEmptyPrompt
iconType="alert"
title={
<h2>
<FormattedMessage
id="xpack.ml.explorer.noJobsFoundLabel"
defaultMessage="No jobs found"
/>
</h2>
}
actions={
<Link to={ADJobsManagementUrl}>
<EuiButton color="primary" fill>
<FormattedMessage
id="xpack.ml.explorer.createNewJobLinkText"
defaultMessage="Create job"
/>
</EuiButton>
</Link>
}
data-test-subj="mlNoJobsFound"
/>
);
};

View file

@ -8,6 +8,9 @@ import React from 'react';
import { shallow } from 'enzyme';
import { ExplorerNoJobsFound } from './explorer_no_jobs_found';
jest.mock('../../../contexts/kibana/use_create_url', () => ({
useMlLink: jest.fn().mockReturnValue('/jobs'),
}));
describe('ExplorerNoInfluencersFound', () => {
test('snapshot', () => {
const wrapper = shallow(<ExplorerNoJobsFound />);

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import React, { useCallback } from 'react';
import {
EuiButtonEmpty,
@ -28,6 +28,10 @@ import { CHART_TYPE } from '../explorer_constants';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { MlTooltipComponent } from '../../components/chart_tooltip';
import { withKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { ML_APP_URL_GENERATOR } from '../../../../common/constants/ml_url_generator';
import { PLUGIN_ID } from '../../../../common/constants/app';
import { addItemToRecentlyAccessed } from '../../util/recently_accessed';
const textTooManyBuckets = i18n.translate('xpack.ml.explorer.charts.tooManyBucketsDescription', {
defaultMessage:
@ -51,7 +55,23 @@ function getChartId(series) {
}
// Wrapper for a single explorer chart
function ExplorerChartContainer({ series, severity, tooManyBuckets, wrapLabel }) {
function ExplorerChartContainer({
series,
severity,
tooManyBuckets,
wrapLabel,
navigateToApp,
mlUrlGenerator,
}) {
const redirectToSingleMetricViewer = useCallback(async () => {
const singleMetricViewerLink = await getExploreSeriesLink(mlUrlGenerator, series);
addItemToRecentlyAccessed('timeseriesexplorer', series.jobId, singleMetricViewerLink);
await navigateToApp(PLUGIN_ID, {
path: singleMetricViewerLink,
});
}, [mlUrlGenerator]);
const { detectorLabel, entityFields } = series;
const chartType = getChartType(series);
@ -106,7 +126,7 @@ function ExplorerChartContainer({ series, severity, tooManyBuckets, wrapLabel })
iconSide="right"
iconType="visLine"
size="xs"
onClick={() => window.open(getExploreSeriesLink(series), '_blank')}
onClick={redirectToSingleMetricViewer}
>
<FormattedMessage id="xpack.ml.explorer.charts.viewLabel" defaultMessage="View" />
</EuiButtonEmpty>
@ -150,12 +170,24 @@ function ExplorerChartContainer({ series, severity, tooManyBuckets, wrapLabel })
}
// Flex layout wrapper for all explorer charts
export const ExplorerChartsContainer = ({
export const ExplorerChartsContainerUI = ({
chartsPerRow,
seriesToPlot,
severity,
tooManyBuckets,
kibana,
}) => {
const {
services: {
application: { navigateToApp },
share: {
urlGenerators: { getUrlGenerator },
},
},
} = kibana;
const mlUrlGenerator = getUrlGenerator(ML_APP_URL_GENERATOR);
// <EuiFlexGrid> doesn't allow a setting of `columns={1}` when chartsPerRow would be 1.
// If that's the case we trick it doing that with the following settings:
const chartsWidth = chartsPerRow === 1 ? 'calc(100% - 20px)' : 'auto';
@ -177,9 +209,13 @@ export const ExplorerChartsContainer = ({
severity={severity}
tooManyBuckets={tooManyBuckets}
wrapLabel={wrapLabel}
navigateToApp={navigateToApp}
mlUrlGenerator={mlUrlGenerator}
/>
</EuiFlexItem>
))}
</EuiFlexGrid>
);
};
export const ExplorerChartsContainer = withKibana(ExplorerChartsContainerUI);

View file

@ -40,6 +40,12 @@ jest.mock('../../services/job_service', () => ({
},
}));
jest.mock('../../../../../../../src/plugins/kibana_react/public', () => ({
withKibana: (comp) => {
return comp;
},
}));
describe('ExplorerChartsContainer', () => {
const mockedGetBBox = { x: 0, y: -11.5, width: 12.1875, height: 14.5 };
const originalGetBBox = SVGElement.prototype.getBBox;
@ -47,10 +53,22 @@ describe('ExplorerChartsContainer', () => {
beforeEach(() => (SVGElement.prototype.getBBox = () => mockedGetBBox));
afterEach(() => (SVGElement.prototype.getBBox = originalGetBBox));
const kibanaContextMock = {
services: {
application: { navigateToApp: jest.fn() },
share: {
urlGenerators: { getUrlGenerator: jest.fn() },
},
},
};
test('Minimal Initialization', () => {
const wrapper = shallow(
<I18nProvider>
<ExplorerChartsContainer {...getDefaultChartsData()} />
<ExplorerChartsContainer
{...getDefaultChartsData()}
kibana={kibanaContextMock}
severity={10}
/>
</I18nProvider>
);
@ -71,10 +89,11 @@ describe('ExplorerChartsContainer', () => {
],
chartsPerRow: 1,
tooManyBuckets: false,
severity: 10,
};
const wrapper = mount(
<I18nProvider>
<ExplorerChartsContainer {...props} />
<ExplorerChartsContainer {...props} kibana={kibanaContextMock} />
</I18nProvider>
);
@ -98,10 +117,11 @@ describe('ExplorerChartsContainer', () => {
],
chartsPerRow: 1,
tooManyBuckets: false,
severity: 10,
};
const wrapper = mount(
<I18nProvider>
<ExplorerChartsContainer {...props} />
<ExplorerChartsContainer {...props} kibana={kibanaContextMock} />
</I18nProvider>
);

View file

@ -5,13 +5,20 @@
*/
import PropTypes from 'prop-types';
import React from 'react';
import React, { useMemo } from 'react';
import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useCreateADLinks } from '../../../../components/custom_hooks/use_create_ad_links';
import { Link } from 'react-router-dom';
import { useMlKibana } from '../../../../contexts/kibana';
export function ResultLinks({ jobs }) {
export function ResultLinks({ jobs, isManagementTable }) {
const {
services: {
http: { basePath },
},
} = useMlKibana();
const openJobsInSingleMetricViewerText = i18n.translate(
'xpack.ml.jobsList.resultActions.openJobsInSingleMetricViewerText',
{
@ -37,29 +44,59 @@ export function ResultLinks({ jobs }) {
const singleMetricEnabled = jobs.length === 1 && jobs[0].isSingleMetricViewerJob;
const jobActionsDisabled = jobs.length === 1 && jobs[0].deleting === true;
const { createLinkWithUserDefaults } = useCreateADLinks();
const timeSeriesExplorerLink = useMemo(
() => createLinkWithUserDefaults('timeseriesexplorer', jobs),
[jobs]
);
const anomalyExplorerLink = useMemo(() => createLinkWithUserDefaults('explorer', jobs), [jobs]);
return (
<React.Fragment>
{singleMetricVisible && (
<EuiToolTip position="bottom" content={openJobsInSingleMetricViewerText}>
<EuiButtonIcon
href={createLinkWithUserDefaults('timeseriesexplorer', jobs)}
iconType="visLine"
aria-label={openJobsInSingleMetricViewerText}
className="results-button"
isDisabled={singleMetricEnabled === false || jobActionsDisabled === true}
data-test-subj="mlOpenJobsInSingleMetricViewerButton"
/>
{isManagementTable ? (
<EuiButtonIcon
href={`${basePath.get()}/app/ml/${timeSeriesExplorerLink}`}
iconType="visLine"
aria-label={openJobsInSingleMetricViewerText}
className="results-button"
isDisabled={singleMetricEnabled === false || jobActionsDisabled === true}
data-test-subj="mlOpenJobsInSingleMetricViewerFromManagementButton"
/>
) : (
<Link to={timeSeriesExplorerLink}>
<EuiButtonIcon
iconType="visLine"
aria-label={openJobsInSingleMetricViewerText}
className="results-button"
isDisabled={singleMetricEnabled === false || jobActionsDisabled === true}
data-test-subj="mlOpenJobsInSingleMetricViewerButton"
/>
</Link>
)}
</EuiToolTip>
)}
<EuiToolTip position="bottom" content={openJobsInAnomalyExplorerText}>
<EuiButtonIcon
href={createLinkWithUserDefaults('explorer', jobs)}
iconType="visTable"
aria-label={openJobsInAnomalyExplorerText}
className="results-button"
isDisabled={jobActionsDisabled === true}
data-test-subj="mlOpenJobsInAnomalyExplorerButton"
/>
{isManagementTable ? (
<EuiButtonIcon
href={`${basePath.get()}/app/ml/${anomalyExplorerLink}`}
iconType="visTable"
aria-label={openJobsInSingleMetricViewerText}
className="results-button"
isDisabled={singleMetricEnabled === false || jobActionsDisabled === true}
data-test-subj="mlOpenJobsInSingleMetricViewerFromManagementButton"
/>
) : (
<Link to={anomalyExplorerLink}>
<EuiButtonIcon
iconType="visTable"
aria-label={openJobsInAnomalyExplorerText}
className="results-button"
isDisabled={jobActionsDisabled === true}
data-test-subj="mlOpenJobsInAnomalyExplorerButton"
/>
</Link>
)}
</EuiToolTip>
<div className="actions-border" />
</React.Fragment>

View file

@ -5,10 +5,10 @@
*/
import React from 'react';
import { EuiLink } from '@elastic/eui';
import { detectorToString } from '../../../../util/string_utils';
import { formatValues, filterObjects } from './format_values';
import { i18n } from '@kbn/i18n';
import { Link } from 'react-router-dom';
export function extractJobDetails(job) {
if (Object.keys(job).length === 0) {
@ -61,7 +61,7 @@ export function extractJobDetails(job) {
if (job.calendars) {
calendars.items = job.calendars.map((c) => [
'',
<EuiLink href={`#/settings/calendars_list/edit_calendar/${c}?_g=()`}>{c}</EuiLink>,
<Link to={`/settings/calendars_list/edit_calendar/${c}?_g=()`}>{c}</Link>,
]);
// remove the calendars list from the general section
// so not to show it twice.

View file

@ -5,8 +5,6 @@
*/
import PropTypes from 'prop-types';
import rison from 'rison-node';
import React, { Component } from 'react';
import {
@ -30,13 +28,19 @@ import {
getLatestDataOrBucketTimestamp,
isTimeSeriesViewJob,
} from '../../../../../../../common/util/job_utils';
import { withKibana } from '../../../../../../../../../../src/plugins/kibana_react/public';
import {
ML_APP_URL_GENERATOR,
ML_PAGES,
} from '../../../../../../../common/constants/ml_url_generator';
import { PLUGIN_ID } from '../../../../../../../common/constants/app';
const MAX_FORECASTS = 500;
/**
* Table component for rendering the lists of forecasts run on an ML job.
*/
export class ForecastsTable extends Component {
export class ForecastsTableUI extends Component {
constructor(props) {
super(props);
this.state = {
@ -78,7 +82,17 @@ export class ForecastsTable extends Component {
}
}
openSingleMetricView(forecast) {
async openSingleMetricView(forecast) {
const {
services: {
application: { navigateToApp },
share: {
urlGenerators: { getUrlGenerator },
},
},
} = this.props.kibana;
// Creates the link to the Single Metric Viewer.
// Set the total time range from the start of the job data to the end of the forecast,
const dataCounts = this.props.job.data_counts;
@ -93,31 +107,7 @@ export class ForecastsTable extends Component {
? new Date(forecast.forecast_end_timestamp).toISOString()
: new Date(resultLatest).toISOString();
const _g = rison.encode({
ml: {
jobIds: [this.props.job.job_id],
},
refreshInterval: {
display: 'Off',
pause: false,
value: 0,
},
time: {
from,
to,
mode: 'absolute',
},
});
const appState = {
query: {
query_string: {
analyze_wildcard: true,
query: '*',
},
},
};
let mlTimeSeriesExplorer = {};
if (forecast !== undefined) {
// Set the zoom to show duration before the forecast equal to the length of the forecast.
const forecastDurationMs =
@ -126,8 +116,7 @@ export class ForecastsTable extends Component {
forecast.forecast_start_timestamp - forecastDurationMs,
jobEarliest
);
appState.mlTimeSeriesExplorer = {
mlTimeSeriesExplorer = {
forecastId: forecast.forecast_id,
zoom: {
from: new Date(zoomFrom).toISOString(),
@ -136,11 +125,39 @@ export class ForecastsTable extends Component {
};
}
const _a = rison.encode(appState);
const url = `?_g=${_g}&_a=${_a}`;
addItemToRecentlyAccessed('timeseriesexplorer', this.props.job.job_id, url);
window.open(`#/timeseriesexplorer${url}`, '_self');
const mlUrlGenerator = getUrlGenerator(ML_APP_URL_GENERATOR);
const singleMetricViewerForecastLink = await mlUrlGenerator.createUrl({
page: ML_PAGES.SINGLE_METRIC_VIEWER,
pageState: {
timeRange: {
from,
to,
mode: 'absolute',
},
refreshInterval: {
display: 'Off',
pause: false,
value: 0,
},
jobIds: [this.props.job.job_id],
query: {
query_string: {
analyze_wildcard: true,
query: '*',
},
},
...mlTimeSeriesExplorer,
},
excludeBasePath: true,
});
addItemToRecentlyAccessed(
'timeseriesexplorer',
this.props.job.job_id,
singleMetricViewerForecastLink
);
await navigateToApp(PLUGIN_ID, {
path: singleMetricViewerForecastLink,
});
}
render() {
@ -322,6 +339,8 @@ export class ForecastsTable extends Component {
);
}
}
ForecastsTable.propTypes = {
ForecastsTableUI.propTypes = {
job: PropTypes.object.isRequired,
};
export const ForecastsTable = withKibana(ForecastsTableUI);

View file

@ -8,7 +8,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import { JobGroup } from '../job_group';
import { getGroupIdsUrl, TAB_IDS } from '../../../../util/get_selected_ids_url';
import { AnomalyDetectionJobIdLink } from './job_id_link';
export function JobDescription({ job, isManagementTable }) {
return (
@ -17,11 +17,7 @@ export function JobDescription({ job, isManagementTable }) {
{job.description} &nbsp;
{job.groups.map((group) => {
if (isManagementTable === true) {
return (
<a key={group} href={getGroupIdsUrl(TAB_IDS.ANOMALY_DETECTION, [group])}>
<JobGroup name={group} />
</a>
);
return <AnomalyDetectionJobIdLink key={group} groupId={group} />;
}
return <JobGroup key={group} name={group} />;
})}

View file

@ -0,0 +1,63 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { EuiLink } from '@elastic/eui';
import { useMlKibana, useMlUrlGenerator } from '../../../../contexts/kibana';
import { ML_PAGES } from '../../../../../../common/constants/ml_url_generator';
import { AnomalyDetectionQueryState } from '../../../../../../common/types/ml_url_generator';
// @ts-ignore
import { JobGroup } from '../job_group';
interface JobIdLink {
id: string;
}
interface GroupIdLink {
groupId: string;
children: string;
}
type AnomalyDetectionJobIdLinkProps = JobIdLink | GroupIdLink;
function isGroupIdLink(props: JobIdLink | GroupIdLink): props is GroupIdLink {
return (props as GroupIdLink).groupId !== undefined;
}
export const AnomalyDetectionJobIdLink = (props: AnomalyDetectionJobIdLinkProps) => {
const mlUrlGenerator = useMlUrlGenerator();
const {
services: {
application: { navigateToUrl },
},
} = useMlKibana();
const redirectToJobsManagementPage = async () => {
const pageState: AnomalyDetectionQueryState = {};
if (isGroupIdLink(props)) {
pageState.groupIds = [props.groupId];
} else {
pageState.jobId = props.id;
}
const url = await mlUrlGenerator.createUrl({
page: ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE,
pageState,
});
await navigateToUrl(url);
};
if (isGroupIdLink(props)) {
return (
<EuiLink key={props.groupId} onClick={() => redirectToJobsManagementPage()}>
<JobGroup name={props.groupId} />
</EuiLink>
);
} else {
return (
<EuiLink key={props.id} onClick={() => redirectToJobsManagementPage()}>
{props.id}
</EuiLink>
);
}
};

View file

@ -14,12 +14,12 @@ import { toLocaleString } from '../../../../util/string_utils';
import { ResultLinks, actionsMenuContent } from '../job_actions';
import { JobDescription } from './job_description';
import { JobIcon } from '../../../../components/job_message_icon';
import { getJobIdUrl, TAB_IDS } from '../../../../util/get_selected_ids_url';
import { TIME_FORMAT } from '../../../../../../common/constants/time_format';
import { EuiBadge, EuiBasicTable, EuiButtonIcon, EuiLink, EuiScreenReaderOnly } from '@elastic/eui';
import { EuiBadge, EuiBasicTable, EuiButtonIcon, EuiScreenReaderOnly } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { AnomalyDetectionJobIdLink } from './job_id_link';
const PAGE_SIZE = 10;
const PAGE_SIZE_OPTIONS = [10, 25, 50];
@ -71,7 +71,7 @@ export class JobsList extends Component {
return id;
}
return <EuiLink href={getJobIdUrl(TAB_IDS.ANOMALY_DETECTION, id)}>{id}</EuiLink>;
return <AnomalyDetectionJobIdLink key={id} id={id} />;
}
getPageOfJobs(index, size, sortField, sortDirection) {
@ -241,7 +241,7 @@ export class JobsList extends Component {
name: i18n.translate('xpack.ml.jobsList.actionsLabel', {
defaultMessage: 'Actions',
}),
render: (item) => <ResultLinks jobs={[item]} />,
render: (item) => <ResultLinks jobs={[item]} isManagementTable={isManagementTable} />,
},
];

View file

@ -11,13 +11,13 @@ import React from 'react';
import { EuiButton } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
function newJob() {
window.location.href = `#/jobs/new_job`;
}
import { useCreateAndNavigateToMlLink } from '../../../../contexts/kibana/use_create_url';
import { ML_PAGES } from '../../../../../../common/constants/ml_url_generator';
export function NewJobButton() {
const buttonEnabled = checkPermission('canCreateJob') && mlNodesAvailable();
const newJob = useCreateAndNavigateToMlLink(ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_INDEX);
return (
<EuiButton
data-test-subj="mlCreateNewJobButton"

View file

@ -394,7 +394,7 @@ export function clearSelectedJobIdFromUrl(url) {
url = decodeURIComponent(url);
if (url.includes('mlManagement') && (url.includes('jobId') || url.includes('groupIds'))) {
const urlParams = getUrlVars(url);
const clearedParams = `ml#/jobs?_g=${urlParams._g}`;
const clearedParams = `jobs?_g=${urlParams._g}`;
window.history.replaceState({}, document.title, clearedParams);
}
}

View file

@ -26,6 +26,7 @@ import { PLUGIN_ID } from '../../../../../../../../../../../common/constants/app
import { Calendar } from '../../../../../../../../../../../common/types/calendars';
import { useMlKibana } from '../../../../../../../../../contexts/kibana';
import { GLOBAL_CALENDAR } from '../../../../../../../../../../../common/constants/calendars';
import { ML_PAGES } from '../../../../../../../../../../../common/constants/ml_url_generator';
export const CalendarsSelection: FC = () => {
const {
@ -73,7 +74,7 @@ export const CalendarsSelection: FC = () => {
};
const manageCalendarsHref = getUrlForApp(PLUGIN_ID, {
path: '/settings/calendars_list',
path: ML_PAGES.CALENDARS_MANAGE,
});
return (

View file

@ -39,7 +39,10 @@ import { JobSectionTitle, DatafeedSectionTitle } from './components/common';
export const SummaryStep: FC<StepProps> = ({ setCurrentStep, isCurrentStep }) => {
const {
services: { notifications },
services: {
notifications,
http: { basePath },
},
} = useMlKibana();
const navigateToPath = useNavigateToPath();
@ -108,7 +111,7 @@ export const SummaryStep: FC<StepProps> = ({ setCurrentStep, isCurrentStep }) =>
jobCreator.end,
isSingleMetricJobCreator(jobCreator) === true ? 'timeseriesexplorer' : 'explorer'
);
window.open(url, '_blank');
navigateToPath(`${basePath.get()}/app/ml/${url}`);
}
function clickResetJob() {

View file

@ -4,19 +4,24 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { ApplicationStart } from 'kibana/public';
import { IndexPatternsContract } from '../../../../../../../../../src/plugins/data/public';
import { mlJobService } from '../../../../services/job_service';
import { loadIndexPatterns, getIndexPatternIdFromName } from '../../../../util/index_utils';
import { CombinedJob } from '../../../../../../common/types/anomaly_detection_jobs';
import { CREATED_BY_LABEL, JOB_TYPE } from '../../../../../../common/constants/new_job';
export async function preConfiguredJobRedirect(indexPatterns: IndexPatternsContract) {
export async function preConfiguredJobRedirect(
indexPatterns: IndexPatternsContract,
basePath: string,
navigateToUrl: ApplicationStart['navigateToUrl']
) {
const { job } = mlJobService.tempJobCloningObjects;
if (job) {
try {
await loadIndexPatterns(indexPatterns);
const redirectUrl = getWizardUrlFromCloningJob(job);
window.location.href = `#/${redirectUrl}`;
await navigateToUrl(`${basePath}/app/ml/${redirectUrl}`);
return Promise.reject();
} catch (error) {
return Promise.resolve();

View file

@ -19,6 +19,7 @@ import {
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { useNavigateToPath } from '../../../../contexts/kibana';
import { useMlContext } from '../../../../contexts/ml';
import { isSavedSearchSavedObject } from '../../../../../../common/types/kibana';
import { DataRecognizer } from '../../../../components/data_recognizer';
@ -26,10 +27,15 @@ import { addItemToRecentlyAccessed } from '../../../../util/recently_accessed';
import { timeBasedIndexCheck } from '../../../../util/index_utils';
import { CreateJobLinkCard } from '../../../../components/create_job_link_card';
import { CategorizationIcon } from './categorization_job_icon';
import { ML_PAGES } from '../../../../../../common/constants/ml_url_generator';
import { useCreateAndNavigateToMlLink } from '../../../../contexts/kibana/use_create_url';
export const Page: FC = () => {
const mlContext = useMlContext();
const navigateToPath = useNavigateToPath();
const onSelectDifferentIndex = useCreateAndNavigateToMlLink(
ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_INDEX
);
const [recognizerResultsCount, setRecognizerResultsCount] = useState(0);
@ -193,7 +199,7 @@ export const Page: FC = () => {
defaultMessage="Anomaly detection can only be run over indices which are time based."
/>
<br />
<EuiLink href="#jobs/new_job">
<EuiLink onClick={onSelectDifferentIndex}>
<FormattedMessage
id="xpack.ml.newJob.wizard.jobType.selectDifferentIndexLinkText"
defaultMessage="Select a different index"

View file

@ -21,7 +21,8 @@ import {
EuiPanel,
} from '@elastic/eui';
import { merge } from 'lodash';
import { useMlKibana } from '../../../contexts/kibana';
import moment from 'moment';
import { useMlKibana, useMlUrlGenerator } from '../../../contexts/kibana';
import { ml } from '../../../services/ml_api_service';
import { useMlContext } from '../../../contexts/ml';
import {
@ -32,7 +33,6 @@ import {
KibanaObjectResponse,
ModuleJob,
} from '../../../../../common/types/modules';
import { mlJobService } from '../../../services/job_service';
import { CreateResultCallout } from './components/create_result_callout';
import { KibanaObjects } from './components/kibana_objects';
import { ModuleJobs } from './components/module_jobs';
@ -40,6 +40,8 @@ import { checkForSavedObjects } from './resolvers';
import { JobSettingsForm, JobSettingsFormValues } from './components/job_settings_form';
import { TimeRange } from '../common/components';
import { JobId } from '../../../../../common/types/anomaly_detection_jobs';
import { ML_PAGES } from '../../../../../common/constants/ml_url_generator';
import { TIME_FORMAT } from '../../../../../common/constants/time_format';
export interface ModuleJobUI extends ModuleJob {
datafeedResult?: DatafeedResponse;
@ -71,6 +73,8 @@ export const Page: FC<PageProps> = ({ moduleId, existingGroupIds }) => {
const {
services: { notifications },
} = useMlKibana();
const urlGenerator = useMlUrlGenerator();
// #region State
const [jobPrefix, setJobPrefix] = useState<string>('');
const [jobs, setJobs] = useState<ModuleJobUI[]>([]);
@ -185,14 +189,20 @@ export const Page: FC<PageProps> = ({ moduleId, existingGroupIds }) => {
})
);
setKibanaObjects(merge(kibanaObjects, kibanaResponse));
setResultsUrl(
mlJobService.createResultsUrl(
jobsResponse.filter(({ success }) => success).map(({ id }) => id),
resultTimeRange.start,
resultTimeRange.end,
'explorer'
)
);
const url = await urlGenerator.createUrl({
page: ML_PAGES.ANOMALY_EXPLORER,
pageState: {
jobIds: jobsResponse.filter(({ success }) => success).map(({ id }) => id),
timeRange: {
from: moment(resultTimeRange.start).format(TIME_FORMAT),
to: moment(resultTimeRange.end).format(TIME_FORMAT),
mode: 'absolute',
},
},
});
setResultsUrl(url);
const failedJobsCount = jobsResponse.reduce((count, { success }) => {
return success ? count : count + 1;
}, 0);

View file

@ -6,33 +6,40 @@
import { i18n } from '@kbn/i18n';
import { getToastNotifications, getSavedObjectsClient } from '../../../util/dependency_cache';
import { mlJobService } from '../../../services/job_service';
import { ml } from '../../../services/ml_api_service';
import { KibanaObjects } from './page';
import { NavigateToPath } from '../../../contexts/kibana';
import { CreateLinkWithUserDefaults } from '../../../components/custom_hooks/use_create_ad_links';
/**
* Checks whether the jobs in a data recognizer module have been created.
* Redirects to the Anomaly Explorer to view the jobs if they have been created,
* or the recognizer job wizard for the module if not.
*/
export function checkViewOrCreateJobs(moduleId: string, indexPatternId: string): Promise<any> {
export function checkViewOrCreateJobs(
moduleId: string,
indexPatternId: string,
createLinkWithUserDefaults: CreateLinkWithUserDefaults,
navigateToPath: NavigateToPath
): Promise<any> {
return new Promise((resolve, reject) => {
// Load the module, and check if the job(s) in the module have been created.
// If so, load the jobs in the Anomaly Explorer.
// Otherwise open the data recognizer wizard for the module.
// Always want to call reject() so as not to load original page.
ml.dataRecognizerModuleJobsExist({ moduleId })
.then((resp: any) => {
.then(async (resp: any) => {
if (resp.jobsExist === true) {
const resultsPageUrl = mlJobService.createResultsUrlForJobs(resp.jobs, 'explorer');
window.location.href = resultsPageUrl;
// also honor user's time filter setting in Advanced Settings
const url = createLinkWithUserDefaults('explorer', resp.jobs);
await navigateToPath(url);
reject();
} else {
window.location.href = `#/jobs/new_job/recognize?id=${moduleId}&index=${indexPatternId}`;
await navigateToPath(`/jobs/new_job/recognize?id=${moduleId}&index=${indexPatternId}`);
reject();
}
})
.catch((err: Error) => {
.catch(async (err: Error) => {
// eslint-disable-next-line no-console
console.error(`Error checking whether jobs in module ${moduleId} exists`, err);
const toastNotifications = getToastNotifications();
@ -46,8 +53,7 @@ export function checkViewOrCreateJobs(moduleId: string, indexPatternId: string):
'An error occurred trying to check whether the jobs in the module have been created.',
}),
});
window.location.href = '#/jobs';
await navigateToPath(`/jobs`);
reject();
});
});

View file

@ -31,6 +31,7 @@ import { getDocLinks } from '../../../../util/dependency_cache';
import { JobsListView } from '../../../../jobs/jobs_list/components/jobs_list_view/index';
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';
interface Tab {
'data-test-subj': string;
@ -75,8 +76,9 @@ function getTabs(isMlEnabledInSpace: boolean): Tab[] {
export const JobsListPage: FC<{
coreStart: CoreStart;
share: SharePluginStart;
history: ManagementAppMountParams['history'];
}> = ({ coreStart, history }) => {
}> = ({ coreStart, share, history }) => {
const [initialized, setInitialized] = useState(false);
const [accessDenied, setAccessDenied] = useState(false);
const [isMlEnabledInSpace, setIsMlEnabledInSpace] = useState(false);
@ -136,7 +138,7 @@ export const JobsListPage: FC<{
return (
<I18nContext>
<KibanaContextProvider services={{ ...coreStart }}>
<KibanaContextProvider services={{ ...coreStart, share }}>
<Router history={history}>
<EuiPageContent
id="kibanaManagementMLSection"

View file

@ -13,13 +13,15 @@ import { JobsListPage } from './components';
import { getJobsListBreadcrumbs } from '../breadcrumbs';
import { setDependencyCache, clearCache } from '../../util/dependency_cache';
import './_index.scss';
import { SharePluginStart } from '../../../../../../../src/plugins/share/public';
const renderApp = (
element: HTMLElement,
history: ManagementAppMountParams['history'],
coreStart: CoreStart
coreStart: CoreStart,
share: SharePluginStart
) => {
ReactDOM.render(React.createElement(JobsListPage, { coreStart, history }), element);
ReactDOM.render(React.createElement(JobsListPage, { coreStart, history, share }), element);
return () => {
unmountComponentAtNode(element);
clearCache();
@ -30,7 +32,7 @@ export async function mountApp(
core: CoreSetup<MlStartDependencies>,
params: ManagementAppMountParams
) {
const [coreStart] = await core.getStartServices();
const [coreStart, pluginsStart] = await core.getStartServices();
setDependencyCache({
docLinks: coreStart.docLinks!,
@ -41,5 +43,5 @@ export async function mountApp(
params.setBreadcrumbs(getJobsListBreadcrumbs());
return renderApp(params.element, params.history, coreStart);
return renderApp(params.element, params.history, coreStart, pluginsStart.share);
}

View file

@ -9,7 +9,7 @@ import { ml } from '../services/ml_api_service';
let mlNodeCount: number = 0;
let userHasPermissionToViewMlNodeCount: boolean = false;
export async function checkMlNodesAvailable() {
export async function checkMlNodesAvailable(redirectToJobsManagementPage: () => Promise<void>) {
try {
const nodes = await getMlNodeCount();
if (nodes.count !== undefined && nodes.count > 0) {
@ -20,7 +20,7 @@ export async function checkMlNodesAvailable() {
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
window.location.href = '#/jobs';
await redirectToJobsManagementPage();
Promise.reject();
}
}

View file

@ -4,30 +4,23 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useCallback, FC } from 'react';
import React, { FC, useMemo } from 'react';
import { EuiToolTip, EuiButtonEmpty } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useNavigateToPath } from '../../../contexts/kibana';
import { Link } from 'react-router-dom';
import { useMlLink } from '../../../contexts/kibana';
import { getAnalysisType } from '../../../data_frame_analytics/common/analytics';
import {
getResultsUrl,
DataFrameAnalyticsListRow,
} from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/common';
import { DataFrameAnalyticsListRow } from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/common';
import { getViewLinkStatus } from '../../../data_frame_analytics/pages/analytics_management/components/action_view/get_view_link_status';
import { ML_PAGES } from '../../../../../common/constants/ml_url_generator';
import { DataFrameAnalysisConfigType } from '../../../../../common/types/data_frame_analytics';
interface Props {
item: DataFrameAnalyticsListRow;
}
export const ViewLink: FC<Props> = ({ item }) => {
const navigateToPath = useNavigateToPath();
const clickHandler = useCallback(() => {
const analysisType = getAnalysisType(item.config.analysis);
navigateToPath(getResultsUrl(item.id, analysisType));
}, []);
const { disabled, tooltipContent } = getViewLinkStatus(item);
const viewJobResultsButtonText = i18n.translate(
@ -38,23 +31,34 @@ export const ViewLink: FC<Props> = ({ item }) => {
);
const tooltipText = disabled === false ? viewJobResultsButtonText : tooltipContent;
const analysisType = useMemo(() => getAnalysisType(item.config.analysis), [item]);
const viewAnalyticsResultsLink = useMlLink({
page: ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION,
pageState: {
jobId: item.id,
analysisType: analysisType as DataFrameAnalysisConfigType,
},
excludeBasePath: true,
});
return (
<EuiToolTip position="bottom" content={tooltipText}>
<EuiButtonEmpty
color="text"
size="xs"
onClick={clickHandler}
iconType="visTable"
aria-label={viewJobResultsButtonText}
className="results-button"
data-test-subj="mlOverviewAnalyticsJobViewButton"
isDisabled={disabled}
>
{i18n.translate('xpack.ml.overview.analytics.viewActionName', {
defaultMessage: 'View',
})}
</EuiButtonEmpty>
<Link to={viewAnalyticsResultsLink}>
<EuiButtonEmpty
color="text"
size="xs"
iconType="visTable"
aria-label={viewJobResultsButtonText}
className="results-button"
data-test-subj="mlOverviewAnalyticsJobViewButton"
isDisabled={disabled}
>
{i18n.translate('xpack.ml.overview.analytics.viewActionName', {
defaultMessage: 'View',
})}
</EuiButtonEmpty>
</Link>
</EuiToolTip>
);
};

View file

@ -23,6 +23,8 @@ import { AnalyticsTable } from './table';
import { getAnalyticsFactory } from '../../../data_frame_analytics/pages/analytics_management/services/analytics_service';
import { DataFrameAnalyticsListRow } from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/common';
import { AnalyticStatsBarStats, StatsBar } from '../../../components/stats_bar';
import { useMlUrlGenerator, useNavigateToPath } from '../../../contexts/kibana';
import { ML_PAGES } from '../../../../../common/constants/ml_url_generator';
interface Props {
jobCreationDisabled: boolean;
@ -35,6 +37,16 @@ export const AnalyticsPanel: FC<Props> = ({ jobCreationDisabled }) => {
const [errorMessage, setErrorMessage] = useState<any>(undefined);
const [isInitialized, setIsInitialized] = useState(false);
const mlUrlGenerator = useMlUrlGenerator();
const navigateToPath = useNavigateToPath();
const redirectToDataFrameAnalyticsManagementPage = async () => {
const path = await mlUrlGenerator.createUrl({
page: ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE,
});
await navigateToPath(path, true);
};
const getAnalytics = getAnalyticsFactory(
setAnalytics,
setAnalyticsStats,
@ -75,7 +87,6 @@ export const AnalyticsPanel: FC<Props> = ({ jobCreationDisabled }) => {
{isInitialized === false && (
<EuiLoadingSpinner className="mlOverviewPanel__spinner" size="xl" />
)}
    
{errorMessage === undefined && isInitialized === true && analytics.length === 0 && (
<EuiEmptyPrompt
iconType="createAdvancedJob"
@ -95,7 +106,7 @@ export const AnalyticsPanel: FC<Props> = ({ jobCreationDisabled }) => {
}
actions={
<EuiButton
href="#/data_frame_analytics?"
onClick={redirectToDataFrameAnalyticsManagementPage}
color="primary"
fill
iconType="plusInCircle"
@ -111,6 +122,7 @@ export const AnalyticsPanel: FC<Props> = ({ jobCreationDisabled }) => {
)}
{isInitialized === true && analytics.length > 0 && (
<>
<EuiSpacer />
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiText size="m">
@ -136,7 +148,7 @@ export const AnalyticsPanel: FC<Props> = ({ jobCreationDisabled }) => {
defaultMessage: 'Refresh',
})}
</EuiButtonEmpty>
<EuiButton size="s" fill href="#/data_frame_analytics?">
<EuiButton size="s" fill onClick={redirectToDataFrameAnalyticsManagementPage}>
{i18n.translate('xpack.ml.overview.analyticsList.manageJobsButtonText', {
defaultMessage: 'Manage jobs',
})}

View file

@ -7,6 +7,7 @@
import React, { FC } from 'react';
import { EuiToolTip, EuiButtonEmpty } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { Link } from 'react-router-dom';
import { MlSummaryJobs } from '../../../../../common/types/anomaly_detection_jobs';
import { useCreateADLinks } from '../../../components/custom_hooks/use_create_ad_links';
@ -26,19 +27,20 @@ export const ExplorerLink: FC<Props> = ({ jobsList }) => {
return (
<EuiToolTip position="bottom" content={openJobsInAnomalyExplorerText}>
<EuiButtonEmpty
color="text"
size="xs"
href={createLinkWithUserDefaults('explorer', jobsList)}
iconType="visTable"
aria-label={openJobsInAnomalyExplorerText}
className="results-button"
data-test-subj={`openOverviewJobsInAnomalyExplorer`}
>
{i18n.translate('xpack.ml.overview.anomalyDetection.viewActionName', {
defaultMessage: 'View',
})}
</EuiButtonEmpty>
<Link to={createLinkWithUserDefaults('explorer', jobsList)}>
<EuiButtonEmpty
color="text"
size="xs"
iconType="visTable"
aria-label={openJobsInAnomalyExplorerText}
className="results-button"
data-test-subj={`openOverviewJobsInAnomalyExplorer`}
>
{i18n.translate('xpack.ml.overview.anomalyDetection.viewActionName', {
defaultMessage: 'View',
})}
</EuiButtonEmpty>
</Link>
</EuiToolTip>
);
};

View file

@ -16,12 +16,13 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import moment from 'moment';
import { useMlKibana } from '../../../contexts/kibana';
import { useMlKibana, useMlUrlGenerator, useNavigateToPath } from '../../../contexts/kibana';
import { AnomalyDetectionTable } from './table';
import { ml } from '../../../services/ml_api_service';
import { getGroupsFromJobs, getStatsBarData, getJobsWithTimerange } from './utils';
import { Dictionary } from '../../../../../common/types/common';
import { MlSummaryJobs, MlSummaryJob } from '../../../../../common/types/anomaly_detection_jobs';
import { ML_PAGES } from '../../../../../common/constants/ml_url_generator';
export type GroupsDictionary = Dictionary<Group>;
@ -39,8 +40,6 @@ type MaxScoresByGroup = Dictionary<{
index?: number;
}>;
const createJobLink = '#/jobs/new_job/step/index_or_search';
function getDefaultAnomalyScores(groups: Group[]): MaxScoresByGroup {
const anomalyScores: MaxScoresByGroup = {};
groups.forEach((group) => {
@ -58,6 +57,23 @@ export const AnomalyDetectionPanel: FC<Props> = ({ jobCreationDisabled }) => {
const {
services: { notifications },
} = useMlKibana();
const mlUrlGenerator = useMlUrlGenerator();
const navigateToPath = useNavigateToPath();
const redirectToJobsManagementPage = async () => {
const path = await mlUrlGenerator.createUrl({
page: ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE,
});
await navigateToPath(path, true);
};
const redirectToCreateJobSelectIndexPage = async () => {
const path = await mlUrlGenerator.createUrl({
page: ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_INDEX,
});
await navigateToPath(path, true);
};
const [isLoading, setIsLoading] = useState(false);
const [groups, setGroups] = useState<GroupsDictionary>({});
const [groupsCount, setGroupsCount] = useState<number>(0);
@ -157,7 +173,7 @@ export const AnomalyDetectionPanel: FC<Props> = ({ jobCreationDisabled }) => {
return (
<EuiPanel className={panelClass}>
{typeof errorMessage !== 'undefined' && errorDisplay}
{isLoading && <EuiLoadingSpinner className="mlOverviewPanel__spinner" size="xl" />}   
{isLoading && <EuiLoadingSpinner className="mlOverviewPanel__spinner" size="xl" />}
{isLoading === false && typeof errorMessage === 'undefined' && groupsCount === 0 && (
<EuiEmptyPrompt
iconType="createSingleMetricJob"
@ -180,7 +196,7 @@ export const AnomalyDetectionPanel: FC<Props> = ({ jobCreationDisabled }) => {
actions={
<EuiButton
color="primary"
href={createJobLink}
onClick={redirectToCreateJobSelectIndexPage}
fill
iconType="plusInCircle"
isDisabled={jobCreationDisabled}
@ -203,7 +219,7 @@ export const AnomalyDetectionPanel: FC<Props> = ({ jobCreationDisabled }) => {
defaultMessage: 'Refresh',
})}
</EuiButtonEmpty>
<EuiButton size="s" fill href="#/jobs?">
<EuiButton size="s" fill onClick={redirectToJobsManagementPage}>
{i18n.translate('xpack.ml.overview.anomalyDetection.manageJobsButtonText', {
defaultMessage: 'Manage jobs',
})}

View file

@ -88,7 +88,7 @@ export const AnomalyDetectionTable: FC<Props> = ({ items, jobsList, statsBarData
<span>
{i18n.translate('xpack.ml.overview.anomalyDetection.tableMaxScore', {
defaultMessage: 'Max anomaly score',
})}{' '}
})}
<EuiIcon size="s" color="subdued" type="questionInCircle" className="eui-alignTop" />
</span>
</EuiToolTip>
@ -203,6 +203,7 @@ export const AnomalyDetectionTable: FC<Props> = ({ items, jobsList, statsBarData
return (
<Fragment>
<EuiSpacer />
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiText size="m">

View file

@ -54,6 +54,20 @@ export const CREATE_JOB_BREADCRUMB: ChromeBreadcrumb = Object.freeze({
href: '/jobs/new_job',
});
export const CALENDAR_MANAGEMENT_BREADCRUMB: ChromeBreadcrumb = Object.freeze({
text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagementLabel', {
defaultMessage: 'Calendar management',
}),
href: '/settings/calendars_list',
});
export const FILTER_LISTS_BREADCRUMB: ChromeBreadcrumb = Object.freeze({
text: i18n.translate('xpack.ml.settings.breadcrumbs.filterListsLabel', {
defaultMessage: 'Filter lists',
}),
href: '/settings/filter_lists',
});
const breadcrumbs = {
ML_BREADCRUMB,
SETTINGS_BREADCRUMB,
@ -61,6 +75,8 @@ const breadcrumbs = {
DATA_FRAME_ANALYTICS_BREADCRUMB,
DATA_VISUALIZER_BREADCRUMB,
CREATE_JOB_BREADCRUMB,
CALENDAR_MANAGEMENT_BREADCRUMB,
FILTER_LISTS_BREADCRUMB,
};
type Breadcrumb = keyof typeof breadcrumbs;
@ -76,10 +92,12 @@ export const breadcrumbOnClickFactory = (
export const getBreadcrumbWithUrlForApp = (
breadcrumbName: Breadcrumb,
navigateToPath: NavigateToPath
navigateToPath: NavigateToPath,
basePath: string
): EuiBreadcrumb => {
return {
...breadcrumbs[breadcrumbName],
text: breadcrumbs[breadcrumbName].text,
href: `${basePath}/app/ml${breadcrumbs[breadcrumbName].href}`,
onClick: breadcrumbOnClickFactory(breadcrumbs[breadcrumbName].href, navigateToPath),
};
};

View file

@ -21,13 +21,17 @@ export interface ResolverResults {
interface BasicResolverDependencies {
indexPatterns: IndexPatternsContract;
redirectToMlAccessDeniedPage: () => Promise<void>;
}
export const basicResolvers = ({ indexPatterns }: BasicResolverDependencies): Resolvers => ({
export const basicResolvers = ({
indexPatterns,
redirectToMlAccessDeniedPage,
}: BasicResolverDependencies): Resolvers => ({
checkFullLicense,
getMlNodeCount,
loadMlServerInfo,
loadIndexPatterns: () => loadIndexPatterns(indexPatterns),
checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver,
checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage),
loadSavedSearches,
});

View file

@ -12,7 +12,7 @@ import { AppMountParameters, IUiSettingsClient, ChromeStart } from 'kibana/publi
import { ChromeBreadcrumb } from 'kibana/public';
import { IndexPatternsContract } from 'src/plugins/data/public';
import { useNavigateToPath } from '../contexts/kibana';
import { useMlKibana, useNavigateToPath } from '../contexts/kibana';
import { MlContext, MlContextValue } from '../contexts/ml';
import { UrlStateProvider } from '../util/url_state';
@ -39,6 +39,7 @@ interface PageDependencies {
history: AppMountParameters['history'];
indexPatterns: IndexPatternsContract;
setBreadcrumbs: ChromeStart['setBreadcrumbs'];
redirectToMlAccessDeniedPage: () => Promise<void>;
}
export const PageLoader: FC<{ context: MlContextValue }> = ({ context, children }) => {
@ -75,10 +76,16 @@ const MlRoutes: FC<{
pageDeps: PageDependencies;
}> = ({ pageDeps }) => {
const navigateToPath = useNavigateToPath();
const {
services: {
http: { basePath },
},
} = useMlKibana();
return (
<>
{Object.entries(routes).map(([name, routeFactory]) => {
const route = routeFactory(navigateToPath);
const route = routeFactory(navigateToPath, basePath.get());
return (
<Route

View file

@ -18,11 +18,14 @@ import { Page } from '../../../data_frame_analytics/pages/analytics_creation';
import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../../breadcrumbs';
import { loadNewJobCapabilities } from '../../../services/new_job_capabilities_service';
export const analyticsJobsCreationRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
export const analyticsJobsCreationRouteFactory = (
navigateToPath: NavigateToPath,
basePath: string
): MlRoute => ({
path: '/data_frame_analytics/new_job',
render: (props, deps) => <PageWrapper {...props} deps={deps} />,
breadcrumbs: [
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath),
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameManagementLabel', {
defaultMessage: 'Data Frame Analytics',

View file

@ -10,21 +10,25 @@ import { decode } from 'rison-node';
import { i18n } from '@kbn/i18n';
import { NavigateToPath } from '../../../contexts/kibana';
import { NavigateToPath, useMlKibana, useMlUrlGenerator } from '../../../contexts/kibana';
import { MlRoute, PageLoader, PageProps } from '../../router';
import { useResolver } from '../../use_resolver';
import { basicResolvers } from '../../resolvers';
import { Page } from '../../../data_frame_analytics/pages/analytics_exploration';
import { ANALYSIS_CONFIG_TYPE } from '../../../data_frame_analytics/common/analytics';
import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs';
import { ML_PAGES } from '../../../../../common/constants/ml_url_generator';
import { DataFrameAnalysisConfigType } from '../../../../../common/types/data_frame_analytics';
export const analyticsJobExplorationRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
export const analyticsJobExplorationRouteFactory = (
navigateToPath: NavigateToPath,
basePath: string
): MlRoute => ({
path: '/data_frame_analytics/exploration',
render: (props, deps) => <PageWrapper {...props} deps={deps} />,
breadcrumbs: [
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath),
getBreadcrumbWithUrlForApp('DATA_FRAME_ANALYTICS_BREADCRUMB', navigateToPath),
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
getBreadcrumbWithUrlForApp('DATA_FRAME_ANALYTICS_BREADCRUMB', navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameExplorationLabel', {
defaultMessage: 'Exploration',
@ -38,16 +42,31 @@ const PageWrapper: FC<PageProps> = ({ location, deps }) => {
const { context } = useResolver('', undefined, deps.config, basicResolvers(deps));
const { _g }: Record<string, any> = parse(location.search, { sort: false });
const urlGenerator = useMlUrlGenerator();
const {
services: {
application: { navigateToUrl },
},
} = useMlKibana();
const redirectToAnalyticsManagementPage = async () => {
const url = await urlGenerator.createUrl({ page: ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE });
await navigateToUrl(url);
};
let globalState: any = null;
try {
globalState = decode(_g);
} catch (error) {
// eslint-disable-next-line no-console
console.error('Could not parse global state');
window.location.href = '#data_frame_analytics';
console.error(
'Could not parse global state. Redirecting to Data Frame Analytics Management Page.'
);
redirectToAnalyticsManagementPage();
return <></>;
}
const jobId: string = globalState.ml.jobId;
const analysisType: ANALYSIS_CONFIG_TYPE = globalState.ml.analysisType;
const analysisType: DataFrameAnalysisConfigType = globalState.ml.analysisType;
return (
<PageLoader context={context}>

View file

@ -15,12 +15,15 @@ import { basicResolvers } from '../../resolvers';
import { Page } from '../../../data_frame_analytics/pages/analytics_management';
import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs';
export const analyticsJobsListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
export const analyticsJobsListRouteFactory = (
navigateToPath: NavigateToPath,
basePath: string
): MlRoute => ({
path: '/data_frame_analytics',
render: (props, deps) => <PageWrapper {...props} deps={deps} />,
breadcrumbs: [
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath),
getBreadcrumbWithUrlForApp('DATA_FRAME_ANALYTICS_BREADCRUMB', navigateToPath),
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
getBreadcrumbWithUrlForApp('DATA_FRAME_ANALYTICS_BREADCRUMB', navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameListLabel', {
defaultMessage: 'Job Management',

View file

@ -15,12 +15,15 @@ import { basicResolvers } from '../../resolvers';
import { Page } from '../../../data_frame_analytics/pages/analytics_management';
import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs';
export const modelsListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
export const modelsListRouteFactory = (
navigateToPath: NavigateToPath,
basePath: string
): MlRoute => ({
path: '/data_frame_analytics/models',
render: (props, deps) => <PageWrapper {...props} deps={deps} />,
breadcrumbs: [
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath),
getBreadcrumbWithUrlForApp('DATA_FRAME_ANALYTICS_BREADCRUMB', navigateToPath),
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
getBreadcrumbWithUrlForApp('DATA_FRAME_ANALYTICS_BREADCRUMB', navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.modelsListLabel', {
defaultMessage: 'Model Management',

View file

@ -21,19 +21,25 @@ import { checkBasicLicense } from '../../../license';
import { checkFindFileStructurePrivilegeResolver } from '../../../capabilities/check_capabilities';
import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs';
export const selectorRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
export const selectorRouteFactory = (
navigateToPath: NavigateToPath,
basePath: string
): MlRoute => ({
path: '/datavisualizer',
render: (props, deps) => <PageWrapper {...props} deps={deps} />,
breadcrumbs: [
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath),
getBreadcrumbWithUrlForApp('DATA_VISUALIZER_BREADCRUMB', navigateToPath),
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
getBreadcrumbWithUrlForApp('DATA_VISUALIZER_BREADCRUMB', navigateToPath, basePath),
],
});
const PageWrapper: FC<PageProps> = ({ location, deps }) => {
const { redirectToMlAccessDeniedPage } = deps;
const { context } = useResolver(undefined, undefined, deps.config, {
checkBasicLicense,
checkFindFileStructurePrivilege: checkFindFileStructurePrivilegeResolver,
checkFindFileStructurePrivilege: () =>
checkFindFileStructurePrivilegeResolver(redirectToMlAccessDeniedPage),
});
return (
<PageLoader context={context}>

View file

@ -24,12 +24,15 @@ import { loadIndexPatterns } from '../../../util/index_utils';
import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs';
export const fileBasedRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
export const fileBasedRouteFactory = (
navigateToPath: NavigateToPath,
basePath: string
): MlRoute => ({
path: '/filedatavisualizer',
render: (props, deps) => <PageWrapper {...props} deps={deps} />,
breadcrumbs: [
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath),
getBreadcrumbWithUrlForApp('DATA_VISUALIZER_BREADCRUMB', navigateToPath),
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
getBreadcrumbWithUrlForApp('DATA_VISUALIZER_BREADCRUMB', navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.dataVisualizer.fileBasedLabel', {
defaultMessage: 'File',
@ -40,10 +43,13 @@ export const fileBasedRouteFactory = (navigateToPath: NavigateToPath): MlRoute =
});
const PageWrapper: FC<PageProps> = ({ location, deps }) => {
const { redirectToMlAccessDeniedPage } = deps;
const { context } = useResolver('', undefined, deps.config, {
checkBasicLicense,
loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns),
checkFindFileStructurePrivilege: checkFindFileStructurePrivilegeResolver,
checkFindFileStructurePrivilege: () =>
checkFindFileStructurePrivilegeResolver(redirectToMlAccessDeniedPage),
});
return (
<PageLoader context={context}>

View file

@ -20,13 +20,18 @@ import { checkGetJobsCapabilitiesResolver } from '../../../capabilities/check_ca
import { loadIndexPatterns } from '../../../util/index_utils';
import { checkMlNodesAvailable } from '../../../ml_nodes_check';
import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs';
import { ML_PAGES } from '../../../../../common/constants/ml_url_generator';
import { useCreateAndNavigateToMlLink } from '../../../contexts/kibana/use_create_url';
export const indexBasedRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
export const indexBasedRouteFactory = (
navigateToPath: NavigateToPath,
basePath: string
): MlRoute => ({
path: '/jobs/new_job/datavisualizer',
render: (props, deps) => <PageWrapper {...props} deps={deps} />,
breadcrumbs: [
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath),
getBreadcrumbWithUrlForApp('DATA_VISUALIZER_BREADCRUMB', navigateToPath),
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
getBreadcrumbWithUrlForApp('DATA_VISUALIZER_BREADCRUMB', navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.indexLabel', {
defaultMessage: 'Index',
@ -37,12 +42,17 @@ export const indexBasedRouteFactory = (navigateToPath: NavigateToPath): MlRoute
});
const PageWrapper: FC<PageProps> = ({ location, deps }) => {
const { redirectToMlAccessDeniedPage } = deps;
const redirectToJobsManagementPage = useCreateAndNavigateToMlLink(
ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE
);
const { index, savedSearchId }: Record<string, any> = parse(location.search, { sort: false });
const { context } = useResolver(index, savedSearchId, deps.config, {
checkBasicLicense,
loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns),
checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver,
checkMlNodesAvailable,
checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage),
checkMlNodesAvailable: () => checkMlNodesAvailable(redirectToJobsManagementPage),
});
return (

View file

@ -35,12 +35,15 @@ import { useTimefilter } from '../../contexts/kibana';
import { isViewBySwimLaneData } from '../../explorer/swimlane_container';
import { JOB_ID } from '../../../../common/constants/anomalies';
export const explorerRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
export const explorerRouteFactory = (
navigateToPath: NavigateToPath,
basePath: string
): MlRoute => ({
path: '/explorer',
render: (props, deps) => <PageWrapper {...props} deps={deps} />,
breadcrumbs: [
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath),
getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath),
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.anomalyDetection.anomalyExplorerLabel', {
defaultMessage: 'Anomaly Explorer',

View file

@ -20,12 +20,12 @@ import { JobsPage } from '../../jobs/jobs_list';
import { useTimefilter } from '../../contexts/kibana';
import { getBreadcrumbWithUrlForApp } from '../breadcrumbs';
export const jobListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
export const jobListRouteFactory = (navigateToPath: NavigateToPath, basePath: string): MlRoute => ({
path: '/jobs',
render: (props, deps) => <PageWrapper {...props} deps={deps} />,
breadcrumbs: [
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath),
getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath),
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.anomalyDetection.jobManagementLabel', {
defaultMessage: 'Job Management',

View file

@ -8,7 +8,7 @@ import React, { FC } from 'react';
import { i18n } from '@kbn/i18n';
import { NavigateToPath } from '../../../contexts/kibana';
import { NavigateToPath, useMlKibana } from '../../../contexts/kibana';
import { MlRoute, PageLoader, PageProps } from '../../router';
import { useResolver } from '../../use_resolver';
@ -19,6 +19,8 @@ import { checkBasicLicense } from '../../../license';
import { loadIndexPatterns } from '../../../util/index_utils';
import { checkGetJobsCapabilitiesResolver } from '../../../capabilities/check_capabilities';
import { checkMlNodesAvailable } from '../../../ml_nodes_check';
import { ML_PAGES } from '../../../../../common/constants/ml_url_generator';
import { useCreateAndNavigateToMlLink } from '../../../contexts/kibana/use_create_url';
enum MODE {
NEW_JOB,
@ -30,9 +32,9 @@ interface IndexOrSearchPageProps extends PageProps {
mode: MODE;
}
const getBreadcrumbs = (navigateToPath: NavigateToPath) => [
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath),
getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath),
const getBreadcrumbs = (navigateToPath: NavigateToPath, basePath: string) => [
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectIndexOrSearchLabel', {
defaultMessage: 'Create job',
@ -41,7 +43,10 @@ const getBreadcrumbs = (navigateToPath: NavigateToPath) => [
},
];
export const indexOrSearchRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
export const indexOrSearchRouteFactory = (
navigateToPath: NavigateToPath,
basePath: string
): MlRoute => ({
path: '/jobs/new_job/step/index_or_search',
render: (props, deps) => (
<PageWrapper
@ -51,10 +56,13 @@ export const indexOrSearchRouteFactory = (navigateToPath: NavigateToPath): MlRou
mode={MODE.NEW_JOB}
/>
),
breadcrumbs: getBreadcrumbs(navigateToPath),
breadcrumbs: getBreadcrumbs(navigateToPath, basePath),
});
export const dataVizIndexOrSearchRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
export const dataVizIndexOrSearchRouteFactory = (
navigateToPath: NavigateToPath,
basePath: string
): MlRoute => ({
path: '/datavisualizer_index_select',
render: (props, deps) => (
<PageWrapper
@ -64,19 +72,31 @@ export const dataVizIndexOrSearchRouteFactory = (navigateToPath: NavigateToPath)
mode={MODE.DATAVISUALIZER}
/>
),
breadcrumbs: getBreadcrumbs(navigateToPath),
breadcrumbs: getBreadcrumbs(navigateToPath, basePath),
});
const PageWrapper: FC<IndexOrSearchPageProps> = ({ nextStepPath, deps, mode }) => {
const {
services: {
http: { basePath },
application: { navigateToUrl },
},
} = useMlKibana();
const { redirectToMlAccessDeniedPage } = deps;
const redirectToJobsManagementPage = useCreateAndNavigateToMlLink(
ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE
);
const newJobResolvers = {
...basicResolvers(deps),
preConfiguredJobRedirect: () => preConfiguredJobRedirect(deps.indexPatterns),
preConfiguredJobRedirect: () =>
preConfiguredJobRedirect(deps.indexPatterns, basePath.get(), navigateToUrl),
};
const dataVizResolvers = {
checkBasicLicense,
loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns),
checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver,
checkMlNodesAvailable,
checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage),
checkMlNodesAvailable: () => checkMlNodesAvailable(redirectToJobsManagementPage),
};
const { context } = useResolver(

View file

@ -17,12 +17,12 @@ import { basicResolvers } from '../../resolvers';
import { Page } from '../../../jobs/new_job/pages/job_type';
import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs';
export const jobTypeRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
export const jobTypeRouteFactory = (navigateToPath: NavigateToPath, basePath: string): MlRoute => ({
path: '/jobs/new_job/step/job_type',
render: (props, deps) => <PageWrapper {...props} deps={deps} />,
breadcrumbs: [
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath),
getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath),
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectJobType', {
defaultMessage: 'Create job',

View file

@ -9,7 +9,7 @@ import React, { FC } from 'react';
import { i18n } from '@kbn/i18n';
import { NavigateToPath } from '../../../contexts/kibana';
import { NavigateToPath, useNavigateToPath } from '../../../contexts/kibana';
import { MlRoute, PageLoader, PageProps } from '../../router';
import { useResolver } from '../../use_resolver';
@ -18,14 +18,18 @@ import { Page } from '../../../jobs/new_job/recognize';
import { checkViewOrCreateJobs } from '../../../jobs/new_job/recognize/resolvers';
import { mlJobService } from '../../../services/job_service';
import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs';
import { useCreateADLinks } from '../../../components/custom_hooks/use_create_ad_links';
export const recognizeRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
export const recognizeRouteFactory = (
navigateToPath: NavigateToPath,
basePath: string
): MlRoute => ({
path: '/jobs/new_job/recognize',
render: (props, deps) => <PageWrapper {...props} deps={deps} />,
breadcrumbs: [
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath),
getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath),
getBreadcrumbWithUrlForApp('CREATE_JOB_BREADCRUMB', navigateToPath),
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath, basePath),
getBreadcrumbWithUrlForApp('CREATE_JOB_BREADCRUMB', navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectIndexOrSearchLabelRecognize', {
defaultMessage: 'Recognized index',
@ -60,10 +64,14 @@ const CheckViewOrCreateWrapper: FC<PageProps> = ({ location, deps }) => {
const { id: moduleId, index: indexPatternId }: Record<string, any> = parse(location.search, {
sort: false,
});
const { createLinkWithUserDefaults } = useCreateADLinks();
const navigateToPath = useNavigateToPath();
// the single resolver checkViewOrCreateJobs redirects only. so will always reject
useResolver(undefined, undefined, deps.config, {
checkViewOrCreateJobs: () => checkViewOrCreateJobs(moduleId, indexPatternId),
checkViewOrCreateJobs: () =>
checkViewOrCreateJobs(moduleId, indexPatternId, createLinkWithUserDefaults, navigateToPath),
});
return null;
};

View file

@ -19,19 +19,21 @@ import { mlJobService } from '../../../services/job_service';
import { loadNewJobCapabilities } from '../../../services/new_job_capabilities_service';
import { checkCreateJobsCapabilitiesResolver } from '../../../capabilities/check_capabilities';
import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs';
import { useCreateAndNavigateToMlLink } from '../../../contexts/kibana/use_create_url';
import { ML_PAGES } from '../../../../../common/constants/ml_url_generator';
interface WizardPageProps extends PageProps {
jobType: JOB_TYPE;
}
const getBaseBreadcrumbs = (navigateToPath: NavigateToPath) => [
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath),
getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath),
getBreadcrumbWithUrlForApp('CREATE_JOB_BREADCRUMB', navigateToPath),
const getBaseBreadcrumbs = (navigateToPath: NavigateToPath, basePath: string) => [
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath, basePath),
getBreadcrumbWithUrlForApp('CREATE_JOB_BREADCRUMB', navigateToPath, basePath),
];
const getSingleMetricBreadcrumbs = (navigateToPath: NavigateToPath) => [
...getBaseBreadcrumbs(navigateToPath),
const getSingleMetricBreadcrumbs = (navigateToPath: NavigateToPath, basePath: string) => [
...getBaseBreadcrumbs(navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.jobsBreadcrumbs.singleMetricLabel', {
defaultMessage: 'Single metric',
@ -40,8 +42,8 @@ const getSingleMetricBreadcrumbs = (navigateToPath: NavigateToPath) => [
},
];
const getMultiMetricBreadcrumbs = (navigateToPath: NavigateToPath) => [
...getBaseBreadcrumbs(navigateToPath),
const getMultiMetricBreadcrumbs = (navigateToPath: NavigateToPath, basePath: string) => [
...getBaseBreadcrumbs(navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.jobsBreadcrumbs.multiMetricLabel', {
defaultMessage: 'Multi-metric',
@ -50,8 +52,8 @@ const getMultiMetricBreadcrumbs = (navigateToPath: NavigateToPath) => [
},
];
const getPopulationBreadcrumbs = (navigateToPath: NavigateToPath) => [
...getBaseBreadcrumbs(navigateToPath),
const getPopulationBreadcrumbs = (navigateToPath: NavigateToPath, basePath: string) => [
...getBaseBreadcrumbs(navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.jobsBreadcrumbs.populationLabel', {
defaultMessage: 'Population',
@ -60,8 +62,8 @@ const getPopulationBreadcrumbs = (navigateToPath: NavigateToPath) => [
},
];
const getAdvancedBreadcrumbs = (navigateToPath: NavigateToPath) => [
...getBaseBreadcrumbs(navigateToPath),
const getAdvancedBreadcrumbs = (navigateToPath: NavigateToPath, basePath: string) => [
...getBaseBreadcrumbs(navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.jobsBreadcrumbs.advancedConfigurationLabel', {
defaultMessage: 'Advanced configuration',
@ -70,8 +72,8 @@ const getAdvancedBreadcrumbs = (navigateToPath: NavigateToPath) => [
},
];
const getCategorizationBreadcrumbs = (navigateToPath: NavigateToPath) => [
...getBaseBreadcrumbs(navigateToPath),
const getCategorizationBreadcrumbs = (navigateToPath: NavigateToPath, basePath: string) => [
...getBaseBreadcrumbs(navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.jobsBreadcrumbs.categorizationLabel', {
defaultMessage: 'Categorization',
@ -80,41 +82,60 @@ const getCategorizationBreadcrumbs = (navigateToPath: NavigateToPath) => [
},
];
export const singleMetricRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
export const singleMetricRouteFactory = (
navigateToPath: NavigateToPath,
basePath: string
): MlRoute => ({
path: '/jobs/new_job/single_metric',
render: (props, deps) => <PageWrapper {...props} jobType={JOB_TYPE.SINGLE_METRIC} deps={deps} />,
breadcrumbs: getSingleMetricBreadcrumbs(navigateToPath),
breadcrumbs: getSingleMetricBreadcrumbs(navigateToPath, basePath),
});
export const multiMetricRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
export const multiMetricRouteFactory = (
navigateToPath: NavigateToPath,
basePath: string
): MlRoute => ({
path: '/jobs/new_job/multi_metric',
render: (props, deps) => <PageWrapper {...props} jobType={JOB_TYPE.MULTI_METRIC} deps={deps} />,
breadcrumbs: getMultiMetricBreadcrumbs(navigateToPath),
breadcrumbs: getMultiMetricBreadcrumbs(navigateToPath, basePath),
});
export const populationRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
export const populationRouteFactory = (
navigateToPath: NavigateToPath,
basePath: string
): MlRoute => ({
path: '/jobs/new_job/population',
render: (props, deps) => <PageWrapper {...props} jobType={JOB_TYPE.POPULATION} deps={deps} />,
breadcrumbs: getPopulationBreadcrumbs(navigateToPath),
breadcrumbs: getPopulationBreadcrumbs(navigateToPath, basePath),
});
export const advancedRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
export const advancedRouteFactory = (
navigateToPath: NavigateToPath,
basePath: string
): MlRoute => ({
path: '/jobs/new_job/advanced',
render: (props, deps) => <PageWrapper {...props} jobType={JOB_TYPE.ADVANCED} deps={deps} />,
breadcrumbs: getAdvancedBreadcrumbs(navigateToPath),
breadcrumbs: getAdvancedBreadcrumbs(navigateToPath, basePath),
});
export const categorizationRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
export const categorizationRouteFactory = (
navigateToPath: NavigateToPath,
basePath: string
): MlRoute => ({
path: '/jobs/new_job/categorization',
render: (props, deps) => <PageWrapper {...props} jobType={JOB_TYPE.CATEGORIZATION} deps={deps} />,
breadcrumbs: getCategorizationBreadcrumbs(navigateToPath),
breadcrumbs: getCategorizationBreadcrumbs(navigateToPath, basePath),
});
const PageWrapper: FC<WizardPageProps> = ({ location, jobType, deps }) => {
const redirectToJobsManagementPage = useCreateAndNavigateToMlLink(
ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE
);
const { index, savedSearchId }: Record<string, any> = parse(location.search, { sort: false });
const { context, results } = useResolver(index, savedSearchId, deps.config, {
...basicResolvers(deps),
privileges: checkCreateJobsCapabilitiesResolver,
privileges: () => checkCreateJobsCapabilitiesResolver(redirectToJobsManagementPage),
jobCaps: () => loadNewJobCapabilities(index, savedSearchId, deps.indexPatterns),
existingJobsAndGroups: mlJobService.getJobAndGroupIds,
});

View file

@ -22,11 +22,14 @@ import { loadMlServerInfo } from '../../services/ml_server_info';
import { useTimefilter } from '../../contexts/kibana';
import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../breadcrumbs';
export const overviewRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
export const overviewRouteFactory = (
navigateToPath: NavigateToPath,
basePath: string
): MlRoute => ({
path: '/overview',
render: (props, deps) => <PageWrapper {...props} deps={deps} />,
breadcrumbs: [
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath),
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.overview.overviewLabel', {
defaultMessage: 'Overview',
@ -37,9 +40,11 @@ export const overviewRouteFactory = (navigateToPath: NavigateToPath): MlRoute =>
});
const PageWrapper: FC<PageProps> = ({ deps }) => {
const { redirectToMlAccessDeniedPage } = deps;
const { context } = useResolver(undefined, undefined, deps.config, {
checkFullLicense,
checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver,
checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage),
getMlNodeCount,
loadMlServerInfo,
});
@ -52,7 +57,7 @@ const PageWrapper: FC<PageProps> = ({ deps }) => {
);
};
export const appRootRouteFactory = (): MlRoute => ({
export const appRootRouteFactory = (navigateToPath: NavigateToPath, basePath: string): MlRoute => ({
path: '/',
render: () => <Page />,
breadcrumbs: [],

View file

@ -10,7 +10,6 @@
*/
import React, { FC } from 'react';
import { i18n } from '@kbn/i18n';
import { NavigateToPath } from '../../../contexts/kibana';
@ -25,27 +24,27 @@ import {
} from '../../../capabilities/check_capabilities';
import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes';
import { CalendarsList } from '../../../settings/calendars';
import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../../breadcrumbs';
import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs';
export const calendarListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
export const calendarListRouteFactory = (
navigateToPath: NavigateToPath,
basePath: string
): MlRoute => ({
path: '/settings/calendars_list',
render: (props, deps) => <PageWrapper {...props} deps={deps} />,
breadcrumbs: [
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath),
getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath),
{
text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagementLabel', {
defaultMessage: 'Calendar management',
}),
onClick: breadcrumbOnClickFactory('/settings/calendars_list', navigateToPath),
},
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath, basePath),
getBreadcrumbWithUrlForApp('CALENDAR_MANAGEMENT_BREADCRUMB', navigateToPath, basePath),
],
});
const PageWrapper: FC<PageProps> = ({ deps }) => {
const { redirectToMlAccessDeniedPage } = deps;
const { context } = useResolver(undefined, undefined, deps.config, {
checkFullLicense,
checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver,
checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage),
getMlNodeCount,
});

View file

@ -26,6 +26,8 @@ import {
import { checkMlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes';
import { NewCalendar } from '../../../settings/calendars';
import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../../breadcrumbs';
import { useCreateAndNavigateToMlLink } from '../../../contexts/kibana/use_create_url';
import { ML_PAGES } from '../../../../../common/constants/ml_url_generator';
enum MODE {
NEW,
@ -36,12 +38,16 @@ interface NewCalendarPageProps extends PageProps {
mode: MODE;
}
export const newCalendarRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
export const newCalendarRouteFactory = (
navigateToPath: NavigateToPath,
basePath: string
): MlRoute => ({
path: '/settings/calendars_list/new_calendar',
render: (props, deps) => <PageWrapper {...props} deps={deps} mode={MODE.NEW} />,
breadcrumbs: [
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath),
getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath),
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath, basePath),
getBreadcrumbWithUrlForApp('CALENDAR_MANAGEMENT_BREADCRUMB', navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagement.createLabel', {
defaultMessage: 'Create',
@ -51,12 +57,16 @@ export const newCalendarRouteFactory = (navigateToPath: NavigateToPath): MlRoute
],
});
export const editCalendarRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
export const editCalendarRouteFactory = (
navigateToPath: NavigateToPath,
basePath: string
): MlRoute => ({
path: '/settings/calendars_list/edit_calendar/:calendarId',
render: (props, deps) => <PageWrapper {...props} deps={deps} mode={MODE.EDIT} />,
breadcrumbs: [
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath),
getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath),
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath, basePath),
getBreadcrumbWithUrlForApp('CALENDAR_MANAGEMENT_BREADCRUMB', navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagement.editLabel', {
defaultMessage: 'Edit',
@ -72,11 +82,15 @@ const PageWrapper: FC<NewCalendarPageProps> = ({ location, mode, deps }) => {
const pathMatch: string[] | null = location.pathname.match(/.+\/(.+)$/);
calendarId = pathMatch && pathMatch.length > 1 ? pathMatch[1] : undefined;
}
const { redirectToMlAccessDeniedPage } = deps;
const redirectToJobsManagementPage = useCreateAndNavigateToMlLink(
ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE
);
const { context } = useResolver(undefined, undefined, deps.config, {
checkFullLicense,
checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver,
checkMlNodesAvailable,
checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage),
checkMlNodesAvailable: () => checkMlNodesAvailable(redirectToJobsManagementPage),
});
useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false });

View file

@ -10,7 +10,6 @@
*/
import React, { FC } from 'react';
import { i18n } from '@kbn/i18n';
import { NavigateToPath } from '../../../contexts/kibana';
@ -26,27 +25,27 @@ import {
import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes';
import { FilterLists } from '../../../settings/filter_lists';
import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../../breadcrumbs';
import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs';
export const filterListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
export const filterListRouteFactory = (
navigateToPath: NavigateToPath,
basePath: string
): MlRoute => ({
path: '/settings/filter_lists',
render: (props, deps) => <PageWrapper {...props} deps={deps} />,
breadcrumbs: [
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath),
getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath),
{
text: i18n.translate('xpack.ml.settings.breadcrumbs.filterListsLabel', {
defaultMessage: 'Filter lists',
}),
onClick: breadcrumbOnClickFactory('/settings/filter_lists', navigateToPath),
},
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath, basePath),
getBreadcrumbWithUrlForApp('FILTER_LISTS_BREADCRUMB', navigateToPath, basePath),
],
});
const PageWrapper: FC<PageProps> = ({ deps }) => {
const { redirectToMlAccessDeniedPage } = deps;
const { context } = useResolver(undefined, undefined, deps.config, {
checkFullLicense,
checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver,
checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage),
getMlNodeCount,
});

View file

@ -27,6 +27,8 @@ import { checkMlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes';
import { EditFilterList } from '../../../settings/filter_lists';
import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../../breadcrumbs';
import { useCreateAndNavigateToMlLink } from '../../../contexts/kibana/use_create_url';
import { ML_PAGES } from '../../../../../common/constants/ml_url_generator';
enum MODE {
NEW,
@ -37,12 +39,17 @@ interface NewFilterPageProps extends PageProps {
mode: MODE;
}
export const newFilterListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
export const newFilterListRouteFactory = (
navigateToPath: NavigateToPath,
basePath: string
): MlRoute => ({
path: '/settings/filter_lists/new_filter_list',
render: (props, deps) => <PageWrapper {...props} mode={MODE.NEW} deps={deps} />,
breadcrumbs: [
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath),
getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath),
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath, basePath),
getBreadcrumbWithUrlForApp('FILTER_LISTS_BREADCRUMB', navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.settings.breadcrumbs.filterLists.createLabel', {
defaultMessage: 'Create',
@ -52,12 +59,16 @@ export const newFilterListRouteFactory = (navigateToPath: NavigateToPath): MlRou
],
});
export const editFilterListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
export const editFilterListRouteFactory = (
navigateToPath: NavigateToPath,
basePath: string
): MlRoute => ({
path: '/settings/filter_lists/edit_filter_list/:filterId',
render: (props, deps) => <PageWrapper {...props} mode={MODE.EDIT} deps={deps} />,
breadcrumbs: [
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath),
getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath),
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath, basePath),
getBreadcrumbWithUrlForApp('FILTER_LISTS_BREADCRUMB', navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.settings.breadcrumbs.filterLists.editLabel', {
defaultMessage: 'Edit',
@ -73,11 +84,15 @@ const PageWrapper: FC<NewFilterPageProps> = ({ location, mode, deps }) => {
const pathMatch: string[] | null = location.pathname.match(/.+\/(.+)$/);
filterId = pathMatch && pathMatch.length > 1 ? pathMatch[1] : undefined;
}
const { redirectToMlAccessDeniedPage } = deps;
const redirectToJobsManagementPage = useCreateAndNavigateToMlLink(
ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE
);
const { context } = useResolver(undefined, undefined, deps.config, {
checkFullLicense,
checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver,
checkMlNodesAvailable,
checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage),
checkMlNodesAvailable: () => checkMlNodesAvailable(redirectToJobsManagementPage),
});
useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false });

View file

@ -26,19 +26,24 @@ import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes';
import { AnomalyDetectionSettingsContext, Settings } from '../../../settings';
import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs';
export const settingsRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
export const settingsRouteFactory = (
navigateToPath: NavigateToPath,
basePath: string
): MlRoute => ({
path: '/settings',
render: (props, deps) => <PageWrapper {...props} deps={deps} />,
breadcrumbs: [
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath),
getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath),
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath, basePath),
],
});
const PageWrapper: FC<PageProps> = ({ deps }) => {
const { redirectToMlAccessDeniedPage } = deps;
const { context } = useResolver(undefined, undefined, deps.config, {
checkFullLicense,
checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver,
checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage),
getMlNodeCount,
});

View file

@ -19,6 +19,11 @@ jest.mock('../../contexts/kibana/kibana_context', () => {
useMlKibana: () => {
return {
services: {
chrome: { docTitle: { change: jest.fn() } },
application: { getUrlForApp: jest.fn(), navigateToUrl: jest.fn() },
share: {
urlGenerators: { getUrlGenerator: jest.fn() },
},
uiSettings: { get: jest.fn() },
data: {
query: {

View file

@ -39,12 +39,15 @@ import { basicResolvers } from '../resolvers';
import { getBreadcrumbWithUrlForApp } from '../breadcrumbs';
import { useTimefilter } from '../../contexts/kibana';
export const timeSeriesExplorerRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
export const timeSeriesExplorerRouteFactory = (
navigateToPath: NavigateToPath,
basePath: string
): MlRoute => ({
path: '/timeseriesexplorer',
render: (props, deps) => <PageWrapper {...props} deps={deps} />,
breadcrumbs: [
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath),
getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath),
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.anomalyDetection.singleMetricViewerLabel', {
defaultMessage: 'Single Metric Viewer',

View file

@ -16,6 +16,8 @@ import { createSearchItems } from '../jobs/new_job/utils/new_job_utils';
import { ResolverResults, Resolvers } from './resolvers';
import { MlContextValue } from '../contexts/ml';
import { useNotifications } from '../contexts/kibana';
import { useCreateAndNavigateToMlLink } from '../contexts/kibana/use_create_url';
import { ML_PAGES } from '../../../common/constants/ml_url_generator';
export const useResolver = (
indexPatternId: string | undefined,
@ -34,6 +36,9 @@ export const useResolver = (
const [context, setContext] = useState<any | null>(null);
const [results, setResults] = useState(tempResults);
const redirectToJobsManagementPage = useCreateAndNavigateToMlLink(
ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE
);
useEffect(() => {
(async () => {
@ -73,7 +78,7 @@ export const useResolver = (
defaultMessage: 'An error has occurred',
}),
});
window.location.href = '#/';
await redirectToJobsManagementPage();
}
} else {
setContext({});

View file

@ -797,7 +797,6 @@ function createResultsUrl(jobIds, start, end, resultsPage, mode = 'absolute') {
let path = '';
if (resultsPage !== undefined) {
path += '#/';
path += resultsPage;
}

View file

@ -25,6 +25,8 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { AnomalyDetectionSettingsContext } from './anomaly_detection_settings_context';
import { useNotifications } from '../contexts/kibana';
import { ml } from '../services/ml_api_service';
import { ML_PAGES } from '../../../common/constants/ml_url_generator';
import { useCreateAndNavigateToMlLink } from '../contexts/kibana/use_create_url';
export const AnomalyDetectionSettings: FC = () => {
const [calendarsCount, setCalendarsCount] = useState(0);
@ -35,6 +37,10 @@ export const AnomalyDetectionSettings: FC = () => {
);
const { toasts } = useNotifications();
const redirectToCalendarList = useCreateAndNavigateToMlLink(ML_PAGES.CALENDARS_MANAGE);
const redirectToNewCalendarPage = useCreateAndNavigateToMlLink(ML_PAGES.CALENDARS_NEW);
const redirectToFilterLists = useCreateAndNavigateToMlLink(ML_PAGES.FILTER_LISTS_MANAGE);
const redirectToNewFilterListPage = useCreateAndNavigateToMlLink(ML_PAGES.FILTER_LISTS_NEW);
useEffect(() => {
loadSummaryStats();
@ -126,7 +132,7 @@ export const AnomalyDetectionSettings: FC = () => {
flush="left"
size="l"
color="primary"
href="#/settings/calendars_list"
onClick={redirectToCalendarList}
isDisabled={canGetCalendars === false}
>
<FormattedMessage
@ -141,7 +147,7 @@ export const AnomalyDetectionSettings: FC = () => {
flush="left"
size="l"
color="primary"
href="#/settings/calendars_list/new_calendar"
onClick={redirectToNewCalendarPage}
isDisabled={canCreateCalendar === false}
>
<FormattedMessage
@ -167,7 +173,7 @@ export const AnomalyDetectionSettings: FC = () => {
<p>
<FormattedMessage
id="xpack.ml.settings.anomalyDetection.filterListsText"
defaultMessage=" Filter lists contain values that you can use to include or exclude events from the machine learning analysis."
defaultMessage="Filter lists contain values that you can use to include or exclude events from the machine learning analysis."
/>
</p>
</EuiTextColor>
@ -194,7 +200,7 @@ export const AnomalyDetectionSettings: FC = () => {
flush="left"
size="l"
color="primary"
href="#/settings/filter_lists"
onClick={redirectToFilterLists}
isDisabled={canGetFilters === false}
>
<FormattedMessage
@ -208,7 +214,7 @@ export const AnomalyDetectionSettings: FC = () => {
data-test-subj="mlFilterListsCreateButton"
size="l"
color="primary"
href="#/settings/filter_lists/new_filter_list"
onClick={redirectToNewFilterListPage}
isDisabled={canCreateFilter === false}
>
<FormattedMessage

View file

@ -88,6 +88,7 @@ exports[`CalendarForm Renders calendar form 1`] = `
size="xl"
/>
<EuiSwitch
checked={false}
data-test-subj="mlCalendarApplyToAllJobsSwitch"
disabled={false}
label={
@ -99,6 +100,68 @@ exports[`CalendarForm Renders calendar form 1`] = `
}
name="switch"
/>
<EuiSpacer
size="m"
/>
<EuiFormRow
describedByIds={Array []}
display="row"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label={
<FormattedMessage
defaultMessage="Jobs"
id="xpack.ml.calendarsEdit.calendarForm.jobsLabel"
values={Object {}}
/>
}
labelType="label"
>
<EuiComboBox
async={false}
compressed={false}
data-test-subj="mlCalendarJobSelection"
fullWidth={false}
isClearable={true}
isDisabled={false}
onChange={[MockFunction]}
options={Array []}
selectedOptions={Array []}
singleSelection={false}
sortMatchesBy="none"
/>
</EuiFormRow>
<EuiFormRow
describedByIds={Array []}
display="row"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label={
<FormattedMessage
defaultMessage="Groups"
id="xpack.ml.calendarsEdit.calendarForm.groupsLabel"
values={Object {}}
/>
}
labelType="label"
>
<EuiComboBox
async={false}
compressed={false}
data-test-subj="mlCalendarJobGroupSelection"
fullWidth={false}
isClearable={true}
isDisabled={false}
onChange={[MockFunction]}
onCreateOption={[MockFunction]}
options={Array []}
selectedOptions={Array []}
singleSelection={false}
sortMatchesBy="none"
/>
</EuiFormRow>
<EuiSpacer
size="xl"
/>
@ -137,7 +200,6 @@ exports[`CalendarForm Renders calendar form 1`] = `
grow={false}
>
<EuiButton
href="#/settings/calendars_list"
isDisabled={false}
>
<FormattedMessage

View file

@ -25,6 +25,8 @@ import { EventsTable } from '../events_table';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { ML_PAGES } from '../../../../../../common/constants/ml_url_generator';
import { useCreateAndNavigateToMlLink } from '../../../../contexts/kibana/use_create_url';
function EditHeader({ calendarId, description }) {
return (
@ -81,6 +83,7 @@ export const CalendarForm = ({
const error = isNewCalendarIdValid === false && !isEdit ? [msg] : undefined;
const saveButtonDisabled =
canCreateCalendar === false || saving || !isNewCalendarIdValid || calendarId === '';
const redirectToCalendarsManagementPage = useCreateAndNavigateToMlLink(ML_PAGES.CALENDARS_MANAGE);
return (
<EuiForm>
@ -215,7 +218,7 @@ export const CalendarForm = ({
<EuiSpacer size="l" />
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButton isDisabled={saving} href={'#/settings/calendars_list'}>
<EuiButton isDisabled={saving} onClick={redirectToCalendarsManagementPage}>
<FormattedMessage
id="xpack.ml.calendarsEdit.calendarForm.cancelButtonLabel"
defaultMessage="Cancel"

View file

@ -8,6 +8,9 @@ import { shallowWithIntl, mountWithIntl } from 'test_utils/enzyme_helpers';
import React from 'react';
import { CalendarForm } from './calendar_form';
jest.mock('../../../../contexts/kibana/use_create_url', () => ({
useCreateAndNavigateToMlLink: jest.fn(),
}));
const testProps = {
calendarId: '',
canCreateCalendar: true,
@ -31,6 +34,7 @@ const testProps = {
selectedGroupOptions: [],
selectedJobOptions: [],
showNewEventModal: jest.fn(),
isGlobalCalendar: false,
};
describe('CalendarForm', () => {

View file

@ -20,6 +20,7 @@ import { ImportModal } from './import_modal';
import { ml } from '../../../services/ml_api_service';
import { withKibana } from '../../../../../../../../src/plugins/kibana_react/public';
import { GLOBAL_CALENDAR } from '../../../../../common/constants/calendars';
import { ML_PAGES } from '../../../../../common/constants/ml_url_generator';
class NewCalendarUI extends Component {
static propTypes = {
@ -55,6 +56,16 @@ class NewCalendarUI extends Component {
this.formSetup();
}
returnToCalendarsManagementPage = async () => {
const {
services: {
http: { basePath },
application: { navigateToUrl },
},
} = this.props.kibana;
await navigateToUrl(`${basePath.get()}/app/ml/${ML_PAGES.CALENDARS_MANAGE}`, true);
};
async formSetup() {
try {
const { jobIds, groupIds, calendars } = await getCalendarSettingsData();
@ -146,7 +157,7 @@ class NewCalendarUI extends Component {
try {
await ml.addCalendar(calendar);
window.location = '#/settings/calendars_list';
await this.returnToCalendarsManagementPage();
} catch (error) {
console.log('Error saving calendar', error);
this.setState({ saving: false });
@ -167,7 +178,7 @@ class NewCalendarUI extends Component {
try {
await ml.updateCalendar(calendar);
window.location = '#/settings/calendars_list';
await this.returnToCalendarsManagementPage();
} catch (error) {
console.log('Error saving calendar', error);
this.setState({ saving: false });

Some files were not shown because too many files have changed in this diff Show more